diff --git a/README.md b/README.md index c0d56cf..f7f0d00 100644 --- a/README.md +++ b/README.md @@ -29,8 +29,8 @@ The output files will be added to the same directory for simpler hosting of all the things. There are **two** important setup files: - - `config.yaml` (see `config.yaml.example`) - - `people.json` (see `people.json.example`) + - `config.yaml` (see `config.example.yaml`) + - `people.json` (see `people.example.json`) Once those two files are configured to your contentment, see below. @@ -74,18 +74,15 @@ There are several more options available. -V, --version output the version number -d, --directory The directory to process. -m, --html If set, write the minutes to an index.html file - -w, --wordpress If set, publish the minutes to the blog -e, --email If set, publish the minutes to the mailing list - -t, --twitter If set, publish the minutes to Twitter - -g, --google If set, publish the minutes to G+ -i, --index Build meeting index -q, --quiet Don't print status information to the console ``` -The WordPress, Google, and Twitter related switches also require some custom -environment variables to be setup. For examples of those, see the -[publish.sh.example](publish.sh.example). +Please note, there are custom environment variables which may need to be setup. +For examples of those, see the +[publish.example.sh](publish.example.sh). ## Web-based editor @@ -105,10 +102,22 @@ For this to work you'll need to put `people.json` in your `www/` folder. ## Wrapping bash scripts If you're on a machine that has bash available, there are a couple useful tools -in the `scripts/` folder. To configure them, copy the `publishing.cfg.example` +in the `scripts/` folder. To configure them, copy the `publishing.example.cfg` to `publishing.cfg`, make your changes, and then run the scripts (which wrap the node code). +## Modifying IRC logs + +If there are corrections to `irc.log` needed, do not make them directly, but +instead create a `changes.log` file and add "fake" IRC log entries for any +corrections/additions. + +```irc +00:00:00 scribe+ +``` + +These will be appended to the end of `irc.log` and processed alongside the rest. + ## Development During development, you'll want to test with the working copy version of diff --git a/config.yaml.example b/config.example.yaml similarity index 62% rename from config.yaml.example rename to config.example.yaml index e4ce01e..a137483 100644 --- a/config.yaml.example +++ b/config.example.yaml @@ -25,25 +25,3 @@ email: # required for -e ---------------------------------------------------------------- {{{content}}} -gplus: # required for -g - # Mustache template - vars: content, formattedItems, gDate, minutes_base_url - body: |- - *JSON-LD CG Meeting Summary for {{gDate}}* - - We discussed {{formattedItems}}. - - {{{content}}} - - Full transcript and audio logs are available here: - - {{{minutes_base_url}}}{{gDate}}/ - - #w3c #json-ld -twitter: # required for -t - # Mustache template - vars: group, message, gDate, minutes_base_url - body: |- - JSON-LD CG discusses {{message}}: - {{{minutes_base_url}}}{{gDate}}/ #w3c #json-ld -wordpress: # required for -w - # Mustache template - vars: gDate - title: "JSON-LD CG Meeting Minutes for {{gDate}}" diff --git a/index.js b/index.js index 10972fe..c95a433 100755 --- a/index.js +++ b/index.js @@ -1,6 +1,5 @@ #!/usr/bin/env node -var _ = require('underscore'); var async = require('async'); var email = require('emailjs'); var fs = require('fs'); @@ -8,21 +7,16 @@ var path = require('path'); var program = require('commander'); const Mustache = require('mustache'); var scrawl = require('./www/scrawl'); -var Twitter = require('twitter'); -var wp = require('wordpress'); const yaml = require('js-yaml'); program - .version('0.5.0') + .version('0.6.0') // the setup switches .option('-c, --config ', 'The YAML configuration file.') .option('-d, --directory ', 'The directory to process.') // the do something switches .option('-m, --html', 'If set, write the minutes to an index.html file') - .option('-w, --wordpress', 'If set, publish the minutes to the blog') .option('-e, --email', 'If set, publish the minutes to the mailing list') - .option('-t, --twitter', 'If set, publish the minutes to Twitter') - .option('-g, --google', 'If set, publish the minutes to G+') .option('-i, --index', 'Build meeting index') // the tweak the cli switch .option('-q, --quiet', 'Don\'t print status information to the console') @@ -46,8 +40,7 @@ if(!program.directory) { process.exit(1); } -if (!program.html && !program.wordpress && !program.email && !program.twitter - && !program.google && !program.index) { +if (!program.html && !program.email && !program.index) { console.error('Error: Nothing to do...'); program.outputHelp(); process.exit(1); @@ -56,6 +49,7 @@ if (!program.html && !program.wordpress && !program.email && !program.twitter // setup global variables const dstDir = path.resolve(path.join(program.directory)); const logFile = path.resolve(dstDir, 'irc.log'); +const changesLogFile = path.resolve(dstDir, 'changes.log'); const audioFile = path.resolve(dstDir, 'audio.ogg'); const indexFile = path.resolve(dstDir, 'index.html'); const minutesDir = path.join(dstDir, '/..'); @@ -89,84 +83,7 @@ if (!('minutes_base_url' in config)) { // Location of date-based minutes folders; MUST end in a forward slash scrawl.minutes_base_url = config.minutes_base_url; -// Mustache template - vars: gDate, formattedItems, content, minutes_base_url -const GPLUS_BODY = ('gplus' in config && 'body' in config.gplus) - ? config.gplus.body - : `*Meeting Summary for {{gDate}}* - -We discussed {{formattedItems}}. - -{{{content}}} - -Full transcript and audio logs are available here: - -{{{minutes_base_url}}}{{gDate}}/ -`; - -// Mustache template - vars: group, message, gDate, minutes_base_url -const TWITTER_BODY = ('twitter' in config && 'body' in config.twitter) - ? config.twitter.body - : `{{group}} discusses {{message}}: -{{{minutes_base_url}}}{{gDate}}/`; - -// Mustache template - vars: gDate -const WORDPRESS_TITLE = ('wordpress' in config && 'title' in config.wordpress) - ? config.wordpress.title - : 'Meeting Minutes for {{gDate}}'; - /************************* Utility Functions *********************************/ -function postToWordpress(username, password, content, callback) { - var client = wp.createClient({ - username: username, - password: password, - url: '' - }); - // Re-format the HTML for publication to a Wordpress blog - var datetime = new Date(gDate); - datetime.setHours(37); - var wpSummary = content.post_content; - wpSummary = wpSummary.substring( - wpSummary.indexOf('
'), wpSummary.indexOf('
') + 5); - wpSummary = wpSummary.replace(/href=\"#/g, - 'href="' + scrawl.minutes_base_url + gDate + '/#'); - wpSummary = wpSummary.replace(/href=\"audio/g, - 'href="' + scrawl.minutes_base_url + gDate + '/audio'); - wpSummary = wpSummary.replace(/
<\/div>/g, ''); - wpSummary += '

Detailed minutes and recorded audio for this call are ' + - 'available in the archive.

'; - - // calculate the proper post date - var gmtDate = datetime.toISOString(); - gmtDate = gmtDate.replace('T', ' '); - gmtDate = gmtDate.replace(/\.[0-9]*Z/, ''); - - content.post_content = wpSummary; - content.post_date_gmt = gmtDate; - content.terms_names = ['Meetings']; - content.post_name = gDate + '-minutes'; - content.custom_fields = [{ - s2_meta_field: 'no' - }]; - - client.newPost(content, function(err, data) { - if(err) { - console.log(err); - - console.log('scrawl: You may have to add this information manually:'); - - console.log('Title:\n' + content.post_title); - console.log('Content:\n' + content.post_content); - console.log('Slug:\n' + content.post_name); - } - else { - console.log(data); - // Do something. - } - callback(); - }); -} - function sendEmail(username, password, hostname, content, callback) { var server = email.server.connect({ //user: username, @@ -214,7 +131,15 @@ async.waterfall([ function(callback) { }); }, function(callback) { // read the IRC log file - fs.readFile(logFile, 'utf8', callback); + let log = fs.readFileSync(logFile, 'utf8'); + // read the changes log file if it exists + try { + log += '\n'; + log += fs.readFileSync(changesLogFile, 'utf8'); + } catch(e) { + // ignore if the file doesn't exist + } + callback(null, log); }, function(data, callback) { gLogData = data; // generate the index.html file @@ -274,7 +199,12 @@ async.waterfall([ function(callback) { topic: [], resolution: [] }; - summary.topic = data.match(/topic: (.*)/ig); + if(data.search(/agendum \d+\s+\-\- (.*) \-\-/i)) { + summary.topic = data.match(/agendum \d+\s+\-\- (.*) \-\-/i); + } else if(data.search(/(? 0 && i < items.length - 1) { - formattedItems += ', '; - } - else if(i == items.length - 1) { - formattedItems += ', and '; - } - formattedItems += items[i].replace(/[0-9]{1,2}\. /, '').toLowerCase(); - } - - // format in a way that is readable on G+ - content = Mustache.render(GPLUS_BODY, {gDate, formattedItems, content, - minutes_base_url: scrawl.minutes_base_url}); - - console.log('scrawl: You will need to paste this to your G+ stream:\n'); - console.log(content); - callback(); - } else { - callback(); - } -}, function(callback) { - // publish the minutes to Twitter - if(program.twitter) { - if(!process.env.SCRAWL_TWITTER_CONSUMER_KEY || - !process.env.SCRAWL_TWITTER_SECRET || - !process.env.SCRAWL_TWITTER_TOKEN_KEY || - !process.env.SCRAWL_TWITTER_TOKEN_SECRET) { - console.log('scrawl: You must set the following environment variables ' + - 'for twitter\nposting to work: SCRAWL_TWITTER_CONSUMER_KEY, ' + - 'SCRAWL_TWITTER_SECRET,\nSCRAWL_TWITTER_TOKEN_KEY, ' + - 'SCRAWL_TWITTER_TOKEN_SECRET.'); - return callback(); - } - // create the twitter client - var twitter = new Twitter({ - consumer_key: process.env.SCRAWL_TWITTER_CONSUMER_KEY, - consumer_secret: process.env.SCRAWL_TWITTER_SECRET, - access_token_key: process.env.SCRAWL_TWITTER_TOKEN_KEY, - access_token_secret: process.env.SCRAWL_TWITTER_TOKEN_SECRET - }); - - // get the tweet text - console.log('scrawl: Creating new tweet.'); - var prompt = require('prompt'); - prompt.start(); - prompt.get({ - properties: { - message: { - description: 'Enter the tweet contents (what was discussed)', - pattern: /^.{4,100}$/, - message: 'The message must be between 4-100 characters.' - } - } - }, function(err, results) { - // construct the tweet - var tweet = Mustache.render(TWITTER_BODY, - {group: scrawl.group, - message: results.message, gDate, - minutes_base_url: scrawl.minutes_base_url}); - - // send the tweet - twitter.updateStatus(tweet, function(data) { - console.log('scrawl: Tweet sent:', data.text); - callback(); - }); - }); - } else { - callback(); - } -}, function(callback) { - // publish the wordpress blog post - if(program.wordpress) { - if(!program.quiet) { - console.log('scrawl: Creating new blog post.'); - } - var content = { - post_title: Mustache.render(WORDPRESS_TITLE, {gDate}), - post_content: scrawl.generateMinutes(gLogData, 'html', gDate, haveAudio) - }; - - if(process.env.SCRAWL_WP_USERNAME && process.env.SCRAWL_WP_PASSWORD) { - postToWordpress( - process.env.SCRAWL_WP_USERNAME, process.env.SCRAWL_WP_PASSWORD, - content, callback); - } else { - var prompt = require('prompt'); - prompt.start(); - prompt.get({ - properties: { - username: { - description: 'Enter the WordPress username', - pattern: /^.{4,}$/, - message: 'The username must be at least 4 characters.', - 'default': 'msporny' - }, - password: { - description: 'Enter the user\'s password', - pattern: /^.{4,}$/, - message: 'The password must be at least 4 characters.', - hidden: true, - 'default': 'password' - } - } - }, function(err, results) { - postToWordpress(results.username, results.password, content, callback); - }); - } - } else { - callback(); - } }], function(err) { // check to ensure there were no errors if(err) { diff --git a/package-lock.json b/package-lock.json index 5c89d89..e2624a0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,10 +18,7 @@ "mime": "^2.3.1", "moment": "~2.19.3", "mustache": "^2.3.0", - "prompt": "^1.0.0", - "twitter": "~0.2.5", - "underscore": "~1.5.0", - "wordpress": "~1.4.1" + "prompt": "^1.0.0" }, "bin": { "scrawl": "index.js" @@ -84,18 +81,6 @@ "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" }, - "node_modules/cookies": { - "version": "0.7.1", - "resolved": "https://registry.npmjs.org/cookies/-/cookies-0.7.1.tgz", - "integrity": "sha1-fIphX1SBxhq58WyDNzG8uPZjuZs=", - "dependencies": { - "depd": "~1.1.1", - "keygrip": "~1.0.2" - }, - "engines": { - "node": ">= 0.8" - } - }, "node_modules/cycle": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/cycle/-/cycle-1.0.3.tgz", @@ -109,14 +94,6 @@ "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-0.2.2.tgz", "integrity": "sha1-hLdFiW80xoTpjyzg5Cq69Du6AX0=" }, - "node_modules/depd": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", - "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=", - "engines": { - "node": ">= 0.6" - } - }, "node_modules/emailjs": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/emailjs/-/emailjs-2.1.0.tgz", @@ -275,14 +252,6 @@ "js-yaml": "bin/js-yaml.js" } }, - "node_modules/keygrip": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/keygrip/-/keygrip-1.0.2.tgz", - "integrity": "sha1-rTKXxVcGneqLz+ek+kkbdcXd65E=", - "engines": { - "node": ">= 0.6" - } - }, "node_modules/keypress": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/keypress/-/keypress-0.1.0.tgz", @@ -398,11 +367,6 @@ "ncp": "bin/ncp" } }, - "node_modules/oauth": { - "version": "0.9.15", - "resolved": "https://registry.npmjs.org/oauth/-/oauth-0.9.15.tgz", - "integrity": "sha1-vR/vr2hslrdUda7VGWQS/2DPucE=" - }, "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -508,24 +472,6 @@ "integrity": "sha1-45mpgiV6J22uQou5KEXLcb3CbRk=", "deprecated": "no longer maintained" }, - "node_modules/twitter": { - "version": "0.2.13", - "resolved": "https://registry.npmjs.org/twitter/-/twitter-0.2.13.tgz", - "integrity": "sha1-KcwBmV9aQV99X3guLGo2uCAMs4w=", - "engines": [ - "node >=0.2.0" - ], - "dependencies": { - "cookies": ">=0.1.6", - "keygrip": ">=0.1.7", - "oauth": ">=0.8.4" - } - }, - "node_modules/underscore": { - "version": "1.5.2", - "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.5.2.tgz", - "integrity": "sha512-yejOFsRnTJs0N9CK5Apzf6maDO2djxGoLLrlZlvGs2o9ZQuhIhDL18rtFyy4FBIbOkzA6+4hDgXbgz5EvDQCXQ==" - }, "node_modules/utile": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/utile/-/utile-0.3.0.tgz", @@ -585,40 +531,6 @@ "node": ">= 0.4.0" } }, - "node_modules/wordpress": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/wordpress/-/wordpress-1.4.1.tgz", - "integrity": "sha512-U2zADxCSyyYcpgc5i7ipiDzNx6/e0zq2ldWyqTqr8n88Nj+iHd5JT/WavZkIQ+x0b9QlBv9lHoXyrqxdbckIrw==", - "dependencies": { - "xmlrpc": "1.3.2" - } - }, - "node_modules/wordpress/node_modules/sax": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", - "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==" - }, - "node_modules/wordpress/node_modules/xmlbuilder": { - "version": "8.2.2", - "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-8.2.2.tgz", - "integrity": "sha1-aSSGc0ELS6QuGmE2VR0pIjNap3M=", - "engines": { - "node": ">=4.0" - } - }, - "node_modules/wordpress/node_modules/xmlrpc": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/xmlrpc/-/xmlrpc-1.3.2.tgz", - "integrity": "sha1-JrLqNHhI0Ciqx+dRS1NRl23j6D0=", - "dependencies": { - "sax": "1.2.x", - "xmlbuilder": "8.2.x" - }, - "engines": { - "node": ">=0.8", - "npm": ">=1.0.0" - } - }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", diff --git a/package.json b/package.json index 6c576e1..f7357fb 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "scrawl", - "version": "0.5.0", + "version": "0.6.0", "description": "IRC log processing tool for scribes to assist in creating minutes for W3C group.", "keywords": [ "IRC", @@ -27,9 +27,6 @@ "mime": "^2.3.1", "moment": "~2.19.3", "mustache": "^2.3.0", - "prompt": "^1.0.0", - "twitter": "~0.2.5", - "underscore": "~1.5.0", - "wordpress": "~1.4.1" + "prompt": "^1.0.0" } } diff --git a/people.json.example b/people.example.json similarity index 100% rename from people.json.example rename to people.example.json diff --git a/publish.example.sh b/publish.example.sh new file mode 100644 index 0000000..4fe854d --- /dev/null +++ b/publish.example.sh @@ -0,0 +1,13 @@ +#!/usr/bin/env bash +# +# Generates minutes for the directory given + +echo "Generating minutes and social media posts for $1" +DATESTR=`echo $1 | cut -f2 -d/` +MESSAGE="Add text minutes and audio logs for $DATESTR telecon." + +# Generate minutes +nodejs index.js -d $1 -m -i +git add $1/irc.log $1/index.html $1/audio.ogg +git commit $1/irc.log $1/index.html $1/audio.ogg $1/../index.html -m "$MESSAGE" +git push diff --git a/publishing.cfg.example b/publishing.example.cfg similarity index 76% rename from publishing.cfg.example rename to publishing.example.cfg index 5f71568..bb91d61 100644 --- a/publishing.cfg.example +++ b/publishing.example.cfg @@ -12,9 +12,5 @@ export SCRAWL_EMAIL_SERVER= # Optional port and SSL. Default SSL is false # export SCRAWL_EMAIL_PORT= # export SCRAWL_EMAIL_SSL= -export SCRAWL_TWITTER_CONSUMER_KEY= -export SCRAWL_TWITTER_SECRET= -export SCRAWL_TWITTER_TOKEN_KEY= -export SCRAWL_TWITTER_TOKEN_SECRET= export SCRAWL_LINKEDIN_CLIENT_ID= export SCRAWL_LINKEDIN_CLIENT_SECRET= diff --git a/www/_partials/header.html b/www/_partials/header.html index 1b8274e..56150f9 100644 --- a/www/_partials/header.html +++ b/www/_partials/header.html @@ -77,22 +77,13 @@ div.comment { font-family: 'Droid Serif', serif; margin-top: 0.5em; - font-size: 1.25em; } div.comment-continuation { font-family: 'Droid Serif', serif; - font-size: 1.25em; margin-left: 2em; } -.comment-link { - display: none; -} -.comment:hover .comment-link { - display: inline; -} - div.proposal { padding: 15px; margin: 20px 0px 20px 0px; @@ -123,7 +114,7 @@

The W3C JSON-LD Community Group

-

JSON-LD Syntax and API

Go Back


+

W3C Logo

\ No newline at end of file diff --git a/www/scrawl.js b/www/scrawl.js index 08c306e..0c12fa3 100644 --- a/www/scrawl.js +++ b/www/scrawl.js @@ -5,32 +5,39 @@ */ (function() { /* Standard regular expressions to use when matching lines */ - var commentRx = /^\[?(\S*|\w+ \S+)\]\s+<([^>]*)>\s+(.*)$/; - var scribeRx = /^(scribe|scribenick):.*$/i; - var meetingRx = /^meeting:\s(.*)$/i; - var totalPresentRx = /^total present:\s(.*)$/i; - var dateRx = /^date:\s(.*)$/i; - var chairRx = /^chair:.*$/i; - var audioRx = /^audio:\s?(.*)$/i; - var proposalRx = /^(proposal|proposed):.*$/i; - var presentRx = /^present[:+](.*)$/i; - var resolutionRx = /^(resolution|resolved): ?(.*)$/i; - var useCaseRx = /^(use case|usecase):\s?(.*)$/i; - var topicRx = /^topic:\s*(.*)$/i; - var actionRx = /^action:\s*(.*)$/i; - var voipRx = /^voip.*$/i; - var toVoipRx = /^voip.{0,4}:.*$/i; - var rrsAgentRx = /^RRSAgent.*$/i; - var queueRx = /^q[+-?]\s.*|^q[+-?].*|^ack\s+.*|^ack$/i; - var voteRx = /^[+-][01]\s.*|[+-][01]$/i; - var agendaRx = /^agenda:\s*((https?):.*)$/i; - var urlRx = /((ftp|http|https):\/\/(\w+:{0,1}\w*@)?(\S+)(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!\-\/]))?)/; + const commentRx = /^\[?(\S*)\]?\s+<([^>]*)>\s+(.*)$/; + const scribeRx = /^(scribe|scribenick)[:+\-](.*)$/i; + const meetingRx = /^meeting:\s(.*)$/i; + const totalPresentRx = /^total present:\s(.*)$/i; + const dateRx = /^date:\s(.*)$/i; + const chairRx = /^chair[:+\-].*$/i; + const audioRx = /^audio:\s?(.*)$/i; + const proposalRx = /^(proposal|proposed):.*$/i; + const presentRx = /^present[:+\-](.*)$/i; + const regretsRx = /^regrets[:+\-](.*)$/i; + const resolutionRx = /^(resolution|resolved): ?(.*)$/i; + const useCaseRx = /^(use case|usecase):\s?(.*)$/i; + const agendumRx = /^agendum \d+\s+\-\- (.*) \-\-/i + const topicRx = /^((?:sub)?topic):\s*(.*)$/i; + const actionRx = /^action:\s*(.*)$/i; + const voipRx = /^voip.*$/i; + const toVoipRx = /^voip.{0,4}:.*$/i; + const botRx = /^(rrsagent|zakim|agendabot).*$/i; + const allowedBotRx = /^(ghurlbot|gb).*$/i; + const junkRx = / has (joined|left|changed)/i; + const queueRx = /^qq?[+-?]\s.*|^qq?[+-?].*|^ack\s+.*|^ack$/i; + const voteRx = /^[+-][01]\s.*|[+-][01]$/i; + const agendaRx = /^agenda:\s*((https?):.*)$/i; + const urlRx = /((ftp|http|https):\/\/(\w+:{0,1}\w*@)?(\S+)(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!\-\/]))?)/; + + // Based on https://github.com/w3c/scribe2/blob/51a2e428fb1d6edf1fe1d1eba756c81d9b109cdd/scribe.perl#L820 + var replaceRx = /^ *(s|i)(\/|\|)(.*?)\2(.*?)(?:\2([gG*])? *)?$/; // Compatability code to make this work in both node.js and the browser - var scrawl = {}; - var nodejs = false; + const scrawl = {}; + let nodejs = false; if(typeof module !== 'undefined' && typeof module.exports !== 'undefined') { - var Entities = require('html-entities').XmlEntities; + const Entities = require('html-entities').XmlEntities; var entities = new Entities(); module.exports = scrawl; nodejs = true; @@ -57,19 +64,61 @@ if (!str) { return str; } - var regex = '.{1,' + width + '}(\\s|$)' + + const regex = '.{1,' + width + '}(\\s|$)' + (cut ? '|.{' +width+ '}|.+$' : '|\\S+?(\\s|$)'); return str.match(new RegExp(regex, 'g')).join(brk); }; + // Record diffent categories for people + scrawl.processPeople = function(context, category, nick, line, remove = false) + { + const lower = line.toLowerCase(); + let [, cmd, op, rest] = lower.match(/(\w+)([:+\-])\s*(.*)$/); + + if(!(category in context)) { + context[category] = []; + } + + // category: resets the list + if(op === ':' && remove === true) { + context[category] = []; + } + + // line has form of (category)[:+=] nick or first_last + var whoall = rest + .split(',') + .map(n => n.trim()) + .filter(e => !!e); + + // If no who, use nick + if(whoall.length === 0) {whoall = [nick]} + + for(const who of whoall) { + if(!(who in context.aliases)) { + // Create an alias record, presuming this is First_Last name + context.aliases[who] = who.split('_').join(' '); + } + let name = context.aliases[who] + + if(op === '-' && remove === true) { + // Remove from category + context[category] = context[category].filter(i => i !== name); + } else if(!(context[category].includes(name))){ + // Add to category + context[category].push(name); + } + } + } + + // FIXME: integrate changes from CCG, considering overlap with processPeople scrawl.generateAliases = function() { - var rval = {}; + const rval = {}; - for(var p in scrawl.people) + for(const p in scrawl.people) { - var person = scrawl.people[p]; + const person = scrawl.people[p]; var names = p.split(' '); // append any aliases to the list of known names @@ -79,10 +128,9 @@ } // Add the aliases and names if they don't already exist in the aliases - for(var n in names) + for(const n in names) { - var alias = names[n]; - alias = alias.toLowerCase(); + const alias = names[n].toLowerCase(); if(alias.length > 2 && !(alias in rval)) { rval[alias] = p; @@ -95,7 +143,7 @@ scrawl.htmlencode = function(text) { - var modified = ''; + let modified; if(nodejs) { modified = entities.encodeNonUTF(text); @@ -111,16 +159,22 @@ scrawl.topic = function(msg, id, textMode) { - var rval = ''; + let rval = ''; if(textMode === 'html') { - rval = '

Topic: ' + - scrawl.htmlencode(msg) + '

\n'; + const [h, t] = Number.isInteger(id) ? ['h1', 'Topic'] : ['h2', 'Subtopic']; + rval = '<' + h + ' onmouseout="$(\'#link-topic-' + id + '\').hide()" ' + + 'onmouseover="$(\'#link-topic-' + id + '\').show()" ' + + 'id="topic-' + id + '" class="topic">\n'; + rval += t + ': ' + + scrawl.htmlencode(msg) + '\n'; + rval += '\n'; } else { - rval = '\nTopic: ' + msg + '\n\n'; + rval = '\n' + t + ' ' + msg + '\n\n'; } return rval; @@ -128,7 +182,7 @@ scrawl.action = function(msg, id, textMode) { - var rval = ''; + let rval = ''; if(textMode === 'html') { @@ -145,7 +199,7 @@ scrawl.information = function(msg, textMode) { - var rval = ''; + let rval = ''; if(textMode === 'html') { @@ -162,7 +216,7 @@ scrawl.proposal = function(msg, textMode) { - var rval = ''; + let rval = ''; if(textMode === 'html') { @@ -180,13 +234,17 @@ scrawl.resolution = function(msg, id, textMode) { - var rval = ''; + let rval = ''; if(textMode === 'html') { - rval = '
' + - 'RESOLUTION: ' + - scrawl.htmlencode(msg) + '
\n'; + rval = '
\n'; + rval += 'RESOLUTION: ' + + scrawl.htmlencode(msg) + '\n'; + rval += '
\n'; } else { @@ -199,7 +257,7 @@ scrawl.usecase = function(msg, textMode) { - var rval = ''; + let rval = ''; if(textMode === 'html') { @@ -218,7 +276,7 @@ scrawl.scribe = function(msg, textMode, person, assist) { - var rval = ''; + let rval = ''; // capitalize the first letter of the message if it doesn't start with http if(!(/^(\s)*https?:\/\//.test(msg))) { @@ -230,7 +288,9 @@ if(textMode === 'html') { scrawl.counter += 1; - rval = '\n'; + '" style="display:none;" href="#'+ scrawl.counter + '">✪
\n'; } else { @@ -273,7 +333,7 @@ scrawl.scribeContinuation = function(msg, textMode) { - var rval = ''; + let rval = ''; if(textMode === 'html') { @@ -292,13 +352,13 @@ { if(person !== undefined) { - context.present[person] = true; + scrawl.processPeople(context, 'present', person, 'present+', false); } }; scrawl.error = function(msg, textMode) { - var rval = ''; + let rval = ''; if(textMode === 'html') { @@ -321,79 +381,104 @@ scrawl.htmlFooter = footer; }; - scrawl.processLine = function(context, aliases, line, textMode) + scrawl.preprocessLine = function(context, lines, lineNumber) + { + const line = lines[lineNumber]; + const match = commentRx.exec(line); + if(!match) + { + return; + } + const [_, time, nick, msg] = match; + + // check for a substitution + const replaceMatch = replaceRx.exec(msg); + if(replaceMatch) + { + const [_, cmd, delim, old, replacement, modifier] = replaceMatch; + if (cmd !== 's') + { + console.error(`command not supported on line ${lineNumber}: ${line}`); + return; + } + const maxReplaces = modifier === 'g' || modifier === 'G' ? Infinity : 1; + const endLineN = modifier === 'G' ? lines.length-1 : lineNumber-1; + let numReplaces = 0; + for(let i = endLineN; i >= 0; i--) + { + const line = lines[i]; + const newLine = line.replace(old, replacement); + if(line !== newLine) + { + lines[i] = newLine; + console.log('Replacing', JSON.stringify(old), 'with', JSON.stringify(replacement), ' on line', i) + if(++numReplaces >= maxReplaces) + { + break; + } + } + } + lines[lineNumber] = ''; + } + } + + scrawl.processLine = function(context, line, textMode) { - var rval = ''; - var match = commentRx.exec(line); + let rval = ''; + const match = commentRx.exec(line); - if(match) + if(!match) { - var time = match[1]; - var nick = match[2].toLowerCase(); - var msg = match[3]; + return ''; + } + const [_, time, _nick, msg] = match; + const nick = _nick.toLowerCase(); + const nickName = context.aliases[nick]; // check for a scribe line if(msg.search(scribeRx) !== -1) { - var scribe = msg.split(':')[1].replace(' ', ''); - scribe = scribe.toLowerCase(); - if(scribe in aliases) + if(nick === 'transcriber') { + if(!(nick in context.aliases)) { - if(!context.hasOwnProperty('scribe')) { - context.scribe = []; - } + context.aliases[nick] = 'Transcriber' + } + context.scribe.push('Transcriber') + context.scribenick.push('Transcriber') + rval = scrawl.information('Our Robot Overlords are scribing.'); + } else { + // 'scribe' collects all scribes in the meeting + scrawl.processPeople(context, 'scribe', nick, msg, false); + + // 'scribenick' maintains list of current scribes + scrawl.processPeople(context, 'scribenick', nick, msg, true); - context.scribenick = scribe; - context.scribe.push(aliases[scribe]); - scrawl.present(context, aliases[scribe]); + if(!(msg.includes('scribe-'))) { rval = scrawl.information( context.scribe[context.scribe.length-1] + ' is scribing.', textMode); } + } } else if(msg.search(chairRx) !== -1) { - var chairs = msg.split(':')[1].split(','); - - context.chair = []; - for(var i = 0; i < chairs.length; i++) { - var chair = chairs[i].replace(' ', '').toLowerCase(); - if(chair in aliases) - { - context.chair.push(aliases[chair]); - scrawl.present(context, aliases[chair]); - } - } + scrawl.processPeople(context, 'chair', nick, msg, true); } // check for meeting line else if(msg.search(meetingRx) !== -1) { - var meeting = msg.match(meetingRx)[1]; + const meeting = msg.match(meetingRx)[1]; context.group = meeting; } + // check for regrets line + else if(msg.search(regretsRx) !== -1) + { + scrawl.processPeople(context, 'regrets', nick, msg, true); + } // check for present line else if(msg.search(presentRx) !== -1) { - var present = msg.match(presentRx)[1].toLowerCase(); - var people = present.split(','); - - // try to find the person by full name, last name, and then first name - for(var i = 0; i < people.length; i++) { - if (!people[i]) { - scrawl.present(context, aliases[nick]); - } else { - var person = people[i].replace(/^\s/, '').replace(/\s$/, ''); - var lastName = person.split(' ')[1]; - var firstName = person.split(' ')[0]; - if(person in aliases) { - scrawl.present(context, aliases[person]); - } else if(lastName in aliases) { - scrawl.present(context, aliases[lastName]); - } else { - console.log('Could not find alias for', person); - } - } - } + scrawl.processPeople(context, 'present', nick, msg, true); } // check for audio line else if(msg.search(audioRx) !== -1) @@ -403,39 +488,53 @@ // check for date line else if(msg.search(dateRx) !== -1) { - var date = msg.match(dateRx)[1]; + const date = msg.match(dateRx)[1]; context.date = new Date(date); } // check for topic line else if(msg.search(topicRx) !== -1) { - var topic = msg.match(topicRx)[1]; + const [_, cmd, topic] = msg.match(topicRx); + if(cmd.toLowerCase() == 'topic') { context.topics = context.topics.concat(topic); + context.subTopicIndex = 0; + rval = scrawl.topic(topic, context.topics.length, textMode); + } else { + // subTopic + context.subTopicIndex += 0.1; + rval = scrawl.topic(topic, context.subTopicIndex + context.topics.length, textMode); + } + } + // Agenda handling: the agendum display should be converted into a bona fide topic + else if(nick === 'zakim' && msg.search(agendumRx) !== -1 ) + { + const topic = msg.match(agendumRx)[1]; + context.topics = context.topics.concat(topic); rval = scrawl.topic(topic, context.topics.length, textMode); } // check for action line - else if(msg.search(actionRx) !== -1) + else if(nick !== 'rrsagent' && msg.search(actionRx) !== -1) { - var action = msg.match(actionRx)[1]; + const action = msg.match(actionRx)[1]; context.actions = context.actions.concat(action); rval = scrawl.action(action, context.actions.length, textMode); } // check for Agenda line else if(msg.search(agendaRx) !== -1) { - var agenda = msg.match(agendaRx)[1]; + const agenda = msg.match(agendaRx)[1]; context.agenda = agenda; } // check for proposal line else if(msg.search(proposalRx) !== -1) { - var proposal = msg.split(':')[1]; + const proposal = msg.split(':')[1]; rval = scrawl.proposal(proposal, textMode); } // check for resolution line else if(msg.search(resolutionRx) !== -1) { - var resolution = msg.match(resolutionRx)[2]; + const resolution = msg.match(resolutionRx)[2]; context.resolutions = context.resolutions.concat(resolution); rval = scrawl.resolution( resolution, context.resolutions.length, textMode); @@ -443,18 +542,26 @@ // check for use case line else if(msg.search(useCaseRx) !== -1) { - var usecase = msg.match(useCaseRx)[2]; + const usecase = msg.match(useCaseRx)[2]; rval = scrawl.usecase(usecase, textMode); } else if(msg.search(totalPresentRx) !== -1) { context.totalPresent = msg.match(totalPresentRx)[1]; } - else if(nick.search(voipRx) !== -1 || msg.search(toVoipRx) !== -1 || - nick.search(rrsAgentRx) !== -1 || msg.search(rrsAgentRx) !== -1 ) + else if(nick.search(botRx) !== -1 || msg.search(botRx) !== -1 ) { // the line is from or is addressed to a channel bot, ignore it } + else if(nick.search(allowedBotRx) !== -1) + { + // add line without other processing + rval = scrawl.scribe(msg, textMode); + } + else if( msg.search(junkRx) !== -1 ) + { + // Other junk lines + } else if(msg.search(queueRx) !== -1) { // the line is queue management, ignore it @@ -462,14 +569,14 @@ // the line is a +1/-1 vote else if(msg.search(voteRx) !== -1) { - if(nick in aliases) + if(nick in context.aliases) { - rval = scrawl.scribe(msg, textMode, aliases[nick]); - scrawl.present(context, aliases[nick]); + rval = scrawl.scribe(msg, textMode, context.aliases[nick]); + //scrawl.present(context, nick); } } - // the line is by the scribe - else if(nick === context.scribenick) + // the line is by a scribe + else if(context.scribenick.includes(nickName)) { if(msg.indexOf('…') === 0 || msg.indexOf('...') === 0) { @@ -478,17 +585,17 @@ } else if(msg.indexOf(':') !== -1) { - var alias = msg.split(':', 1)[0].replace(' ', '').toLowerCase(); + const alias = msg.split(':', 1)[0].replace(' ', '').toLowerCase(); - if(alias in aliases) + if(alias in context.aliases) { // the line is a comment made by somebody else that was // scribed - var cleanedMessage = msg.split(':').splice(1).join(':'); + const cleanedMessage = msg.split(':').splice(1).join(':'); - scrawl.present(context, aliases[alias]); + //scrawl.present(context, alias); rval = scrawl.scribe( - cleanedMessage, textMode, aliases[alias]); + cleanedMessage, textMode, context.aliases[alias]); } else { @@ -504,29 +611,29 @@ } } // the line is a comment by somebody else - else if(nick !== context.scribenick) + else if(!context.scribenick.includes(nickName)) { if(msg.indexOf(':') !== -1) { - var alias = msg.split(':', 1)[0].replace(' ', '').toLowerCase(); + const alias = msg.split(':', 1)[0].replace(' ', '').toLowerCase(); - if(alias in aliases) + if(alias in context.aliases) { // the line is a scribe assist - var cleanedMessage = msg.split(':').splice(1).join(':'); + const cleanedMessage = msg.split(':').splice(1).join(':'); - scrawl.present(context, aliases[alias]); + //scrawl.present(context, alias); rval = scrawl.scribe(cleanedMessage, textMode, - aliases[alias], aliases[nick]); + context.aliases[alias], context.aliases[nick]); } else if(alias.indexOf('http') === 0) { - rval = scrawl.scribe(msg, textMode, aliases[nick]); + rval = scrawl.scribe(msg, textMode, context.aliases[nick]); } - else if(aliases.hasOwnProperty(nick)) + else if(nick in context.aliases) { - scrawl.present(context, aliases[nick]); - rval = scrawl.scribe(msg, textMode, aliases[nick]); + //scrawl.present(context, nick); + rval = scrawl.scribe(msg, textMode, context.aliases[nick]); } else { @@ -535,7 +642,7 @@ textMode); } } - else if (!(nick in aliases)) { + else if (!(nick in context.aliases)) { rval = scrawl.error( '(IRC nickname \'' + nick + '\' not recognized)' + line, textMode); @@ -543,14 +650,13 @@ else { // the line is a scribe line by somebody else - scrawl.present(context, aliases[nick]); - rval = scrawl.scribe(msg, textMode, aliases[nick]); + //scrawl.present(context, nick); + rval = scrawl.scribe(msg, textMode, context.aliases[nick]); } } else { rval = scrawl.error('(Strange line format)' + line, textMode); - } } return rval; @@ -558,29 +664,43 @@ scrawl.generateSummary = function(context, textMode) { - var rval = ''; - var time = context.date || new Date(); - var month = '' + (time.getMonth() + 1) - var day = '' + time.getDate() - var group = context.group; - var agenda = context.agenda; - var audio = 'audio.ogg'; - var chair = context.chair; - var scribe = context.scribe.filter(function(item, i, arr) { - return arr.indexOf(item) === i; - }); - var topics = context.topics; - var resolutions = context.resolutions; - var actions = context.actions; - var present = []; + let rval = ''; + let time = context.date || new Date(); + let month = '' + (time.getMonth() + 1) + let day = '' + time.getDate() + const group = context.group; + const agenda = context.agenda; + const audio = 'audio.ogg'; + const chair = context.chair; + const scribe = context.scribe; + const topics = context.topics; + const resolutions = context.resolutions; + const actions = context.actions; + const present = []; + const regrets = []; // build the list of people present - for(var name in context.present) { - var person = scrawl.people[name] + for(const name of context.present) { + const person = scrawl.people[name] || {}; person['name'] = name; + if(person.homepage === undefined) { + person.homepage = 'https://json-ld.org/' + } present.push(person) } + // build the list of regrets + if(context.regrets) { + for(const name of context.regrets) { + const person = scrawl.people[name] || {}; + person['name'] = name; + if(person.homepage === undefined) { + person.homepage = 'https://json-ld.org/' + } + regrets.push(person) + } + } + // modify the time if it was specified if(context.date) { time = new Date(context.date) @@ -605,6 +725,28 @@ rval += '

Minutes for ' + time.getFullYear() + '-' + month + '-' + day +'

\n'; rval += '
\n
\n'; + + // generate the list of people present + peoplePresent = present.map(person => { + return ''+ person.name + '' + }).join(', ') + + if(context.totalPresent) { + peoplePresent += ', ' + context.totalPresent; + } + + // generate the list of regrets + const peopleRegrets = regrets.map(person => { + return ''+ person.name + '' + }).join(', ') + + rval += '
Present
' + peoplePresent + '
\n'; + if(regrets.length > 0) { + rval += '
Regrets
' + peopleRegrets + '
\n'; + } + rval += '
Chair(s)
' + chair.join(', ') + '
\n'; + rval += '
Scribe(s)
' + scribe.join(', ') + '
\n'; + rval += '
Agenda
' + agenda + '
\n'; @@ -613,7 +755,7 @@ rval += '
Topics
    '; for(i in topics) { - var topicNumber = parseInt(i) + 1; + const topicNumber = parseInt(i) + 1; rval += '
  1. ' + topics[i] + ''; } @@ -625,7 +767,7 @@ rval += '
    Resolutions
      '; for(i in resolutions) { - var resolutionNumber = parseInt(i) + 1; + const resolutionNumber = parseInt(i) + 1; rval += '
    1. ' + resolutions[i] + ''; } @@ -637,37 +779,13 @@ rval += '
      Action Items
        '; for(i in actions) { - var actionNumber = parseInt(i) + 1; + const actionNumber = parseInt(i) + 1; rval += '
      1. ' + actions[i] + ''; } rval += '
      '; } - // generate the list of people present - var peoplePresent = '' - for(var i = 0; i < present.length; i++) { - var person = present[i]; - - if(i > 0) { - peoplePresent += ', '; - } - - if ('homepage' in person) { - peoplePresent += ''+ - person.name + ''; - } else { - peoplePresent += person.name; - } - } - if(context.totalPresent) { - peoplePresent += ', ' + context.totalPresent; - } - - rval += '
      Organizer
      ' + chair.join(', ') + '
      \n'; - rval += '
      Scribe
      ' + scribe.join(', ') + '
      \n'; - rval += '
      Present
      ' + peoplePresent + '
      \n'; - if(context.audio) { rval += '
      Audio Log
      ' + '\n' + @@ -682,20 +800,15 @@ else { // generate the list of people present - var peoplePresent = '' - for(var i = 0; i < present.length; i++) { - var person = present[i]; + var peoplePresent = present.map(person => person.name).join(', ') - if(i > 0) { - peoplePresent += ', '; - } - - peoplePresent += person.name - } if(context.totalPresent) { peoplePresent += ', ' + context.totalPresent; } + // generate the list of regrets + const peopleRegrets = regrets.map(person => person.name).join(', ') + rval += group; rval += ' Minutes for ' + time.getFullYear() + '-' + month + '-' + day + '\n\n'; @@ -706,7 +819,7 @@ rval += 'Topics:\n'; for(i in topics) { - var topicNumber = parseInt(i) + 1; + const topicNumber = parseInt(i) + 1; rval += scrawl.wordwrap( ' ' + topicNumber + '. ' + topics[i], 65, '\n ') + '\n'; @@ -718,7 +831,7 @@ rval += 'Resolutions:\n'; for(i in resolutions) { - var resolutionNumber = parseInt(i) + 1; + const resolutionNumber = parseInt(i) + 1; rval += scrawl.wordwrap( ' ' + resolutionNumber + '. ' + resolutions[i], 65, '\n ') + '\n'; @@ -730,7 +843,7 @@ rval += 'Action Items:\n'; for(i in actions) { - var actionNumber = parseInt(i) + 1; + const actionNumber = parseInt(i) + 1; rval += scrawl.wordwrap( ' ' + actionNumber + '. ' + actions[i], 65, '\n ') + '\n'; @@ -741,6 +854,12 @@ rval += 'Scribe:\n ' + scribe.join(' and ') + '\n'; rval += 'Present:\n ' + scrawl.wordwrap(peoplePresent, 65, '\n ') + '\n'; + + if(regrets.length > 0) { + rval += 'Regrets:\n ' + + scrawl.wordwrap(peopleRegrets, 65, '\n ') + '\n'; + } + if(context.audio) { rval += 'Audio:\n ' + scrawl.minutes_base_url + time.getFullYear() + '-' + @@ -755,11 +874,9 @@ scrawl.generateMinutes = function(ircLog, textMode, date, haveAudio) { - var rval = ''; - var minutes = ''; - var summary = ''; - var ircLines = ircLog.split(/\r?\n/); - var aliases = scrawl.generateAliases(); + let minutes = ''; + const ircLines = ircLog.split(/\r?\n/); + const aliases = scrawl.generateAliases(ircLog); scrawl.counter = 0; // TODO: expose this better? @@ -773,12 +890,14 @@ { 'group': scrawl.group, 'chair': chair, - 'present': {}, + 'present': [], 'scribe': [], + 'scribenick': [], 'topics': [], 'resolutions': [], 'actions': [], - 'audio': haveAudio + 'audio': haveAudio, + 'aliases': aliases }; if(date) { @@ -786,22 +905,25 @@ context.date.setHours(36); } - // process each IRC log line + // pre-process each IRC log line for(var i = 0; i < ircLines.length; i++) { - var line = ircLines[i]; - minutes += scrawl.processLine(context, aliases, line, textMode); + scrawl.preprocessLine(context, ircLines, i); + } + + // process each IRC log line + for(line of ircLines) + { + minutes += scrawl.processLine(context, line, textMode); } // generate the meeting summary - summary = scrawl.generateSummary(context, textMode); + const summary = scrawl.generateSummary(context, textMode); // fix spacing around proposals, actions, and resolutions minutes = minutes.replace(/\n\n\n/gm, '\n\n'); // create the final log output - rval = summary + minutes; - - return rval; + return summary + minutes; } })();