import type { Logger } from "pino"; import PocketBase from "pocketbase"; export type MicroBlogPostImage = { id: string; collectionId: string; image: string; alt?: string; remoteURL: string; }; export type MicroBlogPostTag = { id: string; tag: string; }; export type MicroBlogPostSource = | "blue_sky" | "mastodon" | "pleroma" | "pixelfed"; export type MicroBlogPost = { source: MicroBlogPostSource; fullPost: any; remoteId: string; authorId: string; id: string; expand: { images?: MicroBlogPostImage[]; tags?: { id: string; }[]; }; }; export class MicroBlogBackend { private pb: PocketBase; private clientSetTime?: Date; constructor(private logger: Logger) { this.pb = new PocketBase("https://personal-pocket-base.fly.dev"); } private async login() { const pw = process.env.POCKET_BASE_PW!; const userName = process.env.POCKET_BASE_USER!; this.logger.info({ userName }, "Logging in to pocketbase"); await this.pb.collection("users").authWithPassword(userName, pw); this.clientSetTime = new Date(); } private async checkLogin() { if (!this.clientSetTime) { await this.login(); return; } const now = new Date(); const diff = now.getTime() - this.clientSetTime.getTime(); const day = 86_400_000; if (diff > day) { await this.login(); return; } } public async getLatestPostId( postSource: MicroBlogPostSource ): Promise { await this.checkLogin(); try { const post = await this.pb .collection("micro_blog_posts") .getFirstListItem(`source = '${postSource}'`, { sort: "-posted" }); return post.id; } catch (error: any) { if (error.status === 404) { return undefined; } throw error; } } async getTag(tag: string): Promise { await this.checkLogin(); try { const remoteTag = await this.pb .collection("micro_blog_tags") .getFirstListItem(`tag = '${tag}'`); return remoteTag; } catch (e: any) { if (e.status === 404) { return undefined; } throw e; } } async getImageByRemoteURL( remoteURL: string ): Promise { await this.checkLogin(); try { const remoteImage = await this.pb .collection("micro_blog_images") .getFirstListItem(`remoteURL = '${remoteURL}'`); return remoteImage; } catch (e: any) { if (e.status === 404) { return undefined; } throw e; } } private async checkForPost( remoteId: string ): Promise { await this.checkLogin(); try { return await this.pb .collection("micro_blog_posts") .getFirstListItem(`remoteId = '${remoteId}'`); } catch (e: any) { if (e.status === 404) { return undefined; } throw e; } } public async savePost( post: Omit ): Promise { const existingPost = await this.checkForPost(post.remoteId); if (!existingPost) { return await this.pb .collection("micro_blog_posts") .create(post); } this.logger.info({ existingPost }, "Found existing post"); return existingPost; } public async setTag(rawTag: string, postId: string) { let tag = await this.getTag(rawTag); if (!tag) { tag = await this.pb .collection("micro_blog_tags") .create({ tag: rawTag }); } if (!tag) { throw new Error("Failed to create tag"); } await this.pb.collection("micro_blog_posts").update(postId, { "tags+": tag.id, }); } public async saveAndSetImage( imageToSave: Omit, postId: string ) { let image = await this.getImageByRemoteURL(imageToSave.remoteURL); if (!image) { const imageResponse = await fetch(imageToSave.remoteURL); if (!imageResponse.ok) { throw new Error("Failed to download image"); } const imageBlob = await imageResponse.blob(); const imageFile = new File([imageBlob], "image.jpg", { type: imageBlob.type, }); const data = { ...imageToSave, image: imageFile, }; image = await this.pb .collection("micro_blog_images") .create(data); } if (!image) { throw new Error("Failed to create image"); } await this.pb.collection("micro_blog_posts").update(postId, { "images+": image.id, }); } public async getPosts(page: number, limit = 20) { await this.checkLogin(); const resultList = await this.pb .collection("micro_blog_posts") .getList(page, limit, { sort: "-posted", expand: "images,tags", filter: `(source = "blue_sky" || source = "pleroma")`, // filter: 'source = "pleroma"', }); return resultList; } }