├── 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 |
--------------------------------------------------------------------------------