Compare commits

..

4 Commits

Author SHA1 Message Date
b295777041 add websocket functionality 2025-07-01 15:25:00 -04:00
9145b07da7 trim input data for better results 2025-06-30 21:09:37 -04:00
593aa09a18 add dependencies 2025-06-30 20:39:16 -04:00
acddefe1e8 linting 2025-06-30 17:22:49 -04:00
8 changed files with 131 additions and 35 deletions

View File

@ -6,3 +6,4 @@ OLLAMA_URL="http://localhost:11434" # OLLAMA connection URL
OLLAMA_SYSTEM_PROMPT="" # system prompt - used to help tune the responses from the AI OLLAMA_SYSTEM_PROMPT="" # system prompt - used to help tune the responses from the AI
OLLAMA_MODEL="" # Ollama model for responses e.g dolphin-mistral:latest OLLAMA_MODEL="" # Ollama model for responses e.g dolphin-mistral:latest
INSTANCE_BEARER_TOKEN="" # instance auth/bearer token (check the "verify_credentials" endpoint request headers in Chrome DevTools if on Soapbox) INSTANCE_BEARER_TOKEN="" # instance auth/bearer token (check the "verify_credentials" endpoint request headers in Chrome DevTools if on Soapbox)
SOAPBOX_WS_PROTOCOL="" # this is the header required to authenticate to the websocket. No idea why Soapbox does it like this. You can get it in the request headers for the socket in Chrome DevTools

View File

@ -4,9 +4,9 @@
2. Install npm 22.11.0 if you don't have it already 2. Install npm 22.11.0 if you don't have it already
3. `cd` into the project directory 3. `cd` into the project directory
4. Run `npm install` 4. Run `npm install`
5. Run `npx prisma init --datasource-provider sqlite --output ../generated/prisma`
6. Run `npx prisma migrate dev --name init` 6. Run `npx prisma migrate dev --name init`
7. To run the software on a cronjob, use `npm run once` 7. To run the software on a cronjob, use `npm run once`
8. To run continuously, use `npm run ws`
### Database Migrations ### Database Migrations

35
package-lock.json generated
View File

@ -13,9 +13,11 @@
"dotenv": "^17.0.0", "dotenv": "^17.0.0",
"striptags": "^3.2.0", "striptags": "^3.2.0",
"ts-node": "^10.9.2", "ts-node": "^10.9.2",
"typescript": "^5.8.3" "typescript": "^5.8.3",
"ws": "^8.18.3"
}, },
"devDependencies": { "devDependencies": {
"@types/ws": "^8.18.1",
"prisma": "^6.10.1" "prisma": "^6.10.1"
} }
}, },
@ -171,6 +173,16 @@
"undici-types": "~7.8.0" "undici-types": "~7.8.0"
} }
}, },
"node_modules/@types/ws": {
"version": "8.18.1",
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz",
"integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/node": "*"
}
},
"node_modules/acorn": { "node_modules/acorn": {
"version": "8.15.0", "version": "8.15.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
@ -344,6 +356,27 @@
"integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/ws": {
"version": "8.18.3",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz",
"integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==",
"license": "MIT",
"engines": {
"node": ">=10.0.0"
},
"peerDependencies": {
"bufferutil": "^4.0.1",
"utf-8-validate": ">=5.0.2"
},
"peerDependenciesMeta": {
"bufferutil": {
"optional": true
},
"utf-8-validate": {
"optional": true
}
}
},
"node_modules/yn": { "node_modules/yn": {
"version": "3.1.1", "version": "3.1.1",
"resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz",

View File

@ -3,7 +3,7 @@
"version": "1.0.0", "version": "1.0.0",
"main": "index.js", "main": "index.js",
"scripts": { "scripts": {
"once": "tsc && node -r dotenv/config dist/main.js", "start": "tsc && node -r dotenv/config dist/main.js",
"build": "tsc" "build": "tsc"
}, },
"type": "module", "type": "module",
@ -16,9 +16,11 @@
"dotenv": "^17.0.0", "dotenv": "^17.0.0",
"striptags": "^3.2.0", "striptags": "^3.2.0",
"ts-node": "^10.9.2", "ts-node": "^10.9.2",
"typescript": "^5.8.3" "typescript": "^5.8.3",
"ws": "^8.18.3"
}, },
"devDependencies": { "devDependencies": {
"@types/ws": "^8.18.1",
"prisma": "^6.10.1" "prisma": "^6.10.1"
} }
} }

View File

@ -3,37 +3,39 @@ 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";
const prisma = new PrismaClient(); const prisma = new PrismaClient();
const getNotifications = async () => { // const getNotifications = async () => {
try { // try {
const request = await fetch( // const request = await fetch(
`${process.env.PLEROMA_INSTANCE_URL}/api/v1/notifications?types[]=mention`, // `${process.env.PLEROMA_INSTANCE_URL}/api/v1/notifications?types[]=mention`,
{ // {
method: "GET", // method: "GET",
headers: { // headers: {
Authorization: `Bearer ${process.env.INSTANCE_BEARER_TOKEN}`, // Authorization: `Bearer ${process.env.INSTANCE_BEARER_TOKEN}`,
}, // },
} // }
); // );
const notifications: Notification[] = await request.json(); // const notifications: Notification[] = await request.json();
return notifications; // return notifications;
} catch (error: any) { // } catch (error: any) {
throw new Error(error.message); // throw new Error(error.message);
} // }
}; // };
const notifications = await getNotifications(); // const notifications = await getNotifications();
const storeUserData = async (notification: Notification): Promise<void> => { const storeUserData = async (notification: Notification): Promise<void> => {
try { try {
const user = await prisma.user.upsert({ await prisma.user.upsert({
where: { userFqn: notification.status.account.fqn }, where: { userFqn: notification.status.account.fqn },
update: { update: {
lastRespondedTo: new Date(Date.now()), lastRespondedTo: new Date(Date.now()),
@ -82,6 +84,13 @@ const storePromptData = async (
} }
}; };
const trimInputData = (input: string) => {
const strippedInput = striptags(input);
const split = strippedInput.split(" ");
const promptStringIndex = split.indexOf("!prompt");
return split.slice(promptStringIndex + 1).join(" "); // returns everything after the !prompt
};
const generateOllamaRequest = async ( const generateOllamaRequest = async (
notification: Notification notification: Notification
): Promise<OllamaResponse | undefined> => { ): Promise<OllamaResponse | undefined> => {
@ -105,7 +114,7 @@ const generateOllamaRequest = async (
const ollamaRequestBody: OllamaRequest = { const ollamaRequestBody: OllamaRequest = {
model: process.env.OLLAMA_MODEL as string, model: process.env.OLLAMA_MODEL as string,
system: process.env.OLLAMA_SYSTEM_PROMPT as string, system: process.env.OLLAMA_SYSTEM_PROMPT as string,
prompt: `@${notification.status.account.fqn} says: ${striptags( prompt: `@${notification.status.account.fqn} says: ${trimInputData(
notification.status.content notification.status.content
)}`, )}`,
stream: false, stream: false,
@ -116,7 +125,6 @@ const generateOllamaRequest = async (
}); });
const ollamaResponse: OllamaResponse = await response.json(); const ollamaResponse: OllamaResponse = await response.json();
await storePromptData(notification, ollamaResponse); await storePromptData(notification, ollamaResponse);
// await postReplyToStatus(notification, ollamaResponse);
return ollamaResponse; return ollamaResponse;
} }
} catch (error: any) { } catch (error: any) {
@ -165,13 +173,35 @@ const postReplyToStatus = async (
} }
}; };
if (notifications) { const ws = createWebsocket();
await Promise.all(
notifications.map(async (notification) => { ws.on("upgrade", () => {
const ollamaResponse = await generateOllamaRequest(notification); console.log(
if (ollamaResponse) { `Websocket connection to ${process.env.PLEROMA_INSTANCE_DOMAIN} successful.`
postReplyToStatus(notification, ollamaResponse);
}
})
); );
});
ws.on("message", async (data) => {
const message: WSEvent = JSON.parse(data.toString("utf-8"));
if (message.event !== "notification") {
// only watch for notification events
return;
} }
console.log("Websocket message received.");
const payload = JSON.parse(message.payload) as Notification;
const ollamaResponse = await generateOllamaRequest(payload);
if (ollamaResponse) {
await postReplyToStatus(payload, ollamaResponse);
}
});
// if (notifications) {
// await Promise.all(
// notifications.map(async (notification) => {
// const ollamaResponse = await generateOllamaRequest(notification);
// if (ollamaResponse) {
// postReplyToStatus(notification, ollamaResponse);
// }
// })
// );
// }

22
src/websocket.ts Normal file
View File

@ -0,0 +1,22 @@
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);
}
};

View File

@ -7,6 +7,8 @@
"outDir": "./dist", "outDir": "./dist",
"rootDir": "./src", "rootDir": "./src",
"strict": true, "strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"forceConsistentCasingInFileNames": true, "forceConsistentCasingInFileNames": true,
"skipLibCheck": true, "skipLibCheck": true,
"resolveJsonModule": true "resolveJsonModule": true

6
types.d.ts vendored
View File

@ -64,3 +64,9 @@ export interface Mention {
url: string; url: string;
username: string; username: string;
} }
export interface WSEvent {
event: "update" | "status.update" | "notification";
payload: string;
stream: "user" | "direct";
}