Diff
Modified: trunk/Tools/ChangeLog (260219 => 260220)
--- trunk/Tools/ChangeLog 2020-04-16 20:41:12 UTC (rev 260219)
+++ trunk/Tools/ChangeLog 2020-04-16 20:42:16 UTC (rev 260220)
@@ -1,3 +1,21 @@
+2020-04-16 Yusuke Suzuki <[email protected]>
+
+ Add slack-aware WKR implementation
+ https://bugs.webkit.org/show_bug.cgi?id=210425
+
+ Reviewed by Alexey Proskuryakov.
+
+ This patch adds new WKR implementation, which is posting changes to #changes in WebKit slack.
+ Currently, we are polling git.webkit.org's feed once a minute. The more efficient way to implement
+ this bot is introducing post-commit hook, but for now, this polling strategy just works.
+
+ * WKR/.gitignore: Added.
+ * WKR/ReadMe.md: Added.
+ * WKR/WKR.mjs: Added.
+ * WKR/data/.gitignore: Added.
+ * WKR/package-lock.json: Added.
+ * WKR/package.json: Added.
+
2020-04-16 Daniel Bates <[email protected]>
[iOS] Add a way to focus a text input and place a caret
Added: trunk/Tools/WKR/.gitignore (0 => 260220)
--- trunk/Tools/WKR/.gitignore (rev 0)
+++ trunk/Tools/WKR/.gitignore 2020-04-16 20:42:16 UTC (rev 260220)
@@ -0,0 +1 @@
+.env
Added: trunk/Tools/WKR/ReadMe.md (0 => 260220)
--- trunk/Tools/WKR/ReadMe.md (rev 0)
+++ trunk/Tools/WKR/ReadMe.md 2020-04-16 20:42:16 UTC (rev 260220)
@@ -0,0 +1,14 @@
+# WKR
+
+New WKR bot implementation for WebKit Slack #changes channel.
+This bot is fetching git.webkit.org RSS feed periodically, extracting data from that, and posting them to #changes channel to replace IRC's WKR bot purpose.
+
+## Steps to run
+
+1. Run `npm install` to install libraries
+2. Put `.env` file, which includes `slackURL="<Slack Endpoint URL>"` (See [https://webkit.slack.com/apps/A0F7XDUAZ-incoming-webhooks?next_id=0](this page) for getting this URL).
+3. Run `npm start`
+
+## Details
+
+The lastest posted revision data is stored in `data/` directory. You can clean up state if you remove files in `data/`.
Added: trunk/Tools/WKR/WKR.mjs (0 => 260220)
--- trunk/Tools/WKR/WKR.mjs (rev 0)
+++ trunk/Tools/WKR/WKR.mjs 2020-04-16 20:42:16 UTC (rev 260220)
@@ -0,0 +1,219 @@
+/*
+ * Copyright (C) 2020 Apple Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
+ * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+import fs from "fs"
+import path from "path"
+import RSS from "rss-parser"
+import replaceAll from "replaceall"
+import storage from "node-persist"
+import axios from "axios"
+import dotenv from "dotenv"
+
+dotenv.config();
+
+// Change to true when debugging to avoid posting notifications to Slack.
+const DEBUG = false;
+
+const contributorsURL = "https://svn.webkit.org/repository/webkit/trunk/Tools/Scripts/webkitpy/common/config/contributors.json";
+const commitEndpointURL = "https://git.webkit.org/?p=WebKit-https.git;a=atom";
+const defaultInterval = 60 * 1000;
+
+async function sleep(milliseconds)
+{
+ await new Promise(function (resolve) {
+ setTimeout(resolve, milliseconds);
+ });
+}
+
+class Contributors {
+ static async create()
+ {
+ let response = await axios.get(contributorsURL);
+ return new Contributors(response.data);
+ }
+
+ constructor(data)
+ {
+ this.emails = new Map();
+ this.entries = Object.values(data);
+ for (let [fullName, entry] of Object.entries(data)) {
+ entry.fullName = fullName;
+ for (let email of entry.emails)
+ this.emails.set(email, entry);
+ }
+ }
+
+ queryWithEmail(email)
+ {
+ return this.emails.get(email);
+ }
+}
+
+class Commit {
+ static findAndRemove(change, regExp)
+ {
+ let matched = change.match(regExp);
+ if (matched) {
+ change = change.replace(regExp, "");
+ return [matched[1], change];
+ }
+ return [null, change];
+ }
+
+ static cleanUpChange(change, contributors)
+ {
+ for (let entry of contributors.entries) {
+ if (!entry.nicks)
+ continue;
+ let nameWithNicks = `${entry.fullName} (@${entry.nicks[0]})`;
+ if (change.includes(entry.fullName)) {
+ change = replaceAll(entry.fullName, nameWithNicks, change);
+ for (let email of entry.emails)
+ change = replaceAll(`<${email}>`, "", change);
+ } else {
+ for (let email of entry.emails)
+ change = replaceAll(` ${email} `, ` ${nameWithNicks} `, change);
+ }
+ }
+ return change;
+ }
+
+ constructor(feedItem, contributors)
+ {
+ let originalChange = feedItem.contentSnippet;
+ let change = Commit.cleanUpChange(originalChange, contributors);
+ [this.revision, change] = Commit.findAndRemove(change, /^git-svn-id: https:\/\/svn\.webkit\.org\/repository\/webkit\/trunk@(\d+) /im);
+ [this.patchBy, change] = Commit.findAndRemove(change, /^Patch\s+by\s+(.+?)\s+on(?:\s+\d{4}-\d{2}-\d{2})?\n?/im);
+ [this.revert, change] = Commit.findAndRemove(change, /(?:rolling out|reverting) (r?\d+(?:(?:,\s*|,?\s*and\s+)?r?\d+)*)\.?\s*/im);
+ [this.bugzilla, change] = Commit.findAndRemove(change, /https?:\/\/(?:bugs\.webkit\.org\/show_bug\.cgi\?id=|webkit\.org\/b\/)(\d+)/im);
+ this.email = feedItem.author;
+
+ let lines = originalChange.split('\n');
+ this.title = feedItem.title;
+ if (lines.length)
+ this.title = lines[0];
+
+ if (this.patchBy)
+ this.author = this.patchBy;
+ if (!this.author) {
+ this.author = this.email;
+ if (this.email) {
+ let entry = contributors.queryWithEmail(this.email);
+ if (entry && entry.nicks && entry.nicks[0])
+ this.author = `${entry.fullName} (@${entry.nicks[0]})`;
+ }
+ }
+ if (this.revert) {
+ let matched = change.match(/^\"?(.+?)\"? \(Requested\s+by\s+(.+?)\s+on\s+#webkit\)\./im);
+ if (matched) {
+ let reason = matched[0];
+ let author = matched[1];
+ // FIXME: Implement more descriptive message when we detect that this commit is revert commit.
+ }
+ }
+ if (!this.revision)
+ throw new Error(`Canont find revision`);
+ this.revision = Number.parseInt(this.revision, 10);
+ this.url = ""
+ }
+
+ message()
+ {
+ let results = [];
+ results.push(`${this.title}`);
+ results.push(`${this.url} by ${this.author}`);
+ if (this.bugzilla)
+ results.push(`https://webkit.org/b/${this.bugzilla}`);
+ return results.join('\n');
+ }
+}
+
+class WKR {
+ constructor(revision)
+ {
+ this.revision = revision;
+ }
+
+ async postToSlack(commit)
+ {
+ let data = {
+ "text": commit.message()
+ };
+ console.log(data);
+ if (!DEBUG)
+ await axios.post(process.env.slackURL, JSON.stringify(data));
+ await sleep(500);
+ }
+
+ async action(interval)
+ {
+ try {
+ console.log(`${Date.now()}: poll data`);
+ let contributors = await Contributors.create();
+ let parser = new RSS;
+ let response = await parser.parseURL(commitEndpointURL);
+ let commits = response.items.map((feedItem) => new Commit(feedItem, contributors));
+ commits.sort((a, b) => a.revision - b.revision);
+ if (this.revision) {
+ commits = commits.filter((commit) => commit.revision > this.revision);
+ for (let commit of commits)
+ await this.postToSlack(commit);
+ }
+
+ let latestCommit = commits[commits.length - 1];
+ if (latestCommit) {
+ this.revision = latestCommit.revision;
+ await storage.setItem("revision", this.revision);
+ }
+ } catch (error) {
+ console.error(String(error));
+ }
+ return defaultInterval;
+ }
+
+ static async create()
+ {
+ await storage.init({
+ dir: 'data',
+ });
+ let revision = await storage.getItem("revision");
+ console.log(`Previous Revision: ${revision}`);
+ console.log(`Endpoint: ${process.env.slackURL}`);
+ return new WKR(revision);
+ }
+
+ static async main()
+ {
+ let bot = await WKR.create();
+ let interval = defaultInterval;
+ for (;;) {
+ let start = Date.now();
+ interval = await bot.action(interval);
+ await sleep(Math.max(interval - (Date.now() - start), 0));
+ }
+ }
+}
+
+WKR.main();
Added: trunk/Tools/WKR/data/.gitignore (0 => 260220)
--- trunk/Tools/WKR/data/.gitignore (rev 0)
+++ trunk/Tools/WKR/data/.gitignore 2020-04-16 20:42:16 UTC (rev 260220)
@@ -0,0 +1,2 @@
+*
+!.gitignore
Added: trunk/Tools/WKR/package-lock.json (0 => 260220)
--- trunk/Tools/WKR/package-lock.json (rev 0)
+++ trunk/Tools/WKR/package-lock.json 2020-04-16 20:42:16 UTC (rev 260220)
@@ -0,0 +1,101 @@
+{
+ "name": "WKR",
+ "version": "1.0.0",
+ "lockfileVersion": 1,
+ "requires": true,
+ "dependencies": {
+ "axios": {
+ "version": "0.19.2",
+ "resolved": "https://registry.npmjs.org/axios/-/axios-0.19.2.tgz",
+ "integrity": "sha512-fjgm5MvRHLhx+osE2xoekY70AhARk3a6hkN+3Io1jc00jtquGvxYlKlsFUhmUET0V5te6CcZI7lcv2Ym61mjHA==",
+ "requires": {
+ "follow-redirects": "1.5.10"
+ }
+ },
+ "debug": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz",
+ "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==",
+ "requires": {
+ "ms": "2.0.0"
+ }
+ },
+ "dotenv": {
+ "version": "8.2.0",
+ "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-8.2.0.tgz",
+ "integrity": "sha512-8sJ78ElpbDJBHNeBzUbUVLsqKdccaa/BXF1uPTw3GrvQTBgrQrtObr2mUrE38vzYd8cEv+m/JBfDLioYcfXoaw=="
+ },
+ "entities": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/entities/-/entities-1.1.2.tgz",
+ "integrity": "sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w=="
+ },
+ "follow-redirects": {
+ "version": "1.5.10",
+ "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.5.10.tgz",
+ "integrity": "sha512-0V5l4Cizzvqt5D44aTXbFZz+FtyXV1vrDN6qrelxtfYQKW0KO0W2T/hkE8xvGa/540LkZlkaUjO4ailYTFtHVQ==",
+ "requires": {
+ "debug": "=3.1.0"
+ }
+ },
+ "minimist": {
+ "version": "1.2.5",
+ "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz",
+ "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw=="
+ },
+ "mkdirp": {
+ "version": "0.5.5",
+ "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz",
+ "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==",
+ "requires": {
+ "minimist": "^1.2.5"
+ }
+ },
+ "ms": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+ "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
+ },
+ "node-persist": {
+ "version": "3.0.5",
+ "resolved": "https://registry.npmjs.org/node-persist/-/node-persist-3.0.5.tgz",
+ "integrity": "sha512-zJmBA58kI9QAxXLMc4NLswgzXVIqKfsfQtiySMF6eEQ3kVvoM3YHzcP0//L9u30Fqx3cYe1FL/a+fyB3VwO/oQ==",
+ "requires": {
+ "mkdirp": "~0.5.1"
+ }
+ },
+ "replaceall": {
+ "version": "0.1.6",
+ "resolved": "https://registry.npmjs.org/replaceall/-/replaceall-0.1.6.tgz",
+ "integrity": "sha1-gdgax663LX9cSUKt8ml6MiBojY4="
+ },
+ "rss-parser": {
+ "version": "3.7.6",
+ "resolved": "https://registry.npmjs.org/rss-parser/-/rss-parser-3.7.6.tgz",
+ "integrity": "sha512-wWWh3/pPLAPgWyfkCC9jB83jSBenU6VPMymfXiysi8wJxaN7KNkW4vU3Jm8jQxExAribFvXREy+RtaL3XQubeA==",
+ "requires": {
+ "entities": "^1.1.1",
+ "xml2js": "^0.4.19"
+ }
+ },
+ "sax": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz",
+ "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw=="
+ },
+ "xml2js": {
+ "version": "0.4.23",
+ "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.23.tgz",
+ "integrity": "sha512-ySPiMjM0+pLDftHgXY4By0uswI3SPKLDw/i3UXbnO8M/p28zqexCUoPmQFrYD+/1BzhGJSs2i1ERWKJAtiLrug==",
+ "requires": {
+ "sax": ">=0.6.0",
+ "xmlbuilder": "~11.0.0"
+ }
+ },
+ "xmlbuilder": {
+ "version": "11.0.1",
+ "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz",
+ "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA=="
+ }
+ }
+}
Added: trunk/Tools/WKR/package.json (0 => 260220)
--- trunk/Tools/WKR/package.json (rev 0)
+++ trunk/Tools/WKR/package.json 2020-04-16 20:42:16 UTC (rev 260220)
@@ -0,0 +1,20 @@
+{
+ "name": "WKR",
+ "version": "1.0.0",
+ "description": "WKR bot implementation",
+ "main": "WKR.js",
+ "dependencies": {
+ "axios": "^0.19.2",
+ "dotenv": "^8.2.0",
+ "node-persist": "^3.0.5",
+ "replaceall": "^0.1.6",
+ "rss-parser": "^3.7.6"
+ },
+ "devDependencies": {},
+ "scripts": {
+ "test": "echo \"Error: no test specified\" && exit 1",
+ "start": "$NODE ./WKR.mjs"
+ },
+ "author": "webkit.org",
+ "license": "BSD-2-Clause"
+}