{"action":"created","comment":{"url":"https://api.github.com/repos/euank/EuIrcBot/pulls/comments/152471622","pull_request_review_id":78337764,"id":152471622,"diff_hunk":"@@ -0,0 +1,320 @@\n+/*\n+ * This module prints ActivityPub Note and Person objects based on a URL. Its\n+ * primary use is to print information about permalinks on Mastodon instances,\n+ * hence the module name, but could theoretically be used on any endpoint that\n+ * supports those ActivityPub object types.\n+ *\n+ * Because of the focus on Mastodon instances (on a sufficiently recent\n+ * version), assumptions are made about how to operate. This module is not\n+ * expected to be a fully-fledged ActivityPub client. Issues with handling URLs\n+ * on Mastodon 1.6.0 and later are considered bugs. Other issues will be\n+ * handled on a case-by-case basis.\n+ *\n+ * Mastodon instances can be on any hostname. Instance autodiscovery is\n+ * somewhat irresponsible as it would cause a request to be made every time a\n+ * URL is posted to a channel.\n+ *\n+ * Instances are thus discovered from other instances organically. A small list\n+ * of instances is included in default_instances.json, which is used in a\n+ * default configuration. When a toot is fetched, this bot also fetches\n+ * /api/v1/timelines/public and learns new instances from that.\n+ *\n+ * In case an instance isn't already known, users can share a toot URL with the\n+ * !masto command, which will force the bot to do an API lookup.\n+ *\n+ * Bot administrators can block hostnames from being used as instances by\n+ * adding them to the \"blocked\" key in this module's config.json.\n+ */\n+\n+var arrayUniq = require('array-uniq')\n+  , Cacheman = require('cacheman')\n+  , contentType = require('content-type')\n+  , execSync = require('child_process').execSync\n+  , htmlToTextMod = require('html-to-text')\n+  , robotsParser = require('robots-parser')\n+  , robotsTxtCache = new Cacheman()\n+  , sprintf = require('extsprintf').sprintf\n+  , util = require('util')\n+  , urlMod = require('url');\n+\n+/* variables defined in module.exports.init */\n+var bot = null\n+  , log = null\n+  , moduleConfig = null\n+  , request = null;\n+\n+function robotRequest(options, fn) {\n+  var url = options.url;\n+  var userAgent = moduleConfig != null && moduleConfig.userAgent || 'EuIrcBot';\n+  var robotsTxtUrl = urlMod.resolve(url, '/robots.txt');\n+\n+  var handle = function (robotsTxt) {\n+    robots = robotsParser(robotsTxtUrl, robotsTxt);\n+    /* `User-agent: *` rules are generally overzealous. We're not a\n+     * search engine spider. */\n+    delete robots._rules['*'];\n+\n+    if (robots.isAllowed(url, userAgent)) {\n+      request(options, fn);\n+    } else {\n+      fn(new Error(util.format('URL access disallowed by %s: %s', robotsTxtUrl, url)));\n+    }\n+  };\n+\n+  robotsTxtCache.get(robotsTxtUrl, function (err, robotsTxt) {\n+    if (err)\n+      return fn(new Error('robots.txt cache error: ' + err.message));\n+    if (robotsTxt)\n+      return handle(robotsTxt);\n+\n+    request(robotsTxtUrl, function (err, response, robotsTxt) {\n+      if (err)\n+        return fn(new Error(util.format('failed to fetch %s: %s', robotsTxtUrl, err.message)));\n+\n+      /* Determine TTL for this robots.txt. Google says that requests are\n+       * \"generally cached for up to one day\" but that it may adjust that\n+       * based on max-age Cache-Control headers, which seems reasonable. */\n+      var ttl = 86400;\n+      if (response.headers['cache-control']) {\n+        var match = response.headers['cache-control'].match(/\\bmax-age=([0-9]+)\\b/);\n+        if (match) {\n+          /* Clamp this value to between (300, 604800) to guard against\n+           * unreasonable configuration / bugs in this code. */\n+          ttl = Math.min(Math.max(parseInt(match[1], 10), 300), 604800);\n+        }\n+      }\n+      robotsTxtCache.set(robotsTxtUrl, robotsTxt, ttl, function (err) {\n+        if (err)\n+          return fn(new Error('robots.txt cache error: ' + err.message));\n+\n+        handle(robotsTxt);\n+      });\n+    });\n+  });\n+}\n+\n+function defaultConfig() {\n+  return {'instances': require('./default_instances.json')};\n+}\n+\n+function writeConfig() {\n+  bot.writeDataFile('config.json', JSON.stringify(moduleConfig), function(err) {\n+    if (err)\n+      log.error('error writing mastodon module data: ' + err);\n+  });\n+}\n+\n+function checkHostname(hostname) {\n+  if (moduleConfig.blocks && moduleConfig.blocks.includes(hostname))\n+    return false;\n+\n+  return moduleConfig.instances && moduleConfig.instances.includes(hostname);\n+}\n+\n+/* TODO: Add support for validating GNU social? */\n+/* TODO: Don't check instances more than once a day. */\n+function forceCheckHostname(url, fn) {\n+  robotRequest(\n+    { url: urlMod.resolve(url, '/api/v1/instance')},\n+    (error, response, body) => {\n+      if (error || response.statusCode != 200)\n+        return fn(false);\n+\n+      try {\n+        body = JSON.parse(body);\n+      } catch (e) {\n+        return fn(false);\n+      }\n+\n+      if (!body.uri)\n+        return fn(false);\n+\n+      fn(true);\n+      log.info(`adding ${response.request.host} to instance list`);\n+      if (moduleConfig.instances) {\n+        moduleConfig.instances.push(response.request.host);\n+      } else {\n+        moduleConfig.instances = [response.request.host];\n+      }\n+      writeConfig();\n+    }\n+  );\n+}\n+\n+/*\n+ * fetchResource only attempts to fetch the URL given as a particular content\n+ * type. This is less than what a full ActivityPub-implementing client needs to\n+ * handle; following Link headers and <link> HTML tags of the correct content\n+ * types would also be required.\n+ */\n+function fetchResource(url, fn) {\n+  robotRequest({\n+    url: url,\n+    headers: { 'Accept': 'application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\", application/activity+json' },\n+  }, (error, response, body) => {\n+    if (error) {\n+      fn(error);\n+    } else if (response.statusCode != 200) {\n+      fn(new Error('response status code is ' + response.statusCode));\n+    } else if (!['application/ld+json', 'application/activity+json'].includes(\n+      contentType.parse(response.headers['content-type']).type\n+    )) {\n+      fn(new Error('resource is not an ActivityPub record'));\n+    } else {\n+      try {\n+        var record = JSON.parse(body);\n+        if (!['Note', 'Person'].includes(record.type)) {\n+          fn(new Error('record is not a Note or Person'));\n+        } else {\n+          fn(null, record);\n+        }\n+      } catch (e) {\n+        fn(e);\n+      }\n+    }\n+  });\n+}\n+\n+function htmlToText(s) {\n+  return htmlToTextMod.fromString(s, {\n+    wordwrap: null,\n+    ignoreHref: true,\n+  });\n+}\n+\n+function formatRecord(record, fn) {\n+  if (record.type == 'Note') {\n+    fetchResource(record.attributedTo, (error, userRecord) => {\n+      var str = '';\n+      if (error) {\n+        str += '(error getting user): ';\n+      } else {\n+        str += `${userRecord.name} (@${userRecord.preferredUsername}): `;\n+      }\n+      if (record.sensitive) {\n+        str += sprintf('[content warning] %s', record.summary);\n+      } else {\n+        str += htmlToText(record.content);\n+      }\n+      fn(str);\n+    });\n+  } else if (record.type == 'Person') {\n+    fn(`${record.name} (@${record.preferredUsername}): ` + htmlToText(record.summary));\n+  }\n+}\n+\n+module.exports.commands = ['masto'];\n+\n+module.exports.init = function(b) {\n+  bot = b;\n+  log = bot.log;\n+\n+  bot.readDataFile('config.json', function(err, data) {\n+    if (err) {\n+      log.info('initializing mastodon module data');\n+      moduleConfig = defaultConfig();\n+    } else {\n+      try {\n+        moduleConfig = JSON.parse(data);\n+      } catch(ex) {\n+        moduleConfig = defaultConfig();\n+      }\n+    }\n+    writeConfig();\n+  });\n+\n+  var userAgent = moduleConfig && moduleConfig.userAgent || 'EuIrcBot';\n+  try {\n+    var ver = require('../../package.json').version;\n+    userAgent += '/' + ver;\n+  } catch (ex) {\n+    log.error('error getting package version: ' + ex);\n+  }\n+\n+  var userAgentUrl = (moduleConfig && moduleConfig.userAgentUrl\n+    || 'https://github.com/euank/EuIrcBot/blob/{commitish}/modules/mastodon/info.md');\n+  if (userAgentUrl.includes('{commitish}')) {\n+    var commitish = 'master';\n+    try {\n+      commitish = execSync('git rev-parse --short HEAD', {'encoding': 'utf8'}).trim();\n+    } catch (ex) {\n+      log.error('error determining deployed commit: ' + ex);\n+    }\n+    userAgentUrl = userAgentUrl.replace(/{commitish}/g, commitish);\n+  }\n+  userAgent += ' (+' + userAgentUrl + ')';\n+\n+  request = require('request').defaults({\n+    headers: {'User-Agent': userAgent}\n+  });\n+};\n+\n+function handleUrl(url, reply) {\n+  fetchResource(url, (error, record) => {\n+    if (error) {\n+      error.mastodonUrl = url;\n+      log.info(error);\n+      reply('error: ' + error.message);\n+    } else {\n+      formatRecord(record, (s) => reply.custom({ replaceNewlines: true }, s));\n+    }\n+  });\n+\n+  /*\n+   * Instance autodiscovery: hit /api/v1/timelines/public and look for any new\n+   * instance domains, then run those through forceCheckHostname to see if they\n+   * ought to be added to our list.\n+   *\n+   * This will cause Pleroma instances to be added to the whitelist, even\n+   * though they don't support ActivityPub objects, because they support the\n+   * Mastodon client API.\n+   */\n+\n+  robotRequest(\n+    { url: urlMod.resolve(url, '/api/v1/timelines/public') },\n+    (error, response, body) => {\n+      if (error || response.statusCode != 200)\n+        return;\n+\n+      try {\n+        body = JSON.parse(body);\n+      } catch (e) {\n+        return;\n+      }\n+\n+      arrayUniq(body.map((item) => urlMod.parse(item.url))).forEach((url) => {\n+        if (moduleConfig.blocks && moduleConfig.blocks.includes(url.host))\n+          return;\n+        if (!moduleConfig.instances || !moduleConfig.instances.includes(url.host))\n+          forceCheckHostname(url, () => {});\n+      });\n+    }\n+  );\n+}\n+\n+/*\n+ * When a user calls \"!masto [url]\", both run and url get called. The first\n+ * thing we do in each of these methods is check the domain against\n+ * knownInstances; run only continues if the instance is unknown, and url only\n+ * continues if the instance is known.\n+ */\n+\n+module.exports.run = function(remainder, parts, reply, command, from, to, text, raw) {\n+  url = urlMod.parse(remainder);\n+  if (!checkHostname(url.host)) {\n+    forceCheckHostname(url, (ok) => {\n+      if (ok) {\n+        handleUrl(url.href, reply);\n+      } else {\n+        reply('error: not a mastodon instance');\n+      }\n+    });\n+  }\n+};\n+\n+module.exports.url = function(url, reply) {\n+  url = urlMod.parse(url);","path":"modules/mastodon/index.js","position":316,"original_position":316,"commit_id":"015c28e39efa102d8a96da7c99af999993024117","original_commit_id":"015c28e39efa102d8a96da7c99af999993024117","user":{"login":"euank","id":2147649,"avatar_url":"https://avatars0.githubusercontent.com/u/2147649?v=4","gravatar_id":"","url":"https://api.github.com/users/euank","html_url":"https://github.com/euank","followers_url":"https://api.github.com/users/euank/followers","following_url":"https://api.github.com/users/euank/following{/other_user}","gists_url":"https://api.github.com/users/euank/gists{/gist_id}","starred_url":"https://api.github.com/users/euank/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/euank/subscriptions","organizations_url":"https://api.github.com/users/euank/orgs","repos_url":"https://api.github.com/users/euank/repos","events_url":"https://api.github.com/users/euank/events{/privacy}","received_events_url":"https://api.github.com/users/euank/received_events","type":"User","site_admin":false},"body":"Actually, my bad for that comment. I forgot you rewrote the `onurl` module to ensure it parses before calling this, so this can't throw. You're right, disregard my comment. 👍 ","created_at":"2017-11-22T05:17:00Z","updated_at":"2017-11-22T05:17:00Z","html_url":"https://github.com/euank/EuIrcBot/pull/164#discussion_r152471622","pull_request_url":"https://api.github.com/repos/euank/EuIrcBot/pulls/164","author_association":"OWNER","_links":{"self":{"href":"https://api.github.com/repos/euank/EuIrcBot/pulls/comments/152471622"},"html":{"href":"https://github.com/euank/EuIrcBot/pull/164#discussion_r152471622"},"pull_request":{"href":"https://api.github.com/repos/euank/EuIrcBot/pulls/164"}},"in_reply_to_id":152456174},"pull_request":{"url":"https://api.github.com/repos/euank/EuIrcBot/pulls/164","id":150703204,"html_url":"https://github.com/euank/EuIrcBot/pull/164","diff_url":"https://github.com/euank/EuIrcBot/pull/164.diff","patch_url":"https://github.com/euank/EuIrcBot/pull/164.patch","issue_url":"https://api.github.com/repos/euank/EuIrcBot/issues/164","number":164,"state":"open","locked":false,"title":"Add mastodon module","user":{"login":"ilianaw","id":52814,"avatar_url":"https://avatars2.githubusercontent.com/u/52814?v=4","gravatar_id":"","url":"https://api.github.com/users/ilianaw","html_url":"https://github.com/ilianaw","followers_url":"https://api.github.com/users/ilianaw/followers","following_url":"https://api.github.com/users/ilianaw/following{/other_user}","gists_url":"https://api.github.com/users/ilianaw/gists{/gist_id}","starred_url":"https://api.github.com/users/ilianaw/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ilianaw/subscriptions","organizations_url":"https://api.github.com/users/ilianaw/orgs","repos_url":"https://api.github.com/users/ilianaw/repos","events_url":"https://api.github.com/users/ilianaw/events{/privacy}","received_events_url":"https://api.github.com/users/ilianaw/received_events","type":"User","site_admin":false},"body":"```\r\n01:35 <@ilianaw> https://witches.town/@iliana\r\n01:35 < ^_^> iliana (@iliana): linux witch ∗ she/her ⚧ ∗ gay as hell ∗ seattle ∗ [en] ∗ https://keybase.io/ilianaw ∗ also shitposting at @wobscale ∗ #nobot\r\n01:35 <@ilianaw> https://glitch.social/@bea/98948753952798509\r\n01:35 < ^_^> beatrix bitrot (@bea): @iliana its weeken ilanianaina\r\n01:35 <@ilianaw> !masto http://invalid.example\r\n01:35 < ^_^> error: not a mastodon instance\r\n```","created_at":"2017-11-05T01:36:35Z","updated_at":"2017-11-22T05:17:00Z","closed_at":null,"merged_at":null,"merge_commit_sha":"46609e63d9a70e8a20a8a0d9549af24192e8a7ed","assignee":null,"assignees":[],"requested_reviewers":[],"milestone":null,"commits_url":"https://api.github.com/repos/euank/EuIrcBot/pulls/164/commits","review_comments_url":"https://api.github.com/repos/euank/EuIrcBot/pulls/164/comments","review_comment_url":"https://api.github.com/repos/euank/EuIrcBot/pulls/comments{/number}","comments_url":"https://api.github.com/repos/euank/EuIrcBot/issues/164/comments","statuses_url":"https://api.github.com/repos/euank/EuIrcBot/statuses/015c28e39efa102d8a96da7c99af999993024117","head":{"label":"ilianaw:mastodon-support","ref":"mastodon-support","sha":"015c28e39efa102d8a96da7c99af999993024117","user":{"login":"ilianaw","id":52814,"avatar_url":"https://avatars2.githubusercontent.com/u/52814?v=4","gravatar_id":"","url":"https://api.github.com/users/ilianaw","html_url":"https://github.com/ilianaw","followers_url":"https://api.github.com/users/ilianaw/followers","following_url":"https://api.github.com/users/ilianaw/following{/other_user}","gists_url":"https://api.github.com/users/ilianaw/gists{/gist_id}","starred_url":"https://api.github.com/users/ilianaw/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ilianaw/subscriptions","organizations_url":"https://api.github.com/users/ilianaw/orgs","repos_url":"https://api.github.com/users/ilianaw/repos","events_url":"https://api.github.com/users/ilianaw/events{/privacy}","received_events_url":"https://api.github.com/users/ilianaw/received_events","type":"User","site_admin":false},"repo":{"id":103865210,"name":"EuIrcBot","full_name":"ilianaw/EuIrcBot","owner":{"login":"ilianaw","id":52814,"avatar_url":"https://avatars2.githubusercontent.com/u/52814?v=4","gravatar_id":"","url":"https://api.github.com/users/ilianaw","html_url":"https://github.com/ilianaw","followers_url":"https://api.github.com/users/ilianaw/followers","following_url":"https://api.github.com/users/ilianaw/following{/other_user}","gists_url":"https://api.github.com/users/ilianaw/gists{/gist_id}","starred_url":"https://api.github.com/users/ilianaw/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ilianaw/subscriptions","organizations_url":"https://api.github.com/users/ilianaw/orgs","repos_url":"https://api.github.com/users/ilianaw/repos","events_url":"https://api.github.com/users/ilianaw/events{/privacy}","received_events_url":"https://api.github.com/users/ilianaw/received_events","type":"User","site_admin":false},"private":false,"html_url":"https://github.com/ilianaw/EuIrcBot","description":"A featureful nodejs irc bot","fork":true,"url":"https://api.github.com/repos/ilianaw/EuIrcBot","forks_url":"https://api.github.com/repos/ilianaw/EuIrcBot/forks","keys_url":"https://api.github.com/repos/ilianaw/EuIrcBot/keys{/key_id}","collaborators_url":"https://api.github.com/repos/ilianaw/EuIrcBot/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/ilianaw/EuIrcBot/teams","hooks_url":"https://api.github.com/repos/ilianaw/EuIrcBot/hooks","issue_events_url":"https://api.github.com/repos/ilianaw/EuIrcBot/issues/events{/number}","events_url":"https://api.github.com/repos/ilianaw/EuIrcBot/events","assignees_url":"https://api.github.com/repos/ilianaw/EuIrcBot/assignees{/user}","branches_url":"https://api.github.com/repos/ilianaw/EuIrcBot/branches{/branch}","tags_url":"https://api.github.com/repos/ilianaw/EuIrcBot/tags","blobs_url":"https://api.github.com/repos/ilianaw/EuIrcBot/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/ilianaw/EuIrcBot/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/ilianaw/EuIrcBot/git/refs{/sha}","trees_url":"https://api.github.com/repos/ilianaw/EuIrcBot/git/trees{/sha}","statuses_url":"https://api.github.com/repos/ilianaw/EuIrcBot/statuses/{sha}","languages_url":"https://api.github.com/repos/ilianaw/EuIrcBot/languages","stargazers_url":"https://api.github.com/repos/ilianaw/EuIrcBot/stargazers","contributors_url":"https://api.github.com/repos/ilianaw/EuIrcBot/contributors","subscribers_url":"https://api.github.com/repos/ilianaw/EuIrcBot/subscribers","subscription_url":"https://api.github.com/repos/ilianaw/EuIrcBot/subscription","commits_url":"https://api.github.com/repos/ilianaw/EuIrcBot/commits{/sha}","git_commits_url":"https://api.github.com/repos/ilianaw/EuIrcBot/git/commits{/sha}","comments_url":"https://api.github.com/repos/ilianaw/EuIrcBot/comments{/number}","issue_comment_url":"https://api.github.com/repos/ilianaw/EuIrcBot/issues/comments{/number}","contents_url":"https://api.github.com/repos/ilianaw/EuIrcBot/contents/{+path}","compare_url":"https://api.github.com/repos/ilianaw/EuIrcBot/compare/{base}...{head}","merges_url":"https://api.github.com/repos/ilianaw/EuIrcBot/merges","archive_url":"https://api.github.com/repos/ilianaw/EuIrcBot/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/ilianaw/EuIrcBot/downloads","issues_url":"https://api.github.com/repos/ilianaw/EuIrcBot/issues{/number}","pulls_url":"https://api.github.com/repos/ilianaw/EuIrcBot/pulls{/number}","milestones_url":"https://api.github.com/repos/ilianaw/EuIrcBot/milestones{/number}","notifications_url":"https://api.github.com/repos/ilianaw/EuIrcBot/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/ilianaw/EuIrcBot/labels{/name}","releases_url":"https://api.github.com/repos/ilianaw/EuIrcBot/releases{/id}","deployments_url":"https://api.github.com/repos/ilianaw/EuIrcBot/deployments","created_at":"2017-09-17T22:04:41Z","updated_at":"2017-09-17T22:04:43Z","pushed_at":"2017-11-21T23:46:27Z","git_url":"git://github.com/ilianaw/EuIrcBot.git","ssh_url":"git@github.com:ilianaw/EuIrcBot.git","clone_url":"https://github.com/ilianaw/EuIrcBot.git","svn_url":"https://github.com/ilianaw/EuIrcBot","homepage":null,"size":473,"stargazers_count":0,"watchers_count":0,"language":"JavaScript","has_issues":false,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":0,"mirror_url":null,"archived":false,"open_issues_count":0,"forks":0,"open_issues":0,"watchers":0,"default_branch":"master"}},"base":{"label":"euank:master","ref":"master","sha":"f92f501e367779626a6fc10fc5147398486c05bf","user":{"login":"euank","id":2147649,"avatar_url":"https://avatars0.githubusercontent.com/u/2147649?v=4","gravatar_id":"","url":"https://api.github.com/users/euank","html_url":"https://github.com/euank","followers_url":"https://api.github.com/users/euank/followers","following_url":"https://api.github.com/users/euank/following{/other_user}","gists_url":"https://api.github.com/users/euank/gists{/gist_id}","starred_url":"https://api.github.com/users/euank/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/euank/subscriptions","organizations_url":"https://api.github.com/users/euank/orgs","repos_url":"https://api.github.com/users/euank/repos","events_url":"https://api.github.com/users/euank/events{/privacy}","received_events_url":"https://api.github.com/users/euank/received_events","type":"User","site_admin":false},"repo":{"id":14604011,"name":"EuIrcBot","full_name":"euank/EuIrcBot","owner":{"login":"euank","id":2147649,"avatar_url":"https://avatars0.githubusercontent.com/u/2147649?v=4","gravatar_id":"","url":"https://api.github.com/users/euank","html_url":"https://github.com/euank","followers_url":"https://api.github.com/users/euank/followers","following_url":"https://api.github.com/users/euank/following{/other_user}","gists_url":"https://api.github.com/users/euank/gists{/gist_id}","starred_url":"https://api.github.com/users/euank/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/euank/subscriptions","organizations_url":"https://api.github.com/users/euank/orgs","repos_url":"https://api.github.com/users/euank/repos","events_url":"https://api.github.com/users/euank/events{/privacy}","received_events_url":"https://api.github.com/users/euank/received_events","type":"User","site_admin":false},"private":false,"html_url":"https://github.com/euank/EuIrcBot","description":"A featureful nodejs irc bot","fork":false,"url":"https://api.github.com/repos/euank/EuIrcBot","forks_url":"https://api.github.com/repos/euank/EuIrcBot/forks","keys_url":"https://api.github.com/repos/euank/EuIrcBot/keys{/key_id}","collaborators_url":"https://api.github.com/repos/euank/EuIrcBot/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/euank/EuIrcBot/teams","hooks_url":"https://api.github.com/repos/euank/EuIrcBot/hooks","issue_events_url":"https://api.github.com/repos/euank/EuIrcBot/issues/events{/number}","events_url":"https://api.github.com/repos/euank/EuIrcBot/events","assignees_url":"https://api.github.com/repos/euank/EuIrcBot/assignees{/user}","branches_url":"https://api.github.com/repos/euank/EuIrcBot/branches{/branch}","tags_url":"https://api.github.com/repos/euank/EuIrcBot/tags","blobs_url":"https://api.github.com/repos/euank/EuIrcBot/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/euank/EuIrcBot/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/euank/EuIrcBot/git/refs{/sha}","trees_url":"https://api.github.com/repos/euank/EuIrcBot/git/trees{/sha}","statuses_url":"https://api.github.com/repos/euank/EuIrcBot/statuses/{sha}","languages_url":"https://api.github.com/repos/euank/EuIrcBot/languages","stargazers_url":"https://api.github.com/repos/euank/EuIrcBot/stargazers","contributors_url":"https://api.github.com/repos/euank/EuIrcBot/contributors","subscribers_url":"https://api.github.com/repos/euank/EuIrcBot/subscribers","subscription_url":"https://api.github.com/repos/euank/EuIrcBot/subscription","commits_url":"https://api.github.com/repos/euank/EuIrcBot/commits{/sha}","git_commits_url":"https://api.github.com/repos/euank/EuIrcBot/git/commits{/sha}","comments_url":"https://api.github.com/repos/euank/EuIrcBot/comments{/number}","issue_comment_url":"https://api.github.com/repos/euank/EuIrcBot/issues/comments{/number}","contents_url":"https://api.github.com/repos/euank/EuIrcBot/contents/{+path}","compare_url":"https://api.github.com/repos/euank/EuIrcBot/compare/{base}...{head}","merges_url":"https://api.github.com/repos/euank/EuIrcBot/merges","archive_url":"https://api.github.com/repos/euank/EuIrcBot/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/euank/EuIrcBot/downloads","issues_url":"https://api.github.com/repos/euank/EuIrcBot/issues{/number}","pulls_url":"https://api.github.com/repos/euank/EuIrcBot/pulls{/number}","milestones_url":"https://api.github.com/repos/euank/EuIrcBot/milestones{/number}","notifications_url":"https://api.github.com/repos/euank/EuIrcBot/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/euank/EuIrcBot/labels{/name}","releases_url":"https://api.github.com/repos/euank/EuIrcBot/releases{/id}","deployments_url":"https://api.github.com/repos/euank/EuIrcBot/deployments","created_at":"2013-11-22T00:04:18Z","updated_at":"2017-11-10T12:36:56Z","pushed_at":"2017-11-21T23:46:28Z","git_url":"git://github.com/euank/EuIrcBot.git","ssh_url":"git@github.com:euank/EuIrcBot.git","clone_url":"https://github.com/euank/EuIrcBot.git","svn_url":"https://github.com/euank/EuIrcBot","homepage":null,"size":446,"stargazers_count":15,"watchers_count":15,"language":"JavaScript","has_issues":true,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":15,"mirror_url":null,"archived":false,"open_issues_count":30,"forks":15,"open_issues":30,"watchers":15,"default_branch":"master"}},"_links":{"self":{"href":"https://api.github.com/repos/euank/EuIrcBot/pulls/164"},"html":{"href":"https://github.com/euank/EuIrcBot/pull/164"},"issue":{"href":"https://api.github.com/repos/euank/EuIrcBot/issues/164"},"comments":{"href":"https://api.github.com/repos/euank/EuIrcBot/issues/164/comments"},"review_comments":{"href":"https://api.github.com/repos/euank/EuIrcBot/pulls/164/comments"},"review_comment":{"href":"https://api.github.com/repos/euank/EuIrcBot/pulls/comments{/number}"},"commits":{"href":"https://api.github.com/repos/euank/EuIrcBot/pulls/164/commits"},"statuses":{"href":"https://api.github.com/repos/euank/EuIrcBot/statuses/015c28e39efa102d8a96da7c99af999993024117"}},"author_association":"COLLABORATOR"}}
{"actor":{"display_login":"euank"}}