telegram/presence-subscriber.js

const { Bot } = require('grammy');
const logger = require('../logger').child({ module: 'telegram-presence-subscriber' });
const { getHealth } = require('../services/health');
const { getRedis } = require('../services/redis');

const bot_config = {};
if (process.env?.ENV === 'dev') {
    bot_config.client = {
        buildUrl: ({}, token, method) => `https://api.telegram.org/bot${token}/test/${method}`
    }
}
/**
 * @property {Bot?}
 */
const bot = process.env.TELEGRAM_TOKEN ? new Bot(process.env.TELEGRAM_TOKEN, bot_config) : null;

const presence_notification_map = {};

const getDescriptionHeading = (chat_desription) => {
    if (!chat_desription?.length) return '';

    // checking that there is a "--" line delimeter in description
    if (!chat_desription.match(/\n--\n?/gm)?.length) return '';

    return `${chat_desription.match(/.*\n--/gm)[0]}`;
}

class PresenceNotification {
    constructor(chat_id, presence_data) {
        this.chat_id = chat_id;
        this.guild_id = presence_data.guild_id;
        this.guild_name = presence_data.guild_name;
        this.presence_collection = new Map();
        this.current_update_promise = null;
        this.is_deleted = false;
    }

    isEmpty() {
        for (const [{}, presence_data] of this.presence_collection.entries()) {
            if (presence_data.activity) {
                return false;
            }
        }
        return true;
    }

    setPresence(telegram_user_id, presence_data) {
        this.presence_collection.set(telegram_user_id, presence_data);
    }

    getLogMeta() {
        let meta = {};

        meta['discord_guild'] = this.guild_name;
        meta['discord_guild_id'] = this.guild_id;
        meta['telegram_chat_id'] = this.chat_id;
        meta['telegram_user_ids'] = Object.keys(this.presence_collection).join(',');

        return meta;
    }

    getNotificationText() {
        let text = '\nАктивность';
        for (const [{}, { member_name, activity, call_me_by = null }] of this.presence_collection.entries()) {
            if (activity) text += `\n${call_me_by || member_name} — ${activity}`;
        }
        return text;
    }

    isDeleted() {
        return this.is_deleted;
    }
}

/**
 * 
 * @param {*} presence_data 
 * @param {*} chat_id 
 * @returns {PresenceNotification}
 */
function getPresenceNotification(presence_data, chat_id) {
    if (presence_data instanceof PresenceNotification) {
        return presence_data;
    }

    if (!presence_notification_map[chat_id]) {
        presence_notification_map[chat_id] = new PresenceNotification(chat_id, presence_data);
    }

    return presence_notification_map[chat_id];
}

async function editDescription(presence_notification, new_description) {
    if (!presence_notification || !bot) return;

    presence_notification.current_update_promise = bot.api.setChatDescription(
        presence_notification.chat_id,
        new_description
    ).then(() => {
        logger.debug(
            `Updated description in [chat: ${presence_notification.chat_id}] with new description: ${new_description}`,
            { ...presence_notification.getLogMeta() }
        )
    }).catch(err => {
        if (err.description.includes('chat description not modified')) {
            logger.noise(`Same presence already exists in [chat: ${presence_notification.chat_id}], skipping`, { ...presence_notification.getLogMeta() })
            return;
        }
        logger.error(
            `Error while updating description in [chat: ${presence_notification.chat_id}]`,
            { error: err.stack || err, ...presence_notification.getLogMeta() }
        )
    });
    return presence_notification.current_update_promise;
}

async function updatePresence(telegram_chat_id, telegram_user_id, presence_data) {
    if (!bot) {
        return;
    }

    const presence_notification = getPresenceNotification(presence_data, telegram_chat_id);

    const old_presence_text = presence_notification.getNotificationText();

    try {
        const chat_member = await bot.api.getChatMember(telegram_chat_id, telegram_user_id);

        presence_notification.setPresence(telegram_user_id, { 
            call_me_by: `${chat_member.user.username ? `@${chat_member.user.username}` : chat_member.user.first_name}`, 
            ...presence_data 
        });
    }
    catch (err) {
        logger.warn(
            `Failed to fetch [user: ${telegram_user_id}] in [chat: ${telegram_chat_id}], failing back to Discord member's display name`,
            { error: err.stack || err, ...presence_notification.getLogMeta() }
        );
        presence_notification.setPresence(telegram_user_id, presence_data);
    }

    if (old_presence_text === presence_notification.getNotificationText()) {
        logger.debug(`Nothing new for [chat: ${telegram_chat_id}], skipping update`, { ...presence_notification.getLogMeta() });
        return;
    }

    let chat_data;
    try {
        chat_data = await bot.api.getChat(telegram_chat_id);
    }
    catch (err) {
        logger.error(
            `Error while fetching [chat: ${telegram_chat_id}], skipping update`,
            { error: err.stack || err, ...presence_notification.getLogMeta() }
        );
        return;
    }

    const { description } = chat_data;
    let new_description = getDescriptionHeading(description);

    if (presence_notification.isEmpty()) {
        return editDescription(presence_notification, new_description);
    }

    new_description += presence_notification.getNotificationText();

    if (presence_notification.current_update_promise !== null) {
        return presence_notification.current_update_promise.then(() => {
            logger.debug(
                `Scheduling presence notification update to [chat: ${presence_notification.chat_id}]`,
                { ...presence_notification.getLogMeta() }
            );
            editDescription(presence_notification, new_description);
        });
    }
    return editDescription(presence_notification, new_description);
}

async function deletePresence(telegram_chat_id) {
    if (!telegram_chat_id || !bot) return;

    if (!presence_notification_map[telegram_chat_id]) return;

    if (presence_notification_map[telegram_chat_id]?.isDeleted()) return;

    let chat_data;
    try {
        chat_data = await bot.api.getChat(telegram_chat_id);
    }
    catch (err) {
        logger.error(
            `Error while fetching [chat: ${telegram_chat_id}], skipping update`,
            { error: err.stack || err, telegram_chat_id }
        );
        return;
    }

    const { description } = chat_data;
    let new_description = getDescriptionHeading(description);

    return editDescription(presence_notification_map[telegram_chat_id], new_description).then(() => {
        presence_notification_map[telegram_chat_id].is_deleted = true;
        logger.silly(
            `Marking presence as deleted for [chat: ${telegram_chat_id}]`,
            { ...presence_notification_map[telegram_chat_id].getLogMeta() }
        );
    });
}

async function testPermissions(telegram_chat_id) {
    let isPermitted = false;

    try {
        await bot.init();
    }
    catch (err) {
        logger.error(
            `Could not init bot (fetch bot info)`,
            { error: err.stack || err }
        );
        throw err;
    }

    let chat_member;
    try {
        chat_member = await bot.api.getChatMember(telegram_chat_id, bot.botInfo.id);
    }
    catch (err) {
        logger.error(
            `Error while fetching bot's member object for [chat: ${telegram_chat_id}]`,
            { error: err.stack || err }
        );
        return false;
    }

    if (chat_member?.can_change_info) {
        isPermitted = true;
    }

    return isPermitted;
}

module.exports = {
    updatePresence,
    deletePresence,
    testPermissions
};