feat: initial commit

This commit is contained in:
devilreef 2025-11-21 19:04:14 +06:00
commit e54dee08c8
29 changed files with 4857 additions and 0 deletions

View 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}`)}
`,
)
})
}

View 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`)
})
}

View 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')
})
}

View 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)}`)
})
}

View 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')}`)
})
}

View 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)}`)
})
}

View 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
View 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
})
})

View 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']

View 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
}
}