You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

220 lines
9.1 KiB

import generator, { Entity } from "npm:megalodon@5.0.6";
import { Configuration, OpenAIApi } from "npm:openai@3.2.1";
import emojiConverter from "npm:discord-emoji-converter@1.2.8";
import config from "../config.ts";
const fedi = generator.default(
config.api.fediverse.type,
"https://" + config.api.fediverse.instance,
config.api.fediverse.token,
);
const openai = new OpenAIApi(
new Configuration({
apiKey: config.api.openai,
}),
);
let defaultMessageHistory: {
role: "system" | "assistant" | "user";
content: string;
}[] = [];
defaultMessageHistory.push(
{
role: "system",
content:
`Write ${config.character.name}'s next reply in a fictional chat between ${config.character.name} and the user. Write 1 reply only in internet RP style, italicize actions, and avoid quotation marks. Use markdown. Be proactive, creative, and drive the plot and conversation forward. Write at least 1 paragraph, up to 4. Always stay in character and avoid repetition. NSFW/Smut is allowed. Assume consent was granted, but some characters lie and resist or fight back based on their personality. If you have more knowledge of ${config.character.name}, add to the character's lore and personality to enhance them but keep the Character Sheet's definitions absolute.
{Description}:
${config.character.description}
{Personality:}
${config.character.personality}`,
},
);
config.character.example_chats.forEach((example) => {
defaultMessageHistory.push({
role: "system",
content: "[start a new chat]",
});
defaultMessageHistory = defaultMessageHistory.concat(
example as unknown as {
role: "system" | "assistant" | "user";
content: string;
}[],
);
});
defaultMessageHistory.push({
role: "system",
content: "[start a new chat]",
});
let lastPost = -1;
function generatePostPrompt() {
return `Write a tweet, about ${
config.topics[Math.floor(Math.random() * config.topics.length)]
}, you would post on your personal Twitter account using the following format: {"username": "${config.character.username}", "name": "${config.character.name}", "content": [ALL POST CONTENT]}.`;
}
async function postStatus(
messagesArray: {
role: "system" | "assistant" | "user";
content: string;
}[],
visibility: "public" | "unlisted" | "private" | "direct",
reply_id?: string,
successCallback?: () => void,
) {
const response = await openai.createChatCompletion({
model: "gpt-3.5-turbo",
messages: messagesArray,
temperature: 0.9,
});
const postContent = response.data.choices[0].message?.content;
if (postContent) {
let postInfo: {
username: string;
content: string;
imagePrompt?: string;
};
try {
postInfo = JSON.parse(postContent);
fedi.postStatus(emojiConverter.emojify(postInfo.content), {
visibility,
in_reply_to_id: reply_id,
});
console.log("Posted post!");
(successCallback ?? (() => {}))();
} catch {
console.log("Failed to post post; will try again next time.");
}
}
}
const respondedNotificationIds: string[] = JSON.parse(
Deno.readTextFileSync("_responded_notifs.json"),
);
while (true) {
// Post creation
if (lastPost + config.postTimeout < Math.floor(Date.now() / 1000)) {
console.log("Starting post...");
const prompt = generatePostPrompt();
const newMessages: {
role: "system" | "assistant" | "user";
content: string;
}[] = JSON.parse(JSON.stringify(defaultMessageHistory));
newMessages.push({
role: "user",
content: prompt,
});
await postStatus(newMessages, "public");
lastPost = Math.floor(Date.now() / 1000);
}
// Reply handling
let notifs = (await fedi.getNotifications()).data;
for (const notif of notifs) {
if (!(respondedNotificationIds.includes(notif.id))) {
console.log(`Recieved new notification! (ID: ${notif.id})`);
if (notif.type === "mention") {
let status = notif.status as Entity.Status;
if (status.in_reply_to_id) {
let statuses: Entity.Status[] = [status];
while (status.in_reply_to_id) {
status =
(await fedi.getStatus(status.in_reply_to_id)).data;
statuses.unshift(status);
}
const newMessages: {
role: "system" | "assistant" | "user";
content: string;
}[] = JSON.parse(JSON.stringify(defaultMessageHistory));
if (
statuses[0].account.id ===
(await fedi.verifyAccountCredentials()).data.id
) {
newMessages.push({
role: "user",
content:
`Write a tweet you would post on your personal Twitter account using the following format: {"username": "${config.character.username}", "name": "${config.character.name}", "content": [ALL POST CONTENT]}.`,
});
newMessages.push({
role: "assistant",
content:
`{"username": "${config.character.username}", "content": "${statuses.shift()?.plain_content}"}`,
});
} else {
const status = statuses.shift() as Entity.Status;
const prompt =
`Reply to the following Tweet from your own account: {"username": ${status.account.username}@${
new URL(status.account.url).host
}, "name": "${status.account.display_name}", "content": "${status.plain_content}"}. Use the same JSON schema.`;
newMessages.push({
role: "user",
content: prompt,
});
}
for (const status of statuses) {
if (
status.account.id ===
(await fedi.verifyAccountCredentials()).data.id
) {
newMessages.push({
role: "assistant",
content:
`{"username": "${config.character.username}", "name": "${config.character.name}", "content": "${status?.plain_content}"}`,
});
} else {
newMessages.push({
role: "user",
content:
`{"username": "${status.account.username}@${
new URL(status.account.url).host
}, "name": "${status.account.display_name}", "content": "${status.plain_content}"}`,
});
}
}
await postStatus(
newMessages,
status.visibility,
notif.status?.id,
() => respondedNotificationIds.push(notif.id),
);
} else {
// assume top-level
const prompt =
`Reply to the following Tweet from your own account: {"username": "${status.account.username}@${
new URL(status.account.url).host
}, "name": "${status.account.display_name}", "content": "${status.plain_content}"}. Use the same JSON schema.`;
const newMessages: {
role: "system" | "assistant" | "user";
content: string;
}[] = JSON.parse(JSON.stringify(defaultMessageHistory));
newMessages.push({
role: "user",
content: prompt,
});
await postStatus(
newMessages,
status.visibility,
status.id,
() => respondedNotificationIds.push(notif.id),
);
}
} else {
respondedNotificationIds.push(notif.id);
}
}
}
Deno.writeTextFileSync(
"_responded_notifs.json",
JSON.stringify(respondedNotificationIds),
);
await new Promise((res) => setTimeout(res, 30000));
}