feat: initial commit
This commit is contained in:
commit
e54dee08c8
29 changed files with 4857 additions and 0 deletions
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
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue