Skip to content

Commit

Permalink
Merge pull request #58 from RB-Lab/rb/fix-send-message-reply
Browse files Browse the repository at this point in the history
reply with message on /sendMessage (fix #19)
  • Loading branch information
jehy committed Nov 28, 2021
2 parents 95902f4 + a9a39a1 commit e68c987
Show file tree
Hide file tree
Showing 4 changed files with 134 additions and 27 deletions.
2 changes: 1 addition & 1 deletion bin/start.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
'use strict';

const TelegramServer = require('../lib/index');
const {default: TelegramServer} = require('../lib/index');
// eslint-disable-next-line import/no-unresolved
const config = require('../config/config.json');

Expand Down
4 changes: 2 additions & 2 deletions src/routes/bot/sendMessage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ import type { Route } from '../route';
export const sendMessage: Route = (app, telegramServer) => {
handle(app, '/bot:token/sendMessage', (req, res, _next) => {
const botToken = req.params.token;
telegramServer.addBotMessage(req.body, botToken);
const data = { ok: true, result: null };
const result = telegramServer.addBotMessage(req.body, botToken);
const data = { ok: true, result };
res.sendResult(data);
});
};
94 changes: 74 additions & 20 deletions src/telegramServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@ import assert from 'assert';
import request from 'axios';
import debugTest from 'debug';
import EventEmitter from 'events';
import type { Express } from 'express';
import type { ErrorRequestHandler, Express } from 'express';
import express from 'express';
import http from 'http';
import shutdown from 'http-shutdown';
import type { MessageEntity, Params } from 'typegram';
import type {
InlineKeyboardMarkup, Message, MessageEntity, Params,
} from 'typegram';
import { requestLogger } from './modules/requestLogger';
import { sendResult } from './modules/sendResult';
import type {
Expand Down Expand Up @@ -36,14 +38,14 @@ interface StoredUpdate {
isRead: boolean;
}

type BotIncommingMessage = Params<'sendMessage', never>[0] & {
// TODO parse reply_markup to its actual type, see https://git.io/J1kiM
reply_markup?: string;
};
type BotIncommingMessage = Params<'sendMessage', never>[0];

type BotEditTextIncommingMessage = Params<'editMessageText', never>[0];

type BotEditTextIncommingMessage = Params<'editMessageText', never>[0] & {
reply_markup?: string;
};
type RawIncommingMessage = {
reply_markup?: string | object;
entities?:string | object
}

export interface StoredBotUpdate extends StoredUpdate {
message: BotIncommingMessage;
Expand Down Expand Up @@ -135,13 +137,15 @@ export class TelegramServer extends EventEmitter {
this.webServer.use((_req, res) => {
res.sendError(new Error('Route not found'));
});
// Catch express bodyParser error, like http://stackoverflow.com/questions/15819337/catch-express-bodyparser-error
// TODO check if signature with `error` first actually still works
// eslint-disable-next-line @typescript-eslint/no-explicit-any
this.webServer.use((error, _req, res: any) => {
/**
* Catch uncought errors e.g. express bodyParser error
* @see https://expressjs.com/en/guide/error-handling.html#the-default-error-handler
*/
const globalErrorHandler: ErrorRequestHandler = (error, _req, res, _next) => {
debugServer(`Error: ${error}`);
res.sendError(new Error(`Something went wrong. ${error}`));
});
};
this.webServer.use(globalErrorHandler);
}

static normalizeConfig(config: Partial<TelegramServerConfig>) {
Expand All @@ -161,9 +165,10 @@ export class TelegramServer extends EventEmitter {
return new TelegramClient(this.config.apiURL, botToken, options);
}

addBotMessage(message: BotIncommingMessage, botToken: string) {
addBotMessage(rawMessage: BotIncommingMessage, botToken: string) {
const d = new Date();
const millis = d.getTime();
const message = TelegramServer.normalizeMessage(rawMessage);
const add = {
time: millis,
botToken,
Expand All @@ -173,12 +178,33 @@ export class TelegramServer extends EventEmitter {
isRead: false,
};
this.storage.botMessages.push(add);

// only InlineKeyboardMarkup is allowed in response
let inlineMarkup: InlineKeyboardMarkup | undefined;
if (message.reply_markup && 'inline_keyboard' in message.reply_markup) {
inlineMarkup = message.reply_markup;
}
const msg: Message.TextMessage = {
...message,
reply_markup: inlineMarkup,
message_id: this.messageId,
date: add.time,
text: message.text,
chat: {
id: Number(message.chat_id),
first_name: 'Bot',
type: 'private',
},
};

this.messageId++;
this.updateId++;
this.emit('AddedBotMessage');
return msg;
}

editMessageText(message: BotEditTextIncommingMessage) {
editMessageText(rawMessage: BotEditTextIncommingMessage) {
const message = TelegramServer.normalizeMessage(rawMessage);
const update = this.storage.botMessages.find(
(u) =>(
String(u.messageId) === String(message.message_id)
Expand All @@ -191,6 +217,12 @@ export class TelegramServer extends EventEmitter {
}
}

/**
* @FIXME
* (node:103570) MaxListenersExceededWarning: Possible EventEmitter memory leak detected. 11
* EditedMessageText listeners added to [TelegramServer]. Use emitter.setMaxListeners() to
* increase limit (Use `node --trace-warnings ...` to show where the warning was created)
*/
async waitBotEdits() {
return new Promise<void>((resolve) => {
this.on('EditedMessageText', () => resolve());
Expand Down Expand Up @@ -264,7 +296,7 @@ export class TelegramServer extends EventEmitter {
const resp = await request({
url: webhook.url,
method: 'POST',
data: this.formatUpdate(update),
data: TelegramServer.formatUpdate(update),
});
if (resp.status > 204) {
debugServer(
Expand Down Expand Up @@ -343,7 +375,7 @@ export class TelegramServer extends EventEmitter {
return messages.map((update) => {
// eslint-disable-next-line no-param-reassign
update.isRead = true;
return this.formatUpdate(update);
return TelegramServer.formatUpdate(update);
});
}

Expand Down Expand Up @@ -445,8 +477,7 @@ export class TelegramServer extends EventEmitter {
return true;
}

// eslint-disable-next-line class-methods-use-this
private formatUpdate(update: StoredClientUpdate) {
private static formatUpdate(update: StoredClientUpdate) {
if ('callbackQuery' in update) {
return {
update_id: update.updateId,
Expand All @@ -470,4 +501,27 @@ export class TelegramServer extends EventEmitter {
},
};
}

/**
* Telegram API docs say that `reply_markup` and `entities` must JSON serialized string
* however e.g. Telegraf sends it as an object and the real Telegram API works just fine
* with that, so aparently those fields are _sometimes_ a JSON serialized strings.
* For testing purposes it is easier to have everything uniformely parsed, thuse we parse them.
* @see https://git.io/J1kiM for more info on that topic.
* @param message incomming message that can have JSON-serialized strings
* @returns the same message but with reply_markdown & entities parsed
*/
private static normalizeMessage <T extends RawIncommingMessage>(message: T) {
if ('reply_markup' in message) {
// eslint-disable-next-line no-param-reassign
message.reply_markup = typeof message.reply_markup === 'string'
? JSON.parse(message.reply_markup) : message.reply_markup;
}
if ('entities' in message) {
// eslint-disable-next-line no-param-reassign
message.entities = typeof message.entities === 'string'
? JSON.parse(message.entities) : message.entities;
}
return message;
}
}
61 changes: 57 additions & 4 deletions src/test/generic.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import debug from 'debug';
import { assert } from 'chai';
import TelegramBot from 'node-telegram-bot-api';
import type { InlineKeyboardMarkup, KeyboardButton, ReplyKeyboardMarkup } from 'typegram';
import { getServerAndClient, assertEventuallyTrue, delay } from './utils';
import {
TelegramBotEx,
Expand All @@ -11,9 +12,23 @@ import {
CallbackQBot,
Logger,
} from './testBots';
import type { StoredBotUpdate } from '../telegramServer';

const debugTest = debug('TelegramServer:test');

function isReplyKeyboard(markup: object | undefined): markup is ReplyKeyboardMarkup {
return markup !== undefined && 'keyboard' in markup;
}
function isInlineKeyboard(markup: object | undefined): markup is InlineKeyboardMarkup {
return markup !== undefined && 'inline_keyboard' in markup;
}
function isCommonButton(btn: unknown): btn is KeyboardButton.CommonButton {
return typeof btn === 'object' && btn !== null && 'text' in btn;
}
function isBotUpdate(upd: object | undefined): upd is StoredBotUpdate {
return upd !== undefined && 'message' in upd;
}

describe('Telegram Server', () => {
const token = 'sampleToken';

Expand Down Expand Up @@ -91,6 +106,38 @@ describe('Telegram Server', () => {
await server.stop();
});

it('should message in response to /sendMessage', (done) => {
getServerAndClient(token).then(({ server, client }) => {
const botOptions = { polling: true, baseApiUrl: server.config.apiURL };
const bot = new TelegramBot(token, botOptions);
bot.onText(/\/start/, async (msg) => {
const chatId = msg.chat.id;
if (!chatId) return;
const reply = await bot.sendMessage(chatId, 'ololo #azaza', {
reply_to_message_id: msg.message_id,
reply_markup: {
inline_keyboard: [[{text: 'foo', callback_data: 'bar'}]],
},
});
const update = server.getUpdatesHistory(token).find((upd) => reply.message_id === upd.messageId);
if (!isBotUpdate(update)) {
assert.fail('Cannot find bot update with messageId porvided in response');
}
assert.equal(update.message.text, reply.text);
if (!isInlineKeyboard(update.message.reply_markup)) {
assert.fail('Wrong keyboard type in stored update');
}
assert.deepEqual(reply.reply_markup, update.message.reply_markup!);

await server.stop();
await bot.stopPolling();
done();
});

return client.sendMessage(client.makeMessage('/start'));
}).catch((err) => assert.fail(err));
});

it('should fully implement user-bot interaction', async () => {
const { server, client } = await getServerAndClient(token);
let message = client.makeMessage('/start');
Expand All @@ -107,8 +154,11 @@ describe('Telegram Server', () => {
updates.result.length,
'Updates queue should contain one message!',
);
const { keyboard } = JSON.parse(updates.result[0].message.reply_markup!);
message = client.makeMessage(keyboard[0][0].text);
const markup = updates.result[0].message.reply_markup!;
if (!isReplyKeyboard(markup) || !isCommonButton(markup.keyboard[0][0])) {
throw new Error('No keyboard in update');
}
message = client.makeMessage(markup.keyboard[0][0].text);
await client.sendMessage(message);
const updates2 = await client.getUpdates();
Logger.serverUpdate(updates2.result);
Expand Down Expand Up @@ -161,8 +211,11 @@ describe('Telegram Server', () => {
updates.result.length,
'Updates queue should contain one message!',
);
const { keyboard } = JSON.parse(updates.result[0].message.reply_markup!);
message = client.makeMessage(keyboard[0][0].text);
const markup = updates.result[0].message.reply_markup!;
if (!isReplyKeyboard(markup) || !isCommonButton(markup.keyboard[0][0])) {
throw new Error('No keyboard in update');
}
message = client.makeMessage(markup.keyboard[0][0].text);
await client.sendMessage(message);
const updates2 = await client.getUpdates();
Logger.serverUpdate(updates2.result);
Expand Down

0 comments on commit e68c987

Please sign in to comment.