#!/usr/bin/env node require('./Lib/Syncers.js').importAll(); const Path = require('path'); const Http = require('http'); const Https = require('https'); const Axios = require('axios'); const JsDom = require('jsdom').JSDOM; // true: Reuse the local directory structure for URL slugs // false: Keep a flat URL slug composing only of the file name const KeepTree = false; // Word separation characters in slugs; first is preferred const WordSeps = '-–—_| '; // Local OAuth server const Host = '127.0.0.1'; // TODO: // * Handle locally removed posts, only present on remote (delete or hide them on remote) // * Handle posts which filename changed and redirect URL has been set let [Auth, Session] = [ JSON.parse(process.env.WordpressAuth || '{}'), JSON.parse(process.env.WordpressSession || '{}'), ]; const ApiStr = { protocol: 'https://', host: 'public-api.wordpress.com', path: '/rest/v1.1', }; ApiStr.url = `${ApiStr.protocol}${ApiStr.host}${ApiStr.path}`; const Msg = { NoAuth: '\nPlease set the "WordpressAuth" ENV variable as a JSON string with keys "client_id" and "client_secret".\n()', NoSession: "\nNo valid session is available. You need to log in.\nOpen this link in a Web browser to log into Wordpress.com:\n", GotSession: '\nGot a new session string. Store it, and load it via the "WordpressSession" ENV variable for future use:\n', }; const AuthOpts = () => { return { headers: { "Authorization": `Bearer ${Session.access_token}`, "Content-Type": "application/x-www-form-urlencoded", }, }; }; let [LocalPosts, RemotePosts] = [{}, {}]; let [GotLocalPosts, GotRemotePosts] = [false, false]; // https://stackoverflow.com/a/73594511 Fs.walkSync = (Dir, Files = []) => { const dirFiles = Fs.readdirSync(Dir); for (const f of dirFiles) { const stat = Fs.lstatSync(Dir + Path.sep + f) if (stat.isDirectory()) { Fs.walkSync(Dir + Path.sep + f, Files); } else { Files.push(Dir + Path.sep + f); }; }; return Files; }; const MakeSlug = (File) => { let Slug = File .slice('./Posts/'.length) .split('.').slice(0, -1).join('.'); const Last = Slug.split('/').slice(-1)[0]; return ((!KeepTree && !(IsSlugTooSimple(Last) || Slug in LocalPosts)) ? Last : Slug); }; const IsSlugTooSimple = (Slug) => { let Nums = 0; WordSeps.split('').forEach((Sep) => { Slug = Slug.replaceAll(Sep, WordSeps[0]); }); Slug = Slug.split(WordSeps[0]); for (let i=0; i { const Serv = Http.createServer((Req, Res) => { Res.setHeader('Content-Type', 'text/plain'); const Query = new URLSearchParams(Req.url.slice(1).replaceAll('?', '')); const AuthCode = Query.get('code'); if (AuthCode) { Res.statusCode = 200; Res.end('This window can now be closed.'); Req = Https.request({ method: "POST", host: ApiStr.host, path: "/oauth2/token", headers: AuthOpts().headers, }, (Res) => { let Data = ''; Res.on('data', (Frag) => { Data += Frag; }).on('end', () => { console.log(`${Msg.GotSession}'${Data}'`); Session = JSON.parse(Data); }); }); Req.write(`&client_id=${Auth.client_id}&client_secret=${Auth.client_secret}&code=${AuthCode}&redirect_uri=http://${Host}:${Serv.address().port}&grant_type=authorization_code`); Req.end(); }; Res.statusCode = 500; Res.end(); }); Serv.listen(0, Host, () => { if (Auth.client_id && Auth.client_secret) { console.log(`${Msg.NoSession}<${ApiStr.protocol}${ApiStr.host}/oauth2/authorize?client_id=${Auth.client_id}&redirect_uri=http://${Host}:${Serv.address().port}&response_type=code>`); } else { console.log(Msg.NoAuth); Serv.close(); }; }); }; const GetLocalPosts = () => { Fs.walkSync('./Posts').forEach((File) => { const Meta = ParseMeta(Fs.readFileSync(File, 'utf8').trim().split('\n\n')[0]); if (Meta.Meta.Type === 'Post' || !Meta.Meta.Type) { const BuiltFile = `./public.Content/${File.split('.').slice(0, -1).join('.')}.html`; const Content = Fs.readFileSync(BuiltFile, 'utf8'); const Slug = MakeSlug(File); LocalPosts[Slug] = Object.assign({ Path: Slug, Content: Content, Macros: Meta.Macros, }, Meta.Meta); LocalPosts[Slug].Title ||= JsDom.fragment(Content) .querySelector('h1, h2, h3, h4, h5, h6') .querySelector('.SectionTitle, .staticoso-SectionTitle') .textContent; }; }); GotLocalPosts = true; }; const GetRemotePosts = (Page) => { const QueryOpts = new URLSearchParams({ context: "edit", fields: "ID,slug,status,categories,tags,title,content", page_handle: encodeURIComponent(Page || ''), }).toString(); // https://developer.wordpress.com/docs/api/1.1/get/sites/%24site/posts/ Axios.get(`${ApiStr.url}/sites/${Session.blog_id}/posts/?&${QueryOpts}`, AuthOpts()).then((Res) => { const NextPage = Res.data.meta.next_page; Res.data.posts.forEach((Post) => { RemotePosts[Post.slug] = Post; }); if (NextPage) { GetRemotePosts(NextPage); process.stdout.write('.'); } else { GotRemotePosts = true; }; }); }; const IsLocalRemotePostEqual = (Loc, Rem) => { if (true && Loc.Title === Rem.title && Loc.Content === Rem.content && Loc.Description === Rem.excerpt && Loc.Categories === Rem.categories && Loc.Tags === Rem.tags && Loc.CreatedOn === Rem.date ) return true; else return false; }; const AfterFetch = () => { //let HaveNewPosts = false; console.log(LocalPosts); console.log(RemotePosts); Object.values(LocalPosts).forEach(async (Post) => { const Slug = Post.Path; const RemPost = RemotePosts[Slug]; const ReqBody = { ID: RemPost ? RemPost.ID : "", slug: Slug, status: "draft", date: Post.CreatedOn || "", title: Post.Title, content: Post.Content || "", excerpt: Post.Description || "", categories: Post.Categories || "", tags: Post.Tags || "", }; const QueryOpts = new URLSearchParams({ context: "edit", }).toString(); console.log(Slug, Slug in RemotePosts); if (RemPost) { // Post is on remote: Check if remote data is same as local, update remote if not if (!IsLocalRemotePostEqual(Post, RemPost)) { // https://developer.wordpress.com/docs/api/1.1/post/sites/%24site/posts/%24post_ID/ //await Axios.post(`${ApiStr.url}/sites/${Session.blog_id}/posts/new/?&${QueryOpts}`, Object.assign(ReqBody, {}), AuthOpts()).then((Res) => { //console.log(Res.data); //}); }; } else { // Post doesnt exist; create blank post on remote (as draft), then edit it like the first case //HaveNewPosts = true; // https://developer.wordpress.com/docs/api/1.1/post/sites/%24site/posts/new/ //await Axios.post(`${ApiStr.url}/sites/${Session.blog_id}/posts/new/?&${QueryOpts}`, Object.assign(ReqBody, { status: "draft", }), AuthOpts()).then((Res) => { //console.log(Res.data); //}); }; }); }; const TryUpsync = () => { console.log('[I] ^ Reading local posts...'); GetLocalPosts(); console.log('[I] ^ Fetching remote posts...'); GetRemotePosts(); var Interv = setInterval(() => { if (GotLocalPosts && GotRemotePosts) { clearInterval(Interv); AfterFetch(); }; }, 50); }; if (!Session.access_token) { AuthServer(); }; if (Session.access_token) { TryUpsync(); };