We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
move caching out of memory
There's one or two places in the app where we have an in-memory cache that will prevent us from scaling. I believe the two places are:
- /on/twitter/ - we store state in memory during the round-trip to Twitter for oauth
- /on/confirm.html - something about "connect tokens"

move caching out of memory
There's one or two places in the app where we have an in-memory cache that will prevent us from scaling. I believe the two places are:
- /on/twitter/ - we store state in memory during the round-trip to Twitter for oauth
- /on/confirm.html - something about "connect tokens"


--- 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/models/account_elsewhere.py`
Content:
```
1 from __future__ import absolute_import, division, print_function, unicode_literals
2 
3 import json
4 from urlparse import urlsplit, urlunsplit
5 import xml.etree.ElementTree as ET
6 
7 from aspen import Response
8 from postgres.orm import Model
9 from psycopg2 import IntegrityError
10 import xmltodict
11 
12 import gratipay
13 from gratipay.exceptions import ProblemChangingUsername
14 from gratipay.utils.username import safely_reserve_a_username
15 
16 
17 class UnknownAccountElsewhere(Exception): pass
18 
19 
20 class AccountElsewhere(Model):
21 
22     typname = "elsewhere_with_participant"
23 
24     def __init__(self, *args, **kwargs):
25         super(AccountElsewhere, self).__init__(*args, **kwargs)
26         self.platform_data = getattr(self.platforms, self.platform)
27 
28 
29     # Constructors
30     # ============
31 
32     @classmethod
33     def from_user_id(cls, platform, user_id):
34         """Return an existing AccountElsewhere based on platform and user_id.
35         """
36         return cls._from_thing('user_id', platform, user_id)
37 
38     @classmethod
39     def from_user_name(cls, platform, user_name):
40         """Return an existing AccountElsewhere based on platform and user_name.
41         """
42         return cls._from_thing('user_name', platform, user_name)
43 
44     @classmethod
45     def _from_thing(cls, thing, platform, value):
46         assert thing in ('user_id', 'user_name')
47         exception = UnknownAccountElsewhere(thing, platform, value)
48         return cls.db.one("""
49 
50             SELECT elsewhere.*::elsewhere_with_participant
51               FROM elsewhere
52              WHERE platform = %s
53                AND {} = %s
54 
55         """.format(thing), (platform, value), default=exception)
56 
57     @classmethod
58     def get_many(cls, platform, user_infos):
59         accounts = cls.db.all("""\
60 
61             SELECT elsewhere.*::elsewhere_with_participant
62               FROM elsewhere
63              WHERE platform = %s
64                AND user_id = any(%s)
65 
66         """, (platform, [i.user_id for i in user_infos]))
67         found_user_ids = set(a.user_id for a in accounts)
68         for i in user_infos:
69             if i.user_id not in found_user_ids:
70                 accounts.append(cls.upsert(i))
71         return accounts
72 
73     @classmethod
74     def upsert(cls, i):
75         """Insert or update a user's info.
76         """
77 
78         # Clean up avatar_url
79         if i.avatar_url:
80             scheme, netloc, path, query, fragment = urlsplit(i.avatar_url)
81             fragment = ''
82             if netloc.endswith('githubusercontent.com') or \
83                netloc.endswith('gravatar.com'):
84                 query = 's=128'
85             i.avatar_url = urlunsplit((scheme, netloc, path, query, fragment))
86 
87         # Serialize extra_info
88         if isinstance(i.extra_info, ET.Element):
89             i.extra_info = xmltodict.parse(ET.tostring(i.extra_info))
90         i.extra_info = json.dumps(i.extra_info)
91 
92         cols, vals = zip(*i.__dict__.items())
93         cols = ', '.join(cols)
94         placeholders = ', '.join(['%s']*len(vals))
95 
96         try:
97             # Try to insert the account
98             # We do this with a transaction so that if the insert fails, the
99             # participant we reserved for them is rolled back as well.
100             with cls.db.get_cursor() as cursor:
101                 username = safely_reserve_a_username(cursor)
102                 cursor.execute("""
103                     INSERT INTO elsewhere
104                                 (participant, {0})
105                          VALUES (%s, {1})
106                 """.format(cols, placeholders), (username,)+vals)
107                 # Propagate elsewhere.is_team to participants.number
108                 if i.is_team:
109                     cursor.execute("""
110                         UPDATE participants
111                            SET number = 'plural'::participant_number
112                          WHERE username = %s
113                     """, (username,))
114         except IntegrityError:
115             # The account is already in the DB, update it instead
116             username = cls.db.one("""
117                 UPDATE elsewhere
118                    SET ({0}) = ({1})
119                  WHERE platform=%s AND user_id=%s
120              RETURNING participant
121             """.format(cols, placeholders), vals+(i.platform, i.user_id))
122             if not username:
123                 raise
124 
125         # Return account after propagating avatar_url to participant
126         account = AccountElsewhere.from_user_id(i.platform, i.user_id)
127         account.participant.update_avatar()
128         return account
129 
130 
131     # Random Stuff
132     # ============
133 
134     def get_auth_session(self):
135         if not self.token:
136             return
137         params = dict(token=self.token)
138         if 'refresh_token' in self.token:
139             params['token_updater'] = self.save_token
140         return self.platform_data.get_auth_session(**params)
141 
142     @property
143     def gratipay_url(self):
144         scheme = gratipay.canonical_scheme
145         host = gratipay.canonical_host
146         platform = self.platform
147         user_name = self.user_name
148         return "{scheme}://{host}/on/{platform}/{user_name}/".format(**locals())
149 
150     @property
151     def html_url(self):
152         return self.platform_data.account_url.format(
153             user_id=self.user_id,
154             user_name=self.user_name,
155             platform_data=self.platform_data
156         )
157 
158     def opt_in(self, desired_username):
159         """Given a desired username, return a User object.
160         """
161         from gratipay.security.user import User
162         user = User.from_username(self.participant.username)
163         assert not user.ANON, self.participant  # sanity check
164         if self.participant.is_claimed:
165             newly_claimed = False
166         else:
167             newly_claimed = True
168             user.participant.set_as_claimed()
169             user.participant.notify_patrons(self)
170             try:
171                 user.participant.change_username(desired_username)
172             except ProblemChangingUsername:
173                 pass
174         if user.participant.is_closed:
175             user.participant.update_is_closed(False)
176         return user, newly_claimed
177 
178     def save_token(self, token):
179         """Saves the given access token in the database.
180         """
181         self.db.run("""
182             UPDATE elsewhere
183                SET token = %s
184              WHERE id=%s
185         """, (token, self.id))
186         self.set_attributes(token=token)
187 
188 
189 def get_account_elsewhere(website, state):
190     path = state['request'].line.uri.path
191     platform = getattr(website.platforms, path['platform'], None)
192     if platform is None:
193         raise Response(404)
194     user_name = path['user_name']
195     try:
196         account = AccountElsewhere.from_user_name(platform.name, user_name)
197     except UnknownAccountElsewhere:
198         account = None
199     if not account:
200         try:
201             user_info = platform.get_user_info(user_name)
202         except Response as r:
203             if r.code == 404:
204                 _ = state['_']
205                 err = _("There doesn't seem to be a user named {0} on {1}.",
206                         user_name, platform.display_name)
207                 raise Response(404, err)
208             raise
209         account = AccountElsewhere.upsert(user_info)
210     return platform, account
211
```
--- 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/models/account_elsewhere.py b/gratipay/models/account_elsewhere.py
--- a/gratipay/models/account_elsewhere.py
+++ b/gratipay/models/account_elsewhere.py
@@ -1,19 +1,26 @@
 from __future__ import absolute_import, division, print_function, unicode_literals
 
+from datetime import timedelta
 import json
 from urlparse import urlsplit, urlunsplit
+import uuid
 import xml.etree.ElementTree as ET
 
 from aspen import Response
+from aspen.utils import utcnow
 from postgres.orm import Model
 from psycopg2 import IntegrityError
 import xmltodict
 
 import gratipay
 from gratipay.exceptions import ProblemChangingUsername
+from gratipay.security.crypto import constant_time_compare
 from gratipay.utils.username import safely_reserve_a_username
 
 
+CONNECT_TOKEN_TIMEOUT = timedelta(hours=24)
+
+
 class UnknownAccountElsewhere(Exception): pass
 
 
@@ -29,6 +36,16 @@
     # Constructors
     # ============
 
+    @classmethod
+    def from_id(cls, id):
+        """Return an existing AccountElsewhere based on id.
+        """
+        return cls.db.one("""
+            SELECT elsewhere.*::elsewhere_with_participant
+              FROM elsewhere
+             WHERE id = %s
+        """, (id,))
+
     @classmethod
     def from_user_id(cls, platform, user_id):
         """Return an existing AccountElsewhere based on platform and user_id.
@@ -128,6 +145,31 @@
         return account
 
 
+    # Connect tokens
+    # ==============
+
+    def check_connect_token(self, token):
+        return (
+            self.connect_token and
+            constant_time_compare(self.connect_token, token) and
+            self.connect_expires > utcnow()
+        )
+
+    def make_connect_token(self):
+        token = uuid.uuid4().hex
+        expires = utcnow() + CONNECT_TOKEN_TIMEOUT
+        return self.save_connect_token(token, expires)
+
+    def save_connect_token(self, token, expires):
+        return self.db.one("""
+            UPDATE elsewhere
+               SET connect_token = %s
+                 , connect_expires = %s
+             WHERE id = %s
+         RETURNING connect_token, connect_expires
+        """, (token, expires, self.id))
+
+
     # Random Stuff
     # ============

{"golden_diff": "diff --git a/gratipay/models/account_elsewhere.py b/gratipay/models/account_elsewhere.py\n--- a/gratipay/models/account_elsewhere.py\n+++ b/gratipay/models/account_elsewhere.py\n@@ -1,19 +1,26 @@\n from __future__ import absolute_import, division, print_function, unicode_literals\n \n+from datetime import timedelta\n import json\n from urlparse import urlsplit, urlunsplit\n+import uuid\n import xml.etree.ElementTree as ET\n \n from aspen import Response\n+from aspen.utils import utcnow\n from postgres.orm import Model\n from psycopg2 import IntegrityError\n import xmltodict\n \n import gratipay\n from gratipay.exceptions import ProblemChangingUsername\n+from gratipay.security.crypto import constant_time_compare\n from gratipay.utils.username import safely_reserve_a_username\n \n \n+CONNECT_TOKEN_TIMEOUT = timedelta(hours=24)\n+\n+\n class UnknownAccountElsewhere(Exception): pass\n \n \n@@ -29,6 +36,16 @@\n     # Constructors\n     # ============\n \n+    @classmethod\n+    def from_id(cls, id):\n+        \"\"\"Return an existing AccountElsewhere based on id.\n+        \"\"\"\n+        return cls.db.one(\"\"\"\n+            SELECT elsewhere.*::elsewhere_with_participant\n+              FROM elsewhere\n+             WHERE id = %s\n+        \"\"\", (id,))\n+\n     @classmethod\n     def from_user_id(cls, platform, user_id):\n         \"\"\"Return an existing AccountElsewhere based on platform and user_id.\n@@ -128,6 +145,31 @@\n         return account\n \n \n+    # Connect tokens\n+    # ==============\n+\n+    def check_connect_token(self, token):\n+        return (\n+            self.connect_token and\n+            constant_time_compare(self.connect_token, token) and\n+            self.connect_expires > utcnow()\n+        )\n+\n+    def make_connect_token(self):\n+        token = uuid.uuid4().hex\n+        expires = utcnow() + CONNECT_TOKEN_TIMEOUT\n+        return self.save_connect_token(token, expires)\n+\n+    def save_connect_token(self, token, expires):\n+        return self.db.one(\"\"\"\n+            UPDATE elsewhere\n+               SET connect_token = %s\n+                 , connect_expires = %s\n+             WHERE id = %s\n+         RETURNING connect_token, connect_expires\n+        \"\"\", (token, expires, self.id))\n+\n+\n     # Random Stuff\n     # ============\n", "issue": "move caching out of memory\nThere's one or two places in the app where we have an in-memory cache that will prevent us from scaling. I believe the two places are:\n- /on/twitter/ - we store state in memory during the round-trip to Twitter for oauth\n- /on/confirm.html - something about \"connect tokens\"\n\nmove caching out of memory\nThere's one or two places in the app where we have an in-memory cache that will prevent us from scaling. I believe the two places are:\n- /on/twitter/ - we store state in memory during the round-trip to Twitter for oauth\n- /on/confirm.html - something about \"connect tokens\"\n\n", "before_files": [{"content": "from __future__ import absolute_import, division, print_function, unicode_literals\n\nimport json\nfrom urlparse import urlsplit, urlunsplit\nimport xml.etree.ElementTree as ET\n\nfrom aspen import Response\nfrom postgres.orm import Model\nfrom psycopg2 import IntegrityError\nimport xmltodict\n\nimport gratipay\nfrom gratipay.exceptions import ProblemChangingUsername\nfrom gratipay.utils.username import safely_reserve_a_username\n\n\nclass UnknownAccountElsewhere(Exception): pass\n\n\nclass AccountElsewhere(Model):\n\n    typname = \"elsewhere_with_participant\"\n\n    def __init__(self, *args, **kwargs):\n        super(AccountElsewhere, self).__init__(*args, **kwargs)\n        self.platform_data = getattr(self.platforms, self.platform)\n\n\n    # Constructors\n    # ============\n\n    @classmethod\n    def from_user_id(cls, platform, user_id):\n        \"\"\"Return an existing AccountElsewhere based on platform and user_id.\n        \"\"\"\n        return cls._from_thing('user_id', platform, user_id)\n\n    @classmethod\n    def from_user_name(cls, platform, user_name):\n        \"\"\"Return an existing AccountElsewhere based on platform and user_name.\n        \"\"\"\n        return cls._from_thing('user_name', platform, user_name)\n\n    @classmethod\n    def _from_thing(cls, thing, platform, value):\n        assert thing in ('user_id', 'user_name')\n        exception = UnknownAccountElsewhere(thing, platform, value)\n        return cls.db.one(\"\"\"\n\n            SELECT elsewhere.*::elsewhere_with_participant\n              FROM elsewhere\n             WHERE platform = %s\n               AND {} = %s\n\n        \"\"\".format(thing), (platform, value), default=exception)\n\n    @classmethod\n    def get_many(cls, platform, user_infos):\n        accounts = cls.db.all(\"\"\"\\\n\n            SELECT elsewhere.*::elsewhere_with_participant\n              FROM elsewhere\n             WHERE platform = %s\n               AND user_id = any(%s)\n\n        \"\"\", (platform, [i.user_id for i in user_infos]))\n        found_user_ids = set(a.user_id for a in accounts)\n        for i in user_infos:\n            if i.user_id not in found_user_ids:\n                accounts.append(cls.upsert(i))\n        return accounts\n\n    @classmethod\n    def upsert(cls, i):\n        \"\"\"Insert or update a user's info.\n        \"\"\"\n\n        # Clean up avatar_url\n        if i.avatar_url:\n            scheme, netloc, path, query, fragment = urlsplit(i.avatar_url)\n            fragment = ''\n            if netloc.endswith('githubusercontent.com') or \\\n               netloc.endswith('gravatar.com'):\n                query = 's=128'\n            i.avatar_url = urlunsplit((scheme, netloc, path, query, fragment))\n\n        # Serialize extra_info\n        if isinstance(i.extra_info, ET.Element):\n            i.extra_info = xmltodict.parse(ET.tostring(i.extra_info))\n        i.extra_info = json.dumps(i.extra_info)\n\n        cols, vals = zip(*i.__dict__.items())\n        cols = ', '.join(cols)\n        placeholders = ', '.join(['%s']*len(vals))\n\n        try:\n            # Try to insert the account\n            # We do this with a transaction so that if the insert fails, the\n            # participant we reserved for them is rolled back as well.\n            with cls.db.get_cursor() as cursor:\n                username = safely_reserve_a_username(cursor)\n                cursor.execute(\"\"\"\n                    INSERT INTO elsewhere\n                                (participant, {0})\n                         VALUES (%s, {1})\n                \"\"\".format(cols, placeholders), (username,)+vals)\n                # Propagate elsewhere.is_team to participants.number\n                if i.is_team:\n                    cursor.execute(\"\"\"\n                        UPDATE participants\n                           SET number = 'plural'::participant_number\n                         WHERE username = %s\n                    \"\"\", (username,))\n        except IntegrityError:\n            # The account is already in the DB, update it instead\n            username = cls.db.one(\"\"\"\n                UPDATE elsewhere\n                   SET ({0}) = ({1})\n                 WHERE platform=%s AND user_id=%s\n             RETURNING participant\n            \"\"\".format(cols, placeholders), vals+(i.platform, i.user_id))\n            if not username:\n                raise\n\n        # Return account after propagating avatar_url to participant\n        account = AccountElsewhere.from_user_id(i.platform, i.user_id)\n        account.participant.update_avatar()\n        return account\n\n\n    # Random Stuff\n    # ============\n\n    def get_auth_session(self):\n        if not self.token:\n            return\n        params = dict(token=self.token)\n        if 'refresh_token' in self.token:\n            params['token_updater'] = self.save_token\n        return self.platform_data.get_auth_session(**params)\n\n    @property\n    def gratipay_url(self):\n        scheme = gratipay.canonical_scheme\n        host = gratipay.canonical_host\n        platform = self.platform\n        user_name = self.user_name\n        return \"{scheme}://{host}/on/{platform}/{user_name}/\".format(**locals())\n\n    @property\n    def html_url(self):\n        return self.platform_data.account_url.format(\n            user_id=self.user_id,\n            user_name=self.user_name,\n            platform_data=self.platform_data\n        )\n\n    def opt_in(self, desired_username):\n        \"\"\"Given a desired username, return a User object.\n        \"\"\"\n        from gratipay.security.user import User\n        user = User.from_username(self.participant.username)\n        assert not user.ANON, self.participant  # sanity check\n        if self.participant.is_claimed:\n            newly_claimed = False\n        else:\n            newly_claimed = True\n            user.participant.set_as_claimed()\n            user.participant.notify_patrons(self)\n            try:\n                user.participant.change_username(desired_username)\n            except ProblemChangingUsername:\n                pass\n        if user.participant.is_closed:\n            user.participant.update_is_closed(False)\n        return user, newly_claimed\n\n    def save_token(self, token):\n        \"\"\"Saves the given access token in the database.\n        \"\"\"\n        self.db.run(\"\"\"\n            UPDATE elsewhere\n               SET token = %s\n             WHERE id=%s\n        \"\"\", (token, self.id))\n        self.set_attributes(token=token)\n\n\ndef get_account_elsewhere(website, state):\n    path = state['request'].line.uri.path\n    platform = getattr(website.platforms, path['platform'], None)\n    if platform is None:\n        raise Response(404)\n    user_name = path['user_name']\n    try:\n        account = AccountElsewhere.from_user_name(platform.name, user_name)\n    except UnknownAccountElsewhere:\n        account = None\n    if not account:\n        try:\n            user_info = platform.get_user_info(user_name)\n        except Response as r:\n            if r.code == 404:\n                _ = state['_']\n                err = _(\"There doesn't seem to be a user named {0} on {1}.\",\n                        user_name, platform.display_name)\n                raise Response(404, err)\n            raise\n        account = AccountElsewhere.upsert(user_info)\n    return platform, account\n", "path": "gratipay/models/account_elsewhere.py"}], "after_files": [{"content": "from __future__ import absolute_import, division, print_function, unicode_literals\n\nfrom datetime import timedelta\nimport json\nfrom urlparse import urlsplit, urlunsplit\nimport uuid\nimport xml.etree.ElementTree as ET\n\nfrom aspen import Response\nfrom aspen.utils import utcnow\nfrom postgres.orm import Model\nfrom psycopg2 import IntegrityError\nimport xmltodict\n\nimport gratipay\nfrom gratipay.exceptions import ProblemChangingUsername\nfrom gratipay.security.crypto import constant_time_compare\nfrom gratipay.utils.username import safely_reserve_a_username\n\n\nCONNECT_TOKEN_TIMEOUT = timedelta(hours=24)\n\n\nclass UnknownAccountElsewhere(Exception): pass\n\n\nclass AccountElsewhere(Model):\n\n    typname = \"elsewhere_with_participant\"\n\n    def __init__(self, *args, **kwargs):\n        super(AccountElsewhere, self).__init__(*args, **kwargs)\n        self.platform_data = getattr(self.platforms, self.platform)\n\n\n    # Constructors\n    # ============\n\n    @classmethod\n    def from_id(cls, id):\n        \"\"\"Return an existing AccountElsewhere based on id.\n        \"\"\"\n        return cls.db.one(\"\"\"\n            SELECT elsewhere.*::elsewhere_with_participant\n              FROM elsewhere\n             WHERE id = %s\n        \"\"\", (id,))\n\n    @classmethod\n    def from_user_id(cls, platform, user_id):\n        \"\"\"Return an existing AccountElsewhere based on platform and user_id.\n        \"\"\"\n        return cls._from_thing('user_id', platform, user_id)\n\n    @classmethod\n    def from_user_name(cls, platform, user_name):\n        \"\"\"Return an existing AccountElsewhere based on platform and user_name.\n        \"\"\"\n        return cls._from_thing('user_name', platform, user_name)\n\n    @classmethod\n    def _from_thing(cls, thing, platform, value):\n        assert thing in ('user_id', 'user_name')\n        exception = UnknownAccountElsewhere(thing, platform, value)\n        return cls.db.one(\"\"\"\n\n            SELECT elsewhere.*::elsewhere_with_participant\n              FROM elsewhere\n             WHERE platform = %s\n               AND {} = %s\n\n        \"\"\".format(thing), (platform, value), default=exception)\n\n    @classmethod\n    def get_many(cls, platform, user_infos):\n        accounts = cls.db.all(\"\"\"\\\n\n            SELECT elsewhere.*::elsewhere_with_participant\n              FROM elsewhere\n             WHERE platform = %s\n               AND user_id = any(%s)\n\n        \"\"\", (platform, [i.user_id for i in user_infos]))\n        found_user_ids = set(a.user_id for a in accounts)\n        for i in user_infos:\n            if i.user_id not in found_user_ids:\n                accounts.append(cls.upsert(i))\n        return accounts\n\n    @classmethod\n    def upsert(cls, i):\n        \"\"\"Insert or update a user's info.\n        \"\"\"\n\n        # Clean up avatar_url\n        if i.avatar_url:\n            scheme, netloc, path, query, fragment = urlsplit(i.avatar_url)\n            fragment = ''\n            if netloc.endswith('githubusercontent.com') or \\\n               netloc.endswith('gravatar.com'):\n                query = 's=128'\n            i.avatar_url = urlunsplit((scheme, netloc, path, query, fragment))\n\n        # Serialize extra_info\n        if isinstance(i.extra_info, ET.Element):\n            i.extra_info = xmltodict.parse(ET.tostring(i.extra_info))\n        i.extra_info = json.dumps(i.extra_info)\n\n        cols, vals = zip(*i.__dict__.items())\n        cols = ', '.join(cols)\n        placeholders = ', '.join(['%s']*len(vals))\n\n        try:\n            # Try to insert the account\n            # We do this with a transaction so that if the insert fails, the\n            # participant we reserved for them is rolled back as well.\n            with cls.db.get_cursor() as cursor:\n                username = safely_reserve_a_username(cursor)\n                cursor.execute(\"\"\"\n                    INSERT INTO elsewhere\n                                (participant, {0})\n                         VALUES (%s, {1})\n                \"\"\".format(cols, placeholders), (username,)+vals)\n                # Propagate elsewhere.is_team to participants.number\n                if i.is_team:\n                    cursor.execute(\"\"\"\n                        UPDATE participants\n                           SET number = 'plural'::participant_number\n                         WHERE username = %s\n                    \"\"\", (username,))\n        except IntegrityError:\n            # The account is already in the DB, update it instead\n            username = cls.db.one(\"\"\"\n                UPDATE elsewhere\n                   SET ({0}) = ({1})\n                 WHERE platform=%s AND user_id=%s\n             RETURNING participant\n            \"\"\".format(cols, placeholders), vals+(i.platform, i.user_id))\n            if not username:\n                raise\n\n        # Return account after propagating avatar_url to participant\n        account = AccountElsewhere.from_user_id(i.platform, i.user_id)\n        account.participant.update_avatar()\n        return account\n\n\n    # Connect tokens\n    # ==============\n\n    def check_connect_token(self, token):\n        return (\n            self.connect_token and\n            constant_time_compare(self.connect_token, token) and\n            self.connect_expires > utcnow()\n        )\n\n    def make_connect_token(self):\n        token = uuid.uuid4().hex\n        expires = utcnow() + CONNECT_TOKEN_TIMEOUT\n        return self.save_connect_token(token, expires)\n\n    def save_connect_token(self, token, expires):\n        return self.db.one(\"\"\"\n            UPDATE elsewhere\n               SET connect_token = %s\n                 , connect_expires = %s\n             WHERE id = %s\n         RETURNING connect_token, connect_expires\n        \"\"\", (token, expires, self.id))\n\n\n    # Random Stuff\n    # ============\n\n    def get_auth_session(self):\n        if not self.token:\n            return\n        params = dict(token=self.token)\n        if 'refresh_token' in self.token:\n            params['token_updater'] = self.save_token\n        return self.platform_data.get_auth_session(**params)\n\n    @property\n    def gratipay_url(self):\n        scheme = gratipay.canonical_scheme\n        host = gratipay.canonical_host\n        platform = self.platform\n        user_name = self.user_name\n        return \"{scheme}://{host}/on/{platform}/{user_name}/\".format(**locals())\n\n    @property\n    def html_url(self):\n        return self.platform_data.account_url.format(\n            user_id=self.user_id,\n            user_name=self.user_name,\n            platform_data=self.platform_data\n        )\n\n    def opt_in(self, desired_username):\n        \"\"\"Given a desired username, return a User object.\n        \"\"\"\n        from gratipay.security.user import User\n        user = User.from_username(self.participant.username)\n        assert not user.ANON, self.participant  # sanity check\n        if self.participant.is_claimed:\n            newly_claimed = False\n        else:\n            newly_claimed = True\n            user.participant.set_as_claimed()\n            user.participant.notify_patrons(self)\n            try:\n                user.participant.change_username(desired_username)\n            except ProblemChangingUsername:\n                pass\n        if user.participant.is_closed:\n            user.participant.update_is_closed(False)\n        return user, newly_claimed\n\n    def save_token(self, token):\n        \"\"\"Saves the given access token in the database.\n        \"\"\"\n        self.db.run(\"\"\"\n            UPDATE elsewhere\n               SET token = %s\n             WHERE id=%s\n        \"\"\", (token, self.id))\n        self.set_attributes(token=token)\n\n\ndef get_account_elsewhere(website, state):\n    path = state['request'].line.uri.path\n    platform = getattr(website.platforms, path['platform'], None)\n    if platform is None:\n        raise Response(404)\n    user_name = path['user_name']\n    try:\n        account = AccountElsewhere.from_user_name(platform.name, user_name)\n    except UnknownAccountElsewhere:\n        account = None\n    if not account:\n        try:\n            user_info = platform.get_user_info(user_name)\n        except Response as r:\n            if r.code == 404:\n                _ = state['_']\n                err = _(\"There doesn't seem to be a user named {0} on {1}.\",\n                        user_name, platform.display_name)\n                raise Response(404, err)\n            raise\n        account = AccountElsewhere.upsert(user_info)\n    return platform, account\n", "path": "gratipay/models/account_elsewhere.py"}]}