From b63964a1def7cdd0840d1685578725beeacc3483 Mon Sep 17 00:00:00 2001 From: Simen de Lange Date: Mon, 7 Jan 2019 21:22:32 +0100 Subject: [PATCH 001/102] Mostly works. Have not tested channels yet --- lib/discord2telegram/setup.js | 75 ++++++++------- lib/telegram2discord/handleUpdates.js | 78 ---------------- lib/telegram2discord/makeUpdateEmitter.js | 106 ---------------------- lib/telegram2discord/messageConverter.js | 2 +- lib/telegram2discord/setup.js | 94 +++++++++++-------- main.js | 4 +- package.json | 3 +- 7 files changed, 103 insertions(+), 259 deletions(-) delete mode 100644 lib/telegram2discord/handleUpdates.js delete mode 100644 lib/telegram2discord/makeUpdateEmitter.js diff --git a/lib/discord2telegram/setup.js b/lib/discord2telegram/setup.js index cd53b7b8..e4a643ed 100644 --- a/lib/discord2telegram/setup.js +++ b/lib/discord2telegram/setup.js @@ -22,7 +22,7 @@ const Bridge = require("../bridgestuff/Bridge"); * @param {Logger} logger The Logger instance to log messages to * @param {String} verb Either "joined" or "left" * @param {BridgeMap} bridgeMap Map of existing bridges - * @param {BotAPI} tgBot The Telegram bot to send the messages to + * @param {Telegraf} tgBot The Telegram bot to send the messages to * * @returns {Function} Function which can be given to the 'guildMemberAdd' or 'guildMemberRemove' events of a Discord bot * @@ -54,11 +54,13 @@ function makeJoinLeaveFunc(logger, verb, bridgeMap, tgBot) { try { // Send it - await tgBot.sendMessage({ + await tgBot.telegram.sendMessage( + bridge.telegram.chatId, text, - chat_id: bridge.telegram.chatId, - parse_mode: "HTML" - }); + { + parse_mode: "HTML" + } + ); } catch (err) { logger.error(`[${bridge.name}] Could not notify Telegram about a user that ${verb} Discord`, err); } @@ -75,7 +77,7 @@ function makeJoinLeaveFunc(logger, verb, bridgeMap, tgBot) { * * @param {Logger} logger The Logger instance to log messages to * @param {Discord.Client} dcBot The Discord bot - * @param {BotAPI} tgBot The Telegram bot + * @param {Telegraf} tgBot The Telegram bot * @param {MessageMap} messageMap Map between IDs of messages * @param {BridgeMap} bridgeMap Map of the bridges to use * @param {Settings} settings Settings to use @@ -133,11 +135,13 @@ function setup(logger, dcBot, tgBot, messageMap, bridgeMap, settings) { ? `${senderName}\n${url}` : `${url}` ; - const tgMessage = await tgBot.sendMessage({ - chat_id: bridge.telegram.chatId, - text: textToSend, - parse_mode: "HTML" - }); + const tgMessage = await tgBot.telegram.sendMessage( + bridge.telegram.chatId, + textToSend, + { + parse_mode: "HTML" + } + ); messageMap.insert(MessageMap.DISCORD_TO_TELEGRAM, bridge, message.id, tgMessage.message_id); } catch (err) { logger.error(`[${bridge.name}] Telegram did not accept an attachment:`, err); @@ -156,12 +160,14 @@ function setup(logger, dcBot, tgBot, messageMap, bridgeMap, settings) { try { // Send it - tgBot.sendMessage({ + tgBot.telegram.sendMessage( + bridge.telegram.chatId, text, - chat_id: bridge.telegram.chatId, - parse_mode: "HTML", - disable_web_page_preview: true - }); + { + parse_mode: "HTML", + disable_web_page_preview: true + } + ); } catch (err) { logger.error(`[${bridge.name}] Telegram did not accept an embed:`, err); } @@ -179,11 +185,13 @@ function setup(logger, dcBot, tgBot, messageMap, bridgeMap, settings) { ? `${senderName}\n${processedMessage}` : processedMessage ; - const tgMessage = await tgBot.sendMessage({ - chat_id: bridge.telegram.chatId, - text: textToSend, - parse_mode: "HTML" - }); + const tgMessage = await tgBot.telegram.sendMessage( + bridge.telegram.chatId, + textToSend, + { + parse_mode: "HTML" + } + ); // Make the mapping so future edits can work messageMap.insert(MessageMap.DISCORD_TO_TELEGRAM, bridge, message.id, tgMessage.message_id); @@ -234,12 +242,15 @@ function setup(logger, dcBot, tgBot, messageMap, bridgeMap, settings) { ? `${senderName}\n${processedMessage}` : processedMessage ; - await tgBot.editMessageText({ - chat_id: bridge.telegram.chatId, - message_id: tgMessageId, - text: textToSend, - parse_mode: "HTML" - }); + await tgBot.telegram.editMessageText( + bridge.telegram.chatId, + tgMessageId, + null, + textToSend, + { + parse_mode: "HTML" + } + ); } catch (err) { logger.error(`[${bridge.name}] Could not edit Telegram message:`, err); } @@ -267,7 +278,7 @@ function setup(logger, dcBot, tgBot, messageMap, bridgeMap, settings) { // Try to delete them await Promise.all( - tgMessageIds.map((tgMessageId) => tgBot.deleteMessage({chat_id: bridge.telegram.chatId, message_id: tgMessageId})) + tgMessageIds.map((tgMessageId) => tgBot.telegram.deleteMessage(bridge.telegram.chatId, tgMessageId)) ); } catch (err) { logger.error(`[${bridge.name}] Could not delete Telegram message:`, err); @@ -288,10 +299,10 @@ function setup(logger, dcBot, tgBot, messageMap, bridgeMap, settings) { bridgeMap.bridges.forEach(async (bridge) => { try { - await tgBot.sendMessage({ - chat_id: bridge.telegram.chatId, - text: "**TEDICROSS**\nThe discord side of the bot disconnected! Please check the log" - }); + await tgBot.telegram.sendMessage( + bridge.telegram.chatId, + "**TEDICROSS**\nThe discord side of the bot disconnected! Please check the log" + ); } catch (err) { logger.error(`[${bridge.name}] Could not send message to Telegram:`, err); } diff --git a/lib/telegram2discord/handleUpdates.js b/lib/telegram2discord/handleUpdates.js deleted file mode 100644 index 8cb11379..00000000 --- a/lib/telegram2discord/handleUpdates.js +++ /dev/null @@ -1,78 +0,0 @@ -"use strict"; - -/************************** - * Import important stuff * - **************************/ - -// Nothing - -/***************************** - * The handleUpdate function * - *****************************/ - -/** - * Handles update objects - * - * @param {Update[]} updates The updates to handle - * @param {EventEmitter} emitter The event emitter to use for emitting the updates - * - * @returns {Integer} ID of the last processed update - */ -function handleUpdates(updates, emitter) { - return updates.reduce((newOffset, update) => handleUpdate(update, emitter), 0); -} - -/** - * Finds out what type of update an update object is, and emits it as an event - * - * @param {Update} update The update object to handle - * @param {EventEmitter} emitter The event emitter to use - * - * @returns {Integer} ID of the update - */ -function handleUpdate(update, emitter) { - // Check what type of update this is - if (update.message !== undefined || update.channel_post !== undefined) { - // Extract the message. Treat ordinary messages and channel posts the same - const message = update.message || update.channel_post; - - // Determine type - if (message.text !== undefined) { - emitter.emit("text", message); - } else if (message.photo !== undefined) { - emitter.emit("photo", message); - } else if (message.document !== undefined) { - emitter.emit("document", message); - } else if (message.voice !== undefined) { - emitter.emit("voice", message); - } else if (message.audio !== undefined) { - emitter.emit("audio", message); - } else if (message.video !== undefined) { - emitter.emit("video", message); - } else if (message.sticker !== undefined) { - emitter.emit("sticker", message); - } else if (message.new_chat_members !== undefined) { - emitter.emit("newParticipants", message); - } else if (message.left_chat_member !== undefined) { - emitter.emit("participantLeft", message); - } - } else if (update.edited_message !== undefined) { - // Extract the message - const message = update.edited_message; - - // This is an update to a message - emitter.emit("messageEdit", message); - } - - // Return the new offset - return update.update_id; -} - -/************* - * Export it * - *************/ - -module.exports = { - handleUpdates, - handleUpdate -}; diff --git a/lib/telegram2discord/makeUpdateEmitter.js b/lib/telegram2discord/makeUpdateEmitter.js deleted file mode 100644 index 72d83717..00000000 --- a/lib/telegram2discord/makeUpdateEmitter.js +++ /dev/null @@ -1,106 +0,0 @@ -"use strict"; - -/************************** - * Import important stuff * - **************************/ - -const moment = require("moment"); -const { EventEmitter } = require("events"); -const { handleUpdates } = require("./handleUpdates"); - -const DEFAULT_TIMEOUT = moment.duration(1, "minute").asSeconds(); - -/***************************** - * The UpdateGetter function * - *****************************/ - -/** - * Runs getUpdates until there are no more updates to get. This is meant to run - * at the startup of the bot to remove initial cached messages if the bot has - * been down for a while - * - * @param {teleapiwrapper.BotAPI} bot The bot to get updates for - * - * @returns {Promise} A promise which resolves when all updates have been cleared - * - * @private - */ -async function clearInitialUpdates(bot) { - // Get updates for the bot. -1 means only the latest update will be fetched - const updates = await bot.getUpdates({offset: -1, timeout: 0}); - - // If an update was fetched, confirm it by sending a new request with its ID +1 - if (updates.length > 0) { - const offset = updates[updates.length-1].update_id + 1; - await bot.getUpdates({offset, timeout: 0}); - } - - // All updates are now cleared -} - -/** - * Fetches Telegram updates - * - * @param {Logger} logger The Logger instance to log messages to - * @param {BotAPI} bot The telegram bot to use - * @param {Integer} offset The update offset to use - * @param {EventEmitter} emitter The emitter to emit the updates from - * - * @private - */ -async function fetchUpdates(logger, bot, offset, emitter) { - // Log the event if debugging is on - logger.debug("Fetching Telegram updates"); - - try { - // Do the fetching - const updates = await bot.getUpdates({offset, timeout: DEFAULT_TIMEOUT}); - offset = handleUpdates(updates, emitter) + 1; - } catch (err) { - logger.error("Couldn't fetch Telegram messages. Reason:", `${err.name}: ${err.message}`); - logger.debug(err.stack); - } finally { - // Do it again, no matter what happened. XXX There is currently no way to stop it - fetchUpdates(logger, bot, offset, emitter); - } -} - -/** - * Creates an event emitter emitting update events for a Telegram bot - * - * @param {Logger} logger The Logger instance to log messages to - * @param {teleapiwrapper.BotAPI} bot The bot to get updates for - * @param {Settings} settings The settings to use - * - * @returns {EventEmitter} The event emitter - */ -function makeUpdateEmitter(logger, bot, settings) { - // Create an event emitter - const emitter = new EventEmitter(); - - // Clear old messages, if wanted - let p = Promise.resolve(); - if (settings.telegram.skipOldMessages) { - // Log the start of the clearing if debugging is on - logger.debug("Clearing old Telegram messages"); - - // Start clearing messages - p = clearInitialUpdates(bot) - .then(() => { - // Log that the clearing has ended if debugging is on - logger.debug("Old Telegram messages cleared"); - }); - } - - // Start the fetching - p.then(() => fetchUpdates(logger, bot, 0, emitter)); - - // Return the event emitter - return emitter; -} - -/*********************** - * Export the function * - ***********************/ - -module.exports = makeUpdateEmitter; diff --git a/lib/telegram2discord/messageConverter.js b/lib/telegram2discord/messageConverter.js index df5374e0..9e339c9d 100644 --- a/lib/telegram2discord/messageConverter.js +++ b/lib/telegram2discord/messageConverter.js @@ -66,7 +66,7 @@ function getDisplayNameFromUser(user, useFirstNameInsteadOfUsername) { * Converts Telegram messages to appropriate from and text * * @param {Message} message The Telegram message to convert - * @param {BotAPI} tgBot The Telegram bot + * @param {Telegraf} tgBot The Telegram bot * @param {Settings} settings The settings to use * @param {Discord.Client} dcBot The Discord bot * @param {Bridge} bridge The bridge the message is crossing diff --git a/lib/telegram2discord/setup.js b/lib/telegram2discord/setup.js index c7b4e2a6..2b2fc05c 100644 --- a/lib/telegram2discord/setup.js +++ b/lib/telegram2discord/setup.js @@ -4,18 +4,18 @@ * Import important stuff * **************************/ -const makeUpdateEmitter = require("./makeUpdateEmitter"); const messageConverter = require("./messageConverter"); const MessageMap = require("../MessageMap"); const R = require("ramda"); const mime = require("mime/lite"); const Bridge = require("../bridgestuff/Bridge"); const Discord = require("discord.js"); +const request = require("request"); /** * Creates a function which sends files from Telegram to discord * - * @param {BotAPI} tgBot The Telegram bot + * @param {Telegraf} tgBot The Telegram bot * @param {Discord.Client} dcBot The Discord bot * @param {MessageMap} messageMap Map between IDs of messages * @param {Settings} settings The settings to use @@ -47,8 +47,11 @@ function makeFileSender(tgBot, dcBot, messageMap, settings) { await dcBot.ready; // Start getting the file - const file = await tgBot.getFile({file_id: fileId}); - const fileStream = await tgBot.helperGetFileStream(file); + const [file, fileLink] = await Promise.all([ + tgBot.telegram.getFile(fileId), + tgBot.telegram.getFileLink(fileId) + ]); + const fileStream = request(fileLink); // Get the extension, if necessary const extension = resolveExtension @@ -96,45 +99,47 @@ function makeNameObject(user) { * Curryed function creating handlers handling messages which should not be relayed, and passing through those which should * * @param {Logger} logger The Logger instance to log messages to - * @param {BotAPI} tgBot The Telegram bot + * @param {Telegraf} tgBot The Telegram bot * @param {BridgeMap} bridgeMap Map of the bridges to use * @param {Function} func The message handler to wrap - * @param {Message} message The Telegram message triggering the wrapped function + * @param {Context} ctx The Telegram message triggering the wrapped function, wrapped in a context * * @private */ -const createMessageHandler = R.curry((logger, tgBot, bridgeMap, func, message) => { - if (message.text === "/chatinfo") { +const createMessageHandler = R.curry((logger, tgBot, bridgeMap, func, ctx) => { + if (ctx.message.text === "/chatinfo") { + // TODO Make middleware // This is a request for chat info. Give it, no matter which chat this is from - tgBot.sendMessage({ - chat_id: message.chat.id, - text: "chatID: " + message.chat.id - }); + tgBot.telegram.sendMessage( + ctx.message.chat.id, + "chatID: " + ctx.message.chat.id + ); } else { // Get the bridge - const bridges = bridgeMap.fromTelegramChatId(message.chat.id); + const bridges = bridgeMap.fromTelegramChatId(ctx.message.chat.id); // Check if the message came from the correct chat if (bridges === undefined) { // Tell the sender that this is a private bot - tgBot.sendMessage({ - chat_id: message.chat.id, - text: "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. " - + "Join our [Telegram group](https://t.me/TediCrossSupport) or [Discord server](https://discord.gg/MfzGMzy) for help" - , - parse_mode: "markdown" - }) + tgBot.telegram.sendMessage( + ctx.message.chat.id, + "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. " + + "Join our [Telegram group](https://t.me/TediCrossSupport) or [Discord server](https://discord.gg/MfzGMzy) for help", + { + parse_mode: "markdown" + } + ) .catch((err) => { // Hmm... Could not send the message for some reason - logger.error("Could not tell user to get their own TediCross instance:", err, message); + logger.error("Could not tell user to get their own TediCross instance:", err, ctx.message); }); } else { bridges.forEach((bridge) => { // Do the thing, if this is not a discord-to-telegram bridge if (bridge.direction !== Bridge.DIRECTION_DISCORD_TO_TELEGRAM) { - func(message, bridge); + func(ctx, bridge); } }); } @@ -149,16 +154,13 @@ const createMessageHandler = R.curry((logger, tgBot, bridgeMap, func, message) = * Sets up the receiving of Telegram messages, and relaying them to Discord * * @param {Logger} logger The Logger instance to log messages to - * @param {BotAPI} tgBot The Telegram bot + * @param {Telegraf} tgBot The Telegram bot * @param {Discord.Client} dcBot The Discord bot * @param {MessageMap} messageMap Map between IDs of messages * @param {BridgeMap} bridgeMap Map of the bridges to use * @param {Settings} settings The settings to use */ function setup(logger, tgBot, dcBot, messageMap, bridgeMap, settings) { - // Start longpolling - const updateEmitter = makeUpdateEmitter(logger, tgBot, settings); - // Make the file sender const sendFile = makeFileSender(tgBot, dcBot, messageMap, settings); @@ -166,7 +168,7 @@ function setup(logger, tgBot, dcBot, messageMap, bridgeMap, settings) { const wrapFunction = createMessageHandler(logger, tgBot, bridgeMap); // Set up event listener for text messages from Telegram - updateEmitter.on("text", wrapFunction(async (message, bridge) => { + tgBot.on("text", wrapFunction(async ({ message }, bridge) => { // Turn the text discord friendly const messageObj = messageConverter(message, tgBot, settings, dcBot, bridge); @@ -215,7 +217,7 @@ function setup(logger, tgBot, dcBot, messageMap, bridgeMap, settings) { })); // Set up event listener for photo messages from Telegram - updateEmitter.on("photo", wrapFunction(async (message, bridge) => { + tgBot.on("photo", wrapFunction(async ({ message }, bridge) => { try { await sendFile(bridge, { message, @@ -229,7 +231,7 @@ function setup(logger, tgBot, dcBot, messageMap, bridgeMap, settings) { })); // Set up event listener for stickers from Telegram - updateEmitter.on("sticker", wrapFunction(async (message, bridge) => { + tgBot.on("sticker", wrapFunction(async ({ message }, bridge) => { try { await sendFile(bridge, { message, @@ -243,7 +245,7 @@ function setup(logger, tgBot, dcBot, messageMap, bridgeMap, settings) { })); // Set up event listener for filetypes not caught by the other filetype handlers - updateEmitter.on("document", wrapFunction(async (message, bridge) => { + tgBot.on("document", wrapFunction(async ({ message }, bridge) => { // message.file_name can for some reason be undefined some times. Default to "file.ext" let fileName = message.document.file_name; if (fileName === undefined) { @@ -264,7 +266,7 @@ function setup(logger, tgBot, dcBot, messageMap, bridgeMap, settings) { })); // Set up event listener for voice messages - updateEmitter.on("voice", wrapFunction(async (message, bridge) => { + tgBot.on("voice", wrapFunction(async ({ message }, bridge) => { try { await sendFile(bridge, { message, @@ -278,7 +280,7 @@ function setup(logger, tgBot, dcBot, messageMap, bridgeMap, settings) { })); // Set up event listener for audio messages - updateEmitter.on("audio", wrapFunction(async (message, bridge) => { + tgBot.on("audio", wrapFunction(async ({ message }, bridge) => { try { await sendFile(bridge, { message, @@ -292,7 +294,7 @@ function setup(logger, tgBot, dcBot, messageMap, bridgeMap, settings) { })); // Set up event listener for video messages - updateEmitter.on("video", wrapFunction(async (message, bridge) => { + tgBot.on("video", wrapFunction(async ({ message }, bridge) => { try { await sendFile(bridge, { message, @@ -307,7 +309,7 @@ function setup(logger, tgBot, dcBot, messageMap, bridgeMap, settings) { })); // Listen for users joining the chat - updateEmitter.on("newParticipants", wrapFunction(({new_chat_members}, bridge) => { + tgBot.on("newParticipants", wrapFunction(({ message: { new_chat_members } }, bridge) => { // Ignore it if the settings say no if (!bridge.telegram.relayJoinMessages) { return; @@ -328,7 +330,7 @@ function setup(logger, tgBot, dcBot, messageMap, bridgeMap, settings) { })); // Listen for users leaving the chat - updateEmitter.on("participantLeft", wrapFunction(async ({left_chat_member}, bridge) => { + tgBot.on("participantLeft", wrapFunction(async ({ message: { left_chat_member } }, bridge) => { // Ignore it if the settings say no if (!bridge.telegram.relayLeaveMessages) { return; @@ -348,7 +350,9 @@ function setup(logger, tgBot, dcBot, messageMap, bridgeMap, settings) { })); // Set up event listener for message edits - updateEmitter.on("messageEdit", wrapFunction(async (tgMessage, bridge) => { + tgBot.on("messageEdit", wrapFunction(async (ctx, bridge) => { + const tgMessage = ctx.message; + try { // Wait for the Discord bot to become ready await dcBot.ready; @@ -374,7 +378,7 @@ function setup(logger, tgBot, dcBot, messageMap, bridgeMap, settings) { })); // Make a promise which resolves when the dcBot is ready - tgBot.ready = tgBot.getMe() + tgBot.ready = tgBot.telegram.getMe() .then((bot) => { // Log the bot's info logger.info(`Telegram: ${bot.username} (${bot.id})`); @@ -389,6 +393,18 @@ function setup(logger, tgBot, dcBot, messageMap, bridgeMap, settings) { // Pass it on throw err; }); + + // Start getting updates + let p = Promise.resolve(); + if (settings.telegram.skipOldMessages) { + // Clear old updates + p = tgBot.telegram.getUpdates(0, 100, -1) + .then(updates => updates.length > 0 + ? tgBot.telegram.getUpdates(0, 100, updates[updates.length-1].update_id) + : [] + ); + } + p.then(() => tgBot.startPolling()); } /***************************** diff --git a/main.js b/main.js index ae20c637..328c92d7 100644 --- a/main.js +++ b/main.js @@ -14,7 +14,7 @@ const Settings = require("./lib/settings/Settings"); const migrateSettingsToYAML = require("./lib/migrateSettingsToYAML"); // Telegram stuff -const { BotAPI } = require("teleapiwrapper"); +const Telegraf = require("telegraf"); const telegramSetup = require("./lib/telegram2discord/setup"); // Discord stuff @@ -40,7 +40,7 @@ const logger = new Logger(settings.debug); settings.toFile(settingsPathYAML); // Create a Telegram bot -const tgBot = new BotAPI(settings.telegram.token); +const tgBot = new Telegraf(settings.telegram.token, { channelMode: true }); // Create a Discord bot const dcBot = new Discord.Client(); diff --git a/package.json b/package.json index 35edbc7f..774ea8e4 100644 --- a/package.json +++ b/package.json @@ -17,8 +17,9 @@ "mime": "^2.2.0", "moment": "^2.21.0", "ramda": "^0.25.0", + "request": "^2.88.0", "simple-markdown": "^0.3.2", - "teleapiwrapper": "^2.3.2" + "telegraf": "^3.25.5" }, "devDependencies": { "eslint": "^4.19.1" From 42bcdd2b44a1a29e9b7178b52380fee78545b8a4 Mon Sep 17 00:00:00 2001 From: Simen de Lange Date: Fri, 18 Jan 2019 17:23:37 +0100 Subject: [PATCH 002/102] Channels now appear to work, with a small hack --- lib/telegram2discord/setup.js | 31 +++++++++++++++++-------------- package.json | 2 +- 2 files changed, 18 insertions(+), 15 deletions(-) diff --git a/lib/telegram2discord/setup.js b/lib/telegram2discord/setup.js index 2b2fc05c..15840e31 100644 --- a/lib/telegram2discord/setup.js +++ b/lib/telegram2discord/setup.js @@ -107,22 +107,25 @@ function makeNameObject(user) { * @private */ const createMessageHandler = R.curry((logger, tgBot, bridgeMap, func, ctx) => { - if (ctx.message.text === "/chatinfo") { + // Channel posts are not on ctx.message, but are very similar. Use the proper one + const message = ctx.channelPost !== undefined ? ctx.channelPost : ctx.message; + + if (message.text === "/chatinfo") { // TODO Make middleware // This is a request for chat info. Give it, no matter which chat this is from tgBot.telegram.sendMessage( - ctx.message.chat.id, - "chatID: " + ctx.message.chat.id + message.chat.id, + "chatID: " + message.chat.id ); } else { // Get the bridge - const bridges = bridgeMap.fromTelegramChatId(ctx.message.chat.id); + const bridges = bridgeMap.fromTelegramChatId(message.chat.id); // Check if the message came from the correct chat if (bridges === undefined) { // Tell the sender that this is a private bot tgBot.telegram.sendMessage( - ctx.message.chat.id, + message.chat.id, "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. " @@ -133,13 +136,13 @@ const createMessageHandler = R.curry((logger, tgBot, bridgeMap, func, ctx) => { ) .catch((err) => { // Hmm... Could not send the message for some reason - logger.error("Could not tell user to get their own TediCross instance:", err, ctx.message); + logger.error("Could not tell user to get their own TediCross instance:", err, message); }); } else { bridges.forEach((bridge) => { // Do the thing, if this is not a discord-to-telegram bridge if (bridge.direction !== Bridge.DIRECTION_DISCORD_TO_TELEGRAM) { - func(ctx, bridge); + func(message, bridge); } }); } @@ -168,7 +171,7 @@ function setup(logger, tgBot, dcBot, messageMap, bridgeMap, settings) { const wrapFunction = createMessageHandler(logger, tgBot, bridgeMap); // Set up event listener for text messages from Telegram - tgBot.on("text", wrapFunction(async ({ message }, bridge) => { + tgBot.on("text", wrapFunction(async (message, bridge) => { // Turn the text discord friendly const messageObj = messageConverter(message, tgBot, settings, dcBot, bridge); @@ -217,7 +220,7 @@ function setup(logger, tgBot, dcBot, messageMap, bridgeMap, settings) { })); // Set up event listener for photo messages from Telegram - tgBot.on("photo", wrapFunction(async ({ message }, bridge) => { + tgBot.on("photo", wrapFunction(async (message, bridge) => { try { await sendFile(bridge, { message, @@ -231,7 +234,7 @@ function setup(logger, tgBot, dcBot, messageMap, bridgeMap, settings) { })); // Set up event listener for stickers from Telegram - tgBot.on("sticker", wrapFunction(async ({ message }, bridge) => { + tgBot.on("sticker", wrapFunction(async (message, bridge) => { try { await sendFile(bridge, { message, @@ -245,7 +248,7 @@ function setup(logger, tgBot, dcBot, messageMap, bridgeMap, settings) { })); // Set up event listener for filetypes not caught by the other filetype handlers - tgBot.on("document", wrapFunction(async ({ message }, bridge) => { + tgBot.on("document", wrapFunction(async (message, bridge) => { // message.file_name can for some reason be undefined some times. Default to "file.ext" let fileName = message.document.file_name; if (fileName === undefined) { @@ -266,7 +269,7 @@ function setup(logger, tgBot, dcBot, messageMap, bridgeMap, settings) { })); // Set up event listener for voice messages - tgBot.on("voice", wrapFunction(async ({ message }, bridge) => { + tgBot.on("voice", wrapFunction(async (message, bridge) => { try { await sendFile(bridge, { message, @@ -280,7 +283,7 @@ function setup(logger, tgBot, dcBot, messageMap, bridgeMap, settings) { })); // Set up event listener for audio messages - tgBot.on("audio", wrapFunction(async ({ message }, bridge) => { + tgBot.on("audio", wrapFunction(async (message, bridge) => { try { await sendFile(bridge, { message, @@ -294,7 +297,7 @@ function setup(logger, tgBot, dcBot, messageMap, bridgeMap, settings) { })); // Set up event listener for video messages - tgBot.on("video", wrapFunction(async ({ message }, bridge) => { + tgBot.on("video", wrapFunction(async (message, bridge) => { try { await sendFile(bridge, { message, diff --git a/package.json b/package.json index 774ea8e4..d2b412d4 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,7 @@ "ramda": "^0.25.0", "request": "^2.88.0", "simple-markdown": "^0.3.2", - "telegraf": "^3.25.5" + "telegraf": "^3.26.0" }, "devDependencies": { "eslint": "^4.19.1" From 29616678d35ec035d90cf531b970bf3c493b90af Mon Sep 17 00:00:00 2001 From: Simen de Lange Date: Fri, 18 Jan 2019 18:13:39 +0100 Subject: [PATCH 003/102] Updated dependencies --- package.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index d2b412d4..9240dff5 100644 --- a/package.json +++ b/package.json @@ -12,13 +12,13 @@ "lint": "eslint ." }, "dependencies": { - "discord.js": "^11.3.0", - "js-yaml": "^3.11.0", - "mime": "^2.2.0", - "moment": "^2.21.0", + "discord.js": "^11.4.2", + "js-yaml": "^3.12.1", + "mime": "^2.4.0", + "moment": "^2.23.0", "ramda": "^0.25.0", "request": "^2.88.0", - "simple-markdown": "^0.3.2", + "simple-markdown": "^0.3.3", "telegraf": "^3.26.0" }, "devDependencies": { @@ -26,7 +26,7 @@ }, "eslintConfig": { "parserOptions": { - "ecmaVersion": 2017 + "ecmaVersion": 2018 }, "env": { "es6": true, From e916d214d420fa79b49217daa6a71774367d9adf Mon Sep 17 00:00:00 2001 From: Simen de Lange Date: Sat, 19 Jan 2019 10:46:43 +0100 Subject: [PATCH 004/102] Made a dockerfile --- Dockerfile | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 Dockerfile diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..6dcb7a7c --- /dev/null +++ b/Dockerfile @@ -0,0 +1,15 @@ +FROM node:10-alpine + +WORKDIR /opt/TediCross/ + +ADD . . + +RUN npm install --production + +# Hack to make the settings file work from the data/ directory +# Remove this line if you build with the settings file integrated +RUN ln -s data/settings.yaml settings.yaml + +VOLUME /opt/TediCross/data/ + +ENTRYPOINT /usr/local/bin/npm start From 560eefeae4f7ca349bdad3f9dfe02c5e3feb046d Mon Sep 17 00:00:00 2001 From: Simen de Lange Date: Sat, 19 Jan 2019 12:04:30 +0100 Subject: [PATCH 005/102] Made a README file on how to use Docker --- README-Docker.md | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 README-Docker.md diff --git a/README-Docker.md b/README-Docker.md new file mode 100644 index 00000000..85c9da68 --- /dev/null +++ b/README-Docker.md @@ -0,0 +1,27 @@ +TediCross with Docker +===================== + +**This document assumes you know how to use [Docker](https://en.wikipedia.org/wiki/Docker_(software)). If you are completely clueless, please disregard Docker and follow the ordinary install instructions** + +TediCross is available as a Docker image, through [DockerHub](https://cloud.docker.com/u/tedicross/repository/docker/tedicross/tedicross) + +It requires the `data/` directory to be mounted as a volume. + +Unlike the non-docker version, the `settings.yaml` file must be in the `data/` directory instead of in the root of the project. + +Using the official Docker image +------------------------------- + +The official docker image is used like this: + +``` +docker run \ + -v /path/to/data/:/opt/TediCross/data \ + -e TELEGRAM_BOT_TOKEN="Your Telegram token" \ + -e DISCORD_BOT_TOKEN="Your Discord token" \ + tedicross +``` + +Of course, you can add `-d` or `--rm` or a name or whatever else you want to that command + +If you have the tokens in the settings file instead of reading them from the environment, you can of course drop the `-e` lines From 3d074bfc8e5ec3b4229840f8387526a156d5599f Mon Sep 17 00:00:00 2001 From: Simen de Lange Date: Sat, 19 Jan 2019 12:05:12 +0100 Subject: [PATCH 006/102] Set an abritrary version --- README-Docker.md | 2 +- version.txt | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 version.txt diff --git a/README-Docker.md b/README-Docker.md index 85c9da68..ee58d9c5 100644 --- a/README-Docker.md +++ b/README-Docker.md @@ -19,7 +19,7 @@ docker run \ -v /path/to/data/:/opt/TediCross/data \ -e TELEGRAM_BOT_TOKEN="Your Telegram token" \ -e DISCORD_BOT_TOKEN="Your Discord token" \ - tedicross + tedicross/tedicross ``` Of course, you can add `-d` or `--rm` or a name or whatever else you want to that command diff --git a/version.txt b/version.txt new file mode 100644 index 00000000..a3df0a69 --- /dev/null +++ b/version.txt @@ -0,0 +1 @@ +0.8.0 From 86450ca5c8c55ef94229eb15a9517ef19ea27e11 Mon Sep 17 00:00:00 2001 From: Simen de Lange Date: Thu, 7 Feb 2019 20:12:54 +0100 Subject: [PATCH 007/102] Edits now works again T2D --- lib/telegram2discord/setup.js | 74 +++++++++++++++++++---------------- 1 file changed, 41 insertions(+), 33 deletions(-) diff --git a/lib/telegram2discord/setup.js b/lib/telegram2discord/setup.js index 15840e31..0d03ba2d 100644 --- a/lib/telegram2discord/setup.js +++ b/lib/telegram2discord/setup.js @@ -108,7 +108,13 @@ function makeNameObject(user) { */ const createMessageHandler = R.curry((logger, tgBot, bridgeMap, func, ctx) => { // Channel posts are not on ctx.message, but are very similar. Use the proper one - const message = ctx.channelPost !== undefined ? ctx.channelPost : ctx.message; + const message = R.cond([ + // XXX I tried both R.has and R.hasIn as conditions. Neither worked for some reason + [ctx => !R.isNil(ctx.channelPost), R.prop("channelPost")], + [ctx => !R.isNil(ctx.editedChannelPost), R.prop("editedChannelPost")], + [ctx => !R.isNil(ctx.message), R.prop("message")], + [ctx => !R.isNil(ctx.editedMessage), R.prop("editedMessage")], + ])(ctx); if (message.text === "/chatinfo") { // TODO Make middleware @@ -117,35 +123,37 @@ const createMessageHandler = R.curry((logger, tgBot, bridgeMap, func, ctx) => { message.chat.id, "chatID: " + message.chat.id ); - } else { - // Get the bridge - const bridges = bridgeMap.fromTelegramChatId(message.chat.id); - - // Check if the message came from the correct chat - if (bridges === undefined) { - // Tell the sender that this is a private bot - tgBot.telegram.sendMessage( - message.chat.id, - "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. " - + "Join our [Telegram group](https://t.me/TediCrossSupport) or [Discord server](https://discord.gg/MfzGMzy) for help", - { - parse_mode: "markdown" - } - ) - .catch((err) => { - // Hmm... Could not send the message for some reason - logger.error("Could not tell user to get their own TediCross instance:", err, message); - }); - } else { - bridges.forEach((bridge) => { - // Do the thing, if this is not a discord-to-telegram bridge - if (bridge.direction !== Bridge.DIRECTION_DISCORD_TO_TELEGRAM) { - func(message, bridge); - } + + return; + } + + // Get the bridge + const bridges = bridgeMap.fromTelegramChatId(message.chat.id); + + // Check if the message came from the correct chat + if (bridges === undefined) { + // Tell the sender that this is a private bot + tgBot.telegram.sendMessage( + message.chat.id, + "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. " + + "Join our [Telegram group](https://t.me/TediCrossSupport) or [Discord server](https://discord.gg/MfzGMzy) for help", + { + parse_mode: "markdown" + } + ) + .catch((err) => { + // Hmm... Could not send the message for some reason + logger.error("Could not tell user to get their own TediCross instance:", err, message); }); - } + } else { + bridges.forEach((bridge) => { + // Do the thing, if this is not a discord-to-telegram bridge + if (bridge.direction !== Bridge.DIRECTION_DISCORD_TO_TELEGRAM) { + func(message, bridge); + } + }); } }); @@ -353,9 +361,7 @@ function setup(logger, tgBot, dcBot, messageMap, bridgeMap, settings) { })); // Set up event listener for message edits - tgBot.on("messageEdit", wrapFunction(async (ctx, bridge) => { - const tgMessage = ctx.message; - + const handleEdits = wrapFunction(async (tgMessage, bridge) => { try { // Wait for the Discord bot to become ready await dcBot.ready; @@ -378,7 +384,9 @@ function setup(logger, tgBot, dcBot, messageMap, bridgeMap, settings) { // Log it logger.error(`[${bridge.name}] Could not edit Discord message:`, err); } - })); + }); + tgBot.on("edited_message", handleEdits); + tgBot.on("edited_channel_post", handleEdits); // Make a promise which resolves when the dcBot is ready tgBot.ready = tgBot.telegram.getMe() From 86141b1594c4ce5840ebe1388676c8a73a9aa8d2 Mon Sep 17 00:00:00 2001 From: Simen de Lange Date: Thu, 7 Feb 2019 20:14:24 +0100 Subject: [PATCH 008/102] Got rid of unnecessary comma --- lib/telegram2discord/setup.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/telegram2discord/setup.js b/lib/telegram2discord/setup.js index 0d03ba2d..c5b683bb 100644 --- a/lib/telegram2discord/setup.js +++ b/lib/telegram2discord/setup.js @@ -113,7 +113,7 @@ const createMessageHandler = R.curry((logger, tgBot, bridgeMap, func, ctx) => { [ctx => !R.isNil(ctx.channelPost), R.prop("channelPost")], [ctx => !R.isNil(ctx.editedChannelPost), R.prop("editedChannelPost")], [ctx => !R.isNil(ctx.message), R.prop("message")], - [ctx => !R.isNil(ctx.editedMessage), R.prop("editedMessage")], + [ctx => !R.isNil(ctx.editedMessage), R.prop("editedMessage")] ])(ctx); if (message.text === "/chatinfo") { From 681cbf0c0175446dd770a22f9f4750655173c9d6 Mon Sep 17 00:00:00 2001 From: Simen de Lange Date: Thu, 7 Feb 2019 20:23:15 +0100 Subject: [PATCH 009/102] Moved version into package.json --- package.json | 2 +- version.txt | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) delete mode 100644 version.txt diff --git a/package.json b/package.json index 9240dff5..b309576f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "tedicross", - "version": "0.1.0", + "version": "0.8.0", "description": "Better DiteCross", "license": "MIT", "repository": { diff --git a/version.txt b/version.txt deleted file mode 100644 index a3df0a69..00000000 --- a/version.txt +++ /dev/null @@ -1 +0,0 @@ -0.8.0 From 53f5c2a9c9d73725a3e6f5e77790d238f529fa18 Mon Sep 17 00:00:00 2001 From: Simen de Lange Date: Thu, 7 Feb 2019 20:23:37 +0100 Subject: [PATCH 010/102] Bumped version to 0.8.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index b309576f..e7b4fd02 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "tedicross", - "version": "0.8.0", + "version": "0.8.1", "description": "Better DiteCross", "license": "MIT", "repository": { From 68795ebf1c529cb947cac5749720a4913deca66e Mon Sep 17 00:00:00 2001 From: Simen de Lange Date: Sat, 16 Feb 2019 16:39:39 +0100 Subject: [PATCH 011/102] Hopefully stopped spamming with `skipOldMessages: false` --- lib/discord2telegram/relayOldMessages.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/lib/discord2telegram/relayOldMessages.js b/lib/discord2telegram/relayOldMessages.js index c23d3e59..e42aa723 100644 --- a/lib/discord2telegram/relayOldMessages.js +++ b/lib/discord2telegram/relayOldMessages.js @@ -39,10 +39,18 @@ async function relayOldMessages(logger, dcBot, latestDiscordMessageIds, bridgeMa // Get messages which have arrived on each bridge since the bot was last shut down .map(async ({bridge, messageId}) => { try { + // Check if the message exists. This will throw if it does not + // XXX If the message does not exist, the following `fetchMessages` call will not throw, but instead return *everything* it can get its hands on, spamming Telegram + await dcBot.channels.get(bridge.discord.channelId).fetchMessage(messageId); + + // Get messages since that one const messages = await dcBot.channels.get(bridge.discord.channelId).fetchMessages({limit: 100, after: messageId}); + + // Relay them sortAndRelay(messages.array()); } catch (err) { logger.error(err); + logger.log("The previous error is probably nothing to worry about"); } }); } From d132cb7cc8224993b51c88f6ae3ea9f3da1ae916 Mon Sep 17 00:00:00 2001 From: Simen de Lange Date: Sat, 16 Feb 2019 16:50:44 +0100 Subject: [PATCH 012/102] Added option to not cross-delete messages D2T --- lib/bridgestuff/BridgeSettingsDiscord.js | 12 ++++++ lib/discord2telegram/setup.js | 5 +++ lib/settings/Settings.js | 49 ++++-------------------- 3 files changed, 25 insertions(+), 41 deletions(-) diff --git a/lib/bridgestuff/BridgeSettingsDiscord.js b/lib/bridgestuff/BridgeSettingsDiscord.js index 3d7c7398..1930655e 100644 --- a/lib/bridgestuff/BridgeSettingsDiscord.js +++ b/lib/bridgestuff/BridgeSettingsDiscord.js @@ -62,6 +62,13 @@ class BridgeSettingsDiscord { * @type {Boolean} */ this.sendUsernames = settings.sendUsernames; + + /** + * Whether or not to delete messages on Telegram when a message is deleted on Discord + * + * @type {Boolean} + */ + this.crossDeleteOnTelegram = settings.crossDeleteOnTelegram; } /** @@ -91,6 +98,11 @@ class BridgeSettingsDiscord { if (Boolean(settings.sendUsernames) !== settings.sendUsernames) { throw new Error("`settings.sendUsernames` must be a boolean"); } + + // Check that crossDeleteOnTelegram is a boolean + if (Boolean(settings.crossDeleteOnTelegram) !== settings.crossDeleteOnTelegram) { + throw new Error("`settings.crossDeleteOnTelegram` must be a boolean"); + } } } diff --git a/lib/discord2telegram/setup.js b/lib/discord2telegram/setup.js index e4a643ed..66b2180b 100644 --- a/lib/discord2telegram/setup.js +++ b/lib/discord2telegram/setup.js @@ -269,6 +269,11 @@ function setup(logger, dcBot, tgBot, messageMap, bridgeMap, settings) { const isFromTelegram = message.author.id === dcBot.user.id; bridges.forEach(async (bridge) => { + // Ignore it if cross deletion is disabled + if (!bridge.discord.crossDeleteOnTelegram) { + return; + } + try { // Get the corresponding Telegram message IDs const tgMessageIds = isFromTelegram diff --git a/lib/settings/Settings.js b/lib/settings/Settings.js index dc504503..9084726a 100644 --- a/lib/settings/Settings.js +++ b/lib/settings/Settings.js @@ -127,46 +127,6 @@ class Settings { // Make a clone, to not operate directly on the provided object const settings = R.clone(rawSettings); - // Check if the bridge map exists - if (settings.bridgeMap === undefined || settings.bridgeMap.length === 0) { - - // Check if a bridge on the old format should be migrated - const migrate = ( - settings.telegram.chatID !== undefined || - settings.discord.serverID !== undefined || - settings.discord.channelID !== undefined - ); - - if (migrate) { - // Migrate the old settings to the bridge map - settings.bridgeMap = [ - { - name: "Migrated bridge", - telegram: settings.telegram.chatID, - discord: { - guild: settings.discord.serverID, - channel: settings.discord.channelID - } - } - ]; - - // Delete the old properties - delete settings.telegram.chatID; - delete settings.discord.serverID; - delete settings.discord.channelID; - } - } - - // ...and convert `bridgeMap` to just `bridges` - if (settings.bridgeMap !== undefined) { - - // Move it - settings.bridges = settings.bridgeMap; - - // Delete the old property - delete settings.bridgeMap; - } - // Convert the bridge objects if necessary for (const bridge of settings.bridges) { if (!(bridge.telegram instanceof Object)) { @@ -221,7 +181,7 @@ class Settings { } } - // Add the `sendUsername` option to the bridges 2018-11-25 + // 2018-11-25: Add the `sendUsername` option to the bridges for (const bridge of settings.bridges) { // Do the Telegram part if (bridge.telegram.sendUsernames === undefined) { @@ -234,6 +194,13 @@ class Settings { } } + // 2019-02-16: Add the `crossDeleteOnTelegram` option to Discord + for (const bridge of settings.bridges) { + if (bridge.discord.crossDeleteOnTelegram === undefined) { + bridge.discord.crossDeleteOnTelegram = true; + } + } + // All done! return settings; } From af8c8288b2009e27fea12ebd044ca33bf30219c9 Mon Sep 17 00:00:00 2001 From: Simen de Lange Date: Sat, 16 Feb 2019 16:59:55 +0100 Subject: [PATCH 013/102] Join/Leave messages again work T2D --- lib/telegram2discord/setup.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/telegram2discord/setup.js b/lib/telegram2discord/setup.js index c5b683bb..beb1c6ee 100644 --- a/lib/telegram2discord/setup.js +++ b/lib/telegram2discord/setup.js @@ -320,7 +320,7 @@ function setup(logger, tgBot, dcBot, messageMap, bridgeMap, settings) { })); // Listen for users joining the chat - tgBot.on("newParticipants", wrapFunction(({ message: { new_chat_members } }, bridge) => { + tgBot.on("new_chat_members", wrapFunction(({ new_chat_members }, bridge) => { // Ignore it if the settings say no if (!bridge.telegram.relayJoinMessages) { return; @@ -341,7 +341,7 @@ function setup(logger, tgBot, dcBot, messageMap, bridgeMap, settings) { })); // Listen for users leaving the chat - tgBot.on("participantLeft", wrapFunction(async ({ message: { left_chat_member } }, bridge) => { + tgBot.on("left_chat_member", wrapFunction(async ({ left_chat_member }, bridge) => { // Ignore it if the settings say no if (!bridge.telegram.relayLeaveMessages) { return; From 6a541b5ee95a6ff75e2bd30c599674ff998cf362 Mon Sep 17 00:00:00 2001 From: Simen de Lange Date: Sat, 16 Feb 2019 17:00:21 +0100 Subject: [PATCH 014/102] Bumped version to 0.8.2 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index e7b4fd02..bbd097e6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "tedicross", - "version": "0.8.1", + "version": "0.8.2", "description": "Better DiteCross", "license": "MIT", "repository": { From 41da7354eac4e40f026bc9e588ab02ee4d4100fe Mon Sep 17 00:00:00 2001 From: Simen de Lange Date: Sat, 16 Feb 2019 17:03:54 +0100 Subject: [PATCH 015/102] Added the crossDeleteOnTelegram option to example.settings.json --- example.settings.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/example.settings.yaml b/example.settings.yaml index fbf1ac81..9db8eb34 100644 --- a/example.settings.yaml +++ b/example.settings.yaml @@ -22,4 +22,5 @@ bridges: relayJoinMessages: true relayLeaveMessages: true sendUsernames: true + crossDeleteOnTelegram: true debug: false From 49fea14e3807878dbb3f4e89853657d8f3140c50 Mon Sep 17 00:00:00 2001 From: Simen de Lange Date: Sun, 3 Mar 2019 10:40:13 +0100 Subject: [PATCH 016/102] FAQ about deleting messages T2D --- README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/README.md b/README.md index 6e7fe994..1a25927b 100644 --- a/README.md +++ b/README.md @@ -114,6 +114,12 @@ The Telegram team unfortunately decided that bots cannot interact with each othe See https://core.telegram.org/bots/faq#why-doesn-39t-my-bot-see-messages-from-other-bots +### Deleting a message in Telegram does not delete it in Discord + +Telegram bots are unfortunately completely unable to detect when a message is deleted. There is no way to implement T2D cross-deletion until Telegram implements this. + +Deleting messaged D2T works. + ### When running `npm install`, it complains about missing dependencies? The [Discord library](https://discord.js.org/#/) TediCross is using has support for audio channels and voice chat. For this, it needs some additional libraries, like [node-opus](https://www.npmjs.com/package/node-opus), [libsodium](https://www.npmjs.com/package/libsodium) and others. TediCross does not do audio, so these warnings can safely be ignored From 49063b82e8637a048c5ef57d859b36cbee6651a9 Mon Sep 17 00:00:00 2001 From: Thomas Rory Gummerson Date: Wed, 20 Mar 2019 23:05:49 +0100 Subject: [PATCH 017/102] Added command-line arguments --- .../LatestDiscordMessageIds.js | 4 ++-- lib/discord2telegram/setup.js | 4 ++-- main.js | 23 ++++++++++++++++--- package.json | 3 ++- 4 files changed, 26 insertions(+), 8 deletions(-) diff --git a/lib/discord2telegram/LatestDiscordMessageIds.js b/lib/discord2telegram/LatestDiscordMessageIds.js index 2f19ccc6..45ded9b6 100644 --- a/lib/discord2telegram/LatestDiscordMessageIds.js +++ b/lib/discord2telegram/LatestDiscordMessageIds.js @@ -21,7 +21,7 @@ class LatestDiscordMessageIds { * @param {Logger} logger The Logger instance to log messages to * @param {String} filename Name of the file to persistently store the map in. Will be put in the `data/` directory */ - constructor(logger, filename) { + constructor(logger, filename, dirname = path.join(__dirname, "..", "..", "data")) { /** * The Logger instance to log messages to * @@ -36,7 +36,7 @@ class LatestDiscordMessageIds { * * @private */ - this._filename = path.join(__dirname, "..", "..", "data", filename); + this._filename = path.join(dirname, filename); /** * The actual map diff --git a/lib/discord2telegram/setup.js b/lib/discord2telegram/setup.js index 66b2180b..f9446a06 100644 --- a/lib/discord2telegram/setup.js +++ b/lib/discord2telegram/setup.js @@ -82,9 +82,9 @@ function makeJoinLeaveFunc(logger, verb, bridgeMap, tgBot) { * @param {BridgeMap} bridgeMap Map of the bridges to use * @param {Settings} settings Settings to use */ -function setup(logger, dcBot, tgBot, messageMap, bridgeMap, settings) { +function setup(logger, dcBot, tgBot, messageMap, bridgeMap, settings, datadir) { // Create the map of latest message IDs and bridges - const latestDiscordMessageIds = new LatestDiscordMessageIds(logger, "latestDiscordMessageIds.json"); + const latestDiscordMessageIds = new LatestDiscordMessageIds(logger, "latestDiscordMessageIds.json", datadir); const useNickname = settings.discord.useNickname; // Listen for users joining the server diff --git a/main.js b/main.js index 328c92d7..c19c3606 100644 --- a/main.js +++ b/main.js @@ -21,13 +21,30 @@ const telegramSetup = require("./lib/telegram2discord/setup"); const Discord = require("discord.js"); const discordSetup = require("./lib/discord2telegram/setup"); +// Arg parsing +const yargs = require("yargs"); + /************* * TediCross * *************/ +// Get commandline arguments if any +const args = yargs + .alias("v", "version") + .alias("h", "help") + .option("config", { + alias: "c", + default: path.join(__dirname, "settings.yaml"), + describe: "Specify settings file", + type: "string" + }) + .option("data-dir", { + alias: "d" + }).argv; + // Migrate the settings from JSON to YAML const settingsPathJSON = path.join(__dirname, "settings.json"); -const settingsPathYAML = path.join(__dirname, "settings.yaml"); +const settingsPathYAML = args.config || path.join(__dirname, "settings.yaml"); migrateSettingsToYAML(settingsPathJSON, settingsPathYAML); // Get the settings @@ -55,5 +72,5 @@ const bridgeMap = new BridgeMap(settings.bridges.map((bridgeSettings) => new Bri * Set up the bridge * *********************/ -discordSetup(logger, dcBot, tgBot, messageMap, bridgeMap, settings); -telegramSetup(logger, tgBot, dcBot, messageMap, bridgeMap, settings); +discordSetup(logger, dcBot, tgBot, messageMap, bridgeMap, settings, args.dataDir); +telegramSetup(logger, tgBot, dcBot, messageMap, bridgeMap, settings, args.dataDir); diff --git a/package.json b/package.json index bbd097e6..ac9c4fbe 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,8 @@ "ramda": "^0.25.0", "request": "^2.88.0", "simple-markdown": "^0.3.3", - "telegraf": "^3.26.0" + "telegraf": "^3.26.0", + "yargs": "^13.2.2" }, "devDependencies": { "eslint": "^4.19.1" From cda41691609dd3cb43596ff310cbd0909370c8b2 Mon Sep 17 00:00:00 2001 From: Thomas Rory Gummerson Date: Thu, 21 Mar 2019 00:07:32 +0100 Subject: [PATCH 018/102] Set default and type for data-dir, added description to data-dir cli argument --- lib/discord2telegram/LatestDiscordMessageIds.js | 2 +- main.js | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/lib/discord2telegram/LatestDiscordMessageIds.js b/lib/discord2telegram/LatestDiscordMessageIds.js index 45ded9b6..dcfa6600 100644 --- a/lib/discord2telegram/LatestDiscordMessageIds.js +++ b/lib/discord2telegram/LatestDiscordMessageIds.js @@ -21,7 +21,7 @@ class LatestDiscordMessageIds { * @param {Logger} logger The Logger instance to log messages to * @param {String} filename Name of the file to persistently store the map in. Will be put in the `data/` directory */ - constructor(logger, filename, dirname = path.join(__dirname, "..", "..", "data")) { + constructor(logger, filename, dirname) { /** * The Logger instance to log messages to * diff --git a/main.js b/main.js index c19c3606..4bbd0df9 100644 --- a/main.js +++ b/main.js @@ -39,7 +39,10 @@ const args = yargs type: "string" }) .option("data-dir", { - alias: "d" + alias: "d", + default: path.join(__dirname, "data"), + describe: "Specify the directory to store data in", + type: "string" }).argv; // Migrate the settings from JSON to YAML From d95a40e77f2c94fb0357f72360d462dde340ea54 Mon Sep 17 00:00:00 2001 From: Simen de Lange Date: Fri, 22 Mar 2019 17:43:20 +0100 Subject: [PATCH 019/102] Some code cleaning --- .../LatestDiscordMessageIds.js | 22 +++++++++---------- lib/discord2telegram/setup.js | 6 +++-- main.js | 12 +++++----- 3 files changed, 19 insertions(+), 21 deletions(-) diff --git a/lib/discord2telegram/LatestDiscordMessageIds.js b/lib/discord2telegram/LatestDiscordMessageIds.js index dcfa6600..a024727c 100644 --- a/lib/discord2telegram/LatestDiscordMessageIds.js +++ b/lib/discord2telegram/LatestDiscordMessageIds.js @@ -5,7 +5,7 @@ **************************/ const fs = require("fs"); -const path = require("path"); +const promisify = require("util").promisify; /************************************* * The LatestDiscordMessageIds class * @@ -19,9 +19,9 @@ class LatestDiscordMessageIds { * Creates a new instance which keeps track of messages and bridges * * @param {Logger} logger The Logger instance to log messages to - * @param {String} filename Name of the file to persistently store the map in. Will be put in the `data/` directory + * @param {String} filepath Path to the file to persistently store the map in */ - constructor(logger, filename, dirname) { + constructor(logger, filepath) { /** * The Logger instance to log messages to * @@ -36,7 +36,7 @@ class LatestDiscordMessageIds { * * @private */ - this._filename = path.join(dirname, filename); + this._filepath = filepath; /** * The actual map @@ -49,19 +49,19 @@ class LatestDiscordMessageIds { try { // Check if the file exists. This throws if it doesn't - fs.accessSync(this._filename, fs.constants.F_OK); + fs.accessSync(this._filepath, fs.constants.F_OK); } catch (e) { // Nope, it doesn't. Create it - fs.writeFileSync(this._filename, JSON.stringify({})); + fs.writeFileSync(this._filepath, JSON.stringify({})); } // Read the file let data = null; try { - data = fs.readFileSync(this._filename); + data = fs.readFileSync(this._filepath); } catch (err) { // Well, the file has been confirmed to exist, so there must be no read access - this._logger.error(`Cannot read the file ${this._filename}:`, err); + this._logger.error(`Cannot read the file ${this._filepath}:`, err); data = JSON.stringify({}); } @@ -70,7 +70,7 @@ class LatestDiscordMessageIds { this._map = JSON.parse(data); } catch (err) { // Invalid JSON - this._logger.error(`Could not read or parse the file ${this._filename}:`, err); + this._logger.error(`Could not read or parse the file ${this._filepath}:`, err); this._map = {}; } @@ -100,9 +100,7 @@ class LatestDiscordMessageIds { // Write it to file when previous writes have completed this._finishedWriting = this._finishedWriting - .then(() => new Promise((resolve, reject) => { - fs.writeFile(this._filename, JSON.stringify(this._map, null, "\t"), (err) => err ? reject(err) : resolve()); - })) + .then(() => promisify(fs.writeFile)(this._filepath, JSON.stringify(this._map, null, "\t"))) .catch((err) => this._logger.error("Writing last Discord message ID to file failed!", err)); } diff --git a/lib/discord2telegram/setup.js b/lib/discord2telegram/setup.js index f9446a06..549d2581 100644 --- a/lib/discord2telegram/setup.js +++ b/lib/discord2telegram/setup.js @@ -11,6 +11,7 @@ const LatestDiscordMessageIds = require("./LatestDiscordMessageIds"); const handleEmbed = require("./handleEmbed"); const relayOldMessages = require("./relayOldMessages"); const Bridge = require("../bridgestuff/Bridge"); +const path = require("path"); /*********** * Helpers * @@ -81,10 +82,11 @@ function makeJoinLeaveFunc(logger, verb, bridgeMap, tgBot) { * @param {MessageMap} messageMap Map between IDs of messages * @param {BridgeMap} bridgeMap Map of the bridges to use * @param {Settings} settings Settings to use + * @param {String} datadirPath Path to the directory to put data files in */ -function setup(logger, dcBot, tgBot, messageMap, bridgeMap, settings, datadir) { +function setup(logger, dcBot, tgBot, messageMap, bridgeMap, settings, datadirPath) { // Create the map of latest message IDs and bridges - const latestDiscordMessageIds = new LatestDiscordMessageIds(logger, "latestDiscordMessageIds.json", datadir); + const latestDiscordMessageIds = new LatestDiscordMessageIds(logger, path.join(datadirPath, "latestDiscordMessageIds.json")); const useNickname = settings.discord.useNickname; // Listen for users joining the server diff --git a/main.js b/main.js index 4bbd0df9..1f23bb26 100644 --- a/main.js +++ b/main.js @@ -5,6 +5,7 @@ **************************/ // General stuff +const yargs = require("yargs"); const path = require("path"); const Logger = require("./lib/Logger"); const MessageMap = require("./lib/MessageMap"); @@ -21,33 +22,30 @@ const telegramSetup = require("./lib/telegram2discord/setup"); const Discord = require("discord.js"); const discordSetup = require("./lib/discord2telegram/setup"); -// Arg parsing -const yargs = require("yargs"); - /************* * TediCross * *************/ -// Get commandline arguments if any +// Get command line arguments if any const args = yargs .alias("v", "version") .alias("h", "help") .option("config", { alias: "c", default: path.join(__dirname, "settings.yaml"), - describe: "Specify settings file", + describe: "Specify path to settings file", type: "string" }) .option("data-dir", { alias: "d", default: path.join(__dirname, "data"), - describe: "Specify the directory to store data in", + describe: "Specify the path to the directory to store data in", type: "string" }).argv; // Migrate the settings from JSON to YAML const settingsPathJSON = path.join(__dirname, "settings.json"); -const settingsPathYAML = args.config || path.join(__dirname, "settings.yaml"); +const settingsPathYAML = args.config; migrateSettingsToYAML(settingsPathJSON, settingsPathYAML); // Get the settings From ac63109da0fdad5de169c8828374bb7cd7e91d39 Mon Sep 17 00:00:00 2001 From: Simen de Lange Date: Fri, 22 Mar 2019 18:42:31 +0100 Subject: [PATCH 020/102] No longer crashes if settings file is not writable --- lib/settings/Settings.js | 84 +++++++++------------------------------- main.js | 34 ++++++++++++++-- 2 files changed, 49 insertions(+), 69 deletions(-) diff --git a/lib/settings/Settings.js b/lib/settings/Settings.js index 9084726a..98b6ebd5 100644 --- a/lib/settings/Settings.js +++ b/lib/settings/Settings.js @@ -78,6 +78,16 @@ class Settings { fs.writeFileSync(filepath, notepadFriendlyYaml); } + /** + * Makes a raw settings object from this object + * + * @returns {Object} A plain object with the settings + */ + toObj() { + // Hacky way to turn this into a plain object... + return JSON.parse(JSON.stringify(this)); + } + /** * Validates a raw settings object, checking if it is usable for creating a Settings object * @@ -127,43 +137,6 @@ class Settings { // Make a clone, to not operate directly on the provided object const settings = R.clone(rawSettings); - // Convert the bridge objects if necessary - for (const bridge of settings.bridges) { - if (!(bridge.telegram instanceof Object)) { - bridge.telegram = { - chatId: bridge.telegram, - relayJoinLeaveMessages: true - }; - bridge.discord = { - serverId: bridge.discord.guild, - channelId: bridge.discord.channel, - relayJoinLeaveMessages: true - }; - } - - // Default to bidirectional bridges - if (bridge.direction === undefined) { - bridge.direction = Bridge.DIRECTION_BOTH; - } - } - - // Get rid of the `telegram.auth` object - if (settings.telegram.auth !== undefined) { - settings.telegram.token = settings.telegram.auth.token; - delete settings.telegram.auth; - } - - // Get rid of the `discord.auth` object - if (settings.discord.auth !== undefined) { - settings.discord.token = settings.discord.auth.token; - delete settings.discord.auth; - } - - // Get rid of the `telegram.commaAfterSenderName` property - if (settings.telegram.commaAfterSenderName !== undefined) { - delete settings.telegram.commaAfterSenderName; - } - // Split the `relayJoinLeaveMessages` for (const bridge of settings.bridges) { // Do the Telegram part @@ -206,39 +179,18 @@ class Settings { } /** - * Creates a new settings object from file - * - * @param {String} filepath Path to the settings file to use. Absolute path is recommended + * Creates a new settings object from a plain object * - * @returns {Settings} A settings object + * @param {Object} obj The object to create a settings object from * - * @throws If the file does not contain a YAML object, or it cannot be read/written + * @returns {Settings} The settings object */ - static fromFile(filepath) { - // Read the file - let contents = null; - try { - contents = fs.readFileSync(filepath); - } catch (err) { - // Could not read it. Check if it exists - if (err.code === "ENOENT") { - // It didn't. Claim it contained an empty YAML object - contents = jsYaml.safeDump({}); - - // ...and make it so that it actually does - fs.writeFileSync(filepath, contents); - } else { - // Pass the error on - throw err; - } - } - - // Parse the contents as YAML - let settings = jsYaml.safeLoad(contents); - + static fromObj(obj) { // Assign defaults and migrate to newest format - settings = Settings.applyDefaults(settings); - settings = Settings.migrate(settings); + const settings = R.compose( + Settings.migrate, + Settings.applyDefaults + )(obj); // Create and return the settings object return new Settings(settings); diff --git a/main.js b/main.js index 1f23bb26..b436e566 100644 --- a/main.js +++ b/main.js @@ -13,6 +13,10 @@ const Bridge = require("./lib/bridgestuff/Bridge"); const BridgeMap = require("./lib/bridgestuff/BridgeMap"); const Settings = require("./lib/settings/Settings"); const migrateSettingsToYAML = require("./lib/migrateSettingsToYAML"); +const jsYaml = require("js-yaml"); +const fs = require("fs"); +const R = require("ramda"); +const os = require("os"); // Telegram stuff const Telegraf = require("telegraf"); @@ -49,13 +53,37 @@ const settingsPathYAML = args.config; migrateSettingsToYAML(settingsPathJSON, settingsPathYAML); // Get the settings -const settings = Settings.fromFile(settingsPathYAML); +const rawSettingsObj = jsYaml.safeLoad(fs.readFileSync(settingsPathYAML)); +const settings = Settings.fromObj(rawSettingsObj); // Initialize logger const logger = new Logger(settings.debug); -// Save the settings, as they might have changed -settings.toFile(settingsPathYAML); +// Write the settings back to the settings file if they have been modified +const newRawSettingsObj = settings.toObj(); +if (R.not(R.equals(rawSettingsObj, newRawSettingsObj))) { + // Turn it into notepad friendly YAML + const yaml = jsYaml.safeDump(newRawSettingsObj).replace(/\n/g, "\r\n"); + + try { + fs.writeFileSync(settingsPathYAML, yaml); + } catch (err) { + if (err.code === "EACCES") { + // The settings file is not writable. Give a warning + logger.warn("Changes to TediCross' settings have been introduced. Your settings file it not writable, so it could not be automatically updated. TediCross will still work, with the modified settings, but you will see this warning until you update your settings file"); + + // Write the settings to temp instead + const tmpPath = path.join(os.tmpdir(), "tedicross-settings.yaml"); + try { + fs.writeFileSync(tmpPath, yaml); + logger.info(`The new settings file has instead been written to '${tmpPath}'. Copy it to its proper location to get rid of the warning`); + } catch (err) { + logger.warn(`An attempt was made to put the modified settings file at '${tmpPath}', but it could not be done. See the following error message`); + logger.warn(err); + } + } + } +} // Create a Telegram bot const tgBot = new Telegraf(settings.telegram.token, { channelMode: true }); From b20a09b4be0ed4f14e97e799d2b0ab00dc77d9c5 Mon Sep 17 00:00:00 2001 From: Simen de Lange Date: Fri, 22 Mar 2019 20:40:54 +0100 Subject: [PATCH 021/102] Bumped version to 0.8.3 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index ac9c4fbe..c9bb68e6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "tedicross", - "version": "0.8.2", + "version": "0.8.3", "description": "Better DiteCross", "license": "MIT", "repository": { From 9901cd16638f4e2f97662b1b32a5b97b7d58a7b3 Mon Sep 17 00:00:00 2001 From: Simen de Lange Date: Tue, 9 Apr 2019 17:46:24 +0200 Subject: [PATCH 022/102] Removed links to support chats from generic message --- lib/discord2telegram/setup.js | 4 +--- lib/telegram2discord/setup.js | 3 +-- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/lib/discord2telegram/setup.js b/lib/discord2telegram/setup.js index 549d2581..4c9e3bb3 100644 --- a/lib/discord2telegram/setup.js +++ b/lib/discord2telegram/setup.js @@ -208,9 +208,7 @@ function setup(logger, dcBot, tgBot, messageMap, bridgeMap, settings, datadirPat message.reply( "This is an instance of a TediCross bot, bridging a chat in Telegram with one in Discord. " + "If you wish to use TediCross yourself, please download and create an instance. " - + "You may join our Discord server (https://discord.gg/MfzGMzy) " - + "or Telegram group (https://t.me/TediCrossSupport) for help. " - + "See also https://github.com/TediCross/TediCross" + + "See https://github.com/TediCross/TediCross" ); } }); diff --git a/lib/telegram2discord/setup.js b/lib/telegram2discord/setup.js index beb1c6ee..6a92f371 100644 --- a/lib/telegram2discord/setup.js +++ b/lib/telegram2discord/setup.js @@ -137,8 +137,7 @@ const createMessageHandler = R.curry((logger, tgBot, bridgeMap, func, ctx) => { message.chat.id, "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. " - + "Join our [Telegram group](https://t.me/TediCrossSupport) or [Discord server](https://discord.gg/MfzGMzy) for help", + + "If you wish to use TediCross yourself, please download and create an instance.", { parse_mode: "markdown" } From 855ff92855d3907c91e9edd4e8149468c000050a Mon Sep 17 00:00:00 2001 From: Simen de Lange Date: Tue, 9 Apr 2019 18:03:48 +0200 Subject: [PATCH 023/102] Ignores casing on mentions @suppen will mention both @Suppen and @suppen and @SuPpEn --- lib/telegram2discord/handleEntities.js | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/lib/telegram2discord/handleEntities.js b/lib/telegram2discord/handleEntities.js index 3e60d0b1..269cfd31 100644 --- a/lib/telegram2discord/handleEntities.js +++ b/lib/telegram2discord/handleEntities.js @@ -51,9 +51,16 @@ function handleEntities(text, entities, dcBot, bridge) { // A mention. Substitute the Discord user ID or Discord role ID if one exists // XXX Telegram considers it a mention if it is a valid Telegram username, not necessarily taken. This means the mention matches the regexp /^@[a-zA-Z0-9_]{5,}$/ // In turn, this means short usernames and roles in Discord, like '@devs', will not be possible to mention - const mentionable = part.substring(1); - const dcUser = dcBot.channels.get(bridge.discord.channelId).members.find("displayName", mentionable); - const dcRole = dcBot.guilds.get(bridge.discord.serverId).roles.find("name", mentionable); + const mentionable = new RegExp(`^${part.substring(1)}$`, "i"); + const findFn = prop => + R.compose( + R.not, + R.isEmpty, + R.match(mentionable), + R.prop(prop) + ); + const dcUser = dcBot.channels.get(bridge.discord.channelId).members.find(findFn("displayName")); + const dcRole = dcBot.guilds.get(bridge.discord.serverId).roles.find(findFn("name")); if (!R.isNil(dcUser)) { substitute = `<@${dcUser.id}>`; } else if (!R.isNil(dcRole)) { From de6135e552485311a97172cc44f921b5637bc576 Mon Sep 17 00:00:00 2001 From: Simen de Lange Date: Tue, 9 Apr 2019 18:19:51 +0200 Subject: [PATCH 024/102] Same treatment to channel names --- lib/telegram2discord/handleEntities.js | 27 +++++++++++++++----------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/lib/telegram2discord/handleEntities.js b/lib/telegram2discord/handleEntities.js index 269cfd31..7eb50add 100644 --- a/lib/telegram2discord/handleEntities.js +++ b/lib/telegram2discord/handleEntities.js @@ -6,6 +6,18 @@ const R = require("ramda"); +/********************* + * Make some helpers * + *********************/ + +const findFn = (prop, regexp) => + R.compose( + R.not, + R.isEmpty, + R.match(regexp), + R.prop(prop) + ); + /***************************** * Define the entity handler * *****************************/ @@ -52,15 +64,8 @@ function handleEntities(text, entities, dcBot, bridge) { // XXX Telegram considers it a mention if it is a valid Telegram username, not necessarily taken. This means the mention matches the regexp /^@[a-zA-Z0-9_]{5,}$/ // In turn, this means short usernames and roles in Discord, like '@devs', will not be possible to mention const mentionable = new RegExp(`^${part.substring(1)}$`, "i"); - const findFn = prop => - R.compose( - R.not, - R.isEmpty, - R.match(mentionable), - R.prop(prop) - ); - const dcUser = dcBot.channels.get(bridge.discord.channelId).members.find(findFn("displayName")); - const dcRole = dcBot.guilds.get(bridge.discord.serverId).roles.find(findFn("name")); + const dcUser = dcBot.channels.get(bridge.discord.channelId).members.find(findFn("displayName", mentionable)); + const dcRole = dcBot.guilds.get(bridge.discord.serverId).roles.find(findFn("name", mentionable)); if (!R.isNil(dcUser)) { substitute = `<@${dcUser.id}>`; } else if (!R.isNil(dcRole)) { @@ -101,10 +106,10 @@ function handleEntities(text, entities, dcBot, bridge) { } case "hashtag": { // Possible name of a Discord channel on the same Discord server - const channelName = part.substring(1); + const channelName = new RegExp(`^${part.substring(1).toLowerCase()}$`); // Find out if this is a channel on the bridged Discord server - const channel = dcBot.guilds.get(bridge.discord.serverId).channels.find("name", channelName); + const channel = dcBot.guilds.get(bridge.discord.serverId).channels.find(findFn("name", channelName)); // Make Discord recognize it as a channel mention if (channel !== null) { From 6a08ab7d3bf7b1b96ae259050c2b11f2fa9b57e5 Mon Sep 17 00:00:00 2001 From: Simen de Lange Date: Tue, 9 Apr 2019 22:16:42 +0200 Subject: [PATCH 025/102] Moved the "Reply to @user" text out of the embed --- lib/telegram2discord/setup.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/telegram2discord/setup.js b/lib/telegram2discord/setup.js index 6a92f371..885e3212 100644 --- a/lib/telegram2discord/setup.js +++ b/lib/telegram2discord/setup.js @@ -194,11 +194,11 @@ function setup(logger, tgBot, dcBot, messageMap, bridgeMap, settings) { if (messageObj.reply !== null) { // Make a Discord embed and send it first const embed = new Discord.RichEmbed({ - description: `Reply to **${messageObj.reply.author}**\n${messageObj.reply.text}` + description: messageObj.reply.text }); const textToSend = bridge.telegram.sendUsernames - ? `**${messageObj.from}**` + ? `**${messageObj.from}** (In reply to **${messageObj.reply.author}**)` : undefined ; From 87af78c3165186d387ac2a4d25d5110606a7edb2 Mon Sep 17 00:00:00 2001 From: Simen de Lange Date: Sun, 21 Apr 2019 14:20:21 +0200 Subject: [PATCH 026/102] Removed settings migrations over one year old --- lib/settings/Settings.js | 24 ++---------------------- 1 file changed, 2 insertions(+), 22 deletions(-) diff --git a/lib/settings/Settings.js b/lib/settings/Settings.js index 98b6ebd5..ddb00662 100644 --- a/lib/settings/Settings.js +++ b/lib/settings/Settings.js @@ -137,23 +137,6 @@ class Settings { // Make a clone, to not operate directly on the provided object const settings = R.clone(rawSettings); - // Split the `relayJoinLeaveMessages` - for (const bridge of settings.bridges) { - // Do the Telegram part - if (bridge.telegram.relayJoinLeaveMessages !== undefined) { - bridge.telegram.relayJoinMessages = bridge.telegram.relayJoinLeaveMessages; - bridge.telegram.relayLeaveMessages = bridge.telegram.relayJoinLeaveMessages; - delete bridge.telegram.relayJoinLeaveMessages; - } - - // Do the Discord part - if (bridge.discord.relayJoinLeaveMessages !== undefined) { - bridge.discord.relayJoinMessages = bridge.discord.relayJoinLeaveMessages; - bridge.discord.relayLeaveMessages = bridge.discord.relayJoinLeaveMessages; - delete bridge.discord.relayJoinLeaveMessages; - } - } - // 2018-11-25: Add the `sendUsername` option to the bridges for (const bridge of settings.bridges) { // Do the Telegram part @@ -186,14 +169,11 @@ class Settings { * @returns {Settings} The settings object */ static fromObj(obj) { - // Assign defaults and migrate to newest format - const settings = R.compose( + return R.compose( + R.construct(Settings), Settings.migrate, Settings.applyDefaults )(obj); - - // Create and return the settings object - return new Settings(settings); } /** From 5841eeb459af32db875bc0169a185429fbe55bad Mon Sep 17 00:00:00 2001 From: Simen de Lange Date: Sun, 21 Apr 2019 15:09:15 +0200 Subject: [PATCH 027/102] Setting for how to display replies in Discord --- lib/settings/DiscordSettings.js | 25 ++++++++++++++++ lib/settings/Settings.js | 10 +++++++ lib/telegram2discord/handleEntities.js | 1 + lib/telegram2discord/messageConverter.js | 19 ++++++++---- lib/telegram2discord/setup.js | 37 ++++++++++++++---------- 5 files changed, 72 insertions(+), 20 deletions(-) diff --git a/lib/settings/DiscordSettings.js b/lib/settings/DiscordSettings.js index 66123881..98dc6a5b 100644 --- a/lib/settings/DiscordSettings.js +++ b/lib/settings/DiscordSettings.js @@ -49,6 +49,20 @@ class DiscordSettings { * @type {Boolean} */ this.useNickname = settings.useNickname; + + /** + * How to display replies from Telegram. Either `embed` or `inline` + * + * @type {String} + */ + this.displayTelegramReplies = settings.displayTelegramReplies; + + /** + * How much of the original message to show in replies from Telegram + * + * @type {Integer} + */ + this.replyLength = settings.replyLength; } /** @@ -105,9 +119,20 @@ class DiscordSettings { throw new Error("`settings.skipOldMessages` must be a boolean"); } + // Check that `useNickname` is a boolean if (Boolean(settings.useNickname) !== settings.useNickname) { throw new Error("`settings.useNickname` must be a boolean"); } + + // Check that `displayTelegramReplies` is an acceptable string + if (!["embed", "inline"].includes(settings.displayTelegramReplies)) { + throw new Error("`settings.displayTelegramReplies` must be either \"embed\" or \"inline\""); + } + + // Check that `replyLength` is an integer + if (!Number.isInteger(settings.replyLength) || settings.replyLength <= 0) { + throw new ("`settings.replyLength` must be an integer greater than 0"); + } } /** diff --git a/lib/settings/Settings.js b/lib/settings/Settings.js index ddb00662..a74f1c65 100644 --- a/lib/settings/Settings.js +++ b/lib/settings/Settings.js @@ -157,6 +157,16 @@ class Settings { } } + // 2019-04-21: Add the `displayTelegramReplies` option to Discord + if (R.isNil(settings.discord.displayTelegramReplies)) { + settings.discord.displayTelegramReplies = "embed"; + } + + // 2019-04-21: Add the `replyLength` option to Discord + if (R.isNil(settings.discord.replyLength)) { + settings.discord.replyLength = 100; + } + // All done! return settings; } diff --git a/lib/telegram2discord/handleEntities.js b/lib/telegram2discord/handleEntities.js index 7eb50add..0c6f3e9a 100644 --- a/lib/telegram2discord/handleEntities.js +++ b/lib/telegram2discord/handleEntities.js @@ -10,6 +10,7 @@ const R = require("ramda"); * Make some helpers * *********************/ +// XXX This is also present in `messageConverter`. Merge somehow const findFn = (prop, regexp) => R.compose( R.not, diff --git a/lib/telegram2discord/messageConverter.js b/lib/telegram2discord/messageConverter.js index 9e339c9d..25a27231 100644 --- a/lib/telegram2discord/messageConverter.js +++ b/lib/telegram2discord/messageConverter.js @@ -10,6 +10,15 @@ const R = require("ramda"); /*********** * Helpers * ***********/ + +// XXX This is also present in `messageConverter`. Merge somehow +const findFn = (prop, regexp) => + R.compose( + R.not, + R.isEmpty, + R.match(regexp), + R.prop(prop) + ); /** * Gets the display name of a user @@ -93,8 +102,8 @@ function messageConverter(message, tgBot, settings, dcBot, bridge) { // Is this a reply to the bot, i.e. to a Discord user? if (message.reply_to_message.from !== undefined && message.reply_to_message.from.id === tgBot.me.id) { // Get the name of the Discord user this is a reply to - const dcUsername = message.reply_to_message.text.split("\n")[0]; - const dcUser = dcBot.channels.get(bridge.discord.channelId).members.find("displayName", dcUsername); + const dcUsername = new RegExp(message.reply_to_message.text.split("\n")[0]); + const dcUser = dcBot.channels.get(bridge.discord.channelId).members.find(findFn("displayName", dcUsername)); originalAuthor = !R.isNil(dcUser) ? `<@${dcUser.id}>` : dcUsername; } @@ -108,9 +117,9 @@ function messageConverter(message, tgBot, settings, dcBot, bridge) { originalText = originalText.join("\n"); } - // Take only the first 100 characters, or up to second newline - originalText = originalText.length > 100 - ? originalText.slice(0, 100) + "…" + // Take only the first few characters, or up to second newline + originalText = originalText.length > settings.discord.replyLength + ? originalText.slice(0, settings.discord.replyLength) + "…" : originalText ; const newlineIndices = [...originalText].reduce((indices, c, i) => { diff --git a/lib/telegram2discord/setup.js b/lib/telegram2discord/setup.js index 885e3212..fdee1ca6 100644 --- a/lib/telegram2discord/setup.js +++ b/lib/telegram2discord/setup.js @@ -190,26 +190,33 @@ function setup(logger, tgBot, dcBot, messageMap, bridgeMap, settings) { // Get the channel to send to const channel = dcBot.channels.get(bridge.discord.channelId); + // Make the header + let header = bridge.telegram.sendUsernames ? `**${messageObj.from}**` : ""; + // Handle replies if (messageObj.reply !== null) { - // Make a Discord embed and send it first - const embed = new Discord.RichEmbed({ - description: messageObj.reply.text - }); - - const textToSend = bridge.telegram.sendUsernames - ? `**${messageObj.from}** (In reply to **${messageObj.reply.author}**)` - : undefined - ; - - await channel.send(textToSend, {embed}); + // Add the reply data to the header + header = header + ` (In reply to **${messageObj.reply.author}**)`; + + // Figure out how to display the reply in Discord + if (settings.discord.displayTelegramReplies === "embed") { + // Make a Discord embed and send it first + const embed = new Discord.RichEmbed({ + description: messageObj.reply.text + }); + + await channel.send(header, {embed}); + + // Clear the header + header = ""; + } else if (settings.discord.displayTelegramReplies === "inline") { + // Just modify the header + header = `${header.slice(0, -1)}: _${messageObj.reply.text}_)`; + } } // Discord doesn't handle messages longer than 2000 characters. Split it up into chunks that big - const messageText = (!bridge.telegram.sendUsernames || messageObj.reply !== null) - ? messageObj.text - : `**${messageObj.from}**\n${messageObj.text}` - ; + const messageText = header + "\n" + messageObj.text; const chunks = R.splitEvery(2000, messageText); // Send them in serial From e7a5aad29f6c6d8b0f55ae1522841906e445884c Mon Sep 17 00:00:00 2001 From: Simen de Lange Date: Sun, 21 Apr 2019 15:11:14 +0200 Subject: [PATCH 028/102] Documented the new settings --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 1a25927b..37015b11 100644 --- a/README.md +++ b/README.md @@ -73,6 +73,8 @@ As mentioned in the step by step installation guide, there is a settings file. H * `discord.relayJoinMessages`: Whether or not to relay messages to Telegram about people joining the Discord chat * `discord.relayLeaveMessages`: Whether or not to relay messages to Telegram about people leaving the Discord chat * `discord.sendUsernames`: Whether or not to send the sender's name with the messages to Telegram + * `discord.displayTelegramReplies`: How to display Telegram replies. Either the string `inline` or `embed` + * `discord.replyLength`: How many characters of the original message to display on replies The available settings will occasionally change. The bot takes care of this automatically From 9e91774b14ce899cbc2bef0e810a073d7593f33b Mon Sep 17 00:00:00 2001 From: Simen de Lange Date: Sun, 21 Apr 2019 15:35:24 +0200 Subject: [PATCH 029/102] Added the new settings to the example settings file --- example.settings.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/example.settings.yaml b/example.settings.yaml index 9db8eb34..02474bcb 100644 --- a/example.settings.yaml +++ b/example.settings.yaml @@ -8,6 +8,8 @@ discord: useNickname: false token: DISCORD_BOT_TOKEN_HERE # Discord bot tokens look like this: MjI3MDA1NzIvOBQ2MzAzMiMz.DRf-aw.N0MVYtDxXYPSQew4g2TPqvQve2c skipOldMessages: true + displayTelegramReplies: embed + replyLength: 100 bridges: - name: Default bridge direction: both From 89aa6dc65ed72e7874fdc8a2a298f281bee15c42 Mon Sep 17 00:00:00 2001 From: Simen de Lange Date: Sun, 21 Apr 2019 15:46:04 +0200 Subject: [PATCH 030/102] 0.8.4 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index c9bb68e6..7f411e10 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "tedicross", - "version": "0.8.3", + "version": "0.8.4", "description": "Better DiteCross", "license": "MIT", "repository": { From 9ac6eb56c587dbb6f771bca00f2afd52de51844c Mon Sep 17 00:00:00 2001 From: Simen de Lange Date: Sun, 21 Apr 2019 16:00:08 +0200 Subject: [PATCH 031/102] Updated vulnerable dependency --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 7f411e10..bcd1eb2e 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,7 @@ "moment": "^2.23.0", "ramda": "^0.25.0", "request": "^2.88.0", - "simple-markdown": "^0.3.3", + "simple-markdown": "^0.4.4", "telegraf": "^3.26.0", "yargs": "^13.2.2" }, From 65d10cd2ef9ebcb3528f114b2decc6f4d5268d4e Mon Sep 17 00:00:00 2001 From: Simen de Lange Date: Sun, 21 Apr 2019 16:00:42 +0200 Subject: [PATCH 032/102] 0.8.5 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index bcd1eb2e..d26e7944 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "tedicross", - "version": "0.8.4", + "version": "0.8.5", "description": "Better DiteCross", "license": "MIT", "repository": { From 37615c090854ee2bff6fd6ceaa677208876320e0 Mon Sep 17 00:00:00 2001 From: Simen de Lange Date: Sun, 21 Apr 2019 16:42:03 +0200 Subject: [PATCH 033/102] Moved the documentation of the new settings to its proper place --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 37015b11..e1f816a2 100644 --- a/README.md +++ b/README.md @@ -60,6 +60,8 @@ As mentioned in the step by step installation guide, there is a settings file. H * `token`: The Discord bot's token. It is needed for the bot to authenticate to the Discord servers and be able to send and receive messages. If set to `"env"`, TediCross will read the token from the environment variable `DISCORD_BOT_TOKEN` * `skipOldMessages`: Whether or not to skip through all previous messages sent since the bot was last turned off and start processing new messages ONLY. Defaults to true. Note that there is no guarantee the old messages will arrive at Telegram in order. **NOTE:** [Telegram has a limit](https://core.telegram.org/bots/faq#my-bot-is-hitting-limits-how-do-i-avoid-this) on how quickly a bot can send messages. If there is a big backlog, this will cause problems * `useNickname`: Uses the sending user's nickname instead of username when relaying messages to Telegram + * `displayTelegramReplies`: How to display Telegram replies. Either the string `inline` or `embed` + * `replyLength`: How many characters of the original message to display on replies * `debug`: If set to `true`, activates debugging output from the bot. Defaults to `false` * `bridges`: An array containing all your chats and channels. For each object in this array, you should have the following properties: * `name`: A internal name of the chat. Appears in the log @@ -73,8 +75,6 @@ As mentioned in the step by step installation guide, there is a settings file. H * `discord.relayJoinMessages`: Whether or not to relay messages to Telegram about people joining the Discord chat * `discord.relayLeaveMessages`: Whether or not to relay messages to Telegram about people leaving the Discord chat * `discord.sendUsernames`: Whether or not to send the sender's name with the messages to Telegram - * `discord.displayTelegramReplies`: How to display Telegram replies. Either the string `inline` or `embed` - * `discord.replyLength`: How many characters of the original message to display on replies The available settings will occasionally change. The bot takes care of this automatically From ae6c94d830bd7a616ee4ca78d39c2e105e27df67 Mon Sep 17 00:00:00 2001 From: Simen de Lange Date: Mon, 22 Apr 2019 12:57:47 +0200 Subject: [PATCH 034/102] Option to ignore commands on Telegram --- lib/settings/Settings.js | 5 +++++ lib/settings/TelegramSettings.js | 12 ++++++++++++ lib/telegram2discord/setup.js | 7 +++++-- 3 files changed, 22 insertions(+), 2 deletions(-) diff --git a/lib/settings/Settings.js b/lib/settings/Settings.js index a74f1c65..debaf95d 100644 --- a/lib/settings/Settings.js +++ b/lib/settings/Settings.js @@ -167,6 +167,11 @@ class Settings { settings.discord.replyLength = 100; } + // 2019-04-22: Add the `ignoreCommands` option to Telegram + if (R.isNil(settings.telegram.ignoreCommands)) { + settings.telegram.ignoreCommands = false; + } + // All done! return settings; } diff --git a/lib/settings/TelegramSettings.js b/lib/settings/TelegramSettings.js index 03a48b85..3a027960 100644 --- a/lib/settings/TelegramSettings.js +++ b/lib/settings/TelegramSettings.js @@ -66,6 +66,13 @@ class TelegramSettings { * @type {Boolean} */ this.sendEmojiWithStickers = settings.sendEmojiWithStickers; + + /** + * Whether or not to ignore messages starting with "/" (commands) + * + * @type {Boolean} + */ + this.ignoreCommands = settings.ignoreCommands; } /** @@ -136,6 +143,11 @@ class TelegramSettings { if (Boolean(settings.sendEmojiWithStickers) !== settings.sendEmojiWithStickers) { throw new Error("`settings.sendEmojiWithStickers` must be a boolean"); } + + // Check that ignoreCommands is a boolean + if (Boolean(settings.ignoreCommands) !== settings.ignoreCommands) { + throw new Error("`settings.ignoreCommands` must be a boolean"); + } } /** diff --git a/lib/telegram2discord/setup.js b/lib/telegram2discord/setup.js index fdee1ca6..45adec7e 100644 --- a/lib/telegram2discord/setup.js +++ b/lib/telegram2discord/setup.js @@ -106,7 +106,7 @@ function makeNameObject(user) { * * @private */ -const createMessageHandler = R.curry((logger, tgBot, bridgeMap, func, ctx) => { +const createMessageHandler = R.curry((logger, tgBot, bridgeMap, settings, func, ctx) => { // Channel posts are not on ctx.message, but are very similar. Use the proper one const message = R.cond([ // XXX I tried both R.has and R.hasIn as conditions. Neither worked for some reason @@ -124,6 +124,9 @@ const createMessageHandler = R.curry((logger, tgBot, bridgeMap, func, ctx) => { "chatID: " + message.chat.id ); + return; + } else if (message.text.startsWith("/") && settings.telegram.ignoreCommands) { + // This is a command, and commands should be ignored return; } @@ -175,7 +178,7 @@ function setup(logger, tgBot, dcBot, messageMap, bridgeMap, settings) { const sendFile = makeFileSender(tgBot, dcBot, messageMap, settings); // Create the message handler wrapper - const wrapFunction = createMessageHandler(logger, tgBot, bridgeMap); + const wrapFunction = createMessageHandler(logger, tgBot, bridgeMap, settings); // Set up event listener for text messages from Telegram tgBot.on("text", wrapFunction(async (message, bridge) => { From f53c0ba6307efc980e0e5d7755f9cd11510c0a6c Mon Sep 17 00:00:00 2001 From: Simen de Lange Date: Mon, 22 Apr 2019 13:05:03 +0200 Subject: [PATCH 035/102] Moved the `ignoreCommands` setting to per-bridge --- README.md | 1 + lib/bridgestuff/BridgeSettingsTelegram.js | 12 ++++++++++++ lib/settings/Settings.js | 6 ++++-- lib/settings/TelegramSettings.js | 12 ------------ lib/telegram2discord/setup.js | 8 +++++--- 5 files changed, 22 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index 37015b11..a25b742a 100644 --- a/README.md +++ b/README.md @@ -68,6 +68,7 @@ As mentioned in the step by step installation guide, there is a settings file. H * `telegram.relayJoinMessages`: Whether or not to relay messages to Discord about people joining the Telegram chat * `telegram.relayLeaveMessages`: Whether or not to relay messages to Discord about people leaving the Telegram chat * `telegram.sendUsernames`: Whether or not to send the sender's name with the messages to Discord + * `telegram.ignoreCommands`: If set to `true`, messages starting with a `/` are not relayed to Discord * `discord.guild`: ID of the server the Discord end of the bridge is in. If a message to the bot originates from within this server, but not the correct channel, it is ignored, instead of triggering a reply telling the sender to get their own bot. See step 11 on how to aquire it * `discord.channel`: ID of the channel the Discord end of the bridge is in. See step 11 on how to aquire it * `discord.relayJoinMessages`: Whether or not to relay messages to Telegram about people joining the Discord chat diff --git a/lib/bridgestuff/BridgeSettingsTelegram.js b/lib/bridgestuff/BridgeSettingsTelegram.js index aaf613ec..5746efd6 100644 --- a/lib/bridgestuff/BridgeSettingsTelegram.js +++ b/lib/bridgestuff/BridgeSettingsTelegram.js @@ -53,6 +53,13 @@ class BridgeSettingsTelegram { * @type {Boolean} */ this.sendUsernames = settings.sendUsernames; + + /** + * Whether or not to ignore messages starting with "/" (commands) + * + * @type {Boolean} + */ + this.ignoreCommands = settings.ignoreCommands; } /** @@ -82,6 +89,11 @@ class BridgeSettingsTelegram { if (Boolean(settings.sendUsernames) !== settings.sendUsernames) { throw new Error("`settings.sendUsernames` must be a boolean"); } + + // Check that ignoreCommands is a boolean + if (Boolean(settings.ignoreCommands) !== settings.ignoreCommands) { + throw new Error("`settings.ignoreCommands` must be a boolean"); + } } } diff --git a/lib/settings/Settings.js b/lib/settings/Settings.js index debaf95d..00c10493 100644 --- a/lib/settings/Settings.js +++ b/lib/settings/Settings.js @@ -168,8 +168,10 @@ class Settings { } // 2019-04-22: Add the `ignoreCommands` option to Telegram - if (R.isNil(settings.telegram.ignoreCommands)) { - settings.telegram.ignoreCommands = false; + for (const bridge of settings.bridges) { + if (R.isNil(bridge.telegram.ignoreCommands)) { + bridge.telegram.ignoreCommands = false; + } } // All done! diff --git a/lib/settings/TelegramSettings.js b/lib/settings/TelegramSettings.js index 3a027960..03a48b85 100644 --- a/lib/settings/TelegramSettings.js +++ b/lib/settings/TelegramSettings.js @@ -66,13 +66,6 @@ class TelegramSettings { * @type {Boolean} */ this.sendEmojiWithStickers = settings.sendEmojiWithStickers; - - /** - * Whether or not to ignore messages starting with "/" (commands) - * - * @type {Boolean} - */ - this.ignoreCommands = settings.ignoreCommands; } /** @@ -143,11 +136,6 @@ class TelegramSettings { if (Boolean(settings.sendEmojiWithStickers) !== settings.sendEmojiWithStickers) { throw new Error("`settings.sendEmojiWithStickers` must be a boolean"); } - - // Check that ignoreCommands is a boolean - if (Boolean(settings.ignoreCommands) !== settings.ignoreCommands) { - throw new Error("`settings.ignoreCommands` must be a boolean"); - } } /** diff --git a/lib/telegram2discord/setup.js b/lib/telegram2discord/setup.js index 45adec7e..ea34481c 100644 --- a/lib/telegram2discord/setup.js +++ b/lib/telegram2discord/setup.js @@ -124,9 +124,6 @@ const createMessageHandler = R.curry((logger, tgBot, bridgeMap, settings, func, "chatID: " + message.chat.id ); - return; - } else if (message.text.startsWith("/") && settings.telegram.ignoreCommands) { - // This is a command, and commands should be ignored return; } @@ -151,6 +148,11 @@ const createMessageHandler = R.curry((logger, tgBot, bridgeMap, settings, func, }); } else { bridges.forEach((bridge) => { + // Check if it is a command, and if commands should be ignored + if (message.text.startsWith("/") && bridge.telegram.ignoreCommands) { + return; + } + // Do the thing, if this is not a discord-to-telegram bridge if (bridge.direction !== Bridge.DIRECTION_DISCORD_TO_TELEGRAM) { func(message, bridge); From 5c3f10364d1c6774593b3fd09533bd41862557e1 Mon Sep 17 00:00:00 2001 From: Simen de Lange Date: Mon, 22 Apr 2019 13:05:48 +0200 Subject: [PATCH 036/102] 0.8.6 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index d26e7944..f5c78646 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "tedicross", - "version": "0.8.5", + "version": "0.8.6", "description": "Better DiteCross", "license": "MIT", "repository": { From 868463964edd7570f7a87429e1476b32ed71bab3 Mon Sep 17 00:00:00 2001 From: Simen de Lange Date: Mon, 22 Apr 2019 17:31:41 +0200 Subject: [PATCH 037/102] Fixed bug which caused T2D to crash on non-text messages --- lib/telegram2discord/messageConverter.js | 2 +- lib/telegram2discord/setup.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/telegram2discord/messageConverter.js b/lib/telegram2discord/messageConverter.js index 25a27231..0c493f29 100644 --- a/lib/telegram2discord/messageConverter.js +++ b/lib/telegram2discord/messageConverter.js @@ -11,7 +11,7 @@ const R = require("ramda"); * Helpers * ***********/ -// XXX This is also present in `messageConverter`. Merge somehow +// XXX This is also present in `handleEntities`. Merge somehow const findFn = (prop, regexp) => R.compose( R.not, diff --git a/lib/telegram2discord/setup.js b/lib/telegram2discord/setup.js index ea34481c..5aa50fdb 100644 --- a/lib/telegram2discord/setup.js +++ b/lib/telegram2discord/setup.js @@ -149,7 +149,7 @@ const createMessageHandler = R.curry((logger, tgBot, bridgeMap, settings, func, } else { bridges.forEach((bridge) => { // Check if it is a command, and if commands should be ignored - if (message.text.startsWith("/") && bridge.telegram.ignoreCommands) { + if (!R.isNil(message.text) && message.text.startsWith("/") && bridge.telegram.ignoreCommands) { return; } From 4d081346ae60d650216bc0c0e12eebb357c42314 Mon Sep 17 00:00:00 2001 From: Simen de Lange Date: Mon, 22 Apr 2019 17:36:09 +0200 Subject: [PATCH 038/102] Fixed rogue `/`s appearing on usernames in replies --- lib/telegram2discord/messageConverter.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/telegram2discord/messageConverter.js b/lib/telegram2discord/messageConverter.js index 0c493f29..04b45935 100644 --- a/lib/telegram2discord/messageConverter.js +++ b/lib/telegram2discord/messageConverter.js @@ -102,8 +102,8 @@ function messageConverter(message, tgBot, settings, dcBot, bridge) { // Is this a reply to the bot, i.e. to a Discord user? if (message.reply_to_message.from !== undefined && message.reply_to_message.from.id === tgBot.me.id) { // Get the name of the Discord user this is a reply to - const dcUsername = new RegExp(message.reply_to_message.text.split("\n")[0]); - const dcUser = dcBot.channels.get(bridge.discord.channelId).members.find(findFn("displayName", dcUsername)); + const dcUsername = message.reply_to_message.text.split("\n")[0]; + const dcUser = dcBot.channels.get(bridge.discord.channelId).members.find(findFn("displayName", new RegExp(dcUsername))); originalAuthor = !R.isNil(dcUser) ? `<@${dcUser.id}>` : dcUsername; } From 104ee0f194b66051e0cc9d1f2c9424e059594a45 Mon Sep 17 00:00:00 2001 From: Simen de Lange Date: Mon, 22 Apr 2019 17:36:33 +0200 Subject: [PATCH 039/102] 0.8.7 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index f5c78646..50e906d1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "tedicross", - "version": "0.8.6", + "version": "0.8.7", "description": "Better DiteCross", "license": "MIT", "repository": { From ab8283382802d6d1ceefd0a0c4f0f383de3165df Mon Sep 17 00:00:00 2001 From: Simen de Lange Date: Mon, 22 Apr 2019 21:28:29 +0200 Subject: [PATCH 040/102] Channel matching is now case sensitive --- lib/telegram2discord/handleEntities.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/telegram2discord/handleEntities.js b/lib/telegram2discord/handleEntities.js index 0c6f3e9a..a920540a 100644 --- a/lib/telegram2discord/handleEntities.js +++ b/lib/telegram2discord/handleEntities.js @@ -107,7 +107,7 @@ function handleEntities(text, entities, dcBot, bridge) { } case "hashtag": { // Possible name of a Discord channel on the same Discord server - const channelName = new RegExp(`^${part.substring(1).toLowerCase()}$`); + const channelName = new RegExp(`^${part.substring(1)}$`); // Find out if this is a channel on the bridged Discord server const channel = dcBot.guilds.get(bridge.discord.serverId).channels.find(findFn("name", channelName)); From ef49dade03dd7b5fe48bb377e8337cb895ad2e9e Mon Sep 17 00:00:00 2001 From: teejo75 <437200+teejo75@users.noreply.github.com> Date: Wed, 24 Apr 2019 21:22:05 +1000 Subject: [PATCH 041/102] Correct the case of the chatinfo message. I copied and pasted the chatID as output in telegram. This did not work, as it is expecting chatId in the settings.yaml. --- lib/telegram2discord/setup.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/telegram2discord/setup.js b/lib/telegram2discord/setup.js index 5aa50fdb..a87a4287 100644 --- a/lib/telegram2discord/setup.js +++ b/lib/telegram2discord/setup.js @@ -121,7 +121,7 @@ const createMessageHandler = R.curry((logger, tgBot, bridgeMap, settings, func, // This is a request for chat info. Give it, no matter which chat this is from tgBot.telegram.sendMessage( message.chat.id, - "chatID: " + message.chat.id + "chatId: " + message.chat.id ); return; From 451ee96e76dc7cca37a8220860c941b0b6c700fa Mon Sep 17 00:00:00 2001 From: Thomas Rory Gummerson Date: Wed, 15 May 2019 09:15:54 +0200 Subject: [PATCH 042/102] Fix sticker thumbnails being sent instead of sticker images --- lib/telegram2discord/setup.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/telegram2discord/setup.js b/lib/telegram2discord/setup.js index a87a4287..eb88dba9 100644 --- a/lib/telegram2discord/setup.js +++ b/lib/telegram2discord/setup.js @@ -257,7 +257,7 @@ function setup(logger, tgBot, dcBot, messageMap, bridgeMap, settings) { try { await sendFile(bridge, { message, - fileId: message.sticker.thumb.file_id, + fileId: message.sticker.file_id, fileName: "sticker.webp", // Telegram will insist that it is a jpg, but it really is a webp caption: settings.telegram.sendEmojiWithStickers ? message.sticker.emoji : undefined }); From 2f3ac40e45cd20831bfbb97c95af2e5fd0911c9c Mon Sep 17 00:00:00 2001 From: Thomas Rory Gummerson Date: Wed, 15 May 2019 09:16:10 +0200 Subject: [PATCH 043/102] 0.8.8 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 50e906d1..22c904d9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "tedicross", - "version": "0.8.7", + "version": "0.8.8", "description": "Better DiteCross", "license": "MIT", "repository": { From 5f5075cee0f6054091d3982e1b1e6e2e6fd3d423 Mon Sep 17 00:00:00 2001 From: Simen de Lange Date: Sun, 26 May 2019 12:08:10 +0200 Subject: [PATCH 044/102] Removed symlink from docker. Replaced by command line parameter --- Dockerfile | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/Dockerfile b/Dockerfile index 6dcb7a7c..9b6e7d80 100644 --- a/Dockerfile +++ b/Dockerfile @@ -6,10 +6,6 @@ ADD . . RUN npm install --production -# Hack to make the settings file work from the data/ directory -# Remove this line if you build with the settings file integrated -RUN ln -s data/settings.yaml settings.yaml - VOLUME /opt/TediCross/data/ -ENTRYPOINT /usr/local/bin/npm start +ENTRYPOINT /usr/local/bin/npm start -- -c data/settings.yaml From 673cb80b442d4a9181e749bb45aad1006d80506c Mon Sep 17 00:00:00 2001 From: Simen de Lange Date: Sun, 26 May 2019 12:29:04 +0200 Subject: [PATCH 045/102] Updated dependencies --- package.json | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/package.json b/package.json index 50e906d1..c66eaeb2 100644 --- a/package.json +++ b/package.json @@ -12,18 +12,18 @@ "lint": "eslint ." }, "dependencies": { - "discord.js": "^11.4.2", - "js-yaml": "^3.12.1", - "mime": "^2.4.0", - "moment": "^2.23.0", - "ramda": "^0.25.0", + "discord.js": "^11.5.0", + "js-yaml": "^3.13.1", + "mime": "^2.4.2", + "moment": "^2.24.0", + "ramda": "^0.26.1", "request": "^2.88.0", "simple-markdown": "^0.4.4", - "telegraf": "^3.26.0", - "yargs": "^13.2.2" + "telegraf": "^3.29.0", + "yargs": "^13.2.4" }, "devDependencies": { - "eslint": "^4.19.1" + "eslint": "^5.16.0" }, "eslintConfig": { "parserOptions": { From d4a856071840ec57eee551ddbc633922921853a3 Mon Sep 17 00:00:00 2001 From: Simen de Lange Date: Sun, 26 May 2019 13:26:18 +0200 Subject: [PATCH 046/102] Started using middlewares on telegram bot --- lib/telegram2discord/middlewares.js | 121 ++++++++++++++++++++++++++++ lib/telegram2discord/setup.js | 51 ++++-------- 2 files changed, 136 insertions(+), 36 deletions(-) create mode 100644 lib/telegram2discord/middlewares.js diff --git a/lib/telegram2discord/middlewares.js b/lib/telegram2discord/middlewares.js new file mode 100644 index 00000000..cce2a387 --- /dev/null +++ b/lib/telegram2discord/middlewares.js @@ -0,0 +1,121 @@ +"use strict"; + +/************************** + * Import important stuff * + **************************/ + +const R = require("ramda"); + +/**************************** + * The middleware functions * + ****************************/ + +/** + * Adds a `tediCross` property to the context + * + * @param {Object} context The context to add the property to + * @param {Function} next Function to pass control to next middleware + * + * @returns {undefined} + */ +function addTediCrossObj(ctx, next) { + ctx.tediCross = {}; + next(); +} + +/** + * Replies to a message with info about the chat. One of the four optional arguments must be present + * + * @param {Object} ctx The Telegraf context + * @param {Object} ctx.tediCross The TediCross object on the context + * @param {Object} [ctx.channelPost] + * @param {Object} [ctx.editedChannelPost] + * @param {Object} [ctx.message] + * @param {Object} [ctx.editedChannelPost] + * @param {Function} next Function to pass control to next middleware + * + * @returns {undefined} + */ +function getMessageObj(ctx, next) { + // Get the proper message object + const message = R.cond([ + // XXX I tried both R.has and R.hasIn as conditions. Neither worked for some reason + [ctx => !R.isNil(ctx.channelPost), R.prop("channelPost")], + [ctx => !R.isNil(ctx.editedChannelPost), R.prop("editedChannelPost")], + [ctx => !R.isNil(ctx.message), R.prop("message")], + [ctx => !R.isNil(ctx.editedMessage), R.prop("editedMessage")] + ])(ctx); + + // Put it on the context + ctx.tediCross.message = message; + + next(); +} + +/** + * Replies to a message with info about the chat + * + * @param {Object} ctx The Telegraf context + * @param {Object} ctx.tediCross The TediCross object on the context + * @param {Object} ctx.tediCross.message The message to reply to + * @param {Object} ctx.tediCross.message.chat The object of the chat the message is from + * @param {Integer} ctx.tediCross.message.chat.id ID of the chat the message is from + * + * @returns {undefined} + */ +function chatinfo(ctx) { + ctx.reply(`chatID: ${ctx.tediCross.message.chat.id}`); +} + +/** + * Adds the bridges to the tediCross object on the context + * + * @param {Object} context The context to add the property to + * @param {Function} next Function to pass control to next middleware + * + * @returns {undefined} + */ +function addBridgesToContext(ctx, next) { + ctx.tediCross.bridges = ctx.bridgeMap.fromTelegramChatId(ctx.tediCross.message.chat.id); + next(); +} + +/** + * Replies to the message telling the user this is a private bot + * + * @param {Object} ctx The Telegraf context + * @param {Function} ctx.reply The context's reply function + * @param {Function} next Function to pass control to next middleware + * + * @returns {undefined} + */ +function informThisIsPrivateBot(ctx, next) { + R.ifElse( + R.compose( + R.isNil, + R.path(["tediCross", "bridges"]) + ), + ctx => + 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.", + { + parse_mode: "markdown" + } + ), + next + )(ctx); +} + +/*************** + * Export them * + ***************/ + +module.exports = { + addTediCrossObj, + getMessageObj, + chatinfo, + addBridgesToContext, + informThisIsPrivateBot +}; diff --git a/lib/telegram2discord/setup.js b/lib/telegram2discord/setup.js index 5aa50fdb..c23efb56 100644 --- a/lib/telegram2discord/setup.js +++ b/lib/telegram2discord/setup.js @@ -11,6 +11,7 @@ const mime = require("mime/lite"); const Bridge = require("../bridgestuff/Bridge"); const Discord = require("discord.js"); const request = require("request"); +const middlewares = require("./middlewares"); /** * Creates a function which sends files from Telegram to discord @@ -108,45 +109,13 @@ function makeNameObject(user) { */ const createMessageHandler = R.curry((logger, tgBot, bridgeMap, settings, func, ctx) => { // Channel posts are not on ctx.message, but are very similar. Use the proper one - const message = R.cond([ - // XXX I tried both R.has and R.hasIn as conditions. Neither worked for some reason - [ctx => !R.isNil(ctx.channelPost), R.prop("channelPost")], - [ctx => !R.isNil(ctx.editedChannelPost), R.prop("editedChannelPost")], - [ctx => !R.isNil(ctx.message), R.prop("message")], - [ctx => !R.isNil(ctx.editedMessage), R.prop("editedMessage")] - ])(ctx); - - if (message.text === "/chatinfo") { - // TODO Make middleware - // This is a request for chat info. Give it, no matter which chat this is from - tgBot.telegram.sendMessage( - message.chat.id, - "chatID: " + message.chat.id - ); - - return; - } + const message = ctx.tediCross.message; - // Get the bridge - const bridges = bridgeMap.fromTelegramChatId(message.chat.id); + // Get the bridges + const bridges = ctx.tediCross.bridges; // Check if the message came from the correct chat - if (bridges === undefined) { - // Tell the sender that this is a private bot - tgBot.telegram.sendMessage( - message.chat.id, - "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.", - { - parse_mode: "markdown" - } - ) - .catch((err) => { - // Hmm... Could not send the message for some reason - logger.error("Could not tell user to get their own TediCross instance:", err, message); - }); - } else { + if (bridges !== undefined) { bridges.forEach((bridge) => { // Check if it is a command, and if commands should be ignored if (!R.isNil(message.text) && message.text.startsWith("/") && bridge.telegram.ignoreCommands) { @@ -176,6 +145,16 @@ const createMessageHandler = R.curry((logger, tgBot, bridgeMap, settings, func, * @param {Settings} settings The settings to use */ function setup(logger, tgBot, dcBot, messageMap, bridgeMap, settings) { + // Put the bridges on the global tgBot context + tgBot.context.bridgeMap = bridgeMap; + + // Apply middlewares + tgBot.use(middlewares.addTediCrossObj); + tgBot.use(middlewares.getMessageObj); + tgBot.command("chatinfo", middlewares.chatinfo); + tgBot.use(middlewares.addBridgesToContext); + tgBot.use(middlewares.informThisIsPrivateBot); + // Make the file sender const sendFile = makeFileSender(tgBot, dcBot, messageMap, settings); From 93a48cba28bb207381e6ac93b97803b544a9cee6 Mon Sep 17 00:00:00 2001 From: Simen de Lange Date: Sun, 26 May 2019 14:40:59 +0200 Subject: [PATCH 047/102] Simplified bridgegetting --- lib/bridgestuff/BridgeMap.js | 7 +++---- lib/discord2telegram/setup.js | 23 +++++++---------------- lib/telegram2discord/middlewares.js | 11 +++++++---- lib/telegram2discord/setup.js | 27 +++++++++++---------------- 4 files changed, 28 insertions(+), 40 deletions(-) diff --git a/lib/bridgestuff/BridgeMap.js b/lib/bridgestuff/BridgeMap.js index 93959227..191396b3 100644 --- a/lib/bridgestuff/BridgeMap.js +++ b/lib/bridgestuff/BridgeMap.js @@ -4,8 +4,7 @@ * Import important stuff * **************************/ -// eslint-disable-next-line no-unused-vars -const Bridge = require("./Bridge"); +const R = require("ramda"); /*********************** * The BridgeMap class * @@ -73,7 +72,7 @@ class BridgeMap { * @returns {Bridge[]} The bridges corresponding to the chat ID */ fromTelegramChatId(telegramChatId) { - return this._telegramToBridge.get(telegramChatId); + return R.defaultTo([], this._telegramToBridge.get(telegramChatId)); } /** @@ -84,7 +83,7 @@ class BridgeMap { * @returns {Bridges[]} The bridges corresponding to the channel ID */ fromDiscordChannelId(discordChannelId) { - return this._discordToBridge.get(discordChannelId); + return R.defaultTo([], this._discordToBridge.get(discordChannelId)); } /** diff --git a/lib/discord2telegram/setup.js b/lib/discord2telegram/setup.js index 4c9e3bb3..61d37a85 100644 --- a/lib/discord2telegram/setup.js +++ b/lib/discord2telegram/setup.js @@ -12,6 +12,7 @@ const handleEmbed = require("./handleEmbed"); const relayOldMessages = require("./relayOldMessages"); const Bridge = require("../bridgestuff/Bridge"); const path = require("path"); +const R = require("ramda"); /*********** * Helpers * @@ -120,7 +121,7 @@ function setup(logger, dcBot, tgBot, messageMap, bridgeMap, settings, datadirPat // Check if the message is from the correct chat const bridges = bridgeMap.fromDiscordChannelId(message.channel.id); - if (bridges !== undefined) { + if (!R.isEmpty(bridges)) { bridges.forEach(async (bridge) => { // Ignore it if this is a telegram-to-discord bridge if (bridge.direction === Bridge.DIRECTION_TELEGRAM_TO_DISCORD) { @@ -203,7 +204,7 @@ function setup(logger, dcBot, tgBot, messageMap, bridgeMap, settings, datadirPat } } }); - } else if (message.channel.guild === undefined || !bridgeMap.knownDiscordServer(message.channel.guild.id)) { // Check if it is the correct server + } else if (R.isNil(message.channel.guild) || !bridgeMap.knownDiscordServer(message.channel.guild.id)) { // Check if it is the correct server // The message is from the wrong chat. Inform the sender that this is a private bot message.reply( "This is an instance of a TediCross bot, bridging a chat in Telegram with one in Discord. " @@ -220,13 +221,8 @@ function setup(logger, dcBot, tgBot, messageMap, bridgeMap, settings, datadirPat return; } - // Ignore it if it's not from a known bridge - const bridges = bridgeMap.fromDiscordChannelId(newMessage.channel.id); - if (bridges === undefined) { - return; - } - - bridges.forEach(async (bridge) => { + // Pass it on to the bridges + bridgeMap.fromDiscordChannelId(newMessage.channel.id).forEach(async (bridge) => { try { // Get the corresponding Telegram message ID const [tgMessageId] = messageMap.getCorresponding(MessageMap.DISCORD_TO_TELEGRAM, bridge, newMessage.id); @@ -259,16 +255,11 @@ function setup(logger, dcBot, tgBot, messageMap, bridgeMap, settings, datadirPat // Listen for deleted messages function onMessageDelete(message) { - // Ignore it if it's not from a known bridge - const bridges = bridgeMap.fromDiscordChannelId(message.channel.id); - if (bridges === undefined) { - return; - } - // Check if it is a relayed message const isFromTelegram = message.author.id === dcBot.user.id; - bridges.forEach(async (bridge) => { + // Hand it on to the bridges + bridgeMap.fromDiscordChannelId(message.channel.id).forEach(async (bridge) => { // Ignore it if cross deletion is disabled if (!bridge.discord.crossDeleteOnTelegram) { return; diff --git a/lib/telegram2discord/middlewares.js b/lib/telegram2discord/middlewares.js index cce2a387..40a74542 100644 --- a/lib/telegram2discord/middlewares.js +++ b/lib/telegram2discord/middlewares.js @@ -24,7 +24,7 @@ function addTediCrossObj(ctx, next) { } /** - * Replies to a message with info about the chat. One of the four optional arguments must be present + * Replies to a message with info about the chat. One of the four optional arguments must be present. Requires the tediCross context to work * * @param {Object} ctx The Telegraf context * @param {Object} ctx.tediCross The TediCross object on the context @@ -68,7 +68,7 @@ function chatinfo(ctx) { } /** - * Adds the bridges to the tediCross object on the context + * Adds the bridges to the tediCross object on the context. Requires the tediCross context to work * * @param {Object} context The context to add the property to * @param {Function} next Function to pass control to next middleware @@ -81,7 +81,7 @@ function addBridgesToContext(ctx, next) { } /** - * Replies to the message telling the user this is a private bot + * Replies to the message telling the user this is a private bot if there are no bridges on the tediCross context * * @param {Object} ctx The Telegraf context * @param {Function} ctx.reply The context's reply function @@ -91,10 +91,12 @@ function addBridgesToContext(ctx, next) { */ function informThisIsPrivateBot(ctx, next) { R.ifElse( + // If there are no bridges R.compose( - R.isNil, + R.isEmpty, R.path(["tediCross", "bridges"]) ), + // Inform the user ctx => ctx.reply( "This is an instance of a [TediCross](https://github.com/TediCross/TediCross) bot, " @@ -104,6 +106,7 @@ function informThisIsPrivateBot(ctx, next) { parse_mode: "markdown" } ), + // Otherwise go to next middleware next )(ctx); } diff --git a/lib/telegram2discord/setup.js b/lib/telegram2discord/setup.js index c23efb56..d466c990 100644 --- a/lib/telegram2discord/setup.js +++ b/lib/telegram2discord/setup.js @@ -108,26 +108,21 @@ function makeNameObject(user) { * @private */ const createMessageHandler = R.curry((logger, tgBot, bridgeMap, settings, func, ctx) => { - // Channel posts are not on ctx.message, but are very similar. Use the proper one + // Get the message object const message = ctx.tediCross.message; - // Get the bridges - const bridges = ctx.tediCross.bridges; - // Check if the message came from the correct chat - if (bridges !== undefined) { - bridges.forEach((bridge) => { - // Check if it is a command, and if commands should be ignored - if (!R.isNil(message.text) && message.text.startsWith("/") && bridge.telegram.ignoreCommands) { - return; - } + ctx.tediCross.bridges.forEach((bridge) => { + // Check if it is a command, and if commands should be ignored + if (!R.isNil(message.text) && message.text.startsWith("/") && bridge.telegram.ignoreCommands) { + return; + } - // Do the thing, if this is not a discord-to-telegram bridge - if (bridge.direction !== Bridge.DIRECTION_DISCORD_TO_TELEGRAM) { - func(message, bridge); - } - }); - } + // Do the thing, if this is not a discord-to-telegram bridge + if (bridge.direction !== Bridge.DIRECTION_DISCORD_TO_TELEGRAM) { + func(message, bridge); + } + }); }); /********************** From dc0d74b8cb842a9c2ea0f177d043ab874c279502 Mon Sep 17 00:00:00 2001 From: Simen de Lange Date: Sun, 26 May 2019 16:12:42 +0200 Subject: [PATCH 048/102] More middlewareification --- example.settings.yaml | 2 +- lib/telegram2discord/From.js | 66 ++++++++++++++ lib/telegram2discord/middlewares.js | 71 ++++++++++++++- lib/telegram2discord/processMessage.js | 114 ------------------------- lib/telegram2discord/setup.js | 70 +++++++++------ 5 files changed, 180 insertions(+), 143 deletions(-) create mode 100644 lib/telegram2discord/From.js delete mode 100644 lib/telegram2discord/processMessage.js diff --git a/example.settings.yaml b/example.settings.yaml index 02474bcb..038ae453 100644 --- a/example.settings.yaml +++ b/example.settings.yaml @@ -11,7 +11,7 @@ discord: displayTelegramReplies: embed replyLength: 100 bridges: - - name: Default bridge + - name: First bridge direction: both telegram: chatId: TELEGRAM_CHAT_ID # Remember that Telegram group and channel IDs are negative. Include the `-` if you are bridging a group or a channel diff --git a/lib/telegram2discord/From.js b/lib/telegram2discord/From.js new file mode 100644 index 00000000..ee309730 --- /dev/null +++ b/lib/telegram2discord/From.js @@ -0,0 +1,66 @@ +"use strict"; + +/************************** + * Import important stuff * + **************************/ + +const R = require("ramda"); + +/********************** + * The From functions * + **********************/ + +/** + * Information about the sender of a Telegram message + * + * @typedef {Object} From + * @prop {String} firstName First name of the sender + * @prop {String} lastName Last name of the sender + * @param {String} username Username of the sender + */ + +/** + * Creates a new From object + * + * @param {String} firstName First name of the sender + * @param {String} [lastName] Last name of the sender + * @param {String} [username] Username of the sender + * + * @returns {From} The From object + * + * @memberof From + */ +function createFromObj(firstName, lastName, username) { + return { + firstName, + lastName, + username + }; +} + +/** + * Makes a display name out of a from object + * + * @param {Boolean} useFirstNameInsteadOfUsername Whether or not to always use the first name instead of the username + * @param {From} from The from object + * + * @returns {String} The display name + * + * @memberof From + */ +function makeDisplayName(useFirstNameInsteadOfUsername, from) { + return R.ifElse( + from => useFirstNameInsteadOfUsername || R.isNil(from.username), + R.prop("firstName"), + R.prop("username") + )(from); +} + +/*************** + * Export them * + ***************/ + +module.exports = { + createFromObj, + makeDisplayName +}; diff --git a/lib/telegram2discord/middlewares.js b/lib/telegram2discord/middlewares.js index 40a74542..6ead9ab0 100644 --- a/lib/telegram2discord/middlewares.js +++ b/lib/telegram2discord/middlewares.js @@ -5,6 +5,8 @@ **************************/ const R = require("ramda"); +const Bridge = require("../bridgestuff/Bridge"); +const From = require("./From"); /**************************** * The middleware functions * @@ -70,7 +72,8 @@ function chatinfo(ctx) { /** * Adds the bridges to the tediCross object on the context. Requires the tediCross context to work * - * @param {Object} context The context to add the property to + * @param {Object} ctx The context to add the property to + * @param {Object} ctx.tediCross The TediCross object on the context * @param {Function} next Function to pass control to next middleware * * @returns {undefined} @@ -80,6 +83,41 @@ function addBridgesToContext(ctx, next) { next(); } +/** + * Removes d2t bridges from the bridge list + * + * @param {Object} ctx The Telegraf context to use + * @param {Object} ctx.tediCross The TediCross object on the context + * @param {Bridge[]} ctx.tediCross.bridges The bridges the message could use + * @param {Function} next Function to pass control to next middleware + * + * @returns {undefined} + */ +function removeD2TBridges(ctx, next) { + ctx.tediCross.bridges = ctx.tediCross.bridges.filter(R.compose( + R.not, + R.equals(Bridge.DIRECTION_DISCORD_TO_TELEGRAM), + R.prop("direction") + )); + + next(); +} + +/** + * Removes bridges with the `ignoreCommand` flag from the bridge list + * + * @param {Object} ctx The Telegraf context to use + * @param {Object} ctx.tediCross The TediCross object on the context + * @param {Bridge[]} ctx.tediCross.bridges The bridges the message could use + * @param {Function} next Function to pass control to next middleware + * + * @returns {undefined} + */ +function removeBridgesIgnoringCommands(ctx, next) { + ctx.tediCross.bridges = ctx.tediCross.bridges.filter(R.path(["telegram", "ignoreCommands"])); + next(); +} + /** * Replies to the message telling the user this is a private bot if there are no bridges on the tediCross context * @@ -111,6 +149,32 @@ function informThisIsPrivateBot(ctx, next) { )(ctx); } +/** + * Adds a `from` object to the tediCross context + * + * @param {Object} ctx The context to add the property to + * @param {Object} ctx.tediCross The tediCross on the context + * @param {Object} ctx.tediCross.message The message object to create the `from` object from + * + * @returns {undefined} + */ +function addFromObj(ctx, next) { + ctx.tediCross.from = R.ifElse( + // Check if the `from` object exists + R.compose(R.isNil, R.prop("from")), + // This message is from a channel + message => From.createFromObj(message.chat.title), + // This message is from a user + message => From.createFromObj( + message.from.first_name, + message.from.last_name, + message.from.username + ) + )(ctx.tediCross.message); + + next(); +} + /*************** * Export them * ***************/ @@ -120,5 +184,8 @@ module.exports = { getMessageObj, chatinfo, addBridgesToContext, - informThisIsPrivateBot + removeD2TBridges, + removeBridgesIgnoringCommands, + informThisIsPrivateBot, + addFromObj }; diff --git a/lib/telegram2discord/processMessage.js b/lib/telegram2discord/processMessage.js deleted file mode 100644 index 371d5536..00000000 --- a/lib/telegram2discord/processMessage.js +++ /dev/null @@ -1,114 +0,0 @@ -"use strict"; - -/************************** - * Import important stuff * - **************************/ - -const handleEntities = require("./handleEntities"); - -/********************* - * Make some helpers * - *********************/ - -/** - * Creates a 'from' object from a channel - * - * @param {Object} chat The chat object of the channel - * - * @returns {Object} An object on the form {type, title} where 'type' is "channel" - * - * @private - */ -function channelFrom(chat) { - return { - type: "channel", - title: chat.title - }; -} - -/** - * Creates a 'from' object from a user - * - * @param {Object} user The user's user object - * - * @returns {Object} An object on the form {type, firstName, lastName, username} where 'type' is "user" - * - * @private - */ -function userFrom(user) { - return { - type: "user", - firstName: user.first_name, - lastName: user.last_name, - username: user.username - }; -} - -/** - * Processes a reply - * - * @param {Object} reply The reply to process - * - * @returns {Object} The processed reply - * - * @private - */ -function processReply(reply, tgBot) { - const processed = processMessage(reply); - - // If the reply was to the bot, it was really to the Discord user - if (processed.from.username === tgBot.me.username) { - // Extract the Discord user's name from the message and update the 'from' object - const usernameMatch = /^\*\*([^*]+)\*\*/.exec(processed.text); - if (usernameMatch !== undefined) { - processed.from = userFrom({ - first_name: usernameMatch[1], - last_name: undefined, - username: usernameMatch[1] - }); - } - } - return processed; -} - -/************************ - * The processor itself * - ************************/ - -/** - * Processes a message into a more handleable internal TediCross format - * - * @param {Object} message A raw message object from Telegram. Either from 'update.message', 'update.channel_post', 'update.edited_message' or 'message.edited_channel_post' - * @param {Bridge} bridge The bridge this message is on - * @param {Discord} dcBot The discord bot - * @param {BotAPI} tgBot The telegram bot - * - * @returns {ProcessedMessage} The processed message - */ -function processMessage(message, bridge, dcBot, tgBot) { - return { - from: message.chat.type === "channel" - ? channelFrom(message.chat) - : userFrom(message.from) - , - reply: message.reply_to_message !== undefined - ? processReply(message.reply_to_message, tgBot) - : null - , - forward: message.forward_from !== undefined - ? userFrom(message.forward_from) - : message.forward_from_chat !== undefined - ? channelFrom(message.forward_from_chat) - : null - , - text: message.text !== undefined - ? handleEntities(message.text, message.entities, dcBot, bridge) - : undefined - }; -} - -/************* - * Export it * - *************/ - -module.exports = processMessage; diff --git a/lib/telegram2discord/setup.js b/lib/telegram2discord/setup.js index d466c990..ba35a184 100644 --- a/lib/telegram2discord/setup.js +++ b/lib/telegram2discord/setup.js @@ -99,29 +99,17 @@ function makeNameObject(user) { /** * Curryed function creating handlers handling messages which should not be relayed, and passing through those which should * - * @param {Logger} logger The Logger instance to log messages to * @param {Telegraf} tgBot The Telegram bot - * @param {BridgeMap} bridgeMap Map of the bridges to use * @param {Function} func The message handler to wrap * @param {Context} ctx The Telegram message triggering the wrapped function, wrapped in a context * * @private */ -const createMessageHandler = R.curry((logger, tgBot, bridgeMap, settings, func, ctx) => { - // Get the message object - const message = ctx.tediCross.message; - +const createMessageHandler = R.curry((tgBot, func, ctx) => { // Check if the message came from the correct chat ctx.tediCross.bridges.forEach((bridge) => { - // Check if it is a command, and if commands should be ignored - if (!R.isNil(message.text) && message.text.startsWith("/") && bridge.telegram.ignoreCommands) { - return; - } - - // Do the thing, if this is not a discord-to-telegram bridge - if (bridge.direction !== Bridge.DIRECTION_DISCORD_TO_TELEGRAM) { - func(message, bridge); - } + // Do the thing + func(ctx, bridge); }); }); @@ -149,15 +137,19 @@ function setup(logger, tgBot, dcBot, messageMap, bridgeMap, settings) { tgBot.command("chatinfo", middlewares.chatinfo); tgBot.use(middlewares.addBridgesToContext); tgBot.use(middlewares.informThisIsPrivateBot); + tgBot.use(middlewares.removeD2TBridges); + tgBot.command(middlewares.removeBridgesIgnoringCommands); + tgBot.use(middlewares.addFromObj); // Not used at the moment // Make the file sender const sendFile = makeFileSender(tgBot, dcBot, messageMap, settings); // Create the message handler wrapper - const wrapFunction = createMessageHandler(logger, tgBot, bridgeMap, settings); + const wrapFunction = createMessageHandler(tgBot); // Set up event listener for text messages from Telegram - tgBot.on("text", wrapFunction(async (message, bridge) => { + tgBot.on("text", wrapFunction(async (ctx, bridge) => { + const message = ctx.tediCross.message; // Turn the text discord friendly const messageObj = messageConverter(message, tgBot, settings, dcBot, bridge); @@ -213,7 +205,10 @@ function setup(logger, tgBot, dcBot, messageMap, bridgeMap, settings) { })); // Set up event listener for photo messages from Telegram - tgBot.on("photo", wrapFunction(async (message, bridge) => { + tgBot.on("photo", wrapFunction(async (ctx, bridge) => { + + const message = ctx.tediCross.message; + try { await sendFile(bridge, { message, @@ -227,7 +222,10 @@ function setup(logger, tgBot, dcBot, messageMap, bridgeMap, settings) { })); // Set up event listener for stickers from Telegram - tgBot.on("sticker", wrapFunction(async (message, bridge) => { + tgBot.on("sticker", wrapFunction(async (ctx, bridge) => { + + const message = ctx.tediCross.message; + try { await sendFile(bridge, { message, @@ -241,7 +239,10 @@ function setup(logger, tgBot, dcBot, messageMap, bridgeMap, settings) { })); // Set up event listener for filetypes not caught by the other filetype handlers - tgBot.on("document", wrapFunction(async (message, bridge) => { + tgBot.on("document", wrapFunction(async (ctx, bridge) => { + + const message = ctx.tediCross.message; + // message.file_name can for some reason be undefined some times. Default to "file.ext" let fileName = message.document.file_name; if (fileName === undefined) { @@ -262,7 +263,10 @@ function setup(logger, tgBot, dcBot, messageMap, bridgeMap, settings) { })); // Set up event listener for voice messages - tgBot.on("voice", wrapFunction(async (message, bridge) => { + tgBot.on("voice", wrapFunction(async (ctx, bridge) => { + + const message = ctx.tediCross.message; + try { await sendFile(bridge, { message, @@ -276,7 +280,10 @@ function setup(logger, tgBot, dcBot, messageMap, bridgeMap, settings) { })); // Set up event listener for audio messages - tgBot.on("audio", wrapFunction(async (message, bridge) => { + tgBot.on("audio", wrapFunction(async (ctx, bridge) => { + + const message = ctx.tediCross.message; + try { await sendFile(bridge, { message, @@ -290,7 +297,10 @@ function setup(logger, tgBot, dcBot, messageMap, bridgeMap, settings) { })); // Set up event listener for video messages - tgBot.on("video", wrapFunction(async (message, bridge) => { + tgBot.on("video", wrapFunction(async (ctx, bridge) => { + + const message = ctx.tediCross.message; + try { await sendFile(bridge, { message, @@ -305,7 +315,10 @@ function setup(logger, tgBot, dcBot, messageMap, bridgeMap, settings) { })); // Listen for users joining the chat - tgBot.on("new_chat_members", wrapFunction(({ new_chat_members }, bridge) => { + tgBot.on("new_chat_members", wrapFunction((ctx, bridge) => { + + const new_chat_members = ctx.tediCross.message.new_chat_members; + // Ignore it if the settings say no if (!bridge.telegram.relayJoinMessages) { return; @@ -326,7 +339,10 @@ function setup(logger, tgBot, dcBot, messageMap, bridgeMap, settings) { })); // Listen for users leaving the chat - tgBot.on("left_chat_member", wrapFunction(async ({ left_chat_member }, bridge) => { + tgBot.on("left_chat_member", wrapFunction(async (ctx, bridge) => { + + const left_chat_member = ctx.tediCross.message.left_chat_member; + // Ignore it if the settings say no if (!bridge.telegram.relayLeaveMessages) { return; @@ -346,8 +362,10 @@ function setup(logger, tgBot, dcBot, messageMap, bridgeMap, settings) { })); // Set up event listener for message edits - const handleEdits = wrapFunction(async (tgMessage, bridge) => { + const handleEdits = wrapFunction(async (ctx, bridge) => { try { + const tgMessage = ctx.tediCross.message; + // Wait for the Discord bot to become ready await dcBot.ready; From 57ed7204aff7acbabf010f7394af1c26d0b2ed56 Mon Sep 17 00:00:00 2001 From: Simen de Lange Date: Mon, 27 May 2019 19:19:51 +0200 Subject: [PATCH 049/102] More TG middlewares massaging the message --- lib/telegram2discord/From.js | 46 ++ lib/telegram2discord/messageConverter.js | 4 +- lib/telegram2discord/middlewares.js | 173 +++++++- lib/telegram2discord/setup.js | 525 +++++++++++------------ 4 files changed, 469 insertions(+), 279 deletions(-) diff --git a/lib/telegram2discord/From.js b/lib/telegram2discord/From.js index ee309730..195a4da4 100644 --- a/lib/telegram2discord/From.js +++ b/lib/telegram2discord/From.js @@ -38,6 +38,49 @@ function createFromObj(firstName, lastName, username) { }; } +/** + * Creates a new From object from a Telegram message + * + * @param {Object} message The Telegram message to create the from object from + * + * @returns {From} The from object + */ +function createFromObjFromMessage(message) { + return R.ifElse( + // Check if the `from` object exists + R.compose(R.isNil, R.prop("from")), + // This message is from a channel + message => createFromObj(message.chat.title), + // This message is from a user + R.compose( + createFromObjFromUser, + R.prop("from") + ) + )(message); +} + +/** + * Creates a new From object from a Telegram User object + * + * @param {Object} user The Telegram user object to create the from object from + * + * @returns {From} The From object created from the user + */ +function createFromObjFromUser(user) { + return createFromObj(user.first_name, user.last_name, user.username); +} + +/** + * Creates a From object from a Telegram chat object + * + * @param {Object} chat The Telegram chat object to create the from object from + * + * @returns {From} The From object created from the chat + */ +function createFromObjFromChat(chat) { + return createFromObj(chat.title); +} + /** * Makes a display name out of a from object * @@ -62,5 +105,8 @@ function makeDisplayName(useFirstNameInsteadOfUsername, from) { module.exports = { createFromObj, + createFromObjFromMessage, + createFromObjFromUser, + createFromObjFromChat, makeDisplayName }; diff --git a/lib/telegram2discord/messageConverter.js b/lib/telegram2discord/messageConverter.js index 04b45935..800c1061 100644 --- a/lib/telegram2discord/messageConverter.js +++ b/lib/telegram2discord/messageConverter.js @@ -100,7 +100,7 @@ function messageConverter(message, tgBot, settings, dcBot, bridge) { let originalText = ""; // Is this a reply to the bot, i.e. to a Discord user? - if (message.reply_to_message.from !== undefined && message.reply_to_message.from.id === tgBot.me.id) { + if (message.reply_to_message.from !== undefined && message.reply_to_message.from.id === tgBot.context.self.id) { // Get the name of the Discord user this is a reply to const dcUsername = message.reply_to_message.text.split("\n")[0]; const dcUser = dcBot.channels.get(bridge.discord.channelId).members.find(findFn("displayName", new RegExp(dcUsername))); @@ -112,7 +112,7 @@ function messageConverter(message, tgBot, settings, dcBot, bridge) { originalText = message.reply_to_message.text; // Is this a reply to the bot, i.e. to a Discord user? - if (message.reply_to_message.from !== undefined && message.reply_to_message.from.id === tgBot.me.id) { + if (message.reply_to_message.from !== undefined && message.reply_to_message.from.id === tgBot.context.self.id) { [ , ...originalText] = message.reply_to_message.text.split("\n"); originalText = originalText.join("\n"); } diff --git a/lib/telegram2discord/middlewares.js b/lib/telegram2discord/middlewares.js index 6ead9ab0..47fc2a65 100644 --- a/lib/telegram2discord/middlewares.js +++ b/lib/telegram2discord/middlewares.js @@ -7,6 +7,8 @@ const R = require("ramda"); const Bridge = require("../bridgestuff/Bridge"); const From = require("./From"); +const mime = require("mime/lite"); +const request = require("request"); /**************************** * The middleware functions * @@ -159,22 +161,163 @@ function informThisIsPrivateBot(ctx, next) { * @returns {undefined} */ function addFromObj(ctx, next) { - ctx.tediCross.from = R.ifElse( - // Check if the `from` object exists - R.compose(R.isNil, R.prop("from")), - // This message is from a channel - message => From.createFromObj(message.chat.title), - // This message is from a user - message => From.createFromObj( - message.from.first_name, - message.from.last_name, - message.from.username - ) - )(ctx.tediCross.message); + ctx.tediCross.from = From.createFromObjFromMessage(ctx.tediCross.message); + next(); +} + +/** + * Adds a `reply` object to the tediCross context, if the message is a reply + * + * @param {Object} ctx The context to add the property to + * @param {Object} ctx.tediCross The tediCross on the context + * @param {Object} ctx.tediCross.message The message object to create the `reply` object from + * + * @returns {undefined} + */ +function addReplyObj(ctx, next) { + const repliedToMessage = ctx.tediCross.message.reply_to_message; + + if (!R.isNil(repliedToMessage)) { + // This is a reply + ctx.tediCross.replyTo = { + message: repliedToMessage, + originalFrom: From.createFromObjFromMessage(repliedToMessage), + isReplyToTediCross: !R.isNil(repliedToMessage.from) && repliedToMessage.from.id === ctx.self.id + }; + } + + next(); +} + +/** + * Adds a `forward` object to the tediCross context, if the message is a forward + * + * @param {Object} ctx The context to add the property to + * @param {Object} ctx.tediCross The tediCross on the context + * @param {Object} ctx.tediCross.message The message object to create the `forward` object from + * + * @returns {undefined} + */ +function addForwardFrom(ctx, next) { + const msg = ctx.message; + + if (!R.isNil(msg.forward_from) || !R.isNil(msg.forward_from_chat)) { + ctx.tediCross.forwardFrom = R.ifElse( + // If there is no `forward_from` prop + R.compose(R.isNil, R.prop("forward_from")), + // Then this is a forward from a chat (channel) + R.compose(From.createFromObjFromChat, R.prop("forward_from_chat")), + // Else it is from a user + R.compose(From.createFromObjFromUser, R.prop("forward_from")) + )(msg); + } next(); } +/** + * Adds a text object to the tediCross property on the context + * + * @param {Object} ctx The context to add the property to + * @param {Object} ctx.tediCross The tediCross on the context + * @param {Object} ctx.tediCross.message The message object to get the text data from + * + * @returns {undefined} + */ +function addTextObj(ctx, next) { + if (!R.isNil(ctx.tediCross.message.text)) { + // Text + ctx.tediCross.text = { + raw: ctx.tediCross.message.text, + entities: R.defaultTo([], ctx.tediCross.message.entities) + }; + } else if (!R.isNil(ctx.tediCross.message.caption)) { + // Animation, audio, document, photo, video or voice, + ctx.tediCross.text = { + raw: ctx.tediCross.message.caption, + entities: R.defaultTo([], ctx.tediCross.message.caption_entities) + }; + } else if (!R.isNil(ctx.tediCross.sticker)) { + // Stickers have an emoji instead of text + ctx.tediCross.text = { + raw: ctx.tediCross.sticker.emoji, + entities: [] + } + } + + next(); +} + +/** + * Adds a file object to the tediCross property on the context + * + * @param {Object} ctx The context to add the property to + * @param {Object} ctx.tediCross The tediCross on the context + * @param {Object} ctx.tediCross.message The message object to get the file data from + * + * @returns {undefined} + */ +function addFileObj(ctx, next) { + const message = ctx.tediCross.message; + + if (!R.isNil(message.audio)) { + // Audio + ctx.tediCross.file = { + type: "audio", + fileId: message.audio.file_id, + fileName: message.audio.title + "." + mime.getExtension(message.audio.mime_type) + }; + } else if (!R.isNil(message.document)) { + // Generic file + ctx.tediCross.file = { + type: "document", + fileId: message.document.file_id, + fileName: message.document.file_name + }; + } else if (!R.isNil(message.photo)) { + // Photo. It has an array of photos of different sizes. Use the first and biggest + const photo = R.head(message.photo); + ctx.tediCross.file = { + type: "photo", + fileId: photo.file_id, + fileName: "photo.jpg" // Telegram will convert it to a jpg no matter which format is orignally sent + }; + } else if (!R.isNil(message.sticker)) { + // Sticker + ctx.tediCross.file = { + type: "sticker", + fileId: message.sticker.file_id, + fileName: "sticker.webp" + }; + } else if (!R.isNil(message.video)) { + // Video + ctx.tediCross.file = { + type: "video", + fileId: message.video.file_id, + fileName: "video" + "." + mime.getExtension(message.video.mime_type), + }; + } else if (!R.isNil(message.voice)) { + // Voice + ctx.tediCross.file = { + type: "voice", + fileId: message.voice.file_id, + fileName: "voice" + "." + mime.getExtension(message.voice.mime_type), + }; + } + + Promise.resolve() + .then(() => { + // Get a stream to the file, if one was found + if (!R.isNil(ctx.tediCross.file)) { + return ctx.telegram.getFileLink(ctx.tediCross.file.fileId) + .then(fileLink => { + ctx.tediCross.file.stream = request(fileLink); + }); + } + }) + .then(next); +} + /*************** * Export them * ***************/ @@ -187,5 +330,9 @@ module.exports = { removeD2TBridges, removeBridgesIgnoringCommands, informThisIsPrivateBot, - addFromObj + addFromObj, + addReplyObj, + addForwardFrom, + addTextObj, + addFileObj }; diff --git a/lib/telegram2discord/setup.js b/lib/telegram2discord/setup.js index ba35a184..ff274dea 100644 --- a/lib/telegram2discord/setup.js +++ b/lib/telegram2discord/setup.js @@ -8,7 +8,6 @@ const messageConverter = require("./messageConverter"); const MessageMap = require("../MessageMap"); const R = require("ramda"); const mime = require("mime/lite"); -const Bridge = require("../bridgestuff/Bridge"); const Discord = require("discord.js"); const request = require("request"); const middlewares = require("./middlewares"); @@ -97,20 +96,15 @@ function makeNameObject(user) { } /** - * Curryed function creating handlers handling messages which should not be relayed, and passing through those which should + * Curryed function making middleware be handled for every bridge * - * @param {Telegraf} tgBot The Telegram bot * @param {Function} func The message handler to wrap * @param {Context} ctx The Telegram message triggering the wrapped function, wrapped in a context * * @private */ -const createMessageHandler = R.curry((tgBot, func, ctx) => { - // Check if the message came from the correct chat - ctx.tediCross.bridges.forEach((bridge) => { - // Do the thing - func(ctx, bridge); - }); +const createMessageHandler = R.curry((func, ctx) => { + ctx.tediCross.bridges.forEach((bridge) => func(ctx, bridge)); }); /********************** @@ -128,279 +122,279 @@ const createMessageHandler = R.curry((tgBot, func, ctx) => { * @param {Settings} settings The settings to use */ function setup(logger, tgBot, dcBot, messageMap, bridgeMap, settings) { - // Put the bridges on the global tgBot context - tgBot.context.bridgeMap = bridgeMap; - - // Apply middlewares - tgBot.use(middlewares.addTediCrossObj); - tgBot.use(middlewares.getMessageObj); - tgBot.command("chatinfo", middlewares.chatinfo); - tgBot.use(middlewares.addBridgesToContext); - tgBot.use(middlewares.informThisIsPrivateBot); - tgBot.use(middlewares.removeD2TBridges); - tgBot.command(middlewares.removeBridgesIgnoringCommands); - tgBot.use(middlewares.addFromObj); // Not used at the moment - - // Make the file sender - const sendFile = makeFileSender(tgBot, dcBot, messageMap, settings); - - // Create the message handler wrapper - const wrapFunction = createMessageHandler(tgBot); - - // Set up event listener for text messages from Telegram - tgBot.on("text", wrapFunction(async (ctx, bridge) => { - const message = ctx.tediCross.message; - - // Turn the text discord friendly - const messageObj = messageConverter(message, tgBot, settings, dcBot, bridge); + tgBot.ready = tgBot.telegram.getMe() + .then(me => { + // Log the bot's info + logger.info(`Telegram: ${me.username} (${me.id})`); + + // Add some global context + tgBot.context.self = me; // XXX For some strange reason Telegraf crashes if I try to call the prop `me` + tgBot.context.bridgeMap = bridgeMap; + + // Apply middlewares + tgBot.use(middlewares.addTediCrossObj); + tgBot.use(middlewares.getMessageObj); + tgBot.command("chatinfo", middlewares.chatinfo); + tgBot.use(middlewares.addBridgesToContext); + tgBot.use(middlewares.informThisIsPrivateBot); + tgBot.use(middlewares.removeD2TBridges); + tgBot.command(middlewares.removeBridgesIgnoringCommands); + tgBot.use(middlewares.addFromObj); // Not used at the moment + tgBot.use(middlewares.addReplyObj); // Not used at the moment + tgBot.use(middlewares.addForwardFrom); // Not used at the moment + tgBot.use(middlewares.addTextObj); // Not used at the moment + tgBot.use(middlewares.addFileObj); // Not used at the moment + + tgBot.use((ctx, next) => {console.log(ctx.tediCross); next();}); + + // Make the file sender + const sendFile = makeFileSender(tgBot, dcBot, messageMap, settings); + + // Set up event listener for text messages from Telegram + tgBot.on("text", createMessageHandler(async (ctx, bridge) => { + const message = ctx.tediCross.message; + + // Turn the text discord friendly + const messageObj = messageConverter(message, tgBot, settings, dcBot, bridge); + + try { + // Pass it on to Discord when the dcBot is ready + await dcBot.ready; + + // Get the channel to send to + const channel = dcBot.channels.get(bridge.discord.channelId); + + // Make the header + let header = bridge.telegram.sendUsernames ? `**${messageObj.from}**` : ""; + + // Handle replies + if (messageObj.reply !== null) { + // Add the reply data to the header + header = header + ` (In reply to **${messageObj.reply.author}**)`; + + // Figure out how to display the reply in Discord + if (settings.discord.displayTelegramReplies === "embed") { + // Make a Discord embed and send it first + const embed = new Discord.RichEmbed({ + description: messageObj.reply.text + }); + + await channel.send(header, {embed}); + + // Clear the header + header = ""; + } else if (settings.discord.displayTelegramReplies === "inline") { + // Just modify the header + header = `${header.slice(0, -1)}: _${messageObj.reply.text}_`; + } + } + + // Discord doesn't handle messages longer than 2000 characters. Split it up into chunks that big + const messageText = header + "\n" + messageObj.text; + const chunks = R.splitEvery(2000, messageText); + + // Send them in serial + let dcMessage = null; + for (const chunk of chunks) { + dcMessage = await channel.send(chunk); + } + + // Make the mapping so future edits can work XXX Only the last chunk is considered + messageMap.insert(MessageMap.TELEGRAM_TO_DISCORD, bridge, message.message_id, dcMessage.id); + } catch (err) { + logger.error(`[${bridge.name}] Discord did not accept a text message:`, err); + logger.error(`[${bridge.name}] Failed message:`, message.text); + } + })); - try { - // Pass it on to Discord when the dcBot is ready - await dcBot.ready; + // Set up event listener for photo messages from Telegram + tgBot.on("photo", createMessageHandler(async (ctx, bridge) => { - // Get the channel to send to - const channel = dcBot.channels.get(bridge.discord.channelId); + const message = ctx.tediCross.message; - // Make the header - let header = bridge.telegram.sendUsernames ? `**${messageObj.from}**` : ""; + try { + await sendFile(bridge, { + message, + fileId: message.photo[message.photo.length-1].file_id, + fileName: "photo.jpg", // Telegram will convert it to jpg no matter what filetype is actually sent + caption: message.caption + }); + } catch (err) { + logger.error(`[${bridge.name}] Could not send photo`, err); + } + })); - // Handle replies - if (messageObj.reply !== null) { - // Add the reply data to the header - header = header + ` (In reply to **${messageObj.reply.author}**)`; + // Set up event listener for stickers from Telegram + tgBot.on("sticker", createMessageHandler(async (ctx, bridge) => { - // Figure out how to display the reply in Discord - if (settings.discord.displayTelegramReplies === "embed") { - // Make a Discord embed and send it first - const embed = new Discord.RichEmbed({ - description: messageObj.reply.text + const message = ctx.tediCross.message; + + try { + await sendFile(bridge, { + message, + fileId: message.sticker.file_id, + fileName: "sticker.webp", // Telegram will insist that it is a jpg, but it really is a webp + caption: settings.telegram.sendEmojiWithStickers ? message.sticker.emoji : undefined }); + } catch (err) { + logger.error(`[${bridge.name}] Could not send sticker`, err); + } + })); + + // Set up event listener for filetypes not caught by the other filetype handlers + tgBot.on("document", createMessageHandler(async (ctx, bridge) => { - await channel.send(header, {embed}); + const message = ctx.tediCross.message; - // Clear the header - header = ""; - } else if (settings.discord.displayTelegramReplies === "inline") { - // Just modify the header - header = `${header.slice(0, -1)}: _${messageObj.reply.text}_)`; + // message.file_name can for some reason be undefined some times. Default to "file.ext" + let fileName = message.document.file_name; + if (fileName === undefined) { + fileName = "file." + mime.getExtension(message.document.mime_type); } - } - - // Discord doesn't handle messages longer than 2000 characters. Split it up into chunks that big - const messageText = header + "\n" + messageObj.text; - const chunks = R.splitEvery(2000, messageText); - - // Send them in serial - let dcMessage = null; - for (const chunk of chunks) { - dcMessage = await channel.send(chunk); - } - - // Make the mapping so future edits can work XXX Only the last chunk is considered - messageMap.insert(MessageMap.TELEGRAM_TO_DISCORD, bridge, message.message_id, dcMessage.id); - } catch (err) { - logger.error(`[${bridge.name}] Discord did not accept a text message:`, err); - logger.error(`[${bridge.name}] Failed message:`, message.text); - } - })); - - // Set up event listener for photo messages from Telegram - tgBot.on("photo", wrapFunction(async (ctx, bridge) => { - - const message = ctx.tediCross.message; - - try { - await sendFile(bridge, { - message, - fileId: message.photo[message.photo.length-1].file_id, - fileName: "photo.jpg", // Telegram will convert it to jpg no matter what filetype is actually sent - caption: message.caption - }); - } catch (err) { - logger.error(`[${bridge.name}] Could not send photo`, err); - } - })); - - // Set up event listener for stickers from Telegram - tgBot.on("sticker", wrapFunction(async (ctx, bridge) => { - - const message = ctx.tediCross.message; - - try { - await sendFile(bridge, { - message, - fileId: message.sticker.thumb.file_id, - fileName: "sticker.webp", // Telegram will insist that it is a jpg, but it really is a webp - caption: settings.telegram.sendEmojiWithStickers ? message.sticker.emoji : undefined - }); - } catch (err) { - logger.error(`[${bridge.name}] Could not send sticker`, err); - } - })); - - // Set up event listener for filetypes not caught by the other filetype handlers - tgBot.on("document", wrapFunction(async (ctx, bridge) => { - - const message = ctx.tediCross.message; - - // message.file_name can for some reason be undefined some times. Default to "file.ext" - let fileName = message.document.file_name; - if (fileName === undefined) { - fileName = "file." + mime.getExtension(message.document.mime_type); - } - - try { - // Pass it on to Discord - await sendFile(bridge, { - message, - fileId: message.document.file_id, - fileName: fileName, - resolveExtension: false - }); - } catch (err) { - logger.error(`[${bridge.name}] Could not send document`, err); - } - })); - - // Set up event listener for voice messages - tgBot.on("voice", wrapFunction(async (ctx, bridge) => { - - const message = ctx.tediCross.message; - - try { - await sendFile(bridge, { - message, - fileId: message.voice.file_id, - fileName: "voice" + "." + mime.getExtension(message.voice.mime_type), - resolveExtension: false - }); - } catch (err) { - logger.error(`[${bridge.name}] Could not send voice`, err); - } - })); - - // Set up event listener for audio messages - tgBot.on("audio", wrapFunction(async (ctx, bridge) => { - - const message = ctx.tediCross.message; - - try { - await sendFile(bridge, { - message, - fileId: message.audio.file_id, - fileName: message.audio.title, - resolveExtension: true - }); - } catch (err) { - logger.error(`[${bridge.name}] Could not send audio`, err); - } - })); - - // Set up event listener for video messages - tgBot.on("video", wrapFunction(async (ctx, bridge) => { - - const message = ctx.tediCross.message; - - try { - await sendFile(bridge, { - message, - caption: message.caption, - fileId: message.video.file_id, - fileName: "video" + "." + mime.getExtension(message.video.mime_type), - resolveExtension: false - }); - } catch (err) { - logger.error(`[${bridge.name}] Could not send video`, err); - } - })); - // Listen for users joining the chat - tgBot.on("new_chat_members", wrapFunction((ctx, bridge) => { + try { + // Pass it on to Discord + await sendFile(bridge, { + message, + fileId: message.document.file_id, + fileName: fileName, + resolveExtension: false + }); + } catch (err) { + logger.error(`[${bridge.name}] Could not send document`, err); + } + })); - const new_chat_members = ctx.tediCross.message.new_chat_members; + // Set up event listener for voice messages + tgBot.on("voice", createMessageHandler(async (ctx, bridge) => { - // Ignore it if the settings say no - if (!bridge.telegram.relayJoinMessages) { - return; - } + const message = ctx.tediCross.message; - // Notify Discord about each user - new_chat_members.forEach((user) => { - // Make the text to send - const nameObj = makeNameObject(user); - const text = `**${nameObj.name} (${nameObj.username})** joined the Telegram side of the chat`; + try { + await sendFile(bridge, { + message, + fileId: message.voice.file_id, + fileName: "voice" + "." + mime.getExtension(message.voice.mime_type), + resolveExtension: false + }); + } catch (err) { + logger.error(`[${bridge.name}] Could not send voice`, err); + } + })); - // Pass it on - dcBot.ready.then(() => { - return dcBot.channels.get(bridge.discord.channelId).send(text); - }) - .catch((err) => logger.error(`[${bridge.name}] Could not notify Discord about a user that joined Telegram`, err)); - }); - })); + // Set up event listener for audio messages + tgBot.on("audio", createMessageHandler(async (ctx, bridge) => { - // Listen for users leaving the chat - tgBot.on("left_chat_member", wrapFunction(async (ctx, bridge) => { + const message = ctx.tediCross.message; - const left_chat_member = ctx.tediCross.message.left_chat_member; + try { + await sendFile(bridge, { + message, + fileId: message.audio.file_id, + fileName: message.audio.title, + resolveExtension: true + }); + } catch (err) { + logger.error(`[${bridge.name}] Could not send audio`, err); + } + })); - // Ignore it if the settings say no - if (!bridge.telegram.relayLeaveMessages) { - return; - } + // Set up event listener for video messages + tgBot.on("video", createMessageHandler(async (ctx, bridge) => { - // Make the text to send - const nameObj = makeNameObject(left_chat_member); - const text = `**${nameObj.name} (${nameObj.username})** left the Telegram side of the chat`; - - try { - // Pass it on when Discord is ready - await dcBot.ready; - await dcBot.channels.get(bridge.discord.channelId).send(text); - } catch (err) { - logger.error(`[${bridge.name}] Could not notify Discord about a user that left Telegram`, err); - } - })); - - // Set up event listener for message edits - const handleEdits = wrapFunction(async (ctx, bridge) => { - try { - const tgMessage = ctx.tediCross.message; - - // Wait for the Discord bot to become ready - await dcBot.ready; - - // Find the ID of this message on Discord - const [dcMessageId] = messageMap.getCorresponding(MessageMap.TELEGRAM_TO_DISCORD, bridge, tgMessage.message_id); - - // Get the messages from Discord - const dcMessage = await dcBot.channels.get(bridge.discord.channelId).fetchMessage(dcMessageId); - - const messageObj = messageConverter(tgMessage, tgBot, settings, dcBot, bridge); - - // Try to edit the message - const textToSend = bridge.telegram.sendUsernames - ? `**${messageObj.from}**\n${messageObj.text}` - : messageObj.text - ; - await dcMessage.edit(textToSend); - } catch (err) { - // Log it - logger.error(`[${bridge.name}] Could not edit Discord message:`, err); - } - }); - tgBot.on("edited_message", handleEdits); - tgBot.on("edited_channel_post", handleEdits); - - // Make a promise which resolves when the dcBot is ready - tgBot.ready = tgBot.telegram.getMe() - .then((bot) => { - // Log the bot's info - logger.info(`Telegram: ${bot.username} (${bot.id})`); + const message = ctx.tediCross.message; + + try { + await sendFile(bridge, { + message, + caption: message.caption, + fileId: message.video.file_id, + fileName: "video" + "." + mime.getExtension(message.video.mime_type), + resolveExtension: false + }); + } catch (err) { + logger.error(`[${bridge.name}] Could not send video`, err); + } + })); + + // Listen for users joining the chat + tgBot.on("new_chat_members", createMessageHandler((ctx, bridge) => { + + const new_chat_members = ctx.tediCross.message.new_chat_members; + + // Ignore it if the settings say no + if (!bridge.telegram.relayJoinMessages) { + return; + } + + // Notify Discord about each user + new_chat_members.forEach((user) => { + // Make the text to send + const nameObj = makeNameObject(user); + const text = `**${nameObj.name} (${nameObj.username})** joined the Telegram side of the chat`; + + // Pass it on + dcBot.ready.then(() => { + return dcBot.channels.get(bridge.discord.channelId).send(text); + }) + .catch((err) => logger.error(`[${bridge.name}] Could not notify Discord about a user that joined Telegram`, err)); + }); + })); + + // Listen for users leaving the chat + tgBot.on("left_chat_member", createMessageHandler(async (ctx, bridge) => { + + const left_chat_member = ctx.tediCross.message.left_chat_member; + + // Ignore it if the settings say no + if (!bridge.telegram.relayLeaveMessages) { + return; + } + + // Make the text to send + const nameObj = makeNameObject(left_chat_member); + const text = `**${nameObj.name} (${nameObj.username})** left the Telegram side of the chat`; + + try { + // Pass it on when Discord is ready + await dcBot.ready; + await dcBot.channels.get(bridge.discord.channelId).send(text); + } catch (err) { + logger.error(`[${bridge.name}] Could not notify Discord about a user that left Telegram`, err); + } + })); + + // Set up event listener for message edits + const handleEdits = createMessageHandler(async (ctx, bridge) => { + try { + const tgMessage = ctx.tediCross.message; + + // Wait for the Discord bot to become ready + await dcBot.ready; + + // Find the ID of this message on Discord + const [dcMessageId] = messageMap.getCorresponding(MessageMap.TELEGRAM_TO_DISCORD, bridge, tgMessage.message_id); - // Put the data on the bot - tgBot.me = bot; + // Get the messages from Discord + const dcMessage = await dcBot.channels.get(bridge.discord.channelId).fetchMessage(dcMessageId); + + const messageObj = messageConverter(tgMessage, tgBot, settings, dcBot, bridge); + + // Try to edit the message + const textToSend = bridge.telegram.sendUsernames + ? `**${messageObj.from}**\n${messageObj.text}` + : messageObj.text + ; + await dcMessage.edit(textToSend); + } catch (err) { + // Log it + logger.error(`[${bridge.name}] Could not edit Discord message:`, err); + } + }); + tgBot.on("edited_message", handleEdits); + tgBot.on("edited_channel_post", handleEdits); }) - .catch((err) => { + .catch(err => { // Log the error( logger.error("Failed at getting the Telegram bot's me-object:", err); @@ -408,16 +402,19 @@ function setup(logger, tgBot, dcBot, messageMap, bridgeMap, settings) { throw err; }); - // Start getting updates + /* Start getting updates */ let p = Promise.resolve(); + + // Clear old updates if wanted if (settings.telegram.skipOldMessages) { - // Clear old updates p = tgBot.telegram.getUpdates(0, 100, -1) .then(updates => updates.length > 0 ? tgBot.telegram.getUpdates(0, 100, updates[updates.length-1].update_id) : [] ); } + + // Start the polling p.then(() => tgBot.startPolling()); } From 915ea71f21862fa205f06717383f0d2fdb7d2834 Mon Sep 17 00:00:00 2001 From: Simen de Lange Date: Mon, 27 May 2019 22:55:48 +0200 Subject: [PATCH 050/102] Got it mostly working again --- lib/telegram2discord/middlewares.js | 3 +- lib/telegram2discord/setup.js | 177 +++++++--------------------- 2 files changed, 47 insertions(+), 133 deletions(-) diff --git a/lib/telegram2discord/middlewares.js b/lib/telegram2discord/middlewares.js index 47fc2a65..fab763e5 100644 --- a/lib/telegram2discord/middlewares.js +++ b/lib/telegram2discord/middlewares.js @@ -242,7 +242,7 @@ function addTextObj(ctx, next) { ctx.tediCross.text = { raw: ctx.tediCross.sticker.emoji, entities: [] - } + }; } next(); @@ -260,6 +260,7 @@ function addTextObj(ctx, next) { function addFileObj(ctx, next) { const message = ctx.tediCross.message; + // Figure out if a file is present if (!R.isNil(message.audio)) { // Audio ctx.tediCross.file = { diff --git a/lib/telegram2discord/setup.js b/lib/telegram2discord/setup.js index ff274dea..2d840afd 100644 --- a/lib/telegram2discord/setup.js +++ b/lib/telegram2discord/setup.js @@ -11,6 +11,8 @@ const mime = require("mime/lite"); const Discord = require("discord.js"); const request = require("request"); const middlewares = require("./middlewares"); +const From = require("./From"); +const handleEntities = require("./handleEntities"); /** * Creates a function which sends files from Telegram to discord @@ -104,7 +106,10 @@ function makeNameObject(user) { * @private */ const createMessageHandler = R.curry((func, ctx) => { - ctx.tediCross.bridges.forEach((bridge) => func(ctx, bridge)); + // Wait for the Discord bot to become ready + ctx.dcBot.ready.then(() => + ctx.tediCross.bridges.forEach((bridge) => func(ctx, bridge)) + ); }); /********************** @@ -130,6 +135,7 @@ function setup(logger, tgBot, dcBot, messageMap, bridgeMap, settings) { // Add some global context tgBot.context.self = me; // XXX For some strange reason Telegraf crashes if I try to call the prop `me` tgBot.context.bridgeMap = bridgeMap; + tgBot.context.dcBot = dcBot; // Apply middlewares tgBot.use(middlewares.addTediCrossObj); @@ -139,34 +145,50 @@ function setup(logger, tgBot, dcBot, messageMap, bridgeMap, settings) { tgBot.use(middlewares.informThisIsPrivateBot); tgBot.use(middlewares.removeD2TBridges); tgBot.command(middlewares.removeBridgesIgnoringCommands); - tgBot.use(middlewares.addFromObj); // Not used at the moment - tgBot.use(middlewares.addReplyObj); // Not used at the moment - tgBot.use(middlewares.addForwardFrom); // Not used at the moment - tgBot.use(middlewares.addTextObj); // Not used at the moment + tgBot.use(middlewares.addFromObj); + tgBot.use(middlewares.addReplyObj); + tgBot.use(middlewares.addForwardFrom); + tgBot.use(middlewares.addTextObj); tgBot.use(middlewares.addFileObj); // Not used at the moment tgBot.use((ctx, next) => {console.log(ctx.tediCross); next();}); - // Make the file sender - const sendFile = makeFileSender(tgBot, dcBot, messageMap, settings); - - // Set up event listener for text messages from Telegram - tgBot.on("text", createMessageHandler(async (ctx, bridge) => { - const message = ctx.tediCross.message; + // Prepare and send the message to Discord + tgBot.use(createMessageHandler(async (ctx, bridge) => { + // Shorthand for the tediCross context + const tc = ctx.tediCross; + + // Make the header + const header = R.ifElse( + R.always(bridge.telegram.sendUsernames), + tc => { + // Get the name of the sender of this message + const senderName = From.makeDisplayName(settings.telegram.useFirstNameInsteadOfUsername, tc.from); + + let header = `**${senderName}**`; + + if (!R.isNil(tc.replyTo)) { + // TODO Handle replies to the TG bot (i.e. cross-replies) + // TODO Handle inline replies + const repliedToName = From.makeDisplayName(settings.telegram.useFirstNameInsteadOfUsername, tc.replyTo.originalFrom); + header = `**${senderName}** (in reply to **${repliedToName}**)`; + } else if (!R.isNil(tc.forwardFrom)) { + const origSender = From.makeDisplayName(settings.telegram.useFirstNameInsteadOfUsername, tc.forwardFrom); + header = `**${origSender}** (forwarded by **${senderName}**)`; + } + return header; + }, + R.always("") + )(tc); - // Turn the text discord friendly - const messageObj = messageConverter(message, tgBot, settings, dcBot, bridge); + // Make the text to send + const text = handleEntities(tc.text.raw, tc.text.entities, ctx.dcBot, bridge); try { - // Pass it on to Discord when the dcBot is ready - await dcBot.ready; - // Get the channel to send to const channel = dcBot.channels.get(bridge.discord.channelId); - // Make the header - let header = bridge.telegram.sendUsernames ? `**${messageObj.from}**` : ""; - +/* // Handle replies if (messageObj.reply !== null) { // Add the reply data to the header @@ -188,9 +210,10 @@ function setup(logger, tgBot, dcBot, messageMap, bridgeMap, settings) { header = `${header.slice(0, -1)}: _${messageObj.reply.text}_`; } } +*/ // Discord doesn't handle messages longer than 2000 characters. Split it up into chunks that big - const messageText = header + "\n" + messageObj.text; + const messageText = header + "\n" + text; const chunks = R.splitEvery(2000, messageText); // Send them in serial @@ -200,120 +223,10 @@ function setup(logger, tgBot, dcBot, messageMap, bridgeMap, settings) { } // Make the mapping so future edits can work XXX Only the last chunk is considered - messageMap.insert(MessageMap.TELEGRAM_TO_DISCORD, bridge, message.message_id, dcMessage.id); + messageMap.insert(MessageMap.TELEGRAM_TO_DISCORD, bridge, tc.message.message_id, dcMessage.id); } catch (err) { logger.error(`[${bridge.name}] Discord did not accept a text message:`, err); - logger.error(`[${bridge.name}] Failed message:`, message.text); - } - })); - - // Set up event listener for photo messages from Telegram - tgBot.on("photo", createMessageHandler(async (ctx, bridge) => { - - const message = ctx.tediCross.message; - - try { - await sendFile(bridge, { - message, - fileId: message.photo[message.photo.length-1].file_id, - fileName: "photo.jpg", // Telegram will convert it to jpg no matter what filetype is actually sent - caption: message.caption - }); - } catch (err) { - logger.error(`[${bridge.name}] Could not send photo`, err); - } - })); - - // Set up event listener for stickers from Telegram - tgBot.on("sticker", createMessageHandler(async (ctx, bridge) => { - - const message = ctx.tediCross.message; - - try { - await sendFile(bridge, { - message, - fileId: message.sticker.file_id, - fileName: "sticker.webp", // Telegram will insist that it is a jpg, but it really is a webp - caption: settings.telegram.sendEmojiWithStickers ? message.sticker.emoji : undefined - }); - } catch (err) { - logger.error(`[${bridge.name}] Could not send sticker`, err); - } - })); - - // Set up event listener for filetypes not caught by the other filetype handlers - tgBot.on("document", createMessageHandler(async (ctx, bridge) => { - - const message = ctx.tediCross.message; - - // message.file_name can for some reason be undefined some times. Default to "file.ext" - let fileName = message.document.file_name; - if (fileName === undefined) { - fileName = "file." + mime.getExtension(message.document.mime_type); - } - - try { - // Pass it on to Discord - await sendFile(bridge, { - message, - fileId: message.document.file_id, - fileName: fileName, - resolveExtension: false - }); - } catch (err) { - logger.error(`[${bridge.name}] Could not send document`, err); - } - })); - - // Set up event listener for voice messages - tgBot.on("voice", createMessageHandler(async (ctx, bridge) => { - - const message = ctx.tediCross.message; - - try { - await sendFile(bridge, { - message, - fileId: message.voice.file_id, - fileName: "voice" + "." + mime.getExtension(message.voice.mime_type), - resolveExtension: false - }); - } catch (err) { - logger.error(`[${bridge.name}] Could not send voice`, err); - } - })); - - // Set up event listener for audio messages - tgBot.on("audio", createMessageHandler(async (ctx, bridge) => { - - const message = ctx.tediCross.message; - - try { - await sendFile(bridge, { - message, - fileId: message.audio.file_id, - fileName: message.audio.title, - resolveExtension: true - }); - } catch (err) { - logger.error(`[${bridge.name}] Could not send audio`, err); - } - })); - - // Set up event listener for video messages - tgBot.on("video", createMessageHandler(async (ctx, bridge) => { - - const message = ctx.tediCross.message; - - try { - await sendFile(bridge, { - message, - caption: message.caption, - fileId: message.video.file_id, - fileName: "video" + "." + mime.getExtension(message.video.mime_type), - resolveExtension: false - }); - } catch (err) { - logger.error(`[${bridge.name}] Could not send video`, err); + logger.error(`[${bridge.name}] Failed message:`, text); } })); From 514277b0e8fe5c6b0bcd77513d7cc79a28f68349 Mon Sep 17 00:00:00 2001 From: Simen de Lange Date: Tue, 28 May 2019 22:00:56 +0200 Subject: [PATCH 051/102] More middleware stuff and tried nesting up logic --- lib/telegram2discord/messageConverter.js | 12 +- lib/telegram2discord/middlewares.js | 127 +++++++++------ lib/telegram2discord/setup.js | 187 +++++++++-------------- 3 files changed, 158 insertions(+), 168 deletions(-) diff --git a/lib/telegram2discord/messageConverter.js b/lib/telegram2discord/messageConverter.js index 800c1061..76a8c838 100644 --- a/lib/telegram2discord/messageConverter.js +++ b/lib/telegram2discord/messageConverter.js @@ -6,6 +6,7 @@ const handleEntities = require("./handleEntities"); const R = require("ramda"); +const From = require("./From"); /*********** * Helpers * @@ -82,7 +83,9 @@ function getDisplayNameFromUser(user, useFirstNameInsteadOfUsername) { * * @return {Object} A object containing message information as from, text etc */ -function messageConverter(message, tgBot, settings, dcBot, bridge) { +function messageConverter(ctx, tgBot, settings, dcBot, bridge) { + const message = ctx.tediCross.message; + // Convert the text to Discord format const text = handleEntities(message.text, message.entities, dcBot, bridge); @@ -93,14 +96,13 @@ function messageConverter(message, tgBot, settings, dcBot, bridge) { let fromName = getDisplayName(message.from, message.chat, settings.telegram.useFirstNameInsteadOfUsername); // Check if it is a reply - if (message.reply_to_message !== undefined) { - + if (!R.isNil(ctx.tediCross.replyTo)) { // Get the name of the user this is a reply to let originalAuthor = getDisplayName(message.reply_to_message.from, message.chat, settings.telegram.useFirstNameInsteadOfUsername); let originalText = ""; // Is this a reply to the bot, i.e. to a Discord user? - if (message.reply_to_message.from !== undefined && message.reply_to_message.from.id === tgBot.context.self.id) { + if (message.reply_to_message.from !== undefined && message.reply_to_message.from.id === tgBot.context.TediCross.me.id) { // Get the name of the Discord user this is a reply to const dcUsername = message.reply_to_message.text.split("\n")[0]; const dcUser = dcBot.channels.get(bridge.discord.channelId).members.find(findFn("displayName", new RegExp(dcUsername))); @@ -112,7 +114,7 @@ function messageConverter(message, tgBot, settings, dcBot, bridge) { originalText = message.reply_to_message.text; // Is this a reply to the bot, i.e. to a Discord user? - if (message.reply_to_message.from !== undefined && message.reply_to_message.from.id === tgBot.context.self.id) { + if (message.reply_to_message.from !== undefined && message.reply_to_message.from.id === tgBot.context.TediCross.me.id) { [ , ...originalText] = message.reply_to_message.text.split("\n"); originalText = originalText.join("\n"); } diff --git a/lib/telegram2discord/middlewares.js b/lib/telegram2discord/middlewares.js index fab763e5..90b32e5c 100644 --- a/lib/telegram2discord/middlewares.js +++ b/lib/telegram2discord/middlewares.js @@ -10,6 +10,39 @@ const From = require("./From"); const mime = require("mime/lite"); const request = require("request"); +/*********** + * Helpers * + ***********/ + +/** + * Creates a text object from a Telegram message + * + * @param {Object} message The message object + * + * @returns {Object} The text object, or undefined if no text was found + */ +function createTextObjFromMessage(message) { + return R.cond([ + // Text + [R.has("text"), ({ text, entities }) => ({ + raw: text, + entities: R.defaultTo([], entities) + })], + // Animation, audio, document, photo, video or voice + [R.has("caption"), ({ caption, caption_entities }) => ({ + raw: caption, + entities: R.defaultTo([], caption_entities) + })], + // Stickers have an emoji instead of text + [R.has("sticker"), message => ({ + raw: R.path(["sticker", "emoji"], message), + entities: [] + })], + // Default to undefined + [R.T, R.always(undefined)] + ])(message); +} + /**************************** * The middleware functions * ****************************/ @@ -17,7 +50,7 @@ const request = require("request"); /** * Adds a `tediCross` property to the context * - * @param {Object} context The context to add the property to + * @param {Object} ctx The context to add the property to * @param {Function} next Function to pass control to next middleware * * @returns {undefined} @@ -28,7 +61,7 @@ function addTediCrossObj(ctx, next) { } /** - * Replies to a message with info about the chat. One of the four optional arguments must be present. Requires the tediCross context to work + * Adds a message object to the tediCross context. One of the four optional arguments must be present. Requires the tediCross context to work * * @param {Object} ctx The Telegraf context * @param {Object} ctx.tediCross The TediCross object on the context @@ -40,9 +73,9 @@ function addTediCrossObj(ctx, next) { * * @returns {undefined} */ -function getMessageObj(ctx, next) { - // Get the proper message object - const message = R.cond([ +function addMessageObj(ctx, next) { + // 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.channelPost), R.prop("channelPost")], [ctx => !R.isNil(ctx.editedChannelPost), R.prop("editedChannelPost")], @@ -50,9 +83,6 @@ function getMessageObj(ctx, next) { [ctx => !R.isNil(ctx.editedMessage), R.prop("editedMessage")] ])(ctx); - // Put it on the context - ctx.tediCross.message = message; - next(); } @@ -76,12 +106,14 @@ function chatinfo(ctx) { * * @param {Object} ctx The context to add the property to * @param {Object} ctx.tediCross The TediCross object on the context + * @param {Object} ctx.TediCross The global TediCross context + * @param {Object} ctx.TediCross.bridgeMap The bridge map of the application * @param {Function} next Function to pass control to next middleware * * @returns {undefined} */ function addBridgesToContext(ctx, next) { - ctx.tediCross.bridges = ctx.bridgeMap.fromTelegramChatId(ctx.tediCross.message.chat.id); + ctx.tediCross.bridges = ctx.TediCross.bridgeMap.fromTelegramChatId(ctx.tediCross.message.chat.id); next(); } @@ -182,7 +214,8 @@ function addReplyObj(ctx, next) { ctx.tediCross.replyTo = { message: repliedToMessage, originalFrom: From.createFromObjFromMessage(repliedToMessage), - isReplyToTediCross: !R.isNil(repliedToMessage.from) && repliedToMessage.from.id === ctx.self.id + text: R.defaultTo(undefined, createTextObjFromMessage(repliedToMessage)), + isReplyToTediCross: !R.isNil(repliedToMessage.from) && R.equals(repliedToMessage.from.id, ctx.TediCross.me.id) }; } @@ -199,7 +232,7 @@ function addReplyObj(ctx, next) { * @returns {undefined} */ function addForwardFrom(ctx, next) { - const msg = ctx.message; + const msg = ctx.tediCross.message; if (!R.isNil(msg.forward_from) || !R.isNil(msg.forward_from_chat)) { ctx.tediCross.forwardFrom = R.ifElse( @@ -216,7 +249,7 @@ function addForwardFrom(ctx, next) { } /** - * Adds a text object to the tediCross property on the context + * Adds a text object to the tediCross property on the context, if there is text in the message * * @param {Object} ctx The context to add the property to * @param {Object} ctx.tediCross The tediCross on the context @@ -225,24 +258,10 @@ function addForwardFrom(ctx, next) { * @returns {undefined} */ function addTextObj(ctx, next) { - if (!R.isNil(ctx.tediCross.message.text)) { - // Text - ctx.tediCross.text = { - raw: ctx.tediCross.message.text, - entities: R.defaultTo([], ctx.tediCross.message.entities) - }; - } else if (!R.isNil(ctx.tediCross.message.caption)) { - // Animation, audio, document, photo, video or voice, - ctx.tediCross.text = { - raw: ctx.tediCross.message.caption, - entities: R.defaultTo([], ctx.tediCross.message.caption_entities) - }; - } else if (!R.isNil(ctx.tediCross.sticker)) { - // Stickers have an emoji instead of text - ctx.tediCross.text = { - raw: ctx.tediCross.sticker.emoji, - entities: [] - }; + const text = createTextObjFromMessage(ctx.tediCross.message); + + if (!R.isNil(text)) { + ctx.tediCross.text = text; } next(); @@ -265,58 +284,71 @@ function addFileObj(ctx, next) { // Audio ctx.tediCross.file = { type: "audio", - fileId: message.audio.file_id, - fileName: message.audio.title + "." + mime.getExtension(message.audio.mime_type) + id: message.audio.file_id, + name: message.audio.title + "." + mime.getExtension(message.audio.mime_type) }; } else if (!R.isNil(message.document)) { // Generic file ctx.tediCross.file = { type: "document", - fileId: message.document.file_id, - fileName: message.document.file_name + id: message.document.file_id, + name: message.document.file_name }; } else if (!R.isNil(message.photo)) { // Photo. It has an array of photos of different sizes. Use the first and biggest const photo = R.head(message.photo); ctx.tediCross.file = { type: "photo", - fileId: photo.file_id, - fileName: "photo.jpg" // Telegram will convert it to a jpg no matter which format is orignally sent + id: photo.file_id, + name: "photo.jpg" // Telegram will convert it to a jpg no matter which format is orignally sent }; } else if (!R.isNil(message.sticker)) { // Sticker ctx.tediCross.file = { type: "sticker", - fileId: message.sticker.file_id, - fileName: "sticker.webp" + id: message.sticker.file_id, + name: "sticker.webp" }; } else if (!R.isNil(message.video)) { // Video ctx.tediCross.file = { type: "video", - fileId: message.video.file_id, - fileName: "video" + "." + mime.getExtension(message.video.mime_type), + id: message.video.file_id, + name: "video" + "." + mime.getExtension(message.video.mime_type), }; } else if (!R.isNil(message.voice)) { // Voice ctx.tediCross.file = { type: "voice", - fileId: message.voice.file_id, - fileName: "voice" + "." + mime.getExtension(message.voice.mime_type), + id: message.voice.file_id, + name: "voice" + "." + mime.getExtension(message.voice.mime_type), }; } - Promise.resolve() + next(); +} + +/** + * Adds a file stream to the file object on the tedicross context, if there is one + * + * @param {Object} ctx The context to add the property to + * @param {Object} ctx.tediCross The tediCross on the context + * + * @returns {Promise} Promise resolving to nothing when the operation is complete + */ +function addFileStream(ctx, next) { + return Promise.resolve() .then(() => { // Get a stream to the file, if one was found if (!R.isNil(ctx.tediCross.file)) { - return ctx.telegram.getFileLink(ctx.tediCross.file.fileId) + return ctx.telegram.getFileLink(ctx.tediCross.file.id) .then(fileLink => { ctx.tediCross.file.stream = request(fileLink); }); } }) - .then(next); + .then(next) + .then(R.always(undefined)); } /*************** @@ -325,7 +357,7 @@ function addFileObj(ctx, next) { module.exports = { addTediCrossObj, - getMessageObj, + addMessageObj, chatinfo, addBridgesToContext, removeD2TBridges, @@ -335,5 +367,6 @@ module.exports = { addReplyObj, addForwardFrom, addTextObj, - addFileObj + addFileObj, + addFileStream }; diff --git a/lib/telegram2discord/setup.js b/lib/telegram2discord/setup.js index 2d840afd..59b55697 100644 --- a/lib/telegram2discord/setup.js +++ b/lib/telegram2discord/setup.js @@ -7,68 +7,31 @@ const messageConverter = require("./messageConverter"); const MessageMap = require("../MessageMap"); const R = require("ramda"); -const mime = require("mime/lite"); -const Discord = require("discord.js"); -const request = require("request"); const middlewares = require("./middlewares"); const From = require("./From"); const handleEntities = require("./handleEntities"); /** - * Creates a function which sends files from Telegram to discord + * Clears old messages on a tgBot, making sure there are no updates in the queue * - * @param {Telegraf} tgBot The Telegram bot - * @param {Discord.Client} dcBot The Discord bot - * @param {MessageMap} messageMap Map between IDs of messages - * @param {Settings} settings The settings to use - * - * @returns {Function} A function which can be used to send files from Telegram to Discord + * @param {Telegraf} tgBot The Telegram bot to clear messages on * - * @private + * @returns {Promise} Promise resolving to nothing when the clearing is done */ -function makeFileSender(tgBot, dcBot, messageMap, settings) { - /** - * Sends a file to Discord - * - * @param {String} arg.discordChannel Discord channel ID - * @param {Message} arg.message The message the file comes from - * @param {String} arg.fileId ID of the file to download from Telegram's servers - * @param {String} arg.fileName Name of the file to send - * @param {String} [arg.caption] Additional text to send with the file - * @param {Boolean} [arg.resolveExtension] Set to true if the bot should try to find the file extension itself, in which case it will be appended to the file name. Defaults to false - */ - return async function (bridge, {message, fileId, fileName, caption = "", resolveExtension = false}) { - // Make the text to send - const messageObj = messageConverter(message, tgBot, settings, dcBot, bridge); - const textToSend = bridge.telegram.sendUsernames - ? `**${messageObj.from}**:\n${caption}` - : caption - ; - - // Wait for the Discord bot to become ready - await dcBot.ready; - - // Start getting the file - const [file, fileLink] = await Promise.all([ - tgBot.telegram.getFile(fileId), - tgBot.telegram.getFileLink(fileId) - ]); - const fileStream = request(fileLink); - - // Get the extension, if necessary - const extension = resolveExtension - ? "." + file.file_path.split(".").pop() - : "" - ; - - // Send it to Discord - const dcMessage = await dcBot.channels.get(bridge.discord.channelId).send( - textToSend, - new Discord.Attachment(fileStream, fileName + extension) - ); - - messageMap.insert(MessageMap.TELEGRAM_TO_DISCORD, bridge, message.message_id, dcMessage.id); - }; +function clearOldMessages(tgBot, offset = -1) { + const timeout = 0; + const limit = 100; + return tgBot.telegram.getUpdates(timeout, limit, offset) + .then(R.ifElse( + R.isEmpty, + R.always(undefined), + R.compose( + newOffset => clearOldMessages(tgBot, newOffset), + R.add(1), + R.prop("update_id"), + R.last + ) + )); } /** @@ -107,7 +70,7 @@ function makeNameObject(user) { */ const createMessageHandler = R.curry((func, ctx) => { // Wait for the Discord bot to become ready - ctx.dcBot.ready.then(() => + ctx.TediCross.dcBot.ready.then(() => ctx.tediCross.bridges.forEach((bridge) => func(ctx, bridge)) ); }); @@ -127,19 +90,27 @@ const createMessageHandler = R.curry((func, ctx) => { * @param {Settings} settings The settings to use */ function setup(logger, tgBot, dcBot, messageMap, bridgeMap, settings) { - tgBot.ready = tgBot.telegram.getMe() - .then(me => { + tgBot.ready = Promise.all([ + // Get info about the bot + tgBot.telegram.getMe(), + // Clear old messages, if wanted + settings.telegram.skipOldMessages ? clearOldMessages(tgBot) : Promise.resolve() + ]) + .then(([me]) => { // Log the bot's info logger.info(`Telegram: ${me.username} (${me.id})`); // Add some global context - tgBot.context.self = me; // XXX For some strange reason Telegraf crashes if I try to call the prop `me` - tgBot.context.bridgeMap = bridgeMap; - tgBot.context.dcBot = dcBot; + tgBot.context.TediCross = { + me, + bridgeMap, + dcBot, + settings + }; // Apply middlewares tgBot.use(middlewares.addTediCrossObj); - tgBot.use(middlewares.getMessageObj); + tgBot.use(middlewares.addMessageObj); tgBot.command("chatinfo", middlewares.chatinfo); tgBot.use(middlewares.addBridgesToContext); tgBot.use(middlewares.informThisIsPrivateBot); @@ -150,8 +121,39 @@ function setup(logger, tgBot, dcBot, messageMap, bridgeMap, settings) { tgBot.use(middlewares.addForwardFrom); tgBot.use(middlewares.addTextObj); tgBot.use(middlewares.addFileObj); // Not used at the moment + tgBot.use(middlewares.addFileStream); // Not used at the moment - tgBot.use((ctx, next) => {console.log(ctx.tediCross); next();}); +tgBot.use((ctx, next) => {console.log(ctx.tediCross); next();}); + + // Set up event listener for message edits + const handleEdits = createMessageHandler(async (ctx, bridge) => { + try { + const tgMessage = ctx.tediCross.message; + + // Wait for the Discord bot to become ready + await dcBot.ready; + + // Find the ID of this message on Discord + const [dcMessageId] = messageMap.getCorresponding(MessageMap.TELEGRAM_TO_DISCORD, bridge, tgMessage.message_id); + + // Get the messages from Discord + const dcMessage = await dcBot.channels.get(bridge.discord.channelId).fetchMessage(dcMessageId); + + const messageObj = messageConverter(ctx, tgBot, settings, dcBot, bridge); + + // Try to edit the message + const textToSend = bridge.telegram.sendUsernames + ? `**${messageObj.from}**\n${messageObj.text}` + : messageObj.text + ; + await dcMessage.edit(textToSend); + } catch (err) { + // Log it + logger.error(`[${bridge.name}] Could not edit Discord message:`, err); + } + }); + tgBot.on("edited_message", handleEdits); + tgBot.on("edited_channel_post", handleEdits); // Prepare and send the message to Discord tgBot.use(createMessageHandler(async (ctx, bridge) => { @@ -165,14 +167,17 @@ function setup(logger, tgBot, dcBot, messageMap, bridgeMap, settings) { // Get the name of the sender of this message const senderName = From.makeDisplayName(settings.telegram.useFirstNameInsteadOfUsername, tc.from); + // Make the default header let header = `**${senderName}**`; if (!R.isNil(tc.replyTo)) { + // Add reply info to the header // TODO Handle replies to the TG bot (i.e. cross-replies) // TODO Handle inline replies const repliedToName = From.makeDisplayName(settings.telegram.useFirstNameInsteadOfUsername, tc.replyTo.originalFrom); header = `**${senderName}** (in reply to **${repliedToName}**)`; } else if (!R.isNil(tc.forwardFrom)) { + // Handle forwards const origSender = From.makeDisplayName(settings.telegram.useFirstNameInsteadOfUsername, tc.forwardFrom); header = `**${origSender}** (forwarded by **${senderName}**)`; } @@ -182,13 +187,13 @@ function setup(logger, tgBot, dcBot, messageMap, bridgeMap, settings) { )(tc); // Make the text to send - const text = handleEntities(tc.text.raw, tc.text.entities, ctx.dcBot, bridge); + const text = handleEntities(tc.text.raw, tc.text.entities, ctx.TediCross.dcBot, bridge); try { // Get the channel to send to const channel = dcBot.channels.get(bridge.discord.channelId); -/* + /* // Handle replies if (messageObj.reply !== null) { // Add the reply data to the header @@ -276,59 +281,9 @@ function setup(logger, tgBot, dcBot, messageMap, bridgeMap, settings) { logger.error(`[${bridge.name}] Could not notify Discord about a user that left Telegram`, err); } })); - - // Set up event listener for message edits - const handleEdits = createMessageHandler(async (ctx, bridge) => { - try { - const tgMessage = ctx.tediCross.message; - - // Wait for the Discord bot to become ready - await dcBot.ready; - - // Find the ID of this message on Discord - const [dcMessageId] = messageMap.getCorresponding(MessageMap.TELEGRAM_TO_DISCORD, bridge, tgMessage.message_id); - - // Get the messages from Discord - const dcMessage = await dcBot.channels.get(bridge.discord.channelId).fetchMessage(dcMessageId); - - const messageObj = messageConverter(tgMessage, tgBot, settings, dcBot, bridge); - - // Try to edit the message - const textToSend = bridge.telegram.sendUsernames - ? `**${messageObj.from}**\n${messageObj.text}` - : messageObj.text - ; - await dcMessage.edit(textToSend); - } catch (err) { - // Log it - logger.error(`[${bridge.name}] Could not edit Discord message:`, err); - } - }); - tgBot.on("edited_message", handleEdits); - tgBot.on("edited_channel_post", handleEdits); }) - .catch(err => { - // Log the error( - logger.error("Failed at getting the Telegram bot's me-object:", err); - - // Pass it on - throw err; - }); - - /* Start getting updates */ - let p = Promise.resolve(); - - // Clear old updates if wanted - if (settings.telegram.skipOldMessages) { - p = tgBot.telegram.getUpdates(0, 100, -1) - .then(updates => updates.length > 0 - ? tgBot.telegram.getUpdates(0, 100, updates[updates.length-1].update_id) - : [] - ); - } - - // Start the polling - p.then(() => tgBot.startPolling()); + // Start getting updates + .then(() => tgBot.startPolling()); } /***************************** From 1513ce107a123efc70e009ab4014d9d0fad9ded4 Mon Sep 17 00:00:00 2001 From: Simen de Lange Date: Sat, 1 Jun 2019 15:02:34 +0200 Subject: [PATCH 052/102] Got the header correct for replies --- README.md | 1 + example.settings.yaml | 1 + lib/settings/DiscordSettings.js | 12 ++++ lib/settings/Settings.js | 5 ++ lib/telegram2discord/messageConverter.js | 1 - lib/telegram2discord/middlewares.js | 21 +++++- lib/telegram2discord/setup.js | 89 ++++++++++++++++++++++-- 7 files changed, 120 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 9d3056e1..746aacb7 100644 --- a/README.md +++ b/README.md @@ -62,6 +62,7 @@ As mentioned in the step by step installation guide, there is a settings file. H * `useNickname`: Uses the sending user's nickname instead of username when relaying messages to Telegram * `displayTelegramReplies`: How to display Telegram replies. Either the string `inline` or `embed` * `replyLength`: How many characters of the original message to display on replies + * `maxReplyLines`: How many lines of the original message to display on replies * `debug`: If set to `true`, activates debugging output from the bot. Defaults to `false` * `bridges`: An array containing all your chats and channels. For each object in this array, you should have the following properties: * `name`: A internal name of the chat. Appears in the log diff --git a/example.settings.yaml b/example.settings.yaml index 038ae453..7ac8a6e0 100644 --- a/example.settings.yaml +++ b/example.settings.yaml @@ -10,6 +10,7 @@ discord: skipOldMessages: true displayTelegramReplies: embed replyLength: 100 + maxReplyLines: 2 bridges: - name: First bridge direction: both diff --git a/lib/settings/DiscordSettings.js b/lib/settings/DiscordSettings.js index 98dc6a5b..4cf9b814 100644 --- a/lib/settings/DiscordSettings.js +++ b/lib/settings/DiscordSettings.js @@ -63,6 +63,13 @@ class DiscordSettings { * @type {Integer} */ this.replyLength = settings.replyLength; + + /** + * How many lines of the original message to show in replies from Telegram + * + * @type {Integer} + */ + this.maxReplyLines = settings.maxReplyLines; } /** @@ -133,6 +140,11 @@ class DiscordSettings { if (!Number.isInteger(settings.replyLength) || settings.replyLength <= 0) { throw new ("`settings.replyLength` must be an integer greater than 0"); } + + // Check that `maxReplyLines` is an integer + if (!Number.isInteger(settings.maxReplyLines) || settings.maxReplyLines <= 0) { + throw new ("`settings.maxReplyLines` must be an integer greater than 0"); + } } /** diff --git a/lib/settings/Settings.js b/lib/settings/Settings.js index 00c10493..4d512661 100644 --- a/lib/settings/Settings.js +++ b/lib/settings/Settings.js @@ -174,6 +174,11 @@ class Settings { } } + // 2019-05-31: Add the `maxReplyLines` option to Discord + if (R.isNil(settings.discord.maxReplyLines)) { + settings.discord.maxReplyLines = 2; + } + // All done! return settings; } diff --git a/lib/telegram2discord/messageConverter.js b/lib/telegram2discord/messageConverter.js index 76a8c838..dd714398 100644 --- a/lib/telegram2discord/messageConverter.js +++ b/lib/telegram2discord/messageConverter.js @@ -6,7 +6,6 @@ const handleEntities = require("./handleEntities"); const R = require("ramda"); -const From = require("./From"); /*********** * Helpers * diff --git a/lib/telegram2discord/middlewares.js b/lib/telegram2discord/middlewares.js index 90b32e5c..e1bf20c1 100644 --- a/lib/telegram2discord/middlewares.js +++ b/lib/telegram2discord/middlewares.js @@ -39,7 +39,7 @@ function createTextObjFromMessage(message) { entities: [] })], // Default to undefined - [R.T, R.always(undefined)] + [R.T, R.always({ raw: "", entities: [] })] ])(message); } @@ -211,12 +211,27 @@ function addReplyObj(ctx, next) { if (!R.isNil(repliedToMessage)) { // This is a reply + const isReplyToTediCross = !R.isNil(repliedToMessage.from) && R.equals(repliedToMessage.from.id, ctx.TediCross.me.id); ctx.tediCross.replyTo = { + isReplyToTediCross, message: repliedToMessage, originalFrom: From.createFromObjFromMessage(repliedToMessage), - text: R.defaultTo(undefined, createTextObjFromMessage(repliedToMessage)), - isReplyToTediCross: !R.isNil(repliedToMessage.from) && R.equals(repliedToMessage.from.id, ctx.TediCross.me.id) + text: createTextObjFromMessage(repliedToMessage), }; + + // Handle replies to TediCross + if (isReplyToTediCross) { + // Get the username of the Discord user who sent this and remove it from the text + const split = R.split("\n", ctx.tediCross.replyTo.text.raw); + ctx.tediCross.replyTo.dcUsername = R.head(split); + ctx.tediCross.replyTo.text.raw = R.join("\n", R.tail(split)); + ctx.tediCross.replyTo.text.entities = R.tail(ctx.tediCross.replyTo.text.entities); + } + + // Turn the original text into "" if there is no text + if (R.isEmpty(ctx.tediCross.replyTo.text.raw)) { + ctx.tediCross.replyTo.text.raw = ""; + } } next(); diff --git a/lib/telegram2discord/setup.js b/lib/telegram2discord/setup.js index 59b55697..d695c2a8 100644 --- a/lib/telegram2discord/setup.js +++ b/lib/telegram2discord/setup.js @@ -11,6 +11,10 @@ const middlewares = require("./middlewares"); const From = require("./From"); const handleEntities = require("./handleEntities"); +/*********** + * Helpers * + ***********/ + /** * Clears old messages on a tgBot, making sure there are no updates in the queue * @@ -34,6 +38,56 @@ function clearOldMessages(tgBot, offset = -1) { )); } +/** + * Makes the reply text to show on Discord + * + * @param {Object} replyTo The replyTo object from the tediCross context + * @param {Integer} replyLength How many characters to take from the original + * @param {Integer} maxReplyLines How many lines to cut the reply text after + * + * @returns {String} The reply text to display + */ +function makeReplyText(replyTo, replyLength, maxReplyLines) { + // Make the reply string + return R.compose( + // Add ellipsis if the text was cut + R.ifElse( + R.compose( + R.equals(R.length(replyTo.text.raw)), + R.length + ), + R.identity, + R.concat(R.__, "…") + ), + // Take only a number of lines + R.join("\n"), + R.slice(0, maxReplyLines), + R.split("\n"), + // Take only a portion of the text + R.slice(0, replyLength), + )(replyTo.text.raw); +} + +/** + * Makes a discord mention out of a username + * + * @param {String} username The username to make the mention from + * @param {Discord.Client} dcBot The Discord bot to look up the user's ID with + * @param {String} channelId ID of the Discord channel to look up the username in + * + * @returns {String} A Discord mention of the user + */ +function makeDiscordMention(username, dcBot, channelId) { + // Get the name of the Discord user this is a reply to + const dcUser = dcBot.channels.get(channelId).members.find(R.propEq("displayName", username)); + + return R.ifElse( + R.isNil, + R.always(username), + dcUser => `<@${dcUser.id}>` + )(dcUser); +} + /** * Makes a name object (for lack of better term) of a user object. It contains the user's full name, and the username or the text `No username` * @@ -125,6 +179,7 @@ function setup(logger, tgBot, dcBot, messageMap, bridgeMap, settings) { tgBot.use((ctx, next) => {console.log(ctx.tediCross); next();}); +/* Old */ // Set up event listener for message edits const handleEdits = createMessageHandler(async (ctx, bridge) => { try { @@ -154,6 +209,7 @@ tgBot.use((ctx, next) => {console.log(ctx.tediCross); next();}); }); tgBot.on("edited_message", handleEdits); tgBot.on("edited_channel_post", handleEdits); +/* /Old */ // Prepare and send the message to Discord tgBot.use(createMessageHandler(async (ctx, bridge) => { @@ -172,10 +228,31 @@ tgBot.use((ctx, next) => {console.log(ctx.tediCross); next();}); if (!R.isNil(tc.replyTo)) { // Add reply info to the header - // TODO Handle replies to the TG bot (i.e. cross-replies) - // TODO Handle inline replies - const repliedToName = From.makeDisplayName(settings.telegram.useFirstNameInsteadOfUsername, tc.replyTo.originalFrom); - header = `**${senderName}** (in reply to **${repliedToName}**)`; + const repliedToName = R.ifElse( + R.prop("isReplyToTediCross"), + R.compose( + username => makeDiscordMention(username, ctx.TediCross.dcBot, bridge.discord.channelId), + R.prop("dcUsername") + ), + R.compose( + R.partial(From.makeDisplayName, [ctx.TediCross.settings.telegram.useFirstNameInsteadOfUsername]), + R.prop("originalFrom") + ) + )(tc.replyTo); + + + header = `**${senderName}** (in reply to **${repliedToName}**`; + + if (ctx.TediCross.settings.discord.displayTelegramReplies === "inline") { + // Make the reply text + const replyText = makeReplyText(tc.replyTo, ctx.TediCross.settings.discord.replyLength, ctx.TediCross.settings.discord.maxReplyLines); + + // Put the reply text in the header, replacing newlines with spaces + header = `${header}: _${R.replace(/\n/g, " ", replyText)}_)`; + } else { + // Append the closing parenthesis + header = `${header})`; + } } else if (!R.isNil(tc.forwardFrom)) { // Handle forwards const origSender = From.makeDisplayName(settings.telegram.useFirstNameInsteadOfUsername, tc.forwardFrom); @@ -193,7 +270,7 @@ tgBot.use((ctx, next) => {console.log(ctx.tediCross); next();}); // Get the channel to send to const channel = dcBot.channels.get(bridge.discord.channelId); - /* +/* Old // Handle replies if (messageObj.reply !== null) { // Add the reply data to the header @@ -215,7 +292,7 @@ tgBot.use((ctx, next) => {console.log(ctx.tediCross); next();}); header = `${header.slice(0, -1)}: _${messageObj.reply.text}_`; } } -*/ +/Old */ // Discord doesn't handle messages longer than 2000 characters. Split it up into chunks that big const messageText = header + "\n" + text; From 45da05c3b053dba85d08054cf5492327544c44d7 Mon Sep 17 00:00:00 2001 From: Simen de Lange Date: Sun, 2 Jun 2019 19:01:05 +0200 Subject: [PATCH 053/102] Replies now work well But the code for it isn't pretty --- lib/telegram2discord/handleEntities.js | 2 +- lib/telegram2discord/middlewares.js | 7 +++- lib/telegram2discord/setup.js | 50 +++++++++++--------------- 3 files changed, 27 insertions(+), 32 deletions(-) diff --git a/lib/telegram2discord/handleEntities.js b/lib/telegram2discord/handleEntities.js index a920540a..978db637 100644 --- a/lib/telegram2discord/handleEntities.js +++ b/lib/telegram2discord/handleEntities.js @@ -134,7 +134,7 @@ function handleEntities(text, entities, dcBot, bridge) { } // Put the markdown links on the end, if there are any - if (markdownLinks.length > 0) { + if (!R.isEmpty(markdownLinks)) { substitutedText.push("\n\n"); for (let i = 0; i < markdownLinks.length; i++) { // Find out where the corresponding text is diff --git a/lib/telegram2discord/middlewares.js b/lib/telegram2discord/middlewares.js index e1bf20c1..07cd0f25 100644 --- a/lib/telegram2discord/middlewares.js +++ b/lib/telegram2discord/middlewares.js @@ -225,7 +225,12 @@ function addReplyObj(ctx, next) { const split = R.split("\n", ctx.tediCross.replyTo.text.raw); ctx.tediCross.replyTo.dcUsername = R.head(split); ctx.tediCross.replyTo.text.raw = R.join("\n", R.tail(split)); - ctx.tediCross.replyTo.text.entities = R.tail(ctx.tediCross.replyTo.text.entities); + + // Cut off the first entity (the bold text on the username) and reduce the offset of the rest by the length of the username and the newline + ctx.tediCross.replyTo.text.entities = R.compose( + R.map(entity => R.mergeRight(entity, { offset: entity.offset - ctx.tediCross.replyTo.dcUsername.length - 1 })), + R.tail + )(ctx.tediCross.replyTo.text.entities); } // Turn the original text into "" if there is no text diff --git a/lib/telegram2discord/setup.js b/lib/telegram2discord/setup.js index d695c2a8..fa6cabf8 100644 --- a/lib/telegram2discord/setup.js +++ b/lib/telegram2discord/setup.js @@ -10,6 +10,7 @@ const R = require("ramda"); const middlewares = require("./middlewares"); const From = require("./From"); const handleEntities = require("./handleEntities"); +const Discord = require("discord.js"); /*********** * Helpers * @@ -213,11 +214,14 @@ tgBot.use((ctx, next) => {console.log(ctx.tediCross); next();}); // Prepare and send the message to Discord tgBot.use(createMessageHandler(async (ctx, bridge) => { + // Get the channel to send to + const channel = dcBot.channels.get(bridge.discord.channelId); + // Shorthand for the tediCross context const tc = ctx.tediCross; // Make the header - const header = R.ifElse( + let header = R.ifElse( R.always(bridge.telegram.sendUsernames), tc => { // Get the name of the sender of this message @@ -263,36 +267,23 @@ tgBot.use((ctx, next) => {console.log(ctx.tediCross); next();}); R.always("") )(tc); - // Make the text to send - const text = handleEntities(tc.text.raw, tc.text.entities, ctx.TediCross.dcBot, bridge); - try { - // Get the channel to send to - const channel = dcBot.channels.get(bridge.discord.channelId); - -/* Old - // Handle replies - if (messageObj.reply !== null) { - // Add the reply data to the header - header = header + ` (In reply to **${messageObj.reply.author}**)`; - - // Figure out how to display the reply in Discord - if (settings.discord.displayTelegramReplies === "embed") { - // Make a Discord embed and send it first - const embed = new Discord.RichEmbed({ - description: messageObj.reply.text - }); - - await channel.send(header, {embed}); - - // Clear the header - header = ""; - } else if (settings.discord.displayTelegramReplies === "inline") { - // Just modify the header - header = `${header.slice(0, -1)}: _${messageObj.reply.text}_`; - } + // Handle embed replies + if (!R.isNil(tc.replyTo) && ctx.TediCross.settings.discord.displayTelegramReplies === "embed") { + const replyText = handleEntities(tc.replyTo.text.raw, tc.replyTo.text.entities, ctx.TediCross.dcBot, bridge); + + const embed = new Discord.RichEmbed({ + description: replyText + }); + + await channel.send(header, { embed }); + + // Reset the header so it's not sent again + header = ""; } -/Old */ + + // Make the text to send + const text = handleEntities(tc.text.raw, tc.text.entities, ctx.TediCross.dcBot, bridge); // Discord doesn't handle messages longer than 2000 characters. Split it up into chunks that big const messageText = header + "\n" + text; @@ -308,7 +299,6 @@ tgBot.use((ctx, next) => {console.log(ctx.tediCross); next();}); messageMap.insert(MessageMap.TELEGRAM_TO_DISCORD, bridge, tc.message.message_id, dcMessage.id); } catch (err) { logger.error(`[${bridge.name}] Discord did not accept a text message:`, err); - logger.error(`[${bridge.name}] Failed message:`, text); } })); From d67144d89f9dff0fda74822cc22902afb7283559 Mon Sep 17 00:00:00 2001 From: Simen de Lange Date: Sun, 2 Jun 2019 19:32:36 +0200 Subject: [PATCH 054/102] No longer crashes when replying to messages more than 2048 chars --- lib/telegram2discord/setup.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/telegram2discord/setup.js b/lib/telegram2discord/setup.js index fa6cabf8..4595df04 100644 --- a/lib/telegram2discord/setup.js +++ b/lib/telegram2discord/setup.js @@ -270,10 +270,12 @@ tgBot.use((ctx, next) => {console.log(ctx.tediCross); next();}); try { // Handle embed replies if (!R.isNil(tc.replyTo) && ctx.TediCross.settings.discord.displayTelegramReplies === "embed") { + // Make the text const replyText = handleEntities(tc.replyTo.text.raw, tc.replyTo.text.entities, ctx.TediCross.dcBot, bridge); const embed = new Discord.RichEmbed({ - description: replyText + // Discord will not accept embeds with more than 2048 characters + description: R.slice(0, 2048, replyText) }); await channel.send(header, { embed }); From fe1d830a94fab0c430c427dfb7b8a984b40f514d Mon Sep 17 00:00:00 2001 From: Simen de Lange Date: Sun, 2 Jun 2019 21:04:09 +0200 Subject: [PATCH 055/102] Files work again --- lib/telegram2discord/middlewares.js | 4 ++-- lib/telegram2discord/setup.js | 11 ++++++----- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/lib/telegram2discord/middlewares.js b/lib/telegram2discord/middlewares.js index 07cd0f25..1f0c04bf 100644 --- a/lib/telegram2discord/middlewares.js +++ b/lib/telegram2discord/middlewares.js @@ -315,8 +315,8 @@ function addFileObj(ctx, next) { name: message.document.file_name }; } else if (!R.isNil(message.photo)) { - // Photo. It has an array of photos of different sizes. Use the first and biggest - const photo = R.head(message.photo); + // Photo. It has an array of photos of different sizes. Use the last and biggest + const photo = R.last(message.photo); ctx.tediCross.file = { type: "photo", id: photo.file_id, diff --git a/lib/telegram2discord/setup.js b/lib/telegram2discord/setup.js index 4595df04..2c99209b 100644 --- a/lib/telegram2discord/setup.js +++ b/lib/telegram2discord/setup.js @@ -284,6 +284,9 @@ tgBot.use((ctx, next) => {console.log(ctx.tediCross); next();}); header = ""; } + // Handle file + const attachment = !R.isNil(tc.file) ? new Discord.Attachment(tc.file.stream, tc.file.name) : null; + // Make the text to send const text = handleEntities(tc.text.raw, tc.text.entities, ctx.TediCross.dcBot, bridge); @@ -291,11 +294,9 @@ tgBot.use((ctx, next) => {console.log(ctx.tediCross); next();}); const messageText = header + "\n" + text; const chunks = R.splitEvery(2000, messageText); - // Send them in serial - let dcMessage = null; - for (const chunk of chunks) { - dcMessage = await channel.send(chunk); - } + // Send them in serial, with the attachment first, if there is one + await channel.send(R.head(chunks), attachment); + const dcMessage = await R.reduce((p, chunk) => p.then(() => channel.send(chunk)), Promise.resolve()); // Make the mapping so future edits can work XXX Only the last chunk is considered messageMap.insert(MessageMap.TELEGRAM_TO_DISCORD, bridge, tc.message.message_id, dcMessage.id); From b97d3fdea72f29f1a39d519138024e071f4c8786 Mon Sep 17 00:00:00 2001 From: Simen de Lange Date: Sun, 2 Jun 2019 21:23:46 +0200 Subject: [PATCH 056/102] Middleware for relayJoin/LeaveMessages --- lib/telegram2discord/middlewares.js | 42 ++++++++++++-- lib/telegram2discord/setup.js | 90 +++++++++++++---------------- 2 files changed, 77 insertions(+), 55 deletions(-) diff --git a/lib/telegram2discord/middlewares.js b/lib/telegram2discord/middlewares.js index 1f0c04bf..3c758c65 100644 --- a/lib/telegram2discord/middlewares.js +++ b/lib/telegram2discord/middlewares.js @@ -128,11 +128,9 @@ function addBridgesToContext(ctx, next) { * @returns {undefined} */ function removeD2TBridges(ctx, next) { - ctx.tediCross.bridges = ctx.tediCross.bridges.filter(R.compose( - R.not, - R.equals(Bridge.DIRECTION_DISCORD_TO_TELEGRAM), - R.prop("direction") - )); + ctx.tediCross.bridges = R.reject( + R.propEq("direction", Bridge.DIRECTION_DISCORD_TO_TELEGRAM) + )(ctx.tediCross.bridges); next(); } @@ -148,7 +146,37 @@ function removeD2TBridges(ctx, next) { * @returns {undefined} */ function removeBridgesIgnoringCommands(ctx, next) { - ctx.tediCross.bridges = ctx.tediCross.bridges.filter(R.path(["telegram", "ignoreCommands"])); + ctx.tediCross.bridges = R.filter(R.path(["telegram", "ignoreCommands"]), ctx.tediCross.bridges); + next(); +} + +/** + * Removes bridges with `telegram.relayJoinMessages === false` + * + * @param {Object} ctx The Telegraf context to use + * @param {Object} ctx.tediCross The TediCross object on the context + * @param {Bridge[]} ctx.tediCross.bridges The bridges the message could use + * @param {Function} next Function to pass control to next middleware + * + * @returns {undefined} + */ +function removeBridgesIgnoringJoinMessages(ctx, next) { + ctx.tediCross.bridges = R.filter(R.path(["telegram", "relayJoinMessages"]), ctx.tediCross.bridges); + next(); +} + +/** + * Removes bridges with `telegram.relayLeaveMessages === false` + * + * @param {Object} ctx The Telegraf context to use + * @param {Object} ctx.tediCross The TediCross object on the context + * @param {Bridge[]} ctx.tediCross.bridges The bridges the message could use + * @param {Function} next Function to pass control to next middleware + * + * @returns {undefined} + */ +function removeBridgesIgnoringLeaveMessages(ctx, next) { + ctx.tediCross.bridges = R.filter(R.path(["telegram", "relayLeaveMessages"]), ctx.tediCross.bridges); next(); } @@ -382,6 +410,8 @@ module.exports = { addBridgesToContext, removeD2TBridges, removeBridgesIgnoringCommands, + removeBridgesIgnoringJoinMessages, + removeBridgesIgnoringLeaveMessages, informThisIsPrivateBot, addFromObj, addReplyObj, diff --git a/lib/telegram2discord/setup.js b/lib/telegram2discord/setup.js index 2c99209b..dcae6821 100644 --- a/lib/telegram2discord/setup.js +++ b/lib/telegram2discord/setup.js @@ -171,12 +171,14 @@ function setup(logger, tgBot, dcBot, messageMap, bridgeMap, settings) { tgBot.use(middlewares.informThisIsPrivateBot); tgBot.use(middlewares.removeD2TBridges); tgBot.command(middlewares.removeBridgesIgnoringCommands); + tgBot.on("new_chat_members", middlewares.removeBridgesIgnoringJoinMessages); + tgBot.on("left_chat_member", middlewares.removeBridgesIgnoringLeaveMessages); tgBot.use(middlewares.addFromObj); tgBot.use(middlewares.addReplyObj); tgBot.use(middlewares.addForwardFrom); tgBot.use(middlewares.addTextObj); - tgBot.use(middlewares.addFileObj); // Not used at the moment - tgBot.use(middlewares.addFileStream); // Not used at the moment + tgBot.use(middlewares.addFileObj); + tgBot.use(middlewares.addFileStream); tgBot.use((ctx, next) => {console.log(ctx.tediCross); next();}); @@ -210,6 +212,43 @@ tgBot.use((ctx, next) => {console.log(ctx.tediCross); next();}); }); tgBot.on("edited_message", handleEdits); tgBot.on("edited_channel_post", handleEdits); + + // Listen for users joining the chat + tgBot.on("new_chat_members", createMessageHandler((ctx, bridge) => { + + const new_chat_members = ctx.tediCross.message.new_chat_members; + + // Notify Discord about each user + new_chat_members.forEach((user) => { + // Make the text to send + const nameObj = makeNameObject(user); + const text = `**${nameObj.name} (${nameObj.username})** joined the Telegram side of the chat`; + + // Pass it on + dcBot.ready.then(() => { + return dcBot.channels.get(bridge.discord.channelId).send(text); + }) + .catch((err) => logger.error(`[${bridge.name}] Could not notify Discord about a user that joined Telegram`, err)); + }); + })); + + // Listen for users leaving the chat + tgBot.on("left_chat_member", createMessageHandler(async (ctx, bridge) => { + + const left_chat_member = ctx.tediCross.message.left_chat_member; + + // Make the text to send + const nameObj = makeNameObject(left_chat_member); + const text = `**${nameObj.name} (${nameObj.username})** left the Telegram side of the chat`; + + try { + // Pass it on when Discord is ready + await dcBot.ready; + await dcBot.channels.get(bridge.discord.channelId).send(text); + } catch (err) { + logger.error(`[${bridge.name}] Could not notify Discord about a user that left Telegram`, err); + } + })); /* /Old */ // Prepare and send the message to Discord @@ -304,53 +343,6 @@ tgBot.use((ctx, next) => {console.log(ctx.tediCross); next();}); logger.error(`[${bridge.name}] Discord did not accept a text message:`, err); } })); - - // Listen for users joining the chat - tgBot.on("new_chat_members", createMessageHandler((ctx, bridge) => { - - const new_chat_members = ctx.tediCross.message.new_chat_members; - - // Ignore it if the settings say no - if (!bridge.telegram.relayJoinMessages) { - return; - } - - // Notify Discord about each user - new_chat_members.forEach((user) => { - // Make the text to send - const nameObj = makeNameObject(user); - const text = `**${nameObj.name} (${nameObj.username})** joined the Telegram side of the chat`; - - // Pass it on - dcBot.ready.then(() => { - return dcBot.channels.get(bridge.discord.channelId).send(text); - }) - .catch((err) => logger.error(`[${bridge.name}] Could not notify Discord about a user that joined Telegram`, err)); - }); - })); - - // Listen for users leaving the chat - tgBot.on("left_chat_member", createMessageHandler(async (ctx, bridge) => { - - const left_chat_member = ctx.tediCross.message.left_chat_member; - - // Ignore it if the settings say no - if (!bridge.telegram.relayLeaveMessages) { - return; - } - - // Make the text to send - const nameObj = makeNameObject(left_chat_member); - const text = `**${nameObj.name} (${nameObj.username})** left the Telegram side of the chat`; - - try { - // Pass it on when Discord is ready - await dcBot.ready; - await dcBot.channels.get(bridge.discord.channelId).send(text); - } catch (err) { - logger.error(`[${bridge.name}] Could not notify Discord about a user that left Telegram`, err); - } - })); }) // Start getting updates .then(() => tgBot.startPolling()); From 54087b21d2ca22e749aadfd14e127941dd6a9c34 Mon Sep 17 00:00:00 2001 From: Simen de Lange Date: Sun, 2 Jun 2019 23:18:22 +0200 Subject: [PATCH 057/102] Added endwares --- lib/telegram2discord/endwares.js | 275 ++++++++++++++++++++++ lib/telegram2discord/messageConverter.js | 25 +- lib/telegram2discord/setup.js | 277 ++--------------------- 3 files changed, 305 insertions(+), 272 deletions(-) create mode 100644 lib/telegram2discord/endwares.js diff --git a/lib/telegram2discord/endwares.js b/lib/telegram2discord/endwares.js new file mode 100644 index 00000000..9b3dfc53 --- /dev/null +++ b/lib/telegram2discord/endwares.js @@ -0,0 +1,275 @@ +"use strict"; + +/************************** + * Import important stuff * + **************************/ + +const R = require("ramda"); +const From = require("./From"); +const handleEntities = require("./handleEntities"); +const Discord = require("discord.js"); +const MessageMap = require("../MessageMap"); +const messageConverter = require("./messageConverter"); + +/*********** + * Helpers * + ***********/ + +/** + * Makes an endware function be handled by all bridges it applies to. Curried + * + * @param {Function} func The message handler to wrap + * @param {Context} ctx The Telegraf context + * + * @returns {undefined} + * + * @private + */ +const createMessageHandler = R.curry((func, ctx) => { + // Wait for the Discord bot to become ready + ctx.TediCross.dcBot.ready.then(() => + R.forEach(bridge => func(ctx, bridge))(ctx.tediCross.bridges) + ); +}); + +/** + * Makes the reply text to show on Discord + * + * @param {Object} replyTo The replyTo object from the tediCross context + * @param {Integer} replyLength How many characters to take from the original + * @param {Integer} maxReplyLines How many lines to cut the reply text after + * + * @returns {String} The reply text to display + */ +function makeReplyText(replyTo, replyLength, maxReplyLines) { + // Make the reply string + return R.compose( + // Add ellipsis if the text was cut + R.ifElse( + R.compose( + R.equals(R.length(replyTo.text.raw)), + R.length + ), + R.identity, + R.concat(R.__, "…") + ), + // Take only a number of lines + R.join("\n"), + R.slice(0, maxReplyLines), + R.split("\n"), + // Take only a portion of the text + R.slice(0, replyLength), + )(replyTo.text.raw); +} + +/** + * Makes a discord mention out of a username + * + * @param {String} username The username to make the mention from + * @param {Discord.Client} dcBot The Discord bot to look up the user's ID with + * @param {String} channelId ID of the Discord channel to look up the username in + * + * @returns {String} A Discord mention of the user + */ +function makeDiscordMention(username, dcBot, channelId) { + // Get the name of the Discord user this is a reply to + const dcUser = dcBot.channels.get(channelId).members.find(R.propEq("displayName", username)); + + return R.ifElse( + R.isNil, + R.always(username), + dcUser => `<@${dcUser.id}>` + )(dcUser); +} + +/************************* + * The endware functions * + *************************/ + +/** + * Handles users joining chats + * + * @param {Object} ctx The Telegraf context + * @param {Object} ctx.tediCross.message The Telegram message received + * @param {Object} ctx.tediCross.message The Telegram message received + * @param {Object} ctx.tediCross.message.new_chat_members List of the users who joined the chat + * @param {Object} ctx.TediCross The global TediCross context of the message + * + * @returns {undefined} + */ +const newChatMembers = createMessageHandler((ctx, bridge) => + // Notify Discord about each user + R.forEach(user => { + // Make the text to send + const from = From.createFromObjFromUser(user); + const text = `**${from.firstName} (${R.defaultTo("No username", from.username)})** joined the Telegram side of the chat`; + + // Pass it on + ctx.TediCross.dcBot.channels.get(bridge.discord.channelId) + .send(text); + })(ctx.tediCross.message.new_chat_members) +); + +/** + * Handles users leaving chats + * + * @param {Object} ctx The Telegraf context + * @param {Object} ctx.tediCross The TediCross context of the message + * @param {Object} ctx.tediCross.message The Telegram message received + * @param {Object} ctx.tediCross.message.left_chat_member The user object of the user who left + * @param {Object} ctx.TediCross The global TediCross context of the message + * + * @returns {undefined} + */ +const leftChatMember = createMessageHandler((ctx, bridge) => { + // Make the text to send + const from = From.createFromObjFromUser(ctx.tediCross.message.left_chat_member); + const text = `**${from.firstName} (${R.defaultTo("No username", from.username)})** left the Telegram side of the chat`; + + // Pass it on + ctx.TediCross.dcBot.channels.get(bridge.discord.channelId) + .send(text); +}); + +/** + * Relays a message from Telegram to Discord + * + * @param {Object} ctx The Telegraf context + * @param {Object} ctx.tediCross The TediCross context of the message + * @param {Object} ctx.TediCross The global TediCross context of the message + * + * @returns {undefined} + */ +const relayMessage = createMessageHandler(async (ctx, bridge) => { + // Get the channel to send to + const channel = ctx.TediCross.dcBot.channels.get(bridge.discord.channelId); + + // Shorthand for the tediCross context + const tc = ctx.tediCross; + + // Make the header + let header = R.ifElse( + R.always(bridge.telegram.sendUsernames), + tc => { + // Get the name of the sender of this message + const senderName = From.makeDisplayName(ctx.TediCross.settings.telegram.useFirstNameInsteadOfUsername, tc.from); + + // Make the default header + let header = `**${senderName}**`; + + if (!R.isNil(tc.replyTo)) { + // Add reply info to the header + const repliedToName = R.ifElse( + R.prop("isReplyToTediCross"), + R.compose( + username => makeDiscordMention(username, ctx.TediCross.dcBot, bridge.discord.channelId), + R.prop("dcUsername") + ), + R.compose( + R.partial(From.makeDisplayName, [ctx.TediCross.settings.telegram.useFirstNameInsteadOfUsername]), + R.prop("originalFrom") + ) + )(tc.replyTo); + + + header = `**${senderName}** (in reply to **${repliedToName}**`; + + if (ctx.TediCross.settings.discord.displayTelegramReplies === "inline") { + // Make the reply text + const replyText = makeReplyText(tc.replyTo, ctx.TediCross.settings.discord.replyLength, ctx.TediCross.settings.discord.maxReplyLines); + + // Put the reply text in the header, replacing newlines with spaces + header = `${header}: _${R.replace(/\n/g, " ", replyText)}_)`; + } else { + // Append the closing parenthesis + header = `${header})`; + } + } else if (!R.isNil(tc.forwardFrom)) { + // Handle forwards + const origSender = From.makeDisplayName(ctx.TediCross.settings.telegram.useFirstNameInsteadOfUsername, tc.forwardFrom); + header = `**${origSender}** (forwarded by **${senderName}**)`; + } + return header; + }, + R.always("") + )(tc); + + // Handle embed replies + if (!R.isNil(tc.replyTo) && ctx.TediCross.settings.discord.displayTelegramReplies === "embed") { + // Make the text + const replyText = handleEntities(tc.replyTo.text.raw, tc.replyTo.text.entities, ctx.TediCross.dcBot, bridge); + + const embed = new Discord.RichEmbed({ + // Discord will not accept embeds with more than 2048 characters + description: R.slice(0, 2048, replyText) + }); + + await channel.send(header, { embed }); + + // Reset the header so it's not sent again + header = ""; + } + + // Handle file + const attachment = !R.isNil(tc.file) ? new Discord.Attachment(tc.file.stream, tc.file.name) : null; + + // Make the text to send + const text = handleEntities(tc.text.raw, tc.text.entities, ctx.TediCross.dcBot, bridge); + + // Discord doesn't handle messages longer than 2000 characters. Split it up into chunks that big + const messageText = header + "\n" + text; + const chunks = R.splitEvery(2000, messageText); + + // Send them in serial, with the attachment first, if there is one + let dcMessage = await channel.send(R.head(chunks), attachment); + if (R.length(chunks) > 1) { + dcMessage = await R.reduce((p, chunk) => p.then(() => channel.send(chunk)), Promise.resolve(), R.tail(chunks)); + } + + // Make the mapping so future edits can work XXX Only the last chunk is considered + ctx.TediCross.messageMap.insert(MessageMap.TELEGRAM_TO_DISCORD, bridge, tc.message.message_id, dcMessage.id); +}); + +/** + * Handles message edits + * + * @param {Object} ctx The Telegraf context + * + * @returns {undefined} + */ +const handleEdits = createMessageHandler(async (ctx, bridge) => { + try { + const tgMessage = ctx.tediCross.message; + + // Find the ID of this message on Discord + const [dcMessageId] = ctx.TediCross.messageMap.getCorresponding(MessageMap.TELEGRAM_TO_DISCORD, bridge, tgMessage.message_id); + + // Get the messages from Discord + const dcMessage = await ctx.TediCross.dcBot.channels + .get(bridge.discord.channelId) + .fetchMessage(dcMessageId); + + const messageObj = messageConverter(ctx, bridge); + + // Try to edit the message + const textToSend = bridge.telegram.sendUsernames + ? `**${messageObj.from}**\n${messageObj.text}` + : messageObj.text + ; + await dcMessage.edit(textToSend); + } catch (err) { + // Log it + ctx.TediCross.logger.error(`[${bridge.name}] Could not edit Discord message:`, err); + } +}); + +/*************** + * Export them * + ***************/ + +module.exports = { + newChatMembers, + leftChatMember, + relayMessage, + handleEdits +}; diff --git a/lib/telegram2discord/messageConverter.js b/lib/telegram2discord/messageConverter.js index dd714398..a35d7e24 100644 --- a/lib/telegram2discord/messageConverter.js +++ b/lib/telegram2discord/messageConverter.js @@ -74,37 +74,34 @@ function getDisplayNameFromUser(user, useFirstNameInsteadOfUsername) { /** * Converts Telegram messages to appropriate from and text * - * @param {Message} message The Telegram message to convert - * @param {Telegraf} tgBot The Telegram bot - * @param {Settings} settings The settings to use - * @param {Discord.Client} dcBot The Discord bot + * @param {Object} ctx The Telegraf context * @param {Bridge} bridge The bridge the message is crossing * * @return {Object} A object containing message information as from, text etc */ -function messageConverter(ctx, tgBot, settings, dcBot, bridge) { +function messageConverter(ctx, bridge) { const message = ctx.tediCross.message; // Convert the text to Discord format - const text = handleEntities(message.text, message.entities, dcBot, bridge); + const text = handleEntities(message.text, message.entities, ctx.TediCross.dcBot, bridge); // Handle for the reply object let reply = null; // Find out who the message is from - let fromName = getDisplayName(message.from, message.chat, settings.telegram.useFirstNameInsteadOfUsername); + let fromName = getDisplayName(message.from, message.chat, ctx.TediCross.settings.telegram.useFirstNameInsteadOfUsername); // Check if it is a reply if (!R.isNil(ctx.tediCross.replyTo)) { // Get the name of the user this is a reply to - let originalAuthor = getDisplayName(message.reply_to_message.from, message.chat, settings.telegram.useFirstNameInsteadOfUsername); + let originalAuthor = getDisplayName(message.reply_to_message.from, message.chat, ctx.TediCross.settings.telegram.useFirstNameInsteadOfUsername); let originalText = ""; // Is this a reply to the bot, i.e. to a Discord user? - if (message.reply_to_message.from !== undefined && message.reply_to_message.from.id === tgBot.context.TediCross.me.id) { + if (message.reply_to_message.from !== undefined && message.reply_to_message.from.id === ctx.TediCross.me.id) { // Get the name of the Discord user this is a reply to const dcUsername = message.reply_to_message.text.split("\n")[0]; - const dcUser = dcBot.channels.get(bridge.discord.channelId).members.find(findFn("displayName", new RegExp(dcUsername))); + const dcUser = ctx.TediCross.dcBot.channels.get(bridge.discord.channelId).members.find(findFn("displayName", new RegExp(dcUsername))); originalAuthor = !R.isNil(dcUser) ? `<@${dcUser.id}>` : dcUsername; } @@ -113,14 +110,14 @@ function messageConverter(ctx, tgBot, settings, dcBot, bridge) { originalText = message.reply_to_message.text; // Is this a reply to the bot, i.e. to a Discord user? - if (message.reply_to_message.from !== undefined && message.reply_to_message.from.id === tgBot.context.TediCross.me.id) { + if (message.reply_to_message.from !== undefined && message.reply_to_message.from.id === ctx.TediCross.me.id) { [ , ...originalText] = message.reply_to_message.text.split("\n"); originalText = originalText.join("\n"); } // Take only the first few characters, or up to second newline - originalText = originalText.length > settings.discord.replyLength - ? originalText.slice(0, settings.discord.replyLength) + "…" + originalText = originalText.length > ctx.TediCross.settings.discord.replyLength + ? originalText.slice(0, ctx.TediCross.settings.discord.replyLength) + "…" : originalText ; const newlineIndices = [...originalText].reduce((indices, c, i) => { @@ -143,7 +140,7 @@ function messageConverter(ctx, tgBot, settings, dcBot, bridge) { const forward_from = message.forward_from || message.forward_from_chat; if (forward_from !== undefined) { // Find the name of the user this was forwarded from - const forwardFrom = getDisplayName(forward_from, forward_from, settings.telegram.useFirstNameInsteadOfUsername); + const forwardFrom = getDisplayName(forward_from, forward_from, ctx.TediCross.settings.telegram.useFirstNameInsteadOfUsername); // Add it to the 'from' text fromName = `${forwardFrom} (forwarded by ${fromName})`; diff --git a/lib/telegram2discord/setup.js b/lib/telegram2discord/setup.js index dcae6821..7a6130c4 100644 --- a/lib/telegram2discord/setup.js +++ b/lib/telegram2discord/setup.js @@ -4,13 +4,9 @@ * Import important stuff * **************************/ -const messageConverter = require("./messageConverter"); -const MessageMap = require("../MessageMap"); const R = require("ramda"); const middlewares = require("./middlewares"); -const From = require("./From"); -const handleEntities = require("./handleEntities"); -const Discord = require("discord.js"); +const endwares = require("./endwares"); /*********** * Helpers * @@ -39,97 +35,6 @@ function clearOldMessages(tgBot, offset = -1) { )); } -/** - * Makes the reply text to show on Discord - * - * @param {Object} replyTo The replyTo object from the tediCross context - * @param {Integer} replyLength How many characters to take from the original - * @param {Integer} maxReplyLines How many lines to cut the reply text after - * - * @returns {String} The reply text to display - */ -function makeReplyText(replyTo, replyLength, maxReplyLines) { - // Make the reply string - return R.compose( - // Add ellipsis if the text was cut - R.ifElse( - R.compose( - R.equals(R.length(replyTo.text.raw)), - R.length - ), - R.identity, - R.concat(R.__, "…") - ), - // Take only a number of lines - R.join("\n"), - R.slice(0, maxReplyLines), - R.split("\n"), - // Take only a portion of the text - R.slice(0, replyLength), - )(replyTo.text.raw); -} - -/** - * Makes a discord mention out of a username - * - * @param {String} username The username to make the mention from - * @param {Discord.Client} dcBot The Discord bot to look up the user's ID with - * @param {String} channelId ID of the Discord channel to look up the username in - * - * @returns {String} A Discord mention of the user - */ -function makeDiscordMention(username, dcBot, channelId) { - // Get the name of the Discord user this is a reply to - const dcUser = dcBot.channels.get(channelId).members.find(R.propEq("displayName", username)); - - return R.ifElse( - R.isNil, - R.always(username), - dcUser => `<@${dcUser.id}>` - )(dcUser); -} - -/** - * Makes a name object (for lack of better term) of a user object. It contains the user's full name, and the username or the text `No username` - * - * @param {User} user The user object to make the name object of - * - * @returns {Object} The name object, with `name` and `username` as properties - */ -function makeNameObject(user) { - // Make the user's full name - const name = user.first_name - + (user.last_name !== undefined - ? " " + user.last_name - : "" - ); - - // Make the user's username - const username = user.username !== undefined - ? "@" + user.username - : "No username"; - - return { - name, - username - }; -} - -/** - * Curryed function making middleware be handled for every bridge - * - * @param {Function} func The message handler to wrap - * @param {Context} ctx The Telegram message triggering the wrapped function, wrapped in a context - * - * @private - */ -const createMessageHandler = R.curry((func, ctx) => { - // Wait for the Discord bot to become ready - ctx.TediCross.dcBot.ready.then(() => - ctx.tediCross.bridges.forEach((bridge) => func(ctx, bridge)) - ); -}); - /********************** * The setup function * **********************/ @@ -160,7 +65,9 @@ function setup(logger, tgBot, dcBot, messageMap, bridgeMap, settings) { me, bridgeMap, dcBot, - settings + settings, + messageMap, + logger }; // Apply middlewares @@ -180,169 +87,23 @@ function setup(logger, tgBot, dcBot, messageMap, bridgeMap, settings) { tgBot.use(middlewares.addFileObj); tgBot.use(middlewares.addFileStream); +// TODO Temporary logging to see what's going on. Remove before release tgBot.use((ctx, next) => {console.log(ctx.tediCross); next();}); -/* Old */ - // Set up event listener for message edits - const handleEdits = createMessageHandler(async (ctx, bridge) => { - try { - const tgMessage = ctx.tediCross.message; - - // Wait for the Discord bot to become ready - await dcBot.ready; - - // Find the ID of this message on Discord - const [dcMessageId] = messageMap.getCorresponding(MessageMap.TELEGRAM_TO_DISCORD, bridge, tgMessage.message_id); - - // Get the messages from Discord - const dcMessage = await dcBot.channels.get(bridge.discord.channelId).fetchMessage(dcMessageId); - - const messageObj = messageConverter(ctx, tgBot, settings, dcBot, bridge); - - // Try to edit the message - const textToSend = bridge.telegram.sendUsernames - ? `**${messageObj.from}**\n${messageObj.text}` - : messageObj.text - ; - await dcMessage.edit(textToSend); - } catch (err) { - // Log it - logger.error(`[${bridge.name}] Could not edit Discord message:`, err); - } - }); - tgBot.on("edited_message", handleEdits); - tgBot.on("edited_channel_post", handleEdits); - - // Listen for users joining the chat - tgBot.on("new_chat_members", createMessageHandler((ctx, bridge) => { - - const new_chat_members = ctx.tediCross.message.new_chat_members; - - // Notify Discord about each user - new_chat_members.forEach((user) => { - // Make the text to send - const nameObj = makeNameObject(user); - const text = `**${nameObj.name} (${nameObj.username})** joined the Telegram side of the chat`; - - // Pass it on - dcBot.ready.then(() => { - return dcBot.channels.get(bridge.discord.channelId).send(text); - }) - .catch((err) => logger.error(`[${bridge.name}] Could not notify Discord about a user that joined Telegram`, err)); - }); - })); - - // Listen for users leaving the chat - tgBot.on("left_chat_member", createMessageHandler(async (ctx, bridge) => { - - const left_chat_member = ctx.tediCross.message.left_chat_member; - - // Make the text to send - const nameObj = makeNameObject(left_chat_member); - const text = `**${nameObj.name} (${nameObj.username})** left the Telegram side of the chat`; - - try { - // Pass it on when Discord is ready - await dcBot.ready; - await dcBot.channels.get(bridge.discord.channelId).send(text); - } catch (err) { - logger.error(`[${bridge.name}] Could not notify Discord about a user that left Telegram`, err); - } - })); -/* /Old */ - - // Prepare and send the message to Discord - tgBot.use(createMessageHandler(async (ctx, bridge) => { - // Get the channel to send to - const channel = dcBot.channels.get(bridge.discord.channelId); - - // Shorthand for the tediCross context - const tc = ctx.tediCross; - - // Make the header - let header = R.ifElse( - R.always(bridge.telegram.sendUsernames), - tc => { - // Get the name of the sender of this message - const senderName = From.makeDisplayName(settings.telegram.useFirstNameInsteadOfUsername, tc.from); - - // Make the default header - let header = `**${senderName}**`; - - if (!R.isNil(tc.replyTo)) { - // Add reply info to the header - const repliedToName = R.ifElse( - R.prop("isReplyToTediCross"), - R.compose( - username => makeDiscordMention(username, ctx.TediCross.dcBot, bridge.discord.channelId), - R.prop("dcUsername") - ), - R.compose( - R.partial(From.makeDisplayName, [ctx.TediCross.settings.telegram.useFirstNameInsteadOfUsername]), - R.prop("originalFrom") - ) - )(tc.replyTo); - - - header = `**${senderName}** (in reply to **${repliedToName}**`; - - if (ctx.TediCross.settings.discord.displayTelegramReplies === "inline") { - // Make the reply text - const replyText = makeReplyText(tc.replyTo, ctx.TediCross.settings.discord.replyLength, ctx.TediCross.settings.discord.maxReplyLines); - - // Put the reply text in the header, replacing newlines with spaces - header = `${header}: _${R.replace(/\n/g, " ", replyText)}_)`; - } else { - // Append the closing parenthesis - header = `${header})`; - } - } else if (!R.isNil(tc.forwardFrom)) { - // Handle forwards - const origSender = From.makeDisplayName(settings.telegram.useFirstNameInsteadOfUsername, tc.forwardFrom); - header = `**${origSender}** (forwarded by **${senderName}**)`; - } - return header; - }, - R.always("") - )(tc); - - try { - // Handle embed replies - if (!R.isNil(tc.replyTo) && ctx.TediCross.settings.discord.displayTelegramReplies === "embed") { - // Make the text - const replyText = handleEntities(tc.replyTo.text.raw, tc.replyTo.text.entities, ctx.TediCross.dcBot, bridge); - - const embed = new Discord.RichEmbed({ - // Discord will not accept embeds with more than 2048 characters - description: R.slice(0, 2048, replyText) - }); - - await channel.send(header, { embed }); - - // Reset the header so it's not sent again - header = ""; - } - - // Handle file - const attachment = !R.isNil(tc.file) ? new Discord.Attachment(tc.file.stream, tc.file.name) : null; - - // Make the text to send - const text = handleEntities(tc.text.raw, tc.text.entities, ctx.TediCross.dcBot, bridge); - - // Discord doesn't handle messages longer than 2000 characters. Split it up into chunks that big - const messageText = header + "\n" + text; - const chunks = R.splitEvery(2000, messageText); - - // Send them in serial, with the attachment first, if there is one - await channel.send(R.head(chunks), attachment); - const dcMessage = await R.reduce((p, chunk) => p.then(() => channel.send(chunk)), Promise.resolve()); - - // Make the mapping so future edits can work XXX Only the last chunk is considered - messageMap.insert(MessageMap.TELEGRAM_TO_DISCORD, bridge, tc.message.message_id, dcMessage.id); - } catch (err) { - logger.error(`[${bridge.name}] Discord did not accept a text message:`, err); - } - })); + // Apply endwares + tgBot.on("new_chat_members", endwares.newChatMembers); + tgBot.on("left_chat_member", endwares.leftChatMember); + tgBot.on("edited_message", endwares.handleEdits); + tgBot.on("edited_channel_post", endwares.handleEdits); + R.forEach(updateType => tgBot.on(updateType, endwares.relayMessage), [ + "text", + "audio", + "document", + "photo", + "sticker", + "video", + "voice" + ]); }) // Start getting updates .then(() => tgBot.startPolling()); From c1ef4748d80fd16665f65e8f42339dc811a40605 Mon Sep 17 00:00:00 2001 From: Simen de Lange Date: Thu, 13 Jun 2019 19:56:46 +0200 Subject: [PATCH 058/102] Some minor cleanup --- lib/discord2telegram/setup.js | 2 ++ lib/telegram2discord/endwares.js | 16 +++++++++++ lib/telegram2discord/middlewares.js | 42 ++++++++++++++++++++++------- lib/telegram2discord/setup.js | 22 ++++++--------- 4 files changed, 59 insertions(+), 23 deletions(-) diff --git a/lib/discord2telegram/setup.js b/lib/discord2telegram/setup.js index 61d37a85..88ca7f0d 100644 --- a/lib/discord2telegram/setup.js +++ b/lib/discord2telegram/setup.js @@ -305,6 +305,8 @@ function setup(logger, dcBot, tgBot, messageMap, bridgeMap, settings, datadirPat }); }); + setTimeout(() => dcBot.emit("error", new Error()), 10000); + // Listen for errors dcBot.on("error", (err) => { if (err.code === "ECONNRESET") { diff --git a/lib/telegram2discord/endwares.js b/lib/telegram2discord/endwares.js index 9b3dfc53..120bde9b 100644 --- a/lib/telegram2discord/endwares.js +++ b/lib/telegram2discord/endwares.js @@ -86,6 +86,21 @@ function makeDiscordMention(username, dcBot, channelId) { * The endware functions * *************************/ +/** + * Replies to a message with info about the chat + * + * @param {Object} ctx The Telegraf context + * @param {Object} ctx.tediCross The TediCross object on the context + * @param {Object} ctx.tediCross.message The message to reply to + * @param {Object} ctx.tediCross.message.chat The object of the chat the message is from + * @param {Integer} ctx.tediCross.message.chat.id ID of the chat the message is from + * + * @returns {undefined} + */ +function chatinfo(ctx) { + ctx.reply(`chatID: ${ctx.tediCross.message.chat.id}`); +} + /** * Handles users joining chats * @@ -268,6 +283,7 @@ const handleEdits = createMessageHandler(async (ctx, bridge) => { ***************/ module.exports = { + chatinfo, newChatMembers, leftChatMember, relayMessage, diff --git a/lib/telegram2discord/middlewares.js b/lib/telegram2discord/middlewares.js index 3c758c65..54697f99 100644 --- a/lib/telegram2discord/middlewares.js +++ b/lib/telegram2discord/middlewares.js @@ -87,18 +87,19 @@ function addMessageObj(ctx, next) { } /** - * Replies to a message with info about the chat + * Adds the message ID as a prop to the tedicross context * * @param {Object} ctx The Telegraf context - * @param {Object} ctx.tediCross The TediCross object on the context - * @param {Object} ctx.tediCross.message The message to reply to - * @param {Object} ctx.tediCross.message.chat The object of the chat the message is from - * @param {Integer} ctx.tediCross.message.chat.id ID of the chat the message is from + * @param {Object} ctx.tediCross The Tedicross object on the context + * @param {Object} ctx.tediCross.message The message object being handled + * @param {Function} next Function to pass control to next middleware * * @returns {undefined} */ -function chatinfo(ctx) { - ctx.reply(`chatID: ${ctx.tediCross.message.chat.id}`); +function addMessageId(ctx, next) { + ctx.tediCross.messageId = ctx.tediCross.message.message_id; + + next(); } /** @@ -217,6 +218,7 @@ function informThisIsPrivateBot(ctx, next) { * @param {Object} ctx The context to add the property to * @param {Object} ctx.tediCross The tediCross on the context * @param {Object} ctx.tediCross.message The message object to create the `from` object from + * @param {Function} next Function to pass control to next middleware * * @returns {undefined} */ @@ -231,6 +233,7 @@ function addFromObj(ctx, next) { * @param {Object} ctx The context to add the property to * @param {Object} ctx.tediCross The tediCross on the context * @param {Object} ctx.tediCross.message The message object to create the `reply` object from + * @param {Function} next Function to pass control to next middleware * * @returns {undefined} */ @@ -276,6 +279,7 @@ function addReplyObj(ctx, next) { * @param {Object} ctx The context to add the property to * @param {Object} ctx.tediCross The tediCross on the context * @param {Object} ctx.tediCross.message The message object to create the `forward` object from + * @param {Function} next Function to pass control to next middleware * * @returns {undefined} */ @@ -302,6 +306,7 @@ function addForwardFrom(ctx, next) { * @param {Object} ctx The context to add the property to * @param {Object} ctx.tediCross The tediCross on the context * @param {Object} ctx.tediCross.message The message object to get the text data from + * @param {Function} next Function to pass control to next middleware * * @returns {undefined} */ @@ -321,6 +326,7 @@ function addTextObj(ctx, next) { * @param {Object} ctx The context to add the property to * @param {Object} ctx.tediCross The tediCross on the context * @param {Object} ctx.tediCross.message The message object to get the file data from + * @param {Function} next Function to pass control to next middleware * * @returns {undefined} */ @@ -381,6 +387,7 @@ function addFileObj(ctx, next) { * * @param {Object} ctx The context to add the property to * @param {Object} ctx.tediCross The tediCross on the context + * @param {Function} next Function to pass control to next middleware * * @returns {Promise} Promise resolving to nothing when the operation is complete */ @@ -399,6 +406,22 @@ function addFileStream(ctx, next) { .then(R.always(undefined)); } +/** + * Adds a boolean telling if the message is an edit + * + * @param {Object} ctx The context to add the property to + * @param {Object} ctx.tediCross The tedicross object on the context + * @param {Object} ctx.tediCross.message The message being handled + * @param {Function} next Function to pass control to next middleware + * + * @returns {undefined} + */ +function addEditBool(ctx, next) { + ctx.tediCross.isEdit = !R.isNil(ctx.tediCross.message.edit_date); + + next(); +} + /*************** * Export them * ***************/ @@ -406,7 +429,7 @@ function addFileStream(ctx, next) { module.exports = { addTediCrossObj, addMessageObj, - chatinfo, + addMessageId, addBridgesToContext, removeD2TBridges, removeBridgesIgnoringCommands, @@ -418,5 +441,6 @@ module.exports = { addForwardFrom, addTextObj, addFileObj, - addFileStream + addFileStream, + addEditBool }; diff --git a/lib/telegram2discord/setup.js b/lib/telegram2discord/setup.js index 7a6130c4..7a367f22 100644 --- a/lib/telegram2discord/setup.js +++ b/lib/telegram2discord/setup.js @@ -70,40 +70,34 @@ function setup(logger, tgBot, dcBot, messageMap, bridgeMap, settings) { logger }; - // Apply middlewares + // Apply middlewares and endwares tgBot.use(middlewares.addTediCrossObj); tgBot.use(middlewares.addMessageObj); - tgBot.command("chatinfo", middlewares.chatinfo); + tgBot.use(middlewares.addMessageId); + tgBot.command("chatinfo", endwares.chatinfo); tgBot.use(middlewares.addBridgesToContext); tgBot.use(middlewares.informThisIsPrivateBot); tgBot.use(middlewares.removeD2TBridges); tgBot.command(middlewares.removeBridgesIgnoringCommands); tgBot.on("new_chat_members", middlewares.removeBridgesIgnoringJoinMessages); tgBot.on("left_chat_member", middlewares.removeBridgesIgnoringLeaveMessages); + tgBot.on("new_chat_members", endwares.newChatMembers); + tgBot.on("left_chat_member", endwares.leftChatMember); tgBot.use(middlewares.addFromObj); tgBot.use(middlewares.addReplyObj); tgBot.use(middlewares.addForwardFrom); tgBot.use(middlewares.addTextObj); tgBot.use(middlewares.addFileObj); tgBot.use(middlewares.addFileStream); + tgBot.use(middlewares.addEditBool); // TODO Temporary logging to see what's going on. Remove before release -tgBot.use((ctx, next) => {console.log(ctx.tediCross); next();}); + tgBot.use((ctx, next) => {console.log(ctx.tediCross); next();}); // Apply endwares - tgBot.on("new_chat_members", endwares.newChatMembers); - tgBot.on("left_chat_member", endwares.leftChatMember); tgBot.on("edited_message", endwares.handleEdits); tgBot.on("edited_channel_post", endwares.handleEdits); - R.forEach(updateType => tgBot.on(updateType, endwares.relayMessage), [ - "text", - "audio", - "document", - "photo", - "sticker", - "video", - "voice" - ]); + tgBot.use(endwares.relayMessage); }) // Start getting updates .then(() => tgBot.startPolling()); From b1bfe82e46419b8f5b10569443885fddc980d7b0 Mon Sep 17 00:00:00 2001 From: Simen de Lange Date: Thu, 13 Jun 2019 21:56:01 +0200 Subject: [PATCH 059/102] Edits are now in the middleware flow --- lib/telegram2discord/endwares.js | 182 ++++++---------------------- lib/telegram2discord/middlewares.js | 156 ++++++++++++++++++++++-- lib/telegram2discord/setup.js | 8 +- 3 files changed, 179 insertions(+), 167 deletions(-) diff --git a/lib/telegram2discord/endwares.js b/lib/telegram2discord/endwares.js index 120bde9b..6d017c16 100644 --- a/lib/telegram2discord/endwares.js +++ b/lib/telegram2discord/endwares.js @@ -6,8 +6,6 @@ const R = require("ramda"); const From = require("./From"); -const handleEntities = require("./handleEntities"); -const Discord = require("discord.js"); const MessageMap = require("../MessageMap"); const messageConverter = require("./messageConverter"); @@ -32,56 +30,6 @@ const createMessageHandler = R.curry((func, ctx) => { ); }); -/** - * Makes the reply text to show on Discord - * - * @param {Object} replyTo The replyTo object from the tediCross context - * @param {Integer} replyLength How many characters to take from the original - * @param {Integer} maxReplyLines How many lines to cut the reply text after - * - * @returns {String} The reply text to display - */ -function makeReplyText(replyTo, replyLength, maxReplyLines) { - // Make the reply string - return R.compose( - // Add ellipsis if the text was cut - R.ifElse( - R.compose( - R.equals(R.length(replyTo.text.raw)), - R.length - ), - R.identity, - R.concat(R.__, "…") - ), - // Take only a number of lines - R.join("\n"), - R.slice(0, maxReplyLines), - R.split("\n"), - // Take only a portion of the text - R.slice(0, replyLength), - )(replyTo.text.raw); -} - -/** - * Makes a discord mention out of a username - * - * @param {String} username The username to make the mention from - * @param {Discord.Client} dcBot The Discord bot to look up the user's ID with - * @param {String} channelId ID of the Discord channel to look up the username in - * - * @returns {String} A Discord mention of the user - */ -function makeDiscordMention(username, dcBot, channelId) { - // Get the name of the Discord user this is a reply to - const dcUser = dcBot.channels.get(channelId).members.find(R.propEq("displayName", username)); - - return R.ifElse( - R.isNil, - R.always(username), - dcUser => `<@${dcUser.id}>` - )(dcUser); -} - /************************* * The endware functions * *************************/ @@ -155,95 +103,34 @@ const leftChatMember = createMessageHandler((ctx, bridge) => { * * @returns {undefined} */ -const relayMessage = createMessageHandler(async (ctx, bridge) => { - // Get the channel to send to - const channel = ctx.TediCross.dcBot.channels.get(bridge.discord.channelId); - - // Shorthand for the tediCross context - const tc = ctx.tediCross; - - // Make the header - let header = R.ifElse( - R.always(bridge.telegram.sendUsernames), - tc => { - // Get the name of the sender of this message - const senderName = From.makeDisplayName(ctx.TediCross.settings.telegram.useFirstNameInsteadOfUsername, tc.from); - - // Make the default header - let header = `**${senderName}**`; - - if (!R.isNil(tc.replyTo)) { - // Add reply info to the header - const repliedToName = R.ifElse( - R.prop("isReplyToTediCross"), - R.compose( - username => makeDiscordMention(username, ctx.TediCross.dcBot, bridge.discord.channelId), - R.prop("dcUsername") - ), - R.compose( - R.partial(From.makeDisplayName, [ctx.TediCross.settings.telegram.useFirstNameInsteadOfUsername]), - R.prop("originalFrom") - ) - )(tc.replyTo); - - - header = `**${senderName}** (in reply to **${repliedToName}**`; - - if (ctx.TediCross.settings.discord.displayTelegramReplies === "inline") { - // Make the reply text - const replyText = makeReplyText(tc.replyTo, ctx.TediCross.settings.discord.replyLength, ctx.TediCross.settings.discord.maxReplyLines); - - // Put the reply text in the header, replacing newlines with spaces - header = `${header}: _${R.replace(/\n/g, " ", replyText)}_)`; - } else { - // Append the closing parenthesis - header = `${header})`; - } - } else if (!R.isNil(tc.forwardFrom)) { - // Handle forwards - const origSender = From.makeDisplayName(ctx.TediCross.settings.telegram.useFirstNameInsteadOfUsername, tc.forwardFrom); - header = `**${origSender}** (forwarded by **${senderName}**)`; - } - return header; - }, - R.always("") - )(tc); - - // Handle embed replies - if (!R.isNil(tc.replyTo) && ctx.TediCross.settings.discord.displayTelegramReplies === "embed") { - // Make the text - const replyText = handleEntities(tc.replyTo.text.raw, tc.replyTo.text.entities, ctx.TediCross.dcBot, bridge); - - const embed = new Discord.RichEmbed({ - // Discord will not accept embeds with more than 2048 characters - description: R.slice(0, 2048, replyText) - }); - - await channel.send(header, { embed }); - - // Reset the header so it's not sent again - header = ""; - } - - // Handle file - const attachment = !R.isNil(tc.file) ? new Discord.Attachment(tc.file.stream, tc.file.name) : null; - - // Make the text to send - const text = handleEntities(tc.text.raw, tc.text.entities, ctx.TediCross.dcBot, bridge); - - // Discord doesn't handle messages longer than 2000 characters. Split it up into chunks that big - const messageText = header + "\n" + text; - const chunks = R.splitEvery(2000, messageText); - - // Send them in serial, with the attachment first, if there is one - let dcMessage = await channel.send(R.head(chunks), attachment); - if (R.length(chunks) > 1) { - dcMessage = await R.reduce((p, chunk) => p.then(() => channel.send(chunk)), Promise.resolve(), R.tail(chunks)); - } - - // Make the mapping so future edits can work XXX Only the last chunk is considered - ctx.TediCross.messageMap.insert(MessageMap.TELEGRAM_TO_DISCORD, bridge, tc.message.message_id, dcMessage.id); -}); +function relayMessage(ctx) { + R.forEach(async prepared => { + // Get the channel to send to + const channel = ctx.TediCross.dcBot.channels.get(prepared.bridge.discord.channelId); + + // Make the header + let header = prepared.header; + + // Handle embed replies + if (prepared.embed) { + await channel.send(header, { embed: prepared.embed }); + header = ""; + } + + // Discord doesn't handle messages longer than 2000 characters. Split it up into chunks that big + const messageText = header + "\n" + prepared.text; + const chunks = R.splitEvery(2000, messageText); + + // Send them in serial, with the attachment first, if there is one + let dcMessage = await channel.send(R.head(chunks), { attachment: prepared.attachment }); + if (R.length(chunks) > 1) { + dcMessage = await R.reduce((p, chunk) => p.then(() => channel.send(chunk)), Promise.resolve(), R.tail(chunks)); + } + + // Make the mapping so future edits can work XXX Only the last chunk is considered + ctx.TediCross.messageMap.insert(MessageMap.TELEGRAM_TO_DISCORD, prepared.bridge, ctx.tediCross.messageId, dcMessage.id); + })(ctx.tediCross.prepared); +} /** * Handles message edits @@ -264,14 +151,13 @@ const handleEdits = createMessageHandler(async (ctx, bridge) => { .get(bridge.discord.channelId) .fetchMessage(dcMessageId); - const messageObj = messageConverter(ctx, bridge); + R.forEach(async prepared => { + // Discord doesn't handle messages longer than 2000 characters. Take only the first 2000 + const messageText = R.slice(0, 2000, prepared.header + "\n" + prepared.text); - // Try to edit the message - const textToSend = bridge.telegram.sendUsernames - ? `**${messageObj.from}**\n${messageObj.text}` - : messageObj.text - ; - await dcMessage.edit(textToSend); + // Send them in serial, with the attachment first, if there is one + await dcMessage.edit(messageText, { attachment: prepared.attachment }); + })(ctx.tediCross.prepared); } catch (err) { // Log it ctx.TediCross.logger.error(`[${bridge.name}] Could not edit Discord message:`, err); diff --git a/lib/telegram2discord/middlewares.js b/lib/telegram2discord/middlewares.js index 54697f99..bfb7855d 100644 --- a/lib/telegram2discord/middlewares.js +++ b/lib/telegram2discord/middlewares.js @@ -9,6 +9,8 @@ const Bridge = require("../bridgestuff/Bridge"); const From = require("./From"); const mime = require("mime/lite"); const request = require("request"); +const handleEntities = require("./handleEntities"); +const Discord = require("discord.js"); /*********** * Helpers * @@ -43,6 +45,56 @@ function createTextObjFromMessage(message) { ])(message); } +/** + * Makes the reply text to show on Discord + * + * @param {Object} replyTo The replyTo object from the tediCross context + * @param {Integer} replyLength How many characters to take from the original + * @param {Integer} maxReplyLines How many lines to cut the reply text after + * + * @returns {String} The reply text to display + */ +function makeReplyText(replyTo, replyLength, maxReplyLines) { + // Make the reply string + return R.compose( + // Add ellipsis if the text was cut + R.ifElse( + R.compose( + R.equals(R.length(replyTo.text.raw)), + R.length + ), + R.identity, + R.concat(R.__, "…") + ), + // Take only a number of lines + R.join("\n"), + R.slice(0, maxReplyLines), + R.split("\n"), + // Take only a portion of the text + R.slice(0, replyLength), + )(replyTo.text.raw); +} + +/** + * Makes a discord mention out of a username + * + * @param {String} username The username to make the mention from + * @param {Discord.Client} dcBot The Discord bot to look up the user's ID with + * @param {String} channelId ID of the Discord channel to look up the username in + * + * @returns {String} A Discord mention of the user + */ +function makeDiscordMention(username, dcBot, channelId) { + // Get the name of the Discord user this is a reply to + const dcUser = dcBot.channels.get(channelId).members.find(R.propEq("displayName", username)); + + return R.ifElse( + R.isNil, + R.always(username), + dcUser => `<@${dcUser.id}>` + )(dcUser); +} + /**************************** * The middleware functions * ****************************/ @@ -406,18 +458,96 @@ function addFileStream(ctx, next) { .then(R.always(undefined)); } -/** - * Adds a boolean telling if the message is an edit - * - * @param {Object} ctx The context to add the property to - * @param {Object} ctx.tediCross The tedicross object on the context - * @param {Object} ctx.tediCross.message The message being handled - * @param {Function} next Function to pass control to next middleware - * - * @returns {undefined} - */ -function addEditBool(ctx, next) { - ctx.tediCross.isEdit = !R.isNil(ctx.tediCross.message.edit_date); +function addPreparedObj(ctx, next) { + // Shorthand for the tediCross context + const tc = ctx.tediCross; + + ctx.tediCross.prepared = R.map( + bridge => { + // Make the header + const header = R.ifElse( + R.always(bridge.telegram.sendUsernames), + tc => { + // Get the name of the sender of this message + const senderName = From.makeDisplayName(ctx.TediCross.settings.telegram.useFirstNameInsteadOfUsername, tc.from); + + // Make the default header + let header = `**${senderName}**`; + + if (!R.isNil(tc.replyTo)) { + // Add reply info to the header + const repliedToName = R.ifElse( + R.prop("isReplyToTediCross"), + R.compose( + username => makeDiscordMention(username, ctx.TediCross.dcBot, bridge.discord.channelId), + R.prop("dcUsername") + ), + R.compose( + R.partial(From.makeDisplayName, [ctx.TediCross.settings.telegram.useFirstNameInsteadOfUsername]), + R.prop("originalFrom") + ) + )(tc.replyTo); + + + header = `**${senderName}** (in reply to **${repliedToName}**`; + + if (ctx.TediCross.settings.discord.displayTelegramReplies === "inline") { + // Make the reply text + const replyText = makeReplyText(tc.replyTo, ctx.TediCross.settings.discord.replyLength, ctx.TediCross.settings.discord.maxReplyLines); + + // Put the reply text in the header, replacing newlines with spaces + header = `${header}: _${R.replace(/\n/g, " ", replyText)}_)`; + } else { + // Append the closing parenthesis + header = `${header})`; + } + } else if (!R.isNil(tc.forwardFrom)) { + // Handle forwards + const origSender = From.makeDisplayName(ctx.TediCross.settings.telegram.useFirstNameInsteadOfUsername, tc.forwardFrom); + header = `**${origSender}** (forwarded by **${senderName}**)`; + } + return header; + }, + R.always("") + )(tc); + + // Handle embed replies + const embed = R.ifElse( + tc => !R.isNil(tc.replyTo) && ctx.TediCross.settings.discord.displayTelegramReplies === "embed", + tc => { + // Make the text + const replyText = handleEntities(tc.replyTo.text.raw, tc.replyTo.text.entities, ctx.TediCross.dcBot, bridge); + + return new Discord.RichEmbed({ + // Discord will not accept embeds with more than 2048 characters + description: R.slice(0, 2048, replyText) + }); + }, + R.always(undefined) + )(tc); + + // Handle file + const file = R.ifElse( + R.compose( + R.isNil, + R.prop("file") + ), + R.always(undefined), + tc => new Discord.Attachment(tc.file.stream, tc.file.name) + )(tc); + + // Make the text to send + const text = handleEntities(tc.text.raw, tc.text.entities, ctx.TediCross.dcBot, bridge); + + return { + bridge, + header, + embed, + file, + text + }; + } + )(tc.bridges); next(); } @@ -442,5 +572,5 @@ module.exports = { addTextObj, addFileObj, addFileStream, - addEditBool + addPreparedObj }; diff --git a/lib/telegram2discord/setup.js b/lib/telegram2discord/setup.js index 7a367f22..ac72269e 100644 --- a/lib/telegram2discord/setup.js +++ b/lib/telegram2discord/setup.js @@ -89,14 +89,10 @@ function setup(logger, tgBot, dcBot, messageMap, bridgeMap, settings) { tgBot.use(middlewares.addTextObj); tgBot.use(middlewares.addFileObj); tgBot.use(middlewares.addFileStream); - tgBot.use(middlewares.addEditBool); - -// TODO Temporary logging to see what's going on. Remove before release - tgBot.use((ctx, next) => {console.log(ctx.tediCross); next();}); + tgBot.use(middlewares.addPreparedObj); // Apply endwares - tgBot.on("edited_message", endwares.handleEdits); - tgBot.on("edited_channel_post", endwares.handleEdits); + tgBot.on(["edited_message", "edited_channel_post"], endwares.handleEdits); tgBot.use(endwares.relayMessage); }) // Start getting updates From 5635aaa22deb25ca317892703c1ae28cc2b38456 Mon Sep 17 00:00:00 2001 From: Simen de Lange Date: Sun, 23 Jun 2019 13:10:58 +0200 Subject: [PATCH 060/102] Removed debug message --- lib/discord2telegram/setup.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/lib/discord2telegram/setup.js b/lib/discord2telegram/setup.js index 88ca7f0d..61d37a85 100644 --- a/lib/discord2telegram/setup.js +++ b/lib/discord2telegram/setup.js @@ -305,8 +305,6 @@ function setup(logger, dcBot, tgBot, messageMap, bridgeMap, settings, datadirPat }); }); - setTimeout(() => dcBot.emit("error", new Error()), 10000); - // Listen for errors dcBot.on("error", (err) => { if (err.code === "ECONNRESET") { From c5d283f38233a55892e7e9194213546568fb7e6c Mon Sep 17 00:00:00 2001 From: Simen de Lange Date: Sun, 23 Jun 2019 13:11:56 +0200 Subject: [PATCH 061/102] 0.9.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 108180ac..e2618b72 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "tedicross", - "version": "0.8.8", + "version": "0.9.0", "description": "Better DiteCross", "license": "MIT", "repository": { From 6b0aa320c0eed72d9cb09b872032e9d4a753b66a Mon Sep 17 00:00:00 2001 From: Simen de Lange Date: Mon, 24 Jun 2019 19:29:05 +0200 Subject: [PATCH 062/102] Fixed files not being sent T2D --- lib/telegram2discord/endwares.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/telegram2discord/endwares.js b/lib/telegram2discord/endwares.js index 6d017c16..a455ec91 100644 --- a/lib/telegram2discord/endwares.js +++ b/lib/telegram2discord/endwares.js @@ -122,7 +122,7 @@ function relayMessage(ctx) { const chunks = R.splitEvery(2000, messageText); // Send them in serial, with the attachment first, if there is one - let dcMessage = await channel.send(R.head(chunks), { attachment: prepared.attachment }); + let dcMessage = await channel.send(R.head(chunks), { file: prepared.file }); if (R.length(chunks) > 1) { dcMessage = await R.reduce((p, chunk) => p.then(() => channel.send(chunk)), Promise.resolve(), R.tail(chunks)); } From 350a638305c187f9be3fdd06d4b25bf0a0d729a8 Mon Sep 17 00:00:00 2001 From: Simen de Lange Date: Mon, 24 Jun 2019 19:29:30 +0200 Subject: [PATCH 063/102] 0.9.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index e2618b72..2a3b80f4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "tedicross", - "version": "0.9.0", + "version": "0.9.1", "description": "Better DiteCross", "license": "MIT", "repository": { From 8a9e2c2693cc75071c000c3e240d80490dce27db Mon Sep 17 00:00:00 2001 From: Simen de Lange Date: Fri, 12 Jul 2019 10:19:37 +0200 Subject: [PATCH 064/102] Made the setting `telegram.sendEmojisWithStickers` work again --- lib/telegram2discord/middlewares.js | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/lib/telegram2discord/middlewares.js b/lib/telegram2discord/middlewares.js index bfb7855d..974467ea 100644 --- a/lib/telegram2discord/middlewares.js +++ b/lib/telegram2discord/middlewares.js @@ -23,7 +23,7 @@ const Discord = require("discord.js"); * * @returns {Object} The text object, or undefined if no text was found */ -function createTextObjFromMessage(message) { +function createTextObjFromMessage(ctx, message) { return R.cond([ // Text [R.has("text"), ({ text, entities }) => ({ @@ -37,7 +37,11 @@ function createTextObjFromMessage(message) { })], // Stickers have an emoji instead of text [R.has("sticker"), message => ({ - raw: R.path(["sticker", "emoji"], message), + raw: R.ifElse( + () => ctx.TediCross.settings.telegram.sendEmojisWithStickers, + R.path(["sticker", "emoji"]), + R.always("") + )(message), entities: [] })], // Default to undefined @@ -299,7 +303,7 @@ function addReplyObj(ctx, next) { isReplyToTediCross, message: repliedToMessage, originalFrom: From.createFromObjFromMessage(repliedToMessage), - text: createTextObjFromMessage(repliedToMessage), + text: createTextObjFromMessage(ctx, repliedToMessage), }; // Handle replies to TediCross @@ -363,7 +367,7 @@ function addForwardFrom(ctx, next) { * @returns {undefined} */ function addTextObj(ctx, next) { - const text = createTextObjFromMessage(ctx.tediCross.message); + const text = createTextObjFromMessage(ctx, ctx.tediCross.message); if (!R.isNil(text)) { ctx.tediCross.text = text; From c5d05187e6881eef36b57d8d30a2f20a8613664f Mon Sep 17 00:00:00 2001 From: Simen de Lange Date: Fri, 12 Jul 2019 10:20:10 +0200 Subject: [PATCH 065/102] 0.9.2 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 2a3b80f4..ecc73ccb 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "tedicross", - "version": "0.9.1", + "version": "0.9.2", "description": "Better DiteCross", "license": "MIT", "repository": { From ca01324e4b9439be0702d2505afa9a58a1af3e0f Mon Sep 17 00:00:00 2001 From: Simen de Lange Date: Fri, 12 Jul 2019 10:24:15 +0200 Subject: [PATCH 066/102] Properly fixed previous issue --- README.md | 2 +- lib/telegram2discord/middlewares.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 746aacb7..6b9ff4f8 100644 --- a/README.md +++ b/README.md @@ -55,7 +55,7 @@ As mentioned in the step by step installation guide, there is a settings file. H * `useFirstNameInsteadOfUsername`: **EXPERIMENTAL** If set to `false`, the messages sent to Discord will be tagged with the sender's username. If set to `true`, the messages sent to Discord will be tagged with the sender's first name (or nickname). Note that Discord users can't @-mention Telegram users by their first name. Defaults to `false` * `colonAfterSenderName`: Whether or not to put a colon after the name of the sender in messages from Discord to Telegram. If true, the name is displayed `Name:`. If false, it is displayed `Name`. Defaults to false * `skipOldMessages`: Whether or not to skip through all previous messages cached from the telegram-side and start processing new messages ONLY. Defaults to true. Note that there is no guarantee the old messages will arrive at Discord in order - * `sendEmojisWithStickers`: Whether or not to send the corresponding emoji when relaying stickers to Discord + * `sendEmojiWithStickers`: Whether or not to send the corresponding emoji when relaying stickers to Discord * `discord`: Object authorizing and defining the Discord bot's behaviour * `token`: The Discord bot's token. It is needed for the bot to authenticate to the Discord servers and be able to send and receive messages. If set to `"env"`, TediCross will read the token from the environment variable `DISCORD_BOT_TOKEN` * `skipOldMessages`: Whether or not to skip through all previous messages sent since the bot was last turned off and start processing new messages ONLY. Defaults to true. Note that there is no guarantee the old messages will arrive at Telegram in order. **NOTE:** [Telegram has a limit](https://core.telegram.org/bots/faq#my-bot-is-hitting-limits-how-do-i-avoid-this) on how quickly a bot can send messages. If there is a big backlog, this will cause problems diff --git a/lib/telegram2discord/middlewares.js b/lib/telegram2discord/middlewares.js index 974467ea..15d00226 100644 --- a/lib/telegram2discord/middlewares.js +++ b/lib/telegram2discord/middlewares.js @@ -38,7 +38,7 @@ function createTextObjFromMessage(ctx, message) { // Stickers have an emoji instead of text [R.has("sticker"), message => ({ raw: R.ifElse( - () => ctx.TediCross.settings.telegram.sendEmojisWithStickers, + () => ctx.TediCross.settings.telegram.sendEmojiWithStickers, R.path(["sticker", "emoji"]), R.always("") )(message), From 7dd5bb59852775774ebb3b324e60a1fcbe1ea0c9 Mon Sep 17 00:00:00 2001 From: Simen de Lange Date: Fri, 12 Jul 2019 10:24:32 +0200 Subject: [PATCH 067/102] 0.9.3 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index ecc73ccb..7e740885 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "tedicross", - "version": "0.9.2", + "version": "0.9.3", "description": "Better DiteCross", "license": "MIT", "repository": { From 09e8596edb3ac4c7a29893b592da7682af01469e Mon Sep 17 00:00:00 2001 From: Simen de Lange Date: Fri, 12 Jul 2019 10:33:28 +0200 Subject: [PATCH 068/102] Fixed typo in readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 6b9ff4f8..a23281cb 100644 --- a/README.md +++ b/README.md @@ -122,7 +122,7 @@ See https://core.telegram.org/bots/faq#why-doesn-39t-my-bot-see-messages-from-ot Telegram bots are unfortunately completely unable to detect when a message is deleted. There is no way to implement T2D cross-deletion until Telegram implements this. -Deleting messaged D2T works. +Deleting messages D2T works. ### When running `npm install`, it complains about missing dependencies? From 7070d223b9432dc557ca3676add706d950f9760c Mon Sep 17 00:00:00 2001 From: Simen de Lange Date: Tue, 30 Jul 2019 00:44:01 +0200 Subject: [PATCH 069/102] Added "support" for animated stickers --- lib/telegram2discord/middlewares.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/lib/telegram2discord/middlewares.js b/lib/telegram2discord/middlewares.js index 15d00226..3e65f768 100644 --- a/lib/telegram2discord/middlewares.js +++ b/lib/telegram2discord/middlewares.js @@ -414,9 +414,14 @@ function addFileObj(ctx, next) { }; } else if (!R.isNil(message.sticker)) { // Sticker +console.log(message.sticker); ctx.tediCross.file = { type: "sticker", - id: message.sticker.file_id, + id: R.ifElse( + R.propEq("is_animated", true), + R.path(["thumb", "file_id"]), + R.prop("file_id") + )(message.sticker), name: "sticker.webp" }; } else if (!R.isNil(message.video)) { From af92ec6f8df7cb37e46b232d4b35cd0cdfb0eea9 Mon Sep 17 00:00:00 2001 From: Simen de Lange Date: Tue, 30 Jul 2019 00:44:32 +0200 Subject: [PATCH 070/102] 0.9.4 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 7e740885..808fd5f4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "tedicross", - "version": "0.9.3", + "version": "0.9.4", "description": "Better DiteCross", "license": "MIT", "repository": { From b0a603f764dc6a83039296c6128ab79c1322bec2 Mon Sep 17 00:00:00 2001 From: Simen de Lange Date: Sat, 3 Aug 2019 12:04:48 +0200 Subject: [PATCH 071/102] Removed a debug log line --- lib/telegram2discord/middlewares.js | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/telegram2discord/middlewares.js b/lib/telegram2discord/middlewares.js index 3e65f768..4cc2e416 100644 --- a/lib/telegram2discord/middlewares.js +++ b/lib/telegram2discord/middlewares.js @@ -414,7 +414,6 @@ function addFileObj(ctx, next) { }; } else if (!R.isNil(message.sticker)) { // Sticker -console.log(message.sticker); ctx.tediCross.file = { type: "sticker", id: R.ifElse( From 96dc5f006f36b53eb2221cc74d429a966641c42f Mon Sep 17 00:00:00 2001 From: Simen de Lange Date: Mon, 5 Aug 2019 09:20:33 +0200 Subject: [PATCH 072/102] Changed name from "lib/" to "src/" --- main.js | 16 ++++++++-------- {lib => src}/Logger.js | 0 {lib => src}/MessageMap.js | 0 {lib => src}/bridgestuff/Bridge.js | 0 {lib => src}/bridgestuff/BridgeMap.js | 0 .../bridgestuff/BridgeSettingsDiscord.js | 0 .../bridgestuff/BridgeSettingsTelegram.js | 0 .../discord2telegram/LatestDiscordMessageIds.js | 0 {lib => src}/discord2telegram/handleEmbed.js | 0 {lib => src}/discord2telegram/md2html.js | 0 .../discord2telegram/relayOldMessages.js | 0 {lib => src}/discord2telegram/setup.js | 0 {lib => src}/migrateSettingsToYAML.js | 0 {lib => src}/settings/DiscordSettings.js | 0 {lib => src}/settings/Settings.js | 0 {lib => src}/settings/TelegramSettings.js | 0 {lib => src}/telegram2discord/From.js | 0 {lib => src}/telegram2discord/endwares.js | 0 {lib => src}/telegram2discord/handleEntities.js | 0 .../telegram2discord/messageConverter.js | 0 {lib => src}/telegram2discord/middlewares.js | 0 {lib => src}/telegram2discord/setup.js | 0 22 files changed, 8 insertions(+), 8 deletions(-) rename {lib => src}/Logger.js (100%) rename {lib => src}/MessageMap.js (100%) rename {lib => src}/bridgestuff/Bridge.js (100%) rename {lib => src}/bridgestuff/BridgeMap.js (100%) rename {lib => src}/bridgestuff/BridgeSettingsDiscord.js (100%) rename {lib => src}/bridgestuff/BridgeSettingsTelegram.js (100%) rename {lib => src}/discord2telegram/LatestDiscordMessageIds.js (100%) rename {lib => src}/discord2telegram/handleEmbed.js (100%) rename {lib => src}/discord2telegram/md2html.js (100%) rename {lib => src}/discord2telegram/relayOldMessages.js (100%) rename {lib => src}/discord2telegram/setup.js (100%) rename {lib => src}/migrateSettingsToYAML.js (100%) rename {lib => src}/settings/DiscordSettings.js (100%) rename {lib => src}/settings/Settings.js (100%) rename {lib => src}/settings/TelegramSettings.js (100%) rename {lib => src}/telegram2discord/From.js (100%) rename {lib => src}/telegram2discord/endwares.js (100%) rename {lib => src}/telegram2discord/handleEntities.js (100%) rename {lib => src}/telegram2discord/messageConverter.js (100%) rename {lib => src}/telegram2discord/middlewares.js (100%) rename {lib => src}/telegram2discord/setup.js (100%) diff --git a/main.js b/main.js index b436e566..0ecccac7 100644 --- a/main.js +++ b/main.js @@ -7,12 +7,12 @@ // General stuff const yargs = require("yargs"); const path = require("path"); -const Logger = require("./lib/Logger"); -const MessageMap = require("./lib/MessageMap"); -const Bridge = require("./lib/bridgestuff/Bridge"); -const BridgeMap = require("./lib/bridgestuff/BridgeMap"); -const Settings = require("./lib/settings/Settings"); -const migrateSettingsToYAML = require("./lib/migrateSettingsToYAML"); +const Logger = require("./src/Logger"); +const MessageMap = require("./src/MessageMap"); +const Bridge = require("./src/bridgestuff/Bridge"); +const BridgeMap = require("./src/bridgestuff/BridgeMap"); +const Settings = require("./src/settings/Settings"); +const migrateSettingsToYAML = require("./src/migrateSettingsToYAML"); const jsYaml = require("js-yaml"); const fs = require("fs"); const R = require("ramda"); @@ -20,11 +20,11 @@ const os = require("os"); // Telegram stuff const Telegraf = require("telegraf"); -const telegramSetup = require("./lib/telegram2discord/setup"); +const telegramSetup = require("./src/telegram2discord/setup"); // Discord stuff const Discord = require("discord.js"); -const discordSetup = require("./lib/discord2telegram/setup"); +const discordSetup = require("./src/discord2telegram/setup"); /************* * TediCross * diff --git a/lib/Logger.js b/src/Logger.js similarity index 100% rename from lib/Logger.js rename to src/Logger.js diff --git a/lib/MessageMap.js b/src/MessageMap.js similarity index 100% rename from lib/MessageMap.js rename to src/MessageMap.js diff --git a/lib/bridgestuff/Bridge.js b/src/bridgestuff/Bridge.js similarity index 100% rename from lib/bridgestuff/Bridge.js rename to src/bridgestuff/Bridge.js diff --git a/lib/bridgestuff/BridgeMap.js b/src/bridgestuff/BridgeMap.js similarity index 100% rename from lib/bridgestuff/BridgeMap.js rename to src/bridgestuff/BridgeMap.js diff --git a/lib/bridgestuff/BridgeSettingsDiscord.js b/src/bridgestuff/BridgeSettingsDiscord.js similarity index 100% rename from lib/bridgestuff/BridgeSettingsDiscord.js rename to src/bridgestuff/BridgeSettingsDiscord.js diff --git a/lib/bridgestuff/BridgeSettingsTelegram.js b/src/bridgestuff/BridgeSettingsTelegram.js similarity index 100% rename from lib/bridgestuff/BridgeSettingsTelegram.js rename to src/bridgestuff/BridgeSettingsTelegram.js diff --git a/lib/discord2telegram/LatestDiscordMessageIds.js b/src/discord2telegram/LatestDiscordMessageIds.js similarity index 100% rename from lib/discord2telegram/LatestDiscordMessageIds.js rename to src/discord2telegram/LatestDiscordMessageIds.js diff --git a/lib/discord2telegram/handleEmbed.js b/src/discord2telegram/handleEmbed.js similarity index 100% rename from lib/discord2telegram/handleEmbed.js rename to src/discord2telegram/handleEmbed.js diff --git a/lib/discord2telegram/md2html.js b/src/discord2telegram/md2html.js similarity index 100% rename from lib/discord2telegram/md2html.js rename to src/discord2telegram/md2html.js diff --git a/lib/discord2telegram/relayOldMessages.js b/src/discord2telegram/relayOldMessages.js similarity index 100% rename from lib/discord2telegram/relayOldMessages.js rename to src/discord2telegram/relayOldMessages.js diff --git a/lib/discord2telegram/setup.js b/src/discord2telegram/setup.js similarity index 100% rename from lib/discord2telegram/setup.js rename to src/discord2telegram/setup.js diff --git a/lib/migrateSettingsToYAML.js b/src/migrateSettingsToYAML.js similarity index 100% rename from lib/migrateSettingsToYAML.js rename to src/migrateSettingsToYAML.js diff --git a/lib/settings/DiscordSettings.js b/src/settings/DiscordSettings.js similarity index 100% rename from lib/settings/DiscordSettings.js rename to src/settings/DiscordSettings.js diff --git a/lib/settings/Settings.js b/src/settings/Settings.js similarity index 100% rename from lib/settings/Settings.js rename to src/settings/Settings.js diff --git a/lib/settings/TelegramSettings.js b/src/settings/TelegramSettings.js similarity index 100% rename from lib/settings/TelegramSettings.js rename to src/settings/TelegramSettings.js diff --git a/lib/telegram2discord/From.js b/src/telegram2discord/From.js similarity index 100% rename from lib/telegram2discord/From.js rename to src/telegram2discord/From.js diff --git a/lib/telegram2discord/endwares.js b/src/telegram2discord/endwares.js similarity index 100% rename from lib/telegram2discord/endwares.js rename to src/telegram2discord/endwares.js diff --git a/lib/telegram2discord/handleEntities.js b/src/telegram2discord/handleEntities.js similarity index 100% rename from lib/telegram2discord/handleEntities.js rename to src/telegram2discord/handleEntities.js diff --git a/lib/telegram2discord/messageConverter.js b/src/telegram2discord/messageConverter.js similarity index 100% rename from lib/telegram2discord/messageConverter.js rename to src/telegram2discord/messageConverter.js diff --git a/lib/telegram2discord/middlewares.js b/src/telegram2discord/middlewares.js similarity index 100% rename from lib/telegram2discord/middlewares.js rename to src/telegram2discord/middlewares.js diff --git a/lib/telegram2discord/setup.js b/src/telegram2discord/setup.js similarity index 100% rename from lib/telegram2discord/setup.js rename to src/telegram2discord/setup.js From fd7b0f89ce479a7d9526b4830bbe2f56ead597b2 Mon Sep 17 00:00:00 2001 From: Simen de Lange Date: Mon, 5 Aug 2019 09:54:10 +0200 Subject: [PATCH 073/102] Now supports locations sent from Telegram --- src/telegram2discord/middlewares.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/telegram2discord/middlewares.js b/src/telegram2discord/middlewares.js index 4cc2e416..14c1302e 100644 --- a/src/telegram2discord/middlewares.js +++ b/src/telegram2discord/middlewares.js @@ -44,6 +44,11 @@ function createTextObjFromMessage(ctx, message) { )(message), entities: [] })], + // Locations must be turned into an URL + [R.has("location"), ({ location }) => ({ + raw: `https://maps.google.com/maps?q=${location.latitude},${location.longitude}&ll=${location.latitude},${location.longitude}&z=16`, + entities: [] + })], // Default to undefined [R.T, R.always({ raw: "", entities: [] })] ])(message); From 0bdfc0dc835294821cffac09feb9a34a7ed3ae91 Mon Sep 17 00:00:00 2001 From: Simen de Lange Date: Mon, 5 Aug 2019 09:59:06 +0200 Subject: [PATCH 074/102] 0.9.5 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 808fd5f4..b7bc6db6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "tedicross", - "version": "0.9.4", + "version": "0.9.5", "description": "Better DiteCross", "license": "MIT", "repository": { From 5814be8920d254d60af634b7ff401de2afc5f60b Mon Sep 17 00:00:00 2001 From: Simen de Lange Date: Sun, 18 Aug 2019 12:05:37 +0200 Subject: [PATCH 075/102] Fixed issue #119 --- src/telegram2discord/middlewares.js | 108 ++++++++++++++++++---------- 1 file changed, 69 insertions(+), 39 deletions(-) diff --git a/src/telegram2discord/middlewares.js b/src/telegram2discord/middlewares.js index 14c1302e..52283b79 100644 --- a/src/telegram2discord/middlewares.js +++ b/src/telegram2discord/middlewares.js @@ -478,51 +478,81 @@ function addPreparedObj(ctx, next) { ctx.tediCross.prepared = R.map( bridge => { // Make the header - const header = R.ifElse( - R.always(bridge.telegram.sendUsernames), - tc => { - // Get the name of the sender of this message - const senderName = From.makeDisplayName(ctx.TediCross.settings.telegram.useFirstNameInsteadOfUsername, tc.from); - - // Make the default header - let header = `**${senderName}**`; - - if (!R.isNil(tc.replyTo)) { - // Add reply info to the header - const repliedToName = R.ifElse( - R.prop("isReplyToTediCross"), - R.compose( - username => makeDiscordMention(username, ctx.TediCross.dcBot, bridge.discord.channelId), - R.prop("dcUsername") - ), - R.compose( - R.partial(From.makeDisplayName, [ctx.TediCross.settings.telegram.useFirstNameInsteadOfUsername]), - R.prop("originalFrom") - ) - )(tc.replyTo); - - + // WARNING! Butt-ugly code! If you see a nice way to clean this up, please do it + const header = (() => { + // Get the name of the sender of this message + const senderName = From.makeDisplayName(ctx.TediCross.settings.telegram.useFirstNameInsteadOfUsername, tc.from); + + // Get the name of the original sender, if this is a forward + const originalSender = R.isNil(tc.forwardFrom) + ? null + : From.makeDisplayName(ctx.TediCross.settings.telegram.useFirstNameInsteadOfUsername, tc.forwardFrom) + ; + + // Get the name of the replied-to user, if this is a reply + const repliedToName = R.isNil(tc.replyTo) + ? null + : R.ifElse( + R.prop("isReplyToTediCross"), + R.compose( + username => makeDiscordMention(username, ctx.TediCross.dcBot, bridge.discord.channelId), + R.prop("dcUsername") + ), + R.compose( + R.partial(From.makeDisplayName, [ctx.TediCross.settings.telegram.useFirstNameInsteadOfUsername]), + R.prop("originalFrom") + ) + )(tc.replyTo) + ; + + // The original text, if this is a reply + const repliedToText = R.isNil(tc.replyTo) + ? null + : (ctx.TediCross.settings.discord.displayTelegramReplies === "inline" + ? makeReplyText(tc.replyTo, ctx.TediCross.settings.discord.replyLength, ctx.TediCross.settings.discord.maxReplyLines) + : null + ) + ; + + let header = ""; + if (bridge.telegram.sendUsernames) { + if (!R.isNil(tc.forwardFrom)) { + // Forward + header = `**${originalSender}** (forwarded by **${senderName}**)`; + } else if (!R.isNil(tc.replyTo)) { + // Reply header = `**${senderName}** (in reply to **${repliedToName}**`; - if (ctx.TediCross.settings.discord.displayTelegramReplies === "inline") { - // Make the reply text - const replyText = makeReplyText(tc.replyTo, ctx.TediCross.settings.discord.replyLength, ctx.TediCross.settings.discord.maxReplyLines); - - // Put the reply text in the header, replacing newlines with spaces - header = `${header}: _${R.replace(/\n/g, " ", replyText)}_)`; + if (!R.isNil(repliedToText)) { + header = `${header}: _${R.replace(/\n/g, " ", repliedToText)}_)`; } else { - // Append the closing parenthesis header = `${header})`; } - } else if (!R.isNil(tc.forwardFrom)) { - // Handle forwards - const origSender = From.makeDisplayName(ctx.TediCross.settings.telegram.useFirstNameInsteadOfUsername, tc.forwardFrom); - header = `**${origSender}** (forwarded by **${senderName}**)`; + } else { + // Ordinary message + header = `**${senderName}**`; } - return header; - }, - R.always("") - )(tc); + } else { + if (!R.isNil(tc.forwardFrom)) { + // Forward + header = `(forward from **${originalSender}**)`; + } else if (!R.isNil(tc.replyTo)) { + // Reply + header = `(in reply to **${repliedToName}**`; + + if (!R.isNil(repliedToText)) { + header = `${header}: _${R.replace(/\n/g, " ", repliedToText)}_)`; + } else { + header = `${header})`; + } + } else { + // Ordinary message + header = ""; + } + } + + return header; + })(); // Handle embed replies const embed = R.ifElse( From 97f07f2885f8991bbd64e993b6cbb115caf39c54 Mon Sep 17 00:00:00 2001 From: Simen de Lange Date: Sun, 18 Aug 2019 12:08:41 +0200 Subject: [PATCH 076/102] 0.9.6 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index b7bc6db6..6541d380 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "tedicross", - "version": "0.9.5", + "version": "0.9.6", "description": "Better DiteCross", "license": "MIT", "repository": { From f6e0a3834e9f2e505ed105b52fb100aa5945949f Mon Sep 17 00:00:00 2001 From: Simen de Lange Date: Sun, 25 Aug 2019 08:40:03 +0200 Subject: [PATCH 077/102] The chatinfo command and result are deleted after a minute --- src/discord2telegram/setup.js | 13 ++++++++++++- src/sleep.js | 29 +++++++++++++++++++++++++++++ src/telegram2discord/endwares.js | 22 ++++++++++++++++++++-- 3 files changed, 61 insertions(+), 3 deletions(-) create mode 100644 src/sleep.js diff --git a/src/discord2telegram/setup.js b/src/discord2telegram/setup.js index 61d37a85..c3ff0471 100644 --- a/src/discord2telegram/setup.js +++ b/src/discord2telegram/setup.js @@ -13,6 +13,7 @@ const relayOldMessages = require("./relayOldMessages"); const Bridge = require("../bridgestuff/Bridge"); const path = require("path"); const R = require("ramda"); +const { sleepOneMinute } = require("../sleep"); /*********** * Helpers * @@ -110,7 +111,17 @@ function setup(logger, dcBot, tgBot, messageMap, bridgeMap, settings, datadirPat message.reply( "serverId: '" + message.guild.id + "'\n" + "channelId: '" + message.channel.id + "'\n" - ); + ) + .then(sleepOneMinute) + .then(info => Promise.all([ + info.delete(), + message.delete() + ])) + .catch(R.ifElse( + R.propEq("message", "Unknown Message"), + R.always(undefined), + err => {throw err;} + )); // Don't process the message any further return; diff --git a/src/sleep.js b/src/sleep.js new file mode 100644 index 00000000..ddc185d9 --- /dev/null +++ b/src/sleep.js @@ -0,0 +1,29 @@ +"use strict"; + +const util = require("util"); +const R = require("ramda"); +const moment = require("moment"); + +/** + * Makes a promise which resolves after a set number of milliseconds + * + * @param {Integer} ms Number of milliseconds to slieep + * @param {Any} [arg] Optional argument to resolve the promise to + * + * @returns {Promise} Promise resolving after the given number of ms + */ +const sleep = util.promisify(setTimeout); + +/** + * Makes a promise which resolves after one minute + * + * @param {Any} [arg] Optional argument to resolve the promise to + * + * @returns {Promise} Promise resolving after one minute + */ +const sleepOneMinute = R.partial(sleep, [moment.duration(1, "minute").asMilliseconds()]); + +module.exports = { + sleep, + sleepOneMinute +}; diff --git a/src/telegram2discord/endwares.js b/src/telegram2discord/endwares.js index a455ec91..815f1b8d 100644 --- a/src/telegram2discord/endwares.js +++ b/src/telegram2discord/endwares.js @@ -7,7 +7,7 @@ const R = require("ramda"); const From = require("./From"); const MessageMap = require("../MessageMap"); -const messageConverter = require("./messageConverter"); +const { sleepOneMinute } = require("../sleep"); /*********** * Helpers * @@ -46,7 +46,25 @@ const createMessageHandler = R.curry((func, ctx) => { * @returns {undefined} */ function chatinfo(ctx) { - ctx.reply(`chatID: ${ctx.tediCross.message.chat.id}`); + // Reply with the info + ctx.reply(`chatID: ${ctx.tediCross.message.chat.id}`) + // Wait some time + .then(sleepOneMinute) + // Delete the info and the command + .then(({ message_id, chat }) => Promise.all([ + // Delete the info + ctx.telegram.deleteMessage( + chat.id, + message_id + ), + // Delete the command + ctx.deleteMessage() + ])) + .catch(R.ifElse( + R.propEq("description", "Bad Request: message to delete not found"), + R.always(undefined), + err => {throw err;} + )); } /** From 479348ae45eadcc9722c58d38f31d8e6d43b1324 Mon Sep 17 00:00:00 2001 From: Simen de Lange Date: Sun, 25 Aug 2019 09:22:41 +0200 Subject: [PATCH 078/102] "This is an instance..." now autodelets after a minute --- src/discord2telegram/helpers.js | 34 ++++++++++++++++++++ src/discord2telegram/setup.js | 13 ++++---- src/telegram2discord/endwares.js | 14 +++------ src/telegram2discord/helpers.js | 49 +++++++++++++++++++++++++++++ src/telegram2discord/middlewares.js | 8 ++++- 5 files changed, 101 insertions(+), 17 deletions(-) create mode 100644 src/discord2telegram/helpers.js create mode 100644 src/telegram2discord/helpers.js diff --git a/src/discord2telegram/helpers.js b/src/discord2telegram/helpers.js new file mode 100644 index 00000000..aa82d118 --- /dev/null +++ b/src/discord2telegram/helpers.js @@ -0,0 +1,34 @@ +"use strict"; + +/************************** + * Import important stuff * + **************************/ + +const R = require("ramda"); + +/******************** + * Make the helpers * + ********************/ + +/** + * Ignores errors arising from trying to delete an already deleted message. Rethrows other errors + * + * @param {Error} err The error to check + * + * @returns {undefined} + * + * @throws {Error} The error, if it is another type + */ +const ignoreAlreadyDeletedError = R.ifElse( + R.propEq("message", "Unknown Message"), + R.always(undefined), + err => {throw err;} +); + +/*************** + * Export them * + ***************/ + +module.exports = { + ignoreAlreadyDeletedError +}; diff --git a/src/discord2telegram/setup.js b/src/discord2telegram/setup.js index c3ff0471..53816d6c 100644 --- a/src/discord2telegram/setup.js +++ b/src/discord2telegram/setup.js @@ -14,6 +14,7 @@ const Bridge = require("../bridgestuff/Bridge"); const path = require("path"); const R = require("ramda"); const { sleepOneMinute } = require("../sleep"); +const helpers = require("./helpers"); /*********** * Helpers * @@ -117,11 +118,7 @@ function setup(logger, dcBot, tgBot, messageMap, bridgeMap, settings, datadirPat info.delete(), message.delete() ])) - .catch(R.ifElse( - R.propEq("message", "Unknown Message"), - R.always(undefined), - err => {throw err;} - )); + .catch(helpers.ignoreAlreadyDeletedError); // Don't process the message any further return; @@ -221,7 +218,11 @@ function setup(logger, dcBot, tgBot, messageMap, bridgeMap, settings, datadirPat "This is an instance of a TediCross bot, bridging a chat in Telegram with one in Discord. " + "If you wish to use TediCross yourself, please download and create an instance. " + "See https://github.com/TediCross/TediCross" - ); + ) + // Delete it again after some time + .then(sleepOneMinute) + .then(message => message.delete()) + .catch(helpers.ignoreAlreadyDeletedError); } }); diff --git a/src/telegram2discord/endwares.js b/src/telegram2discord/endwares.js index 815f1b8d..655deaf2 100644 --- a/src/telegram2discord/endwares.js +++ b/src/telegram2discord/endwares.js @@ -8,6 +8,7 @@ const R = require("ramda"); const From = require("./From"); const MessageMap = require("../MessageMap"); const { sleepOneMinute } = require("../sleep"); +const helpers = require("./helpers"); /*********** * Helpers * @@ -51,20 +52,13 @@ function chatinfo(ctx) { // Wait some time .then(sleepOneMinute) // Delete the info and the command - .then(({ message_id, chat }) => Promise.all([ + .then(message => Promise.all([ // Delete the info - ctx.telegram.deleteMessage( - chat.id, - message_id - ), + helpers.deleteMessage(ctx, message), // Delete the command ctx.deleteMessage() ])) - .catch(R.ifElse( - R.propEq("description", "Bad Request: message to delete not found"), - R.always(undefined), - err => {throw err;} - )); + .catch(helpers.ignoreAlreadyDeletedError); } /** diff --git a/src/telegram2discord/helpers.js b/src/telegram2discord/helpers.js new file mode 100644 index 00000000..bc7cec6c --- /dev/null +++ b/src/telegram2discord/helpers.js @@ -0,0 +1,49 @@ +"use strict"; + +/************************** + * Import important stuff * + **************************/ + +const R = require("ramda"); + +/******************** + * Make the helpers * + ********************/ + +/** + * Ignores errors arising from trying to delete an already deleted message. Rethrows other errors + * + * @param {Error} err The error to check + * + * @returns {undefined} + * + * @throws {Error} The error, if it is another type + */ +const ignoreAlreadyDeletedError = R.ifElse( + R.propEq("description", "Bad Request: message to delete not found"), + R.always(undefined), + err => {throw err;} +); + +/** + * Deletes a Telegram message + * + * @param {Context} ctx The Telegraf context to use + * @param {Object} message The message to delete + * + * @returns {Promise} Promise resolving when the message is deleted + */ +const deleteMessage = R.curry((ctx, { chat, message_id }) => + ctx.telegram.deleteMessage( + chat.id, + message_id + )); + +/*************** + * Export them * + ***************/ + +module.exports = { + ignoreAlreadyDeletedError, + deleteMessage +}; diff --git a/src/telegram2discord/middlewares.js b/src/telegram2discord/middlewares.js index 52283b79..82e19134 100644 --- a/src/telegram2discord/middlewares.js +++ b/src/telegram2discord/middlewares.js @@ -11,6 +11,8 @@ const mime = require("mime/lite"); const request = require("request"); const handleEntities = require("./handleEntities"); const Discord = require("discord.js"); +const { sleepOneMinute } = require("../sleep"); +const helpers = require("./helpers"); /*********** * Helpers * @@ -267,7 +269,11 @@ function informThisIsPrivateBot(ctx, next) { { parse_mode: "markdown" } - ), + ) + // Delete it again after a while + .then(sleepOneMinute) + .then(helpers.deleteMessage(ctx)) + .catch(helpers.ignoreAlreadyDeletedError), // Otherwise go to next middleware next )(ctx); From bc3ee6566bf379198b4b383f17b210068a1333ac Mon Sep 17 00:00:00 2001 From: Simen de Lange Date: Sun, 25 Aug 2019 09:28:03 +0200 Subject: [PATCH 079/102] Updated dependencies --- package.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index 6541d380..6eca4db6 100644 --- a/package.json +++ b/package.json @@ -12,15 +12,15 @@ "lint": "eslint ." }, "dependencies": { - "discord.js": "^11.5.0", + "discord.js": "^11.5.1", "js-yaml": "^3.13.1", - "mime": "^2.4.2", + "mime": "^2.4.4", "moment": "^2.24.0", "ramda": "^0.26.1", "request": "^2.88.0", "simple-markdown": "^0.4.4", - "telegraf": "^3.29.0", - "yargs": "^13.2.4" + "telegraf": "^3.32.0", + "yargs": "^13.3.0" }, "devDependencies": { "eslint": "^5.16.0" From d1e684ef7e4c794c4b8bbdede24b886bd5c6e87e Mon Sep 17 00:00:00 2001 From: Simen de Lange Date: Sun, 25 Aug 2019 09:50:34 +0200 Subject: [PATCH 080/102] Dockerfile now uses the 'tedicross' user instead of rooti --- Dockerfile | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 9b6e7d80..bc082f20 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,10 +2,13 @@ FROM node:10-alpine WORKDIR /opt/TediCross/ -ADD . . +COPY . . RUN npm install --production +RUN adduser -S tedicross +USER tedicross + VOLUME /opt/TediCross/data/ ENTRYPOINT /usr/local/bin/npm start -- -c data/settings.yaml From 3d531aa5e924a767dae2d315224d4f85a87ae54a Mon Sep 17 00:00:00 2001 From: Simen de Lange Date: Sun, 25 Aug 2019 09:51:10 +0200 Subject: [PATCH 081/102] 0.9.7 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 6eca4db6..014e2d71 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "tedicross", - "version": "0.9.6", + "version": "0.9.7", "description": "Better DiteCross", "license": "MIT", "repository": { From 4793cd53ad23443afb0cfa26ec7291505a2091c3 Mon Sep 17 00:00:00 2001 From: Simen de Lange Date: Thu, 24 Oct 2019 17:09:47 +0200 Subject: [PATCH 082/102] Fixed bug with names with < in them --- src/discord2telegram/helpers.js | 16 +++++++++++++++- src/discord2telegram/md2html.js | 6 ++---- src/discord2telegram/setup.js | 16 +++++++++++++++- 3 files changed, 32 insertions(+), 6 deletions(-) diff --git a/src/discord2telegram/helpers.js b/src/discord2telegram/helpers.js index aa82d118..e9138f79 100644 --- a/src/discord2telegram/helpers.js +++ b/src/discord2telegram/helpers.js @@ -25,10 +25,24 @@ const ignoreAlreadyDeletedError = R.ifElse( err => {throw err;} ); +/** + * Converts characters '&', '<' and '>' in strings into HTML safe strings + * + * @param {String} text The text to escape the characters in + * + * @returns {String} The escaped string + */ +const escapeHTMLSpecialChars = R.compose( + R.replace(/>/g, ">"), + R.replace(//g, ">"); + const processedText = escapeHTMLSpecialChars(text); // Parse the markdown and build HTML out of it const html = mdParse(processedText) diff --git a/src/discord2telegram/setup.js b/src/discord2telegram/setup.js index 53816d6c..acd8341e 100644 --- a/src/discord2telegram/setup.js +++ b/src/discord2telegram/setup.js @@ -125,7 +125,21 @@ function setup(logger, dcBot, tgBot, messageMap, bridgeMap, settings, datadirPat } // Get info about the sender - const senderName = (useNickname && message.member ? message.member.displayName : message.author.username) + (settings.telegram.colonAfterSenderName ? ":" : ""); + const senderName = R.compose( + // Make it HTML safe + helpers.escapeHTMLSpecialChars, + // Add a colon if wanted + R.when( + R.always(settings.telegram.colonAfterSenderName), + senderName => senderName + ":" + ), + // Figure out what name to use + R.ifElse( + message => useNickname && !R.isNil(message.member), + R.path(["member", "displayName"]), + R.path(["author", "username"]) + ) + )(message); // Check if the message is from the correct chat const bridges = bridgeMap.fromDiscordChannelId(message.channel.id); From d89b22035a9d7017ca637660102a9b645be8c625 Mon Sep 17 00:00:00 2001 From: Simen de Lange Date: Thu, 24 Oct 2019 17:10:22 +0200 Subject: [PATCH 083/102] 0.9.8 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 014e2d71..eb84fce8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "tedicross", - "version": "0.9.7", + "version": "0.9.8", "description": "Better DiteCross", "license": "MIT", "repository": { From d8e5a46544b61b27732605548fa599965ea074c8 Mon Sep 17 00:00:00 2001 From: Simen de Lange Date: Fri, 8 Nov 2019 22:12:56 +0100 Subject: [PATCH 084/102] Renamed `ignoreCommands` to `relayCommands` --- README.md | 2 +- example.settings.yaml | 1 + src/bridgestuff/BridgeSettingsTelegram.js | 10 +++++----- src/settings/Settings.js | 10 +++++++++- src/telegram2discord/middlewares.js | 4 ++-- 5 files changed, 18 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index a23281cb..8d867864 100644 --- a/README.md +++ b/README.md @@ -71,7 +71,7 @@ As mentioned in the step by step installation guide, there is a settings file. H * `telegram.relayJoinMessages`: Whether or not to relay messages to Discord about people joining the Telegram chat * `telegram.relayLeaveMessages`: Whether or not to relay messages to Discord about people leaving the Telegram chat * `telegram.sendUsernames`: Whether or not to send the sender's name with the messages to Discord - * `telegram.ignoreCommands`: If set to `true`, messages starting with a `/` are not relayed to Discord + * `telegram.relayCommands`: If set to `false`, messages starting with a `/` are not relayed to Discord * `discord.guild`: ID of the server the Discord end of the bridge is in. If a message to the bot originates from within this server, but not the correct channel, it is ignored, instead of triggering a reply telling the sender to get their own bot. See step 11 on how to aquire it * `discord.channel`: ID of the channel the Discord end of the bridge is in. See step 11 on how to aquire it * `discord.relayJoinMessages`: Whether or not to relay messages to Telegram about people joining the Discord chat diff --git a/example.settings.yaml b/example.settings.yaml index 7ac8a6e0..698de8f2 100644 --- a/example.settings.yaml +++ b/example.settings.yaml @@ -19,6 +19,7 @@ bridges: relayJoinMessages: true relayLeaveMessages: true sendUsernames: true + relayCommands: true discord: serverId: 'DISCORD_SERVER_ID' # This ID must be wrapped in single quotes. Example: '244791815503347712' channelId: 'DISCORD_CHANNEL_ID' # This ID must be wrapped in single quotes. Example: '244791815503347712' diff --git a/src/bridgestuff/BridgeSettingsTelegram.js b/src/bridgestuff/BridgeSettingsTelegram.js index 5746efd6..e8340b03 100644 --- a/src/bridgestuff/BridgeSettingsTelegram.js +++ b/src/bridgestuff/BridgeSettingsTelegram.js @@ -55,11 +55,11 @@ class BridgeSettingsTelegram { this.sendUsernames = settings.sendUsernames; /** - * Whether or not to ignore messages starting with "/" (commands) + * Whether or not to relay messages starting with "/" (commands) * * @type {Boolean} */ - this.ignoreCommands = settings.ignoreCommands; + this.relayCommands = settings.relayCommands; } /** @@ -90,9 +90,9 @@ class BridgeSettingsTelegram { throw new Error("`settings.sendUsernames` must be a boolean"); } - // Check that ignoreCommands is a boolean - if (Boolean(settings.ignoreCommands) !== settings.ignoreCommands) { - throw new Error("`settings.ignoreCommands` must be a boolean"); + // Check that relayCommands is a boolean + if (Boolean(settings.relayCommands) !== settings.relayCommands) { + throw new Error("`settings.relayCommands` must be a boolean"); } } } diff --git a/src/settings/Settings.js b/src/settings/Settings.js index 4d512661..e0c607da 100644 --- a/src/settings/Settings.js +++ b/src/settings/Settings.js @@ -169,7 +169,7 @@ class Settings { // 2019-04-22: Add the `ignoreCommands` option to Telegram for (const bridge of settings.bridges) { - if (R.isNil(bridge.telegram.ignoreCommands)) { + if (R.isNil(bridge.telegram.ignoreCommands) && R.isNil(bridge.telegram.relayCommands)) { bridge.telegram.ignoreCommands = false; } } @@ -179,6 +179,14 @@ class Settings { settings.discord.maxReplyLines = 2; } + // 2019-11-08: Turn `ignoreCommands` into `relayCommands`, as `ignoreCommands` accidently did the opposite of what it was supposed to do + for (const bridge of settings.bridges) { + if (R.isNil(bridge.telegram.relayCommands)) { + bridge.telegram.relayCommands = bridge.telegram.ignoreCommands; + delete bridge.telegram.ignoreCommands; + } + } + // All done! return settings; } diff --git a/src/telegram2discord/middlewares.js b/src/telegram2discord/middlewares.js index 14c1302e..6ae583ca 100644 --- a/src/telegram2discord/middlewares.js +++ b/src/telegram2discord/middlewares.js @@ -198,7 +198,7 @@ function removeD2TBridges(ctx, next) { } /** - * Removes bridges with the `ignoreCommand` flag from the bridge list + * Removes bridges with the `relayCommands` flag set to false from the bridge list * * @param {Object} ctx The Telegraf context to use * @param {Object} ctx.tediCross The TediCross object on the context @@ -208,7 +208,7 @@ function removeD2TBridges(ctx, next) { * @returns {undefined} */ function removeBridgesIgnoringCommands(ctx, next) { - ctx.tediCross.bridges = R.filter(R.path(["telegram", "ignoreCommands"]), ctx.tediCross.bridges); + ctx.tediCross.bridges = R.filter(R.path(["telegram", "relayCommands"]), ctx.tediCross.bridges); next(); } From 1970c2384ea3cdc18ecd6ff1bd7cf4bb915188ea Mon Sep 17 00:00:00 2001 From: Simen de Lange Date: Fri, 8 Nov 2019 22:15:30 +0100 Subject: [PATCH 085/102] Now actually removes the `ignoreCommands` option --- src/settings/Settings.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/settings/Settings.js b/src/settings/Settings.js index e0c607da..25ad4c61 100644 --- a/src/settings/Settings.js +++ b/src/settings/Settings.js @@ -183,8 +183,8 @@ class Settings { for (const bridge of settings.bridges) { if (R.isNil(bridge.telegram.relayCommands)) { bridge.telegram.relayCommands = bridge.telegram.ignoreCommands; - delete bridge.telegram.ignoreCommands; } + delete bridge.telegram.ignoreCommands; } // All done! From eb2c0685f516f4ba48bb110287dc509358e9b01f Mon Sep 17 00:00:00 2001 From: Simen de Lange Date: Fri, 8 Nov 2019 22:45:48 +0100 Subject: [PATCH 086/102] Got rid of the `serverId` setting --- README.md | 5 ++--- example.settings.yaml | 1 - src/bridgestuff/BridgeMap.js | 21 --------------------- src/bridgestuff/BridgeSettingsDiscord.js | 8 -------- src/discord2telegram/setup.js | 24 +++++++++++++++++++++--- src/settings/Settings.js | 6 ++++++ 6 files changed, 29 insertions(+), 36 deletions(-) diff --git a/README.md b/README.md index 8d867864..95d85da1 100644 --- a/README.md +++ b/README.md @@ -39,7 +39,7 @@ Setting up the bot requires basic knowledge of the command line, which is bash o 10. Start TediCross: `npm start` 11. Ask the bots for the remaining details. In the Telegram chat and the Discord channel, write `/chatinfo`. Put the info you get in the settings file. - If you want to bridge a Telegram group or channel, remember that the ID is negative. Include the `-` when entering it into the settings file - - It is important that the Discord IDs are wrapped with single quotes when entered into the settings file. `'244791815503347712'`, not `244791815503347712` + - It is important that the Discord channel ID is wrapped with single quotes when entered into the settings file. `'244791815503347712'`, not `244791815503347712` 12. Restart TediCross. You stop it by pressing CTRL + C in the terminal it is running in Done! You now have a nice bridge between a Telegram chat and a Discord channel @@ -72,8 +72,7 @@ As mentioned in the step by step installation guide, there is a settings file. H * `telegram.relayLeaveMessages`: Whether or not to relay messages to Discord about people leaving the Telegram chat * `telegram.sendUsernames`: Whether or not to send the sender's name with the messages to Discord * `telegram.relayCommands`: If set to `false`, messages starting with a `/` are not relayed to Discord - * `discord.guild`: ID of the server the Discord end of the bridge is in. If a message to the bot originates from within this server, but not the correct channel, it is ignored, instead of triggering a reply telling the sender to get their own bot. See step 11 on how to aquire it - * `discord.channel`: ID of the channel the Discord end of the bridge is in. See step 11 on how to aquire it + * `discord.channelId`: ID of the channel the Discord end of the bridge is in. See step 11 on how to aquire it * `discord.relayJoinMessages`: Whether or not to relay messages to Telegram about people joining the Discord chat * `discord.relayLeaveMessages`: Whether or not to relay messages to Telegram about people leaving the Discord chat * `discord.sendUsernames`: Whether or not to send the sender's name with the messages to Telegram diff --git a/example.settings.yaml b/example.settings.yaml index 698de8f2..2d77af0e 100644 --- a/example.settings.yaml +++ b/example.settings.yaml @@ -21,7 +21,6 @@ bridges: sendUsernames: true relayCommands: true discord: - serverId: 'DISCORD_SERVER_ID' # This ID must be wrapped in single quotes. Example: '244791815503347712' channelId: 'DISCORD_CHANNEL_ID' # This ID must be wrapped in single quotes. Example: '244791815503347712' relayJoinMessages: true relayLeaveMessages: true diff --git a/src/bridgestuff/BridgeMap.js b/src/bridgestuff/BridgeMap.js index 191396b3..4c95164b 100644 --- a/src/bridgestuff/BridgeMap.js +++ b/src/bridgestuff/BridgeMap.js @@ -45,22 +45,12 @@ class BridgeMap { */ this._telegramToBridge = new Map(); - /** - * Set of Discord servers which are bridged - * - * @type {Set} - * - * @private - */ - this._discordServers = new Set(); - // Populate the maps and set bridges.forEach((bridge) => { const d = this._discordToBridge.get(bridge.discord.channelId) || []; const t = this._telegramToBridge.get(bridge.telegram.chatId) || []; this._discordToBridge.set(bridge.discord.channelId, [...d, bridge]); this._telegramToBridge.set(bridge.telegram.chatId, [...t, bridge]); - this._discordServers.add(bridge.discord.serverId); }); } @@ -85,17 +75,6 @@ class BridgeMap { fromDiscordChannelId(discordChannelId) { return R.defaultTo([], this._discordToBridge.get(discordChannelId)); } - - /** - * Checks if a Discord server ID is known - * - * @param {String} discordServerId Discord server ID to check - * - * @returns {Boolean} True if the server is known, false otherwise - */ - knownDiscordServer(discordServerId) { - return this._discordServers.has(discordServerId); - } } /************* diff --git a/src/bridgestuff/BridgeSettingsDiscord.js b/src/bridgestuff/BridgeSettingsDiscord.js index 1930655e..c4113ac5 100644 --- a/src/bridgestuff/BridgeSettingsDiscord.js +++ b/src/bridgestuff/BridgeSettingsDiscord.js @@ -18,7 +18,6 @@ class BridgeSettingsDiscord { * Creates a new BridgeSettingsDiscord object * * @param {Object} settings Settings for the Discord side of the bridge - * @param {String} settings.serverId ID of the Discord server this bridge is part of * @param {String} settings.channelId ID of the Discord channel this bridge is part of * @param {Boolean} settings.relayJoinMessages Whether or not to relay join messages from Discord to Telegram * @param {Boolean} settings.relayLeaveMessages Whether or not to relay leave messages from Discord to Telegram @@ -28,13 +27,6 @@ class BridgeSettingsDiscord { constructor(settings) { BridgeSettingsDiscord.validate(settings); - /** - * ID of the Discord server this bridge is part of - * - * @type {String} - */ - this.serverId = settings.serverId; - /** * ID of the Discord channel this bridge is part of * diff --git a/src/discord2telegram/setup.js b/src/discord2telegram/setup.js index 61d37a85..9858f0b1 100644 --- a/src/discord2telegram/setup.js +++ b/src/discord2telegram/setup.js @@ -90,6 +90,9 @@ function setup(logger, dcBot, tgBot, messageMap, bridgeMap, settings, datadirPat const latestDiscordMessageIds = new LatestDiscordMessageIds(logger, path.join(datadirPath, "latestDiscordMessageIds.json")); const useNickname = settings.discord.useNickname; + // Set of server IDs. Will be filled when the bot is ready + const knownServerIds = new Set(); + // Listen for users joining the server dcBot.on("guildMemberAdd", makeJoinLeaveFunc(logger, "joined", bridgeMap, tgBot)); @@ -108,8 +111,7 @@ function setup(logger, dcBot, tgBot, messageMap, bridgeMap, settings, datadirPat if (message.channel.type === "text" && message.cleanContent === "/chatinfo") { // It is. Give it message.reply( - "serverId: '" + message.guild.id + "'\n" + - "channelId: '" + message.channel.id + "'\n" + "\nchannelId: '" + message.channel.id + "'" ); // Don't process the message any further @@ -204,7 +206,7 @@ function setup(logger, dcBot, tgBot, messageMap, bridgeMap, settings, datadirPat } } }); - } else if (R.isNil(message.channel.guild) || !bridgeMap.knownDiscordServer(message.channel.guild.id)) { // Check if it is the correct server + } else if (R.isNil(message.channel.guild) || !knownServerIds.has(message.channel.guild.id)) { // Check if it is the correct server // The message is from the wrong chat. Inform the sender that this is a private bot message.reply( "This is an instance of a TediCross bot, bridging a chat in Telegram with one in Discord. " @@ -359,6 +361,22 @@ function setup(logger, dcBot, tgBot, messageMap, bridgeMap, settings, datadirPat // Log the event logger.info(`Discord: ${dcBot.user.username} (${dcBot.user.id})`); + // Get the server IDs from the channels + R.compose( + // Add them to the known server ID set + R.reduce((knownServerIds, serverId) => knownServerIds.add(serverId), knownServerIds), + // Remove the invalid channels + R.filter(R.complement(R.isNil)), + // Extract the server IDs from the channels + R.map(R.path(["guild", "id"])), + // Get the channels + R.map(channelId => dcBot.channels.get(channelId)), + // Get the channel IDs + R.map(R.path(["discord", "channelId"])), + // Get the bridges + R.prop("bridges") + )(bridgeMap); + // Mark the bot as ready resolve(); }); diff --git a/src/settings/Settings.js b/src/settings/Settings.js index 25ad4c61..52021060 100644 --- a/src/settings/Settings.js +++ b/src/settings/Settings.js @@ -187,6 +187,12 @@ class Settings { delete bridge.telegram.ignoreCommands; } + // 2019-11-08: Remove the `serverId` setting from the discord part of the bridges + for (const bridge of settings.bridges) { + delete bridge.discord.serverId; + } + + // All done! return settings; } From c0949c610f0345a15d154bc3245bf6d3a6557a7b Mon Sep 17 00:00:00 2001 From: Simen de Lange Date: Fri, 8 Nov 2019 22:50:45 +0100 Subject: [PATCH 087/102] 0.9.5 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 808fd5f4..b7bc6db6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "tedicross", - "version": "0.9.4", + "version": "0.9.5", "description": "Better DiteCross", "license": "MIT", "repository": { From a6b40cdf49ae2ff4f9b4904c241d6d1847eaa3bd Mon Sep 17 00:00:00 2001 From: Simen de Lange Date: Fri, 8 Nov 2019 22:55:06 +0100 Subject: [PATCH 088/102] 0.9.9 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index eb84fce8..8473c70e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "tedicross", - "version": "0.9.8", + "version": "0.9.9", "description": "Better DiteCross", "license": "MIT", "repository": { From e5d4b5ac0dcc5bb2a1e223c7c4135d33e1a915c8 Mon Sep 17 00:00:00 2001 From: Simen de Lange Date: Tue, 12 Nov 2019 19:05:33 +0100 Subject: [PATCH 089/102] Removed last references to serverId setting --- src/telegram2discord/handleEntities.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/telegram2discord/handleEntities.js b/src/telegram2discord/handleEntities.js index 978db637..6b4dcf18 100644 --- a/src/telegram2discord/handleEntities.js +++ b/src/telegram2discord/handleEntities.js @@ -64,9 +64,10 @@ function handleEntities(text, entities, dcBot, bridge) { // A mention. Substitute the Discord user ID or Discord role ID if one exists // XXX Telegram considers it a mention if it is a valid Telegram username, not necessarily taken. This means the mention matches the regexp /^@[a-zA-Z0-9_]{5,}$/ // In turn, this means short usernames and roles in Discord, like '@devs', will not be possible to mention + const channel = dcBot.channels.get(bridge.discord.channelId); const mentionable = new RegExp(`^${part.substring(1)}$`, "i"); - const dcUser = dcBot.channels.get(bridge.discord.channelId).members.find(findFn("displayName", mentionable)); - const dcRole = dcBot.guilds.get(bridge.discord.serverId).roles.find(findFn("name", mentionable)); + const dcUser = channel.members.find(findFn("displayName", mentionable)); + const dcRole = channel.guild.roles.find(findFn("name", mentionable)); if (!R.isNil(dcUser)) { substitute = `<@${dcUser.id}>`; } else if (!R.isNil(dcRole)) { @@ -110,7 +111,7 @@ function handleEntities(text, entities, dcBot, bridge) { const channelName = new RegExp(`^${part.substring(1)}$`); // Find out if this is a channel on the bridged Discord server - const channel = dcBot.guilds.get(bridge.discord.serverId).channels.find(findFn("name", channelName)); + const channel = dcBot.channels.get(bridge.discord.channelId).guild.channels.find(findFn("name", channelName)); // Make Discord recognize it as a channel mention if (channel !== null) { From d5e3e3857edf2b27bc190fd8a81e95f945f84826 Mon Sep 17 00:00:00 2001 From: Simen de Lange Date: Tue, 12 Nov 2019 19:05:54 +0100 Subject: [PATCH 090/102] 0.9.10 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 8473c70e..802b465a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "tedicross", - "version": "0.9.9", + "version": "0.9.10", "description": "Better DiteCross", "license": "MIT", "repository": { From 22e3bb37ed5780c9cc24eeab5eae721c20e46c48 Mon Sep 17 00:00:00 2001 From: trgwii Date: Wed, 15 Jan 2020 12:38:10 +0100 Subject: [PATCH 091/102] Add a link to ordinary install instructions --- README-Docker.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README-Docker.md b/README-Docker.md index ee58d9c5..d039d1ee 100644 --- a/README-Docker.md +++ b/README-Docker.md @@ -1,7 +1,7 @@ TediCross with Docker ===================== -**This document assumes you know how to use [Docker](https://en.wikipedia.org/wiki/Docker_(software)). If you are completely clueless, please disregard Docker and follow the ordinary install instructions** +**This document assumes you know how to use [Docker](https://en.wikipedia.org/wiki/Docker_(software)). If you are completely clueless, please disregard Docker and follow the ordinary [install instructions](README.md#step-by-step-installation)** TediCross is available as a Docker image, through [DockerHub](https://cloud.docker.com/u/tedicross/repository/docker/tedicross/tedicross) From 1a3ee184cad4664fde39aa9319d573483fb76329 Mon Sep 17 00:00:00 2001 From: Simen de Lange Date: Fri, 24 Jan 2020 16:32:29 +0100 Subject: [PATCH 092/102] Added "start-dev" command to npm --- README.md | 2 +- package.json | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 95d85da1..fb159537 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,7 @@ Setting up the bot requires basic knowledge of the command line, which is bash o 1. Install [nodejs](https://nodejs.org) 2. Clone this git repo, or download it as a zip or whatever 3. Open a terminal and enter the repo with the [`cd`](https://en.wikipedia.org/wiki/Cd_(command)) command. Something like `cd Downloads/TediCross-master`. Your exact command may differ - 4. Run the command `npm install` + 4. Run the command `npm install --production` 5. Make a copy of the file `example.settings.yaml` and name it `settings.yaml` 6. Aquire a bot token for Telegram ([How to create a Telegram bot](https://core.telegram.org/bots#3-how-do-i-create-a-bot)) and put it in the settings file - The Telegram bot must be able to access all messages. Talk to [@BotFather](https://t.me/BotFather) to disable privacy mode for the bot diff --git a/package.json b/package.json index 802b465a..9ffc2e0d 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,7 @@ }, "scripts": { "start": "node main.js", + "start-dev": "nodemon main.js", "lint": "eslint ." }, "dependencies": { @@ -23,7 +24,8 @@ "yargs": "^13.3.0" }, "devDependencies": { - "eslint": "^5.16.0" + "eslint": "^5.16.0", + "nodemon": "^2.0.2" }, "eslintConfig": { "parserOptions": { From 09ead120f577ec25071259444333099a3e16714f Mon Sep 17 00:00:00 2001 From: Simen de Lange Date: Sat, 25 Jan 2020 21:55:30 +0100 Subject: [PATCH 093/102] More sensible error when dc channel is not found --- src/telegram2discord/endwares.js | 9 ++++----- src/telegram2discord/helpers.js | 27 ++++++++++++++++++++++++++- 2 files changed, 30 insertions(+), 6 deletions(-) diff --git a/src/telegram2discord/endwares.js b/src/telegram2discord/endwares.js index 655deaf2..816d7770 100644 --- a/src/telegram2discord/endwares.js +++ b/src/telegram2discord/endwares.js @@ -80,7 +80,7 @@ const newChatMembers = createMessageHandler((ctx, bridge) => const text = `**${from.firstName} (${R.defaultTo("No username", from.username)})** joined the Telegram side of the chat`; // Pass it on - ctx.TediCross.dcBot.channels.get(bridge.discord.channelId) + helpers.getDiscordChannel(ctx, bridge) .send(text); })(ctx.tediCross.message.new_chat_members) ); @@ -102,7 +102,7 @@ const leftChatMember = createMessageHandler((ctx, bridge) => { const text = `**${from.firstName} (${R.defaultTo("No username", from.username)})** left the Telegram side of the chat`; // Pass it on - ctx.TediCross.dcBot.channels.get(bridge.discord.channelId) + helpers.getDiscordChannel(ctx, bridge) .send(text); }); @@ -118,7 +118,7 @@ const leftChatMember = createMessageHandler((ctx, bridge) => { function relayMessage(ctx) { R.forEach(async prepared => { // Get the channel to send to - const channel = ctx.TediCross.dcBot.channels.get(prepared.bridge.discord.channelId); + const channel = helpers.getDiscordChannel(ctx, prepared.bridge); // Make the header let header = prepared.header; @@ -159,8 +159,7 @@ const handleEdits = createMessageHandler(async (ctx, bridge) => { const [dcMessageId] = ctx.TediCross.messageMap.getCorresponding(MessageMap.TELEGRAM_TO_DISCORD, bridge, tgMessage.message_id); // Get the messages from Discord - const dcMessage = await ctx.TediCross.dcBot.channels - .get(bridge.discord.channelId) + const dcMessage = await helpers.getDiscordChannel(ctx, bridge) .fetchMessage(dcMessageId); R.forEach(async prepared => { diff --git a/src/telegram2discord/helpers.js b/src/telegram2discord/helpers.js index bc7cec6c..ad950aa6 100644 --- a/src/telegram2discord/helpers.js +++ b/src/telegram2discord/helpers.js @@ -39,11 +39,36 @@ const deleteMessage = R.curry((ctx, { chat, message_id }) => message_id )); +/** + * Gets a Discord channel by ID from a Discord bot + * + * @param {Context} ctx The Telegraf context to use + * @param {Bridge} bridge The bridge to get the channel for + * + * @returns {Discord.Channel} The channel + * + * @throws {Error} If the channel was not found + */ +const getDiscordChannel = R.curry((ctx, { name, discord: { channelId } }) => { + // Get the channel + const channel = ctx.TediCross.dcBot.channels.get(channelId); + + // Verify it exists + if (R.isNil(channel)) { + ctx.TediCross.logger.error(`[${name}] Could not get Discord channel with ID '${channelId}'. Please verify it is correct. Remember the quotes!`); + throw new Error(`Could not find channel with ID '${channelId}'`); + } + + // Return it + return channel; +}); + /*************** * Export them * ***************/ module.exports = { ignoreAlreadyDeletedError, - deleteMessage + deleteMessage, + getDiscordChannel }; From 33ca5e2a24afaaac80cf81d317488f58f12ee9b3 Mon Sep 17 00:00:00 2001 From: Simen de Lange Date: Sun, 26 Jan 2020 21:49:00 +0100 Subject: [PATCH 094/102] Improved start-dev command --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 9ffc2e0d..83455ac9 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ }, "scripts": { "start": "node main.js", - "start-dev": "nodemon main.js", + "start-dev": "nodemon main.js -i data/", "lint": "eslint ." }, "dependencies": { From c4e2a894326a1567df4fefe0e9191bae403ca726 Mon Sep 17 00:00:00 2001 From: Simen de Lange Date: Sun, 26 Jan 2020 22:44:30 +0100 Subject: [PATCH 095/102] Keeps spoilers hidden in replies on Discord See issue #127 --- src/telegram2discord/endwares.js | 7 +++---- src/telegram2discord/middlewares.js | 20 +++++++++++++++++--- 2 files changed, 20 insertions(+), 7 deletions(-) diff --git a/src/telegram2discord/endwares.js b/src/telegram2discord/endwares.js index 816d7770..188f42ec 100644 --- a/src/telegram2discord/endwares.js +++ b/src/telegram2discord/endwares.js @@ -46,7 +46,7 @@ const createMessageHandler = R.curry((func, ctx) => { * * @returns {undefined} */ -function chatinfo(ctx) { +const chatinfo = ctx => { // Reply with the info ctx.reply(`chatID: ${ctx.tediCross.message.chat.id}`) // Wait some time @@ -59,7 +59,7 @@ function chatinfo(ctx) { ctx.deleteMessage() ])) .catch(helpers.ignoreAlreadyDeletedError); -} +}; /** * Handles users joining chats @@ -115,7 +115,7 @@ const leftChatMember = createMessageHandler((ctx, bridge) => { * * @returns {undefined} */ -function relayMessage(ctx) { +const relayMessage = ctx => R.forEach(async prepared => { // Get the channel to send to const channel = helpers.getDiscordChannel(ctx, prepared.bridge); @@ -142,7 +142,6 @@ function relayMessage(ctx) { // Make the mapping so future edits can work XXX Only the last chunk is considered ctx.TediCross.messageMap.insert(MessageMap.TELEGRAM_TO_DISCORD, prepared.bridge, ctx.tediCross.messageId, dcMessage.id); })(ctx.tediCross.prepared); -} /** * Handles message edits diff --git a/src/telegram2discord/middlewares.js b/src/telegram2discord/middlewares.js index 2e387fdd..530c76f3 100644 --- a/src/telegram2discord/middlewares.js +++ b/src/telegram2discord/middlewares.js @@ -65,7 +65,9 @@ function createTextObjFromMessage(ctx, message) { * * @returns {String} The reply text to display */ -function makeReplyText(replyTo, replyLength, maxReplyLines) { +const makeReplyText = (replyTo, replyLength, maxReplyLines) => { + const countDoublePipes = str => str.match(/\|\|/g).length; + // Make the reply string return R.compose( // Add ellipsis if the text was cut @@ -77,6 +79,18 @@ function makeReplyText(replyTo, replyLength, maxReplyLines) { R.identity, R.concat(R.__, "…") ), + // Handle spoilers (pairs of "||" in Discord) + R.ifElse( + // If one of a pair of "||" has been removed + quote => R.and( + countDoublePipes(quote, "||") % 2 === 1, + countDoublePipes(replyTo.text.raw) % 2 === 0 + ), + // Add one to the end + R.concat(R.__, "||"), + // Otherwise do nothing + R.identity + ), // Take only a number of lines R.join("\n"), R.slice(0, maxReplyLines), @@ -84,7 +98,7 @@ function makeReplyText(replyTo, replyLength, maxReplyLines) { // Take only a portion of the text R.slice(0, replyLength), )(replyTo.text.raw); -} +}; /** * Makes a discord mention out of a username @@ -565,7 +579,7 @@ function addPreparedObj(ctx, next) { tc => !R.isNil(tc.replyTo) && ctx.TediCross.settings.discord.displayTelegramReplies === "embed", tc => { // Make the text - const replyText = handleEntities(tc.replyTo.text.raw, tc.replyTo.text.entities, ctx.TediCross.dcBot, bridge); + const replyText = makeReplyText(tc.replyTo, ctx.TediCross.settings.discord.replyLength, ctx.TediCross.settings.discord.maxReplyLines); return new Discord.RichEmbed({ // Discord will not accept embeds with more than 2048 characters From ef9e19f0204c5ea664844dfa7b6aa737568e6ff7 Mon Sep 17 00:00:00 2001 From: Simen de Lange Date: Fri, 31 Jan 2020 19:59:12 +0100 Subject: [PATCH 096/102] No longer spams info message when misconfigured --- src/discord2telegram/setup.js | 28 ++++++++++++------- src/telegram2discord/middlewares.js | 42 +++++++++++++++++++---------- src/telegram2discord/setup.js | 6 ++++- 3 files changed, 51 insertions(+), 25 deletions(-) diff --git a/src/discord2telegram/setup.js b/src/discord2telegram/setup.js index 4309b1cd..c98f8c98 100644 --- a/src/discord2telegram/setup.js +++ b/src/discord2telegram/setup.js @@ -92,6 +92,9 @@ function setup(logger, dcBot, tgBot, messageMap, bridgeMap, settings, datadirPat const latestDiscordMessageIds = new LatestDiscordMessageIds(logger, path.join(datadirPath, "latestDiscordMessageIds.json")); const useNickname = settings.discord.useNickname; + // Make a set to keep track of where the "This is an instance of TediCross..." message has been sent the last minute + const antiInfoSpamSet = new Set(); + // Set of server IDs. Will be filled when the bot is ready const knownServerIds = new Set(); @@ -229,16 +232,21 @@ function setup(logger, dcBot, tgBot, messageMap, bridgeMap, settings, datadirPat } }); } else if (R.isNil(message.channel.guild) || !knownServerIds.has(message.channel.guild.id)) { // Check if it is the correct server - // The message is from the wrong chat. Inform the sender that this is a private bot - message.reply( - "This is an instance of a TediCross bot, bridging a chat in Telegram with one in Discord. " - + "If you wish to use TediCross yourself, please download and create an instance. " - + "See https://github.com/TediCross/TediCross" - ) - // Delete it again after some time - .then(sleepOneMinute) - .then(message => message.delete()) - .catch(helpers.ignoreAlreadyDeletedError); + // The message is from the wrong chat. Inform the sender that this is a private bot, if they have not been informed the last minute + if (!antiInfoSpamSet.has(message.channel.id)) { + antiInfoSpamSet.add(message.channel.id); + + message.reply( + "This is an instance of a TediCross bot, bridging a chat in Telegram with one in Discord. " + + "If you wish to use TediCross yourself, please download and create an instance. " + + "See https://github.com/TediCross/TediCross" + ) + // Delete it again after some time + .then(sleepOneMinute) + .then(message => message.delete()) + .catch(helpers.ignoreAlreadyDeletedError) + .then(() => antiInfoSpamSet.delet(message.channel.id)); + } } }); diff --git a/src/telegram2discord/middlewares.js b/src/telegram2discord/middlewares.js index 530c76f3..63e27f38 100644 --- a/src/telegram2discord/middlewares.js +++ b/src/telegram2discord/middlewares.js @@ -274,20 +274,34 @@ function informThisIsPrivateBot(ctx, next) { R.isEmpty, R.path(["tediCross", "bridges"]) ), - // Inform the user - ctx => - 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.", - { - parse_mode: "markdown" - } - ) - // Delete it again after a while - .then(sleepOneMinute) - .then(helpers.deleteMessage(ctx)) - .catch(helpers.ignoreAlreadyDeletedError), + // 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 + ctx => R.not(ctx.TediCross.antiInfoSpamSet.has(ctx.message.chat.id)), + // Inform the chat this is an instance of TediCross + ctx => { + // Update the anti spam set + ctx.TediCross.antiInfoSpamSet.add(ctx.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.", + { + parse_mode: "markdown" + } + ) + .then(msg => + // Delete it again after a while + sleepOneMinute() + .then(() => helpers.deleteMessage(ctx, msg)) + .catch(helpers.ignoreAlreadyDeletedError) + // Remove it from the anti spam set again + .then(() => ctx.TediCross.antiInfoSpamSet.delete(ctx.message.chat.id)) + ); + } + ), // Otherwise go to next middleware next )(ctx); diff --git a/src/telegram2discord/setup.js b/src/telegram2discord/setup.js index ac72269e..2d164a48 100644 --- a/src/telegram2discord/setup.js +++ b/src/telegram2discord/setup.js @@ -60,6 +60,9 @@ function setup(logger, tgBot, dcBot, messageMap, bridgeMap, settings) { // Log the bot's info logger.info(`Telegram: ${me.username} (${me.id})`); + // Set keeping track of where the "This is an instance of TediCross..." has been sent the last minute + const antiInfoSpamSet = new Set(); + // Add some global context tgBot.context.TediCross = { me, @@ -67,7 +70,8 @@ function setup(logger, tgBot, dcBot, messageMap, bridgeMap, settings) { dcBot, settings, messageMap, - logger + logger, + antiInfoSpamSet }; // Apply middlewares and endwares From 564f0cc167c01c33b0107a82f50416bf10a57a02 Mon Sep 17 00:00:00 2001 From: Simen de Lange Date: Sun, 9 Feb 2020 19:21:20 +0100 Subject: [PATCH 097/102] Added option "blockquote" as reply display on Discord --- src/settings/DiscordSettings.js | 6 +++--- src/settings/Settings.js | 13 ------------- src/telegram2discord/middlewares.js | 30 ++++++++++++++++++++++++++--- 3 files changed, 30 insertions(+), 19 deletions(-) diff --git a/src/settings/DiscordSettings.js b/src/settings/DiscordSettings.js index 4cf9b814..a00aaf3c 100644 --- a/src/settings/DiscordSettings.js +++ b/src/settings/DiscordSettings.js @@ -51,7 +51,7 @@ class DiscordSettings { this.useNickname = settings.useNickname; /** - * How to display replies from Telegram. Either `embed` or `inline` + * How to display replies from Telegram. Either `blockquote`, `embed` or `inline` * * @type {String} */ @@ -132,8 +132,8 @@ class DiscordSettings { } // Check that `displayTelegramReplies` is an acceptable string - if (!["embed", "inline"].includes(settings.displayTelegramReplies)) { - throw new Error("`settings.displayTelegramReplies` must be either \"embed\" or \"inline\""); + if (!["blockquote", "embed", "inline"].includes(settings.displayTelegramReplies)) { + throw new Error("`settings.displayTelegramReplies` must be either \"blockquote\", \"embed\" or \"inline\""); } // Check that `replyLength` is an integer diff --git a/src/settings/Settings.js b/src/settings/Settings.js index 52021060..934796ea 100644 --- a/src/settings/Settings.js +++ b/src/settings/Settings.js @@ -137,19 +137,6 @@ class Settings { // Make a clone, to not operate directly on the provided object const settings = R.clone(rawSettings); - // 2018-11-25: Add the `sendUsername` option to the bridges - for (const bridge of settings.bridges) { - // Do the Telegram part - if (bridge.telegram.sendUsernames === undefined) { - bridge.telegram.sendUsernames = true; - } - - // Do the Discord part - if (bridge.discord.sendUsernames === undefined) { - bridge.discord.sendUsernames = true; - } - } - // 2019-02-16: Add the `crossDeleteOnTelegram` option to Discord for (const bridge of settings.bridges) { if (bridge.discord.crossDeleteOnTelegram === undefined) { diff --git a/src/telegram2discord/middlewares.js b/src/telegram2discord/middlewares.js index 63e27f38..709d8aeb 100644 --- a/src/telegram2discord/middlewares.js +++ b/src/telegram2discord/middlewares.js @@ -66,7 +66,10 @@ function createTextObjFromMessage(ctx, message) { * @returns {String} The reply text to display */ const makeReplyText = (replyTo, replyLength, maxReplyLines) => { - const countDoublePipes = str => str.match(/\|\|/g).length; + const countDoublePipes = R.tryCatch( + str => str.match(/\|\|/g).length, + R.always(0) + ); // Make the reply string return R.compose( @@ -588,9 +591,12 @@ function addPreparedObj(ctx, next) { return header; })(); + // Helper method to shorten code for testing reply display type + const isReplyType = R.curry((type, tc) => !R.isNil(tc.replyTo) && ctx.TediCross.settings.discord.displayTelegramReplies === type); + // Handle embed replies const embed = R.ifElse( - tc => !R.isNil(tc.replyTo) && ctx.TediCross.settings.discord.displayTelegramReplies === "embed", + isReplyType("embed"), tc => { // Make the text const replyText = makeReplyText(tc.replyTo, ctx.TediCross.settings.discord.replyLength, ctx.TediCross.settings.discord.maxReplyLines); @@ -603,6 +609,16 @@ function addPreparedObj(ctx, next) { R.always(undefined) )(tc); + // Handle blockquote replies + const replyQuote = R.ifElse( + isReplyType("blockquote"), + R.compose( + R.replace(/^/gm, "> "), + tc => makeReplyText(tc.replyTo, ctx.TediCross.settings.discord.replyLength, ctx.TediCross.settings.discord.maxReplyLines), + ), + R.always(undefined) + )(tc); + // Handle file const file = R.ifElse( R.compose( @@ -614,7 +630,15 @@ function addPreparedObj(ctx, next) { )(tc); // Make the text to send - const text = handleEntities(tc.text.raw, tc.text.entities, ctx.TediCross.dcBot, bridge); + const text = (() => { + let text = handleEntities(tc.text.raw, tc.text.entities, ctx.TediCross.dcBot, bridge); + + if (isReplyType("blockquote", tc)) { + text = replyQuote + "\n" + text; + } + + return text; + })(); return { bridge, From 10d1309188ad936ff53d3084cde48204567a3331 Mon Sep 17 00:00:00 2001 From: Simen de Lange Date: Sun, 8 Mar 2020 19:52:41 +0100 Subject: [PATCH 098/102] Removed reply style option on Discord. It will now be blockquotes --- README.md | 2 +- src/discord2telegram/setup.js | 2 +- src/settings/DiscordSettings.js | 12 ------- src/settings/Settings.js | 4 +++ src/telegram2discord/endwares.js | 11 +------ src/telegram2discord/middlewares.js | 49 +++-------------------------- 6 files changed, 12 insertions(+), 68 deletions(-) diff --git a/README.md b/README.md index fb159537..bcd41749 100644 --- a/README.md +++ b/README.md @@ -60,7 +60,7 @@ As mentioned in the step by step installation guide, there is a settings file. H * `token`: The Discord bot's token. It is needed for the bot to authenticate to the Discord servers and be able to send and receive messages. If set to `"env"`, TediCross will read the token from the environment variable `DISCORD_BOT_TOKEN` * `skipOldMessages`: Whether or not to skip through all previous messages sent since the bot was last turned off and start processing new messages ONLY. Defaults to true. Note that there is no guarantee the old messages will arrive at Telegram in order. **NOTE:** [Telegram has a limit](https://core.telegram.org/bots/faq#my-bot-is-hitting-limits-how-do-i-avoid-this) on how quickly a bot can send messages. If there is a big backlog, this will cause problems * `useNickname`: Uses the sending user's nickname instead of username when relaying messages to Telegram - * `displayTelegramReplies`: How to display Telegram replies. Either the string `inline` or `embed` + * `displayTelegramReplies`: How to display Telegram replies. Either the string `blockqoute`, `inline` or `embed` * `replyLength`: How many characters of the original message to display on replies * `maxReplyLines`: How many lines of the original message to display on replies * `debug`: If set to `true`, activates debugging output from the bot. Defaults to `false` diff --git a/src/discord2telegram/setup.js b/src/discord2telegram/setup.js index c98f8c98..35cbea9d 100644 --- a/src/discord2telegram/setup.js +++ b/src/discord2telegram/setup.js @@ -245,7 +245,7 @@ function setup(logger, dcBot, tgBot, messageMap, bridgeMap, settings, datadirPat .then(sleepOneMinute) .then(message => message.delete()) .catch(helpers.ignoreAlreadyDeletedError) - .then(() => antiInfoSpamSet.delet(message.channel.id)); + .then(() => antiInfoSpamSet.delete(message.channel.id)); } } }); diff --git a/src/settings/DiscordSettings.js b/src/settings/DiscordSettings.js index a00aaf3c..b515a526 100644 --- a/src/settings/DiscordSettings.js +++ b/src/settings/DiscordSettings.js @@ -50,13 +50,6 @@ class DiscordSettings { */ this.useNickname = settings.useNickname; - /** - * How to display replies from Telegram. Either `blockquote`, `embed` or `inline` - * - * @type {String} - */ - this.displayTelegramReplies = settings.displayTelegramReplies; - /** * How much of the original message to show in replies from Telegram * @@ -131,11 +124,6 @@ class DiscordSettings { throw new Error("`settings.useNickname` must be a boolean"); } - // Check that `displayTelegramReplies` is an acceptable string - if (!["blockquote", "embed", "inline"].includes(settings.displayTelegramReplies)) { - throw new Error("`settings.displayTelegramReplies` must be either \"blockquote\", \"embed\" or \"inline\""); - } - // Check that `replyLength` is an integer if (!Number.isInteger(settings.replyLength) || settings.replyLength <= 0) { throw new ("`settings.replyLength` must be an integer greater than 0"); diff --git a/src/settings/Settings.js b/src/settings/Settings.js index 934796ea..c0f2f025 100644 --- a/src/settings/Settings.js +++ b/src/settings/Settings.js @@ -179,6 +179,10 @@ class Settings { delete bridge.discord.serverId; } + // 2020-02-09: Removed the `displayTelegramReplies` option from Discord + if (!R.isNil(settings.discord.displayTelegramReplies)) { + delete settings.discord.displayTelegramReplies; + } // All done! return settings; diff --git a/src/telegram2discord/endwares.js b/src/telegram2discord/endwares.js index 188f42ec..461efdcd 100644 --- a/src/telegram2discord/endwares.js +++ b/src/telegram2discord/endwares.js @@ -120,17 +120,8 @@ const relayMessage = ctx => // Get the channel to send to const channel = helpers.getDiscordChannel(ctx, prepared.bridge); - // Make the header - let header = prepared.header; - - // Handle embed replies - if (prepared.embed) { - await channel.send(header, { embed: prepared.embed }); - header = ""; - } - // Discord doesn't handle messages longer than 2000 characters. Split it up into chunks that big - const messageText = header + "\n" + prepared.text; + const messageText = prepared.header + "\n" + prepared.text; const chunks = R.splitEvery(2000, messageText); // Send them in serial, with the attachment first, if there is one diff --git a/src/telegram2discord/middlewares.js b/src/telegram2discord/middlewares.js index 709d8aeb..334c9536 100644 --- a/src/telegram2discord/middlewares.js +++ b/src/telegram2discord/middlewares.js @@ -542,15 +542,7 @@ function addPreparedObj(ctx, next) { )(tc.replyTo) ; - // The original text, if this is a reply - const repliedToText = R.isNil(tc.replyTo) - ? null - : (ctx.TediCross.settings.discord.displayTelegramReplies === "inline" - ? makeReplyText(tc.replyTo, ctx.TediCross.settings.discord.replyLength, ctx.TediCross.settings.discord.maxReplyLines) - : null - ) - ; - + // Build the header let header = ""; if (bridge.telegram.sendUsernames) { if (!R.isNil(tc.forwardFrom)) { @@ -558,13 +550,7 @@ function addPreparedObj(ctx, next) { header = `**${originalSender}** (forwarded by **${senderName}**)`; } else if (!R.isNil(tc.replyTo)) { // Reply - header = `**${senderName}** (in reply to **${repliedToName}**`; - - if (!R.isNil(repliedToText)) { - header = `${header}: _${R.replace(/\n/g, " ", repliedToText)}_)`; - } else { - header = `${header})`; - } + header = `**${senderName}** (in reply to **${repliedToName}**)`; } else { // Ordinary message header = `**${senderName}**`; @@ -575,13 +561,7 @@ function addPreparedObj(ctx, next) { header = `(forward from **${originalSender}**)`; } else if (!R.isNil(tc.replyTo)) { // Reply - header = `(in reply to **${repliedToName}**`; - - if (!R.isNil(repliedToText)) { - header = `${header}: _${R.replace(/\n/g, " ", repliedToText)}_)`; - } else { - header = `${header})`; - } + header = `(in reply to **${repliedToName}**)`; } else { // Ordinary message header = ""; @@ -591,27 +571,9 @@ function addPreparedObj(ctx, next) { return header; })(); - // Helper method to shorten code for testing reply display type - const isReplyType = R.curry((type, tc) => !R.isNil(tc.replyTo) && ctx.TediCross.settings.discord.displayTelegramReplies === type); - - // Handle embed replies - const embed = R.ifElse( - isReplyType("embed"), - tc => { - // Make the text - const replyText = makeReplyText(tc.replyTo, ctx.TediCross.settings.discord.replyLength, ctx.TediCross.settings.discord.maxReplyLines); - - return new Discord.RichEmbed({ - // Discord will not accept embeds with more than 2048 characters - description: R.slice(0, 2048, replyText) - }); - }, - R.always(undefined) - )(tc); - // Handle blockquote replies const replyQuote = R.ifElse( - isReplyType("blockquote"), + tc => !R.isNil(tc.replyTo), R.compose( R.replace(/^/gm, "> "), tc => makeReplyText(tc.replyTo, ctx.TediCross.settings.discord.replyLength, ctx.TediCross.settings.discord.maxReplyLines), @@ -633,7 +595,7 @@ function addPreparedObj(ctx, next) { const text = (() => { let text = handleEntities(tc.text.raw, tc.text.entities, ctx.TediCross.dcBot, bridge); - if (isReplyType("blockquote", tc)) { + if (!R.isNil(replyQuote)) { text = replyQuote + "\n" + text; } @@ -643,7 +605,6 @@ function addPreparedObj(ctx, next) { return { bridge, header, - embed, file, text }; From 4c78e4398617ac700b439faaeee8ee1a744c569b Mon Sep 17 00:00:00 2001 From: Simen de Lange Date: Sat, 14 Mar 2020 20:28:12 +0100 Subject: [PATCH 099/102] Changed user in docker image and documented it --- Dockerfile | 6 +++--- README-Docker.md | 4 ++++ 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/Dockerfile b/Dockerfile index bc082f20..92609732 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM node:10-alpine +FROM node:12-alpine WORKDIR /opt/TediCross/ @@ -6,8 +6,8 @@ COPY . . RUN npm install --production -RUN adduser -S tedicross -USER tedicross +# The node user (from node:12-alpine) has UID 1000, meaning most people with single-user systems will not have to change UID +USER node VOLUME /opt/TediCross/data/ diff --git a/README-Docker.md b/README-Docker.md index d039d1ee..93079a75 100644 --- a/README-Docker.md +++ b/README-Docker.md @@ -25,3 +25,7 @@ docker run \ Of course, you can add `-d` or `--rm` or a name or whatever else you want to that command If you have the tokens in the settings file instead of reading them from the environment, you can of course drop the `-e` lines + +### Permissions + +The dockerfile says the container should start as the user with UID 1000. This should be fine for most single user Linux systems, but may cause problems for multi user systems. If you get a permission error when starting the container, try changing the user the container is using. Find your user's UID with the command `id -u $USER`, then add the argument `-u ` to the `docker run` command. For example `docker run -u 1001 -v ...` From 9507fecc6e6821b1bd6519aa4b960aaae6e1b560 Mon Sep 17 00:00:00 2001 From: Simen de Lange Date: Sat, 14 Mar 2020 20:29:26 +0100 Subject: [PATCH 100/102] 0.9.11 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 83455ac9..4c955531 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "tedicross", - "version": "0.9.10", + "version": "0.9.11", "description": "Better DiteCross", "license": "MIT", "repository": { From da2ad78ba5c9f70e296b83bd8904f77818f52ad7 Mon Sep 17 00:00:00 2001 From: Myke500 Date: Sat, 28 Mar 2020 21:59:05 -0700 Subject: [PATCH 101/102] Update package.json --- package.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 4c955531..fcc3e522 100644 --- a/package.json +++ b/package.json @@ -1,12 +1,12 @@ { - "name": "tedicross", - "version": "0.9.11", - "description": "Better DiteCross", + "name": "GoatBot", + "version": "0.2.500", + "description": "Almost Better TediCross", "license": "MIT", "repository": { "type": "git", - "url": "git+https://github.com/TediCross/TediCross.git" - }, + "url": "git+https://github.com/Myke500/GoatBot.git" + }, "scripts": { "start": "node main.js", "start-dev": "nodemon main.js -i data/", From 2e25ec9cb0e3ecb77481b68ae8939338d3d1a0a4 Mon Sep 17 00:00:00 2001 From: Myke500 Date: Sat, 28 Mar 2020 22:00:58 -0700 Subject: [PATCH 102/102] Version Bump --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index fcc3e522..1e280816 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "GoatBot", - "version": "0.2.500", + "version": "0.9.500", "description": "Almost Better TediCross", "license": "MIT", "repository": {