Discord-InterChat/InterChat

View on GitHub
src/services/HubJoinService.ts

Summary

Maintainability
A
3 hrs
Test Coverage
import BlacklistManager from '#main/managers/BlacklistManager.js';
import HubManager from '#main/managers/HubManager.js';
import { HubService } from '#main/services/HubService.js';

import { TranslationKeys } from '#types/TranslationKeys.d.ts';
import { createConnection, getHubConnections } from '#utils/ConnectedListUtils.js';
import { emojis } from '#utils/Constants.js';
import db from '#utils/Db.js';
import { logJoinToHub } from '#utils/hub/logger/JoinLeave.js';
import { sendToHub } from '#utils/hub/utils.js';
import { supportedLocaleCodes, t } from '#utils/Locale.js';
import { check } from '#utils/ProfanityUtils.js';
import { getOrCreateWebhook, getReplyMethod } from '#utils/Utils.js';
import { stripIndents } from 'common-tags';
import {
  ChatInputCommandInteraction,
  GuildTextBasedChannel,
  MessageComponentInteraction,
} from 'discord.js';

export class HubJoinService {
  private readonly interaction:
    | ChatInputCommandInteraction<'cached'>
    | MessageComponentInteraction<'cached'>;
  private readonly locale: supportedLocaleCodes;
  private readonly hubService: HubService;

  constructor(
    interaction: ChatInputCommandInteraction<'cached'> | MessageComponentInteraction<'cached'>,
    locale: supportedLocaleCodes,
    hubService: HubService = new HubService(),
  ) {
    this.interaction = interaction;
    this.locale = locale;
    this.hubService = hubService;
  }

  async joinRandomHub(channel: GuildTextBasedChannel) {
    const hub = await db.hub.findMany({
      where: { private: false },
      orderBy: { connections: { _count: 'asc' } },
      take: 10,
    });

    const randomHub = hub[Math.floor(Math.random() * hub.length)];
    return await this.joinHub(channel, randomHub.name);
  }

  async joinHub(channel: GuildTextBasedChannel, hubInviteOrName: string | undefined) {
    if (!this.interaction.deferred) await this.interaction.deferReply({ ephemeral: true });

    const checksPassed = await this.runChecks(channel);
    if (!checksPassed) return false;

    const hub = await this.fetchHub(hubInviteOrName);
    if (!hub) {
      await this.interaction.followUp({
        content: t('hub.notFound', this.locale, { emoji: emojis.no }),
        ephemeral: true,
      });
      return false;
    }

    if ((await this.isAlreadyInHub(channel, hub.id)) || (await this.isBlacklisted(hub))) {
      return false;
    }

    const webhook = await this.createWebhook(channel);
    if (!webhook) return false;

    // Create the connection
    await createConnection({
      serverId: channel.guildId,
      channelId: channel.id,
      parentId: channel.isThread() ? channel.parentId : undefined,
      webhookURL: webhook.url,
      hub: { connect: { id: hub.id } },
      connected: true,
      compact: true,
      profFilter: true,
    });

    await this.sendSuccessMessages(hub, channel);
    return true;
  }

  private async runChecks(channel: GuildTextBasedChannel) {
    if (!channel.permissionsFor(this.interaction.member).has('ManageMessages', true)) {
      await this.replyError('errors.missingPermissions', {
        permissions: 'Manage Messages',
        emoji: emojis.no,
      });
      return false;
    }

    const { hasSlurs, hasProfanity } = check(this.interaction.guild.name);
    if (hasSlurs || hasProfanity) {
      await this.replyError('errors.serverNameInappropriate', { emoji: emojis.no });
      return false;
    }

    return true;
  }

  private async fetchHub(hubNameOrInvite?: string) {
    const hubName = hubNameOrInvite ?? 'InterChat Central';

    // Check if it's an invite code
    if (hubNameOrInvite) {
      const fetchedInvite = await db.hubInvite.findFirst({
        where: { code: hubNameOrInvite },
        include: { hub: true },
      });

      if (fetchedInvite) return new HubManager(fetchedInvite.hub);
    }

    // Otherwise search by name
    return await this.hubService.fetchHub({ name: hubName });
  }

  private async isAlreadyInHub(channel: GuildTextBasedChannel, hubId: string) {
    const channelInHub = await db.connection.findFirst({
      where: { OR: [{ channelId: channel.id }, { serverId: channel.guildId, hubId }] },
      include: { hub: { select: { name: true } } },
    });

    if (channelInHub) {
      await this.replyError('hub.alreadyJoined', {
        channel: `<#${channelInHub.channelId}>`,
        hub: `${channelInHub.hub?.name}`,
        emoji: emojis.no,
      });
      return true;
    }
    return false;
  }

  private async isBlacklisted(hub: HubManager) {
    const userBlManager = new BlacklistManager('user', this.interaction.user.id);
    const serverBlManager = new BlacklistManager('server', this.interaction.guildId);

    const userBlacklist = await userBlManager.fetchBlacklist(hub.id);
    const serverBlacklist = await serverBlManager.fetchBlacklist(hub.id);

    if (userBlacklist || serverBlacklist) {
      await this.replyError('errors.blacklisted', { emoji: emojis.no, hub: hub.data.name });
      return true;
    }

    return false;
  }

  private async createWebhook(channel: GuildTextBasedChannel) {
    const webhook = await getOrCreateWebhook(channel);
    if (!webhook) {
      await this.replyError('errors.botMissingPermissions', {
        permissions: 'Manage Webhooks',
        emoji: emojis.no,
      });
      return null;
    }
    return webhook;
  }

  private async sendSuccessMessages(hub: HubManager, channel: GuildTextBasedChannel) {
    const replyMethod = getReplyMethod(this.interaction);
    await this.interaction[replyMethod]({
      content: t('hub.join.success', this.locale, {
        channel: `${channel}`,
        hub: hub.data.name,
      }),
      embeds: [],
      components: [],
    });

    const totalConnections =
      (await getHubConnections(hub.id))?.reduce(
        (total, c) => total + (c.connected && c.channelId !== channel.id ? 1 : 0),
        0,
      ) ?? 0;

    const serverCountMessage =
      totalConnections === 0
        ? 'There are no other servers connected to this hub. *cricket noises* 🦗'
        : `We now have ${totalConnections} servers in this hub! 🎉`;

    // Announce to hub
    await sendToHub(hub.id, {
      username: `InterChat | ${hub.data.name}`,
      content: stripIndents`
        A new server has joined the hub! ${emojis.clipart}

        **Server Name:** __${this.interaction.guild.name}__
        **Member Count:** __${this.interaction.guild.memberCount}__

        ${serverCountMessage}
      `,
    });

    // Send log
    await logJoinToHub(hub.id, this.interaction.guild, {
      totalConnections,
      hubName: hub.data.name,
    });
  }

  private async replyError<K extends keyof TranslationKeys>(
    key: K,
    options?: { [key in TranslationKeys[K]]: string },
  ) {
    const content = t(key, this.locale, options);
    const replyMethod = getReplyMethod(this.interaction);

    await this.interaction[replyMethod]({
      content,
      ephemeral: true,
    });
  }
}