import * as Sentry from "@sentry/node" ; import { ChatClient } from "twitch-chat-client/lib" ; import signale from "signale" ; import { Channel } from "../interfaces/channel.interface" ; import { Identification } from "../interfaces/identification.interface" ; import GraphQL from "./graphql" ; import { CHANNEL , CHANNELS , CHANNEL_ADDED , CHANNEL_DELETED , CHANNEL_UPDATED , LATEST_IDENTIFICATION , UPDATE_CHANNEL , } from "../queries/queries" ; export default class Channels { key value public configurations = new Map < string , Channel > ( ) ; key value public pending = new Map < string , boolean > ( ) ; key value public cooldownNotice = new Map < string , boolean > ( ) ; /**
     * Array of Channels the Bot is currently part of, this is a temporary solution
     * for a missing `twitch-chat-client` feature
     */ public partOf : string [ ] = [ ] ; /**
     * ChatClient instance set in the TwitchClient class after initialization
     */ public client ? : ChatClient ; constructor ( private graphql ) { } param id public async getChannel ( id ) { return this . graphql . client . query ( { query : CHANNEL , variables : { id } } ) . then ( ( res ) => res . data . channel ) . catch ( ( error ) => { Sentry . captureException ( error ) ; signale . error ( `Unexpected error while fetching configuration for Channel \`${ id }\`` ) ; signale . error ( error ) ; throw new Error ( `Unexpected error while fetching configuration for Channel \`${ id }\`` ) ; } ) ; } /**
     * Start listening for Channel additions, updates and deletions
     */ public listen ( ) { signale . await ( "Listening for Channel changes" ) ; this . graphql . client . subscribe ( { query : CHANNEL_ADDED , } ) . subscribe ( { next : async ( res ) => { const id = res . data . channelAdded . id ; try { const channel = await this . getChannel ( id ) ; signale . scope ( channel . name ) . info ( `New Channel \`${ channel . name }\` was added` ) ; this . configurations . set ( channel . name , channel ) ; this . pending . set ( channel . name , false ) ; this . cooldownNotice . set ( channel . name , false ) ; this . client ?. join ( channel . name ) . then ( ( ) => { signale . success ( `Joined Channel \`${ channel . name }\`` ) ; this . partOf . push ( channel . name ) ; } ) . catch ( ( error ) => { signale . error ( `Error joining Channel \`${ channel . name }\`` ) ; signale . error ( error ) ; } ) ; } catch ( error ) { Sentry . captureException ( error ) ; signale . error ( `Error fetching configuration for Channel with Id \`${ id }\`` ) ; throw new Error ( `Error fetching configuration for Channel with Id \`${ id }\`` ) ; } } , error : ( error ) => { Sentry . captureException ( error ) ; signale . error ( "An error occurred while processing a `channelAdded` event" ) ; signale . error ( error ) ; } , } ) ; this . graphql . client . subscribe ( { query : CHANNEL_UPDATED , } ) . subscribe ( { next : async ( res ) => { const id = res . data . channelUpdated . id ; try { const channel = await this . getChannel ( id ) ; signale . info ( `Configuration for Channel \`${ channel . name }\` was updated` ) ; signale . info ( channel ) ; this . configurations . set ( channel . name , channel ) ; this . pending . set ( channel . name , false ) ; this . cooldownNotice . set ( channel . name , false ) ; if ( this . partOf . includes ( channel . name ) && ! channel . enabled ) { signale . info ( `Leaving Channel \`${ channel . name }\` because it is no longer \`enabled\`` ) ; this . partOf = this . partOf . filter ( ( c ) => c !== channel . name ) ; this . client ?. part ( channel . name ) ; } if ( ! this . partOf . includes ( channel . name ) && channel . enabled ) { signale . scope ( channel . name ) . info ( `Joining Channel because it was \`enabled\` again` ) ; this . client ?. join ( channel . name ) . then ( ( ) => { signale . success ( `Joined Channel \`${ channel . name }\`` ) ; this . partOf . push ( channel . name ) ; } ) . catch ( ( error ) => { signale . error ( `Error joining Channel \`${ channel }\`` ) ; signale . error ( error ) ; } ) ; } } catch ( error ) { Sentry . captureException ( error ) ; signale . error ( `Error fetching configuration for Channel with Id \`${ id }\`` ) ; throw new Error ( `Error fetching configuration for Channel with Id \`${ id }\`` ) ; } } , error : ( error ) => { Sentry . captureException ( error ) ; signale . error ( "An error occurred while processing a `channelUpdated` event" ) ; signale . error ( error ) ; } , } ) ; this . graphql . client . subscribe ( { query : CHANNEL_DELETED , } ) . subscribe ( { next : async ( res ) => { const id = res . data . channelDeleted . id ; const channel = this . configurations . get ( id ) ; if ( ! channel ) return signale . warn ( `Could not find configuration for Channel with Id \`${ id }\`, hence nothing to unlink` ) ; signale . info ( `Configuration for Channel \`${ channel . name }\` was deleted` ) ; this . configurations . delete ( id ) ; this . partOf = this . partOf . filter ( ( c ) => c !== channel . name ) ; this . client ?. part ( channel . name ) ; } , error : ( error ) => { Sentry . captureException ( error ) ; signale . error ( "An error occurred while processing a `channelDeleted` event" ) ; signale . error ( error ) ; } , } ) ; } returns public async updateChannel ( id ) { try { const channel = await this . graphql . client . query ( { query : CHANNEL , variables : { id } , } ) . then ( ( res ) => res . data . channel ) ; this . configurations . set ( channel . name , channel ) ; this . pending . set ( channel . name , false ) ; this . cooldownNotice . set ( channel . name , false ) ; return channel ; } catch ( error ) { Sentry . captureException ( error ) ; signale . fatal ( `An unexpected error occurred while fetching the Channel configuration for ${ id }` ) ; signale . fatal ( error ) ; throw new Error ( `An unexpected error occurred while fetching the Channel configuration for ${ id }` ) ; } } returns public async getConfigurations ( ) { try { let channels = await this . graphql . client . query ( { query : CHANNELS , } ) . then ( ( res ) => res . data . channels ) ; if ( process . env . NODE_ENV !== "production" ) { channels = [ channels [ 0 ] ] ; } return channels . map ( ( channel ) => { this . configurations . set ( channel . name , channel ) ; this . pending . set ( channel . name , false ) ; this . cooldownNotice . set ( channel . name , false ) ; return channel . name ; } ) ; } catch ( error ) { Sentry . captureException ( error ) ; signale . fatal ( `An unexpected error occurred while fetching all Channel configurations` ) ; signale . fatal ( error ) ; throw new Error ( `An unexpected error occurred while fetching all Channel configurations` ) ; } } param channel public async onCooldown ( channel ) { try { const identification = await this . graphql . client . query ( { query : LATEST_IDENTIFICATION , variables : { id : channel . id } , } ) . then ( ( res ) => res . data . channel . identifications [ 0 ] ) ; if ( ! identification ) { signale . warn ( `Found no previous Identification for Channel \`${ channel . name }\`` ) ; return { onCooldown : false , since : undefined , remaining : undefined , identification : undefined , } ; } const since = Math . round ( Math . abs ( ( new Date ( ) . getTime ( ) - new Date ( identification . date ) . getTime ( ) ) / 1000 ) ) ; const remaining = channel . cooldown - since ; const onCooldown = since > 0 && since < channel . cooldown ; signale . info ( `${ since }s passed since last Identification in Channel \`${ channel . name }\`` ) ; return { onCooldown , since , remaining , identification , } ; } catch ( error ) { Sentry . captureException ( error ) ; signale . scope ( channel . name ) . error ( `Unexpected error checking Cooldown for Channel \`${ channel . name }\`` ) ; signale . scope ( channel . name ) . error ( error ) ; throw new Error ( `Unexpected error checking Cooldown for Channel \`${ channel . name }\`` ) ; } } param channel public async disable ( channel ) { signale . await ( `Disabling Channel \`${ channel }\`` ) ; const config = this . configurations . get ( channel ) ; if ( ! config ) { signale . error ( `Could not find Channel configuration for Channel \`${ channel }\`` ) ; throw Error ( `Could not find Channel configuration for Channel \`${ channel }\`` ) ; } return this . graphql . client . mutate ( { mutation : UPDATE_CHANNEL , variables : { id : config . id , enabled : false } , } ) . then ( ( res ) => { signale . success ( `Disabled Channel \`${ config . name }\`` ) ; return res . data . updateChannel ; } ) . catch ( ( error ) => { Sentry . captureException ( error ) ; signale . error ( `Error disabling Channel \`${ config . name }\`` ) ; signale . error ( error ) ; } ) ; } }