wafrn instance bluesky labeler (mirror of https://git.jbc.lol/jbcrn/wadge)
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