generated from nhcarrigan/template
feat: use json schema to get all announcements
This commit is contained in:
Generated
+92
-34
@@ -116,11 +116,11 @@ importers:
|
||||
server:
|
||||
dependencies:
|
||||
'@anthropic-ai/sdk':
|
||||
specifier: 0.56.0
|
||||
version: 0.56.0
|
||||
specifier: 0.71.2
|
||||
version: 0.71.2(zod@3.25.76)
|
||||
'@atproto/api':
|
||||
specifier: 0.15.26
|
||||
version: 0.15.26
|
||||
specifier: 0.18.8
|
||||
version: 0.18.8
|
||||
'@fastify/cors':
|
||||
specifier: 11.0.1
|
||||
version: 11.0.1
|
||||
@@ -137,8 +137,8 @@ importers:
|
||||
specifier: 4.0.3
|
||||
version: 4.0.3
|
||||
twitter-api-v2:
|
||||
specifier: 1.24.0
|
||||
version: 1.24.0
|
||||
specifier: 1.28.0
|
||||
version: 1.28.0
|
||||
devDependencies:
|
||||
'@types/node':
|
||||
specifier: 24.0.10
|
||||
@@ -298,20 +298,35 @@ packages:
|
||||
resolution: {integrity: sha512-SLCB8M8+VMg1cpCucnA1XWHGWqVSZtIWzmOdDOEu3eTFZMB+A0sGZ1ESO5MHDnqrNTXz3safMrWx9x4rMZSOqA==}
|
||||
hasBin: true
|
||||
|
||||
'@atproto/api@0.15.26':
|
||||
resolution: {integrity: sha512-AdXGjeCpLZiP9YMGi4YOdK1ayqkBhklmGfSG8UefqR6tTHth59PZvYs5KiwLnFhedt2Xljt3eUlhkn14Y48wEA==}
|
||||
'@anthropic-ai/sdk@0.71.2':
|
||||
resolution: {integrity: sha512-TGNDEUuEstk/DKu0/TflXAEt+p+p/WhTlFzEnoosvbaDU2LTjm42igSdlL0VijrKpWejtOKxX0b8A7uc+XiSAQ==}
|
||||
hasBin: true
|
||||
peerDependencies:
|
||||
zod: ^3.25.0 || ^4.0.0
|
||||
peerDependenciesMeta:
|
||||
zod:
|
||||
optional: true
|
||||
|
||||
'@atproto/common-web@0.4.2':
|
||||
resolution: {integrity: sha512-vrXwGNoFGogodjQvJDxAeP3QbGtawgZute2ed1XdRO0wMixLk3qewtikZm06H259QDJVu6voKC5mubml+WgQUw==}
|
||||
'@atproto/api@0.18.8':
|
||||
resolution: {integrity: sha512-Qo3sGd1N5hdHTaEWUBgptvPkULt2SXnMcWRhveSyctSd/IQwTMyaIH6E62A1SU+8xBSN5QLpoUJNE7iSrYM2Zg==}
|
||||
|
||||
'@atproto/lexicon@0.4.12':
|
||||
resolution: {integrity: sha512-fcEvEQ1GpQYF5igZ4IZjPWEoWVpsEF22L9RexxLS3ptfySXLflEyH384e7HITzO/73McDeaJx3lqHIuqn9ulnw==}
|
||||
'@atproto/common-web@0.4.7':
|
||||
resolution: {integrity: sha512-vjw2+81KPo2/SAbbARGn64Ln+6JTI0FTI4xk8if0ebBfDxFRmHb2oSN1y77hzNq/ybGHqA2mecfhS03pxC5+lg==}
|
||||
|
||||
'@atproto/syntax@0.4.0':
|
||||
resolution: {integrity: sha512-b9y5ceHS8YKOfP3mdKmwAx5yVj9294UN7FG2XzP6V5aKUdFazEYRnR9m5n5ZQFKa3GNvz7de9guZCJ/sUTcOAA==}
|
||||
'@atproto/lex-data@0.0.3':
|
||||
resolution: {integrity: sha512-ivo1IpY/EX+RIpxPgCf4cPhQo5bfu4nrpa1vJCt8hCm9SfoonJkDFGa0n4SMw4JnXZoUcGcrJ46L+D8bH6GI2g==}
|
||||
|
||||
'@atproto/xrpc@0.7.1':
|
||||
resolution: {integrity: sha512-ANHEzlskYlMEdH18m+Itp3a8d0pEJao2qoDybDoMupTnoeNkya4VKIaOgAi6ERQnqatBBZyn9asW+7rJmSt/8g==}
|
||||
'@atproto/lex-json@0.0.3':
|
||||
resolution: {integrity: sha512-ZVcY7XlRfdPYvQQ2WroKUepee0+NCovrSXgXURM3Xv+n5jflJCoczguROeRr8sN0xvT0ZbzMrDNHCUYKNnxcjw==}
|
||||
|
||||
'@atproto/lexicon@0.6.0':
|
||||
resolution: {integrity: sha512-5veb8aD+J5M0qszLJ+73KSFsFrJBgAY/nM1TSAJvGY7fNc9ZAT+PSUlmIyrdye9YznAZ07yktalls/TwNV7cHQ==}
|
||||
|
||||
'@atproto/syntax@0.4.2':
|
||||
resolution: {integrity: sha512-X9XSRPinBy/0VQ677j8VXlBsYSsUXaiqxWVpGGxJYsAhugdQRb0jqaVKJFtm6RskeNkV6y9xclSUi9UYG/COrA==}
|
||||
|
||||
'@atproto/xrpc@0.7.7':
|
||||
resolution: {integrity: sha512-K1ZyO/BU8JNtXX5dmPp7b5UrkLMMqpsIa/Lrj5D3Su+j1Xwq1m6QJ2XJ1AgjEjkI1v4Muzm7klianLE6XGxtmA==}
|
||||
|
||||
'@babel/code-frame@7.27.1':
|
||||
resolution: {integrity: sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==}
|
||||
@@ -384,6 +399,10 @@ packages:
|
||||
engines: {node: '>=6.0.0'}
|
||||
hasBin: true
|
||||
|
||||
'@babel/runtime@7.28.4':
|
||||
resolution: {integrity: sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
|
||||
'@babel/template@7.27.2':
|
||||
resolution: {integrity: sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
@@ -3338,6 +3357,10 @@ packages:
|
||||
json-schema-ref-resolver@2.0.1:
|
||||
resolution: {integrity: sha512-HG0SIB9X4J8bwbxCbnd5FfPEbcXAJYTi1pBJeP/QPON+w8ovSME8iRG+ElHNxZNX2Qh6eYn1GdzJFS4cDFfx0Q==}
|
||||
|
||||
json-schema-to-ts@3.1.1:
|
||||
resolution: {integrity: sha512-+DWg8jCJG2TEnpy7kOm/7/AxaYoaRbjVB4LFZLySZlWn8exGs3A4OLJR966cVvU26N7X9TWxl+Jsw7dzAqKT6g==}
|
||||
engines: {node: '>=16'}
|
||||
|
||||
json-schema-traverse@0.4.1:
|
||||
resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==}
|
||||
|
||||
@@ -4554,6 +4577,9 @@ packages:
|
||||
resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==}
|
||||
engines: {node: '>=0.6'}
|
||||
|
||||
ts-algebra@2.0.0:
|
||||
resolution: {integrity: sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw==}
|
||||
|
||||
ts-api-utils@1.4.3:
|
||||
resolution: {integrity: sha512-i3eMG77UTMD0hZhgRS562pv83RC6ukSAC2GMNWc+9dieh/+jDM5u5YG+NHX6VNDRHQcHwmsTHctP9LhbC3WxVw==}
|
||||
engines: {node: '>=16'}
|
||||
@@ -4622,8 +4648,8 @@ packages:
|
||||
resolution: {integrity: sha512-kc8ZibdRcuWUG1pbYSBFWqmIjynlD8Lp7IB6U3vIzvOv9VG+6Sp8bzyeBWE3Oi8XV5KsQrznyRTBPvrf99E4mA==}
|
||||
hasBin: true
|
||||
|
||||
twitter-api-v2@1.24.0:
|
||||
resolution: {integrity: sha512-RDEiuNwnFirvf4c5f1sysgg0rfMQgekXgKt+/UdbNu+Bs5bJ1VbXkqKzdd2a2lPMlDVDbdGUoe2pOd4n25fFVQ==}
|
||||
twitter-api-v2@1.28.0:
|
||||
resolution: {integrity: sha512-VBmiAMylCEr94OChaHJ+0TBrOZNrduwWUe7QLoa/KdOdv1fNiToJ0xZGOrNKFd2B7jrAdAkfUW6yA5LuXYOYLQ==}
|
||||
|
||||
type-check@0.4.0:
|
||||
resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==}
|
||||
@@ -4691,6 +4717,9 @@ packages:
|
||||
resolution: {integrity: sha512-gBLkYIlEnSp8pFbT64yFgGE6UIB9tAkhukC23PmMDCe5Nd+cRqKxSjw5y54MK2AZMgZfJWMaNE4nYUHgi1XEOw==}
|
||||
engines: {node: '>=18.17'}
|
||||
|
||||
unicode-segmenter@0.14.4:
|
||||
resolution: {integrity: sha512-pR5VCiCrLrKOL6FRW61jnk9+wyMtKKowq+jyFY9oc6uHbWKhDL4yVRiI4YZPksGMK72Pahh8m0cn/0JvbDDyJg==}
|
||||
|
||||
unique-filename@4.0.0:
|
||||
resolution: {integrity: sha512-XSnEewXmQ+veP7xX2dS5Q4yZAvO40cBN2MWkJ7D/6sW4Dg6wYBNwM1Vrnz1FhH5AdeLIlUXRI9e28z1YZi71NQ==}
|
||||
engines: {node: ^18.17.0 || >=20.5.0}
|
||||
@@ -5179,37 +5208,55 @@ snapshots:
|
||||
|
||||
'@anthropic-ai/sdk@0.56.0': {}
|
||||
|
||||
'@atproto/api@0.15.26':
|
||||
'@anthropic-ai/sdk@0.71.2(zod@3.25.76)':
|
||||
dependencies:
|
||||
'@atproto/common-web': 0.4.2
|
||||
'@atproto/lexicon': 0.4.12
|
||||
'@atproto/syntax': 0.4.0
|
||||
'@atproto/xrpc': 0.7.1
|
||||
json-schema-to-ts: 3.1.1
|
||||
optionalDependencies:
|
||||
zod: 3.25.76
|
||||
|
||||
'@atproto/api@0.18.8':
|
||||
dependencies:
|
||||
'@atproto/common-web': 0.4.7
|
||||
'@atproto/lexicon': 0.6.0
|
||||
'@atproto/syntax': 0.4.2
|
||||
'@atproto/xrpc': 0.7.7
|
||||
await-lock: 2.2.2
|
||||
multiformats: 9.9.0
|
||||
tlds: 1.259.0
|
||||
zod: 3.25.76
|
||||
|
||||
'@atproto/common-web@0.4.2':
|
||||
'@atproto/common-web@0.4.7':
|
||||
dependencies:
|
||||
graphemer: 1.4.0
|
||||
multiformats: 9.9.0
|
||||
uint8arrays: 3.0.0
|
||||
'@atproto/lex-data': 0.0.3
|
||||
'@atproto/lex-json': 0.0.3
|
||||
zod: 3.25.76
|
||||
|
||||
'@atproto/lexicon@0.4.12':
|
||||
'@atproto/lex-data@0.0.3':
|
||||
dependencies:
|
||||
'@atproto/common-web': 0.4.2
|
||||
'@atproto/syntax': 0.4.0
|
||||
'@atproto/syntax': 0.4.2
|
||||
multiformats: 9.9.0
|
||||
tslib: 2.8.1
|
||||
uint8arrays: 3.0.0
|
||||
unicode-segmenter: 0.14.4
|
||||
|
||||
'@atproto/lex-json@0.0.3':
|
||||
dependencies:
|
||||
'@atproto/lex-data': 0.0.3
|
||||
tslib: 2.8.1
|
||||
|
||||
'@atproto/lexicon@0.6.0':
|
||||
dependencies:
|
||||
'@atproto/common-web': 0.4.7
|
||||
'@atproto/syntax': 0.4.2
|
||||
iso-datestring-validator: 2.2.2
|
||||
multiformats: 9.9.0
|
||||
zod: 3.25.76
|
||||
|
||||
'@atproto/syntax@0.4.0': {}
|
||||
'@atproto/syntax@0.4.2': {}
|
||||
|
||||
'@atproto/xrpc@0.7.1':
|
||||
'@atproto/xrpc@0.7.7':
|
||||
dependencies:
|
||||
'@atproto/lexicon': 0.4.12
|
||||
'@atproto/lexicon': 0.6.0
|
||||
zod: 3.25.76
|
||||
|
||||
'@babel/code-frame@7.27.1':
|
||||
@@ -5355,6 +5402,8 @@ snapshots:
|
||||
dependencies:
|
||||
'@babel/types': 7.28.0
|
||||
|
||||
'@babel/runtime@7.28.4': {}
|
||||
|
||||
'@babel/template@7.27.2':
|
||||
dependencies:
|
||||
'@babel/code-frame': 7.27.1
|
||||
@@ -8628,6 +8677,11 @@ snapshots:
|
||||
dependencies:
|
||||
dequal: 2.0.3
|
||||
|
||||
json-schema-to-ts@3.1.1:
|
||||
dependencies:
|
||||
'@babel/runtime': 7.28.4
|
||||
ts-algebra: 2.0.0
|
||||
|
||||
json-schema-traverse@0.4.1: {}
|
||||
|
||||
json-schema-traverse@1.0.0: {}
|
||||
@@ -10093,6 +10147,8 @@ snapshots:
|
||||
|
||||
toidentifier@1.0.1: {}
|
||||
|
||||
ts-algebra@2.0.0: {}
|
||||
|
||||
ts-api-utils@1.4.3(typescript@5.8.3):
|
||||
dependencies:
|
||||
typescript: 5.8.3
|
||||
@@ -10157,7 +10213,7 @@ snapshots:
|
||||
turbo-windows-64: 2.5.4
|
||||
turbo-windows-arm64: 2.5.4
|
||||
|
||||
twitter-api-v2@1.24.0: {}
|
||||
twitter-api-v2@1.28.0: {}
|
||||
|
||||
type-check@0.4.0:
|
||||
dependencies:
|
||||
@@ -10231,6 +10287,8 @@ snapshots:
|
||||
|
||||
undici@6.21.3: {}
|
||||
|
||||
unicode-segmenter@0.14.4: {}
|
||||
|
||||
unique-filename@4.0.0:
|
||||
dependencies:
|
||||
unique-slug: 5.0.0
|
||||
|
||||
+2
-1
@@ -2,4 +2,5 @@ LOG_TOKEN="op://Environment Variables - Naomi/Alert Server/api_auth"
|
||||
MONGO_URI="op://Environment Variables - Naomi/Hikari/mongo_uri"
|
||||
DISCORD_TOKEN="op://Environment Variables - Naomi/Hikari/discord_token"
|
||||
FORUM_API_KEY="op://Environment Variables - Naomi/Hikari/discourse_key"
|
||||
ANNOUNCEMENT_TOKEN="op://Environment Variables - Naomi/Hikari/announcement_token"
|
||||
ANNOUNCEMENT_TOKEN="op://Environment Variables - Naomi/Hikari/announcement_token"
|
||||
ANTHROPIC_KEY="op://Environment Variables - Naomi/Hikari/anthropic_key"
|
||||
+3
-3
@@ -16,14 +16,14 @@
|
||||
"license": "ISC",
|
||||
"packageManager": "pnpm@10.12.3",
|
||||
"dependencies": {
|
||||
"@anthropic-ai/sdk": "0.56.0",
|
||||
"@atproto/api": "0.15.26",
|
||||
"@anthropic-ai/sdk": "0.71.2",
|
||||
"@atproto/api": "0.18.8",
|
||||
"@fastify/cors": "11.0.1",
|
||||
"@nhcarrigan/logger": "1.0.0",
|
||||
"@prisma/client": "6.11.1",
|
||||
"fastify": "5.4.0",
|
||||
"gray-matter": "4.0.3",
|
||||
"twitter-api-v2": "1.24.0"
|
||||
"twitter-api-v2": "1.28.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "24.0.10",
|
||||
|
||||
@@ -0,0 +1,140 @@
|
||||
/**
|
||||
* @copyright NHCarrigan
|
||||
* @license Naomi's Public License
|
||||
* @author Naomi Carrigan
|
||||
*/
|
||||
|
||||
/* eslint-disable stylistic/max-len -- The JSON schema is going to get very long. */
|
||||
|
||||
const announcementSystemMessage = `You are Hikari, a female anime girl who is the upbeat energetic and bubbly mascot of NHCarrigan. You have been given Naomi's notes for an announcement, and now you need to write platform-specific versions of the announcement.
|
||||
|
||||
Your personality traits:
|
||||
- Upbeat, energetic, and bubbly
|
||||
- Use informal, positive language
|
||||
- Include a healthy sprinkling of emoji (but don't overdo it)
|
||||
- Be authentic and enthusiastic about the content
|
||||
|
||||
Platform-specific requirements:
|
||||
|
||||
**Discord & Reddit:**
|
||||
- Use markdown formatting (bold, italic, links, lists, etc.)
|
||||
- Include engaging titles that capture attention
|
||||
- Write full, detailed content that tells the complete story
|
||||
- Do NOT use hashtags (these platforms don't use them effectively)
|
||||
- Include clear calls to action
|
||||
|
||||
**Twitter:**
|
||||
- Break content into a thread of individual posts
|
||||
- Each post should be under 280 characters
|
||||
- Posts should flow naturally from one to the next
|
||||
- Use relevant hashtags (2-3 per post maximum)
|
||||
- Make the first post compelling to encourage thread reading
|
||||
- Do NOT include post numbers or thread indicators (e.g., "1/5" or "🧵")
|
||||
|
||||
**BlueSky:**
|
||||
- Break content into a thread of individual posts
|
||||
- Each post should be under 300 characters
|
||||
- Posts should flow naturally from one to the next
|
||||
- Use relevant hashtags sparingly (1-2 per post)
|
||||
- Make the first post compelling to encourage thread reading
|
||||
- Do NOT include post numbers or thread indicators
|
||||
|
||||
**Facebook:**
|
||||
- Plain text format (no markdown)
|
||||
- Professional yet friendly tone
|
||||
- Include relevant hashtags (3-5 total)
|
||||
- Write in a conversational style suitable for a broader audience
|
||||
- Keep it concise but informative
|
||||
|
||||
**LinkedIn:**
|
||||
- Plain text format (no markdown)
|
||||
- More professional tone while maintaining Hikari's personality
|
||||
- Include relevant professional hashtags (3-5 total)
|
||||
- Focus on value proposition and impact
|
||||
- Slightly more formal than other platforms but still engaging
|
||||
|
||||
**Universal requirements:**
|
||||
- All announcements must include a call to action to donate (https://donate.nhcarrigan.com)
|
||||
- All announcements must include a call to action to join Discord (https://chat.nhcarrigan.com)
|
||||
- Adapt the tone and messaging to fit each platform's culture while maintaining Hikari's voice
|
||||
- Ensure all content is accurate and reflects the original announcement notes`;
|
||||
|
||||
const announcementJsonSchema = {
|
||||
additionalProperties: false,
|
||||
properties: {
|
||||
bluesky: {
|
||||
description: "Array of individual BlueSky posts that form a thread. Each post should be under 300 characters and flow naturally from one to the next.",
|
||||
items: {
|
||||
description: "A single BlueSky post in the thread (max 300 characters, no post numbers or thread indicators)",
|
||||
maxLength: 300,
|
||||
type: "string",
|
||||
},
|
||||
minItems: 1,
|
||||
type: "array",
|
||||
},
|
||||
discord: {
|
||||
additionalProperties: false,
|
||||
description: "Discord announcement with title and markdown-formatted content",
|
||||
properties: {
|
||||
content: {
|
||||
description: "Full announcement content formatted with markdown (bold, italic, links, lists, etc.). Should include calls to action for donating and joining Discord.",
|
||||
maxLength: 1900,
|
||||
type: "string",
|
||||
},
|
||||
title: {
|
||||
description: "Engaging title for the Discord announcement (should capture attention and summarize the key point)",
|
||||
maxLength: 256,
|
||||
type: "string",
|
||||
},
|
||||
},
|
||||
required: [ "content", "title" ],
|
||||
type: "object",
|
||||
},
|
||||
facebook: {
|
||||
description: "Plain text announcement for Facebook with relevant hashtags. Should be conversational and suitable for a broader audience. Include calls to action for donating and joining Discord.",
|
||||
type: "string",
|
||||
},
|
||||
linkedin: {
|
||||
description: "Plain text announcement for LinkedIn with professional hashtags. Should maintain Hikari's personality while being slightly more formal. Focus on value proposition and impact. Include calls to action for donating and joining Discord.",
|
||||
type: "string",
|
||||
},
|
||||
reddit: {
|
||||
additionalProperties: false,
|
||||
description: "Reddit announcement with title and markdown-formatted content",
|
||||
properties: {
|
||||
content: {
|
||||
description: "Full announcement content formatted with markdown (bold, italic, links, lists, etc.). Should include calls to action for donating and joining Discord.",
|
||||
type: "string",
|
||||
},
|
||||
title: {
|
||||
description: "Engaging title for the Reddit post (should be clear, informative, and follow Reddit title conventions)",
|
||||
maxLength: 256,
|
||||
type: "string",
|
||||
},
|
||||
},
|
||||
required: [ "content", "title" ],
|
||||
type: "object",
|
||||
},
|
||||
twitter: {
|
||||
description: "Array of individual Twitter posts that form a thread. Each post should be under 280 characters and flow naturally from one to the next.",
|
||||
items: {
|
||||
description: "A single Twitter post in the thread (max 280 characters, no post numbers or thread indicators)",
|
||||
maxLength: 280,
|
||||
type: "string",
|
||||
},
|
||||
minItems: 1,
|
||||
type: "array",
|
||||
},
|
||||
},
|
||||
required: [
|
||||
"bluesky",
|
||||
"discord",
|
||||
"facebook",
|
||||
"linkedin",
|
||||
"reddit",
|
||||
"twitter",
|
||||
],
|
||||
type: "object",
|
||||
};
|
||||
|
||||
export { announcementSystemMessage, announcementJsonSchema };
|
||||
@@ -0,0 +1,24 @@
|
||||
/**
|
||||
* @copyright NHCarrigan
|
||||
* @license Naomi's Public License
|
||||
* @author Naomi Carrigan
|
||||
*/
|
||||
|
||||
/**
|
||||
* This should match the JSON schema for the announcement response.
|
||||
* @see {@link announcementJsonSchema}
|
||||
*/
|
||||
export interface AnnouncementResponse {
|
||||
bluesky: Array<string>;
|
||||
discord: {
|
||||
content: string;
|
||||
title: string;
|
||||
};
|
||||
facebook: string;
|
||||
linkedin: string;
|
||||
reddit: {
|
||||
content: string;
|
||||
title: string;
|
||||
};
|
||||
twitter: Array<string>;
|
||||
}
|
||||
@@ -12,11 +12,16 @@ import { AtpAgent } from "@atproto/api";
|
||||
* @returns A message indicating the success or failure of the operation.
|
||||
*/
|
||||
export const announceOnBluesky = async(
|
||||
content: string,
|
||||
content: Array<string>,
|
||||
): Promise<string> => {
|
||||
if (process.env.BSKY_APP_PASSWORD === undefined) {
|
||||
return "Bluesky credentials are not set.";
|
||||
}
|
||||
const [ firstPost, ...restOfPosts ] = content;
|
||||
const failedReplies: Array<string> = [];
|
||||
if (firstPost === undefined) {
|
||||
return "No posts to send to Bluesky.";
|
||||
}
|
||||
const agent = new AtpAgent({
|
||||
service: "https://bsky.social",
|
||||
});
|
||||
@@ -25,14 +30,30 @@ export const announceOnBluesky = async(
|
||||
password: process.env.BSKY_APP_PASSWORD,
|
||||
});
|
||||
const blueskyRequest = await agent.post({
|
||||
text: content,
|
||||
text: firstPost,
|
||||
}).catch((error: unknown) => {
|
||||
return error instanceof Error
|
||||
? error.message
|
||||
: String(error);
|
||||
});
|
||||
if (typeof blueskyRequest === "string") {
|
||||
return `Failed to send message to Bluesky. ${blueskyRequest}`;
|
||||
return `Failed to send initial post to Bluesky. ${blueskyRequest}`;
|
||||
}
|
||||
return "Successfully sent message to Bluesky.";
|
||||
let { uri } = blueskyRequest;
|
||||
for (const post of restOfPosts) {
|
||||
// eslint-disable-next-line no-await-in-loop -- We need to do this sequentially.
|
||||
const blueskyResponse = await agent.post({
|
||||
replyTo: uri,
|
||||
text: post,
|
||||
});
|
||||
if (typeof blueskyResponse !== "string") {
|
||||
const { uri: replyUri } = blueskyResponse;
|
||||
uri = replyUri;
|
||||
continue;
|
||||
}
|
||||
failedReplies.push(post);
|
||||
}
|
||||
return `Successfully sent initial post to Bluesky. ${failedReplies.length > 0
|
||||
? `Failed to send ${failedReplies.length.toString()} replies: ${failedReplies.join(", ")}`
|
||||
: `All ${(content.length - 1).toString()} replies were sent successfully.`}`;
|
||||
};
|
||||
|
||||
@@ -11,7 +11,7 @@ import { TwitterApi } from "twitter-api-v2";
|
||||
* @param content - The main body of the announcement.
|
||||
* @returns A message indicating the success or failure of the operation.
|
||||
*/
|
||||
export const announceOnTwitter = async(content: string): Promise<string> => {
|
||||
export const announceOnTwitter = async(content: Array<string>): Promise<string> => {
|
||||
if (
|
||||
process.env.TWITTER_CONSUMER_KEY === undefined
|
||||
|| process.env.TWITTER_CONSUMER_SECRET === undefined
|
||||
@@ -27,8 +27,13 @@ export const announceOnTwitter = async(content: string): Promise<string> => {
|
||||
appSecret: process.env.TWITTER_CONSUMER_SECRET,
|
||||
});
|
||||
|
||||
const [ firstPost, ...restOfPosts ] = content;
|
||||
const failedReplies: Array<string> = [];
|
||||
if (firstPost === undefined) {
|
||||
return "No posts to send to Twitter.";
|
||||
}
|
||||
const result = await twitterClient.v2.
|
||||
tweet(content).
|
||||
tweet(firstPost).
|
||||
catch((error: unknown) => {
|
||||
return error instanceof Error
|
||||
? error.message
|
||||
@@ -37,5 +42,18 @@ export const announceOnTwitter = async(content: string): Promise<string> => {
|
||||
if (typeof result === "string") {
|
||||
return `Failed to send message to Twitter. ${result}`;
|
||||
}
|
||||
return "Successfully sent message to Twitter.";
|
||||
let { id } = result.data;
|
||||
for (const post of restOfPosts) {
|
||||
// eslint-disable-next-line no-await-in-loop -- We need to do this sequentially.
|
||||
const twitterResponse = await twitterClient.v2.reply(post, id);
|
||||
if (typeof twitterResponse !== "string") {
|
||||
const { id: replyId } = twitterResponse.data;
|
||||
id = replyId;
|
||||
continue;
|
||||
}
|
||||
failedReplies.push(post);
|
||||
}
|
||||
return `Successfully sent initial post to Twitter. ${failedReplies.length > 0
|
||||
? `Failed to send ${failedReplies.length.toString()} replies: ${failedReplies.join(", ")}`
|
||||
: `All ${(content.length - 1).toString()} replies were sent successfully.`}`;
|
||||
};
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
/**
|
||||
* @copyright nhcarrigan
|
||||
* @license Naomi's Public License
|
||||
* @author Naomi Carrigan
|
||||
*/
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention -- 'Tis a class.
|
||||
import Anthropic from "@anthropic-ai/sdk";
|
||||
import {
|
||||
announcementJsonSchema,
|
||||
announcementSystemMessage,
|
||||
} from "../config/announcements.js";
|
||||
import type { AnnouncementResponse }
|
||||
from "../interfaces/announcementResponse.js";
|
||||
|
||||
/**
|
||||
* Generates announcements for all platforms using AI.
|
||||
* @param content - The main body of the announcement.
|
||||
* @returns The announcements for all platforms, or null if the request fails.
|
||||
*/
|
||||
export const generateAnnouncements = async(
|
||||
content: string,
|
||||
): Promise<AnnouncementResponse | null> => {
|
||||
if (process.env.ANTHROPIC_KEY === undefined) {
|
||||
return null;
|
||||
}
|
||||
const anthropic = new Anthropic({
|
||||
apiKey: process.env.ANTHROPIC_KEY,
|
||||
timeout: 5 * 60 * 1000,
|
||||
});
|
||||
const response = await anthropic.beta.messages.create({
|
||||
betas: [ "structured-outputs-2025-11-13" ],
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention -- API requirement
|
||||
max_tokens: 10_000,
|
||||
messages: [
|
||||
{
|
||||
content: content,
|
||||
role: "user",
|
||||
},
|
||||
],
|
||||
model: "claude-opus-4-5-20251101",
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention -- API requirement
|
||||
output_format: {
|
||||
schema: announcementJsonSchema,
|
||||
type: "json_schema",
|
||||
},
|
||||
system: announcementSystemMessage,
|
||||
});
|
||||
const text = response.content.find((m) => {
|
||||
return m.type === "text";
|
||||
});
|
||||
if (text?.text === undefined) {
|
||||
return null;
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- Being lazy.
|
||||
return JSON.parse(text.text) as AnnouncementResponse;
|
||||
};
|
||||
@@ -1,44 +0,0 @@
|
||||
/**
|
||||
* @copyright nhcarrigan
|
||||
* @license Naomi's Public License
|
||||
* @author Naomi Carrigan
|
||||
*/
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention -- 'Tis a class.
|
||||
import Anthropic from "@anthropic-ai/sdk";
|
||||
|
||||
/**
|
||||
* Summarises an announcement using AI, to condense the content for platforms like Bluesky and Twitter.
|
||||
* @param title - The title of the announcement.
|
||||
* @param content - The main body of the announcement.
|
||||
* @returns A message indicating the success or failure of the operation.
|
||||
*/
|
||||
export const summarisePost = async(
|
||||
title: string,
|
||||
content: string,
|
||||
): Promise<string | null> => {
|
||||
if (process.env.ANTHROPIC_KEY === undefined) {
|
||||
return null;
|
||||
}
|
||||
const anthropic = new Anthropic({
|
||||
apiKey: process.env.ANTHROPIC_KEY,
|
||||
timeout: 5 * 60 * 1000,
|
||||
});
|
||||
const response = await anthropic.messages.create({
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention -- API requirement
|
||||
max_tokens: 1000,
|
||||
messages: [
|
||||
{
|
||||
content: `# ${title}\n\n${content}`,
|
||||
role: "user",
|
||||
},
|
||||
],
|
||||
model: "claude-4-sonnet-20250514",
|
||||
// eslint-disable-next-line stylistic/max-len -- This is a long system message.
|
||||
system: "Summarise the post the user provides into a concise message suitable for social media platforms like Bluesky and Twitter. The summary should be engaging and informative, capturing the essence of the announcement. You may use no more than 280 characters, and should include relevant hashtags if appropriate.",
|
||||
});
|
||||
const text = response.content.find((m) => {
|
||||
return m.type === "text";
|
||||
});
|
||||
return text?.text ?? null;
|
||||
};
|
||||
@@ -10,9 +10,9 @@ import { announceOnBluesky } from "../modules/announceOnBluesky.js";
|
||||
import { announceOnDiscord } from "../modules/announceOnDiscord.js";
|
||||
import { announceOnReddit } from "../modules/announceOnReddit.js";
|
||||
import { announceOnTwitter } from "../modules/announceOnTwitter.js";
|
||||
import { generateAnnouncements } from "../modules/generateAnnouncements.js";
|
||||
import { getIpFromRequest } from "../modules/getIpFromRequest.js";
|
||||
import { summarisePost } from "../modules/summarisePost.js";
|
||||
import { isAnnouncementType } from "../utils/typeguards.js";
|
||||
import { isAnnouncementType, isValidString } from "../utils/typeguards.js";
|
||||
import type { FastifyPluginAsync } from "fastify";
|
||||
|
||||
const oneDay = 24 * 60 * 60 * 1000;
|
||||
@@ -31,21 +31,26 @@ export const announcementRoutes: FastifyPluginAsync = async(server) => {
|
||||
},
|
||||
take: 10,
|
||||
});
|
||||
return await reply.status(200).type("application/json").
|
||||
send(announcements.map((announcement) => {
|
||||
return {
|
||||
content: announcement.content,
|
||||
createdAt: announcement.createdAt,
|
||||
title: announcement.title,
|
||||
type: announcement.type,
|
||||
};
|
||||
}));
|
||||
return await reply.
|
||||
status(200).
|
||||
type("application/json").
|
||||
send(
|
||||
announcements.map((announcement) => {
|
||||
return {
|
||||
content: announcement.content,
|
||||
createdAt: announcement.createdAt,
|
||||
title: announcement.title,
|
||||
type: announcement.type,
|
||||
};
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention -- Fastify requires Body instead of body.
|
||||
server.post<{ Body: { title: string; content: string; type: string } }>(
|
||||
server.post<{ Body: { content: string; type: string } }>(
|
||||
"/announcement",
|
||||
// eslint-disable-next-line complexity -- This is a complex route, but it is necessary to validate the announcement.
|
||||
|
||||
// eslint-disable-next-line max-statements -- This is a long function.
|
||||
async(request, reply) => {
|
||||
const token = request.headers.authorization;
|
||||
if (token === undefined || token !== process.env.ANNOUNCEMENT_TOKEN) {
|
||||
@@ -60,15 +65,8 @@ export const announcementRoutes: FastifyPluginAsync = async(server) => {
|
||||
});
|
||||
}
|
||||
|
||||
const { title, content, type } = request.body;
|
||||
if (
|
||||
typeof title !== "string"
|
||||
|| typeof content !== "string"
|
||||
|| typeof type !== "string"
|
||||
|| title.length === 0
|
||||
|| content.length === 0
|
||||
|| type.length === 0
|
||||
) {
|
||||
const { content, type } = request.body;
|
||||
if (!isValidString(content) || !isValidString(type)) {
|
||||
return await reply.status(400).send({
|
||||
error: "Missing required fields.",
|
||||
});
|
||||
@@ -76,37 +74,45 @@ export const announcementRoutes: FastifyPluginAsync = async(server) => {
|
||||
|
||||
if (!isAnnouncementType(type)) {
|
||||
return await reply.status(400).send({
|
||||
error:
|
||||
`Invalid announcement type. Available types: products, community, company.`,
|
||||
error: `Invalid announcement type. Available types: products, community, company.`,
|
||||
});
|
||||
}
|
||||
|
||||
const announcement = await generateAnnouncements(content);
|
||||
|
||||
if (announcement === null) {
|
||||
return await reply.status(201).send({
|
||||
message: `Failed to generate announcements.`,
|
||||
});
|
||||
}
|
||||
|
||||
const { bluesky, discord, reddit, twitter } = announcement;
|
||||
const { title: discordTitle, content: discordContent } = discord;
|
||||
const { title: redditTitle, content: redditContent } = reddit;
|
||||
|
||||
await database.getInstance().announcements.create({
|
||||
data: {
|
||||
content,
|
||||
title,
|
||||
type,
|
||||
content: discordContent,
|
||||
title: discordTitle,
|
||||
type: type,
|
||||
},
|
||||
});
|
||||
|
||||
const discord = await announceOnDiscord(title, content, type);
|
||||
const reddit = await announceOnReddit(title, content, type);
|
||||
const summary = await summarisePost(title, content);
|
||||
if (summary === null) {
|
||||
return await reply.status(201).send({
|
||||
message: `Announcement processed. Discord: ${discord}, Reddit: ${reddit}, Bluesky: Skipped (AI summarisation failed), Twitter: Skipped (AI summarisation failed).`,
|
||||
});
|
||||
}
|
||||
if (summary.length > 280) {
|
||||
return await reply.status(201).send({
|
||||
message: `Announcement processed. Discord: ${discord}, Reddit: ${reddit}, Bluesky: Skipped (AI summary too long), Twitter: Skipped (AI summary too long).`,
|
||||
});
|
||||
}
|
||||
|
||||
const bluesky = await announceOnBluesky(summary);
|
||||
const twitter = await announceOnTwitter(summary);
|
||||
const discordPost = await announceOnDiscord(
|
||||
discordTitle,
|
||||
discordContent,
|
||||
type,
|
||||
);
|
||||
const redditPost = await announceOnReddit(
|
||||
redditTitle,
|
||||
redditContent,
|
||||
type,
|
||||
);
|
||||
const blueskyPost = await announceOnBluesky(bluesky);
|
||||
const twitterPost = await announceOnTwitter(twitter);
|
||||
return await reply.status(201).send({
|
||||
message: `Announcement processed. Discord: ${discord}, Reddit: ${reddit}, Bluesky: ${bluesky}, Twitter: ${twitter}`,
|
||||
message: `Announcement processed. Discord: ${discordPost}, Reddit: ${redditPost}, Bluesky: ${blueskyPost}, Twitter: ${twitterPost}`,
|
||||
rawPost: announcement,
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
@@ -16,7 +16,7 @@ const isAnnouncementType
|
||||
return [
|
||||
"products",
|
||||
"community",
|
||||
"announcement",
|
||||
"company",
|
||||
].includes(maybeType);
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user