import 'dotenv/config'; import { stripHtml } from "string-strip-html"; import { LabelerServer } from "@skyware/labeler"; import { declareLabeler, getLabelerLabelDefinitions } from '@skyware/labeler/scripts'; import WebSocket from 'ws'; import { CronJob } from 'cron'; const server = new LabelerServer({ did: process.env.LABELER_DID ?? '', signingKey: process.env.SIGNING_KEY ?? '' }); const credentials = { pds: process.env.BSKY_PDS ?? '', identifier: process.env.LABELER_DID ?? '', password: process.env.WAFRN_PASSWORD ?? '' } export class WafrnApi { _token = ''; _instance = ''; _recentPosts = []; constructor() { } async init(server, email, password) { this._instance = server; const res = await fetch(`https://${server}/api/login`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ email: email, password: password }) }) if (!res.ok) { throw new Error('Could not sign in, ' + await res.text()); } const json = await res.json(); if (!json.success || !res.ok) { throw new Error('Could not sign in, ' + await res.text()); } this._token = json.token; this.loadWs(); new CronJob( '*/1 * * * *', () => { this.checkIfMatches() }, null, true, 'America/Los_Angeles' ); } loadWs() { const ws = new WebSocket(`wss://${this._instance}/api/notifications/socket`); ws.on('open', () => { ws.send(JSON.stringify({ type: "auth", object: this._token })) console.log('connected'); }) ws.on('message', async (msg) => { const message = msg.toString(); const msgJson = JSON.parse(message); console.log(msgJson); if (msgJson.type === "MENTION") { setTimeout(() => this.checkIfMatches(), 15000); } }) ws.on('close', () => { console.log('reconnecting in 10s...'); setTimeout(() => { this.loadWs(); }, 10000); }) } async checkIfMatches() { const notifs = await this.getNotifs(); const firstNotif = notifs.notifications[0]; if (this._recentPosts.length === 0) this._recentPosts.push(`${firstNotif.userId}${firstNotif.postId}`); if (this._recentPosts.includes(`${firstNotif.userId}${firstNotif.postId}`)) return; for (let i = 0; i < notifs.notifications.length; i++) { console.log(this._recentPosts); const thisNotif = notifs.notifications[i]; if (this._recentPosts.includes(`${thisNotif.userId}${thisNotif.postId}`)) return; else this._recentPosts.push(`${thisNotif.userId}${thisNotif.postId}`); const postNotif = await this.getPost(thisNotif.postId); const mdContent = postNotif.posts[0].markdownContent ?? stripHtml(postNotif.posts[0].content).result; const contSpl = mdContent.split(' '); const mention = contSpl[0]; const rest = '' + contSpl.slice(1).join(' ').toLowerCase(); const restSpl = rest.split(' '); const command = '' + restSpl[0]; const args = '' + restSpl.slice(1).join(' ').toLowerCase(); let user = thisNotif.user.url; let u2 = user; if (!user.startsWith('@')) user = '@' + user; if (!user.replace(/^\@/, '').includes('@')) user = user + '@wf.jbc.lol' console.log(contSpl, mention, rest); console.log(postNotif); console.log(thisNotif); if (postNotif.posts[0].privacy !== 10) { return; } console.log(mention, command, args); if (command === 'is-wafrn') { const ifWafrn = await this.checkIfWafrn(!!args ? args : user); await this.send("", `${!!args ? args + ' is' : 'You are'} ${ifWafrn.isWafrn ? 'in' : 'not in'} a Wafrn instance (${ifWafrn.httpError ? 'either not a Fediverse instance or HTTP/fetch error getting nodeinfo, try again later' : `${ifWafrn.software.name} ${ifWafrn.software.version}`})`, postNotif.posts[0].privacy, "", postNotif.users.map(t => t.id), postNotif.posts[0].id); console.log(`sent post in woot ${postNotif.posts[0].id}`); return; } else if (command === 'setup') { const ifWafrn = await this.checkIfWafrn(user); if (!ifWafrn.isWafrn) { await this.send("", 'You are not in a Wafrn instance.', postNotif.posts[0].privacy, "", postNotif.users.map(t => t.id), postNotif.posts[0].id); return; } const res = await fetch(`https://${ifWafrn.instHost}/api/user?id=${ifWafrn.name}`, { method: 'GET' }) if (!res.ok) { console.log(await res.text()); await this.send("", 'There is an error while getting your user info. You might want to wait a bit. You also need to make sure your profile is visible to public as it\'s the only way Wadge can detect it, sadly, but you can just reinstate it later.', postNotif.posts[0].privacy, "", postNotif.users.map(t => t.id), postNotif.posts[0].id); return; } const resJson = await res.json(); console.log(resJson); const bskyDid = await resJson.bskyDid; if (!bskyDid) { await this.send("", `You don't have Bluesky enabled. Enable it on https://${ifWafrn.instHost}/settings/account or import your existing one on https://${ifWafrn.instHost}/profile/migrate-bluesky`, postNotif.posts[0].privacy, "", postNotif.users.map(t => t.id), postNotif.posts[0].id); return; } function onlyUnique(value, index, array) { return array.indexOf(value) === index; } try { const extLabels = await getLabelerLabelDefinitions(credentials); if (!extLabels.find(x => x.identifier === ifWafrn.instHost?.toLowerCase().replaceAll('.', '-'))) { const labels = [ ...(extLabels ?? []), { identifier: ifWafrn.instHost?.toLowerCase().replaceAll('.', '-'), name: ifWafrn.instHost, description: `Main account of this Bluesky user is on ${ifWafrn.instHost} Wafrn instance`, adultOnly: false, severity: 'inform', blurs: "none", defaultSetting: "warn", locales: [{ lang: "en", name: ifWafrn.instHost, description: `Main account of this Bluesky user is on ${ifWafrn.instHost} Wafrn instance` }] } ] await declareLabeler( credentials, labels.filter(onlyUnique), true ) } let label = await server.createLabel({ uri: bskyDid, val: ifWafrn.instHost?.toLowerCase().replaceAll('.', '-') }) console.log(label); await this.send("", `Done! Added your instance on your account label list! Next up is to like the bot to actually see it!`, postNotif.posts[0].privacy, "", postNotif.users.map(t => t.id), postNotif.posts[0].id); console.log('added user to list'); return; } catch (e) { console.error(e); await this.send("", `An error occured, pinging @jbcrn\n\`\`\`\n${e}\n\`\`\``, postNotif.posts[0].privacy, "", postNotif.users.map(t => t.id), postNotif.posts[0].id); return; } } await this.send("", 'Set up by mentioning this bot and adding `setup`!', postNotif.posts[0].privacy, "", postNotif.users.map(t => t.id), postNotif.posts[0].id); } } async saveMembers() { writeFileSync('./members.json', JSON.stringify(WAFRING_JSON)); } async checkIfWafrn(uname) { const nameSpl = uname.split('@'); const prov = nameSpl[2] ?? 'wf.jbc.lol'; console.log(prov) const nfRes = await fetch(`https://${prov}/.well-known/nodeinfo`); if (!nfRes.ok) return { isWafrn: false, httpError: true }; const link = await nfRes.json(); console.log(link); const nfInst = await fetch(link.links[0].href); if (!nfInst.ok) return { isWafrn: false, httpError: true }; const nfJson = await nfInst.json(); if (nfJson.software.name.toLowerCase() !== 'wafrn') return { isWafrn: false, instHost: prov, name: nameSpl[1], software: nfJson.software, metadata: nfJson.metadata }; return { isWafrn: true, instHost: prov, name: nameSpl[1], software: nfJson.software, metadata: nfJson.metadata }; } async send(username, html, type = 10, warning = '', mentions = [], parent = '') { if (!this._token) { throw new Error('Please log in first'); } const res = await fetch(`https://${this._instance}/api/v3/createPost`, { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${this._token}` }, body: JSON.stringify({ content: `${!!username ? `@${username}\n\n` : ''}${html}`, "content_warning": warning, medias: [], mentionedUserIds: mentions, parent: parent, privacy: type, tags: "" }) }) if (!res.ok) { throw new Error('Could not create dm'); } } async getNotifs() { const res = await fetch(`https://${this._instance}/api/v3/notificationsScroll?date=${new Date().getTime()}&page=0`, { method: 'GET', headers: { Authorization: `Bearer ${this._token}` }, }); if (!res.ok) { throw new Error('Could not get notifs'); } const resJson = await res.json(); return resJson; } async getPost(postId) { const res = await fetch(`https://${this._instance}/api/v2/post/${postId}`, { method: 'GET', headers: { Authorization: `Bearer ${this._token}` }, }); const text = await res.text(); if (!res.ok || !text) { throw new Error('Could not get post, ' + text); } let resJson = JSON.parse(text); return resJson; } } const w = new WafrnApi(); w.init(process.env.WAFRN_INSTANCE ?? 'app.wafrn.net', process.env.WAFRN_EMAIL ?? '', process.env.WAFRN_PASSWORD ?? ''); server.start(2343, (error, address) => { if (error) { console.error("Failed to start server:", error); } else { console.log("Labeler server running on port", address); } if (!!process.env.LABEL_ITSELF) { setTimeout(async () => { let label = await server.createLabel({ uri: process.env.LABELER_DID, val: process.env.WAFRN_INSTANCE?.toLowerCase().replaceAll('.', '-') }) console.log('done'); }, 2000) } });