Discord-InterChat/InterChat

View on GitHub
src/utils/network/messageUtils.ts

Summary

Maintainability
A
0 mins
Test Coverage
import Constants, { RedisKeys } from '#utils/Constants.js';
import Logger from '#main/utils/Logger.js';
import getRedis from '#main/utils/Redis.js';
import type { Message, Snowflake } from 'discord.js';
import isEmpty from 'lodash/isEmpty.js';

export interface OriginalMessage {
  hubId: string;
  messageId: string;
  guildId: string;
  authorId: string;
  timestamp: number;
  reactions?: { [key: string]: Snowflake[] };
  referredMessageId?: string;
}

export interface Broadcast {
  mode: number;
  messageId: string;
  channelId: string;
  originalMsgId: string;
}

export const storeMessage = async (originalMsgId: string, messageData: OriginalMessage) => {
  const key = `${RedisKeys.message}:${originalMsgId}`;
  const redis = getRedis();

  Logger.debug(`Storing message ${originalMsgId} in cache`);

  await redis.hset(key, messageData);
  await redis.expire(key, 86400); // 1 day in seconds
};

export const getOriginalMessage = async (originalMsgId: string) => {
  const key = `${RedisKeys.message}:${originalMsgId}`;
  const res = await getRedis().hgetall(key);

  if (isEmpty(res)) return null;

  return { ...res, timestamp: parseInt(res.timestamp) } as OriginalMessage;
};

export const addBroadcasts = async (
  hubId: string,
  originalMsgId: Snowflake,
  ...broadcasts: Broadcast[]
) => {
  try {
    const redis = getRedis();
    const broadcastsKey = `${RedisKeys.broadcasts}:${originalMsgId}:${hubId}`;
    const pipeline = redis.pipeline();

    // Prepare all operations in a single reduce to minimize iterations
    const { broadcastEntries, reverseLookupKeys } = broadcasts.reduce(
      (acc, broadcast) => {
        const { messageId, channelId, mode } = broadcast;
        const broadcastInfo = JSON.stringify({ mode, messageId, channelId, originalMsgId });

        // Add to broadcasts entries
        acc.broadcastEntries.push(channelId, broadcastInfo);

        // Store reverse lookup key for later expiry setting
        const reverseKey = `${RedisKeys.messageReverse}:${messageId}`;
        acc.reverseLookupKeys.push(reverseKey);

        // Add reverse lookup to pipeline
        pipeline.set(reverseKey, `${originalMsgId}:${hubId}`);

        return acc;
      },
      {
        broadcastEntries: [] as string[],
        reverseLookupKeys: [] as string[],
      },
    );

    Logger.debug(`Adding ${broadcasts.length} broadcasts for message ${originalMsgId}`);

    // Add main broadcast hash
    pipeline.hset(broadcastsKey, broadcastEntries);
    pipeline.expire(broadcastsKey, 86400);

    // Set expiry for all reverse lookups in the same pipeline
    reverseLookupKeys.forEach((key) => {
      pipeline.expire(key, 86400);
    });

    // Execute all Redis operations in a single pipeline
    await pipeline.exec();

    Logger.debug(`Added ${broadcasts.length} broadcasts for message ${originalMsgId}`);
  }
  catch (error) {
    Logger.error('Failed to add broadcasts', error);
  }
};

export const getBroadcasts = async (originalMsgId: string, hubId: string) => {
  const key = `${RedisKeys.broadcasts}:${originalMsgId}:${hubId}`;
  const broadcasts = await getRedis().hgetall(key);
  const entries = Object.entries(broadcasts);

  // Parse the JSON strings back into objects
  return Object.fromEntries(entries.map(([k, v]) => [k, JSON.parse(v)])) as Record<
    string,
    Broadcast
  >;
};

export const getBroadcast = async (
  originalMsgId: string,
  hubId: string,
  find: { channelId: string },
) => {
  const broadcast = await getRedis().hget(
    `${RedisKeys.broadcasts}:${originalMsgId}:${hubId}`,
    find.channelId,
  );
  return broadcast ? (JSON.parse(broadcast) as Broadcast) : null;
};

export const findOriginalMessage = async (broadcastedMessageId: string) => {
  const reverseLookupKey = `${RedisKeys.messageReverse}:${broadcastedMessageId}`;
  const lookup = await getRedis().get(reverseLookupKey);

  if (!lookup) return null;
  const [originalMsgId] = lookup.split(':');

  return await getOriginalMessage(originalMsgId);
};

export const storeMessageTimestamp = async (message: Message) => {
  Logger.debug(`Storing message timestamp for channel ${message.channelId}`);
  await getRedis().hset(`${RedisKeys.msgTimestamp}`, message.channelId, message.createdTimestamp);
  Logger.debug(`Stored message timestamp for channel ${message.channelId}`);
};

export const deleteMessageCache = async (originalMsgId: Snowflake) => {
  const redis = getRedis();
  const original = await getOriginalMessage(originalMsgId);
  if (!original) return 0;

  // delete broadcats, reverse lookups and original message
  const broadcats = Object.values(await getBroadcasts(originalMsgId, original.hubId));
  await redis.del(`${RedisKeys.broadcasts}:${originalMsgId}:${original.hubId}`);
  await redis.del(broadcats.map((b) => `${RedisKeys.messageReverse}:${b.messageId}`)); // multi delete
  const count = await redis.del(`${RedisKeys.message}:${originalMsgId}`);

  return count;
};

export const getMessageIdFromStr = (str: string) => {
  const match = str.match(Constants.Regex.MessageLink)?.[3] ?? str.match(/(\d{17,19})/)?.[0];
  return match;
};