const { BaseSubscriber } = require('./utils');
const { sendNotification, deleteNotification } = require('../telegram/channel-subscriber');
const { ChannelType } = require('discord.js');
const subscribers = {};
/**
* Channel Subscriber
* @namespace ChannelSubscriber
*/
function isDifferent(obj1, obj2) {
if (Object.keys(obj1).length !== Object.keys(obj2).length) {
return true;
}
for(const k of Object.keys(obj1)) {
if (typeof obj1[k] === 'object' && typeof obj2[k] === 'object') {
if(isDifferent(obj1[k], obj2[k])) return true;
}
else {
if (obj1[k] !== obj2[k]) return true;
}
}
return false;
}
/**
* ChannelSubscriber
* @class
* @extends {Discord.Utils.BaseSubscriber}
* @memberof ChannelSubscriber
*/
class ChannelSubscriber extends BaseSubscriber {
constructor() {
super('channel_subscriber');
this.last_state = null;
}
set _channel(channel) {
this.log_meta.discord_channel_id = channel?.id;
this.log_meta.discord_channel = channel?.name;
this.__channel = channel;
}
get _channel() {
return this.__channel;
}
get _dump_key() {
return `${this._guild?.id}:${this._subscriber_type}:${this._channel?.id}`;
}
async update(channel) {
if (!this.active) {
return;
}
const parsed_state = this._parseState(channel);
if (this.last_state && (!isDifferent(parsed_state, this.last_state) || !isDifferent(this.last_state, parsed_state))) {
return;
}
this.last_state = parsed_state;
this.logger.debug(
`Cought updated voice channel 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 channel state notification for ${this._guild.name}:${this._channel.name}`,
{ error: err.stack || err, telegram_chat_id}
);
})
);
}
return Promise.allSettled(promises);
}
}
/**
* Parse notification data from current channel state
* @param {import('discord.js').GuildChannel} channel
* @returns {ChannelSubscriber.DiscordNotificationData}
*/
_parseState(channel) {
if (!channel) return;
let parsed_state = {};
parsed_state.channel_id = channel.id;
parsed_state.channel_name = channel.name;
parsed_state.channel_url = channel.url;
parsed_state.channel_type = channel.type;
parsed_state.guild_id = channel.guild.id;
parsed_state.guild_name = channel.guild.name;
switch(channel.type) {
case ChannelType.GuildVoice:
parsed_state.channel_type = 'voice';
break;
case ChannelType.GuildStageVoice:
parsed_state.channel_type = 'stage';
break;
case ChannelType.GuildForum:
parsed_state.channel_type = 'forum';
break;
case ChannelType.GuildAnnouncement:
parsed_state.channel_type = 'announcements';
break;
case ChannelType.GuildText:
parsed_state.channel_type = 'text';
break;
default:
break;
}
parsed_state.members = [];
channel.members.forEach((member) => {
parsed_state.members.push({
user_id: member.user.id,
user_name: member.user.username,
streaming: member.voice.streaming,
member_id: member.id,
member_name: member.displayName,
muted: member.voice.mute,
deafened: member.voice.deaf,
// server_muted: member.voice.serverMute,
// server_deafened: member.voice.serverDeaf,
camera: member.voice.selfVideo,
activity: member?.presence?.activities?.[0]?.name?.toLowerCase() === 'status'
? member.presence.activities[0].details
: member?.presence?.activities?.[0]?.name
});
});
return parsed_state;
}
start(channel, telegram_chat_id) {
if (!channel || !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._channel = channel;
this._guild = channel.guild;
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)];
this.logger.info(`Deleting channel state notification for ${this._guild.name}:${this._channel.name} in [chat: ${telegram_chat_id}]`);
deleteNotification(telegram_chat_id, this._channel.id);
}
else {
this.logger.info(`Deleting channel state notifications for ${this._guild.name}:${this._channel.name} in [chats: ${JSON.stringify(this.telegram_chat_ids)}]`);
this.telegram_chat_ids.forEach((telegram_chat_id) => {
deleteNotification(telegram_chat_id, this._channel.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),
last_state: JSON.stringify(this.last_state)
}).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(channel) {
if (!this.redis) {
return;
}
if (!channel && !this._guild && !this._channel) {
this.logger.warn('Not enough input values to restore data.', { ...this.log_meta });
return;
}
this._channel = channel;
this._guild = channel.guild;
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._dump_key}`, { ...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.last_state = data.last_state && JSON.parse(data.last_state);
this.logger.info(`Parsed data ${this._dump_key}`, { parsed_data: JSON.stringify({ active: this.active, telegram_chat_ids: this.telegram_chat_ids, last_state: this.last_state }), ...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 });
});
}
}
const isActive = (channel, telegram_chat_id) => {
if (!channel) {
return false;
}
let key = `${channel.guild.id}:${channel.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 = (channel, telegram_chat_id) => {
if (!channel || !telegram_chat_id) return;
let key = `${channel.guild.id}:${channel.id}`;
if (isActive(channel, telegram_chat_id)) {
return;
}
if (!subscribers[key]) {
subscribers[key] = new ChannelSubscriber();
}
subscribers[key].start(channel, telegram_chat_id);
};
const stop = (channel, telegram_chat_id) => {
if (!channel) {
return;
}
let key = `${channel.guild.id}:${channel.id}`;
if (!isActive(channel, telegram_chat_id)) {
return;
}
subscribers[key].stop(telegram_chat_id);
};
const update = async (channel) => {
if (!channel){
return;
}
let key = `${channel.guild.id}:${channel.id}`;
return subscribers[key].update(channel);
}
const restore = async (channel) => {
if (!channel) {
return;
}
let key = `${channel.guild.id}:${channel.id}`;
subscribers[key] = new ChannelSubscriber();
return subscribers[key].restore(channel);
}
module.exports = {
ChannelSubscriber,
isActive,
create,
stop,
update,
restore,
};