├── .github
└── ISSUE_TEMPLATE.md
├── LICENSE
├── README.md
├── code
├── breakupchatlog.js
├── monthlyArchives
│ ├── archive.js
│ ├── lib
│ │ ├── filesystem.js
│ │ └── utils.js
│ ├── package.json
│ ├── template.html
│ └── webfiles
│ │ ├── 2015
│ │ └── 12
│ │ │ └── index.json
│ │ └── 2016
│ │ ├── 01
│ │ └── index.json
│ │ ├── 02
│ │ └── index.json
│ │ └── 03
│ │ └── index.json
├── pingserver.js
├── readchatlog.js
├── templateedit.html
└── websocketdemo.html
├── defaults
├── menubar.html
├── menubar.opml
├── template.html
└── template.opml
├── docs
├── callbacks.md
├── docker.md
├── domains.md
├── ec2.md
├── homepage.md
├── macros.md
├── plugins.md
├── publicfiles.md
├── s3.md
├── setup.md
├── updating.md
└── whitelist.md
└── misc
└── about.opml
/.github/ISSUE_TEMPLATE.md:
--------------------------------------------------------------------------------
1 | ##### Description of the problem
2 |
3 | ##### Steps to reproduce
4 |
5 | 1. [First step]
6 |
7 | 2. [Second step]
8 |
9 | 3. [and so on...]
10 |
11 | ##### What you expected to happen
12 |
13 | ##### Other data
14 |
15 | [What OS, browser, are you using the Mac app, example data, anything that might help someone figure out what happened on your system. Remember, nothing can happen until someone else can reproduce the problem you're experiencing.]
16 |
17 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2016 Dave Winer
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ## 1999-project
2 |
3 |
4 |
5 | 1999.io needs a place for docs, support, sample code, and a place to store the default templates. This is that place.
6 |
7 |
8 |
9 | #### Updates
10 |
11 | ##### v0.61 - 9/28/16 by DW
12 |
13 | Added a howto for storing the public files for nodeStorage in a different location.
14 |
15 | ##### v0.60 - 9/17/16 by DW
16 |
17 | You can now specify the text on the home page of your 1999.io installation.
18 |
19 | ##### v0.59 - 9/4/16 by DW
20 |
21 | You can now edit the Twitter and Facebook metadata for each post in 1999.io.
22 |
23 | The default template was updated accordingly.
24 |
25 | ##### v0.58 - 8/14/16 by DW
26 |
27 | Added docs explaining how to set up the Editors menu for 1999.io users.
28 |
29 | ##### v0.57 - 8/11/16 by DW
30 |
31 | Added docs showing how to use S3 storage instead of the local filesystem.
32 |
33 | ##### v0.56 - 8/10/16 by DW
34 |
35 | Added docs for new googleAnalyticsAccount configuration option.
36 |
37 | ##### v0.55 - 7/24/16 by DW
38 |
39 | New instructions for running 1999-server on Docker.
40 |
41 | ##### v0.54 - 7/8/16 by DW
42 |
43 | Added a doc for the EC2 machine image.
44 |
45 | ##### v0.53 - 6/6/16 by DW
46 |
47 | Added a new utility app that generates monthly archive pages. The feature was recently added to 1999.io. This builds archive pages for months before the feature came online. I'm using this app to generate the pages for Scripting News.
48 |
49 | ##### v0.52 - 6/4/16 by DW
50 |
51 | Added about.opml to the new misc folder. It's just there to support review work on the About page for 1999.io. Using GitHub you will be able to follow changes to the document.
52 |
53 | ##### v0.51 - 5/30/16 by DW
54 |
55 | The new domains configuration option is documented.
56 |
57 | ##### v0.50 - 5/16/16 by DW
58 |
59 | PlugIns are now available in 1999.io. A new howto says how servers set them up and the templateEdit plugin is documented.
60 |
61 | ##### v0.49 - 5/13/16 by DW
62 |
63 | New howto shows how to configure the homepage of the editor.
64 |
65 | ##### v0.48 - 4/27/16 by DW
66 |
67 | Added a template for the Issues section, per the instructions on the GitHub site.
68 |
69 | Thanks to the medium-editor project for the example.
70 |
71 | ##### v0.47 - 4/17/16 by DW
72 |
73 | Demo app for websocket support in 1999.io server. You can hook it up to your chatlog, and see how you might develop a client app for your blog using the realtime power of WebSockets. One of the few real-world demos for WebSockets.
74 |
75 | Note: This app previously was in its own repository. It really belonged in this collection of demos.
76 |
77 | Also: You can run this app from a web page on 1999.io.
78 |
79 | ##### v0.46 - 4/16/16 by DW
80 |
81 | New sample app, readchatlog.js. Reads a chatLog.js file, which contains all the information about a user's blog, and lists each item to the console, including the ID, title and when it was posted. Just want to get people thinking of apps they might build off the chatLog.json files.
82 |
83 | ##### v0.45 - 4/12/16 by DW
84 |
85 | New docs page on macros that are available to templates while pages are being rendered.
86 |
87 | ##### v0.44 - 4/8/16 by DW
88 |
89 | Updates to the setup docs.
90 |
91 | ##### v0.44 - 4/6/16 by DW
92 |
93 | New utility script for breaking up big chatLog.json files, one file per month in a sub-folder called months/.
94 |
95 | ##### v0.43 - 4/4/16 by DW
96 |
97 | Started a new code folder for sample code. First bit of code I'm sharing is the source for ping.1999.io, designed to run as a PagePark script.
98 |
99 | ##### v0.42 - 4/2/16 by DW
100 |
101 | Added docs for publish callbacks.
102 |
103 | Moved the startup docs from the blog.
104 |
105 | Two more: updating and whitelist.
106 |
107 | ##### v0.41 - 4/2/16 by DW
108 |
109 | Made a small change to template.opml. There's a new macro on the page title: [%defaulttitlestyle%].
110 |
111 | <div class="divMessageTitle" id="idMessageTitle" [%defaulttitlestyle%]>[%title%]</div>
112 |
113 | For story pages its the empty string, but for the home page it's: " style=\"display: none;\" "
114 |
115 | This stops the title from flashing when the home page is displayed. I found it really ugly and unacceptable.
116 |
117 | Now, if you've modified your template, and you want this functionality you'll have to add this to your template.
118 |
119 | ##### v0.40 - 4/2/16 by DW
120 |
121 | Started the project off with the defaults.
122 |
123 | Included are the default menubar and template in OPML and the menubar in HTML.
124 |
125 | These will stay updated along with any changes I make to the ones actually used by the 1999.io app.
126 |
127 | They're provided as a reference and a way to track changes to the templates.
128 |
129 |
--------------------------------------------------------------------------------
/code/breakupchatlog.js:
--------------------------------------------------------------------------------
1 | var myProductName = "Break Up Chatlog", myVerion = "0.40c";
2 |
3 | var request = require ("request");
4 | var fs = require ("fs");
5 |
6 | var urlChatlogJs = "http://friends.farm/users/davewiner/chatLog.json";
7 |
8 | function padWithZeros (num, ctplaces) {
9 | var s = num.toString ();
10 | while (s.length < ctplaces) {
11 | s = "0" + s;
12 | }
13 | return (s);
14 | }
15 | function jsonStringify (jstruct, flFixBreakage) {
16 | if (flFixBreakage === undefined) {
17 | flFixBreakage = false;
18 | }
19 | var s = JSON.stringify (jstruct, undefined, 4);
20 | if (flFixBreakage) {
21 | s = s.replace (/\u2028/g,'\\u2028').replace (/\u2029/g,'\\u2029');
22 | }
23 | return (s);
24 | }
25 | function readChatlog (callback) {
26 | request (urlChatlogJs, function (err, response, jsontext) {
27 | if (err) {
28 | console.log ("readChatlog: err.message == " + err.message);
29 | callback (undefined);
30 | }
31 | else {
32 | callback (jsontext);
33 | }
34 | });
35 | }
36 | function startup () {
37 | console.log ("\n" + myProductName + " v" + myVerion);
38 |
39 | readChatlog (function (jsontext) {
40 | try {
41 | var theLog = JSON.parse (jsontext), lastMonth = -1, currentArray = new Array ();
42 | function writeArray () {
43 | if (currentArray.length > 0) {
44 | var yearPart = new Date (currentArray [0].when).getUTCFullYear ().toString ();
45 | var f = "months/" + yearPart + "." + padWithZeros (lastMonth, 2) + ".json";
46 | console.log ("Writing " + currentArray.length + " item(s) to file " + f);
47 | fs.writeFileSync (f, jsonStringify (currentArray));
48 | }
49 | }
50 | for (var i = 0; i < theLog.chatLog.length; i++) {
51 | var item = theLog.chatLog [i], when = new Date (item.when), theMonth = when.getUTCMonth () + 1;
52 | if (theMonth != lastMonth) {
53 | writeArray ();
54 | lastMonth = theMonth;
55 | currentArray = new Array ();
56 | }
57 | currentArray [currentArray.length] = item;
58 | }
59 | writeArray ();
60 | }
61 | catch (err) {
62 | console.log ("startup: urlChatlogJs == " + urlChatlogJs + ", err.message == " + err.message);
63 | }
64 | });
65 |
66 |
67 | }
68 |
69 | startup ();
70 |
--------------------------------------------------------------------------------
/code/monthlyArchives/archive.js:
--------------------------------------------------------------------------------
1 | var myProductName = "Monthly Archive", myVerion = "0.40b";
2 |
3 | var utils = require ("./lib/utils.js");
4 | var dateFormat = require ("dateformat");
5 | var filesystem = require ("./lib/filesystem.js");
6 | var fs = require ("fs");
7 | var request = require ("request");
8 |
9 | var folderpath = "webfiles/";
10 | var urlScriptingTemplate = "http://1999.io/testing/monthlyarchives/template.html";
11 | var maxPageItems = 35;
12 | var templateText;
13 |
14 | function getTemplateText (callback) {
15 | request (urlScriptingTemplate, function (err, response, templatetext) {
16 | if (!err && response.statusCode == 200) {
17 | callback (templatetext);
18 | }
19 | else {
20 | console.log ("getTemplateText: err.message == " + err.message);
21 | callback (undefined);
22 | }
23 | });
24 | }
25 | function renderMonthlyArchivePage (theLog, callback) {
26 | function getPostTitle (item) {
27 | var theTitle = "";
28 | if ((item.payload !== undefined) && (item.payload.title !== undefined)) {
29 | return (item.payload.title);
30 | }
31 | return (theTitle);
32 | }
33 | function getUrlRendering (item) {
34 | if ((item.payload !== undefined) && (item.payload.urlRendering !== undefined)) {
35 | return (item.payload.urlRendering);
36 | }
37 | return ("");
38 | }
39 | function getImage (item) {
40 | if ((item.payload !== undefined) && (item.payload.image !== undefined)) {
41 | return ("");
42 | }
43 | return ("");
44 | }
45 | function isItemPublished (item) {
46 | if ((item.payload !== undefined) && (item.payload.flPublished !== undefined)) {
47 | return (item.payload.flPublished);
48 | }
49 | return (false);
50 | }
51 | function formatDateTime (d) {
52 | d = new Date (d);
53 | return (dateFormat (d, "m/d/yyyy; h:MM TT"));
54 | }
55 | if (theLog !== undefined) {
56 | var htmltext = "", indentlevel = 0, whenstart = new Date (), ctItems = 0, theMonth = undefined;
57 | function add (s) {
58 | htmltext += utils.filledString ("\t", indentlevel) + s + "\n";
59 | }
60 | add ("
"); indentlevel++;
61 | for (var i = theLog.chatLog.length - 1; i >= 0; i--) {
62 | if (ctItems >= maxPageItems) {
63 | break;
64 | }
65 | var item = theLog.chatLog [i];
66 | if (theMonth === undefined) {
67 | theMonth = new Date (item.when);
68 | }
69 | if (isItemPublished (item)) {
70 | var urlRendering = getUrlRendering (item);
71 | add ("
"); indentlevel--;
80 | var pagetable = {
81 | title: "Archive page for " + dateFormat (theMonth, "mmmm yyyy"),
82 | bodytext: htmltext,
83 | whenlastupdate: dateFormat (whenstart, "dddd, mmmm dS, yyyy; h:MM TT")
84 | };
85 | var pagetext = utils.multipleReplaceAll (templateText, pagetable, false, "[%", "%]");
86 | return (pagetext);
87 | }
88 | }
89 | function processFile (f) {
90 | fs.readFile (f, function (err, data) {
91 | if (err) {
92 | console.log ("processFile: f == " + f + ", err.message == " + err.message);
93 | }
94 | else {
95 | var jstruct = JSON.parse (data.toString ());
96 | var theLog = {
97 | chatLog: jstruct
98 | };
99 | pagetext = renderMonthlyArchivePage (theLog);
100 | var fhtml = utils.stringPopExtension (f) + ".html";
101 | console.log ("processFile: fhtml == " + fhtml + ", pagetext.length == " + pagetext.length);
102 | fs.writeFile (fhtml, pagetext);
103 | }
104 | });
105 |
106 | }
107 | function buildArchive (callback) {
108 | getTemplateText (function (s) {
109 | if (s !== undefined) {
110 | templateText = s; //copy into global
111 | filesystem.recursivelyVisitFiles (folderpath, function (f) {
112 | if (f === undefined) {
113 | callback ();
114 | }
115 | else {
116 | if (utils.endsWith (f, ".json")) {
117 | processFile (f);
118 | }
119 | }
120 | });
121 | }
122 | });
123 | }
124 |
125 | buildArchive (function () {
126 | });
127 |
--------------------------------------------------------------------------------
/code/monthlyArchives/lib/filesystem.js:
--------------------------------------------------------------------------------
1 | exports.deleteDirectory = fsDeleteDirectory;
2 | exports.sureFilePath = fsSureFilePath;
3 | exports.newObject = fsNewObject;
4 | exports.getObject = fsGetObject;
5 | exports.recursivelyVisitFiles = fsRecursivelyVisitFiles;
6 |
7 | var fs = require ("fs");
8 |
9 | var fsStats = {
10 | ctWrites: 0,
11 | ctBytesWritten: 0,
12 | ctWriteErrors: 0,
13 | ctReads: 0,
14 | ctBytesRead: 0,
15 | ctReadErrors: 0
16 | };
17 |
18 |
19 |
20 | function fsSureFilePath (path, callback) {
21 | var splits = path.split ("/");
22 | path = ""; //1/8/15 by DW
23 | if (splits.length > 0) {
24 | function doLevel (levelnum) {
25 | if (levelnum < (splits.length - 1)) {
26 | path += splits [levelnum] + "/";
27 | fs.exists (path, function (flExists) {
28 | if (flExists) {
29 | doLevel (levelnum + 1);
30 | }
31 | else {
32 | fs.mkdir (path, undefined, function () {
33 | doLevel (levelnum + 1);
34 | });
35 | }
36 | });
37 | }
38 | else {
39 | if (callback != undefined) {
40 | callback ();
41 | }
42 | }
43 | }
44 | doLevel (0);
45 | }
46 | else {
47 | if (callback != undefined) {
48 | callback ();
49 | }
50 | }
51 | }
52 | function fsNewObject (path, data, type, acl, callback, metadata) {
53 | fsSureFilePath (path, function () {
54 | fs.writeFile (path, data, function (err) {
55 | var dataAboutWrite = {
56 | };
57 | if (err) {
58 | console.log ("fsNewObject: error == " + JSON.stringify (err, undefined, 4));
59 | fsStats.ctWriteErrors++;
60 | if (callback != undefined) {
61 | callback (err, dataAboutWrite);
62 | }
63 | }
64 | else {
65 | fsStats.ctWrites++;
66 | fsStats.ctBytesWritten += data.length;
67 | if (callback != undefined) {
68 | callback (err, dataAboutWrite);
69 | }
70 | }
71 | });
72 | });
73 | }
74 | function fsGetObject (path, callback) {
75 | fs.readFile (path, "utf8", function (err, data) {
76 | var dataAboutRead = {
77 | Body: data
78 | };
79 | if (err) {
80 | fsStats.ctReadErrors++;
81 | }
82 | else {
83 | fsStats.ctReads++;
84 | fsStats.ctBytesRead += dataAboutRead.Body.length;
85 | }
86 | callback (err, dataAboutRead);
87 | });
88 | }
89 | function fsListObjects (path, callback) {
90 | function endsWithChar (s, chPossibleEndchar) {
91 | if ((s === undefined) || (s.length == 0)) {
92 | return (false);
93 | }
94 | else {
95 | return (s [s.length - 1] == chPossibleEndchar);
96 | }
97 | }
98 | fs.readdir (path, function (err, list) {
99 | if (!endsWithChar (path, "/")) {
100 | path += "/";
101 | }
102 | if (list !== undefined) { //6/4/15 by DW
103 | for (var i = 0; i < list.length; i++) {
104 | var obj = {
105 | s3path: path + list [i],
106 | path: path + list [i], //11/21/14 by DW
107 | Size: 1
108 | };
109 | callback (obj);
110 | }
111 | }
112 | callback ({flLastObject: true});
113 | });
114 | }
115 | function fsRecursivelyVisitFiles (folderpath, fileCallback, completionCallback) { //3/23/16 by DW
116 | if (folderpath [folderpath.length - 1] != "/") {
117 | folderpath += "/";
118 | }
119 | fs.readdir (folderpath, function (err, list) {
120 | function doListItem (ix) {
121 | if (ix < list.length) {
122 | var f = folderpath + list [ix];
123 | fs.stat (f, function (err, stats) {
124 | if (err) {
125 | doListItem (ix + 1);
126 | }
127 | else {
128 | if (stats.isDirectory ()) { //dive into the directory
129 | fsRecursivelyVisitFiles (f, fileCallback, function () {
130 | doListItem (ix + 1);
131 | });
132 | }
133 | else {
134 | if (fileCallback !== undefined) {
135 | fileCallback (f);
136 | doListItem (ix + 1);
137 | }
138 | }
139 | }
140 | });
141 | }
142 | else {
143 | if (completionCallback !== undefined) {
144 | completionCallback ();
145 | }
146 | else {
147 | if (fileCallback !== undefined) {
148 | fileCallback (undefined);
149 | }
150 | }
151 | }
152 | }
153 | if (list !== undefined) { //6/4/15 by DW
154 | doListItem (0);
155 | }
156 | });
157 | }
158 | function fsDeleteDirectory (folderpath, callback) { //3/25/16 by DW
159 | if (folderpath [folderpath.length - 1] != "/") {
160 | folderpath += "/";
161 | }
162 | fs.readdir (folderpath, function (err, list) {
163 | if (err) {
164 | console.log ("fsDeleteDirectory: err.message == " + err.message);
165 | }
166 | else {
167 | function doListItem (ix) {
168 | if (ix < list.length) {
169 | var f = folderpath + list [ix];
170 | fs.stat (f, function (err, stats) {
171 | if (err) {
172 | doListItem (ix + 1);
173 | }
174 | else {
175 | if (stats.isDirectory ()) { //dive into the directory
176 | fsDeleteDirectory (f, function () {
177 | doListItem (ix + 1);
178 | });
179 | }
180 | else {
181 | fs.unlink (f, function () {
182 | doListItem (ix + 1);
183 | });
184 | }
185 | }
186 | });
187 | }
188 | else {
189 | fs.rmdir (folderpath, function () {
190 | if (callback !== undefined) {
191 | callback ();
192 | }
193 | });
194 | }
195 | }
196 | doListItem (0);
197 | }
198 | });
199 | }
200 |
201 |
--------------------------------------------------------------------------------
/code/monthlyArchives/lib/utils.js:
--------------------------------------------------------------------------------
1 | var fs = require ("fs");
2 |
3 | exports.beginsWith = beginsWith;
4 | exports.endsWith = endsWith;
5 | exports.stringCountFields = stringCountFields;
6 | exports.stringDelete = stringDelete;
7 | exports.stringMid = stringMid;
8 | exports.padWithZeros = padWithZeros;
9 | exports.getDatePath = getDatePath;
10 | exports.secondsSince = secondsSince;
11 | exports.bumpUrlString = bumpUrlString;
12 | exports.stringContains = stringContains;
13 | exports.sameDay = sameDay;
14 | exports.jsonStringify = jsonStringify;
15 | exports.stringNthField = stringNthField;
16 | exports.getBoolean = getBoolean;
17 | exports.isAlpha = isAlpha;
18 | exports.isNumeric = isNumeric;
19 | exports.stringLastField = stringLastField;
20 | exports.multipleReplaceAll = multipleReplaceAll;
21 | exports.replaceAll = replaceAll; //2/17/15 by DW
22 | exports.kilobyteString = kilobyteString;
23 | exports.megabyteString = megabyteString;
24 | exports.gigabyteString = gigabyteString;
25 | exports.stringLower = stringLower;
26 | exports.filledString = filledString;
27 | exports.innerCaseName = innerCaseName;
28 | exports.copyScalars = copyScalars;
29 | exports.stripMarkup = stripMarkup;
30 | exports.replaceAll = replaceAll;
31 | exports.hotUpText = hotUpText;
32 | exports.secondsSince = secondsSince;
33 | exports.encodeXml = encodeXml;
34 | exports.getFileModDate = getFileModDate; //8/26/15 by DW
35 | exports.getRandomPassword = getRandomPassword; //8/28/15 by DW
36 | exports.trimWhitespace = trimWhitespace; //9/1/15 by DW
37 | exports.viewDate = viewDate; //11/29/15 by DW
38 | exports.stringPopExtension = stringPopExtension; //6/6/16 by DW
39 |
40 | function sameDay (d1, d2) {
41 | //returns true if the two dates are on the same day
42 | d1 = new Date (d1);
43 | d2 = new Date (d2);
44 | return ((d1.getFullYear () == d2.getFullYear ()) && (d1.getMonth () == d2.getMonth ()) && (d1.getDate () == d2.getDate ()));
45 | }
46 | function sameMonth (d1, d2) { //5/29/16 by DW -- return true if the two dates are in the same month
47 | d1 = new Date (d1);
48 | d2 = new Date (d2);
49 | return ((d1.getFullYear () == d2.getFullYear ()) && (d1.getMonth () == d2.getMonth ()));
50 | }
51 | function dayGreaterThanOrEqual (d1, d2) { //9/2/14 by DW
52 | d1 = new Date (d1);
53 | d1.setHours (0);
54 | d1.setMinutes (0);
55 | d1.setSeconds (0);
56 |
57 | d2 = new Date (d2);
58 | d2.setHours (0);
59 | d2.setMinutes (0);
60 | d2.setSeconds (0);
61 |
62 | return (d1 >= d2);
63 | }
64 | function stringLower (s) {
65 | if (s === undefined) { //1/26/15 by DW
66 | return ("");
67 | }
68 | s = s.toString (); //1/26/15 by DW
69 | return (s.toLowerCase ());
70 | }
71 | function secondsSince (when) {
72 | var now = new Date ();
73 | when = new Date (when);
74 | return ((now - when) / 1000);
75 | }
76 | function padWithZeros (num, ctplaces) {
77 | var s = num.toString ();
78 | while (s.length < ctplaces) {
79 | s = "0" + s;
80 | }
81 | return (s);
82 | }
83 | function getDatePath (theDate, flLastSeparator) {
84 | if (theDate === undefined) {
85 | theDate = new Date ();
86 | }
87 | else {
88 | theDate = new Date (theDate); //8/12/14 by DW -- make sure it's a date type
89 | }
90 | if (flLastSeparator === undefined) {
91 | flLastSeparator = true;
92 | }
93 |
94 | var month = padWithZeros (theDate.getMonth () + 1, 2);
95 | var day = padWithZeros (theDate.getDate (), 2);
96 | var year = theDate.getFullYear ();
97 |
98 | if (flLastSeparator) {
99 | return (year + "/" + month + "/" + day + "/");
100 | }
101 | else {
102 | return (year + "/" + month + "/" + day);
103 | }
104 | }
105 | function multipleReplaceAll (s, adrTable, flCaseSensitive, startCharacters, endCharacters) {
106 | if(flCaseSensitive===undefined){
107 | flCaseSensitive = false;
108 | }
109 | if(startCharacters===undefined){
110 | startCharacters="";
111 | }
112 | if(endCharacters===undefined){
113 | endCharacters="";
114 | }
115 | for( var item in adrTable){
116 | var replacementValue = adrTable[item];
117 | var regularExpressionModifier = "g";
118 | if(!flCaseSensitive){
119 | regularExpressionModifier = "gi";
120 | }
121 | var regularExpressionString = (startCharacters+item+endCharacters).replace(/([.?*+^$[\]\\(){}|-])/g, "\\$1");
122 | var regularExpression = new RegExp(regularExpressionString, regularExpressionModifier);
123 | s = s.replace(regularExpression, replacementValue);
124 | }
125 | return s;
126 | }
127 | function endsWith (s, possibleEnding, flUnicase) {
128 | if ((s === undefined) || (s.length == 0)) {
129 | return (false);
130 | }
131 | var ixstring = s.length - 1;
132 | if (flUnicase === undefined) {
133 | flUnicase = true;
134 | }
135 | if (flUnicase) {
136 | for (var i = possibleEnding.length - 1; i >= 0; i--) {
137 | if (stringLower (s [ixstring--]) != stringLower (possibleEnding [i])) {
138 | return (false);
139 | }
140 | }
141 | }
142 | else {
143 | for (var i = possibleEnding.length - 1; i >= 0; i--) {
144 | if (s [ixstring--] != possibleEnding [i]) {
145 | return (false);
146 | }
147 | }
148 | }
149 | return (true);
150 | }
151 | function stringContains (s, whatItMightContain, flUnicase) { //11/9/14 by DW
152 | if (flUnicase === undefined) {
153 | flUnicase = true;
154 | }
155 | if (flUnicase) {
156 | s = s.toLowerCase ();
157 | whatItMightContain = whatItMightContain.toLowerCase ();
158 | }
159 | return (s.indexOf (whatItMightContain) != -1);
160 | }
161 | function beginsWith (s, possibleBeginning, flUnicase) {
162 | if (s === undefined) { //7/15/15 by DW
163 | return (false);
164 | }
165 | if (s.length == 0) { //1/1/14 by DW
166 | return (false);
167 | }
168 | if (flUnicase === undefined) {
169 | flUnicase = true;
170 | }
171 | if (flUnicase) {
172 | for (var i = 0; i < possibleBeginning.length; i++) {
173 | if (stringLower (s [i]) != stringLower (possibleBeginning [i])) {
174 | return (false);
175 | }
176 | }
177 | }
178 | else {
179 | for (var i = 0; i < possibleBeginning.length; i++) {
180 | if (s [i] != possibleBeginning [i]) {
181 | return (false);
182 | }
183 | }
184 | }
185 | return (true);
186 | }
187 | function isAlpha (ch) {
188 | return (((ch >= 'a') && (ch <= 'z')) || ((ch >= 'A') && (ch <= 'Z')));
189 | }
190 | function isNumeric (ch) {
191 | return ((ch >= '0') && (ch <= '9'));
192 | }
193 | function trimLeading (s, ch) {
194 | while (s.charAt (0) === ch) {
195 | s = s.substr (1);
196 | }
197 | return (s);
198 | }
199 | function trimTrailing (s, ch) {
200 | while (s.charAt (s.length - 1) === ch) {
201 | s = s.substr (0, s.length - 1);
202 | }
203 | return (s);
204 | }
205 | function trimWhitespace (s) { //rewrite -- 5/30/14 by DW
206 | function isWhite (ch) {
207 | switch (ch) {
208 | case " ": case "\r": case "\n": case "\t":
209 | return (true);
210 | }
211 | return (false);
212 | }
213 | if (s === undefined) { //9/10/14 by DW
214 | return ("");
215 | }
216 | while (isWhite (s.charAt (0))) {
217 | s = s.substr (1);
218 | }
219 | while (s.length > 0) {
220 | if (!isWhite (s.charAt (0))) {
221 | break;
222 | }
223 | s = s.substr (1);
224 | }
225 | while (s.length > 0) {
226 | if (!isWhite (s.charAt (s.length - 1))) {
227 | break;
228 | }
229 | s = s.substr (0, s.length - 1);
230 | }
231 | return (s);
232 | }
233 | function addPeriodAtEnd (s) {
234 | s = trimWhitespace (s);
235 | if (s.length == 0) {
236 | return (s);
237 | }
238 | switch (s [s.length - 1]) {
239 | case ".":
240 | case ",":
241 | case "?":
242 | case "\"":
243 | case "'":
244 | case ":":
245 | case ";":
246 | case "!":
247 | return (s);
248 | default:
249 | return (s + ".");
250 | }
251 | }
252 | function getBoolean (val) { //12/5/13 by DW
253 | switch (typeof (val)) {
254 | case "string":
255 | if (val.toLowerCase () == "true") {
256 | return (true);
257 | }
258 | break;
259 | case "boolean":
260 | return (val);
261 | case "number":
262 | if (val == 1) {
263 | return (true);
264 | }
265 | break;
266 | }
267 | return (false);
268 | }
269 | function bumpUrlString (s) { //5/10/14 by DW
270 | if (s === undefined) {
271 | s = "0";
272 | }
273 | function bumpChar (ch) {
274 | function num (ch) {
275 | return (ch.charCodeAt (0));
276 | }
277 | if ((ch >= "0") && (ch <= "8")) {
278 | ch = String.fromCharCode (num (ch) + 1);
279 | }
280 | else {
281 | if (ch == "9") {
282 | ch = "a";
283 | }
284 | else {
285 | if ((ch >= "a") && (ch <= "y")) {
286 | ch = String.fromCharCode (num (ch) + 1);
287 | }
288 | else {
289 | throw "rollover!";
290 | }
291 | }
292 | }
293 | return (ch);
294 | }
295 | try {
296 | var chlast = bumpChar (s [s.length - 1]);
297 | s = s.substr (0, s.length - 1) + chlast;
298 | return (s);
299 | }
300 | catch (tryError) {
301 | if (s.length == 1) {
302 | return ("00");
303 | }
304 | else {
305 | s = s.substr (0, s.length - 1);
306 | s = bumpUrlString (s) + "0";
307 | return (s);
308 | }
309 | }
310 | }
311 | function stringDelete (s, ix, ct) {
312 | var start = ix - 1;
313 | var end = (ix + ct) - 1;
314 | var s1 = s.substr (0, start);
315 | var s2 = s.substr (end);
316 | return (s1 + s2);
317 | }
318 | function replaceAll (s, searchfor, replacewith) {
319 | function escapeRegExp (string) {
320 | return string.replace(/([.*+?^=!:${}()|\[\]\/\\])/g, "\\$1");
321 | }
322 | return (s.replace (new RegExp (escapeRegExp (searchfor), 'g'), replacewith));
323 | }
324 | function stringCountFields (s, chdelim) {
325 | var ct = 1;
326 | if (s.length == 0) {
327 | return (0);
328 | }
329 | for (var i = 0; i < s.length; i++) {
330 | if (s [i] == chdelim) {
331 | ct++;
332 | }
333 | }
334 | return (ct)
335 | }
336 | function stringNthField (s, chdelim, n) {
337 | var splits = s.split (chdelim);
338 | if (splits.length >= n) {
339 | return splits [n-1];
340 | }
341 | return ("");
342 | }
343 | function dateYesterday (d) {
344 | return (new Date (new Date (d) - (24 * 60 * 60 * 1000)));
345 | }
346 | function stripMarkup (s) { //5/24/14 by DW
347 | if ((s === undefined) || (s == null) || (s.length == 0)) {
348 | return ("");
349 | }
350 | return (s.replace (/(<([^>]+)>)/ig, ""));
351 | }
352 | function maxStringLength (s, len, flWholeWordAtEnd, flAddElipses) {
353 | if ((s === undefined) || (s === null)) {
354 | return ("");
355 | }
356 | else {
357 | if (flWholeWordAtEnd === undefined) {
358 | flWholeWordAtEnd = true;
359 | }
360 | if (flAddElipses === undefined) { //6/2/14 by DW
361 | flAddElipses = true;
362 | }
363 | if (s.length > len) {
364 | s = s.substr (0, len);
365 | if (flWholeWordAtEnd) {
366 | while (s.length > 0) {
367 | if (s [s.length - 1] == " ") {
368 | if (flAddElipses) {
369 | s += "...";
370 | }
371 | break;
372 | }
373 | s = s.substr (0, s.length - 1); //pop last char
374 | }
375 | }
376 | }
377 | return (s);
378 | }
379 | }
380 | function random (lower, upper) {
381 | var range = upper - lower + 1;
382 | return (Math.floor ((Math.random () * range) + lower));
383 | }
384 | function removeMultipleBlanks (s) { //7/30/14 by DW
385 | return (s.toString().replace (/ +/g, " "));
386 | }
387 | function jsonStringify (jstruct, flFixBreakage) { //7/30/14 by DW
388 | //Changes
389 | //6/16/15; 10:43:25 AM by DW
390 | //Andrew Shell reported an issue in the encoding of JSON that's solved by doing character replacement.
391 | //However, this is too big a change to make for all the code that calls this library routine, so we added a boolean flag, flFixBreakage.
392 | //If this proves to be harmless, we'll change the default to true.
393 | //http://river4.smallpict.com/2015/06/16/jsonEncodingIssueSolved.html
394 | if (flFixBreakage === undefined) {
395 | flFixBreakage = false;
396 | }
397 | var s = JSON.stringify (jstruct, undefined, 4);
398 | if (flFixBreakage) {
399 | s = s.replace (/\u2028/g,'\\u2028').replace (/\u2029/g,'\\u2029');
400 | }
401 | return (s);
402 | }
403 | function stringAddCommas (x) { //5/27/14 by DW
404 | return x.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",");
405 | }
406 | function readHttpFile (url, callback, timeoutInMilliseconds, headers) { //5/27/14 by DW xxx
407 | if (timeoutInMilliseconds === undefined) {
408 | timeoutInMilliseconds = 30000;
409 | }
410 | var jxhr = $.ajax ({
411 | url: url,
412 | dataType: "text",
413 | headers: headers,
414 | timeout: timeoutInMilliseconds
415 | })
416 | .success (function (data, status) {
417 | callback (data);
418 | })
419 | .error (function (status) {
420 | console.log ("readHttpFile: url == " + url + ", error == " + jsonStringify (status));
421 | callback (undefined);
422 | });
423 | }
424 | function readHttpFileThruProxy (url, type, callback) { //10/25/14 by DW
425 | var urlReadFileApi = "http://pub2.fargo.io/httpReadUrl"; //"http://pub2.fargo.io:5347/httpReadUrl";
426 | if (type === undefined) {
427 | type = "text/plain";
428 | }
429 | var urlAjax = urlReadFileApi + "?url=" + encodeURIComponent (url) + "&type=" + encodeURIComponent (type);
430 | var jxhr = $.ajax ({
431 | url: urlAjax,
432 | dataType: "text" ,
433 | timeout: 30000
434 | })
435 | .success (function (data, status) {
436 | if (callback != undefined) {
437 | callback (data);
438 | }
439 | })
440 | .error (function (status) {
441 | console.log ("readHttpFileThruProxy: url == " + url + ", error == " + status.statusText + ".");
442 | if (callback != undefined) {
443 | callback (undefined);
444 | }
445 | });
446 | }
447 | function stringPopLastField (s, chdelim) { //5/28/14 by DW
448 | if (s.length == 0) {
449 | return (s);
450 | }
451 | if (endsWith (s, chdelim)) {
452 | s = stringDelete (s, s.length, 1);
453 | }
454 | while (s.length > 0) {
455 | if (endsWith (s, chdelim)) {
456 | return (stringDelete (s, s.length, 1));
457 | }
458 | s = stringDelete (s, s.length, 1);
459 | }
460 | return (s);
461 | }
462 | function stringPopExtension (s) { //4/29/15 by DW
463 | for (var i = s.length - 1; i >= 0; i--) {
464 | if (s [i] == ".") {
465 | return (stringMid (s, 1, i));
466 | }
467 | }
468 | return (s);
469 | }
470 | function filledString (ch, ct) { //6/4/14 by DW
471 | var s = "";
472 | for (var i = 0; i < ct; i++) {
473 | s += ch;
474 | }
475 | return (s);
476 | }
477 | function encodeXml (s) { //7/15/14 by DW
478 | if (s === undefined) {
479 | return ("");
480 | }
481 | else {
482 | var charMap = {
483 | '<': '<',
484 | '>': '>',
485 | '&': '&',
486 | '"': '&'+'quot;'
487 | };
488 | s = s.toString();
489 | s = s.replace(/\u00A0/g, " ");
490 | var escaped = s.replace(/[<>&"]/g, function(ch) {
491 | return charMap [ch];
492 | });
493 | return escaped;
494 | }
495 | }
496 | function decodeXml (s) { //11/7/14 by DW
497 | return (s.replace (/</g,'<').replace(/>/g,'>').replace(/&/g,'&'));
498 | }
499 | function hotUpText (s, url) { //7/18/14 by DW
500 |
501 | if (url === undefined) { //makes it easier to call -- 3/14/14 by DW
502 | return (s);
503 | }
504 |
505 | function linkit (s) {
506 | return ("" + s + "");
507 | }
508 | var ixleft = s.indexOf ("["), ixright = s.indexOf ("]");
509 | if ((ixleft == -1) || (ixright == -1)) {
510 | return (linkit (s));
511 | }
512 | if (ixright < ixleft) {
513 | return (linkit (s));
514 | }
515 |
516 | var linktext = s.substr (ixleft + 1, ixright - ixleft - 1); //string.mid (s, ixleft, ixright - ixleft + 1);
517 | linktext = "" + linktext + "";
518 |
519 | var leftpart = s.substr (0, ixleft);
520 | var rightpart = s.substr (ixright + 1, s.length);
521 | s = leftpart + linktext + rightpart;
522 | return (s);
523 | }
524 | function getDomainFromUrl (url) { //7/11/15 by DW
525 | if ((url != null ) && (url != "")) {
526 | url = url.replace("www.","").replace("www2.", "").replace("feedproxy.", "").replace("feeds.", "");
527 | var root = url.split('?')[0]; // cleans urls of form http://domain.com?a=1&b=2
528 | url = root.split('/')[2];
529 | }
530 | return (url);
531 | };
532 | function getFavicon (url) { //7/18/14 by DW
533 | var domain = getDomainFromUrl (url);
534 | return ("http://www.google.com/s2/favicons?domain=" + domain);
535 | };
536 | function getURLParameter (name) { //7/21/14 by DW
537 | return (decodeURI ((RegExp(name + '=' + '(.+?)(&|$)').exec(location.search)||[,null])[1]));
538 | }
539 | function urlSplitter (url) { //7/15/14 by DW
540 | var pattern = /^(?:([A-Za-z]+):)?(\/{0,3})([0-9.\-A-Za-z]+)(?::(\d+))?(?:\/([^?#]*))?(?:\?([^#]*))?(?:#(.*))?$/;
541 | var result = pattern.exec (url);
542 | if (result == null) {
543 | result = [];
544 | result [5] = url;
545 | }
546 | var splitUrl = {
547 | scheme: result [1],
548 | host: result [3],
549 | port: result [4],
550 | path: result [5],
551 | query: result [6],
552 | hash: result [7]
553 | };
554 | return (splitUrl);
555 | }
556 | function innerCaseName (text) { //8/12/14 by DW
557 | var s = "", ch, flNextUpper = false;
558 | text = stripMarkup (text);
559 | for (var i = 0; i < text.length; i++) {
560 | ch = text [i];
561 | if (isAlpha (ch) || isNumeric (ch)) {
562 | if (flNextUpper) {
563 | ch = ch.toUpperCase ();
564 | flNextUpper = false;
565 | }
566 | else {
567 | ch = ch.toLowerCase ();
568 | }
569 | s += ch;
570 | }
571 | else {
572 | if (ch == ' ') {
573 | flNextUpper = true;
574 | }
575 | }
576 | }
577 | return (s);
578 | }
579 | function hitCounter (counterGroup, counterServer) { //8/12/14 by DW
580 | var defaultCounterGroup = "scripting", defaultCounterServer = "http://counter2.fargo.io:5337/counter";
581 | var thispageurl = location.href;
582 | if (counterGroup === undefined) {
583 | counterGroup = defaultCounterGroup;
584 | }
585 | if (counterServer === undefined) {
586 | counterServer = defaultCounterServer;
587 | }
588 | if (thispageurl === undefined) {
589 | thispageurl = "";
590 | }
591 | if (endsWith (thispageurl, "#")) {
592 | thispageurl = thispageurl.substr (0, thispageurl.length - 1);
593 | }
594 | var jxhr = $.ajax ({
595 | url: counterServer + "?group=" + encodeURIComponent (counterGroup) + "&referer=" + encodeURIComponent (document.referrer) + "&url=" + encodeURIComponent (thispageurl),
596 | dataType: "jsonp",
597 | jsonpCallback : "getData",
598 | timeout: 30000
599 | })
600 | .success (function (data, status, xhr) {
601 | console.log ("hitCounter: counter ping accepted by server, group == " + counterGroup + ", page url == " + thispageurl);
602 | })
603 | .error (function (status, textStatus, errorThrown) {
604 | console.log ("hitCounter: counter ping error: " + textStatus);
605 | });
606 | }
607 | function stringMid (s, ix, len) { //8/12/14 by DW
608 | return (s.substr (ix-1, len));
609 | }
610 | function getCmdKeyPrefix () { //8/15/14 by DW
611 | if (navigator.platform.toLowerCase ().substr (0, 3) == "mac") {
612 | return ("⌘");
613 | }
614 | else {
615 | return ("Ctrl+");
616 | }
617 | }
618 | function getRandomSnarkySlogan () { //8/15/14 by DW
619 | var snarkySlogans = [
620 | "Good for the environment.",
621 | "All baking done on premises.",
622 | "Still diggin!",
623 | "It's even worse than it appears.",
624 | "You should never argue with a crazy man.",
625 | "Welcome back my friends to the show that never ends.",
626 | "Greetings, citizen of Planet Earth. We are your overlords. :-)",
627 | "We don't need no stinkin rock stars.",
628 | "This aggression will not stand.",
629 | "Pay no attention to the man behind the curtain.",
630 | "Only steal from the best.",
631 | "Reallll soooon now...",
632 | "What a long strange trip it's been.",
633 | "Ask not what the Internet can do for you.",
634 | "When in doubt, blog.",
635 | "Shut up and eat your vegetables.",
636 | "Don't slam the door on the way out.",
637 | "Yeah well, that's just, you know, like, your opinion, man.",
638 | "So, it has come to this.",
639 | "We now return to our regularly scheduled program.",
640 | "That rug really tied the room together.",
641 | "It's a good time for a backup.",
642 | "Takes a lickin, keeps on tickin.",
643 | "People return to places that send them away."
644 | ]
645 | return (snarkySlogans [random (0, snarkySlogans.length - 1)]);
646 | }
647 | function dayOfWeekToString (theDay) { //8/23/14 by DW
648 | var weekday = [
649 | "Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"
650 | ];
651 | return (weekday[theDay]);
652 | }
653 | function viewDate (when, flShortDayOfWeek) { //8/23/14 by DW
654 | var now = new Date ();
655 | when = new Date (when);
656 | if (sameDay (when, now)) {
657 | return (timeString (when, false)) //2/9/13 by DW;
658 | }
659 | else {
660 | var oneweek = 1000 * 60 * 60 * 24 * 7;
661 | var cutoff = now - oneweek;
662 | if (when > cutoff) { //within the last week
663 | var s = dayOfWeekToString (when.getDay ());
664 | if (flShortDayOfWeek) {
665 | s = s.substring (0, 3);
666 | }
667 | return (s);
668 | }
669 | else {
670 | return (when.toLocaleDateString ());
671 | }
672 | }
673 | }
674 | function timeString (when, flIncludeSeconds) { //8/26/14 by DW
675 | var hour = when.getHours (), minutes = when.getMinutes (), ampm = "AM", s;
676 | if (hour >= 12) {
677 | ampm = "PM";
678 | }
679 | if (hour > 12) {
680 | hour -= 12;
681 | }
682 | if (hour == 0) {
683 | hour = 12;
684 | }
685 | if (minutes < 10) {
686 | minutes = "0" + minutes;
687 | }
688 | if (flIncludeSeconds) {
689 | var seconds = when.getSeconds ();
690 | if (seconds < 10) {
691 | seconds = "0" + seconds;
692 | }
693 | s = hour + ":" + minutes + ":" + seconds + ampm;
694 | }
695 | else {
696 | s = hour + ":" + minutes + ampm;
697 | }
698 | return (s);
699 | }
700 | function stringLastField (s, chdelim) { //8/27/14 by DW
701 | var ct = stringCountFields (s, chdelim);
702 | if (ct == 0) { //8/31/14 by DW
703 | return (s);
704 | }
705 | return (stringNthField (s, chdelim, ct));
706 | }
707 | function maxLengthString (s, maxlength) { //8/27/14 by DW
708 | if (s.length > maxlength) {
709 | s = s.substr (0, maxlength);
710 | while (true) {
711 | var len = s.length; flbreak = false;
712 | if (len == 0) {
713 | break;
714 | }
715 | if (s [len - 1] == " ") {
716 | flbreak = true;
717 | }
718 | s = s.substr (0, len - 1);
719 | if (flbreak) {
720 | break;
721 | }
722 | }
723 | s = s + "...";
724 | }
725 | return (s);
726 | }
727 | function formatDate (theDate, dateformat, timezone) { //8/28/14 by DW
728 | if (theDate === undefined) {
729 | theDate = new Date ();
730 | }
731 | if (dateformat === undefined) {
732 | dateformat = "%c";
733 | }
734 | if (timezone === undefined) {
735 | timezone = - (new Date ().getTimezoneOffset () / 60);
736 | }
737 | try {
738 | var offset = new Number (timezone);
739 | var d = new Date (theDate);
740 | var localTime = d.getTime ();
741 | var localOffset = d.getTimezoneOffset () * 60000;
742 | var utc = localTime + localOffset;
743 | var newTime = utc + (3600000 * offset);
744 | return (new Date (newTime).strftime (dateformat));
745 | }
746 | catch (tryerror) {
747 | return (new Date (theDate).strftime (dateformat));
748 | }
749 | }
750 | function addPeriodToSentence (s) { //8/29/14 by DW
751 | if (s.length > 0) {
752 | var fladd = true;
753 | var ch = s [s.length - 1];
754 | switch (ch) {
755 | case "!": case "?": case ":":
756 | fladd = false;
757 | break;
758 | default:
759 | if (endsWith (s, ".\"")) {
760 | fladd = false;
761 | }
762 | else {
763 | if (endsWith (s, ".'")) {
764 | fladd = false;
765 | }
766 | }
767 | }
768 | if (fladd) {
769 | s += ".";
770 | }
771 | }
772 | return (s);
773 | }
774 | function copyScalars (source, dest) { //8/31/14 by DW
775 | for (var x in source) {
776 | var type, val = source [x];
777 | if (val instanceof Date) {
778 | val = val.toString ();
779 | }
780 | type = typeof (val);
781 | if ((type != "object") && (type != undefined)) {
782 | dest [x] = val;
783 | }
784 | }
785 | }
786 | function linkToDomainFromUrl (url, flshort, maxlength) { //10/10/14 by DW
787 | var splitUrl = urlSplitter (url), host;
788 | if (splitUrl.host === undefined) { //1/21/16 by DW
789 | host = "";
790 | }
791 | else {
792 | host = splitUrl.host.toLowerCase ();
793 | }
794 | if (flshort === undefined) {
795 | flshort = false;
796 | }
797 | if (flshort) {
798 | var splithost = host.split (".");
799 | if (splithost.length == 3) {
800 | host = splithost [1];
801 | }
802 | else {
803 | host = splithost [0];
804 | }
805 | }
806 | else {
807 | if (beginsWith (host, "www.")) {
808 | host = stringDelete (host, 1, 4);
809 | }
810 | }
811 |
812 | if (maxlength != undefined) { //10/10/14; 10:46:56 PM by DW
813 | if (host.length > maxlength) {
814 | host = stringMid (host, 1, maxlength) + "...";
815 | }
816 | }
817 |
818 | return ("" + host + "");
819 | }
820 | function getRandomPassword (ctchars) { //10/14/14 by DW
821 | var s= "", ch;
822 | while (s.length < ctchars) {
823 | ch = String.fromCharCode (random (33, 122));
824 | if (isAlpha (ch) || isNumeric (ch)) {
825 | s += ch;
826 | }
827 | }
828 | return (s.toLowerCase ());
829 | }
830 | function monthToString (theMonthNum) { //11/4/14 by DW
831 |
832 |
833 | var theDate;
834 | if (theMonthNum === undefined) {
835 | theDate = new Date ();
836 | }
837 | else {
838 | theDate = new Date ((theMonthNum + 1) + "/1/2014");
839 | }
840 | return (formatDate (theDate, "%B"));
841 | }
842 | function getCanonicalName (text) { //11/4/14 by DW
843 | var s = "", ch, flNextUpper = false;
844 | text = stripMarkup (text); //6/30/13 by DW
845 | for (var i = 0; i < text.length; i++) {
846 | ch = text [i];
847 | if (isAlpha (ch) || isNumeric (ch)) {
848 | if (flNextUpper) {
849 | ch = ch.toUpperCase ();
850 | flNextUpper = false;
851 | }
852 | else {
853 | ch = ch.toLowerCase ();
854 | }
855 | s += ch;
856 | }
857 | else {
858 | if (ch == ' ') {
859 | flNextUpper = true;
860 | }
861 | }
862 | }
863 | return (s);
864 | }
865 | function clockNow () { //11/7/14 by DW
866 | return (new Date ());
867 | }
868 | function sleepTillTopOfMinute (callback) { //11/22/14 by DW
869 | var ctseconds = Math.round (60 - (new Date ().getSeconds () + 60) % 60);
870 | if (ctseconds == 0) {
871 | ctseconds = 60;
872 | }
873 | setTimeout (callback, ctseconds * 1000); //8/13/15 by DW -- was hard-coded to "everyMinute" ignored the callback param, fixed
874 | }
875 | function scheduleNextRun (callback, ctMillisecsBetwRuns) { //11/27/14 by DW
876 | var ctmilliseconds = ctMillisecsBetwRuns - (Number (new Date ().getMilliseconds ()) + ctMillisecsBetwRuns) % ctMillisecsBetwRuns;
877 | setTimeout (callback, ctmilliseconds);
878 | }
879 | function urlEncode (s) { //12/4/14 by DW
880 | return (encodeURIComponent (s));
881 | }
882 | function popTweetNameAtStart (s) { //12/8/14 by DW
883 | var ch;
884 | s = trimWhitespace (s);
885 | if (s.length > 0) {
886 | if (s.charAt (0) == "@") {
887 | while (s.charAt (0) != " ") {
888 | s = s.substr (1)
889 | }
890 | while (s.length > 0) {
891 | ch = s.charAt (0);
892 | if ((ch != " ") && (ch != "-")) {
893 | break;
894 | }
895 | s = s.substr (1)
896 | }
897 | }
898 | }
899 | return (s);
900 | }
901 | function httpHeadRequest (url, callback) { //12/17/14 by DW
902 | var jxhr = $.ajax ({
903 | url: url,
904 | type: "HEAD",
905 | dataType: "text",
906 | timeout: 30000
907 | })
908 | .success (function (data, status, xhr) {
909 | callback (xhr); //you can do xhr.getResponseHeader to get one of the header elements
910 | })
911 | }
912 | function httpExt2MIME (ext) { //12/24/14 by DW
913 | var lowerext = stringLower (ext);
914 | var map = {
915 | "au": "audio/basic",
916 | "avi": "application/x-msvideo",
917 | "bin": "application/x-macbinary",
918 | "css": "text/css",
919 | "dcr": "application/x-director",
920 | "dir": "application/x-director",
921 | "dll": "application/octet-stream",
922 | "doc": "application/msword",
923 | "dtd": "text/dtd",
924 | "dxr": "application/x-director",
925 | "exe": "application/octet-stream",
926 | "fatp": "text/html",
927 | "ftsc": "text/html",
928 | "fttb": "text/html",
929 | "gif": "image/gif",
930 | "gz": "application/x-gzip",
931 | "hqx": "application/mac-binhex40",
932 | "htm": "text/html",
933 | "html": "text/html",
934 | "jpeg": "image/jpeg",
935 | "jpg": "image/jpeg",
936 | "js": "application/javascript",
937 | "mid": "audio/x-midi",
938 | "midi": "audio/x-midi",
939 | "mov": "video/quicktime",
940 | "mp3": "audio/mpeg",
941 | "pdf": "application/pdf",
942 | "png": "image/png",
943 | "ppt": "application/mspowerpoint",
944 | "ps": "application/postscript",
945 | "ra": "audio/x-pn-realaudio",
946 | "ram": "audio/x-pn-realaudio",
947 | "sit": "application/x-stuffit",
948 | "sys": "application/octet-stream",
949 | "tar": "application/x-tar",
950 | "text": "text/plain",
951 | "txt": "text/plain",
952 | "wav": "audio/x-wav",
953 | "wrl": "x-world/x-vrml",
954 | "xml": "text/xml",
955 | "zip": "application/zip"
956 | };
957 | for (x in map) {
958 | if (stringLower (x) == lowerext) {
959 | return (map [x]);
960 | }
961 | }
962 | return ("text/plain");
963 | }
964 | function kilobyteString (num) { //1/24/15 by DW
965 | num = Number (num) / 1024;
966 | return (num.toFixed (2) + "K");
967 | }
968 | function megabyteString (num) { //1/24/15 by DW
969 | var onemeg = 1024 * 1024;
970 | if (num <= onemeg) {
971 | return (kilobyteString (num));
972 | }
973 | num = Number (num) / onemeg;
974 | return (num.toFixed (2) + "MB");
975 | }
976 | function gigabyteString (num) { //1/24/15 by DW
977 | var onegig = 1024 * 1024 * 1024;
978 | if (num <= onegig) {
979 | return (megabyteString (num));
980 | }
981 | num = Number (num) / onegig;
982 | return (num.toFixed (2) + "GB");
983 | }
984 | function dateToNumber (theDate) { //2/15/15 by DW
985 | return (Number (new Date (theDate)));
986 | }
987 | function getFileModDate (f, callback) { //8/26/15 by DW
988 | fs.exists (f, function (flExists) {
989 | if (flExists) {
990 | fs.stat (f, function (err, stats) {
991 | if (err) {
992 | callback (undefined);
993 | }
994 | else {
995 | callback (new Date (stats.mtime).toString ());
996 | }
997 | });
998 | }
999 | else {
1000 | callback (undefined);
1001 | }
1002 | });
1003 | }
1004 | function getFileCreationDate (f, callback) { //12/15/15 by DW
1005 | fs.exists (f, function (flExists) {
1006 | if (flExists) {
1007 | fs.stat (f, function (err, stats) {
1008 | if (err) {
1009 | callback (undefined);
1010 | }
1011 | else {
1012 | callback (new Date (stats.birthtime).toString ());
1013 | }
1014 | });
1015 | }
1016 | else {
1017 | callback (undefined);
1018 | }
1019 | });
1020 | }
1021 | function getAppUrl () { //11/13/15 by DW
1022 | var url = stringNthField (window.location.href, "?", 1);
1023 | url = stringNthField (url, "#", 1);
1024 | return (url);
1025 | }
1026 | function getFacebookTimeString (when) { //11/13/15 by DW
1027 | when = new Date (when); //make sure it's a date
1028 | var ctsecs = secondsSince (when), ct, s;
1029 | if (ctsecs < 60) {
1030 | return ("Just now");
1031 | }
1032 |
1033 | var ctminutes = ctsecs / 60;
1034 | if (ctminutes < 60) {
1035 | ct = Math.floor (ctminutes);
1036 | s = ct + " min";
1037 | if (ct != 1) {
1038 | s += "s";
1039 | }
1040 | return (s);
1041 | }
1042 |
1043 | var cthours = ctminutes / 60;
1044 | if (cthours < 24) {
1045 | ct = Math.floor (cthours);
1046 | s = ct + " hr";
1047 | if (ct != 1) {
1048 | s += "s";
1049 | }
1050 | return (s);
1051 | }
1052 |
1053 | var now = new Date ();
1054 | if (sameDay (when, dateYesterday (now))) {
1055 | return ("Yesterday at " + formatDate (when, "%l:%M %p"));
1056 | }
1057 |
1058 | var formatstring = "%b %e";
1059 | if (when.getFullYear () != now.getFullYear ()) {
1060 | formatstring += ", %Y";
1061 | }
1062 | return (formatDate (when, formatstring));
1063 | }
1064 | function stringUpper (s) { //11/15/15 by DW
1065 | if (s === undefined) {
1066 | return ("");
1067 | }
1068 | s = s.toString ();
1069 | return (s.toUpperCase ());
1070 | }
1071 |
1072 | function upperCaseFirstChar (s) { //11/15/15 by DW
1073 | if ((s === undefined) || (s.length == 0)) {
1074 | return ("");
1075 | }
1076 | s = stringUpper (s [0]) + stringDelete (s, 1, 1);
1077 | return (s);
1078 | }
1079 | function cacheConfuse (url) { //3/1/16 by DW
1080 | return (url + "?x=" + random (0, 10000000));
1081 | }
1082 | function equalStrings (s1, s2, flUnicase) { //4/7/16 by DW
1083 | if (flUnicase === undefined) {
1084 | flUnicase = true;
1085 | }
1086 | if (flUnicase) {
1087 | return (s1.toLowerCase () == s2.toLowerCase ());
1088 | }
1089 | else {
1090 | return (s1 == s2);
1091 | }
1092 | }
1093 | function stringInsert (source, dest, ix) { //8/8/16 by DW
1094 | return (dest.substr (0, ix) + source + dest.substr (ix));
1095 | }
1096 |
1097 |
--------------------------------------------------------------------------------
/code/monthlyArchives/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "monthlyArchiveFor1999",
3 | "description": "Generate a folder structure of monthly archives for a 1999.io site.",
4 | "author": "Dave Winer ",
5 | "version": "0.40.0",
6 | "dependencies" : {
7 | "dateformat": "*",
8 | "request": "*"
9 | },
10 | "license": "MIT",
11 | "engines": {
12 | "node": "0.10.*"
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/code/monthlyArchives/template.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | [%title%]
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
21 |
22 |
23 |
24 |
[%title%]
25 | [%bodytext%]
26 |
27 |
32 |
33 |
34 |
--------------------------------------------------------------------------------
/code/pingserver.js:
--------------------------------------------------------------------------------
1 | /* This script handles requests that come into ping.1999.io
2 | It maintains a JSON file in the same directory as the script that counts updates
3 | for each RSS file maintained by each of the participating 1999 servers.
4 |
5 | It's designed to be run by the PagePark web server, but could be modified to be run
6 | in any web server that can run JS scripts.
7 |
8 | 4/4/16; 10:45:25 AM by DW
9 | */
10 | var urlFeed = parsedUrl.query.urlFeed;
11 | if (urlFeed !== undefined) {
12 | var now = new Date (), fname = "domains/ping.1999.io/pingers.json";
13 | fs.readFile (fname, function (err, data) {
14 | var jstruct = new Object ();
15 | if (!err) {
16 | jstruct = JSON.parse (data.toString ());
17 | }
18 | if (jstruct [urlFeed] === undefined) {
19 | jstruct [urlFeed] = {
20 | ct: 1,
21 | when: now
22 | }
23 | }
24 | else {
25 | jstruct [urlFeed].ct++;
26 | jstruct [urlFeed].when = now
27 | }
28 | fs.writeFile (fname, JSON.stringify (jstruct, undefined, 4));
29 | });
30 | }
31 | "Thanks for the ping!";
32 |
--------------------------------------------------------------------------------
/code/readchatlog.js:
--------------------------------------------------------------------------------
1 | var myProductName = "Read ChatLog", myVerion = "0.40a";
2 |
3 | /* This script reads a chatLog.json file
4 | We write the titles of each of the posts to the console.
5 | Each line begins with the ID of the post, followed by the title, followed by the date and time it was posted.
6 | The only point here is to prime the pump a little, to get people thinking of apps they might build off the chatLog.json files.
7 |
8 | 4/16/16 by DW
9 | */
10 |
11 | var urlMyChatLog = "http://friends.farm/users/davewiner/chatLog.json";
12 | var request = require ("request");
13 |
14 | function getTitle (item) {
15 | if ((item.payload !== undefined) && (item.payload.title !== undefined)) {
16 | return (item.payload.title);
17 | }
18 | return ("");
19 | }
20 | function getUrlRendering (item) {
21 | if ((item.payload !== undefined) && (item.payload.urlRendering !== undefined)) {
22 | return (item.payload.urlRendering);
23 | }
24 | return ("");
25 | }
26 | function getImage (item) {
27 | if ((item.payload !== undefined) && (item.payload.image !== undefined)) {
28 | return ("");
29 | }
30 | return ("");
31 | }
32 | function isPublished (item) {
33 | if ((item.payload !== undefined) && (item.payload.flPublished !== undefined)) {
34 | return (item.payload.flPublished);
35 | }
36 | return (false);
37 | }
38 | function formatDateTime (d) {
39 | d = new Date (d);
40 | return (d.toLocaleDateString () + " at " + d.toLocaleTimeString ());
41 | }
42 |
43 | request (urlMyChatLog, function (err, response, jsontext) {
44 | if (err) {
45 | console.log ("readchatlog.js: err.message == " + err.message);
46 | }
47 | else {
48 | var theLog = JSON.parse (jsontext);
49 | for (var i = theLog.chatLog.length - 1; i >= 0; i--) { //list in reverse chronologic order
50 | var item = theLog.chatLog [i];
51 | if (isPublished (item)) {
52 | console.log (item.id + ": " + getTitle (item) + ", " + formatDateTime (item.when));
53 | }
54 | }
55 | }
56 | });
57 |
58 |
--------------------------------------------------------------------------------
/code/templateedit.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | Template Editor
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
103 |
127 |
128 |
129 |
45 |
50 |
51 |
52 |
--------------------------------------------------------------------------------
/defaults/template.opml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | template4.opml
5 | <%dateModified%>
6 |
7 | 1
8 | 300
9 | 700
10 | 900
11 | 1500
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 |
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 |
--------------------------------------------------------------------------------
/docs/callbacks.md:
--------------------------------------------------------------------------------
1 | ## Callbacks for 1999 server
2 |
3 | The callbacks folder is automatically created at the top level of the nodeStorage folder when the first post is published.
4 |
5 | It has a single sub-folder called publish. Each script in that folder will be called for each post that's published. A script is a file with a .js extension. The script has four values set up as globals: relpath, body, type and screenName.
6 |
7 | relpath is the path to the file that's being written relative to the base of the user's folder. Something like 2018/05/02/0012.html.
8 |
9 | body is the contents of the file.
10 |
11 | type is a mime type, something like text/html or application/json.
12 |
13 | screenName is the name of the user who is publishing the file.
14 |
15 | It's enough information to mirror the file in another location, as an example. I use it to keep a copy of my writing in a folder on scripting.com.
16 |
17 | Here's an example of a callback script for publish.
18 |
19 |
--------------------------------------------------------------------------------
/docs/docker.md:
--------------------------------------------------------------------------------
1 | ## Running 1999 server on Docker
2 |
3 | My longtime friend Don Park got this together. Many thanks to Don! ;-)
4 |
5 | #### You need Docker, of course
6 |
7 | The first step is to get Docker running on your server.
8 |
9 | I created a server on Digital Ocean and then followed their instructions for installing Docker. I was able to get the 1999 server to run on a $5 a month server. Not saying how well it will run, but it does run.
10 |
11 | #### Preparing
12 |
13 | You need to have three bits of information available:
14 |
15 | 1. A domain name for the server.
16 |
17 | 2. Your Twitter consumer key, as explained in the docs for config.json.
18 |
19 | 3. The Twitter consumer secret.
20 |
21 | #### The command
22 |
23 | Here's a command that launches the server with the HTTP server on port 80, and the websocket server on port 2000.
24 |
25 | The domain name is oakland.myserver.com (it should point to this server, of course), the consumer key is 12345 and the secret is 67890.
26 |
27 |
38 |
39 | After running the command you should be able to access your server through http://oakland.myserver.com/.
40 |
41 | #### Frequent commands
42 |
43 | sudo docker images -- list all the images you have installed on this machine
44 |
45 | sudo docker ps -- list the containers
46 |
47 | sudo docker stop nodestorage -- stop the nodestorage container
48 |
49 | sudo docker rm nodestorage -- remove the nodestorage container
50 |
51 | #### Pointers
52 |
53 | Amazon has a Docker service.
54 |
55 | Here's a cheat sheet website for Docker commands.
56 |
57 | CoreOS is a Linux distribution built around containers.
58 |
59 | #### Where's the source?
60 |
61 | Here's the repository for the Docker version.
62 |
63 | The JavaScript files are the same as the main project.
64 |
65 | The only difference is there's a Dockerfile in the top level of the repo, and a docker folder with scripts and data.
66 |
67 | The two projects are kept in sync by my build system.
68 |
69 | #### Questions
70 |
71 | Get help on the 1999-server mail list.
72 |
73 |
--------------------------------------------------------------------------------
/docs/domains.md:
--------------------------------------------------------------------------------
1 | ## Mapping a domain to a blog
2 |
3 | You can set up a server, through config.json, to map domains to blogs.
4 |
5 | #### Example
6 |
7 | Here's an example config.json set up to map trump.1999.io and blog.1999.io to their respective sites.
8 |
9 | #### How to
10 |
11 | Add a new top-level object in config.json called domains. It contains strings.
12 |
13 | The name of each string is a domain to be mapped.
14 |
15 | The value of each string is the path to the public file the domain points to.
16 |
17 | #### How it works
18 |
19 | The conversion happens in both directions. When a client requests a page using the domain name, we fetch the content at the location indicated by the value.
20 |
21 | And if there's a reference to a page that's accessible through the domain map, we redirect to the short version.
22 |
23 |
--------------------------------------------------------------------------------
/docs/ec2.md:
--------------------------------------------------------------------------------
1 | ## EC2 machine image for 1999 server
2 |
3 | Now you can launch a 1999.io server from an Amazon AMI.
4 |
5 | It's even easier than the basic Ubuntu setup, because I've done most of the installation for you. It just needs a few bits and you're ready to go.
6 |
7 | Note: These are not poet-level instructions, they're for people with a little experience setting up an AMI and DNS.
8 |
9 | #### How to
10 |
11 | The AMI is ami-abcd2fc6. It's public.
12 |
13 | It can be a micro instance. And it should qualify for the free tier, meaning if you're new to AWS you can run it for free for a year.
14 |
15 | These ports should be open -- 22, 80, 81, 1999, 2000.
16 |
17 | You should assign a domain to the IP address allocated for the server. That will be the value for myDomain in config.json.
18 |
19 | Once the instance is running, edit config.json, as explained in the howto, change myDomain, twitterConsumerKey and twitterConsumerSecret as explained in the setup howto. (You don't have to create it, it's already there.)
20 |
21 | Map port 80 to port 1999. Here's the magic incantation that does that:
22 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/docs/homepage.md:
--------------------------------------------------------------------------------
1 | ## How to customize the editor
2 |
3 | If you run your own 1999 server you can customize the experience for people using the 1999 editor by adding an object to your config.json file.
4 |
5 | These customizations are totally optional.
6 |
7 | #### An example
8 |
9 | Here's an example config.json that contains a homePage object.
10 |
11 | Two items are specified in that object, productnameForDisplay and urlPageTemplate.
12 |
13 | #### productnameForDisplay
14 |
15 | This sets the string that appears in the upper left corner of the editor.
16 |
17 | I've set it to BloatWare for my test server. Here's a screen shot showing what that looks like.
18 |
19 | If you're hosting blogs for other people, you might want to change that to the name of your organization.
20 |
21 | #### urlPageTemplate
22 |
23 | This sets the address of the default template used for rendering pages.
24 |
25 | It must be the address of a plain text file containing the HTML code with macros for the template file.
26 |
27 | It can be a local URL on your server, as shown in the example. I put the file in publicFiles/data/mytemplate.html.
28 |
29 | But you don't have to put it on your server, you can put it anywhere that's publicly accessible by your users' machines.
30 |
31 | If the user designs their own template it overrides this choice. It's only used when the user has not customized their template, or we can't read the user's template.
32 |
33 | #### How to test
34 |
35 | If you want to experiment with the urlPageTemplate feature, and have already edited the template using the outliner, and you're running your own server...
36 |
37 | 1. Open your sub-folder of the publicFiles folder on the server.
38 |
39 | 2. Delete template.opml.
40 |
41 | Here's a screen shot showing the location of template.opml in my server folder.
42 |
43 | Now when 1999.io renders your pages it will use the default template as specified in your homePage object.
44 |
45 | #### googleAnalyticsAccount
46 |
47 | Sets the default Google Analytics account for blogs hosted on your server.
48 |
49 | Here's an example config.json that illustrates.
50 |
51 | The feature is fully explained in a post on the 1999.io blog.
52 |
53 | #### urlHomePageIntroText
54 |
55 | When the user accesses the home page of your site, without being logged in, a message is displayed that says "Please sign on to Twitter to access your stuff."
56 |
57 | You may want to say more. This option lets you control the text and the layout of the text that the user sees.
58 |
59 | Here's an example config.json that illustrates, and this is what it looks like when you visit.
60 |
61 | Do a view-source on the page we link to from the urlHomePageIntroText value. You can see that we put the text inside the same div that the default text is in, and we center it, as the default text is centered. You can lay it out any way you want, or stay with this way of formatting.
62 |
63 | If you have more to say and want to link to other pages, you probably should come up with a better layout for text reading.
64 |
65 |
--------------------------------------------------------------------------------
/docs/macros.md:
--------------------------------------------------------------------------------
1 | ## Macros
2 |
3 | When 1999 renders a page, as part of the process, it passes the text through the pagetable.
4 |
5 | The pagetable is a JavaScript object with names and values. For example, pagetable.title might have the value Test post or pagetable.authorFacebookAccount might be bull.mancuso.
6 |
7 | As the page is rendered, [%title%] would be replaced with Test post, and [%authorFacebookAccount%] would be replaced with bull.mancuso.
8 |
9 | Here's a snapshot of the pagetable for a page I rendered on my test server. Any of those values could be used in the template for the page.
10 |
11 | #### Where the values come from
12 |
13 | The pagetable is built from a variety of sources. Some of them come from the server, like the name of the product, and the address of the API server and WebSocket server. Some of the values are stats kept by the editor, such as ctStartups, or prefs such as flAutoSave. Most of these wouldn't be too useful in a template.
14 |
15 | Some of the values come from Twitter, when we authenticate. For example, personName and profileImageUrl.
16 |
17 | #### The pagetable at runtime
18 |
19 | The pagetable is also available to JavaScript code running in the rendered page. It's a global called pagetable.
20 |
21 | This is the information the default page code uses to put up the prev and next arrows, and the elements in the footer.
22 |
23 | If you open up the JavaScript console you can inspect it. Or you can type viewPagetable () to get a JSONified rendering of the table.
24 |
25 | You'll see that there's more information accessible here, because the pagetable contains objects. Information about the prev and next story pages. All the information about the item behind the rendering as an object.
26 |
27 | Here's a snapshot of the runtime version of the pagetable for the example page.
28 |
29 | #### Historic note
30 |
31 | The pagetable has been in all my web CMSes going back to 1996 and the first website framework.
32 |
33 | It works beautifully as a way for values to be passed from the editor through the rendering process and into the page at runtime.
34 |
35 | I haven't ever come up with a better way to do it. ;-)
36 |
37 |
--------------------------------------------------------------------------------
/docs/plugins.md:
--------------------------------------------------------------------------------
1 | ## Editor plugins
2 |
3 | I want to make it easy for people to add their own editors to 1999.io, playing various roles.
4 |
5 | The plugins described here appear in the new PlugIns menu in 1999.io. When you choose one of the plugins a new page opens. What's on that page is entirely up to the plugin author. The first plugin I'm releasing is a simple lightweight template editor.
6 |
7 | Plugins talk to the server via the JavaScript API. The user does not have to log in to the plugin because they have access to the same credentials 1999.io has since they run in the same domain. The data is stored in localStorage, but you don't have to worry about that, because you access the server through the API, they already know what to do with the localStorage values.
8 |
9 | Obviously, sysops have to be careful about the plugins they run.
10 |
11 | Key point: The plugins can do anything 1999.io does. It would be possible to write a whole new editor for 1999.io websites. Or write specialized tools that do things that 1999.io does not do. The hope is that, over time, many such plugins will be written.
12 |
13 | #### How to configure
14 |
15 | There's a new top-level section in config.json, plugIns, for configuring the pluglins. Here's an example config.json that has a plugIns section.
16 |
17 | Each plugin has an identifier, which is the name of the sub-object, a value called name, which is what's displayed in the PlugIns menu in 1999.io, and url which is the address of the HTML page for the plugin.
18 |
19 | #### How the plugins are accessed
20 |
21 | Suppose you have a plugin whose identifier is imageEdit, and your server is running on designshop.com.
22 |
23 | Then the address of the plugin would be http://designshop.com/plugin?name=imageEdit
24 |
25 | #### The templateEdit plugin
26 |
27 | The only plugin I'm releasing along with the plugins feature is templateEdit.
28 |
29 | When you choose it from the PlugIns menu, a new page opens with the text of template in the edit box.
30 |
31 | When you click the Update button, the template is saved. This means that all future page builds on this site will flow through this version of the template.
32 |
33 | This is not reversible. You will no longer be able to edit the template using the outline editor in the Main menu in 1999.io. This is a very important point.
34 |
35 | However there is an escape clause. If you have access to the server, you can reverse it by deleting misc/template.html in the user's public folder.
36 |
37 | #### Writing your own plugin
38 |
39 | The source code for the templateEdit plugin is in the code folder in this repo. It's meant to serve as example code for the kinds of things plugins do.
40 |
41 | #### Using plugins in the editor
42 |
43 | Here's a blog post on the 1999 user blog, showing how to access the PlugIns menu in the 1999.io editor.
44 |
45 | #### The Editors menu in 1999.io
46 |
47 | Here's a video that demos the Editors menu in 1999.io.
48 |
49 | This is how you get an editor to appear in that menu, on the server.
50 |
51 | Here's an example config.json that adds one item to the menu, the HTML source editor.
52 |
53 | A few facts about the Editors menu.
54 |
55 | 0. The user must enable the Editors menu in the Misc panel of the Settings dialog.
56 |
57 | 1. They are only activated when the user is editing a post.
58 |
59 | 2. When the command is chosen, the editor, which is just an HTML page, is loaded, and it's provided with parameters about the item through localStorage.
60 |
61 | 3. The user edits the text of the item in the editor, and when they save, it is then sent to the server via the nodeStorage API. Since the editor is served through the same domain as 1999.io, it has access to the Twitter credentials. Obviously you should be careful about installing editors written by other people.
62 |
63 | 4. I've written an example HTML editor, do a view source on this page to see how it works.
64 |
65 |
--------------------------------------------------------------------------------
/docs/publicfiles.md:
--------------------------------------------------------------------------------
1 | ## You can put your public files anywhere
2 |
3 | There's a value in config.json called publicFiles that specifies where the server keeps the public files for each user.
4 |
5 | It defaults to publicFiles/ which stores the files in the same folder as the storage.js app, but you can put the files anywhere that can be reached through the file system.
6 |
7 | For example, I set up the server to store the public files on an external hard disk called Broadway in a 1999 folder, in this config.json file.
8 |
9 | A common use-case for this is to store the public files in a folder that's served by a web server like nginx or Apache. Note that you don't have to do this because nodeStorage is also a web server and can serve the files it generates. But there are also good reasons to use different server software.
10 |
11 | If you're moving the files from an existing installation, be sure to copy the files from the original folder to the new place.
12 |
13 |
--------------------------------------------------------------------------------
/docs/s3.md:
--------------------------------------------------------------------------------
1 | ## Using Amazon S3 to store data
2 |
3 | You can easily configure the server to store data on Amazon S3. You just have to set up config.json to indicate that you're not using the local filesystem and tell it where on S3 you want to store the public files and the private files.
4 |
5 | #### Your AWS credentials
6 |
7 | You have to set it up so that the AWS library routines can find your credentials.
8 |
9 | Here's a page on the AWS docs site that explains the options.
10 |
11 | #### How to
12 |
13 | Here's a config.json that's set up to work with S3 storage.
14 |
15 | Your account must have the ability to read and write from the locations you're using.
16 |
17 | For the private files it's a good idea to use a bucket that is not accessible over the web, but nodeStorage will work with whatever space you give it, public or private.
18 |
19 |
--------------------------------------------------------------------------------
/docs/setup.md:
--------------------------------------------------------------------------------
1 | ## How to set up a 1999.io server on Ubuntu
2 |
3 | 1999.io is a new blogging environment that's fast and easy, and has lots of features no other blogging software has. This doc shows you how to set up a 1999.io server on Ubuntu.
4 |
5 |
6 |
7 | #### Overview
8 |
9 | 0. These instructions are for Ubuntu. Other versions of Linux or Windows or Mac OS will require you to figure out how to install Node.js yourself.
10 |
11 | 1. 1999's server is special configuration of nodeStorage, which can be used to run other software.
12 |
13 | 2. It's written in JavaScript and runs under Node.js.
14 |
15 | 3. We use Twitter for identity, so creating a connection to Twitter is part of the setup process.
16 |
17 | 4. If you want to try out the software before installing you can create a test site on my.1999.io. The usual caveats apply. I can't run these test sites forever, but I have no immediate plans to take it down. The safest bet if you plan to use 1999.io to blog for real is to run your own server.
18 |
19 | 5. There's an Amazon EC2 machine image. If you're setting up a server on EC2, using the image does a bunch of the work for you.
20 |
21 | Dave Winer, May 2016
22 |
23 | #### Install Node.js
24 |
25 |
32 |
33 | We also install npm, a requirement to run Node apps.
34 |
35 | nodejs-legacy makes it possible to run apps by saying node app.js instead of having to use nodejs, an oddity of Ubuntu.
36 |
37 | #### Install git
38 |
39 |
sudo apt-get install git
40 |
41 | I like to install git, because it makes it easy to install nodeStorage from GitHub.
42 |
43 | It's also required for the server to be able to install updates, which it does with git.
44 |
45 | #### Download nodeStorage
46 |
47 | nodeStorage is the name of the 1999 server software. It can be used for other apps. This doc shows you how to set it up for running 1999.
48 |
49 |
50 |
51 | #### Install it
52 |
53 | Change into the nodestorage directory you created in the previous step.
54 |
55 |
cd nodestorage
56 |
57 | npm install
58 |
59 | #### Create config.json
60 |
61 | Launch your favorite Unix editor. I like nano because I'm a newbie, and it has a nice menu at the bottom of the screen if you don't know the commands.
62 |
63 |
nano config.json
64 |
65 | Here's a template for config.json, copy the text, and paste it into nano in your terminal window.
66 |
67 | Now I'm going to go through all the elements step by step, explaining what you have to do to set their values.
68 |
69 | 1. myPort -- enter the number of the port you want to use. Make sure that this port is open in your firewall, if you have one on this server.
70 |
71 | 2. websocketPort -- we use a technology called WebSockets to push updates back to the editing app, and to pages that people are reading. So you must specify a port for the WS server to run on, and as with the main HTTP port, above, it must be open in your firewall.
72 |
73 | 3. myDomain -- enter a domain name that points to this server. If you don't have one, you can enter the IP address of the machine, it will work as well as the domain name. Be sure to include the port you're using in the domain, as shown in the example.
74 |
75 | 4. where -- leave it as-is. It will store the public and private files in sub-folders of the nodestorage folder.
76 |
77 | 6. twitterConsumerKey and twitterConsumerSecret -- I'll explain how to set this up in the next section. For now, leave them as set in the example. The strings are nonsense, just placeholders.
78 |
79 | Save the file by typing Control-O, then exit with Control-X.
80 |
81 | #### Set up your Twitter app
82 |
83 | 1. Go to apps.twitter.com and click Create New App in the upper-right corner. A page with a form appears, asking for details of your app.
84 |
85 | 2. Give your app a name. If it's for your book club, you could call it The Hometown Book Club Blogs. If the name is already being used by someone, choose a different name. For my example I used "My test editor 2789". It was available. ;-)
86 |
87 | 3. The website URL will be the value for myDomain specified in your config.json file, as an HTTP url. From the example, it would be http://1999.bullmancuso.io:1999/
88 |
89 | 4. The callback URL for nodeStorage instances is the url of the app, as specified in the previous step, followed by "callbackFromTwitter" (don't include the quotes).
90 |
91 | 5. Here's a screen shot of a filled-out Twitter app page.
92 |
93 | 6. If it all worked, you should have a Twitter app set up. Click on the Test OAuth button in the upper-right corner of the page, and it will show you two values, consumer key and consumer secret.
94 |
95 | 7. Open config.json in your editor, and replace the placeholder values for twitterConsumerKey and twitterConsumerSecret with these values and save the file.
96 |
97 | #### Launch nodeStorage
98 |
99 |
node storage.js
100 |
101 | #### Test your setup
102 |
103 | 1. Go to the home page of your server. In the example above it would be http://1999.bullmancuso.io:1999/.
104 |
105 | 1. You should see a page a menu on the right edge of the menu bar entitled Sign on here.
106 |
107 | 2. From that menu choose Sign on Twitter...
108 |
109 | 5. You should get the Twitter authorization page. Authorize your app.
110 |
111 | 6. It should send you back to your home page, where you should see an edit box at the top and a few menus. Type some text in the box and click the Post button.
112 |
113 | 7. If it worked, you should see a new post below that. If you click on the wedge you'll see a menu of options for the post. If you click on the Eye icon next to the menu you should see a rendering of the page.
114 |
115 | 9. Pat yourself on the back. You are now a DevOps dudess or duderino. On your way to being a Full Stack Developer. ;-)
116 |
117 | #### Where to go from here
118 |
119 | There are lots of ways to get apps to launch in the background, I like forever because it keeps the app running even if it crashes.
120 |
121 |
sudo npm install forever -g
122 |
123 |
--------------------------------------------------------------------------------
/docs/updating.md:
--------------------------------------------------------------------------------
1 | ## How updating works
2 |
3 | The server automatically checks for updates every fifteen minutes. This is of course, configurable. That's what this document explains.
4 |
5 | The update server is GitHub. The nodeStorage repo as the official release version.
6 |
7 | When there's a new version your server will automatically update itself, unless you've configured it not to. However it will not automatically restart. You can set it up so it does. See the section below.
8 |
9 | #### How to configure updates
10 |
11 | You can configure the updates through config.json by specifying a top-level object named updates. It has three optional elements, enabled, fnameStorageJs and ctMinutesBetwChecks.
12 |
13 | If enabled is false, it won't check for updates.
14 |
15 | If you've changed the name of storage.js, set fnameStorageJs to the name of the file (that's the file it will update). I like to change the name so if I'm running more than one instance of the storage server on a system I can tell which is which when looking at a forever list.
16 |
17 | Change ctMinutesBetwChecks if you want it to check more or less frequently.
18 |
19 | #### Automatic restart on change
20 |
21 | You can set it so that your server automatically quits when there's been a change to a file you can specify. If you're running a utility like forever, it can then relaunch the server.
22 |
23 | Turn the feature on with flWatchAppDateChange. And specify the file it should watch with fnameApp.
24 |
25 | #### Example
26 |
27 | Here's an example of a config.json that has an updates object and automatically restarts when there's a change to the main JavaScript file.
28 |
29 |
--------------------------------------------------------------------------------
/docs/whitelist.md:
--------------------------------------------------------------------------------
1 | ## Setting up a whitelist
2 |
3 | If you want your 1999 server to be open for anyone to create a blog there, then you don't need to specify a whitelist. However if you want to limit who can set up a blog, you need a whitelist.
4 |
5 | #### What is a whitelist?
6 |
7 | It's an array of Twitter screen names named userWhitelist that you add to your config.json file.
8 |
9 | An example of a config.json with a userWhitelist specified.
10 |
11 | #### You can also put it in a separate file
12 |
13 | If you want more flexibility in editing your whitelist, you can put it in a separate file accessible over HTTP. Here's an example of such a file.
14 |
15 | Instead of putting the array directly in your config.json file, you would point to it using the urlUserWhitelist value.
16 |
17 | The server reads the whitelist file once a minute, so you can change the whitelist without restarting the server. config.json is only read when the server starts up.
18 |
19 | Here's an example of a config.json file that specifies a remote whitelist file.
20 |
21 |
--------------------------------------------------------------------------------
/misc/about.opml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | about.opml
5 | <%dateModified%>
6 |
7 | 1
8 | 300
9 | 700
10 | 900
11 | 1500
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 |
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 |
--------------------------------------------------------------------------------