import Semaphore from "semaphore-async-await" ; import { ChatClient } from "../../client/client" ; import { RoomState } from "../../message/twitch-types/roomstate" ; import { UserState } from "../../message/twitch-types/userstate" ; import { applyReplacements } from "../../utils/apply-function-replacements" ; import { EditableTimeout } from "../../utils/editable-timeout" ; import { ClientMixin } from "../base-mixin" ; import { canSpamFast } from "./utils" ; export class SlowModeRateLimiter implements ClientMixin { public static GLOBAL_SLOW_MODE_COOLDOWN = 1.5 ; private readonly client : ChatClient ; private readonly maxQueueLength : number ; private readonly semaphores : Record < string , Semaphore > = { } ; private readonly runningTimers : Record < string , EditableTimeout > = { } ; public constructor ( client , maxQueueLength = 10 ) { this . client = client ; this . maxQueueLength = maxQueueLength ; } public applyToClient ( client ) { const genericReplament = async ( oldFn , channelName , message ) : Promise < void > => { const releaseFn = await this . acquire ( channelName ) ; if ( releaseFn == null ) { return ; } try { return await oldFn ( channelName , message ) ; } finally { releaseFn ( ) ; } } ; applyReplacements ( this , client , { say : genericReplament , me : genericReplament , privmsg : genericReplament , } ) ; if ( client . roomStateTracker != null ) { client . roomStateTracker . on ( "newChannelState" , this . onRoomStateChange . bind ( this ) ) ; } if ( client . userStateTracker != null ) { client . userStateTracker . on ( "newChannelState" , this . onUserStateChange . bind ( this ) ) ; } } private getSemaphore ( channelName ) { let semaphore = this . semaphores [ channelName ] ; if ( semaphore == null ) { semaphore = new Semaphore ( 1 ) ; this . semaphores [ channelName ] = semaphore ; } return semaphore ; } private onUserStateChange ( channelName , newState ) { const { fastSpam , certain } = canSpamFast ( channelName , this . client . configuration . username , newState ) ; const runningTimer = this . runningTimers [ channelName ] ; if ( fastSpam && runningTimer != null ) { runningTimer . update ( 0 ) ; } if ( certain && channelName in this . semaphores ) { const semaphore = this . getSemaphore ( channelName ) ; const waiterQueue = semaphore . promiseResolverQueue ; const removedWaiters = waiterQueue . splice ( 10 ) ; for ( const removedWaiter of removedWaiters ) { removedWaiter ( false ) ; } } } private onRoomStateChange ( channelName , newState ) { const newSlowModeDuration = Math . max ( newState . slowModeDuration , SlowModeRateLimiter . GLOBAL_SLOW_MODE_COOLDOWN ) ; const runningTimer = this . runningTimers [ channelName ] ; if ( runningTimer != null ) { runningTimer . update ( newSlowModeDuration ) ; } } private async acquire ( channelName ) { const { fastSpam , certain } = canSpamFast ( channelName , this . client . configuration . username , this . client . userStateTracker ) ; if ( fastSpam ) { return ( ) => { } ; } const semaphore = this . getSemaphore ( channelName ) ; const waiterQueue = semaphore . promiseResolverQueue ; if ( certain && waiterQueue . length >= this . maxQueueLength ) { return undefined ; } const releaseFn = ( ) : void => { const { fastSpam : fastSpamAfterRelease } = canSpamFast ( channelName , this . client . configuration . username , this . client . userStateTracker ) ; if ( fastSpamAfterRelease ) { semaphore . release ( ) ; return ; } const slowModeDuration = this . getSlowModeDuration ( channelName ) ; this . runningTimers [ channelName ] = new EditableTimeout ( ( ) => { delete this . runningTimers [ channelName ] ; semaphore . release ( ) ; } , slowModeDuration * 1000 ) ; } ; const success = await semaphore . acquire ( ) ; if ( ! success ) { return undefined ; } const { fastSpam : fastSpamAfterAwait } = canSpamFast ( channelName , this . client . configuration . username , this . client . userStateTracker ) ; if ( fastSpamAfterAwait ) { semaphore . release ( ) ; return ( ) => { } ; } return releaseFn ; } private getSlowModeDuration ( channelName ) { if ( this . client . roomStateTracker != null ) { const roomState = this . client . roomStateTracker . getChannelState ( channelName ) ; if ( roomState != null ) { return Math . max ( roomState . slowModeDuration , SlowModeRateLimiter . GLOBAL_SLOW_MODE_COOLDOWN ) ; } } return SlowModeRateLimiter . GLOBAL_SLOW_MODE_COOLDOWN ; } }