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         self.check_api_response_status(response)
118         self.check_ratelimit_headers(response)
119 
120         return response
121 
122     def check_api_response_status(self, response):
123         """Pass through any 404, convert any other non-200 into a 500.
124         """
125         status = response.status_code
126         if status == 404:
127             raise Response(404, response.text)
128         elif status != 200:
129             log('{} api responded with {}:\n{}'.format(self.name, status, response.text)
130                , level=logging.ERROR)
131             raise Response(500, '{} lookup failed with {}'.format(self.name, status))
132 
133     def check_ratelimit_headers(self, response):
134         """Emit log messages if we're running out of ratelimit.
135         """
136         prefix = getattr(self, 'ratelimit_headers_prefix', None)
137         if prefix:
138             limit = response.headers[prefix+'limit']
139             remaining = response.headers[prefix+'remaining']
140             reset = response.headers[prefix+'reset']
141             try:
142                 limit, remaining, reset = int(limit), int(remaining), int(reset)
143             except (TypeError, ValueError):
144                 d = dict(limit=limit, remaining=remaining, reset=reset)
145                 log('Got weird rate headers from %s: %s' % (self.name, d))
146             else:
147                 percent_remaining = remaining/limit
148                 if percent_remaining < 0.5:
149                     reset = to_age(datetime.fromtimestamp(reset, tz=utc))
150                     log_msg = (
151                         '{0} API: {1:.1%} of ratelimit has been consumed, '
152                         '{2} requests remaining, resets {3}.'
153                     ).format(self.name, 1 - percent_remaining, remaining, reset)
154                     log_lvl = logging.WARNING
155                     if percent_remaining < 0.2:
156                         log_lvl = logging.ERROR
157                     elif percent_remaining < 0.05:
158                         log_lvl = logging.CRITICAL
159                     log(log_msg, log_lvl)
160 
161     def extract_user_info(self, info):
162         """
163         Given a user_info object of variable type (depending on the platform),
164         extract the relevant information by calling the platform's extractors
165         (`x_user_name`, `x_user_id`, etc).
166 
167         Returns a `UserInfo`. The `user_id` attribute is guaranteed to have a
168         unique non-empty value.
169         """
170         r = UserInfo(platform=self.name)
171         info = self.x_user_info(r, info, info)
172         r.user_name = self.x_user_name(r, info, None)
173         if self.x_user_id.__func__ is not_available:
174             r.user_id = r.user_name
175         else:
176             r.user_id = self.x_user_id(r, info)
177         assert r.user_id is not None
178         r.user_id = unicode(r.user_id)
179         assert len(r.user_id) > 0
180         r.display_name = self.x_display_name(r, info, None)
181         r.email = self.x_email(r, info, None)
182         r.avatar_url = self.x_avatar_url(r, info, None)
183         if not r.avatar_url:
184             gravatar_id = self.x_gravatar_id(r, info, None)
185             if r.email and not gravatar_id:
186                 gravatar_id = hashlib.md5(r.email.strip().lower()).hexdigest()
187             if gravatar_id:
188                 r.avatar_url = 'https://secure.gravatar.com/avatar/'+gravatar_id
189         r.is_team = self.x_is_team(r, info, False)
190         r.extra_info = info
191         return r
192 
193     def get_team_members(self, team_name, page_url=None):
194         """Given a team_name on the platform, return the team's membership list
195         from the API.
196         """
197         default_url = self.api_team_members_path.format(user_name=quote(team_name))
198         r = self.api_get(page_url or default_url)
199         members, count, pages_urls = self.api_paginator(r, self.api_parser(r))
200         members = [self.extract_user_info(m) for m in members]
201         return members, count, pages_urls
202 
203     def get_user_info(self, user_name, sess=None):
204         """Given a user_name on the platform, get the user's info from the API.
205         """
206         try:
207             path = self.api_user_info_path.format(user_name=quote(user_name))
208         except KeyError:
209             raise Response(404)
210         info = self.api_parser(self.api_get(path, sess=sess))
211         return self.extract_user_info(info)
212 
213     def get_user_self_info(self, sess):
214         """Get the authenticated user's info from the API.
215         """
216         r = self.api_get(self.api_user_self_info_path, sess=sess)
217         info = self.extract_user_info(self.api_parser(r))
218         token = getattr(sess, 'token', None)
219         if token:
220             info.token = json.dumps(token)
221         return info
222 
223 
224 class PlatformOAuth1(Platform):
225 
226     request_token_path = '/oauth/request_token'
227     authorize_path = '/oauth/authorize'
228     access_token_path = '/oauth/access_token'
229 
230     def get_auth_session(self, token=None):
231         args = ()
232         if token:
233             args = (token['token'], token['token_secret'])
234         return OAuth1Session(self.api_key, self.api_secret, *args,
235                              callback_uri=self.callback_url)
236 
237     def get_auth_url(self, **kw):
238         sess = self.get_auth_session()
239         r = sess.fetch_request_token(self.auth_url+self.request_token_path)
240         url = sess.authorization_url(self.auth_url+self.authorize_path)
241         return url, r['oauth_token'], r['oauth_token_secret']
242 
243     def get_query_id(self, querystring):
244         return querystring['oauth_token']
245 
246     def handle_auth_callback(self, url, token, token_secret):
247         sess = self.get_auth_session(dict(token=token, token_secret=token_secret))
248         sess.parse_authorization_response(url)
249         r = sess.fetch_access_token(self.auth_url+self.access_token_path)
250         sess.token = dict(token=r['oauth_token'],
251                           token_secret=r['oauth_token_secret'])
252         return sess
253 
254 
255 class PlatformOAuth2(Platform):
256 
257     oauth_default_scope = None
258     oauth_email_scope = None
259     oauth_payment_scope = None
260 
261     def get_auth_session(self, state=None, token=None, token_updater=None):
262         return OAuth2Session(self.api_key, state=state, token=token,
263                              token_updater=token_updater,
264                              redirect_uri=self.callback_url,
265                              scope=self.oauth_default_scope)
266 
267     def get_auth_url(self, **kw):
268         sess = self.get_auth_session()
269         url, state = sess.authorization_url(self.auth_url)
270         return url, state, ''
271 
272     def get_query_id(self, querystring):
273         return querystring['state']
274 
275     def handle_auth_callback(self, url, state, unused_arg):
276         sess = self.get_auth_session(state=state)
277         sess.fetch_token(self.access_token_url,
278                          client_secret=self.api_secret,
279                          authorization_response=url)
280         return sess
281
```
--- 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
@@ -135,12 +135,16 @@
         """
         prefix = getattr(self, 'ratelimit_headers_prefix', None)
         if prefix:
-            limit = response.headers[prefix+'limit']
-            remaining = response.headers[prefix+'remaining']
-            reset = response.headers[prefix+'reset']
+            limit = response.headers.get(prefix+'limit')
+            remaining = response.headers.get(prefix+'remaining')
+            reset = response.headers.get(prefix+'reset')
+
             try:
                 limit, remaining, reset = int(limit), int(remaining), int(reset)
             except (TypeError, ValueError):
+                limit, remaining, reset = None, None, None
+
+            if None in (limit, remaining, reset):
                 d = dict(limit=limit, remaining=remaining, reset=reset)
                 log('Got weird rate headers from %s: %s' % (self.name, d))
             else:

{"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@@ -135,12 +135,16 @@\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+            limit = response.headers.get(prefix+'limit')\n+            remaining = response.headers.get(prefix+'remaining')\n+            reset = response.headers.get(prefix+'reset')\n+\n             try:\n                 limit, remaining, reset = int(limit), int(remaining), int(reset)\n             except (TypeError, ValueError):\n+                limit, remaining, reset = None, None, None\n+\n+            if None in (limit, remaining, reset):\n                 d = dict(limit=limit, remaining=remaining, reset=reset)\n                 log('Got weird rate headers from %s: %s' % (self.name, d))\n             else:\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        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"}], "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.get(prefix+'limit')\n            remaining = response.headers.get(prefix+'remaining')\n            reset = response.headers.get(prefix+'reset')\n\n            try:\n                limit, remaining, reset = int(limit), int(remaining), int(reset)\n            except (TypeError, ValueError):\n                limit, remaining, reset = None, None, None\n\n            if None in (limit, remaining, reset):\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"}]}