2023-05-06 00:13:32 +02:00
#!/usr/bin/env node
2023-05-07 16:35:05 +02:00
require ( './Lib/Syncers.js' ) . importAll ( ) ;
const Path = require ( 'path' ) ;
2023-05-06 00:13:32 +02:00
const Http = require ( 'http' ) ;
const Https = require ( 'https' ) ;
2023-05-07 16:35:05 +02:00
// true: Reuse the local directory structure for URL slugs
// false: Keep a flat URL slug composing only of the file name
const KeepTree = false ;
// Local OAuth server
2023-05-06 00:13:32 +02:00
const Host = '127.0.0.1' ;
2023-05-07 16:35:05 +02:00
2023-05-06 00:13:32 +02:00
let [ Auth , Session ] = [
JSON . parse ( process . env . WordpressAuth || '{}' ) ,
JSON . parse ( process . env . WordpressSession || '{}' ) ,
] ;
const ApiBase = 'public-api.wordpress.com' ;
const Msg = {
NoAuth : '\nPlease set the "WordpressAuth" ENV variable as a JSON string with keys "client_id" and "client_secret".\n(<https://developer.wordpress.com/apps/>)' ,
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 AuthHeaders = ( ) => {
return {
headers : {
"Authorization" : ` Bearer ${ Session . access _token } ` ,
} ,
} ;
} ;
2023-05-07 16:35:05 +02:00
let [ LocalPosts , RemotePosts ] = [ [ ] , [ ] ] ;
let FetchingRemotePosts = 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 ;
} ;
2023-05-06 00:13:32 +02:00
// https://developer.wordpress.com/docs/oauth2/
const AuthServer = ( ) => {
const Serv = Http . createServer ( ( Req , Res ) => {
Res . setHeader ( 'Content-Type' , 'text/plain' ) ;
2023-05-07 16:35:05 +02:00
const Query = new URLSearchParams ( Req . url . slice ( 1 ) . replaceAll ( '?' , '' ) ) ;
const AuthCode = Query . get ( 'code' ) ;
2023-05-06 00:13:32 +02:00
if ( AuthCode ) {
Res . statusCode = 200 ;
Res . end ( 'This window can now be closed.' ) ;
Req = Https . request ( {
method : "POST" ,
host : ApiBase ,
path : "/oauth2/token" ,
headers : { "Content-Type" : "application/x-www-form-urlencoded" , } ,
} , ( 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 } <https://public-api.wordpress.com/oauth2/authorize?client_id= ${ Auth . client _id } &redirect_uri=http:// ${ Host } : ${ Serv . address ( ) . port } &response_type=code> ` ) ;
} else {
console . log ( Msg . NoAuth ) ;
Serv . close ( ) ;
} ;
} ) ;
} ;
2023-05-07 16:35:05 +02:00
const GetRemotePosts = ( Page ) => {
FetchingRemotePosts = true ;
const Opts = {
Url : ` https:// ${ ApiBase } /rest/v1.1/sites/ ${ Session . blog _id } /posts/ ` ,
fields : "ID,slug,status,categories,tags,title,content" ,
page _handle : encodeURIComponent ( Page || '' ) ,
} ;
Https . get ( ` ${ Opts . Url } ?&fields= ${ Opts . fields } &context=edit&page_handle= ${ Opts . page _handle } ` , AuthHeaders ( ) ,
2023-05-06 00:13:32 +02:00
( Res ) => {
let Data = '' ;
Res . on ( 'data' , ( Frag ) => {
Data += Frag ;
} ) . on ( 'end' , ( ) => {
2023-05-07 16:35:05 +02:00
const JsonData = JSON . parse ( Data ) ;
const NextPage = JsonData . meta . next _page ;
RemotePosts = RemotePosts . concat ( JsonData . posts ) ;
if ( NextPage ) {
GetRemotePosts ( NextPage ) ;
process . stdout . write ( '.' ) ;
} else {
FetchingRemotePosts = false ;
} ;
} ) ;
} ) ;
} ;
const TryUpsync = ( ) => {
console . log ( '[I] Reading local posts...' ) ;
Fs . walkSync ( './Posts' ) . forEach ( ( File ) => {
let Content = Fs . readFileSync ( File , 'utf8' ) . trim ( ) ;
const Meta = ParseMeta ( Content . split ( '\n\n' ) [ 0 ] ) ;
if ( Meta . Meta . Type === 'Post' || ! Meta . Meta . Type ) {
Content = Content . split ( '\n\n' ) . slice ( '1' ) . join ( '\n\n' ) ;
const Slug = KeepTree
? File . slice ( './Posts/' . length )
: File . split ( '/' ) . slice ( - 1 ) [ 0 ] ;
const Obj = { Path : Slug , Meta : Meta , Content : Content , } ;
LocalPosts . push ( Obj ) ;
//LocalPosts[Slug] = Obj;
} ;
} ) ;
console . log ( LocalPosts ) ;
console . log ( '[I] Fetching remote posts...' ) ;
GetRemotePosts ( ) ;
var Interv = setInterval ( ( ) => {
if ( ! FetchingRemotePosts ) {
clearInterval ( Interv ) ;
2023-05-06 00:13:32 +02:00
console . log ( RemotePosts ) ;
// Find out which posts exist on remote and which not
// Exist on remote and local:
// Check if remote data is same as local, update remote if not
2023-05-07 16:35:05 +02:00
// https://developer.wordpress.com/docs/api/1.1/post/sites/%24site/posts/%24post_ID/
2023-05-06 00:13:32 +02:00
// Exist on local only:
// Create blank post on remote (as draft), then edit it like the first case
2023-05-07 16:35:05 +02:00
// https://developer.wordpress.com/docs/api/1.1/post/sites/%24site/posts/new/
2023-05-06 00:13:32 +02:00
// Exist on remote only:
// Was probably deleted or moved locally, what to do here?
2023-05-07 16:35:05 +02:00
} ;
} , 50 ) ;
2023-05-06 00:13:32 +02:00
} ;
if ( ! Session . access _token ) {
AuthServer ( ) ;
} ;
if ( Session . access _token ) {
TryUpsync ( ) ;
} ;