├── LICENSE ├── README.md ├── exampleFeed.xml ├── package.json └── tweetstorss.js /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Dave Winer 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | #### What it is 2 | 3 | A node.js app that periodically reads a Twitter account and generates an RSS feed from it. 4 | 5 | Written by Dave Winer. 6 | 7 | #### Overview 8 | 9 | Once it's set up, every minute it gets the most recent tweets for one Twitter account, and writes an RSS file with the content of those tweets. 10 | 11 | I've included an example feed in the repo to show you what one looks like. 12 | 13 | #### Set up with Twitter 14 | 15 | You need to set four environment variables, to connect this app with Twitter. 16 | 17 | To begin, create a new app at apps.twitter.com. From there, you'll need to set environment variables with these four values. 18 | 19 | 1. twitterConsumerKey 20 | 21 | 2. twitterConsumerSecret 22 | 23 | 3. twitterAccessToken 24 | 25 | 4. twitterAccessTokenSecret 26 | 27 | You can get these values by clicking on the Test OAuth button in apps.twitter.com, as shown in this screen shot. 28 | 29 | All four values are shown on that page. Perfect. ;-) 30 | 31 | #### Which Twitter account, where to save the feed 32 | 33 | twitterScreenName -- the screen_name of the user whose timeline you want to convert to RSS. Examples of screen names: davewiner, nyt, dsearls, nakedjen. 34 | 35 | pathRssFile -- this is the local filesystem path for the file we'll maintain. It's optional, if you don't specify it we write the file as rss.xml in the same folder as the app. 36 | 37 | #### Managing multiple feeds 38 | 39 | In version 0.45 I added a feature that allows you to watch more than one Twitter account, producing a separate RSS feed for each. 40 | 41 | If there's a file called config.json in the same folder as tweetstorss.js, the app will read it every minute, and use the accounts listed in the file, instead of the twitterScreenName environment variable. 42 | 43 | Here's an example of the config.json file that's running on my system. 44 | 45 | 1. The folder value says where to store the generated RSS feeds. It can be a relative path as shown in the example, or can be a path from the root of your filesystem. 46 | 47 | 2. The items array is a set of objects, each of which specifies a Twitter username and the name of the RSS file created from the account. 48 | 49 | Because we read config.json every time we do a scan, you can change the file without having to relaunch tweetstorss.js. 50 | 51 | #### Questions, comments? 52 | 53 | Please use the server-snacks list for support. 54 | 55 | -------------------------------------------------------------------------------- /exampleFeed.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | NYT's RSS Feed 6 | http://twitter.com/NYT/ 7 | A feed generated from NYT's tweets by https://github.com/scripting/tweetsToRss 8 | Fri, 16 Jan 2015 13:17:00 GMT 9 | Fri, 16 Jan 2015 13:21:18 GMT 10 | en-us 11 | tweetsToRss 12 | http://cyber.law.harvard.edu/rss/rss.html 13 | NYT 14 | 15 | Well: Ask Well: Is Rebounding Good Exercise?. 16 | Fri, 16 Jan 2015 13:17:00 GMT 17 | http://t.co/LnnoQdedcn 18 | http://twitter.com/NYT/status/556077661077143552 19 | 20 | 21 | 22 | DealBook: Currency Traders Rattled in Wake of Swiss Central Bank Move. 23 | Fri, 16 Jan 2015 13:13:00 GMT 24 | http://t.co/YMopTUpIKo 25 | http://twitter.com/NYT/status/556076654796484609 26 | 27 | 28 | 29 | DealBook: Goldman Sachs Profit Drops but Beats Estimates. 30 | Fri, 16 Jan 2015 13:12:00 GMT 31 | http://t.co/xZEiUmyM7k 32 | http://twitter.com/NYT/status/556076402685267971 33 | 34 | 35 | 36 | Top E.U. Regulator Says Amazon’s Tax Deal With Luxembourg May Break State Aid Rules. 37 | Fri, 16 Jan 2015 13:02:00 GMT 38 | http://t.co/yMzKWTkoYz 39 | http://twitter.com/NYT/status/556073886773952512 40 | 41 | 42 | 43 | DealBook: Currency Traders Rattled in Wake of Swiss Central Bank Move. 44 | Fri, 16 Jan 2015 12:27:00 GMT 45 | http://t.co/g4R3k6H31W 46 | http://twitter.com/NYT/status/556065078576181248 47 | 48 | 49 | 50 | A.F.C. Championship Game Matchup. 51 | Fri, 16 Jan 2015 12:24:00 GMT 52 | http://t.co/h6Vv5YnPht 53 | http://twitter.com/NYT/status/556064322712248321 54 | 55 | 56 | 57 | N.F.C. Championship Game Matchup. 58 | Fri, 16 Jan 2015 12:23:00 GMT 59 | http://t.co/G3GPozX3fH 60 | http://twitter.com/NYT/status/556064071733477376 61 | 62 | 63 | 64 | Patrick Chappatte: Boko Haram in Nigeria. 65 | Fri, 16 Jan 2015 12:22:00 GMT 66 | http://t.co/XqPU2fRR7p 67 | http://twitter.com/NYT/status/556063820628885504 68 | 69 | 70 | 71 | Contributing Op-Ed Writer: Xi’s Selective Punishment. 72 | Fri, 16 Jan 2015 11:53:00 GMT 73 | http://t.co/nh9SSqaPbF 74 | http://twitter.com/NYT/status/556056521738821632 75 | 76 | 77 | 78 | Contributing Op-Ed Writer: Don’t Limit Speech in France. 79 | Fri, 16 Jan 2015 11:52:00 GMT 80 | http://t.co/geL7z5oC8z 81 | http://twitter.com/NYT/status/556056269992505344 82 | 83 | 84 | 85 | Op-Ed Contributor: The Shape of Japan to Come. 86 | Fri, 16 Jan 2015 11:43:00 GMT 87 | http://t.co/vz3CyNkDla 88 | http://twitter.com/NYT/status/556054005852692483 89 | 90 | 91 | 92 | Op-Ed Contributors: Argentina’s Lessons for Greece. 93 | Fri, 16 Jan 2015 11:42:00 GMT 94 | http://t.co/cPW7Y8Y6uN 95 | http://twitter.com/NYT/status/556053753359773696 96 | 97 | 98 | 99 | City Room: New York Today: Where Empty Bottles Go. 100 | Fri, 16 Jan 2015 11:22:00 GMT 101 | http://t.co/7JcF5qrhlU 102 | http://twitter.com/NYT/status/556048721193209856 103 | 104 | 105 | 106 | Letter: Encouraging Women to Be Heard. 107 | Fri, 16 Jan 2015 10:52:00 GMT 108 | http://t.co/K9SJxrttPC 109 | http://twitter.com/NYT/status/556041171764793344 110 | 111 | 112 | 113 | Music Review: Sam Smith’s Sold-Out Show at Madison Square Garden. 114 | Fri, 16 Jan 2015 10:37:00 GMT 115 | http://t.co/52M3vXTVlg 116 | http://twitter.com/NYT/status/556037395880345600 117 | 118 | 119 | 120 | The Learning Network: What Are Your Favorite Movies Ever?. 121 | Fri, 16 Jan 2015 10:27:00 GMT 122 | http://t.co/ao1M3bwEaz 123 | http://twitter.com/NYT/status/556034880220717057 124 | 125 | 126 | 127 | The Learning Network: Test Yourself | A Go-Kart Star Called Little Alonso. 128 | Fri, 16 Jan 2015 10:03:00 GMT 129 | http://t.co/ReFCoByiDv 130 | http://twitter.com/NYT/status/556028838753599488 131 | 132 | 133 | 134 | The Learning Network: 6 Q’s About the News | Racial Bias, Even When We Have Good Intentions. 135 | Fri, 16 Jan 2015 10:02:00 GMT 136 | http://t.co/s5gK3s7OeN 137 | http://twitter.com/NYT/status/556028588617904128 138 | 139 | 140 | 141 | Editorial: Standing Up to the N.R.A.. 142 | Fri, 16 Jan 2015 09:38:00 GMT 143 | http://t.co/8t0Rf9wBSl 144 | http://twitter.com/NYT/status/556022547721244672 145 | 146 | 147 | 148 | Editorial: Gov. Cuomo on New York City’s Woes. 149 | Fri, 16 Jan 2015 09:37:00 GMT 150 | http://t.co/cDs1JJjWLP 151 | http://twitter.com/NYT/status/556022296583086080 152 | 153 | 154 | 155 | 156 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tweetsToRss", 3 | "description": "A node.js app that periodically reads a Twitter account and generates an RSS feed from it.", 4 | "author": "Dave Winer ", 5 | "version": "0.40.0", 6 | "scripts": { 7 | "start": "node tweetstorss.js" 8 | }, 9 | "dependencies" : { 10 | "node-twitter-api": "*" 11 | }, 12 | "license": "MIT", 13 | "engines": { 14 | "node": "0.10.*" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /tweetstorss.js: -------------------------------------------------------------------------------- 1 | //The MIT License (MIT) 2 | 3 | //Copyright (c) 2015 Dave Winer 4 | 5 | //Permission is hereby granted, free of charge, to any person obtaining a copy 6 | //of this software and associated documentation files (the "Software"), to deal 7 | //in the Software without restriction, including without limitation the rights 8 | //to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | //copies of the Software, and to permit persons to whom the Software is 10 | //furnished to do so, subject to the following conditions: 11 | 12 | //The above copyright notice and this permission notice shall be included in all 13 | //copies or substantial portions of the Software. 14 | 15 | //THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | //IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | //FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | //AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | //LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | //OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | //SOFTWARE. 22 | 23 | var myVersion = "0.45", myProductName = "tweetsToRss", myProductUrl = "https://github.com/scripting/tweetsToRss"; 24 | 25 | var fs = require ("fs"); 26 | var twitterAPI = require ("node-twitter-api"); 27 | 28 | var twitterConsumerKey = process.env.twitterConsumerKey; 29 | var twitterConsumerSecret = process.env.twitterConsumerSecret; 30 | var accessToken = process.env.twitterAccessToken; 31 | var accessTokenSecret = process.env.twitterAccessTokenSecret; 32 | var twitterScreenName = process.env.twitterScreenName; 33 | var pathRssFile = process.env.pathRssFile; 34 | 35 | var defaultRssFilePath = "rss.xml"; 36 | var flSkipReplies = true; 37 | 38 | var configStruct = undefined; //1/16/15 by DW 39 | var fnameConfig = "config.json"; 40 | 41 | 42 | 43 | function loadConfigStruct (callback) { //1/16/15 by DW 44 | fs.readFile (fnameConfig, "utf8", function (err, data) { 45 | if (!err) { 46 | try { 47 | configStruct = JSON.parse (data); 48 | if (configStruct.folder != undefined) { 49 | if (!endsWith (configStruct.folder, "/")) { 50 | configStruct.folder += "/"; 51 | } 52 | } 53 | } 54 | catch (tryError) { 55 | console.log ("loadConfigStruct: error == " + tryError.message); 56 | } 57 | } 58 | if (callback != undefined) { 59 | callback (); 60 | } 61 | }); 62 | } 63 | 64 | function twTwitterDateToGMT (twitterDate) { //7/16/14 by DW 65 | return (new Date (twitterDate).toGMTString ()); 66 | } 67 | function stringLower (s) { //1/13/15 by DW 68 | return (s.toLowerCase ()); 69 | } 70 | function filledString (ch, ct) { //6/4/14 by DW 71 | var s = ""; 72 | for (var i = 0; i < ct; i++) { 73 | s += ch; 74 | } 75 | return (s); 76 | } 77 | function encodeXml (s) { //7/15/14 by DW 78 | if (s === undefined) { 79 | return (""); 80 | } 81 | else { 82 | var charMap = { 83 | '<': '<', 84 | '>': '>', 85 | '&': '&', 86 | '"': '&'+'quot;' 87 | }; 88 | s = s.toString(); 89 | s = s.replace(/\u00A0/g, " "); 90 | var escaped = s.replace(/[<>&"]/g, function(ch) { 91 | return charMap [ch]; 92 | }); 93 | return escaped; 94 | } 95 | } 96 | function trimWhitespace (s) { //rewrite -- 5/30/14 by DW 97 | function isWhite (ch) { 98 | switch (ch) { 99 | case " ": case "\r": case "\n": case "\t": 100 | return (true); 101 | } 102 | return (false); 103 | } 104 | if (s === undefined) { //9/10/14 by DW 105 | return (""); 106 | } 107 | while (isWhite (s.charAt (0))) { 108 | s = s.substr (1); 109 | } 110 | while (s.length > 0) { 111 | if (!isWhite (s.charAt (0))) { 112 | break; 113 | } 114 | s = s.substr (1); 115 | } 116 | while (s.length > 0) { 117 | if (!isWhite (s.charAt (s.length - 1))) { 118 | break; 119 | } 120 | s = s.substr (0, s.length - 1); 121 | } 122 | return (s); 123 | } 124 | function beginsWith (s, possibleBeginning, flUnicase) { 125 | if (s.length == 0) { //1/1/14 by DW 126 | return (false); 127 | } 128 | if (flUnicase === undefined) { 129 | flUnicase = true; 130 | } 131 | if (flUnicase) { 132 | for (var i = 0; i < possibleBeginning.length; i++) { 133 | if (s [i].toLowerCase () != possibleBeginning [i].toLowerCase ()) { 134 | return (false); 135 | } 136 | } 137 | } 138 | else { 139 | for (var i = 0; i < possibleBeginning.length; i++) { 140 | if (s [i] != possibleBeginning [i]) { 141 | return (false); 142 | } 143 | } 144 | } 145 | return (true); 146 | } 147 | function endsWith (s, possibleEnding, flUnicase) { 148 | if ((s === undefined) || (s.length == 0)) { 149 | return (false); 150 | } 151 | var ixstring = s.length - 1; 152 | if (flUnicase === undefined) { 153 | flUnicase = true; 154 | } 155 | if (flUnicase) { 156 | for (var i = possibleEnding.length - 1; i >= 0; i--) { 157 | if (stringLower (s [ixstring--]) != stringLower (possibleEnding [i])) { 158 | return (false); 159 | } 160 | } 161 | } 162 | else { 163 | for (var i = possibleEnding.length - 1; i >= 0; i--) { 164 | if (s [ixstring--] != possibleEnding [i]) { 165 | return (false); 166 | } 167 | } 168 | } 169 | return (true); 170 | } 171 | function getBoolean (val) { 172 | switch (typeof (val)) { 173 | case "string": 174 | if (val.toLowerCase () == "true") { 175 | return (true); 176 | } 177 | break; 178 | case "boolean": 179 | return (val); 180 | break; 181 | case "number": 182 | if (val != 0) { 183 | return (true); 184 | } 185 | break; 186 | } 187 | return (false); 188 | } 189 | function jsonStringify (jstruct) { 190 | return (JSON.stringify (jstruct, undefined, 4)); 191 | } 192 | function stringMid (s, ix, len) { 193 | return (s.substr (ix-1, len)); 194 | } 195 | function fsSureFilePath (path, callback) { 196 | var splits = path.split ("/"); 197 | path = ""; //1/8/15 by DW 198 | if (splits.length > 0) { 199 | function doLevel (levelnum) { 200 | if (levelnum < (splits.length - 1)) { 201 | path += splits [levelnum] + "/"; 202 | fs.exists (path, function (flExists) { 203 | if (flExists) { 204 | doLevel (levelnum + 1); 205 | } 206 | else { 207 | fs.mkdir (path, undefined, function () { 208 | doLevel (levelnum + 1); 209 | }); 210 | } 211 | }); 212 | } 213 | else { 214 | if (callback != undefined) { 215 | callback (); 216 | } 217 | } 218 | } 219 | doLevel (0); 220 | } 221 | else { 222 | if (callback != undefined) { 223 | callback (); 224 | } 225 | } 226 | } 227 | 228 | function newTwitter (myCallback) { 229 | var twitter = new twitterAPI ({ 230 | consumerKey: twitterConsumerKey, 231 | consumerSecret: twitterConsumerSecret, 232 | callback: myCallback 233 | }); 234 | return (twitter); 235 | } 236 | function getTwitterTimeline (username, callback) { 237 | var twitter = newTwitter (); 238 | var params = {screen_name: username, trim_user: "false"}; 239 | twitter.getTimeline ("user", params, accessToken, accessTokenSecret, function (err, data, response) { 240 | if (err) { 241 | var errinfo = JSON.parse (err.data); 242 | console.log ("getTwitterTimeline: error == \"" + errinfo.errors [0].message + "\""); 243 | } 244 | else { 245 | if (callback != undefined) { 246 | callback (data); 247 | } 248 | } 249 | }); 250 | } 251 | function getFeed (username, fname, callback) { 252 | if (username != undefined) { 253 | var rssHeadElements, rssHistory = new Array (); 254 | function buildRssFeed (headElements, historyArray) { 255 | function encode (s) { 256 | if (s === undefined) { 257 | return (""); 258 | } 259 | var lines = encodeXml (s).split (String.fromCharCode (10)); 260 | var returnedstring = ""; 261 | for (var i = 0; i < lines.length; i++) { 262 | returnedstring += trimWhitespace (lines [i]); 263 | if (i < (lines.length - 1)) { 264 | returnedstring += " "; 265 | } 266 | } 267 | return (returnedstring); 268 | } 269 | function whenMostRecentTweet () { 270 | if (historyArray.length > 0) { 271 | return (new Date (historyArray [0].when)); 272 | } 273 | else { 274 | return (new Date (0)); 275 | } 276 | } 277 | function buildOutlineXml (theOutline) { 278 | function addOutline (outline) { 279 | var s = " 0); 282 | } 283 | function addAtt (name) { 284 | if (outline [name] != undefined) { 285 | s += " " + name + "=\"" + encode (outline [name]) + "\" "; 286 | } 287 | } 288 | addAtt ("text"); 289 | addAtt ("type"); 290 | addAtt ("created"); 291 | addAtt ("name"); 292 | 293 | if (hasSubs (outline)) { 294 | add (s + ">"); 295 | indentlevel++; 296 | for (var i = 0; i < outline.subs.length; i++) { 297 | addOutline (outline.subs [i]); 298 | } 299 | add (""); 300 | indentlevel--; 301 | } 302 | else { 303 | add (s + "/>"); 304 | } 305 | 306 | } 307 | addOutline (theOutline); 308 | return (xmltext); 309 | } 310 | var xmltext = "", indentlevel = 0, starttime = new Date (); nowstring = starttime.toGMTString (); 311 | var username = headElements.twitterScreenName, maxitems = headElements.maxFeedItems; 312 | function add (s) { 313 | xmltext += filledString ("\t", indentlevel) + s + "\n"; 314 | } 315 | function addAccount (servicename, username) { 316 | if ((username != undefined) && (username.length > 0)) { 317 | add ("" + encode (username) + ""); 318 | } 319 | } 320 | add ("") 321 | add ("") 322 | add (""); indentlevel++ 323 | add (""); indentlevel++; 324 | //add header elements 325 | add ("" + encode (headElements.title) + ""); 326 | add ("" + encode (headElements.link) + ""); 327 | add ("" + encode (headElements.description) + ""); 328 | add ("" + whenMostRecentTweet ().toUTCString () + ""); 329 | add ("" + nowstring + ""); 330 | add ("" + encode (headElements.language) + ""); 331 | add ("" + headElements.generator + ""); 332 | add ("" + headElements.docs + ""); 333 | addAccount ("twitter", username); 334 | //add items 335 | var ctitems = 0; 336 | for (var i = 0; (i < historyArray.length) && (ctitems < maxitems); i++) { 337 | var item = historyArray [i], itemcreated = twTwitterDateToGMT (item.when), itemtext = encode (item.text); 338 | var linktotweet = encode ("https://twitter.com/" + username + "/status/" + item.idTweet); 339 | add (""); indentlevel++; 340 | add ("" + itemtext + ""); 341 | add ("" + itemcreated + ""); 342 | //link -- 8/12/14 by DW 343 | if (item.link != undefined) { 344 | add ("" + encode (item.link) + ""); 345 | } 346 | else { 347 | add ("" + linktotweet + ""); 348 | } 349 | //source:linkShort -- 8/26/14 by DW 350 | if (item.linkShort != undefined) { 351 | add ("" + encode (item.linkShort) + ""); 352 | } 353 | //guid -- 8/12/14 by DW 354 | if (item.guid != undefined) { 355 | if (getBoolean (item.guid.flPermalink)) { 356 | add ("" + encode (item.guid.value) + ""); 357 | } 358 | else { 359 | add ("" + encode (item.guid.value) + ""); 360 | } 361 | } 362 | else { 363 | add ("" + linktotweet + ""); 364 | } 365 | //enclosure -- 8/11/14 by DW 366 | if (item.enclosure != undefined) { 367 | var enc = item.enclosure; 368 | if ((enc.url != undefined) && (enc.type != undefined) && (enc.length != undefined)) { 369 | add (""); 370 | } 371 | } 372 | //source:outline 373 | if (item.outline != undefined) { //10/15/14 by DW 374 | buildOutlineXml (item.outline); 375 | } 376 | else { 377 | if (item.idTweet != undefined) { 378 | add (""); 379 | } 380 | if (item.enclosure != undefined) { //9/23/14 by DW 381 | var enc = item.enclosure; 382 | if (enc.type != undefined) { //10/25/14 by DW 383 | if (beginsWith (enc.type.toLowerCase (), "image")) { 384 | add (""); 385 | } 386 | } 387 | } 388 | } 389 | add (""); indentlevel--; 390 | ctitems++; 391 | } 392 | add (""); indentlevel--; 393 | add (""); indentlevel--; 394 | return (xmltext); 395 | } 396 | function addFeedItem (t) { 397 | var username = t.user.screen_name; 398 | var userbaseurl = "http://twitter.com/" + username + "/"; 399 | rssHeadElements = { 400 | title: username + "'s RSS Feed", 401 | link: userbaseurl, 402 | description: "A feed generated from " + username + "'s tweets by " + myProductUrl, 403 | language: "en-us", 404 | generator: myProductName, 405 | docs: "http://cyber.law.harvard.edu/rss/rss.html", 406 | twitterScreenName: username, 407 | maxFeedItems: 25 408 | }; 409 | var historyItem = { 410 | when: new Date (t.created_at), 411 | idTweet: t.id_str, 412 | guid: { 413 | flPermalink: true, 414 | value: userbaseurl + "status/" + t.id_str 415 | } 416 | }; 417 | //try to split the tweet text into text and a link 418 | var thetext = t.text, thelink = undefined; 419 | for (var i = thetext.length - 1; i >= 0; i--) { 420 | if (thetext [i] == " ") { 421 | thelink = thetext.substr (i + 1); 422 | if (beginsWith (thelink.toLowerCase (), "http://")) { 423 | historyItem.text = stringMid (thetext, 1, i); 424 | historyItem.link = thelink; 425 | } 426 | else { 427 | historyItem.text = thetext; 428 | } 429 | break; 430 | } 431 | } 432 | rssHistory [rssHistory.length] = historyItem; 433 | } 434 | getTwitterTimeline (username, function (theTweets) { 435 | for (var i = 0; i < theTweets.length; i++) { 436 | var thisTweet = theTweets [i], s = thisTweet.text, flInclude = true; 437 | if (flSkipReplies) { 438 | if (thisTweet.in_reply_to_status_id != null) { //it's a reply 439 | flInclude = false; 440 | } 441 | } 442 | if (flInclude) { 443 | addFeedItem (thisTweet); 444 | } 445 | } 446 | var xmltext = buildRssFeed (rssHeadElements, rssHistory); 447 | fsSureFilePath (fname, function () { 448 | fs.writeFile (fname, xmltext, function (err) { 449 | console.log ("getFeed: " + xmltext.length + " chars in " + fname); 450 | if (callback != undefined) { 451 | callback (); 452 | } 453 | }); 454 | }); 455 | }); 456 | } 457 | else { 458 | if (callback != undefined) { 459 | callback (); 460 | } 461 | } 462 | } 463 | 464 | function everyMinute () { 465 | console.log (""); 466 | console.log ("everyMinute: " + new Date ().toLocaleTimeString ()); 467 | loadConfigStruct (function () { 468 | if (configStruct != undefined) { 469 | function readOne (ix) { 470 | if (ix < configStruct.items.length) { 471 | var item = configStruct.items [ix], fname = item.feedname; 472 | if (configStruct.folder != undefined) { 473 | fname = configStruct.folder + fname; 474 | } 475 | getFeed (item.username, fname, function () { 476 | readOne (ix + 1); 477 | }); 478 | } 479 | } 480 | readOne (0); 481 | } 482 | else { 483 | getFeed (twitterScreenName, pathRssFile) 484 | } 485 | }); 486 | } 487 | function startup () { 488 | console.log (); 489 | console.log (myProductName + " v" + myVersion + "."); 490 | console.log (); 491 | 492 | //check pathRssFile -- 1/12/15 by DW 493 | if (pathRssFile == undefined) { 494 | pathRssFile = defaultRssFilePath; 495 | } 496 | else { 497 | pathRssFile = trimWhitespace (pathRssFile); 498 | if (endsWith (pathRssFile, "/")) { 499 | pathRssFile += "rss.xml"; 500 | } 501 | } 502 | 503 | everyMinute (); //call once at startup, then every minute 504 | setInterval (everyMinute, 60000); 505 | } 506 | startup (); 507 | 508 | --------------------------------------------------------------------------------