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/docs/NewPlatform.md b/docs/NewPlatform.md index 02ad42c..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,19 +30,25 @@ export default class FooBar extends Platform { assetsFolder = "_foobar"; postFileName = "post.json"; - + pluginSettings = { + limitfiles: { + prefer: ["video"], + total_max: 1, + }, + }; + constructor(user: User) { super(user); } /** @inheritdoc */ - async preparePost(source: Source): Promise { - const post = await super.preparePost(source); - if (post) { - // prepare your post here - await post.save(); + async preparePost(post: Post) { + // prepare your platform specific stuff here + // that includes plugins ... + // for example + if (post.hasFiles(FileGroup.VIDEO)) { + post.removeFiles(FileGroup.IMAGE); } - return post; } /** @inheritdoc */ 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 + { - 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; + async preparePost(post: Post) { + this.user.log.trace("Platform.preparePost: noop", post.id); + // noop } /** @@ -504,14 +485,32 @@ export default class Platform { } /** - * @returns array of instances of the plugins given with the settings given. + * @returns array of instances of the plugins + * + * - will load plugins given in Platform.pluginSettings + * - with the settings given in that object + * - but if Platform.pluginSettings.name is set, + * also load that value from user settings and + * override the default platform settings */ - loadPlugins(pluginSettings: { [pluginid: string]: object }): Plugin[] { + loadPlugins(): Plugin[] { const plugins: Plugin[] = []; + let pluginSettings = this.pluginSettings; + if (this.pluginSettings.name) { + const userPluginSettings = JSON.parse( + this.user.data.get("settings", this.pluginSettings.name, "{}"), + ); + pluginSettings = { + ...this.pluginSettings, + ...(userPluginSettings || {}), + }; + } Object.values(pluginClasses).forEach((pluginClass) => { 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 c0dc816..4ef1498 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,77 +45,57 @@ 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.user; delete data.source; delete data.platform; 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 +117,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; @@ -151,28 +128,36 @@ 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 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 * - * Does not save the post. + * Calls platform.preparePost() + * Calls plugin.process(this) for all plugins + * Saves the post */ async prepare() { - this.platform.user.log.trace("Post", "prepare"); + this.user.log.trace("Post", "prepare"); + + if (this.status === PostStatus.PUBLISHED) { + return; + } // 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 +179,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 @@ -244,8 +221,27 @@ 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; + } + + 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; + } + } + } - // done + await this.save(); } /** @@ -257,33 +253,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 +296,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 +324,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 +521,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 +530,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 +588,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 +634,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 +645,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 +675,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 +699,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 4331fe5..ff3040e 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"; /** @@ -39,38 +39,12 @@ export default class Source { */ constructor(feed: Feed, path: string) { this.feed = feed; - this.id = this.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 getSourceId(path: string): string { - return basename(path); // ah, simple - } - /** * Get the stage of this source. * @@ -90,31 +64,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. * @@ -195,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, @@ -292,35 +241,13 @@ export default class Source { } /** - * 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) + * 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, - ); - return await platform.getPost(this); + this.feed.user.log.trace("Source", "getPost", this.id, platform.id); + return await PostFactory.resolve(platform, this); } /** @@ -341,7 +268,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); } diff --git a/src/platforms/Bluesky/Bluesky.ts b/src/platforms/Bluesky/Bluesky.ts index 2096adb..7e4a6cc 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"; @@ -18,6 +17,7 @@ export default class Bluesky extends Platform { assetsFolder = "_bluesky"; postFileName = "post.json"; pluginSettings = { + name: "BLUESKY_PLUGIN_SETTINGS", textsize: { max_length: 300, }, @@ -95,37 +95,16 @@ export default class Bluesky extends Platform { /** @inheritdoc */ - 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; - } - } - - // Annimated GIF will be sent as a video. Only one animated GIF can be sent per post. + async preparePost(post: Post) { + this.user.log.trace("Bluesky.preparePost", post.id); - // video - // Supported formats: MP4. - // Duration max: 4 minutes. - // Duration min: 1 second. - // Aspect ratio must be between 1:3 and 3:1. + // Annimated GIF will be sent as a video. Only one animated GIF can be sent per post. - await post.save(); - } - return post; + // video + // Supported formats: MP4. + // Duration max: 4 minutes. + // Duration min: 1 second. + // Aspect ratio must be between 1:3 and 3:1. } /** @inheritdoc */ diff --git a/src/platforms/Facebook/Facebook.ts b/src/platforms/Facebook/Facebook.ts index 3e3457a..6506890 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"; @@ -23,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,24 +101,9 @@ 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(); - } - return post; + async preparePost(post: Post) { + this.user.log.trace("Facebook.preparePost", post.id); + // all good } /** @inheritdoc */ diff --git a/src/platforms/Instagram/Instagram.ts b/src/platforms/Instagram/Instagram.ts index a2b2d4a..dfd66ca 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"; @@ -22,6 +20,7 @@ export default class Instagram extends Platform { assetsFolder = "_instagram"; postFileName = "post.json"; pluginSettings = { + name: "INSTAGRAM_PLUGIN_SETTINGS", limitfiles: { total_max: 10, }, @@ -101,35 +100,16 @@ 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); - } - } - - await post.save(); + async preparePost(post: Post) { + this.user.log.trace("Instagram.preparePost", post.id); + // instagram: require media + if ( + post.getFiles(FileGroup.IMAGE).length + + post.getFiles(FileGroup.VIDEO).length === + 0 + ) { + post.valid = false; } - return post; } /** @inheritdoc */ diff --git a/src/platforms/LinkedIn/LinkedIn.ts b/src/platforms/LinkedIn/LinkedIn.ts index 13bab4c..01bae34 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"; @@ -14,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, @@ -102,24 +102,9 @@ 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(); - } - return post; + async preparePost(post: Post) { + this.user.log.trace("LinkedIn.preparePost", post.id); + // all good } /** @inheritdoc */ diff --git a/src/platforms/Reddit/Reddit.ts b/src/platforms/Reddit/Reddit.ts index 427a26f..257d2b9 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"; @@ -19,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, @@ -122,70 +121,54 @@ 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), + async preparePost(post: Post) { + this.user.log.trace("Reddit.preparePost", post.id); + // 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(); } - return post; + if (videoposter) { + await post.addFile(videoposter); + } } /** @inheritdoc */ diff --git a/src/platforms/Twitter/Twitter.ts b/src/platforms/Twitter/Twitter.ts index 51586ae..211e891 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"; @@ -27,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, @@ -111,66 +111,48 @@ 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); - } + async preparePost(post: Post) { + this.user.log.trace("Twitter.preparePost", post.id); - // 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(); } - return post; + + // 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; + } } /** @inheritdoc */ diff --git a/src/platforms/YouTube/YouTube.ts b/src/platforms/YouTube/YouTube.ts index 5b97b19..4bfaf6b 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"; @@ -12,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, @@ -102,24 +102,9 @@ 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(); - } - return post; + async preparePost(post: Post) { + this.user.log.trace("YouTube.preparePost", post.id); + // all good } /** @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),