Compare commits
2 Commits
9a7cd118b3
...
cbf6b1d3eb
Author | SHA1 | Date | |
---|---|---|---|
cbf6b1d3eb | |||
e2ce397118 |
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 };
|
169
src/main.ts
169
src/main.ts
@ -7,10 +7,23 @@ import {
|
|||||||
} 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 envConfig = {
|
export const envConfig = {
|
||||||
pleromaInstanceUrl: process.env.PLEROMA_INSTANCE_URL || "",
|
pleromaInstanceUrl: process.env.PLEROMA_INSTANCE_URL || "",
|
||||||
pleromaInstanceDomain: process.env.PLEROMA_INSTANCE_DOMAIN || "",
|
pleromaInstanceDomain: process.env.PLEROMA_INSTANCE_DOMAIN || "",
|
||||||
whitelistOnly: process.env.ONLY_WHITELIST === "true" ? true : false || "true",
|
whitelistOnly: process.env.ONLY_WHITELIST === "true" ? true : false || "true",
|
||||||
@ -32,119 +45,6 @@ const ollamaConfig: OllamaConfigOptions = {
|
|||||||
temperature: 1.2,
|
temperature: 1.2,
|
||||||
};
|
};
|
||||||
|
|
||||||
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 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);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const alreadyRespondedTo = async (
|
|
||||||
notification: Notification
|
|
||||||
): Promise<boolean> => {
|
|
||||||
try {
|
|
||||||
const duplicate = await prisma.response.findFirst({
|
|
||||||
where: { pleromaNotificationId: notification.id, isProcessing: true },
|
|
||||||
});
|
|
||||||
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.updateMany({
|
|
||||||
where: { pleromaNotificationId: notification.id },
|
|
||||||
data: {
|
|
||||||
response: ollamaResponseBody.response,
|
|
||||||
request: trimInputData(notification.status.content),
|
|
||||||
to: notification.account.fqn,
|
|
||||||
isProcessing: false,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
} catch (error: any) {
|
|
||||||
throw new Error(error.message);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
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,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
} catch (error: any) {
|
|
||||||
throw new Error(error.message);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const isFromWhitelistedDomain = async (
|
|
||||||
notification: Notification
|
|
||||||
): Promise<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}`
|
|
||||||
);
|
|
||||||
// delete the notification so we don't keep trying to fetch it
|
|
||||||
await deleteNotification(notification);
|
|
||||||
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> => {
|
||||||
@ -192,11 +92,16 @@ const postReplyToStatus = async (
|
|||||||
ollamaResponseBody: OllamaResponse
|
ollamaResponseBody: OllamaResponse
|
||||||
) => {
|
) => {
|
||||||
const { pleromaInstanceUrl, bearerToken } = envConfig;
|
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 (
|
||||||
@ -228,36 +133,6 @@ const postReplyToStatus = async (
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let notifications = [];
|
let notifications = [];
|
||||||
const beginFetchCycle = async () => {
|
const beginFetchCycle = async () => {
|
||||||
setInterval(async () => {
|
setInterval(async () => {
|
||||||
@ -285,6 +160,6 @@ console.log(
|
|||||||
} seconds.`
|
} seconds.`
|
||||||
);
|
);
|
||||||
console.log(
|
console.log(
|
||||||
`Accepting prompts from domains: ${envConfig.whitelistedDomains.join(", ")}`
|
`Accepting prompts from: ${envConfig.whitelistedDomains.join(", ")}`
|
||||||
);
|
);
|
||||||
await beginFetchCycle();
|
await beginFetchCycle();
|
||||||
|
41
src/prisma.ts
Normal file
41
src/prisma.ts
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
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,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} 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 };
|
73
src/util.ts
Normal file
73
src/util.ts
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
import striptags from "striptags";
|
||||||
|
import { prisma } from "./main.js";
|
||||||
|
import { envConfig } from "./main.js";
|
||||||
|
import { Notification } from "../types.js";
|
||||||
|
import { deleteNotification } from "./api.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,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (error: any) {
|
||||||
|
throw new Error(error.message);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const isFromWhitelistedDomain = async (
|
||||||
|
notification: Notification
|
||||||
|
): Promise<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}`
|
||||||
|
);
|
||||||
|
// delete the notification so we don't keep trying to fetch it
|
||||||
|
await deleteNotification(notification);
|
||||||
|
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, isProcessing: true },
|
||||||
|
});
|
||||||
|
if (duplicate) {
|
||||||
|
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,
|
||||||
|
};
|
9
types.d.ts
vendored
9
types.d.ts
vendored
@ -73,6 +73,15 @@ export interface Mention {
|
|||||||
username: string;
|
username: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface PleromaEmoji {
|
||||||
|
[emojiName: string]: PleromaEmojiMetadata;
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
* Experimental settings, I wouldn't recommend messing with these if you don't know how they work (I don't either)
|
||||||
*/
|
*/
|
||||||
|
Reference in New Issue
Block a user