Compare commits
18 Commits
eb5282a50d
...
main
Author | SHA1 | Date | |
---|---|---|---|
c3d4f1b1ff | |||
57ab59d342 | |||
71ae54930c | |||
3466a984ac | |||
cbf6b1d3eb | |||
e2ce397118 | |||
9a7cd118b3 | |||
7a60a672d4 | |||
419285487a | |||
09722507c6 | |||
41317301bf | |||
00a2eb63bc | |||
6c8f779294 | |||
ff5c7506ff | |||
5c51acc8d1 | |||
d4ee457d74 | |||
b8f6023029 | |||
ea5e783ee5 |
@ -1,7 +1,8 @@
|
|||||||
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
|
||||||
|
13
package-lock.json
generated
13
package-lock.json
generated
@ -1,21 +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"
|
||||||
},
|
},
|
||||||
"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"
|
||||||
}
|
}
|
||||||
@ -164,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"
|
||||||
}
|
}
|
||||||
|
@ -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,16 +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"
|
||||||
},
|
},
|
||||||
"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;
|
@ -0,0 +1,19 @@
|
|||||||
|
-- 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,
|
||||||
|
"isComplete" BOOLEAN NOT NULL DEFAULT true
|
||||||
|
);
|
||||||
|
INSERT INTO "new_Response" ("createdAt", "id", "isProcessing", "pleromaNotificationId", "processedAt", "request", "response", "to") SELECT "createdAt", "id", "isProcessing", "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;
|
@ -14,11 +14,13 @@ 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)
|
||||||
|
isComplete Boolean @default(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
model User {
|
model User {
|
||||||
|
75
src/api.ts
Normal file
75
src/api.ts
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
import { envConfig, prisma } from "./main.js";
|
||||||
|
import { PleromaEmoji, Notification } from "../types.js";
|
||||||
|
|
||||||
|
const getNotifications = async () => {
|
||||||
|
const { bearerToken, pleromaInstanceUrl } = envConfig;
|
||||||
|
try {
|
||||||
|
const request = await fetch(
|
||||||
|
`${pleromaInstanceUrl}/api/v1/notifications?types[]=mention`,
|
||||||
|
{
|
||||||
|
method: "GET",
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${bearerToken}`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const notifications: Notification[] = await request.json();
|
||||||
|
|
||||||
|
return notifications;
|
||||||
|
} catch (error: any) {
|
||||||
|
throw new Error(error.message);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getInstanceEmojis = async () => {
|
||||||
|
const { bearerToken, pleromaInstanceUrl } = envConfig;
|
||||||
|
try {
|
||||||
|
const request = await fetch(`${pleromaInstanceUrl}/api/v1/pleroma/emoji`, {
|
||||||
|
method: "GET",
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${bearerToken}`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (!request.ok) {
|
||||||
|
console.error(`Emoji GET failed: ${request.status}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const emojis: PleromaEmoji[] = await request.json();
|
||||||
|
return Object.keys(emojis);
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error(`Could not fetch emojis: ${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) {
|
||||||
|
throw new Error(error.message);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export { deleteNotification, getInstanceEmojis, getNotifications };
|
197
src/main.ts
197
src/main.ts
@ -3,120 +3,84 @@ import {
|
|||||||
OllamaResponse,
|
OllamaResponse,
|
||||||
NewStatusBody,
|
NewStatusBody,
|
||||||
Notification,
|
Notification,
|
||||||
|
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 {
|
||||||
|
getInstanceEmojis,
|
||||||
|
deleteNotification,
|
||||||
|
getNotifications,
|
||||||
|
} from "./api.js";
|
||||||
|
import { storeUserData, storePromptData } from "./prisma.js";
|
||||||
|
import {
|
||||||
|
isFromWhitelistedDomain,
|
||||||
|
alreadyRespondedTo,
|
||||||
|
recordPendingResponse,
|
||||||
|
trimInputData,
|
||||||
|
selectRandomEmoji,
|
||||||
|
} from "./util.js";
|
||||||
|
|
||||||
const prisma = new PrismaClient();
|
export const prisma = new PrismaClient();
|
||||||
|
|
||||||
const getNotifications = async () => {
|
export 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,
|
||||||
{
|
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 || "",
|
||||||
const notifications: Notification[] = await request.json();
|
fetchInterval: process.env.FETCH_INTERVAL
|
||||||
|
? parseInt(process.env.FETCH_INTERVAL)
|
||||||
return notifications;
|
: 15000,
|
||||||
} catch (error: any) {
|
bearerToken: process.env.INSTANCE_BEARER_TOKEN || "",
|
||||||
throw new Error(error.message);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const storeUserData = async (notification: Notification): Promise<void> => {
|
const ollamaConfig: OllamaConfigOptions = {
|
||||||
try {
|
temperature: 1.4,
|
||||||
await prisma.user.upsert({
|
top_k: 100,
|
||||||
where: { userFqn: notification.status.account.fqn },
|
top_p: 0.8,
|
||||||
update: {
|
|
||||||
lastRespondedTo: new Date(Date.now()),
|
|
||||||
},
|
|
||||||
create: {
|
|
||||||
userFqn: notification.status.account.fqn,
|
|
||||||
lastRespondedTo: new Date(Date.now()),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
} catch (error: any) {
|
|
||||||
throw new Error(error.message);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const alreadyRespondedTo = async (
|
// this could be helpful
|
||||||
notification: Notification
|
// https://replicate.com/blog/how-to-prompt-llama
|
||||||
): Promise<boolean> => {
|
|
||||||
try {
|
|
||||||
const duplicate = await prisma.response.findFirst({
|
|
||||||
where: { pleromaNotificationId: notification.status.id },
|
|
||||||
});
|
|
||||||
if (duplicate) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
} catch (error: any) {
|
|
||||||
throw new Error(error.message);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const storePromptData = async (
|
|
||||||
notification: Notification,
|
|
||||||
ollamaResponseBody: OllamaResponse
|
|
||||||
) => {
|
|
||||||
try {
|
|
||||||
await prisma.response.create({
|
|
||||||
data: {
|
|
||||||
response: ollamaResponseBody.response,
|
|
||||||
request: striptags(notification.status.content),
|
|
||||||
to: notification.account.fqn,
|
|
||||||
pleromaNotificationId: notification.status.id,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
} catch (error: any) {
|
|
||||||
throw new Error(error.message);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
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> => {
|
||||||
|
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 && // sanity check, sort of
|
!notification.status.account.bot && // sanity check, sort of
|
||||||
notification.type === "mention"
|
notification.type === "mention" &&
|
||||||
) {
|
notification.status.visibility !== "private" // for safety, let's only respond to public messages
|
||||||
if (
|
|
||||||
process.env.ONLY_LOCAL_REPLIES === "true" &&
|
|
||||||
!notification.status.account.fqn.includes(
|
|
||||||
`@${process.env.PLEROMA_INSTANCE_DOMAIN}`
|
|
||||||
)
|
|
||||||
) {
|
) {
|
||||||
|
if (whitelistOnly && !isFromWhitelistedDomain(notification)) {
|
||||||
|
await deleteNotification(notification);
|
||||||
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: `[INST] @${
|
||||||
notification.status.content
|
notification.status.account.fqn
|
||||||
)}`,
|
} says: ${trimInputData(notification.status.content)} [/INST]`,
|
||||||
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),
|
||||||
});
|
});
|
||||||
@ -133,11 +97,17 @@ const postReplyToStatus = async (
|
|||||||
notification: Notification,
|
notification: Notification,
|
||||||
ollamaResponseBody: OllamaResponse
|
ollamaResponseBody: OllamaResponse
|
||||||
) => {
|
) => {
|
||||||
|
const { pleromaInstanceUrl, bearerToken } = envConfig;
|
||||||
|
const emojiList = await getInstanceEmojis();
|
||||||
|
let randomEmoji;
|
||||||
|
if (emojiList) {
|
||||||
|
randomEmoji = selectRandomEmoji(emojiList);
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
let mentions: string[];
|
let mentions: string[];
|
||||||
const statusBody: NewStatusBody = {
|
const statusBody: NewStatusBody = {
|
||||||
content_type: "text/markdown",
|
content_type: "text/markdown",
|
||||||
status: ollamaResponseBody.response,
|
status: `${ollamaResponseBody.response} :${randomEmoji}:`,
|
||||||
in_reply_to_id: notification.status.id,
|
in_reply_to_id: notification.status.id,
|
||||||
};
|
};
|
||||||
if (
|
if (
|
||||||
@ -150,17 +120,14 @@ 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}`);
|
||||||
@ -172,34 +139,6 @@ const postReplyToStatus = async (
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const deleteNotification = async (notification: Notification) => {
|
|
||||||
try {
|
|
||||||
if (!notification.id) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const response = await fetch(
|
|
||||||
`${process.env.PLEROMA_INSTANCE_URL}/api/v1/notifications/${notification.id}/dismiss`,
|
|
||||||
{
|
|
||||||
method: "POST",
|
|
||||||
headers: {
|
|
||||||
Authorization: `Bearer ${process.env.INSTANCE_BEARER_TOKEN}`,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
if (!response.ok) {
|
|
||||||
console.error(
|
|
||||||
`Could not delete notification ID: ${notification.id}\nReason: ${response.status} - ${response.statusText}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} catch (error: any) {
|
|
||||||
throw new Error(error.message);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const fetchInterval = process.env.FETCH_INTERVAL
|
|
||||||
? parseInt(process.env.FETCH_INTERVAL)
|
|
||||||
: 15000;
|
|
||||||
|
|
||||||
let notifications = [];
|
let notifications = [];
|
||||||
const beginFetchCycle = async () => {
|
const beginFetchCycle = async () => {
|
||||||
setInterval(async () => {
|
setInterval(async () => {
|
||||||
@ -218,12 +157,20 @@ const beginFetchCycle = async () => {
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}, 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
|
}, 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
|
||||||
};
|
};
|
||||||
|
|
||||||
console.log(
|
console.log(
|
||||||
`Fetching notifications from ${process.env.PLEROMA_INSTANCE_DOMAIN}, every ${
|
`Fetching notifications from ${envConfig.pleromaInstanceDomain}, every ${
|
||||||
fetchInterval / 1000
|
envConfig.fetchInterval / 1000
|
||||||
} seconds.`
|
} seconds.`
|
||||||
);
|
);
|
||||||
|
console.log(
|
||||||
|
`Accepting prompts from: ${envConfig.whitelistedDomains.join(", ")}`
|
||||||
|
);
|
||||||
|
console.log(
|
||||||
|
`Using model: ${envConfig.ollamaModel}\nConfig: ${JSON.stringify(
|
||||||
|
ollamaConfig
|
||||||
|
)}`
|
||||||
|
);
|
||||||
await beginFetchCycle();
|
await beginFetchCycle();
|
||||||
|
42
src/prisma.ts
Normal file
42
src/prisma.ts
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
import { Notification, OllamaResponse } from "../types.js";
|
||||||
|
import { trimInputData } from "./util.js";
|
||||||
|
import { prisma } from "./main.js";
|
||||||
|
|
||||||
|
const storePromptData = async (
|
||||||
|
notification: Notification,
|
||||||
|
ollamaResponseBody: OllamaResponse
|
||||||
|
) => {
|
||||||
|
try {
|
||||||
|
await prisma.response.updateMany({
|
||||||
|
where: { pleromaNotificationId: notification.id },
|
||||||
|
data: {
|
||||||
|
response: ollamaResponseBody.response,
|
||||||
|
request: trimInputData(notification.status.content),
|
||||||
|
to: notification.account.fqn,
|
||||||
|
isProcessing: false,
|
||||||
|
isComplete: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (error: any) {
|
||||||
|
throw new Error(error.message);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const storeUserData = async (notification: Notification): Promise<void> => {
|
||||||
|
try {
|
||||||
|
await prisma.user.upsert({
|
||||||
|
where: { userFqn: notification.status.account.fqn },
|
||||||
|
update: {
|
||||||
|
lastRespondedTo: new Date(Date.now()),
|
||||||
|
},
|
||||||
|
create: {
|
||||||
|
userFqn: notification.status.account.fqn,
|
||||||
|
lastRespondedTo: new Date(Date.now()),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (error: any) {
|
||||||
|
throw new Error(error.message);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export { storeUserData, storePromptData };
|
70
src/util.ts
Normal file
70
src/util.ts
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
import striptags from "striptags";
|
||||||
|
import { prisma } from "./main.js";
|
||||||
|
import { envConfig } from "./main.js";
|
||||||
|
import { Notification } from "../types.js";
|
||||||
|
|
||||||
|
const trimInputData = (input: string): string => {
|
||||||
|
const strippedInput = striptags(input);
|
||||||
|
const split = strippedInput.split(" ");
|
||||||
|
const promptStringIndex = split.indexOf("!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,
|
||||||
|
isProcessing: true,
|
||||||
|
isComplete: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (error: any) {
|
||||||
|
throw new Error(error.message);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const isFromWhitelistedDomain = (notification: Notification): boolean => {
|
||||||
|
try {
|
||||||
|
const domain = notification.status.account.fqn.split("@")[1];
|
||||||
|
if (envConfig.whitelistedDomains.includes(domain)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
console.log(
|
||||||
|
`Rejecting prompt request from non-whitelisted domain: ${domain}`
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error(`Error with domain check: ${error.message}`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const alreadyRespondedTo = async (
|
||||||
|
notification: Notification
|
||||||
|
): Promise<boolean> => {
|
||||||
|
try {
|
||||||
|
const duplicate = await prisma.response.findFirst({
|
||||||
|
where: { pleromaNotificationId: notification.id },
|
||||||
|
});
|
||||||
|
if (duplicate?.isProcessing || duplicate?.isComplete) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
} catch (error: any) {
|
||||||
|
throw new Error(error.message);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const selectRandomEmoji = (emojiList: string[]) => {
|
||||||
|
return emojiList[Math.floor(Math.random() * emojiList.length)];
|
||||||
|
};
|
||||||
|
|
||||||
|
export {
|
||||||
|
alreadyRespondedTo,
|
||||||
|
selectRandomEmoji,
|
||||||
|
trimInputData,
|
||||||
|
recordPendingResponse,
|
||||||
|
isFromWhitelistedDomain,
|
||||||
|
};
|
68
types.d.ts
vendored
68
types.d.ts
vendored
@ -40,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 {
|
||||||
@ -57,8 +61,9 @@ export interface Status {
|
|||||||
created_at: string | Date; // when the post was created
|
created_at: string | Date; // when the post was created
|
||||||
id: string; // ID of the reply itself
|
id: string; // ID of the reply itself
|
||||||
in_reply_to_account_id: string; // account ID of the reply
|
in_reply_to_account_id: string; // account ID of the reply
|
||||||
in_reply_to_id?: string; // status that the user has replied to
|
in_reply_to_id: string; // status that the user has replied to
|
||||||
mentions?: Mention[]; // array of mentions
|
mentions: Mention[]; // array of mentions
|
||||||
|
visibility: "private" | "public" | "unlisted";
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Mention {
|
export interface Mention {
|
||||||
@ -68,8 +73,57 @@ export interface Mention {
|
|||||||
username: string;
|
username: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface WSEvent {
|
export interface PleromaEmoji {
|
||||||
event: "update" | "status.update" | "notification";
|
[emojiName: string]: PleromaEmojiMetadata;
|
||||||
payload: string;
|
}
|
||||||
stream: "user" | "direct";
|
|
||||||
|
interface PleromaEmojiMetadata {
|
||||||
|
image_url: string;
|
||||||
|
tags: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Experimental settings, I wouldn't recommend messing with these if you don't know how they work (I don't either)
|
||||||
|
*/
|
||||||
|
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