From ac1cad9fb61b6056a2c8895f6389bb07adefe82c Mon Sep 17 00:00:00 2001 From: Travis Shears Date: Tue, 25 Jun 2024 11:46:54 +0200 Subject: [PATCH] add mastodon fetcher --- README.md | 5 ++- serverless.yml | 4 ++ src/mastodon.ts | 108 ++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 116 insertions(+), 1 deletion(-) create mode 100644 src/mastodon.ts diff --git a/README.md b/README.md index cb1ec3c..5559ecf 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ The Micro Blog Repo is made up of a PocketBase db and these lambda functions. To - [X] Post support - [X] Post with image support - [X] [Pixelfed via gram.social](https://gram.social/i/web/profile/703621281309160235) -- [ ] [Mastodon via dice.camp](https://dice.camp/@travisshears) +- [X] [Mastodon via dice.camp](https://dice.camp/@travisshears) - [ ] [Nostr profile1qqs9....](https://snort.social/nprofile1qqs9udcv9uhqggjz87js9rtaph4lajlxnxsvwvm7zwdjt6etzyk52rgpypmhxue69uhkummnw3ezuetfde6kuer6wasku7nfvuh8xurpvdjj7qgkwaehxw309ajkgetw9ehx7um5wghxcctwvshszrnhwden5te0dehhxtnvdakz7z94haj) @@ -32,7 +32,10 @@ $ yarn exec serverless invoke --function bluesky ```shell +$ nvm use $ yarn exec serverless invoke local --function bluesky ``` +_note: insure you are on the correct version of nodejs, 20_ + There is also the new `serverless dev` command. I still need to check that out. diff --git a/serverless.yml b/serverless.yml index c6734e5..e7694d3 100644 --- a/serverless.yml +++ b/serverless.yml @@ -24,3 +24,7 @@ functions: handler: src/bluesky.run events: - schedule: rate(1 hour) + mastodon: + handler: src/mastodon.run + events: + - schedule: rate(1 hour) diff --git a/src/mastodon.ts b/src/mastodon.ts new file mode 100644 index 0000000..802983e --- /dev/null +++ b/src/mastodon.ts @@ -0,0 +1,108 @@ +import pino from "pino"; +import { lambdaRequestTracker, pinoLambdaDestination } from "pino-lambda"; +import { MicroBlogBackend } from "./pocketbase"; + +// custom destination formatter +const destination = pinoLambdaDestination(); +const logger = pino({}, destination); +const withRequest = lambdaRequestTracker(); + +const pb = new MicroBlogBackend(logger); + +const baseURL = "https://dice.camp"; +const accountId = "112364858295724350"; +const username = "travisshears"; + +type MastodonPost = { + media_attachments: { + type: string; //'image', + url: string; + description: string; // 'Blurry gate', + }[]; + id: string; + content: string; + account: { + id: string; + }; + created_at: string; + tags: { name: string }[]; +}; + +const getPostUntilId = async ({ + lastSavedId, + maxId, + carryPosts = [], +}: { + lastSavedId?: string; + maxId?: string; + carryPosts?: MastodonPost[]; +}): Promise => { + const params = new URLSearchParams(); + params.append("limit", "10"); + const urlWithParams = new URL( + `${baseURL}/api/v1/accounts/${accountId}/statuses` + ); + urlWithParams.search = params.toString(); + + const res = await fetch(urlWithParams); + const posts = (await res.json()) as MastodonPost[]; + const containsId = posts.some((post) => post.id === lastSavedId); + + if (!containsId && posts.length >= 5) { + return getPostUntilId({ + lastSavedId, + carryPosts: carryPosts?.concat(posts), + maxId: posts[posts.length - 1]?.id, + }); + } + + const allPosts = carryPosts?.concat(posts).reverse(); + if (lastSavedId) { + const index = allPosts.findIndex((post) => post.id === lastSavedId); + return allPosts.slice(index + 1); + } + return allPosts; +}; + +const savePost = async (post: MastodonPost) => { + const postData = { + remoteId: post.id, + authorId: post.account.id, + posted: post.created_at, + source: "mastodon" as const, + fullPost: post, + }; + return await pb.savePost(postData); +}; + +const saveTags = async (post: MastodonPost, postId: string) => { + logger.info({ tags: post.tags }, "saving tags"); + for (const tag of post.tags) { + await pb.setTag(tag.name, postId); + } +}; + +const saveImages = async (post: MastodonPost, postId: string) => { + logger.info({ images: post.media_attachments }, "saving images"); + for (const image of post.media_attachments) { + await pb.saveAndSetImage( + { remoteURL: image.url, alt: image.description }, + postId + ); + } +}; + +exports.run = async (event: any, context: any) => { + withRequest(event, context); + const lastSavedPostId = await pb.getLatestPostRemoteIDBySource("mastodon"); + console.log({ lastSavedPostId }); + const posts = await getPostUntilId({ lastSavedId: lastSavedPostId }); + console.log({ posts }); + + for (const post of posts) { + logger.info({ post }, "saving post"); + const savedNewPost = await savePost(post); + await saveTags(post, savedNewPost.id); + await saveImages(post, savedNewPost.id); + } +};