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
This commit is contained in:
parent
c4514cd4c4
commit
7ed373337f
7 changed files with 97 additions and 22 deletions
16
CLAUDE.md
16
CLAUDE.md
|
|
@ -12,29 +12,33 @@ pnpm lint # ESLint check (npx eslint src/)
|
||||||
|
|
||||||
## Architecture
|
## 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/
|
src/
|
||||||
├── index.ts # Entry point, initializes clients
|
├── index.ts # Entry point, initializes clients
|
||||||
├── env.ts # Environment variables (DISCORD_TOKEN, TELEGRAM_BOT_TOKEN)
|
├── 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
|
├── types.ts # Shared TypeScript interfaces
|
||||||
├── discord/
|
├── discord/
|
||||||
│ ├── client.ts # discord.js-selfbot-v13 client
|
│ ├── client.ts # discord.js-selfbot-v13 client
|
||||||
│ └── handlers.ts # messageCreate handler, role priority filtering
|
│ └── handlers.ts # messageCreate handler, channel/role matching
|
||||||
└── telegram/
|
└── telegram/
|
||||||
├── client.ts # wrappergram client
|
├── client.ts # wrappergram client
|
||||||
└── sender.ts # Message forwarding, media group handling
|
└── 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
|
||||||
|
|
||||||
- `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`)
|
- `.env` - Tokens (see `.env.example`)
|
||||||
|
|
||||||
## Code Style
|
## Code Style
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,6 @@ RUN corepack enable
|
||||||
COPY . /app
|
COPY . /app
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
RUN pnpm install --prod --frozen-lockfile
|
RUN pnpm install --frozen-lockfile
|
||||||
|
|
||||||
CMD ["pnpm", "start"]
|
CMD ["pnpm", "start"]
|
||||||
|
|
@ -10,6 +10,24 @@
|
||||||
{ "id": "111111111111111111", "name": "Developer", "topicId": 5 },
|
{ "id": "111111111111111111", "name": "Developer", "topicId": 5 },
|
||||||
{ "id": "222222222222222222", "name": "Moderator", "topicId": 6 }
|
{ "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 }
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -23,9 +23,15 @@ function loadConfig(): Config {
|
||||||
if (!server.guildId) {
|
if (!server.guildId) {
|
||||||
throw new Error(`Server "${server.name}" missing 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`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (server.roles) {
|
||||||
for (const role of server.roles) {
|
for (const role of server.roles) {
|
||||||
if (!role.id || !role.name || typeof role.topicId !== 'number') {
|
if (!role.id || !role.name || typeof role.topicId !== 'number') {
|
||||||
throw new Error(`Invalid role config in server "${server.name}"`)
|
throw new Error(`Invalid role config in server "${server.name}"`)
|
||||||
|
|
@ -33,6 +39,15 @@ function loadConfig(): Config {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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}"`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return config
|
return config
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import type { Message } from 'discord.js-selfbot-v13'
|
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 { config } from '../config.js'
|
||||||
import { forwardMessage } from '../telegram/sender.js'
|
import { forwardMessage } from '../telegram/sender.js'
|
||||||
import { discord } from './client.js'
|
import { discord } from './client.js'
|
||||||
|
|
@ -8,6 +8,12 @@ export function setupHandlers(): void {
|
||||||
discord.on('messageCreate', handleMessage)
|
discord.on('messageCreate', handleMessage)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface MatchResult {
|
||||||
|
topicId: number
|
||||||
|
label: string
|
||||||
|
type: 'channel' | 'role'
|
||||||
|
}
|
||||||
|
|
||||||
async function handleMessage(message: Message): Promise<void> {
|
async function handleMessage(message: Message): Promise<void> {
|
||||||
if (!message.guild || !message.member)
|
if (!message.guild || !message.member)
|
||||||
return
|
return
|
||||||
|
|
@ -16,8 +22,24 @@ async function handleMessage(message: Message): Promise<void> {
|
||||||
if (!serverConfig)
|
if (!serverConfig)
|
||||||
return
|
return
|
||||||
|
|
||||||
const matchedRole = findHighestPriorityRole(message.member.roles.cache, serverConfig.roles)
|
// Check channels first (higher priority), then roles
|
||||||
if (!matchedRole)
|
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
|
return
|
||||||
|
|
||||||
const attachments: AttachmentInfo[] = message.attachments.map(att => ({
|
const attachments: AttachmentInfo[] = message.attachments.map(att => ({
|
||||||
|
|
@ -27,24 +49,32 @@ async function handleMessage(message: Message): Promise<void> {
|
||||||
}))
|
}))
|
||||||
|
|
||||||
const messageLink = `https://discord.com/channels/${message.guild.id}/${message.channel.id}/${message.id}`
|
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 {
|
try {
|
||||||
await forwardMessage({
|
await forwardMessage({
|
||||||
topicId: matchedRole.topicId,
|
topicId: match.topicId,
|
||||||
author: message.author.displayName ?? message.author.username,
|
author: message.author.displayName ?? message.author.username,
|
||||||
role: matchedRole.name,
|
role: match.type === 'role' ? match.label : null,
|
||||||
channel: 'name' in message.channel ? (message.channel.name ?? 'unknown') : 'DM',
|
channel: channelName,
|
||||||
content: message.content,
|
content: message.content,
|
||||||
attachments,
|
attachments,
|
||||||
messageLink,
|
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) {
|
catch (err) {
|
||||||
console.error('Failed to forward message:', 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(
|
function findHighestPriorityRole(
|
||||||
memberRoles: Map<string, unknown>,
|
memberRoles: Map<string, unknown>,
|
||||||
configRoles: RoleConfig[],
|
configRoles: RoleConfig[],
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,8 @@ interface DownloadedAttachment {
|
||||||
export async function forwardMessage(opts: ForwardMessageOptions): Promise<void> {
|
export async function forwardMessage(opts: ForwardMessageOptions): Promise<void> {
|
||||||
const { topicId, author, role, channel, content, attachments, messageLink } = opts
|
const { topicId, author, role, channel, content, attachments, messageLink } = opts
|
||||||
|
|
||||||
const text = `<b>${escapeHtml(author)}</b> (${escapeHtml(role)}) in <code>#${escapeHtml(channel)}</code>\n${escapeHtml(content)}\n\n<a href="${messageLink}">Jump to message</a>`
|
const roleText = role ? ` (${escapeHtml(role)})` : ''
|
||||||
|
const text = `<b>${escapeHtml(author)}</b>${roleText} in <code>#${escapeHtml(channel)}</code>\n${escapeHtml(content)}\n\n<a href="${messageLink}">Jump to message</a>`
|
||||||
|
|
||||||
await telegram.api.sendMessage({
|
await telegram.api.sendMessage({
|
||||||
chat_id: config.telegram.chatId,
|
chat_id: config.telegram.chatId,
|
||||||
|
|
|
||||||
11
src/types.ts
11
src/types.ts
|
|
@ -4,10 +4,17 @@ export interface RoleConfig {
|
||||||
topicId: number
|
topicId: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ChannelConfig {
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
topicId: number
|
||||||
|
}
|
||||||
|
|
||||||
export interface ServerConfig {
|
export interface ServerConfig {
|
||||||
name: string
|
name: string
|
||||||
guildId: string
|
guildId: string
|
||||||
roles: RoleConfig[]
|
roles?: RoleConfig[]
|
||||||
|
channels?: ChannelConfig[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Config {
|
export interface Config {
|
||||||
|
|
@ -20,7 +27,7 @@ export interface Config {
|
||||||
export interface ForwardMessageOptions {
|
export interface ForwardMessageOptions {
|
||||||
topicId: number
|
topicId: number
|
||||||
author: string
|
author: string
|
||||||
role: string
|
role: string | null
|
||||||
channel: string
|
channel: string
|
||||||
content: string
|
content: string
|
||||||
attachments: AttachmentInfo[]
|
attachments: AttachmentInfo[]
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue