{"action":"created","comment":{"url":"https://api.github.com/repos/euank/EuIrcBot/pulls/comments/44631340","id":44631340,"diff_hunk":"@@ -0,0 +1,396 @@\n+var later  = require('later');\n+var moment = require('moment');\n+var sugar  = require('sugar');\n+var hash = require('json-hash'); \n+\n+require(\"moment-duration-format\");\n+\n+var bot;\n+\n+var minimumCreationDelay = 0; \n+var minimumInterval = 60; \n+var noCommands = false;\n+var digestLength = 8;\n+\n+// digest -> schedule\n+var schedules = {};\n+var lastSchedule = null;\n+// { 'id': <digest of data before adding timer and stuff>,\n+//   'created': <created timestamp>,\n+//   'schedule:' <later schedule>,\n+//   'blame': <owner>,\n+//   'calls': <number of calls left>,\n+//   'channel': <channel to print in>,\n+//   'command': <what to say / execute>,\n+//   'timer': <our timer instance if any>\n+//\n+\n+// m1 - m2 so pass in backwards for positive\n+function getDifference(m1,m2) {\n+  return moment.duration(moment(m1).diff(moment(m2), \"milliseconds\"));\n+}\n+\n+function getAverageInterval(s) {\n+  var numSamples = 5;\n+  var next = later.schedule(s).next(numSamples+2); // skip NEXT occurrence\n+  var totalSeconds = 0;\n+\n+  next.forEach(function(e,i,d) {\n+    e = moment(e);\n+  });\n+  \n+  for(i=1; i<=numSamples; i++)\n+  {\n+    totalSeconds += parseInt(getDifference(next[i+1], next[i]).format(\"s\"));\n+  }\n+\n+  return totalSeconds/numSamples;\n+}\n+\n+function writeSchedule(data) {\n+  bot.writeDataFile(\"later.json\", \n+      JSON.stringify(schedules), function(err) {\n+        if(err) console.log(\"Error writing command file: \" + err);\n+  });\n+}\n+\n+function newSchedule(data) {\n+  var s;\n+\n+  // reminders use cron\n+  if(data.calls == -1)\n+  {\n+    try {\n+      s = later.parse.text(data.schedule);\n+    } catch (ex) {\n+      return ex;\n+    }\n+\n+    if(typeof(s) == \"number\" || s.error >= 0 )\n+    {\n+      var m = \"Provided schedule query doesn't parse:\\n\";\n+      var offset = 0;\n+\n+      if( typeof(s) == \"number\")\n+        offset = s;\n+      else\n+        offset = s.error;\n+      \n+      m += \"    \" + data.schedule.slice(0,offset) + '\\u032D' \n+           + data.schedule.slice(offset,data.schedule.length);\n+      return m;\n+    }\n+    \n+    data.schedule = s;\n+  }\n+  else\n+    s = data.schedule = later.parse.cron(data.schedule);\n+\n+\n+  // check that command is valid\n+  //   - check if commands are disabled\n+  //   - doesn't call schedule\n+  //   - doesn't call a dumbcommand which calls schedule\n+  //     * checked via perceived caller\n+  //   - isn't too close to last call\n+  // check execution frequency is below some minimum (config)\n+  \n+  if(noCommands && data.command.substring(0, bot.config.commandPrefix.length) \n+      == bot.config.commandPrefix) \n+    return \"Commands are disabled\";\n+\n+  if(data.command.match(/^!schedule/))\n+    return data.blame + \": fuck you\"; \n+  \n+  if(data.blame == bot.client.nick)\n+    return \"Cannot call schedule recursively.\";\n+  \n+  if(minimumCreationDelay > 0 && schedules.length > 0 && lastSchedule != 0\n+      && moment().diff(schedules[lastSchedule].created, 'seconds') <= minimumCreationDelay)\n+    return \"Schedule creation is rate limited. Please wait at least \" + minimumCreationDelay\n+           + \" seconds between schedule creation.\";\n+\n+  if(data.calls >= 5 || data.calls == -1)\n+  {\n+    //s.range should be the range, in seconds, between schedule calls...\n+    //but it's undefined.\n+    var interval = getAverageInterval(s);\n+    if(minimumInterval > 0 && interval < minimumInterval)\n+      return \"Parsed average frequency of \" + moment.duration(interval, \"seconds\").format() + \" is below the minimum \"\n+             + \"interval of \" + minimumInterval + \" seconds\";\n+  }\n+\n+  // Handle private message schedules. These show as channel being us--- it should\n+  // be them.\n+  if(data.channel == bot.client.nick)\n+    data.channel = data.blame;\n+\n+  // get digest for our data\n+  var digest = hash.digest(data).substr(0,digestLength);\n+  data.id = digest;\n+  data.timer = null;\n+  schedules[digest] = data;\n+\n+  lasSchedule = digest;\n+\n+  registerCommand(data);\n+  writeSchedule(schedules);\n+  \n+  var next = getDifference(later.schedule(s).next(1), moment()).format();\n+  if(next == \"0\")\n+    next = getDifference(later.schedule(s).next(2)[1], moment()).format();\n+\n+  if(next.match(/^\\d+$/))\n+    next = next + \" seconds\";\n+\n+  return \"Created \" + digest + \", the first execution is in \" + next + \".\";\n+}\n+\n+function registerCommand(data) {\n+  var command;\n+\n+  command = function() {\n+    var s = schedules[data.id];\n+\n+    // deactivate and delete command\n+    if(data.calls == 0)\n+    {\n+      if(s.timer != null)\n+        s.timer.clear();\n+       \n+      delete schedules[data.id];\n+      writeSchedule(schedules);\n+      return; // command expired\n+    }\n+\n+    if(data.target != undefined)\n+      bot.sayTo(data.channel, data.target + ': ' + data.command);\n+    else\n+      bot.sayTo(data.channel, data.command);\n+    bot.client.emit('message', bot.client.nick, data.channel,\n+      data.command, data.command);\n+\n+    if(s.calls != -1)\n+    {\n+      s.calls -= 1;\n+\n+      // write changes\n+      writeSchedule(schedules);\n+    }\n+  };\n+\n+  schedules[data.id].timer = later.setInterval(command, data.schedule);\n+}\n+\n+module.exports.init = function(b) {\n+  bot = b;\n+  bot.readDataFile(\"later.json\", function(err, data) {\n+    if(err) {\n+      console.log(\"Initializing later.json\");\n+      schedules = {};\n+      writeSchedule(schedules);\n+    } else {\n+      try {\n+        schedules = JSON.parse(data);\n+\n+        // process schedules\n+        Object.keys(schedules).forEach(function(i) {\n+          var e = schedules[i];\n+\n+          e.created = moment(e.created);\n+          e.timer = null;\n+          registerCommand(e);\n+        });\n+      } catch(ex) {\n+        console.log(\"Error parsing: \" + ex);\n+        console.log(\"Corrupted later.json for schedule! Resetting file...\");\n+        schedules = {};\n+        writeSchedule(schedules);\n+      }\n+    }\n+  });\n+\n+  bot.getConfig(\"schedule.json\", function(err, conf) {\n+    if(!err) {\n+      minimumInterval      = conf.minimum_interval;\n+      minimumCreationDelay = conf.minimum_creation_delay;\n+      noCommands           = conf.no_commands;\n+      digestLength         = conf.digest_length;\n+    }\n+  });\n+};\n+\n+module.exports.commands = {\n+  remindme: null, // alias for remind with target from\n+  remind: function(r, parts, reply, command, from, to, text, raw) {\n+            if(parts.length < 2)\n+            {\n+              reply(\"Usage: !remind ([target]) \\\"timeframe\\\" \\\"text\\\"\");\n+              reply(\"       Optional target defaults no one (channel wide).\");\n+              return;\n+            }\n+            \n+            var target, schedule, command;\n+\n+            if(parts.length == 2)\n+            {\n+              schedule = parts[0];\n+              command  = parts[1];\n+            }\n+            else\n+            {\n+              target   = parts[0];\n+              schedule = parts[1];\n+              command  = parts[2];\n+            }\n+\n+            if(schedule.match(/in|at|after|tomorrow|next|from/i))\n+            {\n+              // use sugar to handle relative dates--- moment/later sucks at this\n+              var s = Date.create(schedule);\n+              if(!s.isValid())\n+                return reply(\"Invalid relative date provided.\");\n+              if(!s.isFuture())\n+                return reply(\"Cannot make reminders for past dates/times.\");\n+\n+              // massage into a cron format\n+              schedule = s.utc().format('{m} {H} {d} {M} *');\n+            }\n+\n+            reply(newSchedule({\n+              'blame': from,\n+              'created': new moment(),\n+              'schedule': schedule,\n+              'command': command,\n+              'channel': to,\n+              'calls': 1,\n+              'target': target\n+            }));\n+          },\n+  schedule: {\n+    _default: function(x,y,reply) {\n+      reply(\"Usage: !schedule [<add>|<remove>|<list>|<help>] [arguments]\");\n+      reply(\"       Also see !schedule help <subcommand> for more details\");\n+    },\n+    help: {\n+      _default: null, // to be aliased later to ^\n+      add: function(x,y,reply) {\n+        reply(\"Usage: !schedule add \\\"timeframe\\\" \\\"command or text\\\"\");\n+        reply(\"       Timeframe syntax is here: http://bunkat.github.io/later/parsers.html#text\");\n+        reply(\"       If a valid command isn't specified, it is treated as text to print.\");\n+      },\n+      remove: function(x,y,reply) {\n+        reply(\"Usage: !schedule remove [digest]\");\n+        reply(\"       Removes the schedule specified by the digest.\");\n+      },\n+      list: function(x,y,reply) {\n+        reply(\"Usage: !schedule list [offset=0]\");\n+        reply(\"       Provides a list of schedules, in pages, from the specified offset of 0.\");\n+      },\n+    },\n+    add: function(r, parts, reply, command, from, to, text, raw) {\n+      if(parts.length !== 2) return reply(\"add must have *exactly* two arguments\");\n+\n+      reply(newSchedule({\n+        'blame': from,\n+        'created': new moment(),\n+        'schedule': parts[0],\n+        'command': parts[1],\n+        'channel': to,\n+        'calls': -1\n+      }));\n+    },\n+    list: function(x, parts, reply, command, from, to) {\n+      var offset = 0;\n+      var count  = 5;\n+      var ci     = count;\n+      var oi     = 0;\n+      var size = Object.keys(schedules).length;\n+\n+      if(parts.length >= 1)\n+        oi = offset = parseInt(parts[0]);","path":"modules/schedule/index.js","position":311,"original_position":311,"commit_id":"66ca3c1faf806447e83910c37e56faaf6e93c7e1","original_commit_id":"66ca3c1faf806447e83910c37e56faaf6e93c7e1","user":{"login":"euank","id":2147649,"avatar_url":"https://avatars.githubusercontent.com/u/2147649?v=3","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":"nit, can throw","created_at":"2015-11-12T08:29:16Z","updated_at":"2015-11-12T08:29:16Z","html_url":"https://github.com/euank/EuIrcBot/pull/102#discussion_r44631340","pull_request_url":"https://api.github.com/repos/euank/EuIrcBot/pulls/102","_links":{"self":{"href":"https://api.github.com/repos/euank/EuIrcBot/pulls/comments/44631340"},"html":{"href":"https://github.com/euank/EuIrcBot/pull/102#discussion_r44631340"},"pull_request":{"href":"https://api.github.com/repos/euank/EuIrcBot/pulls/102"}}},"pull_request":{"url":"https://api.github.com/repos/euank/EuIrcBot/pulls/102","id":50072824,"html_url":"https://github.com/euank/EuIrcBot/pull/102","diff_url":"https://github.com/euank/EuIrcBot/pull/102.diff","patch_url":"https://github.com/euank/EuIrcBot/pull/102.patch","issue_url":"https://api.github.com/repos/euank/EuIrcBot/issues/102","number":102,"state":"open","locked":false,"title":"[WIP] Reminders and scheduled commands / text using later.js","user":{"login":"brhoades","id":4763746,"avatar_url":"https://avatars.githubusercontent.com/u/4763746?v=3","gravatar_id":"","url":"https://api.github.com/users/brhoades","html_url":"https://github.com/brhoades","followers_url":"https://api.github.com/users/brhoades/followers","following_url":"https://api.github.com/users/brhoades/following{/other_user}","gists_url":"https://api.github.com/users/brhoades/gists{/gist_id}","starred_url":"https://api.github.com/users/brhoades/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/brhoades/subscriptions","organizations_url":"https://api.github.com/users/brhoades/orgs","repos_url":"https://api.github.com/users/brhoades/repos","events_url":"https://api.github.com/users/brhoades/events{/privacy}","received_events_url":"https://api.github.com/users/brhoades/received_events","type":"User","site_admin":false},"body":"This isn't quite ready for merge, but I'd like feedback. Currently this module allows the following: \r\n  * (!)schedule a command to be ran at a specific frequency \r\n  * (!)remind yourself (!remindme) or others (!remind [target]) for a one-off event.\r\n  * Execute arbitrary commands or simply print text with timers.\r\n  * List and delete previous schedule commands.\r\n  * Saves and persists schedules between sessions.\r\n  * Detailed help statements for each subcommand.\r\n\r\nIt uses later.js to parse text:\r\n\r\nhttp://bunkat.github.io/later/parsers.html#text\r\n\r\nWhich also handles execution:\r\n\r\nhttp://bunkat.github.io/later/execute.html#set-interval\r\n\r\nFor the most part this works fine, there are just problems with filtering and making sure people won't exploit the system. I'm pretty sure, with the scheduling language being as robust as it is, it's impossible to truly prevent while keeping functionality.\r\n\r\nAnyway, feedback is welcome. This thing is a mess and may remain that way.\r\n","created_at":"2015-11-08T22:48:03Z","updated_at":"2015-11-12T08:29:16Z","closed_at":null,"merged_at":null,"merge_commit_sha":"9b26ba648ccbab916b4fb5fb70e95dd46a0c6d8a","assignee":null,"milestone":null,"commits_url":"https://api.github.com/repos/euank/EuIrcBot/pulls/102/commits","review_comments_url":"https://api.github.com/repos/euank/EuIrcBot/pulls/102/comments","review_comment_url":"https://api.github.com/repos/euank/EuIrcBot/pulls/comments{/number}","comments_url":"https://api.github.com/repos/euank/EuIrcBot/issues/102/comments","statuses_url":"https://api.github.com/repos/euank/EuIrcBot/statuses/66ca3c1faf806447e83910c37e56faaf6e93c7e1","head":{"label":"brhoades:later","ref":"later","sha":"66ca3c1faf806447e83910c37e56faaf6e93c7e1","user":{"login":"brhoades","id":4763746,"avatar_url":"https://avatars.githubusercontent.com/u/4763746?v=3","gravatar_id":"","url":"https://api.github.com/users/brhoades","html_url":"https://github.com/brhoades","followers_url":"https://api.github.com/users/brhoades/followers","following_url":"https://api.github.com/users/brhoades/following{/other_user}","gists_url":"https://api.github.com/users/brhoades/gists{/gist_id}","starred_url":"https://api.github.com/users/brhoades/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/brhoades/subscriptions","organizations_url":"https://api.github.com/users/brhoades/orgs","repos_url":"https://api.github.com/users/brhoades/repos","events_url":"https://api.github.com/users/brhoades/events{/privacy}","received_events_url":"https://api.github.com/users/brhoades/received_events","type":"User","site_admin":false},"repo":{"id":22275858,"name":"EuIrcBot","full_name":"brhoades/EuIrcBot","owner":{"login":"brhoades","id":4763746,"avatar_url":"https://avatars.githubusercontent.com/u/4763746?v=3","gravatar_id":"","url":"https://api.github.com/users/brhoades","html_url":"https://github.com/brhoades","followers_url":"https://api.github.com/users/brhoades/followers","following_url":"https://api.github.com/users/brhoades/following{/other_user}","gists_url":"https://api.github.com/users/brhoades/gists{/gist_id}","starred_url":"https://api.github.com/users/brhoades/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/brhoades/subscriptions","organizations_url":"https://api.github.com/users/brhoades/orgs","repos_url":"https://api.github.com/users/brhoades/repos","events_url":"https://api.github.com/users/brhoades/events{/privacy}","received_events_url":"https://api.github.com/users/brhoades/received_events","type":"User","site_admin":false},"private":false,"html_url":"https://github.com/brhoades/EuIrcBot","description":"A featureful nodejs irc bot","fork":true,"url":"https://api.github.com/repos/brhoades/EuIrcBot","forks_url":"https://api.github.com/repos/brhoades/EuIrcBot/forks","keys_url":"https://api.github.com/repos/brhoades/EuIrcBot/keys{/key_id}","collaborators_url":"https://api.github.com/repos/brhoades/EuIrcBot/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/brhoades/EuIrcBot/teams","hooks_url":"https://api.github.com/repos/brhoades/EuIrcBot/hooks","issue_events_url":"https://api.github.com/repos/brhoades/EuIrcBot/issues/events{/number}","events_url":"https://api.github.com/repos/brhoades/EuIrcBot/events","assignees_url":"https://api.github.com/repos/brhoades/EuIrcBot/assignees{/user}","branches_url":"https://api.github.com/repos/brhoades/EuIrcBot/branches{/branch}","tags_url":"https://api.github.com/repos/brhoades/EuIrcBot/tags","blobs_url":"https://api.github.com/repos/brhoades/EuIrcBot/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/brhoades/EuIrcBot/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/brhoades/EuIrcBot/git/refs{/sha}","trees_url":"https://api.github.com/repos/brhoades/EuIrcBot/git/trees{/sha}","statuses_url":"https://api.github.com/repos/brhoades/EuIrcBot/statuses/{sha}","languages_url":"https://api.github.com/repos/brhoades/EuIrcBot/languages","stargazers_url":"https://api.github.com/repos/brhoades/EuIrcBot/stargazers","contributors_url":"https://api.github.com/repos/brhoades/EuIrcBot/contributors","subscribers_url":"https://api.github.com/repos/brhoades/EuIrcBot/subscribers","subscription_url":"https://api.github.com/repos/brhoades/EuIrcBot/subscription","commits_url":"https://api.github.com/repos/brhoades/EuIrcBot/commits{/sha}","git_commits_url":"https://api.github.com/repos/brhoades/EuIrcBot/git/commits{/sha}","comments_url":"https://api.github.com/repos/brhoades/EuIrcBot/comments{/number}","issue_comment_url":"https://api.github.com/repos/brhoades/EuIrcBot/issues/comments{/number}","contents_url":"https://api.github.com/repos/brhoades/EuIrcBot/contents/{+path}","compare_url":"https://api.github.com/repos/brhoades/EuIrcBot/compare/{base}...{head}","merges_url":"https://api.github.com/repos/brhoades/EuIrcBot/merges","archive_url":"https://api.github.com/repos/brhoades/EuIrcBot/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/brhoades/EuIrcBot/downloads","issues_url":"https://api.github.com/repos/brhoades/EuIrcBot/issues{/number}","pulls_url":"https://api.github.com/repos/brhoades/EuIrcBot/pulls{/number}","milestones_url":"https://api.github.com/repos/brhoades/EuIrcBot/milestones{/number}","notifications_url":"https://api.github.com/repos/brhoades/EuIrcBot/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/brhoades/EuIrcBot/labels{/name}","releases_url":"https://api.github.com/repos/brhoades/EuIrcBot/releases{/id}","created_at":"2014-07-26T01:34:54Z","updated_at":"2014-07-26T01:37:18Z","pushed_at":"2015-11-12T05:59:40Z","git_url":"git://github.com/brhoades/EuIrcBot.git","ssh_url":"git@github.com:brhoades/EuIrcBot.git","clone_url":"https://github.com/brhoades/EuIrcBot.git","svn_url":"https://github.com/brhoades/EuIrcBot","homepage":null,"size":561,"stargazers_count":0,"watchers_count":0,"language":"JavaScript","has_issues":false,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":1,"mirror_url":null,"open_issues_count":0,"forks":1,"open_issues":0,"watchers":0,"default_branch":"master"}},"base":{"label":"euank:master","ref":"master","sha":"face1a7a502dfb09a5fd13ade989f9e2997dd6a0","user":{"login":"euank","id":2147649,"avatar_url":"https://avatars.githubusercontent.com/u/2147649?v=3","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://avatars.githubusercontent.com/u/2147649?v=3","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}","created_at":"2013-11-22T00:04:18Z","updated_at":"2015-08-09T22:12:02Z","pushed_at":"2015-11-12T05:59:40Z","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":1500,"stargazers_count":8,"watchers_count":8,"language":"JavaScript","has_issues":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":11,"mirror_url":null,"open_issues_count":25,"forks":11,"open_issues":25,"watchers":8,"default_branch":"master"}},"_links":{"self":{"href":"https://api.github.com/repos/euank/EuIrcBot/pulls/102"},"html":{"href":"https://github.com/euank/EuIrcBot/pull/102"},"issue":{"href":"https://api.github.com/repos/euank/EuIrcBot/issues/102"},"comments":{"href":"https://api.github.com/repos/euank/EuIrcBot/issues/102/comments"},"review_comments":{"href":"https://api.github.com/repos/euank/EuIrcBot/pulls/102/comments"},"review_comment":{"href":"https://api.github.com/repos/euank/EuIrcBot/pulls/comments{/number}"},"commits":{"href":"https://api.github.com/repos/euank/EuIrcBot/pulls/102/commits"},"statuses":{"href":"https://api.github.com/repos/euank/EuIrcBot/statuses/66ca3c1faf806447e83910c37e56faaf6e93c7e1"}}}}