From 56ac9697fe3c8388278b9f2ca24619e1ec416674 Mon Sep 17 00:00:00 2001 From: pike Date: Sat, 30 Sep 2023 10:18:24 +0200 Subject: [PATCH 1/3] fix: Add devDeps --- .gitignore | 1 - package.json | 4 ++++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 439ac59..c148fce 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,6 @@ # Feed feed .DS_Store -package-lock.json # Logs logs diff --git a/package.json b/package.json index ea3fd44..ba1428b 100644 --- a/package.json +++ b/package.json @@ -17,5 +17,9 @@ "dotenv": "^16.0.3", "node-fetch": "^2.6.7", "sharp": "^0.31.1" + }, + "devDependencies": { + "ts-node": "^10.9.1", + "typescript": "^5.0.4" } } From d9df1687483434993547eff5259c5e4a1476a573 Mon Sep 17 00:00:00 2001 From: pike Date: Sat, 30 Sep 2023 11:19:31 +0200 Subject: [PATCH 2/3] feat: Code cleanup, report output --- src/Feed.ts | 190 ++++++++++++++++++++++++++++++++++++++++++++++++ src/Folder.ts | 54 ++++++++++++++ src/Platform.ts | 127 ++++++++++++++++++++++++++++++++ src/Post.ts | 78 ++++++++++++++++++++ 4 files changed, 449 insertions(+) create mode 100644 src/Feed.ts create mode 100644 src/Folder.ts create mode 100644 src/Platform.ts create mode 100644 src/Post.ts diff --git a/src/Feed.ts b/src/Feed.ts new file mode 100644 index 0000000..a78f119 --- /dev/null +++ b/src/Feed.ts @@ -0,0 +1,190 @@ + +import * as fs from 'fs'; +import * as path from 'path'; +import * as dotenv from 'dotenv'; +import Platform from "./Platform"; +import Folder from "./Folder"; +import Post from "./Post"; +import { PostStatus } from "./Post"; +import * as platforms from './platforms'; +import { PlatformSlug } from './platforms'; + +export default class Feed { + + path: string = ''; + platforms: { + [slug in PlatformSlug]? : Platform; + } = {}; + folders: Folder[] = []; + interval: number; + + constructor(configPath?: string) { + if (configPath) { + const configPathResolved = path.resolve(__dirname+'/../../'+configPath); + dotenv.config({ path:configPathResolved }); + } else { + dotenv.config(); + } + if (process.env.FAIRPOST_FEED_PATH) { + this.path = process.env.FAIRPOST_FEED_PATH; + } else { + throw new Error('Problem reading .env config file'); + } + this.interval = Number(process.env.FAIRPOST_FEED_INTERVAL ?? 7); + + const activePlatformSlugs = process.env.FAIRPOST_FEED_PLATFORMS.split(','); + const platformClasses = fs.readdirSync(path.resolve(__dirname+'/platforms')); + platformClasses.forEach(file=> { + const constructor = file.replace('.ts','').replace('.js',''); + if (platforms[constructor] !== undefined) { + const platform = new platforms[constructor](); + platform.active = activePlatformSlugs.includes(platform.slug); + if (platform.active) { + this.platforms[platform.slug] = platform; + } + } + }); + } + + getPlatforms(platforms?:PlatformSlug[]): Platform[] { + return platforms?.map(platform=>this.platforms[platform]) ?? Object.values(this.platforms); + } + + getAllFolders(): Folder[] { + if (this.folders.length) { + return this.folders; + } + if (!fs.existsSync(this.path)) { + fs.mkdirSync(this.path); + } + const paths = fs.readdirSync(this.path).filter(path => { + return fs.statSync(this.path+'/'+path).isDirectory() && + !path.startsWith('_') && !path.startsWith('.'); + }); + if (paths) { + this.folders = paths.map(path => new Folder(this.path+'/'+path)); + } + return this.folders; + } + + getFolders(paths?: string[]): Folder[] { + return paths?.map(path=>new Folder(this.path+'/'+path)) ?? this.getAllFolders(); + } + + getPosts(filters?: { + paths?:string[] + platforms?:PlatformSlug[], + status?:PostStatus + }): Post[] { + const posts: Post[] = []; + const platforms = this.getPlatforms(filters?.platforms); + const folders = this.getFolders(filters?.paths); + for (const folder of folders) { + for (const platform of platforms) { + const post = platform.getPost(folder); + if (post && (!filters?.status || filters.status.includes(post.status))) { + posts.push(post); + } + } + } + return posts; + } + + async preparePosts(filters?: { + paths?:string[] + platforms?:PlatformSlug[] + }): Promise { + + const posts: Post[] = []; + const platforms = this.getPlatforms(filters?.platforms); + const folders = this.getFolders(filters?.paths); + for (const folder of folders) { + for (const platform of platforms) { + const post = platform.getPost(folder); + if (post?.status!==PostStatus.PUBLISHED) { + const newPost = await platform.preparePost(folder); + if (newPost) { + posts.push(newPost); + } + } + } + } + return posts; + } + + + getLastPost(platform:PlatformSlug): Post | void { + let lastPost: Post = undefined; + const posts = this.getPosts({ + platforms: [platform], + status: PostStatus.PUBLISHED + }); + for (const post of posts) { + if (!lastPost || post.posted >= lastPost.posted) { + lastPost = post; + } + } + return lastPost; + } + + + getNextPostDate(platform:PlatformSlug): Date { + let nextDate = null; + const lastPost = this.getLastPost(platform); + if (lastPost) { + nextDate = new Date(lastPost.posted); + nextDate.setDate(nextDate.getDate()+this.interval); + } else { + nextDate = new Date(); + } + return nextDate; + } + + scheduleNextPosts(date?: Date, filters?: { + paths?:string[] + platforms?:PlatformSlug[] + }): Post[] { + const posts: Post[] = []; + const platforms = this.getPlatforms(filters?.platforms); + const folders = this.getFolders(filters?.paths); + for (const platform of platforms) { + const nextDate = date?date:this.getNextPostDate(platform.slug); + for (const folder of folders) { + const post = platform.getPost(folder); + if (post.valid && post?.status===PostStatus.UNSCHEDULED) { + post.schedule(nextDate); + posts.push(post); + break; + } + + } + } + return posts; + } + + async publishDuePosts(filters?: { + paths?:string[] + platforms?:PlatformSlug[] + }, dryrun:boolean = false): Promise { + const now = new Date(); + const posts: Post[] = []; + const platforms = this.getPlatforms(filters?.platforms); + const folders = this.getFolders(filters?.paths); + for (const platform of platforms) { + for (const folder of folders) { + const post = platform.getPost(folder); + if (post?.status===PostStatus.SCHEDULED) { + if (post.scheduled <= now) { + console.log('Posting',platform.slug,folder.path); + await platform.publishPost(post,dryrun); + posts.push(post); + break; + } + } + } + } + return posts; + } + + +} \ No newline at end of file diff --git a/src/Folder.ts b/src/Folder.ts new file mode 100644 index 0000000..3473b9d --- /dev/null +++ b/src/Folder.ts @@ -0,0 +1,54 @@ +import * as fs from 'fs'; +import * as path from 'path'; + +export default class Folder { + + path: string; + files?: { + text: string[], + image: string[], + video: string[], + other: string[] + }; + + constructor(path: string) { + this.path = path; + } + + getFiles() { + if (this.files!=undefined) { + return { + text: [ ...this.files.text ], + image: [ ...this.files.image ], + video: [ ...this.files.video ], + other: [ ...this.files.other ] + }; + } + const files = fs.readdirSync(this.path).filter(file => { + return fs.statSync(this.path+'/'+file).isFile() && + !file.startsWith('_') && + !file.startsWith('.'); + }); + this.files = { + text: [], + image: [], + video: [], + other: [] + }; + this.files.text = files.filter(file=>["txt"].includes(file.split('.')?.pop()??'')); + this.files.image = files.filter(file=>["jpg","jpeg","png"].includes(file.split('.')?.pop()??'')); + this.files.video = files.filter(file=>["mp4"].includes(file.split('.')?.pop()??'')); + this.files.other = files.filter(file=> + !this.files.text?.includes(file) + && !this.files.image?.includes(file) + && !this.files.video?.includes(file) + ); + return { + text: [ ...this.files.text ], + image: [ ...this.files.image ], + video: [ ...this.files.video ], + other: [ ...this.files.other ] + }; + } + +} \ No newline at end of file diff --git a/src/Platform.ts b/src/Platform.ts new file mode 100644 index 0000000..1a81b8e --- /dev/null +++ b/src/Platform.ts @@ -0,0 +1,127 @@ +import * as fs from 'fs'; +import * as path from 'path'; +import Folder from "./Folder"; +import Post from "./Post"; +import { PostStatus } from "./Post"; +import { PlatformSlug } from "./platforms"; + + + +export default class Platform { + + active: boolean = false; + slug: PlatformSlug = PlatformSlug.UNKNOWN; + defaultBody: string = "Fairpost feed"; + + /* + * getPostFileName + * + * Return the intended name for a post of this + * platform to be saved in this folder. + */ + getPostFileName() { + return '_'+this.slug+'.json'; + } + + /* + * getPost + * + * Return the post for this platform for the + * given folder, if it exists. + */ + + getPost(folder: Folder): Post | undefined { + + //console.log(this.slug,'getPost',folder.path); + + if (fs.existsSync(folder.path+'/'+this.getPostFileName())) { + const data = JSON.parse(fs.readFileSync(folder.path+'/'+this.getPostFileName(), 'utf8')); + if (data) { + return new Post(folder,this,data); + } + } + return; + } + + /* + * preparePost + * + * Prepare the post for this platform for the + * given folder, and save it. Optionally create + * derivates of media and save those, too. + * + * If the post exists and is published, ignore. + * If the post exists and is failed, set it back to + * unscheduled. + */ + async preparePost(folder: Folder): Promise { + + console.log(this.slug,'preparePost',folder.path); + + const post = this.getPost(folder) ?? new Post(folder,this); + if (post.status===PostStatus.PUBLISHED) { + return; + } + if (post.status===PostStatus.FAILED) { + post.status=PostStatus.UNSCHEDULED; + } + + + // some default logic. override this + // in your own platform if you need. + + post.files = folder.getFiles(); + + if (post.files.text?.includes('body.txt')) { + post.body = fs.readFileSync(post.folder.path+'/body.txt','utf8'); + } else if (post.files.text.length === 1 ) { + const bodyFile = post.files.text[0]; + post.body = fs.readFileSync(post.folder.path+'/'+bodyFile,'utf8'); + } else { + post.body = this.defaultBody; + } + + if (post.files.text?.includes('title.txt')) { + post.title = fs.readFileSync(post.folder.path+'/title.txt','utf8'); + } else { + post.title = post.body.split('\n', 1)[0]; + } + + if (post.files.text?.includes('tags.txt')) { + post.tags = fs.readFileSync(post.folder.path+'/tags.txt','utf8'); + } + + if (post.title) { + post.valid = true; + } + + if (post.status===PostStatus.UNKNOWN) { + post.status=PostStatus.UNSCHEDULED; + } + + post.save(); + return post; + } + + /* + * publishPost + * + * publish the post for this platform, sync. + * set the posted date to now. + * add the result to post.results + * on success, set the status to published and return true, + * else set the status to failed and return false + */ + + async publishPost(post: Post, dryrun:boolean = false): Promise { + post.posted = new Date(); + post.results.push({ + error: 'publishing not implemented for '+this.slug + }); + post.status = PostStatus.FAILED; + post.save(); + return false; + } +} + + diff --git a/src/Post.ts b/src/Post.ts new file mode 100644 index 0000000..081eecf --- /dev/null +++ b/src/Post.ts @@ -0,0 +1,78 @@ + +import * as fs from 'fs'; +import * as path from 'path'; +import Folder from "./Folder"; +import Platform from "./Platform"; + +export default class Post { + folder: Folder; + platform: Platform; + valid: boolean = false; + status: PostStatus = PostStatus.UNKNOWN; + scheduled?: Date; + posted?: Date; + results: {}[] = []; + title: string = ''; + body?: string; + tags?: string; + files?: { + text: string[], + image: string[], + video: string[], + other: string[] + }; + + constructor(folder: Folder, platform: Platform, data?: any) { + this.folder = folder; + this.platform = platform; + if (data) { + Object.assign(this, data); + this.scheduled = data.scheduled ? new Date(data.scheduled): undefined; + this.posted = data.posted ? new Date(data.posted): undefined; + } + } + + + /* + * save + * + * Save this post for this platform for the + * given folder. + */ + + save(): void { + const data = { ...this}; + delete data.folder; + delete data.platform; + fs.writeFileSync( + this.folder.path+'/'+this.platform.getPostFileName(), + JSON.stringify(data,null,"\t") + ); + } + + schedule(date:Date): void { + this.scheduled = date; + this.status = PostStatus.SCHEDULED; + this.save(); + } + + report(): string { + let report = ''; + report += '\nPost: '+this.platform.slug+' : '+this.folder.path; + report += '\n - valid: '+this.valid; + report += '\n - status: '+this.status; + report += '\n - scheduled: '+this.scheduled; + report += '\n - posted: '+this.posted; + return report; + } + +} + +export enum PostStatus { + UNKNOWN = "unknown", + UNSCHEDULED = "unscheduled", + SCHEDULED = "scheduled", + PUBLISHED = "published", + FAILED = "failed" +} + From da2936f8b5fb771b6597c92ce27313bc2da7f5cd Mon Sep 17 00:00:00 2001 From: pike Date: Sat, 30 Sep 2023 11:21:30 +0200 Subject: [PATCH 3/3] fix: Forgot changes --- README.md | 14 ++- index.ts | 146 +++++++++++++++++---------- package-lock.json | 18 ++-- package.json | 2 +- src/classes/Feed.ts | 190 ----------------------------------- src/classes/Folder.ts | 54 ---------- src/classes/Platform.ts | 127 ----------------------- src/classes/Post.ts | 68 ------------- src/platforms/AsFacebook.ts | 4 +- src/platforms/AsInstagram.ts | 4 +- src/platforms/AsLinkedIn.ts | 4 +- src/platforms/AsReddit.ts | 4 +- src/platforms/AsTikTok.ts | 4 +- src/platforms/AsTwitter.ts | 4 +- src/platforms/AsYouTube.ts | 4 +- src/platforms/Ayrshare.ts | 8 +- 16 files changed, 134 insertions(+), 521 deletions(-) delete mode 100644 src/classes/Feed.ts delete mode 100644 src/classes/Folder.ts delete mode 100644 src/classes/Platform.ts delete mode 100644 src/classes/Post.ts diff --git a/README.md b/README.md index b96bc70..c16cfae 100644 --- a/README.md +++ b/README.md @@ -75,7 +75,7 @@ next post automatically. ``` fairpost.js help -fairpost.js get-feed [--config=xxx] +fairpost.js get-feed fairpost.js get-folders [--folders=xxx,xxx] fairpost.js prepare-posts [--platforms=xxx,xxx] [--folders=xxx,xxx] fairpost.js get-posts [--status=xxx] [--platforms=xxx,xxx] [--folders=xxx,xxx] @@ -83,6 +83,18 @@ fairpost.js schedule-next-post [--date=xxxx-xx-xx] [--platforms=xxx,xxx] [--fold fairpost.js publish-due-posts [--platforms=xxx,xxx] [--folders=xxx,xxx] [--dry-run] ``` +### Common arguments + +``` +# Select which config file to use +fairpost.js [command] [arguments] --config=.env-test + +# Set the cli output format to pure json +fairpost.js [command] [arguments] --report=json + +``` + + ## Add a new platform To add support for a new platform, add a class to `src/platforms` diff --git a/index.ts b/index.ts index 030d167..0b36ee7 100644 --- a/index.ts +++ b/index.ts @@ -3,8 +3,8 @@ Fairpost cli handler */ -import Feed from './src/classes/Feed'; -import { PostStatus } from './src/classes/Post'; +import Feed from './src/Feed'; +import { PostStatus } from './src/Post'; import { PlatformSlug } from './src/platforms'; // arguments @@ -13,6 +13,7 @@ const COMMAND = process.argv[2] ?? 'help' // options const DRY_RUN = !!getOption('dry-run') ?? false; const CONFIG = (getOption('config') as string ) ?? '.env'; +const REPORT = (getOption('report') as string ) ?? 'text'; const PLATFORMS = (getOption('platforms') as string)?.split(',') as PlatformSlug[] ?? undefined; const FOLDERS = (getOption('folders') as string)?.split(',') ?? undefined; const DATE = (getOption('date') as string) ?? undefined; @@ -32,61 +33,100 @@ function getOption(key:string):boolean|string|null { /* main */ async function main() { + let result: any = ''; + let report = ''; + const feed = new Feed(CONFIG); - console.log('Fairpost '+feed.path+' starting .. ',DRY_RUN?'dry-run':''); - console.log(); + report += 'Fairpost '+feed.path+' starting .. ',DRY_RUN?'dry-run':''; + report += '\n'; - let result: any = ''; - switch(COMMAND) { - case 'get-feed': - result = feed; - break; - case 'get-platforms': - result = feed.getPlatforms(PLATFORMS); - break; - case 'get-folders': - result = feed.getFolders(FOLDERS); - break; - case 'get-posts': - result = feed.getPosts({ - paths:FOLDERS, - platforms:PLATFORMS, - status: STATUS - }); - break; - case 'prepare-posts': - result = await feed.preparePosts({ - paths:FOLDERS, - platforms:PLATFORMS - }); - break; - case 'schedule-next-posts': - result = feed.scheduleNextPosts(DATE ? new Date(DATE): undefined,{ - paths:FOLDERS, - platforms:PLATFORMS - }); - break; - case 'publish-due-posts': - result = await feed.publishDuePosts({ - paths:FOLDERS, - platforms:PLATFORMS - }, DRY_RUN); - break; - default: - const cmd = process.argv[1]; - console.log(` -${cmd} help -${cmd} get-feed [--config=xxx] -${cmd} get-platforms [--platforms=xxx,xxx] -${cmd} get-folders [--folders=xxx,xxx] -${cmd} prepare-posts [--platforms=xxx,xxx] [--folders=xxx,xxx] -${cmd} get-posts [--status=xxx] [--platforms=xxx,xxx] [--folders=xxx,xxx] -${cmd} schedule-next-post [--date=xxxx-xx-xx] [--platforms=xxx,xxx] [--folders=xxx,xxx] -${cmd} publish-due-posts [--platforms=xxx,xxx] [--folders=xxx,xxx] [--dry-run] - `); + try { + switch(COMMAND) { + case 'get-feed': + result = feed; + report = 'Feed: '+feed.path; + break; + case 'get-platforms': + const platforms = feed.getPlatforms(PLATFORMS); + platforms.forEach(platform => { + report += 'Platform: '+platform.slug+'\n'; + }); + result = platforms; + break; + case 'get-folders': + const folders = feed.getFolders(FOLDERS); + folders.forEach(folder => { + report += 'Folder: '+folder.path+'\n'; + }); + result = folders; + break; + case 'get-posts': + const allposts = feed.getPosts({ + paths:FOLDERS, + platforms:PLATFORMS, + status: STATUS + }); + allposts.forEach(post => { + report += post.report(); + }); + result = allposts; + break; + case 'prepare-posts': + const prepposts = await feed.preparePosts({ + paths:FOLDERS, + platforms:PLATFORMS + }); + prepposts.forEach(post => { + report += post.report(); + }); + result = prepposts; + break; + case 'schedule-next-posts': + const nextposts = feed.scheduleNextPosts(DATE ? new Date(DATE): undefined,{ + paths:FOLDERS, + platforms:PLATFORMS + }); + nextposts.forEach(post => { + report += post.report(); + }); + result = nextposts; + break; + case 'publish-due-posts': + const pubposts = await feed.publishDuePosts({ + paths:FOLDERS, + platforms:PLATFORMS + }, DRY_RUN); + pubposts.forEach(post => { + report += post.report(); + }); + result = nextposts; + break; + default: + const cmd = process.argv[1]; + result = [ + `${cmd} help`, + `${cmd} get-feed [--config=xxx]`, + `${cmd} get-platforms [--platforms=xxx,xxx]`, + `${cmd} get-folders [--folders=xxx,xxx]`, + `${cmd} prepare-posts [--platforms=xxx,xxx] [--folders=xxx,xxx]`, + `${cmd} get-posts [--status=xxx] [--platforms=xxx,xxx] [--folders=xxx,xxx]`, + `${cmd} schedule-next-post [--date=xxxx-xx-xx] [--platforms=xxx,xxx] [--folders=xxx,xxx]`, + `${cmd} publish-due-posts [--platforms=xxx,xxx] [--folders=xxx,xxx] [--dry-run]` + ]; + result.forEach(line => report += '\n'+line); + } + } catch (e) { + console.error(e.getMessage()); + } + + switch(REPORT) { + case 'json': + console.log(JSON.stringify(result,null,'\t')); + break; + default: + console.log(report); } - console.log(JSON.stringify(result,null,'\t')); } diff --git a/package-lock.json b/package-lock.json index 7f174dd..26bb308 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,7 +11,7 @@ "dependencies": { "dotenv": "^16.0.3", "node-fetch": "^2.6.7", - "sharp": "^0.31.1" + "sharp": "0.31.1" }, "devDependencies": { "ts-node": "^10.9.1", @@ -507,16 +507,16 @@ } }, "node_modules/sharp": { - "version": "0.31.3", - "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.31.3.tgz", - "integrity": "sha512-XcR4+FCLBFKw1bdB+GEhnUNXNXvnt0tDo4WsBsraKymuo/IAuPuCBVAL2wIkUw2r/dwFW5Q5+g66Kwl2dgDFVg==", + "version": "0.31.1", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.31.1.tgz", + "integrity": "sha512-GR8M1wBwOiFKLkm9JPun27OQnNRZdHfSf9VwcdZX6UrRmM1/XnOrLFTF0GAil+y/YK4E6qcM/ugxs80QirsHxg==", "hasInstallScript": true, "dependencies": { "color": "^4.2.3", "detect-libc": "^2.0.1", "node-addon-api": "^5.0.0", "prebuild-install": "^7.1.1", - "semver": "^7.3.8", + "semver": "^7.3.7", "simple-get": "^4.0.1", "tar-fs": "^2.1.1", "tunnel-agent": "^0.6.0" @@ -1083,15 +1083,15 @@ } }, "sharp": { - "version": "0.31.3", - "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.31.3.tgz", - "integrity": "sha512-XcR4+FCLBFKw1bdB+GEhnUNXNXvnt0tDo4WsBsraKymuo/IAuPuCBVAL2wIkUw2r/dwFW5Q5+g66Kwl2dgDFVg==", + "version": "0.31.1", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.31.1.tgz", + "integrity": "sha512-GR8M1wBwOiFKLkm9JPun27OQnNRZdHfSf9VwcdZX6UrRmM1/XnOrLFTF0GAil+y/YK4E6qcM/ugxs80QirsHxg==", "requires": { "color": "^4.2.3", "detect-libc": "^2.0.1", "node-addon-api": "^5.0.0", "prebuild-install": "^7.1.1", - "semver": "^7.3.8", + "semver": "^7.3.7", "simple-get": "^4.0.1", "tar-fs": "^2.1.1", "tunnel-agent": "^0.6.0" diff --git a/package.json b/package.json index ba1428b..09a3c62 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,7 @@ "dependencies": { "dotenv": "^16.0.3", "node-fetch": "^2.6.7", - "sharp": "^0.31.1" + "sharp": "0.31.1" }, "devDependencies": { "ts-node": "^10.9.1", diff --git a/src/classes/Feed.ts b/src/classes/Feed.ts deleted file mode 100644 index 56a1824..0000000 --- a/src/classes/Feed.ts +++ /dev/null @@ -1,190 +0,0 @@ - -import * as fs from 'fs'; -import * as path from 'path'; -import * as dotenv from 'dotenv'; -import Platform from "./Platform"; -import Folder from "./Folder"; -import Post from "./Post"; -import { PostStatus } from "./Post"; -import * as platforms from '../platforms'; -import { PlatformSlug } from '../platforms'; - -export default class Feed { - - path: string = ''; - platforms: { - [slug in PlatformSlug]? : Platform; - } = {}; - folders: Folder[] = []; - interval: number; - - constructor(configPath?: string) { - if (configPath) { - const configPathResolved = path.resolve(__dirname+'/../../../'+configPath); - dotenv.config({ path:configPathResolved }); - } else { - dotenv.config(); - } - if (process.env.FAIRPOST_FEED_PATH) { - this.path = process.env.FAIRPOST_FEED_PATH; - } else { - throw new Error('Problem reading .env config file'); - } - this.interval = Number(process.env.FAIRPOST_FEED_INTERVAL ?? 7); - - const activePlatformSlugs = process.env.FAIRPOST_FEED_PLATFORMS.split(','); - const platformClasses = fs.readdirSync(path.resolve(__dirname+'/../platforms')); - platformClasses.forEach(file=> { - const constructor = file.replace('.ts','').replace('.js',''); - if (platforms[constructor] !== undefined) { - const platform = new platforms[constructor](); - platform.active = activePlatformSlugs.includes(platform.slug); - if (platform.active) { - this.platforms[platform.slug] = platform; - } - } - }); - } - - getPlatforms(platforms?:PlatformSlug[]): Platform[] { - return platforms?.map(platform=>this.platforms[platform]) ?? Object.values(this.platforms); - } - - getAllFolders(): Folder[] { - if (this.folders.length) { - return this.folders; - } - if (!fs.existsSync(this.path)) { - fs.mkdirSync(this.path); - } - const paths = fs.readdirSync(this.path).filter(path => { - return fs.statSync(this.path+'/'+path).isDirectory() && - !path.startsWith('_') && !path.startsWith('.'); - }); - if (paths) { - this.folders = paths.map(path => new Folder(this.path+'/'+path)); - } - return this.folders; - } - - getFolders(paths?: string[]): Folder[] { - return paths?.map(path=>new Folder(this.path+'/'+path)) ?? this.getAllFolders(); - } - - getPosts(filters?: { - paths?:string[] - platforms?:PlatformSlug[], - status?:PostStatus - }): Post[] { - const posts: Post[] = []; - const platforms = this.getPlatforms(filters?.platforms); - const folders = this.getFolders(filters?.paths); - for (const folder of folders) { - for (const platform of platforms) { - const post = platform.getPost(folder); - if (post && (!filters?.status || filters.status.includes(post.status))) { - posts.push(post); - } - } - } - return posts; - } - - async preparePosts(filters?: { - paths?:string[] - platforms?:PlatformSlug[] - }): Promise { - - const posts: Post[] = []; - const platforms = this.getPlatforms(filters?.platforms); - const folders = this.getFolders(filters?.paths); - for (const folder of folders) { - for (const platform of platforms) { - const post = platform.getPost(folder); - if (post?.status!==PostStatus.PUBLISHED) { - const newPost = await platform.preparePost(folder); - if (newPost) { - posts.push(newPost); - } - } - } - } - return posts; - } - - - getLastPost(platform:PlatformSlug): Post | void { - let lastPost: Post = undefined; - const posts = this.getPosts({ - platforms: [platform], - status: PostStatus.PUBLISHED - }); - for (const post of posts) { - if (!lastPost || post.posted >= lastPost.posted) { - lastPost = post; - } - } - return lastPost; - } - - - getNextPostDate(platform:PlatformSlug): Date { - let nextDate = null; - const lastPost = this.getLastPost(platform); - if (lastPost) { - nextDate = new Date(lastPost.posted); - nextDate.setDate(nextDate.getDate()+this.interval); - } else { - nextDate = new Date(); - } - return nextDate; - } - - scheduleNextPosts(date?: Date, filters?: { - paths?:string[] - platforms?:PlatformSlug[] - }): Post[] { - const posts: Post[] = []; - const platforms = this.getPlatforms(filters?.platforms); - const folders = this.getFolders(filters?.paths); - for (const platform of platforms) { - const nextDate = date?date:this.getNextPostDate(platform.slug); - for (const folder of folders) { - const post = platform.getPost(folder); - if (post.valid && post?.status===PostStatus.UNSCHEDULED) { - post.schedule(nextDate); - posts.push(post); - break; - } - - } - } - return posts; - } - - async publishDuePosts(filters?: { - paths?:string[] - platforms?:PlatformSlug[] - }, dryrun:boolean = false): Promise { - const now = new Date(); - const posts: Post[] = []; - const platforms = this.getPlatforms(filters?.platforms); - const folders = this.getFolders(filters?.paths); - for (const platform of platforms) { - for (const folder of folders) { - const post = platform.getPost(folder); - if (post?.status===PostStatus.SCHEDULED) { - if (post.scheduled <= now) { - console.log('Posting',platform.slug,folder.path); - await platform.publishPost(post,dryrun); - posts.push(post); - break; - } - } - } - } - return posts; - } - - -} \ No newline at end of file diff --git a/src/classes/Folder.ts b/src/classes/Folder.ts deleted file mode 100644 index 3473b9d..0000000 --- a/src/classes/Folder.ts +++ /dev/null @@ -1,54 +0,0 @@ -import * as fs from 'fs'; -import * as path from 'path'; - -export default class Folder { - - path: string; - files?: { - text: string[], - image: string[], - video: string[], - other: string[] - }; - - constructor(path: string) { - this.path = path; - } - - getFiles() { - if (this.files!=undefined) { - return { - text: [ ...this.files.text ], - image: [ ...this.files.image ], - video: [ ...this.files.video ], - other: [ ...this.files.other ] - }; - } - const files = fs.readdirSync(this.path).filter(file => { - return fs.statSync(this.path+'/'+file).isFile() && - !file.startsWith('_') && - !file.startsWith('.'); - }); - this.files = { - text: [], - image: [], - video: [], - other: [] - }; - this.files.text = files.filter(file=>["txt"].includes(file.split('.')?.pop()??'')); - this.files.image = files.filter(file=>["jpg","jpeg","png"].includes(file.split('.')?.pop()??'')); - this.files.video = files.filter(file=>["mp4"].includes(file.split('.')?.pop()??'')); - this.files.other = files.filter(file=> - !this.files.text?.includes(file) - && !this.files.image?.includes(file) - && !this.files.video?.includes(file) - ); - return { - text: [ ...this.files.text ], - image: [ ...this.files.image ], - video: [ ...this.files.video ], - other: [ ...this.files.other ] - }; - } - -} \ No newline at end of file diff --git a/src/classes/Platform.ts b/src/classes/Platform.ts deleted file mode 100644 index 2f0458e..0000000 --- a/src/classes/Platform.ts +++ /dev/null @@ -1,127 +0,0 @@ -import * as fs from 'fs'; -import * as path from 'path'; -import Folder from "./Folder"; -import Post from "./Post"; -import { PostStatus } from "./Post"; -import { PlatformSlug } from "../platforms"; - - - -export default class Platform { - - active: boolean = false; - slug: PlatformSlug = PlatformSlug.UNKNOWN; - defaultBody: string = "Fairpost feed"; - - /* - * getPostFileName - * - * Return the intended name for a post of this - * platform to be saved in this folder. - */ - getPostFileName() { - return '_'+this.slug+'.json'; - } - - /* - * getPost - * - * Return the post for this platform for the - * given folder, if it exists. - */ - - getPost(folder: Folder): Post | undefined { - - //console.log(this.slug,'getPost',folder.path); - - if (fs.existsSync(folder.path+'/'+this.getPostFileName())) { - const data = JSON.parse(fs.readFileSync(folder.path+'/'+this.getPostFileName(), 'utf8')); - if (data) { - return new Post(folder,this,data); - } - } - return; - } - - /* - * preparePost - * - * Prepare the post for this platform for the - * given folder, and save it. Optionally create - * derivates of media and save those, too. - * - * If the post exists and is published, ignore. - * If the post exists and is failed, set it back to - * unscheduled. - */ - async preparePost(folder: Folder): Promise { - - console.log(this.slug,'preparePost',folder.path); - - const post = this.getPost(folder) ?? new Post(folder,this); - if (post.status===PostStatus.PUBLISHED) { - return; - } - if (post.status===PostStatus.FAILED) { - post.status=PostStatus.UNSCHEDULED; - } - - - // some default logic. override this - // in your own platform if you need. - - post.files = folder.getFiles(); - - if (post.files.text?.includes('body.txt')) { - post.body = fs.readFileSync(post.folder.path+'/body.txt','utf8'); - } else if (post.files.text.length === 1 ) { - const bodyFile = post.files.text[0]; - post.body = fs.readFileSync(post.folder.path+'/'+bodyFile,'utf8'); - } else { - post.body = this.defaultBody; - } - - if (post.files.text?.includes('title.txt')) { - post.title = fs.readFileSync(post.folder.path+'/title.txt','utf8'); - } else { - post.title = post.body.split('\n', 1)[0]; - } - - if (post.files.text?.includes('tags.txt')) { - post.tags = fs.readFileSync(post.folder.path+'/tags.txt','utf8'); - } - - if (post.title) { - post.valid = true; - } - - if (post.status===PostStatus.UNKNOWN) { - post.status=PostStatus.UNSCHEDULED; - } - - post.save(); - return post; - } - - /* - * publishPost - * - * publish the post for this platform, sync. - * set the posted date to now. - * add the result to post.results - * on success, set the status to published and return true, - * else set the status to failed and return false - */ - - async publishPost(post: Post, dryrun:boolean = false): Promise { - post.posted = new Date(); - post.results.push({ - error: 'publishing not implemented for '+this.slug - }); - post.status = PostStatus.FAILED; - post.save(); - return false; - } -} - - diff --git a/src/classes/Post.ts b/src/classes/Post.ts deleted file mode 100644 index 92f0876..0000000 --- a/src/classes/Post.ts +++ /dev/null @@ -1,68 +0,0 @@ - -import * as fs from 'fs'; -import * as path from 'path'; -import Folder from "./Folder"; -import Platform from "./Platform"; - -export default class Post { - folder: Folder; - platform: Platform; - valid: boolean = false; - status: PostStatus = PostStatus.UNKNOWN; - scheduled?: Date; - posted?: Date; - results: {}[] = []; - title: string = ''; - body?: string; - tags?: string; - files?: { - text: string[], - image: string[], - video: string[], - other: string[] - }; - - constructor(folder: Folder, platform: Platform, data?: any) { - this.folder = folder; - this.platform = platform; - if (data) { - Object.assign(this, data); - this.scheduled = data.scheduled ? new Date(data.scheduled): undefined; - this.posted = data.posted ? new Date(data.posted): undefined; - } - } - - - /* - * save - * - * Save this post for this platform for the - * given folder. - */ - - save(): void { - const data = { ...this}; - delete data.folder; - delete data.platform; - fs.writeFileSync( - this.folder.path+'/'+this.platform.getPostFileName(), - JSON.stringify(data,null,"\t") - ); - } - - schedule(date:Date): void { - this.scheduled = date; - this.status = PostStatus.SCHEDULED; - this.save(); - } - -} - -export enum PostStatus { - UNKNOWN = "unknown", - UNSCHEDULED = "unscheduled", - SCHEDULED = "scheduled", - PUBLISHED = "published", - FAILED = "failed" -} - diff --git a/src/platforms/AsFacebook.ts b/src/platforms/AsFacebook.ts index 24bc0b5..17a1105 100644 --- a/src/platforms/AsFacebook.ts +++ b/src/platforms/AsFacebook.ts @@ -1,8 +1,8 @@ import Ayrshare from "./Ayrshare"; import { PlatformSlug } from "."; -import Folder from "../classes/Folder"; -import Post from "../classes/Post"; +import Folder from "../Folder"; +import Post from "../Post"; import * as fs from 'fs'; import * as sharp from 'sharp'; diff --git a/src/platforms/AsInstagram.ts b/src/platforms/AsInstagram.ts index f5d2467..425943c 100644 --- a/src/platforms/AsInstagram.ts +++ b/src/platforms/AsInstagram.ts @@ -1,8 +1,8 @@ import Ayrshare from "./Ayrshare"; import { PlatformSlug } from "."; -import Folder from "../classes/Folder"; -import Post from "../classes/Post"; +import Folder from "../Folder"; +import Post from "../Post"; import * as sharp from 'sharp'; export default class AsInstagram extends Ayrshare { diff --git a/src/platforms/AsLinkedIn.ts b/src/platforms/AsLinkedIn.ts index d53c6a2..cd2fd6a 100644 --- a/src/platforms/AsLinkedIn.ts +++ b/src/platforms/AsLinkedIn.ts @@ -1,8 +1,8 @@ import Ayrshare from "./Ayrshare"; import { PlatformSlug } from "."; -import Folder from "../classes/Folder"; -import Post from "../classes/Post"; +import Folder from "../Folder"; +import Post from "../Post"; import * as fs from 'fs'; import * as sharp from 'sharp'; diff --git a/src/platforms/AsReddit.ts b/src/platforms/AsReddit.ts index 0b639ec..fa80d19 100644 --- a/src/platforms/AsReddit.ts +++ b/src/platforms/AsReddit.ts @@ -1,8 +1,8 @@ import Ayrshare from "./Ayrshare"; import { PlatformSlug } from "."; -import Folder from "../classes/Folder"; -import Post from "../classes/Post"; +import Folder from "../Folder"; +import Post from "../Post"; export default class AsReddit extends Ayrshare { slug = PlatformSlug.ASREDDIT; diff --git a/src/platforms/AsTikTok.ts b/src/platforms/AsTikTok.ts index 57fb237..12cd2a3 100644 --- a/src/platforms/AsTikTok.ts +++ b/src/platforms/AsTikTok.ts @@ -1,8 +1,8 @@ import Ayrshare from "./Ayrshare"; import { PlatformSlug } from "."; -import Folder from "../classes/Folder"; -import Post from "../classes/Post"; +import Folder from "../Folder"; +import Post from "../Post"; export default class AsTikTok extends Ayrshare { slug = PlatformSlug.ASTIKTOK; diff --git a/src/platforms/AsTwitter.ts b/src/platforms/AsTwitter.ts index b1dd9b0..9a35071 100644 --- a/src/platforms/AsTwitter.ts +++ b/src/platforms/AsTwitter.ts @@ -1,8 +1,8 @@ import Ayrshare from "./Ayrshare"; import { PlatformSlug } from "."; -import Folder from "../classes/Folder"; -import Post from "../classes/Post"; +import Folder from "../Folder"; +import Post from "../Post"; import * as fs from 'fs'; import * as sharp from 'sharp'; diff --git a/src/platforms/AsYouTube.ts b/src/platforms/AsYouTube.ts index e3cf37a..e74513c 100644 --- a/src/platforms/AsYouTube.ts +++ b/src/platforms/AsYouTube.ts @@ -1,8 +1,8 @@ import Ayrshare from "./Ayrshare"; import { PlatformSlug } from "."; -import Folder from "../classes/Folder"; -import Post from "../classes/Post"; +import Folder from "../Folder"; +import Post from "../Post"; export default class AsYouTube extends Ayrshare { slug = PlatformSlug.ASYOUTUBE; diff --git a/src/platforms/Ayrshare.ts b/src/platforms/Ayrshare.ts index 673abce..7d0e7b1 100644 --- a/src/platforms/Ayrshare.ts +++ b/src/platforms/Ayrshare.ts @@ -3,10 +3,10 @@ import * as fs from 'fs'; import * as path from 'path'; import { randomUUID } from 'crypto'; import { PlatformSlug } from "."; -import Platform from "../classes/Platform"; -import Folder from "../classes/Folder"; -import Post from "../classes/Post"; -import { PostStatus } from "../classes/Post"; +import Platform from "../Platform"; +import Folder from "../Folder"; +import Post from "../Post"; +import { PostStatus } from "../Post"; interface AyrshareResult { success: boolean;