discord/event-subscriber.js

const { BaseSubscriber } = require('./utils');
const { sendNotification, deleteNotification, ReverseMap } = require('../telegram/event-subscriber');

/** @type {{[guild_id: string]: EventSubscriber}} */
const subscribers = {};

class EventSubscriber extends BaseSubscriber {
    constructor() {
        super('event_subscriber');
    }

    start(guild, telegram_chat_id) {
        if (!guild || !telegram_chat_id) return;
        if (this.active
            && this.telegram_chat_ids
            && this.telegram_chat_ids.includes(telegram_chat_id)) return;
        this.active = true;
        this.telegram_chat_ids.push(telegram_chat_id);
        this._guild = guild;
        this.event_ids = new Set();
        ReverseMap.add(this._guild.id, telegram_chat_id);
        this.dump();
    }

    stop(telegram_chat_id) {
        if (telegram_chat_id && this.telegram_chat_ids.length) {
            delete this.telegram_chat_ids[this.telegram_chat_ids.indexOf(telegram_chat_id)];
            ReverseMap.remove(this._guild.id, telegram_chat_id);
            this.logger.info(`Deleting event notifications for ${this._guild.name} in [chat: ${telegram_chat_id}]`);
            this.event_ids.forEach((event_id) => {
                deleteNotification(telegram_chat_id, event_id);
            });
        }
        else {
            this.logger.info(`Deleting event notifications for ${this._guild.name} in [chats: ${JSON.stringify(this.telegram_chat_ids)}]`);
            this.telegram_chat_ids.forEach((telegram_chat_id) => {
                ReverseMap.remove(this._guild.id, telegram_chat_id);
                this.event_ids.forEach((event_id) => {
                    deleteNotification(telegram_chat_id, event_id);
                });
            });
            this.telegram_chat_ids = [];
        }
        
        if (!this.telegram_chat_ids.length) {
            this.active = false;
        }

        this.dump();
    }

    async dump() {
        if (!this.redis) {
            return;
        }
        return this.redis.hmset(this._dump_key, {
            active: this.active,
            telegram_chat_ids: JSON.stringify(this.telegram_chat_ids),
            event_ids: JSON.stringify(Array.from(this.event_ids))
        }).catch(err => {
            this.logger.error(`Error while dumping data for ${this._dump_key}`, { error: err.stack || err });
            if (this._dump_retries < 15) {
                this.logger.info(`Retrying dumping data for ${this._dump_key}`);
                setTimeout(this.dump.bind(this), 15000);
                this._dump_retries += 1;
            }
            else {
                this.logger.info(`Giving up on trying to dump data for ${this._dump_key}`);
                this._dump_retries = 0;
            }
        }).then(res => {
            if (res) {
                this._dump_retries = 0;
            }
        });
    }

    
    async restore(guild) {
        if (!this.redis) {
            return;
        }
        if (!guild) {
            this.logger.warn('Not enough input values to restore data.', { ...this.log_meta });
            return;
        }
        this._guild = guild;
        this.event_ids = new Set();

        let data;
        try {
            data = await this.redis.hgetall(this._dump_key);
        }
        catch (err) {
            this.logger.error(`Error while restoring data for ${this._dump_key}`, { error: err.stack || err });
            if (this._restore_retries < 15) {
                this.logger.info(`Retrying restoring data for ${this._dump_key}`, { ...this.log_meta });
                setTimeout(this.restore.bind(this), 15000);
                this._restore_retries += 1;
            }
            else {
                this.logger.info(`Giving up on trying to restore data for ${this._guild.id}:channel_subscriber:${this._channel.id}`, { ...this.log_meta });
                this._restore_retries = 0;
            }
            return;
        }

        if (!data || !data.active) {
            this.logger.info(`Nothing to restore for ${this._dump_key}`, { ...this.log_meta });
            return;
        }
        else {
            this.logger.info(`Restored data for ${this._guild.id}: ${JSON.stringify(data)}`, { ...this.log_meta });
        }

        this.active = data.active === 'true';
        this.telegram_chat_ids = data.telegram_chat_ids.length ? JSON.parse(data.telegram_chat_ids) : [];
        this.event_ids = new Set(data.event_ids ? JSON.parse(data.event_ids) : []);
        
        this.logger.info(`Parsed data ${this._dump_key}`, { parsed_data: JSON.stringify({ active: this.active, telegram_chat_ids: this.telegram_chat_ids, event_ids: this.event_ids }), ...this.log_meta });
    }

    async deleteDump() {
        if (!this.redis) {
            return;
        }

        return this.redis.del(this._dump_key).catch((err) => {
            this.logger.error(`Error while deleting dump for ${this._dump_key}`, { error: err.stack || err });
        });
    }

    _parseState(event) {
        if (!event) return;

        let parsed_state = {};

        parsed_state.event_id = event.id;
        parsed_state.event_name = event.name;
        parsed_state.event_description = event.description;
        parsed_state.event_active = event.isActive();
        parsed_state.event_url = event.url;
        // parsed_state.event_cover_url = event.coverImageURL(); // very small for some reason
        parsed_state.guild_id = event.guild.id;
        parsed_state.guild_name = event.guild.name;
        parsed_state.channel_id = event.channel?.id;
        parsed_state.channel_name = event.channel?.name;
        parsed_state.channel_url = event.channel?.url;
        parsed_state.start = event.scheduledStartTimestamp;
        parsed_state.end = event.scheduledEndTimestamp;

        return parsed_state;
    }

    async update(event) {
        if (!this.active) return;

        const parsed_state = this._parseState(event);

        if (!parsed_state.event_active) {
            this.event_ids.delete(parsed_state.event_id);
        }
        else {
            this.event_ids.add(parsed_state.event_id);
        }

        this.dump();

        this.logger.debug(
            `Cought updated event state: ${JSON.stringify(parsed_state)}`,
            { state: parsed_state }
        );

        if (parsed_state && this.telegram_chat_ids.length) {
            const promises = [];
            for (const telegram_chat_id of this.telegram_chat_ids) {
                promises.push(
                    sendNotification(parsed_state, telegram_chat_id).catch(err => {
                        this.logger.error(
                            `Couldn't send event state notification for ${event.name}:${event.guild.name}`,
                            { error: err.stack || err }
                        );
                    })
                );
            }
            return Promise.allSettled(promises);
        }
    }

    async cleanup(existing_event_ids) {
        const unactive_event_ids = Array.from(this.event_ids).filter(event_id => !existing_event_ids.includes(event_id));

        this.logger.debug(`Deleting any existing notifications for disappeared events with ids ${JSON.stringify(unactive_event_ids)}`, { ...this.log_meta });
        unactive_event_ids.forEach(event_id => {
            this.telegram_chat_ids.forEach(telegram_chat_id => {
                deleteNotification(telegram_chat_id, event_id);
            });
        });
    }

    async getScheduled() {
        if (!this.__guild) return null;

        return this._guild.scheduledEvents.cache.filter(e => e.isScheduled()).map(e => this._parseState(e));
    }
}

const isActive = (guild, telegram_chat_id) => {
    if (!guild) {
        return false;
    }

    let key = guild.id;

    if (!subscribers[key]?.active) {
        return false;
    }
    if (telegram_chat_id && !subscribers[key].telegram_chat_ids.includes(telegram_chat_id)) {
        return false;
    }

    return true;
};

const create = (guild, telegram_chat_id) => {
    if (!guild || !telegram_chat_id) return;

    let key = guild.id;

    if (isActive(guild, telegram_chat_id)) {
        return;
    }

    if (!subscribers[key]) {
        subscribers[key] = new EventSubscriber();
    }

    subscribers[key].start(guild, telegram_chat_id);
};

const stop = (guild, telegram_chat_id) => {
    if (!guild) {
        return;
    }

    let key = guild.id;
    
    if (!isActive(guild, telegram_chat_id)) {
        return;
    }

    subscribers[key]?.stop(telegram_chat_id);
};

const update = async (event) => {
    if (!event){
        return;
    }

    let key = event.guild.id;

    return subscribers[key]?.update(event);
};

const restore = async (guild) => {
    if (!guild) {
        return;
    }
    
    let key = guild.id;

    subscribers[key] = new EventSubscriber();
    return subscribers[key].restore(guild);
};

const cleanup = async (guild, existing_event_ids) => {
    if (!guild || !existing_event_ids?.length) return;

    let key = guild.id;

    return subscribers[key]?.cleanup(existing_event_ids);
};

const getScheduled = async (guild_id) => {
    if (!guild_id) return;

    return subscribers[guild_id] ? subscribers[guild_id].getScheduled() : [];
};

module.exports = {
    EventSubscriber,
    isActive,
    create,
    stop,
    update,
    restore,
    cleanup,
    getScheduled,
};