Created
July 20, 2024 00:04
-
-
Save feelfreetofee/fbd089cc90cba24a5a974cd27c868df2 to your computer and use it in GitHub Desktop.
Chatbot working on Browser and Bun
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| export class Twitch { | |
| #event = new EventTarget | |
| on(...data) { | |
| this.#event.addEventListener(...data) | |
| } | |
| removeListener(...data) { | |
| this.#event.removeEventListener(...data) | |
| } | |
| #capabilities = [] | |
| #username | |
| get username() { | |
| return this.#username | |
| } | |
| #password | |
| #channels = [] | |
| get channels() { | |
| return this.#channels | |
| } | |
| join(channel) { | |
| if (!this.#channels.includes(channel)) | |
| return this.ws.send(`JOIN #${channel}`) | |
| } | |
| leave(channel) { | |
| if (this.#channels.includes(channel)) | |
| return this.ws.send(`PART #${channel}`) | |
| } | |
| send(channel, message, id) { | |
| if (this.#channels.includes(channel)) | |
| this.ws.send(`PRIVMSG #${channel} :${message}`) | |
| // this.ws.send(`@reply-parent-msg-id=${id} PRIVMSG #${channel} :${message}`) | |
| } | |
| constructor(options) { | |
| this.debug = options.debug | |
| // TODO: Only store username and password, if one missing switch to anonymous mode | |
| if (!('username' in options)) | |
| options.username = `justinfan${(''+Math.random()).slice(2)}` | |
| else if (typeof options.username !== 'string') | |
| return console.error('username must be a string') | |
| this.#username = options.username | |
| if ('password' in options) | |
| this.#password = options.password | |
| if ('capabilities' in options && options.capabilities instanceof Array) { | |
| for (const capability of options.capabilities) | |
| if (typeof capability === 'string') | |
| this.#capabilities.push(capability) | |
| this.on('CAP', e => e.detail.arguments === '* ACK' && (this.#capabilities = e.detail.parameters.split(' ').map(s => s.slice(10)))) | |
| } | |
| this.on('PING', e => this.ws.send(`PONG :${e.detail.parameters}`) || this.debug && console.log('PONG', e.detail.parameters)) | |
| this.on('JOIN', e => { | |
| console.log(e) | |
| if (e.detail.source === this.#username) | |
| return this.#channels.push(e.detail.channel) | |
| }) | |
| this.on('PART', e => { | |
| if (e.detail.source === this.#username) | |
| return this.#channels.splice(this.#channels.indexOf(e.detail.channel), 1) | |
| }) | |
| this.#connect() | |
| } | |
| #authorize() { | |
| this.ws.send(`CAP REQ :${this.#capabilities.map(cap => `twitch.tv/${cap}`).join(' ')}`) | |
| if (this.#password) | |
| this.ws.send(`PASS oauth:${this.#password}`) | |
| this.ws.send(`NICK ${this.#username}`) | |
| for (const channel of this.#channels) | |
| this.join(channel) | |
| } | |
| #connect() { | |
| this.ws = Object.assign(new WebSocket('wss://irc-ws.chat.twitch.tv'), { | |
| onopen: e => this.#authorize(), | |
| onmessage: e => { | |
| if (this.debug) | |
| console.log(e.data) | |
| for (const message of e.data.matchAll(/(?:@(.+?) )?(?::(?:(.+?)[!@.])?.*tmi\.twitch\.tv )?(?:(.+?) )(?:([^#]+?)[ =]{1,3})?(?:#([^:].+?))? ?(?::(.+))?(?=\r\n|$)/g)) { | |
| const detail = {} | |
| if (message[1]) { | |
| detail.tags = {} | |
| for (const tag of message[1].matchAll(/(.+?)=([^;]*)(?:;|$)/g)) | |
| tag[2] && (detail.tags[tag[1]] = tag[2]) | |
| } | |
| if (message[2]) | |
| detail.source = message[2] | |
| if (message[4]) | |
| detail.arguments = message[4] | |
| if (message[5]) | |
| detail.channel = message[5] | |
| if (message[6]) | |
| detail.parameters = message[6] | |
| this.#event.dispatchEvent(new CustomEvent(message[3], {'detail': detail})) | |
| } | |
| }, | |
| // TODO: Handle disconnection | |
| onclose: e => { | |
| console.log('close', e) | |
| // this.ws = this.#connect() | |
| }, | |
| onerror: e => { | |
| console.log('error', e) | |
| } | |
| }) | |
| } | |
| } | |
| export class Auth { | |
| #client_id | |
| #scopes = [] | |
| #_token | |
| get #token() { | |
| return this.#_token || (this.#_token = localStorage.getItem(`twitch_token:${this.#client_id}`)) | |
| } | |
| set #token(token) { | |
| localStorage[token ? 'setItem' : 'removeItem'](`twitch_token:${this.#client_id}`, this.#_token = token) | |
| } | |
| #checkToken() { | |
| return this.validateToken(this.#token)?.then(r => r || (this.#token = undefined)) | |
| } | |
| validateToken = token => token && fetch('https://id.twitch.tv/oauth2/validate', { | |
| headers: { | |
| authorization: `Bearer ${token}` | |
| } | |
| }).then(r => r.ok && r.json()).then(r => r && (r.token = token) && r) | |
| async implicit() { | |
| let token = await this.#checkToken() | |
| if (token) | |
| return token | |
| else if (token = new URLSearchParams(window.location.hash.slice(1)).get('access_token')) | |
| return history.replaceState(null, '', window.location.pathname) || this.validateToken(this.#token = token) | |
| else | |
| location.href = `https://id.twitch.tv/oauth2/authorize?response_type=token&redirect_uri=${window.origin}&client_id=${this.#client_id}&scope=${this.#scopes.join('+')}` | |
| } | |
| constructor(options) { | |
| if (!('client_id' in options)) | |
| return console.error('client_id is required') | |
| else if (typeof options.client_id !== 'string') | |
| return console.error('client_id must be a string') | |
| this.#client_id = options.client_id | |
| if (!('scopes' in options)) | |
| return console.error('scopes are required') | |
| else if (!(options.scopes instanceof Array)) | |
| return console.error('scopes must be an array of strings') | |
| for (const scope of options.scopes) | |
| if (typeof scope === 'string') | |
| this.#scopes.push(scope) | |
| } | |
| } |
Author
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
https://dev.twitch.tv/docs/irc/