We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
Minor text injection vulnerability
See https://gratipay.com/on/twitter/who%20you%20say%20you%20are...%20Visit%20gratipay.evilsite.com%20to%20verify%20your%20account%20and%20send%20me%20money.%20Maybe%20you%20don%27t%20have%20twitter/failure.html?action

Pretty minor I admit but could potentially be exploited with encoding. We should validate twitter/github/facebook usernames.


--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `gratipay/elsewhere/__init__.py`
Content:
```
1 """This subpackage contains functionality for working with accounts elsewhere.
2 """
3 from __future__ import division, print_function, unicode_literals
4 
5 from collections import OrderedDict
6 from datetime import datetime
7 import hashlib
8 import json
9 import logging
10 from urllib import quote
11 import xml.etree.ElementTree as ET
12 
13 from aspen import log, Response
14 from aspen.utils import to_age, utc
15 from requests_oauthlib import OAuth1Session, OAuth2Session
16 
17 from gratipay.elsewhere._extractors import not_available
18 
19 
20 ACTIONS = {'opt-in', 'connect', 'lock', 'unlock'}
21 PLATFORMS = 'facebook google bitbucket bountysource github openstreetmap twitter venmo'.split()
22 
23 
24 class UnknownAccountElsewhere(Exception): pass
25 
26 
27 class PlatformRegistry(object):
28     """Registry of platforms we support connecting to Gratipay accounts.
29     """
30     def __init__(self, platforms):
31         self.__dict__ = OrderedDict((p.name, p) for p in platforms)
32 
33     def __contains__(self, platform):
34         return platform.name in self.__dict__
35 
36     def __iter__(self):
37         return iter(self.__dict__.values())
38 
39 
40 class UserInfo(object):
41     """A simple container for a user's info.
42 
43     Accessing a non-existing attribute returns `None`.
44     """
45 
46     def __init__(self, **kw):
47         self.__dict__.update(kw)
48 
49     def __getattr__(self, key):
50         return self.__dict__.get(key, None)
51 
52     def __setattr__(self, key, value):
53         if value is None:
54             self.__dict__.pop(key, None)
55         else:
56             self.__dict__[key] = value
57 
58 
59 class Platform(object):
60 
61     allows_team_connect = False
62 
63     # "x" stands for "extract"
64     x_user_info = not_available
65     x_user_id = not_available
66     x_user_name = not_available
67     x_display_name = not_available
68     x_email = not_available
69     x_gravatar_id = not_available
70     x_avatar_url = not_available
71     x_is_team = not_available
72 
73     required_attrs = ( 'account_url'
74                      , 'display_name'
75                      , 'name'
76                      )
77 
78     def __init__(self, api_key, api_secret, callback_url, api_url=None, auth_url=None):
79         self.api_key = api_key
80         self.api_secret = api_secret
81         self.callback_url = callback_url
82         if api_url:
83             self.api_url = api_url
84         if auth_url:
85             self.auth_url = auth_url
86         elif not getattr(self, 'auth_url', None):
87             self.auth_url = self.api_url
88 
89         # Determine the appropriate response parser using `self.api_format`
90         api_format = getattr(self, 'api_format', None)
91         if api_format == 'json':
92             self.api_parser = lambda r: r.json()
93         elif api_format == 'xml':
94             self.api_parser = lambda r: ET.fromstring(r.content)
95         elif api_format:
96             raise ValueError('unknown API format: '+str(api_format))
97 
98         # Make sure the subclass was implemented properly.
99         missing_attrs = [a for a in self.required_attrs if not hasattr(self, a)]
100         if missing_attrs:
101             msg = "The class %s is missing these required attributes: %s"
102             msg %= self.__class__.__name__, ', '.join(missing_attrs)
103             raise AttributeError(msg)
104 
105     def api_get(self, path, sess=None, **kw):
106         """
107         Given a `path` (e.g. /users/foo), this function sends a GET request to
108         the platform's API (e.g. https://api.github.com/users/foo).
109 
110         The response is returned, after checking its status code and ratelimit
111         headers.
112         """
113         if not sess:
114             sess = self.get_auth_session()
115         response = sess.get(self.api_url+path, **kw)
116 
117         # Check status
118         status = response.status_code
119         if status == 404:
120             raise Response(404, response.text)
121         elif status != 200:
122             log('{} api responded with {}:\n{}'.format(self.name, status, response.text)
123                , level=logging.ERROR)
124             raise Response(500, '{} lookup failed with {}'.format(self.name, status))
125 
126         # Check ratelimit headers
127         prefix = getattr(self, 'ratelimit_headers_prefix', None)
128         if prefix:
129             limit = response.headers[prefix+'limit']
130             remaining = response.headers[prefix+'remaining']
131             reset = response.headers[prefix+'reset']
132             try:
133                 limit, remaining, reset = int(limit), int(remaining), int(reset)
134             except (TypeError, ValueError):
135                 d = dict(limit=limit, remaining=remaining, reset=reset)
136                 log('Got weird rate headers from %s: %s' % (self.name, d))
137             else:
138                 percent_remaining = remaining/limit
139                 if percent_remaining < 0.5:
140                     reset = to_age(datetime.fromtimestamp(reset, tz=utc))
141                     log_msg = (
142                         '{0} API: {1:.1%} of ratelimit has been consumed, '
143                         '{2} requests remaining, resets {3}.'
144                     ).format(self.name, 1 - percent_remaining, remaining, reset)
145                     log_lvl = logging.WARNING
146                     if percent_remaining < 0.2:
147                         log_lvl = logging.ERROR
148                     elif percent_remaining < 0.05:
149                         log_lvl = logging.CRITICAL
150                     log(log_msg, log_lvl)
151 
152         return response
153 
154     def extract_user_info(self, info):
155         """
156         Given a user_info object of variable type (depending on the platform),
157         extract the relevant information by calling the platform's extractors
158         (`x_user_name`, `x_user_id`, etc).
159 
160         Returns a `UserInfo`. The `user_id` attribute is guaranteed to have a
161         unique non-empty value.
162         """
163         r = UserInfo(platform=self.name)
164         info = self.x_user_info(r, info, info)
165         r.user_name = self.x_user_name(r, info, None)
166         if self.x_user_id.__func__ is not_available:
167             r.user_id = r.user_name
168         else:
169             r.user_id = self.x_user_id(r, info)
170         assert r.user_id is not None
171         r.user_id = unicode(r.user_id)
172         assert len(r.user_id) > 0
173         r.display_name = self.x_display_name(r, info, None)
174         r.email = self.x_email(r, info, None)
175         r.avatar_url = self.x_avatar_url(r, info, None)
176         if not r.avatar_url:
177             gravatar_id = self.x_gravatar_id(r, info, None)
178             if r.email and not gravatar_id:
179                 gravatar_id = hashlib.md5(r.email.strip().lower()).hexdigest()
180             if gravatar_id:
181                 r.avatar_url = 'https://secure.gravatar.com/avatar/'+gravatar_id
182         r.is_team = self.x_is_team(r, info, False)
183         r.extra_info = info
184         return r
185 
186     def get_team_members(self, team_name, page_url=None):
187         """Given a team_name on the platform, return the team's membership list
188         from the API.
189         """
190         default_url = self.api_team_members_path.format(user_name=quote(team_name))
191         r = self.api_get(page_url or default_url)
192         members, count, pages_urls = self.api_paginator(r, self.api_parser(r))
193         members = [self.extract_user_info(m) for m in members]
194         return members, count, pages_urls
195 
196     def get_user_info(self, user_name, sess=None):
197         """Given a user_name on the platform, get the user's info from the API.
198         """
199         try:
200             path = self.api_user_info_path.format(user_name=quote(user_name))
201         except KeyError:
202             raise Response(404)
203         info = self.api_parser(self.api_get(path, sess=sess))
204         return self.extract_user_info(info)
205 
206     def get_user_self_info(self, sess):
207         """Get the authenticated user's info from the API.
208         """
209         r = self.api_get(self.api_user_self_info_path, sess=sess)
210         info = self.extract_user_info(self.api_parser(r))
211         token = getattr(sess, 'token', None)
212         if token:
213             info.token = json.dumps(token)
214         return info
215 
216 
217 class PlatformOAuth1(Platform):
218 
219     request_token_path = '/oauth/request_token'
220     authorize_path = '/oauth/authorize'
221     access_token_path = '/oauth/access_token'
222 
223     def get_auth_session(self, token=None):
224         args = ()
225         if token:
226             args = (token['token'], token['token_secret'])
227         return OAuth1Session(self.api_key, self.api_secret, *args,
228                              callback_uri=self.callback_url)
229 
230     def get_auth_url(self, **kw):
231         sess = self.get_auth_session()
232         r = sess.fetch_request_token(self.auth_url+self.request_token_path)
233         url = sess.authorization_url(self.auth_url+self.authorize_path)
234         return url, r['oauth_token'], r['oauth_token_secret']
235 
236     def get_query_id(self, querystring):
237         return querystring['oauth_token']
238 
239     def handle_auth_callback(self, url, token, token_secret):
240         sess = self.get_auth_session(dict(token=token, token_secret=token_secret))
241         sess.parse_authorization_response(url)
242         r = sess.fetch_access_token(self.auth_url+self.access_token_path)
243         sess.token = dict(token=r['oauth_token'],
244                           token_secret=r['oauth_token_secret'])
245         return sess
246 
247 
248 class PlatformOAuth2(Platform):
249 
250     oauth_default_scope = None
251     oauth_email_scope = None
252     oauth_payment_scope = None
253 
254     def get_auth_session(self, state=None, token=None, token_updater=None):
255         return OAuth2Session(self.api_key, state=state, token=token,
256                              token_updater=token_updater,
257                              redirect_uri=self.callback_url,
258                              scope=self.oauth_default_scope)
259 
260     def get_auth_url(self, **kw):
261         sess = self.get_auth_session()
262         url, state = sess.authorization_url(self.auth_url)
263         return url, state, ''
264 
265     def get_query_id(self, querystring):
266         return querystring['state']
267 
268     def handle_auth_callback(self, url, state, unused_arg):
269         sess = self.get_auth_session(state=state)
270         sess.fetch_token(self.access_token_url,
271                          client_secret=self.api_secret,
272                          authorization_response=url)
273         return sess
274
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/gratipay/elsewhere/__init__.py b/gratipay/elsewhere/__init__.py
--- a/gratipay/elsewhere/__init__.py
+++ b/gratipay/elsewhere/__init__.py
@@ -114,7 +114,14 @@
             sess = self.get_auth_session()
         response = sess.get(self.api_url+path, **kw)
 
-        # Check status
+        self.check_api_response_status(response)
+        self.check_ratelimit_headers(response)
+
+        return response
+
+    def check_api_response_status(self, response):
+        """Pass through any 404, convert any other non-200 into a 500.
+        """
         status = response.status_code
         if status == 404:
             raise Response(404, response.text)
@@ -123,7 +130,9 @@
                , level=logging.ERROR)
             raise Response(500, '{} lookup failed with {}'.format(self.name, status))
 
-        # Check ratelimit headers
+    def check_ratelimit_headers(self, response):
+        """Emit log messages if we're running out of ratelimit.
+        """
         prefix = getattr(self, 'ratelimit_headers_prefix', None)
         if prefix:
             limit = response.headers[prefix+'limit']
@@ -149,8 +158,6 @@
                         log_lvl = logging.CRITICAL
                     log(log_msg, log_lvl)
 
-        return response
-
     def extract_user_info(self, info):
         """
         Given a user_info object of variable type (depending on the platform),

{"golden_diff": "diff --git a/gratipay/elsewhere/__init__.py b/gratipay/elsewhere/__init__.py\n--- a/gratipay/elsewhere/__init__.py\n+++ b/gratipay/elsewhere/__init__.py\n@@ -114,7 +114,14 @@\n             sess = self.get_auth_session()\n         response = sess.get(self.api_url+path, **kw)\n \n-        # Check status\n+        self.check_api_response_status(response)\n+        self.check_ratelimit_headers(response)\n+\n+        return response\n+\n+    def check_api_response_status(self, response):\n+        \"\"\"Pass through any 404, convert any other non-200 into a 500.\n+        \"\"\"\n         status = response.status_code\n         if status == 404:\n             raise Response(404, response.text)\n@@ -123,7 +130,9 @@\n                , level=logging.ERROR)\n             raise Response(500, '{} lookup failed with {}'.format(self.name, status))\n \n-        # Check ratelimit headers\n+    def check_ratelimit_headers(self, response):\n+        \"\"\"Emit log messages if we're running out of ratelimit.\n+        \"\"\"\n         prefix = getattr(self, 'ratelimit_headers_prefix', None)\n         if prefix:\n             limit = response.headers[prefix+'limit']\n@@ -149,8 +158,6 @@\n                         log_lvl = logging.CRITICAL\n                     log(log_msg, log_lvl)\n \n-        return response\n-\n     def extract_user_info(self, info):\n         \"\"\"\n         Given a user_info object of variable type (depending on the platform),\n", "issue": "Minor text injection vulnerability\nSee https://gratipay.com/on/twitter/who%20you%20say%20you%20are...%20Visit%20gratipay.evilsite.com%20to%20verify%20your%20account%20and%20send%20me%20money.%20Maybe%20you%20don%27t%20have%20twitter/failure.html?action\n\nPretty minor I admit but could potentially be exploited with encoding. We should validate twitter/github/facebook usernames.\n\n", "before_files": [{"content": "\"\"\"This subpackage contains functionality for working with accounts elsewhere.\n\"\"\"\nfrom __future__ import division, print_function, unicode_literals\n\nfrom collections import OrderedDict\nfrom datetime import datetime\nimport hashlib\nimport json\nimport logging\nfrom urllib import quote\nimport xml.etree.ElementTree as ET\n\nfrom aspen import log, Response\nfrom aspen.utils import to_age, utc\nfrom requests_oauthlib import OAuth1Session, OAuth2Session\n\nfrom gratipay.elsewhere._extractors import not_available\n\n\nACTIONS = {'opt-in', 'connect', 'lock', 'unlock'}\nPLATFORMS = 'facebook google bitbucket bountysource github openstreetmap twitter venmo'.split()\n\n\nclass UnknownAccountElsewhere(Exception): pass\n\n\nclass PlatformRegistry(object):\n    \"\"\"Registry of platforms we support connecting to Gratipay accounts.\n    \"\"\"\n    def __init__(self, platforms):\n        self.__dict__ = OrderedDict((p.name, p) for p in platforms)\n\n    def __contains__(self, platform):\n        return platform.name in self.__dict__\n\n    def __iter__(self):\n        return iter(self.__dict__.values())\n\n\nclass UserInfo(object):\n    \"\"\"A simple container for a user's info.\n\n    Accessing a non-existing attribute returns `None`.\n    \"\"\"\n\n    def __init__(self, **kw):\n        self.__dict__.update(kw)\n\n    def __getattr__(self, key):\n        return self.__dict__.get(key, None)\n\n    def __setattr__(self, key, value):\n        if value is None:\n            self.__dict__.pop(key, None)\n        else:\n            self.__dict__[key] = value\n\n\nclass Platform(object):\n\n    allows_team_connect = False\n\n    # \"x\" stands for \"extract\"\n    x_user_info = not_available\n    x_user_id = not_available\n    x_user_name = not_available\n    x_display_name = not_available\n    x_email = not_available\n    x_gravatar_id = not_available\n    x_avatar_url = not_available\n    x_is_team = not_available\n\n    required_attrs = ( 'account_url'\n                     , 'display_name'\n                     , 'name'\n                     )\n\n    def __init__(self, api_key, api_secret, callback_url, api_url=None, auth_url=None):\n        self.api_key = api_key\n        self.api_secret = api_secret\n        self.callback_url = callback_url\n        if api_url:\n            self.api_url = api_url\n        if auth_url:\n            self.auth_url = auth_url\n        elif not getattr(self, 'auth_url', None):\n            self.auth_url = self.api_url\n\n        # Determine the appropriate response parser using `self.api_format`\n        api_format = getattr(self, 'api_format', None)\n        if api_format == 'json':\n            self.api_parser = lambda r: r.json()\n        elif api_format == 'xml':\n            self.api_parser = lambda r: ET.fromstring(r.content)\n        elif api_format:\n            raise ValueError('unknown API format: '+str(api_format))\n\n        # Make sure the subclass was implemented properly.\n        missing_attrs = [a for a in self.required_attrs if not hasattr(self, a)]\n        if missing_attrs:\n            msg = \"The class %s is missing these required attributes: %s\"\n            msg %= self.__class__.__name__, ', '.join(missing_attrs)\n            raise AttributeError(msg)\n\n    def api_get(self, path, sess=None, **kw):\n        \"\"\"\n        Given a `path` (e.g. /users/foo), this function sends a GET request to\n        the platform's API (e.g. https://api.github.com/users/foo).\n\n        The response is returned, after checking its status code and ratelimit\n        headers.\n        \"\"\"\n        if not sess:\n            sess = self.get_auth_session()\n        response = sess.get(self.api_url+path, **kw)\n\n        # Check status\n        status = response.status_code\n        if status == 404:\n            raise Response(404, response.text)\n        elif status != 200:\n            log('{} api responded with {}:\\n{}'.format(self.name, status, response.text)\n               , level=logging.ERROR)\n            raise Response(500, '{} lookup failed with {}'.format(self.name, status))\n\n        # Check ratelimit headers\n        prefix = getattr(self, 'ratelimit_headers_prefix', None)\n        if prefix:\n            limit = response.headers[prefix+'limit']\n            remaining = response.headers[prefix+'remaining']\n            reset = response.headers[prefix+'reset']\n            try:\n                limit, remaining, reset = int(limit), int(remaining), int(reset)\n            except (TypeError, ValueError):\n                d = dict(limit=limit, remaining=remaining, reset=reset)\n                log('Got weird rate headers from %s: %s' % (self.name, d))\n            else:\n                percent_remaining = remaining/limit\n                if percent_remaining < 0.5:\n                    reset = to_age(datetime.fromtimestamp(reset, tz=utc))\n                    log_msg = (\n                        '{0} API: {1:.1%} of ratelimit has been consumed, '\n                        '{2} requests remaining, resets {3}.'\n                    ).format(self.name, 1 - percent_remaining, remaining, reset)\n                    log_lvl = logging.WARNING\n                    if percent_remaining < 0.2:\n                        log_lvl = logging.ERROR\n                    elif percent_remaining < 0.05:\n                        log_lvl = logging.CRITICAL\n                    log(log_msg, log_lvl)\n\n        return response\n\n    def extract_user_info(self, info):\n        \"\"\"\n        Given a user_info object of variable type (depending on the platform),\n        extract the relevant information by calling the platform's extractors\n        (`x_user_name`, `x_user_id`, etc).\n\n        Returns a `UserInfo`. The `user_id` attribute is guaranteed to have a\n        unique non-empty value.\n        \"\"\"\n        r = UserInfo(platform=self.name)\n        info = self.x_user_info(r, info, info)\n        r.user_name = self.x_user_name(r, info, None)\n        if self.x_user_id.__func__ is not_available:\n            r.user_id = r.user_name\n        else:\n            r.user_id = self.x_user_id(r, info)\n        assert r.user_id is not None\n        r.user_id = unicode(r.user_id)\n        assert len(r.user_id) > 0\n        r.display_name = self.x_display_name(r, info, None)\n        r.email = self.x_email(r, info, None)\n        r.avatar_url = self.x_avatar_url(r, info, None)\n        if not r.avatar_url:\n            gravatar_id = self.x_gravatar_id(r, info, None)\n            if r.email and not gravatar_id:\n                gravatar_id = hashlib.md5(r.email.strip().lower()).hexdigest()\n            if gravatar_id:\n                r.avatar_url = 'https://secure.gravatar.com/avatar/'+gravatar_id\n        r.is_team = self.x_is_team(r, info, False)\n        r.extra_info = info\n        return r\n\n    def get_team_members(self, team_name, page_url=None):\n        \"\"\"Given a team_name on the platform, return the team's membership list\n        from the API.\n        \"\"\"\n        default_url = self.api_team_members_path.format(user_name=quote(team_name))\n        r = self.api_get(page_url or default_url)\n        members, count, pages_urls = self.api_paginator(r, self.api_parser(r))\n        members = [self.extract_user_info(m) for m in members]\n        return members, count, pages_urls\n\n    def get_user_info(self, user_name, sess=None):\n        \"\"\"Given a user_name on the platform, get the user's info from the API.\n        \"\"\"\n        try:\n            path = self.api_user_info_path.format(user_name=quote(user_name))\n        except KeyError:\n            raise Response(404)\n        info = self.api_parser(self.api_get(path, sess=sess))\n        return self.extract_user_info(info)\n\n    def get_user_self_info(self, sess):\n        \"\"\"Get the authenticated user's info from the API.\n        \"\"\"\n        r = self.api_get(self.api_user_self_info_path, sess=sess)\n        info = self.extract_user_info(self.api_parser(r))\n        token = getattr(sess, 'token', None)\n        if token:\n            info.token = json.dumps(token)\n        return info\n\n\nclass PlatformOAuth1(Platform):\n\n    request_token_path = '/oauth/request_token'\n    authorize_path = '/oauth/authorize'\n    access_token_path = '/oauth/access_token'\n\n    def get_auth_session(self, token=None):\n        args = ()\n        if token:\n            args = (token['token'], token['token_secret'])\n        return OAuth1Session(self.api_key, self.api_secret, *args,\n                             callback_uri=self.callback_url)\n\n    def get_auth_url(self, **kw):\n        sess = self.get_auth_session()\n        r = sess.fetch_request_token(self.auth_url+self.request_token_path)\n        url = sess.authorization_url(self.auth_url+self.authorize_path)\n        return url, r['oauth_token'], r['oauth_token_secret']\n\n    def get_query_id(self, querystring):\n        return querystring['oauth_token']\n\n    def handle_auth_callback(self, url, token, token_secret):\n        sess = self.get_auth_session(dict(token=token, token_secret=token_secret))\n        sess.parse_authorization_response(url)\n        r = sess.fetch_access_token(self.auth_url+self.access_token_path)\n        sess.token = dict(token=r['oauth_token'],\n                          token_secret=r['oauth_token_secret'])\n        return sess\n\n\nclass PlatformOAuth2(Platform):\n\n    oauth_default_scope = None\n    oauth_email_scope = None\n    oauth_payment_scope = None\n\n    def get_auth_session(self, state=None, token=None, token_updater=None):\n        return OAuth2Session(self.api_key, state=state, token=token,\n                             token_updater=token_updater,\n                             redirect_uri=self.callback_url,\n                             scope=self.oauth_default_scope)\n\n    def get_auth_url(self, **kw):\n        sess = self.get_auth_session()\n        url, state = sess.authorization_url(self.auth_url)\n        return url, state, ''\n\n    def get_query_id(self, querystring):\n        return querystring['state']\n\n    def handle_auth_callback(self, url, state, unused_arg):\n        sess = self.get_auth_session(state=state)\n        sess.fetch_token(self.access_token_url,\n                         client_secret=self.api_secret,\n                         authorization_response=url)\n        return sess\n", "path": "gratipay/elsewhere/__init__.py"}], "after_files": [{"content": "\"\"\"This subpackage contains functionality for working with accounts elsewhere.\n\"\"\"\nfrom __future__ import division, print_function, unicode_literals\n\nfrom collections import OrderedDict\nfrom datetime import datetime\nimport hashlib\nimport json\nimport logging\nfrom urllib import quote\nimport xml.etree.ElementTree as ET\n\nfrom aspen import log, Response\nfrom aspen.utils import to_age, utc\nfrom requests_oauthlib import OAuth1Session, OAuth2Session\n\nfrom gratipay.elsewhere._extractors import not_available\n\n\nACTIONS = {'opt-in', 'connect', 'lock', 'unlock'}\nPLATFORMS = 'facebook google bitbucket bountysource github openstreetmap twitter venmo'.split()\n\n\nclass UnknownAccountElsewhere(Exception): pass\n\n\nclass PlatformRegistry(object):\n    \"\"\"Registry of platforms we support connecting to Gratipay accounts.\n    \"\"\"\n    def __init__(self, platforms):\n        self.__dict__ = OrderedDict((p.name, p) for p in platforms)\n\n    def __contains__(self, platform):\n        return platform.name in self.__dict__\n\n    def __iter__(self):\n        return iter(self.__dict__.values())\n\n\nclass UserInfo(object):\n    \"\"\"A simple container for a user's info.\n\n    Accessing a non-existing attribute returns `None`.\n    \"\"\"\n\n    def __init__(self, **kw):\n        self.__dict__.update(kw)\n\n    def __getattr__(self, key):\n        return self.__dict__.get(key, None)\n\n    def __setattr__(self, key, value):\n        if value is None:\n            self.__dict__.pop(key, None)\n        else:\n            self.__dict__[key] = value\n\n\nclass Platform(object):\n\n    allows_team_connect = False\n\n    # \"x\" stands for \"extract\"\n    x_user_info = not_available\n    x_user_id = not_available\n    x_user_name = not_available\n    x_display_name = not_available\n    x_email = not_available\n    x_gravatar_id = not_available\n    x_avatar_url = not_available\n    x_is_team = not_available\n\n    required_attrs = ( 'account_url'\n                     , 'display_name'\n                     , 'name'\n                     )\n\n    def __init__(self, api_key, api_secret, callback_url, api_url=None, auth_url=None):\n        self.api_key = api_key\n        self.api_secret = api_secret\n        self.callback_url = callback_url\n        if api_url:\n            self.api_url = api_url\n        if auth_url:\n            self.auth_url = auth_url\n        elif not getattr(self, 'auth_url', None):\n            self.auth_url = self.api_url\n\n        # Determine the appropriate response parser using `self.api_format`\n        api_format = getattr(self, 'api_format', None)\n        if api_format == 'json':\n            self.api_parser = lambda r: r.json()\n        elif api_format == 'xml':\n            self.api_parser = lambda r: ET.fromstring(r.content)\n        elif api_format:\n            raise ValueError('unknown API format: '+str(api_format))\n\n        # Make sure the subclass was implemented properly.\n        missing_attrs = [a for a in self.required_attrs if not hasattr(self, a)]\n        if missing_attrs:\n            msg = \"The class %s is missing these required attributes: %s\"\n            msg %= self.__class__.__name__, ', '.join(missing_attrs)\n            raise AttributeError(msg)\n\n    def api_get(self, path, sess=None, **kw):\n        \"\"\"\n        Given a `path` (e.g. /users/foo), this function sends a GET request to\n        the platform's API (e.g. https://api.github.com/users/foo).\n\n        The response is returned, after checking its status code and ratelimit\n        headers.\n        \"\"\"\n        if not sess:\n            sess = self.get_auth_session()\n        response = sess.get(self.api_url+path, **kw)\n\n        self.check_api_response_status(response)\n        self.check_ratelimit_headers(response)\n\n        return response\n\n    def check_api_response_status(self, response):\n        \"\"\"Pass through any 404, convert any other non-200 into a 500.\n        \"\"\"\n        status = response.status_code\n        if status == 404:\n            raise Response(404, response.text)\n        elif status != 200:\n            log('{} api responded with {}:\\n{}'.format(self.name, status, response.text)\n               , level=logging.ERROR)\n            raise Response(500, '{} lookup failed with {}'.format(self.name, status))\n\n    def check_ratelimit_headers(self, response):\n        \"\"\"Emit log messages if we're running out of ratelimit.\n        \"\"\"\n        prefix = getattr(self, 'ratelimit_headers_prefix', None)\n        if prefix:\n            limit = response.headers[prefix+'limit']\n            remaining = response.headers[prefix+'remaining']\n            reset = response.headers[prefix+'reset']\n            try:\n                limit, remaining, reset = int(limit), int(remaining), int(reset)\n            except (TypeError, ValueError):\n                d = dict(limit=limit, remaining=remaining, reset=reset)\n                log('Got weird rate headers from %s: %s' % (self.name, d))\n            else:\n                percent_remaining = remaining/limit\n                if percent_remaining < 0.5:\n                    reset = to_age(datetime.fromtimestamp(reset, tz=utc))\n                    log_msg = (\n                        '{0} API: {1:.1%} of ratelimit has been consumed, '\n                        '{2} requests remaining, resets {3}.'\n                    ).format(self.name, 1 - percent_remaining, remaining, reset)\n                    log_lvl = logging.WARNING\n                    if percent_remaining < 0.2:\n                        log_lvl = logging.ERROR\n                    elif percent_remaining < 0.05:\n                        log_lvl = logging.CRITICAL\n                    log(log_msg, log_lvl)\n\n    def extract_user_info(self, info):\n        \"\"\"\n        Given a user_info object of variable type (depending on the platform),\n        extract the relevant information by calling the platform's extractors\n        (`x_user_name`, `x_user_id`, etc).\n\n        Returns a `UserInfo`. The `user_id` attribute is guaranteed to have a\n        unique non-empty value.\n        \"\"\"\n        r = UserInfo(platform=self.name)\n        info = self.x_user_info(r, info, info)\n        r.user_name = self.x_user_name(r, info, None)\n        if self.x_user_id.__func__ is not_available:\n            r.user_id = r.user_name\n        else:\n            r.user_id = self.x_user_id(r, info)\n        assert r.user_id is not None\n        r.user_id = unicode(r.user_id)\n        assert len(r.user_id) > 0\n        r.display_name = self.x_display_name(r, info, None)\n        r.email = self.x_email(r, info, None)\n        r.avatar_url = self.x_avatar_url(r, info, None)\n        if not r.avatar_url:\n            gravatar_id = self.x_gravatar_id(r, info, None)\n            if r.email and not gravatar_id:\n                gravatar_id = hashlib.md5(r.email.strip().lower()).hexdigest()\n            if gravatar_id:\n                r.avatar_url = 'https://secure.gravatar.com/avatar/'+gravatar_id\n        r.is_team = self.x_is_team(r, info, False)\n        r.extra_info = info\n        return r\n\n    def get_team_members(self, team_name, page_url=None):\n        \"\"\"Given a team_name on the platform, return the team's membership list\n        from the API.\n        \"\"\"\n        default_url = self.api_team_members_path.format(user_name=quote(team_name))\n        r = self.api_get(page_url or default_url)\n        members, count, pages_urls = self.api_paginator(r, self.api_parser(r))\n        members = [self.extract_user_info(m) for m in members]\n        return members, count, pages_urls\n\n    def get_user_info(self, user_name, sess=None):\n        \"\"\"Given a user_name on the platform, get the user's info from the API.\n        \"\"\"\n        try:\n            path = self.api_user_info_path.format(user_name=quote(user_name))\n        except KeyError:\n            raise Response(404)\n        info = self.api_parser(self.api_get(path, sess=sess))\n        return self.extract_user_info(info)\n\n    def get_user_self_info(self, sess):\n        \"\"\"Get the authenticated user's info from the API.\n        \"\"\"\n        r = self.api_get(self.api_user_self_info_path, sess=sess)\n        info = self.extract_user_info(self.api_parser(r))\n        token = getattr(sess, 'token', None)\n        if token:\n            info.token = json.dumps(token)\n        return info\n\n\nclass PlatformOAuth1(Platform):\n\n    request_token_path = '/oauth/request_token'\n    authorize_path = '/oauth/authorize'\n    access_token_path = '/oauth/access_token'\n\n    def get_auth_session(self, token=None):\n        args = ()\n        if token:\n            args = (token['token'], token['token_secret'])\n        return OAuth1Session(self.api_key, self.api_secret, *args,\n                             callback_uri=self.callback_url)\n\n    def get_auth_url(self, **kw):\n        sess = self.get_auth_session()\n        r = sess.fetch_request_token(self.auth_url+self.request_token_path)\n        url = sess.authorization_url(self.auth_url+self.authorize_path)\n        return url, r['oauth_token'], r['oauth_token_secret']\n\n    def get_query_id(self, querystring):\n        return querystring['oauth_token']\n\n    def handle_auth_callback(self, url, token, token_secret):\n        sess = self.get_auth_session(dict(token=token, token_secret=token_secret))\n        sess.parse_authorization_response(url)\n        r = sess.fetch_access_token(self.auth_url+self.access_token_path)\n        sess.token = dict(token=r['oauth_token'],\n                          token_secret=r['oauth_token_secret'])\n        return sess\n\n\nclass PlatformOAuth2(Platform):\n\n    oauth_default_scope = None\n    oauth_email_scope = None\n    oauth_payment_scope = None\n\n    def get_auth_session(self, state=None, token=None, token_updater=None):\n        return OAuth2Session(self.api_key, state=state, token=token,\n                             token_updater=token_updater,\n                             redirect_uri=self.callback_url,\n                             scope=self.oauth_default_scope)\n\n    def get_auth_url(self, **kw):\n        sess = self.get_auth_session()\n        url, state = sess.authorization_url(self.auth_url)\n        return url, state, ''\n\n    def get_query_id(self, querystring):\n        return querystring['state']\n\n    def handle_auth_callback(self, url, state, unused_arg):\n        sess = self.get_auth_session(state=state)\n        sess.fetch_token(self.access_token_url,\n                         client_secret=self.api_secret,\n                         authorization_response=url)\n        return sess\n", "path": "gratipay/elsewhere/__init__.py"}]}