Skip to content

Instantly share code, notes, and snippets.

@tobbez
Created March 13, 2010 21:09
Show Gist options
  • Save tobbez/331550 to your computer and use it in GitHub Desktop.
Save tobbez/331550 to your computer and use it in GitHub Desktop.
WhatStats
// ==UserScript==
// @name WhatStats
// @description Provides pretty charts on your user profile page on What.cd
// @namespace tobbez
// @author tobbez
// @version 1.0
// @date 2010-03-11
// @include http*://*what.cd/*
// ==/UserScript==
_id = function(id, parent) {
if (parent === undefined) parent = document;
return parent.getElementById(id);
}
_tn = function(tn, parent) {
if (parent === undefined) parent = document;
return parent.getElementsByTagName(tn);
}
_cn = function(cn, parent) {
if (parent === undefined) parent = document;
return parent.getElementsByClassName(cn);
}
whatstats = {};
/* utility functions */
whatstats.util = {};
whatstats.util.init = function() {
whatstats.util.loadLibraryCount = 0;
}
whatstats.util.cleanNumString = function(num) {
return num.replace(/,/g, '');
}
whatstats.util.toInt = function(num) {
return parseInt(whatstats.util.cleanNumString(num));
}
whatstats.util.toFloat = function(num) {
return parseFloat(whatstats.util.cleanNumString(num));
}
whatstats.util.prefixToNum = function(prefix) {
var ret = 0;
switch(prefix) {
case 'B':
ret = 1;
break;
case 'KB':
ret = 1024;
break;
case 'MB':
ret = 1048576;
break;
case 'GB':
ret = 1073741824;
break;
case 'TB':
ret = 1099511627776;
break;
case 'PB':
ret = 1125899906842624;
break;
default:
}
return ret;
}
whatstats.util.bytesToString = function(bytes) {
var unit = 'B';
var res = bytes;
if(bytes > 1125899906842624) {
unit = 'PB';
res /= 1125899906842624;
} else if (bytes > 1099511627776) {
unit = 'TB';
res /= 1099511627776;
} else if (bytes > 1073741824) {
unit = 'GB';
res /= 1073741824;
} else if (bytes > 1048576) {
unit = 'MB';
res /= 1048576;
} else if (bytes > 1024) {
unit = 'KB';
res /= 1024;
}
return res + ' ' + unit;
}
whatstats.util.loadLibrary = function(url, callback) {
whatstats.util.loadLibraryCount++;
var script = document.createElement('script');
script.type = "text/javascript";
script.src = url;
script.onload = function() {
whatstats.util.loadLibraryCount--;
if(callback) callback();
}
_tn('head')[0].appendChild(script);
}
whatstats.util.allLibrariesLoaded = function() {
return whatstats.util.loadLibraryCount == 0;
}
whatstats.util.dateToString = function(d) {
var p = function(s){return s<10?'0'+s:s;}
return d.getFullYear() + '-' + p(d.getMonth()) + '-' + p(d.getDate()) + ' ' + p(d.getHours()) + ':' + p(d.getMinutes()) + ':' + p(d.getSeconds());
}
/* main functions */
whatstats.init = function() {
whatstats.util.init();
whatstats.db = openDatabase("whatstats", "1.0", "Database for WhatStats", 5242880);
whatstats.db.transaction(
function(tx) {
tx.executeSql('CREATE TABLE IF NOT EXISTS datapoints ' +
'(id INTEGER PRIMARY KEY ASC, timestamp REAL, ' +
'up REAL, down REAL, ratio REAL, ' +
'posts INTEGER, comments INTEGER, requests_filled INTEGER, requests_filled_bytes REAL, requests_voted INTEGER, requests_voted_bytes REAL, ' +
'uploaded INTEGER, seeding INTEGER, leeching INTEGER, snatched INTEGER, invited INTEGER, ' +
'percent_up INTEGER, percent_down INTEGER, percent_torrents INTEGER, percent_requests INTEGER, percent_posts INTEGER, percent_total INTEGER)');
}
);
whatstats.updateTimerHandle = setInterval("whatstats.updateTimerCallback()", 15000);
whatstats.updateTimerCallback();
if (_cn('username')[0].href == document.location.href) {
setTimeout('whatstats.displayStatsBegin()', 100);
}
}
whatstats.clearDatabase = function() {
whatstats.db.transaction(
function(tx) {
tx.executeSql('DELETE FROM datapoints');
}
);
}
whatstats.displayStatsBegin = function() {
whatstats.util.loadLibrary(
'http://github.com/DmitryBaranovskiy/raphael/blob/master/raphael.js?raw=true',
function() {
whatstats.util.loadLibrary(
'http://github.com/DmitryBaranovskiy/g.raphael/blob/master/g.raphael-min.js?raw=true',
function() {
whatstats.util.loadLibrary('http://github.com/tobbez/g.raphael/raw/master/g.line-min.js');
});
});
whatstats.displayStatsHandle = setInterval('whatstats.displayStats()', 100);
}
whatstats.displayTable = function() {
whatstats.db.readTransaction(
function(tx) {
tx.executeSql(
'SELECT * FROM datapoints', [],
function (tx, res) {
if(res.rows.length == 0) {
alert("There is no data to view");
}
var tbl = document.createElement('table');
var tr = document.createElement('tr');
tr.className = 'colhead_dark';
for (var i in res.rows.item(0)) {
var td = document.createElement('td');
td.textContent = i;
tr.appendChild(td);
}
tbl.appendChild(tr);
for (var i = 0; i < res.rows.length; i++) {
tr = document.createElement('tr');
tr.className = i % 2 == 0 ? 'rowa' : 'rowb';
for (var j in res.rows.item(i)) {
td = document.createElement('td');
td.textContent = res.rows.item(i)[j];
tr.appendChild(td);
}
tbl.appendChild(tr);
}
whatstats.createOverlay(function(overlay) {overlay.appendChild(tbl);});
}
)
}
);
}
whatstats.createOverlay = function(fn) {
var closeImg = "data:image/png,%89PNG%0D%0A%1A%0A%00%00%00%0DIHDR%00%00%00%10%00%00%00%10%08%06%00%00%00%1F%F3%FFa%00%00%00%19tEXtSoftware%00Adobe%20ImageReadyq%C9e%3C%00%00%01%CDIDATx%DA%A4S%3DK%03A%10%9D%BD%5CVMb%13L%E5%A9%60%94%88%8D%20*%C4*%9D%7F%40%FC%0B%82%FF%C5%1F%60g!%16bak%23%11%04%83V6%1EJ%04E%9B%40%08%92%5C.%97%FB%F4%CD%E6%83D%23%0A9x%DC%CE%DC%BE7o%E7fE%14E4%CE%23X%E0L%88%94%20%DAG%BC%03%A4%FF%E0%D4%80K%94%3D%DA%8B%22K%E7%0C%82%FD%F9%D5%D5%83%95%7C%DE%90%89%84%FC%8D%C9%C5%5C%DBv%CD%DB%DB%EC%BBir%EAP%09%04%A8%BC%BC%B5eX%8E%23%C9q%FEr-%17%D7%D7%8DW%D3d%B7%87ZW%20%1Dj%9A%B4%2C%8B%18%C6%C9%09%F5%D6%A3r%5E%14%C9%A0%7BT%25%E0%03%0E*7%1A%0DZ%B9%B8Pe%F8%CD%F1%A8%1C%8B%F8%5D%3BzO%C0%B6mj6%9Bt%BE%B4D%BB%E5%B2%12%9C%3F%3DU%F92bM%D3%E8jc%83%E2%F18e2%99%BE%40%DF%81%EB%BA%E4%FB%3EM'%93t%3C3C%26%9AT%A9T%94%90%10%82%AEA%E6%EE%0A%CF%A3V%BD%FE%D3%01%93%5D%D8%B3k5J%86!%C5b1UMJ%A9%90%1A%E8%A2%0E%91!%07%1E%D0%061%AAVi*%08(W%2C%0E%91u%5D%A7%C2%D3%13%25%B0%8F!Q%CC%FB~%04%01%81%04%FE%F3%DC%00%F9as%93%EE%D7%D6%94%00c%FB%E5E%09L%C2%E1%0F%07%12%3D%E0%8Fo%85%82%22%3F%82%3C%8B%98Q%CA%E5%D4%E6%9Bl%B6%E3%02%85%BC%C1%1E%B41%9E%01%BA8%05%9D%05%C4%CF%20%2F%0C%9C%99%D7%25%90%7B%B9V%18%BA%ED%CEHw%1C%7Cb%B6K%F5%FA%07%06%C4%9DD%9Ce%9B%DF%D0%CBy%20%DF9%CE%07s%FA%0EZ%B8%18H%12%F0%EF%CB4%01N%FF6%8E%F3%7C%090%00%BC%E5%F4%2F%C0%25%EDt%00%00%00%00IEND%AEB%60%82";
var overlay = document.createElement('div');
overlay.className = 'box';
overlay.id = 'whatstats_overlay';
overlay.style.width = window.innerWidth + 'px';
overlay.style.height = window.innerHeight + 'px';
overlay.style.position = 'fixed';
overlay.style.left = '0px';
overlay.style.top = '0px';
overlay.style.overflow = 'scroll';
overlay.innerHTML = '<img alt="Close" title="Close" onClick="javascript:_tn(\'body\')[0].removeChild(_id(\'whatstats_overlay\'));" src="' + closeImg + '" /></a><br />';
fn(overlay);
_tn('body')[0].appendChild(overlay);
}
whatstats.displaySql = function() {
whatstats.db.readTransaction(
function(tx1) {
tx1.executeSql(
'SELECT sql FROM sqlite_master WHERE type="table" AND name="datapoints"', [],
function (tx2, res1) {
tx2.executeSql(
'SELECT * FROM datapoints', [],
function(tx3, res2) {
var sql = '';
sql += res1.rows.item(0).sql + ';\n';
for(var i = 0; i < res2.rows.length; i++) {
var f = [];
for (var j in res2.rows.item(i)) {
f.push(res2.rows.item(i)[j]);
}
sql += 'INSERT INTO datapoints VALUES(' + f.join(', ') + ');\n';
}
whatstats.createOverlay(
function(overlay) {
var c = document.createElement('center');
var t = document.createElement('textarea');
t.rows = '40';
t.cols = '80';
t.value = sql;
c.appendChild(t);
overlay.appendChild(c);
}
);
}
);
}
);
}
);
}
whatstats.displayStats = function() {
if (!whatstats.util.allLibrariesLoaded()) {
return;
}
clearTimeout(whatstats.displayStatsHandle);
var box_head = document.createElement('div');
box_head.className = 'head';
box_head.innerHTML =
'Extra stats<span style="float: right;">' +
'[<a href="javascript:whatstats.displayTable();">View Data in Table</a>]' +
'[<a href="javascript:whatstats.displaySql();">Dump SQL</a>]' +
'[<a href="javascript:if(confirm(\'Are you sure you want to delete all data?\')) whatstats.clearDatabase();">Clear Data</a>]</span>';
var box_content = document.createElement('div');
var box = document.createElement('div');
box.className = 'box';
box.appendChild(box_head);
box.appendChild(box_content);
_cn('main_column')[0].insertBefore(box, _cn('main_column')[0].lastElementChild);
box_content.innerHTML = '<div id="whatstats_chart"></div><hr/>';
whatstats.db.readTransaction(
function(tx) {
tx.executeSql(
'SELECT * FROM datapoints ORDER BY timestamp DESC LIMIT 42', [],
function(tx, res) {
var fields = {};
for (var i = 0; i < res.rows.length; i++) {
for (var j in res.rows.item(i)) {
if (!fields[j]) {
fields[j] = [];
}
fields[j].push(res.rows.item(i)[j]);
}
}
if (fields.timestamp.length < 3) {
box_content.textContent = "Sorry, not enough data recorded yet!";
box_content.className = 'pad'
return;
}
var r = Raphael('whatstats_chart', 588, 1800);
var hoverInBytes = function() {
this.flag = r.g.popup(this.x, this.y, whatstats.util.dateToString(new Date(this.axis*1000)) + '\n' + whatstats.util.bytesToString(this.value) || "0").insertBefore(this);
}
var hoverIn = function() {
this.flag = r.g.popup(this.x, this.y, whatstats.util.dateToString(new Date(this.axis*1000)) + '\n' + this.value || "0").insertBefore(this);
}
var colorTypeMap = {};
colorTypeMap[Raphael.fn.g.colors[0]] = 'Data uploaded';
colorTypeMap[Raphael.fn.g.colors[1]] = 'Data downloaded';
colorTypeMap[Raphael.fn.g.colors[7]] = 'Torrents Uploaded';
colorTypeMap[Raphael.fn.g.colors[5]] = 'Requests Filled';
colorTypeMap[Raphael.fn.g.colors[3]] = 'Forum Posts';
colorTypeMap['hsb(0.3,0.3,0.3)'] = 'Total';
var hoverInPercentage = function() {
this.flag = r.g.popup(this.x, this.y, whatstats.util.dateToString(new Date(this.axis*1000)) + '\n' + colorTypeMap[this.line.attrs.stroke] + ': ' + this.value + '%').insertBefore(this);
}
var hoverOut = function() {
this.flag.remove();
}
r.text(5, 10, 'Showing Period: ' + whatstats.util.dateToString(new Date(fields.timestamp[fields.timestamp.length-1]*1000)) + ' -- ' + whatstats.util.dateToString(new Date(fields.timestamp[0]*1000)))
.attr({'font': '10px tahoma, helvetica, sans-serif', 'text-anchor': 'start'});
r.text(5, 45, 'Data Uploaded').attr({'font': '12px tahoma, helvetica, sans-serif', 'text-anchor': 'start'});
r.g.linechart(55, 50, 470, 100, [fields.timestamp], [fields.up], {colors: [Raphael.fn.g.colors[0]], symbol: 's', shade: true}).hover(hoverInBytes, hoverOut);
r.path("M0,150 L588,150");
r.text(5, 165, 'Data Downloaded').attr({'font': '12px tahoma, helvetica, sans-serif', 'text-anchor': 'start'});
r.g.linechart(55, 150, 470, 100, [fields.timestamp], [fields.down], {colors: [Raphael.fn.g.colors[1]], symbol: 's', shade: true}).hover(hoverInBytes, hoverOut);
r.path("M0,250 L588,250");
r.text(5, 265, 'Ratio').attr({'font': '12px tahoma, helvetica, sans-serif', 'text-anchor': 'start'});
r.g.linechart(55, 250, 470, 100, [fields.timestamp], [fields.ratio], {colors: [Raphael.fn.g.colors[2]], symbol: 's', shade: true}).hover(hoverIn, hoverOut);
r.path("M0,350 L588,350");
r.text(5, 365, 'Forum Posts').attr({'font': '12px tahoma, helvetica, sans-serif', 'text-anchor': 'start'});
r.g.linechart(55, 350, 470, 100, [fields.timestamp], [fields.posts], {colors: [Raphael.fn.g.colors[3]], symbol: 's', shade: true}).hover(hoverIn, hoverOut);
r.path("M0,450 L588,450");
r.text(5, 465, 'Torrent Comments').attr({'font': '12px tahoma, helvetica, sans-serif', 'text-anchor': 'start'});
r.g.linechart(55, 450, 470, 100, [fields.timestamp], [fields.comments], {colors: [Raphael.fn.g.colors[4]], symbol: 's', shade: true}).hover(hoverIn, hoverOut);
r.path("M0,550 L588,550");
r.text(5, 565, 'Requests Filled').attr({'font': '12px tahoma, helvetica, sans-serif', 'text-anchor': 'start'});
r.g.linechart(55, 550, 470, 100, [fields.timestamp], [fields.requests_filled], {colors: [Raphael.fn.g.colors[5]], symbol: 's', shade: true}).hover(hoverIn, hoverOut);
r.path("M0,650 L588,650");
r.text(5, 665, 'Requests Filled (bytes)').attr({'font': '12px tahoma, helvetica, sans-serif', 'text-anchor': 'start'});
r.g.linechart(55, 650, 470, 100, [fields.timestamp], [fields.requests_filled_bytes], {colors: [Raphael.fn.g.colors[5]], symbol: 's', shade: true}).hover(hoverInBytes, hoverOut);
r.path("M0,750 L588,750");
r.text(5, 765, 'Requests Voted').attr({'font': '12px tahoma, helvetica, sans-serif', 'text-anchor': 'start'});
r.g.linechart(55, 750, 470, 100, [fields.timestamp], [fields.requests_voted], {colors: [Raphael.fn.g.colors[6]], symbol: 's', shade: true}).hover(hoverIn, hoverOut);
r.path("M0,850 L588,850");
r.text(5, 865, 'Requests Voted (bytes)').attr({'font': '12px tahoma, helvetica, sans-serif', 'text-anchor': 'start'});
r.g.linechart(55, 850, 470, 100, [fields.timestamp], [fields.requests_voted_bytes], {colors: [Raphael.fn.g.colors[6]], symbol: 's', shade: true}).hover(hoverInBytes, hoverOut);
r.path("M0,950 L588,950");
r.text(5, 965, 'Torrents Uploaded').attr({'font': '12px tahoma, helvetica, sans-serif', 'text-anchor': 'start'});
r.g.linechart(55, 950, 470, 100, [fields.timestamp], [fields.uploaded], {colors: [Raphael.fn.g.colors[7]], symbol: 's', shade: true}).hover(hoverIn, hoverOut);
r.path("M0,1050 L588,1050");
r.text(5, 1065, 'Torrents Seeding').attr({'font': '12px tahoma, helvetica, sans-serif', 'text-anchor': 'start'});
r.g.linechart(55, 1050, 470, 100, [fields.timestamp], [fields.seeding], {colors: [Raphael.fn.g.colors[8]], symbol: 's', shade: true}).hover(hoverIn, hoverOut);
r.path("M0,1150 L588,1150");
r.text(5, 1165, 'Torrents Leeching').attr({'font': '12px tahoma, helvetica, sans-serif', 'text-anchor': 'start'});
r.g.linechart(55, 1150, 470, 100, [fields.timestamp], [fields.leeching], {colors: [Raphael.fn.g.colors[9]], symbol: 's', shade: true}).hover(hoverIn, hoverOut);
r.path("M0,1250 L588,1250");
r.text(5, 1265, 'Torrents Snatched').attr({'font': '12px tahoma, helvetica, sans-serif', 'text-anchor': 'start'});
r.g.linechart(55, 1250, 470, 100, [fields.timestamp], [fields.snatched], {colors: ['hsb(0,1,0.5)'], symbol: 's', shade: true}).hover(hoverIn, hoverOut);
r.path("M0,1350 L588,1350");
r.text(5, 1365, 'Users Invited').attr({'font': '12px tahoma, helvetica, sans-serif', 'text-anchor': 'start'});
r.g.linechart(55, 1350, 470, 100, [fields.timestamp], [fields.invited], {colors: ['hsb(0.9,0.5,0.5)'], symbol: 's', shade: true}).hover(hoverIn, hoverOut);
r.path("M0,1450 L588,1450");
r.text(5, 1465, 'Percentages').attr({'font': '12px tahoma, helvetica, sans-serif', 'text-anchor': 'start'});
r.g.linechart(55, 1465, 470, 300, [fields.timestamp],
[fields.percent_up, fields.percent_down, fields.percent_torrents, fields.percent_requests, fields.percent_posts, fields.percent_total],
{shade: true, colors: [Raphael.fn.g.colors[0], Raphael.fn.g.colors[1], Raphael.fn.g.colors[7], Raphael.fn.g.colors[5], Raphael.fn.g.colors[3], 'hsb(0.3,0.3,0.3)'],
legend: [], legendpos: 'east'}).hover(hoverInPercentage, hoverOut);
});
}
);
}
whatstats.updateStats = function() {
whatstats.userPageDocument = document.implementation.createHTMLDocument('');
var xhr = new XMLHttpRequest();
xhr.open('GET', _cn('username')[0].href, false);
xhr.send(null);
whatstats.userPageDocument.open();
whatstats.userPageDocument.write(xhr.responseText);
whatstats.userPageDocument.close();
var tmp;
tmp = _tn('li', _cn('stats', whatstats.userPageDocument)[0])[2].textContent.split(' ');
var up = whatstats.util.toFloat(tmp[1]) * whatstats.util.prefixToNum(tmp[2]);
tmp = _tn('li', _cn('stats', whatstats.userPageDocument)[0])[3].textContent.split(' ');
var down = whatstats.util.toFloat(tmp[1]) * whatstats.util.prefixToNum(tmp[2]);
tmp = _tn('li', _cn('stats', whatstats.userPageDocument)[0])[4].textContent.split(' ');
var ratio = whatstats.util.toFloat(tmp[1]);
tmp = _tn('li', _cn('stats', whatstats.userPageDocument)[3])[0].textContent.split(' ');
var posts = whatstats.util.toInt(tmp[2]);
tmp = _tn('li', _cn('stats', whatstats.userPageDocument)[3])[1].textContent.split(' ');
var comments = whatstats.util.toInt(tmp[2]);
tmp = _tn('li', _cn('stats', whatstats.userPageDocument)[3])[2].textContent.split(' ');
var requests_filled = whatstats.util.toInt(tmp[2]);
var requests_filled_bytes = whatstats.util.toFloat(tmp[4]) * whatstats.util.prefixToNum(tmp[5]);
tmp = _tn('li', _cn('stats', whatstats.userPageDocument)[3])[3].textContent.split(' ');
var requests_voted = whatstats.util.toInt(tmp[2]);
var requests_voted_bytes = whatstats.util.toFloat(tmp[4]) * whatstats.util.prefixToNum(tmp[5]);
tmp = _tn('li', _cn('stats', whatstats.userPageDocument)[3])[4].textContent.split(' ');
var uploaded = whatstats.util.toInt(tmp[1]);
tmp = _tn('li', _cn('stats', whatstats.userPageDocument)[3])[5].textContent.split(' ');
var seeding = whatstats.util.toInt(tmp[1]);
tmp = _tn('li', _cn('stats', whatstats.userPageDocument)[3])[6].textContent.split(' ');
var leeching = whatstats.util.toInt(tmp[1]);
tmp = _tn('li', _cn('stats', whatstats.userPageDocument)[3])[7].textContent.split(' ');
var snatched = whatstats.util.toInt(tmp[1]);
tmp = _tn('li', _cn('stats', whatstats.userPageDocument)[3])[8].textContent.split(' ');
var invited = whatstats.util.toInt(tmp[1]);
tmp = _tn('li', _cn('stats', whatstats.userPageDocument)[1])[0].textContent.split(' ');
var percent_up = whatstats.util.toInt(tmp[2]);
tmp = _tn('li', _cn('stats', whatstats.userPageDocument)[1])[1].textContent.split(' ');
var percent_down = whatstats.util.toInt(tmp[2]);
tmp = _tn('li', _cn('stats', whatstats.userPageDocument)[1])[2].textContent.split(' ');
var percent_torrents = whatstats.util.toInt(tmp[2]);
tmp = _tn('li', _cn('stats', whatstats.userPageDocument)[1])[3].textContent.split(' ');
var percent_requests = whatstats.util.toInt(tmp[2]);
tmp = _tn('li', _cn('stats', whatstats.userPageDocument)[1])[4].textContent.split(' ');
var percent_posts = whatstats.util.toInt(tmp[2]);
tmp = _tn('li', _cn('stats', whatstats.userPageDocument)[1])[5].textContent.split(' ');
var percent_total = whatstats.util.toInt(tmp[2]);
whatstats.db.transaction(function(tx) {
tx.executeSql('INSERT INTO datapoints ' +
'(timestamp, ' +
'up, down, ratio, ' +
'posts, comments, requests_filled, requests_filled_bytes, requests_voted, requests_voted_bytes, ' +
'uploaded, seeding, leeching, snatched, invited, ' +
'percent_up, percent_down, percent_torrents, percent_requests, percent_posts, percent_total' +
') VALUES (strftime("%s", "now"), ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)',
[up, down, ratio,
posts, comments, requests_filled, requests_filled_bytes, requests_voted, requests_voted_bytes,
uploaded, seeding, leeching, snatched, invited,
percent_up, percent_down, percent_torrents, percent_requests, percent_posts, percent_total]);
});
}
whatstats.updateTimerCallback = function() {
whatstats.db.readTransaction(function(tx) {
var res = tx.executeSql(
'SELECT timestamp FROM datapoints ORDER BY timestamp DESC LIMIT 1;', [],
function(tx, res) {
if (res.rows.length == 0 || (new Date().getTime()/1000) - res.rows.item(0).timestamp > 3600 * 4) {
setTimeout("whatstats.updateStats()", 100);
}});
});
}
document.addEventListener("DOMContentLoaded", whatstats.init, false);
if (window.navigator.userAgent.toLowerCase().indexOf('chrome') > -1)
whatstats.init();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment