diff --git a/README.md b/README.md index 3537745..5162c91 100644 --- a/README.md +++ b/README.md @@ -7,21 +7,22 @@ Fairpost helps you manage users social media feeds from a single entry point, using Node. It supports Facebook, Instagram, Reddit, Twitter, YouTube and LinkedIn. -A Feed is just a folder on disk, and all subfolders are Posts, +A Feed is just a folder on disk, and all subfolders are Source Posts, containing at least one text file (the post body) and -optionally images or video. +optionally images or video. The Source Post will be transformed +into real posts for each connected platform. + Fairpost is *opinionated*, meaning, it will decide -how a folder with contents can best be presented -as a post on each platform. +how a Source Post with contents can best be presented +as a Post on each platform. -By default there is one user with a feed located in `./feed`. -Read [Set up for multiple users](./docs/MultipleUsers.md) -on how to set it up for more users. +For each platform, you'll have to register the app on the +platform. This usually results in an AppId and AppSecret or +something similar, which should be stored in global config. -Edit `.env` to manage the platforms -you want to support, the interval for new posts, -etcetera. For each platform, you'll have to -register the app to post on the users behalf. +Then for each user, you'll have to allow the app to +post on their behalf. This is usually done via an +online (oauth) consent page in a webbrowser. Commonly, you would call this script every day or week for every user. Fairpost can then automatically **prepare** the folders, @@ -31,7 +32,8 @@ add folders with content. Or, if you prefer, you can manually publish one specific folder as posts on all supported and enabled -platforms at once. +platforms at once, or just one post on one platform, +etcetera. ## Setting up @@ -48,11 +50,28 @@ cp .env.dist .env && nano .env # run ./fairpost.js help ``` - -## Enable platforms -Read how to enable various social media platforms in the [docs](docs). +### Set up platforms + +Read how to set up various social media platforms in the [docs](docs). + +### Create a user and connect a platform + +Read how to connect various social media platforms in the [docs](docs); +but in general, the steps are + +``` +# create a user foobar +./fairpost.js create-user --userid=foobar + +# edit the users .env, finetune settings +# and enable platform `bla` +nano users/foobar/.env +# connect platform `bla` to user `foobar` +./fairpost.js @foobar setup-platform --platform=bla + +``` ## Feed planning ### Prepare @@ -99,9 +118,9 @@ a certain post to a certain platform if you like. Access and refresh tokens for various platforms may expire sooner or later. Before you do anything, try -`fairpost.js refresh-platforms`. Eventually, even +`fairpost.js @userid refresh-platforms`. Eventually, even refresh tokens may expire, and you will have to run -`fairpost.js setup-platform --platform=bla` again +`fairpost.js @userid setup-platform --platform=bla` again to get a new pair of tokens. @@ -110,40 +129,41 @@ to get a new pair of tokens. ``` # basic commands: fairpost: help -fairpost: get-feed [--config=xxx] -fairpost: setup-platform --platform=xxx -fairpost: setup-platforms [--platforms=xxx,xxx] -fairpost: test-platform --platform=xxx -fairpost: test-platforms [--platforms=xxx,xxx] -fairpost: refresh-platform --platform=xxx -fairpost: refresh-platforms [--platforms=xxx,xxx] -fairpost: get-platform --platform=xxx -fairpost: get-platforms [--platforms=xxx,xxx] -fairpost: get-folder --folder=xxx -fairpost: get-folders [--folders=xxx,xxx] -fairpost: get-post --post=xxx:xxx -fairpost: get-posts [--status=xxx] [--folders=xxx,xxx] [--platforms=xxx,xxx] -fairpost: prepare-post --post=xxx:xxx -fairpost: schedule-post --post=xxx:xxx --date=xxxx-xx-xx -fairpost: schedule-posts [--folders=xxx,xxx|--folder=xxx] [--platforms=xxx,xxx|--platform=xxx] --date=xxxx-xx-xx -fairpost: publish-post --post=xxx:xxx [--dry-run] -fairpost: publish-posts [--folders=xxx,xxx|--folder=xxx] [--platforms=xxx,xxx|--platform=xxx] +fairpost: @userid get-user +fairpost: @userid get-feed +fairpost: @userid setup-platform --platform=xxx +fairpost: @userid setup-platforms [--platforms=xxx,xxx] +fairpost: @userid test-platform --platform=xxx +fairpost: @userid test-platforms [--platforms=xxx,xxx] +fairpost: @userid refresh-platform --platform=xxx +fairpost: @userid refresh-platforms [--platforms=xxx,xxx] +fairpost: @userid get-platform --platform=xxx +fairpost: @userid get-platforms [--platforms=xxx,xxx] +fairpost: @userid get-folder --folder=xxx +fairpost: @userid get-folders [--folders=xxx,xxx] +fairpost: @userid get-post --post=xxx:xxx +fairpost: @userid get-posts [--status=xxx] [--folders=xxx,xxx] [--platforms=xxx,xxx] +fairpost: @userid prepare-post --post=xxx:xxx +fairpost: @userid schedule-post --post=xxx:xxx --date=xxxx-xx-xx +fairpost: @userid schedule-posts [--folders=xxx,xxx|--folder=xxx] [--platforms=xxx,xxx|--platform=xxx] --date=xxxx-xx-xx +fairpost: @userid schedule-next-post [--date=xxxx-xx-xx] [--platforms=xxx,xxx|--platform=xxx] +fairpost: @userid publish-post --post=xxx:xxx [--dry-run] +fairpost: @userid publish-posts [--folders=xxx,xxx|--folder=xxx] [--platforms=xxx,xxx|--platform=xxx] # feed planning: -fairpost: prepare-posts [--folders=xxx,xxx|--folder=xxx] [--platforms=xxx,xxx|--platform=xxx] -fairpost: schedule-next-post [--date=xxxx-xx-xx] [--folders=xxx,xxx] [--platforms=xxx,xxx] -fairpost: publish-due-posts [--folders=xxx,xxx] [--platforms=xxx,xxx] [--dry-run] +fairpost: @userid prepare-posts [--folders=xxx,xxx|--folder=xxx] [--platforms=xxx,xxx|--platform=xxx] +fairpost: @userid schedule-next-posts [--date=xxxx-xx-xx] [--folders=xxx,xxx] [--platforms=xxx,xxx] +fairpost: @userid publish-due-posts [--folders=xxx,xxx] [--platforms=xxx,xxx] [--dry-run] -# api server +# admin only: +fairpost: create-user --userid=xxx +fairpost: get-user --userid=xxx fairpost: serve ``` ### Common arguments ``` -# Select which user to handle -fairpost.js @[user] [command] [arguments] - # Set the cli output format to pure json fairpost.js [command] [arguments] --output=json diff --git a/docs/Ayrshare.md b/docs/Ayrshare.md index a9f51d8..f06e829 100644 --- a/docs/Ayrshare.md +++ b/docs/Ayrshare.md @@ -8,6 +8,10 @@ But if you have an Ayrshare account, you can enable it here and enable the platforms that you have connected to Ayrshare, to publish to those platforms via Ayrshare. +An Ayrshare account can only manage each platform +once per user; you will have to create a new account +for each user. + Ayrshare posts will not be scheduled on Ayrshare; they will be published instantly. Use Fairpost for scheduling posts. @@ -20,18 +24,18 @@ The Ayrshare platforms supported by FairPost are - astiktok - asyoutube -If you only have one user, your user .env is -the same as your global .env -## Setting up the Ayrshare platform +## Connect the platform to a user + +### Get an api key for the user - get an account at Ayrshare - get your Api key at https://app.ayrshare.com/api -- store this key as FAIRPOST_AYRSHARE_API_KEY in your global .env +- store this key as FAIRPOST_AYRSHARE_API_KEY in your users .env -### Enable and test the facebook platform +### Enable and test a random platform - Add one or more of the 'as*' platforms to `FAIRPOST_FEED_PLATFORMS` in the users `.env` - - call `./fairpost.js test-platforms` + - call `./fairpost.js @userid test-platforms` # Limitations diff --git a/docs/Facebook.md b/docs/Facebook.md index e1bcd72..aa6a19d 100644 --- a/docs/Facebook.md +++ b/docs/Facebook.md @@ -3,10 +3,8 @@ The `facebook` platform manages a facebook **page** (not your feed) using the plain graph api - no extensions installed. -If you only have one user, your user .env is -the same as your global .env -## Setting up the Facebook platform +## Set up the platform ### Create a new App in your facebook account @@ -18,15 +16,17 @@ the same as your global .env - save this as `FAIRPOST_FACEBOOK_APP_SECRET` in your global .env - keep the app under development, otherwise the localhost return url wont work -### Find the page id of the page you want the app to manage - - go to https://business.facebook.com/ - - find your page (currently under 'settings > accounts > pages') - - save the page id as `FAIRPOST_FACEBOOK_PAGE_ID` in your users .env +## Connect the platform to a user ### Enable the platform - Add 'facebook' to your `FAIRPOST_FEED_PLATFORMS` in your users `.env` +### Find the page id of the page you want the app to manage + - go to https://business.facebook.com/ + - find your page (currently under 'settings > accounts > pages') + - save the page id as `FAIRPOST_FACEBOOK_PAGE_ID` in your users .env + ### Get a (long lived) Page Access Token for the page you want the app to manage This token should last forever. It involves getting a user access token, @@ -48,11 +48,11 @@ tokens, you can turn on Live mode and start posting. - go to https://developers.facebook.com/ - select your app, edit it - set App Mode to 'dev' -- call `./fairpost.js setup-platform --platform=facebook` +- call `./fairpost.js @userid setup-platform --platform=facebook` - follow instructions from the command line ### Test the platform - - call `./fairpost.js test-platform --platform=facebook` + - call `./fairpost.js @userid test-platform --platform=facebook` ### Set the App to Live Mode before you use the app, set the App Mode to 'Live' @@ -61,18 +61,14 @@ before you use the app, set the App Mode to 'Live' - set App Mode to 'live' - use https://github.com/commonpike/fairpost/blob/master/public/privacy-policy.md for the privacy policy url -### Other settings -`FAIRPOST_FACEBOOK_PUBLISH_POSTS` - if false, posts will be posted but not be published -## Manage additional pages with the same app +## Connect the platform to another user One fairpost user can only manage one page. If you create a second user, you can use the same app to manage a different page. The app is registered on your account, so if you can manage the other page, so can the app. -To get this working, you need to follow instruction at [Set up for multiple users](./docs/MultipleUsers.md) - -## Add a second user -- call `./fairpost.js add-user --user=foo` # todo +### Add a second user +- call `./fairpost.js create-user --userid=foo` ### Enable the app on the other page @@ -92,6 +88,10 @@ To get this working, you need to follow instruction at [Set up for multiple user ### Test the platform for the other page - call `./fairpost.js @foo test-platform --platform=facebook` +## More user settings + +`FAIRPOST_FACEBOOK_PUBLISH_POSTS` - if false, posts will be posted but not be published + # Limitations ## Images diff --git a/docs/Instagram.md b/docs/Instagram.md index a9fb49d..1d11881 100644 --- a/docs/Instagram.md +++ b/docs/Instagram.md @@ -11,10 +11,8 @@ It uses the related facebook account to upload temporary files, because the instagram api requires files in posts to have an url. -If you only have one user, your user .env is -the same as your global .env -## Setting up the Instagram platform +## Set up the platform ### Create a new App in your facebook account @@ -30,6 +28,7 @@ the same as your global .env - under 'settings', find your app secret - save this as `FAIRPOST_INSTAGRAM_APP_SECRET` in your global .env +## Connect the platform to a user ### Find your instagram user id - go to https://www.instagram.com/web/search/topsearch/?query={username} @@ -60,11 +59,11 @@ tokens, you can turn on Live mode and start posting. - go to https://developers.facebook.com/ - select your app, edit it - set App Mode to 'dev' -- call `./fairpost.js setup-platform --platform=instagram` +- call `./fairpost.js @userid setup-platform --platform=instagram` - follow instructions from the command line ### Test the platform - - call `./fairpost.js test-platform --platform=instagram` + - call `./fairpost.js @userid test-platform --platform=instagram` ### Set the App to Live Mode before you use the app, set the App Mode to 'Live' @@ -73,14 +72,12 @@ before you use the app, set the App Mode to 'Live' - set App Mode to 'live' - use https://github.com/commonpike/fairpost/blob/master/public/privacy-policy.md for the privacy policy url -## Manage additional pages with the same app +## Connect the platform to another user One fairpost user can only manage one page. If you create a second user, you can use the same app id to manage a different page. The app is registered on your account, so if you can manage the other page, so can the app. -To get this working, you need to follow instruction at [Set up for multiple users](./docs/MultipleUsers.md) - -## Add a second user -- call `./fairpost.js add-user --user=foo` # todo +### Add a second user +- call `./fairpost.js create-user --userid=foo` ### Find your other instagram user id - go to https://www.instagram.com/web/search/topsearch/?query={username} diff --git a/docs/LinkedIn.md b/docs/LinkedIn.md index cfa0055..74a8370 100644 --- a/docs/LinkedIn.md +++ b/docs/LinkedIn.md @@ -2,11 +2,7 @@ The LinkedIn platform posts to your companies feed. -If you only have one user, your user .env is -the same as your global .env - -## Setting up the LinkedIn platform - +## Set up the platform ### Create a new App in your linkedin account - create an company your account can manage @@ -26,6 +22,7 @@ https://www.linkedin.com/developers/apps/new in your global `.env` - add redirect url for your app as set in your .env (http://localhost:8000/callback) +## Connect the platform to a user ### Enable the platform - Add 'linkedin' to your `FAIRPOST_FEED_PLATFORMS` in your users `.env` @@ -35,20 +32,18 @@ https://www.linkedin.com/developers/apps/new This token last for 60 days and should be refreshed. The refresh token (if given) lasts for 1 year. - - call `./fairpost.js setup-platform --platform=linkedin` + - call `./fairpost.js @userid setup-platform --platform=linkedin` - follow instructions from the command line ### Test the platform - - call `./fairpost.js test-platform --platform=linkedin` + - call `./fairpost.js @userid test-platform --platform=linkedin` -## Manage additional pages with the same app +## Connect the platform to another user One fairpost user can only manage one page. If you create a second user, you can use the same app id to manage a different page. The app is registered on your account, so if you can manage the other page, so can the app. -To get this working, you need to follow instruction at [Set up for multiple users](./docs/MultipleUsers.md) - -## Add a second user -- call `./fairpost.js add-user --user=foo` # todo +### Create a new user +- call `./fairpost.js create-user --userid=foo` - add linkedin to its FAIRPOST_PLATFORMS - find your company id (in the url, like , 93841222) - save this as `FAIRPOST_LINKEDIN_COMPANY_ID` in your users .env diff --git a/docs/MultipleUsers.md b/docs/MultipleUsers.md index 37bcf30..a73ec19 100644 --- a/docs/MultipleUsers.md +++ b/docs/MultipleUsers.md @@ -1,32 +1,8 @@ # Set up for multiple users -By default, Fairpost serves a single user. If -your client is allowed to manage multiple feeds/pages/channels/etc. on some platforms, you can change it to manage those. - For each feed/page/channel/etc, Fairpost uses one 'user' that has it's own configuration, logging and feed folder. +If you don't specify a user, Fairpost assumes it's 'admin'. The admin user has no homedir and uses global config, logging and storage. -At least one user called `admin` is required. -If you don't specify a user, Fairpost assumes it's 'admin'. - -## Setup - -``` -mkdir ./users -cp -a ./etc/skeleton ./users/admin -cp -a ./etc/skeleton ./users/foobar -mv ./users/foobar/.env.dist ./users/foobar/.env - -# enter your users platform details: -nano ./users/foobar/.env - -# change the global .env to multi-user setup: -# - comment out 'app single user settings' -# - uncomment 'app multi user settings' -nano .env - -# test it -./fairpost.js @foobar get-user -``` ## User storage @@ -37,22 +13,11 @@ to enter the feed/page/channel/etc settings for each platform. The rest is set g The other stores, like access tokens, will also be stored in the user directory in the path specified in his .env -## Separate logging - -To enable seperate logging for seperate users, -edit `./log4js.json` and change -``` -"categories": { - "default": { "appenders": ["global"], "level": "info" } - } -``` - to -``` - "categories": { - "default": { "appenders": ["user","global-filtered"], "level": "info" } - } -``` - -This will generate two logfiles: one `./users/foobar/var/log/fairpost.log` and one in `./var/log/fairpost.log`, each with possibly different logging levels. - -The user could also host it's own log4js.json; settings there would completely override the settings in the global log4j.json. +## User logging + +When command are called on behalf of a user, `./log4js.json` uses the +'user' category instead of the default category. It logs in a dedicated +logfile for the user, but also append some messages to the global log. + +The user could also host it's own log4js.json; settings there would +completely override the settings in the global log4j.json. diff --git a/docs/Reddit.md b/docs/Reddit.md index 4a2fc61..3993b57 100644 --- a/docs/Reddit.md +++ b/docs/Reddit.md @@ -1,9 +1,6 @@ # Platform: Reddit -If you only have one user, your user .env is -the same as your global .env - -## Setting up the Reddit platform +## Set up the platform ### Create a new App in your Reddit account @@ -16,6 +13,8 @@ the same as your global .env - read the terms here https://www.reddit.com/wiki/api/#wiki_read_the_full_api_terms_and_sign_up_for_usage - request access to the API using the request form, and wait until approved +## Connect the platform to a user + ### Enable the platform - Add 'reddit' to your `FAIRPOST_FEED_PLATFORMS` in your users `.env` @@ -23,12 +22,12 @@ the same as your global .env This token only lasts for 24 hours and should be refreshed. - - call `./fairpost.js setup-platform --platform=reddit` + - call `./fairpost.js @userid setup-platform --platform=reddit` - follow instructions from the command line ### Test the platform - - call `./fairpost.js test-platform --platform=reddit` + - call `./fairpost.js @userid test-platform --platform=reddit` # Random documentation diff --git a/docs/TikTok.md b/docs/TikTok.md index d31f4a4..008f0a3 100644 --- a/docs/TikTok.md +++ b/docs/TikTok.md @@ -4,10 +4,8 @@ Tiktok does not allow services to run for a single user. This platform is not yet working. -If you only have one user, your user .env is -the same as your global .env -# setup +## Set up the platform - sign up for a developer account https://developers.tiktok.com/apps/ - create a personal app diff --git a/docs/Twitter.md b/docs/Twitter.md index ba6a3b0..f0c701a 100644 --- a/docs/Twitter.md +++ b/docs/Twitter.md @@ -3,10 +3,8 @@ The Twitter platform is using https://github.com/PLhery/node-twitter-api-v2 -If you only have one user, your user .env is -the same as your global .env -## Setting up the Twitter platform +## Set up the platform The Twitter api was being rebuild when Elon Musk bought it and broke it. Part of it now runs on @@ -37,6 +35,8 @@ keys will not be needed anymore. - save `FAIRPOST_TWITTER_CLIENT_ID` in your global .env - save `FAIRPOST_TWITTER_CLIENT_SECRET` in your global .env +## Connect the platform to a user + ### Enable the platform - Add 'twitter' to your `FAIRPOST_FEED_PLATFORMS` in your users `.env` @@ -44,21 +44,19 @@ keys will not be needed anymore. This token should last forever (?) - - call `./fairpost.js setup-platform --platform=twitter` + - call `./fairpost.js @userid setup-platform --platform=twitter` - follow instructions from the command line ### Test the platform - - call `./fairpost.js test-platform --platform=twitter` + - call `./fairpost.js @userid test-platform --platform=twitter` -## Manage additional feeds with the same app +## Connect the platform to another user One fairpost user can only manage one feed. If you create a second user, you can use the same app to manage a different feed. OAuth2 allows you to enable the app for your second account, but the OAuth1 part is tied to your first account and requires you to specify an 'additional_owner' for the uploaded media. -To get this working, you need to follow instruction at [Set up for multiple users](./docs/MultipleUsers.md) - -## Add a second user -- call `./fairpost.js add-user --user=foo` # todo +### Add a second user +- call `./fairpost.js create-user --userid=foo` ### Get an OAuth2 Access Token for your other page diff --git a/docs/Youtube.md b/docs/Youtube.md index 5739a3d..d3fa7cd 100644 --- a/docs/Youtube.md +++ b/docs/Youtube.md @@ -11,10 +11,8 @@ the YouTube Terms of Service: https://www.youtube.com/t/terms Your posts will be preprocessed to fit YouTube. The limitations imposed by Fairpost are not imposed by YouTube. -If you only have one user, your user .env is -the same as your global .env -## Setting up the YouTube platform +## Set up the platform ### Create a new project in your account @@ -36,22 +34,9 @@ Below is how to do it manually. - Under credentials, create OAuth 2.0 Client IDs - Save as `FAIRPOST_YOUTUBE_CLIENT_ID` and `FAIRPOST_YOUTUBE_CLIENT_SECRET` in your global .env -### Enable the platform - - Add 'youtube' to your `FAIRPOST_FEED_PLATFORMS` in your users `.env` - -### Get an OAuth2 Access Token for your platform - -This token last for a few hours and should be refreshed. -The refresh token (if given) lasts until it is revoked. - - - call `./fairpost.js setup-platform --platform=youtube` - - follow instructions from the command line - -### Test the platform - - call `./fairpost.js test-platform --platform=youtube` - ### Get your app audited +You can already proceed below to test the app for private videoos. To have Fairpost publish **public** videos, your app has to be audited - go to https://support.google.com/youtube/contact/yt_api_form @@ -60,18 +45,26 @@ To have Fairpost publish **public** videos, your app has to be audited - For the 'document describing your implementation', post this file - wait. +## Connect the platform to a user + +### Enable the platform + - Add 'youtube' to your `FAIRPOST_FEED_PLATFORMS` in your users `.env` + +### Get an OAuth2 Access Token for your platform -### Other user settings +This token last for a few hours and should be refreshed. +The refresh token (if given) lasts until it is revoked. -- `FAIRPOST_YOUTUBE_PRIVACY` = public | private | unlisted -- `FAIRPOST_YOUTUBE_CATEGORY` = valid youtube category id + - call `./fairpost.js @userid setup-platform --platform=youtube` + - follow instructions from the command line + +### Test the platform + - call `./fairpost.js @userid test-platform --platform=youtube` -## Manage additional pages with the same app -To get this working, you need to follow instruction at [Set up for multiple users](./docs/MultipleUsers.md) +## Connect the platform to another user -## Add a second user -- call `./fairpost.js add-user --user=foo` # todo +- call `./fairpost.js create-user --user=foo` - add youtube to its FAIRPOST_PLATFORMS ### Get an OAuth2 Access Token for your other page @@ -82,6 +75,11 @@ To get this working, you need to follow instruction at [Set up for multiple user ### Test the other installation - call `./fairpost.js @foo test-platform --platform=youtube` +## More user settings + +- `FAIRPOST_YOUTUBE_PRIVACY` = public | private | unlisted +- `FAIRPOST_YOUTUBE_CATEGORY` = valid youtube category id + # Limitations ## Video diff --git a/feed/README.md b/feed/README.md deleted file mode 100644 index dd9ac7f..0000000 --- a/feed/README.md +++ /dev/null @@ -1,7 +0,0 @@ -# Feed folder - -This folder can actually be anywhere, the path -is defined in your `.env` file. - -Post your posts here, each in a separate folder. -Folder names starting with an underscore are ignored. \ No newline at end of file diff --git a/log4js.json b/log4js.json index 5bad6a7..a679962 100644 --- a/log4js.json +++ b/log4js.json @@ -19,6 +19,7 @@ } }, "categories": { - "default": { "appenders": ["global"], "level": "info" } + "default": { "appenders": ["global"], "level": "info" }, + "user": { "appenders": ["user","global-filtered"], "level": "info" } } } \ No newline at end of file diff --git a/src/cli.ts b/src/cli.ts index c33c594..b3e1c95 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -19,6 +19,7 @@ const COMMAND = process.argv[2]?.includes("@") // options const DRY_RUN = !!getOption("dry-run") ?? false; +const USERID = (getOption("userid") as string) ?? ""; const OUTPUT = (getOption("output") as string) ?? "text"; const PLATFORMS = ((getOption("platforms") as string)?.split(",") as PlatformId[]) ?? undefined; @@ -47,6 +48,7 @@ async function main() { try { const { result, report } = await CommandHandler.execute(user, COMMAND, { dryrun: DRY_RUN, + userid: USERID, platforms: PLATFORMS, platform: PLATFORM, folders: FOLDERS, diff --git a/src/models/Store.ts b/src/models/Store.ts index 6b28c90..c454985 100644 --- a/src/models/Store.ts +++ b/src/models/Store.ts @@ -25,17 +25,21 @@ export default class Store { envData: { [key: string]: string } = {}; constructor(userid: string) { this.loadGlobalEnv(); - this.envPath = this.getEnv("USER_ENVPATH", "users/%user%/.env").replace( - "%user%", - userid, - ); + if (userid !== "admin") { + this.envPath = this.getEnv("USER_ENVPATH", "users/%user%/.env").replace( + "%user%", + userid, + ); + this.jsonPath = this.getEnv( + "USER_JSONPATH", + "users/%user%/var/lib/storage.json", + ).replace("%user%", userid); + } else { + this.envPath = path.resolve(__dirname, "../../.env"); + this.jsonPath = path.resolve(__dirname, "../../var/lib/storage.json"); + } this.loadUserEnv(); this.loadConsoleEnv(); - - this.jsonPath = this.getEnv( - "USER_JSONPATH", - "users/%user%/var/lib/storage.json", - ).replace("%user%", userid); this.loadJson(); } diff --git a/src/models/User.ts b/src/models/User.ts index b9e590f..5429f22 100644 --- a/src/models/User.ts +++ b/src/models/User.ts @@ -27,17 +27,22 @@ export default class User { constructor(id: string = "default") { this.id = id; this.store = new Store(this.id); - this.homedir = this.get("settings", "USER_HOMEDIR", "users/%user%").replace( - "%user%", - this.id, - ); - this.logger = this.getLogger(); - if ( - !fs.existsSync(this.homedir) || - !fs.statSync(this.homedir).isDirectory() - ) { - throw this.error("No such user: " + id); + if (this.id !== "admin") { + this.homedir = this.get( + "settings", + "USER_HOMEDIR", + "users/%user%", + ).replace("%user%", this.id); + if ( + !fs.existsSync(this.homedir) || + !fs.statSync(this.homedir).isDirectory() + ) { + throw this.error("No such user: " + id); + } + } else { + this.homedir = path.resolve(__dirname, "../../"); } + this.logger = this.getLogger(); } /** @@ -53,11 +58,35 @@ export default class User { return report; } + /** + * @returns the new user + */ + + public createUser(userId: string): User { + if (this.id !== "admin") { + throw this.error("Only admin can create users"); + } + const src = path.resolve(__dirname, "../../etc/skeleton"); + const dst = this.get("settings", "USER_HOMEDIR", "users/%user%").replace( + "%user%", + userId, + ); + if (fs.existsSync(dst)) { + throw this.error("Homedir already exists: " + dst); + } + fs.cpSync(src, dst, { recursive: true }); + fs.renameSync(dst + "/.env.dist", dst + "/.env"); + return new User(userId); + } + /** * @returns the feed for this user */ public getFeed(): Feed { + if (this.id === "admin") { + throw this.error("Admin does not have a feed"); + } this.loadPlatforms(); return new Feed(this); } @@ -68,6 +97,9 @@ export default class User { * active */ private loadPlatforms(): void { + if (this.id === "admin") { + throw this.error("Admin does not have platforms"); + } const activePlatformIds = this.get("settings", "FEED_PLATFORMS", "").split( ",", ); @@ -141,28 +173,42 @@ export default class User { "LOGGER_CONFIG", "log4js.json", ); - const category = this.store.get("settings", "LOGGER_CATEGORY", "default"); + const category = this.id === "admin" ? "default" : "user"; const level = this.store.get("settings", "LOGGER_LEVEL", "INFO"); const addConsole = this.store.get("settings", "LOGGER_CONSOLE", "false") === "true"; // eslint-disable-next-line @typescript-eslint/no-var-requires const config = fs.existsSync(this.homedir + "/" + configFile) - ? require( - path.resolve(__dirname + "/../../", this.homedir + "/" + configFile), + ? JSON.parse( + fs.readFileSync( + path.resolve( + __dirname + "/../../", + this.homedir + "/" + configFile, + ), + "utf8", + ), ) - : require(path.resolve(__dirname + "/../../", configFile)); + : JSON.parse( + fs.readFileSync( + path.resolve(__dirname + "/../../", configFile), + "utf8", + ), + ); if (!config.categories[category]) { throw new Error( "Logger: Log4js category " + category + " not found in " + configFile, ); } + for (const appender in config.appenders) { - if (config.appenders[appender].filename) { - config.appenders[appender].filename = config.appenders[ - appender - ].filename?.replace("%user%", this.id); - } + config.appenders[appender].filename = config.appenders[ + appender + ].filename?.replace("%user%", this.id); + } + + if (this.id === "admin") { + config.categories = { default: config.categories["default"] }; } if ( diff --git a/src/services/CommandHandler.ts b/src/services/CommandHandler.ts index c0c613b..7a8df61 100644 --- a/src/services/CommandHandler.ts +++ b/src/services/CommandHandler.ts @@ -41,24 +41,52 @@ class CommandHandler { ); } - const feed = user.getFeed(); + const feed = user.id !== "admin" ? user.getFeed() : null; user.trace( "Fairpost " + user.id + " " + command, args.dryrun ? " dry-run" : "", ); switch (command) { + case "create-user": { + if (user.id !== "admin") { + throw user.error("only admins can create-user"); + } + if (!args.userid) { + throw user.error("userid is required"); + } + if (!args.userid.match("^[a-z][a-z0-9_\\-\\.]{3,31}$")) { + throw user.error( + "invalid userid: must be between 4 and 32 long, start with a character and contain only (a-z,0-9,-,_,.)", + ); + } + const newUser = user.createUser(args.userid); + result = newUser; + report = newUser.report(); + break; + } case "get-user": { - result = user; - report = user.report(); + if (args.userid) { + if (user.id !== "admin") { + throw user.error("only admins can get-user other users"); + } + const other = new User(args.userid); + result = other; + report = other.report(); + } else { + result = user; + report = user.report(); + } break; } case "get-feed": { + if (!feed) throw user.error("User " + user.id + " has no feed"); result = feed; report = feed.report(); break; } case "setup-platform": { + if (!feed) throw user.error("User " + user.id + " has no feed"); if (!args.platform) { throw user.error( "CommandHandler " + command, @@ -72,12 +100,14 @@ class CommandHandler { break; } case "setup-platforms": { + if (!feed) throw user.error("User " + user.id + " has no feed"); await feed.setupPlatforms(args.platforms); result = "Success"; // or error report = "Result: \n" + JSON.stringify(result, null, "\t"); break; } case "get-platform": { + if (!feed) throw user.error("User " + user.id + " has no feed"); if (!args.platform) { throw user.error( "CommandHandler " + command, @@ -90,6 +120,7 @@ class CommandHandler { break; } case "get-platforms": { + if (!feed) throw user.error("User " + user.id + " has no feed"); const platforms = feed.getPlatforms(args.platforms); report += platforms.length + " Platforms\n------\n"; platforms.forEach((platform) => { @@ -99,6 +130,7 @@ class CommandHandler { break; } case "test-platform": { + if (!feed) throw user.error("User " + user.id + " has no feed"); if (!args.platform) { throw user.error( "CommandHandler " + command, @@ -110,11 +142,13 @@ class CommandHandler { break; } case "test-platforms": { + if (!feed) throw user.error("User " + user.id + " has no feed"); result = await feed.testPlatforms(args.platforms); report = "Result: \n" + JSON.stringify(result, null, "\t"); break; } case "refresh-platform": { + if (!feed) throw user.error("User " + user.id + " has no feed"); if (!args.platform) { throw user.error( "CommandHandler " + command, @@ -126,11 +160,13 @@ class CommandHandler { break; } case "refresh-platforms": { + if (!feed) throw user.error("User " + user.id + " has no feed"); result = await feed.refreshPlatforms(args.platforms); report = "Result: \n" + JSON.stringify(result, null, "\t"); break; } case "get-folder": { + if (!feed) throw user.error("User " + user.id + " has no feed"); if (!args.folder) { throw user.error( "CommandHandler " + command, @@ -147,6 +183,7 @@ class CommandHandler { break; } case "get-folders": { + if (!feed) throw user.error("User " + user.id + " has no feed"); const folders = feed.getFolders(args.folders); report += folders.length + " Folders\n------\n"; folders.forEach((folder) => { @@ -156,6 +193,7 @@ class CommandHandler { break; } case "get-post": { + if (!feed) throw user.error("User " + user.id + " has no feed"); if (!args.folder) { throw user.error( "CommandHandler " + command, @@ -178,6 +216,7 @@ class CommandHandler { break; } case "get-posts": { + if (!feed) throw user.error("User " + user.id + " has no feed"); const allposts = feed.getPosts({ folders: args.folders, platforms: args.platforms, @@ -191,6 +230,7 @@ class CommandHandler { break; } case "prepare-post": { + if (!feed) throw user.error("User " + user.id + " has no feed"); if (!args.folder) { throw user.error( "CommandHandler " + command, @@ -213,6 +253,7 @@ class CommandHandler { break; } case "prepare-posts": { + if (!feed) throw user.error("User " + user.id + " has no feed"); if (!args.platforms && args.platform) { args.platforms = [args.platform]; } @@ -230,6 +271,7 @@ class CommandHandler { break; } case "schedule-post": { + if (!feed) throw user.error("User " + user.id + " has no feed"); if (!args.folder) { throw user.error( "CommandHandler " + command, @@ -258,6 +300,7 @@ class CommandHandler { break; } case "schedule-posts": { + if (!feed) throw user.error("User " + user.id + " has no feed"); if (!args.platforms && args.platform) { args.platforms = [args.platform]; } @@ -290,6 +333,7 @@ class CommandHandler { break; } case "schedule-next-post": { + if (!feed) throw user.error("User " + user.id + " has no feed"); if (!args.platforms && args.platform) { args.platforms = [args.platform]; } @@ -312,6 +356,7 @@ class CommandHandler { break; } case "publish-post": { + if (!feed) throw user.error("User " + user.id + " has no feed"); if (!args.folder) { throw user.error( "CommandHandler " + command, @@ -334,6 +379,7 @@ class CommandHandler { break; } case "publish-posts": { + if (!feed) throw user.error("User " + user.id + " has no feed"); if (!args.platforms && args.platform) { args.platforms = [args.platform]; } @@ -362,6 +408,7 @@ class CommandHandler { /* feed planning */ case "schedule-next-posts": { + if (!feed) throw user.error("User " + user.id + " has no feed"); const nextposts = feed.scheduleNextPosts( args.date ? new Date(args.date) : undefined, { @@ -376,6 +423,7 @@ class CommandHandler { break; } case "publish-due-posts": { + if (!feed) throw user.error("User " + user.id + " has no feed"); const dueposts = await feed.publishDuePosts( { folders: args.folders, @@ -415,30 +463,33 @@ class CommandHandler { result = [ "# basic commands:", `${cmd} help`, - `${cmd} get-feed [--config=xxx]`, - `${cmd} setup-platform --platform=xxx`, - `${cmd} setup-platforms [--platforms=xxx,xxx]`, - `${cmd} test-platform --platform=xxx`, - `${cmd} test-platforms [--platforms=xxx,xxx]`, - `${cmd} refresh-platform --platform=xxx`, - `${cmd} refresh-platforms [--platforms=xxx,xxx]`, - `${cmd} get-platform --platform=xxx`, - `${cmd} get-platforms [--platforms=xxx,xxx]`, - `${cmd} get-folder --folder=xxx`, - `${cmd} get-folders [--folders=xxx,xxx]`, - `${cmd} get-post --post=xxx:xxx`, - `${cmd} get-posts [--status=xxx] [--folders=xxx,xxx] [--platforms=xxx,xxx] `, - `${cmd} prepare-post --post=xxx:xxx`, - `${cmd} schedule-post --post=xxx:xxx --date=xxxx-xx-xx `, - `${cmd} schedule-posts [--folders=xxx,xxx|--folder=xxx] [--platforms=xxx,xxx|--platform=xxx] --date=xxxx-xx-xx`, - `${cmd} schedule-next-post [--date=xxxx-xx-xx] [--platforms=xxx,xxx|--platform=xxx] `, - `${cmd} publish-post --post=xxx:xxx [--dry-run]`, - `${cmd} publish-posts [--folders=xxx,xxx|--folder=xxx] [--platforms=xxx,xxx|--platform=xxx]`, + `${cmd} @userid get-user`, + `${cmd} @userid get-feed`, + `${cmd} @userid setup-platform --platform=xxx`, + `${cmd} @userid setup-platforms [--platforms=xxx,xxx]`, + `${cmd} @userid test-platform --platform=xxx`, + `${cmd} @userid test-platforms [--platforms=xxx,xxx]`, + `${cmd} @userid refresh-platform --platform=xxx`, + `${cmd} @userid refresh-platforms [--platforms=xxx,xxx]`, + `${cmd} @userid get-platform --platform=xxx`, + `${cmd} @userid get-platforms [--platforms=xxx,xxx]`, + `${cmd} @userid get-folder --folder=xxx`, + `${cmd} @userid get-folders [--folders=xxx,xxx]`, + `${cmd} @userid get-post --post=xxx:xxx`, + `${cmd} @userid get-posts [--status=xxx] [--folders=xxx,xxx] [--platforms=xxx,xxx] `, + `${cmd} @userid prepare-post --post=xxx:xxx`, + `${cmd} @userid schedule-post --post=xxx:xxx --date=xxxx-xx-xx `, + `${cmd} @userid schedule-posts [--folders=xxx,xxx|--folder=xxx] [--platforms=xxx,xxx|--platform=xxx] --date=xxxx-xx-xx`, + `${cmd} @userid schedule-next-post [--date=xxxx-xx-xx] [--platforms=xxx,xxx|--platform=xxx] `, + `${cmd} @userid publish-post --post=xxx:xxx [--dry-run]`, + `${cmd} @userid publish-posts [--folders=xxx,xxx|--folder=xxx] [--platforms=xxx,xxx|--platform=xxx]`, "\n# feed planning:", - `${cmd} prepare-posts [--folders=xxx,xxx|--folder=xxx] [--platforms=xxx,xxx|--platform=xxx]`, - `${cmd} schedule-next-posts [--date=xxxx-xx-xx] [--folders=xxx,xxx] [--platforms=xxx,xxx] `, - `${cmd} publish-due-posts [--folders=xxx,xxx] [--platforms=xxx,xxx] [--dry-run]`, - "\n# api server:", + `${cmd} @userid prepare-posts [--folders=xxx,xxx|--folder=xxx] [--platforms=xxx,xxx|--platform=xxx]`, + `${cmd} @userid schedule-next-posts [--date=xxxx-xx-xx] [--folders=xxx,xxx] [--platforms=xxx,xxx] `, + `${cmd} @userid publish-due-posts [--folders=xxx,xxx] [--platforms=xxx,xxx] [--dry-run]`, + "\n# admin only:", + `${cmd} create-user --userid=xxx`, + `${cmd} get-user --userid=xxx`, `${cmd} serve`, ]; (result as string[]).forEach((line) => (report += "\n" + line)); @@ -452,6 +503,7 @@ class CommandHandler { } interface CommandArguments { dryrun?: boolean; + userid?: string; platforms?: PlatformId[]; platform?: PlatformId; folders?: string[]; diff --git a/src/services/Server.ts b/src/services/Server.ts index 2006f5d..8f1f3a2 100644 --- a/src/services/Server.ts +++ b/src/services/Server.ts @@ -46,6 +46,7 @@ export default class Server { ]; const dryrun = parsed.searchParams.get("dry-run") === "true"; const output = parsed.searchParams.get("output") ?? "json"; + const userid = parsed.searchParams.get("userid") || undefined; const date = parsed.searchParams.get("date"); const post = parsed.searchParams.get("post"); const [folder, platform] = post @@ -63,6 +64,7 @@ export default class Server { const args = { dryrun: dryrun || undefined, + userid: userid, platforms: platforms, platform: platform, folders: folders, diff --git a/users/.gitkeep b/users/.gitkeep new file mode 100644 index 0000000..e69de29