{"action":"created","comment":{"url":"https://api.github.com/repos/DoSomething/gambit/pulls/comments/81170825","pull_request_review_id":2170708,"id":81170825,"diff_hunk":"@@ -22,433 +17,390 @@ const CMD_REPORTBACK = (process.env.GAMBIT_CMD_REPORTBACK || 'P');\n  */\n class CampaignBotController {\n \n-  constructor(req, res) {\n-    this.debug(req, `incoming_message_url:${req.incoming_message}`);\n-    if (req.incoming_image_url) {\n-      this.debug(req, `incoming_message_url:${req.incoming_image_url}`);\n-    }\n-\n-    this.campaignId = req.query.campaign;\n-    this.campaign = app.getConfig(app.ConfigName.CAMPAIGNS, this.campaignId);\n-    if (!this.campaign) {\n-      return this.error(req, res, 'campaign config not found');;\n-    }\n+  /**\n+   * Controls chatbot conversations to Signup and Reportback to DS Campaigns.\n+   * @constructor\n+   * @param {number} campaignBotId - Campaignbot to use for response content.\n+   */\n+  constructor(campaignBotId) {\n+    this.bot = app.getConfig(app.ConfigName.CAMPAIGNBOTS, campaignBotId);\n+    logger.info(`CampaignBotController loaded campaignBot:${campaignBotId}`);\n \n-    // @todo Config variable for default CampaignBot ID\n-    // @todo Store campaignbot property on our config.campaigns to override bot\n-    // on a per DS Campaign basis.\n-    this.bot = app.getConfig(app.ConfigName.CAMPAIGNBOTS, 41);\n     if (!this.bot) {\n-      return this.error(req, res, 'bot config not found');\n-    }\n-\n-    let mobileCommonsCampaign = this.campaign.staging_mobilecommons_campaign;\n-    if (process.env.NODE_ENV === 'production') {\n-      mobileCommonsCampaign = this.campaign.current_mobilecommons_campaign;\n-    }\n-    const configName = app.ConfigName.CHATBOT_MOBILECOMMONS_CAMPAIGNS;\n-    this.mobileCommonsConfig = app.getConfig(configName, mobileCommonsCampaign);\n-    if (!this.mobileCommonsConfig) {\n-      return this.error(req, res, 'mobileCommonsConfig not found');\n-    }\n-\n-    // This seems like it should move into router.js (or policies dir)\n-    if (!req.user_id) {\n-      return this.error(req, res, 'chatbot.req.user_id undefined');\n+      return this.error(req, `CampaignBot not found for id:${campaignBotId}`);\n     }\n-\n-    return this.loadUserAndSignup(req, res);\n   }\n \n-  collectCaption(req, res, promptUser) {\n-    this.debug(req, `collectCaption prompt:${promptUser}`);\n+  /**\n+   * Handles asking for and saving the given property to our draft submission.\n+   * Posts completed submissions to DS API\n+   * @param {object} req - Express request\n+   * @param {property} string - The ReportbackSubmission property to get/save\n+   * @param {bool} ask - If true, returns the ask message to send, else\n+   *     validate, save, and send message to ask for next property.\n+   */\n+  collectReportbackProperty(req, property, ask) {\n+    this.debug(req, `collectReportbackProperty:${property}`);\n \n-    if (promptUser) {\n-      return this.sendMessage(req, res, this.bot.msg_ask_caption);\n+    if (req.query.start || ask) {\n+      return this.renderResponseMessage(req, this.bot[`msg_ask_${property}`]);\n     }\n+    \n+    let input = req.incoming_message;\n \n-    this.reportbackSubmission.caption = req.incoming_message;\n-    return this.reportbackSubmission\n-      .save()\n-      .then(() => {\n-        this.debug(req, `saved caption:${this.reportbackSubmission.caption}`);\n-        // If this is our current user's first Reportback Submission:\n-        if (!this.signup.total_quantity_submitted) {\n-          return this.collectWhyParticipated(req, res, true);\n-        }\n-        return this.postReportback(req, res);    \n-      });\n-  }\n-\n-  collectPhoto(req, res, promptUser) {\n-    this.debug(req, `collectPhoto prompt:${promptUser}`);\n-\n-    if (promptUser) {\n-      return this.sendMessage(req, res, this.bot.msg_ask_photo);\n+    if (property === 'quantity') {\n+      if (helpers.hasLetters(input) || !parseInt(input)) {\n+        return this.renderResponseMessage(req, this.bot.msg_invalid_quantity);\n+      }\n+      input = parseInt(input);\n     }\n-    if (!req.incoming_image_url) {\n-      return this.sendMessage(req, res, this.bot.msg_no_photo_sent);\n+    else if (property === 'photo') {\n+      input = req.incoming_image_url;\n+      if (!input) {\n+        return this.renderResponseMessage(req, this.bot.msg_no_photo_sent);\n+      }\n     }\n \n-    this.reportbackSubmission.image_url = req.incoming_image_url;\n+    req.signup.draft_reportback_submission[property] = input;\n \n-    return this.reportbackSubmission\n+    return req.signup.draft_reportback_submission\n       .save()\n       .then(() => {\n-        this.debug(req, `saved image_url:${this.reportbackSubmission.image_url}`);\n-        return this.collectCaption(req, res, true);\n+        this.debug(req, `saved ${property}:${input}`);\n+        switch(property) {\n+          case 'quantity':\n+            return this.renderResponseMessage(req, this.bot.msg_ask_photo);\n+          case 'photo':\n+            return this.renderResponseMessage(req, this.bot.msg_ask_caption);\n+          case 'caption':\n+            // @todo Only ask for why when first reportback.\n+            return this.renderResponseMessage(req, this.bot.msg_ask_why_participated);\n+          default:\n+            return this.postReportback(req);\n+        }\n       });\n   }\n \n-  collectQuantity(req, res, promptUser) {\n-    this.debug(req, `collectQuantity prompt:${promptUser}`);\n-    \n-    if (promptUser) {\n-      return this.sendMessage(req, res, this.bot.msg_ask_quantity);\n-    }\n-\n-    const quantity = req.incoming_message;\n-    // @todo: Extract any numbers we can find instead of invalidating input based\n-    // on whether it contains any words (could contain noun, verb, or 'around 90')\n-    if (helpers.hasLetters(quantity) || !parseInt(quantity)) {\n-      return this.sendMessage(req, res, this.bot.msg_invalid_quantity);\n-    }\n-\n-    this.reportbackSubmission.quantity = parseInt(quantity);\n-\n-    return this.reportbackSubmission\n-      .save()\n-      .then(() => {\n-        this.debug(req, `saved quantity:${this.reportbackSubmission.quantity}`);\n-        return this.collectPhoto(req, res, true);\n-      });\n-  }\n+  /**\n+   * Handles conversation for collecting ReportbackSubmission data or posting.\n+   * @param {object} req - Express request\n+   */\n+  continueReportbackSubmission(req) {\n+    this.debug(req, 'continueReportbackSubmission');\n \n-  collectWhyParticipated(req, res, promptUser) {\n-    this.debug(req, `collectWhyParticipated prompt:${promptUser}`);\n+    const submission = req.signup.draft_reportback_submission;\n+    const ask = ( req.query.start || false );\n \n-    if (promptUser) {\n-      return this.sendMessage(req, res, this.bot.msg_ask_why_participated);\n+    if (!submission.quantity) {\n+      return this.collectReportbackProperty(req, 'quantity', ask);\n+    }\n+    if (!submission.photo) {\n+      return this.collectReportbackProperty(req, 'photo', ask);\n+    } \n+    if (!submission.caption) {\n+      return this.collectReportbackProperty(req, 'caption', ask);\n+    }\n+    if (!submission.why_participated) {\n+      return this.collectReportbackProperty(req, 'why_participated', ask);\n     }\n \n-    this.reportbackSubmission.why_participated = req.incoming_message;\n-\n-    return this.reportbackSubmission\n-      .save()\n-      .then(() => {\n-        this.debug(req, `saved why_participated:${req.incoming_message}`);\n-        return this.postReportback(req, res);\n-      });\n+    // If we're here, we have a completed submission but the POST request\n+    // likely failed.\n+    // TODO: Check failed_at before sending? Message about whoops trying again?\n+    logger.warn('no messages sent from continueReportbackSubmission');\n+    return this.postReportback(req);\n   }\n \n-  createReportbackSubmission(req, res) {\n+  /**\n+   * Creates new ReportbackSubmission model and updates Signup model's draft.\n+   * @param {object} req - Express request\n+   * @return {string} - Message to send and begin collecting Reportback data.\n+   */\n+  createReportbackSubmission(req) {\n     this.debug(req, 'createReportbackSubmission');\n \n-    this.reportbackSubmission = new dbRbSubmissions({\n-      campaign: this.campaignId,\n-      user: req.user_id, \n-    });\n+    return app.locals.db.reportbackSubmissions\n+      .create({\n+        campaign: req.campaign_id,\n+        user: req.user_id, \n+      })\n+      .then(reportbackSubmission => {","path":"api/controllers/CampaignBotController.js","position":225,"original_position":225,"commit_id":"1d688ae60f04cbe64924c976dacd9eb1f886b111","original_commit_id":"1d688ae60f04cbe64924c976dacd9eb1f886b111","user":{"login":"aaronschachter","id":1236811,"avatar_url":"https://avatars.githubusercontent.com/u/1236811?v=3","gravatar_id":"","url":"https://api.github.com/users/aaronschachter","html_url":"https://github.com/aaronschachter","followers_url":"https://api.github.com/users/aaronschachter/followers","following_url":"https://api.github.com/users/aaronschachter/following{/other_user}","gists_url":"https://api.github.com/users/aaronschachter/gists{/gist_id}","starred_url":"https://api.github.com/users/aaronschachter/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/aaronschachter/subscriptions","organizations_url":"https://api.github.com/users/aaronschachter/orgs","repos_url":"https://api.github.com/users/aaronschachter/repos","events_url":"https://api.github.com/users/aaronschachter/events{/privacy}","received_events_url":"https://api.github.com/users/aaronschachter/received_events","type":"User","site_admin":false},"body":"I was leaving them out per https://github.com/DoSomething/gambit/pull/628#discussion_r80087957 -- but didn't realize that's only for one liners. Definitely looking to get #638 in next so won't have to worry about it anymore 🌴 🌊  🐚 ","created_at":"2016-09-29T15:55:23Z","updated_at":"2016-09-29T15:55:24Z","html_url":"https://github.com/DoSomething/gambit/pull/632#discussion_r81170825","pull_request_url":"https://api.github.com/repos/DoSomething/gambit/pulls/632","_links":{"self":{"href":"https://api.github.com/repos/DoSomething/gambit/pulls/comments/81170825"},"html":{"href":"https://github.com/DoSomething/gambit/pull/632#discussion_r81170825"},"pull_request":{"href":"https://api.github.com/repos/DoSomething/gambit/pulls/632"}}},"pull_request":{"url":"https://api.github.com/repos/DoSomething/gambit/pulls/632","id":87127010,"html_url":"https://github.com/DoSomething/gambit/pull/632","diff_url":"https://github.com/DoSomething/gambit/pull/632.diff","patch_url":"https://github.com/DoSomething/gambit/pull/632.patch","issue_url":"https://api.github.com/repos/DoSomething/gambit/issues/632","number":632,"state":"closed","locked":false,"title":"CampaignBot router + refactor","user":{"login":"aaronschachter","id":1236811,"avatar_url":"https://avatars.githubusercontent.com/u/1236811?v=3","gravatar_id":"","url":"https://api.github.com/users/aaronschachter","html_url":"https://github.com/aaronschachter","followers_url":"https://api.github.com/users/aaronschachter/followers","following_url":"https://api.github.com/users/aaronschachter/following{/other_user}","gists_url":"https://api.github.com/users/aaronschachter/gists{/gist_id}","starred_url":"https://api.github.com/users/aaronschachter/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/aaronschachter/subscriptions","organizations_url":"https://api.github.com/users/aaronschachter/orgs","repos_url":"https://api.github.com/users/aaronschachter/repos","events_url":"https://api.github.com/users/aaronschachter/events{/privacy}","received_events_url":"https://api.github.com/users/aaronschachter/received_events","type":"User","site_admin":false},"body":"#### What's this PR do?\r\nTo the end user, all this PR does sync an existing Signup from Northstar if you've already signed up for the DS Campaign XYZ, but it's your first time texting into the CampaignBot for XYZ.\r\n\r\nIt also refactors the monolithic `CampaignBotController.constructor` callback hell into smaller functions, and moves handling our Express response into `router.js` per @deadlybutter's suggestion in https://github.com/DoSomething/gambit/pull/631#discussion_r80523908\r\n\r\nPlus:\r\n* Refactor functions like `collectQuantity` and `collectPhoto` into a `collectReportbackProperty(req, property, ask)` function\r\n* Adds a `getCurrentSignup` req to call Northstar JS Signups.index and check for existence of Signup. TODO -- Check for `Signup.campaignRun.current` right now it's hardcoded to just use first Signup returned\r\n* Cleans up `/config/index.js` -- saves our User, Signup, and ReportbackSubmission models to `app.locals.db`, refactor Northstar and Phoenix clients to `app.locals.clients`\r\n* Stores the current User and Signup in our Express request object instead of defining on `CampaignBotController` - refs https://github.com/DoSomething/gambit/pull/631/files#r80523498\r\n* Creates `CampaignBotController` once in `server.js` to avoid unnecessarily creating/destroying -- refs https://github.com/DoSomething/gambit/pull/631/files#r80525317\r\n\r\n#### How should this be reviewed?\r\nTesting as an end user is one way... reviewing code is a whole lot trickier since this refactors the entire `CampaignBotController` class. I jumped off the deep end on this one, but it should make writing unit tests for these smaller functions a whole lot easier in the future.\r\n\r\nEnd user testing needs a full test of all available routes (including legacy) -- since we're making some changes into main files like `server.js`, `router.js`, etc\r\n\r\nCode review could start from the `campaignBot(req, res)` function in `router.js` and check out `CampaignBotController` classes accordingly.\r\n\r\n#### Any background context you want to provide?\r\n\r\n* Planning to move `/api/router.js` into `/config/router.js` - was going to give that a shot in this branch but it breaks the routes nested in `/api/legacy/`\r\n\r\n* Removes check for `user.supports_mms` for now. This seems like something we may not need to store at all -- could be handled in our `msg_no_photo_sent` copy.\r\n\r\n* It's so tempting to refactor the `DonorsChooseBotController` too.\r\n\r\n#### Relevant tickets\r\n#606\r\n\r\n#### Checklist\r\n- [x] Tested on staging.\r\n\r\ncc @sergii-tkachenko ","created_at":"2016-09-28T14:06:54Z","updated_at":"2016-09-29T15:55:24Z","closed_at":"2016-09-28T23:04:59Z","merged_at":"2016-09-28T23:04:59Z","merge_commit_sha":"59b2f49750807de65c219cc4de30fa471cb4b298","assignee":null,"assignees":[],"milestone":null,"commits_url":"https://api.github.com/repos/DoSomething/gambit/pulls/632/commits","review_comments_url":"https://api.github.com/repos/DoSomething/gambit/pulls/632/comments","review_comment_url":"https://api.github.com/repos/DoSomething/gambit/pulls/comments{/number}","comments_url":"https://api.github.com/repos/DoSomething/gambit/issues/632/comments","statuses_url":"https://api.github.com/repos/DoSomething/gambit/statuses/1d688ae60f04cbe64924c976dacd9eb1f886b111","head":{"label":"DoSomething:signups","ref":"signups","sha":"1d688ae60f04cbe64924c976dacd9eb1f886b111","user":{"login":"DoSomething","id":731685,"avatar_url":"https://avatars.githubusercontent.com/u/731685?v=3","gravatar_id":"","url":"https://api.github.com/users/DoSomething","html_url":"https://github.com/DoSomething","followers_url":"https://api.github.com/users/DoSomething/followers","following_url":"https://api.github.com/users/DoSomething/following{/other_user}","gists_url":"https://api.github.com/users/DoSomething/gists{/gist_id}","starred_url":"https://api.github.com/users/DoSomething/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/DoSomething/subscriptions","organizations_url":"https://api.github.com/users/DoSomething/orgs","repos_url":"https://api.github.com/users/DoSomething/repos","events_url":"https://api.github.com/users/DoSomething/events{/privacy}","received_events_url":"https://api.github.com/users/DoSomething/received_events","type":"Organization","site_admin":false},"repo":{"id":17884814,"name":"gambit","full_name":"DoSomething/gambit","owner":{"login":"DoSomething","id":731685,"avatar_url":"https://avatars.githubusercontent.com/u/731685?v=3","gravatar_id":"","url":"https://api.github.com/users/DoSomething","html_url":"https://github.com/DoSomething","followers_url":"https://api.github.com/users/DoSomething/followers","following_url":"https://api.github.com/users/DoSomething/following{/other_user}","gists_url":"https://api.github.com/users/DoSomething/gists{/gist_id}","starred_url":"https://api.github.com/users/DoSomething/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/DoSomething/subscriptions","organizations_url":"https://api.github.com/users/DoSomething/orgs","repos_url":"https://api.github.com/users/DoSomething/repos","events_url":"https://api.github.com/users/DoSomething/events{/privacy}","received_events_url":"https://api.github.com/users/DoSomething/received_events","type":"Organization","site_admin":false},"private":false,"html_url":"https://github.com/DoSomething/gambit","description":":black_joker: The DoSomething.org SMS campaign engine. ","fork":false,"url":"https://api.github.com/repos/DoSomething/gambit","forks_url":"https://api.github.com/repos/DoSomething/gambit/forks","keys_url":"https://api.github.com/repos/DoSomething/gambit/keys{/key_id}","collaborators_url":"https://api.github.com/repos/DoSomething/gambit/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/DoSomething/gambit/teams","hooks_url":"https://api.github.com/repos/DoSomething/gambit/hooks","issue_events_url":"https://api.github.com/repos/DoSomething/gambit/issues/events{/number}","events_url":"https://api.github.com/repos/DoSomething/gambit/events","assignees_url":"https://api.github.com/repos/DoSomething/gambit/assignees{/user}","branches_url":"https://api.github.com/repos/DoSomething/gambit/branches{/branch}","tags_url":"https://api.github.com/repos/DoSomething/gambit/tags","blobs_url":"https://api.github.com/repos/DoSomething/gambit/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/DoSomething/gambit/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/DoSomething/gambit/git/refs{/sha}","trees_url":"https://api.github.com/repos/DoSomething/gambit/git/trees{/sha}","statuses_url":"https://api.github.com/repos/DoSomething/gambit/statuses/{sha}","languages_url":"https://api.github.com/repos/DoSomething/gambit/languages","stargazers_url":"https://api.github.com/repos/DoSomething/gambit/stargazers","contributors_url":"https://api.github.com/repos/DoSomething/gambit/contributors","subscribers_url":"https://api.github.com/repos/DoSomething/gambit/subscribers","subscription_url":"https://api.github.com/repos/DoSomething/gambit/subscription","commits_url":"https://api.github.com/repos/DoSomething/gambit/commits{/sha}","git_commits_url":"https://api.github.com/repos/DoSomething/gambit/git/commits{/sha}","comments_url":"https://api.github.com/repos/DoSomething/gambit/comments{/number}","issue_comment_url":"https://api.github.com/repos/DoSomething/gambit/issues/comments{/number}","contents_url":"https://api.github.com/repos/DoSomething/gambit/contents/{+path}","compare_url":"https://api.github.com/repos/DoSomething/gambit/compare/{base}...{head}","merges_url":"https://api.github.com/repos/DoSomething/gambit/merges","archive_url":"https://api.github.com/repos/DoSomething/gambit/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/DoSomething/gambit/downloads","issues_url":"https://api.github.com/repos/DoSomething/gambit/issues{/number}","pulls_url":"https://api.github.com/repos/DoSomething/gambit/pulls{/number}","milestones_url":"https://api.github.com/repos/DoSomething/gambit/milestones{/number}","notifications_url":"https://api.github.com/repos/DoSomething/gambit/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/DoSomething/gambit/labels{/name}","releases_url":"https://api.github.com/repos/DoSomething/gambit/releases{/id}","deployments_url":"https://api.github.com/repos/DoSomething/gambit/deployments","created_at":"2014-03-18T22:51:45Z","updated_at":"2016-08-10T14:09:34Z","pushed_at":"2016-09-29T15:50:14Z","git_url":"git://github.com/DoSomething/gambit.git","ssh_url":"git@github.com:DoSomething/gambit.git","clone_url":"https://github.com/DoSomething/gambit.git","svn_url":"https://github.com/DoSomething/gambit","homepage":"","size":1247,"stargazers_count":2,"watchers_count":2,"language":"JavaScript","has_issues":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":4,"mirror_url":null,"open_issues_count":25,"forks":4,"open_issues":25,"watchers":2,"default_branch":"develop"}},"base":{"label":"DoSomething:develop","ref":"develop","sha":"facd755741e43f4051150efb84a4ff3841c80abe","user":{"login":"DoSomething","id":731685,"avatar_url":"https://avatars.githubusercontent.com/u/731685?v=3","gravatar_id":"","url":"https://api.github.com/users/DoSomething","html_url":"https://github.com/DoSomething","followers_url":"https://api.github.com/users/DoSomething/followers","following_url":"https://api.github.com/users/DoSomething/following{/other_user}","gists_url":"https://api.github.com/users/DoSomething/gists{/gist_id}","starred_url":"https://api.github.com/users/DoSomething/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/DoSomething/subscriptions","organizations_url":"https://api.github.com/users/DoSomething/orgs","repos_url":"https://api.github.com/users/DoSomething/repos","events_url":"https://api.github.com/users/DoSomething/events{/privacy}","received_events_url":"https://api.github.com/users/DoSomething/received_events","type":"Organization","site_admin":false},"repo":{"id":17884814,"name":"gambit","full_name":"DoSomething/gambit","owner":{"login":"DoSomething","id":731685,"avatar_url":"https://avatars.githubusercontent.com/u/731685?v=3","gravatar_id":"","url":"https://api.github.com/users/DoSomething","html_url":"https://github.com/DoSomething","followers_url":"https://api.github.com/users/DoSomething/followers","following_url":"https://api.github.com/users/DoSomething/following{/other_user}","gists_url":"https://api.github.com/users/DoSomething/gists{/gist_id}","starred_url":"https://api.github.com/users/DoSomething/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/DoSomething/subscriptions","organizations_url":"https://api.github.com/users/DoSomething/orgs","repos_url":"https://api.github.com/users/DoSomething/repos","events_url":"https://api.github.com/users/DoSomething/events{/privacy}","received_events_url":"https://api.github.com/users/DoSomething/received_events","type":"Organization","site_admin":false},"private":false,"html_url":"https://github.com/DoSomething/gambit","description":":black_joker: The DoSomething.org SMS campaign engine. ","fork":false,"url":"https://api.github.com/repos/DoSomething/gambit","forks_url":"https://api.github.com/repos/DoSomething/gambit/forks","keys_url":"https://api.github.com/repos/DoSomething/gambit/keys{/key_id}","collaborators_url":"https://api.github.com/repos/DoSomething/gambit/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/DoSomething/gambit/teams","hooks_url":"https://api.github.com/repos/DoSomething/gambit/hooks","issue_events_url":"https://api.github.com/repos/DoSomething/gambit/issues/events{/number}","events_url":"https://api.github.com/repos/DoSomething/gambit/events","assignees_url":"https://api.github.com/repos/DoSomething/gambit/assignees{/user}","branches_url":"https://api.github.com/repos/DoSomething/gambit/branches{/branch}","tags_url":"https://api.github.com/repos/DoSomething/gambit/tags","blobs_url":"https://api.github.com/repos/DoSomething/gambit/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/DoSomething/gambit/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/DoSomething/gambit/git/refs{/sha}","trees_url":"https://api.github.com/repos/DoSomething/gambit/git/trees{/sha}","statuses_url":"https://api.github.com/repos/DoSomething/gambit/statuses/{sha}","languages_url":"https://api.github.com/repos/DoSomething/gambit/languages","stargazers_url":"https://api.github.com/repos/DoSomething/gambit/stargazers","contributors_url":"https://api.github.com/repos/DoSomething/gambit/contributors","subscribers_url":"https://api.github.com/repos/DoSomething/gambit/subscribers","subscription_url":"https://api.github.com/repos/DoSomething/gambit/subscription","commits_url":"https://api.github.com/repos/DoSomething/gambit/commits{/sha}","git_commits_url":"https://api.github.com/repos/DoSomething/gambit/git/commits{/sha}","comments_url":"https://api.github.com/repos/DoSomething/gambit/comments{/number}","issue_comment_url":"https://api.github.com/repos/DoSomething/gambit/issues/comments{/number}","contents_url":"https://api.github.com/repos/DoSomething/gambit/contents/{+path}","compare_url":"https://api.github.com/repos/DoSomething/gambit/compare/{base}...{head}","merges_url":"https://api.github.com/repos/DoSomething/gambit/merges","archive_url":"https://api.github.com/repos/DoSomething/gambit/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/DoSomething/gambit/downloads","issues_url":"https://api.github.com/repos/DoSomething/gambit/issues{/number}","pulls_url":"https://api.github.com/repos/DoSomething/gambit/pulls{/number}","milestones_url":"https://api.github.com/repos/DoSomething/gambit/milestones{/number}","notifications_url":"https://api.github.com/repos/DoSomething/gambit/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/DoSomething/gambit/labels{/name}","releases_url":"https://api.github.com/repos/DoSomething/gambit/releases{/id}","deployments_url":"https://api.github.com/repos/DoSomething/gambit/deployments","created_at":"2014-03-18T22:51:45Z","updated_at":"2016-08-10T14:09:34Z","pushed_at":"2016-09-29T15:50:14Z","git_url":"git://github.com/DoSomething/gambit.git","ssh_url":"git@github.com:DoSomething/gambit.git","clone_url":"https://github.com/DoSomething/gambit.git","svn_url":"https://github.com/DoSomething/gambit","homepage":"","size":1247,"stargazers_count":2,"watchers_count":2,"language":"JavaScript","has_issues":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":4,"mirror_url":null,"open_issues_count":25,"forks":4,"open_issues":25,"watchers":2,"default_branch":"develop"}},"_links":{"self":{"href":"https://api.github.com/repos/DoSomething/gambit/pulls/632"},"html":{"href":"https://github.com/DoSomething/gambit/pull/632"},"issue":{"href":"https://api.github.com/repos/DoSomething/gambit/issues/632"},"comments":{"href":"https://api.github.com/repos/DoSomething/gambit/issues/632/comments"},"review_comments":{"href":"https://api.github.com/repos/DoSomething/gambit/pulls/632/comments"},"review_comment":{"href":"https://api.github.com/repos/DoSomething/gambit/pulls/comments{/number}"},"commits":{"href":"https://api.github.com/repos/DoSomething/gambit/pulls/632/commits"},"statuses":{"href":"https://api.github.com/repos/DoSomething/gambit/statuses/1d688ae60f04cbe64924c976dacd9eb1f886b111"}}}}
{"actor":{"display_login":"aaronschachter"}}