From 1b03a0da670066aa3395c6b8c79718cb26ec40e7 Mon Sep 17 00:00:00 2001 From: Fairpost Date: Sun, 3 May 2026 10:51:09 +0200 Subject: [PATCH 01/10] feat: Move Soure.getSource() to Feed - is owner --- src/models/Feed.ts | 16 ++++++++++++---- src/models/Platform.ts | 2 +- src/models/Source.ts | 29 ++--------------------------- 3 files changed, 15 insertions(+), 32 deletions(-) diff --git a/src/models/Feed.ts b/src/models/Feed.ts index a85fdaf..b44d7fe 100644 --- a/src/models/Feed.ts +++ b/src/models/Feed.ts @@ -132,7 +132,8 @@ export default class Feed { }); for await (const file of files) { - const source = await Source.getSource(this, basename(file.path)); + const sourceId = Source.getSourceId(file.path); + const source = await this.getSource(sourceId); this.cache[source.id] = source; sources.push(source); } @@ -162,8 +163,15 @@ export default class Feed { if (id in this.cache) { return this.cache[id]; } - const source = await Source.getSource(this, id, stage); - this.cache[source.id] = source; - return source; + const stages = stage ? [stage] : Object.values(SourceStage); + for (const stage of stages) { + const sourcePath = Source.getSourcePath(this, id, stage); + if (await this.user.files.isDir(sourcePath)) { + const source = new Source(this, sourcePath); + this.cache[source.id] = source; + return source; + } + } + throw this.user.log.error("getSource", "Source not found: " + id, stage); } } diff --git a/src/models/Platform.ts b/src/models/Platform.ts index 71b3f3f..48a91fa 100644 --- a/src/models/Platform.ts +++ b/src/models/Platform.ts @@ -498,7 +498,7 @@ export default class Platform { this.user.log.trace("Platform", this.id, "publishDuePost", dryrun); const post = await this.getDuePost(sources); if (post) { - await post.publish(dryrun); + await this.publishPost(post,dryrun); return post; } } diff --git a/src/models/Source.ts b/src/models/Source.ts index 4331fe5..d567bce 100644 --- a/src/models/Source.ts +++ b/src/models/Source.ts @@ -39,7 +39,7 @@ export default class Source { */ constructor(feed: Feed, path: string) { this.feed = feed; - this.id = this.getSourceId(path); + this.id = Source.getSourceId(path); this.path = path; this.mapper = new SourceMapper(this); this.stage = this.getSourceStage(); @@ -67,7 +67,7 @@ export default class Source { * @param path the path for the new or existing source * @returns the id for the new or existing source */ - public getSourceId(path: string): string { + public static getSourceId(path: string): string { return basename(path); // ah, simple } @@ -90,31 +90,6 @@ export default class Source { return SourceStage.UNKNOWN; } - /** - * getSource - * - * get a new source and do some async checks. - * @param feed - the feed this source belongs to - * @param id - the id of the source - * @param stage - optional stage to find the source in - * @returns new source object - */ - - public static async getSource( - feed: Feed, - id: string, - stage?: SourceStage, - ): Promise { - const stages = stage ? [stage] : Object.values(SourceStage); - for (const stage of stages) { - const sourcePath = Source.getSourcePath(feed, id, stage); - if (await feed.user.files.isDir(sourcePath)) { - return new Source(feed, sourcePath); - } - } - throw feed.user.log.error("getSource", "No source in stage: " + id, stage); - } - /** * Update the stage of a source. * From 2b04a44886e740527d926d7f8950599170669c50 Mon Sep 17 00:00:00 2001 From: Fairpost Date: Sun, 3 May 2026 10:52:16 +0200 Subject: [PATCH 02/10] chore: Lint fix --- package.json | 2 +- src/models/Platform.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 3894721..457f181 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "fairpost", - "version": "4.0.0", + "version": "5.0.0", "type": "module", "engines": { "node": "22.17.0" diff --git a/src/models/Platform.ts b/src/models/Platform.ts index 48a91fa..ff6c5f3 100644 --- a/src/models/Platform.ts +++ b/src/models/Platform.ts @@ -498,7 +498,7 @@ export default class Platform { this.user.log.trace("Platform", this.id, "publishDuePost", dryrun); const post = await this.getDuePost(sources); if (post) { - await this.publishPost(post,dryrun); + await this.publishPost(post, dryrun); return post; } } From 0ba83130a4e4337791d0275ab1622ae334e63679 Mon Sep 17 00:00:00 2001 From: Fairpost Date: Sun, 3 May 2026 11:03:47 +0200 Subject: [PATCH 03/10] feat: Move other static Source.* to Feed - is owner --- src/models/Feed.ts | 28 +++++++++++++++++++++++++--- src/models/Source.ts | 30 ++---------------------------- 2 files changed, 27 insertions(+), 31 deletions(-) diff --git a/src/models/Feed.ts b/src/models/Feed.ts index b44d7fe..43536c0 100644 --- a/src/models/Feed.ts +++ b/src/models/Feed.ts @@ -7,7 +7,7 @@ import { basename } from "path"; /** * Feed - the sources handler of fairpost * - * The feed is a container of sources. The sources + * The feed is the owner of sources. The sources * path is set by USER_FEEDPATH. Every dir in there, * if not starting with _ or ., is a source. * @@ -78,6 +78,27 @@ export default class Feed { return this.path + "/" + stageFolder; } + /** + * getSourcePath + * + * Get the path for a source in this feed, based on stage and id + * @param id - the id of the source + * @param stage - the stage of the source + * @returns the path to the source + */ + public getSourcePath(id: string, stage: SourceStage): string { + return this.getStagePath(stage) + "/" + id; + } + + /** + * get source id based on the path of a source + * @param path the path for the new or existing source + * @returns the id for the new or existing source + */ + public getSourceId(path: string): string { + return basename(path); // ah, simple + } + /** * Get multiple sources * @param sourceIds optional array of ids of source you want to get @@ -132,7 +153,7 @@ export default class Feed { }); for await (const file of files) { - const sourceId = Source.getSourceId(file.path); + const sourceId = this.getSourceId(file.path); const source = await this.getSource(sourceId); this.cache[source.id] = source; sources.push(source); @@ -152,6 +173,7 @@ export default class Feed { return sources; } } + /** * Get one source, and use a local cache. * @param id - id of the source @@ -165,7 +187,7 @@ export default class Feed { } const stages = stage ? [stage] : Object.values(SourceStage); for (const stage of stages) { - const sourcePath = Source.getSourcePath(this, id, stage); + const sourcePath = this.getSourcePath(id, stage); if (await this.user.files.isDir(sourcePath)) { const source = new Source(this, sourcePath); this.cache[source.id] = source; diff --git a/src/models/Source.ts b/src/models/Source.ts index d567bce..4c69033 100644 --- a/src/models/Source.ts +++ b/src/models/Source.ts @@ -39,38 +39,12 @@ export default class Source { */ constructor(feed: Feed, path: string) { this.feed = feed; - this.id = Source.getSourceId(path); + this.id = feed.getSourceId(path); this.path = path; this.mapper = new SourceMapper(this); this.stage = this.getSourceStage(); } - /** - * getSourcePath - * - * Get the path for a source in a feed, based on stage and id - * @param feed - the feed this source belongs to - * @param id - the id of the source - * @param stage - the stage of the source - * @returns the path to the source - */ - public static getSourcePath( - feed: Feed, - id: string, - stage: SourceStage, - ): string { - return feed.getStagePath(stage) + "/" + id; - } - - /** - * get source id based on the path of a source - * @param path the path for the new or existing source - * @returns the id for the new or existing source - */ - public static getSourceId(path: string): string { - return basename(path); // ah, simple - } - /** * Get the stage of this source. * @@ -170,7 +144,7 @@ export default class Source { } } - const newPath = Source.getSourcePath(this.feed, newId, newStage); + const newPath = this.feed.getSourcePath(newId, newStage); if (await this.feed.user.files.exists(newPath)) { this.feed.user.log.error( this.id, From 5253391396e26e631372c86be6a44885e48258d9 Mon Sep 17 00:00:00 2001 From: Fairpost Date: Sun, 3 May 2026 12:01:33 +0200 Subject: [PATCH 04/10] feat: Create and export PostFactory Post does not belong to platform or source, but to both --- docs/Fairpost.md | 5 +- src/models/Platform.ts | 4 +- src/models/Post.ts | 197 +++++++++++++++++++---------------------- src/models/Source.ts | 6 +- 4 files changed, 99 insertions(+), 113 deletions(-) diff --git a/docs/Fairpost.md b/docs/Fairpost.md index ed1c4a1..a583b95 100644 --- a/docs/Fairpost.md +++ b/docs/Fairpost.md @@ -37,11 +37,14 @@ const post = source.getPost(platform); ## Post Status and Source Stage +A Post has a `status` in the publishing flow, +like 'scheduled' or 'canceled'. + The stage of a source depends on the statuses of all posts in the source. The first stage is `incoming`, the final stage is `archived`. The path to the source folder, containing all -the posts, is defined by the soure stage. +the posts, is defined by the source stage. ## DTOs diff --git a/src/models/Platform.ts b/src/models/Platform.ts index ff6c5f3..cccde13 100644 --- a/src/models/Platform.ts +++ b/src/models/Platform.ts @@ -6,7 +6,7 @@ import { FieldMapping, SourceStage, PostStatus } from "../types/index.ts"; import Source from "./Source.ts"; import Operator from "./Operator.ts"; import Plugin from "./Plugin.ts"; -import Post from "./Post.ts"; +import Post, { PostFactory } from "./Post.ts"; import User from "./User.ts"; /** @@ -201,7 +201,7 @@ export default class Platform { const postId = this.getPostId(source); if (!(postId in this.cache)) { this.user.log.trace("Platform", this.id, "getPost", source.id); - const post = await Post.getPost(this, source); + const post = await PostFactory.resolve(this, source); this.cache[postId] = post; } return this.cache[postId]; diff --git a/src/models/Post.ts b/src/models/Post.ts index c0dc816..12993e1 100644 --- a/src/models/Post.ts +++ b/src/models/Post.ts @@ -1,4 +1,5 @@ import { FileGroup, FileInfo, PostStatus, PostResult } from "../types/index.ts"; +import User from "./User.ts"; import Source from "./Source.ts"; import Platform from "./Platform.ts"; import { isSimilarArray } from "../utilities.ts"; @@ -7,7 +8,7 @@ import PostMapper from "../mappers/PostMapper.ts"; /** * Post - a post within a source * - * A post belongs to one platform and one source; + * A post belongs to both one platform and one source; * it is *prepared* and later *published* by the platform. * The post serializes to a json file in the source, * where it can be read later for further processing. @@ -21,12 +22,13 @@ import PostMapper from "../mappers/PostMapper.ts"; */ export default class Post { id: string; + user: User; source: Source; platform: Platform; valid: boolean = false; status: PostStatus = PostStatus.UNKNOWN; + originalStatus: PostStatus = PostStatus.UNKNOWN; prepared: boolean = false; - private originalStatus: PostStatus = PostStatus.UNKNOWN; scheduled?: Date; published?: Date; results: PostResult[] = []; @@ -43,57 +45,36 @@ export default class Post { /** * Dont call the constructor yourself; - * instead, call `await Post.getPost()` + * instead, call `await PostFactory.resolve()` * @param platform * @param source */ constructor(platform: Platform, source: Source) { + if (platform.user !== source.feed.user) { + source.feed.user.log.error( + "Creating source post from wrong platform", + platform.id, + source.id, + ); + throw platform.user.log.error( + "Creating platform post from wrong source", + platform.id, + source.id, + ); + } + this.user = platform.user; this.id = platform.getPostId(source); this.platform = platform; this.source = source; this.mapper = new PostMapper(this); } - /** - * getPost - * - * get a new post and load the async data. - * @param platform - the platform this post belongs to - * @param source - the source this post is derived from - * @returns new post object - */ - static async getPost(platform: Platform, source: Source): Promise { - const post = new Post(platform, source); - const postFilePath = platform.getPostFilePath(source); - if (!(await platform.user.files.exists(postFilePath))) { - return post; - } - const contents = await platform.user.files.readFile(postFilePath); - const data = JSON.parse(contents); - if (!data) { - throw platform.user.log.error( - "Cant parse post ", - post.id, - post.source.id, - ); - } - Object.assign(post, data); - post.id = platform.getPostId(source); - post.prepared = true; - post.scheduled = post.scheduled ? new Date(post.scheduled) : undefined; - post.published = post.published ? new Date(post.published) : undefined; - post.ignoreFiles = post.ignoreFiles ?? []; - post.originalStatus = post.status; - - return post; - } - /** * Save this post to disk */ async save() { - this.platform.user.log.trace("Post", "save"); + this.user.log.trace("Post", "save"); // eslint-disable-next-line @typescript-eslint/no-explicit-any const data = { ...this } as { [key: string]: any }; delete data.source; @@ -101,19 +82,19 @@ export default class Post { delete data.mapper; delete data.prepared; delete data.originalStatus; - await this.platform.user.files.write( + await this.user.files.write( this.platform.getPostFilePath(this.source), JSON.stringify(data, null, "\t"), ); if (this.originalStatus !== this.status) { // update the source status if necessary - // note, this may *move* the source and all posts + // note, this may *move* the source and all posts in it const originalSourceStage = this.source.stage; const newSourceStage = await this.source.updateStage(); // update the users report - const report = await this.platform.user.getReport(); + const report = await this.user.getReport(); if (report.platforms[this.platform.id]) { if (!report.platforms[this.platform.id]?.count[this.originalStatus]) { report.platforms[this.platform.id]!.count[this.originalStatus] = 1; @@ -135,13 +116,8 @@ export default class Post { report.feed.count[newSourceStage]!++; } // save the report - await this.platform.user.putReport(report); - this.platform.user.log.trace( - "Post", - this.id, - "save", - "updated user report", - ); + await this.user.putReport(report); + this.user.log.trace("Post", this.id, "save", "updated user report"); } // all up to date this.originalStatus = this.status; @@ -164,15 +140,15 @@ export default class Post { */ async prepare() { - this.platform.user.log.trace("Post", "prepare"); + this.user.log.trace("Post", "prepare"); // purge non-existing files and // update existing files if (!this.prepared) { const assetsPath = this.getFilePath(this.platform.assetsFolder); - if (!(await this.platform.user.files.exists(assetsPath))) { - await this.platform.user.files.mkdir(assetsPath); + if (!(await this.user.files.exists(assetsPath))) { + await this.user.files.mkdir(assetsPath); } } else { await this.purgeFiles(); @@ -194,44 +170,36 @@ export default class Post { const textFiles = this.getFiles(FileGroup.TEXT); if (this.hasFile("body.txt")) { - this.body = await this.platform.user.files.readFile( - this.getFilePath("body.txt"), - ); + this.body = await this.user.files.readFile(this.getFilePath("body.txt")); } else if (textFiles.length === 1) { const bodyFile = textFiles[0].name; - this.body = await this.platform.user.files.readFile( - this.getFilePath(bodyFile), - ); + this.body = await this.user.files.readFile(this.getFilePath(bodyFile)); } else { this.body = this.platform.defaultBody; } if (this.hasFile("title.txt")) { - this.title = await this.platform.user.files.readFile( + this.title = await this.user.files.readFile( this.getFilePath("title.txt"), ); } else if (this.hasFile("subject.txt")) { - this.title = await this.platform.user.files.readFile( + this.title = await this.user.files.readFile( this.getFilePath("subject.txt"), ); } if (this.hasFile("tags.txt")) { this.tags = ( - await this.platform.user.files.readFile(this.getFilePath("tags.txt")) + await this.user.files.readFile(this.getFilePath("tags.txt")) ).split(/\s/); } if (this.hasFile("mentions.txt")) { this.mentions = this.mentions = ( - await this.platform.user.files.readFile( - this.getFilePath("mentions.txt"), - ) + await this.user.files.readFile(this.getFilePath("mentions.txt")) ).split(/\s/); } if (this.hasFile("geo.txt")) { - this.geo = await this.platform.user.files.readFile( - this.getFilePath("geo.txt"), - ); + this.geo = await this.user.files.readFile(this.getFilePath("geo.txt")); } // decompile the body to see if there are @@ -257,33 +225,33 @@ export default class Post { */ async setStatus(status: PostStatus) { - this.platform.user.log.trace("Post", "setStatus", status); + this.user.log.trace("Post", "setStatus", status); if (!this.prepared) { - throw this.platform.user.log.error("Post is not prepared"); + throw this.user.log.error("Post is not prepared"); } if (!this.valid) { - throw this.platform.user.log.error("Post is not valid"); + throw this.user.log.error("Post is not valid"); } if (this.status === status) { - throw this.platform.user.log.error("Post already on status " + status); + throw this.user.log.error("Post already on status " + status); } - this.platform.user.log.warn("Changing post status to " + status); + this.user.log.warn("Changing post status to " + status); switch (status) { case PostStatus.UNSCHEDULED: - this.platform.user.log.warn("Removing scheduled and published dates"); + this.user.log.warn("Removing scheduled and published dates"); delete this.scheduled; delete this.published; break; case PostStatus.SCHEDULED: - this.platform.user.log.warn( + this.user.log.warn( "Resetting scheduled date, removing published date, r", ); this.scheduled = this.scheduled || new Date(); delete this.published; break; case PostStatus.PUBLISHED: - this.platform.user.log.warn("Resetting scheduled and published dates"); + this.user.log.warn("Resetting scheduled and published dates"); this.scheduled = this.scheduled || new Date(); this.published = this.published || new Date(); break; @@ -300,18 +268,18 @@ export default class Post { */ async schedule(date: Date) { - this.platform.user.log.trace("Post", "schedule", date); + this.user.log.trace("Post", "schedule", date); if (!this.prepared) { - throw this.platform.user.log.error("Post is not prepared"); + throw this.user.log.error("Post is not prepared"); } if (!this.valid) { - throw this.platform.user.log.error("Post is not valid"); + throw this.user.log.error("Post is not valid"); } if (this.status === PostStatus.CANCELED) { - throw this.platform.user.log.error("Post has status canceled"); + throw this.user.log.error("Post has status canceled"); } if (this.status !== PostStatus.UNSCHEDULED) { - this.platform.user.log.warn("Rescheduling post"); + this.user.log.warn("Rescheduling post"); } this.scheduled = date; this.status = PostStatus.SCHEDULED; @@ -328,22 +296,22 @@ export default class Post { * @returns boolean if success */ async publish(dryrun: boolean): Promise { - this.platform.user.log.trace("Post", "publish"); + this.user.log.trace("Post", "publish"); if (!this.prepared) { - throw this.platform.user.log.error("Post is not prepared"); + throw this.user.log.error("Post is not prepared"); } if (!this.valid) { - throw this.platform.user.log.error("Post is not valid", this.id); + throw this.user.log.error("Post is not valid", this.id); } if (this.status === PostStatus.CANCELED) { - throw this.platform.user.log.error("Post has status canceled", this.id); + throw this.user.log.error("Post has status canceled", this.id); } if (this.published) { - throw this.platform.user.log.error("Post was already published", this.id); + throw this.user.log.error("Post was already published", this.id); } // why ? // if (!dryrun) post.schedule(now); - this.platform.user.log.info("Publishing", this.id); + this.user.log.info("Publishing", this.id); return await this.platform.publishPost(this, dryrun); } @@ -525,11 +493,8 @@ export default class Post { */ async purgeFiles() { for (const file of this.getFiles()) { - if ( - file.original && - !(await this.platform.user.files.exists(file.original)) - ) { - this.platform.user.log.info( + if (file.original && !(await this.user.files.exists(file.original))) { + this.user.log.info( "Post", "purgeFiles", "purging non-existant derivate", @@ -537,10 +502,8 @@ export default class Post { ); this.removeFile(file.name); } - if ( - !(await this.platform.user.files.exists(this.getFilePath(file.name))) - ) { - this.platform.user.log.info( + if (!(await this.user.files.exists(this.getFilePath(file.name)))) { + this.user.log.info( "Post", "purgeFiles", "purging non-existent file", @@ -597,15 +560,11 @@ export default class Post { if (!this.files) { this.files = []; } - this.platform.user.log.trace("Post.addFile", newFile); + this.user.log.trace("Post.addFile", newFile); this.files.push(newFile); return newFile; } else { - this.platform.user.log.warn( - "Post.addFile", - "Not replacing existing file", - name, - ); + this.user.log.warn("Post.addFile", "Not replacing existing file", name); } } @@ -647,7 +606,7 @@ export default class Post { search: string, replace: string, ): Promise { - this.platform.user.log.trace("Post.replaceFile", search, replace); + this.user.log.trace("Post.replaceFile", search, replace); const index = this.files?.findIndex((file) => file.name === search) ?? -1; if (index > -1) { const oldFile = this.getFile(search); @@ -658,11 +617,7 @@ export default class Post { return this.files[index]; } } else { - this.platform.user.log.warn( - "Post.replaceFile", - "metadata not found", - search, - ); + this.user.log.warn("Post.replaceFile", "metadata not found", search); } } @@ -692,7 +647,7 @@ export default class Post { this.results.push(result); if (result.error) { - this.platform.user.log.warn( + this.user.log.warn( "Post.processResult", this.id, "failed", @@ -716,3 +671,31 @@ export default class Post { return result.success; } } + +export class PostFactory { + static async resolve(platform: Platform, source: Source): Promise { + const post = new Post(platform, source); + const postFilePath = platform.getPostFilePath(source); + if (!(await platform.user.files.exists(postFilePath))) { + // post doesnt exist yet - tis a new post + return post; + } + const contents = await platform.user.files.readFile(postFilePath); + const data = JSON.parse(contents); + if (!data) { + throw platform.user.log.error( + "Cant parse post ", + post.id, + post.source.id, + ); + } + Object.assign(post, data); + post.id = platform.getPostId(source); + post.prepared = true; + post.scheduled = post.scheduled ? new Date(post.scheduled) : undefined; + post.published = post.published ? new Date(post.published) : undefined; + post.ignoreFiles = post.ignoreFiles ?? []; + post.originalStatus = post.status; + return post; + } +} diff --git a/src/models/Source.ts b/src/models/Source.ts index 4c69033..e417a17 100644 --- a/src/models/Source.ts +++ b/src/models/Source.ts @@ -9,7 +9,7 @@ import { FileGroup, } from "../types/index.ts"; import Platform from "./Platform.ts"; -import Post from "./Post.ts"; +import Post, { PostFactory } from "./Post.ts"; import SourceMapper from "../mappers/SourceMapper.ts"; /** @@ -269,7 +269,7 @@ export default class Source { this.id, platform.id, ); - return await platform.getPost(this); + return await PostFactory.resolve(platform, this); } /** @@ -290,7 +290,7 @@ export default class Source { } for (const platform of platforms) { try { - const post = await this.getPost(platform); + const post = await PostFactory.resolve(platform, this); if (!status || status === post.status) { posts.push(post); } From b1d22de3b846cb5af533c70ed5ce1be0166cb641 Mon Sep 17 00:00:00 2001 From: Fairpost Date: Sun, 3 May 2026 12:25:18 +0200 Subject: [PATCH 05/10] feat: Remove source.preparePost() --- src/models/Source.ts | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/src/models/Source.ts b/src/models/Source.ts index e417a17..3d51c1e 100644 --- a/src/models/Source.ts +++ b/src/models/Source.ts @@ -240,22 +240,6 @@ export default class Source { return file; } - /** - * preparePost - * this is just an alias of Platform.preparePost(source) - */ - - public async preparePost(platform: Platform): Promise { - this.feed.user.log.trace( - "Source", - this.id, - "preparePost", - this.id, - platform.id, - ); - return await platform.preparePost(this); - } - /** * getPost * this is just an alias of Platform.getPost(source) From 675f6ea17fa63a4fd37ea43334d3ce529880c7fb Mon Sep 17 00:00:00 2001 From: Fairpost Date: Sun, 3 May 2026 15:12:09 +0200 Subject: [PATCH 06/10] feat: Remove Platform super.preparePost .. .. call post.prepare directly --- docs/NewPlatform.md | 10 +-- src/models/Platform.ts | 28 ++----- src/models/Post.ts | 11 +++ src/models/Source.ts | 12 +-- src/platforms/Bluesky/Bluesky.ts | 48 ++++++------ src/platforms/Facebook/Facebook.ts | 28 +++---- src/platforms/Instagram/Instagram.ts | 48 ++++++------ src/platforms/LinkedIn/LinkedIn.ts | 28 +++---- src/platforms/Reddit/Reddit.ts | 112 +++++++++++++-------------- src/platforms/Twitter/Twitter.ts | 96 +++++++++++------------ src/platforms/YouTube/YouTube.ts | 28 +++---- 11 files changed, 220 insertions(+), 229 deletions(-) diff --git a/docs/NewPlatform.md b/docs/NewPlatform.md index 02ad42c..0011f6c 100644 --- a/docs/NewPlatform.md +++ b/docs/NewPlatform.md @@ -35,11 +35,11 @@ export default class FooBar extends Platform { /** @inheritdoc */ async preparePost(source: Source): Promise { - const post = await super.preparePost(source); - if (post) { - // prepare your post here - await post.save(); - } + const post = await this.getPost(source); + await post.prepare(); + // prepare your platform specific stuff here + // that includes plugins ... + await post.save(); return post; } diff --git a/src/models/Platform.ts b/src/models/Platform.ts index cccde13..f97a9d9 100644 --- a/src/models/Platform.ts +++ b/src/models/Platform.ts @@ -343,8 +343,7 @@ export default class Platform { * Prepare a post for this platform for the * given source. If it doesn't exist, create it. * - * Override this in your own platform, but - * always call super.preparePost() + * Override this in your own platform ! * * If the post exists and is published, ignores it. * If the post exists and is failed, sets it back to @@ -357,27 +356,16 @@ export default class Platform { * before, and manually adapted later. For example, * post.status may have manually been set to canceled. * @param source - the source for which to prepare a post for this platform - * @param save - wether to save the post already * @returns the prepared post */ - async preparePost(source: Source, save?: true): Promise { + async preparePost(source: Source): Promise { this.user.log.trace("Platform", this.id, "preparePost"); - const post = await this.getPost(source); - if (post.status === PostStatus.PUBLISHED) { - return post; - } - await post.prepare(); - if (post.status === PostStatus.UNKNOWN) { - post.status = PostStatus.UNSCHEDULED; - } - if (post.status === PostStatus.FAILED) { - post.status = PostStatus.UNSCHEDULED; - } - if (save) { - await post.save(); - } - - return post; + throw this.user.log.error( + "Prepare not implemented for " + + this.id + + ". Read the docs in the docs folder.", + source.id, + ); } /** diff --git a/src/models/Post.ts b/src/models/Post.ts index 12993e1..63f6fb2 100644 --- a/src/models/Post.ts +++ b/src/models/Post.ts @@ -77,6 +77,7 @@ export default class Post { this.user.log.trace("Post", "save"); // eslint-disable-next-line @typescript-eslint/no-explicit-any const data = { ...this } as { [key: string]: any }; + delete data.user; delete data.source; delete data.platform; delete data.mapper; @@ -142,6 +143,10 @@ export default class Post { async prepare() { this.user.log.trace("Post", "prepare"); + if (this.status === PostStatus.PUBLISHED) { + return; + } + // purge non-existing files and // update existing files @@ -212,6 +217,12 @@ export default class Post { if (this.title) { this.valid = true; } + if (this.status === PostStatus.UNKNOWN) { + this.status = PostStatus.UNSCHEDULED; + } + if (this.status === PostStatus.FAILED) { + this.status = PostStatus.UNSCHEDULED; + } // done } diff --git a/src/models/Source.ts b/src/models/Source.ts index 3d51c1e..ff3040e 100644 --- a/src/models/Source.ts +++ b/src/models/Source.ts @@ -241,18 +241,12 @@ export default class Source { } /** - * getPost - * this is just an alias of Platform.getPost(source) + * Get a single post from this source + * @param platform - platform for the post */ public async getPost(platform: Platform): Promise { - this.feed.user.log.trace( - "Source", - this.id, - "getPost", - this.id, - platform.id, - ); + this.feed.user.log.trace("Source", "getPost", this.id, platform.id); return await PostFactory.resolve(platform, this); } diff --git a/src/platforms/Bluesky/Bluesky.ts b/src/platforms/Bluesky/Bluesky.ts index 2096adb..ac835b3 100644 --- a/src/platforms/Bluesky/Bluesky.ts +++ b/src/platforms/Bluesky/Bluesky.ts @@ -97,34 +97,34 @@ export default class Bluesky extends Platform { async preparePost(source: Source): Promise { this.user.log.trace("Bluesky.preparePost", source.id); - const post = await super.preparePost(source); - if (post) { - const userPluginSettings = JSON.parse( - this.user.data.get("settings", "BLUESKY_PLUGIN_SETTINGS", "{}"), - ); - const pluginSettings = { - ...this.pluginSettings, - ...(userPluginSettings || {}), - }; - const plugins = this.loadPlugins(pluginSettings); - for (const plugin of plugins) { - try { - await plugin.process(post); - } catch { - post.valid = false; - } + const post = await this.getPost(source); + await post.prepare(); + const userPluginSettings = JSON.parse( + this.user.data.get("settings", "BLUESKY_PLUGIN_SETTINGS", "{}"), + ); + const pluginSettings = { + ...this.pluginSettings, + ...(userPluginSettings || {}), + }; + const plugins = this.loadPlugins(pluginSettings); + for (const plugin of plugins) { + try { + await plugin.process(post); + } catch { + post.valid = false; } + } - // Annimated GIF will be sent as a video. Only one animated GIF can be sent per post. + // Annimated GIF will be sent as a video. Only one animated GIF can be sent per post. - // video - // Supported formats: MP4. - // Duration max: 4 minutes. - // Duration min: 1 second. - // Aspect ratio must be between 1:3 and 3:1. + // video + // Supported formats: MP4. + // Duration max: 4 minutes. + // Duration min: 1 second. + // Aspect ratio must be between 1:3 and 3:1. + + await post.save(); - await post.save(); - } return post; } diff --git a/src/platforms/Facebook/Facebook.ts b/src/platforms/Facebook/Facebook.ts index 3e3457a..cf4a471 100644 --- a/src/platforms/Facebook/Facebook.ts +++ b/src/platforms/Facebook/Facebook.ts @@ -104,21 +104,21 @@ export default class Facebook extends Platform { /** @inheritdoc */ async preparePost(source: Source): Promise { this.user.log.trace("Facebook.preparePost", source.id); - const post = await super.preparePost(source); - if (post && post.files) { - const userPluginSettings = JSON.parse( - this.user.data.get("settings", "FACEBOOK_PLUGIN_SETTINGS", "{}"), - ); - const pluginSettings = { - ...this.pluginSettings, - ...(userPluginSettings || {}), - }; - const plugins = this.loadPlugins(pluginSettings); - for (const plugin of plugins) { - await plugin.process(post); - } - await post.save(); + const post = await this.getPost(source); + await post.prepare(); + const userPluginSettings = JSON.parse( + this.user.data.get("settings", "FACEBOOK_PLUGIN_SETTINGS", "{}"), + ); + const pluginSettings = { + ...this.pluginSettings, + ...(userPluginSettings || {}), + }; + const plugins = this.loadPlugins(pluginSettings); + for (const plugin of plugins) { + await plugin.process(post); } + await post.save(); + return post; } diff --git a/src/platforms/Instagram/Instagram.ts b/src/platforms/Instagram/Instagram.ts index a2b2d4a..bec20f5 100644 --- a/src/platforms/Instagram/Instagram.ts +++ b/src/platforms/Instagram/Instagram.ts @@ -103,32 +103,32 @@ export default class Instagram extends Platform { /** @inheritdoc */ async preparePost(source: Source): Promise { this.user.log.trace("Instagram.preparePost", source.id); - const post = await super.preparePost(source); - if (post && post.files) { - // instagram: require media - if ( - post.getFiles(FileGroup.IMAGE).length + - post.getFiles(FileGroup.VIDEO).length === - 0 - ) { - post.valid = false; - } - if (post.valid) { - const userPluginSettings = JSON.parse( - this.user.data.get("settings", "INSTAGRAM_PLUGIN_SETTINGS", "{}"), - ); - const pluginSettings = { - ...this.pluginSettings, - ...(userPluginSettings || {}), - }; - const plugins = this.loadPlugins(pluginSettings); - for (const plugin of plugins) { - await plugin.process(post); - } + const post = await this.getPost(source); + await post.prepare(); + // instagram: require media + if ( + post.getFiles(FileGroup.IMAGE).length + + post.getFiles(FileGroup.VIDEO).length === + 0 + ) { + post.valid = false; + } + if (post.valid) { + const userPluginSettings = JSON.parse( + this.user.data.get("settings", "INSTAGRAM_PLUGIN_SETTINGS", "{}"), + ); + const pluginSettings = { + ...this.pluginSettings, + ...(userPluginSettings || {}), + }; + const plugins = this.loadPlugins(pluginSettings); + for (const plugin of plugins) { + await plugin.process(post); } - - await post.save(); } + + await post.save(); + return post; } diff --git a/src/platforms/LinkedIn/LinkedIn.ts b/src/platforms/LinkedIn/LinkedIn.ts index 13bab4c..d407687 100644 --- a/src/platforms/LinkedIn/LinkedIn.ts +++ b/src/platforms/LinkedIn/LinkedIn.ts @@ -104,21 +104,21 @@ export default class LinkedIn extends Platform { /** @inheritdoc */ async preparePost(source: Source): Promise { this.user.log.trace("LinkedIn.preparePost", source.id); - const post = await super.preparePost(source); - if (post) { - const userPluginSettings = JSON.parse( - this.user.data.get("settings", "LINKEDIN_PLUGIN_SETTINGS", "{}"), - ); - const pluginSettings = { - ...this.pluginSettings, - ...(userPluginSettings || {}), - }; - const plugins = this.loadPlugins(pluginSettings); - for (const plugin of plugins) { - await plugin.process(post); - } - await post.save(); + const post = await this.getPost(source); + + const userPluginSettings = JSON.parse( + this.user.data.get("settings", "LINKEDIN_PLUGIN_SETTINGS", "{}"), + ); + const pluginSettings = { + ...this.pluginSettings, + ...(userPluginSettings || {}), + }; + const plugins = this.loadPlugins(pluginSettings); + for (const plugin of plugins) { + await plugin.process(post); } + await post.save(); + return post; } diff --git a/src/platforms/Reddit/Reddit.ts b/src/platforms/Reddit/Reddit.ts index 427a26f..9041977 100644 --- a/src/platforms/Reddit/Reddit.ts +++ b/src/platforms/Reddit/Reddit.ts @@ -124,67 +124,67 @@ export default class Reddit extends Platform { /** @inheritdoc */ async preparePost(source: Source): Promise { this.user.log.trace("Reddit.preparePost", source.id); - const post = await super.preparePost(source); - if (post) { - // TODO: extract video thumbnail - let videoposter = ""; - if (post.hasFiles(FileGroup.VIDEO)) { - let srcposter = ""; - const dstposter = this.assetsFolder + "/reddit-poster.png"; - const posters = post - .getFiles(FileGroup.IMAGE) - .filter((file) => file.basename === "poster"); - if (posters.length) { - srcposter = posters[0].name; - } else if (post.hasFiles(FileGroup.IMAGE)) { - // copy the first image to poster - srcposter = post.getFiles(FileGroup.IMAGE)[0].name; - } else { - // create a poster using ffmpeg - try { - throw this.user.log.error( - "video poster.jpg missing - thumbnails not implemented", - ); - // https://creatomate.com/blog/how-to-use-ffmpeg-in-nodejs - // const video = post.getFiles('video')[0]; - // this.user.log.trace("Reddit.preparePost", "creating thumbnail", video.name, dstposter); - // this.generateThumbnail(post.getFilePath(video.name),post.getFilePath(dstposter)); - } catch { - post.valid = false; - } - } - if (srcposter) { - // copy that file to its dest - this.user.log.trace( - "Reddit.preparePost", - "copying poster", - srcposter, - dstposter, - ); - await this.user.files.copy( - post.getFilePath(srcposter), - post.getFilePath(dstposter), + const post = await this.getPost(source); + await post.prepare(); + // TODO: extract video thumbnail + let videoposter = ""; + if (post.hasFiles(FileGroup.VIDEO)) { + let srcposter = ""; + const dstposter = this.assetsFolder + "/reddit-poster.png"; + const posters = post + .getFiles(FileGroup.IMAGE) + .filter((file) => file.basename === "poster"); + if (posters.length) { + srcposter = posters[0].name; + } else if (post.hasFiles(FileGroup.IMAGE)) { + // copy the first image to poster + srcposter = post.getFiles(FileGroup.IMAGE)[0].name; + } else { + // create a poster using ffmpeg + try { + throw this.user.log.error( + "video poster.jpg missing - thumbnails not implemented", ); - post.removeFiles(FileGroup.IMAGE); - videoposter = dstposter; + // https://creatomate.com/blog/how-to-use-ffmpeg-in-nodejs + // const video = post.getFiles('video')[0]; + // this.user.log.trace("Reddit.preparePost", "creating thumbnail", video.name, dstposter); + // this.generateThumbnail(post.getFilePath(video.name),post.getFilePath(dstposter)); + } catch { + post.valid = false; } } - const userPluginSettings = JSON.parse( - this.user.data.get("settings", "REDDIT_PLUGIN_SETTINGS", "{}"), - ); - const pluginSettings = { - ...this.pluginSettings, - ...(userPluginSettings || {}), - }; - const plugins = this.loadPlugins(pluginSettings); - for (const plugin of plugins) { - await plugin.process(post); - } - if (videoposter) { - await post.addFile(videoposter); + if (srcposter) { + // copy that file to its dest + this.user.log.trace( + "Reddit.preparePost", + "copying poster", + srcposter, + dstposter, + ); + await this.user.files.copy( + post.getFilePath(srcposter), + post.getFilePath(dstposter), + ); + post.removeFiles(FileGroup.IMAGE); + videoposter = dstposter; } - await post.save(); } + const userPluginSettings = JSON.parse( + this.user.data.get("settings", "REDDIT_PLUGIN_SETTINGS", "{}"), + ); + const pluginSettings = { + ...this.pluginSettings, + ...(userPluginSettings || {}), + }; + const plugins = this.loadPlugins(pluginSettings); + for (const plugin of plugins) { + await plugin.process(post); + } + if (videoposter) { + await post.addFile(videoposter); + } + await post.save(); + return post; } diff --git a/src/platforms/Twitter/Twitter.ts b/src/platforms/Twitter/Twitter.ts index 51586ae..0b1cdec 100644 --- a/src/platforms/Twitter/Twitter.ts +++ b/src/platforms/Twitter/Twitter.ts @@ -113,63 +113,61 @@ export default class Twitter extends Platform { /** @inheritdoc */ async preparePost(source: Source): Promise { this.user.log.trace("Twitter.preparePost", source.id); - const post = await super.preparePost(source); - if (post) { - const userPluginSettings = JSON.parse( - this.user.data.get("settings", "TWITTER_PLUGIN_SETTINGS", "{}"), - ); - const pluginSettings = { - ...this.pluginSettings, - ...(userPluginSettings || {}), - }; - const plugins = this.loadPlugins(pluginSettings); - for (const plugin of plugins) { - await plugin.process(post); - } + const post = await this.getPost(source); + await post.prepare(); + const userPluginSettings = JSON.parse( + this.user.data.get("settings", "TWITTER_PLUGIN_SETTINGS", "{}"), + ); + const pluginSettings = { + ...this.pluginSettings, + ...(userPluginSettings || {}), + }; + const plugins = this.loadPlugins(pluginSettings); + for (const plugin of plugins) { + await plugin.process(post); + } - // remove files whose mime are not supported, - // this could be a plugin - for (const file of post.getFiles()) { - if ( - !Object.values(EUploadMimeType).includes( - file.mimetype as EUploadMimeType, - ) - ) { - this.user.log.trace( - "Removing unsupported file type: " + file.mimetype, - ); - post.removeFile(file.name); - } + // remove files whose mime are not supported, + // this could be a plugin + for (const file of post.getFiles()) { + if ( + !Object.values(EUploadMimeType).includes( + file.mimetype as EUploadMimeType, + ) + ) { + this.user.log.trace("Removing unsupported file type: " + file.mimetype); + post.removeFile(file.name); } + } - // limit the post body to 140 characters - // this could be a plugin - const charLimit = 140; - if (post.body && post.body.length >= charLimit) { - const splitBody = post.body.match(/[^.\n]+[.\n]*|[.\n]+/g); - if (splitBody) { - let newBody = ""; - let nextLine = splitBody.shift(); - while (nextLine && newBody.length + nextLine.length < charLimit) { - newBody += nextLine; - nextLine = splitBody.shift(); - } - if (newBody !== "") { - post.body = newBody; - } + // limit the post body to 140 characters + // this could be a plugin + const charLimit = 140; + if (post.body && post.body.length >= charLimit) { + const splitBody = post.body.match(/[^.\n]+[.\n]*|[.\n]+/g); + if (splitBody) { + let newBody = ""; + let nextLine = splitBody.shift(); + while (nextLine && newBody.length + nextLine.length < charLimit) { + newBody += nextLine; + nextLine = splitBody.shift(); } - if (post.body.length >= charLimit) { - post.body = post.body.substring(0, charLimit - 4) + "..."; + if (newBody !== "") { + post.body = newBody; } } - - // twitter requires a real body or images - if (!post.body && !post.hasFiles(FileGroup.IMAGE)) { - this.user.log.warn("Twitter post has no body"); - post.valid = false; + if (post.body.length >= charLimit) { + post.body = post.body.substring(0, charLimit - 4) + "..."; } - await post.save(); } + + // twitter requires a real body or images + if (!post.body && !post.hasFiles(FileGroup.IMAGE)) { + this.user.log.warn("Twitter post has no body"); + post.valid = false; + } + await post.save(); + return post; } diff --git a/src/platforms/YouTube/YouTube.ts b/src/platforms/YouTube/YouTube.ts index 5b97b19..9bbb6e1 100644 --- a/src/platforms/YouTube/YouTube.ts +++ b/src/platforms/YouTube/YouTube.ts @@ -104,21 +104,21 @@ export default class YouTube extends Platform { /** @inheritdoc */ async preparePost(source: Source): Promise { this.user.log.trace("YouTube.preparePost", source.id); - const post = await super.preparePost(source); - if (post) { - const userPluginSettings = JSON.parse( - this.user.data.get("settings", "YOUTUBE_PLUGIN_SETTINGS", "{}"), - ); - const pluginSettings = { - ...this.pluginSettings, - ...(userPluginSettings || {}), - }; - const plugins = this.loadPlugins(pluginSettings); - for (const plugin of plugins) { - await plugin.process(post); - } - await post.save(); + const post = await this.getPost(source); + await post.prepare(); + const userPluginSettings = JSON.parse( + this.user.data.get("settings", "YOUTUBE_PLUGIN_SETTINGS", "{}"), + ); + const pluginSettings = { + ...this.pluginSettings, + ...(userPluginSettings || {}), + }; + const plugins = this.loadPlugins(pluginSettings); + for (const plugin of plugins) { + await plugin.process(post); } + await post.save(); + return post; } From 9f413c725513c910e28e412ada2ee12c5df988eb Mon Sep 17 00:00:00 2001 From: Fairpost Date: Sun, 3 May 2026 21:45:48 +0200 Subject: [PATCH 07/10] feat: Call post.prepare() instead of platform.preparePost - aligns with post.publish - post.prepare now calls platform.preparePost instead of vv --- docs/NewPlatform.md | 10 +++++----- src/models/Platform.ts | 15 ++++----------- src/models/Post.ts | 10 +++++++--- src/platforms/Bluesky/Bluesky.ts | 11 ++--------- src/platforms/Facebook/Facebook.ts | 11 ++--------- src/platforms/Instagram/Instagram.ts | 12 ++---------- src/platforms/LinkedIn/LinkedIn.ts | 9 ++------- src/platforms/Reddit/Reddit.ts | 11 ++--------- src/platforms/Twitter/Twitter.ts | 10 ++-------- src/platforms/YouTube/YouTube.ts | 10 ++-------- src/services/Fairpost.ts | 6 ++++-- 11 files changed, 34 insertions(+), 81 deletions(-) diff --git a/docs/NewPlatform.md b/docs/NewPlatform.md index 0011f6c..b7e35e3 100644 --- a/docs/NewPlatform.md +++ b/docs/NewPlatform.md @@ -34,13 +34,13 @@ export default class FooBar extends Platform { } /** @inheritdoc */ - async preparePost(source: Source): Promise { - const post = await this.getPost(source); - await post.prepare(); + async preparePost(post: Post) { // prepare your platform specific stuff here // that includes plugins ... - await post.save(); - return post; + // for example + if (post.hasFiles(FileGroup.VIDEO)) { + post.removeFiles(FileGroup.IMAGE); + } } /** @inheritdoc */ diff --git a/src/models/Platform.ts b/src/models/Platform.ts index f97a9d9..0918f03 100644 --- a/src/models/Platform.ts +++ b/src/models/Platform.ts @@ -340,31 +340,24 @@ export default class Platform { /** * preparePost * - * Prepare a post for this platform for the - * given source. If it doesn't exist, create it. - * + * Prepare the post for this platform. * Override this in your own platform ! * - * If the post exists and is published, ignores it. - * If the post exists and is failed, sets it back to - * unscheduled. - * * Do not throw errors. Instead, catch and log them, * and set the post.valid to false * * Presume the post may have already been prepared * before, and manually adapted later. For example, * post.status may have manually been set to canceled. - * @param source - the source for which to prepare a post for this platform - * @returns the prepared post + * @param post - the post to prepare */ - async preparePost(source: Source): Promise { + async preparePost(post: Post) { this.user.log.trace("Platform", this.id, "preparePost"); throw this.user.log.error( "Prepare not implemented for " + this.id + ". Read the docs in the docs folder.", - source.id, + post.id, ); } diff --git a/src/models/Post.ts b/src/models/Post.ts index 63f6fb2..0c85f7e 100644 --- a/src/models/Post.ts +++ b/src/models/Post.ts @@ -128,15 +128,18 @@ export default class Post { /** * Prepare this post * - * Called from Platform.preparePost; - * * The post may already be prepared before, * but then things may have changed. * + * If the is published, ignores it. + * If the is failed, sets it back to + * unscheduled. + * * always updates the files, they may have changed * on disk; but also maintains some properties that may have * been changed manually * + * Finally, Calls platform.preparePost() * Does not save the post. */ @@ -224,7 +227,8 @@ export default class Post { this.status = PostStatus.UNSCHEDULED; } - // done + await this.platform.preparePost(this); + await this.save(); } /** diff --git a/src/platforms/Bluesky/Bluesky.ts b/src/platforms/Bluesky/Bluesky.ts index ac835b3..4be1cd1 100644 --- a/src/platforms/Bluesky/Bluesky.ts +++ b/src/platforms/Bluesky/Bluesky.ts @@ -1,5 +1,4 @@ import { FileGroup, FieldMapping } from "../../types/index.ts"; -import Source from "../../models/Source.ts"; import Operator from "../../models/Operator.ts"; import Platform from "../../models/Platform.ts"; @@ -95,10 +94,8 @@ export default class Bluesky extends Platform { /** @inheritdoc */ - async preparePost(source: Source): Promise { - this.user.log.trace("Bluesky.preparePost", source.id); - const post = await this.getPost(source); - await post.prepare(); + async preparePost(post: Post) { + this.user.log.trace("Bluesky.preparePost", post.id); const userPluginSettings = JSON.parse( this.user.data.get("settings", "BLUESKY_PLUGIN_SETTINGS", "{}"), ); @@ -122,10 +119,6 @@ export default class Bluesky extends Platform { // Duration max: 4 minutes. // Duration min: 1 second. // Aspect ratio must be between 1:3 and 3:1. - - await post.save(); - - return post; } /** @inheritdoc */ diff --git a/src/platforms/Facebook/Facebook.ts b/src/platforms/Facebook/Facebook.ts index cf4a471..3a46d11 100644 --- a/src/platforms/Facebook/Facebook.ts +++ b/src/platforms/Facebook/Facebook.ts @@ -1,8 +1,6 @@ import { basename } from "path"; import { FileGroup, FieldMapping, OAuthRequest } from "../../types/index.ts"; -import Source from "../../models/Source.ts"; - import FacebookApi from "./FacebookApi.ts"; import FacebookAuth from "./FacebookAuth.ts"; import PlatformMapper from "../../mappers/PlatformMapper.ts"; @@ -102,10 +100,8 @@ export default class Facebook extends Platform { } /** @inheritdoc */ - async preparePost(source: Source): Promise { - this.user.log.trace("Facebook.preparePost", source.id); - const post = await this.getPost(source); - await post.prepare(); + async preparePost(post: Post) { + this.user.log.trace("Facebook.preparePost", post.id); const userPluginSettings = JSON.parse( this.user.data.get("settings", "FACEBOOK_PLUGIN_SETTINGS", "{}"), ); @@ -117,9 +113,6 @@ export default class Facebook extends Platform { for (const plugin of plugins) { await plugin.process(post); } - await post.save(); - - return post; } /** @inheritdoc */ diff --git a/src/platforms/Instagram/Instagram.ts b/src/platforms/Instagram/Instagram.ts index bec20f5..ba8a03b 100644 --- a/src/platforms/Instagram/Instagram.ts +++ b/src/platforms/Instagram/Instagram.ts @@ -1,8 +1,6 @@ import { basename } from "path"; import { FileGroup, FieldMapping, OAuthRequest } from "../../types/index.ts"; -import Source from "../../models/Source.ts"; - import InstagramApi from "./InstagramApi.ts"; import InstagramAuth from "./InstagramAuth.ts"; import PlatformMapper from "../../mappers/PlatformMapper.ts"; @@ -101,10 +99,8 @@ export default class Instagram extends Platform { } /** @inheritdoc */ - async preparePost(source: Source): Promise { - this.user.log.trace("Instagram.preparePost", source.id); - const post = await this.getPost(source); - await post.prepare(); + async preparePost(post: Post) { + this.user.log.trace("Instagram.preparePost", post.id); // instagram: require media if ( post.getFiles(FileGroup.IMAGE).length + @@ -126,10 +122,6 @@ export default class Instagram extends Platform { await plugin.process(post); } } - - await post.save(); - - return post; } /** @inheritdoc */ diff --git a/src/platforms/LinkedIn/LinkedIn.ts b/src/platforms/LinkedIn/LinkedIn.ts index d407687..c1d06fa 100644 --- a/src/platforms/LinkedIn/LinkedIn.ts +++ b/src/platforms/LinkedIn/LinkedIn.ts @@ -1,5 +1,4 @@ import { FileGroup, FieldMapping, OAuthRequest } from "../../types/index.ts"; -import Source from "../../models/Source.ts"; import { handleApiError, handleEmptyResponse } from "../../utilities.ts"; import LinkedInApi from "./LinkedInApi.ts"; @@ -102,9 +101,8 @@ export default class LinkedIn extends Platform { } /** @inheritdoc */ - async preparePost(source: Source): Promise { - this.user.log.trace("LinkedIn.preparePost", source.id); - const post = await this.getPost(source); + async preparePost(post: Post) { + this.user.log.trace("LinkedIn.preparePost", post.id); const userPluginSettings = JSON.parse( this.user.data.get("settings", "LINKEDIN_PLUGIN_SETTINGS", "{}"), @@ -117,9 +115,6 @@ export default class LinkedIn extends Platform { for (const plugin of plugins) { await plugin.process(post); } - await post.save(); - - return post; } /** @inheritdoc */ diff --git a/src/platforms/Reddit/Reddit.ts b/src/platforms/Reddit/Reddit.ts index 9041977..eeb007b 100644 --- a/src/platforms/Reddit/Reddit.ts +++ b/src/platforms/Reddit/Reddit.ts @@ -1,8 +1,6 @@ import { basename } from "path"; import { FileGroup, FieldMapping, OAuthRequest } from "../../types/index.ts"; -import Source from "../../models/Source.ts"; - import Platform from "../../models/Platform.ts"; import Post from "../../models/Post.ts"; import RedditApi from "./RedditApi.ts"; @@ -122,10 +120,8 @@ export default class Reddit extends Platform { } /** @inheritdoc */ - async preparePost(source: Source): Promise { - this.user.log.trace("Reddit.preparePost", source.id); - const post = await this.getPost(source); - await post.prepare(); + async preparePost(post: Post) { + this.user.log.trace("Reddit.preparePost", post.id); // TODO: extract video thumbnail let videoposter = ""; if (post.hasFiles(FileGroup.VIDEO)) { @@ -183,9 +179,6 @@ export default class Reddit extends Platform { if (videoposter) { await post.addFile(videoposter); } - await post.save(); - - return post; } /** @inheritdoc */ diff --git a/src/platforms/Twitter/Twitter.ts b/src/platforms/Twitter/Twitter.ts index 0b1cdec..20db571 100644 --- a/src/platforms/Twitter/Twitter.ts +++ b/src/platforms/Twitter/Twitter.ts @@ -1,5 +1,4 @@ import { FileGroup, FieldMapping } from "../../types/index.ts"; -import Source from "../../models/Source.ts"; import Platform from "../../models/Platform.ts"; import Post from "../../models/Post.ts"; @@ -111,10 +110,8 @@ export default class Twitter extends Platform { } /** @inheritdoc */ - async preparePost(source: Source): Promise { - this.user.log.trace("Twitter.preparePost", source.id); - const post = await this.getPost(source); - await post.prepare(); + async preparePost(post: Post) { + this.user.log.trace("Twitter.preparePost", post.id); const userPluginSettings = JSON.parse( this.user.data.get("settings", "TWITTER_PLUGIN_SETTINGS", "{}"), ); @@ -166,9 +163,6 @@ export default class Twitter extends Platform { this.user.log.warn("Twitter post has no body"); post.valid = false; } - await post.save(); - - return post; } /** @inheritdoc */ diff --git a/src/platforms/YouTube/YouTube.ts b/src/platforms/YouTube/YouTube.ts index 9bbb6e1..7c04b67 100644 --- a/src/platforms/YouTube/YouTube.ts +++ b/src/platforms/YouTube/YouTube.ts @@ -1,5 +1,4 @@ import { FileGroup, FieldMapping, OAuthRequest } from "../../types/index.ts"; -import Source from "../../models/Source.ts"; import Platform from "../../models/Platform.ts"; import Post from "../../models/Post.ts"; @@ -102,10 +101,8 @@ export default class YouTube extends Platform { } /** @inheritdoc */ - async preparePost(source: Source): Promise { - this.user.log.trace("YouTube.preparePost", source.id); - const post = await this.getPost(source); - await post.prepare(); + async preparePost(post: Post) { + this.user.log.trace("YouTube.preparePost", post.id); const userPluginSettings = JSON.parse( this.user.data.get("settings", "YOUTUBE_PLUGIN_SETTINGS", "{}"), ); @@ -117,9 +114,6 @@ export default class YouTube extends Platform { for (const plugin of plugins) { await plugin.process(post); } - await post.save(); - - return post; } /** @inheritdoc */ diff --git a/src/services/Fairpost.ts b/src/services/Fairpost.ts index 7991988..7c60fae 100644 --- a/src/services/Fairpost.ts +++ b/src/services/Fairpost.ts @@ -670,7 +670,8 @@ class Fairpost { const platform = user.getPlatform(args.platform); const feed = user.getFeed(); const source = await feed.getSource(args.source); - const post = await platform.preparePost(source); + const post = await platform.getPost(source); + await post.prepare(); output = await post.mapper.getDto(operator); break; } @@ -701,7 +702,8 @@ class Fairpost { output[platform.id] = []; } try { - const post = await platform.preparePost(source); + const post = await platform.getPost(source); + await post.prepare(); (output[platform.id] as CombinedResult[]).push({ success: true, result: await post.mapper.getDto(operator), From 3d7951e45b8b44681d84605defc6ee858e0945bd Mon Sep 17 00:00:00 2001 From: Fairpost Date: Sun, 3 May 2026 23:28:20 +0200 Subject: [PATCH 08/10] feat: Add Platform.pluginSettings.name and process on load --- docs/Plugins.md | 43 ++++++++++++++++------------ src/models/Platform.ts | 28 ++++++++++++++++-- src/models/Post.ts | 1 + src/platforms/Bluesky/Bluesky.ts | 10 ++----- src/platforms/Facebook/Facebook.ts | 10 ++----- src/platforms/Instagram/Instagram.ts | 10 ++----- src/platforms/LinkedIn/LinkedIn.ts | 10 ++----- src/platforms/Reddit/Reddit.ts | 10 ++----- src/platforms/Twitter/Twitter.ts | 10 ++----- src/platforms/YouTube/YouTube.ts | 10 ++----- 10 files changed, 65 insertions(+), 77 deletions(-) diff --git a/docs/Plugins.md b/docs/Plugins.md index 80b0a0c..5f8ce51 100644 --- a/docs/Plugins.md +++ b/docs/Plugins.md @@ -11,12 +11,35 @@ defaults/settings differs per plugin. It's the platform source that defines the required plugins and its default settings; some platform may -allow a user to add more plugins and/or change the +allow a user to add more plugins and/or override the settings. ## Calling a plugin -To have a plugin process a post, simply create the +Inside a `Platform`, `pluginSettings` is an object +with plugin ids as keys and default settings as values. +One optional key, 'name', is reserved for the name of these +settings in User.settings, that can override these +defaults. + +```php + { const pluginId = pluginClass.id(); if (pluginId in pluginSettings) { - plugins?.push(new pluginClass(pluginSettings[pluginId])); + if (typeof pluginSettings[pluginId] === "object") { + plugins?.push(new pluginClass(pluginSettings[pluginId])); + } } }); return plugins; diff --git a/src/models/Post.ts b/src/models/Post.ts index 0c85f7e..8a1b516 100644 --- a/src/models/Post.ts +++ b/src/models/Post.ts @@ -228,6 +228,7 @@ export default class Post { } await this.platform.preparePost(this); + await this.save(); } diff --git a/src/platforms/Bluesky/Bluesky.ts b/src/platforms/Bluesky/Bluesky.ts index 4be1cd1..774a38b 100644 --- a/src/platforms/Bluesky/Bluesky.ts +++ b/src/platforms/Bluesky/Bluesky.ts @@ -17,6 +17,7 @@ export default class Bluesky extends Platform { assetsFolder = "_bluesky"; postFileName = "post.json"; pluginSettings = { + name: "BLUESKY_PLUGIN_SETTINGS", textsize: { max_length: 300, }, @@ -96,14 +97,7 @@ export default class Bluesky extends Platform { async preparePost(post: Post) { this.user.log.trace("Bluesky.preparePost", post.id); - const userPluginSettings = JSON.parse( - this.user.data.get("settings", "BLUESKY_PLUGIN_SETTINGS", "{}"), - ); - const pluginSettings = { - ...this.pluginSettings, - ...(userPluginSettings || {}), - }; - const plugins = this.loadPlugins(pluginSettings); + const plugins = this.loadPlugins(); for (const plugin of plugins) { try { await plugin.process(post); diff --git a/src/platforms/Facebook/Facebook.ts b/src/platforms/Facebook/Facebook.ts index 3a46d11..a7570f2 100644 --- a/src/platforms/Facebook/Facebook.ts +++ b/src/platforms/Facebook/Facebook.ts @@ -21,6 +21,7 @@ export default class Facebook extends Platform { assetsFolder = "_facebook"; postFileName = "post.json"; pluginSettings = { + name: "FACEBOOK_PLUGIN_SETTINGS", limitfiles: { exclusive: ["video"], video_max: 1, @@ -102,14 +103,7 @@ export default class Facebook extends Platform { /** @inheritdoc */ async preparePost(post: Post) { this.user.log.trace("Facebook.preparePost", post.id); - const userPluginSettings = JSON.parse( - this.user.data.get("settings", "FACEBOOK_PLUGIN_SETTINGS", "{}"), - ); - const pluginSettings = { - ...this.pluginSettings, - ...(userPluginSettings || {}), - }; - const plugins = this.loadPlugins(pluginSettings); + const plugins = this.loadPlugins(); for (const plugin of plugins) { await plugin.process(post); } diff --git a/src/platforms/Instagram/Instagram.ts b/src/platforms/Instagram/Instagram.ts index ba8a03b..e36bac7 100644 --- a/src/platforms/Instagram/Instagram.ts +++ b/src/platforms/Instagram/Instagram.ts @@ -20,6 +20,7 @@ export default class Instagram extends Platform { assetsFolder = "_instagram"; postFileName = "post.json"; pluginSettings = { + name: "INSTAGRAM_PLUGIN_SETTINGS", limitfiles: { total_max: 10, }, @@ -110,14 +111,7 @@ export default class Instagram extends Platform { post.valid = false; } if (post.valid) { - const userPluginSettings = JSON.parse( - this.user.data.get("settings", "INSTAGRAM_PLUGIN_SETTINGS", "{}"), - ); - const pluginSettings = { - ...this.pluginSettings, - ...(userPluginSettings || {}), - }; - const plugins = this.loadPlugins(pluginSettings); + const plugins = this.loadPlugins(); for (const plugin of plugins) { await plugin.process(post); } diff --git a/src/platforms/LinkedIn/LinkedIn.ts b/src/platforms/LinkedIn/LinkedIn.ts index c1d06fa..30dd8a0 100644 --- a/src/platforms/LinkedIn/LinkedIn.ts +++ b/src/platforms/LinkedIn/LinkedIn.ts @@ -13,6 +13,7 @@ export default class LinkedIn extends Platform { assetsFolder = "_linkedin"; postFileName = "post.json"; pluginSettings = { + name: "LINKEDIN_PLUGIN_SETTINGS", limitfiles: { exclusive: ["video"], video_max: 1, @@ -104,14 +105,7 @@ export default class LinkedIn extends Platform { async preparePost(post: Post) { this.user.log.trace("LinkedIn.preparePost", post.id); - const userPluginSettings = JSON.parse( - this.user.data.get("settings", "LINKEDIN_PLUGIN_SETTINGS", "{}"), - ); - const pluginSettings = { - ...this.pluginSettings, - ...(userPluginSettings || {}), - }; - const plugins = this.loadPlugins(pluginSettings); + const plugins = this.loadPlugins(); for (const plugin of plugins) { await plugin.process(post); } diff --git a/src/platforms/Reddit/Reddit.ts b/src/platforms/Reddit/Reddit.ts index eeb007b..94975b5 100644 --- a/src/platforms/Reddit/Reddit.ts +++ b/src/platforms/Reddit/Reddit.ts @@ -17,6 +17,7 @@ export default class Reddit extends Platform { assetsFolder = "_reddit"; postFileName = "post.json"; pluginSettings = { + name: "REDDIT_PLUGIN_SETTINGS", limitfiles: { prefer: ["video"], total_max: 1, @@ -165,14 +166,7 @@ export default class Reddit extends Platform { videoposter = dstposter; } } - const userPluginSettings = JSON.parse( - this.user.data.get("settings", "REDDIT_PLUGIN_SETTINGS", "{}"), - ); - const pluginSettings = { - ...this.pluginSettings, - ...(userPluginSettings || {}), - }; - const plugins = this.loadPlugins(pluginSettings); + const plugins = this.loadPlugins(); for (const plugin of plugins) { await plugin.process(post); } diff --git a/src/platforms/Twitter/Twitter.ts b/src/platforms/Twitter/Twitter.ts index 20db571..61238ec 100644 --- a/src/platforms/Twitter/Twitter.ts +++ b/src/platforms/Twitter/Twitter.ts @@ -26,6 +26,7 @@ export default class Twitter extends Platform { assetsFolder = "_twitter"; postFileName = "post.json"; pluginSettings = { + name: "TWITTER_PLUGIN_SETTINGS", limitfiles: { video_max: 0, image_max: 4, @@ -112,14 +113,7 @@ export default class Twitter extends Platform { /** @inheritdoc */ async preparePost(post: Post) { this.user.log.trace("Twitter.preparePost", post.id); - const userPluginSettings = JSON.parse( - this.user.data.get("settings", "TWITTER_PLUGIN_SETTINGS", "{}"), - ); - const pluginSettings = { - ...this.pluginSettings, - ...(userPluginSettings || {}), - }; - const plugins = this.loadPlugins(pluginSettings); + const plugins = this.loadPlugins(); for (const plugin of plugins) { await plugin.process(post); } diff --git a/src/platforms/YouTube/YouTube.ts b/src/platforms/YouTube/YouTube.ts index 7c04b67..a945c4e 100644 --- a/src/platforms/YouTube/YouTube.ts +++ b/src/platforms/YouTube/YouTube.ts @@ -11,6 +11,7 @@ export default class YouTube extends Platform { assetsFolder = "_youtube"; postFileName = "post.json"; pluginSettings = { + name: "YOUTUBE_PLUGIN_SETTINGS", limitfiles: { exclusive: ["video"], video_min: 1, @@ -103,14 +104,7 @@ export default class YouTube extends Platform { /** @inheritdoc */ async preparePost(post: Post) { this.user.log.trace("YouTube.preparePost", post.id); - const userPluginSettings = JSON.parse( - this.user.data.get("settings", "YOUTUBE_PLUGIN_SETTINGS", "{}"), - ); - const pluginSettings = { - ...this.pluginSettings, - ...(userPluginSettings || {}), - }; - const plugins = this.loadPlugins(pluginSettings); + const plugins = this.loadPlugins(); for (const plugin of plugins) { await plugin.process(post); } From f36533323173ec97906ebf08fe44fac329940b58 Mon Sep 17 00:00:00 2001 From: Fairpost Date: Sun, 3 May 2026 23:44:16 +0200 Subject: [PATCH 09/10] feat: Move plugins.process(post) to post.prepare() --- src/models/Platform.ts | 10 +++------- src/models/Post.ts | 11 +++++++++++ src/platforms/Bluesky/Bluesky.ts | 8 -------- src/platforms/Facebook/Facebook.ts | 5 +---- src/platforms/Instagram/Instagram.ts | 6 ------ src/platforms/LinkedIn/LinkedIn.ts | 6 +----- src/platforms/Reddit/Reddit.ts | 4 ---- src/platforms/Twitter/Twitter.ts | 4 ---- src/platforms/YouTube/YouTube.ts | 5 +---- 9 files changed, 17 insertions(+), 42 deletions(-) diff --git a/src/models/Platform.ts b/src/models/Platform.ts index 95104fb..6b1df68 100644 --- a/src/models/Platform.ts +++ b/src/models/Platform.ts @@ -355,14 +355,10 @@ export default class Platform { * post.status may have manually been set to canceled. * @param post - the post to prepare */ + async preparePost(post: Post) { - this.user.log.trace("Platform", this.id, "preparePost"); - throw this.user.log.error( - "Prepare not implemented for " + - this.id + - ". Read the docs in the docs folder.", - post.id, - ); + this.user.log.trace("Platform.preparePost: noop", post.id); + // noop } /** diff --git a/src/models/Post.ts b/src/models/Post.ts index 8a1b516..3cad829 100644 --- a/src/models/Post.ts +++ b/src/models/Post.ts @@ -229,6 +229,17 @@ export default class Post { await this.platform.preparePost(this); + if (this.valid) { + const plugins = this.platform.loadPlugins(); + for (const plugin of plugins) { + try { + await plugin.process(this); + } catch { + this.valid = false; + } + } + } + await this.save(); } diff --git a/src/platforms/Bluesky/Bluesky.ts b/src/platforms/Bluesky/Bluesky.ts index 774a38b..7e4a6cc 100644 --- a/src/platforms/Bluesky/Bluesky.ts +++ b/src/platforms/Bluesky/Bluesky.ts @@ -97,14 +97,6 @@ export default class Bluesky extends Platform { async preparePost(post: Post) { this.user.log.trace("Bluesky.preparePost", post.id); - const plugins = this.loadPlugins(); - for (const plugin of plugins) { - try { - await plugin.process(post); - } catch { - post.valid = false; - } - } // Annimated GIF will be sent as a video. Only one animated GIF can be sent per post. diff --git a/src/platforms/Facebook/Facebook.ts b/src/platforms/Facebook/Facebook.ts index a7570f2..6506890 100644 --- a/src/platforms/Facebook/Facebook.ts +++ b/src/platforms/Facebook/Facebook.ts @@ -103,10 +103,7 @@ export default class Facebook extends Platform { /** @inheritdoc */ async preparePost(post: Post) { this.user.log.trace("Facebook.preparePost", post.id); - const plugins = this.loadPlugins(); - for (const plugin of plugins) { - await plugin.process(post); - } + // all good } /** @inheritdoc */ diff --git a/src/platforms/Instagram/Instagram.ts b/src/platforms/Instagram/Instagram.ts index e36bac7..dfd66ca 100644 --- a/src/platforms/Instagram/Instagram.ts +++ b/src/platforms/Instagram/Instagram.ts @@ -110,12 +110,6 @@ export default class Instagram extends Platform { ) { post.valid = false; } - if (post.valid) { - const plugins = this.loadPlugins(); - for (const plugin of plugins) { - await plugin.process(post); - } - } } /** @inheritdoc */ diff --git a/src/platforms/LinkedIn/LinkedIn.ts b/src/platforms/LinkedIn/LinkedIn.ts index 30dd8a0..01bae34 100644 --- a/src/platforms/LinkedIn/LinkedIn.ts +++ b/src/platforms/LinkedIn/LinkedIn.ts @@ -104,11 +104,7 @@ export default class LinkedIn extends Platform { /** @inheritdoc */ async preparePost(post: Post) { this.user.log.trace("LinkedIn.preparePost", post.id); - - const plugins = this.loadPlugins(); - for (const plugin of plugins) { - await plugin.process(post); - } + // all good } /** @inheritdoc */ diff --git a/src/platforms/Reddit/Reddit.ts b/src/platforms/Reddit/Reddit.ts index 94975b5..257d2b9 100644 --- a/src/platforms/Reddit/Reddit.ts +++ b/src/platforms/Reddit/Reddit.ts @@ -166,10 +166,6 @@ export default class Reddit extends Platform { videoposter = dstposter; } } - const plugins = this.loadPlugins(); - for (const plugin of plugins) { - await plugin.process(post); - } if (videoposter) { await post.addFile(videoposter); } diff --git a/src/platforms/Twitter/Twitter.ts b/src/platforms/Twitter/Twitter.ts index 61238ec..211e891 100644 --- a/src/platforms/Twitter/Twitter.ts +++ b/src/platforms/Twitter/Twitter.ts @@ -113,10 +113,6 @@ export default class Twitter extends Platform { /** @inheritdoc */ async preparePost(post: Post) { this.user.log.trace("Twitter.preparePost", post.id); - const plugins = this.loadPlugins(); - for (const plugin of plugins) { - await plugin.process(post); - } // remove files whose mime are not supported, // this could be a plugin diff --git a/src/platforms/YouTube/YouTube.ts b/src/platforms/YouTube/YouTube.ts index a945c4e..4bfaf6b 100644 --- a/src/platforms/YouTube/YouTube.ts +++ b/src/platforms/YouTube/YouTube.ts @@ -104,10 +104,7 @@ export default class YouTube extends Platform { /** @inheritdoc */ async preparePost(post: Post) { this.user.log.trace("YouTube.preparePost", post.id); - const plugins = this.loadPlugins(); - for (const plugin of plugins) { - await plugin.process(post); - } + // all good } /** @inheritdoc */ From b60265120a9a20e48c080eeca3c37909c07a1608 Mon Sep 17 00:00:00 2001 From: Fairpost Date: Mon, 4 May 2026 00:12:38 +0200 Subject: [PATCH 10/10] feat: Update docs and revert Platform.publishDuePost --- docs/NewPlatform.md | 12 ++++++++++-- src/models/Platform.ts | 2 +- src/models/Post.ts | 9 +++++---- 3 files changed, 16 insertions(+), 7 deletions(-) diff --git a/docs/NewPlatform.md b/docs/NewPlatform.md index b7e35e3..f516fff 100644 --- a/docs/NewPlatform.md +++ b/docs/NewPlatform.md @@ -12,7 +12,9 @@ that works depends on the platform; ymmv. To add support for a new platform, add a class to `src/platforms` extending `src/classes/Platform`. You want to override at least the -method `preparePost(source)` and `publishPost(post,dryrun)`. +method `publishPost(post,dryrun)` and the `pluginSettings` +to configure things like maximum image or text size. For more +detail, you can override the `preparePost(post)` method, too. Make sure not to throw errors in or below publishPost; instead, just return false and let the `Post.processResult()` itself. @@ -28,7 +30,13 @@ export default class FooBar extends Platform { assetsFolder = "_foobar"; postFileName = "post.json"; - + pluginSettings = { + limitfiles: { + prefer: ["video"], + total_max: 1, + }, + }; + constructor(user: User) { super(user); } diff --git a/src/models/Platform.ts b/src/models/Platform.ts index 6b1df68..1674a51 100644 --- a/src/models/Platform.ts +++ b/src/models/Platform.ts @@ -479,7 +479,7 @@ export default class Platform { this.user.log.trace("Platform", this.id, "publishDuePost", dryrun); const post = await this.getDuePost(sources); if (post) { - await this.publishPost(post, dryrun); + await post.publish(dryrun); return post; } } diff --git a/src/models/Post.ts b/src/models/Post.ts index 3cad829..4ef1498 100644 --- a/src/models/Post.ts +++ b/src/models/Post.ts @@ -131,16 +131,17 @@ export default class Post { * The post may already be prepared before, * but then things may have changed. * - * If the is published, ignores it. - * If the is failed, sets it back to + * If the post is published, ignores it. + * If the post is failed, sets it back to * unscheduled. * * always updates the files, they may have changed * on disk; but also maintains some properties that may have * been changed manually * - * Finally, Calls platform.preparePost() - * Does not save the post. + * Calls platform.preparePost() + * Calls plugin.process(this) for all plugins + * Saves the post */ async prepare() {