{"version":3,"file":"slow-mode.mjs","names":[],"sources":["../../../src/mixins/ratelimiters/slow-mode.ts"],"sourcesContent":["import { Sema } from \"async-sema\";\n\nimport type { ClientMixin } from \"../base-mixin\";\nimport type { ChatClient } from \"~/client/client\";\nimport type { RoomState } from \"~/message/twitch-types/roomstate\";\nimport type { UserState } from \"~/message/twitch-types/userstate\";\nimport { canSpamFast } from \"./utils\";\nimport { applyReplacements } from \"~/utils/apply-function-replacements\";\nimport { EditableTimeout } from \"~/utils/editable-timeout\";\n\nexport class SlowModeRateLimiter implements ClientMixin {\n  public static GLOBAL_SLOW_MODE_COOLDOWN = 1.5;\n\n  private readonly client: ChatClient;\n  private readonly maxQueueLength: number;\n  private readonly semaphores: Record<string, Sema> = {};\n  private readonly runningTimers: Record<string, EditableTimeout> = {};\n\n  public constructor(client: ChatClient, maxQueueLength = 10) {\n    this.client = client;\n    this.maxQueueLength = maxQueueLength;\n  }\n\n  public applyToClient(client: ChatClient): void {\n    const genericReplacement = async <A extends unknown[]>(\n      oldFunction: (channelName: string, ...args: A) => Promise<void>,\n      channelName: string,\n      ...args: A\n    ): Promise<void> => {\n      const releaseFunction = await this.acquire(channelName);\n      if (!releaseFunction) {\n        // queue is full & message is dropped\n        return;\n      }\n\n      try {\n        // eslint-disable-next-line ts/return-await\n        return oldFunction(channelName, ...args);\n      } finally {\n        releaseFunction();\n      }\n    };\n\n    applyReplacements(this, client, {\n      say: genericReplacement,\n      me: genericReplacement,\n      privmsg: genericReplacement,\n    });\n\n    client.roomStateTracker.on(\n      \"newChannelState\",\n      this.onRoomStateChange.bind(this),\n    );\n\n    client.userStateTracker.on(\n      \"newChannelState\",\n      this.onUserStateChange.bind(this),\n    );\n  }\n\n  private getSemaphore(channelName: string): Sema {\n    let semaphore = this.semaphores[channelName];\n    if (semaphore == null) {\n      semaphore = new Sema(1);\n      this.semaphores[channelName] = semaphore;\n    }\n\n    return semaphore;\n  }\n\n  private onUserStateChange(channelName: string, newState: UserState): void {\n    const { fastSpam } = canSpamFast(\n      channelName,\n      this.client.configuration.username,\n      newState,\n    );\n\n    const runningTimer = this.runningTimers[channelName];\n    if (fastSpam && runningTimer != null) runningTimer.update(0);\n  }\n\n  private onRoomStateChange(channelName: string, newState: RoomState): void {\n    // new slow mode?\n\n    const newSlowModeDuration = Math.max(\n      newState.slowModeDuration,\n      SlowModeRateLimiter.GLOBAL_SLOW_MODE_COOLDOWN,\n    );\n\n    const runningTimer = this.runningTimers[channelName];\n    if (runningTimer) runningTimer.update(newSlowModeDuration);\n  }\n\n  private async acquire(\n    channelName: string,\n  ): Promise<(() => void) | undefined> {\n    const { fastSpam, certain } = canSpamFast(\n      channelName,\n      this.client.configuration.username,\n      this.client.userStateTracker,\n    );\n\n    // nothing is acquired and nothing has to be released\n    if (fastSpam) {\n      // eslint-disable-next-line ts/no-empty-function\n      return () => {};\n    }\n\n    const semaphore = this.getSemaphore(channelName);\n\n    // too many waiting. Message will be dropped.\n    // note that we do NOT drop messages when we are unsure about\n    // fast spam state (e.g. before the first USERSTATE is received)\n    if (certain && semaphore.nrWaiting() >= this.maxQueueLength) {\n      return undefined;\n    }\n\n    const releaseFunction = (): void => {\n      const { fastSpam: fastSpamAfterRelease } = canSpamFast(\n        channelName,\n        this.client.configuration.username,\n        this.client.userStateTracker,\n      );\n\n      if (fastSpamAfterRelease) {\n        semaphore.release();\n        return;\n      }\n\n      const slowModeDuration = this.getSlowModeDuration(channelName);\n\n      this.runningTimers[channelName] = new EditableTimeout(() => {\n        // eslint-disable-next-line ts/no-dynamic-delete\n        delete this.runningTimers[channelName];\n        semaphore.release();\n      }, slowModeDuration * 1000);\n    };\n\n    await semaphore.acquire();\n\n    // if we were released by a incoming USERSTATE change (the timer was\n    // edited) and spam can now be fast, return the token immediately\n    // and return a no-op releaseFn.\n    const { fastSpam: fastSpamAfterAwait } = canSpamFast(\n      channelName,\n      this.client.configuration.username,\n      this.client.userStateTracker,\n    );\n\n    if (fastSpamAfterAwait) {\n      semaphore.release();\n      // eslint-disable-next-line ts/no-empty-function\n      return () => {};\n    }\n\n    return releaseFunction;\n  }\n\n  private getSlowModeDuration(channelName: string): number {\n    const roomState = this.client.roomStateTracker.getChannelState(channelName);\n    if (roomState != null) {\n      return Math.max(\n        roomState.slowModeDuration,\n        SlowModeRateLimiter.GLOBAL_SLOW_MODE_COOLDOWN,\n      );\n    }\n\n    return SlowModeRateLimiter.GLOBAL_SLOW_MODE_COOLDOWN;\n  }\n}\n"],"mappings":";;;;;AAUA,IAAa,sBAAb,MAAa,oBAA2C;CACtD,OAAc,4BAA4B;CAE1C;CACA;CACA,aAAoD,EAAE;CACtD,gBAAkE,EAAE;CAEpE,YAAmB,QAAoB,iBAAiB,IAAI;AAC1D,OAAK,SAAS;AACd,OAAK,iBAAiB;;CAGxB,cAAqB,QAA0B;EAC7C,MAAM,qBAAqB,OACzB,aACA,aACA,GAAG,SACe;GAClB,MAAM,kBAAkB,MAAM,KAAK,QAAQ,YAAY;AACvD,OAAI,CAAC,gBAEH;AAGF,OAAI;AAEF,WAAO,YAAY,aAAa,GAAG,KAAK;aAChC;AACR,qBAAiB;;;AAIrB,oBAAkB,MAAM,QAAQ;GAC9B,KAAK;GACL,IAAI;GACJ,SAAS;GACV,CAAC;AAEF,SAAO,iBAAiB,GACtB,mBACA,KAAK,kBAAkB,KAAK,KAAK,CAClC;AAED,SAAO,iBAAiB,GACtB,mBACA,KAAK,kBAAkB,KAAK,KAAK,CAClC;;CAGH,aAAqB,aAA2B;EAC9C,IAAI,YAAY,KAAK,WAAW;AAChC,MAAI,aAAa,MAAM;AACrB,eAAY,IAAI,KAAK,EAAE;AACvB,QAAK,WAAW,eAAe;;AAGjC,SAAO;;CAGT,kBAA0B,aAAqB,UAA2B;EACxE,MAAM,EAAE,aAAa,YACnB,aACA,KAAK,OAAO,cAAc,UAC1B,SACD;EAED,MAAM,eAAe,KAAK,cAAc;AACxC,MAAI,YAAY,gBAAgB,KAAM,cAAa,OAAO,EAAE;;CAG9D,kBAA0B,aAAqB,UAA2B;EAGxE,MAAM,sBAAsB,KAAK,IAC/B,SAAS,kBACT,oBAAoB,0BACrB;EAED,MAAM,eAAe,KAAK,cAAc;AACxC,MAAI,aAAc,cAAa,OAAO,oBAAoB;;CAG5D,MAAc,QACZ,aACmC;EACnC,MAAM,EAAE,UAAU,YAAY,YAC5B,aACA,KAAK,OAAO,cAAc,UAC1B,KAAK,OAAO,iBACb;AAGD,MAAI,SAEF,cAAa;EAGf,MAAM,YAAY,KAAK,aAAa,YAAY;AAKhD,MAAI,WAAW,UAAU,WAAW,IAAI,KAAK,eAC3C;EAGF,MAAM,wBAA8B;GAClC,MAAM,EAAE,UAAU,yBAAyB,YACzC,aACA,KAAK,OAAO,cAAc,UAC1B,KAAK,OAAO,iBACb;AAED,OAAI,sBAAsB;AACxB,cAAU,SAAS;AACnB;;GAGF,MAAM,mBAAmB,KAAK,oBAAoB,YAAY;AAE9D,QAAK,cAAc,eAAe,IAAI,sBAAsB;AAE1D,WAAO,KAAK,cAAc;AAC1B,cAAU,SAAS;MAClB,mBAAmB,IAAK;;AAG7B,QAAM,UAAU,SAAS;EAKzB,MAAM,EAAE,UAAU,uBAAuB,YACvC,aACA,KAAK,OAAO,cAAc,UAC1B,KAAK,OAAO,iBACb;AAED,MAAI,oBAAoB;AACtB,aAAU,SAAS;AAEnB,gBAAa;;AAGf,SAAO;;CAGT,oBAA4B,aAA6B;EACvD,MAAM,YAAY,KAAK,OAAO,iBAAiB,gBAAgB,YAAY;AAC3E,MAAI,aAAa,KACf,QAAO,KAAK,IACV,UAAU,kBACV,oBAAoB,0BACrB;AAGH,SAAO,oBAAoB"}