Discord-InterChat/InterChat

View on GitHub
src/managers/InfractionManager/BaseInfractionManager.ts

Summary

Maintainability
A
0 mins
Test Coverage
import type { ConvertDatesToString } from '#types/Utils.d.ts';
import { cacheData, getCachedData } from '#utils/CacheUtils.js';
import {
  InfractionStatus,
  InfractionType,
  Prisma,
  ServerInfraction,
  UserInfraction,
} from '@prisma/client';
import { type Client, type Snowflake, type User } from 'discord.js';
import { isDate } from 'util/types';

export default abstract class BaseInfractionManager<T extends UserInfraction | ServerInfraction> {
  protected abstract modelName: 'UserInfraction' | 'ServerInfraction';
  public readonly targetId: Snowflake;
  private readonly cacheExpirySecs = 5 * 60;

  constructor(targetId: Snowflake) {
    this.targetId = targetId;
  }

  public abstract addInfraction(
    type: InfractionType,
    opts: {
      hubId: string;
      reason: string;
      moderatorId: Snowflake;
      expiresAt: Date | null;
    },
  ): Promise<T>;

  public abstract removeInfraction(type: InfractionType, hubId: string): Promise<T | null>;

  public abstract updateInfraction(
    filter: { type: InfractionType; hubId: string; status?: InfractionStatus },
    data: Prisma.UserInfractionUpdateInput | Prisma.ServerInfractionUpdateInput,
  ): Promise<T | null>;

  protected abstract queryEntityInfractions(hubId: string): Promise<T[]>;

  public async getHubInfractions(hubId: string, opts?: { type?: InfractionType; count?: number }) {
    const fetched = await getCachedData(
      `${this.modelName}:${this.targetId}:${hubId}`,
      async () => await this.queryEntityInfractions(hubId),
      this.cacheExpirySecs,
    );

    let infractionsArr = fetched.data ?? [];
    if (opts?.type) infractionsArr = infractionsArr.filter((i) => i.type === opts.type);
    if (opts?.count) infractionsArr = infractionsArr.slice(0, opts.count);

    return this.updateInfractionDates(infractionsArr);
  }

  public async fetchInfraction(type: InfractionType, hubId: string, status?: InfractionStatus) {
    const infractions = await this.getHubInfractions(hubId, { type });
    const infraction = infractions.find(
      (i) => (status ? i.status === status : true) && i.type === type,
    );

    return infraction ?? null;
  }

  public async revokeInfraction(
    type: InfractionType,
    hubId: string,
    status: Exclude<InfractionStatus, 'ACTIVE'> = 'REVOKED',
  ) {
    const revoked = await this.updateInfraction({ type, hubId, status: 'ACTIVE' }, { status });
    return revoked;
  }

  public abstract logUnblacklist(
    client: Client,
    hubId: string,
    id: string,
    opts: { mod: User; reason?: string },
  ): Promise<void>;

  protected async refreshCache(hubId: string) {
    const key = `${this.modelName}:${this.targetId}:${hubId}`;
    const infractions = await this.queryEntityInfractions(hubId);
    await cacheData(key, JSON.stringify(infractions), this.cacheExpirySecs);
  }

  protected async cacheEntity(entity: T) {
    const entitySnowflake = 'userId' in entity ? entity.userId : entity.serverId;
    const key = `${this.modelName}:${entitySnowflake}:${entity.hubId}`;
    const existing = (await this.getHubInfractions(entity.hubId, { type: entity.type })).filter(
      (i) => i.id !== entity.id,
    );

    return cacheData(key, JSON.stringify([...existing, entity]), this.cacheExpirySecs);
  }

  protected async removeCachedEntity(entity: T) {
    const existingInfractions = await this.getHubInfractions(entity.hubId, { type: entity.type });
    const entitySnowflake = 'userId' in entity ? entity.userId : entity.serverId;
    return cacheData(
      `${this.modelName}:${entitySnowflake}:${entity.hubId}`,
      JSON.stringify(existingInfractions.filter((i) => i.id !== entity.id)),
      this.cacheExpirySecs,
    );
  }

  protected updateInfractionDates(infractions: ConvertDatesToString<T>[]) {
    if (infractions.length === 0) return [];
    else if (isDate(infractions[0].dateIssued)) return infractions as T[];

    return infractions.map((infrac) => ({
      ...infrac,
      dateIssued: new Date(infrac.dateIssued),
      appealedAt: infrac.appealedAt ? new Date(infrac.appealedAt) : null,
      expiresAt: infrac.expiresAt ? new Date(infrac.expiresAt) : null,
    }));
  }

  public filterValidInfractions(infractions: UserInfraction[]): UserInfraction[] {
    return infractions.filter(({ expiresAt }) => !expiresAt || expiresAt > new Date()) ?? [];
  }

  public isExpiredInfraction(infraction: T | null) {
    return !infraction || (!!infraction.expiresAt && infraction.expiresAt <= new Date());
  }
}