Compare commits

..

13 Commits

Author SHA1 Message Date
161c8c1e80 fix zoznam 2026-09-23 20:43:26 +02:00
8c0d10f28e fix zoznam 2026-09-23 20:34:02 +02:00
44d8bd582b fix zoznam 2026-09-23 20:32:50 +02:00
29144c8af4 fix zoznam 2026-09-23 19:48:42 +02:00
a7168aeddb fix zoznam 2026-09-23 19:47:32 +02:00
8abe3ede90 fix zoznam 2026-09-23 19:44:04 +02:00
2237aae4f8 fix db 2026-09-23 19:39:36 +02:00
cbf1fec04d docker compose 2026-09-23 19:06:46 +02:00
808bba3695 edit dockerfile 2026-09-23 19:02:26 +02:00
8132fca9b6 edit dockerfile 2026-09-23 19:00:41 +02:00
0ccac0087e dockerfile 2026-09-23 18:57:19 +02:00
aa1d60b19e init 2026-09-23 18:46:15 +02:00
e010811d3a init 2026-09-23 18:31:15 +02:00
9 changed files with 1398 additions and 0 deletions

2
.gitignore vendored
View File

@@ -136,3 +136,5 @@ dist
.yarn/install-state.gz
.pnp.*
bun.lock
data/

11
Dockerfile Normal file
View File

@@ -0,0 +1,11 @@
# use the official Bun image
# see all versions at https://hub.docker.com/r/oven/bun/tags
FROM oven/bun:1 AS base
WORKDIR /usr/src/app
COPY . .
RUN bun install
# run the app
USER bun
ENTRYPOINT [ "bun", "run", "index.js" ]

152
buttons/reakcnerole.js Normal file
View File

@@ -0,0 +1,152 @@
import { ApplicationCommandOptionTypes, ApplicationCommandTypes, ButtonStyles, ComponentTypes, MessageFlags, Permissions, SeparatorSpacingSize } from "oceanic.js";
export default {
data: {
name: "reakcnerole",
},
handle: async (interaction, database) => {
await interaction.defer(MessageFlags.EPHEMERAL)
const db = await database.table(`rr`)
const splitID = interaction.data.customID.split('.')
const kolekcia = splitID[1]
const rolaID = splitID[2]
const exists = await db.has(`${interaction.guildID}.${kolekcia}`)
if (!exists)
return await interaction.createFollowup({
flags: MessageFlags.IS_COMPONENTS_V2,
components: [
{
type: ComponentTypes.CONTAINER,
components: [
{
type: ComponentTypes.TEXT_DISPLAY,
content: `❌ kolekcia ${kolekcia} neexistuje`
}
]
}
]
})
const rolaExistuje = await db.has(`${interaction.guildID}.${kolekcia}.roles`, (r) => r.id === rolaID)
if (!rolaExistuje)
return await interaction.createFollowup({
flags: MessageFlags.IS_COMPONENTS_V2,
components: [
{
type: ComponentTypes.CONTAINER,
components: [
{
type: ComponentTypes.TEXT_DISPLAY,
content: `❌ rola v kolekcii ${kolekcia} neexistuje`
}
]
}
]
})
const rolaDB = await db.get(`${interaction.guildID}.${kolekcia}.roles`, (r) => r.id === rolaID)
const rola = interaction.guild.roles.find(r => r.id === rolaID)
if (!rola)
return await interaction.createFollowup({
flags: MessageFlags.IS_COMPONENTS_V2,
components: [
{
type: ComponentTypes.CONTAINER,
components: [
{
type: ComponentTypes.TEXT_DISPLAY,
content: `❌ rola neexistuje`
}
]
}
]
})
const botPermissions = interaction.guild.clientMember.permissions
if (!botPermissions.has(Permissions.MANAGE_ROLES))
return await interaction.createFollowup({
flags: MessageFlags.IS_COMPONENTS_V2,
components: [
{
type: ComponentTypes.CONTAINER,
components: [
{
type: ComponentTypes.TEXT_DISPLAY,
content: `❌ nemam prava na zmenu roli`
}
]
}
]
})
const memberMaRolu = interaction.member.roles.includes(rolaID)
if (memberMaRolu) {
await interaction.member.removeRole(rolaID, `reakcne role`)
return await interaction.createFollowup({
flags: MessageFlags.IS_COMPONENTS_V2,
components: [
{
type: ComponentTypes.CONTAINER,
components: [
{
type: ComponentTypes.TEXT_DISPLAY,
content: `✅ rola ${rola.name} ti bola odobrana`
}
]
}
]
})
}
const jednaRola = await db.get(`${interaction.guildID}.${kolekcia}.options.jednaRola`)
const roly = interaction.member.roles
const rolyDB = new Set((await db.get(`${interaction.guildID}.${kolekcia}.roles`)).map(i => i.id))
const memberMaRoluZKolekcie = roly.some(i => rolyDB.has(i))
if (jednaRola && memberMaRoluZKolekcie)
return await interaction.createFollowup({
flags: MessageFlags.IS_COMPONENTS_V2,
components: [
{
type: ComponentTypes.CONTAINER,
components: [
{
type: ComponentTypes.TEXT_DISPLAY,
content: `❌ uz mas nejaku rolu z kolekcie`
}
]
}
]
})
await interaction.member.addRole(rolaID, `reakcne role`)
await interaction.createFollowup({
flags: MessageFlags.IS_COMPONENTS_V2,
components: [
{
type: ComponentTypes.CONTAINER,
components: [
{
type: ComponentTypes.TEXT_DISPLAY,
content: `✅ rola ${rola.name} ti bola pridana`
}
]
}
]
})
}
}

741
commands/rr.js Normal file
View File

@@ -0,0 +1,741 @@
import { ApplicationCommandOptionTypes, ApplicationCommandTypes, ButtonStyles, ComponentTypes, MessageFlags, Permissions, SeparatorSpacingSize, TextInputStyles } from "oceanic.js";
export default {
data: {
type: ApplicationCommandTypes.CHAT_INPUT, // CHAT_INPUT = slash commands - full list: https://docs.oceanic.ws/latest/enums/Constants.ApplicationCommandTypes.html
name: "rr",
description: "nastav reakcne role",
options: [
{
type: ApplicationCommandOptionTypes.SUB_COMMAND, // A string input - full list: https://docs.oceanic.ws/latest/enums/Constants.ApplicationCommandOptionTypes.html
name: "zoznam",
description: "zobrazit zoznam kolekcii"
},
{
type: ApplicationCommandOptionTypes.SUB_COMMAND, // A string input - full list: https://docs.oceanic.ws/latest/enums/Constants.ApplicationCommandOptionTypes.html
name: "zoznam_roli",
description: "zobrazit zoznam roli v kolekcii",
options: [
{
type: ApplicationCommandOptionTypes.STRING, // A string input - full list: https://docs.oceanic.ws/latest/enums/Constants.ApplicationCommandOptionTypes.html
name: "kolekcia",
description: "kolekcia, z ktorej sa zobrazi zoznam roli",
required: true
},
]
},
{
type: ApplicationCommandOptionTypes.SUB_COMMAND, // A string input - full list: https://docs.oceanic.ws/latest/enums/Constants.ApplicationCommandOptionTypes.html
name: "vytvorit",
description: "vytvorit novu kolekciu",
options: [
{
type: ApplicationCommandOptionTypes.STRING, // A string input - full list: https://docs.oceanic.ws/latest/enums/Constants.ApplicationCommandOptionTypes.html
name: "kolekcia",
description: "nazov kolekcie na vytvorenie",
required: true
},
{
type: ApplicationCommandOptionTypes.BOOLEAN, // A string input - full list: https://docs.oceanic.ws/latest/enums/Constants.ApplicationCommandOptionTypes.html
name: "jedna_rola",
description: "moznost vybrat iba jednej role z kolekcie"
}
]
},
{
type: ApplicationCommandOptionTypes.SUB_COMMAND, // A string input - full list: https://docs.oceanic.ws/latest/enums/Constants.ApplicationCommandOptionTypes.html
name: "pridat_rolu",
description: "pridat rolu do kolekcie",
options: [
{
type: ApplicationCommandOptionTypes.STRING, // A string input - full list: https://docs.oceanic.ws/latest/enums/Constants.ApplicationCommandOptionTypes.html
name: "kolekcia",
description: "kolekcia, do ktorej bude rola pridana",
required: true
},
{
type: ApplicationCommandOptionTypes.ROLE, // A string input - full list: https://docs.oceanic.ws/latest/enums/Constants.ApplicationCommandOptionTypes.html
name: "rola",
description: "rola na pridanie do kolekcie",
required: true
},
{
type: ApplicationCommandOptionTypes.STRING, // A string input - full list: https://docs.oceanic.ws/latest/enums/Constants.ApplicationCommandOptionTypes.html
name: "nazov",
description: "nazov role v kolekcii"
}
]
},
{
type: ApplicationCommandOptionTypes.SUB_COMMAND, // A string input - full list: https://docs.oceanic.ws/latest/enums/Constants.ApplicationCommandOptionTypes.html
name: "odobrat_rolu",
description: "odobrat rolu z kolekcie",
options: [
{
type: ApplicationCommandOptionTypes.STRING, // A string input - full list: https://docs.oceanic.ws/latest/enums/Constants.ApplicationCommandOptionTypes.html
name: "kolekcia",
description: "kolekcia, z ktorej bude rola odobrana",
required: true
},
{
type: ApplicationCommandOptionTypes.ROLE, // A string input - full list: https://docs.oceanic.ws/latest/enums/Constants.ApplicationCommandOptionTypes.html
name: "rola",
description: "rola na odobranie z kolekcie",
required: true
}
]
},
{
type: ApplicationCommandOptionTypes.SUB_COMMAND, // A string input - full list: https://docs.oceanic.ws/latest/enums/Constants.ApplicationCommandOptionTypes.html
name: "zmazat",
description: "zmazat kolekciu",
options: [
{
type: ApplicationCommandOptionTypes.STRING, // A string input - full list: https://docs.oceanic.ws/latest/enums/Constants.ApplicationCommandOptionTypes.html
name: "kolekcia",
description: "kolekcia na zmazanie",
required: true
}
]
},
{
type: ApplicationCommandOptionTypes.SUB_COMMAND, // A string input - full list: https://docs.oceanic.ws/latest/enums/Constants.ApplicationCommandOptionTypes.html
name: "poslat",
description: "poslat kolekciu do kanala",
options: [
{
type: ApplicationCommandOptionTypes.STRING, // A string input - full list: https://docs.oceanic.ws/latest/enums/Constants.ApplicationCommandOptionTypes.html
name: "kolekcia",
description: "kolekcia na odoslanie",
required: true
}
]
},
{
type: ApplicationCommandOptionTypes.SUB_COMMAND, // A string input - full list: https://docs.oceanic.ws/latest/enums/Constants.ApplicationCommandOptionTypes.html
name: "upravit",
description: "upravit kolekciu",
options: [
{
type: ApplicationCommandOptionTypes.STRING, // A string input - full list: https://docs.oceanic.ws/latest/enums/Constants.ApplicationCommandOptionTypes.html
name: "kolekcia",
description: "kolekcia na upravenie",
required: true
},
{
type: ApplicationCommandOptionTypes.BOOLEAN, // A string input - full list: https://docs.oceanic.ws/latest/enums/Constants.ApplicationCommandOptionTypes.html
name: "jedna_rola",
description: "moznost vybrat iba jednej role z kolekcie"
},
{
type: ApplicationCommandOptionTypes.STRING, // A string input - full list: https://docs.oceanic.ws/latest/enums/Constants.ApplicationCommandOptionTypes.html
name: "novy_nazov",
description: "novy nazov kolekcie"
}
]
},
{
type: ApplicationCommandOptionTypes.SUB_COMMAND, // A string input - full list: https://docs.oceanic.ws/latest/enums/Constants.ApplicationCommandOptionTypes.html
name: "upravit_rolu",
description: "upravit rolu v kolekcii",
options: [
{
type: ApplicationCommandOptionTypes.STRING, // A string input - full list: https://docs.oceanic.ws/latest/enums/Constants.ApplicationCommandOptionTypes.html
name: "kolekcia",
description: "kolekcia, z ktorej je rola na upravenie",
required: true
},
{
type: ApplicationCommandOptionTypes.ROLE, // A string input - full list: https://docs.oceanic.ws/latest/enums/Constants.ApplicationCommandOptionTypes.html
name: "rola",
description: "rola na upravenie",
required: true
},
{
type: ApplicationCommandOptionTypes.STRING, // A string input - full list: https://docs.oceanic.ws/latest/enums/Constants.ApplicationCommandOptionTypes.html
name: "nazov",
description: "nazov role v kolekcii"
}
]
},
{
type: ApplicationCommandOptionTypes.SUB_COMMAND, // A string input - full list: https://docs.oceanic.ws/latest/enums/Constants.ApplicationCommandOptionTypes.html
name: "popis",
description: "upravit popis kolekcie",
options: [
{
type: ApplicationCommandOptionTypes.STRING, // A string input - full list: https://docs.oceanic.ws/latest/enums/Constants.ApplicationCommandOptionTypes.html
name: "kolekcia",
description: "kolekcia, ktorej popis bude upraveny",
required: true
}
]
}
],
dmPermission: false, // false = usable in guilds only, true = both guild & direct message
defaultMemberPermissions: "8" // The bitfield of the default permissions required to use this command (8 = Administrator)
},
handle: async (interaction, database) => {
const db = await database.table(`rr`)
const action = interaction.data.options.getSubCommand()[0]
if (action !== `popis`) await interaction.defer(MessageFlags.EPHEMERAL);
switch (action) {
case "vytvorit": {
const kolekcia = interaction.data.options.getString("kolekcia")
const jednaRola = interaction.data.options.getString("jedna_rola")
const exists = await db.has(`${interaction.guildID}.${kolekcia}`)
if (exists)
return await interaction.createFollowup({
flags: MessageFlags.IS_COMPONENTS_V2,
components: [
{
type: ComponentTypes.CONTAINER,
components: [
{
type: ComponentTypes.TEXT_DISPLAY,
content: `❌ kolekcia ${kolekcia} uz existuje`
}
]
}
]
})
await db.set(`${interaction.guildID}.${kolekcia}`, { options: { jednaRola }, popis: null, roles: [] })
await interaction.createFollowup({
flags: MessageFlags.IS_COMPONENTS_V2,
components: [
{
type: ComponentTypes.CONTAINER,
components: [
{
type: ComponentTypes.TEXT_DISPLAY,
content: `✅ kolekcia ${kolekcia} vytvorena`
}
]
}
]
})
break
} case "pridat_rolu": {
const kolekcia = interaction.data.options.getString("kolekcia")
const exists = await db.has(`${interaction.guildID}.${kolekcia}`)
if (!exists)
return await interaction.createFollowup({
flags: MessageFlags.IS_COMPONENTS_V2,
components: [
{
type: ComponentTypes.CONTAINER,
components: [
{
type: ComponentTypes.TEXT_DISPLAY,
content: `❌ kolekcia ${kolekcia} neexistuje`
}
]
}
]
})
const rola = interaction.data.options.getRole("rola")
const rolaExistuje = await db.has(`${interaction.guildID}.${kolekcia}.roles`, (r) => r.id === rola.id)
const nazov = interaction.data.options.getString("nazov")
if (rolaExistuje)
return await interaction.createFollowup({
flags: MessageFlags.IS_COMPONENTS_V2,
components: [
{
type: ComponentTypes.CONTAINER,
components: [
{
type: ComponentTypes.TEXT_DISPLAY,
content: `❌ rola ${rola.name} uz v kolekcii ${kolekcia} existuje`
}
]
}
]
})
await db.push(`${interaction.guildID}.${kolekcia}.roles`, { id: rola.id, nazov })
await interaction.createFollowup({
flags: MessageFlags.IS_COMPONENTS_V2,
components: [
{
type: ComponentTypes.CONTAINER,
components: [
{
type: ComponentTypes.TEXT_DISPLAY,
content: `✅ rola ${rola.name} pridana do kolekcie ${kolekcia}`
}
]
}
]
})
break
} case "odobrat_rolu": {
const kolekcia = interaction.data.options.getString("kolekcia")
const exists = await db.has(`${interaction.guildID}.${kolekcia}`)
if (!exists)
return await interaction.createFollowup({
flags: MessageFlags.IS_COMPONENTS_V2,
components: [
{
type: ComponentTypes.CONTAINER,
components: [
{
type: ComponentTypes.TEXT_DISPLAY,
content: `❌ kolekcia ${kolekcia} neexistuje`
}
]
}
]
})
const rola = interaction.data.options.getRole("rola")
const rolaExistuje = await db.has(`${interaction.guildID}.${kolekcia}.roles`, (r) => r.id === rola.id)
if (!rolaExistuje)
return await interaction.createFollowup({
flags: MessageFlags.IS_COMPONENTS_V2,
components: [
{
type: ComponentTypes.CONTAINER,
components: [
{
type: ComponentTypes.TEXT_DISPLAY,
content: `❌ rola ${rola.name} v kolekcii ${kolekcia} neexistuje`
}
]
}
]
})
await db.delete(`${interaction.guildID}.${kolekcia}.roles`, (r) => r.id === rola.id)
await interaction.createFollowup({
flags: MessageFlags.IS_COMPONENTS_V2,
components: [
{
type: ComponentTypes.CONTAINER,
components: [
{
type: ComponentTypes.TEXT_DISPLAY,
content: `✅ rola ${rola.name} odobrana z kolekcie ${kolekcia}`
}
]
}
]
})
break
} case "zmazat": {
const kolekcia = interaction.data.options.getString("kolekcia")
const exists = await db.has(`${interaction.guildID}.${kolekcia}`)
if (!exists)
return await interaction.createFollowup({
flags: MessageFlags.IS_COMPONENTS_V2,
components: [
{
type: ComponentTypes.CONTAINER,
components: [
{
type: ComponentTypes.TEXT_DISPLAY,
content: `❌ kolekcia ${kolekcia} neexistuje`
}
]
}
]
})
await db.delete(`${interaction.guildID}.${kolekcia}`)
await interaction.createFollowup({
flags: MessageFlags.IS_COMPONENTS_V2,
components: [
{
type: ComponentTypes.CONTAINER,
components: [
{
type: ComponentTypes.TEXT_DISPLAY,
content: `✅ kolekcia ${kolekcia} zmazana`
}
]
}
]
})
break
} case "poslat": {
const kolekcia = interaction.data.options.getString("kolekcia")
const exists = await db.has(`${interaction.guildID}.${kolekcia}`)
if (!exists)
return await interaction.createFollowup({
flags: MessageFlags.IS_COMPONENTS_V2,
components: [
{
type: ComponentTypes.CONTAINER,
components: [
{
type: ComponentTypes.TEXT_DISPLAY,
content: `❌ kolekcia ${kolekcia} neexistuje`
}
]
}
]
})
const botPermissions = interaction.channel.permissionsOf(interaction.guild.clientMember)
if (!botPermissions.has(Permissions.SEND_MESSAGES))
return await interaction.createFollowup({
flags: MessageFlags.IS_COMPONENTS_V2,
components: [
{
type: ComponentTypes.CONTAINER,
components: [
{
type: ComponentTypes.TEXT_DISPLAY,
content: `❌ nemam prava na poslanie spravy`
}
]
}
]
})
const popis = await db.get(`${interaction.guildID}.${kolekcia}.popis`)
const roleDB = await db.get(`${interaction.guildID}.${kolekcia}.roles`)
const komponenty = []
komponenty.push({
type: ComponentTypes.TEXT_DISPLAY,
content: `# ${kolekcia}`
})
if (popis)
komponenty.push({
type: ComponentTypes.TEXT_DISPLAY,
content: popis
})
komponenty.push({
type: ComponentTypes.SEPARATOR,
spacing: SeparatorSpacingSize.LARGE,
divider: true
})
const komponentyRoli = []
let i = 0
for (const rolaDB of roleDB) {
const rola = interaction.guild.roles.find(r => r.id === rolaDB.id)
if (!rola) continue
if (i % 5 === 0)
komponentyRoli.push({
type: ComponentTypes.ACTION_ROW,
components: []
})
komponentyRoli[Math.floor(i / 5)].components.push(
{
type: ComponentTypes.BUTTON,
style: ButtonStyles.PRIMARY,
label: rolaDB.nazov ?? rola.name,
customID: `reakcnerole.${kolekcia}.${rolaDB.id}`
}
)
i++
}
if (!komponentyRoli.length)
komponentyRoli.push({
type: ComponentTypes.TEXT_DISPLAY,
content: `zatial ziadne role`
})
await interaction.channel.createMessage({
flags: MessageFlags.IS_COMPONENTS_V2,
components: [
{
type: ComponentTypes.CONTAINER,
components: [...komponenty, ...komponentyRoli]
}
]
})
await interaction.createFollowup({
flags: MessageFlags.IS_COMPONENTS_V2,
components: [
{
type: ComponentTypes.CONTAINER,
components: [
{
type: ComponentTypes.TEXT_DISPLAY,
content: `✅ sprava poslana`
}
]
}
]
})
break
} case "upravit": {
const kolekcia = interaction.data.options.getString("kolekcia")
const jednaRola = interaction.data.options.getBoolean("jedna_rola")
const novyNazov = interaction.data.options.getString("novy_nazov")
const exists = await db.has(`${interaction.guildID}.${kolekcia}`)
if (!exists)
return await interaction.createFollowup({
flags: MessageFlags.IS_COMPONENTS_V2,
components: [
{
type: ComponentTypes.CONTAINER,
components: [
{
type: ComponentTypes.TEXT_DISPLAY,
content: `❌ kolekcia ${kolekcia} neexistuje`
}
]
}
]
})
if (jednaRola !== undefined)
await db.set(`${interaction.guildID}.${kolekcia}.options`, { jednaRola })
if (novyNazov)
await db.rename(`${interaction.guildID}.${kolekcia}`, `${interaction.guildID}.${novyNazov}`)
await interaction.createFollowup({
flags: MessageFlags.IS_COMPONENTS_V2,
components: [
{
type: ComponentTypes.CONTAINER,
components: [
{
type: ComponentTypes.TEXT_DISPLAY,
content: `✅ kolekcia ${novyNazov ?? kolekcia} upravena`
}
]
}
]
})
break
} case "upravit_rolu": {
const kolekcia = interaction.data.options.getString("kolekcia")
const exists = await db.has(`${interaction.guildID}.${kolekcia}`)
if (!exists)
return await interaction.createFollowup({
flags: MessageFlags.IS_COMPONENTS_V2,
components: [
{
type: ComponentTypes.CONTAINER,
components: [
{
type: ComponentTypes.TEXT_DISPLAY,
content: `❌ kolekcia ${kolekcia} neexistuje`
}
]
}
]
})
const rola = interaction.data.options.getRole("rola")
const rolaExistuje = await db.has(`${interaction.guildID}.${kolekcia}.roles`, (r) => r.id === rola.id)
const nazov = interaction.data.options.getString("nazov")
if (!rolaExistuje)
return await interaction.createFollowup({
flags: MessageFlags.IS_COMPONENTS_V2,
components: [
{
type: ComponentTypes.CONTAINER,
components: [
{
type: ComponentTypes.TEXT_DISPLAY,
content: `❌ rola ${rola.name} v kolekcii ${kolekcia} neexistuje`
}
]
}
]
})
if (nazov) await db.update(`${interaction.guildID}.${kolekcia}.roles`, (r => r.id === rola.id), { nazov })
await interaction.createFollowup({
flags: MessageFlags.IS_COMPONENTS_V2,
components: [
{
type: ComponentTypes.CONTAINER,
components: [
{
type: ComponentTypes.TEXT_DISPLAY,
content: `✅ rola ${rola.name} upravena v kolekcii ${kolekcia}`
}
]
}
]
})
break
} case "popis": {
const kolekcia = interaction.data.options.getString("kolekcia")
const exists = await db.has(`${interaction.guildID}.${kolekcia}`)
if (!exists)
return await interaction.createFollowup({
flags: MessageFlags.IS_COMPONENTS_V2,
components: [
{
type: ComponentTypes.CONTAINER,
components: [
{
type: ComponentTypes.TEXT_DISPLAY,
content: `❌ kolekcia ${kolekcia} neexistuje`
}
]
}
]
})
await interaction.createModal({
customID: `reakcnerole.${kolekcia}.popis`,
title: `${kolekcia} - upravit popis`,
components: [
{
type: ComponentTypes.LABEL,
label: `popis`,
component: {
type: ComponentTypes.TEXT_INPUT,
style: TextInputStyles.PARAGRAPH,
customID: `popis`,
required: false
}
}
]
})
break
} case "zoznam": {
const kolekcie = await db.get(`${interaction.guildID}`)
let textZoznam = `# zoznam kolekcií`
if (Object.keys(kolekcie)?.length) {
for (const kolekcia of Object.keys(kolekcie)) {
textZoznam += `\n- ${kolekcia}`
}
} else {
textZoznam += `\nziadne kolekcie`
}
await interaction.createFollowup({
flags: MessageFlags.IS_COMPONENTS_V2,
components: [
{
type: ComponentTypes.CONTAINER,
components: [
{
type: ComponentTypes.TEXT_DISPLAY,
content: textZoznam
}
]
}
]
})
break
} case "zoznam_roli": {
const kolekcia = interaction.data.options.getString("kolekcia")
const exists = await db.has(`${interaction.guildID}.${kolekcia}`)
if (!exists)
return await interaction.createFollowup({
flags: MessageFlags.IS_COMPONENTS_V2,
components: [
{
type: ComponentTypes.CONTAINER,
components: [
{
type: ComponentTypes.TEXT_DISPLAY,
content: `❌ kolekcia ${kolekcia} neexistuje`
}
]
}
]
})
const role = await db.get(`${interaction.guildID}.${kolekcia}.roles`)
let textZoznam = `# zoznam rolí (${kolekcia})`
if (role?.length) {
for (const rola of role) {
textZoznam += `\n- ${rola.nazov ? `${rola.nazov} `: ``}<@&${rola.id}>`
}
} else {
textZoznam += `\nziadne role`
}
await interaction.createFollowup({
flags: MessageFlags.IS_COMPONENTS_V2,
components: [
{
type: ComponentTypes.CONTAINER,
components: [
{
type: ComponentTypes.TEXT_DISPLAY,
content: textZoznam
}
]
}
]
})
break
} default: {
await interaction.createFollowup({
flags: MessageFlags.IS_COMPONENTS_V2,
components: [
{
type: ComponentTypes.CONTAINER,
components: [
{
type: ComponentTypes.TEXT_DISPLAY,
content: `❌ neplatna akcia`
}
]
}
]
})
}
}
}
}

4
docker-compose.yml Normal file
View File

@@ -0,0 +1,4 @@
services:
itckar-bot:
build:
context: .

108
index.js Normal file
View File

@@ -0,0 +1,108 @@
import { ApplicationCommandTypes, Client, ComponentTypes, InteractionTypes } from "oceanic.js";
import { readdir } from "node:fs/promises";
import { KVDB } from "./utils/kvdb";
const db = new KVDB({
user: process.env.POSTGRESQL_USERNAME,
password: process.env.POSTGRESQL_PASSWORD,
host: process.env.POSTGRESQL_HOST,
port: process.env.POSTGRESQL_PORT,
database: process.env.POSTGRESQL_DATABASE
});
await db.init();
const client = new Client({
token: process.env.TOKEN
});
const commandFiles = await readdir("./commands", { recursive: true });
const commandsData = []
const commands = new Map()
for (const commandFile of commandFiles) {
const command = (await import(`./commands/${commandFile}`)).default
commandsData.push(command.data)
commands.set(command.data.name, command)
}
const buttonFiles = await readdir("./buttons", { recursive: true });
const buttonsData = []
const buttons = new Map()
for (const buttonFile of buttonFiles) {
const button = (await import(`./buttons/${buttonFile}`)).default
buttonsData.push(button.data)
buttons.set(button.data.name, button)
}
const modalFiles = await readdir("./modals", { recursive: true });
const modalsData = []
const modals = new Map()
for (const modalFile of modalFiles) {
const modal = (await import(`./modals/${modalFile}`)).default
modalsData.push(modal.data)
modals.set(modal.data.name, modal)
}
client.on("ready", async() => {
await client.application.bulkEditGlobalCommands(commandsData);
console.log("Ready as", client.user.tag);
});
// if you do not add a listener for the error event, any errors will cause an UncaughtError to be thrown,
// and your process may be killed as a result.
client.on("error", (err) => {
console.error("bro vazne..", err);
});
client.on("interactionCreate", async(interaction) => {
switch(interaction.type) {
// https://docs.oceanic.ws/latest/classes/CommandInteraction.html
case InteractionTypes.APPLICATION_COMMAND:
// data = https://docs.oceanic.ws/latest/interfaces/Types_Interactions.ApplicationCommandInteractionData.html
switch(interaction.data.type) {
// Chat Input commands are what you use in the chat, i.e. slash commands
case ApplicationCommandTypes.CHAT_INPUT:
const command = commands.get(interaction.data.name)
if (!command)
return await interaction.reply("invalid command")
await command.handle(interaction, db)
break
}
break
case InteractionTypes.MESSAGE_COMPONENT:
switch(interaction.data.componentType) {
case ComponentTypes.BUTTON:
const button = buttons.get(interaction.data.customID.split(".")[0])
if (!button)
return await interaction.reply("something went wrong")
await button.handle(interaction, db)
break
}
break
case InteractionTypes.MODAL_SUBMIT:
const modal = modals.get(interaction.data.customID.split(".")[0])
if (!modal)
return await interaction.reply("something went wrong")
await modal.handle(interaction, db)
break
}
});
await client.connect();

54
modals/reakcnerole.js Normal file
View File

@@ -0,0 +1,54 @@
import { ApplicationCommandOptionTypes, ApplicationCommandTypes, ButtonStyles, ComponentTypes, MessageFlags, Permissions, SeparatorSpacingSize } from "oceanic.js";
export default {
data: {
name: "reakcnerole",
},
handle: async (interaction, database) => {
await interaction.defer(MessageFlags.EPHEMERAL)
const db = await database.table(`rr`)
const splitID = interaction.data.customID.split('.')
const kolekcia = splitID[1]
const exists = await db.has(`${interaction.guildID}.${kolekcia}`)
if (!exists)
return await interaction.createFollowup({
flags: MessageFlags.IS_COMPONENTS_V2,
components: [
{
type: ComponentTypes.CONTAINER,
components: [
{
type: ComponentTypes.TEXT_DISPLAY,
content: `❌ kolekcia ${kolekcia} neexistuje`
}
]
}
]
})
const popis = interaction.data.components.getTextInput(`popis`)
await db.set(`${interaction.guildID}.${kolekcia}.popis`, popis)
await interaction.createFollowup({
flags: MessageFlags.IS_COMPONENTS_V2,
components: [
{
type: ComponentTypes.CONTAINER,
components: [
{
type: ComponentTypes.TEXT_DISPLAY,
content: `✅ popis kolekcie ${kolekcia} zmeneny`
}
]
}
]
})
}
}

6
package.json Normal file
View File

@@ -0,0 +1,6 @@
{
"dependencies": {
"oceanic.js": "^1.15.0",
"pg": "^8.23.0"
}
}

320
utils/kvdb.js Normal file
View File

@@ -0,0 +1,320 @@
/*
vibecoded :sob:
*/
import { Pool } from "pg";
export class KVDB {
/**
* @param {Pool | object} poolOrConfig - A pg.Pool instance or pg connection config object
* @param {string} [table="main_kv"] - The table/context name (defaults to "main_kv")
*/
constructor(poolOrConfig, table = "main_kv") {
this.pool = poolOrConfig instanceof Pool ? poolOrConfig : new Pool(poolOrConfig);
// Sanitize table name to prevent SQL injection in DDL
this.tableName = table.replace(/[^a-zA-Z0-9_]/g, "");
}
/**
* Initialize the database table. Call this once after instantiating the class.
* @returns {Promise<void>}
*/
async init() {
await this.pool.query(`
CREATE TABLE IF NOT EXISTS "${this.tableName}" (
key TEXT PRIMARY KEY,
value JSONB
)
`);
}
/**
* Switch database table/context sharing the same connection pool
* @param {string} newTableName
* @returns {Promise<KVDB>}
*/
async table(newTableName) {
const newDb = new KVDB(this.pool, newTableName);
await newDb.init();
return newDb;
}
/**
* Set a key or nested dot-notation property.
* Examples:
* await db.set("user", { name: "Alex" })
* await db.set("user.age", 30)
* @param {string} keyPath
* @param {any} value
* @returns {Promise<void>}
*/
async set(keyPath, value) {
const parts = keyPath.split(".");
const rootKey = parts[0];
const jsonValue = JSON.stringify(value);
if (parts.length === 1) {
await this.pool.query(
`INSERT INTO "${this.tableName}" (key, value) VALUES ($1, $2::jsonb)
ON CONFLICT(key) DO UPDATE SET value = EXCLUDED.value`,
[rootKey, jsonValue]
);
return;
}
const pathArr = parts.slice(1);
await this.pool.query(
`INSERT INTO "${this.tableName}" (key, value) VALUES ($1, '{}'::jsonb)
ON CONFLICT(key) DO NOTHING`,
[rootKey]
);
await this.pool.query(
`UPDATE "${this.tableName}"
SET value = jsonb_set(value, $1::text[], $2::jsonb, true)
WHERE key = $3`,
[pathArr, jsonValue, rootKey]
);
}
/**
* Get a key/nested property, OR find an element inside an array using a predicate function.
* @param {string} keyPath
* @param {Function} [predicate] - Optional matching function for array items
* @returns {Promise<any>}
*/
async get(keyPath, predicate) {
const parts = keyPath.split(".");
const rootKey = parts[0];
let data = null;
if (parts.length === 1) {
const { rows } = await this.pool.query(
`SELECT value FROM "${this.tableName}" WHERE key = $1`,
[rootKey]
);
// pg parses JSONB automatically
data = rows.length > 0 ? rows[0].value : null;
} else {
const pathArr = parts.slice(1);
const { rows } = await this.pool.query(
`SELECT value #> $1::text[] AS result FROM "${this.tableName}" WHERE key = $2`,
[pathArr, rootKey]
);
data = (rows.length > 0 && rows[0].result !== undefined) ? rows[0].result : null;
}
if (typeof predicate === "function") {
if (!Array.isArray(data)) return null;
return data.find(predicate) ?? null;
}
return data;
}
/**
* Check if a key/property exists, OR check if an array contains a specific item / predicate match.
* @param {string} keyPath
* @param {any | Function} [itemOrPredicate]
* @returns {Promise<boolean>}
*/
async has(keyPath, itemOrPredicate) {
if (itemOrPredicate !== undefined) {
const arr = await this.get(keyPath);
if (!Array.isArray(arr)) return false;
if (typeof itemOrPredicate === "function") {
return arr.some(itemOrPredicate);
}
if (typeof itemOrPredicate === "object" && itemOrPredicate !== null) {
const targetString = JSON.stringify(itemOrPredicate);
return arr.some((el) => JSON.stringify(el) === targetString);
}
return arr.includes(itemOrPredicate);
}
const parts = keyPath.split(".");
const rootKey = parts[0];
if (parts.length === 1) {
const { rows } = await this.pool.query(
`SELECT 1 FROM "${this.tableName}" WHERE key = $1`,
[rootKey]
);
return rows.length > 0;
}
const pathArr = parts.slice(1);
const { rows } = await this.pool.query(
`SELECT 1 FROM "${this.tableName}"
WHERE key = $1 AND value #> $2::text[] IS NOT NULL`,
[rootKey, pathArr]
);
return rows.length > 0;
}
/**
* Delete a key, property, OR remove item(s) from an array using a predicate or value.
* @param {string} keyPath
* @param {any | Function} [itemOrPredicate]
* @returns {Promise<void>}
*/
async delete(keyPath, itemOrPredicate) {
if (itemOrPredicate !== undefined) {
const arr = await this.get(keyPath);
if (!Array.isArray(arr)) return;
let filtered;
if (typeof itemOrPredicate === "function") {
filtered = arr.filter((item) => !itemOrPredicate(item));
} else if (typeof itemOrPredicate === "object" && itemOrPredicate !== null) {
const targetString = JSON.stringify(itemOrPredicate);
filtered = arr.filter((item) => JSON.stringify(item) !== targetString);
} else {
filtered = arr.filter((item) => item !== itemOrPredicate);
}
await this.set(keyPath, filtered);
return;
}
const parts = keyPath.split(".");
const rootKey = parts[0];
if (parts.length === 1) {
await this.pool.query(`DELETE FROM "${this.tableName}" WHERE key = $1`, [rootKey]);
return;
}
const pathArr = parts.slice(1);
await this.pool.query(
`UPDATE "${this.tableName}"
SET value = value #- $1::text[]
WHERE key = $2`,
[pathArr, rootKey]
);
}
/**
* Appends an element to an array at the target key or nested path.
* @param {string} keyPath
* @param {any} value
* @returns {Promise<void>}
*/
async push(keyPath, value) {
const parts = keyPath.split(".");
const rootKey = parts[0];
const wrappedValue = JSON.stringify([value]);
if (parts.length === 1) {
await this.pool.query(
`INSERT INTO "${this.tableName}" (key, value) VALUES ($1, $2::jsonb)
ON CONFLICT(key) DO UPDATE
SET value = CASE
WHEN jsonb_typeof("${this.tableName}".value) = 'array'
THEN "${this.tableName}".value || $2::jsonb
ELSE $2::jsonb
END`,
[rootKey, wrappedValue]
);
} else {
const pathArr = parts.slice(1);
await this.pool.query(
`INSERT INTO "${this.tableName}" (key, value) VALUES ($1, '{}'::jsonb)
ON CONFLICT(key) DO NOTHING`,
[rootKey]
);
await this.pool.query(
`UPDATE "${this.tableName}"
SET value = jsonb_set(
value,
$1::text[],
COALESCE(value #> $1::text[], '[]'::jsonb) || $2::jsonb,
true
)
WHERE key = $3`,
[pathArr, wrappedValue, rootKey]
);
}
}
/**
* Updates matching item(s) in an array using a predicate and an update callback/object.
* @param {string} keyPath
* @param {Function} predicate
* @param {Object | Function} updateFnOrObject
* @returns {Promise<boolean>}
*/
async update(keyPath, predicate, updateFnOrObject) {
const arr = await this.get(keyPath);
if (!Array.isArray(arr)) return false;
let updatedAny = false;
const newArr = arr.map((item) => {
if (!predicate(item)) return item;
updatedAny = true;
if (typeof updateFnOrObject === "function") {
return updateFnOrObject(item);
}
if (typeof item === "object" && item !== null && typeof updateFnOrObject === "object") {
return { ...item, ...updateFnOrObject };
}
return updateFnOrObject;
});
if (updatedAny) {
await this.set(keyPath, newArr);
}
return updatedAny;
}
/**
* Renames a key or nested property path without changing its value.
* @param {string} oldKeyPath
* @param {string} newKeyPath
* @returns {Promise<boolean>} True if target key existed and was renamed
*/
async rename(oldKeyPath, newKeyPath) {
if (!(await this.has(oldKeyPath))) return false;
const oldParts = oldKeyPath.split(".");
let targetNewPath = newKeyPath;
if (oldParts.length > 1 && !newKeyPath.startsWith(oldParts.slice(0, -1).join(".") + ".")) {
const parentPath = oldParts.slice(0, -1).join(".");
targetNewPath = `${parentPath}.${newKeyPath}`;
}
const value = await this.get(oldKeyPath);
const oldRoot = oldParts[0];
const newParts = targetNewPath.split(".");
const newRoot = newParts[0];
if (oldParts.length === 1 && newParts.length === 1) {
await this.pool.query(
`UPDATE "${this.tableName}" SET key = $1 WHERE key = $2`,
[newRoot, oldRoot]
);
} else {
await this.set(targetNewPath, value);
await this.delete(oldKeyPath);
}
return true;
}
}