Compare commits
5 Commits
a64afa7e7b
...
d85acd2179
Author | SHA1 | Date | |
---|---|---|---|
d85acd2179 | |||
856cc84208 | |||
ca4643092f | |||
b4b656f808 | |||
92f1366574 |
@ -5,5 +5,5 @@ ONLY_LOCAL_REPLIES="true" # reply to only users locally on your instance
|
|||||||
OLLAMA_URL="http://localhost:11434" # OLLAMA connection URL
|
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)
|
FETCH_INTERVAL="" # interval for fetching new notifications from the instance, in milliseconds, recommend at least 15000
|
||||||
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
|
INSTANCE_BEARER_TOKEN="" # instance auth/bearer token (check the "verify_credentials" endpoint request headers in Chrome DevTools if on Soapbox)
|
24
package-lock.json
generated
24
package-lock.json
generated
@ -13,8 +13,7 @@
|
|||||||
"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",
|
"@types/ws": "^8.18.1",
|
||||||
@ -356,27 +355,6 @@
|
|||||||
"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",
|
||||||
|
@ -16,8 +16,7 @@
|
|||||||
"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",
|
"@types/ws": "^8.18.1",
|
||||||
|
149
src/main.ts
149
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,88 +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) {
|
|
||||||
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("pong", (data) => {
|
|
||||||
console.log(`Pong received: ${JSON.stringify(data.toString("utf-8"))}`);
|
|
||||||
});
|
|
||||||
|
|
||||||
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) {
|
||||||
console.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