Skip to content
Merged
11 changes: 9 additions & 2 deletions .env.dist
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,16 @@ FAIRPOST_OAUTH_PORT=8000
# rest api server settings
FAIRPOST_SERVER_BIND=localhost
FAIRPOST_SERVER_PORT=8000
FAIRPOST_SERVER_CORS=*
# live access
#FAIRPOST_SERVER_CORS=http://foobar.ui
#FAIRPOST_SESSION_SECURE=false
#FAIRPOST_SESSION_SAMESITE=strict # strict|lax|none
#FAIRPOST_SESSION_TIMEOUT=3600
ls
# dev access
FAIRPOST_SERVER_CORS=https://localhost:5173
FAIRPOST_SESSION_SECURE=true
FAIRPOST_SESSION_SAMESITE=strict # strict|lax|none
FAIRPOST_SESSION_SAMESITE=none # strict|lax|none
FAIRPOST_SESSION_TIMEOUT=3600

# user feed settings (defaults)
Expand Down
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
34 changes: 20 additions & 14 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 Expand Up @@ -144,14 +152,12 @@ for you to throw:
throw this.user.log.error('foo', 'bar', 'quz');
```

### Using Plugins to prepare your Post
### Using Plugins after preparing your Post

Inside `preparePost`, you can call plugins to, for example,
limit the files to a certain type or scale down images, etcetera.
When a Post is prepared, after `Platform.preparePost`
has finished, the post calls `this.platform.loadPlugins()`
to run plugins configured for this platform.
See [Plugins](Plugins.md) for a more detailed description.
If you want users to be able to finetune the plugin settings,
or even enable additional plugins, read the plugin ids and/or
settings using `User.get(...)`.

### Add input/output for custom settings in your platform

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