I've made a userscript that oneboxes links to GitHub repos, issues or pull requests in Chat after seeing the request One-box repositories, issue tickets and such on GitHub in the chat on Meta.
It uses the GitHub API to get any relevant information and displaying it in a onebox-like manner. It uses MutationObserver
to check for new messages and looks for GitHub links in those messages.
It contains a few functions:
replaceVars(url, details)
- replaces placeholders inurl
(in the form of{placeholder name}
) with their real values fromdetails
useApi(url, details, callback)
- sends the GET request to Github to get information from the API.url
is the URL the GET request is sent to, andcallback
is... the callbackextractFromUrlAndGetInfo(link, $obj)
- gets the necessary details from the URL someone posted in chat (link
is the URL they posted).$obj
is the jQuery object which is the actual message (this will be repalced with the oneboxed form of the message)getInfo(type, details, $element)
- usesuseApi()
to get the details from the API, and depending on thetype
(repo
/issue
/pull
), usesreplaceVars()
to replace different placeholders. The details for the replacement are indetails
and$element
is the jQuery object which is the actual message.
My main questions are:
- Is my code readable easily? If not, how can I improve it?
- Is there a cleaner way to do something (anything!) in the script? For example, to check whether there is another word, I've used
indexOf(' ')
- is there a more preferable way to check for multiple words? - Can the code be cleaned up anymore?
// ==UserScript==
// @name SE Chat Github Oneboxer
// @namespace http://stackexchange.com/users/4337810/
// @version 1.1.1
// @description Oneboxes links to Github repos, issues, or pull requests in Chat
// @author ᔕᖺᘎᕊ (http://stackexchange.com/users/4337810/)
// @match *://chat.stackoverfow.com/*
// @match *://chat.meta.stackexchange.com/*
// @match *://chat.stackexchange.com/*
// @require http://timeago.yarp.com/jquery.timeago.js
// @grant none
// ==/UserScript==
$('head').append('<link rel="stylesheet" type="text/css" href="https://cdn.rawgit.com/shu8/SEChat-githubOneboxer/master/style.css">'); //add stylesheet to head (CSS from https://meta.stackexchange.com/q/243259/260841)
function replaceVars(url, details) { //Replace the {placeholders} with their actual values from the details array
return url.replace(/{username}/, details.username).replace(/{repo_name}/, details.repo_name).replace(/{issue_number}/, details.issue_number).replace(/{pull_number}/, details.pull_number);
}
function useApi(url, details, callback) { //use the Github API to get the info
$.ajax({
url: replaceVars(url, details), //the URL should be the replaced version
headers: {
"Accept": "application/vnd.github.VERSION.html", //to get HTML versions of body, and not markdown
"User-Agent": "shu8" //https://developer.github.com/v3/#user-agent-required
},
success: function(data) {
callback(data);
}
});
}
function extractFromUrlAndGetInfo(link, $obj) { //to avoid repetition (for existing/new messages), a function to call getInfo with correct parameters
var info = link.split('github.com')[1];
var username = info.split('/')[1],
repo_name = info.split('/')[2],
issue_number, pull_number;
if (link.indexOf('issues') > -1) { //for issues
issue_number = info.split('/')[4];
getInfo('issue', { //get issue info via the API
username: username, //with the variables
repo_name: repo_name,
issue_number: issue_number
}, $obj); //pass on the jQuery element we need to onebox
} else if (link.indexOf('pull') > -1) { //for pull requests
pull_number = info.split('/')[4];
getInfo('pull', { //get pull info via the API
username: username, //with the variables
repo_name: repo_name,
pull_number: pull_number
}, $obj); //pass on the jQuery element we need to onebox
} else {
getInfo('repo', { //get repo info via the API
username: username, //with the variables
repo_name: repo_name
}, $obj); //pass on the jQuery element we need to onebox
}
}
function getInfo(type, details, $element) {
switch (type) {
case 'issue': //for issues
useApi("https://api.github.com/repos/{username}/{repo_name}/issues/{issue_number}", details, function(data) { //sends URL with placeholders to useApi and a callback follows:
var body = $(data.body_html).find('img').remove().end().text(), //remove images from the body
title = data.title,
number = data.number,
opener = data.user.login,
creationTime = data.created_at,
url = data.html_url,
comments = data.comments,
avatar = data.user.avatar_url,
assignee = (data.assignee == null ? '<span class="milestone">not yet assigned</span>' : '<span class="milestone">assigned to <a href="'+data.assignee.url+'">'+data.assignee.login+'</a></span>'), //not a milestone, but same CSS, so same class as milestone!
labels = (data.labels == null ? '' : data.labels),
milestone = (data.milestone == null ? '<span class="milestone">no milestone</span>' : '<span class="milestone">'+data.milestone.title+' milestone</span>'); //get milestones; surround with span
var labelSpan = ''; //get labels and suround them with spans for their own colour
if(labels!='') {
$.each(labels, function(i,o) {
labelSpan += "<span class='label' style='background-color:#"+o.color+";'>"+o.name+"</span>";
});
}
if (body.length > 519) { //trim the body if it's >= 520
body = body.substr(0, 520);
};
$element.find('.content').html("<div class='ob-github ob-github-main'>" + //make the onebox
"<img title='" + opener + "' src='" + avatar + "'>" +
"<a href='" + url + "' class='title'>" +
"<span class='title'>" + title + "</span></a>" +
" <span class='id'>#" + number + "</span>" + (labelSpan!='' ? labelSpan + milestone : milestone) + //if no labels, show milestone, else, show both
"<div class='ob-github-main-info'>" +
"<span class='author'>" + opener + "</span> opened this issue " +
"<time class='timeago' datetime='" + creationTime + "' is='relative-time'></time>." +
assignee + "<br><p>" +
body + "<span class='comments'> " + comments + " comments</span></p></div>" +
"</div>");
$("time.timeago").timeago();
});
break;
case 'repo': //for repos
useApi("https://api.github.com/repos/{username}/{repo_name}", details, function(data) {
var owner = data.owner.login,
name = data.name,
description = data.description,
url = data.url,
avatar = data.owner.avatar_url,
creationTime = data.created_at;
$element.find('.content').html("<div class='ob-github ob-github-main'>" + //make the onebox
"<img title='" + owner + "' src='" + avatar + "'>" +
"<a href='" + url + "' class='title'>" +
"<span class='title'>" + name + "</span></a>" +
"<div class='ob-github-main-info'>" +
"<span class='author'>" + owner + "</span> made this repo " +
"<time class='timeago' datetime='" + creationTime + "' is='relative-time'></time><p>" +
"<p>" + description + "</p></div>" +
"</div>");
$("time.timeago").timeago();
});
break;
case 'pull': //for pull requests
useApi("https://api.github.com/repos/{username}/{repo_name}/pulls/{pull_number}", details, function(data) {
var title = data.title,
body = $(data.body_html).find('img').remove().end().text(), //remove images from the body
number = data.number,
url = data.url,
creator = data.user.login,
creationTime = data.created_at,
avatar = data.head.user.avatar_url,
comments = data.comments;
$element.find('.content').html("<div class='ob-github ob-github-main'>" +
"<img title='" + creator + "' src='" + avatar + "'>" +
"<a href='" + url + "' class='title'>" +
"<span class='title'>" + title + "</span></a>" +
" <span class='id'>#" + number + "</span>" +
"<div class='ob-github-main-info'>" +
"<span class='author'>" + creator + "</span> submitted this pull request " +
"<time class='timeago' datetime='" + creationTime + "' is='relative-time'></time><br><p>" +
body + "<span class='comments'> " + comments + " comments</span></p></div>" +
"</div>");
$("time.timeago").timeago();
});
break;
}
}
var observer = new MutationObserver(function(mutations) { //MutationObserver
mutations.forEach(function(mutation) {
var i;
for (i = 0; i < mutation.addedNodes.length; i++) {
var $addedNode = $(mutation.addedNodes[i]);
if ($addedNode.hasClass('message')) { //if the new node is a message
if ($addedNode.find('a').length) { //if there is a link in the message
if($addedNode.text().trim().indexOf(' ') == -1) { //if there is no space (ie. nothing other than the link)
if ($addedNode.find('a:last').attr('href').indexOf('github') > -1) { //if the link is to github
var link = $addedNode.find('a:last').attr('href'); //get the link
extractFromUrlAndGetInfo(link, $addedNode); //pass URL and added node to the function which will go on to call useApi and add the onebox
}
}
}
}
}
});
});
setTimeout(function() {
$('.message').each(function() { //loop through EXISTING messages to find oneboxable messages
if ($(this).find('a[href*="github.com"]').length) { //if there is a link to github
if($(this).text().trim().indexOf(' ') == -1) { //if there is no space (ie. nothing other than the link)
var link = $(this).find('a[href*="github.com"]').attr('href');
extractFromUrlAndGetInfo(link, $(this)); //pass URL and message to the function which will go on to call useApi and add the onebox
}
}
});
setTimeout(function() { //use the timeago plugin to add relative times to the onebox for EXISTING messages
$("time.timeago").timeago();
}, 1000);
observer.observe($('#chat')[0], { //observe with the mutation observer for NEW messages
childList: true,
subtree: true
});
}, 1000);
I've posted this on StackApps where you can install this if you like, but I will update it when I get any feedback!
A link to Atom Editor's repo, oneboxed:
If you're interested, test it here, where I did.