diff --git a/.gitignore b/.gitignore index 62c8fca1..6c5db0dc 100644 --- a/.gitignore +++ b/.gitignore @@ -47,4 +47,5 @@ data/ # Built files dist/ -.vscode/settings.json +.vscode/settings.json +/todo.txt diff --git a/package.json b/package.json index d024585e..ed96f2c1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "tedicross", - "version": "0.11.4", + "version": "0.11.6", "description": "Bridging Telegram and Discord", "license": "MIT", "repository": { @@ -18,30 +18,30 @@ "dependencies": { "@types/js-yaml": "^4.0.5", "@types/mime": "^3.0.1", - "@types/ramda": "^0.29.2", + "@types/ramda": "^0.29.3", "@types/semver": "^7.5.0", "@types/yargs": "^17.0.24", - "@typescript-eslint/eslint-plugin": "^5.59.7", - "@typescript-eslint/parser": "^5.59.7", + "@typescript-eslint/eslint-plugin": "^6.1.0", + "@typescript-eslint/parser": "^6.1.0", "discord.js": "^14.11.0", "js-yaml": "^4.1.0", "mime": "^3.0.0", "moment": "^2.29.4", "ramda": "^0.29.0", - "semver": "^7.5.1", + "semver": "^7.5.4", "simple-markdown": "^0.7.3", - "sqlite": "^4.2.1", + "sqlite": "^5.0.1", "sqlite3": "^5.1.6", "telegraf": "^4.12.2", - "typescript": "^5.0.4", + "typescript": "^5.1.6", "yargs": "^17.7.2" }, "devDependencies": { - "eslint": "^8.41.0", + "eslint": "^8.45.0", "eslint-config-prettier": "^8.8.0", - "eslint-plugin-prettier": "^4.2.1", - "nodemon": "^2.0.22", - "prettier": "^2.8.8", + "eslint-plugin-prettier": "^5.0.0", + "nodemon": "^3.0.1", + "prettier": "^3.0.0", "rimraf": "^5.0.1", "ts-node": "^10.9.1" } diff --git a/src/discord2telegram/setup.ts b/src/discord2telegram/setup.ts index 4a8ed3b0..db2d502f 100644 --- a/src/discord2telegram/setup.ts +++ b/src/discord2telegram/setup.ts @@ -120,7 +120,7 @@ export function setup( .reply("\nchannelId: '" + message.channel.id + "'") .then(sleepOneMinute) .then((info: any) => Promise.all([info.delete(), message.delete()])) - .catch(ignoreAlreadyDeletedError); + .catch(ignoreAlreadyDeletedError as any); // Don't process the message any further return; @@ -300,7 +300,7 @@ export function setup( // Delete it again after some time .then(sleepOneMinute) .then((message: any) => message.delete()) - .catch(ignoreAlreadyDeletedError) + .catch(ignoreAlreadyDeletedError as any) .then(() => antiInfoSpamSet.delete(message.channel.id)); } } diff --git a/src/fetchDiscordChannel.ts b/src/fetchDiscordChannel.ts index 7e7ec7a2..90cbcf17 100644 --- a/src/fetchDiscordChannel.ts +++ b/src/fetchDiscordChannel.ts @@ -1,12 +1,12 @@ import { Client, TextChannel } from "discord.js"; -import R from "ramda"; /** * Gets a Discord channel, and logs an error if it doesn't exist * * @returns A Promise resolving to the channel, or rejecting if it could not be fetched for some reason */ -export const fetchDiscordChannel = R.curry((dcBot: Client, bridge) => { +// export const fetchDiscordChannel = R.curry((dcBot: Client, bridge) => { +export const fetchDiscordChannel = (dcBot: Client, bridge: any) => { // Get the channel's ID const channelId = bridge.discord.channelId; @@ -15,5 +15,4 @@ export const fetchDiscordChannel = R.curry((dcBot: Client, bridge) => { console.error(`Could not find Discord channel ${channelId} in bridge ${bridge.name}: ${err.message}`); throw err; }) as unknown as Promise; -}); - +}; diff --git a/src/settings/DiscordSettings.ts b/src/settings/DiscordSettings.ts index 712e3529..cc6df28c 100644 --- a/src/settings/DiscordSettings.ts +++ b/src/settings/DiscordSettings.ts @@ -4,6 +4,7 @@ interface Settings { useNickname: boolean; maxReplyLines: number; skipOldMessages: boolean; + suppressFileTooBigMessages: boolean; } /***************************** @@ -21,6 +22,7 @@ export class DiscordSettings { replyLength: number; maxReplyLines: number; skipOldMessages: boolean; + suppressFileTooBigMessages: boolean; /** * Creates a new DiscordSettings object @@ -49,6 +51,9 @@ export class DiscordSettings { /** How many lines of the original message to show in replies from Telegram */ this.maxReplyLines = settings.maxReplyLines; + + /** Don't send a warning message to Discord if a file is too big to be sent from Telegram to Discord */ + this.suppressFileTooBigMessages = settings.suppressFileTooBigMessages; } /** The bot token to use */ @@ -79,15 +84,16 @@ export class DiscordSettings { * @throws If the object is not suitable. The error message says what the problem is */ static validate(settings: Settings) { - // Check that the settings are indeed in object form - if (!(settings instanceof Object)) { - throw new Error("`settings` must be an object"); - } - - // Check that the token is a string - if (typeof settings.token !== "string") { - throw new Error("`settings.token` must be a string"); - } + // NOTE: redundant checks + // // Check that the settings are indeed in object form + // if (!(settings instanceof Object)) { + // throw new Error("`settings` must be an object"); + // } + // + // // Check that the token is a string + // if (typeof settings.token !== "string") { + // throw new Error("`settings.token` must be a string"); + // } // Check that skipOldMessages is a boolean if (Boolean(settings.skipOldMessages) !== settings.skipOldMessages) { @@ -108,6 +114,11 @@ export class DiscordSettings { if (!Number.isInteger(settings.maxReplyLines) || settings.maxReplyLines <= 0) { throw new Error("`settings.maxReplyLines` must be an integer greater than 0"); } + + // Check that `suppressFileTooBigMessages` is a boolean + if (Boolean(settings.suppressFileTooBigMessages) !== settings.suppressFileTooBigMessages) { + throw new Error("`settings.suppressFileTooBigMessages` must be a boolean"); + } } /** Constant telling the Discord token should be gotten from the environment */ @@ -120,7 +131,8 @@ export class DiscordSettings { return { token: DiscordSettings.GET_TOKEN_FROM_ENVIRONMENT, skipOldMessages: true, - useNickname: false + useNickname: false, + suppressFileTooBigMessages: false }; } } diff --git a/src/telegram2discord/From.ts b/src/telegram2discord/From.ts index f34f6f75..5ac309be 100644 --- a/src/telegram2discord/From.ts +++ b/src/telegram2discord/From.ts @@ -79,9 +79,16 @@ export function createFromObjFromChat(chat: Record) { * @returns The display name */ export function makeDisplayName(useFirstNameInsteadOfUsername: boolean, from: From) { - return R.ifElse( - from => useFirstNameInsteadOfUsername || R.isNil(from.username), - R.prop("firstName"), - R.prop("username") - )(from); + if (useFirstNameInsteadOfUsername || !from.username) { + const prefix: string = from.lastName ? ` ${from.lastName}` : ""; + return `${from.firstName}${prefix}`; + } + + return from.username; + // return R.ifElse( + // from => R.isNil(from.username) || useFirstNameInsteadOfUsername, + // // (useFirstNameInsteadOfUsername || R.isNil(from.username)), + // from => R.prop("firstName", from), + // from => R.prop("username", from) + // )(from); } diff --git a/src/telegram2discord/endwares.ts b/src/telegram2discord/endwares.ts index 8faadf7f..9a09aebf 100644 --- a/src/telegram2discord/endwares.ts +++ b/src/telegram2discord/endwares.ts @@ -72,7 +72,7 @@ export const chatinfo = (ctx: TediCrossContext, next: () => void) => { ctx.deleteMessage() ]) ) - .catch(ignoreAlreadyDeletedError); + .catch(ignoreAlreadyDeletedError as any); } else { next(); } @@ -98,7 +98,7 @@ export const newChatMembers = createMessageHandler((ctx: TediCrossContext, bridg // Pass it on ctx.TediCross.dcBot.ready - .then(() => fetchDiscordChannel(ctx.TediCross.dcBot, bridge).then(channel => channel.send(text))) + .then(() => fetchDiscordChannel(ctx.TediCross.dcBot, bridge).then((channel: any) => channel.send(text))) .catch((err: any) => console.error(`Could not tell Discord about a new chat member on bridge ${bridge.name}: ${err.message}`) ); @@ -139,7 +139,7 @@ export const leftChatMember = createMessageHandler((ctx: TediCrossContext, bridg * @param ctx.tediCross The TediCross context of the message * @param ctx.TediCross The global TediCross context of the message */ -export const relayMessage = (ctx: TediCrossContext) => +export const relayMessage = (ctx: TediCrossContext) => { R.forEach(async (prepared: any) => { try { // Wait for the Discord bot to become ready @@ -172,6 +172,7 @@ export const relayMessage = (ctx: TediCrossContext) => } chunks = R.tail(chunks); } catch (err: any) { + console.dir(`relayMessage err: ${err.message}`); if (err.message === "Request entity too large") { dcMessage = await channel.send( `***${prepared.senderName}** on Telegram sent a file, but it was too large for Discord. If you want it, ask them to send it some other way*` @@ -206,6 +207,7 @@ export const relayMessage = (ctx: TediCrossContext) => console.error(`Could not relay a message to Discord on bridge ${prepared.bridge.name}: ${err.message}`); } })(ctx.tediCross.prepared); +}; /** * Handles message edits @@ -216,12 +218,11 @@ export const handleEdits = createMessageHandler(async (ctx: TediCrossContext, br // Function to "delete" a message on Discord const del = async (ctx: TediCrossContext, bridge: any) => { try { - // Wait for the Discord bot to become ready await ctx.TediCross.dcBot.ready; // Find the ID of this message on Discord - let [dcMessageId] = await ctx.TediCross.messageMap.getCorresponding( + const [dcMessageId] = await ctx.TediCross.messageMap.getCorresponding( MessageMap.TELEGRAM_TO_DISCORD, bridge, ctx.tediCross.message.message_id @@ -254,7 +255,7 @@ export const handleEdits = createMessageHandler(async (ctx: TediCrossContext, br await ctx.TediCross.dcBot.ready; // Find the ID of this message on Discord - let [dcMessageId] = await ctx.TediCross.messageMap.getCorresponding( + const [dcMessageId] = await ctx.TediCross.messageMap.getCorresponding( MessageMap.TELEGRAM_TO_DISCORD, bridge, tgMessage.message_id diff --git a/src/telegram2discord/middlewares.ts b/src/telegram2discord/middlewares.ts index 9d1f6931..a93b337c 100644 --- a/src/telegram2discord/middlewares.ts +++ b/src/telegram2discord/middlewares.ts @@ -19,6 +19,7 @@ import { MessageMap } from "../MessageMap"; * Creates a text object from a Telegram message * * @param message The message object + * @param ctx The TediCrossContext object * * @returns The text object, or undefined if no text was found */ @@ -52,7 +53,7 @@ function createTextObjFromMessage(ctx: TediCrossContext, message: Message) { entities: [] }) ], - // Locations must be turned into an URL + // Locations must be turned into a URL [ R.has("location"), ({ location }: any) => ({ @@ -157,10 +158,10 @@ function addMessageObj(ctx: TediCrossContext, next: () => void) { // Put it on the context ctx.tediCross.message = R.cond([ // XXX I tried both R.has and R.hasIn as conditions. Neither worked for some reason - [ctx => !R.isNil(ctx.update.channel_post), R.path(["update", "channel_post"])], - [ctx => !R.isNil(ctx.update.edited_channel_post), R.path(["update", "edited_channel_post"])], - [ctx => !R.isNil(ctx.update.message), R.path(["update", "message"])], - [ctx => !R.isNil(ctx.update.edited_message), R.path(["update", "edited_message"])] + [ctx => !R.isNil((ctx as any).update.channel_post), R.path(["update", "channel_post"])], + [ctx => !R.isNil((ctx as any).update.edited_channel_post), R.path(["update", "edited_channel_post"])], + [ctx => !R.isNil((ctx as any).update.message), R.path(["update", "message"])], + [ctx => !R.isNil((ctx as any).update.edited_message), R.path(["update", "edited_message"])] ])(ctx) as any; next(); @@ -266,18 +267,18 @@ function informThisIsPrivateBot(ctx: TediCrossContext, next: () => void) { R.compose(R.isEmpty, R.path(["tediCross", "bridges"])), // Inform the user, if enough time has passed since last time R.when( - // When there is no timer for the chat in the anti spam map + // When there is no timer for the chat in the antispam map ctx => R.not(ctx.TediCross.antiInfoSpamSet.has(ctx.tediCross.message.chat.id)), // Inform the chat this is an instance of TediCross ctx => { - // Update the anti spam set + // Update the antispam set ctx.TediCross.antiInfoSpamSet.add(ctx.tediCross.message.chat.id); // Send the reply ctx.reply( "This is an instance of a [TediCross](https://github.com/TediCross/TediCross) bot, " + - "bridging a chat in Telegram with one in Discord. " + - "If you wish to use TediCross yourself, please download and create an instance.", + "bridging a chat in Telegram with one in Discord. " + + "If you wish to use TediCross yourself, please download and create an instance.", { parse_mode: "Markdown" } @@ -286,8 +287,8 @@ function informThisIsPrivateBot(ctx: TediCrossContext, next: () => void) { //@ts-ignore sleepOneMinute() .then(() => deleteMessage(ctx, msg)) - .catch(ignoreAlreadyDeletedError) - // Remove it from the anti spam set again + .catch(ignoreAlreadyDeletedError as any) + // Remove it from the antispam set again .then(() => ctx.TediCross.antiInfoSpamSet.delete(ctx.message!.chat.id)) ); } @@ -436,14 +437,14 @@ function addFileObj(ctx: TediCrossContext, next: () => void) { ctx.tediCross.file = { type: "photo", id: photo.file_id, - name: "photo.jpg" // Telegram will convert it to a jpg no matter which format is orignally sent + name: "photo.jpg" // Telegram will convert it to a jpg no matter which format is originally sent }; } else if (!R.isNil(message.sticker)) { // Sticker ctx.tediCross.file = { type: "sticker", id: R.ifElse( - R.propEq("is_animated", true), + R.propEq(true, "is_animated"), R.path(["thumb", "file_id"]), R.prop("file_id") )(message.sticker), @@ -454,7 +455,7 @@ function addFileObj(ctx: TediCrossContext, next: () => void) { ctx.tediCross.file = { type: "video", id: message.video.file_id, - name: "video" + "." + mime.getExtension(message.video.mime_type) + name: message.video.file_name || `video.${mime.getExtension(message.video.mime_type)}` }; } else if (!R.isNil(message.voice)) { // Voice @@ -490,9 +491,15 @@ function addFileLink(ctx: TediCrossContext, next: () => void) { .then(next) .then(R.always(undefined)) .catch(err => { - if (err.response && err.response.description === "Bad Request: file is too big") { - ctx.reply("File is too big for TediCross to handle", { parse_mode: "HTML" }); + if (ctx.TediCross.settings.discord.suppressFileTooBigMessages) { + console.log(err.response ? err.response.description : "Bad Request"); + } else if (err.response && err.response.description === "Bad Request: file is too big") { + ctx.reply(`File '${ctx.tediCross.file.name}' is too big for TediCross to handle`, { + parse_mode: "HTML" + }).then(); } + + next(); }); } @@ -560,21 +567,21 @@ async function addPreparedObj(ctx: TediCrossContext, next: () => void) { const repliedToName = R.isNil(tc.replyTo) ? null : await R.ifElse( - R.prop("isReplyToTediCross") as any, - R.compose( - (username: string) => makeDiscordMention(username, ctx.TediCross.dcBot, bridge), - R.prop("dcUsername") as any - ), - R.compose( - R.partial(makeDisplayName, [ - ctx.TediCross.settings.telegram.useFirstNameInsteadOfUsername - ]), - //@ts-ignore - R.prop("originalFrom") - ) - )(tc.replyTo); + R.prop("isReplyToTediCross") as any, + R.compose( + (username: string) => makeDiscordMention(username, ctx.TediCross.dcBot, bridge), + R.prop("dcUsername") as any + ), + R.compose( + R.partial(makeDisplayName, [ + ctx.TediCross.settings.telegram.useFirstNameInsteadOfUsername + ]), + //@ts-ignore + R.prop("originalFrom") + ) + )(tc.replyTo); // Build the header - let header = ""; + let header: string; if (bridge.telegram.sendUsernames) { if (!R.isNil(tc.forwardFrom)) { // Forward @@ -624,7 +631,8 @@ async function addPreparedObj(ctx: TediCrossContext, next: () => void) { const file = R.ifElse( R.compose(R.isNil, R.prop("file")), R.always(undefined), - (tc: TediCrossContext["TediCross"]["tc"]) => new Discord.AttachmentBuilder(tc.file.link, tc.file.name) + (tc: TediCrossContext["TediCross"]["tc"]) => + new Discord.AttachmentBuilder(tc.file.link, { name: tc.file.name }) )(tc); // Make the text to send @@ -647,7 +655,7 @@ async function addPreparedObj(ctx: TediCrossContext, next: () => void) { messageToReply, replyId }; - })(tc.bridges) + }, tc.bridges) ); next();