From 7ed373337f06ef1733a9329fe572f0369374c116 Mon Sep 17 00:00:00 2001 From: devilreef <86633411+devilr33f@users.noreply.github.com> Date: Thu, 8 Jan 2026 22:06:07 +0600 Subject: [PATCH] refactor: update message forwarding logic to support channel-based matching alongside role-based matching; enhance configuration structure to include channels; update documentation and example config --- CLAUDE.md | 16 +++++++++------ Dockerfile | 2 +- config.json.example | 18 +++++++++++++++++ src/config.ts | 25 ++++++++++++++++++----- src/discord/handlers.ts | 44 ++++++++++++++++++++++++++++++++++------- src/telegram/sender.ts | 3 ++- src/types.ts | 11 +++++++++-- 7 files changed, 97 insertions(+), 22 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index a2a64c0..37560ac 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -12,29 +12,33 @@ pnpm lint # ESLint check (npx eslint src/) ## Architecture -Discord-to-Telegram message forwarder. Tracks messages from users with specific roles on Discord servers and forwards them to Telegram topics. +Discord-to-Telegram message forwarder. Tracks messages from Discord servers and forwards them to Telegram topics. ``` src/ ├── index.ts # Entry point, initializes clients ├── env.ts # Environment variables (DISCORD_TOKEN, TELEGRAM_BOT_TOKEN) -├── config.ts # Loads config.json, validates server/role structure +├── config.ts # Loads config.json, validates server/role/channel structure ├── types.ts # Shared TypeScript interfaces ├── discord/ │ ├── client.ts # discord.js-selfbot-v13 client -│ └── handlers.ts # messageCreate handler, role priority filtering +│ └── handlers.ts # messageCreate handler, channel/role matching └── telegram/ ├── client.ts # wrappergram client └── sender.ts # Message forwarding, media group handling ``` -**Flow**: Discord messageCreate → filter by guild + role → find highest priority role → forward to Telegram topic with attachments +**Flow**: Discord messageCreate → match by channel or role → forward to Telegram topic with attachments -**Role Priority**: Users with multiple tracked roles → message goes to first matching role's topic (config array order = priority) +**Matching Priority**: +1. Channels checked first (if configured) +2. Roles checked second (highest priority role = first in config array) + +Servers can have `channels`, `roles`, or both. Channel matches skip role display in Telegram message. ## Config -- `config.json` - Server/role/topic mappings (see `config.json.example`) +- `config.json` - Server/channel/role/topic mappings (see `config.json.example`) - `.env` - Tokens (see `.env.example`) ## Code Style diff --git a/Dockerfile b/Dockerfile index 85a2cfc..28771aa 100644 --- a/Dockerfile +++ b/Dockerfile @@ -6,6 +6,6 @@ RUN corepack enable COPY . /app WORKDIR /app -RUN pnpm install --prod --frozen-lockfile +RUN pnpm install --frozen-lockfile CMD ["pnpm", "start"] \ No newline at end of file diff --git a/config.json.example b/config.json.example index 18f45cd..ae8775c 100644 --- a/config.json.example +++ b/config.json.example @@ -10,6 +10,24 @@ { "id": "111111111111111111", "name": "Developer", "topicId": 5 }, { "id": "222222222222222222", "name": "Moderator", "topicId": 6 } ] + }, + { + "name": "Another Server (channel-based)", + "guildId": "987654321098765432", + "channels": [ + { "id": "333333333333333333", "name": "announcements", "topicId": 7 }, + { "id": "444444444444444444", "name": "dev-updates", "topicId": 8 } + ] + }, + { + "name": "Mixed Server (both channels and roles)", + "guildId": "555555555555555555", + "channels": [ + { "id": "666666666666666666", "name": "important", "topicId": 9 } + ], + "roles": [ + { "id": "777777777777777777", "name": "Staff", "topicId": 10 } + ] } ] } diff --git a/src/config.ts b/src/config.ts index d373b58..b16e7a7 100644 --- a/src/config.ts +++ b/src/config.ts @@ -23,12 +23,27 @@ function loadConfig(): Config { if (!server.guildId) { throw new Error(`Server "${server.name}" missing guildId`) } - if (!Array.isArray(server.roles) || server.roles.length === 0) { - throw new Error(`Server "${server.name}" missing roles array`) + + const hasRoles = Array.isArray(server.roles) && server.roles.length > 0 + const hasChannels = Array.isArray(server.channels) && server.channels.length > 0 + + if (!hasRoles && !hasChannels) { + throw new Error(`Server "${server.name}" must have roles or channels configured`) } - for (const role of server.roles) { - if (!role.id || !role.name || typeof role.topicId !== 'number') { - throw new Error(`Invalid role config in server "${server.name}"`) + + if (server.roles) { + for (const role of server.roles) { + if (!role.id || !role.name || typeof role.topicId !== 'number') { + throw new Error(`Invalid role config in server "${server.name}"`) + } + } + } + + if (server.channels) { + for (const channel of server.channels) { + if (!channel.id || !channel.name || typeof channel.topicId !== 'number') { + throw new Error(`Invalid channel config in server "${server.name}"`) + } } } } diff --git a/src/discord/handlers.ts b/src/discord/handlers.ts index 2433ec6..0d28e3f 100644 --- a/src/discord/handlers.ts +++ b/src/discord/handlers.ts @@ -1,5 +1,5 @@ import type { Message } from 'discord.js-selfbot-v13' -import type { AttachmentInfo, RoleConfig } from '../types.js' +import type { AttachmentInfo, ChannelConfig, RoleConfig } from '../types.js' import { config } from '../config.js' import { forwardMessage } from '../telegram/sender.js' import { discord } from './client.js' @@ -8,6 +8,12 @@ export function setupHandlers(): void { discord.on('messageCreate', handleMessage) } +interface MatchResult { + topicId: number + label: string + type: 'channel' | 'role' +} + async function handleMessage(message: Message): Promise { if (!message.guild || !message.member) return @@ -16,8 +22,24 @@ async function handleMessage(message: Message): Promise { if (!serverConfig) return - const matchedRole = findHighestPriorityRole(message.member.roles.cache, serverConfig.roles) - if (!matchedRole) + // Check channels first (higher priority), then roles + let match: MatchResult | null = null + + if (serverConfig.channels) { + const channelMatch = findMatchingChannel(message.channel.id, serverConfig.channels) + if (channelMatch) { + match = { topicId: channelMatch.topicId, label: channelMatch.name, type: 'channel' } + } + } + + if (!match && serverConfig.roles) { + const roleMatch = findHighestPriorityRole(message.member.roles.cache, serverConfig.roles) + if (roleMatch) { + match = { topicId: roleMatch.topicId, label: roleMatch.name, type: 'role' } + } + } + + if (!match) return const attachments: AttachmentInfo[] = message.attachments.map(att => ({ @@ -27,24 +49,32 @@ async function handleMessage(message: Message): Promise { })) const messageLink = `https://discord.com/channels/${message.guild.id}/${message.channel.id}/${message.id}` + const channelName = 'name' in message.channel ? (message.channel.name ?? 'unknown') : 'DM' try { await forwardMessage({ - topicId: matchedRole.topicId, + topicId: match.topicId, author: message.author.displayName ?? message.author.username, - role: matchedRole.name, - channel: 'name' in message.channel ? (message.channel.name ?? 'unknown') : 'DM', + role: match.type === 'role' ? match.label : null, + channel: channelName, content: message.content, attachments, messageLink, }) - console.log(`Forwarded message from ${message.author.tag} (${matchedRole.name}) in ${serverConfig.name}`) + console.log(`Forwarded message from ${message.author.tag} (${match.type}: ${match.label}) in ${serverConfig.name}`) } catch (err) { console.error('Failed to forward message:', err) } } +function findMatchingChannel( + channelId: string, + configChannels: ChannelConfig[], +): ChannelConfig | null { + return configChannels.find(ch => ch.id === channelId) ?? null +} + function findHighestPriorityRole( memberRoles: Map, configRoles: RoleConfig[], diff --git a/src/telegram/sender.ts b/src/telegram/sender.ts index 1dabb0d..41fd11d 100644 --- a/src/telegram/sender.ts +++ b/src/telegram/sender.ts @@ -15,7 +15,8 @@ interface DownloadedAttachment { export async function forwardMessage(opts: ForwardMessageOptions): Promise { const { topicId, author, role, channel, content, attachments, messageLink } = opts - const text = `${escapeHtml(author)} (${escapeHtml(role)}) in #${escapeHtml(channel)}\n${escapeHtml(content)}\n\nJump to message` + const roleText = role ? ` (${escapeHtml(role)})` : '' + const text = `${escapeHtml(author)}${roleText} in #${escapeHtml(channel)}\n${escapeHtml(content)}\n\nJump to message` await telegram.api.sendMessage({ chat_id: config.telegram.chatId, diff --git a/src/types.ts b/src/types.ts index 2c5f849..8c649ff 100644 --- a/src/types.ts +++ b/src/types.ts @@ -4,10 +4,17 @@ export interface RoleConfig { topicId: number } +export interface ChannelConfig { + id: string + name: string + topicId: number +} + export interface ServerConfig { name: string guildId: string - roles: RoleConfig[] + roles?: RoleConfig[] + channels?: ChannelConfig[] } export interface Config { @@ -20,7 +27,7 @@ export interface Config { export interface ForwardMessageOptions { topicId: number author: string - role: string + role: string | null channel: string content: string attachments: AttachmentInfo[]