Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion docs/Fairpost.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
24 changes: 16 additions & 8 deletions docs/NewPlatform.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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<Post> {
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 */
Expand Down
43 changes: 25 additions & 18 deletions docs/Plugins.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
<?php

export default class MyPlatform extends Platform {
pluginSettings = {
name: 'MYPLATFORM_PLUGIN_SETTINGS',
textsize: {
max_length: 300,
},
}
```

Using this, on preparePost, the textSite plugin
will be applied automatically with the settings given,
and these settings can be overriden with MYPLATFORM_PLUGIN_SETTINGS
in the user settings.

To call a plugin manually instead, instantiate the
plugin with optionally its settings, and call the
`process` method. The example below will scale all
images in your post to have a maximum of 300px width,
Expand All @@ -36,22 +59,6 @@ await imgsize.process(post);
post.save();
```

Inside a `Platform`, can load multiple plugins at once,
passing the settings for all of them keyed by their id.
The below code does the same as the above code:

```php
<?php

const plugins = this.loadPlugins({
'limitfiles': { max_images: 3 },
'imagesize': { max_width: 300 }
});
for (const plugin of plugins) {
await plugin.process(post);
}
post.save();
```

## Writing a plugin

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "fairpost",
"version": "4.0.0",
"version": "5.0.0",
"type": "module",
"engines": {
"node": "22.17.0"
Expand Down
40 changes: 35 additions & 5 deletions src/models/Feed.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -132,7 +153,8 @@ export default class Feed {
});

for await (const file of files) {
const source = await Source.getSource(this, basename(file.path));
const sourceId = this.getSourceId(file.path);
const source = await this.getSource(sourceId);
this.cache[source.id] = source;
sources.push(source);
}
Expand All @@ -151,6 +173,7 @@ export default class Feed {
return sources;
}
}

/**
* Get one source, and use a local cache.
* @param id - id of the source
Expand All @@ -162,8 +185,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 = this.getSourcePath(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);
}
}
67 changes: 33 additions & 34 deletions src/models/Platform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";

/**
Expand All @@ -26,6 +26,10 @@ export default class Platform {
postFileName: string = "post.json";
mapper!: PlatformMapper; // child *must* set this
settings: FieldMapping = {};
pluginSettings: {
name?: string;
[pluginid: string]: object | string | undefined;
} = {};
interval: number;
constructor(user: User) {
this.user = user;
Expand Down Expand Up @@ -201,7 +205,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];
Expand Down Expand Up @@ -340,44 +344,21 @@ export default class Platform {
/**
* preparePost
*
* 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()
*
* If the post exists and is published, ignores it.
* If the post exists and is failed, sets it back to
* unscheduled.
* Prepare the post for this platform.
* Override this in your own platform !
*
* 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
* @param save - wether to save the post already
* @returns the prepared post
* @param post - the post to prepare
*/
async preparePost(source: Source, save?: true): Promise<Post> {
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
}

/**
Expand Down Expand Up @@ -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;
Expand Down
Loading
Loading