feat: initial commit
This commit is contained in:
commit
e54dee08c8
29 changed files with 4857 additions and 0 deletions
235
.gitignore
vendored
Normal file
235
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,235 @@
|
||||||
|
# Created by https://www.toptal.com/developers/gitignore/api/node,visualstudiocode,windows,macos,linux
|
||||||
|
# Edit at https://www.toptal.com/developers/gitignore?templates=node,visualstudiocode,windows,macos,linux
|
||||||
|
|
||||||
|
### Linux ###
|
||||||
|
*~
|
||||||
|
|
||||||
|
# temporary files which can be created if a process still has a handle open of a deleted file
|
||||||
|
.fuse_hidden*
|
||||||
|
|
||||||
|
# KDE directory preferences
|
||||||
|
.directory
|
||||||
|
|
||||||
|
# Linux trash folder which might appear on any partition or disk
|
||||||
|
.Trash-*
|
||||||
|
|
||||||
|
# .nfs files are created when an open file is removed but is still being accessed
|
||||||
|
.nfs*
|
||||||
|
|
||||||
|
### macOS ###
|
||||||
|
# General
|
||||||
|
.DS_Store
|
||||||
|
.AppleDouble
|
||||||
|
.LSOverride
|
||||||
|
|
||||||
|
# Icon must end with two \r
|
||||||
|
Icon
|
||||||
|
|
||||||
|
|
||||||
|
# Thumbnails
|
||||||
|
._*
|
||||||
|
|
||||||
|
# Files that might appear in the root of a volume
|
||||||
|
.DocumentRevisions-V100
|
||||||
|
.fseventsd
|
||||||
|
.Spotlight-V100
|
||||||
|
.TemporaryItems
|
||||||
|
.Trashes
|
||||||
|
.VolumeIcon.icns
|
||||||
|
.com.apple.timemachine.donotpresent
|
||||||
|
|
||||||
|
# Directories potentially created on remote AFP share
|
||||||
|
.AppleDB
|
||||||
|
.AppleDesktop
|
||||||
|
Network Trash Folder
|
||||||
|
Temporary Items
|
||||||
|
.apdisk
|
||||||
|
|
||||||
|
### macOS Patch ###
|
||||||
|
# iCloud generated files
|
||||||
|
*.icloud
|
||||||
|
|
||||||
|
### Node ###
|
||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
.pnpm-debug.log*
|
||||||
|
|
||||||
|
# Diagnostic reports (https://nodejs.org/api/report.html)
|
||||||
|
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
||||||
|
|
||||||
|
# Runtime data
|
||||||
|
pids
|
||||||
|
*.pid
|
||||||
|
*.seed
|
||||||
|
*.pid.lock
|
||||||
|
|
||||||
|
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||||
|
lib-cov
|
||||||
|
|
||||||
|
# Coverage directory used by tools like istanbul
|
||||||
|
coverage
|
||||||
|
*.lcov
|
||||||
|
|
||||||
|
# nyc test coverage
|
||||||
|
.nyc_output
|
||||||
|
|
||||||
|
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
|
||||||
|
.grunt
|
||||||
|
|
||||||
|
# Bower dependency directory (https://bower.io/)
|
||||||
|
bower_components
|
||||||
|
|
||||||
|
# node-waf configuration
|
||||||
|
.lock-wscript
|
||||||
|
|
||||||
|
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
||||||
|
build/Release
|
||||||
|
|
||||||
|
# Dependency directories
|
||||||
|
node_modules/
|
||||||
|
jspm_packages/
|
||||||
|
|
||||||
|
# Snowpack dependency directory (https://snowpack.dev/)
|
||||||
|
web_modules/
|
||||||
|
|
||||||
|
# TypeScript cache
|
||||||
|
*.tsbuildinfo
|
||||||
|
|
||||||
|
# Optional npm cache directory
|
||||||
|
.npm
|
||||||
|
|
||||||
|
# Optional eslint cache
|
||||||
|
.eslintcache
|
||||||
|
|
||||||
|
# Optional stylelint cache
|
||||||
|
.stylelintcache
|
||||||
|
|
||||||
|
# Microbundle cache
|
||||||
|
.rpt2_cache/
|
||||||
|
.rts2_cache_cjs/
|
||||||
|
.rts2_cache_es/
|
||||||
|
.rts2_cache_umd/
|
||||||
|
|
||||||
|
# Optional REPL history
|
||||||
|
.node_repl_history
|
||||||
|
|
||||||
|
# Output of 'npm pack'
|
||||||
|
*.tgz
|
||||||
|
|
||||||
|
# Yarn Integrity file
|
||||||
|
.yarn-integrity
|
||||||
|
|
||||||
|
# dotenv environment variable files
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
!.env.example
|
||||||
|
|
||||||
|
# parcel-bundler cache (https://parceljs.org/)
|
||||||
|
.cache
|
||||||
|
.parcel-cache
|
||||||
|
|
||||||
|
# Next.js build output
|
||||||
|
.next
|
||||||
|
out
|
||||||
|
|
||||||
|
# Nuxt.js build / generate output
|
||||||
|
.nuxt
|
||||||
|
dist
|
||||||
|
|
||||||
|
# Gatsby files
|
||||||
|
.cache/
|
||||||
|
# Comment in the public line in if your project uses Gatsby and not Next.js
|
||||||
|
# https://nextjs.org/blog/next-9-1#public-directory-support
|
||||||
|
# public
|
||||||
|
|
||||||
|
# vuepress build output
|
||||||
|
.vuepress/dist
|
||||||
|
|
||||||
|
# vuepress v2.x temp and cache directory
|
||||||
|
.temp
|
||||||
|
|
||||||
|
# Docusaurus cache and generated files
|
||||||
|
.docusaurus
|
||||||
|
|
||||||
|
# Serverless directories
|
||||||
|
.serverless/
|
||||||
|
|
||||||
|
# FuseBox cache
|
||||||
|
.fusebox/
|
||||||
|
|
||||||
|
# DynamoDB Local files
|
||||||
|
.dynamodb/
|
||||||
|
|
||||||
|
# TernJS port file
|
||||||
|
.tern-port
|
||||||
|
|
||||||
|
# Stores VSCode versions used for testing VSCode extensions
|
||||||
|
.vscode-test
|
||||||
|
|
||||||
|
# yarn v2
|
||||||
|
.yarn/cache
|
||||||
|
.yarn/unplugged
|
||||||
|
.yarn/build-state.yml
|
||||||
|
.yarn/install-state.gz
|
||||||
|
.pnp.*
|
||||||
|
|
||||||
|
### Node Patch ###
|
||||||
|
# Serverless Webpack directories
|
||||||
|
.webpack/
|
||||||
|
|
||||||
|
# Optional stylelint cache
|
||||||
|
|
||||||
|
# SvelteKit build / generate output
|
||||||
|
.svelte-kit
|
||||||
|
|
||||||
|
### VisualStudioCode ###
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/settings.json
|
||||||
|
!.vscode/tasks.json
|
||||||
|
!.vscode/launch.json
|
||||||
|
!.vscode/extensions.json
|
||||||
|
!.vscode/*.code-snippets
|
||||||
|
|
||||||
|
# Local History for Visual Studio Code
|
||||||
|
.history/
|
||||||
|
|
||||||
|
# Built Visual Studio Code Extensions
|
||||||
|
*.vsix
|
||||||
|
|
||||||
|
### VisualStudioCode Patch ###
|
||||||
|
# Ignore all local history of files
|
||||||
|
.history
|
||||||
|
.ionide
|
||||||
|
|
||||||
|
### Windows ###
|
||||||
|
# Windows thumbnail cache files
|
||||||
|
Thumbs.db
|
||||||
|
Thumbs.db:encryptable
|
||||||
|
ehthumbs.db
|
||||||
|
ehthumbs_vista.db
|
||||||
|
|
||||||
|
# Dump file
|
||||||
|
*.stackdump
|
||||||
|
|
||||||
|
# Folder config file
|
||||||
|
[Dd]esktop.ini
|
||||||
|
|
||||||
|
# Recycle Bin used on file shares
|
||||||
|
$RECYCLE.BIN/
|
||||||
|
|
||||||
|
# Windows Installer files
|
||||||
|
*.cab
|
||||||
|
*.msi
|
||||||
|
*.msix
|
||||||
|
*.msm
|
||||||
|
*.msp
|
||||||
|
|
||||||
|
# Windows shortcuts
|
||||||
|
*.lnk
|
||||||
|
|
||||||
|
# End of https://www.toptal.com/developers/gitignore/api/node,visualstudiocode,windows,macos,linux
|
||||||
50
.vscode/settings.json
vendored
Normal file
50
.vscode/settings.json
vendored
Normal file
|
|
@ -0,0 +1,50 @@
|
||||||
|
{
|
||||||
|
// Disable the default formatter, use eslint instead
|
||||||
|
"prettier.enable": false,
|
||||||
|
|
||||||
|
// Auto fix
|
||||||
|
"editor.codeActionsOnSave": {
|
||||||
|
"source.fixAll.eslint": "explicit",
|
||||||
|
"source.organizeImports": "never"
|
||||||
|
},
|
||||||
|
|
||||||
|
// Silent the stylistic rules in you IDE, but still auto fix them
|
||||||
|
"eslint.rules.customizations": [
|
||||||
|
{ "rule": "style/*", "severity": "off", "fixable": true },
|
||||||
|
{ "rule": "format/*", "severity": "off", "fixable": true },
|
||||||
|
{ "rule": "*-indent", "severity": "off", "fixable": true },
|
||||||
|
{ "rule": "*-spacing", "severity": "off", "fixable": true },
|
||||||
|
{ "rule": "*-spaces", "severity": "off", "fixable": true },
|
||||||
|
{ "rule": "*-order", "severity": "off", "fixable": true },
|
||||||
|
{ "rule": "*-dangle", "severity": "off", "fixable": true },
|
||||||
|
{ "rule": "*-newline", "severity": "off", "fixable": true },
|
||||||
|
{ "rule": "*quotes", "severity": "off", "fixable": true },
|
||||||
|
{ "rule": "*semi", "severity": "off", "fixable": true }
|
||||||
|
],
|
||||||
|
|
||||||
|
// Enable eslint for all supported languages
|
||||||
|
"eslint.validate": [
|
||||||
|
"javascript",
|
||||||
|
"javascriptreact",
|
||||||
|
"typescript",
|
||||||
|
"typescriptreact",
|
||||||
|
"vue",
|
||||||
|
"html",
|
||||||
|
"markdown",
|
||||||
|
"json",
|
||||||
|
"json5",
|
||||||
|
"jsonc",
|
||||||
|
"yaml",
|
||||||
|
"toml",
|
||||||
|
"xml",
|
||||||
|
"gql",
|
||||||
|
"graphql",
|
||||||
|
"astro",
|
||||||
|
"svelte",
|
||||||
|
"css",
|
||||||
|
"less",
|
||||||
|
"scss",
|
||||||
|
"pcss",
|
||||||
|
"postcss"
|
||||||
|
]
|
||||||
|
}
|
||||||
11
Dockerfile
Normal file
11
Dockerfile
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
FROM node:22-alpine AS base
|
||||||
|
ENV PNPM_HOME="/pnpm"
|
||||||
|
ENV PATH="$PNPM_HOME:$PATH"
|
||||||
|
RUN corepack enable
|
||||||
|
|
||||||
|
COPY . /app
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
RUN pnpm install --prod --frozen-lockfile
|
||||||
|
|
||||||
|
CMD ["pnpm", "start"]
|
||||||
17
drizzle/0000_outgoing_magneto.sql
Normal file
17
drizzle/0000_outgoing_magneto.sql
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
CREATE TABLE "role_members" (
|
||||||
|
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||||
|
"role_id" uuid,
|
||||||
|
"user_id" bigint NOT NULL,
|
||||||
|
"created_at" timestamp DEFAULT now() NOT NULL
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE "roles" (
|
||||||
|
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||||
|
"slug" varchar(32) NOT NULL,
|
||||||
|
"aliases" varchar(32)[] DEFAULT '{}' NOT NULL,
|
||||||
|
"chat_id" bigint NOT NULL,
|
||||||
|
"created_at" timestamp DEFAULT now() NOT NULL,
|
||||||
|
"updated_at" timestamp DEFAULT now() NOT NULL
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
ALTER TABLE "role_members" ADD CONSTRAINT "role_members_role_id_roles_id_fk" FOREIGN KEY ("role_id") REFERENCES "public"."roles"("id") ON DELETE no action ON UPDATE no action;
|
||||||
125
drizzle/meta/0000_snapshot.json
Normal file
125
drizzle/meta/0000_snapshot.json
Normal file
|
|
@ -0,0 +1,125 @@
|
||||||
|
{
|
||||||
|
"id": "2a786bb1-c05b-4d36-a401-293ccb96076f",
|
||||||
|
"prevId": "00000000-0000-0000-0000-000000000000",
|
||||||
|
"version": "7",
|
||||||
|
"dialect": "postgresql",
|
||||||
|
"tables": {
|
||||||
|
"public.role_members": {
|
||||||
|
"name": "role_members",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "uuid",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "gen_random_uuid()"
|
||||||
|
},
|
||||||
|
"role_id": {
|
||||||
|
"name": "role_id",
|
||||||
|
"type": "uuid",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"user_id": {
|
||||||
|
"name": "user_id",
|
||||||
|
"type": "bigint",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"role_members_role_id_roles_id_fk": {
|
||||||
|
"name": "role_members_role_id_roles_id_fk",
|
||||||
|
"tableFrom": "role_members",
|
||||||
|
"tableTo": "roles",
|
||||||
|
"columnsFrom": [
|
||||||
|
"role_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.roles": {
|
||||||
|
"name": "roles",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "uuid",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "gen_random_uuid()"
|
||||||
|
},
|
||||||
|
"slug": {
|
||||||
|
"name": "slug",
|
||||||
|
"type": "varchar(32)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"aliases": {
|
||||||
|
"name": "aliases",
|
||||||
|
"type": "varchar(32)[]",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "'{}'"
|
||||||
|
},
|
||||||
|
"chat_id": {
|
||||||
|
"name": "chat_id",
|
||||||
|
"type": "bigint",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
},
|
||||||
|
"updated_at": {
|
||||||
|
"name": "updated_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"enums": {},
|
||||||
|
"schemas": {},
|
||||||
|
"sequences": {},
|
||||||
|
"roles": {},
|
||||||
|
"policies": {},
|
||||||
|
"views": {},
|
||||||
|
"_meta": {
|
||||||
|
"columns": {},
|
||||||
|
"schemas": {},
|
||||||
|
"tables": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
13
drizzle/meta/_journal.json
Normal file
13
drizzle/meta/_journal.json
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
{
|
||||||
|
"version": "7",
|
||||||
|
"dialect": "postgresql",
|
||||||
|
"entries": [
|
||||||
|
{
|
||||||
|
"idx": 0,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1763720438133,
|
||||||
|
"tag": "0000_outgoing_magneto",
|
||||||
|
"breakpoints": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
13
eslint.config.mjs
Normal file
13
eslint.config.mjs
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
import antfu from '@antfu/eslint-config'
|
||||||
|
|
||||||
|
export default antfu({
|
||||||
|
stylistic: {
|
||||||
|
indent: 2,
|
||||||
|
quotes: 'single',
|
||||||
|
},
|
||||||
|
typescript: true,
|
||||||
|
rules: {
|
||||||
|
'antfu/no-top-level-await': 'off',
|
||||||
|
'no-console': 'off',
|
||||||
|
},
|
||||||
|
})
|
||||||
30
package.json
Normal file
30
package.json
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
{
|
||||||
|
"name": "roleping-bot",
|
||||||
|
"type": "module",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"packageManager": "pnpm@10.23.0+sha512.21c4e5698002ade97e4efe8b8b4a89a8de3c85a37919f957e7a0f30f38fbc5bbdd05980ffe29179b2fb6e6e691242e098d945d1601772cad0fef5fb6411e2a4b",
|
||||||
|
"author": {
|
||||||
|
"name": "devilreef",
|
||||||
|
"url": "https://github.com/devilr33f"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"dev": "cross-env NODE_ENV=development tsx --watch src/index.ts",
|
||||||
|
"start": "tsx src/index.ts"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@gramio/autoload": "^1.1.0",
|
||||||
|
"drizzle-orm": "^0.44.7",
|
||||||
|
"env-var": "^7.5.0",
|
||||||
|
"gramio": "^0.4.11",
|
||||||
|
"pg": "^8.16.3",
|
||||||
|
"tsx": "^4.20.3"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@antfu/eslint-config": "^4.16.2",
|
||||||
|
"@types/node": "^22.16.3",
|
||||||
|
"@types/pg": "^8.15.6",
|
||||||
|
"cross-env": "^7.0.3",
|
||||||
|
"drizzle-kit": "^0.31.7",
|
||||||
|
"eslint": "^9.31.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
3761
pnpm-lock.yaml
generated
Normal file
3761
pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load diff
2
pnpm-workspace.yaml
Normal file
2
pnpm-workspace.yaml
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
onlyBuiltDependencies:
|
||||||
|
- esbuild
|
||||||
65
src/bot/commands/admin/role-add.ts
Normal file
65
src/bot/commands/admin/role-add.ts
Normal file
|
|
@ -0,0 +1,65 @@
|
||||||
|
import type { BotType } from '@/bot/index.js'
|
||||||
|
import { bold, code, format, italic } from 'gramio'
|
||||||
|
import { RESERVED_NAMES, ROLE_NAME_REGEX } from '@/bot/utilities/constants.js'
|
||||||
|
import { isChatAdmin } from '@/bot/utilities/perms.js'
|
||||||
|
import { RoleService } from '@/shared/services/role.js'
|
||||||
|
|
||||||
|
export default function (bot: BotType) {
|
||||||
|
bot.command('roleadd', async (ctx) => {
|
||||||
|
if (!['group', 'supergroup'].includes(ctx.chat.type)) {
|
||||||
|
return ctx.reply('This command can only be used in groups or supergroups')
|
||||||
|
}
|
||||||
|
|
||||||
|
const isAdmin = await isChatAdmin(ctx.chat.id, ctx.from.id)
|
||||||
|
if (!isAdmin) {
|
||||||
|
return ctx.reply('this command can only be used by admins')
|
||||||
|
}
|
||||||
|
|
||||||
|
let [slug] = (ctx.args ?? '').split(' ')
|
||||||
|
slug = slug.toLowerCase()
|
||||||
|
|
||||||
|
if (!slug) {
|
||||||
|
return ctx.reply(format`where is role name bro?\nusage: ${code('/roleadd <name>')}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ROLE_NAME_REGEX.test(slug)) {
|
||||||
|
return ctx.reply(format`invalid role name\n${italic('it should be 4-32 characters long and contain only letters')}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (RESERVED_NAMES.includes(slug)) {
|
||||||
|
return ctx.reply('this name is reserved and can\'t be used')
|
||||||
|
}
|
||||||
|
|
||||||
|
const role = await RoleService.getBySlugOrAlias(slug, ctx.chatId)
|
||||||
|
if (role) {
|
||||||
|
return ctx.reply('role already exists with this slug or alias')
|
||||||
|
}
|
||||||
|
|
||||||
|
const newRole = await RoleService.create({ slug, chatId: ctx.chat.id }).catch(() => null)
|
||||||
|
if (!newRole) {
|
||||||
|
return ctx.reply('failed to create role, skill issue')
|
||||||
|
}
|
||||||
|
|
||||||
|
setImmediate(async () => {
|
||||||
|
const roles = await RoleService.getByChatId(ctx.chatId)
|
||||||
|
await bot.api.setMyCommands({
|
||||||
|
scope: {
|
||||||
|
chat_id: ctx.chatId,
|
||||||
|
type: 'chat'
|
||||||
|
},
|
||||||
|
commands: roles.map((role) => ({
|
||||||
|
command: role.slug,
|
||||||
|
description: `Mention all members of this role`,
|
||||||
|
})),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
return ctx.reply(
|
||||||
|
format`
|
||||||
|
${bold(`role "${slug}" created`)}
|
||||||
|
|
||||||
|
you can now join to this role by using the command ${code(`/join ${slug}`)}
|
||||||
|
`,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
53
src/bot/commands/admin/role-delete.ts
Normal file
53
src/bot/commands/admin/role-delete.ts
Normal file
|
|
@ -0,0 +1,53 @@
|
||||||
|
import { BotType } from "@/bot/index.js"
|
||||||
|
import { isChatAdmin } from "@/bot/utilities/perms.js"
|
||||||
|
import { RoleService } from "@/shared/services/role.js"
|
||||||
|
import { bold, code, format } from "gramio"
|
||||||
|
|
||||||
|
export default (bot: BotType) => {
|
||||||
|
bot.command('roledel', async (ctx) => {
|
||||||
|
if (!['group', 'supergroup'].includes(ctx.chat.type)) {
|
||||||
|
return ctx.reply('this command can only be used in groups or supergroups')
|
||||||
|
}
|
||||||
|
|
||||||
|
const isAdmin = await isChatAdmin(ctx.chat.id, ctx.from.id)
|
||||||
|
if (!isAdmin) {
|
||||||
|
return ctx.reply('this command can only be used by admins')
|
||||||
|
}
|
||||||
|
|
||||||
|
let [roleSlug] = (ctx.args ?? '').trim().split(' ')
|
||||||
|
if (!roleSlug) {
|
||||||
|
return ctx.reply(format`where is the role name bro?\nusage: ${code('/roledel <name>')}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
roleSlug = roleSlug.toLowerCase()
|
||||||
|
|
||||||
|
const role = await RoleService.getBySlugOrAlias(roleSlug, ctx.chatId)
|
||||||
|
if (!role) {
|
||||||
|
return ctx.reply('role not found')
|
||||||
|
}
|
||||||
|
|
||||||
|
const members = await RoleService.getMemberIds(roleSlug, ctx.chatId)
|
||||||
|
await Promise.all(members.map((member) => RoleService.removeMember(role.id!, member)))
|
||||||
|
|
||||||
|
const deletedRole = await RoleService.delete(role.id!)
|
||||||
|
if (!deletedRole) {
|
||||||
|
return ctx.reply('failed to delete role, skill issue')
|
||||||
|
}
|
||||||
|
|
||||||
|
setImmediate(async () => {
|
||||||
|
const roles = await RoleService.getByChatId(ctx.chatId)
|
||||||
|
await bot.api.setMyCommands({
|
||||||
|
scope: {
|
||||||
|
chat_id: ctx.chatId,
|
||||||
|
type: 'chat'
|
||||||
|
},
|
||||||
|
commands: roles.map((role) => ({
|
||||||
|
command: role.slug,
|
||||||
|
description: `Mention all members of this role`,
|
||||||
|
})),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
return ctx.reply(format`role ${bold(role.slug)} deleted`)
|
||||||
|
})
|
||||||
|
}
|
||||||
23
src/bot/commands/debug/reset-autocomplete.ts
Normal file
23
src/bot/commands/debug/reset-autocomplete.ts
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
import { BotType } from "@/bot/index.js"
|
||||||
|
|
||||||
|
export default (bot: BotType) => {
|
||||||
|
bot.command('resetautocomplete', async (ctx) => {
|
||||||
|
if (!['group', 'supergroup'].includes(ctx.chat.type)) {
|
||||||
|
return ctx.reply('this command can only be used in groups or supergroups')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ctx.senderId !== 6968210430) {
|
||||||
|
return ctx.reply('go fuck yourself')
|
||||||
|
}
|
||||||
|
|
||||||
|
await bot.api.setMyCommands({
|
||||||
|
scope: {
|
||||||
|
chat_id: ctx.chatId,
|
||||||
|
type: 'chat'
|
||||||
|
},
|
||||||
|
commands: [],
|
||||||
|
})
|
||||||
|
|
||||||
|
return ctx.reply('autocomplete reset, restart ur client')
|
||||||
|
})
|
||||||
|
}
|
||||||
43
src/bot/commands/user/join.ts
Normal file
43
src/bot/commands/user/join.ts
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
import type { BotType } from '@/bot/index.js'
|
||||||
|
|
||||||
|
import { bold, code, format, italic } from 'gramio'
|
||||||
|
import { ROLE_NAME_REGEX } from '@/bot/utilities/constants.js'
|
||||||
|
import { RoleService } from '@/shared/services/role.js'
|
||||||
|
|
||||||
|
export default (bot: BotType) => {
|
||||||
|
bot.command('join', async (ctx) => {
|
||||||
|
if (!['group', 'supergroup'].includes(ctx.chat.type)) {
|
||||||
|
return ctx.reply('This command can only be used in groups or supergroups')
|
||||||
|
}
|
||||||
|
|
||||||
|
let [slug] = (ctx.args ?? '').trim().split(' ')
|
||||||
|
slug = slug.toLowerCase()
|
||||||
|
|
||||||
|
if (!slug) {
|
||||||
|
return ctx.reply(format`where is role name bro?\nusage: ${code('/join <name>')}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ROLE_NAME_REGEX.test(slug)) {
|
||||||
|
return ctx.reply(format`invalid role name\n${italic('it should be 4-32 characters long and contain only letters')}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const role = await RoleService.getBySlugOrAlias(slug, ctx.chatId)
|
||||||
|
console.log(role)
|
||||||
|
if (!role) {
|
||||||
|
return ctx.reply('role not found')
|
||||||
|
}
|
||||||
|
|
||||||
|
const isMember = await RoleService.getMemberById(role.id!, ctx.chatId, ctx.from.id)
|
||||||
|
console.log(isMember)
|
||||||
|
if (isMember !== null) {
|
||||||
|
return ctx.reply('you are already in this role')
|
||||||
|
}
|
||||||
|
|
||||||
|
const newMember = await RoleService.addMember(role.id!, ctx.from.id)
|
||||||
|
if (!newMember) {
|
||||||
|
return ctx.reply('failed to join role, skill issue')
|
||||||
|
}
|
||||||
|
|
||||||
|
return ctx.reply(format`you have joined the role ${bold(role.slug)}`)
|
||||||
|
})
|
||||||
|
}
|
||||||
18
src/bot/commands/user/myroles.ts
Normal file
18
src/bot/commands/user/myroles.ts
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
import { BotType } from "@/bot/index.js"
|
||||||
|
import { RoleService } from "@/shared/services/role.js"
|
||||||
|
import { format, join } from "gramio"
|
||||||
|
|
||||||
|
export default (bot: BotType) => {
|
||||||
|
bot.command('myroles', async (ctx) => {
|
||||||
|
if (!['group', 'supergroup'].includes(ctx.chat.type)) {
|
||||||
|
return ctx.reply('this command can only be used in groups or supergroups')
|
||||||
|
}
|
||||||
|
|
||||||
|
const roles = await RoleService.getByChatIdAndUserId(ctx.chatId, ctx.from.id)
|
||||||
|
if (!roles) {
|
||||||
|
return ctx.reply('you are not a member of any roles')
|
||||||
|
}
|
||||||
|
|
||||||
|
return ctx.reply(format`you are a member of the following roles:\n${join(roles, (role) => format`- ${role.slug} (${role.memberCount} members)`, '\n')}`)
|
||||||
|
})
|
||||||
|
}
|
||||||
36
src/bot/commands/user/remove.ts
Normal file
36
src/bot/commands/user/remove.ts
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
import { BotType } from "@/bot/index.js"
|
||||||
|
import { RoleService } from "@/shared/services/role.js"
|
||||||
|
import { bold, code, format } from "gramio"
|
||||||
|
|
||||||
|
export default (bot: BotType) => {
|
||||||
|
bot.command('leave', async (ctx) => {
|
||||||
|
if (!['group', 'supergroup'].includes(ctx.chat.type)) {
|
||||||
|
return ctx.reply('this command can only be used in groups or supergroups')
|
||||||
|
}
|
||||||
|
|
||||||
|
let [roleSlug] = (ctx.args ?? '').trim().split(' ')
|
||||||
|
roleSlug = roleSlug.toLowerCase()
|
||||||
|
|
||||||
|
if (!roleSlug) {
|
||||||
|
return ctx.reply(format`where is the role name bro?\nusage: ${code('/leave <name>')}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const role = await RoleService.getBySlugOrAlias(roleSlug, ctx.chatId)
|
||||||
|
if (!role) {
|
||||||
|
return ctx.reply('role not found')
|
||||||
|
}
|
||||||
|
|
||||||
|
const isMember = await RoleService.getMemberById(role.id!, ctx.chatId, ctx.from.id)
|
||||||
|
console.log(isMember)
|
||||||
|
if (isMember === null) {
|
||||||
|
return ctx.reply('you are not a member of this role')
|
||||||
|
}
|
||||||
|
|
||||||
|
const removedMember = await RoleService.removeMember(role.id!, ctx.from.id)
|
||||||
|
if (!removedMember) {
|
||||||
|
return ctx.reply('failed to remove you from the role, skill issue')
|
||||||
|
}
|
||||||
|
|
||||||
|
return ctx.reply(format`you left the role ${bold(role.slug)}`)
|
||||||
|
})
|
||||||
|
}
|
||||||
18
src/bot/commands/user/roles.ts
Normal file
18
src/bot/commands/user/roles.ts
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
import { BotType } from "@/bot/index.js"
|
||||||
|
import { RoleService } from "@/shared/services/role.js"
|
||||||
|
import { format, join } from "gramio"
|
||||||
|
|
||||||
|
export default (bot: BotType) => {
|
||||||
|
bot.command('roles', async (ctx) => {
|
||||||
|
if (!['group', 'supergroup'].includes(ctx.chat.type)) {
|
||||||
|
return ctx.reply('this command can only be used in groups or supergroups')
|
||||||
|
}
|
||||||
|
|
||||||
|
const roles = await RoleService.getByChatId(ctx.chatId)
|
||||||
|
if (!roles) {
|
||||||
|
return ctx.reply('no roles found')
|
||||||
|
}
|
||||||
|
|
||||||
|
return ctx.reply(format`this chat has the following roles:\n${join(roles, (role) => format`- ${role.slug} (${role.memberCount} members)`, '\n')}`)
|
||||||
|
})
|
||||||
|
}
|
||||||
89
src/bot/index.ts
Normal file
89
src/bot/index.ts
Normal file
|
|
@ -0,0 +1,89 @@
|
||||||
|
import { join } from 'node:path'
|
||||||
|
import { autoload } from '@gramio/autoload'
|
||||||
|
import { Bot, mention } from 'gramio'
|
||||||
|
import env from '@/shared/env.js'
|
||||||
|
import { RoleService } from '@/shared/services/role.js'
|
||||||
|
import { MentionService } from '@/shared/services/mention.js'
|
||||||
|
|
||||||
|
let botUsername: string | undefined
|
||||||
|
|
||||||
|
export const bot = new Bot(env.botToken)
|
||||||
|
.extend(autoload({
|
||||||
|
path: join(import.meta.dirname, 'commands'),
|
||||||
|
skipImportErrors: true,
|
||||||
|
}))
|
||||||
|
export type BotType = typeof bot
|
||||||
|
|
||||||
|
bot.on('message', async (ctx, next) => {
|
||||||
|
// if (ctx.chatId !== -1003212318013) {
|
||||||
|
// return
|
||||||
|
// }
|
||||||
|
|
||||||
|
if (!ctx.hasEntities('mention')) {
|
||||||
|
return next()
|
||||||
|
}
|
||||||
|
|
||||||
|
const mentions = ctx.text?.match(/@(\w{4,32})/g) ?? []
|
||||||
|
if (!mentions.length) {
|
||||||
|
return next()
|
||||||
|
}
|
||||||
|
const rolesToMention = await Promise.all(mentions.map((mention) => RoleService.getBySlugOrAlias(mention.slice(1), ctx.chatId))).then((roles) => roles.filter((role) => role !== null))
|
||||||
|
|
||||||
|
if (!rolesToMention.length) {
|
||||||
|
return next()
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const role of rolesToMention) {
|
||||||
|
await MentionService.mentionAll(role.slug, ctx.chatId, ctx.from, ctx.replyMessage?.id ?? undefined)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
bot.on('message', async (ctx, next) => {
|
||||||
|
if (!['group', 'supergroup'].includes(ctx.chat.type)) {
|
||||||
|
return next()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ctx.hasEntities('bot_command')) {
|
||||||
|
return next()
|
||||||
|
}
|
||||||
|
|
||||||
|
const text = ctx.text ?? ''
|
||||||
|
const match = text.match(/^\/(\w{4,32})(?:@(\w{4,32}))?\s*$/)
|
||||||
|
if (!match) {
|
||||||
|
return next()
|
||||||
|
}
|
||||||
|
|
||||||
|
const [, command, atUsername] = match
|
||||||
|
|
||||||
|
// If command is addressed to a specific bot, ensure it's us
|
||||||
|
if (atUsername && botUsername && atUsername.toLowerCase() !== botUsername.toLowerCase()) {
|
||||||
|
return next()
|
||||||
|
}
|
||||||
|
|
||||||
|
const role = await RoleService.getBySlugOrAlias(command, ctx.chatId)
|
||||||
|
if (!role) {
|
||||||
|
return next()
|
||||||
|
}
|
||||||
|
|
||||||
|
await MentionService.mentionAll(role.slug, ctx.chatId, ctx.from, ctx.replyMessage?.id ?? undefined)
|
||||||
|
})
|
||||||
|
|
||||||
|
bot.onStart(async ({ info }) => {
|
||||||
|
botUsername = info.username
|
||||||
|
console.log(`Bot started as @${info.username}`)
|
||||||
|
|
||||||
|
await bot.api.setMyCommands({
|
||||||
|
scope: {
|
||||||
|
type: 'default'
|
||||||
|
},
|
||||||
|
commands: [
|
||||||
|
{ command: 'roleadd', description: 'Add a new role in this chat' },
|
||||||
|
{ command: 'roledel', description: 'Delete a role' },
|
||||||
|
{ command: 'join', description: 'Join a role' },
|
||||||
|
{ command: 'leave', description: 'Leave a role' },
|
||||||
|
{ command: 'myroles', description: 'View your roles in this chat' },
|
||||||
|
{ command: 'roles', description: 'View all roles in this chat' },
|
||||||
|
],
|
||||||
|
suppress: true
|
||||||
|
})
|
||||||
|
})
|
||||||
2
src/bot/utilities/constants.ts
Normal file
2
src/bot/utilities/constants.ts
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
export const ROLE_NAME_REGEX = /^[a-zA-Z]{4,32}$/
|
||||||
|
export const RESERVED_NAMES = ['roleadd', 'roledel', 'join', 'leave', 'myroles', 'roles']
|
||||||
19
src/bot/utilities/perms.ts
Normal file
19
src/bot/utilities/perms.ts
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
import { bot } from '../index.js'
|
||||||
|
|
||||||
|
export async function isChatAdmin(chatId: number, userId: number) {
|
||||||
|
try {
|
||||||
|
// handle hidden administrators
|
||||||
|
if (userId === chatId || userId === 1087968824)
|
||||||
|
return true
|
||||||
|
|
||||||
|
const chatAdmins = await bot.api.getChatAdministrators({
|
||||||
|
chat_id: chatId,
|
||||||
|
})
|
||||||
|
|
||||||
|
return chatAdmins.some(admin => admin.user.id === userId)
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
console.error('Error checking if user is chat admin:', error)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
5
src/index.ts
Normal file
5
src/index.ts
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
import { bot } from './bot/index.js'
|
||||||
|
|
||||||
|
await bot.start({
|
||||||
|
dropPendingUpdates: true
|
||||||
|
})
|
||||||
15
src/shared/database/config.ts
Normal file
15
src/shared/database/config.ts
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
import { existsSync } from 'node:fs'
|
||||||
|
import { env, loadEnvFile } from 'node:process'
|
||||||
|
import { defineConfig } from 'drizzle-kit'
|
||||||
|
|
||||||
|
if (existsSync('.env'))
|
||||||
|
loadEnvFile('.env')
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
out: './drizzle',
|
||||||
|
schema: './src/shared/database/schema.ts',
|
||||||
|
dialect: 'postgresql',
|
||||||
|
dbCredentials: {
|
||||||
|
url: env.DATABASE_URL ?? '',
|
||||||
|
},
|
||||||
|
})
|
||||||
4
src/shared/database/index.ts
Normal file
4
src/shared/database/index.ts
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
import { drizzle } from 'drizzle-orm/node-postgres'
|
||||||
|
import env from '../env.js'
|
||||||
|
|
||||||
|
export default drizzle(env.databaseUrl)
|
||||||
20
src/shared/database/schema.ts
Normal file
20
src/shared/database/schema.ts
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
import { sql } from 'drizzle-orm'
|
||||||
|
import { bigint, pgTable, timestamp, uuid, varchar } from 'drizzle-orm/pg-core'
|
||||||
|
|
||||||
|
export const roleSchema = pgTable('roles', {
|
||||||
|
id: uuid('id').primaryKey().defaultRandom(),
|
||||||
|
slug: varchar('slug', { length: 32 }).notNull(),
|
||||||
|
aliases: varchar('aliases', { length: 32 }).array().notNull().default([]),
|
||||||
|
chatId: bigint('chat_id', { mode: 'number' }).notNull(),
|
||||||
|
|
||||||
|
createdAt: timestamp('created_at').notNull().defaultNow(),
|
||||||
|
updatedAt: timestamp('updated_at').notNull().defaultNow().$onUpdate(() => sql`now()`),
|
||||||
|
})
|
||||||
|
|
||||||
|
export const roleMembersSchema = pgTable('role_members', {
|
||||||
|
id: uuid('id').primaryKey().defaultRandom(),
|
||||||
|
roleId: uuid('role_id').references(() => roleSchema.id),
|
||||||
|
userId: bigint('user_id', { mode: 'number' }).notNull(),
|
||||||
|
|
||||||
|
createdAt: timestamp('created_at').notNull().defaultNow(),
|
||||||
|
})
|
||||||
12
src/shared/env.ts
Normal file
12
src/shared/env.ts
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
import { existsSync } from 'node:fs'
|
||||||
|
import { loadEnvFile } from 'node:process'
|
||||||
|
import env from 'env-var'
|
||||||
|
|
||||||
|
if (existsSync('.env'))
|
||||||
|
loadEnvFile('.env')
|
||||||
|
|
||||||
|
export default {
|
||||||
|
mode: env.get('NODE_ENV').default('production').asString(),
|
||||||
|
databaseUrl: env.get('DATABASE_URL').required().asString(),
|
||||||
|
botToken: env.get('BOT_TOKEN').required().asString(),
|
||||||
|
}
|
||||||
39
src/shared/services/mention.ts
Normal file
39
src/shared/services/mention.ts
Normal file
|
|
@ -0,0 +1,39 @@
|
||||||
|
import { bold, format, join, mention, TelegramUser, User } from 'gramio'
|
||||||
|
import { chunk } from '../utilities/chunk.js'
|
||||||
|
import { RoleService } from './role.js'
|
||||||
|
import { bot } from '@/bot/index.js'
|
||||||
|
|
||||||
|
export class MentionService {
|
||||||
|
static async mentionAll(query: string, chatId: number, caller: User, replyMessageId?: number) {
|
||||||
|
const role = await RoleService.getBySlugOrAlias(query, chatId)
|
||||||
|
if (!role) {
|
||||||
|
throw new Error('Role not found')
|
||||||
|
}
|
||||||
|
|
||||||
|
const members = await RoleService.getMemberIds(query, chatId)
|
||||||
|
const membersChunks = chunk(members, 5)
|
||||||
|
|
||||||
|
for (const memberChunk of membersChunks) {
|
||||||
|
const message = format`
|
||||||
|
${bold('Mass mention by')} ${bold(mention('user', { id: caller.id, is_bot: caller.isBot(), first_name: caller.firstName }))}
|
||||||
|
|
||||||
|
${join(this.buildMentions(memberChunk), (entity) => entity, ' ')}
|
||||||
|
`
|
||||||
|
|
||||||
|
await bot.api.sendMessage({
|
||||||
|
chat_id: role.chatId,
|
||||||
|
text: message,
|
||||||
|
...(replyMessageId ? {
|
||||||
|
reply_parameters: {
|
||||||
|
message_id: replyMessageId,
|
||||||
|
allow_sending_without_reply: true
|
||||||
|
}
|
||||||
|
}: {})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static buildMentions (ids: number[]) {
|
||||||
|
return ids.map((id) => mention('👋', { id, is_bot: false, first_name: '' }))
|
||||||
|
}
|
||||||
|
}
|
||||||
116
src/shared/services/role.ts
Normal file
116
src/shared/services/role.ts
Normal file
|
|
@ -0,0 +1,116 @@
|
||||||
|
import { arrayContains, eq, or, and, count } from 'drizzle-orm'
|
||||||
|
import { alias } from 'drizzle-orm/pg-core'
|
||||||
|
import database from '../database/index.js'
|
||||||
|
import { roleMembersSchema, roleSchema } from '../database/schema.js'
|
||||||
|
|
||||||
|
export class RoleService {
|
||||||
|
static getBySlugOrAlias(query: string, chatId: number) {
|
||||||
|
return database
|
||||||
|
.select()
|
||||||
|
.from(roleSchema)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
or(
|
||||||
|
eq(roleSchema.slug, query),
|
||||||
|
arrayContains(roleSchema.aliases, [query]),
|
||||||
|
),
|
||||||
|
eq(roleSchema.chatId, chatId),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.then(result => result[0] ?? null)
|
||||||
|
}
|
||||||
|
|
||||||
|
static getByChatId(chatId: number) {
|
||||||
|
return database
|
||||||
|
.select({
|
||||||
|
slug: roleSchema.slug,
|
||||||
|
memberCount: count(roleMembersSchema.id).as('memberCount'),
|
||||||
|
})
|
||||||
|
.from(roleSchema)
|
||||||
|
.leftJoin(roleMembersSchema, eq(roleMembersSchema.roleId, roleSchema.id))
|
||||||
|
.where(eq(roleSchema.chatId, chatId))
|
||||||
|
.groupBy(roleSchema.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
static getByChatIdAndUserId(chatId: number, userId: number) {
|
||||||
|
const rmAll = alias(roleMembersSchema, 'rm_all')
|
||||||
|
|
||||||
|
return database
|
||||||
|
.select({
|
||||||
|
slug: roleSchema.slug,
|
||||||
|
memberCount: count(rmAll.id).as('memberCount'),
|
||||||
|
})
|
||||||
|
.from(roleSchema)
|
||||||
|
.innerJoin(roleMembersSchema, eq(roleSchema.id, roleMembersSchema.roleId))
|
||||||
|
.leftJoin(rmAll, eq(roleSchema.id, rmAll.roleId))
|
||||||
|
.where(and(
|
||||||
|
eq(roleSchema.chatId, chatId),
|
||||||
|
eq(roleMembersSchema.userId, userId),
|
||||||
|
))
|
||||||
|
.groupBy(roleSchema.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
static getMemberById(roleId: string, chatId: number, userId: number) {
|
||||||
|
return database
|
||||||
|
.select()
|
||||||
|
.from(roleMembersSchema)
|
||||||
|
.leftJoin(roleSchema, eq(roleMembersSchema.roleId, roleSchema.id))
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(roleMembersSchema.roleId, roleId),
|
||||||
|
eq(roleMembersSchema.userId, userId),
|
||||||
|
eq(roleSchema.chatId, chatId),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.then(result => result[0] ?? null)
|
||||||
|
}
|
||||||
|
|
||||||
|
static getMemberIds(query: string, chatId: number) {
|
||||||
|
return database
|
||||||
|
.select({ userId: roleMembersSchema.userId })
|
||||||
|
.from(roleMembersSchema)
|
||||||
|
.leftJoin(roleSchema, eq(roleMembersSchema.roleId, roleSchema.id))
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
or(
|
||||||
|
eq(roleSchema.slug, query),
|
||||||
|
arrayContains(roleSchema.aliases, [query]),
|
||||||
|
),
|
||||||
|
eq(roleSchema.chatId, chatId),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.then(result => result.map(item => item.userId))
|
||||||
|
}
|
||||||
|
|
||||||
|
static create(data: { slug: string, chatId: number }) {
|
||||||
|
return database
|
||||||
|
.insert(roleSchema)
|
||||||
|
.values({ ...data, aliases: [] })
|
||||||
|
.returning()
|
||||||
|
.then(result => result[0] ?? null)
|
||||||
|
}
|
||||||
|
|
||||||
|
static delete(roleId: string) {
|
||||||
|
return database
|
||||||
|
.delete(roleSchema)
|
||||||
|
.where(eq(roleSchema.id, roleId))
|
||||||
|
.returning()
|
||||||
|
.then(result => result[0] ?? null)
|
||||||
|
}
|
||||||
|
|
||||||
|
static addMember(roleId: string, userId: number) {
|
||||||
|
return database
|
||||||
|
.insert(roleMembersSchema)
|
||||||
|
.values({ roleId, userId })
|
||||||
|
.returning()
|
||||||
|
.then(result => result[0] ?? null)
|
||||||
|
}
|
||||||
|
|
||||||
|
static removeMember(roleId: string, userId: number) {
|
||||||
|
return database
|
||||||
|
.delete(roleMembersSchema)
|
||||||
|
.where(and(eq(roleMembersSchema.roleId, roleId), eq(roleMembersSchema.userId, userId)))
|
||||||
|
.returning()
|
||||||
|
.then(result => result[0] ?? null)
|
||||||
|
}
|
||||||
|
}
|
||||||
4
src/shared/utilities/chunk.ts
Normal file
4
src/shared/utilities/chunk.ts
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
export function chunk(arr: any[], size: number) {
|
||||||
|
return Array.from({ length: Math.ceil(arr.length / size) }, (_: any, i: number) =>
|
||||||
|
arr.slice(i * size, i * size + size))
|
||||||
|
}
|
||||||
19
tsconfig.json
Normal file
19
tsconfig.json
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "es2024",
|
||||||
|
"emitDecoratorMetadata": true,
|
||||||
|
"experimentalDecorators": true,
|
||||||
|
"rootDir": "./src",
|
||||||
|
"module": "nodenext",
|
||||||
|
"moduleResolution": "nodenext",
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["./src/*"]
|
||||||
|
},
|
||||||
|
"strict": true,
|
||||||
|
"outDir": "./dist",
|
||||||
|
"sourceMap": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"skipLibCheck": true
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue