├── demos ├── subscriptionListCleanup │ ├── source.opml │ ├── package.json │ ├── worknotes.md │ ├── readme.md │ └── subscriptionlistcleanup.js ├── feedhunter │ ├── test │ │ ├── package.json │ │ └── testfeedhunter.js │ ├── worknotes.md │ ├── package.json │ ├── readme.md │ ├── feedhunter.js │ └── source.opml ├── feeder │ ├── package.json │ ├── templates │ │ ├── helloworld.html │ │ ├── titleditems.html │ │ ├── mailbox.html │ │ └── jsonify.html │ ├── readme.md │ ├── docs │ │ └── templates.md │ ├── worknotes.md │ └── feeder.js ├── clouddemo │ ├── package.json │ ├── stats.json │ ├── readme.md │ ├── clouddemo.js │ └── source.opml └── titlelessFeedsHowto │ ├── worknotes.md │ ├── styles.css │ ├── index.html │ ├── readme.md │ ├── code.js │ └── source.opml ├── example ├── package.json ├── test.js └── test.json ├── package.json ├── LICENSE ├── README.md ├── worknotes.md └── reallysimple.js /demos/subscriptionListCleanup/source.opml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scripting/reallysimple/HEAD/demos/subscriptionListCleanup/source.opml -------------------------------------------------------------------------------- /demos/feedhunter/test/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "testfeedhunter", 3 | "version": "0.4.0", 4 | "main": "feedhunter.js", 5 | "license": "MIT", 6 | "dependencies" : { 7 | "feedhunter": "*" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /demos/feedhunter/test/testfeedhunter.js: -------------------------------------------------------------------------------- 1 | const feedhunter = require ("feedhunter"); 2 | const htmlUrl = "https://bsky.app/profile/scripting.com"; 3 | feedhunter.huntForFeed (htmlUrl, undefined, function (feedUrl) { 4 | if (feedUrl === undefined) { 5 | console.log ("\nCouldn't find a feed for this page.\n"); 6 | } 7 | else { 8 | console.log ("\nWe found a feed: " + feedUrl + "\n"); 9 | } 10 | }); 11 | -------------------------------------------------------------------------------- /example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "test", 3 | "description": "Testing the reallysimple package", 4 | "version": "0.4.0", 5 | "author": "Dave Winer ", 6 | "license": "MIT", 7 | "main": "test.js", 8 | "dependencies" : { 9 | "marked": "3.0.8", 10 | "node-emoji": "*", 11 | "daveutils": "*", 12 | "reallysimple": "*" 13 | }, 14 | "engines": { 15 | "node": "*" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /demos/subscriptionListCleanup/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "subscriptionlistcleanup", 3 | "description": "Read an OPML subscription list, and loop over all the feeds and only pass on the ones that are reachable and parseable.", 4 | "version": "0.4.3", 5 | "author": "Dave Winer ", 6 | "main": "subscriptionlistcleanup.js", 7 | "dependencies" : { 8 | "opml": "*", 9 | "reallysimple": "*", 10 | "request": "*", 11 | "daveutils": "*" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /demos/feeder/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "feeder", 3 | "description": "A Node server app that hooks the reallysimple package up to the web.", 4 | "version": "0.5.1", 5 | "author": "Dave Winer ", 6 | "license": "MIT", 7 | "main": "feeder.js", 8 | "repository": { 9 | "type": "git", 10 | "url": "https://github.com/scripting/feeder.git" 11 | }, 12 | "dependencies" : { 13 | "daveutils": "*", 14 | "reallysimple": "*", 15 | "request": "*", 16 | "dateformat": "4.5.1", 17 | "davehttp": "*" 18 | }, 19 | "engines": { 20 | "node": "*" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /demos/feedhunter/worknotes.md: -------------------------------------------------------------------------------- 1 | #### 12/23/23; 8:15:45 AM by DW 2 | 3 | Bluesky came out with their first release of RSS feeds for each user's timeline, but their feed discovery link is a relative URL and we were not set up to handle it. In this release we make it work with relative URLs. 4 | 5 | This is what one of their link elements looks like: 6 | 7 | `` 8 | 9 | #### 11/9/23; 8:55:31 AM by DW 10 | 11 | Improved docs, provided example app. 12 | 13 | #### 11/8/23; 9:53:51 AM by DW 14 | 15 | Started. 16 | 17 | -------------------------------------------------------------------------------- /demos/clouddemo/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "clouddemo", 3 | "description": "A working example of a server-based feed reader implementation of rssCloud", 4 | "author": "Dave Winer ", 5 | "version": "0.4.0", 6 | "license": "MIT", 7 | "repository": { 8 | "type": "git", 9 | "url": "https://github.com/scripting/reallysimple.git" 10 | }, 11 | "main": "clouddemo.js", 12 | "files": [ 13 | "clouddemo.js" 14 | ], 15 | "dependencies" : { 16 | "daveutils": "*", 17 | "http": "*", 18 | "davehttp": "*", 19 | "request": "*", 20 | "qs": "*", 21 | "reallysimple": "*", 22 | "xml2js": "*" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /example/test.js: -------------------------------------------------------------------------------- 1 | var myProductName = "test"; myVersion = "0.4.0"; 2 | 3 | const fs = require ("fs"); 4 | const utils = require ("daveutils"); 5 | const reallysimple = require ("reallysimple"); 6 | 7 | const urlfeed = "https://scripting4.wordpress.com/feed/"; 8 | 9 | reallysimple.readFeed (urlfeed, function (err, theFeed) { 10 | if (err) { 11 | console.log (err.message); 12 | } 13 | else { 14 | var jsontext = utils.jsonStringify (theFeed); 15 | console.log ("theFeed == " + jsontext); 16 | fs.writeFile ("test.json", jsontext, function (err) { 17 | if (err) { 18 | console.log (err.message); 19 | } 20 | }); 21 | } 22 | }); 23 | -------------------------------------------------------------------------------- /demos/feedhunter/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "feedhunter", 3 | "description": "A Node.js package that looks for a feed, starting with the address of an HTML file.", 4 | "version": "0.4.4", 5 | "main": "feedhunter.js", 6 | "files": [ 7 | "feedhunter.js", 8 | "package.json", 9 | "readme.md", 10 | "source.opml", 11 | "test/testfeedhunter.js", 12 | "test/package.json" 13 | ], 14 | "license": "GPLV2", 15 | "repository": { 16 | "type": "git", 17 | "url": "https://github.com/scripting/reallysimple/tree/main/demos/feedhunter" 18 | }, 19 | "dependencies" : { 20 | "request": "*", 21 | "reallysimple": "*", 22 | "daveutils": "*" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "reallysimple", 3 | "description": "A Node package that reads RSS-like feeds and calls back with a simple, consistent JavaScript object. Easy to use, hides the history.", 4 | "version": "0.5.3", 5 | "author": "Dave Winer ", 6 | "license": "MIT", 7 | "files": [ 8 | "reallysimple.js", 9 | "worknotes.md" 10 | ], 11 | "main": "reallysimple.js", 12 | "repository": { 13 | "type": "git", 14 | "url": "https://github.com/scripting/reallysimple.git" 15 | }, 16 | "dependencies" : { 17 | "marked": "3.0.8", 18 | "node-emoji": "1.11.0", 19 | "opml": "*", 20 | "daveutils": "*", 21 | "davefeedread": ">=0.5.23" 22 | }, 23 | "engines": { 24 | "node": "*" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /demos/titlelessFeedsHowto/worknotes.md: -------------------------------------------------------------------------------- 1 | #### 12/19/22; 2:59:41 PM by DW 2 | 3 | Moved into the demos folder on the reallySimple repo. 4 | 5 | #### 12/11/22 by DW -- v0.4.1 6 | 7 | Per jz's suggestion, when splitting an titleless item's text, if there is a sentence at the beginning of the text which is short enough, use the sentence text instead of truncating after the end of the sentence. In other words, the sentence end is a more natural place to break the text. 8 | 9 | Added a period at the end of an actual title. Looks much better to separate it from the excerpt text that follows. 10 | 11 | We now display the version number in the upper right corner. 12 | 13 | #### 12/9/22 by DW 14 | 15 | A simple feed item viewer that works with titled or titleless items. 16 | 17 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 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 | -------------------------------------------------------------------------------- /demos/feeder/templates/helloworld.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | [%feedTitle%] 4 | 5 | 19 | 28 | 29 | 30 |
31 |

Hello World

32 |

This is a very simple bare-bones template for the feeder app. More info here.

33 |
34 | 				
35 | 40 |
41 | 42 | 43 | -------------------------------------------------------------------------------- /demos/clouddemo/stats.json: -------------------------------------------------------------------------------- 1 | { 2 | "events": [ 3 | { 4 | "method": "GET", 5 | "path": "/feedupdated", 6 | "params": { 7 | "url": "https://unberkeley.wordpress.com/feed/", 8 | "challenge": "f2cc22ce631795f79314c67ffb06ea" 9 | }, 10 | "myResponse": "f2cc22ce631795f79314c67ffb06ea", 11 | "when": "12/17/2022, 11:24:50 AM" 12 | }, 13 | { 14 | "type": "requestNotification", 15 | "urlCloudServer": "http://unberkeley.wordpress.com:80/?rsscloud=notify", 16 | "response": { 17 | "success": "true", 18 | "msg": "Registration successful." 19 | }, 20 | "feedUrl": "https://unberkeley.wordpress.com/feed/", 21 | "ctSecs": 1.123, 22 | "when": "12/17/2022, 11:24:50 AM" 23 | }, 24 | { 25 | "method": "POST", 26 | "path": "/feedupdated", 27 | "params": { 28 | "url": "https://unberkeley.wordpress.com/feed/" 29 | }, 30 | "myResponse": { 31 | "status": "Got the update. Thanks! :-)" 32 | }, 33 | "when": "12/17/2022, 11:25:17 AM" 34 | } 35 | ] 36 | } 37 | -------------------------------------------------------------------------------- /demos/subscriptionListCleanup/worknotes.md: -------------------------------------------------------------------------------- 1 | #### 10/19/24; 11:24:25 AM by DW 2 | 3 | I took a few hours to try to turn this into an HTTP server. I preserved the code in discuss.root, under today's archive. 4 | 5 | The problem was that it can take a long time, the way the timeouts are set, each time we hit a bad feed. 6 | 7 | To fix all that, I'd have to dig into code that hasn't been touched in a long time. That's not my idea of a quick one day project. It could spiral into much more. 8 | 9 | So I punted. It was a nice idea, a utility that might have attracted exactly the kind of people I want to work with. 10 | 11 | I must get back to WordLand where I am very well dug in, and making good progress. 12 | 13 | #### 12/21/22; 8:51:20 AM by DW 14 | 15 | Moved to the demos folder of the reallySimple repo. Want to build a nice collection for devs working with feeds. 16 | 17 | #### 11/2/22; 10:31:40 AM by DW 18 | 19 | Support command line arguments 20 | 21 | #### 10/23/22; 11:27:38 AM by DW 22 | 23 | Moved it to its own repo. It's generally useful, not just in using FeedLand. 24 | 25 | #### 8/5/22; 4:46:07 PM by DW 26 | 27 | Wrote docs, removed an old config element. Moved to the feedlandSupport repo. 28 | 29 | #### 5/17/22; 10:51:31 AM by DW 30 | 31 | Started. Read an OPML subscription list, and loop over all the feeds and only pass on the ones that are reachable and parseable. 32 | 33 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # reallysimple 2 | 3 | A Node package that reads RSS-like feeds and calls back with a simple, consistent JavaScript object. Easy to use, hides the history. 4 | 5 | #### Code example 6 | 7 | I always like to see the code first... 8 | 9 | ```javascript const reallysimple = require ("reallysimple"); const urlFeed = "https://rss.nytimes.com/services/xml/rss/nyt/World.xml"; reallysimple.readFeed (urlFeed, function (err, theFeed) { if (err) { console.log (err.message); } else { console.log (JSON.stringify (theFeed, undefined, 4)); } }); ``` 10 | 11 | This is what you see when you run the code. 12 | 13 | #### Why? 14 | 15 | I needed a simple routine to call when I wanted to read a feed. 16 | 17 | #### What formats are supported? 18 | 19 | RSS, Atom, and RDF. 20 | 21 | #### Demo 22 | 23 | Here's a demo app that runs a feed through reallySimple. 24 | 25 | #### tinyFeedReader 26 | 27 | tinyFeedReader is a useful Node app that builds on the reallySimple package. 28 | 29 | #### What we build on 30 | 31 | Thanks to Dan MacTough for the feedparser package. 32 | 33 | #### Comments, questions? 34 | 35 | Post comments and questions in the issues section of this repo. 36 | 37 | -------------------------------------------------------------------------------- /demos/titlelessFeedsHowto/styles.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: Ubuntu; 3 | font-size: 18px; 4 | background-color: white; 5 | } 6 | .divPageBody { 7 | width: 60%; 8 | margin-top: 30px; 9 | margin-left: auto; 10 | margin-right: auto; 11 | margin-bottom: 400px; 12 | } 13 | .divVersionNumber { 14 | font-size: 12px; 15 | float: right; 16 | padding-top: 19px; 17 | } 18 | .divItemViewer { 19 | width: 100%; 20 | } 21 | .divFeedTitle { 22 | font-size: 26px; 23 | font-weight: bold; 24 | letter-spacing: -1px; 25 | margin-bottom: 20px; 26 | } 27 | .divItemViewer ul { 28 | margin-left: 0; 29 | } 30 | .divItemViewer li { 31 | overflow: hidden; 32 | list-style-type: none; 33 | padding-top: 3px; 34 | padding-bottom: 5px; 35 | white-space: nowrap; 36 | border-top: 1px dotted lightgray; 37 | } 38 | .spTitleText { 39 | font-size: 13px; 40 | font-weight: bold; 41 | line-height: 1.25em; 42 | margin-bottom: 10px; 43 | } 44 | .spBodyText { 45 | font-size: 13px; 46 | color: #9e9e9e; 47 | line-height: 18px; 48 | } 49 | .hovering { 50 | background-color: whitesmoke; 51 | cursor: pointer; 52 | } 53 | .divUrlInput { 54 | margin-bottom: 30px; 55 | } 56 | .divUrlInput input { 57 | width: 50%; 58 | margin-top: 11px; 59 | margin-right: 5px; 60 | height: 32px; 61 | } 62 | .divUrlInput .btnGo { 63 | height: 32px; 64 | } 65 | .divTitleText { 66 | font-size: 16px; 67 | font-weight: bold; 68 | line-height: 1.25em; 69 | margin-bottom: 10px; 70 | } 71 | .divBodyText { 72 | font-size: 13px; 73 | color: #9e9e9e; 74 | line-height: 18px; 75 | } 76 | .divMoreInfo { 77 | margin-top: 50px; 78 | border-top: 1px dotted silver; 79 | font-size: 13px; 80 | padding-top: 3px; 81 | } 82 | 83 | -------------------------------------------------------------------------------- /demos/feeder/templates/titleditems.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Titled Items: [%feedTitle%] 4 | 5 | 17 | 44 | 45 | 46 |
47 |

The Titled Items template

48 |

This is a very simple template for the feeder app. More info here.

49 |
50 |
51 | 56 |
57 | 58 | 59 | -------------------------------------------------------------------------------- /demos/feeder/readme.md: -------------------------------------------------------------------------------- 1 | # feeder 2 | 3 | A Node server app that hooks the reallysimple package up to the web. 4 | 5 | ### Why? 6 | 7 | To provide service to Drummer and possibly other apps that run in the browser. 8 | 9 | ### Two calls 10 | 11 | Two calls are supported: /returnjson and /returnopml. Both take a url parameter. 12 | 13 | http://feeder.scripting.com/returnjson?url=http://scripting.com/rss.xml 14 | 15 | * Returns a JSON structure containing the information in the feed, as processed by reallysimple. 16 | 17 | http://feeder.scripting.com/returnopml?url=http://scripting.com/rss.xml 18 | 19 | * Returns an OPML structure which you can insert into an outline, with all the items from the feed. 20 | 21 | These calls are used from Drummer to implement the rss.readFeed verb and to allow expanding of rss node types. 22 | 23 | ### Templates 24 | 25 | You an also run the contents of a reallysimple query through a template, which is just a web page, which has the result of the query as a local object you can use JavaScript to render. 26 | 27 | Here's the docs page for templates. 28 | 29 | ### Caveats 30 | 31 | If you're deploying a real application, please run your own copy of this app. 32 | 33 | It's fine to use feeder.scripting.com for testing. 34 | 35 | ### Questions or comments 36 | 37 | Please respond in this thread on the reallysimple issues section. 38 | 39 | -------------------------------------------------------------------------------- /demos/titlelessFeedsHowto/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Feed item viewer 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 |
22 |
23 |
24 |
25 | 26 | 27 |
28 |
29 |
30 |
31 |
For more information and JavaScript source code, see titlelessFeedsHowto on GitHub.
32 |
33 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /demos/subscriptionListCleanup/readme.md: -------------------------------------------------------------------------------- 1 | # subscriptionListCleanup 2 | 3 | A Node app that reads an OPML subscription list, and loop over all the feeds and only pass on the ones that are reachable and parseable. 4 | 5 | #### Who it's for 6 | 7 | You have to be a JavaScript programmer to use it. 8 | 9 | #### How to use 10 | 11 | You can run the app without command line arguments or with. 12 | 13 | Without them, it reads the file specified by config.urlSource, which you can of course change. 14 | 15 | With command line arguments, you can specify whether you want to read from a file or over the web, and which file or URL you want to read. 16 | 17 | Here are three examples that illustrate: 18 | 19 | 2. node subscriptionlistcleanup.js -f mlb.opml 20 | 21 | 3. node subscriptionlistcleanup.js -u http://scripting.com/code/subscriptionlistcleanup/mlb.opml 22 | 23 | 1. node subscriptionlistcleanup.js 24 | 25 | In all cases it will create a file in the data sub-folder with the same name as the original file that contains only the feeds it was able to read and correctly parse as a feed. 26 | 27 | #### Why? 28 | 29 | I had a lot of OPML subscription lists from previous feed reading apps, and I wanted to speed up the process of getting them into my FeedLand database, and keep the no longer functional ones out of the database. 30 | 31 | #### Possible mods 32 | 33 | It's probably a good idea to look at the OPML file it generates in Drummer, because some of the feeds, while they may be valid RSS, Atom or RDF, haven't been updated in a long time. Alternatively you could make this app smarter by checking the result from reallysimple.readFeed and seeing how long it' has been since there's been a new item, and not passing it of if it's been too long. 34 | 35 | #### Comments, questions 36 | 37 | Post them here. 38 | 39 | -------------------------------------------------------------------------------- /demos/feedhunter/readme.md: -------------------------------------------------------------------------------- 1 | # feedHunter 2 | 3 | A Node.js package that looks for a feed, starting with the address of an HTML file. 4 | 5 | ### How to 6 | 7 | ```JAVASCRIPT const feedhunter = require ("feedhunter"); const htmlUrl = "http://bullmancuso.com"; feedhunter.huntForFeed (htmlUrl, undefined, function (feedUrl) { if (feedUrl === undefined) { console.log ("Couldn't find a feed for this page."); } else { console.log ("feedUrl == " + feedUrl); } }); ``` 8 | 9 | Note: The second parameter to feedhunter.huntForFeed allows you to replace our set of standard feed locations with your own list. 10 | 11 | ### What it does 12 | 13 | When the user subscribes to an HTML file in FeedLand, first we look for all the <link> elements in the HTML that point to feeds. We look at each feed in turn, and if we find one that can be read and parses as a feed, we subscribe to that feed and display its Feed Info page. We do that with a call to feedhunter.huntForFeed. 14 | 15 | We also use a set of common feed locations we found by studying the feed list of the indieblog site (a great resource for our work, thanks!), and by studying the feeds people have subscribed to on feedland.org. 16 | 17 | I put this code in a separate package, because it seemed it might be useful in other contexts and people may have other ideas for standard feed locations we could add to the search. 18 | 19 | ### Experiment in FeedLand 20 | 21 | If you want to try an experiment, choose To one feed from the Subscribe sub-menu in FeedLand's main menu, and enter http://bullmancuso.com. 22 | 23 | I put a copy of a very old feed in a weird location, one of the places feedhunter looks. 24 | 25 | If it's working we'll find the feed anyway. :smile: 26 | 27 | ### Questions, comments? 28 | 29 | Create an issue here. 30 | 31 | -------------------------------------------------------------------------------- /demos/clouddemo/readme.md: -------------------------------------------------------------------------------- 1 | # rssCloud server demo 2 | 3 | A working example of a server-based feed reader implementation of rssCloud. 4 | 5 | ### A feed with a cloud element 6 | 7 | This is a Node.js app that works with one feed that has a cloud element. Example. 8 | 9 | When it starts up, it reads the feed, sees if it has a cloud element, and if so it requests notification from the server specified in the cloud element. It tells the cloud server how to notifiy it. 10 | 11 | The cloud server calls back to verify that there's someone listening on the other end who understands the rssCloud protocol. 12 | 13 | We get the test message and send back the expected response and we're in the network, ready to receive notification about the feed with the cloud element. 14 | 15 | ### When a notification comes in 16 | 17 | The notification will come in as a POST request that comes with one parameter in its body, the URL of the feed that updated. 18 | 19 | We then read the feed, and do whatever we want with the info in the feed. 20 | 21 | We respond to the server with a string that it probably ignores. 22 | 23 | ### Must be outside of firewalls 24 | 25 | This server must be callable by servers on the net. You can't even run a test version locally without some tunneling. I 26 | 27 | ### A tour of the code 28 | 29 | I've placed little "markers" in the code in comments 30 | 31 | //1 -- config.thisServer is where I specify how the cloud server should call me by providing my domain, port and path. To run this app you will have to change these values. 32 | 33 | //2 -- We log events into stats.events, if you want to see what's going on, that's a good place to watch. 34 | 35 | //3 -- pleaseNotify is where we call the cloud server asking to be notified when the feed changes. 36 | 37 | //4 -- the feed you want to test is specified in config.defaultFeedUrl. It should be a feed you can update to cause a message to be sent to your copy of this app. I'm using a WordPress feed. They supprt rssCloud. 38 | 39 | //5 -- handlePing -- does nothing. This is where you'd read the feed, look for changed items, update your database, notify users, etc. 40 | 41 | //6 -- this is where we handle notification via POST requests. 42 | 43 | //7 -- where we handle notification via GET requests. 44 | 45 | ### Comments, questions 46 | 47 | I started a thread here. 48 | 49 | -------------------------------------------------------------------------------- /demos/feeder/templates/mailbox.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | [%productnameForDisplay%]: [%feedTitle%] 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 36 | 37 | 38 |
39 | 40 | 41 | 54 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 | 50 |
51 |
52 |
53 |
55 |
56 | 61 | 62 | 63 | -------------------------------------------------------------------------------- /demos/feeder/templates/jsonify.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | JSONify Feed: [%feedTitle%] 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 45 | 69 | 70 | 71 |
72 |

The JSONify Feed template

73 |

A very simple template for the feeder app. More info here.

74 |
75 |
76 |
77 |
78 |
79 | 84 | 85 | 86 | -------------------------------------------------------------------------------- /demos/titlelessFeedsHowto/readme.md: -------------------------------------------------------------------------------- 1 | # titlelessFeedsHowto 2 | 3 | A simple feed item viewer that works with titled or titleless items. 4 | 5 | ### Screen shot 6 | 7 | I always like to see a screen shot up front, and a demo. :smile: 8 | 9 | ### Feeds are for writers and readers 10 | 11 | We take the work of writers, and present it to readers. 12 | 13 | The limits of the software need to bend, whenever possible to the needs of writers and readers. 14 | 15 | We are not here to educate writers and readers on how we think they should write and read. 16 | 17 | We must be flexible and make tools that work well for them, and work better over time as they and we learn more. 18 | 19 | We must all work together to make the writing and reading experience on the web get better all the time. 20 | 21 | ### Fact: Some items have titles, and some don't 22 | 23 | It's up to the writer to decide if an item has a title. 24 | 25 | If it doesn't have a title, it would be incorrect for a feed reader to invent a title for the item. If the writer didn't put one there, you don't get to put one there for them. They made their intention clear. You must respect it. 26 | 27 | This is a writer's medium, not a programmer's. 28 | 29 | ### Your software is probably wrong 30 | 31 | I am giving you code to crib to make your software handle titleless items well. 32 | 33 | In all likelihood your software does not do that currently. That's okay -- we can easily fix this. 34 | 35 | ### Basic technique 36 | 37 | Assume you have two slots you must fill in your software, for each item: title and body. 38 | 39 | If an item has a title, display the title in the title slot, and the first N characters of the description element in the body slot. 40 | 41 | If an item does not have a title, take the first X characters from the description, adjusted for whitespace (i.e. don't break where there isn't a word break), display that text in the title slot, and display the next N characters from the description in the body slot. 42 | 43 | That's how the app in this repo works. 44 | 45 | ### Three examples 46 | 47 | Here's how this app deals with three test feeds. 48 | 49 | 1. NYT feed. It's an easy case because the NYT feeds are uniform. 50 | 51 | 2. Scripting News feed contains both titled and title-less posts. 52 | 53 | 3. My Mastodon feed, where all items are title-less. 54 | 55 | ### Why these examples are better than current practice 56 | 57 | 1. The reader is not confronted with differences between the two types of items. 58 | 59 | 2. The only words you see are ones the writer wrote. 60 | 61 | 3. No text is repeated. 62 | 63 | ### Testing and development 64 | 65 | You can test the method with any feed you like. Try entering different feed addresses in the app. If you spot a problem, share the URL of the feed as an issue in the repo here. 66 | 67 | You also have the source, and can come up with your own design and share it with us, and let's see, maybe this basic method can be improved on. 68 | 69 | PS: Bug reports, not pull requests. :-) 70 | 71 | -------------------------------------------------------------------------------- /demos/feedhunter/feedhunter.js: -------------------------------------------------------------------------------- 1 | var myProductName = "feedhunter", myVersion = "0.4.4"; 2 | 3 | exports.huntForFeed = huntForFeed; 4 | 5 | const fs = require ("fs"); 6 | const utils = require ("daveutils"); 7 | const request = require ("request"); 8 | const reallysimple = require ("reallysimple"); 9 | const urlpackage = require ("url"); 10 | 11 | function httpReadUrl (url, callback) { //8/21/22 by DW 12 | request (url, function (err, response, data) { 13 | if (err) { 14 | callback (err); 15 | } 16 | else { 17 | if (response.statusCode != 200) { 18 | const errstruct = { 19 | message: "Can't read the URL, \"" + url + "\" because we received a status code of " + response.statusCode + ".", 20 | statusCode: response.statusCode 21 | }; 22 | callback (errstruct); 23 | } 24 | else { 25 | callback (undefined, data); 26 | } 27 | } 28 | }); 29 | } 30 | function fixRelativeUrl (htmlUrl, url) { //12/23/23 by DW 31 | if (utils.beginsWith (url, "//") || utils.beginsWith (url, "http://") || utils.beginsWith (url, "https://")) { //not a relative URL 32 | return (url); 33 | } 34 | else { 35 | const jstruct = new URL (url, htmlUrl); 36 | return (jstruct.href); 37 | } 38 | } 39 | function getFeedsLinkedToFromHtml (htmlUrl, callback) { //11/8/23 by DW 40 | function findFeedsFromHTML (html) { 41 | const regex = /]+type="application\/(?:rss\+xml|atom\+xml)"[^>]+href="([^"]+)"[^>]*>/g; 42 | let match; 43 | const feeds = []; 44 | while ((match = regex.exec (html)) !== null) { 45 | feeds.push (match [1]); 46 | } 47 | return (feeds); 48 | } 49 | httpReadUrl (htmlUrl, function (err, htmltext) { 50 | if (err) { 51 | callback (err); 52 | } 53 | else { 54 | var feedlist = findFeedsFromHTML (htmltext); 55 | if (feedlist.length == 0) { 56 | const message = "Can't find any feeds in the HTML text."; 57 | callback ({message}); 58 | } 59 | else { 60 | callback (undefined, feedlist); 61 | } 62 | } 63 | }); 64 | } 65 | function huntForFeed (htmlUrl, options, callback) { 66 | var config = { 67 | filePathsToCheck: [ 68 | "feed", 69 | "rss", 70 | "feeds/posts/default", 71 | "feed.xml", 72 | "rss.xml", 73 | "index.xml", 74 | "blog/feed", 75 | "feeds/videos.xml", 76 | "atom.xml", 77 | "blog", 78 | "rss/index.xml", 79 | "feed.rss", 80 | "feed/atom", 81 | "feed/podcast", 82 | "blog/rss.xml", 83 | "blog/index.xml", 84 | "news/feed", 85 | "blog-feed.xml", 86 | "rss.php", 87 | "blog/rss", 88 | "bridge01", 89 | "feed.atom", 90 | "blog/atom.xml", 91 | "feed/rss", 92 | "index.rss", 93 | "RSSFeed.aspx", 94 | "blog/feed.xml" 95 | ] 96 | }; 97 | if (options !== undefined) { 98 | for (var x in options) { 99 | config [x] = options [x]; 100 | } 101 | } 102 | const parsedUrl = urlpackage.parse (htmlUrl, true); 103 | const origin = parsedUrl.protocol + "//" + parsedUrl.host + "/"; 104 | var fileQueue = new Array (); 105 | getFeedsLinkedToFromHtml (htmlUrl, function (err, feeds) { 106 | if (feeds !== undefined) { 107 | feeds.forEach (function (url) { 108 | url = fixRelativeUrl (htmlUrl, url); //12/23/23 by DW 109 | fileQueue.push (url); 110 | }); 111 | } 112 | config.filePathsToCheck.forEach (function (item) { 113 | fileQueue.push (origin + item); 114 | }); 115 | function checkNext (ix) { 116 | if (ix < fileQueue.length) { 117 | const feedUrl = fileQueue [ix]; 118 | reallysimple.readFeed (feedUrl, function (err, theFeed) { 119 | if (err) { 120 | checkNext (ix + 1); 121 | } 122 | else { 123 | callback (feedUrl); 124 | } 125 | }); 126 | } 127 | else { 128 | callback (undefined); //didn't find a feed 129 | } 130 | } 131 | checkNext (0); 132 | }); 133 | } 134 | -------------------------------------------------------------------------------- /demos/subscriptionListCleanup/subscriptionlistcleanup.js: -------------------------------------------------------------------------------- 1 | const myVersion = "0.4.3", myProductName = "subscriptionlistcleanup"; 2 | 3 | const utils = require ("daveutils"); 4 | const fs = require ("fs"); 5 | const request = require ("request"); 6 | const opml = require ("opml"); 7 | const reallysimple = require ("reallysimple"); 8 | 9 | const config = { 10 | urlSource: "http://scripting.com/code/subscriptionlistcleanup/washpost.opml", 11 | dataFolder: "data/", 12 | timeoutSecs: 5 13 | } 14 | 15 | var fnameSavedFile; 16 | 17 | function httpRequest (url, callback) { 18 | var theRequest = { 19 | url, 20 | timeout: config.timeoutSecs * 1000 21 | }; 22 | request (theRequest, function (err, response, data) { 23 | if (err) { 24 | callback (err); 25 | } 26 | else { 27 | var code = response.statusCode; 28 | if ((code < 200) || (code > 299)) { 29 | const message = "The request returned a status code of " + response.statusCode + "."; 30 | callback ({message}); 31 | } 32 | else { 33 | callback (undefined, data) 34 | } 35 | } 36 | }); 37 | } 38 | function notComment (item) { //return true if the outline element is not a comment 39 | return (!utils.getBoolean (item.isComment)); 40 | } 41 | function writeOutline (theOutline) { 42 | var f = config.dataFolder + fnameSavedFile; 43 | utils.sureFilePath (f, function () { 44 | fs.writeFile (f, opml.stringify (theOutline), function (err) { 45 | if (err) { 46 | console.log (err.message); 47 | } 48 | }); 49 | }); 50 | } 51 | function cleanup (err, opmltext) { 52 | if (err) { 53 | console.log (err.message); 54 | } 55 | else { 56 | opml.parse (opmltext, function (err, theOutline) { 57 | if (!err) { 58 | opml.expandIncludes (theOutline, function (theNewOutline) { 59 | var createdVal = new Date (); 60 | var theGoodOutline = { //the outline with the good feeds 61 | opml: { 62 | head: theOutline.head, 63 | body: { 64 | subs: new Array () 65 | } 66 | } 67 | }; 68 | opml.visitAll (theNewOutline, function (theNode) { 69 | if (notComment (theNode)) { 70 | if (theNode.type == "rss") { 71 | if (theNode.xmlUrl !== undefined) { 72 | var whenstart = new Date (); 73 | reallysimple.readFeed (theNode.xmlUrl, function (err, theFeed) { 74 | var secs = ", " + utils.secondsSince (whenstart) + " secs" 75 | if (err) { 76 | } 77 | else { 78 | console.log (theFeed.title + secs); 79 | var created = (theNode.created === undefined) ? createdVal : theNode.created; 80 | createdVal = new Date (createdVal.getTime () + 1000); //make sure every node has a different created value 81 | theGoodOutline.opml.body.subs.push ({ 82 | type: "rss", 83 | text: theFeed.title, 84 | xmlUrl: theNode.xmlUrl, 85 | htmlUrl: theFeed.link, 86 | created 87 | }); 88 | writeOutline (theGoodOutline); 89 | } 90 | }); 91 | } 92 | } 93 | } 94 | return (true); //keep visiting 95 | }); 96 | }); 97 | } 98 | }); 99 | } 100 | } 101 | function cleanupOverHttp (urlSource) { 102 | fnameSavedFile = utils.stringLastField (urlSource, "/"); //set global 103 | httpRequest (urlSource, cleanup); 104 | } 105 | function cleanupFile (f) { 106 | fnameSavedFile = utils.stringLastField (f, "/"); //set global 107 | fs.readFile (f, cleanup); 108 | } 109 | if (process.argv.length >= 4) { //there's a command line argument 110 | switch (process.argv [2]) { 111 | case "-f": 112 | cleanupFile (process.argv [3]); 113 | break; 114 | case "-u": 115 | cleanupOverHttp (process.argv [3]); 116 | break; 117 | default: 118 | console.log ("The supported options are -f and -u."); 119 | break; 120 | } 121 | } 122 | else { 123 | cleanupOverHttp (config.urlSource); 124 | } 125 | -------------------------------------------------------------------------------- /demos/feeder/docs/templates.md: -------------------------------------------------------------------------------- 1 | # How feeder templates work 2 | 3 | The basic function of feeder is to provide an HTTP interface to the reallysimple package, so you can access its functionality from a browser-based app without having to run a server. 4 | 5 | You can also run the contents of a reallysimple request through a template, which is just a web page, which has the result of the request as a local object you can use JavaScript to render. 6 | 7 | This doc walks you through the Hello World template first, it's a tour of the basic features, and then the Titled Items template that works with the reallysimple object in code. 8 | 9 | ### The Hello World template 10 | 11 | This is the template. 12 | 13 | This is how you invoke it: 14 | 15 | http://feeder.scripting.com/?template=helloworld&feedurl=https://news.ycombinator.com/rss 16 | 17 | The template refers to 3 macros which are filled in by the feeder app serving it. 18 | 19 | 1. [%feedTitle%] -- the title of the feed, displayed in the <title> element in the HTML. 20 | 21 | 2. [%config%] -- configuration info from the server, assigned to a local variable config. 22 | 23 | 3. [%feedJsonText%] -- the JSON object returned by the reallysimple package, assigned to a local variable, theFeed. 24 | 25 | From there, the primary job of the template is to display and allow the user to interact with the contents of the feed, which is accessed locally through theFeed, which is just a JavaScript object. 26 | 27 | config is there mostly for the future, if there's information we might want to send to all templates from the server that's hosting the template. Initially it just has the name of the feeder app and its version. 28 | 29 | The Hello World app just displays what's in theFeed by stringifying it and assigning it to the idFeedInfo DOM object. 30 | 31 | ### The Mailbox template 32 | 33 | Here's the template. 34 | 35 | It displays the contents in a feed in the common mailbox format used by readers such as Feedly and NetNewsWire. 36 | 37 | This is how you invoke it: 38 | 39 | http://feeder.scripting.com/?template=mailbox&feedurl=https://johnnaughton.substack.com/feed 40 | 41 | The mailbox form works well with feeds with lots of titled longform posts, not so well with items that don't have titles, or are short. 42 | 43 | ### The Titled Items template 44 | 45 | Here's the template. 46 | 47 | This template displays items that have titles and links to their <link> value, if it has one. 48 | 49 | This is how you invoke it: 50 | 51 | http://feeder.scripting.com/?template=titleditems&feedurl=https://news.ycombinator.com/rss 52 | 53 | Here's the code that builds the list. 54 | 55 | ```JavaScript function viewTitledItems () { var htmltext = ""; function add (s) { htmltext += s + "\n"; } add ("
    "); theFeed.items.forEach (function (item) { if (item.title !== undefined) { var link = item.title; if (item.link !== undefined) { link = "" + link + ""; } add ("
  • " + link + "
  • "); } }); add ("
"); return (htmltext); } ``` 56 | 57 | This is the punchline for the whole reallysimple stack up to this point. The goal was to make using info from a feed as simple as working with a JavaScript object. At this point you have the full power of JavaScript and the web to work with the info in a feed. 58 | 59 | ### The JSONify Feed template 60 | 61 | Here's the template. 62 | 63 | This is how you invoke it: 64 | 65 | http://feeder.scripting.com/?template=jsonify&feedurl=http://scripting.com/rss.xml 66 | 67 | It displays the JSONified feed in a nice Concord outline. The top level of the object is expanded. The list of items is collapsed. It's a fun way to browse the JSON that the reallysimple package generates. 68 | 69 | ### Questions, comments 70 | 71 | Please post an item in this thread. 72 | 73 | -------------------------------------------------------------------------------- /demos/titlelessFeedsHowto/code.js: -------------------------------------------------------------------------------- 1 | const myVersion = "0.4.1", myProductName = "titlelessFeedsHowto"; 2 | 3 | var defaultFeedUrl = "http://scripting.com/rss.xml"; 4 | 5 | var config = { 6 | maxTitleTextLength: 120, 7 | maxBodyTextLength: 240, 8 | maxItemsInList: 25, 9 | flDisplayBodytext: true 10 | }; 11 | 12 | function getUrlParam (name) { 13 | var val = getURLParameter (name); 14 | if (val == "null") { 15 | return (undefined); 16 | } 17 | else { 18 | return (decodeURIComponent (val)); 19 | } 20 | } 21 | function goToNewUrl () { 22 | var newUrl = $("#idFeedUrlInput").val (); 23 | window.location.href = "?url=" + encodeURIComponent (newUrl); 24 | } 25 | function firstCharsFrom (theString, ctCharsApprox) { 26 | var ixLastWhitespace = 0; 27 | for (var i = 0; i < theString.length; i++) { 28 | if (isWhitespace (theString [i])) { 29 | ixLastWhitespace = i; 30 | } 31 | if (i == ctCharsApprox) { 32 | return (stringMid (theString, 1, ixLastWhitespace)); 33 | } 34 | } 35 | return (theString); 36 | } 37 | function firstSentence (theString) { //12/11/22 38 | for (var i = 0; i < theString.length - 1; i++) { 39 | if (theString [i] == ".") { 40 | if (isWhitespace (theString [i + 1])) { 41 | return (stringMid (theString, 1, i + 1)); 42 | } 43 | } 44 | } 45 | return (""); 46 | } 47 | function httpRequest (url, timeout, headers, callback) { 48 | timeout = (timeout === undefined) ? 30000 : timeout; 49 | var jxhr = $.ajax ({ 50 | url: url, 51 | dataType: "text", 52 | headers, 53 | timeout 54 | }) 55 | .success (function (data, status) { 56 | callback (undefined, data); 57 | }) 58 | .error (function (status) { 59 | var message; 60 | try { //9/18/21 by DW 61 | message = JSON.parse (status.responseText).message; 62 | } 63 | catch (err) { 64 | message = status.responseText; 65 | } 66 | var err = { 67 | code: status.status, 68 | message 69 | }; 70 | callback (err); 71 | }); 72 | } 73 | function readFeed (feedUrl, callback) { 74 | var url = "http://feeder.scripting.com/returnjson?url=" + feedUrl; 75 | httpRequest (url, undefined, undefined, function (err, jsontext) { 76 | if (err) { 77 | callback (err); 78 | } 79 | else { 80 | try { 81 | var jstruct = JSON.parse (jsontext); 82 | callback (undefined, jstruct); 83 | } 84 | catch (err) { 85 | callback (err); 86 | } 87 | } 88 | }); 89 | } 90 | function viewFeedItems (feedUrl) { 91 | readFeed (feedUrl, function (err, theFeed) { 92 | if (err) { 93 | alertDialog (err.message); 94 | } 95 | else { 96 | $("#idFeedTitle").text (theFeed.title); 97 | const itemViewer = $("#idItemViewer"); 98 | const itemList = $("
    "); 99 | var ctItems = 0; 100 | theFeed.items.forEach (function (feedItem) { 101 | if (ctItems < config.maxItemsInList) { 102 | const viewedItem = $("
  • "); 103 | var titleText = "", bodyText = ""; 104 | if (feedItem.title !== undefined) { 105 | titleText = stripMarkup (feedItem.title) + ". "; 106 | bodyText = stripMarkup (feedItem.description); 107 | } 108 | else { 109 | let s = stripMarkup (feedItem.description); 110 | 111 | let firstSen = firstSentence (s); //12/11/22 by DW 112 | if ((firstSen.length <= config.maxTitleTextLength) && (firstSen.length > 0)) { 113 | titleText = firstSen; 114 | } 115 | else { 116 | titleText = firstCharsFrom (s, config.maxTitleTextLength); 117 | } 118 | bodyText = stringDelete (s, 1, titleText.length); 119 | 120 | bodyText = maxStringLength (bodyText, config.maxBodyTextLength, true, true); //whole words, if truncated add elipses 121 | } 122 | const theTitle = $("" + titleText + ""); 123 | const theBody = $("" + bodyText + ""); 124 | 125 | viewedItem.append (theTitle); 126 | if (config.flDisplayBodytext) { 127 | viewedItem.append (theBody); 128 | } 129 | ctItems++; 130 | 131 | itemList.append (viewedItem); 132 | 133 | viewedItem.mouseenter (function () { 134 | viewedItem.addClass ("hovering"); 135 | }); 136 | viewedItem.mouseleave (function () { 137 | viewedItem.removeClass ("hovering"); 138 | }); 139 | viewedItem.click (function () { 140 | if (feedItem.permalink === undefined) { 141 | speakerBeep (); 142 | } 143 | else { 144 | window.location.href = feedItem.permalink; 145 | } 146 | }); 147 | } 148 | }); 149 | itemViewer.append (itemList); 150 | } 151 | }); 152 | } 153 | 154 | function startup () { 155 | console.log ("startup"); 156 | $(".divVersionNumber").text ("v" + myVersion); 157 | var urlParam = getUrlParam ("url"); 158 | var feedUrl = (urlParam === undefined) ? defaultFeedUrl : urlParam; 159 | $("#idFeedUrlInput").val (feedUrl); 160 | viewFeedItems (feedUrl); 161 | hitCounter (); 162 | } 163 | -------------------------------------------------------------------------------- /demos/feeder/worknotes.md: -------------------------------------------------------------------------------- 1 | #### 11/28/25; 10:20:32 AM by DW 2 | 3 | There was a problem in cleanDescription that made returnLinkblogHtml not work properly. 4 | 5 | #### 1/11/25; 11:01:56 AM by DW 6 | 7 | In the jsonify template we were including http files when the app is served via https. 8 | 9 | This way of calling it should work, but doesn't -- 10 | 11 | * https://feeder.scripting.com/?feedurl=https://news.ycombinator.com/rss 12 | 13 | What we do -- 14 | 15 | * if a request is made for / and no template was specified, we do the same thing we do as if returnjson had been called 16 | 17 | * this is what i always expect it to do, when using it as a utility, so let's make it work that way. 18 | 19 | #### 4/20/23; 9:26:31 AM by DW -- v0.5.0 20 | 21 | More new calls: 22 | 23 | /returnlinkblogjson 24 | 25 | Example of call -- 26 | 27 | http://feeder.scripting.com/returnlinkblogjson?feedurl=http://data.feedland.org/feeds/davewiner.xml 28 | 29 | This is the format that Radio3 saves a user's linkblog to. At first I thought I'd need this format, but later decided to create a different endpoint that put all the rendering code in feeder, so I didn't need to replicate it in the apps. 30 | 31 | /returnlinkbloghtml 32 | 33 | Example of call -- 34 | 35 | http://feeder.scripting.com/returnlinkbloghtml?feedurl=http://data.feedland.org/feeds/davewiner.xml 36 | 37 | This renders an RSS feed, probably generated by FeedLand, as a linkblog. 38 | 39 | It's how the Links panel on Scripting News is rendered, and also how we do the linkblog panel for Drummer blogs. 40 | 41 | The Feeder app has become a pretty useful tool, it's a good place to put code for rendering feeds that's needed for browser-based apps. ;-) 42 | 43 | #### 4/19/23; 9:36:56 AM by DW 44 | 45 | New call -- /returnlinkblogday 46 | 47 | Example of call -- 48 | 49 | http://feeder.scripting.com/returnlinkblog?feedurl=http://data.feedland.org/feeds/davewiner.xml 50 | 51 | We return the HTML text you'd display if you wanted to display the links for the current day, as we do in the nightly email. 52 | 53 | I needed a place to test this code before including it in the server app. 54 | 55 | Increasingly that's a role the Feeder app is taking on. A nice development testbed for feed renderings built on the reallysimple package. 56 | 57 | Note if you want a full linkblog-style rendering, use the endpoint I added on the 20th, returnlinkbloghtml. 58 | 59 | #### 4/7/23; 10:00:28 AM by DW 60 | 61 | Return text/plain with charset=utf-8. 62 | 63 | #### 3/29/23; 12:36:50 PM by DW 64 | 65 | Add /returnmarkdown endpoint. 66 | 67 | #### 7/15/22; 9:41:22 AM by DW 68 | 69 | Included the mailbox template, also documented it. 70 | 71 | #### 6/23/22; 11:38:44 AM by DW -- v0.4.10 72 | 73 | A new template, jsonify. 74 | 75 | Two new values returned in serverConfig -- 76 | 77 | feedUrl -- the url of the feed we're asking you to render 78 | 79 | ctSecs -- how many seconds it took to read the feed 80 | 81 | #### 6/22/22; 10:19:39 AM by DW -- v0.4.8 82 | 83 | Mopping up, fixing little things. Today's project is to write docs for the Hello World template. 84 | 85 | #### 6/21/22; 11:32:20 AM by DW-- v0.4.7 86 | 87 | the helloworld template 88 | 89 | it's in the templates folder 90 | 91 | here's how you access it 92 | 93 | http://feeder.scripting.com/?template=helloworld&feedurl=http%3A%2F%2Fscripting.com%2Frss.xml 94 | 95 | have a look at the template source. 96 | 97 | i made it as simple as possible, but I did use jQuery. It wasn't worth the time imho to figure out how to not use jQuery. ;-) 98 | 99 | The next step is to do a bit of docs. Need a fresh start for that. 100 | 101 | And then I want to get some people reviewing this stuff. I don't want all the suggestions to come two months from now. 102 | 103 | We still have a lot of ground to cover, this is just the beginning. 104 | 105 | #### 6/20/22; 12:21:34 PM by DW -- v0.4.6 106 | 107 | feeder now supports templates, so it's easy to add a new way to view a feed. 108 | 109 | here's an example. I implemented the mailbox viewer as a template, it was previously a built-in command. 110 | 111 | http://feeder.scripting.com/?template=mailbox&url=https://fallows.substack.com/feed 112 | 113 | there's a viewers subfolder, to add a template named hello, you'd add a file hello.html to the folder. 114 | 115 | before serving the text, we do some macro substitutions, with the title of the feed, the name and version number of the feeder app, and most important, a JSON structure with the contents of the feed as produced by the reallysimple package. 116 | 117 | next step -- write a hello world template and document it. this will be clearer when that's provided. 118 | 119 | still feeder is just a testbed. these templates will become applications in their own right. 120 | 121 | #### 6/13/22; 12:06:18 PM by DW -- v0.4.5 122 | 123 | Include the charset in the content-type header when returning JSON and XML. 124 | 125 | #### 6/12/22; 6:56:43 PM by DW -- v0.4.4 126 | 127 | We now keep a stats.json file, with info on number of reads, errors, and reads per feed. 128 | 129 | #### 6/12/22; 9:52:26 AM by DW 130 | 131 | I need this functionality for the reallysimple project. 132 | 133 | This predates the package, it's basically where it was developed. 134 | 135 | So I rewrote it to use the package. 136 | 137 | -------------------------------------------------------------------------------- /example/test.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "An experimental blog", 3 | "description": "A place for interesting experiments.", 4 | "pubDate": "2025-10-10T15:04:39.000Z", 5 | "link": "https://scripting4.wordpress.com/", 6 | "language": "en", 7 | "generator": "http://wordpress.com/", 8 | "cloud": { 9 | "domain": "scripting4.wordpress.com", 10 | "port": "80", 11 | "path": "/?rsscloud=notify", 12 | "registerprocedure": "", 13 | "protocol": "http-post", 14 | "type": "rsscloud" 15 | }, 16 | "image": { 17 | "url": "https://s0.wp.com/i/buttonw-com.png", 18 | "title": "An experimental blog" 19 | }, 20 | "linkToSelf": "https://scripting4.wordpress.com/feed/", 21 | "reader": { 22 | "app": "reallysimple v0.5.3 (darwin)", 23 | "ctSecsToRead": 78670.402 24 | }, 25 | "items": [ 26 | { 27 | "description": "

    Oregon is nice this time of year.

    ", 28 | "pubDate": "2025-10-10T15:04:39.000Z", 29 | "link": "https://scripting4.wordpress.com/2025/10/10/420/", 30 | "guid": "http://scripting4.wordpress.com/2025/10/10/420/", 31 | "author": "Dave Winer", 32 | "comments": "https://scripting4.wordpress.com/2025/10/10/420/#respond", 33 | "enclosure": { 34 | "url": "https://1.gravatar.com/avatar/d1f026c099fa51d2957b1612f11412cd08dd69c2bb160968068ed0f337b8918b?s=96&d=identicon&r=G", 35 | "type": "image" 36 | } 37 | }, 38 | { 39 | "description": "

    next in a series of new posts.

    ", 40 | "pubDate": "2025-10-10T14:51:20.000Z", 41 | "link": "https://scripting4.wordpress.com/2025/10/10/419/", 42 | "guid": "http://scripting4.wordpress.com/2025/10/10/419/", 43 | "author": "Dave Winer", 44 | "comments": "https://scripting4.wordpress.com/2025/10/10/419/#respond", 45 | "enclosure": { 46 | "url": "https://1.gravatar.com/avatar/d1f026c099fa51d2957b1612f11412cd08dd69c2bb160968068ed0f337b8918b?s=96&d=identicon&r=G", 47 | "type": "image" 48 | } 49 | }, 50 | { 51 | "description": "

    Now we're going to do a bunch of new posts.

    ", 52 | "pubDate": "2025-10-10T14:46:29.000Z", 53 | "link": "https://scripting4.wordpress.com/2025/10/10/418/", 54 | "guid": "http://scripting4.wordpress.com/2025/10/10/418/", 55 | "author": "Dave Winer", 56 | "comments": "https://scripting4.wordpress.com/2025/10/10/418/#respond", 57 | "enclosure": { 58 | "url": "https://1.gravatar.com/avatar/d1f026c099fa51d2957b1612f11412cd08dd69c2bb160968068ed0f337b8918b?s=96&d=identicon&r=G", 59 | "type": "image" 60 | } 61 | }, 62 | { 63 | "description": "

    Lettuce Z.

    ", 64 | "pubDate": "2025-10-09T15:07:59.000Z", 65 | "link": "https://scripting4.wordpress.com/2025/10/09/417/", 66 | "guid": "http://scripting4.wordpress.com/2025/10/09/417/", 67 | "author": "Dave Winer", 68 | "comments": "https://scripting4.wordpress.com/2025/10/09/417/#respond", 69 | "enclosure": { 70 | "url": "https://1.gravatar.com/avatar/d1f026c099fa51d2957b1612f11412cd08dd69c2bb160968068ed0f337b8918b?s=96&d=identicon&r=G", 71 | "type": "image" 72 | } 73 | }, 74 | { 75 | "description": "

    Guilty pleasure.

    ", 76 | "pubDate": "2025-10-09T15:04:34.000Z", 77 | "link": "https://scripting4.wordpress.com/2025/10/09/416/", 78 | "guid": "http://scripting4.wordpress.com/2025/10/09/416/", 79 | "author": "Dave Winer", 80 | "comments": "https://scripting4.wordpress.com/2025/10/09/416/#respond", 81 | "enclosure": { 82 | "url": "https://1.gravatar.com/avatar/d1f026c099fa51d2957b1612f11412cd08dd69c2bb160968068ed0f337b8918b?s=96&d=identicon&r=G", 83 | "type": "image" 84 | } 85 | }, 86 | { 87 | "description": "

    Gravy time.

    ", 88 | "pubDate": "2025-10-09T15:03:58.000Z", 89 | "link": "https://scripting4.wordpress.com/2025/10/09/415/", 90 | "guid": "http://scripting4.wordpress.com/2025/10/09/415/", 91 | "author": "Dave Winer", 92 | "comments": "https://scripting4.wordpress.com/2025/10/09/415/#respond", 93 | "enclosure": { 94 | "url": "https://1.gravatar.com/avatar/d1f026c099fa51d2957b1612f11412cd08dd69c2bb160968068ed0f337b8918b?s=96&d=identicon&r=G", 95 | "type": "image" 96 | } 97 | }, 98 | { 99 | "description": "

    keep testing

    ", 100 | "pubDate": "2025-10-09T15:02:29.000Z", 101 | "link": "https://scripting4.wordpress.com/2025/10/09/414/", 102 | "guid": "http://scripting4.wordpress.com/2025/10/09/414/", 103 | "author": "Dave Winer", 104 | "comments": "https://scripting4.wordpress.com/2025/10/09/414/#respond", 105 | "enclosure": { 106 | "url": "https://1.gravatar.com/avatar/d1f026c099fa51d2957b1612f11412cd08dd69c2bb160968068ed0f337b8918b?s=96&d=identicon&r=G", 107 | "type": "image" 108 | } 109 | }, 110 | { 111 | "description": "

    Nother test

    ", 112 | "pubDate": "2025-10-09T14:59:47.000Z", 113 | "link": "https://scripting4.wordpress.com/2025/10/09/413/", 114 | "guid": "http://scripting4.wordpress.com/2025/10/09/413/", 115 | "author": "Dave Winer", 116 | "comments": "https://scripting4.wordpress.com/2025/10/09/413/#respond", 117 | "enclosure": { 118 | "url": "https://1.gravatar.com/avatar/d1f026c099fa51d2957b1612f11412cd08dd69c2bb160968068ed0f337b8918b?s=96&d=identicon&r=G", 119 | "type": "image" 120 | } 121 | }, 122 | { 123 | "description": "

    oops i had that feed hidden

    ", 124 | "pubDate": "2025-10-09T14:55:53.000Z", 125 | "link": "https://scripting4.wordpress.com/2025/10/09/412/", 126 | "guid": "http://scripting4.wordpress.com/2025/10/09/412/", 127 | "author": "Dave Winer", 128 | "comments": "https://scripting4.wordpress.com/2025/10/09/412/#respond", 129 | "enclosure": { 130 | "url": "https://1.gravatar.com/avatar/d1f026c099fa51d2957b1612f11412cd08dd69c2bb160968068ed0f337b8918b?s=96&d=identicon&r=G", 131 | "type": "image" 132 | } 133 | }, 134 | { 135 | "description": "

    Let's do a test post.

    ", 136 | "pubDate": "2025-10-09T14:54:51.000Z", 137 | "link": "https://scripting4.wordpress.com/2025/10/09/411/", 138 | "guid": "http://scripting4.wordpress.com/2025/10/09/411/", 139 | "author": "Dave Winer", 140 | "comments": "https://scripting4.wordpress.com/2025/10/09/411/#respond", 141 | "enclosure": { 142 | "url": "https://1.gravatar.com/avatar/d1f026c099fa51d2957b1612f11412cd08dd69c2bb160968068ed0f337b8918b?s=96&d=identicon&r=G", 143 | "type": "image" 144 | } 145 | } 146 | ] 147 | } -------------------------------------------------------------------------------- /worknotes.md: -------------------------------------------------------------------------------- 1 | #### 11/28/25; 10:50:41 AM by DW 2 | 3 | Fixed a bug in the feeder app that was causing the Links page on scripting.com to look awful. There was a change in the way links.daveverse.org was formatting the description elements. 4 | 5 | #### 11/18/25; 8:09:33 AM by DW 6 | 7 | In convertFeed, if there's a source:markdown element present in an item, we were changing the value of the item description to a rendered version of the markdown text. There's no comment explaining why we did this and it seems wrong. It might make some sense if there's no description element present. 8 | 9 | This caused problems in the scripting.com/rss.xml feed because we were generating the markdown text from the description, and it was screwing up fediverse addresses that it thought was a mailto. Like @scripting@daveverse.org. 10 | 11 | I commented the line of code out. It seems it should be up to the source of the feed to decide what the relationship is between the markdown text and the description. 12 | 13 | #### 10/10/25; 11:13:23 AM by DW 14 | 15 | We now look for site id and post id in wordpress-generated sites. 16 | 17 | No we don't. I misunderstood, the id's are not present in the feeds, they are present in the API. 18 | 19 | #### 8/23/25; 10:54:38 AM by DW 20 | 21 | wpSiteId and wpPostId are now numbers. 22 | 23 | #### 8/22/25; 9:30:06 AM by DW -- v0.5.1 24 | 25 | We now look for the wordpress site id and post id, if present... 26 | 27 | * the site id is saved at the top level of the jstruct with the name wpSiteId 28 | 29 | * the post id is saved in the item with the name wpPostId 30 | 31 | This facilitates the Edit This Page function in wordpress editors, such as WordLand. 32 | 33 | Thought of doing a more general solution, but that'll have to wait until other people are in the loop. 34 | 35 | #### 5/25/24; 1:36:02 PM by DW 36 | 37 | Added support for element, map it to linkToSelf, the same as atom:link, implemented on 6/15/22. 38 | 39 | Wrote a blog post about it. 40 | 41 | #### 3/14/24; 1:40:38 PM by DW 42 | 43 | Added support for . 44 | 45 | #### 11/28/23; 3:41:08 PM by DW 46 | 47 | Added support for source:cloud element. 48 | 49 | #### 11/8/23; 10:20:30 AM by DW 50 | 51 | Changed version number on marked package to 3.0.8, the last version that worked afaik. 52 | 53 | #### 5/16/23 by DW -- v0.4.24 54 | 55 | Believe it or not node-emoji of all things introduced a breaking change, so we have to hold at v1.11.0. 56 | 57 | #### 3/22/23 by DW -- v0.4.23 58 | 59 | Allow for the possibility that rss:guid elements might not have atttributes. I know it happens because when it does FeedLand crashes. ;-) 60 | 61 | #### 11/11/22 by DW 62 | 63 | Hack alert: We generate item-level link elements for items that don't have link elements. Explained here. This isn't my code that's doing it, it's a lower-level package that I'm using. I think it was copying an even older package. Which did something they should not have done. 64 | 65 | #### 9/29/22 by DW -- v0.4.20 66 | 67 | I did an update to the davefeedread package, but for some reason NPM isn't picking it up. So I changed the spec in package.json to specify the exact version or greater, to "force" it to use the correct version, I hope. 68 | 69 | #### 8/25/22 by DW -- v0.4.19 70 | 71 | We now return the markdowntext for items that have the source:markdown element. 72 | 73 | We needed this because [app] strips markup from descriptions, by providing markdowntext, we will have a way to regenerate it. The idea is that we can let through the styling that Markdown implements, it's harmless, where the garbage many feeds put in their text is intolerable. 74 | 75 | #### 7/18/22 by DW -- v0.4.17 76 | 77 | If we see a source:markdown element, we generate the description by processing the markdown text. 78 | 79 | #### 7/14/22 by DW -- v0.4.16 80 | 81 | Created a new item-level value called permalink. Previously we were getting the permalink and overwriting the guid value. Which is all well and good if the guid is a permalink. It's possible it's not, as illustrated by this feed, where it's just an id, not a permalink, in which case we overwrite the guid with undefined. Not really good behavior. So now instead of doing that we copy it into the permalink value. 82 | 83 | #### 7/2/22 by DW -- v0.4.15 84 | 85 | Added a new reader section to the returned object, containing information that's not from the feed, rather is about the feed reader software. The first element of this object is ctSecsToRead which is the number of seconds it took to read the feed. Also included the name of the reader app and version. 86 | 87 | #### 6/23/22 by DW -- v0.4.11 88 | 89 | New exported function reallySimple.setConfig. 90 | 91 | Right now there is just one item in its config, timeoutSecs. 92 | 93 | We need to change it in the feeder app, and there may be other config elements to change in the future, so let's do this right. 94 | 95 | Also change the default from 3 to 10. 96 | 97 | #### 6/20/22 by DW -- v0.4.10 98 | 99 | Fixed a syntax error on the first line of reallysimple.js, changing a semicolon to a comma. 100 | 101 | Oddly, this is not seen as a syntax error in my Node, whereas Scott's is reporting an error. 102 | 103 | It's not good syntax no matter what. :smile: 104 | 105 | #### 6/15/22 by DW -- v0.4.9 106 | 107 | If a feed has an <atom:link> head-level element with rel="self", we add a head-level linkToSelf value with its value. 108 | 109 | A blog post on this addition. 110 | 111 | #### 6/12/22 by DW 112 | 113 | Feeder is a server app that connects to the reallysimple package via the web. 114 | 115 | #### 6/11/22 by DW -- v0.4.8 116 | 117 | Announced on Scripting News and Twitter. 118 | 119 | If the cloud element exists but is empty, delete it. 120 | 121 | Only allow url, type and length properties in enclosure objects. 122 | 123 | Don't pass through enclosure properties whose value is null. 124 | 125 | #### 6/5/22 by DW 126 | 127 | Put together the notes on how RSS 0.94 became RSS 2.0 in the summer of 2002. 128 | 129 | Added a menubar to the reallysimple.org website, to organize the various projects. 130 | 131 | #### 5/17/22 by DW 132 | 133 | If an object is undefined there's no need to delete it. 134 | 135 | #### 4/7/22 by DW 136 | 137 | Clean up the readme file. Simplify the example app. Review docs. Start to invite collaborators to the repo, still private. 138 | 139 | #### 3/21/22 by DW 140 | 141 | Reviewing the way we represent links in items in the API. 142 | 143 | The question is this -- how can we get a link to the item from the item. 144 | 145 | The answer, on reflection -- rely on the link element as the permalink. 146 | 147 | #### 3/7/22 by DW 148 | 149 | Start the reallysimple repo. Publish the NPM package. 150 | 151 | #### 3/6/22 by DW 152 | 153 | It now understands various elements from the source namespace, including source:account, source:localtime and source:outline. 154 | 155 | Started a new private GitHub repo for the project and saved the files. 156 | 157 | Added it to my NPM sub-menu, this is going to be a supported project. 158 | 159 | -------------------------------------------------------------------------------- /demos/clouddemo/clouddemo.js: -------------------------------------------------------------------------------- 1 | const myVersion = "0.4.0", myProductName = "clouddemo"; 2 | 3 | const fs = require ("fs"); 4 | const utils = require ("daveutils"); 5 | const qs = require ("querystring"); 6 | const request = require ("request"); 7 | const davehttp = require ("davehttp"); 8 | const xml2js = require ("xml2js"); 9 | const reallysimple = require ("reallysimple"); 10 | 11 | var config = { 12 | port: process.env.PORT || 1422, 13 | flPostEnabled: true, 14 | flLogToConsole: true, //davehttp logs each request to the console 15 | flTraceOnError: false, //davehttp does not try to catch the error 16 | defaultFeedUrl: "https://unberkeley.wordpress.com/feed/", //4 17 | thisServer: { //how the cloud server should call us back -- //1 18 | domain: "clouddemo.rss.land", 19 | port: 80, 20 | feedUpdatedCallback: "/feedupdated" 21 | } 22 | }; 23 | var whenLastRequest = new Date (0); 24 | 25 | var stats = { 26 | events: new Array () //2 27 | } 28 | var flStatsChanged = false; 29 | const fnameStats = "stats.json"; 30 | 31 | function readStats (callback) { 32 | fs.readFile (fnameStats, function (err, jsontext) { 33 | try { 34 | stats = JSON.parse (jsontext); 35 | callback (undefined); 36 | } 37 | catch (err) { 38 | callback (err); 39 | } 40 | }); 41 | } 42 | function statsChanged () { 43 | flStatsChanged = true; 44 | } 45 | function logEvent (infoAboutEvent) { 46 | if (infoAboutEvent.whenstart !== undefined) { 47 | infoAboutEvent.ctSecs = utils.secondsSince (infoAboutEvent.whenstart); 48 | delete infoAboutEvent.whenstart; 49 | } 50 | infoAboutEvent.when = new Date ().toLocaleString (); 51 | stats.events.push (infoAboutEvent); //insert at beginning 52 | statsChanged (); 53 | } 54 | function requestWithRedirect (theRequest, callback) { //12/11/22 by DW 55 | var myRequest = new Object (); 56 | for (var x in theRequest) { 57 | myRequest [x] = theRequest [x]; 58 | } 59 | myRequest.followAllRedirects = false; //we're doing this ourselves 60 | myRequest.maxRedirects = (myRequest.maxRedirects === undefined) ? 0 : myRequest.maxRedirects; 61 | request (myRequest, function (err, response, body) { 62 | const code = response.statusCode; 63 | if ((code == 301) || (code == 302)) { //redirect 64 | if (myRequest.maxRedirects == 0) { 65 | callback (err, response, body); 66 | } 67 | else { 68 | myRequest.maxRedirects--; 69 | myRequest.url = response.headers.location; 70 | requestWithRedirect (myRequest, callback); 71 | } 72 | } 73 | else { 74 | callback (err, response, body); 75 | } 76 | }); 77 | } 78 | function getUrlCloudServer (theCloudElement) { 79 | var url = undefined; 80 | if ((theCloudElement !== undefined) && (theCloudElement.type == "rsscloud")) { 81 | url = "http://" + theCloudElement.domain + ":" + theCloudElement.port + theCloudElement.path; 82 | } 83 | return (url); 84 | } 85 | function pleaseNotify (urlCloudServer, feedUrl, thisServer, callback) { //3 86 | function buildParamList (paramtable) { //12/10/22 by DW 87 | if (paramtable === undefined) { 88 | return (""); 89 | } 90 | else { 91 | var s = ""; 92 | for (var x in paramtable) { 93 | if (paramtable [x] !== undefined) { //8/4/21 by DW 94 | if (s.length > 0) { 95 | s += "&"; 96 | } 97 | s += x + "=" + encodeURIComponent (paramtable [x]); 98 | } 99 | } 100 | return (s); 101 | } 102 | } 103 | const theRequest = { 104 | url: urlCloudServer, 105 | method: "POST", 106 | followAllRedirects: true, 107 | maxRedirects: 5, 108 | headers: { 109 | "Content-Type": "application/x-www-form-urlencoded" 110 | }, 111 | body: buildParamList ({ 112 | domain: thisServer.domain, 113 | port: thisServer.port, 114 | path: thisServer.feedUpdatedCallback, 115 | url1: feedUrl, 116 | protocol: "http-post" 117 | }) 118 | }; 119 | requestWithRedirect (theRequest, function (err, response, body) { 120 | if (err) { 121 | callback (err); 122 | } 123 | else { 124 | callback (undefined, body); 125 | } 126 | }); 127 | } 128 | function requestNotification (feedUrl, callback) { 129 | const whenstart = new Date (); 130 | function getResponseFromXml (xmltext, callback) { 131 | var options = { 132 | explicitArray: false 133 | }; 134 | xml2js.parseString (xmltext, options, function (err, jstruct) { 135 | if (err) { 136 | callback (err); 137 | } 138 | else { 139 | if (jstruct == null) { //12/27/21 by DW 140 | let err = {message: "Internal error: xml2js.parseString returned null."}; 141 | callback (err); 142 | } 143 | else { 144 | callback (undefined, jstruct); 145 | } 146 | } 147 | }); 148 | } 149 | reallysimple.readFeed (feedUrl, function (err, theFeed) { 150 | if (err) { 151 | console.log (err.message); 152 | if (callback !== undefined) { 153 | callback (err); 154 | } 155 | } 156 | else { 157 | var urlCloudServer = getUrlCloudServer (theFeed.cloud); 158 | pleaseNotify (urlCloudServer, feedUrl, config.thisServer, function (err, xmltext) { 159 | if (err) { 160 | console.log ("requestNotification: err.message == " + err.message + ", urlCloudServer == " + urlCloudServer + ", feedUrl == " + feedUrl); 161 | logEvent ({ 162 | type: "requestNotification", 163 | error: err.message, 164 | urlCloudServer, 165 | feedUrl, 166 | whenstart 167 | }); 168 | if (callback !== undefined) { 169 | callback (err); 170 | } 171 | } 172 | else { 173 | getResponseFromXml (xmltext, function (err, jstruct) { 174 | var theEvent = { 175 | type: "requestNotification", 176 | urlCloudServer, 177 | response: jstruct.notifyResult ["$"], 178 | feedUrl, 179 | whenstart 180 | } 181 | logEvent (theEvent); 182 | }); 183 | if (callback !== undefined) { 184 | callback (undefined, theFeed); 185 | } 186 | } 187 | }); 188 | } 189 | }); 190 | } 191 | function handlePing (feedUrl, callback) { //5 192 | //12/17/22; 11:14:18 AM by DW 193 | //this is where you'd put code that reads the feed, looks for new or updated items 194 | //it's the punchline, why we did all this stuff in rssCloud, to get you this bit of info 195 | //much faster. 196 | callback (undefined, {status: "Got the update. Thanks! :-)"}) 197 | } 198 | function handleHttpRequest (theRequest) { 199 | var now = new Date (); 200 | const params = theRequest.params; 201 | function returnPlainText (theString) { 202 | if (theString === undefined) { 203 | theString = ""; 204 | } 205 | theRequest.httpReturn (200, "text/plain", theString); 206 | } 207 | function returnNotFound () { 208 | theRequest.httpReturn (404, "text/plain", "Not found."); 209 | } 210 | function returnError (jstruct) { 211 | theRequest.httpReturn (500, "application/json", utils.jsonStringify (jstruct)); 212 | } 213 | switch (theRequest.method) { 214 | case "POST": 215 | switch (theRequest.lowerpath) { 216 | case config.thisServer.feedUpdatedCallback: //6 217 | var jstruct = qs.parse (theRequest.postBody); 218 | handlePing (jstruct.url, function (err, pingResponse) { //read the feed, add new stuff to database, etc. 219 | returnPlainText (pingResponse.status); 220 | logEvent ({ 221 | method: "POST", 222 | path: config.thisServer.feedUpdatedCallback, 223 | params: jstruct, 224 | myResponse: pingResponse 225 | }); 226 | }); 227 | break; 228 | default: 229 | returnNotFound () 230 | break; 231 | } 232 | break; 233 | case "GET": 234 | switch (theRequest.lowerpath) { 235 | case "/now": 236 | returnPlainText (new Date ()); 237 | return (true); 238 | case config.thisServer.feedUpdatedCallback: //7 239 | handlePing (params.url, function (err, pingResponse) { //read the feed, add new stuff to database, etc. 240 | logEvent ({ 241 | method: "GET", 242 | path: config.thisServer.feedUpdatedCallback, 243 | params, 244 | myResponse: params.challenge 245 | }); 246 | returnPlainText (params.challenge); 247 | }); 248 | break; 249 | default: 250 | returnNotFound (); 251 | break; 252 | } 253 | break; 254 | } 255 | } 256 | function everySecond () { 257 | if (utils.secondsSince (whenLastRequest) > 3600) { //request notification once an hour 258 | requestNotification (config.defaultFeedUrl); 259 | whenLastRequest = new Date (); 260 | } 261 | if (flStatsChanged) { 262 | flStatsChanged = false; 263 | fs.writeFile (fnameStats, utils.jsonStringify (stats), function (err) { 264 | if (err) { 265 | console.log ("everySecond: err.message == " + err.message); 266 | } 267 | }); 268 | } 269 | } 270 | readStats (function (err) { 271 | davehttp.start (config, handleHttpRequest); 272 | setInterval (everySecond, 1000); 273 | }); 274 | -------------------------------------------------------------------------------- /reallysimple.js: -------------------------------------------------------------------------------- 1 | var myProductName = "reallysimple", myVersion = "0.5.3"; 2 | 3 | exports.readFeed = readFeed; 4 | exports.convertFeedToOpml = convertFeedToOpml; 5 | exports.setConfig = setConfig; //6/23/22 by DW 6 | 7 | const utils = require ("daveutils"); 8 | const request = require ("request"); 9 | const process = require ("process"); 10 | const opml = require ("opml"); 11 | const davefeedread = require ("davefeedread"); 12 | const marked = require ("marked"); //7/18/22 by DW 13 | const emoji = require ("node-emoji"); //7/18/22 by DW 14 | 15 | const allowedHeadNames = [ 16 | "title", "link", "description", "language", "copyright", "managingEditor", "webMaster", "lastBuildDate", "pubDate", "category", 17 | "generator", "docs", "cloud", "ttl", "image", "rating", "textInput", "skipHours", "skipDays", "source:account", "source:localtime", "source:cloud", "linkToSelf", "source:blogroll" 18 | ]; 19 | const allowedItemNames = [ 20 | "title", "link", "description", "author", "category", "comments", "enclosures", "guid", "pubDate", "source", "source:outline", "source:likes" 21 | ]; 22 | const allowedEnclosureNames = [ 23 | "url", "type", "length" 24 | ]; 25 | 26 | const wpNamespace = "com-wordpress:feed-additions:1"; //8/22/25 by DW 27 | 28 | var config = { 29 | timeOutSecs: 10 30 | } 31 | 32 | function setConfig (options) { //6/23/22 by DW 33 | for (var x in options) { 34 | config [x] = options [x]; 35 | } 36 | } 37 | function isEmptyObject (obj) { 38 | try { 39 | return (Object.keys (obj).length === 0); 40 | } 41 | catch (err) { 42 | return (true); //6/8/22 by DW 43 | } 44 | } 45 | function getItemPermalink (item) { 46 | var rssguid = item ["rss:guid"], returnedval = undefined; 47 | if (rssguid !== undefined) { 48 | var atts = rssguid ["@"]; 49 | if (atts !== undefined) { //3/22/23 by DW 50 | if (atts.ispermalink === undefined) { 51 | returnedval = rssguid ["#"]; 52 | } 53 | else { 54 | if (utils.getBoolean (atts.ispermalink)) { 55 | returnedval = rssguid ["#"]; 56 | } 57 | } 58 | } 59 | } 60 | if (returnedval !== undefined) { 61 | if (utils.beginsWith (returnedval, "http")) { 62 | return (returnedval); 63 | } 64 | } 65 | return (undefined); 66 | } 67 | function markdownProcess (markdowntext) { 68 | var htmltext = marked.parse (markdowntext); 69 | return (htmltext); 70 | } 71 | function emojiProcess (s) { 72 | function addSpan (code, name) { 73 | return ("" + code + ""); 74 | } 75 | return (emoji.emojify (s, undefined, addSpan)); 76 | } 77 | function stringToNum (theString) { //return a number if we can convert, otherwise return string -- 8/23/25 by DW 78 | const num = Number (theString); 79 | if (num == NaN) { 80 | return (theString); 81 | } 82 | else { 83 | return (num); 84 | } 85 | } 86 | 87 | function convertFeedToOpml (theFeed) { //use this if you want to show an RSS feed in an outline 88 | var theOutline = { 89 | opml: { 90 | head: { 91 | title: theFeed.title 92 | }, 93 | body: { 94 | subs: new Array () 95 | } 96 | } 97 | } 98 | theFeed.items.forEach (function (item) { 99 | var linetext, subtext; 100 | if (item.title === undefined) { 101 | linetext = item.description; 102 | } 103 | else { 104 | linetext = item.title; 105 | subtext = item.description; 106 | } 107 | theOutline.opml.body.subs.push ({ 108 | text: linetext, 109 | type: "link", 110 | url: item.link 111 | }); 112 | }); 113 | return (opml.stringify (theOutline)); 114 | } 115 | function convertFeed (oldFeed, whenstart) { 116 | var newFeed = new Object (); 117 | 118 | function convertOutline (jstruct) { 119 | var theNewOutline = {}; 120 | if (jstruct ["@"] !== undefined) { 121 | utils.copyScalars (jstruct ["@"], theNewOutline); 122 | } 123 | if (jstruct ["source:outline"] !== undefined) { 124 | if (jstruct ["source:outline"] instanceof Array) { 125 | var theArray = jstruct ["source:outline"]; 126 | theNewOutline.subs = []; 127 | theArray.forEach (function (item) { 128 | theNewOutline.subs.push (convertOutline (item)); 129 | }); 130 | } 131 | else { 132 | theNewOutline.subs = [ 133 | convertOutline (jstruct ["source:outline"]) 134 | ]; 135 | } 136 | } 137 | return (theNewOutline); 138 | } 139 | function removeExtraAttributes (theNode) { 140 | function visit (theNode) { 141 | if (theNode.flincalendar !== undefined) { 142 | delete theNode.flincalendar; 143 | } 144 | if (theNode.subs !== undefined) { 145 | theNode.subs.forEach (function (sub) { 146 | visit (sub); 147 | }); 148 | } 149 | } 150 | visit (theNode); 151 | } 152 | function getHeadValuesFromFirstItem () { //3/6/22 by DW 153 | if (oldFeed.items.length > 0) { 154 | var item = oldFeed.items [0]; 155 | if (item.meta !== undefined) { 156 | if (item.meta ["source:account"] !== undefined) { 157 | var account = item.meta ["source:account"]; 158 | newFeed.accounts = new Object (); 159 | if (Array.isArray (account)) { 160 | account.forEach (function (item) { 161 | var service = item ["@"].service 162 | var name = item ["#"]; 163 | newFeed.accounts [service] = name; 164 | }); 165 | } 166 | else { 167 | var service = account ["@"].service; //something like twitter 168 | var name = account ["#"]; 169 | newFeed.accounts [service] = name; 170 | } 171 | } 172 | if (item.meta ["source:localtime"] !== undefined) { 173 | var localtime = item.meta ["source:localtime"]; 174 | newFeed.localtime = localtime ["#"]; 175 | } 176 | if (item.meta ["source:cloud"] !== undefined) { //11/28/23 by DW 177 | const cloud = item.meta ["source:cloud"]; 178 | newFeed.cloudUrl = cloud ["#"]; 179 | } 180 | if (item.meta ["source:blogroll"] !== undefined) { //3/14/24 by DW 181 | var blogroll = item.meta ["source:blogroll"]; 182 | newFeed.blogroll = blogroll ["#"]; 183 | } 184 | if (item.meta ["source:self"] !== undefined) { //5/25/24 by DW 185 | var linkToSelf = item.meta ["source:self"]; 186 | newFeed.linkToSelf = linkToSelf ["#"]; 187 | } 188 | if (item.meta ["rss:site"] !== undefined) { //8/22/25 by DW 189 | const linkToSite = item.meta ["rss:site"]; 190 | const linkToNamespaces = linkToSite ["@"]; 191 | var flFoundNamespace = false; 192 | for (var x in linkToNamespaces) { 193 | if (linkToNamespaces [x] == wpNamespace) { 194 | flFoundNamespace = true; 195 | } 196 | } 197 | if (flFoundNamespace) { 198 | newFeed.wpSiteId = stringToNum (linkToSite ["#"]); 199 | } 200 | } 201 | } 202 | } 203 | } 204 | 205 | for (var x in oldFeed.head) { 206 | let val = oldFeed.head [x]; 207 | if (val != null) { 208 | allowedHeadNames.forEach (function (name) { 209 | if (x == name) { 210 | newFeed [x] = val; 211 | } 212 | }); 213 | } 214 | } 215 | 216 | getHeadValuesFromFirstItem (); //3/6/22 by DW 217 | 218 | if (newFeed.image !== undefined) { //5/17/22 by DW 219 | if (isEmptyObject (newFeed.image)) { 220 | delete newFeed.image; 221 | } 222 | } 223 | if (newFeed.cloud !== undefined) { //6/11/22 by DW 224 | if (isEmptyObject (newFeed.cloud)) { 225 | delete newFeed.cloud; 226 | } 227 | } 228 | 229 | newFeed.reader = { //7/2/22 by DW 230 | app: myProductName + " v" + myVersion + " (" + process.platform + ")", 231 | ctSecsToRead: utils.secondsSince (whenstart) 232 | }; 233 | 234 | newFeed.items = new Array (); 235 | oldFeed.items.forEach (function (item) { 236 | var newItem = new Object (); 237 | for (var x in item) { 238 | val = item [x]; 239 | if (val != null) { 240 | allowedItemNames.forEach (function (name) { 241 | if (x == name) { 242 | if (x == "source:outline") { 243 | val = convertOutline (item ["source:outline"]); 244 | removeExtraAttributes (val); //3/27/22 by DW 245 | newItem.outline = val; 246 | } 247 | else { 248 | if (x == "enclosures") { 249 | if (item.enclosures.length > 0) { 250 | newItem.enclosure = item.enclosures [0]; 251 | } 252 | } 253 | else { 254 | newItem [x] = val; 255 | } 256 | } 257 | } 258 | }); 259 | } 260 | } 261 | newItem.permalink = getItemPermalink (item); //7/14/22 by DW 262 | if (newItem.source !== undefined) { //5/17/22 by DW 263 | if (isEmptyObject (newItem.source)) { 264 | delete newItem.source; 265 | } 266 | } 267 | 268 | if (newItem.enclosure !== undefined) { //6/11/22 by DW 269 | var enc = new Object (); 270 | for (var x in newItem.enclosure) { 271 | allowedEnclosureNames.forEach (function (name) { 272 | if (x == name) { 273 | if (newItem.enclosure [x] != null) { 274 | enc [x] = newItem.enclosure [x]; 275 | } 276 | } 277 | }); 278 | } 279 | newItem.enclosure = enc; 280 | } 281 | 282 | if (item ["source:markdown"] !== undefined) { //7/18/22 by DW 283 | let markdowntext = item ["source:markdown"] ["#"]; 284 | newItem.markdowntext = markdowntext; //8/25/22 by DW 285 | } 286 | 287 | if (item ["rss:post-id"] !== undefined) { //8/22/25 by DW 288 | const linkToPostId = item ["rss:post-id"]; 289 | const linkToNamespaces = linkToPostId ["@"]; 290 | var flFoundNamespace = false; 291 | for (var x in linkToNamespaces) { 292 | if (linkToNamespaces [x] == wpNamespace) { 293 | flFoundNamespace = true; 294 | } 295 | } 296 | if (flFoundNamespace) { 297 | newItem.wpPostId = stringToNum (linkToPostId ["#"]); 298 | } 299 | } 300 | 301 | newFeed.items.push (newItem); 302 | }); 303 | 304 | return (newFeed); 305 | } 306 | 307 | function readFeed (url, callback) { 308 | const whenstart = new Date (); 309 | davefeedread.parseUrl (url, config.timeOutSecs, function (err, theFeed) { 310 | if (err) { 311 | callback (err); 312 | } 313 | else { 314 | callback (undefined, convertFeed (theFeed, whenstart)); 315 | } 316 | }); 317 | } 318 | -------------------------------------------------------------------------------- /demos/feeder/feeder.js: -------------------------------------------------------------------------------- 1 | const myVersion = "0.5.1", myProductName = "feeder"; 2 | 3 | const fs = require ("fs"); 4 | const utils = require ("daveutils"); 5 | const dateformat = require ("dateformat"); 6 | const request = require ("request"); 7 | const davehttp = require ("davehttp"); 8 | const reallysimple = require ("reallysimple"); 9 | 10 | var config = { 11 | port: process.env.PORT || 1403, 12 | flAllowAccessFromAnywhere: true, 13 | flLogToConsole: true, 14 | defaultFeedUrl: "http://nytimes.com/timeswire/feeds/", 15 | fnameStats: "stats.json", 16 | templatesFolderPath: "templates/" 17 | } 18 | 19 | var stats = { 20 | ctLaunches: 0, 21 | whenLastLaunch: undefined, 22 | ctFeedReads: 0, 23 | whenLastFeedRead: undefined, 24 | ctFeedReadErrors: 0, 25 | whenLastFeedReadError: undefined, 26 | ctSecsLastRequest: undefined, 27 | feeds: new Object () 28 | } 29 | var flStatsChanged = false; 30 | 31 | function statsChanged () { 32 | flStatsChanged = true; 33 | } 34 | function buildParamList (params) { 35 | var s = ""; 36 | for (var x in params) { 37 | if (params [x] !== undefined) { 38 | if (s.length > 0) { 39 | s += "&"; 40 | } 41 | s += x + "=" + encodeURIComponent (params [x]); 42 | } 43 | } 44 | return (s); 45 | } 46 | function readFeed (feedUrl=config.defaultFeedUrl, callback) { 47 | const whenstart = new Date (); 48 | reallysimple.readFeed (feedUrl, function (err, theFeed) { 49 | stats.ctFeedReads++; 50 | stats.whenLastFeedRead = whenstart; 51 | stats.ctSecsLastRequest = utils.secondsSince (whenstart); 52 | if (err) { 53 | stats.ctFeedReadErrors++; 54 | stats.whenLastFeedReadError = whenstart; 55 | callback (err); 56 | } 57 | else { 58 | if (stats.feeds [feedUrl] === undefined) { 59 | stats.feeds [feedUrl] = { 60 | ct: 1, 61 | when: whenstart 62 | } 63 | } 64 | else { 65 | let thisFeed = stats.feeds [feedUrl]; 66 | thisFeed.ct++; 67 | thisFeed.when = whenstart; 68 | } 69 | callback (undefined, theFeed); 70 | } 71 | statsChanged (); 72 | }); 73 | } 74 | function viewFeedInTemplate (feedUrl, templateName, callback) { //6/20/22 by DW 75 | const whenstart = new Date (); 76 | function servePage (templatetext, theFeed) { 77 | const feedJsonText = utils.jsonStringify (theFeed); 78 | const serverConfig = { 79 | productName: myProductName, 80 | version: myVersion, 81 | ctSecs: utils.secondsSince (whenstart), //6/23/22 by DW 82 | feedUrl //6/23/22 by DW 83 | }; 84 | var pagetable = { 85 | feedTitle: theFeed.title, 86 | productnameForDisplay: myProductName, //it appears in the mailbox template -- 6/22/22 AM by DW 87 | config: utils.jsonStringify (serverConfig), 88 | riverJsonText: feedJsonText, //for compatibility with River6 89 | feedJsonText 90 | }; 91 | var pagetext = utils.multipleReplaceAll (templatetext.toString (), pagetable, false, "[%", "%]"); 92 | callback (pagetext); 93 | } 94 | readFeed (feedUrl, function (err, theFeed) { 95 | if (err) { //6/23/22 by DW 96 | callback ("Can't view the feed because there was an error reading it: " + err.message + "."); 97 | } 98 | else { 99 | var flnotfound = true; 100 | utils.sureFolder (config.templatesFolderPath, function () { 101 | var f = config.templatesFolderPath + templateName + ".html"; 102 | fs.readFile (f, function (err, templatetext) { 103 | if (err) { 104 | callback ("Can't view the feed because there was an error reading the template."); 105 | } 106 | else { 107 | servePage (templatetext, theFeed); 108 | } 109 | }); 110 | }); 111 | } 112 | }); 113 | } 114 | function readConfig (f, config, callback) { 115 | fs.readFile (f, function (err, jsontext) { 116 | if (!err) { 117 | try { 118 | var jstruct = JSON.parse (jsontext); 119 | for (var x in jstruct) { 120 | config [x] = jstruct [x]; 121 | } 122 | } 123 | catch (err) { 124 | console.log ("Error reading " + f); 125 | } 126 | } 127 | callback (); 128 | }); 129 | } 130 | function convertFeedToMarkdown (theOutline) { //3/29/23 by DW 131 | var mdtext = ""; 132 | function add (s) { 133 | mdtext += s + "\n\n"; 134 | } 135 | theOutline.items.forEach (function (item) { 136 | if (item.pubDate !== undefined) { 137 | add ("# " + item.pubDate.toUTCString ()); 138 | } 139 | if (item.title === undefined) { 140 | if (item.description !== undefined) { 141 | add ("## " + item.description); 142 | } 143 | } 144 | else { 145 | add ("## " + item.title); 146 | if (item.description !== undefined) { 147 | add ("- " + item.description); 148 | } 149 | } 150 | }); 151 | return (mdtext); 152 | } 153 | 154 | function cleanDescription (desc) { //4/19/23 by DW 155 | desc = utils.trimWhitespace (desc); //11/28/25 by DW 156 | if (utils.beginsWith (desc, "

    ")) { 157 | desc = utils.stringDelete (desc, 1, 3); 158 | } 159 | if (utils.endsWith (desc, "

    ")) { 160 | desc = utils.stringMid (desc, 1, desc.length - 5); 161 | } 162 | return (desc); 163 | } 164 | function everySecond () { 165 | if (flStatsChanged) { 166 | flStatsChanged = false; 167 | fs.writeFile (config.fnameStats, utils.jsonStringify (stats), function (err) { 168 | }); 169 | } 170 | } 171 | 172 | function handleHttpRequest (theRequest) { 173 | var params = theRequest.params; 174 | function returnNotFound () { 175 | theRequest.httpReturn (404, "text/plain", "Not found."); 176 | } 177 | function returnRedirect (url) { 178 | const code = 302; 179 | theRequest.httpReturn (code, "text/plain", code + " REDIRECT", {location: url}); 180 | } 181 | 182 | function returnString (s) { 183 | theRequest.httpReturn (200, "text/plain; charset=utf-8", s); //4/7/23 by DW 184 | } 185 | function returnHtml (htmltext) { 186 | theRequest.httpReturn (200, "text/html; charset=utf-8", htmltext); //6/13/22 by DW 187 | } 188 | function returnOpml (opmltext) { 189 | theRequest.httpReturn (200, "text/xml; charset=utf-8", opmltext); //6/13/22 by DW 190 | } 191 | 192 | function returnLinkblogDay (feedUrl, callback) { 193 | const theDay = new Date (); //get the linkblog html for today 194 | readFeed (feedUrl, function (err, theFeed) { 195 | if (err) { 196 | returnError (err); 197 | } 198 | else { 199 | var htmltext = ""; 200 | function add (s) { 201 | htmltext += s + "\n"; 202 | } 203 | var ctitems = 0; 204 | theFeed.items.forEach (function (item) { 205 | if (utils.sameDay (theDay, item.pubDate)) { 206 | var pubdatestring = new Date (item.pubDate).toLocaleTimeString (); 207 | 208 | var link = ""; 209 | if (typeof item.link == "string") { //1/13/23 by DW 210 | link = "" + utils.getDomainFromUrl (item.link) + ""; 211 | } 212 | 213 | add ("
    " + cleanDescription (item.description) + " " + link + "
    "); 214 | ctitems++; 215 | } 216 | }); 217 | if (ctitems > 0) { 218 | htmltext = "

    Linkblog items for the day.

    \n" + htmltext; 219 | } 220 | callback (undefined, htmltext); 221 | } 222 | }); 223 | } 224 | function returnLinkblogJson (feedUrl, callback) { //4/19/23 by DW 225 | readFeed (feedUrl, function (err, theFeed) { 226 | if (err) { 227 | callback (err); 228 | } 229 | else { 230 | var daysArray = new Array (); 231 | theFeed.items.forEach (function (item) { 232 | const convertedItem = { 233 | text: cleanDescription (item.description), 234 | title: item.title, 235 | link: item.link, 236 | linkShort: "", 237 | whenLastEdit: item.pubDate, 238 | flDirty: false, 239 | when: item.pubDate 240 | }; 241 | const pubDate = new Date (item.pubDate); 242 | const datestring = pubDate.toLocaleDateString (); //something like 4/19/2023 243 | var flfound = false; 244 | daysArray.forEach (function (theDay) { 245 | if (utils.sameDay (pubDate, theDay.when)) { 246 | flfound = true; 247 | theDay.jstruct.dayHistory.push (convertedItem); 248 | } 249 | }); 250 | if (!flfound) { 251 | daysArray.push ({ 252 | when: pubDate, 253 | jstruct: { 254 | version: "1.0", 255 | when: new Date (), 256 | whenLastUpdate: new Date (), 257 | dayHistory: [convertedItem] 258 | } 259 | }); 260 | } 261 | }); 262 | callback (undefined, daysArray); 263 | } 264 | }); 265 | } 266 | function returnLinkblogHtml (feedUrl, callback) { //4/20/23 by DW 267 | const firstLinkblogDay = new Date ("April 17, 2023"); 268 | 269 | function getDayTitle (when) { 270 | return (dateformat (when, "dddd, mmmm d, yyyy")); 271 | } 272 | 273 | function buildDaysTable (theFeed) { 274 | var daysTable = new Object (); 275 | theFeed.items.forEach (function (item) { 276 | const pubDate = new Date (item.pubDate); 277 | if (utils.dayGreaterThanOrEqual (pubDate, firstLinkblogDay)) { 278 | const datestring = pubDate.toLocaleDateString (); //something like 4/19/2023 279 | var bucket = daysTable [datestring]; 280 | if (bucket === undefined) { 281 | daysTable [datestring] = new Array (); 282 | bucket = daysTable [datestring]; 283 | } 284 | bucket.push (item); 285 | } 286 | }); 287 | return (daysTable); 288 | } 289 | function appendDay (dayString, theDayItems) { 290 | const when = new Date (dayString); //turn something like 4/19/2023 to a date object 291 | var daytext = "", indentlevel = 0; 292 | 293 | function add (s) { 294 | daytext += utils.filledString ("\t", indentlevel) + s + "\n"; 295 | } 296 | add ("
    " + getDayTitle (when) + "
    "); 297 | add ("
    "); indentlevel++; 298 | theDayItems.forEach (function (item) { 299 | var link = ""; 300 | if (typeof item.link == "string") { //1/13/23 by DW 301 | link = "" + utils.getDomainFromUrl (item.link) + ""; 302 | } 303 | add ("

    " + cleanDescription (item.description) + " " + link + "

    "); //4/18/23 by DW 304 | }); 305 | add ("
    "); indentlevel--; 306 | return (daytext) 307 | } 308 | readFeed (feedUrl, function (err, theFeed) { 309 | if (err) { 310 | callback (err); 311 | } 312 | else { 313 | var daysTable = buildDaysTable (theFeed), htmltext = ""; 314 | for (var x in daysTable) { 315 | htmltext += appendDay (x, daysTable [x]); 316 | } 317 | callback (undefined, htmltext); 318 | } 319 | }); 320 | } 321 | 322 | function returnError (jstruct) { 323 | theRequest.httpReturn (500, "application/json", utils.jsonStringify (jstruct)); 324 | } 325 | function returnData (jstruct) { 326 | if (jstruct === undefined) { 327 | jstruct = {}; 328 | } 329 | theRequest.httpReturn (200, "application/json; charset=utf-8", utils.jsonStringify (jstruct)); //6/13/22 by DW 330 | } 331 | function httpReturn (err, jstruct) { 332 | if (err) { 333 | returnError (err); 334 | } 335 | else { 336 | returnData (jstruct); 337 | } 338 | } 339 | function mailboxRedirect () { 340 | var newUrl = "/?template=mailbox"; 341 | if (params.url !== undefined) { 342 | params.feedurl = params.url; 343 | } 344 | if (params.feedurl !== undefined) { 345 | newUrl += "&feedurl=" + encodeURIComponent (params.feedurl); 346 | } 347 | returnRedirect (newUrl); 348 | } 349 | 350 | if (params.url !== undefined) { //6/20/22 by DW 351 | params.feedurl = params.url; 352 | delete params.url; 353 | let newUrl = theRequest.lowerpath + "?" + buildParamList (params); 354 | returnRedirect (newUrl); 355 | } 356 | else { 357 | switch (theRequest.lowerpath) { 358 | case "/": //6/20/22 by DW 359 | if (params.template === undefined) { //1/11/25 by DW -- don't use a template, just return the json 360 | readFeed (params.feedurl, httpReturn); 361 | } 362 | else { 363 | viewFeedInTemplate (params.feedurl, params.template, returnHtml); 364 | } 365 | break; 366 | case "/stats": 367 | returnData (stats); 368 | break; 369 | case "/returnjson": 370 | readFeed (params.feedurl, httpReturn); 371 | break; 372 | case "/returnopml": 373 | readFeed (params.feedurl, function (err, theFeed) { 374 | if (err) { 375 | returnError (err); 376 | } 377 | else { 378 | returnOpml (reallysimple.convertFeedToOpml (theFeed)); 379 | } 380 | }); 381 | break; 382 | case "/returnmarkdown": //3/29/23 by DW 383 | readFeed (params.feedurl, function (err, theFeed) { 384 | if (err) { 385 | returnError (err); 386 | } 387 | else { 388 | returnString (convertFeedToMarkdown (theFeed)); 389 | } 390 | }); 391 | break; 392 | case "/returnmailbox": //6/18/22 by DW 393 | mailboxRedirect (); 394 | break; 395 | case "/returnlinkblogday": //4/18/23 by DW 396 | returnLinkblogDay (params.feedurl, function (err, htmltext) { 397 | if (err) { 398 | returnError (err); 399 | } 400 | else { 401 | returnHtml (htmltext); 402 | } 403 | }); 404 | break; 405 | case "/returnlinkblogjson": //4/19/23 by DW 406 | returnLinkblogJson (params.feedurl, httpReturn); 407 | break; 408 | case "/returnlinkbloghtml": //4/20/23 by DW 409 | returnLinkblogHtml (params.feedurl, function (err, htmltext) { 410 | if (err) { 411 | returnError (err); 412 | } 413 | else { 414 | returnHtml (htmltext); 415 | } 416 | }); 417 | break; 418 | default: 419 | returnNotFound (); 420 | break; 421 | } 422 | } 423 | } 424 | 425 | readConfig (config.fnameStats, stats, function () { 426 | stats.ctLaunches++; 427 | stats.whenLastLaunch = new Date (); 428 | statsChanged (); 429 | davehttp.start (config, handleHttpRequest) 430 | setInterval (everySecond, 1000); 431 | }); 432 | -------------------------------------------------------------------------------- /demos/feedhunter/source.opml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 14 | 15 | 16 | 17 | nodeEditor: feedHunter 18 | Wed, 08 Nov 2023 19:37:13 GMT 19 | Sat, 09 Mar 2024 15:49:29 GMT 20 | Dave Winer 21 | http://davewiner.com/ 22 | 1, 2, 3, 5, 10, 26 23 | 1 24 | 274 25 | 774 26 | 1094 27 | 1969 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | 289 | 290 | 291 | 292 | 293 | 294 | 295 | 296 | 297 | 298 | 299 | 300 | 301 | 302 | 303 | 304 | 305 | 306 | 307 | 308 | 309 | 310 | 311 | 312 | 313 | 314 | -------------------------------------------------------------------------------- /demos/titlelessFeedsHowto/source.opml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | nodeEditor: titlelessFeedsHowto 6 | Sat, 10 Dec 2022 16:22:27 GMT 7 | Mon, 19 Dec 2022 20:00:32 GMT 8 | Dave Winer 9 | http://davewiner.com/ 10 | 1, 2, 3, 10, 19 11 | 1 12 | 80 13 | 781 14 | 897 15 | 2056 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | 289 | 290 | 291 | 292 | 293 | 294 | 295 | 296 | 297 | 298 | 299 | 300 | 301 | 302 | 303 | 304 | 305 | 306 | 307 | 308 | 309 | 310 | 311 | 312 | 313 | 314 | 315 | 316 | 317 | 318 | 319 | 320 | 321 | 322 | 323 | 324 | 325 | 326 | 327 | 328 | 329 | 330 | 331 | 332 | 333 | 334 | 335 | 336 | 337 | 338 | 339 | 340 | 341 | 342 | 343 | 344 | 345 | 346 | 347 | 348 | 349 | 350 | 351 | 352 | 353 | 354 | 355 | 356 | 357 | 358 | 359 | 360 | 361 | 362 | 363 | 364 | 365 | 366 | 367 | 368 | 369 | 370 | 371 | 372 | 373 | 374 | 375 | 376 | 377 | 378 | 379 | 380 | 381 | 382 | 383 | 384 | 385 | 386 | 387 | 388 | 389 | 390 | 391 | 392 | 393 | 394 | 395 | 396 | 397 | 398 | 399 | 400 | 401 | 402 | 403 | 404 | 405 | 406 | 407 | 408 | 409 | 410 | 411 | 412 | 413 | 414 | 415 | 416 | 417 | 418 | 419 | 420 | 421 | 422 | 423 | 424 | 425 | 426 | 427 | 428 | 429 | 430 | 431 | 432 | 433 | 434 | 435 | 436 | 437 | 438 | 439 | 440 | 441 | 442 | 443 | 444 | 445 | 446 | 447 | 448 | 449 | 450 | 451 | -------------------------------------------------------------------------------- /demos/clouddemo/source.opml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 14 | 15 | 16 | 17 | nodeEditor: cloudDemo 18 | Thu, 15 Dec 2022 19:30:49 GMT 19 | Thu, 16 Feb 2023 15:29:01 GMT 20 | Dave Winer 21 | http://davewiner.com/ 22 | 1, 2, 3, 14, 26, 33, 41, 62, 72, 83, 84, 85, 90, 91, 99, 100, 107, 108, 110, 115, 119, 122, 124 23 | 82 24 | 88 25 | 322 26 | 1050 27 | 1498 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | 289 | 290 | 291 | 292 | 293 | 294 | 295 | 296 | 297 | 298 | 299 | 300 | 301 | 302 | 303 | 304 | 305 | 306 | 307 | 308 | 309 | 310 | 311 | 312 | 313 | 314 | 315 | 316 | 317 | 318 | 319 | 320 | 321 | 322 | 323 | 324 | 325 | 326 | 327 | 328 | 329 | 330 | 331 | 332 | 333 | 334 | 335 | 336 | 337 | 338 | 339 | 340 | 341 | 342 | 343 | 344 | 345 | 346 | 347 | 348 | 349 | 350 | 351 | 352 | 353 | 354 | 355 | 356 | 357 | 358 | 359 | 360 | 361 | 362 | 363 | 364 | 365 | 366 | 367 | 368 | 369 | 370 | 371 | 372 | 373 | 374 | 375 | 376 | 377 | 378 | 379 | 380 | 381 | 382 | 383 | 384 | 385 | 386 | 387 | 388 | 389 | 390 | 391 | 392 | 393 | 394 | 395 | 396 | 397 | 398 | 399 | 400 | 401 | 402 | 403 | 404 | 405 | 406 | 407 | 408 | 409 | 410 | 411 | 412 | 413 | 414 | 415 | 416 | 417 | 418 | 419 | 420 | 421 | 422 | 423 | 424 | 425 | 426 | 427 | 428 | 429 | 430 | 431 | 432 | 433 | 434 | 435 | 436 | 437 | 438 | 439 | 440 | 441 | 442 | 443 | 444 | 445 | 446 | 447 | 448 | 449 | 450 | 451 | 452 | 453 | 454 | 455 | 456 | 457 | 458 | 459 | 460 | 461 | 462 | 463 | 464 | 465 | 466 | 467 | 468 | 469 | 470 | 471 | 472 | 473 | 474 | 475 | 476 | 477 | 478 | 479 | 480 | 481 | 482 | 483 | 484 | 485 | 486 | 487 | 488 | 489 | 490 | 491 | 492 | 493 | 494 | 495 | 496 | 497 | 498 | 499 | 500 | 501 | 502 | 503 | 504 | 505 | 506 | 507 | 508 | 509 | 510 | 511 | 512 | 513 | 514 | 515 | 516 | 517 | 518 | 519 | 520 | 521 | 522 | 523 | 524 | 525 | 526 | 527 | 528 | 529 | 530 | 531 | 532 | 533 | 534 | 535 | 536 | 537 | 538 | 539 | 540 | 541 | 542 | 543 | 544 | 545 | 546 | 547 | 548 | 549 | 550 | 551 | 552 | 553 | 554 | 555 | 556 | 557 | 558 | 559 | 560 | 561 | 562 | 563 | 564 | 565 | 566 | 567 | 568 | 569 | 570 | 571 | 572 | 573 | 574 | 575 | 576 | 577 | 578 | 579 | 580 | 581 | 582 | 583 | 584 | 585 | 586 | 587 | 588 | 589 | 590 | 591 | 592 | 593 | 594 | 595 | 596 | 597 | 598 | 599 | 600 | 601 | 602 | 603 | 604 | 605 | 606 | 607 | 608 | 609 | 610 | 611 | 612 | 613 | 614 | 615 | 616 | 617 | 618 | 619 | 620 | 621 | 622 | 623 | 624 | 625 | 626 | 627 | 628 | 629 | 630 | 631 | 632 | 633 | 634 | 635 | 636 | 637 | 638 | 639 | 640 | 641 | --------------------------------------------------------------------------------