Skip to content

Instantly share code, notes, and snippets.

@feelfreetofee
Created July 20, 2024 00:04
Show Gist options
  • Select an option

  • Save feelfreetofee/fbd089cc90cba24a5a974cd27c868df2 to your computer and use it in GitHub Desktop.

Select an option

Save feelfreetofee/fbd089cc90cba24a5a974cd27c868df2 to your computer and use it in GitHub Desktop.
Chatbot working on Browser and Bun
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)
}
}
@feelfreetofee
Copy link
Author

https://dev.twitch.tv/docs/irc/

new Auth({
	client_id: 'changeme', // https://dev.twitch.tv/console
	scopes: [
		'chat:read',
		'chat:edit'
	]
}).implicit().then(r => {
	const client = new Twitch({
		debug: true,
		username: r.login,
		password: r.token,
		capabilities: [
			'commands',
			'membership',
			'tags'
		]
	})
	
	client.on('001', e => client.join('feelfreetoffee'))
	client.on('JOIN', e => client.send(e.detail.channel, 'Hello World!'))

	client.on('PRIVMSG', console.log)
})

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment