{"action":"created","comment":{"url":"https://api.github.com/repos/freedomofpress/securedrop-client/pulls/comments/228692588","pull_request_review_id":169033440,"id":228692588,"node_id":"MDI0OlB1bGxSZXF1ZXN0UmV2aWV3Q29tbWVudDIyODY5MjU4OA==","diff_hunk":"@@ -129,40 +130,66 @@ def call_api(self, function, callback, timeout, *args, **kwargs):\n         timeout signal. Any further arguments are passed to the function to be\n         called.\n         \"\"\"\n+\n         if not self.api_thread:\n+            self.timer = QTimer()\n+            self.timer.timeout.connect(lambda: self.timeout_api_call.emit())\n+            self.timer.setSingleShot(True)\n+            self.timer.start(20000)\n+\n             self.api_thread = QThread(self.gui)\n             self.api_runner = APICallRunner(function, *args, **kwargs)\n             self.api_runner.moveToThread(self.api_thread)\n-            self.api_runner.call_finished.connect(callback)\n-            self.api_runner.timeout.connect(timeout)\n-            self.finish_api_call.connect(self.api_runner.on_cancel_timeout)\n+\n+            # handle successful call: copy response data, reset the\n+            # client, give the user-provided callback the response\n+            # data\n+            self.api_runner.call_finished.connect(lambda r: self.successful_api_call(r, callback))\n+\n+            # we've started a timer. when that hits zero, call our\n+            # timeout function\n+            self.timeout_api_call.connect(lambda: self.timeout_cleanup(timeout))\n+\n+            # when the thread starts, we want to run `call_api` on `api_runner`\n             self.api_thread.started.connect(self.api_runner.call_api)\n-            self.api_thread.finished.connect(self.call_reset)\n+\n             self.api_thread.start()\n \n+        else:\n+            logger.info(\"There's already an API request running, so I'm not going to ignore this one (XXX this may not be the coolest thing to do...)\")\n+\n     def call_reset(self):\n         \"\"\"\n         Clean up this object's state after an API call.\n         \"\"\"\n         if self.api_thread:\n-            self.finish_api_call.emit()\n+            self.timeout_api_call.disconnect()\n             self.api_runner = None\n             self.api_thread = None\n+            self.timer = None\n \n     def login(self, username, password, totp):\n         \"\"\"\n         Given a username, password and time based one-time-passcode (TOTP),\n         create a new instance representing the SecureDrop api and authenticate.\n         \"\"\"\n-        self.api = sdclientapi.API(self.hostname, username, password, totp)\n+\n+        self.api = sdclientapi.API(self.hostname, username, password, totp, proxy=True)","path":"securedrop_client/logic.py","position":140,"original_position":142,"commit_id":"83ff655a2c1a5b82fe0da9d82e75a09779ff5c38","original_commit_id":"b7aef9b1d12e5ab3e789874d4fc9773400b86d1c","user":{"login":"redshiftzero","id":7832803,"node_id":"MDQ6VXNlcjc4MzI4MDM=","avatar_url":"https://avatars3.githubusercontent.com/u/7832803?v=4","gravatar_id":"","url":"https://api.github.com/users/redshiftzero","html_url":"https://github.com/redshiftzero","followers_url":"https://api.github.com/users/redshiftzero/followers","following_url":"https://api.github.com/users/redshiftzero/following{/other_user}","gists_url":"https://api.github.com/users/redshiftzero/gists{/gist_id}","starred_url":"https://api.github.com/users/redshiftzero/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/redshiftzero/subscriptions","organizations_url":"https://api.github.com/users/redshiftzero/orgs","repos_url":"https://api.github.com/users/redshiftzero/repos","events_url":"https://api.github.com/users/redshiftzero/events{/privacy}","received_events_url":"https://api.github.com/users/redshiftzero/received_events","type":"User","site_admin":false},"body":"yeah we can snip this out for review on non-Qubes OSes (cc #2)","created_at":"2018-10-27T00:03:56Z","updated_at":"2018-10-27T00:03:56Z","html_url":"https://github.com/freedomofpress/securedrop-client/pull/77#discussion_r228692588","pull_request_url":"https://api.github.com/repos/freedomofpress/securedrop-client/pulls/77","author_association":"CONTRIBUTOR","_links":{"self":{"href":"https://api.github.com/repos/freedomofpress/securedrop-client/pulls/comments/228692588"},"html":{"href":"https://github.com/freedomofpress/securedrop-client/pull/77#discussion_r228692588"},"pull_request":{"href":"https://api.github.com/repos/freedomofpress/securedrop-client/pulls/77"}},"in_reply_to_id":228691891},"pull_request":{"url":"https://api.github.com/repos/freedomofpress/securedrop-client/pulls/77","id":226252476,"node_id":"MDExOlB1bGxSZXF1ZXN0MjI2MjUyNDc2","html_url":"https://github.com/freedomofpress/securedrop-client/pull/77","diff_url":"https://github.com/freedomofpress/securedrop-client/pull/77.diff","patch_url":"https://github.com/freedomofpress/securedrop-client/pull/77.patch","issue_url":"https://api.github.com/repos/freedomofpress/securedrop-client/issues/77","number":77,"state":"open","locked":false,"title":"Clean up threading","user":{"login":"joshuathayer","id":103802,"node_id":"MDQ6VXNlcjEwMzgwMg==","avatar_url":"https://avatars1.githubusercontent.com/u/103802?v=4","gravatar_id":"","url":"https://api.github.com/users/joshuathayer","html_url":"https://github.com/joshuathayer","followers_url":"https://api.github.com/users/joshuathayer/followers","following_url":"https://api.github.com/users/joshuathayer/following{/other_user}","gists_url":"https://api.github.com/users/joshuathayer/gists{/gist_id}","starred_url":"https://api.github.com/users/joshuathayer/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/joshuathayer/subscriptions","organizations_url":"https://api.github.com/users/joshuathayer/orgs","repos_url":"https://api.github.com/users/joshuathayer/repos","events_url":"https://api.github.com/users/joshuathayer/events{/privacy}","received_events_url":"https://api.github.com/users/joshuathayer/received_events","type":"User","site_admin":false},"body":"This fixes our API request timeout handling, and in the process tries to clean up how we handle our API thread in general.\r\n\r\nAs a quick overview: in order to retain application interactivity while we do remote API calls, we run those API calls in a separate thread (as provided by Qt's `QThread`) from our main UI thread. That's a common and recommended pattern.\r\n\r\nSpecifically, we have a `logic.Client` class, which is responsible for spawning and managing an API thread. `Client` holds a ref to an `client.APICallRunner` object, which represents the remote API request. When it's time to run the request, the `Client` spawns a new thread, \"moves\" its `APICallRunner` object to the new thread, and starts the new thread. The `APICallRunner`'s `call_api` method does the actual remote request, and its thread is blocked while that request runs.\r\n\r\nThe `Client` is responsible for managing that thread: it needs to have methods to handle successes and failures, including timing out.\r\n\r\nThe first issue this PR addresses was that we were started a `QTimer` in the `APICallRunner` thread, setting its timeout handler to a function in that thread. But since that thread is blocked while the remote request is running, it has no way of handling that callback until the remote request returns. This led to a bug where both the success and timeout handlers were being run: the request would take longer than 5 seconds and then eventually succeed. In the meantime, the timer would _arrange for the timeout handler to be run_ on the API thread. When the remote request returned and the thread became unblocked, the success handler would be run (since the request succeeded), and also the timeout handler would run (since the timer arranged for that to happen).\r\n\r\nI moved the timer and its handler into the `Client` object and its thread. When the timer runs out, we abandon it and its `APICallRunner` object (from the perspective of `Client`), but _the thread is still running_ and will happily call its success callback when it eventually gets unblocked. So, we set a flag in the CallRunner object telling it that it's run out of time, and should silently do nothing when it eventually becomes unblocked.\r\n\r\nNext, this tidies up our callbacks a bit, by handling `Client`-object state changes after request/failure in Client-wide functions, rather than in individual callbacks. After every request,  we want to reset the client to an \"empty\" state: no `APICallRunner` object, no API thread, no callbacks registered. There are slightly different ways to do this on success and on failure. Rather than duplicating that logic in every callback handler, this PR now wraps the user-specific callback in a small function which handles the cleanup on its own.\r\n\r\nNote that this involves a small change to how callbacks get to any response data. Previously, they'd look at `self.api_runner.result`. In this PR, we reset `self.api_runner` _before_ the callback is run, so `self.api_runner.result` will never be defined while in the callback. Rather, the result is passed as an argument to the callback function directly, so callback signatures should be of the form\r\n\r\n    def foo_callback(self, result, result_data):\r\n\r\nwhere result is a boolean, and result_data is the actual data returned from the API (previously found at `self.api_runner.result`)\r\n\r\nThis change allows us to greatly simplify how the client is reset between requests, and reduces the responsibilities of the success/failure callbacks (or, if you've consumed the same koolaide as me, it [decomplects](https://www.infoq.com/presentations/Simple-Made-Easy) those responsibilities).\r\n\r\nMultitheaded stuff is notoriously hard to test and to reason about. I only found these problems because I'm developing against a remote SD instance and was running in to timeout issues. I've manually tested various success and failure cases, but I suspect that there are still race conditions and cases that cause unexpected behavior... though I do think this puts us in a better state than before?\r\n\r\nAlso this work originated as part of my branch adding message loading. I manually pulled out this threading stuff, but there are some changes remaining here which have to do with message loading still... I'll to clean those up shortly. ","created_at":"2018-10-26T23:47:04Z","updated_at":"2018-10-27T00:03:56Z","closed_at":null,"merged_at":null,"merge_commit_sha":"e087d2b2a879dc3e93b9a93165110114724f0118","assignee":null,"assignees":[],"requested_reviewers":[{"login":"ntoll","id":37602,"node_id":"MDQ6VXNlcjM3NjAy","avatar_url":"https://avatars0.githubusercontent.com/u/37602?v=4","gravatar_id":"","url":"https://api.github.com/users/ntoll","html_url":"https://github.com/ntoll","followers_url":"https://api.github.com/users/ntoll/followers","following_url":"https://api.github.com/users/ntoll/following{/other_user}","gists_url":"https://api.github.com/users/ntoll/gists{/gist_id}","starred_url":"https://api.github.com/users/ntoll/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ntoll/subscriptions","organizations_url":"https://api.github.com/users/ntoll/orgs","repos_url":"https://api.github.com/users/ntoll/repos","events_url":"https://api.github.com/users/ntoll/events{/privacy}","received_events_url":"https://api.github.com/users/ntoll/received_events","type":"User","site_admin":false},{"login":"heartsucker","id":3998464,"node_id":"MDQ6VXNlcjM5OTg0NjQ=","avatar_url":"https://avatars0.githubusercontent.com/u/3998464?v=4","gravatar_id":"","url":"https://api.github.com/users/heartsucker","html_url":"https://github.com/heartsucker","followers_url":"https://api.github.com/users/heartsucker/followers","following_url":"https://api.github.com/users/heartsucker/following{/other_user}","gists_url":"https://api.github.com/users/heartsucker/gists{/gist_id}","starred_url":"https://api.github.com/users/heartsucker/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/heartsucker/subscriptions","organizations_url":"https://api.github.com/users/heartsucker/orgs","repos_url":"https://api.github.com/users/heartsucker/repos","events_url":"https://api.github.com/users/heartsucker/events{/privacy}","received_events_url":"https://api.github.com/users/heartsucker/received_events","type":"User","site_admin":false},{"login":"redshiftzero","id":7832803,"node_id":"MDQ6VXNlcjc4MzI4MDM=","avatar_url":"https://avatars3.githubusercontent.com/u/7832803?v=4","gravatar_id":"","url":"https://api.github.com/users/redshiftzero","html_url":"https://github.com/redshiftzero","followers_url":"https://api.github.com/users/redshiftzero/followers","following_url":"https://api.github.com/users/redshiftzero/following{/other_user}","gists_url":"https://api.github.com/users/redshiftzero/gists{/gist_id}","starred_url":"https://api.github.com/users/redshiftzero/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/redshiftzero/subscriptions","organizations_url":"https://api.github.com/users/redshiftzero/orgs","repos_url":"https://api.github.com/users/redshiftzero/repos","events_url":"https://api.github.com/users/redshiftzero/events{/privacy}","received_events_url":"https://api.github.com/users/redshiftzero/received_events","type":"User","site_admin":false}],"requested_teams":[],"labels":[],"milestone":null,"commits_url":"https://api.github.com/repos/freedomofpress/securedrop-client/pulls/77/commits","review_comments_url":"https://api.github.com/repos/freedomofpress/securedrop-client/pulls/77/comments","review_comment_url":"https://api.github.com/repos/freedomofpress/securedrop-client/pulls/comments{/number}","comments_url":"https://api.github.com/repos/freedomofpress/securedrop-client/issues/77/comments","statuses_url":"https://api.github.com/repos/freedomofpress/securedrop-client/statuses/83ff655a2c1a5b82fe0da9d82e75a09779ff5c38","head":{"label":"freedomofpress:cleanup-threading","ref":"cleanup-threading","sha":"83ff655a2c1a5b82fe0da9d82e75a09779ff5c38","user":{"login":"freedomofpress","id":5388147,"node_id":"MDEyOk9yZ2FuaXphdGlvbjUzODgxNDc=","avatar_url":"https://avatars1.githubusercontent.com/u/5388147?v=4","gravatar_id":"","url":"https://api.github.com/users/freedomofpress","html_url":"https://github.com/freedomofpress","followers_url":"https://api.github.com/users/freedomofpress/followers","following_url":"https://api.github.com/users/freedomofpress/following{/other_user}","gists_url":"https://api.github.com/users/freedomofpress/gists{/gist_id}","starred_url":"https://api.github.com/users/freedomofpress/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/freedomofpress/subscriptions","organizations_url":"https://api.github.com/users/freedomofpress/orgs","repos_url":"https://api.github.com/users/freedomofpress/repos","events_url":"https://api.github.com/users/freedomofpress/events{/privacy}","received_events_url":"https://api.github.com/users/freedomofpress/received_events","type":"Organization","site_admin":false},"repo":{"id":141504341,"node_id":"MDEwOlJlcG9zaXRvcnkxNDE1MDQzNDE=","name":"securedrop-client","full_name":"freedomofpress/securedrop-client","private":false,"owner":{"login":"freedomofpress","id":5388147,"node_id":"MDEyOk9yZ2FuaXphdGlvbjUzODgxNDc=","avatar_url":"https://avatars1.githubusercontent.com/u/5388147?v=4","gravatar_id":"","url":"https://api.github.com/users/freedomofpress","html_url":"https://github.com/freedomofpress","followers_url":"https://api.github.com/users/freedomofpress/followers","following_url":"https://api.github.com/users/freedomofpress/following{/other_user}","gists_url":"https://api.github.com/users/freedomofpress/gists{/gist_id}","starred_url":"https://api.github.com/users/freedomofpress/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/freedomofpress/subscriptions","organizations_url":"https://api.github.com/users/freedomofpress/orgs","repos_url":"https://api.github.com/users/freedomofpress/repos","events_url":"https://api.github.com/users/freedomofpress/events{/privacy}","received_events_url":"https://api.github.com/users/freedomofpress/received_events","type":"Organization","site_admin":false},"html_url":"https://github.com/freedomofpress/securedrop-client","description":"a Qt-based GUI for SecureDrop journalists 📰🗞️","fork":false,"url":"https://api.github.com/repos/freedomofpress/securedrop-client","forks_url":"https://api.github.com/repos/freedomofpress/securedrop-client/forks","keys_url":"https://api.github.com/repos/freedomofpress/securedrop-client/keys{/key_id}","collaborators_url":"https://api.github.com/repos/freedomofpress/securedrop-client/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/freedomofpress/securedrop-client/teams","hooks_url":"https://api.github.com/repos/freedomofpress/securedrop-client/hooks","issue_events_url":"https://api.github.com/repos/freedomofpress/securedrop-client/issues/events{/number}","events_url":"https://api.github.com/repos/freedomofpress/securedrop-client/events","assignees_url":"https://api.github.com/repos/freedomofpress/securedrop-client/assignees{/user}","branches_url":"https://api.github.com/repos/freedomofpress/securedrop-client/branches{/branch}","tags_url":"https://api.github.com/repos/freedomofpress/securedrop-client/tags","blobs_url":"https://api.github.com/repos/freedomofpress/securedrop-client/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/freedomofpress/securedrop-client/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/freedomofpress/securedrop-client/git/refs{/sha}","trees_url":"https://api.github.com/repos/freedomofpress/securedrop-client/git/trees{/sha}","statuses_url":"https://api.github.com/repos/freedomofpress/securedrop-client/statuses/{sha}","languages_url":"https://api.github.com/repos/freedomofpress/securedrop-client/languages","stargazers_url":"https://api.github.com/repos/freedomofpress/securedrop-client/stargazers","contributors_url":"https://api.github.com/repos/freedomofpress/securedrop-client/contributors","subscribers_url":"https://api.github.com/repos/freedomofpress/securedrop-client/subscribers","subscription_url":"https://api.github.com/repos/freedomofpress/securedrop-client/subscription","commits_url":"https://api.github.com/repos/freedomofpress/securedrop-client/commits{/sha}","git_commits_url":"https://api.github.com/repos/freedomofpress/securedrop-client/git/commits{/sha}","comments_url":"https://api.github.com/repos/freedomofpress/securedrop-client/comments{/number}","issue_comment_url":"https://api.github.com/repos/freedomofpress/securedrop-client/issues/comments{/number}","contents_url":"https://api.github.com/repos/freedomofpress/securedrop-client/contents/{+path}","compare_url":"https://api.github.com/repos/freedomofpress/securedrop-client/compare/{base}...{head}","merges_url":"https://api.github.com/repos/freedomofpress/securedrop-client/merges","archive_url":"https://api.github.com/repos/freedomofpress/securedrop-client/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/freedomofpress/securedrop-client/downloads","issues_url":"https://api.github.com/repos/freedomofpress/securedrop-client/issues{/number}","pulls_url":"https://api.github.com/repos/freedomofpress/securedrop-client/pulls{/number}","milestones_url":"https://api.github.com/repos/freedomofpress/securedrop-client/milestones{/number}","notifications_url":"https://api.github.com/repos/freedomofpress/securedrop-client/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/freedomofpress/securedrop-client/labels{/name}","releases_url":"https://api.github.com/repos/freedomofpress/securedrop-client/releases{/id}","deployments_url":"https://api.github.com/repos/freedomofpress/securedrop-client/deployments","created_at":"2018-07-19T00:31:37Z","updated_at":"2018-10-25T18:58:21Z","pushed_at":"2018-10-27T00:03:06Z","git_url":"git://github.com/freedomofpress/securedrop-client.git","ssh_url":"git@github.com:freedomofpress/securedrop-client.git","clone_url":"https://github.com/freedomofpress/securedrop-client.git","svn_url":"https://github.com/freedomofpress/securedrop-client","homepage":"","size":302,"stargazers_count":2,"watchers_count":2,"language":"Python","has_issues":true,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":2,"mirror_url":null,"archived":false,"open_issues_count":41,"license":{"key":"agpl-3.0","name":"GNU Affero General Public License v3.0","spdx_id":"AGPL-3.0","url":"https://api.github.com/licenses/agpl-3.0","node_id":"MDc6TGljZW5zZTE="},"forks":2,"open_issues":41,"watchers":2,"default_branch":"master"}},"base":{"label":"freedomofpress:master","ref":"master","sha":"456f2487a0073a85d71b04294652c0973596991d","user":{"login":"freedomofpress","id":5388147,"node_id":"MDEyOk9yZ2FuaXphdGlvbjUzODgxNDc=","avatar_url":"https://avatars1.githubusercontent.com/u/5388147?v=4","gravatar_id":"","url":"https://api.github.com/users/freedomofpress","html_url":"https://github.com/freedomofpress","followers_url":"https://api.github.com/users/freedomofpress/followers","following_url":"https://api.github.com/users/freedomofpress/following{/other_user}","gists_url":"https://api.github.com/users/freedomofpress/gists{/gist_id}","starred_url":"https://api.github.com/users/freedomofpress/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/freedomofpress/subscriptions","organizations_url":"https://api.github.com/users/freedomofpress/orgs","repos_url":"https://api.github.com/users/freedomofpress/repos","events_url":"https://api.github.com/users/freedomofpress/events{/privacy}","received_events_url":"https://api.github.com/users/freedomofpress/received_events","type":"Organization","site_admin":false},"repo":{"id":141504341,"node_id":"MDEwOlJlcG9zaXRvcnkxNDE1MDQzNDE=","name":"securedrop-client","full_name":"freedomofpress/securedrop-client","private":false,"owner":{"login":"freedomofpress","id":5388147,"node_id":"MDEyOk9yZ2FuaXphdGlvbjUzODgxNDc=","avatar_url":"https://avatars1.githubusercontent.com/u/5388147?v=4","gravatar_id":"","url":"https://api.github.com/users/freedomofpress","html_url":"https://github.com/freedomofpress","followers_url":"https://api.github.com/users/freedomofpress/followers","following_url":"https://api.github.com/users/freedomofpress/following{/other_user}","gists_url":"https://api.github.com/users/freedomofpress/gists{/gist_id}","starred_url":"https://api.github.com/users/freedomofpress/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/freedomofpress/subscriptions","organizations_url":"https://api.github.com/users/freedomofpress/orgs","repos_url":"https://api.github.com/users/freedomofpress/repos","events_url":"https://api.github.com/users/freedomofpress/events{/privacy}","received_events_url":"https://api.github.com/users/freedomofpress/received_events","type":"Organization","site_admin":false},"html_url":"https://github.com/freedomofpress/securedrop-client","description":"a Qt-based GUI for SecureDrop journalists 📰🗞️","fork":false,"url":"https://api.github.com/repos/freedomofpress/securedrop-client","forks_url":"https://api.github.com/repos/freedomofpress/securedrop-client/forks","keys_url":"https://api.github.com/repos/freedomofpress/securedrop-client/keys{/key_id}","collaborators_url":"https://api.github.com/repos/freedomofpress/securedrop-client/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/freedomofpress/securedrop-client/teams","hooks_url":"https://api.github.com/repos/freedomofpress/securedrop-client/hooks","issue_events_url":"https://api.github.com/repos/freedomofpress/securedrop-client/issues/events{/number}","events_url":"https://api.github.com/repos/freedomofpress/securedrop-client/events","assignees_url":"https://api.github.com/repos/freedomofpress/securedrop-client/assignees{/user}","branches_url":"https://api.github.com/repos/freedomofpress/securedrop-client/branches{/branch}","tags_url":"https://api.github.com/repos/freedomofpress/securedrop-client/tags","blobs_url":"https://api.github.com/repos/freedomofpress/securedrop-client/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/freedomofpress/securedrop-client/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/freedomofpress/securedrop-client/git/refs{/sha}","trees_url":"https://api.github.com/repos/freedomofpress/securedrop-client/git/trees{/sha}","statuses_url":"https://api.github.com/repos/freedomofpress/securedrop-client/statuses/{sha}","languages_url":"https://api.github.com/repos/freedomofpress/securedrop-client/languages","stargazers_url":"https://api.github.com/repos/freedomofpress/securedrop-client/stargazers","contributors_url":"https://api.github.com/repos/freedomofpress/securedrop-client/contributors","subscribers_url":"https://api.github.com/repos/freedomofpress/securedrop-client/subscribers","subscription_url":"https://api.github.com/repos/freedomofpress/securedrop-client/subscription","commits_url":"https://api.github.com/repos/freedomofpress/securedrop-client/commits{/sha}","git_commits_url":"https://api.github.com/repos/freedomofpress/securedrop-client/git/commits{/sha}","comments_url":"https://api.github.com/repos/freedomofpress/securedrop-client/comments{/number}","issue_comment_url":"https://api.github.com/repos/freedomofpress/securedrop-client/issues/comments{/number}","contents_url":"https://api.github.com/repos/freedomofpress/securedrop-client/contents/{+path}","compare_url":"https://api.github.com/repos/freedomofpress/securedrop-client/compare/{base}...{head}","merges_url":"https://api.github.com/repos/freedomofpress/securedrop-client/merges","archive_url":"https://api.github.com/repos/freedomofpress/securedrop-client/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/freedomofpress/securedrop-client/downloads","issues_url":"https://api.github.com/repos/freedomofpress/securedrop-client/issues{/number}","pulls_url":"https://api.github.com/repos/freedomofpress/securedrop-client/pulls{/number}","milestones_url":"https://api.github.com/repos/freedomofpress/securedrop-client/milestones{/number}","notifications_url":"https://api.github.com/repos/freedomofpress/securedrop-client/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/freedomofpress/securedrop-client/labels{/name}","releases_url":"https://api.github.com/repos/freedomofpress/securedrop-client/releases{/id}","deployments_url":"https://api.github.com/repos/freedomofpress/securedrop-client/deployments","created_at":"2018-07-19T00:31:37Z","updated_at":"2018-10-25T18:58:21Z","pushed_at":"2018-10-27T00:03:06Z","git_url":"git://github.com/freedomofpress/securedrop-client.git","ssh_url":"git@github.com:freedomofpress/securedrop-client.git","clone_url":"https://github.com/freedomofpress/securedrop-client.git","svn_url":"https://github.com/freedomofpress/securedrop-client","homepage":"","size":302,"stargazers_count":2,"watchers_count":2,"language":"Python","has_issues":true,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":2,"mirror_url":null,"archived":false,"open_issues_count":41,"license":{"key":"agpl-3.0","name":"GNU Affero General Public License v3.0","spdx_id":"AGPL-3.0","url":"https://api.github.com/licenses/agpl-3.0","node_id":"MDc6TGljZW5zZTE="},"forks":2,"open_issues":41,"watchers":2,"default_branch":"master"}},"_links":{"self":{"href":"https://api.github.com/repos/freedomofpress/securedrop-client/pulls/77"},"html":{"href":"https://github.com/freedomofpress/securedrop-client/pull/77"},"issue":{"href":"https://api.github.com/repos/freedomofpress/securedrop-client/issues/77"},"comments":{"href":"https://api.github.com/repos/freedomofpress/securedrop-client/issues/77/comments"},"review_comments":{"href":"https://api.github.com/repos/freedomofpress/securedrop-client/pulls/77/comments"},"review_comment":{"href":"https://api.github.com/repos/freedomofpress/securedrop-client/pulls/comments{/number}"},"commits":{"href":"https://api.github.com/repos/freedomofpress/securedrop-client/pulls/77/commits"},"statuses":{"href":"https://api.github.com/repos/freedomofpress/securedrop-client/statuses/83ff655a2c1a5b82fe0da9d82e75a09779ff5c38"}},"author_association":"COLLABORATOR"}}
{"actor":{"display_login":"redshiftzero"}}