import { Badges , ChatUserstate , Client , Options } from "tmi.js" ; import { ConfigurationChangeEvent , Disposable , Event , EventEmitter , ExtensionContext , workspace , WorkspaceConfiguration } from 'vscode' ; import { keytar } from "../common" ; import { Configuration , KeytarKeys , LogLevel , Settings } from "../enums" ; import { log } from '../logger' ; import { API } from './api' ; interface IBadges extends Badges { [ key ] : string | undefined ; follower : string ; } export interface ChatClientMessageReceivedEvent { userState : ChatUserstate ; message : string ; } export class ChatClient implements Disposable { private readonly _onChatClientConnected : EventEmitter < boolean > = new EventEmitter ( ) ; private readonly _onChatClientMessageReceived : EventEmitter < ChatClientMessageReceivedEvent > = new EventEmitter ( ) ; public readonly onChatClientConnected : Event < boolean > = this . _onChatClientConnected . event ; public readonly onChatClientMessageReceived : Event < ChatClientMessageReceivedEvent > = this . _onChatClientMessageReceived . event ; private config ? : WorkspaceConfiguration ; private client ? : Client ; private channel : string = "" ; private announceBot : boolean = true ; private joinMessage : string = "" ; private leaveMessage : string = "" ; private requiredBadges : string [ ] = [ ] ; constructor ( private log ) { } public initialize ( context ) { this . config = workspace . getConfiguration ( Configuration . sectionIdentifier ) ; this . announceBot = this . config . get < boolean > ( Settings . announceBot ) || true ; this . joinMessage = this . config . get < string > ( Settings . joinMessage ) || "" ; this . leaveMessage = this . config . get < string > ( Settings . leaveMessage ) || "" ; this . requiredBadges = this . config . get < string [ ] > ( Settings . requiredBadges ) || [ ] ; context . subscriptions . push ( workspace . onDidChangeConfiguration ( this . onDidChangeConfigurationHandler , this ) ) ; } public async connect ( ) { if ( keytar && this . config && ! this . isConnected ) { const accessToken = await keytar . getPassword ( KeytarKeys . service , KeytarKeys . account ) ; const login = await keytar . getPassword ( KeytarKeys . service , KeytarKeys . userLogin ) ; if ( accessToken && login ) { this . channel = this . config . get < string > ( Settings . channels ) || login ; const opts = { identity : { username : login , password : accessToken } , channels : this . channel . split ( ', ' ) . map ( c => c . trim ( ) ) } ; this . client = Client ( opts ) ; this . client . on ( 'connected' , this . onConnectedHandler . bind ( this ) ) ; this . client . on ( 'message' , this . onMessageHandler . bind ( this ) ) ; this . client . on ( 'join' , this . onJoinHandler . bind ( this ) ) ; const status = await this . client . connect ( ) ; this . _onChatClientConnected . fire ( true ) ; return status ; } } } public async disconnect ( ) { if ( this . isConnected ) { if ( this . announceBot && this . leaveMessage . length > 0 ) { await this . sendMessage ( this . leaveMessage ) ; } if ( this . client ) { await this . client . disconnect ( ) ; this . client = undefined ; } this . _onChatClientConnected . fire ( false ) ; } } public async dispose ( ) { await this . disconnect ( ) ; } private get isConnected ( ) : boolean { return this . client ? this . client . readyState ( ) === 'OPEN' : false ; } private async sendMessage ( message ) { if ( this . isConnected && this . client ) { await this . client . say ( this . channel , message ) ; } } private async onJoinHandler ( channel , username , self ) { if ( self && this . client && this . announceBot && this . joinMessage . length > 0 ) { this . log ( `Joined channel: ${ channel } as ${ username }` ) ; await this . sendMessage ( this . joinMessage ) ; } } private onConnectedHandler ( address , port ) { this . log ( `Connected chat client to ${ address } port ${ port }` ) ; } private async onMessageHandler ( channel , userState , message , self ) { this . log ( `Received '${ message }' from ${ userState [ "display-name" ] }` ) ; if ( self ) { return ; } if ( ! message ) { return ; } const badges = userState . badges as IBadges || { } ; badges . follower = await API . isUserFollowingChannel ( userState . id ! , channel ) === true ? '1' : '0' ; if ( this . requiredBadges . length > 0 && ! badges . broadcaster ) { const canContinue = this . requiredBadges . some ( badge => badges [ badge ] === '1' ) ; if ( ! canContinue ) { this . log ( LogLevel . Warning , `${ userState . username } does not have any of the required badges to use the highlight command.` ) ; return ; } } message = message . toLocaleLowerCase ( ) . trim ( ) ; if ( message . startsWith ( '!line' ) || message . startsWith ( '!highlight' ) ) { if ( message . length === 0 ) { this . sendMessage ( '💡 To use the !line command, use the following format: !line <number> --or-- multiple lines: !line <start>-<end> --or-- with a comment: !line <number> <comment>' ) ; return ; } this . _onChatClientMessageReceived . fire ( { userState , message } ) ; } } private async onDidChangeConfigurationHandler ( event ) { if ( event . affectsConfiguration ( Configuration . sectionIdentifier ) && this . isConnected ) { await this . disconnect ( ) ; await this . connect ( ) ; } } }