miru/src/lib/modules/irc.ts
2025-05-22 16:04:25 +02:00

120 lines
3.7 KiB
TypeScript

import Client, { createChannelConstructor } from '@thaunknown/web-irc'
import type IrcChannel from '@thaunknown/web-irc/channel'
import { EventEmitter } from 'events'
import { writable } from 'simple-store-svelte'
import { decryptMessage, encryptMessage } from './crypt'
import type { ChatMessage, ChatUser } from '$lib/components/ui/chat'
export interface IRCUser { nick: string, ident: string, hostname: string, modes: string[], tags: object }
export interface PrivMessage {
from_server: boolean
nick: string
ident: string
hostname: string
target: string
message: string
outgoing?: boolean
tags: {
msgid: string
time: string
}
time: number
}
export default class MessageClient extends EventEmitter {
irc = new Client(null)
users = writable<Record<string, ChatUser>>({})
messages = writable<ChatMessage[]>([])
channel?: IrcChannel
ident
constructor (ident: ChatUser) {
super()
this.ident = ident
this.irc.on('userlist', async ({ users }: { users: IRCUser[] }) => {
this.users.value = users.reduce((acc, user) => {
const [nick, pfpid, pfpex] = user.nick.split('_')
const [type, id] = user.ident.split('_')
acc[user.ident] = { nick, id, pfpid: `${pfpid}.${pfpex}`, type: type as 'al' | 'guest' }
return acc
}, this.users.value)
})
this.irc.on('join', async (user: IRCUser) => {
try {
const [nick, pfpid, pfpex] = user.nick.split('_')
const [type, id] = user.ident.split('_')
this.users.value[user.ident] = { nick, id, pfpid: `${pfpid}.${pfpex}`, type: type as 'al' | 'guest' }
this.users.update(users => users)
} catch (error) {
console.error(error)
}
})
const deleteUser = (user: IRCUser) => {
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
delete this.users.value[user.ident]
this.users.update(users => users)
}
this.irc.on('quit', deleteUser)
this.irc.on('part', deleteUser)
this.irc.on('kick', deleteUser)
this.irc.on('privmsg', async (priv: PrivMessage) => {
const message = await decryptMessage(priv.message)
try {
this.messages.update(messages => [...messages, {
message,
user: this.users.value[priv.ident],
type: 'incoming',
date: new Date(priv.time)
}])
} catch (e) {
// some1 sent a message without encryption
console.error(e)
}
})
}
async say (message: string) {
const encrypted = await encryptMessage(message)
this.channel!.say(encrypted)
this.messages.update(messages => [...messages, {
user: this.ident,
message,
date: new Date(),
type: 'outgoing'
}])
}
static async new ({ nick, id, pfpid, type }: ChatUser) {
const client = new this({ nick, id, pfpid, type })
await new Promise(resolve => {
client.irc.once('connected', resolve)
client.irc.connect({
version: null,
enable_chghost: true,
enable_setname: true,
message_max_length: 350,
host: 'irc.swiftirc.net',
port: 5004,
tls: true,
path: '',
password: '',
account: {},
nick: `${nick.replaceAll('_', '')}_${pfpid.replace('.', '_')}`,
username: `${type}_${id}`,
gecos: 'https://kiwiirc.com/',
encoding: 'utf8',
auto_reconnect: true,
transport: createChannelConstructor('http://do-e.clients.kiwiirc.com/webirc/kiwiirc/', '', '1') // this people are dumb enough to not refresh the ssl cert so don't use https
})
})
await new Promise(resolve => {
client.irc.once('join', resolve)
client.channel = client.irc.channel('#4e63ad91532eb8849330')
})
return client
}
}