Compare commits
25 Commits
add-websoc
...
41317301bf
Author | SHA1 | Date | |
---|---|---|---|
41317301bf | |||
00a2eb63bc | |||
6c8f779294 | |||
ff5c7506ff | |||
5c51acc8d1 | |||
d4ee457d74 | |||
b8f6023029 | |||
ea5e783ee5 | |||
eb5282a50d | |||
9ee3663890 | |||
d85acd2179 | |||
856cc84208 | |||
ca4643092f | |||
b4b656f808 | |||
92f1366574 | |||
a64afa7e7b | |||
d63aa365e7 | |||
3759c5aa23 | |||
1a151b197b | |||
70180c5d5f | |||
dac037809c | |||
6088a2cbd3 | |||
ed8d148d0a | |||
379099dc7a | |||
c0ed38ac1a |
@ -1,9 +1,10 @@
|
|||||||
DATABASE_URL="file:../dev.db" # SQLite database relative to the ./prisma path
|
DATABASE_URL="file:../dev.db" # SQLite database relative to the ./prisma path
|
||||||
PLEROMA_INSTANCE_URL="https://instance.tld" # Pleroma instance full URL including scheme
|
PLEROMA_INSTANCE_URL="https://instance.tld" # Pleroma instance full URL including scheme
|
||||||
PLEROMA_INSTANCE_DOMAIN="instance.tld" # used if you want to only want to respond to people from a particular instance
|
PLEROMA_INSTANCE_DOMAIN="instance.tld" # used if you want to only want to respond to people from a particular instance
|
||||||
ONLY_LOCAL_REPLIES="true" # reply to only users locally on your instance
|
ONLY_WHITELIST="true" # change to "false" if you want to accept prompts from any and all domains - *** USE WITH CAUTION ***
|
||||||
|
WHITELISTED_DOMAINS="" # comma separated list of domains you want to allow the bot to accept prompts from (i.e. poa.st,nicecrew.digital,detroitriotcity.com,decayable.ink)
|
||||||
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
|
||||||
|
FETCH_INTERVAL="" # interval for fetching new notifications from the instance, in milliseconds, recommend at least 15000
|
||||||
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
|
|
10
README.md
10
README.md
@ -1,15 +1,15 @@
|
|||||||
## Pleroma -> Ollama Bot Setup
|
## Pleroma -> Ollama Bot Setup
|
||||||
|
|
||||||
1. Clone project
|
1. Clone project
|
||||||
2. Install npm 22.11.0 if you don't have it already
|
2. Install Node `v22.11.0` if you don't have it already
|
||||||
|
* If using `nvm`, just `nvm install 22.11.0` and then `nvm use 22.11.0` if necessary
|
||||||
3. `cd` into the project directory
|
3. `cd` into the project directory
|
||||||
4. Run `npm install`
|
4. Run `npm install`
|
||||||
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 start, run `npm run start`
|
||||||
8. To run continuously, use `npm run ws`
|
|
||||||
|
I recommend using `screen` to run this in the background until a `systemd` service can be created. I just haven't bothered to do it yet.
|
||||||
|
|
||||||
### Database Migrations
|
### Database Migrations
|
||||||
|
|
||||||
If you add stuff to the schema, follow the [Prisma development workflow](https://www.prisma.io/docs/orm/prisma-migrate/workflows/development-and-production). This will apply the new schema to the database and generate a new Prisma client with type safety.
|
If you add stuff to the schema, follow the [Prisma development workflow](https://www.prisma.io/docs/orm/prisma-migrate/workflows/development-and-production). This will apply the new schema to the database and generate a new Prisma client with type safety.
|
||||||
|
|
||||||
Setting as a system service will come at some point, or someone could contribute if they wanted.
|
|
37
package-lock.json
generated
37
package-lock.json
generated
@ -1,22 +1,21 @@
|
|||||||
{
|
{
|
||||||
"name": "pleroma-ollama-bot",
|
"name": "pleroma-ollama-bot",
|
||||||
"version": "1.0.0",
|
"version": "1.0.5",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "pleroma-ollama-bot",
|
"name": "pleroma-ollama-bot",
|
||||||
"version": "1.0.0",
|
"version": "1.0.5",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@prisma/client": "^6.10.1",
|
"@prisma/client": "^6.10.1",
|
||||||
"@types/node": "^24.0.5",
|
|
||||||
"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/node": "^24.0.10",
|
||||||
"@types/ws": "^8.18.1",
|
"@types/ws": "^8.18.1",
|
||||||
"prisma": "^6.10.1"
|
"prisma": "^6.10.1"
|
||||||
}
|
}
|
||||||
@ -165,10 +164,9 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@types/node": {
|
"node_modules/@types/node": {
|
||||||
"version": "24.0.5",
|
"version": "24.0.10",
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.0.10.tgz",
|
||||||
"integrity": "sha512-CXEG9E7GCTOZIre0WdDznmnhvF7xi7AmnP/zF496trmLiqlfdtxp9nPRgLVqfmJ8jgtcKcs0EcvOu2yDZSuvTg==",
|
"integrity": "sha512-ENHwaH+JIRTDIEEbDK6QSQntAYGtbvdDXnMXnZaZ6k13Du1dPMmprkEHIL7ok2Wl2aZevetwTAb5S+7yIF+enA==",
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"undici-types": "~7.8.0"
|
"undici-types": "~7.8.0"
|
||||||
}
|
}
|
||||||
@ -356,27 +354,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",
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "pleroma-ollama-bot",
|
"name": "pleroma-ollama-bot",
|
||||||
"version": "1.0.0",
|
"version": "1.0.7",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "tsc && node -r dotenv/config dist/main.js",
|
"start": "tsc && node -r dotenv/config dist/main.js",
|
||||||
@ -9,17 +9,16 @@
|
|||||||
"type": "module",
|
"type": "module",
|
||||||
"keywords": [],
|
"keywords": [],
|
||||||
"author": "NiceCrew",
|
"author": "NiceCrew",
|
||||||
"description": "A simple bot that responds to activities from Pleroma instances using Ollama's API.",
|
"description": "A simple bot that responds to activities from Pleroma instances using Ollama's API at a configurable interval.",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@prisma/client": "^6.10.1",
|
"@prisma/client": "^6.10.1",
|
||||||
"@types/node": "^24.0.5",
|
|
||||||
"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/node": "^24.0.10",
|
||||||
"@types/ws": "^8.18.1",
|
"@types/ws": "^8.18.1",
|
||||||
"prisma": "^6.10.1"
|
"prisma": "^6.10.1"
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,18 @@
|
|||||||
|
-- RedefineTables
|
||||||
|
PRAGMA defer_foreign_keys=ON;
|
||||||
|
PRAGMA foreign_keys=OFF;
|
||||||
|
CREATE TABLE "new_Response" (
|
||||||
|
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||||
|
"pleromaNotificationId" TEXT NOT NULL DEFAULT 'null',
|
||||||
|
"to" TEXT NOT NULL,
|
||||||
|
"request" TEXT,
|
||||||
|
"response" TEXT,
|
||||||
|
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"processedAt" DATETIME,
|
||||||
|
"isProcessing" BOOLEAN NOT NULL DEFAULT true
|
||||||
|
);
|
||||||
|
INSERT INTO "new_Response" ("createdAt", "id", "pleromaNotificationId", "processedAt", "request", "response", "to") SELECT "createdAt", "id", "pleromaNotificationId", "processedAt", "request", "response", "to" FROM "Response";
|
||||||
|
DROP TABLE "Response";
|
||||||
|
ALTER TABLE "new_Response" RENAME TO "Response";
|
||||||
|
PRAGMA foreign_keys=ON;
|
||||||
|
PRAGMA defer_foreign_keys=OFF;
|
@ -0,0 +1,18 @@
|
|||||||
|
-- RedefineTables
|
||||||
|
PRAGMA defer_foreign_keys=ON;
|
||||||
|
PRAGMA foreign_keys=OFF;
|
||||||
|
CREATE TABLE "new_Response" (
|
||||||
|
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||||
|
"pleromaNotificationId" TEXT NOT NULL DEFAULT 'null',
|
||||||
|
"to" TEXT NOT NULL DEFAULT 'null',
|
||||||
|
"request" TEXT NOT NULL DEFAULT 'null',
|
||||||
|
"response" TEXT NOT NULL DEFAULT 'null',
|
||||||
|
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"processedAt" DATETIME,
|
||||||
|
"isProcessing" BOOLEAN NOT NULL DEFAULT true
|
||||||
|
);
|
||||||
|
INSERT INTO "new_Response" ("createdAt", "id", "isProcessing", "pleromaNotificationId", "processedAt", "request", "response", "to") SELECT "createdAt", "id", "isProcessing", "pleromaNotificationId", "processedAt", coalesce("request", 'null') AS "request", coalesce("response", 'null') AS "response", "to" FROM "Response";
|
||||||
|
DROP TABLE "Response";
|
||||||
|
ALTER TABLE "new_Response" RENAME TO "Response";
|
||||||
|
PRAGMA foreign_keys=ON;
|
||||||
|
PRAGMA defer_foreign_keys=OFF;
|
@ -14,11 +14,12 @@ datasource db {
|
|||||||
model Response {
|
model Response {
|
||||||
id Int @id @default(autoincrement())
|
id Int @id @default(autoincrement())
|
||||||
pleromaNotificationId String @default("null")
|
pleromaNotificationId String @default("null")
|
||||||
to String
|
to String @default("null")
|
||||||
request String?
|
request String @default("null")
|
||||||
response String?
|
response String @default("null")
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
processedAt DateTime?
|
processedAt DateTime?
|
||||||
|
isProcessing Boolean @default(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
model User {
|
model User {
|
||||||
|
214
src/main.ts
214
src/main.ts
@ -3,35 +3,56 @@ import {
|
|||||||
OllamaResponse,
|
OllamaResponse,
|
||||||
NewStatusBody,
|
NewStatusBody,
|
||||||
Notification,
|
Notification,
|
||||||
WSEvent,
|
OllamaConfigOptions,
|
||||||
} 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 envConfig = {
|
||||||
// try {
|
pleromaInstanceUrl: process.env.PLEROMA_INSTANCE_URL || "",
|
||||||
// const request = await fetch(
|
pleromaInstanceDomain: process.env.PLEROMA_INSTANCE_DOMAIN || "",
|
||||||
// `${process.env.PLEROMA_INSTANCE_URL}/api/v1/notifications?types[]=mention`,
|
whitelistOnly: process.env.ONLY_WHITELIST === "true" ? true : false || "true",
|
||||||
// {
|
whitelistedDomains: process.env.WHITELISTED_DOMAINS
|
||||||
// method: "GET",
|
? process.env.WHITELISTED_DOMAINS.split(",")
|
||||||
// headers: {
|
: [process.env.PLEROMA_INSTANCE_DOMAIN],
|
||||||
// Authorization: `Bearer ${process.env.INSTANCE_BEARER_TOKEN}`,
|
ollamaUrl: process.env.OLLAMA_URL || "",
|
||||||
// },
|
ollamaSystemPrompt:
|
||||||
// }
|
process.env.OLLAMA_SYSTEM_PROMPT ||
|
||||||
// );
|
"You are a helpful AI assistant. Answer all questions concisely.",
|
||||||
|
ollamaModel: process.env.OLLAMA_MODEL || "",
|
||||||
|
fetchInterval: process.env.FETCH_INTERVAL
|
||||||
|
? parseInt(process.env.FETCH_INTERVAL)
|
||||||
|
: 15000,
|
||||||
|
bearerToken: process.env.INSTANCE_BEARER_TOKEN || "",
|
||||||
|
};
|
||||||
|
|
||||||
// const notifications: Notification[] = await request.json();
|
const ollamaConfig: OllamaConfigOptions = {
|
||||||
|
temperature: 0.6,
|
||||||
|
num_predict: 750,
|
||||||
|
};
|
||||||
|
|
||||||
// return notifications;
|
const getNotifications = async () => {
|
||||||
// } catch (error: any) {
|
const { bearerToken, pleromaInstanceUrl } = envConfig;
|
||||||
// throw new Error(error.message);
|
try {
|
||||||
// }
|
const request = await fetch(
|
||||||
// };
|
`${pleromaInstanceUrl}/api/v1/notifications?types[]=mention`,
|
||||||
|
{
|
||||||
|
method: "GET",
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${bearerToken}`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
// const notifications = await getNotifications();
|
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 {
|
||||||
@ -55,7 +76,7 @@ const alreadyRespondedTo = async (
|
|||||||
): Promise<boolean> => {
|
): Promise<boolean> => {
|
||||||
try {
|
try {
|
||||||
const duplicate = await prisma.response.findFirst({
|
const duplicate = await prisma.response.findFirst({
|
||||||
where: { pleromaNotificationId: notification.status.id },
|
where: { pleromaNotificationId: notification.id, isProcessing: true },
|
||||||
});
|
});
|
||||||
if (duplicate) {
|
if (duplicate) {
|
||||||
return true;
|
return true;
|
||||||
@ -71,12 +92,13 @@ const storePromptData = async (
|
|||||||
ollamaResponseBody: OllamaResponse
|
ollamaResponseBody: OllamaResponse
|
||||||
) => {
|
) => {
|
||||||
try {
|
try {
|
||||||
await prisma.response.create({
|
await prisma.response.updateMany({
|
||||||
|
where: { pleromaNotificationId: notification.id },
|
||||||
data: {
|
data: {
|
||||||
response: ollamaResponseBody.response,
|
response: ollamaResponseBody.response,
|
||||||
request: striptags(notification.status.content),
|
request: trimInputData(notification.status.content),
|
||||||
to: notification.account.fqn,
|
to: notification.account.fqn,
|
||||||
pleromaNotificationId: notification.status.id,
|
isProcessing: false,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
@ -84,42 +106,71 @@ const storePromptData = async (
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const trimInputData = (input: string) => {
|
const trimInputData = (input: string): string => {
|
||||||
const strippedInput = striptags(input);
|
const strippedInput = striptags(input);
|
||||||
const split = strippedInput.split(" ");
|
const split = strippedInput.split(" ");
|
||||||
const promptStringIndex = split.indexOf("!prompt");
|
const promptStringIndex = split.indexOf("!prompt");
|
||||||
return split.slice(promptStringIndex + 1).join(" "); // returns everything after the !prompt
|
split.splice(promptStringIndex, 1);
|
||||||
|
return split.join(" "); // returns everything after the !prompt
|
||||||
|
};
|
||||||
|
|
||||||
|
const recordPendingResponse = async (notification: Notification) => {
|
||||||
|
try {
|
||||||
|
await prisma.response.create({
|
||||||
|
data: {
|
||||||
|
pleromaNotificationId: notification.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (error: any) {
|
||||||
|
throw new Error(error.message);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const isFromWhitelistedDomain = (fqn: string): boolean => {
|
||||||
|
try {
|
||||||
|
const domain = fqn.split("@")[1];
|
||||||
|
if (envConfig.whitelistedDomains.includes(domain)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error(`Error with domain check: ${error.message}`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const generateOllamaRequest = async (
|
const generateOllamaRequest = async (
|
||||||
notification: Notification
|
notification: Notification
|
||||||
): Promise<OllamaResponse | undefined> => {
|
): Promise<OllamaResponse | undefined> => {
|
||||||
|
const { whitelistOnly, ollamaModel, ollamaSystemPrompt, ollamaUrl } =
|
||||||
|
envConfig;
|
||||||
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" &&
|
whitelistOnly &&
|
||||||
!notification.status.account.fqn.includes(
|
!isFromWhitelistedDomain(notification.status.account.fqn)
|
||||||
`@${process.env.PLEROMA_INSTANCE_DOMAIN}`
|
|
||||||
)
|
|
||||||
) {
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (await alreadyRespondedTo(notification)) {
|
if (await alreadyRespondedTo(notification)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
await recordPendingResponse(notification);
|
||||||
await storeUserData(notification);
|
await storeUserData(notification);
|
||||||
const ollamaRequestBody: OllamaRequest = {
|
const ollamaRequestBody: OllamaRequest = {
|
||||||
model: process.env.OLLAMA_MODEL as string,
|
model: ollamaModel,
|
||||||
system: process.env.OLLAMA_SYSTEM_PROMPT as string,
|
system: ollamaSystemPrompt,
|
||||||
prompt: `@${notification.status.account.fqn} says: ${trimInputData(
|
prompt: `@${notification.status.account.fqn} says: ${trimInputData(
|
||||||
notification.status.content
|
notification.status.content
|
||||||
)}`,
|
)}`,
|
||||||
stream: false,
|
stream: false,
|
||||||
|
options: ollamaConfig,
|
||||||
};
|
};
|
||||||
const response = await fetch(`${process.env.OLLAMA_URL}/api/generate`, {
|
const response = await fetch(`${ollamaUrl}/api/generate`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
body: JSON.stringify(ollamaRequestBody),
|
body: JSON.stringify(ollamaRequestBody),
|
||||||
});
|
});
|
||||||
@ -136,6 +187,7 @@ const postReplyToStatus = async (
|
|||||||
notification: Notification,
|
notification: Notification,
|
||||||
ollamaResponseBody: OllamaResponse
|
ollamaResponseBody: OllamaResponse
|
||||||
) => {
|
) => {
|
||||||
|
const { pleromaInstanceUrl, bearerToken } = envConfig;
|
||||||
try {
|
try {
|
||||||
let mentions: string[];
|
let mentions: string[];
|
||||||
const statusBody: NewStatusBody = {
|
const statusBody: NewStatusBody = {
|
||||||
@ -153,55 +205,79 @@ const postReplyToStatus = async (
|
|||||||
statusBody.to = mentions;
|
statusBody.to = mentions;
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await fetch(
|
const response = await fetch(`${pleromaInstanceUrl}/api/v1/statuses`, {
|
||||||
`${process.env.PLEROMA_INSTANCE_URL}/api/v1/statuses`,
|
|
||||||
{
|
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${process.env.INSTANCE_BEARER_TOKEN}`,
|
Authorization: `Bearer ${bearerToken}`,
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
},
|
},
|
||||||
body: JSON.stringify(statusBody),
|
body: JSON.stringify(statusBody),
|
||||||
}
|
});
|
||||||
);
|
|
||||||
|
|
||||||
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) {
|
||||||
|
throw new Error(error.message);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteNotification = async (notification: Notification) => {
|
||||||
|
const { pleromaInstanceUrl, bearerToken } = envConfig;
|
||||||
|
try {
|
||||||
|
if (!notification.id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await prisma.response.updateMany({
|
||||||
|
// this is probably not the best way to do this, but since we may have duplicate notifications, we have to update all of them - probably won't scale (lmao)
|
||||||
|
where: { pleromaNotificationId: notification.id },
|
||||||
|
data: { isProcessing: false },
|
||||||
|
});
|
||||||
|
const response = await fetch(
|
||||||
|
`${pleromaInstanceUrl}/api/v1/notifications/${notification.id}/dismiss`,
|
||||||
|
{
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${bearerToken}`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
if (!response.ok) {
|
||||||
|
console.error(
|
||||||
|
`Could not delete notification ID: ${notification.id}\nReason: ${response.status} - ${response.statusText}`
|
||||||
|
);
|
||||||
|
}
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
throw new Error(error.message);
|
throw new Error(error.message);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const ws = createWebsocket();
|
let notifications = [];
|
||||||
|
const beginFetchCycle = async () => {
|
||||||
ws.on("upgrade", () => {
|
setInterval(async () => {
|
||||||
console.log(
|
notifications = await getNotifications();
|
||||||
`Websocket connection to ${process.env.PLEROMA_INSTANCE_DOMAIN} successful.`
|
if (notifications.length > 0) {
|
||||||
);
|
await Promise.all(
|
||||||
});
|
notifications.map(async (notification) => {
|
||||||
|
try {
|
||||||
ws.on("message", async (data) => {
|
const ollamaResponse = await generateOllamaRequest(notification);
|
||||||
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) {
|
if (ollamaResponse) {
|
||||||
await postReplyToStatus(payload, ollamaResponse);
|
postReplyToStatus(notification, ollamaResponse);
|
||||||
}
|
}
|
||||||
});
|
} catch (error: any) {
|
||||||
|
throw new Error(error.message);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}, envConfig.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
|
||||||
|
};
|
||||||
|
|
||||||
// if (notifications) {
|
console.log(
|
||||||
// await Promise.all(
|
`Fetching notifications from ${envConfig.pleromaInstanceDomain}, every ${
|
||||||
// notifications.map(async (notification) => {
|
envConfig.fetchInterval / 1000
|
||||||
// const ollamaResponse = await generateOllamaRequest(notification);
|
} seconds.`
|
||||||
// if (ollamaResponse) {
|
);
|
||||||
// postReplyToStatus(notification, ollamaResponse);
|
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);
|
|
||||||
}
|
|
||||||
};
|
|
57
types.d.ts
vendored
57
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 {
|
||||||
@ -37,7 +40,11 @@ export interface OllamaRequest {
|
|||||||
/**
|
/**
|
||||||
* Whether to stream responses from the API, or have it sent all as one payload.
|
* Whether to stream responses from the API, or have it sent all as one payload.
|
||||||
*/
|
*/
|
||||||
stream?: boolean = false; // stream response vs get response in one full message
|
stream?: boolean = false;
|
||||||
|
/**
|
||||||
|
* Ollama configuration options
|
||||||
|
*/
|
||||||
|
options?: OllamaConfigOptions;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface OllamaResponse {
|
export interface OllamaResponse {
|
||||||
@ -65,8 +72,48 @@ export interface Mention {
|
|||||||
username: string;
|
username: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface WSEvent {
|
/**
|
||||||
event: "update" | "status.update" | "notification";
|
* Experimental settings, I wouldn't recommend messing with these if you don't know how they work (I don't either)
|
||||||
payload: string;
|
*/
|
||||||
stream: "user" | "direct";
|
export interface OllamaConfigOptions {
|
||||||
|
/**
|
||||||
|
* Number of tokens guaranteed to be kept in memory during response generation. Higher values leave less
|
||||||
|
* possible room for num_ctx
|
||||||
|
*/
|
||||||
|
num_keep?: number;
|
||||||
|
seed?: number;
|
||||||
|
/**
|
||||||
|
* Sets maximum of tokens in the response
|
||||||
|
*/
|
||||||
|
num_predict?: number;
|
||||||
|
top_k?: number;
|
||||||
|
top_p?: number;
|
||||||
|
min_p?: number;
|
||||||
|
typical_p?: number;
|
||||||
|
repeat_last_n?: number;
|
||||||
|
/**
|
||||||
|
* How close of a response should the response be to the original prompt - lower = more focused response
|
||||||
|
*/
|
||||||
|
temperature?: number;
|
||||||
|
repeat_penalty?: number;
|
||||||
|
presence_penalty?: number;
|
||||||
|
frequency_penalty?: number;
|
||||||
|
mirostat?: number;
|
||||||
|
mirostat_tau?: number;
|
||||||
|
mirostat_eta?: number;
|
||||||
|
penalize_newline?: boolean;
|
||||||
|
stop?: string[];
|
||||||
|
numa?: boolean;
|
||||||
|
/**
|
||||||
|
* Number of tokens for the prompt to keep in memory for the response, minus the value of num_keep
|
||||||
|
*/
|
||||||
|
num_ctx?: number;
|
||||||
|
num_batch?: number;
|
||||||
|
num_gpu?: number;
|
||||||
|
main_gpu?: number;
|
||||||
|
low_vram?: boolean;
|
||||||
|
vocab_only?: boolean;
|
||||||
|
use_mmap?: boolean;
|
||||||
|
use_mlock?: boolean;
|
||||||
|
num_thread?: number;
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user