From 3ef87655d8364c885368943a29fab8dcaf4ef13d Mon Sep 17 00:00:00 2001 From: Ruthenic Date: Thu, 9 Mar 2023 20:53:28 -0500 Subject: [PATCH] initial commit --- .gitignore | 1 + .vscode/settings.json | 9 ++ README.md | 1 + _responded_notifs.json | 1 + deno.json | 7 ++ deno.lock | 222 ++++++++++++++++++++++++++++++++++++ generateToken.ts | 21 ++++ src/index.ts | 252 +++++++++++++++++++++++++++++++++++++++++ 8 files changed, 514 insertions(+) create mode 100644 .gitignore create mode 100644 .vscode/settings.json create mode 100644 README.md create mode 100644 _responded_notifs.json create mode 100644 deno.json create mode 100644 deno.lock create mode 100644 generateToken.ts create mode 100644 src/index.ts diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1b8afd0 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +config.ts \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..c8bd270 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,9 @@ +{ + "deno.enable": true, + "deno.unstable": true, + "editor.formatOnSave": true, + "[typescript]": { + "editor.tabSize": 4, + "editor.defaultFormatter": "denoland.vscode-deno" + } +} diff --git a/README.md b/README.md new file mode 100644 index 0000000..1ec522c --- /dev/null +++ b/README.md @@ -0,0 +1 @@ +I might write docs, or a config template, or anything on how to run this.. eventually. Maybe. \ No newline at end of file diff --git a/_responded_notifs.json b/_responded_notifs.json new file mode 100644 index 0000000..fe51488 --- /dev/null +++ b/_responded_notifs.json @@ -0,0 +1 @@ +[] diff --git a/deno.json b/deno.json new file mode 100644 index 0000000..541a5c4 --- /dev/null +++ b/deno.json @@ -0,0 +1,7 @@ +{ + "fmt": { + "options": { + "indentWidth": 4 + } + } +} diff --git a/deno.lock b/deno.lock new file mode 100644 index 0000000..5fcd913 --- /dev/null +++ b/deno.lock @@ -0,0 +1,222 @@ +{ + "version": "2", + "remote": {}, + "npm": { + "specifiers": { + "megalodon@5.0.6": "megalodon@5.0.6", + "megalodon@5.4.1": "megalodon@5.4.1", + "openai@3.2.1": "openai@3.2.1" + }, + "packages": { + "@types/node@18.14.6": { + "integrity": "sha512-93+VvleD3mXwlLI/xASjw0FzKcwzl3OdTCzm1LaRfqgS21gfFtK3zDXM5Op9TeeMsJVOaJ2VRDpT9q4Y3d0AvA==", + "dependencies": {} + }, + "@types/oauth@0.9.1": { + "integrity": "sha512-a1iY62/a3yhZ7qH7cNUsxoI3U/0Fe9+RnuFrpTKr+0WVOzbKlSLojShCKe20aOD1Sppv+i8Zlq0pLDuTJnwS4A==", + "dependencies": { + "@types/node": "@types/node@18.14.6" + } + }, + "@types/ws@8.5.4": { + "integrity": "sha512-zdQDHKUgcX/zBc4GrwsE/7dVdAD8JR4EuiAXiiUhhfyIJXXb2+PrGshFyeXWQPMmmZ2XxgaqclgpIC7eTXc1mg==", + "dependencies": { + "@types/node": "@types/node@18.14.6" + } + }, + "agent-base@6.0.2": { + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "dependencies": { + "debug": "debug@4.3.4" + } + }, + "asynckit@0.4.0": { + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dependencies": {} + }, + "axios@0.26.1": { + "integrity": "sha512-fPwcX4EvnSHuInCMItEhAGnaSEXRBjtzh9fOtsE6E1G6p7vl7edEeZe11QHf18+6+9gR5PbKV/sGKNaD8YaMeA==", + "dependencies": { + "follow-redirects": "follow-redirects@1.15.2" + } + }, + "axios@1.2.2": { + "integrity": "sha512-bz/J4gS2S3I7mpN/YZfGFTqhXTYzRho8Ay38w2otuuDR322KzFIWm/4W2K6gIwvWaws5n+mnb7D1lN9uD+QH6Q==", + "dependencies": { + "follow-redirects": "follow-redirects@1.15.2", + "form-data": "form-data@4.0.0", + "proxy-from-env": "proxy-from-env@1.1.0" + } + }, + "axios@1.3.4": { + "integrity": "sha512-toYm+Bsyl6VC5wSkfkbbNB6ROv7KY93PEBBL6xyDczaIHasAiv4wPqQ/c4RjoQzipxRD2W5g21cOqQulZ7rHwQ==", + "dependencies": { + "follow-redirects": "follow-redirects@1.15.2", + "form-data": "form-data@4.0.0", + "proxy-from-env": "proxy-from-env@1.1.0" + } + }, + "combined-stream@1.0.8": { + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dependencies": { + "delayed-stream": "delayed-stream@1.0.0" + } + }, + "dayjs@1.11.7": { + "integrity": "sha512-+Yw9U6YO5TQohxLcIkrXBeY73WP3ejHWVvx8XCk3gxvQDCTEmS48ZrSZCKciI7Bhl/uCMyxYtE9UqRILmFphkQ==", + "dependencies": {} + }, + "debug@4.3.4": { + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dependencies": { + "ms": "ms@2.1.2" + } + }, + "delayed-stream@1.0.0": { + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dependencies": {} + }, + "follow-redirects@1.15.2": { + "integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==", + "dependencies": {} + }, + "form-data@4.0.0": { + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "dependencies": { + "asynckit": "asynckit@0.4.0", + "combined-stream": "combined-stream@1.0.8", + "mime-types": "mime-types@2.1.35" + } + }, + "https-proxy-agent@5.0.1": { + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "dependencies": { + "agent-base": "agent-base@6.0.2", + "debug": "debug@4.3.4" + } + }, + "ip@2.0.0": { + "integrity": "sha512-WKa+XuLG1A1R0UWhl2+1XQSi+fZWMsYKffMZTTYsiZaUD8k2yDAj5atimTUD2TZkyCkNEeYE5NhFZmupOGtjYQ==", + "dependencies": {} + }, + "megalodon@5.0.6": { + "integrity": "sha512-Tt27g71M852mw14LvCEDgOeIEoP6tHRVRDJMxl0BYVGr/IpYC0Zpd5M3ql0teV3JL8Sl5kSAI2jt0mYzpsuilg==", + "dependencies": { + "@types/oauth": "@types/oauth@0.9.1", + "@types/ws": "@types/ws@8.5.4", + "axios": "axios@1.2.2", + "dayjs": "dayjs@1.11.7", + "form-data": "form-data@4.0.0", + "https-proxy-agent": "https-proxy-agent@5.0.1", + "oauth": "oauth@0.10.0", + "object-assign-deep": "object-assign-deep@0.4.0", + "parse-link-header": "parse-link-header@2.0.0", + "socks-proxy-agent": "socks-proxy-agent@7.0.0", + "typescript": "typescript@4.9.4", + "uuid": "uuid@9.0.0", + "ws": "ws@8.5.0" + } + }, + "megalodon@5.4.1": { + "integrity": "sha512-6UPbODxOFnZPChHEcoZvwTSjJaPmSMQWW40snBIT/YFxpcwH8Q8GfqYRIG46eAKl1PQCUI9gFpEvt9I+l9KKVA==", + "dependencies": { + "@types/oauth": "@types/oauth@0.9.1", + "@types/ws": "@types/ws@8.5.4", + "axios": "axios@1.3.4", + "dayjs": "dayjs@1.11.7", + "form-data": "form-data@4.0.0", + "https-proxy-agent": "https-proxy-agent@5.0.1", + "oauth": "oauth@0.10.0", + "object-assign-deep": "object-assign-deep@0.4.0", + "parse-link-header": "parse-link-header@2.0.0", + "socks-proxy-agent": "socks-proxy-agent@7.0.0", + "typescript": "typescript@4.9.5", + "uuid": "uuid@9.0.0", + "ws": "ws@8.12.1" + } + }, + "mime-db@1.52.0": { + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dependencies": {} + }, + "mime-types@2.1.35": { + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dependencies": { + "mime-db": "mime-db@1.52.0" + } + }, + "ms@2.1.2": { + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dependencies": {} + }, + "oauth@0.10.0": { + "integrity": "sha512-1orQ9MT1vHFGQxhuy7E/0gECD3fd2fCC+PIX+/jgmU/gI3EpRocXtmtvxCO5x3WZ443FLTLFWNDjl5MPJf9u+Q==", + "dependencies": {} + }, + "object-assign-deep@0.4.0": { + "integrity": "sha512-54Uvn3s+4A/cMWx9tlRez1qtc7pN7pbQ+Yi7mjLjcBpWLlP+XbSHiHbQW6CElDiV4OvuzqnMrBdkgxI1mT8V/Q==", + "dependencies": {} + }, + "openai@3.2.1": { + "integrity": "sha512-762C9BNlJPbjjlWZi4WYK9iM2tAVAv0uUp1UmI34vb0CN5T2mjB/qM6RYBmNKMh/dN9fC+bxqPwWJZUTWW052A==", + "dependencies": { + "axios": "axios@0.26.1", + "form-data": "form-data@4.0.0" + } + }, + "parse-link-header@2.0.0": { + "integrity": "sha512-xjU87V0VyHZybn2RrCX5TIFGxTVZE6zqqZWMPlIKiSKuWh/X5WZdt+w1Ki1nXB+8L/KtL+nZ4iq+sfI6MrhhMw==", + "dependencies": { + "xtend": "xtend@4.0.2" + } + }, + "proxy-from-env@1.1.0": { + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "dependencies": {} + }, + "smart-buffer@4.2.0": { + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "dependencies": {} + }, + "socks-proxy-agent@7.0.0": { + "integrity": "sha512-Fgl0YPZ902wEsAyiQ+idGd1A7rSFx/ayC1CQVMw5P+EQx2V0SgpGtf6OKFhVjPflPUl9YMmEOnmfjCdMUsygww==", + "dependencies": { + "agent-base": "agent-base@6.0.2", + "debug": "debug@4.3.4", + "socks": "socks@2.7.1" + } + }, + "socks@2.7.1": { + "integrity": "sha512-7maUZy1N7uo6+WVEX6psASxtNlKaNVMlGQKkG/63nEDdLOWNbiUMoLK7X4uYoLhQstau72mLgfEWcXcwsaHbYQ==", + "dependencies": { + "ip": "ip@2.0.0", + "smart-buffer": "smart-buffer@4.2.0" + } + }, + "typescript@4.9.4": { + "integrity": "sha512-Uz+dTXYzxXXbsFpM86Wh3dKCxrQqUcVMxwU54orwlJjOpO3ao8L7j5lH+dWfTwgCwIuM9GQ2kvVotzYJMXTBZg==", + "dependencies": {} + }, + "typescript@4.9.5": { + "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", + "dependencies": {} + }, + "uuid@9.0.0": { + "integrity": "sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==", + "dependencies": {} + }, + "ws@8.12.1": { + "integrity": "sha512-1qo+M9Ba+xNhPB+YTWUlK6M17brTut5EXbcBaMRN5pH5dFrXz7lzz1ChFSUq3bOUl8yEvSenhHmYUNJxFzdJew==", + "dependencies": {} + }, + "ws@8.5.0": { + "integrity": "sha512-BWX0SWVgLPzYwF8lTzEy1egjhS4S4OEAHfsO8o65WOVsrnSRGaSiUaa9e0ggGlkMTtBlmOpEXiie9RUcBO86qg==", + "dependencies": {} + }, + "xtend@4.0.2": { + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "dependencies": {} + } + } + } +} diff --git a/generateToken.ts b/generateToken.ts new file mode 100644 index 0000000..fc19fac --- /dev/null +++ b/generateToken.ts @@ -0,0 +1,21 @@ +import generator from "npm:megalodon@5.4.1"; + +const client = generator.default("pleroma", "https://social.ruthenic.com"); + +const appData = await client.registerApp("Test", { + scopes: ["read", "write", "follow"], +}); + +console.log(appData.url); + +const code = prompt("Enter auth code:"); + +if (code) { + console.log( + await client.fetchAccessToken( + appData.clientId, + appData.clientSecret, + code, + ), + ); +} diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..dcea32a --- /dev/null +++ b/src/index.ts @@ -0,0 +1,252 @@ +import generator, { Entity } from "npm:megalodon@5.0.6"; +import { Configuration, OpenAIApi } from "npm:openai@3.2.1"; +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]}.`; +} + +let 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, + }); + const response = await openai.createChatCompletion({ + model: "gpt-3.5-turbo", + messages: newMessages, + 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(postInfo.content, { + visibility: "private", + }); + lastPost = Math.floor(Date.now() / 1000); + console.log("Posted post!"); + } catch { + console.log("Failed to post post; will try again next time."); + } + } + } + + // Reply handling + let notifs = (await fedi.getNotifications()).data; + for (const notif of notifs) { + if (!(respondedNotificationIds.includes(notif.id))) { + console.log("Recieved new notification!"); + 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 + ) { + console.log(statuses); + 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}"}`, + }); + } + } + console.log(newMessages); + const response = await openai.createChatCompletion({ + model: "gpt-3.5-turbo", + messages: newMessages, + temperature: 0.9, + }); + const postContent = response.data.choices[0].message + ?.content; + console.log(postContent); + if (postContent) { + let postInfo: { + username: string; + content: string; + imagePrompt?: string; + }; + try { + postInfo = JSON.parse(postContent); + fedi.postStatus(postInfo.content, { + visibility: status.visibility, + in_reply_to_id: notif.status?.id, + }); + lastPost = Math.floor(Date.now() / 1000); + console.log("Posted post!"); + respondedNotificationIds.push(notif.id); + } catch (e) { + console.log( + "Failed to post post; will try again next time.\n" + + e, + ); + } + } + } 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, + }); + const response = await openai.createChatCompletion({ + model: "gpt-3.5-turbo", + messages: newMessages, + 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(postInfo.content, { + visibility: status.visibility, + in_reply_to_id: status.id, + }); + lastPost = Math.floor(Date.now() / 1000); + console.log("Posted post!"); + respondedNotificationIds.push(notif.id); + } catch (e) { + console.log( + "Failed to post post; will try again next time.\n" + + e, + ); + } + } + } + } else { + respondedNotificationIds.push(notif.id); + } + } + } + + Deno.writeTextFileSync( + "_responded_notifs.json", + JSON.stringify(respondedNotificationIds), + ); + await new Promise((res) => setTimeout(res, 5000)); +}