wafrn instance bluesky labeler (mirror of https://git.jbc.lol/jbcrn/wadge)
at main 327 lines 12 kB view raw
1import 'dotenv/config'; 2import { stripHtml } from "string-strip-html"; 3import { LabelerServer } from "@skyware/labeler"; 4import { declareLabeler, getLabelerLabelDefinitions } from '@skyware/labeler/scripts'; 5import WebSocket from 'ws'; 6import { CronJob } from 'cron'; 7 8const server = new LabelerServer({ 9 did: process.env.LABELER_DID ?? '', 10 signingKey: process.env.SIGNING_KEY ?? '' 11}); 12 13const credentials = { 14 pds: process.env.BSKY_PDS ?? '', 15 identifier: process.env.LABELER_DID ?? '', 16 password: process.env.WAFRN_PASSWORD ?? '' 17} 18 19export class WafrnApi { 20 _token = ''; 21 _instance = ''; 22 _recentPosts = []; 23 24 constructor() { } 25 26 async init(server, email, password) { 27 this._instance = server; 28 const res = await fetch(`https://${server}/api/login`, { 29 method: "POST", 30 headers: { 31 "Content-Type": "application/json" 32 }, 33 body: JSON.stringify({ 34 email: email, 35 password: password 36 }) 37 }) 38 if (!res.ok) { 39 throw new Error('Could not sign in, ' + await res.text()); 40 } 41 const json = await res.json(); 42 if (!json.success || !res.ok) { 43 throw new Error('Could not sign in, ' + await res.text()); 44 } 45 46 this._token = json.token; 47 this.loadWs(); 48 49 new CronJob( 50 '*/1 * * * *', 51 () => { 52 this.checkIfMatches() 53 }, 54 null, 55 true, 56 'America/Los_Angeles' 57 ); 58 } 59 60 loadWs() { 61 const ws = new WebSocket(`wss://${this._instance}/api/notifications/socket`); 62 ws.on('open', () => { 63 ws.send(JSON.stringify({ 64 type: "auth", 65 object: this._token 66 })) 67 console.log('connected'); 68 }) 69 70 ws.on('message', async (msg) => { 71 const message = msg.toString(); 72 const msgJson = JSON.parse(message); 73 console.log(msgJson); 74 75 if (msgJson.type === "MENTION") { 76 setTimeout(() => this.checkIfMatches(), 15000); 77 } 78 }) 79 80 ws.on('close', () => { 81 console.log('reconnecting in 10s...'); 82 setTimeout(() => { this.loadWs(); }, 10000); 83 }) 84 } 85 86 async checkIfMatches() { 87 const notifs = await this.getNotifs(); 88 const firstNotif = notifs.notifications[0]; 89 if (this._recentPosts.length === 0) this._recentPosts.push(`${firstNotif.userId}${firstNotif.postId}`); 90 if (this._recentPosts.includes(`${firstNotif.userId}${firstNotif.postId}`)) return; 91 92 for (let i = 0; i < notifs.notifications.length; i++) { 93 console.log(this._recentPosts); 94 const thisNotif = notifs.notifications[i]; 95 if (this._recentPosts.includes(`${thisNotif.userId}${thisNotif.postId}`)) return; 96 else this._recentPosts.push(`${thisNotif.userId}${thisNotif.postId}`); 97 const postNotif = await this.getPost(thisNotif.postId); 98 99 const mdContent = postNotif.posts[0].markdownContent ?? stripHtml(postNotif.posts[0].content).result; 100 const contSpl = mdContent.split(' '); 101 const mention = contSpl[0]; 102 const rest = '' + contSpl.slice(1).join(' ').toLowerCase(); 103 const restSpl = rest.split(' '); 104 const command = '' + restSpl[0]; 105 const args = '' + restSpl.slice(1).join(' ').toLowerCase(); 106 107 let user = thisNotif.user.url; 108 let u2 = user; 109 if (!user.startsWith('@')) user = '@' + user; 110 if (!user.replace(/^\@/, '').includes('@')) user = user + '@wf.jbc.lol' 111 112 console.log(contSpl, mention, rest); 113 console.log(postNotif); 114 console.log(thisNotif); 115 116 if (postNotif.posts[0].privacy !== 10) { 117 return; 118 } 119 120 console.log(mention, command, args); 121 122 if (command === 'is-wafrn') { 123 const ifWafrn = await this.checkIfWafrn(!!args ? args : user); 124 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); 125 126 console.log(`sent post in woot ${postNotif.posts[0].id}`); 127 return; 128 } else if (command === 'setup') { 129 const ifWafrn = await this.checkIfWafrn(user); 130 if (!ifWafrn.isWafrn) { 131 await this.send("", 'You are not in a Wafrn instance.', postNotif.posts[0].privacy, "", postNotif.users.map(t => t.id), postNotif.posts[0].id); 132 return; 133 } 134 135 const res = await fetch(`https://${ifWafrn.instHost}/api/user?id=${ifWafrn.name}`, { 136 method: 'GET' 137 }) 138 139 if (!res.ok) { 140 console.log(await res.text()); 141 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); 142 return; 143 } 144 145 const resJson = await res.json(); 146 console.log(resJson); 147 const bskyDid = await resJson.bskyDid; 148 149 if (!bskyDid) { 150 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); 151 return; 152 } 153 154 function onlyUnique(value, index, array) { 155 return array.indexOf(value) === index; 156 } 157 158 try { 159 const extLabels = await getLabelerLabelDefinitions(credentials); 160 if (!extLabels.find(x => x.identifier === ifWafrn.instHost?.toLowerCase().replaceAll('.', '-'))) { 161 const labels = [ 162 ...(extLabels ?? []), 163 { 164 identifier: ifWafrn.instHost?.toLowerCase().replaceAll('.', '-'), 165 name: ifWafrn.instHost, 166 description: `Main account of this Bluesky user is on ${ifWafrn.instHost} Wafrn instance`, 167 adultOnly: false, 168 severity: 'inform', 169 blurs: "none", 170 defaultSetting: "warn", 171 locales: [{ lang: "en", name: ifWafrn.instHost, description: `Main account of this Bluesky user is on ${ifWafrn.instHost} Wafrn instance` }] 172 } 173 ] 174 175 await declareLabeler( 176 credentials, 177 labels.filter(onlyUnique), 178 true 179 ) 180 } 181 182 let label = await server.createLabel({ 183 uri: bskyDid, 184 val: ifWafrn.instHost?.toLowerCase().replaceAll('.', '-') 185 }) 186 187 console.log(label); 188 189 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); 190 console.log('added user to list'); 191 return; 192 } catch (e) { 193 console.error(e); 194 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); 195 return; 196 } 197 } 198 199 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); 200 } 201 202 } 203 204 async saveMembers() { 205 writeFileSync('./members.json', JSON.stringify(WAFRING_JSON)); 206 } 207 208 async checkIfWafrn(uname) { 209 const nameSpl = uname.split('@'); 210 const prov = nameSpl[2] ?? 'wf.jbc.lol'; 211 console.log(prov) 212 213 const nfRes = await fetch(`https://${prov}/.well-known/nodeinfo`); 214 if (!nfRes.ok) return { 215 isWafrn: false, 216 httpError: true 217 }; 218 219 const link = await nfRes.json(); 220 console.log(link); 221 const nfInst = await fetch(link.links[0].href); 222 if (!nfInst.ok) return { 223 isWafrn: false, 224 httpError: true 225 }; 226 227 const nfJson = await nfInst.json(); 228 if (nfJson.software.name.toLowerCase() !== 'wafrn') return { 229 isWafrn: false, 230 instHost: prov, 231 name: nameSpl[1], 232 software: nfJson.software, 233 metadata: nfJson.metadata 234 }; 235 236 return { 237 isWafrn: true, 238 instHost: prov, 239 name: nameSpl[1], 240 software: nfJson.software, 241 metadata: nfJson.metadata 242 }; 243 } 244 245 async send(username, html, type = 10, warning = '', mentions = [], parent = '') { 246 if (!this._token) { 247 throw new Error('Please log in first'); 248 } 249 250 const res = await fetch(`https://${this._instance}/api/v3/createPost`, { 251 method: "POST", 252 headers: { 253 "Content-Type": "application/json", 254 Authorization: `Bearer ${this._token}` 255 }, 256 body: JSON.stringify({ 257 content: `${!!username ? `@${username}\n\n` : ''}${html}`, 258 "content_warning": warning, 259 medias: [], 260 mentionedUserIds: mentions, 261 parent: parent, 262 privacy: type, 263 tags: "" 264 }) 265 }) 266 if (!res.ok) { 267 throw new Error('Could not create dm'); 268 } 269 } 270 271 async getNotifs() { 272 const res = await fetch(`https://${this._instance}/api/v3/notificationsScroll?date=${new Date().getTime()}&page=0`, { 273 method: 'GET', 274 headers: { 275 Authorization: `Bearer ${this._token}` 276 }, 277 }); 278 279 if (!res.ok) { 280 throw new Error('Could not get notifs'); 281 } 282 283 const resJson = await res.json(); 284 return resJson; 285 } 286 287 async getPost(postId) { 288 const res = await fetch(`https://${this._instance}/api/v2/post/${postId}`, { 289 method: 'GET', 290 headers: { 291 Authorization: `Bearer ${this._token}` 292 }, 293 }); 294 295 const text = await res.text(); 296 297 if (!res.ok || !text) { 298 throw new Error('Could not get post, ' + text); 299 } 300 301 let resJson = JSON.parse(text); 302 return resJson; 303 } 304} 305 306const w = new WafrnApi(); 307w.init(process.env.WAFRN_INSTANCE ?? 'app.wafrn.net', process.env.WAFRN_EMAIL ?? '', process.env.WAFRN_PASSWORD ?? ''); 308 309server.start(2343, (error, address) => { 310 if (error) { 311 console.error("Failed to start server:", error); 312 } else { 313 console.log("Labeler server running on port", address); 314 } 315 316 if (!!process.env.LABEL_ITSELF) { 317 setTimeout(async () => { 318 let label = await server.createLabel({ 319 uri: process.env.LABELER_DID, 320 val: process.env.WAFRN_INSTANCE?.toLowerCase().replaceAll('.', '-') 321 }) 322 323 console.log('done'); 324 }, 2000) 325 } 326}); 327