generated from nhcarrigan/template
feat: post to bluesky
This commit is contained in:
Generated
+87
@@ -112,6 +112,12 @@ importers:
|
||||
|
||||
server:
|
||||
dependencies:
|
||||
'@anthropic-ai/sdk':
|
||||
specifier: 0.56.0
|
||||
version: 0.56.0
|
||||
'@atproto/api':
|
||||
specifier: 0.15.26
|
||||
version: 0.15.26
|
||||
'@fastify/cors':
|
||||
specifier: 11.0.1
|
||||
version: 11.0.1
|
||||
@@ -286,6 +292,21 @@ packages:
|
||||
resolution: {integrity: sha512-SLCB8M8+VMg1cpCucnA1XWHGWqVSZtIWzmOdDOEu3eTFZMB+A0sGZ1ESO5MHDnqrNTXz3safMrWx9x4rMZSOqA==}
|
||||
hasBin: true
|
||||
|
||||
'@atproto/api@0.15.26':
|
||||
resolution: {integrity: sha512-AdXGjeCpLZiP9YMGi4YOdK1ayqkBhklmGfSG8UefqR6tTHth59PZvYs5KiwLnFhedt2Xljt3eUlhkn14Y48wEA==}
|
||||
|
||||
'@atproto/common-web@0.4.2':
|
||||
resolution: {integrity: sha512-vrXwGNoFGogodjQvJDxAeP3QbGtawgZute2ed1XdRO0wMixLk3qewtikZm06H259QDJVu6voKC5mubml+WgQUw==}
|
||||
|
||||
'@atproto/lexicon@0.4.12':
|
||||
resolution: {integrity: sha512-fcEvEQ1GpQYF5igZ4IZjPWEoWVpsEF22L9RexxLS3ptfySXLflEyH384e7HITzO/73McDeaJx3lqHIuqn9ulnw==}
|
||||
|
||||
'@atproto/syntax@0.4.0':
|
||||
resolution: {integrity: sha512-b9y5ceHS8YKOfP3mdKmwAx5yVj9294UN7FG2XzP6V5aKUdFazEYRnR9m5n5ZQFKa3GNvz7de9guZCJ/sUTcOAA==}
|
||||
|
||||
'@atproto/xrpc@0.7.1':
|
||||
resolution: {integrity: sha512-ANHEzlskYlMEdH18m+Itp3a8d0pEJao2qoDybDoMupTnoeNkya4VKIaOgAi6ERQnqatBBZyn9asW+7rJmSt/8g==}
|
||||
|
||||
'@babel/code-frame@7.27.1':
|
||||
resolution: {integrity: sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
@@ -1904,6 +1925,9 @@ packages:
|
||||
avvio@9.1.0:
|
||||
resolution: {integrity: sha512-fYASnYi600CsH/j9EQov7lECAniYiBFiiAtBNuZYLA2leLe9qOvZzqYHFjtIj6gD2VMoMLP14834LFWvr4IfDw==}
|
||||
|
||||
await-lock@2.2.2:
|
||||
resolution: {integrity: sha512-aDczADvlvTGajTDjcjpJMqRkOF6Qdz3YbPZm/PyW6tKPkx2hlYBzxMhEywM/tU72HrVZjgl5VCdRuMlA7pZ8Gw==}
|
||||
|
||||
balanced-match@1.0.2:
|
||||
resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
|
||||
|
||||
@@ -3208,6 +3232,9 @@ packages:
|
||||
resolution: {integrity: sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==}
|
||||
engines: {node: '>=16'}
|
||||
|
||||
iso-datestring-validator@2.2.2:
|
||||
resolution: {integrity: sha512-yLEMkBbLZTlVQqOnQ4FiMujR6T4DEcCb1xizmvXS+OxuhwcbtynoosRzdMA69zZCShCNAbi+gJ71FxZBBXx1SA==}
|
||||
|
||||
istanbul-lib-coverage@3.2.2:
|
||||
resolution: {integrity: sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==}
|
||||
engines: {node: '>=8'}
|
||||
@@ -3600,6 +3627,9 @@ packages:
|
||||
msgpackr@1.11.4:
|
||||
resolution: {integrity: sha512-uaff7RG9VIC4jacFW9xzL3jc0iM32DNHe4jYVycBcjUePT/Klnfj7pqtWJt9khvDFizmjN2TlYniYmSS2LIaZg==}
|
||||
|
||||
multiformats@9.9.0:
|
||||
resolution: {integrity: sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg==}
|
||||
|
||||
mute-stream@1.0.0:
|
||||
resolution: {integrity: sha512-avsJQhyd+680gKXyG/sQc0nXaC6rBkPOfyHYcFb9+hdkqQkR9bdnkJ0AMZhke0oesPqIO+mFFJ+IdBc7mst4IA==}
|
||||
engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
|
||||
@@ -4467,6 +4497,10 @@ packages:
|
||||
resolution: {integrity: sha512-t2T/WLB2WRgZ9EpE4jgPJ9w+i66UZfDc8wHh0xrwiRNN+UwH98GIJkTeZqX9rg0i0ptwzqW+uYeIF0T4F8LR7A==}
|
||||
engines: {node: '>=14.0.0'}
|
||||
|
||||
tlds@1.259.0:
|
||||
resolution: {integrity: sha512-AldGGlDP0PNgwppe2quAvuBl18UcjuNtOnDuUkqhd6ipPqrYYBt3aTxK1QTsBVknk97lS2JcafWMghjGWFtunw==}
|
||||
hasBin: true
|
||||
|
||||
tmp@0.0.33:
|
||||
resolution: {integrity: sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==}
|
||||
engines: {node: '>=0.6.0'}
|
||||
@@ -4607,6 +4641,9 @@ packages:
|
||||
ufo@1.6.1:
|
||||
resolution: {integrity: sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA==}
|
||||
|
||||
uint8arrays@3.0.0:
|
||||
resolution: {integrity: sha512-HRCx0q6O9Bfbp+HHSfQQKD7wU70+lydKVt4EghkdOvlK/NlrF90z+eXV34mUd48rNvVJXwkrMSPpCATkct8fJA==}
|
||||
|
||||
unbox-primitive@1.1.0:
|
||||
resolution: {integrity: sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==}
|
||||
engines: {node: '>= 0.4'}
|
||||
@@ -4922,6 +4959,9 @@ packages:
|
||||
resolution: {integrity: sha512-cYVsTjKl8b+FrnidjibDWskAv7UKOfcwaVZdp/it9n1s9fU3IkgDbhdIRKCW4JDsAlECJY0ytoVPT3sK6kideA==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
zod@3.25.76:
|
||||
resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==}
|
||||
|
||||
zone.js@0.15.1:
|
||||
resolution: {integrity: sha512-XE96n56IQpJM7NAoXswY3XRLcWFW83xe0BiAOeMD7K5k5xecOeul3Qcpx6GqEeeHNkW5DWL5zOyTbEfB4eti8w==}
|
||||
|
||||
@@ -5103,6 +5143,39 @@ snapshots:
|
||||
|
||||
'@anthropic-ai/sdk@0.56.0': {}
|
||||
|
||||
'@atproto/api@0.15.26':
|
||||
dependencies:
|
||||
'@atproto/common-web': 0.4.2
|
||||
'@atproto/lexicon': 0.4.12
|
||||
'@atproto/syntax': 0.4.0
|
||||
'@atproto/xrpc': 0.7.1
|
||||
await-lock: 2.2.2
|
||||
multiformats: 9.9.0
|
||||
tlds: 1.259.0
|
||||
zod: 3.25.76
|
||||
|
||||
'@atproto/common-web@0.4.2':
|
||||
dependencies:
|
||||
graphemer: 1.4.0
|
||||
multiformats: 9.9.0
|
||||
uint8arrays: 3.0.0
|
||||
zod: 3.25.76
|
||||
|
||||
'@atproto/lexicon@0.4.12':
|
||||
dependencies:
|
||||
'@atproto/common-web': 0.4.2
|
||||
'@atproto/syntax': 0.4.0
|
||||
iso-datestring-validator: 2.2.2
|
||||
multiformats: 9.9.0
|
||||
zod: 3.25.76
|
||||
|
||||
'@atproto/syntax@0.4.0': {}
|
||||
|
||||
'@atproto/xrpc@0.7.1':
|
||||
dependencies:
|
||||
'@atproto/lexicon': 0.4.12
|
||||
zod: 3.25.76
|
||||
|
||||
'@babel/code-frame@7.27.1':
|
||||
dependencies:
|
||||
'@babel/helper-validator-identifier': 7.27.1
|
||||
@@ -6794,6 +6867,8 @@ snapshots:
|
||||
'@fastify/error': 4.2.0
|
||||
fastq: 1.19.1
|
||||
|
||||
await-lock@2.2.2: {}
|
||||
|
||||
balanced-match@1.0.2: {}
|
||||
|
||||
base64id@2.0.0: {}
|
||||
@@ -8410,6 +8485,8 @@ snapshots:
|
||||
|
||||
isexe@3.1.1: {}
|
||||
|
||||
iso-datestring-validator@2.2.2: {}
|
||||
|
||||
istanbul-lib-coverage@3.2.2: {}
|
||||
|
||||
istanbul-lib-instrument@5.2.1:
|
||||
@@ -8909,6 +8986,8 @@ snapshots:
|
||||
msgpackr-extract: 3.0.3
|
||||
optional: true
|
||||
|
||||
multiformats@9.9.0: {}
|
||||
|
||||
mute-stream@1.0.0: {}
|
||||
|
||||
mute-stream@2.0.0: {}
|
||||
@@ -9938,6 +10017,8 @@ snapshots:
|
||||
|
||||
tinyspy@4.0.3: {}
|
||||
|
||||
tlds@1.259.0: {}
|
||||
|
||||
tmp@0.0.33:
|
||||
dependencies:
|
||||
os-tmpdir: 1.0.2
|
||||
@@ -10073,6 +10154,10 @@ snapshots:
|
||||
ufo@1.6.1:
|
||||
optional: true
|
||||
|
||||
uint8arrays@3.0.0:
|
||||
dependencies:
|
||||
multiformats: 9.9.0
|
||||
|
||||
unbox-primitive@1.1.0:
|
||||
dependencies:
|
||||
call-bound: 1.0.4
|
||||
@@ -10384,4 +10469,6 @@ snapshots:
|
||||
|
||||
yoctocolors-cjs@2.1.2: {}
|
||||
|
||||
zod@3.25.76: {}
|
||||
|
||||
zone.js@0.15.1: {}
|
||||
|
||||
@@ -16,6 +16,8 @@
|
||||
"license": "ISC",
|
||||
"packageManager": "pnpm@10.12.3",
|
||||
"dependencies": {
|
||||
"@anthropic-ai/sdk": "0.56.0",
|
||||
"@atproto/api": "0.15.26",
|
||||
"@fastify/cors": "11.0.1",
|
||||
"@nhcarrigan/logger": "1.0.0",
|
||||
"@prisma/client": "6.11.1",
|
||||
|
||||
@@ -7,3 +7,5 @@ REDDIT_CLIENT_ID="op://Environment Variables - Naomi/Hikari/reddit_client_id"
|
||||
REDDIT_CLIENT_SECRET="op://Environment Variables - Naomi/Hikari/reddit_client_secret"
|
||||
REDDIT_PASSWORD="op://Environment Variables - Naomi/Hikari/reddit_password"
|
||||
REDDIT_USERNAME="op://Environment Variables - Naomi/Hikari/reddit_username"
|
||||
BSKY_APP_PASSWORD="op://Environment Variables - Naomi/Hikari/bsky_password"
|
||||
ANTHROPIC_KEY="op://Environment Variables - Naomi/Hikari/anthropic_key"
|
||||
@@ -0,0 +1,38 @@
|
||||
/**
|
||||
* @copyright nhcarrigan
|
||||
* @license Naomi's Public License
|
||||
* @author Naomi Carrigan
|
||||
*/
|
||||
|
||||
import { AtpAgent } from "@atproto/api";
|
||||
|
||||
/**
|
||||
* Forwards an announcement to our Bluesky account.
|
||||
* @param content - The main body of the announcement.
|
||||
* @returns A message indicating the success or failure of the operation.
|
||||
*/
|
||||
export const announceOnBluesky = async(
|
||||
content: string,
|
||||
): Promise<string> => {
|
||||
if (process.env.BSKY_APP_PASSWORD === undefined) {
|
||||
return "Bluesky credentials are not set.";
|
||||
}
|
||||
const agent = new AtpAgent({
|
||||
service: "https://bsky.social",
|
||||
});
|
||||
await agent.login({
|
||||
identifier: "nhcarrigan.com",
|
||||
password: process.env.BSKY_APP_PASSWORD,
|
||||
});
|
||||
const blueskyRequest = await agent.post({
|
||||
text: content,
|
||||
}).catch((error: unknown) => {
|
||||
return error instanceof Error
|
||||
? error.message
|
||||
: String(error);
|
||||
});
|
||||
if (typeof blueskyRequest === "string") {
|
||||
return `Failed to send message to Bluesky. ${blueskyRequest}`;
|
||||
}
|
||||
return "Successfully sent message to Bluesky.";
|
||||
};
|
||||
@@ -0,0 +1,43 @@
|
||||
/**
|
||||
* @copyright nhcarrigan
|
||||
* @license Naomi's Public License
|
||||
* @author Naomi Carrigan
|
||||
*/
|
||||
|
||||
import Anthropic from "@anthropic-ai/sdk";
|
||||
|
||||
/**
|
||||
* Summarises an announcement using AI, to condense the content for platforms like Bluesky and Twitter.
|
||||
* @param title
|
||||
* @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;
|
||||
};
|
||||
@@ -6,10 +6,12 @@
|
||||
|
||||
import { blockedIps } from "../cache/blockedIps.js";
|
||||
import { database } from "../db/database.js";
|
||||
import { announceOnBluesky } from "../modules/announceOnBluesky.js";
|
||||
import { announceOnDiscord } from "../modules/announceOnDiscord.js";
|
||||
import { announceOnForum } from "../modules/announceOnForum.js";
|
||||
import { announceOnReddit } from "../modules/announceOnReddit.js";
|
||||
import { getIpFromRequest } from "../modules/getIpFromRequest.js";
|
||||
import { summarisePost } from "../modules/summarisePost.js";
|
||||
import type { FastifyPluginAsync } from "fastify";
|
||||
|
||||
const oneDay = 24 * 60 * 60 * 1000;
|
||||
@@ -42,7 +44,7 @@ export const announcementRoutes: FastifyPluginAsync = async(server) => {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention -- Fastify requires Body instead of body.
|
||||
server.post<{ Body: { title: string; 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 complexity, max-statements -- This is a complex route, but it is necessary to validate the announcement.
|
||||
async(request, reply) => {
|
||||
const token = request.headers.authorization;
|
||||
if (token === undefined || token !== process.env.ANNOUNCEMENT_TOKEN) {
|
||||
@@ -104,8 +106,21 @@ export const announcementRoutes: FastifyPluginAsync = async(server) => {
|
||||
const discord = await announceOnDiscord(title, content, type);
|
||||
const forum = await announceOnForum(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}, Forum: ${forum}, Reddit: ${reddit}, Bluesky: Skipped (AI summarisation failed).`,
|
||||
});
|
||||
}
|
||||
if (summary.length > 280) {
|
||||
return await reply.status(201).send({
|
||||
message: `Announcement processed. Discord: ${discord}, Forum: ${forum}, Reddit: ${reddit}, Bluesky: Skipped (AI summary too long).`,
|
||||
});
|
||||
}
|
||||
|
||||
const bluesky = await announceOnBluesky(summary);
|
||||
return await reply.status(201).send({
|
||||
message: `Announcement processed. Discord: ${discord}, Forum: ${forum}, Reddit: ${reddit}`,
|
||||
message: `Announcement processed. Discord: ${discord}, Forum: ${forum}, Reddit: ${reddit}, Bluesky: ${bluesky}`,
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user