├── database ├── queriesthatwork.md ├── source.opml ├── worknotes.md ├── readme.md └── package.json ├── docs ├── config.json ├── hello.opml ├── misc.opml ├── firstThings.opml ├── template.txt ├── tech.opml ├── categories.opml ├── yourFeed.opml ├── newsProducts.opml └── about.opml ├── utils ├── config.json ├── package.json ├── readme.md └── feedlandutils.js ├── emailtemplate.html ├── README.md ├── package.json ├── source.opml ├── blog.js ├── worknotes.md ├── LICENSE └── feedland.js /database/queriesthatwork.md: -------------------------------------------------------------------------------- 1 | * select * from items where id = 425371\G 2 | 3 | -------------------------------------------------------------------------------- /database/source.opml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scripting/feedland/main/database/source.opml -------------------------------------------------------------------------------- /database/worknotes.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scripting/feedland/main/database/worknotes.md -------------------------------------------------------------------------------- /docs/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "urlGlossaryy": "http://scripting.com/publicfolder/misc/glossary.opml", 3 | "urlGlossaryx": "http://drummer.scripting.com/davewiner/glossary.opml", 4 | "urlOpmlTemplate": "http://scripting.com/code/feedland/docswebsite/template.txt" 5 | } 6 | -------------------------------------------------------------------------------- /utils/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "database": { 3 | "host": "mydatabasehost.com", 4 | "port": 26891, 5 | "user": "admin", 6 | "password": "hahahahahaha", 7 | "charset": "utf8mb4", 8 | "connectionLimit": 100, 9 | "database": "feedland", 10 | "debug": false 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /utils/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "feedlandutils", 3 | "description": "Utilities I need to administer the FeedLand database.", 4 | "author": "Dave Winer ", 5 | "license": "MIT", 6 | "version": "0.4.0", 7 | "dependencies" : { 8 | "daveutils": "*", 9 | "davesql": "*", 10 | "feedlanddatabase": "*" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /database/readme.md: -------------------------------------------------------------------------------- 1 | # FeedLand database 2 | 3 | A database for storing feeds for FeedLand aggregators. 4 | 5 | ### What is this? 6 | 7 | This is the source code for the NPM package feedlanddatabase. 8 | 9 | #### Background 10 | 11 | It's part of the feedland server software, required by the feedland package. 12 | 13 | Licensed under GPL2 as is the feedland package. 14 | 15 | -------------------------------------------------------------------------------- /emailtemplate.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | [%title%] 4 | 5 | 6 | 7 | 8 |
9 |

Click the link below to [%operationToConfirm%].

10 |

[%confirmationUrl%]

11 |
12 | 13 | 14 | -------------------------------------------------------------------------------- /utils/readme.md: -------------------------------------------------------------------------------- 1 | # feedlandUtils 2 | 3 | Sometimes you need to write a utility for database management that can't be done at the command line. This is a shell for such utilities. 4 | 5 | #### How to 6 | 7 | Look for the comment that says: Call your utilities here. You can put code there that uses the database.xxx routines to do the same kind of stuff that the FeedLand client does. 8 | 9 | You must add your own config.json file that includes a database section, as shown in the example file in this folder. 10 | 11 | At some point we will be doing these things in Drummer's scripting language, but not there yet. 12 | 13 | -------------------------------------------------------------------------------- /utils/feedlandutils.js: -------------------------------------------------------------------------------- 1 | var myProductName = "feedlandutils"; myVersion = "0.4.0"; 2 | 3 | const fs = require ("fs"); 4 | const utils = require ("daveutils"); 5 | const request = require ("request"); 6 | const davesql = require ("davesql"); //9/25/22 by DW 7 | const database = require ("feedlanddatabase"); 8 | 9 | fs.readFile ("config.json", function (err, jsontext) { 10 | if (err) { 11 | console.log (err.message); 12 | } 13 | else { 14 | config = JSON.parse (jsontext); 15 | console.log ("config == " + utils.jsonStringify (config)); 16 | davesql.start (config.database, function () { 17 | database.start (config, function () { 18 | 19 | //Call your utilities here -- 10/14/22 by DW 20 | 21 | }); 22 | }); 23 | } 24 | }); 25 | 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # feedLand 2 | 3 | This is the source code for the NPM package feedland. 4 | 5 | ### How to use? 6 | 7 | If you want to install a FeedLand server, follow the instructions in the feedlandInstall repo. 8 | 9 | If you want to build a new server application around FeedLand you can use this package. 10 | 11 | If you want to build around just the database and not the API, you can use the feedlandDatabase NPM package, whose source is in the feedlandDatabase folder here. 12 | 13 | ### This repo is public 14 | 15 | It was announced as public in this post on Scripting News on April 24, 2023. 16 | 17 | -------------------------------------------------------------------------------- /database/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "feedlanddatabase", 3 | "description": "FeedLand code that manages the tables for users, feeds, items, subscriptions, likes.", 4 | "version": "0.8.11", 5 | "author": "Dave Winer ", 6 | "main": "database.js", 7 | "license": "GPLV2", 8 | "files": [ 9 | "database.js", 10 | "worknotes.md", 11 | "database.md" 12 | ], 13 | "repository": { 14 | "type": "git", 15 | "url": "https://github.com/scripting/feedland/tree/main/database" 16 | }, 17 | "peerDependencies": { 18 | "davesql": "^0.6.0" 19 | }, 20 | "dependencies" : { 21 | "request": "*", 22 | "md5": "*", 23 | "sanitize-html": "*", 24 | "marked": "3.0.8", 25 | "daveutils": "*", 26 | "daverss": "^0.6.4", 27 | "daves3": "*", 28 | "nodejs-websocket": "*", 29 | "davegithub": "*", 30 | "reallysimple": "*", 31 | "feedhunter": "*", 32 | "xml2js": "*", 33 | "opml": "*" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "feedland", 3 | "description": "The top level of the FeedLand server, the source code for the NPM feedland package.", 4 | "author": "Dave Winer ", 5 | "version": "0.7.18", 6 | "license": "GPLV2", 7 | "main": "feedland.js", 8 | "files": [ 9 | "blog.js", 10 | "readme.md" 11 | ], 12 | "repository": { 13 | "type": "git", 14 | "url": "https://github.com/scripting/feedland" 15 | }, 16 | "dependencies" : { 17 | "request": "*", 18 | "md5": "*", 19 | "sanitize-html": "*", 20 | "marked": "3.0.8", 21 | "nodejs-websocket": "*", 22 | "daveutils": "^0.4.65", 23 | "davetwitter": "^0.6.39", 24 | "daveappserver": "^0.8.3", 25 | "daverss": "^0.6.4", 26 | "daves3": "^0.4.11", 27 | "davesql": "^0.6.0", 28 | "reallysimple": "^0.5.3", 29 | "davegithub": "^0.5.5", 30 | "feedlanddatabase": "^0.8.11", 31 | "feedhunter": "^0.4.4", 32 | "opml": "^0.5.1", 33 | "wpidentity": "^0.5.27", 34 | "sqllog": "*" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /docs/hello.opml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Hello from FeedLand 5 | Fri, 19 Oct 2022 12:25:05 GMT 6 | We're still testing the software, but thanks for visiting! 7 | http://docs.feedland.org/hello.opml 8 | davewiner 9 | Dave Winer 10 | http://twitter.com/davewiner 11 | ws://drummer.scripting.com:1232/ 12 | Fri, 21 Oct 2022 15:25:56 GMT 13 | 1 14 | 6 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /docs/misc.opml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | FeedLand Extras 5 | Tue, 01 Nov 2022 15:51:42 GMT 6 | New features, documented as they come online. 7 | http://docs.feedland.org/misc.opml 8 | davewiner 9 | Dave Winer 10 | dave.winer@gmail.com 11 | wss://drummer.land/ 12 | Thu, 03 Aug 2023 14:00:18 GMT 13 | 1 14 | 2 15 | 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 | -------------------------------------------------------------------------------- /docs/firstThings.opml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Starting up in FeedLand 5 | Fri, 16 Sep 2022 13:58:36 GMT 6 | http://docs.feedland.org/firstThings.opml 7 | First things a new user should do in FeedLand. 8 | davewiner 9 | Dave Winer 10 | dave.winer@gmail.com 11 | wss://drummer.land/ 12 | Tue, 08 Aug 2023 14:43:16 GMT 13 | 3 14 | 4 15 | 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 | -------------------------------------------------------------------------------- /docs/template.txt: -------------------------------------------------------------------------------- 1 | 2 | 3 | FeedLand: [%title%] 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 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 59 | 119 | 120 | 121 |
122 | 123 | 124 | 143 | 145 | 163 | 164 |
144 | 146 | 162 |
165 |
166 | 172 | 173 | 174 | -------------------------------------------------------------------------------- /docs/tech.opml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | FeedLand tech 5 | Sun, 02 Oct 2022 21:04:21 GMT 6 | Technical notes about FeedLand. 7 | http://docs.feedland.org/tech.opml 8 | davewiner 9 | Dave Winer 10 | http://twitter.com/davewiner 11 | ws://drummer.scripting.com:1232/ 12 | Sat, 08 Oct 2022 16:14:29 GMT 13 | 3,6,9,10,15 14 | 16 15 | 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 | -------------------------------------------------------------------------------- /docs/categories.opml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Categories in FeedLand 5 | Thu, 08 Sep 2022 12:38:49 GMT 6 | http://docs.feedland.org/categories.opml 7 | Categories are essential to FeedLand. 8 | davewiner 9 | Dave Winer 10 | dave.winer@gmail.com 11 | wss://drummer.land/ 12 | Sun, 30 Jul 2023 22:00:21 GMT 13 | 2,3,11,12,15,20,23 14 | 0 15 | 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 | -------------------------------------------------------------------------------- /docs/yourFeed.opml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Your FeedLand feed 5 | Sun, 06 Nov 2022 21:26:18 GMT 6 | There's a feed with your name on it, ready to go. 7 | http://docs.feedland.org/yourFeed.opml 8 | davewiner 9 | Dave Winer 10 | dave.winer@gmail.com 11 | wss://drummer.land/ 12 | Tue, 01 Aug 2023 16:43:31 GMT 13 | 14 | 9 15 | 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 | -------------------------------------------------------------------------------- /source.opml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 14 | 15 | 16 | 17 | config.nodeEditor.projects.feedLand.scripts 18 | Sat, 28 May 2022 17:17:20 GMT 19 | Sat, 29 Nov 2025 15:48:56 GMT 20 | Dave Winer 21 | http://davewiner.com/ 22 | 1, 6, 7, 8, 12 23 | 1 24 | 316 25 | 1050 26 | 1148 27 | 2109 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 | -------------------------------------------------------------------------------- /docs/newsProducts.opml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | News Products 5 | Thu, 18 Aug 2022 15:40:59 GMT 6 | How to create News Products in FeedLand. 7 | http://docs.feedland.org/newsProducts.opml 8 | davewiner 9 | Dave Winer 10 | dave.winer@gmail.com 11 | wss://drummer.land/ 12 | Thu, 14 Sep 2023 14:39:57 GMT 13 | 2,8,11,13,19,21,29,31 14 | 22 15 | 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 | -------------------------------------------------------------------------------- /blog.js: -------------------------------------------------------------------------------- 1 | const fs = require ("fs"); 2 | const marked = require ("marked"); 3 | const request = require ("request"); 4 | const utils = require ("daveutils"); 5 | const rss = require ("daverss"); 6 | const s3 = require ("daves3"); 7 | const davesql = require ("davesql"); 8 | const database = require ("feedlanddatabase"); 9 | 10 | exports.updateBlogSettings = updateBlogSettings; 11 | exports.newPost = newPost; 12 | exports.updatePost = updatePost; 13 | exports.getBlogUrl = getBlogUrl; 14 | exports.buidUsersFeed = buildRss; //11/6/22 by DW 15 | exports.start = start; 16 | 17 | var config = { 18 | }; 19 | 20 | function markdownProcess (s, flGenerateHtmlParagraphs=false) { 21 | var renderer = new marked.Renderer (); 22 | var options = { 23 | renderer: renderer 24 | }; 25 | if (flGenerateHtmlParagraphs) { 26 | return (marked (s, options)); 27 | } 28 | else { 29 | renderer.paragraph = function (s) { 30 | return (s); 31 | }; 32 | return (marked (s, options)); 33 | } 34 | } 35 | function getBlogUrl (screenname) { 36 | var urlfeed = config.urlForFeeds + screenname + ".xml", jstruct; 37 | return (urlfeed); 38 | } 39 | function getBlogLinkValue (screenname) { //11/9/22 by DW 40 | return (config.urlFeedlandApp + "?river=" + config.urlForFeeds + screenname + ".xml"); 41 | } 42 | function buildRss (screenname, callback) { 43 | const feedUrl = getBlogUrl (screenname), whenstart = new Date (); 44 | function notNull (val) { 45 | if (val === undefined) { 46 | return (false); 47 | } 48 | if (val == null) { 49 | return (false); 50 | } 51 | return (true); 52 | } 53 | function checkNull (val) { 54 | if (notNull (val)) { 55 | return (val); 56 | } 57 | return (undefined); 58 | } 59 | function fixMediumEditorQuirks (theText) { 60 | if (utils.endsWith (theText, "\n")) { //7/14/22 by DW 61 | theText = utils.stringMid (theText, 1, theText.length - 1); 62 | } 63 | var splits = theText.split ("

"); 64 | if (splits.length == 2) { 65 | if (utils.beginsWith (theText, "

") && utils.endsWith (theText, "

")) { 66 | theText = utils.stringDelete (theText, 1, 3); 67 | theText = utils.stringMid (theText, 1, theText.length - 4); 68 | } 69 | } 70 | return (theText); 71 | } 72 | database.isFeedInDatabase (feedUrl, function (flInDatabase, feedRec) { 73 | if (flInDatabase) { 74 | database.getUserPrefs (screenname, function (err, thePrefs) { //11/6/22 by DW 75 | var title = checkNull (feedRec.title); 76 | var description = checkNull (feedRec.description); 77 | if (!err) { 78 | title = thePrefs.myFeedTitle; 79 | description = thePrefs.myFeedDescription; 80 | } 81 | 82 | var htmlUrl = checkNull (feedRec.htmlUrl); //11/9/22 by DW 83 | if (htmlUrl === undefined) { 84 | htmlUrl = getBlogLinkValue (screenname); 85 | } 86 | 87 | var headElements = { 88 | title: checkNull (title), 89 | link: htmlUrl, //11/9/22 by DW 90 | description: checkNull (description), 91 | language: checkNull (feedRec.language), 92 | 93 | generator: config.generatorForFeed, //7/14/22 by DW 94 | 95 | docs: config.docsForFeed, //7/14/22 by DW 96 | 97 | maxFeedItems: config.maxFeedItems, 98 | 99 | flRssCloudEnabled: true, 100 | rssCloudDomain: "rpc.rsscloud.io", 101 | rssCloudPort: 5337, 102 | rssCloudPath: "/pleaseNotify", 103 | rssCloudRegisterProcedure: "", 104 | rssCloudProtocol: "http-post", 105 | 106 | rssCloudUrl: "http://rpc.rsscloud.io:5337/pleaseNotify" //11/28/23 by DW 107 | }; 108 | const sqltext = "select * from items where flDeleted=false and feedUrl=" + davesql.encode (feedUrl) + " order by pubDate desc limit " + config.maxFeedItems + ";"; 109 | davesql.runSqltext (sqltext, function (err, result) { 110 | if (err) { 111 | if (callback !== undefined) { 112 | callback (err); 113 | } 114 | } 115 | else { 116 | var historyArray = new Array (); 117 | result.forEach (function (item) { 118 | var description = checkNull (item.description); //5/3/22 by DW 119 | if (description !== undefined) { 120 | description = fixMediumEditorQuirks (description); 121 | } 122 | 123 | var enclosure = undefined; //4/3/23 by DW 124 | if (notNull (item.enclosureUrl) && notNull (item.enclosureLength) && notNull (item.enclosureType)) { //4/3/23 by DW 125 | enclosure = { 126 | url: item.enclosureUrl, 127 | length: item.enclosureLength, 128 | type: item.enclosureType 129 | } 130 | } 131 | 132 | historyArray.push ({ 133 | title: checkNull (item.title), 134 | text: description, 135 | link: (notNull (item.link)) ? item.link : item.guid, //4/5/23 by DW 136 | markdowntext: checkNull (item.markdowntext), //5/5/22 by DW 137 | when: item.pubDate, 138 | enclosure, //4/3/23 by DW 139 | guid: { 140 | flPermalink: utils.beginsWith (item.guid, config.urlFeedlandApp), 141 | value: item.guid 142 | } 143 | }); 144 | }); 145 | 146 | var xmltext = rss.buildRssFeed (headElements, historyArray); 147 | var s3Path = config.s3PathForFeeds + screenname + ".xml"; 148 | s3.newObject (s3Path, xmltext, "text/xml", "public-read", function (err, data) { 149 | if (err) { 150 | console.log ("buildRss: s3Path == " + s3Path + ", err.message == " + err.message); 151 | if (callback !== undefined) { 152 | callback (err); 153 | } 154 | } 155 | else { 156 | database.checkOneFeed (feedUrl, function () { //11/5/22 by DW 157 | rss.cloudPing (undefined, feedUrl); 158 | }); 159 | if (callback !== undefined) { 160 | callback (undefined, feedRec); 161 | } 162 | } 163 | }); 164 | 165 | if (config.flWriteRssFilesLocally) { //9/27/23 by DW 166 | var f = config.localRssPath + screenname + ".xml"; 167 | utils.sureFilePath (f, function () { 168 | fs.writeFile (f, xmltext, function (err) { 169 | if (err) { 170 | console.log ("buildRss: err.message == " + err.message); 171 | } 172 | }); 173 | }); 174 | } 175 | } 176 | }); 177 | }); 178 | } 179 | else { 180 | console.log ("blog.buildRss: feedUrl == " + feedUrl + " is not in the database."); 181 | if (callback !== undefined) { 182 | callback ({message: "Can't build the feed because it's not in the database."}); 183 | } 184 | } 185 | }); 186 | } 187 | function updateBlogSettings (jsontext, screenname, callback) { 188 | var message = "We no longer manage blog settings this way."; //11/7/22 by DW 189 | callback ({message}); 190 | return; 191 | 192 | callback (undefined, new Object ()); //11/6/22 by DW 193 | return; 194 | } 195 | function addFeedIfNecessary (screenname, callback) { //5/8/22 by DW 196 | var feedUrl = getBlogUrl (screenname), whenstart = new Date (), feedRecFromClient; 197 | database.isFeedInDatabase (feedUrl, function (flInDatabase, feedRec) { 198 | if (flInDatabase) { 199 | callback (undefined, feedRec); 200 | } 201 | else { 202 | var feedRec = { 203 | feedUrl, 204 | htmlUrl: getBlogLinkValue (screenname), //11/9/22 by DW 205 | title: screenname + "'s feed", 206 | pubDate: whenstart, 207 | whenCreated: whenstart, 208 | whenUpdated: whenstart, 209 | twitterAccount: screenname, 210 | generator: config.generatorForFeed, 211 | docs: config.docsForFeed, 212 | 213 | ctErrors: 0, 214 | ctConsecutiveErrors: 0, 215 | errorString: "", 216 | whenChecked: whenstart, 217 | ctChecks: 0, 218 | whenLastError: new Date (0) 219 | }; 220 | database.saveFeed (feedRec, function (err, data) { 221 | if (err) { 222 | callback (err); 223 | } 224 | else { 225 | callback (undefined, feedRec); 226 | } 227 | }); 228 | } 229 | }); 230 | } 231 | function getEnclosureInfo (enclosureUrl, callback) { //4/3/23 by DW 232 | var options = { 233 | method: "HEAD", 234 | uri: enclosureUrl 235 | }; 236 | request (options, function (err, response, body) { 237 | if (err) { 238 | callback (err); 239 | } 240 | else { 241 | if (response.statusCode != 200) { 242 | const message = "Can't get enclosure info because there was an error."; 243 | callback ({message}); 244 | } 245 | else { 246 | const jstruct = { 247 | length: response.headers ["content-length"], 248 | type: response.headers ["content-type"] 249 | } 250 | callback (undefined, jstruct); 251 | } 252 | } 253 | }); 254 | } 255 | function checkEnclosure (itemRec, callback) { //4/3/23 by DW 256 | function isNull (val) { 257 | if (val === undefined) { 258 | return (true); 259 | } 260 | if (val == null) { 261 | return (true); 262 | } 263 | return (false); 264 | } 265 | if (isNull (itemRec.enclosureUrl)) { 266 | callback (); 267 | } 268 | else { 269 | if (isNull (itemRec.enclosureType) || isNull (itemRec.enclosureLength)) { 270 | getEnclosureInfo (itemRec.enclosureUrl, function (err, data) { 271 | if (err) { 272 | callback (err); 273 | } 274 | else { 275 | itemRec.enclosureType = data.type; 276 | itemRec.enclosureLength = data.length; 277 | callback (undefined); 278 | } 279 | }); 280 | } 281 | else { 282 | callback (); 283 | } 284 | } 285 | } 286 | function newPost (jsontext, screenname, callback) { 287 | var feedUrl = getBlogUrl (screenname), itemRecFromClient, whenstart = new Date (); 288 | var itemRec = { 289 | feedUrl, 290 | id: undefined, //7/12/22 by DW 291 | guid: undefined, //7/12/22 by DW 292 | pubDate: whenstart, 293 | whenCreated: whenstart, 294 | whenUpdated: whenstart, 295 | flDeleted: false, 296 | outlineJsontext: undefined, 297 | markdowntext: undefined 298 | }; 299 | try { 300 | itemRecFromClient = JSON.parse (jsontext); 301 | } 302 | catch (err) { 303 | callback (err); 304 | return; 305 | } 306 | 307 | if (itemRecFromClient.markdowntext !== undefined) { //5/5/22 by DW 308 | itemRecFromClient.description = markdownProcess (itemRecFromClient.markdowntext, true); 309 | } 310 | for (var x in itemRecFromClient) { 311 | itemRec [x] = itemRecFromClient [x]; 312 | } 313 | 314 | checkEnclosure (itemRec, function (err) { //4/3/23 by DW 315 | database.saveItem (itemRec, function (err, result) { 316 | if (err) { 317 | callback (err); 318 | } 319 | else { 320 | itemRec.id = result.insertId; //7/12/22 by DW 321 | itemRec.guid = config.urlFeedlandApp + "?item=" + itemRec.id; //11/10/22 by DW 322 | database.saveItem (itemRec, function () { //save again so guid makes it into the database -- 7/12/22 by DW 323 | addFeedIfNecessary (screenname, function (err, feedRec) { 324 | database.subscribeToFeed (screenname, feedUrl, function (err, feedRec) { //11/5/22 by DW 325 | buildRss (screenname, function (err, feedRec) { 326 | if (err) { //11/9/22 by DW 327 | console.log ("newPost: err.message == " + err.message); 328 | callback (err); 329 | } 330 | else { 331 | feedRec.whenUpdated = whenstart; 332 | var jstruct = { 333 | item: database.convertDatabaseItem (itemRec), 334 | theFeed: database.convertDatabaseFeed (feedRec) 335 | } 336 | database.updateSocketSubscribers ("newItem", jstruct); 337 | database.saveFeed (feedRec, function () { 338 | callback (undefined, database.convertDatabaseItem (itemRec)); 339 | }); 340 | } 341 | }); 342 | }); 343 | }); 344 | }); 345 | } 346 | }); 347 | }); 348 | } 349 | function updatePost (jsontext, screenname, callback) { 350 | var feedUrl = getBlogUrl (screenname), itemRecFromClient, whenstart = new Date (); 351 | try { 352 | itemRecFromClient = JSON.parse (jsontext); 353 | } 354 | catch (err) { 355 | callback (err); 356 | return; 357 | } 358 | if (itemRecFromClient.markdowntext !== undefined) { //5/5/22 by DW 359 | itemRecFromClient.description = markdownProcess (itemRecFromClient.markdowntext, true); 360 | } 361 | database.isItemInDatabase (feedUrl, itemRecFromClient.guid, function (flThere, itemRec) { 362 | if (flThere) { 363 | for (var x in itemRecFromClient) { //4/2/23 by DW 364 | itemRec [x] = itemRecFromClient [x]; 365 | } 366 | if (itemRec.title !== undefined) { //4/25/23 by DW 367 | if (itemRecFromClient.title === undefined) { //it was deleted by the user 368 | delete itemRec.title; 369 | } 370 | } 371 | checkEnclosure (itemRec, function (err) { //4/3/23 by DW 372 | database.saveItem (itemRec, function (err, feedRec) { 373 | if (err) { 374 | callback (err); 375 | } 376 | else { 377 | buildRss (screenname, function (err, feedRec) { 378 | var jstruct = { 379 | item: database.convertDatabaseItem (itemRec), 380 | theFeed: database.convertDatabaseFeed (feedRec) 381 | } 382 | database.updateSocketSubscribers ("updatedItem", jstruct); 383 | callback (undefined, database.convertDatabaseItem (itemRec)); 384 | }); 385 | } 386 | }); 387 | }); 388 | } 389 | else { 390 | callback ({message: "Can't update the item because it isn't in the database."}); 391 | } 392 | }); 393 | } 394 | function start (options, callback) { 395 | if (options !== undefined) { 396 | for (var x in options) { 397 | config [x] = options [x]; 398 | } 399 | } 400 | if (callback !== undefined) { 401 | callback (undefined); 402 | } 403 | } 404 | -------------------------------------------------------------------------------- /worknotes.md: -------------------------------------------------------------------------------- 1 | #### 10/31/25; 7:30:53 AM by DW -- 0.7.5 2 | 3 | New version of wpIdentity, 0.5.27. 4 | 5 | And new version of daveappserver, which requires wpIdentity too. 6 | 7 | #### 10/26/25; 12:04:25 PM by DW -- v0.7.1 8 | 9 | No changes in this version of feedlandserver. 10 | 11 | New version of feedlanddatabase is v0.8.3. 12 | 13 | getRiver has been completely re-coded, with a new join-based query that we expect will be much faster on systems with large items tables. 14 | 15 | https://github.com/scripting/feedlandInstall/issues/69 16 | 17 | #### 9/5/25; 9:49:43 AM by DW -- v0.7.0 18 | 19 | No changes in this version of feedlandserver. 20 | 21 | We're releasing a new version of feedlanddatabase with support for a new metadata JSON column in the items table. 22 | 23 | The version of feedlanddatabase is v0.8.2. 24 | 25 | Also had to release a new version of wpidentity, v0.5.25, the last public version was 0.5.24. 26 | 27 | Note, I have a private way of updating apps that doesn't go through NPM so I can test new versions in deployed apps without risking breaking other people's apps. 28 | 29 | #### 8/22/25; 12:35:47 PM by DW 30 | 31 | Updated the wpidentity version in package.json to the current one. 32 | 33 | Added sqllog as a dependency. wpidentity needs it. 34 | 35 | #### 5/21/25; 11:25:27 AM by DW 36 | 37 | Added a little debugging code to websocket notification of new items. 38 | 39 | #### 3/23/24; 7:37:28 PM by DW 40 | 41 | New endpoint: /getuserinfowithwordpresstoken. 42 | 43 | #### 3/17/24; 10:24:26 AM by DW 44 | 45 | Until now the most frequently we could check a feed was once a second. 46 | 47 | Now we can do it more frequently. 48 | 49 | See startFeedChecker. 50 | 51 | #### 3/4/24; 5:07:45 PM by DW 52 | 53 | Changed the default of flCanUseFeedIds to false. We really don't have river-building with feedId's debugged, so it should defauit false. My advice would be for everyone to turn it off, so I turned it off for them. 54 | 55 | #### 2/26/24; 11:09:49 AM by DW 56 | 57 | New config setting -- config.httpRequestTimeoutSecs, defaults to 1. 58 | 59 | It's a setting for the reallySimple package that does all our feed reading. 60 | 61 | Down there the default value is 10. 62 | 63 | Really any feed that can't be read in 1 second is trouble. 64 | 65 | I've wanted to fix this for a long time. 66 | 67 | #### 2/1/24; 11:18:11 AM by DW 68 | 69 | If you want a config value to make it to config in database, it must be set brefore we call database.start. 70 | 71 | We were doing it in the wrong order for config.mysqlVersion and config.flFeedsHaveIds. 72 | 73 | Also changed the initial value for config.flFeedsHaveIds to undefined. If it had this value, I would have found the problem yesterday instead of today. 74 | 75 | #### 1/31/24; 10:33:51 AM by DW 76 | 77 | Beginning an upgrade to feedland.org and feedland.com to use feedId in addition to feedUrl in the feeds and subscriptions tables. 78 | 79 | The goal is to optimize building of rivers, and possibly other things in the future. 80 | 81 | First change is adding a new value to config -- flFeedsHaveIds. This is initialized at startup, by seeing if it's an error to ask about feedId in the feeds table. 82 | 83 | #### 1/24/24; 9:02:43 AM by DW 84 | 85 | The call to render a news product now checks if the spec parameter is JSON, and if it is, that's the spec. Otherwise we assume, as before, that it's the URL of the spec, and we read it. 86 | 87 | Look in renderUserNewsproductWithTemplate. 88 | 89 | #### 1/9/24; 1:47:49 PM by DW 90 | 91 | Fixed a couple of problems with productNameForDisplay for news products. 92 | 93 | #### 12/28/23; 10:33:38 AM by DW 94 | 95 | A new way of specifying a news product, using a JSON file in place of OPML. 96 | 97 | Biggest changes in `renderUserNewsproductWithTemplate`. 98 | 99 | #### 12/24/23; 10:08:48 AM by DW 100 | 101 | In /newsproduct call, allow the caller to specify the URL of the source of the newsproduct app. 102 | 103 | This makes it easy for me to test a new version of the app without disturbing the previous version. 104 | 105 | https://feedland.com/newsproduct?template=&app= 106 | 107 | This is only for the templated version, keep everything simple for product specified by the username. 108 | 109 | #### 12/1/23; 12:40:11 PM by DW 110 | 111 | Clean out twitter and facebook metadata. 112 | 113 | #### 11/28/23; 12:30:56 PM by DW 114 | 115 | We no longer generate source:account feed-level for twitter. 116 | 117 | #### 11/22/23; 11:46:16 AM by DW 118 | 119 | New config value, rssCloudNotifyDomain. 120 | 121 | If it's provided we use that in getRssCloudOptions, in place of config.myDomain. 122 | 123 | #### 11/18/23; 12:14:15 PM by DW 124 | 125 | At startup set the value of config.mysqlVersion, available to the client app via the pagetable. 126 | 127 | #### 11/11/23; 1:06:07 PM by DW 128 | 129 | Respect config.flEnableSupervisorMode. 130 | 131 | #### 10/26/23; 10:38:53 AM by DW -- v0.6.7 132 | 133 | Reading lists in all their glory! 134 | 135 | I had to build a whole new layer in the database. The feeling was we couldn't build newsproducts without having the ability to have subscriptions handled offsite, owned by the user, or by an org that creates such lists. We're starting the first one, called FeedCorps, to prime the pump for the bootstrap. I want to create associations of news flows, I think this is how we create new ecosystems for blogging and journalism. 136 | 137 | Most of the new stuff is in feedlanddatabase. 138 | 139 | Also, just had to fix SQL-based notification which went in last month. There was a serious error in the implementation that didn't show up until reading lists came along. See the big comment at the head of notifySocketSubscribersFromSql. 140 | 141 | #### 10/12/23; 8:56:07 AM by DW 142 | 143 | Support for reading lists. 144 | 145 | #### 10/4/23; 12:12:21 PM by DW 146 | 147 | We have to call notifySocketSubscribersFromSql even if config.flWebsocketEnabled is false because we're using that mechanism to clear the river cache. So we check config.flWebsocketEnabled where we're sending the message. 148 | 149 | #### 10/1/23; 10:34:40 AM by DW -- v0.5.97 150 | 151 | Add a callback to notifySocketSubscribersFromSql that's called as we notify user's machines that there's a new item so we can clear the river cache accordingly. Previously we had turned off river caching. I want it back on. 152 | 153 | Commented the console.log that emits the id of each new item. 154 | 155 | #### 9/27/23; 6:46:23 PM by DW -- v0.5.96 156 | 157 | config.flWriteRssFilesLocally is new, default true. 158 | 159 | if true we write out RSS files to the local hard drive. 160 | 161 | set it false obviously if your system doesn't have the ability to write to the local drive. 162 | 163 | #### 9/26/23; 12:08:03 PM by DW 164 | 165 | New config settings 166 | 167 | * config.flUseSqlForSockets = false and config.minSecsBetwSqlSocketChecks = 5 168 | 169 | * config.logMinSecs = 5, config.logMaxResults = 1000 170 | 171 | If flUseSqlForSockets, we find out what the new items are by doing an SQL query, and then sending webocket messages to the users we're connected to. If you're running FeedLand in a multi-instance environment, you have to do it this way. All the servers are scanning feeds and receiving rssCloud pings all the time, the only place where all the info is, is in the database. 172 | 173 | In logSqlCalls turned off call to console.trace. Wasn't generating any useful info. 174 | 175 | #### 9/21/23; 6:24:11 PM by DW 176 | 177 | Wrote a callback for the new davesql logCallback, we watch for calls that took 5 seconds or more or had 1000 or more results. Obviously these numbers need to be configurable. First I want to get a feel for how it works, and later will add a stack trace, so we can see definitively who's making the call in question. 178 | 179 | #### 9/21/23; 12:04:07 PM by DW 180 | 181 | Added feedlandVersion to the pagetable when we build the home page. 182 | 183 | #### 9/20/23; 10:39:13 AM by DW 184 | 185 | Static file storage in an SQL table. 186 | 187 | Added new callback facility in daveappserver, we install our callbacks if config.flStaticFilesInSql is true. 188 | 189 | * getStaticFileInSql 190 | 191 | * publishStaticFileInSql 192 | 193 | #### 9/17/23; 9:18:00 AM by DW 194 | 195 | Did more factoring and now newsproducts support is entirely within feedland.js, and all the code is under a single call -- /newsproduct. 196 | 197 | If a request comes in for /newsproduct with a username parameter, we render the news product specified by the prefs of the indicated user. 198 | 199 | something like https://feedland.org/newsproduct?username=bullmancuso 200 | 201 | If a request comes in for /newsproduct with a template parameter, we get the outline it points to, and set pagetable values accordingly 202 | 203 | something like https://feedland.org/newsproduct?template=http://scripting.com/publicfolder/feedland/products/mlbriver.opml 204 | 205 | In both cases the client is specified by config.urlNewsProductSource, and for now the default should be fine. 206 | 207 | #### 9/16/23; 9:26:30 AM by DW 208 | 209 | Moved the News Product server functionality into FeedLand server. 210 | 211 | New setting: config.urlNewsProductSource. The default value should be acceptable for now, so nothing to add to config.json. 212 | 213 | If a request comes in for "/newsproduct we render the news product specified by the username parameter 214 | 215 | something like http://my.feedland.org/bullmancuso 216 | 217 | If a request comes in with a template parameter, we get the outline, and set pagetable values accordingly 218 | 219 | something like http://feedland.org/?template=http://scripting.com/publicfolder/feedland/products/mlbriver.opml 220 | 221 | The same client app handles both forms of news product. The name of the project is RiverClient, and we should talk about it more. :-) 222 | 223 | #### 8/18/23; 12:28:15 PM by DW 224 | 225 | Started adding back console.log calls. 226 | 227 | handleRssCloudPing 228 | 229 | #### 7/26/23; 2:36:40 PM by DW 230 | 231 | Changed the dependencies in package.json to reference specific versions of the packages I authored using the caret, to basically always ask for the newest version available in that major version. 232 | 233 | "daveappserver": "0.6.24", 234 | 235 | "davefeedread": "0.5.25", 236 | 237 | "davefilesystem": "0.4.5", 238 | 239 | "davegithub": "0.5.4", 240 | 241 | "davehttp": "0.5.1", 242 | 243 | "davemail": "0.5.4", 244 | 245 | "daverss": "0.6.2", 246 | 247 | "daves3": "0.4.11", 248 | 249 | "davesql": "0.4.25", 250 | 251 | "davetwitter": "0.6.39", 252 | 253 | "daveutils": "0.4.64", 254 | 255 | "davezip": "0.4.4", 256 | 257 | "feedland": "0.5.60", 258 | 259 | "feedlanddatabase": "0.6.14", 260 | 261 | "foldertojson": "0.4.7", 262 | 263 | "opml": "0.5.1", 264 | 265 | "opmltojs": "0.4.12", 266 | 267 | "reallysimple": "0.4.24", 268 | 269 | #### 7/25/23; 10:57:10 AM by DW 270 | 271 | Changed the URL for config.urlStarterFeeds to begin with https:// because the read is flowing through a proxy server. 272 | 273 | #### 7/5/23; 9:44:42 AM by DW 274 | 275 | Revised what is returned from the /getserverconfig call. 276 | 277 | we need to know the user's blog url, don't need to know the path to the blog folder on the static server. not sure why that was there. 278 | 279 | see comment at head of the routine. 280 | 281 | #### 6/14/23; 10:01:47 AM by DW 282 | 283 | Added a new POST request to save the users prefs -- "/sendprefs". There was a GET request that did the same and it's still there to prevent breakage with feedlandHome. The new way is supported in the feedlandPlatform code. At some point feedlandHome will be a platform product. 284 | 285 | #### 5/17/23; 4:47:30 PM by DW -- v0.5.40 286 | 287 | Fixed a bug where FeedLand would always request rssCloud notifications at feedland.org, even if the software is running on a different server. 288 | 289 | In getRssCloudOptions, we get the domain and port from the appserver-level config.myDomain value. 290 | 291 | #### 5/8/23 by DW 292 | 293 | New entrypoint -- /getserverconfig 294 | 295 | For apps that are not served by daveappserver. 296 | 297 | #### 4/25/23; 4:17:27 PM by DW -- v0.5.30 298 | 299 | fixed a bug in blog.js -- if you update an item to remove its title, the title wasn't getting removed in the database 300 | 301 | it now works properly. 302 | 303 | /getriver is also called to get a river if all we have is the URL of a feed. 304 | 305 | now it works properly in that case 306 | 307 | #### 4/25/23; 2:06:28 PM by DW -- v0.5.27 308 | 309 | Use the new database.getRiverFromScreenname routine. 310 | 311 | See this thread for an explanation of why this was needed. 312 | 313 | https://github.com/scripting/feedlandInstall/issues/31 314 | 315 | #### 4/5/23; 11:07:47 AM by DW 316 | 317 | In blog.js, if an item already has a link, don't invent one for it. This makes linkblogging work. Previously there was no way to set the link in the user interface. 318 | 319 | #### 4/2/23; 5:35:21 PM by DW 320 | 321 | Changes in blog.js to support saving of title, link and enclosureUrl from the editor. 322 | 323 | #### 3/29/23; 9:40:08 AM by DW 324 | 325 | New setting, config.flNightlyBackup. Default false. 326 | 327 | If true we backup the database at midnight local time. 328 | 329 | I needed this because it seems my database has gotten to large to do this in memory. It appears the feedland.js app crashes every night at midnight. 330 | 331 | This should have always been a config setting, imho. 332 | 333 | #### 3/13/23; 10:29:46 AM by DW 334 | 335 | Preparing to switch feedland.org over to email identity. 336 | 337 | #### 1/22/23; 10:40:58 AM by DW 338 | 339 | New config.json setting -- urlStarterFeeds. 340 | 341 | Moved emailtemplate.html to the feedlandInstall repo. 342 | 343 | #### 1/21/23; 11:24:35 AM by DW 344 | 345 | The idea of doing a new build every time there's a new element of config.json isn't going to work well in the future, because of the way we're including every item specially in the pagetable that the home page is rendered through. It would mean we couldn't add code that depends on a value from config.json unless every installation had already upgraded to the new version of the server software. That has been somewhat manageable with two instances, the public one and my test version, but it would be completely out of hand when there are N instances. 346 | 347 | So here's what we're going to do. 348 | 349 | * There will be a new element of the pagetable called config. 350 | 351 | * It will contain a copy of config, selected items. 352 | 353 | * I don't want the configuration for the database or github access, but I do want to know how various preferences are set. 354 | 355 | * See getConfigJson in feedland.js to see how it's done. 356 | 357 | Prior art: oldschoolblog, look for getConfigJson 358 | 359 | https://github.com/scripting/oldSchoolBlog/blob/master/oldschool.js 360 | 361 | #### 1/18/23; 11:18:17 AM by DW 362 | 363 | This is the NPM package known as "feedland." 364 | 365 | Getting ready for the open source release. 366 | 367 | daveappserver.js and database.js were being loaded from the lib folder, now we're going to use the NPM packages instead. 368 | 369 | i had them in the lib folder so I didn't need to publish the package each time i had to make a change 370 | 371 | but now we're distributing this to user machines, let's use the NPM process for updating as much as possible. 372 | 373 | #### 12/7/22; 7:09:38 PM by DW 374 | 375 | 376 | 377 | alter table users add column emailAddress text; 378 | 379 | alter table users add column emailSecret text; 380 | 381 | 382 | 383 | #### 11/6/22; 9:27:59 AM by DW 384 | 385 | New columns in the users table: 386 | 387 | alter table users add column myFeedTitle text; 388 | 389 | alter table users add column myFeedDescription text; 390 | 391 | #### 10/29/22; 10:42:40 AM by DW 392 | 393 | New setting, config.flRenewSubscriptions. I didn't want to screw with the rssCloud options, too deep on other stuff, needed this fixed quickly because my desktop server was renewing subscriptions, and that can never work. It's behind a firewall and can't receive pings. 394 | 395 | ### 9/23/22 by DW 396 | 397 | No longer backup databases when the server app starts. I just want the backup when the day changes. 398 | 399 | ### 9/13/22 by DW 400 | 401 | Previouslly we were using daveappserver via the lib folder, but it's mature enough to go through NPM, so I changed the require call here. 402 | 403 | ### 7/3/22 by DW 404 | 405 | Disabled publishFileCallback. See the comment at the head of the routine for details. 406 | 407 | -------------------------------------------------------------------------------- /docs/about.opml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Deeper into FeedLand 5 | Mon, 12 Sep 2022 12:32:27 GMT 6 | It's a community of feeds, news and people. 7 | http://docs.feedland.org/about.opml 8 | davewiner 9 | Dave Winer 10 | dave.winer@gmail.com 11 | wss://drummer.land/ 12 | Wed, 02 Aug 2023 22:13:20 GMT 13 | 1,7,9,17 14 | 16 15 | 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 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 2, June 1991 3 | 4 | Copyright (C) 1989, 1991 Free Software Foundation, Inc., 5 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 6 | Everyone is permitted to copy and distribute verbatim copies 7 | of this license document, but changing it is not allowed. 8 | 9 | Preamble 10 | 11 | The licenses for most software are designed to take away your 12 | freedom to share and change it. By contrast, the GNU General Public 13 | License is intended to guarantee your freedom to share and change free 14 | software--to make sure the software is free for all its users. This 15 | General Public License applies to most of the Free Software 16 | Foundation's software and to any other program whose authors commit to 17 | using it. (Some other Free Software Foundation software is covered by 18 | the GNU Lesser General Public License instead.) You can apply it to 19 | your programs, too. 20 | 21 | When we speak of free software, we are referring to freedom, not 22 | price. Our General Public Licenses are designed to make sure that you 23 | have the freedom to distribute copies of free software (and charge for 24 | this service if you wish), that you receive source code or can get it 25 | if you want it, that you can change the software or use pieces of it 26 | in new free programs; and that you know you can do these things. 27 | 28 | To protect your rights, we need to make restrictions that forbid 29 | anyone to deny you these rights or to ask you to surrender the rights. 30 | These restrictions translate to certain responsibilities for you if you 31 | distribute copies of the software, or if you modify it. 32 | 33 | For example, if you distribute copies of such a program, whether 34 | gratis or for a fee, you must give the recipients all the rights that 35 | you have. You must make sure that they, too, receive or can get the 36 | source code. And you must show them these terms so they know their 37 | rights. 38 | 39 | We protect your rights with two steps: (1) copyright the software, and 40 | (2) offer you this license which gives you legal permission to copy, 41 | distribute and/or modify the software. 42 | 43 | Also, for each author's protection and ours, we want to make certain 44 | that everyone understands that there is no warranty for this free 45 | software. If the software is modified by someone else and passed on, we 46 | want its recipients to know that what they have is not the original, so 47 | that any problems introduced by others will not reflect on the original 48 | authors' reputations. 49 | 50 | Finally, any free program is threatened constantly by software 51 | patents. We wish to avoid the danger that redistributors of a free 52 | program will individually obtain patent licenses, in effect making the 53 | program proprietary. To prevent this, we have made it clear that any 54 | patent must be licensed for everyone's free use or not licensed at all. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | GNU GENERAL PUBLIC LICENSE 60 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 61 | 62 | 0. This License applies to any program or other work which contains 63 | a notice placed by the copyright holder saying it may be distributed 64 | under the terms of this General Public License. The "Program", below, 65 | refers to any such program or work, and a "work based on the Program" 66 | means either the Program or any derivative work under copyright law: 67 | that is to say, a work containing the Program or a portion of it, 68 | either verbatim or with modifications and/or translated into another 69 | language. (Hereinafter, translation is included without limitation in 70 | the term "modification".) Each licensee is addressed as "you". 71 | 72 | Activities other than copying, distribution and modification are not 73 | covered by this License; they are outside its scope. The act of 74 | running the Program is not restricted, and the output from the Program 75 | is covered only if its contents constitute a work based on the 76 | Program (independent of having been made by running the Program). 77 | Whether that is true depends on what the Program does. 78 | 79 | 1. You may copy and distribute verbatim copies of the Program's 80 | source code as you receive it, in any medium, provided that you 81 | conspicuously and appropriately publish on each copy an appropriate 82 | copyright notice and disclaimer of warranty; keep intact all the 83 | notices that refer to this License and to the absence of any warranty; 84 | and give any other recipients of the Program a copy of this License 85 | along with the Program. 86 | 87 | You may charge a fee for the physical act of transferring a copy, and 88 | you may at your option offer warranty protection in exchange for a fee. 89 | 90 | 2. You may modify your copy or copies of the Program or any portion 91 | of it, thus forming a work based on the Program, and copy and 92 | distribute such modifications or work under the terms of Section 1 93 | above, provided that you also meet all of these conditions: 94 | 95 | a) You must cause the modified files to carry prominent notices 96 | stating that you changed the files and the date of any change. 97 | 98 | b) You must cause any work that you distribute or publish, that in 99 | whole or in part contains or is derived from the Program or any 100 | part thereof, to be licensed as a whole at no charge to all third 101 | parties under the terms of this License. 102 | 103 | c) If the modified program normally reads commands interactively 104 | when run, you must cause it, when started running for such 105 | interactive use in the most ordinary way, to print or display an 106 | announcement including an appropriate copyright notice and a 107 | notice that there is no warranty (or else, saying that you provide 108 | a warranty) and that users may redistribute the program under 109 | these conditions, and telling the user how to view a copy of this 110 | License. (Exception: if the Program itself is interactive but 111 | does not normally print such an announcement, your work based on 112 | the Program is not required to print an announcement.) 113 | 114 | These requirements apply to the modified work as a whole. If 115 | identifiable sections of that work are not derived from the Program, 116 | and can be reasonably considered independent and separate works in 117 | themselves, then this License, and its terms, do not apply to those 118 | sections when you distribute them as separate works. But when you 119 | distribute the same sections as part of a whole which is a work based 120 | on the Program, the distribution of the whole must be on the terms of 121 | this License, whose permissions for other licensees extend to the 122 | entire whole, and thus to each and every part regardless of who wrote it. 123 | 124 | Thus, it is not the intent of this section to claim rights or contest 125 | your rights to work written entirely by you; rather, the intent is to 126 | exercise the right to control the distribution of derivative or 127 | collective works based on the Program. 128 | 129 | In addition, mere aggregation of another work not based on the Program 130 | with the Program (or with a work based on the Program) on a volume of 131 | a storage or distribution medium does not bring the other work under 132 | the scope of this License. 133 | 134 | 3. You may copy and distribute the Program (or a work based on it, 135 | under Section 2) in object code or executable form under the terms of 136 | Sections 1 and 2 above provided that you also do one of the following: 137 | 138 | a) Accompany it with the complete corresponding machine-readable 139 | source code, which must be distributed under the terms of Sections 140 | 1 and 2 above on a medium customarily used for software interchange; or, 141 | 142 | b) Accompany it with a written offer, valid for at least three 143 | years, to give any third party, for a charge no more than your 144 | cost of physically performing source distribution, a complete 145 | machine-readable copy of the corresponding source code, to be 146 | distributed under the terms of Sections 1 and 2 above on a medium 147 | customarily used for software interchange; or, 148 | 149 | c) Accompany it with the information you received as to the offer 150 | to distribute corresponding source code. (This alternative is 151 | allowed only for noncommercial distribution and only if you 152 | received the program in object code or executable form with such 153 | an offer, in accord with Subsection b above.) 154 | 155 | The source code for a work means the preferred form of the work for 156 | making modifications to it. For an executable work, complete source 157 | code means all the source code for all modules it contains, plus any 158 | associated interface definition files, plus the scripts used to 159 | control compilation and installation of the executable. However, as a 160 | special exception, the source code distributed need not include 161 | anything that is normally distributed (in either source or binary 162 | form) with the major components (compiler, kernel, and so on) of the 163 | operating system on which the executable runs, unless that component 164 | itself accompanies the executable. 165 | 166 | If distribution of executable or object code is made by offering 167 | access to copy from a designated place, then offering equivalent 168 | access to copy the source code from the same place counts as 169 | distribution of the source code, even though third parties are not 170 | compelled to copy the source along with the object code. 171 | 172 | 4. You may not copy, modify, sublicense, or distribute the Program 173 | except as expressly provided under this License. Any attempt 174 | otherwise to copy, modify, sublicense or distribute the Program is 175 | void, and will automatically terminate your rights under this License. 176 | However, parties who have received copies, or rights, from you under 177 | this License will not have their licenses terminated so long as such 178 | parties remain in full compliance. 179 | 180 | 5. You are not required to accept this License, since you have not 181 | signed it. However, nothing else grants you permission to modify or 182 | distribute the Program or its derivative works. These actions are 183 | prohibited by law if you do not accept this License. Therefore, by 184 | modifying or distributing the Program (or any work based on the 185 | Program), you indicate your acceptance of this License to do so, and 186 | all its terms and conditions for copying, distributing or modifying 187 | the Program or works based on it. 188 | 189 | 6. Each time you redistribute the Program (or any work based on the 190 | Program), the recipient automatically receives a license from the 191 | original licensor to copy, distribute or modify the Program subject to 192 | these terms and conditions. You may not impose any further 193 | restrictions on the recipients' exercise of the rights granted herein. 194 | You are not responsible for enforcing compliance by third parties to 195 | this License. 196 | 197 | 7. If, as a consequence of a court judgment or allegation of patent 198 | infringement or for any other reason (not limited to patent issues), 199 | conditions are imposed on you (whether by court order, agreement or 200 | otherwise) that contradict the conditions of this License, they do not 201 | excuse you from the conditions of this License. If you cannot 202 | distribute so as to satisfy simultaneously your obligations under this 203 | License and any other pertinent obligations, then as a consequence you 204 | may not distribute the Program at all. For example, if a patent 205 | license would not permit royalty-free redistribution of the Program by 206 | all those who receive copies directly or indirectly through you, then 207 | the only way you could satisfy both it and this License would be to 208 | refrain entirely from distribution of the Program. 209 | 210 | If any portion of this section is held invalid or unenforceable under 211 | any particular circumstance, the balance of the section is intended to 212 | apply and the section as a whole is intended to apply in other 213 | circumstances. 214 | 215 | It is not the purpose of this section to induce you to infringe any 216 | patents or other property right claims or to contest validity of any 217 | such claims; this section has the sole purpose of protecting the 218 | integrity of the free software distribution system, which is 219 | implemented by public license practices. Many people have made 220 | generous contributions to the wide range of software distributed 221 | through that system in reliance on consistent application of that 222 | system; it is up to the author/donor to decide if he or she is willing 223 | to distribute software through any other system and a licensee cannot 224 | impose that choice. 225 | 226 | This section is intended to make thoroughly clear what is believed to 227 | be a consequence of the rest of this License. 228 | 229 | 8. If the distribution and/or use of the Program is restricted in 230 | certain countries either by patents or by copyrighted interfaces, the 231 | original copyright holder who places the Program under this License 232 | may add an explicit geographical distribution limitation excluding 233 | those countries, so that distribution is permitted only in or among 234 | countries not thus excluded. In such case, this License incorporates 235 | the limitation as if written in the body of this License. 236 | 237 | 9. The Free Software Foundation may publish revised and/or new versions 238 | of the General Public License from time to time. Such new versions will 239 | be similar in spirit to the present version, but may differ in detail to 240 | address new problems or concerns. 241 | 242 | Each version is given a distinguishing version number. If the Program 243 | specifies a version number of this License which applies to it and "any 244 | later version", you have the option of following the terms and conditions 245 | either of that version or of any later version published by the Free 246 | Software Foundation. If the Program does not specify a version number of 247 | this License, you may choose any version ever published by the Free Software 248 | Foundation. 249 | 250 | 10. If you wish to incorporate parts of the Program into other free 251 | programs whose distribution conditions are different, write to the author 252 | to ask for permission. For software which is copyrighted by the Free 253 | Software Foundation, write to the Free Software Foundation; we sometimes 254 | make exceptions for this. Our decision will be guided by the two goals 255 | of preserving the free status of all derivatives of our free software and 256 | of promoting the sharing and reuse of software generally. 257 | 258 | NO WARRANTY 259 | 260 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY 261 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN 262 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES 263 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED 264 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 265 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS 266 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE 267 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, 268 | REPAIR OR CORRECTION. 269 | 270 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 271 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR 272 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, 273 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING 274 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED 275 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY 276 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER 277 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE 278 | POSSIBILITY OF SUCH DAMAGES. 279 | 280 | END OF TERMS AND CONDITIONS 281 | 282 | How to Apply These Terms to Your New Programs 283 | 284 | If you develop a new program, and you want it to be of the greatest 285 | possible use to the public, the best way to achieve this is to make it 286 | free software which everyone can redistribute and change under these terms. 287 | 288 | To do so, attach the following notices to the program. It is safest 289 | to attach them to the start of each source file to most effectively 290 | convey the exclusion of warranty; and each file should have at least 291 | the "copyright" line and a pointer to where the full notice is found. 292 | 293 | 294 | Copyright (C) 295 | 296 | This program is free software; you can redistribute it and/or modify 297 | it under the terms of the GNU General Public License as published by 298 | the Free Software Foundation; either version 2 of the License, or 299 | (at your option) any later version. 300 | 301 | This program is distributed in the hope that it will be useful, 302 | but WITHOUT ANY WARRANTY; without even the implied warranty of 303 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 304 | GNU General Public License for more details. 305 | 306 | You should have received a copy of the GNU General Public License along 307 | with this program; if not, write to the Free Software Foundation, Inc., 308 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 309 | 310 | Also add information on how to contact you by electronic and paper mail. 311 | 312 | If the program is interactive, make it output a short notice like this 313 | when it starts in an interactive mode: 314 | 315 | Gnomovision version 69, Copyright (C) year name of author 316 | Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 317 | This is free software, and you are welcome to redistribute it 318 | under certain conditions; type `show c' for details. 319 | 320 | The hypothetical commands `show w' and `show c' should show the appropriate 321 | parts of the General Public License. Of course, the commands you use may 322 | be called something other than `show w' and `show c'; they could even be 323 | mouse-clicks or menu items--whatever suits your program. 324 | 325 | You should also get your employer (if you work as a programmer) or your 326 | school, if any, to sign a "copyright disclaimer" for the program, if 327 | necessary. Here is a sample; alter the names: 328 | 329 | Yoyodyne, Inc., hereby disclaims all copyright interest in the program 330 | `Gnomovision' (which makes passes at compilers) written by James Hacker. 331 | 332 | , 1 April 1989 333 | Ty Coon, President of Vice 334 | 335 | This General Public License does not permit incorporating your program into 336 | proprietary programs. If your program is a subroutine library, you may 337 | consider it more useful to permit linking proprietary applications with the 338 | library. If this is what you want to do, use the GNU Lesser General 339 | Public License instead of this License. 340 | -------------------------------------------------------------------------------- /feedland.js: -------------------------------------------------------------------------------- 1 | const myVersion = "0.7.18", myProductName = "feedland"; 2 | 3 | exports.start = start; //1/18/23 by DW 4 | 5 | const fs = require ("fs"); 6 | const request = require ("request"); 7 | const utils = require ("daveutils"); 8 | const davetwitter = require ("davetwitter"); 9 | const daveappserver = require ("daveappserver"); 10 | const davesql = require ("davesql"); 11 | const opml = require ("opml"); 12 | const database = require ("feedlanddatabase"); 13 | const reallysimple = require ("reallysimple"); 14 | const blog = require ("./blog.js"); 15 | const process = require ("process"); //9/14/22 by DW 16 | const qs = require ("querystring"); //10/9/22 by DW 17 | const wordpress = require ("wpidentity"); //3/23/24 by DW 18 | 19 | 20 | var config = { 21 | urlForFeeds: "http://data.feedland.org/feeds/", //11/5/22 by DW 22 | s3PathForFeeds: "/data.feedland.org/feeds/", 23 | 24 | flWriteRssFilesLocally: true, //9/27/23 by DW 25 | localRssPath: "data/feeds/", 26 | generatorForFeed: myProductName + " v" + myVersion, 27 | docsForFeed: "https://cyber.harvard.edu/rss/rss.html", 28 | maxFeedItems: 50, 29 | minSecsBetwFeedChecks: 10, 30 | flFeedChecksEnabled: true, //7/23/22 by DW 31 | flSocketServicesEnabled: true, //6/29/22 by DW 32 | getUserOpmlSubscriptions: database.getUserOpmlSubscriptions, //6/27/22 by DW -- callbacks for feedlanddatabase 33 | getStats: daveappserver.getStats, //6/27/22 by DW 34 | notifySocketSubscribers, //6/27/22 by DW 35 | saveStats: daveappserver.saveStats, //6/27/22 by DW 36 | writeWholeFile: daveappserver.writeWholeFile, //6/27/22 by DW 37 | writeWholeFile: daveappserver.writeWholeFile, //6/27/22 by DW 38 | readWholeFile: daveappserver.readWholeFile, //7/3/22 by DW 39 | 40 | buildUsersFeed: blog.buidUsersFeed, //11/6/22 by DW 41 | 42 | rssCloud: { //10/9/22 by DW 43 | enabled: true, 44 | flRequestNotify: true, 45 | minSecsBetwRenews: 60, //how often we look for a feed that's ready to renew 46 | ctSecsBetwRenews: 23 * 60 * 60, //for an individual feed 47 | feedUpdatedCallback: "/feedupdated" 48 | }, 49 | flRenewSubscriptions: true, //10/29/22 by DW 50 | urlFeedlandApp: "https://feedland.org/", //11/10/22 by DW 51 | 52 | flBackupOnStartup: false, //1/9/23 by DW 53 | flNightlyBackup: false, //3/29/23 by DW 54 | 55 | flNewsProducts: false, //1/20/23 by DW 56 | flUserFeeds: false, 57 | flLikesFeeds: false, 58 | 59 | urlStarterFeeds: "https://s3.amazonaws.com/scripting.com/publicfolder/feedland/subscriptionLists/starterfeeds.opml", //2/15/23 by DW 60 | 61 | urlNewsProductSource: "http://scripting.com/code/riverclient/index.html", //9/15/23 by DW 62 | flStaticFilesInSql: false, //9/20/23 by DW 63 | 64 | flUseSqlForSockets: false, //9/26/23 by DW 65 | minSecsBetwSqlSocketChecks: 5, //9/26/23 by DW 66 | 67 | logMinSecs: 5, //9/26/23 by DW 68 | logMaxResults: 1000, //9/26/23 by DW 69 | 70 | flUseReadingLists: true, //10/10/23 by DW 71 | minSecsBetwReadingListChecks: 60, //10/10/23 by DW 72 | minSecsBetwIndividualReadingListCheck: 5 * 60, //10/10/23 by DW 73 | 74 | flWordPressIdentityDefault: false, //11/13/23 by DW 75 | flIncludeImageMetadata: false, //12/1/23 by DW 76 | flFeedsHaveIds: undefined, //1/31/24 by DW 77 | httpRequestTimeoutSecs: 1, //2/26/24 by DW 78 | flCanUseFeedIds: false, //2/26/24 by DW & 3/4/24 by DW 79 | 80 | urlImageForMetadata: "http://scripting.com/images/2022/10/20/someoneElsesFeedList.png", 81 | metaDescription: "The first full feed management system. Share lists of feeds with other users, both in and outside of FeedLand. Writing feeds, reading news.", 82 | 83 | membershipClosedHeadline: "We're not signing up new users yet.", //5/27/24 by DW 84 | membershipClosedExplanation: "We're building a new FeedLand capable of serving even more news and users." 85 | }; 86 | 87 | var whenLastDayRollover = new Date (); 88 | var whenLastFeedCheck = new Date (); 89 | var whenLastCloudRenew = new Date (); 90 | var emailCache = new Object (); //12/14/22 by DW 91 | var idLastNewItem = undefined; //9/26/23 by DW 92 | var whenLastSqlSocketCheck = new Date (); //9/26/23 by DW 93 | var whenLastReadingListCheck = new Date (); //10/10/23 by DW 94 | 95 | function readJsonFile (url, callback) { //12/28/23 by DW 96 | request (url, function (err, response, jsontext) { 97 | if (err) { 98 | callback (err); 99 | } 100 | else { 101 | if ((response.statusCode >= 200) && (response.statusCode <= 299)) { 102 | var jstruct, flgood = true; 103 | try { 104 | jstruct = JSON.parse (jsontext); 105 | } 106 | catch (err) { 107 | callback (err); 108 | flgood = false; 109 | } 110 | if (flgood) { 111 | callback (undefined, jstruct); 112 | } 113 | } 114 | else { 115 | const message = "HTTP error == " + response.statusCode; 116 | callback ({message}); 117 | } 118 | } 119 | }); 120 | } 121 | function getFeedList (callback) { //11/6/23 by DW 122 | const sqltext = "select feedUrl from feeds;"; 123 | davesql.runSqltext (sqltext, function (err, result) { 124 | if (err) { 125 | callback (err); 126 | } 127 | else { 128 | var theList = new Array (); 129 | result.forEach (function (item) { 130 | theList.push (utils.trimWhitespace (item.feedUrl)); 131 | }); 132 | callback (undefined, theList); 133 | } 134 | }); 135 | } 136 | 137 | function viewMemoryUsage () { //9/14/22 by DW 138 | var jstruct = process.memoryUsage (); 139 | var usage = { 140 | rss: utils.gigabyteString (jstruct.rss), 141 | heapTotal: utils.gigabyteString (jstruct.heapTotal), 142 | heapUsed: utils.gigabyteString (jstruct.heapUsed), 143 | external: utils.gigabyteString (jstruct.external) 144 | }; 145 | return (usage); 146 | } 147 | function notifySocketSubscribers (verb, payload, flPayloadIsString, callbackToQualify) { //6/29/22 by DW 148 | if (config.flSocketServicesEnabled) { 149 | daveappserver.notifySocketSubscribers (verb, payload, flPayloadIsString, callbackToQualify); 150 | } 151 | } 152 | 153 | function initLastNewItem (callback) { //9/26/23 by DW 154 | const sqltext = "select id from items order by id desc limit 1;"; 155 | davesql.runSqltext (sqltext, function (err, result) { 156 | if (err) { 157 | console.log ("initLastNewItem: " + err.message); 158 | if (callback !== undefined) { 159 | callback (); 160 | } 161 | } 162 | else { 163 | if (result.length == 0) { 164 | console.log ("initLastNewItem: error setting idLastNewItem."); 165 | if (callback !== undefined) { 166 | callback (); 167 | } 168 | } 169 | else { 170 | idLastNewItem = result [0].id; 171 | console.log ("initLastNewItem: idLastNewItem == " + idLastNewItem); 172 | if (callback !== undefined) { 173 | callback (); 174 | } 175 | } 176 | } 177 | }); 178 | } 179 | 180 | 181 | function notifySocketSubscribersFromSql (callback) { //9/26/23 by DW 182 | const sqltext = "select * from items where id > " + davesql.encode (idLastNewItem) + " order by id desc;", whenstart = new Date (); 183 | davesql.runSqltext (sqltext, function (err, result) { 184 | if (err) { 185 | console.log ("notifySocketSubscribersFromSql: " + err.message); 186 | } 187 | else { 188 | if (result.length > 0) { 189 | let feedRecs = new Object (); //if more than one item for a feed, we only have to get the feed data once 190 | 191 | var ids = ""; //5/21/25 by DW 192 | result.forEach (function (item) { 193 | if (ids.length > 0) { 194 | ids += ", "; 195 | } 196 | ids += item.id 197 | }); 198 | 199 | function doNext (ix) { 200 | if (ix < result.length) { 201 | const item = result [ix]; //the new feed item we're notifying about 202 | function sendmessage (feedRec) { 203 | let jstruct = { 204 | item: database.convertDatabaseItem (item), 205 | theFeed: feedRec 206 | } 207 | if (config.flWebsocketEnabled) { //10/4/23 by DW 208 | database.updateSocketSubscribers ("newItem", jstruct); 209 | } 210 | if (callback !== undefined) { //10/1/23 by DW 211 | callback (jstruct); 212 | } 213 | } 214 | if (feedRecs [item.feedUrl] === undefined) { 215 | database.getFeed (item.feedUrl, function (err, feedRec) { 216 | if (err) { 217 | console.log ("notifySocketSubscribersFromSql: err.message == " + err.message); 218 | } 219 | else { 220 | feedRecs [item.feedUrl] = feedRec; 221 | sendmessage (feedRec); 222 | } 223 | doNext (ix + 1); 224 | }); 225 | } 226 | else { 227 | sendmessage (feedRecs [item.feedUrl]); 228 | doNext (ix + 1); 229 | } 230 | } 231 | else { 232 | } 233 | } 234 | doNext (0); 235 | 236 | 237 | idLastNewItem = result [0].id; 238 | } 239 | } 240 | }); 241 | } 242 | 243 | function unsubList (screenname, urlArray, callback) { //6/28/22 by DW -- move into database code 244 | var returnArray = new Array (); 245 | function unsubnext (ix) { 246 | if (ix >= urlArray.length) { 247 | callback (undefined, returnArray); 248 | } 249 | else { 250 | database.deleteSubscription (screenname, urlArray [ix], function (err, data) { 251 | if (err) { 252 | returnArray.push (err); 253 | } 254 | else { 255 | returnArray.push (data); 256 | } 257 | unsubnext (ix + 1); 258 | }); 259 | } 260 | } 261 | unsubnext (0); 262 | } 263 | function notComment (item) { //return true if the outline element is not a comment 264 | return (!utils.getBoolean (item.isComment)); 265 | } 266 | function subscribeToOpml (screenname, opmltext, flDeleteEnabled, callback) { //6/30/22 by DW 267 | var sublist = new Array (); 268 | console.log ("subscribeToOpml: screenname == " + screenname + ", opmltext.length == " + opmltext.length); 269 | opml.parse (opmltext, function (err, theOutline) { 270 | if (err) { 271 | if (callback !== undefined) { 272 | callback (err); 273 | } 274 | } 275 | else { 276 | opml.expandIncludes (theOutline, function (theNewOutline) { 277 | opml.visitAll (theNewOutline, function (theNode) { 278 | if (notComment (theNode)) { 279 | if (theNode.type == "rss") { 280 | if (theNode.xmlUrl !== undefined) { 281 | console.log (theNode.xmlUrl); 282 | sublist.push (theNode); 283 | } 284 | } 285 | } 286 | return (true); 287 | }); 288 | console.log ("subscribeToOpml: sublist == " + utils.jsonStringify (sublist)); 289 | database.processSubscriptionList (screenname, sublist, flDeleteEnabled); 290 | if (callback !== undefined) { 291 | callback (undefined, sublist); 292 | } 293 | }); 294 | } 295 | }); 296 | } 297 | function publishFileCallback (f, screenname, relpath, type, flprivate, filetext, url) { 298 | } 299 | 300 | function addMacroToPagetable (pagetable) { 301 | function getConfigJson () { 302 | var theConfig = new Object (); 303 | function addvalue (name) { 304 | if (config [name] !== undefined) { 305 | theConfig [name] = config [name]; 306 | } 307 | } 308 | addvalue ("flWebsocketEnabled"); 309 | addvalue ("websocketPort"); 310 | addvalue ("myDomain"); 311 | addvalue ("mailSender"); 312 | addvalue ("confirmEmailSubject"); 313 | addvalue ("confirmationExpiresAfter"); 314 | addvalue ("flUseTwitterIdentity"); 315 | addvalue ("flEnableNewUsers"); 316 | addvalue ("flBackupOnStartup"); 317 | addvalue ("flNewsProducts"); 318 | addvalue ("flUserFeeds"); 319 | addvalue ("flLikesFeeds"); 320 | addvalue ("urlForFeeds"); 321 | addvalue ("s3PathForFeeds"); 322 | addvalue ("s3LikesPath"); 323 | addvalue ("urlNewsProducts"); 324 | addvalue ("maxRiverItems"); 325 | addvalue ("maxNewFeedSubscriptions"); 326 | addvalue ("flUpdateFeedsInBackground"); 327 | addvalue ("minSecsBetwFeedChecks"); 328 | addvalue ("productName"); 329 | addvalue ("productNameForDisplay"); 330 | addvalue ("urlServerHomePageSource"); 331 | addvalue ("urlStarterFeeds"); //1/22/23 by DW 332 | addvalue ("membershipClosedHeadline"); //5/27/24 by DW 333 | addvalue ("membershipClosedExplanation"); 334 | return (utils.jsonStringify (theConfig)); 335 | } 336 | pagetable.urlForFeeds = config.urlForFeeds; 337 | pagetable.flEnableNewUsers = config.flEnableNewUsers; //12/12/22 by DW 338 | pagetable.flUseTwitterIdentity = config.flUseTwitterIdentity; //1/10/23 by DW 339 | pagetable.urlNewsProducts = config.urlNewsProducts; //1/16/23 by DW 340 | pagetable.flNewsProducts = config.flNewsProducts; //1/20/23 by DW 341 | pagetable.flUserFeeds = config.flUserFeeds; //1/20/23 by DW 342 | pagetable.flLikesFeeds = config.flLikesFeeds; //1/20/23 by DW 343 | pagetable.configJson = getConfigJson (); //1/21/23 by DW 344 | pagetable.feedlandVersion = myVersion; //9/21/23 by DW 345 | pagetable.flWordPressIdentityDefault = config.flWordPressIdentityDefault; //11/13/23 by DW 346 | pagetable.mysqlVersion = config.mysqlVersion; //11/18/23 by DW 347 | pagetable.membershipClosedHeadline = config.membershipClosedHeadline; //5/27/24 by DW 348 | pagetable.membershipClosedExplanation = config.membershipClosedExplanation; 349 | database.addMacroToPagetable (pagetable); //12/1/23 by DW 350 | 351 | //12/2/22 by DW & 12/1/23 by DW -- set up the normal case for the Facebook/Twitter metadata 352 | pagetable.metaUrl = config.urlServerForClient; 353 | 354 | const siteName = (config.productNameForDisplay === undefined) ? "FeedLand" : config.productNameForDisplay; 355 | 356 | pagetable.metaTitle = siteName; 357 | 358 | pagetable.metaDescription = config.metaDescription; 359 | 360 | 361 | pagetable.metaTwitterOwnerName = ""; 362 | 363 | pagetable.metaSiteName = siteName; 364 | 365 | if (config.flIncludeImageMetadata) { //12/1/23 by DW 366 | const imgUrl = config.urlImageForMetadata; 367 | pagetable.facebookImage = ""; 368 | pagetable.twitterImage = ""; 369 | } 370 | else { 371 | pagetable.facebookImage = ""; 372 | pagetable.twitterImage = ""; 373 | } 374 | 375 | } 376 | function asyncAddMacroToPagetable (pagetable, theRequest, callback) { //12/2/22 by DW 377 | const maxDescChars = 300; 378 | const params = theRequest.params; 379 | function encode (s) { 380 | if (s == null) { 381 | return (""); 382 | } 383 | else { 384 | return (utils.encodeXml (s)); 385 | } 386 | } 387 | 388 | if (params === undefined) { 389 | callback (); 390 | } 391 | else { 392 | if (params.item === undefined) { 393 | callback (); 394 | } 395 | else { 396 | const sqltext = "select * from items where id = " + davesql.encode (params.item) + ";"; 397 | davesql.runSqltext (sqltext, function (err, result) { 398 | if (err) { 399 | callback (err); 400 | } 401 | else { 402 | if (result.length == 0) { 403 | callback (); 404 | } 405 | else { 406 | let itemRec = result [0]; 407 | console.log ("asyncAddMacroToPagetable: itemRec.title == " + itemRec.title); 408 | pagetable.metaTitle = encode (itemRec.title); 409 | 410 | let desc = utils.stripMarkup (itemRec.description); 411 | desc = utils.maxStringLength (desc, maxDescChars, true, true); 412 | pagetable.metaDescription = encode (desc); 413 | 414 | if (itemRec.link !== undefined) { 415 | pagetable.metaUrl = encode (itemRec.link); 416 | } 417 | 418 | pagetable.metaImageUrl = "http://scripting.com/images/2022/12/03/transparentSpace.png"; 419 | 420 | pagetable.facebookImage = ""; 421 | pagetable.twitterImage = ""; 422 | 423 | callback (); 424 | } 425 | } 426 | }); 427 | } 428 | } 429 | } 430 | 431 | function initNewsproductPagetable (pagetable) { 432 | pagetable.productname = config.productName; 433 | 434 | pagetable.productnameForDisplay = (config.productnameForDisplay === undefined) ? config.productName : config.productnameForDisplay; //1/9/24 by DW 435 | 436 | pagetable.version = myVersion; 437 | pagetable.urlServerForClient = config.urlServerForClient; 438 | pagetable.flEnableLogin = false; 439 | pagetable.urlWebsocketServerForClient = undefined; 440 | } 441 | function renderUserNewsproduct (screenname, callback) { //9/15/23 by DW 442 | function checkUndefined (val) { 443 | if (val === undefined) { 444 | return (""); 445 | } 446 | else { 447 | return (val); 448 | } 449 | } 450 | database.getUserPrefs (screenname, function (err, thePrefs) { 451 | if (err) { 452 | callback (err); 453 | } 454 | else { 455 | if (thePrefs.emailSecret !== undefined) { //1/17/23 by DW 456 | delete thePrefs.emailSecret; 457 | } 458 | const newsProductInfo = { 459 | screenname, 460 | categories: thePrefs.newsproductCategoryList, 461 | title: thePrefs.newsproductTitle, 462 | description: thePrefs.newsproductDescription, 463 | image: thePrefs.newsproductImage, 464 | style: thePrefs.newsproductStyle, 465 | script: thePrefs.newsproductScript 466 | } 467 | var pagetable = { 468 | screenname, 469 | 470 | userPrefs: utils.jsonStringify (thePrefs), 471 | 472 | pageTitle: checkUndefined (newsProductInfo.title), 473 | pageDescription: checkUndefined (newsProductInfo.description), 474 | pageImage: checkUndefined (newsProductInfo.image), 475 | 476 | prefsPath: undefined, 477 | docsPath: undefined, 478 | theOutlineInJson: undefined 479 | } 480 | 481 | initNewsproductPagetable (pagetable); 482 | request (config.urlNewsProductSource, function (err, response, templatetext) { 483 | if (err) { 484 | callback (err); 485 | } 486 | else { 487 | if ((response.statusCode >= 200) && (response.statusCode <= 299)) { 488 | const pagetext = utils.multipleReplaceAll (templatetext.toString (), pagetable, false, "[%", "%]"); 489 | callback (undefined, pagetext); 490 | } 491 | else { 492 | const message = "HTTP error == " + response.statusCode; 493 | callback ({message}); 494 | } 495 | } 496 | }); 497 | } 498 | }); 499 | return (true); 500 | } 501 | function renderUserNewsproductWithTemplate (urlOutlineTemplate, urlNewsProductApp, newsProductSpec, theRequest, callback) { //9/16/23 by DW 502 | const urlServerHomePageSource = (urlNewsProductApp === undefined) ? config.urlNewsProductSource : urlNewsProductApp; //12/24/23 by DW 503 | var pagetable = { 504 | urlTemplate: urlOutlineTemplate, 505 | newsProductInfo: "undefined", 506 | userPrefs: "undefined", 507 | urlServerForClient: config.urlServerForClient, 508 | urlServerHomePageSource //12/24/23 by DW 509 | }; 510 | initNewsproductPagetable (pagetable); 511 | function getOutlineTemplate (callback) { 512 | if (newsProductSpec === undefined) { 513 | opml.readOutline (urlOutlineTemplate, function (err, theOutline) { 514 | if (err) { 515 | callback (err); 516 | } 517 | else { 518 | function getHeadAtt (name) { 519 | var attval = ""; 520 | try { 521 | if (theOutline.opml.head [name] !== undefined) { 522 | attval = theOutline.opml.head [name]; 523 | } 524 | } 525 | catch (err) { 526 | } 527 | return (attval); 528 | } 529 | pagetable.pageTitle = getHeadAtt ("title"); 530 | pagetable.productnameForDisplay = pagetable.pageTitle; //1/9/24 by DW 531 | pagetable.pageDescription = getHeadAtt ("description"); 532 | 533 | var imageUrl = getHeadAtt ("image"); 534 | if (imageUrl.length != 0) { 535 | pagetable.pageImage = ""; 536 | } 537 | else { 538 | pagetable.pageImage = ""; 539 | } 540 | 541 | if (false) { //(config.flExpandIncludes) { //8/22/22 by DW 542 | opml.expandIncludes (theOutline, function (theNewOutline) { //8/11/22 by DW 543 | pagetable.theOutlineInJson = utils.jsonStringify (theNewOutline); 544 | callback (); 545 | }); 546 | } 547 | else { 548 | pagetable.theOutlineInJson = utils.jsonStringify (theOutline); 549 | pagetable.theNewsProductSpec = utils.jsonStringify (new Object ()); //12/28/23 by DW 550 | callback (undefined, theOutline); 551 | } 552 | } 553 | }); 554 | } 555 | else { //12/28/23 by DW 556 | var theSpec; 557 | function isParamJson (theText) { 558 | try { 559 | theText = decodeURIComponent (theText); 560 | theSpec = JSON.parse (theText); 561 | return (true); 562 | } 563 | catch (err) { 564 | return (false); 565 | } 566 | } 567 | function getStuffForPagetable (theSpec) { 568 | function getValue (name) { 569 | try { 570 | if (theSpec [name] === undefined) { 571 | return (""); 572 | } 573 | else { 574 | return (theSpec [name]); 575 | } 576 | } 577 | catch (err) { 578 | return (""); 579 | } 580 | } 581 | pagetable.pageTitle = getValue ("title"); 582 | pagetable.productnameForDisplay = pagetable.pageTitle; //1/9/24 by DW 583 | pagetable.pageDescription = getValue ("description"); 584 | 585 | var imageUrl = getValue ("image"); 586 | if (imageUrl.length != 0) { 587 | pagetable.pageImage = ""; 588 | } 589 | else { 590 | pagetable.pageImage = ""; 591 | } 592 | 593 | pagetable.theNewsProductSpec = utils.jsonStringify (theSpec); 594 | pagetable.theOutlineInJson = utils.jsonStringify (new Object ()); 595 | } 596 | 597 | if (isParamJson (newsProductSpec)) { //1/24/24 by DW 598 | getStuffForPagetable (theSpec); 599 | callback (undefined, theSpec); 600 | } 601 | else { 602 | readJsonFile (newsProductSpec, function (err, theSpec) { //1/24/24 by DW -- assume it's a URL 603 | if (err) { 604 | callback (err); 605 | } 606 | else { 607 | getStuffForPagetable (theSpec); //1/24/24 by DW 608 | callback (undefined, theSpec); 609 | } 610 | }); 611 | } 612 | } 613 | } 614 | function getHtmlTemplate (callback) { 615 | request (pagetable.urlServerHomePageSource, function (err, response, templatetext) { 616 | if (err) { 617 | callback (err); 618 | } 619 | else { 620 | if ((response.statusCode >= 200) && (response.statusCode <= 299)) { 621 | callback (undefined, templatetext); 622 | } 623 | else { 624 | const message = "HTTP error == " + response.statusCode; 625 | callback ({message}); 626 | } 627 | } 628 | }); 629 | } 630 | getOutlineTemplate (function (err, theOutline) { 631 | if (err) { 632 | callback (err); 633 | } 634 | else { 635 | getHtmlTemplate (function (err, htmltemplate) { 636 | if (err) { 637 | callback (err); 638 | } 639 | else { 640 | const pagetext = utils.multipleReplaceAll (htmltemplate.toString (), pagetable, false, "[%", "%]"); 641 | callback (undefined, pagetext); 642 | } 643 | }); 644 | } 645 | }); 646 | } 647 | 648 | function getStaticFileInSql (screenname, relpath, flprivate, callback) { //9/20/23 by DW 649 | const sqltext = "select * from staticfiles where screenname = " + davesql.encode (screenname) + " and relpath = " + davesql.encode (relpath) + " and flprivate = " + davesql.encode (flprivate) + ";"; 650 | console.log ("getStaticFileInSql: screenname == " + screenname + ", relpath == " + relpath); //9/21/23 by DW 651 | davesql.runSqltext (sqltext, function (err, result) { 652 | if (err) { 653 | callback (err); 654 | } 655 | else { 656 | if (result.length == 0) { 657 | const message = "Can't find the file " + relpath + " for the user " + screenname + "."; 658 | callback ({message}); 659 | } 660 | else { 661 | const theFileRec = result [0]; 662 | const theReturnedData = { 663 | filedata: theFileRec.filecontents.toString (), 664 | filestats: { 665 | relpath: theFileRec.relpath, 666 | type: theFileRec.type, 667 | screenname: theFileRec.screenname, 668 | flprivate: theFileRec.flprivate, 669 | ctSaves: theFileRec.ctSaves, 670 | whenCreated: theFileRec.whenCreated, 671 | whenUpdated: theFileRec.whenUpdated 672 | } 673 | }; 674 | callback (undefined, theReturnedData); 675 | } 676 | } 677 | }); 678 | } 679 | function publishStaticFileInSql (screenname, relpath, type, flprivate, filecontents, callback) { //9/20/23 by DW 680 | const now = new Date (); 681 | const fileRec = { 682 | screenname, 683 | relpath, 684 | type, 685 | flprivate, 686 | filecontents, 687 | whenCreated: now, 688 | whenUpdated: now, 689 | ctSaves: 1 690 | }; 691 | console.log ("publishStaticFileInSql: screenname == " + screenname + ", relpath == " + relpath); //9/21/23 by DW 692 | getStaticFileInSql (screenname, relpath, flprivate, function (err, theOriginalFile) { 693 | if (!err) { 694 | fileRec.whenCreated = theOriginalFile.filestats.whenCreated; 695 | fileRec.ctSaves = theOriginalFile.filestats.ctSaves + 1; 696 | } 697 | const sqltext = "replace into staticfiles " + davesql.encodeValues (fileRec) + ";"; 698 | davesql.runSqltext (sqltext, function (err, result) { 699 | if (err) { 700 | callback (err); 701 | } 702 | else { 703 | callback (undefined, fileRec); 704 | } 705 | }); 706 | }); 707 | } 708 | 709 | function addEmailToUserInDatabase (screenname, emailAddress, magicString, flNewUser, callback) { //12/7/22 by DW 710 | database.findUserWithScreenname (screenname, function (flInDatabase, userRec) { 711 | if (flNewUser) { //1/7/23 by DW 712 | if (flInDatabase) { 713 | const message = "Can't create the user \"" + screenname + "\" because there already is a user with that name." 714 | callback ({message}); 715 | } 716 | else { 717 | isEmailInDatabase (emailAddress, function (data) { 718 | if (data.flInDatabase) { 719 | const message = "Can't create the user \"" + emailAddress + "\" because there already is a user with that address." 720 | callback ({message}); 721 | } 722 | else { 723 | if (config.flEnableNewUsers) { 724 | const now = new Date (); 725 | const newUserRec = { 726 | screenname, 727 | emailAddress, 728 | emailSecret: utils.getRandomPassword (10), 729 | whenCreated: now, 730 | whenUpdated: now, 731 | ctStartups: 1, 732 | whenLastStartup: now 733 | }; 734 | const sqltext = "insert into users " + davesql.encodeValues (newUserRec); 735 | davesql.runSqltext (sqltext, function (err, result) { 736 | if (err) { 737 | callback (err); 738 | } 739 | else { 740 | callback (undefined, newUserRec.emailSecret); 741 | } 742 | }); 743 | } 744 | else { 745 | const message = "Can't create the user \"" + screenname + "\" because new users are not being accepted here at this time."; 746 | callback ({message}); 747 | } 748 | } 749 | }); 750 | } 751 | } 752 | else { 753 | var emailSecret = undefined; 754 | if (flInDatabase) { 755 | if (userRec.emailSecret != null) { 756 | emailSecret = userRec.emailSecret; 757 | } 758 | } 759 | if (emailSecret === undefined) { 760 | emailSecret = utils.getRandomPassword (10); 761 | function encode (s) { 762 | return (davesql.encode (s)); 763 | } 764 | const sqltext = "update users set emailAddress = " + encode (emailAddress) + ", emailSecret = " + encode (emailSecret) + " where screenname = " + encode (screenname) + ";"; 765 | davesql.runSqltext (sqltext, function (err, result) { 766 | if (callback !== undefined) { 767 | if (err) { 768 | callback (err); 769 | } 770 | else { 771 | callback (undefined, emailSecret); 772 | } 773 | } 774 | }); 775 | } 776 | else { 777 | callback (undefined, emailSecret); 778 | } 779 | } 780 | }); 781 | } 782 | function regenerateEmailSecret (screenname, callback) { 783 | const emailSecret = utils.getRandomPassword (10); 784 | const sqltext = "update users set emailSecret = " + davesql.encode (emailSecret) + " where screenname = " + davesql.encode (screenname) + ";"; 785 | davesql.runSqltext (sqltext, function (err, result) { 786 | if (callback !== undefined) { 787 | if (err) { 788 | callback (err); 789 | } 790 | else { 791 | callback (undefined, {emailSecret}); 792 | } 793 | } 794 | }); 795 | } 796 | 797 | function loginWordpressUser (accessToken, theUserInfo, callback) { //10/31/23 by DW 798 | function setWordpressAppData (userRec, callback) { 799 | var apps; 800 | if ((userRec.apps === undefined) || (userRec.apps == null)) { //11/4/23 by DW 801 | apps = new Object (); 802 | } 803 | else { 804 | apps = JSON.parse (userRec.apps); 805 | } 806 | var wordpress = (apps.wordpress === undefined) ? new Object () : apps.wordpress; 807 | wordpress.accessToken = accessToken; 808 | wordpress.userInfo = theUserInfo; 809 | apps.wordpress = wordpress; 810 | userRec.apps = apps; 811 | const sqltext = "replace into users " + davesql.encodeValues (userRec); 812 | davesql.runSqltext (sqltext, function (err, result) { 813 | if (err) { 814 | callback (err); 815 | } 816 | else { 817 | callback (undefined, userRec); 818 | } 819 | }); 820 | } 821 | database.findUserWithEmail (theUserInfo.email, function (flEmailUsed, userRec) { 822 | if (flEmailUsed) { 823 | setWordpressAppData (userRec, callback); //return the userRec 824 | } 825 | else { 826 | if (config.flEnableNewUsers) { 827 | const screenname = theUserInfo.username; 828 | database.findUserWithScreenname (screenname, function (flInDatabase, userRec) { 829 | if (flInDatabase) { 830 | const message = "Can't create the user \"" + screenname + "\" because there already is a user with that name."; 831 | callback ({message}); 832 | } 833 | else { 834 | const now = new Date (); 835 | const newUserRec = { 836 | screenname, 837 | emailAddress: theUserInfo.email, 838 | emailSecret: utils.getRandomPassword (10), 839 | whenCreated: now, 840 | whenUpdated: now, 841 | ctStartups: 1, 842 | whenLastStartup: now, 843 | apps: { 844 | wordpress: { 845 | accessToken 846 | } 847 | } 848 | }; 849 | const sqltext = "insert into users " + davesql.encodeValues (newUserRec); 850 | davesql.runSqltext (sqltext, function (err, result) { 851 | if (err) { 852 | callback (err); 853 | } 854 | else { 855 | callback (undefined, newUserRec); 856 | } 857 | }); 858 | } 859 | }); 860 | } 861 | else { 862 | const message = "Can't create the user \"" + screenname + "\" because new users are not being accepted here at this time."; 863 | callback ({message}); 864 | } 865 | } 866 | }); 867 | } 868 | 869 | function getUserInfoWithWordpressToken (token, callback) { //3/23/24 by DW 870 | wordpress.getUserInfo (token, function (err, theUserInfo) { //gets us the email address, among other things 871 | database.findUserWithEmail (theUserInfo.email, function (flEmailUsed, userRec) { 872 | if (flEmailUsed) { 873 | callback (undefined, userRec); 874 | } 875 | else { 876 | const message = "Can't find a user with email address \"" + theUserInfo.email + "\""; 877 | callback ({message}); 878 | } 879 | }); 880 | }); 881 | } 882 | 883 | function removeNullValuesFromObject (obj) { //9/26/22 by DW 884 | for (var x in obj) { 885 | if (obj [x] == null) { 886 | obj [x] = undefined; 887 | } 888 | } 889 | return (obj); 890 | } 891 | function getUserRecFromEmail (emailAddress, emailSecret, callback) { //12/13/22 by DW 892 | const sqltext = "select * from users where emailAddress = " + davesql.encode (emailAddress) + " and emailSecret = " + davesql.encode (emailSecret) + ";"; 893 | davesql.runSqltext (sqltext, function (err, result) { 894 | if (err) { 895 | callback (err); 896 | } 897 | else { 898 | if (result.length == 0) { 899 | callback ({"message": "There is no user with that email address, or the access code is incorrect."}); 900 | } 901 | else { 902 | callback (undefined, removeNullValuesFromObject (result [0])); 903 | } 904 | } 905 | }); 906 | } 907 | function isUserAdmin (emailaddress, callback) { //11/3/23 by DW 908 | database.findUserWithEmail (emailaddress, function (flUserExists, userRec) { 909 | if (flUserExists) { 910 | callback (userRec.role == "admin", userRec); 911 | } 912 | else { 913 | callback (false); 914 | } 915 | }); 916 | } 917 | 918 | function getScreenNameFromEmail (emailAddress, callback) { //1/10/23 by DW 919 | const sqltext = "select * from users where emailAddress = " + davesql.encode (emailAddress) + ";"; 920 | davesql.runSqltext (sqltext, function (err, result) { 921 | if (err) { 922 | callback (err); 923 | } 924 | else { 925 | if (result.length == 0) { 926 | callback ({"message": "There is no user with that email address."}); 927 | } 928 | else { 929 | callback (undefined, result [0].screenname); 930 | } 931 | } 932 | }); 933 | } 934 | function isEmailInDatabase (emailAddress, callback) { //1/12/23 by DW 935 | if (emailAddress === undefined) { 936 | callback ({flInDatabase: false}); 937 | } 938 | else { 939 | getScreenNameFromEmail (emailAddress, function (err, screenname) { 940 | callback ({flInDatabase: err === undefined}); //if there's no error it's in the database 941 | }); 942 | } 943 | } 944 | function getScreenname (params, callback) { //12/23/22 by DW 945 | function badloginCallback () { 946 | callback ({message: "Can't do the thing you want because the accessToken is not valid."}); 947 | } 948 | if (config.flUseTwitterIdentity) { 949 | davetwitter.getScreenName (params.oauth_token, params.oauth_token_secret, function (screenname) { 950 | if (screenname === undefined) { 951 | badloginCallback (); 952 | } 953 | else { 954 | callback (undefined, screenname); 955 | } 956 | }); 957 | } 958 | else { 959 | if ((params.emailaddress !== undefined) && (params.emailcode !== undefined)) { 960 | getUserRecFromEmail (params.emailaddress, params.emailcode, function (err, userRec) { 961 | if (err) { 962 | badloginCallback (); 963 | } 964 | else { 965 | if (config.flEnableSupervisorMode) { //11/11/23 by DW 966 | if (params.actingas === undefined) { //11/4/23 by DW 967 | callback (undefined, userRec.screenname); 968 | } 969 | else { 970 | if (userRec.role == "admin") { 971 | database.findUserWithScreenname (params.actingas, function (flInDatabase) { 972 | if (flInDatabase) { 973 | console.log ("\nfeedland: params.actingas == " + params.actingas + "\n"); 974 | callback (undefined, params.actingas); 975 | } 976 | else { 977 | callback (undefined, userRec.screenname); 978 | } 979 | }); 980 | } 981 | else { 982 | callback (undefined, userRec.screenname); 983 | } 984 | } 985 | } 986 | else { 987 | callback (undefined, userRec.screenname); 988 | } 989 | } 990 | }); 991 | } 992 | else { 993 | badloginCallback (); 994 | } 995 | } 996 | } 997 | function getRssCloudOptions () { 998 | let options = new Object (); 999 | for (var x in config.rssCloud) { 1000 | options [x] = config.rssCloud [x]; 1001 | } 1002 | 1003 | let appconfig = daveappserver.getConfig (); 1004 | 1005 | const rssDomain = (appconfig.rssCloudNotifyDomain === undefined) ? appconfig.myDomain : appconfig.rssCloudNotifyDomain; //11/22/23 by DW 1006 | var splits = rssDomain.split (":"); 1007 | options.domain = splits [0]; 1008 | options.port = (splits.length == 2) ? Number (splits [1]) : 80; 1009 | 1010 | if (options.port == NaN) { 1011 | options.port = 80; 1012 | } 1013 | 1014 | options.websocketPort = (appconfig.flWebsocketEnabled) ? appconfig.websocketPort : undefined; 1015 | return (options); 1016 | } 1017 | function handleRssCloudPing (feedUrl, callback) { //8/18/23 by DW 1018 | console.log ("\nhandleRssCloudPing: feedUrl == " + feedUrl + "\n"); 1019 | database.checkOneFeed (feedUrl, function (err, data) { 1020 | if (err) { //11/20/23 by DW 1021 | console.log ("\nhandleRssCloudPing: err.message == " + err.message + "\n"); 1022 | } 1023 | callback ("Thanks for the update! ;-)"); 1024 | }); 1025 | } 1026 | function getServerConfig (screenname) { //5/8/23 by DW 1027 | var serverConfig = { //7/5/23 by DW 1028 | maxFeedItems: config.maxFeedItems 1029 | } 1030 | if (screenname !== undefined) { //7/5/23 by DW 1031 | serverConfig.urlUsersBlog = blog.getBlogUrl (screenname); 1032 | } 1033 | return (serverConfig); 1034 | } 1035 | 1036 | function handleHttpRequest (theRequest) { 1037 | var now = new Date (); 1038 | const params = theRequest.params; 1039 | function returnPlainText (theString) { 1040 | theString = (theString === undefined) ? "" : theString.toString (); 1041 | theRequest.httpReturn (200, "text/plain", theString); 1042 | } 1043 | function returnHtml (htmltext) { 1044 | theRequest.httpReturn (200, "text/html", htmltext); 1045 | } 1046 | function returnData (jstruct) { 1047 | if (jstruct === undefined) { 1048 | jstruct = {}; 1049 | } 1050 | theRequest.httpReturn (200, "application/json", utils.jsonStringify (jstruct)); 1051 | } 1052 | function httpReturnRedirect (url, code) { //12/26/22 by DW 1053 | var headers = { 1054 | location: url 1055 | }; 1056 | if (code === undefined) { 1057 | code = 302; 1058 | } 1059 | theRequest.httpReturn (code, "text/plain", code + " REDIRECT", headers); 1060 | } 1061 | 1062 | function returnJsontext (jsontext) { //9/14/22 by DW 1063 | theRequest.httpReturn (200, "application/json", jsontext.toString ()); 1064 | } 1065 | function returnError (jstruct) { 1066 | theRequest.httpReturn (500, "application/json", utils.jsonStringify (jstruct)); 1067 | } 1068 | function returnOpml (err, opmltext) { 1069 | if (err) { 1070 | returnError (err); 1071 | } 1072 | else { 1073 | theRequest.httpReturn (200, "text/xml", opmltext); 1074 | } 1075 | } 1076 | function httpReturn (err, returnedValue) { 1077 | if (err) { 1078 | returnError (err); 1079 | } 1080 | else { 1081 | if (typeof returnedValue == "object") { 1082 | returnData (returnedValue); 1083 | } 1084 | else { 1085 | returnJsontext (returnedValue); //9/14/22 by DW 1086 | } 1087 | } 1088 | } 1089 | function xmlReturn (err, xmltext) { //9/17/22 by DW 1090 | if (err) { 1091 | returnError (err); 1092 | } 1093 | else { 1094 | theRequest.httpReturn (200, "text/xml", xmltext); 1095 | } 1096 | } 1097 | function callWithScreenname (callback) { 1098 | getScreenname (params, function (err, screenname) { //12/23/22 by DW 1099 | if (err) { 1100 | returnError (err); 1101 | } 1102 | else { 1103 | callback (screenname); 1104 | } 1105 | }); 1106 | } 1107 | 1108 | switch (theRequest.method) { 1109 | case "POST": 1110 | switch (theRequest.lowerpath) { 1111 | case "/opmlsubscribe": //6/30/22 by DW 1112 | callWithScreenname (function (screenname) { 1113 | subscribeToOpml (screenname, theRequest.postBody, utils.getBoolean (params.flDeleteEnabled), httpReturn); 1114 | }); 1115 | return (true); 1116 | case "/sendprefs": //6/14/23 by DW 1117 | callWithScreenname (function (screenname) { 1118 | database.setUserPrefs (screenname, theRequest.postBody, httpReturn); 1119 | }); 1120 | return (true); 1121 | case config.rssCloud.feedUpdatedCallback: //10/9/22 by DW 1122 | var jstruct = qs.parse (theRequest.postBody); 1123 | handleRssCloudPing (jstruct.url, returnPlainText); //8/18/23 by DW 1124 | return (true); 1125 | default: 1126 | return (false); //not consumed 1127 | } 1128 | case "GET": 1129 | switch (theRequest.lowerpath) { 1130 | case "/returnjson": 1131 | reallysimple.readFeed (theRequest.params.url, httpReturn); 1132 | return (true); //we handled it 1133 | case "/returnopml": 1134 | reallysimple.readFeed (theRequest.params.url, function (err, theFeed) { 1135 | if (err) { 1136 | returnError (err); 1137 | } 1138 | else { 1139 | returnOpml (reallysimple.convertToOpml (theFeed)); 1140 | } 1141 | }); 1142 | return (true); //we handled it 1143 | case "/checkfeednow": 1144 | database.checkOneFeed (theRequest.params.url, httpReturn); 1145 | return (true); 1146 | case "/getupdatedfeed": 1147 | database.getUpdatedFeed (theRequest.params.url, httpReturn); 1148 | return (true); 1149 | case "/getfeedrec": 1150 | database.getDatabaseFeed (theRequest.params.url, httpReturn); 1151 | return (true); 1152 | case "/getfeed": 1153 | database.getFeed (theRequest.params.url, httpReturn); 1154 | return (true); 1155 | case "/deleteitem": //4/22/22 by DW 1156 | database.deleteItem (theRequest.params.id, httpReturn); 1157 | return (true); 1158 | case "/updateblogsettings": //4/28/22 by DW 1159 | callWithScreenname (function (screenname) { 1160 | blog.updateBlogSettings (theRequest.params.jsontext, screenname, httpReturn); 1161 | }); 1162 | return (true); 1163 | case "/newblogpost": //4/28/22 by DW 1164 | callWithScreenname (function (screenname) { 1165 | blog.newPost (theRequest.params.jsontext, screenname, httpReturn); 1166 | }); 1167 | return (true); 1168 | case "/updateblogpost": //4/30/22 by DW 1169 | callWithScreenname (function (screenname) { 1170 | blog.updatePost (theRequest.params.jsontext, screenname, httpReturn); 1171 | }); 1172 | return (true); 1173 | case "/togglelike": //5/6/22 by DW 1174 | callWithScreenname (function (screenname) { 1175 | database.toggleItemLike (screenname, theRequest.params.id, httpReturn); 1176 | }); 1177 | return (true); 1178 | case "/getlikes": //5/6/22 by DW 1179 | callWithScreenname (function (screenname) { 1180 | database.getLikes (theRequest.params.id, httpReturn); 1181 | }); 1182 | return (true); 1183 | case "/getalotoflikes": //5/6/22 by DW 1184 | callWithScreenname (function (screenname) { 1185 | database.getALotOLikes (theRequest.params.idarray, httpReturn); 1186 | }); 1187 | return (true); 1188 | case "/getlikesxml": //9/17/22 by DW 1189 | database.buildLikesFeed (xmlReturn); 1190 | return (true); 1191 | case "/getfollowers": //5/18/22 by DW, 10/28/22 by DW -- no longer requires login 1192 | database.getFollowers (theRequest.params.url, httpReturn); 1193 | return (true); 1194 | case "/isusersubscribed": //5/18/22 by DW 1195 | callWithScreenname (function (screenname) { 1196 | database.isUserSubscribed (params.url, screenname, params.urlreadinglist, httpReturn); //10/23/23 by DW -- new param, urlreadinglist 1197 | }); 1198 | return (true); 1199 | case "/getusersubcriptions": //5/20/22 by DW 1200 | database.getSubscriptions (theRequest.params.screenname, httpReturn); 1201 | return (true); 1202 | case "/setfeedsubscount": //5/21/22 by DW 1203 | database.setFeedSubsCount (theRequest.params.url, httpReturn); 1204 | return (true); 1205 | case "/opml": //5/23/22 by DW 1206 | database.getUserOpmlSubscriptions (params.screenname, params.catname, returnOpml); 1207 | return (true); 1208 | case "/opmlhotlist": //7/30/22 by DW 1209 | database.getHotlistOpml (returnOpml); 1210 | return (true); 1211 | case "/subscribe": //5/26/22 by DW 1212 | callWithScreenname (function (screenname) { 1213 | database.subscribeToFeed (screenname, params.url, httpReturn); 1214 | }); 1215 | return (true); 1216 | case "/unsubscribe": //5/26/22 by DW 1217 | callWithScreenname (function (screenname) { 1218 | database.deleteSubscription (screenname, params.url, httpReturn); 1219 | }); 1220 | return (true); 1221 | case "/unsublist": //6/28/22 by DW 1222 | callWithScreenname (function (screenname) { 1223 | var theArray = new Array (); 1224 | try { 1225 | theArray = JSON.parse (params.list); 1226 | } 1227 | catch (err) { 1228 | returnError (err); 1229 | } 1230 | if (theArray.length > 0) { 1231 | unsubList (screenname, theArray, httpReturn); 1232 | } 1233 | }); 1234 | return (true); 1235 | case "/getrecentsubscriptions": //7/23/22 by DW 1236 | database.getRecentSubscriptions (httpReturn); 1237 | return (true); 1238 | case "/gethotlist": //7/30/22 by DW 1239 | database.getHotlist (httpReturn); 1240 | return (true); 1241 | case "/getriver": //4/25/23 by DW -- get the river just using the screenname 1242 | if (params.url === undefined) { 1243 | if (params.screenname === undefined) { 1244 | callWithScreenname (function (screenname) { 1245 | database.getRiverFromScreenname (screenname, httpReturn); 1246 | }); 1247 | } 1248 | else { 1249 | database.getRiverFromScreenname (params.screenname, httpReturn); 1250 | } 1251 | } 1252 | else { 1253 | database.getRiver (params.url, params.screenname, httpReturn); 1254 | } 1255 | return (true); 1256 | case "/getriverfromlist": //8/3/22 by DW 1257 | database.getRiverFromList (params.list, httpReturn); 1258 | return (true); 1259 | case "/getriverfromopml": //8/21/22 by DW 1260 | database.getRiverFromOpml (params.url, httpReturn); 1261 | return (true); 1262 | case "/getriverfromcategory": //9/6/22 by DW 1263 | database.getRiverFromCategory (params.screenname, params.catname, httpReturn); 1264 | return (true); 1265 | case "/getriverfromeverything": //10/14/22 by DW 1266 | database.getRiverFromEverything (httpReturn); 1267 | return (true); 1268 | case "/getriverfromhotlist": //10/15/22 by DW 1269 | database.getRiverFromHotlist (httpReturn); 1270 | return (true); 1271 | case "/getriverfromuserfeeds": //12/3/22 by DW 1272 | database.getRiverFromUserFeeds (httpReturn); 1273 | return (true); 1274 | case "/getriverfromreadinglist": //11/12/23 by DW 1275 | database.getRiverFromReadingList (params.url, httpReturn); 1276 | return (true); 1277 | case "/getfeeditems": //8/31/22 by DW 1278 | database.getFeedItems (params.url, params.maxItems, httpReturn); 1279 | return (true); 1280 | case "/setsubscriptioncategories": //9/4/22 by DW 1281 | callWithScreenname (function (screenname) { 1282 | database.setCategoriesForSubscription (screenname, params.url, params.jsontext, httpReturn); 1283 | }); 1284 | return (true); 1285 | case "/getfeedsincategory": //9/6/22 by DW 1286 | callWithScreenname (function (screenname) { 1287 | if (params.screenname !== undefined) { //9/13/22 by DW 1288 | screenname = params.screenname; 1289 | } 1290 | database.getFeedsInCategory (screenname, params.catname, httpReturn); 1291 | }); 1292 | return (true); 1293 | case "/getusercategories": //9/13/22 by DW 1294 | database.getUserCategories (params.screenname, httpReturn); 1295 | return (true); 1296 | case "/memoryusage": //9/14/22 by DW 1297 | returnData (viewMemoryUsage ()); 1298 | return (true); 1299 | case "/sendprefs": //9/15/22 by DW 1300 | callWithScreenname (function (screenname) { 1301 | database.setUserPrefs (screenname, params.prefs, httpReturn); 1302 | }); 1303 | return (true); 1304 | case "/getallusers": //9/15/22 by DW 1305 | database.getAllUsers (httpReturn); 1306 | return (true); 1307 | case "/getuserprefs": //9/28/22 by DW 1308 | callWithScreenname (function (screenname) { 1309 | database.getUserPrefs (screenname, httpReturn); 1310 | }); 1311 | return (true); 1312 | case "/getuserinfo": //11/10/23 by DW -- private info like apps configuration are removed from this version 1313 | database.getUserInfo (params.screenname, httpReturn); 1314 | return (true); 1315 | case "/renewfeednow": //10/9/22 by DW 1316 | database.renewFeedNow (params.url, getRssCloudOptions (), httpReturn); 1317 | return (true); 1318 | case "/getcurrentriverbuildlog": //10/10/22 by DW 1319 | database.getCurrentRiverBuildLog (httpReturn); 1320 | return (true); 1321 | case "/getitem": //11/10/22 by DW 1322 | database.getItemFromDatabase (params.id, function (err, itemRec) { 1323 | if (err) { 1324 | returnError (err); 1325 | } 1326 | else { 1327 | returnData (database.convertDatabaseItem (itemRec)); 1328 | } 1329 | }); 1330 | return (true); 1331 | case "/regenerateemailsecret": //12/26/22 by DW 1332 | callWithScreenname (function (screenname) { 1333 | regenerateEmailSecret (screenname, httpReturn); 1334 | }); 1335 | return (true); 1336 | case "/getfeedsearch": //12/26/22 by DW 1337 | database.getFeedSearch (params.searchfor, httpReturn); 1338 | return (true); 1339 | case "/isuserindatabase": //1/6/23 by DW 1340 | if (params.screenname === undefined) { 1341 | returnData ({flInDatabase: false}); 1342 | } 1343 | else { 1344 | database.findUserWithScreenname (params.screenname, function (flInDatabase, userRec) { 1345 | returnData ({flInDatabase}); 1346 | }); 1347 | } 1348 | return (true); 1349 | case "/isemailindatabase": //1/12/23 by DW 1350 | isEmailInDatabase (params.email, returnData); 1351 | return (true); 1352 | case "/isfeedinriver": //2/1/23 by DW 1353 | database.isFeedInRiver (params.url, params.cachekey, httpReturn); 1354 | return (true); 1355 | case "/getserverconfig": //5/8/23 by DW 1356 | returnData (getServerConfig (params.screenname)); //added param -- 7/5/23 by DW 1357 | return (true); 1358 | case "/getfeedlistfromopml": //6/2/23 by DW 1359 | database.getFeedlistFromOpml (params.url, httpReturn); 1360 | return (true); 1361 | case "/newsproduct": //9/16/23 by DW 1362 | if (params.username !== undefined) { 1363 | renderUserNewsproduct (params.username, function (err, htmltext) { 1364 | if (err) { 1365 | returnError (err); 1366 | } 1367 | else { 1368 | returnHtml (htmltext); 1369 | } 1370 | }); 1371 | } 1372 | else { 1373 | if ((params.template !== undefined) || (params.spec !== undefined)) { 1374 | renderUserNewsproductWithTemplate (params.template, params.app, params.spec, theRequest, function (err, htmltext) { //12/24/23 by DW -- new param for app, spec 1375 | if (err) { 1376 | returnError (err); 1377 | } 1378 | else { 1379 | returnHtml (htmltext); 1380 | } 1381 | }); 1382 | } 1383 | else { 1384 | const message = "News products require either a username or template parameter."; 1385 | returnError ({message}); 1386 | } 1387 | } 1388 | return (true); 1389 | 1390 | case "/checkreadinglist": //10/9/23 by DW 1391 | database.checkReadingList (params.url, httpReturn); 1392 | return (true); 1393 | case "/subscribetoreadinglist": //10/9/23 by DW 1394 | callWithScreenname (function (screenname) { 1395 | database.subscribeToReadingList (screenname, params.url, httpReturn); 1396 | }); 1397 | return (true); 1398 | case "/unsubreadinglist": //10/13/23 by DW 1399 | callWithScreenname (function (screenname) { 1400 | database.deleteReadingListSubscription (screenname, params.url, httpReturn); 1401 | }); 1402 | return (true); 1403 | case "/getreadinglistsubscriptions": //10/13/23 by DW 1404 | database.getReadingListSubscriptions (params.screenname, httpReturn); 1405 | return (true); 1406 | case "/getreadinglisstinfo": //10/19/23 by DW 1407 | database.getReadingListsInfo (params.jsontext, httpReturn); 1408 | return (true); 1409 | case "/getreadinglistfollowers": //10/28/23 by DW 1410 | database.getReadingListFollowers (params.url, httpReturn); 1411 | return (true); 1412 | case "/getfeedlist": //11/6/23 by DW 1413 | getFeedList (httpReturn); 1414 | return (true); 1415 | case "/checkmyreadinglistsubs": //12/13/23 by DW 1416 | callWithScreenname (function (screenname) { 1417 | database.checkSubsForOneUserAndOneReadingList (screenname, params.url, httpReturn); 1418 | }); 1419 | return (true); 1420 | 1421 | case "/getuserinfowithwordpresstoken": //3/23/24 by DW 1422 | getUserInfoWithWordpressToken (params.token, httpReturn); 1423 | return (true); 1424 | 1425 | case config.rssCloud.feedUpdatedCallback: //12/12/22 by DW 1426 | returnPlainText (params.challenge); 1427 | return (true); 1428 | default: 1429 | return (false); //not consumed 1430 | } 1431 | break; 1432 | } 1433 | 1434 | return (false); //not consumed 1435 | } 1436 | 1437 | function logSqlCalls (options) { //9/21/23 by DW 1438 | const now = new Date (); 1439 | if (!options.err) { //errors are logged elsewhere, we're looking for performance problems 1440 | var flLog = false; 1441 | if (options.ctsecs >= config.logMinSecs) { 1442 | flLog = true; 1443 | } 1444 | else { 1445 | if (options.result !== undefined) { 1446 | if (options.result.length > config.logMaxResults) { 1447 | flLog = true; 1448 | } 1449 | } 1450 | } 1451 | if (flLog) { 1452 | console.log (); 1453 | console.log ("logSqlCalls: " + now.toLocaleTimeString () + ", ctsecs == " + options.ctsecs + ", options.result.length == " + options.result.length + "."); 1454 | console.log ("\nlogSqlCalls: sqltext == " + options.sqltext); 1455 | console.log (); 1456 | } 1457 | } 1458 | } 1459 | function getMysqlVersion (callback) { //11/18/23 by DW 1460 | const sqltext = "select version () as version;"; 1461 | davesql.runSqltext (sqltext, function (err, result) { 1462 | var theVersion = undefined; 1463 | if (!err) { 1464 | if (result.length > 0) { 1465 | theVersion = result [0].version; 1466 | } 1467 | } 1468 | callback (undefined, theVersion); 1469 | }); 1470 | } 1471 | function getFeedsHaveIds (callback) { //1/31/24 by DW 1472 | const sqltext = "select feedId from feeds;"; 1473 | davesql.runSqltext (sqltext, function (err, result) { 1474 | if (err) { 1475 | callback (false); 1476 | } 1477 | else { 1478 | callback (true); 1479 | } 1480 | }); 1481 | } 1482 | 1483 | 1484 | 1485 | function everyNight () { 1486 | if (config.flNightlyBackup) { //3/29/23 by DW 1487 | database.backupDatabase (); 1488 | } 1489 | database.nightlyDeleteItems (); //11/22/25 by DW 1490 | } 1491 | function everyMinute () { 1492 | } 1493 | function everySecond () { 1494 | var now = new Date (); 1495 | if (config.flRenewSubscriptions) { //10/29/22 by DW 1496 | if (utils.secondsSince (whenLastCloudRenew) >= config.rssCloud.minSecsBetwRenews) { //10/9/22 by DW 1497 | database.renewNextSubscriptionIfReady (getRssCloudOptions ()); 1498 | whenLastCloudRenew = now; 1499 | } 1500 | } 1501 | if (config.flUseSqlForSockets) { //9/26/23 by DW && 10/4/23 by DW 1502 | if (utils.secondsSince (whenLastSqlSocketCheck) >= config.minSecsBetwSqlSocketChecks) { 1503 | notifySocketSubscribersFromSql (function (jstruct) { //10/1/23 by DW -- added callback 1504 | database.clearCachedRivers (jstruct.item.feedUrl); 1505 | }); 1506 | whenLastSqlSocketCheck = now; 1507 | } 1508 | } 1509 | if (config.flUseReadingLists) { //10/10/23 by DW 1510 | if (utils.secondsSince (whenLastReadingListCheck) >= config.minSecsBetwReadingListChecks) { 1511 | database.checkNextReadingListfReady (); 1512 | whenLastReadingListCheck = now; 1513 | } 1514 | } 1515 | if (!utils.sameDay (now, whenLastDayRollover)) { 1516 | whenLastDayRollover = now; 1517 | everyNight (); //8/22/22 by DW 1518 | } 1519 | } 1520 | 1521 | function startFeedChecker () { //3/17/24 by DW 1522 | if (config.flUpdateFeedsInBackground) { 1523 | const ctmillisecs = config.minSecsBetwFeedChecks * 1000; 1524 | console.log ("startFeedChecker: ctmillisecs == " + ctmillisecs); 1525 | setInterval (database.updateNextFeedIfReady, ctmillisecs); 1526 | } 1527 | } 1528 | function start () { 1529 | var options = { 1530 | everySecond, 1531 | everyMinute, 1532 | httpRequest: handleHttpRequest, 1533 | publishFile: publishFileCallback, //3/18/22 by DW 1534 | addMacroToPagetable, //4/30/22 by DW 1535 | asyncAddMacroToPagetable, //12/2/22 by DW 1536 | addEmailToUserInDatabase, //12/7/22 by DW 1537 | getScreenname, //12/23/22 by DW 1538 | getScreenNameFromEmail, //1/10/23 by DW 1539 | findUserWithScreenname: database.findUserWithScreenname, //2/15/23 by DW & 11/4/23 by DW 1540 | findUserWithEmail: database.findUserWithEmail, //11/4/23 by DW 1541 | loginWordpressUser, //10/31/23 by DW 1542 | isUserAdmin //11/3/23 by DW 1543 | } 1544 | daveappserver.start (options, function (appConfig) { 1545 | for (var x in appConfig) { 1546 | config [x] = appConfig [x]; 1547 | } 1548 | if (config.flStaticFilesInSql) { //9/20/23 by DW 1549 | appConfig.getStaticFile = getStaticFileInSql; 1550 | appConfig.publishStaticFile = publishStaticFileInSql; 1551 | } 1552 | reallysimple.setConfig ({timeOutSecs: config.httpRequestTimeoutSecs}); //2/26/24 by DW 1553 | blog.start (config, function () { 1554 | config.database.logCallback = logSqlCalls; //9/21/23 by DW 1555 | davesql.start (config.database, function () { 1556 | getMysqlVersion (function (err, mysqlVersion) { //11/18/23 by DW, 2/1/24; 11:22:16 AM by DW 1557 | config.mysqlVersion = mysqlVersion; 1558 | getFeedsHaveIds (function (flFeedsHaveIds) { //1/31/24 by DW, 2/1/24; 11:22:16 AM by DW 1559 | config.flFeedsHaveIds = flFeedsHaveIds; 1560 | console.log ("start: config.flFeedsHaveIds == " + config.flFeedsHaveIds); 1561 | database.start (config, function () { 1562 | if (config.flWebsocketEnabled && config.flUseSqlForSockets) { //9/26/23 by DW 1563 | initLastNewItem (); //9/26/23 by DW 1564 | } 1565 | if (config.flBackupOnStartup) { //1/9/23 by DW 1566 | database.backupDatabase (); 1567 | } 1568 | if (config.flUpdateFeedsInBackground) { //3/17/24 by DW 1569 | startFeedChecker (); 1570 | } 1571 | }); 1572 | }); 1573 | }); 1574 | }); 1575 | }); 1576 | }); 1577 | } 1578 | --------------------------------------------------------------------------------