223 lines
5.8 KiB
TypeScript
223 lines
5.8 KiB
TypeScript
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"
|
|
| "nostr";
|
|
|
|
export type MicroBlogPost = {
|
|
source: MicroBlogPostSource;
|
|
fullPost: any;
|
|
remoteId: string;
|
|
authorId: string;
|
|
id: string;
|
|
posted: string;
|
|
expand: {
|
|
images?: MicroBlogPostImage[];
|
|
tags?: {
|
|
id: string;
|
|
}[];
|
|
};
|
|
};
|
|
|
|
export class MicroBlogBackend {
|
|
private pb: PocketBase;
|
|
private clientSetTime?: Date;
|
|
constructor(private logger: Logger) {
|
|
this.pb = new PocketBase(process.env.POCKET_BASE_HOST);
|
|
}
|
|
|
|
private async login() {
|
|
const pw = process.env.POCKET_BASE_PW;
|
|
const userName = process.env.POCKET_BASE_USER;
|
|
if (!pw) {
|
|
this.logger.error("POCKET_BASE_PW env var not set");
|
|
throw new Error("POCKET_BASE_PW env var not set");
|
|
}
|
|
if (!userName) {
|
|
this.logger.error("POCKET_BASE_USER env var not set");
|
|
throw new Error("POCKET_BASE_USER env var not set");
|
|
}
|
|
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 getLatestPostBySource(
|
|
postSource: MicroBlogPostSource,
|
|
): Promise<MicroBlogPost | undefined> {
|
|
await this.checkLogin();
|
|
try {
|
|
const post = await this.pb
|
|
.collection<MicroBlogPost>("micro_blog_posts")
|
|
.getFirstListItem(`source = '${postSource}'`, { sort: "-posted" });
|
|
return post;
|
|
} catch (error: any) {
|
|
if (error.status === 404) {
|
|
return undefined;
|
|
}
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
public async getLatestPostRemoteIDBySource(postSource: MicroBlogPostSource) {
|
|
await this.checkLogin();
|
|
const post = await this.getLatestPostBySource(postSource);
|
|
return post?.remoteId;
|
|
}
|
|
|
|
async getTag(tag: string): Promise<MicroBlogPostTag | undefined> {
|
|
await this.checkLogin();
|
|
try {
|
|
const remoteTag = await this.pb
|
|
.collection<MicroBlogPostTag>("micro_blog_tags")
|
|
.getFirstListItem(`tag = '${tag}'`);
|
|
return remoteTag;
|
|
} catch (e: any) {
|
|
if (e.status === 404) {
|
|
return undefined;
|
|
}
|
|
throw e;
|
|
}
|
|
}
|
|
|
|
async getImageByRemoteURL(
|
|
remoteURL: string,
|
|
): Promise<MicroBlogPostImage | undefined> {
|
|
await this.checkLogin();
|
|
try {
|
|
const remoteImage = await this.pb
|
|
.collection<MicroBlogPostImage>("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<MicroBlogPost | undefined> {
|
|
await this.checkLogin();
|
|
try {
|
|
return await this.pb
|
|
.collection<MicroBlogPost>("micro_blog_posts")
|
|
.getFirstListItem(`remoteId = '${remoteId}'`);
|
|
} catch (e: any) {
|
|
if (e.status === 404) {
|
|
return undefined;
|
|
}
|
|
throw e;
|
|
}
|
|
}
|
|
|
|
public async savePost(
|
|
post: Omit<MicroBlogPost, "id" | "expand">,
|
|
): Promise<MicroBlogPost> {
|
|
await this.checkLogin();
|
|
const existingPost = await this.checkForPost(post.remoteId);
|
|
if (!existingPost) {
|
|
return await this.pb
|
|
.collection<MicroBlogPost>("micro_blog_posts")
|
|
.create(post);
|
|
}
|
|
this.logger.info({ existingPost }, "Found existing post");
|
|
return existingPost;
|
|
}
|
|
|
|
public async setTag(rawTag: string, postId: string) {
|
|
await this.checkLogin();
|
|
let tag = await this.getTag(rawTag);
|
|
if (!tag) {
|
|
tag = await this.pb
|
|
.collection<MicroBlogPostTag>("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<MicroBlogPostImage, "id" | "image" | "collectionId">,
|
|
postId: string,
|
|
) {
|
|
await this.checkLogin();
|
|
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<MicroBlogPostTag>("micro_blog_images")
|
|
.create(data);
|
|
this.logger.info({ image }, "Created image");
|
|
}
|
|
if (!image) {
|
|
throw new Error("Failed to create image");
|
|
}
|
|
const res = await this.pb.collection("micro_blog_posts").update(postId, {
|
|
"images+": image.id,
|
|
});
|
|
this.logger.info({ res }, "Updated post with image");
|
|
}
|
|
|
|
public async getPosts(page: number, limit = 20) {
|
|
await this.checkLogin();
|
|
const resultList = await this.pb
|
|
.collection<MicroBlogPost>("micro_blog_posts")
|
|
.getList(page, limit, {
|
|
sort: "-posted",
|
|
expand: "images,tags",
|
|
filter: `(source = "blue_sky" || source = "pleroma")`,
|
|
// filter: 'source = "pleroma"',
|
|
});
|
|
return resultList;
|
|
}
|
|
}
|