err => { if( err ) return finishedExportToTwitter(err); if( Array.isArray(newLastRead) && newLastRead.length > 0 ) putLastReadToFile( path.resolve(__dirname, sourceArea + '.lastread.json'), newLastRead ); var numTweets = msgExports.length; if( numTweets > 0 ){ cl.status( `Done. ${numTweets} tweets posted from ${sourceArea}.` ); } else cl.skip( `Done. No new tweets posted from ${sourceArea}.` ); return finishedExportToTwitter(null); }
var weightedCrop = (tweetText, textLimit) => { var newLength = tweetText.length; var newText; var does_not_fit = true; // `weightedCrop` is called only when `newLength` does not fit initially while( does_not_fit ){ newLength--; if( newLength < 1 ){ cl.fail('Cannot shorten: ' + tweetText); cl.status('Text limit: ' + textLimit); process.exit(1); } newText = tweetText.slice(0, newLength).replace( /[\uD800-\uDBFF]$/g, '' // kill a trailing high surrogate, if any ) + '…'; if( twitxt.parseTweet(newText).weightedLength <= textLimit ){ // does_not_fit = false; return newText; } } };
cl.fail(`A file's path is not given after "--msg=".`); process.exit(1); } return false; } return true; }); if( params.length < 1 ){ clog('Usage:'); clog(' fido2twi sourceArea'); clog(''); clog('Parameter:'); clog(''); clog('sourceArea -- areatag of an echomail area in Fidonet'); clog(''); clog('An optional "--msg=filename" parameter (before or after the above)'); clog('means that an individual message (contained in the given file)'); clog('becomes tweeted from the given echomail area.'); process.exit(1); } const sourceArea = params[0]; if( msgFilePath !== null ){ cl.status(`Posting an individual message from ${sourceArea}...`); } else cl.status(`Looking in ${sourceArea} for tweets...`); require('./f2t-core.js')(sourceArea, { msgFilePath: msgFilePath });
module.exports = (sourceArea, options) => { var confF2T = simteconf( path.resolve(__dirname, 'fido2twi.config'), { skipNames: ['//', '#'] } ); // Read skipped lines: var SkipBySubj = confF2T.all('SkipBySubj'); if( SkipBySubj === null ) SkipBySubj = []; SkipBySubj = SkipBySubj.map( nextSubj => nextSubj.trim() ); // Read HPT areas: var areas = fidoconfig.areas(confF2T.last('AreasHPT'), { encoding: confF2T.last('EncodingHPT') || 'utf8' }); // Read IPFS configuration: var hostportIPFS = confF2T.last('IPFS'); if( hostportIPFS === null ){ cl.fail('IPFS settings are not found in fido2twi.config.'); process.exit(1); } var [hostIPFS, portIPFS] = hostportIPFS.split(':'); if( typeof portIPFS === 'undefined' ){ portIPFS = 5001; cl.status( 'IPFS port is not given in fido2twi.config; assuming port 5001.' ); } // read an array of FGHI URLs of last read messages: var arrLastRead = getLastReadFromFile( path.resolve(__dirname, sourceArea + '.lastread.json') ); var twi = new twitter({ consumer_key: confF2T.last('ConsumerKey'), consumer_secret: confF2T.last('ConsumerSecret'), access_token_key: confF2T.last('AccessTokenKey'), access_token_secret: confF2T.last('AccessTokenSecret') }); async.waterfall([ // read the path of the given echomail area callback => areas.area(sourceArea, (err, areaData) => { if( err ) return quitOnAreaError(err, sourceArea); return callback(null, areaData.path); }), (areaPath, callback) => { // initialize the echobase, read its index var echobase = JAM(areaPath); echobase.readJDX( err => callback(err, echobase) ); }, (echobase, callback) => { // get the username in Twitter twi.get( 'account/verify_credentials', { include_entities: false, skip_status: true, include_email: false }, (err, credentials) => { if( err ) return callback(err); if( typeof credentials.screen_name !== 'string' || credentials.screen_name.length < 1 ) return callback( new Error('Invalid `screen_name` credentials.') ); cl.ok(`Successfully verified credentials of @${ credentials.screen_name}.`); return callback(null, { twiUsername: credentials.screen_name, echobase: echobase }); } ); }, (wrappedData, callback) => { // get the configuration of Twitter twi.get( 'help/configuration', (err, twiConfig) => { if( err ) return callback(err); if( typeof twiConfig.short_url_length_https !== 'number' || twiConfig.short_url_length_https < 2 ) return callback(new Error( "Abnormal Twitter's `short_url_length_https` configuration." )); cl.ok(`Read Twitter's configuration. HTTPS short URLs are ${ twiConfig.short_url_length_https} characters long.`); wrappedData.textLimit = 279 - twiConfig.short_url_length_https; return callback(null, wrappedData); } ); }, (wrappedData, callback) => {//generate an array of tweet texts var echobase = wrappedData.echobase; if( options.msgFilePath !== null ) return postTweetFromMessage( options.msgFilePath, echobase, sourceArea, wrappedData.textLimit, wrappedData.twiUsername, hostIPFS, portIPFS, callback ); var echosize = echobase.size(); if( echosize < 1 ) return callback(null, []); var msgExports = []; var nextMessageNum = echosize; var lastReadEncountered = false; var newLastRead = []; // `msgExports` is filled in reverse chronological order // `msgExports` would contain (string) message texts for Twitter async.doUntil( exportDone => { echobase.readHeader(nextMessageNum, (err, header) => { if( err ) return exportDone(err); nextMessageNum--; var decoded = echobase.decodeHeader(header); var itemFGHIURL = generateMessageFGHIURL( sourceArea, decoded.msgid, decoded.origTime ); // header and URL are enough to decide if an export happens if( arrLastRead.includes(itemFGHIURL) ){ lastReadEncountered = true; return exportDone(null); // do not export previously read } else newLastRead.push(itemFGHIURL); if( typeof decoded.from === 'string' && decoded.from.startsWith('@') // probably a Twitter handle ) return exportDone(null); // do not re-export to Twitter if( typeof decoded.subj === 'string' && SkipBySubj.includes( decoded.subj.trim() ) ) return exportDone(null); // skip → do not post to Twitter if( Array.isArray(decoded.kludges) && decoded.kludges.some(aKludge => aKludge.toLowerCase() === 'sourcesite: twitter' ) ) return exportDone(null); // do not re-export to Twitter // now it's decided that an export should happen generateTweetExport( msgExports, wrappedData.twiUsername, sourceArea, echobase, header, decoded, wrappedData.textLimit, itemFGHIURL, hostIPFS, portIPFS, exportDone ); }); }, // `true` if should stop exporting: () => lastReadEncountered || nextMessageNum < 1 || msgExports.length >= maxExports, err => callback(err, msgExports, newLastRead) ); }, (msgExports, newLastRead, finishedExportToTwitter) => { // `newLastRead` may be not an Array, e.g. after single message post var lastIDX = msgExports.length - 1; async.eachOfSeries( msgExports.reverse(), // restore chronological order (nextMessage, messageIDX, sentToTwitter) => { twi.post( 'statuses/update', { status: nextMessage }, err => { if( err ) return sentToTwitter(err); cl.ok(nextMessage); if( messageIDX < lastIDX ){ setTimeout(() => { return sentToTwitter(null); }, twiDelay); } else return sentToTwitter(null); } ); }, err => { if( err ) return finishedExportToTwitter(err); if( Array.isArray(newLastRead) && newLastRead.length > 0 ) putLastReadToFile( path.resolve(__dirname, sourceArea + '.lastread.json'), newLastRead ); var numTweets = msgExports.length; if( numTweets > 0 ){ cl.status( `Done. ${numTweets} tweets posted from ${sourceArea}.` ); } else cl.skip( `Done. No new tweets posted from ${sourceArea}.` ); return finishedExportToTwitter(null); } ); } ], err => { // waterfall finished if( err ){ cl.fail('fido2twi error:'); console.dir(err); } }); };