-
Notifications
You must be signed in to change notification settings - Fork 192
/
endpoints.py
226 lines (187 loc) · 8.18 KB
/
endpoints.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
"""Flask Blueprint for our "API" module.
This is used to make API calls to GitHub, either via a logged-in users
credentials or as a proxy on behalf of anonymous or unauthenticated users.
"""
import json
from flask import abort
from flask import Blueprint
from flask import g
from flask import make_response
from flask import render_template
from flask import request
from flask import session
from webcompat import app
from webcompat.api.helpers import get_html_comments
from webcompat.helpers import api_request
from webcompat.helpers import get_comment_data
from webcompat.helpers import get_response_headers
from webcompat.helpers import mockable_response
from webcompat.helpers import normalize_api_params
from webcompat import limiter
api_bp = Blueprint('api_bp', __name__, url_prefix='/api',
template_folder='../templates')
JSON_MIME_HTML = 'application/vnd.github.v3.html+json'
HTML_MIME = 'text/html'
ISSUES_PATH = app.config['ISSUES_REPO_URI']
REPO_PATH = ISSUES_PATH[:-7]
@api_bp.route('/issues/<int:number>')
def proxy_issue(number):
"""XHR endpoint to get issue data from GitHub.
either as an authed user, or as one of our proxy bots.
"""
path = 'repos/{0}/{1}'.format(ISSUES_PATH, number)
return api_request('get', path, mime_type=JSON_MIME_HTML)
@api_bp.route('/issues/<int:number>/edit', methods=['PATCH'])
def edit_issue(number):
"""XHR endpoint to push back edits to GitHub for a single issue.
- It only allows change of state and change of milestones.
- It's not proxied, so only users with write access are
able to edit issues.
Format: {'milestone': 2, 'state': 'open'}
"""
if not g.user:
abort(403)
path = 'repos/{0}/{1}'.format(ISSUES_PATH, number)
patch_data = json.loads(request.data)
# Create a list of associated milestones id with their mandatory state.
STATUSES = app.config['STATUSES']
valid_statuses = [(STATUSES[status]['id'], STATUSES[status]['state'])
for status in STATUSES]
data_check = (patch_data['milestone'], patch_data['state'])
# The PATCH data can only be of length: 2
if data_check in valid_statuses and len(patch_data) == 2:
(content, status_code, headers) = api_request('patch',
path, data=request.data)
return (content, status_code,
{'content-type': JSON_MIME_HTML})
# Default will be 403 for this route
abort(403)
@api_bp.route('/issues')
def proxy_issues():
"""List all issues from GitHub on the API endpoint."""
params = request.args.copy()
# If there's a q param, then we need to use the Search API
# and load those results. For logged in users, we handle this at the
# server level.
if g.user and params.get('q'):
return get_search_results(params.get('q'), params)
# Non-authed users should never get here--the request is made to
# GitHub client-side)--but return out of paranoia anyways.
elif params.get('q'):
abort(404)
path = 'repos/{0}'.format(ISSUES_PATH)
return api_request('get', path, params=params)
@api_bp.route('/issues/<username>/<parameter>')
def get_user_activity_issues(username, parameter):
"""Return issues related to a user at the API endpoint.
cf. https://developer.github.com/v3/issues/#list-issues-for-a-repository
This is used for "creator" and "mentioned". A special "needsinfo" parameter
value is converted into a request for labels of the format:
`status-needsinfo-username`
Any logged in user can see details for any other logged in user. We can
extend this to non-logged in users in the future if we want.
"""
if not g.user:
abort(401)
# copy the params so we can add to the dict.
params = request.args.copy()
params['state'] = 'all'
if parameter == 'needsinfo':
params['labels'] = 'status-needsinfo-{0}'.format(username)
else:
params[parameter] = username
path = 'repos/{path}'.format(path=ISSUES_PATH)
return api_request('get', path, params=params)
@api_bp.route('/issues/category/<issue_category>')
def get_issue_category(issue_category):
"""Return all issues for a specific category."""
category_list = app.config['OPEN_STATUSES']
issues_path = 'repos/{0}'.format(ISSUES_PATH)
params = request.args.copy()
if issue_category in category_list:
STATUSES = app.config['STATUSES']
params.add('milestone', STATUSES[issue_category]['id'])
return api_request('get', issues_path, params=params)
elif issue_category == 'closed':
params['state'] = 'closed'
return api_request('get', issues_path, params=params)
else:
# The path doesn’t exist. 404 Not Found.
abort(404)
@api_bp.route('/issues/search')
@limiter.limit('30/minute',
key_func=lambda: session.get('username', 'proxy-user'))
def get_search_results(query_string=None, params=None):
"""XHR endpoint to get results from GitHub's Search API.
We're specifically searching "issues" here, which seems to make the most
sense. Note that the rate limit is different for Search: 30 requests per
minute.
If a user hits the rate limit, the Flask Limiter extension will send a
429. See @app.error_handler(429) in views.py.
This method can take a query_string argument, to be called from other
endpoints, or the query_string can be passed in via the Request object.
"""
params = params or request.args.copy()
query_string = query_string or params.get('q')
# Fail early if no appropriate query_string
if not query_string:
abort(404)
# restrict results to our repo.
query_string += " repo:{0}".format(REPO_PATH)
params['q'] = query_string
# add a required parameter to request only issues and not PRs
params['q'] += ' is:issue'
# convert issues api to search api params here.
params = normalize_api_params(params)
path = 'search/issues'
return api_request('get', path, params=params,
mime_type=JSON_MIME_HTML)
@api_bp.route('/issues/<int:number>/comments', methods=['GET', 'POST'])
def proxy_comments(number):
"""XHR endpoint for GitHub issue comments.
* GET an issue comments
* POST a comment on an issue (only as an authorized GitHub user)
"""
params = request.args.copy()
path = 'repos/{0}/{1}/comments'.format(ISSUES_PATH, number)
if request.method == 'POST' and g.user:
new_comment = api_request('post', path, params=params,
data=get_comment_data(request.data),
mime_type=JSON_MIME_HTML)
return get_html_comments(new_comment)
else:
# TODO: handle the (rare) case for more than 1 page of comments
# for now, we just get the first 100 and rely on the client to
# fetch more
params.update({'per_page': 100})
comments_data = api_request('get', path, params=params,
mime_type=JSON_MIME_HTML)
comments_status = comments_data[1:2]
if comments_status != 304:
return get_html_comments(comments_data)
else:
# in the case of a 304, the browser cache will handle it.
return '', 304, get_response_headers(comments_data, HTML_MIME)
@api_bp.route('/issues/<int:number>/labels', methods=['POST'])
def modify_labels(number):
"""XHR endpoint to modify issue labels.
Sending in an empty array removes them all as well.
This method is not proxied, so only users with write access
will be able to edit labels.
"""
if g.user:
path = 'repos/{0}/{1}/labels'.format(ISSUES_PATH, number)
return api_request('put', path, data=request.data)
else:
abort(403)
@api_bp.route('/issues/labels')
def get_repo_labels():
"""XHR endpoint to get all possible labels in a repo."""
params = request.args.copy()
path = 'repos/{0}/labels'.format(REPO_PATH)
return api_request('get', path, params=params)