more robust notification type safety, remove websocket, add fetch interval
This commit is contained in:
		
							
								
								
									
										145
									
								
								src/main.ts
									
									
									
									
									
								
							
							
						
						
									
										145
									
								
								src/main.ts
									
									
									
									
									
								
							| @ -3,15 +3,32 @@ import { | |||||||
|   OllamaResponse, |   OllamaResponse, | ||||||
|   NewStatusBody, |   NewStatusBody, | ||||||
|   Notification, |   Notification, | ||||||
|   WSEvent, |  | ||||||
| } from "../types.js"; | } from "../types.js"; | ||||||
| import striptags from "striptags"; | import striptags from "striptags"; | ||||||
| import { PrismaClient } from "../generated/prisma/client.js"; | import { PrismaClient } from "../generated/prisma/client.js"; | ||||||
| import { createWebsocket } from "./websocket.js"; |  | ||||||
| import { WebSocket } from "ws"; |  | ||||||
|  |  | ||||||
| const prisma = new PrismaClient(); | const prisma = new PrismaClient(); | ||||||
|  |  | ||||||
|  | const getNotifications = async () => { | ||||||
|  |   try { | ||||||
|  |     const request = await fetch( | ||||||
|  |       `${process.env.PLEROMA_INSTANCE_URL}/api/v1/notifications?types[]=mention`, | ||||||
|  |       { | ||||||
|  |         method: "GET", | ||||||
|  |         headers: { | ||||||
|  |           Authorization: `Bearer ${process.env.INSTANCE_BEARER_TOKEN}`, | ||||||
|  |         }, | ||||||
|  |       } | ||||||
|  |     ); | ||||||
|  |  | ||||||
|  |     const notifications: Notification[] = await request.json(); | ||||||
|  |  | ||||||
|  |     return notifications; | ||||||
|  |   } catch (error: any) { | ||||||
|  |     throw new Error(error.message); | ||||||
|  |   } | ||||||
|  | }; | ||||||
|  |  | ||||||
| const storeUserData = async (notification: Notification): Promise<void> => { | const storeUserData = async (notification: Notification): Promise<void> => { | ||||||
|   try { |   try { | ||||||
|     await prisma.user.upsert({ |     await prisma.user.upsert({ | ||||||
| @ -76,7 +93,8 @@ const generateOllamaRequest = async ( | |||||||
|   try { |   try { | ||||||
|     if ( |     if ( | ||||||
|       striptags(notification.status.content).includes("!prompt") && |       striptags(notification.status.content).includes("!prompt") && | ||||||
|       !notification.status.account.bot |       !notification.status.account.bot && // sanity check, sort of | ||||||
|  |       notification.type === "mention" | ||||||
|     ) { |     ) { | ||||||
|       if ( |       if ( | ||||||
|         process.env.ONLY_LOCAL_REPLIES === "true" && |         process.env.ONLY_LOCAL_REPLIES === "true" && | ||||||
| @ -147,86 +165,63 @@ const postReplyToStatus = async ( | |||||||
|     if (!response.ok) { |     if (!response.ok) { | ||||||
|       throw new Error(`New status request failed: ${response.statusText}`); |       throw new Error(`New status request failed: ${response.statusText}`); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     await deleteNotification(notification); | ||||||
|   } catch (error: any) { |   } catch (error: any) { | ||||||
|     throw new Error(error.message); |     throw new Error(error.message); | ||||||
|   } |   } | ||||||
| }; | }; | ||||||
|  |  | ||||||
| let ws = createWebsocket(); | const deleteNotification = async (notification: Notification) => { | ||||||
| let reconnectAttempts = 0; |  | ||||||
| const maxReconnectAttempts = 10; |  | ||||||
| const baseDelay = 5000; |  | ||||||
|  |  | ||||||
| const reconnect = (ws: WebSocket) => { |  | ||||||
|   if (ws) { |  | ||||||
|     ws.close(); |  | ||||||
|   } |  | ||||||
|   return createWebsocket(); |  | ||||||
| }; |  | ||||||
|  |  | ||||||
| ws.on("close", (event: CloseEvent) => { |  | ||||||
|   try { |   try { | ||||||
|     if (reconnectAttempts < maxReconnectAttempts) { |     if (!notification.id) { | ||||||
|       const delay = baseDelay * Math.pow(1.5, reconnectAttempts); |  | ||||||
|       console.log( |  | ||||||
|         `WebSocket closed.\nReason: ${ |  | ||||||
|           event.reason |  | ||||||
|         }\nAttempting to reconnect in ${delay / 1000} seconds...` |  | ||||||
|       ); |  | ||||||
|  |  | ||||||
|       setTimeout(() => { |  | ||||||
|         console.log( |  | ||||||
|           `Reconnection attempt ${ |  | ||||||
|             reconnectAttempts + 1 |  | ||||||
|           }/${maxReconnectAttempts}` |  | ||||||
|         ); |  | ||||||
|         ws = reconnect(ws); |  | ||||||
|         if (ws.readyState === WebSocket.OPEN) { |  | ||||||
|           console.log(`Reconnection to ${process.env.PLEROMA_INSTANCE_DOMAIN} successful.`) |  | ||||||
|           return; |  | ||||||
|         } |  | ||||||
|         reconnectAttempts++; |  | ||||||
|       }, delay); |  | ||||||
|     } else { |  | ||||||
|       console.error( |  | ||||||
|         `Failed to reconnect after ${maxReconnectAttempts} attempts. Giving up.` |  | ||||||
|       ); |  | ||||||
|     } |  | ||||||
|   } catch (error: any) { |  | ||||||
|     console.error(`Reconnection error: ${error.message}`); |  | ||||||
|     throw new Error(error.message); |  | ||||||
|   } |  | ||||||
| }); |  | ||||||
|  |  | ||||||
| ws.on("upgrade", () => { |  | ||||||
|   console.log( |  | ||||||
|     `Websocket connection to ${process.env.PLEROMA_INSTANCE_DOMAIN} successful.` |  | ||||||
|   ); |  | ||||||
| }); |  | ||||||
|  |  | ||||||
| ws.on("open", () => { |  | ||||||
|   reconnectAttempts = 0; |  | ||||||
|   setInterval(() => { |  | ||||||
|     ws.send(JSON.stringify({ type: "ping" })); |  | ||||||
|     // console.log("Sending ping to keep session alive..."); |  | ||||||
|   }, 20000); |  | ||||||
| }); |  | ||||||
|  |  | ||||||
|  |  | ||||||
| ws.on("message", async (data) => { |  | ||||||
|   try { |  | ||||||
|     const message: WSEvent = JSON.parse(data.toString("utf-8")); |  | ||||||
|     if (message.event !== "notification") { |  | ||||||
|       // only watch for notification events |  | ||||||
|       return; |       return; | ||||||
|     } |     } | ||||||
|     console.log("Websocket message received."); |     const response = await fetch( | ||||||
|     const payload = JSON.parse(message.payload) as Notification; |       `${process.env.PLEROMA_INSTANCE_URL}/api/v1/notifications/${notification.id}/dismiss`, | ||||||
|     const ollamaResponse = await generateOllamaRequest(payload); |       { | ||||||
|     if (ollamaResponse) { |         method: "POST", | ||||||
|       await postReplyToStatus(payload, ollamaResponse); |         headers: { | ||||||
|  |           Authorization: `Bearer ${process.env.INSTANCE_BEARER_TOKEN}`, | ||||||
|  |         }, | ||||||
|  |       } | ||||||
|  |     ); | ||||||
|  |     if (!response.ok) { | ||||||
|  |       console.error(`Could not delete notification ID: ${notification.id}`); | ||||||
|     } |     } | ||||||
|   } catch (error: any) { |   } catch (error: any) { | ||||||
|     throw new Error(error.message); |     throw new Error(error.message); | ||||||
|   } |   } | ||||||
| }); | }; | ||||||
|  |  | ||||||
|  | const fetchInterval = process.env.FETCH_INTERVAL | ||||||
|  |   ? parseInt(process.env.FETCH_INTERVAL) | ||||||
|  |   : 15000; | ||||||
|  |  | ||||||
|  | const beginFetchCycle = async () => { | ||||||
|  |   let notifications = []; | ||||||
|  |   setInterval(async () => { | ||||||
|  |     notifications = await getNotifications(); | ||||||
|  |     if (notifications.length > 0) { | ||||||
|  |       await Promise.all( | ||||||
|  |         notifications.map(async (notification) => { | ||||||
|  |           try { | ||||||
|  |             const ollamaResponse = await generateOllamaRequest(notification); | ||||||
|  |             if (ollamaResponse) { | ||||||
|  |               postReplyToStatus(notification, ollamaResponse); | ||||||
|  |             } | ||||||
|  |           } catch (error: any) { | ||||||
|  |             throw new Error(error.message); | ||||||
|  |           } | ||||||
|  |         }) | ||||||
|  |       ); | ||||||
|  |     } | ||||||
|  |   }, fetchInterval); // lower intervals may cause the bot to respond multiple times to the same message, but we try to mitigate this with the deleteNotification function | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | console.log( | ||||||
|  |   `Fetching notifications from ${process.env.PLEROMA_INSTANCE_DOMAIN}, every ${ | ||||||
|  |     fetchInterval / 1000 | ||||||
|  |   } seconds.` | ||||||
|  | ); | ||||||
|  | await beginFetchCycle(); | ||||||
|  | |||||||
| @ -1,22 +0,0 @@ | |||||||
| import { WebSocket } from "ws"; |  | ||||||
|  |  | ||||||
| const scheme = process.env.PLEROMA_INSTANCE_URL?.startsWith("https") |  | ||||||
|   ? "wss" |  | ||||||
|   : "ws"; // this is so nigger rigged |  | ||||||
| const host = process.env.PLEROMA_INSTANCE_DOMAIN; |  | ||||||
|  |  | ||||||
| export const createWebsocket = (): WebSocket => { |  | ||||||
|   try { |  | ||||||
|     const ws = new WebSocket( // only connects to Soapbox frontends right now, but could pretty easily connect to Pleroma frontends with some tweaking |  | ||||||
|       `${scheme}://${host}/api/v1/streaming?stream=user`, |  | ||||||
|       [process.env.SOAPBOX_WS_PROTOCOL as string], |  | ||||||
|       { |  | ||||||
|         followRedirects: true, |  | ||||||
|       } |  | ||||||
|     ); |  | ||||||
|  |  | ||||||
|     return ws; |  | ||||||
|   } catch (error: any) { |  | ||||||
|     throw new Error(error); |  | ||||||
|   } |  | ||||||
| }; |  | ||||||
							
								
								
									
										3
									
								
								types.d.ts
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										3
									
								
								types.d.ts
									
									
									
									
										vendored
									
									
								
							| @ -1,6 +1,9 @@ | |||||||
| export interface Notification { | export interface Notification { | ||||||
|   account: Account; |   account: Account; | ||||||
|   status: Status; |   status: Status; | ||||||
|  |   id: string; | ||||||
|  |   type: string; | ||||||
|  |   created_at: string; | ||||||
| } | } | ||||||
|  |  | ||||||
| export interface NewStatusBody { | export interface NewStatusBody { | ||||||
|  | |||||||
		Reference in New Issue
	
	Block a user