We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
LinuxFr issue
The link to my LinuxFr account is broken because of case.
https://liberapay.com/Moul/

LinuxFr issue
The link to my LinuxFr account is broken because of case.
https://liberapay.com/Moul/


--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `liberapay/models/account_elsewhere.py`
Content:
```
1 from __future__ import absolute_import, division, print_function, unicode_literals
2 
3 from datetime import timedelta
4 import json
5 import uuid
6 import xml.etree.ElementTree as ET
7 
8 from six.moves.urllib.parse import urlsplit, urlunsplit
9 
10 from aspen import Response
11 from aspen.utils import utcnow
12 from postgres.orm import Model
13 from psycopg2 import IntegrityError
14 import xmltodict
15 
16 from liberapay.constants import AVATAR_QUERY
17 from liberapay.security.crypto import constant_time_compare
18 from liberapay.website import website
19 
20 
21 CONNECT_TOKEN_TIMEOUT = timedelta(hours=24)
22 
23 
24 class UnknownAccountElsewhere(Exception): pass
25 
26 
27 class AccountElsewhere(Model):
28 
29     typname = "elsewhere_with_participant"
30 
31     def __init__(self, *args, **kwargs):
32         super(AccountElsewhere, self).__init__(*args, **kwargs)
33         self.platform_data = getattr(website.platforms, self.platform)
34 
35 
36     # Constructors
37     # ============
38 
39     @classmethod
40     def from_id(cls, id):
41         """Return an existing AccountElsewhere based on id.
42         """
43         return cls.db.one("""
44             SELECT elsewhere.*::elsewhere_with_participant
45               FROM elsewhere
46              WHERE id = %s
47         """, (id,))
48 
49     @classmethod
50     def from_user_id(cls, platform, user_id):
51         """Return an existing AccountElsewhere based on platform and user_id.
52         """
53         return cls._from_thing('user_id', platform, user_id)
54 
55     @classmethod
56     def from_user_name(cls, platform, user_name):
57         """Return an existing AccountElsewhere based on platform and user_name.
58         """
59         return cls._from_thing('user_name', platform, user_name)
60 
61     @classmethod
62     def _from_thing(cls, thing, platform, value):
63         assert thing in ('user_id', 'user_name')
64         if thing == 'user_name':
65             thing = 'lower(user_name)'
66             value = value.lower()
67         exception = UnknownAccountElsewhere(thing, platform, value)
68         return cls.db.one("""
69 
70             SELECT elsewhere.*::elsewhere_with_participant
71               FROM elsewhere
72              WHERE platform = %s
73                AND {} = %s
74 
75         """.format(thing), (platform, value), default=exception)
76 
77     @classmethod
78     def get_many(cls, platform, user_infos):
79         accounts = []
80         found = cls.db.all("""\
81 
82             SELECT elsewhere.*::elsewhere_with_participant
83               FROM elsewhere
84              WHERE platform = %s
85                AND user_id = any(%s)
86 
87         """, (platform, [i.user_id for i in user_infos]))
88         found = {a.user_id: a for a in found}
89         for i in user_infos:
90             if i.user_id in found:
91                 accounts.append(found[i.user_id])
92             else:
93                 accounts.append(cls.upsert(i))
94         return accounts
95 
96     @classmethod
97     def upsert(cls, i):
98         """Insert or update a user's info.
99         """
100 
101         # Clean up avatar_url
102         if i.avatar_url:
103             scheme, netloc, path, query, fragment = urlsplit(i.avatar_url)
104             fragment = ''
105             if netloc.endswith('githubusercontent.com') or \
106                netloc.endswith('gravatar.com') or \
107                netloc.endswith('libravatar.org'):
108                 query = AVATAR_QUERY
109             i.avatar_url = urlunsplit((scheme, netloc, path, query, fragment))
110 
111         # Serialize extra_info
112         if isinstance(i.extra_info, ET.Element):
113             i.extra_info = xmltodict.parse(ET.tostring(i.extra_info))
114         i.extra_info = json.dumps(i.extra_info)
115 
116         cols, vals = zip(*i.__dict__.items())
117         cols = ', '.join(cols)
118         placeholders = ', '.join(['%s']*len(vals))
119 
120         try:
121             # Try to insert the account
122             # We do this with a transaction so that if the insert fails, the
123             # participant we reserved for them is rolled back as well.
124             with cls.db.get_cursor() as cursor:
125                 id = cursor.one("""
126                     INSERT INTO participants DEFAULT VALUES RETURNING id
127                 """)
128                 account = cursor.one("""
129                     INSERT INTO elsewhere
130                                 (participant, {0})
131                          VALUES (%s, {1})
132                       RETURNING elsewhere.*::elsewhere_with_participant
133                 """.format(cols, placeholders), (id,)+vals)
134         except IntegrityError:
135             # The account is already in the DB, update it instead
136             account = cls.db.one("""
137                 UPDATE elsewhere
138                    SET ({0}) = ({1})
139                  WHERE platform=%s AND user_id=%s
140              RETURNING elsewhere.*::elsewhere_with_participant
141             """.format(cols, placeholders), vals+(i.platform, i.user_id))
142             if not account:
143                 raise
144 
145         # Return account after propagating avatar_url to participant
146         account.participant.update_avatar()
147         return account
148 
149 
150     # Connect tokens
151     # ==============
152 
153     def check_connect_token(self, token):
154         return (
155             self.connect_token and
156             constant_time_compare(self.connect_token, token) and
157             self.connect_expires > utcnow()
158         )
159 
160     def make_connect_token(self):
161         token = uuid.uuid4().hex
162         expires = utcnow() + CONNECT_TOKEN_TIMEOUT
163         return self.save_connect_token(token, expires)
164 
165     def save_connect_token(self, token, expires):
166         return self.db.one("""
167             UPDATE elsewhere
168                SET connect_token = %s
169                  , connect_expires = %s
170              WHERE id = %s
171          RETURNING connect_token, connect_expires
172         """, (token, expires, self.id))
173 
174 
175     # Random Stuff
176     # ============
177 
178     def get_auth_session(self):
179         if not self.token:
180             return
181         params = dict(token=self.token)
182         if 'refresh_token' in self.token:
183             params['token_updater'] = self.save_token
184         return self.platform_data.get_auth_session(**params)
185 
186     @property
187     def liberapay_slug(self):
188         return self.user_name or ('~' + self.user_id)
189 
190     @property
191     def liberapay_url(self):
192         scheme = website.canonical_scheme
193         host = website.canonical_host
194         platform = self.platform
195         slug = self.liberapay_slug
196         return "{scheme}://{host}/on/{platform}/{slug}/".format(**locals())
197 
198     @property
199     def html_url(self):
200         return self.platform_data.account_url.format(
201             user_id=self.user_id,
202             user_name=self.user_name,
203             platform_data=self.platform_data
204         )
205 
206     @property
207     def friendly_name(self):
208         if getattr(self.platform, 'optional_user_name', False):
209             return self.display_name or self.user_name or self.user_id
210         else:
211             return self.user_name or self.display_name or self.user_id
212 
213     @property
214     def friendly_name_long(self):
215         r = self.friendly_name
216         display_name = self.display_name
217         if display_name and display_name != r:
218             return '%s (%s)' % (r, display_name)
219         user_name = self.user_name
220         if user_name and user_name != r:
221             return '%s (%s)' % (r, user_name)
222         return r
223 
224     def save_token(self, token):
225         """Saves the given access token in the database.
226         """
227         self.db.run("""
228             UPDATE elsewhere
229                SET token = %s
230              WHERE id=%s
231         """, (token, self.id))
232         self.set_attributes(token=token)
233 
234 
235 def get_account_elsewhere(website, state, api_lookup=True):
236     path = state['request'].line.uri.path
237     platform = getattr(website.platforms, path['platform'], None)
238     if platform is None:
239         raise Response(404)
240     uid = path['user_name']
241     if uid[:1] == '~':
242         key = 'user_id'
243         uid = uid[1:]
244     else:
245         key = 'user_name'
246         if uid[:1] == '@':
247             uid = uid[1:]
248     try:
249         account = AccountElsewhere._from_thing(key, platform.name, uid)
250     except UnknownAccountElsewhere:
251         account = None
252     if not account:
253         if not api_lookup:
254             raise Response(404)
255         try:
256             user_info = platform.get_user_info(key, uid)
257         except Response as r:
258             if r.code == 404:
259                 _ = state['_']
260                 err = _("There doesn't seem to be a user named {0} on {1}.",
261                         uid, platform.display_name)
262                 raise Response(404, err)
263             raise
264         account = AccountElsewhere.upsert(user_info)
265     return platform, account
266
```

Path: `liberapay/elsewhere/linuxfr.py`
Content:
```
1 from __future__ import absolute_import, division, print_function, unicode_literals
2 
3 from liberapay.elsewhere._base import PlatformOAuth2
4 from liberapay.elsewhere._extractors import key
5 
6 
7 class LinuxFr(PlatformOAuth2):
8 
9     # Platform attributes
10     name = 'linuxfr'
11     display_name = 'LinuxFr.org'
12     account_url = 'https://linuxfr.org/users/{user_name}'
13 
14     # Auth attributes
15     # LinuxFr uses https://github.com/doorkeeper-gem/doorkeeper
16     auth_url = 'https://linuxfr.org/api/oauth/authorize'
17     access_token_url = 'https://linuxfr.org/api/oauth/token'
18 
19     # API attributes
20     # https://linuxfr.org/developpeur
21     api_format = 'json'
22     api_url = 'https://linuxfr.org/api/v1'
23     api_user_self_info_path = '/me'
24 
25     # User info extractors
26     x_user_name = key('login')
27     x_email = key('email')
28
```
--- 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/liberapay/elsewhere/linuxfr.py b/liberapay/elsewhere/linuxfr.py
--- a/liberapay/elsewhere/linuxfr.py
+++ b/liberapay/elsewhere/linuxfr.py
@@ -9,7 +9,7 @@
     # Platform attributes
     name = 'linuxfr'
     display_name = 'LinuxFr.org'
-    account_url = 'https://linuxfr.org/users/{user_name}'
+    account_url = 'https://linuxfr.org/users/{user_name_lower}'
 
     # Auth attributes
     # LinuxFr uses https://github.com/doorkeeper-gem/doorkeeper
diff --git a/liberapay/models/account_elsewhere.py b/liberapay/models/account_elsewhere.py
--- a/liberapay/models/account_elsewhere.py
+++ b/liberapay/models/account_elsewhere.py
@@ -200,6 +200,7 @@
         return self.platform_data.account_url.format(
             user_id=self.user_id,
             user_name=self.user_name,
+            user_name_lower=self.user_name.lower(),
             platform_data=self.platform_data
         )

{"golden_diff": "diff --git a/liberapay/elsewhere/linuxfr.py b/liberapay/elsewhere/linuxfr.py\n--- a/liberapay/elsewhere/linuxfr.py\n+++ b/liberapay/elsewhere/linuxfr.py\n@@ -9,7 +9,7 @@\n     # Platform attributes\n     name = 'linuxfr'\n     display_name = 'LinuxFr.org'\n-    account_url = 'https://linuxfr.org/users/{user_name}'\n+    account_url = 'https://linuxfr.org/users/{user_name_lower}'\n \n     # Auth attributes\n     # LinuxFr uses https://github.com/doorkeeper-gem/doorkeeper\ndiff --git a/liberapay/models/account_elsewhere.py b/liberapay/models/account_elsewhere.py\n--- a/liberapay/models/account_elsewhere.py\n+++ b/liberapay/models/account_elsewhere.py\n@@ -200,6 +200,7 @@\n         return self.platform_data.account_url.format(\n             user_id=self.user_id,\n             user_name=self.user_name,\n+            user_name_lower=self.user_name.lower(),\n             platform_data=self.platform_data\n         )\n", "issue": "LinuxFr issue\nThe link to my LinuxFr account is broken because of case.\nhttps://liberapay.com/Moul/\n\nLinuxFr issue\nThe link to my LinuxFr account is broken because of case.\nhttps://liberapay.com/Moul/\n\n", "before_files": [{"content": "from __future__ import absolute_import, division, print_function, unicode_literals\n\nfrom datetime import timedelta\nimport json\nimport uuid\nimport xml.etree.ElementTree as ET\n\nfrom six.moves.urllib.parse import urlsplit, urlunsplit\n\nfrom aspen import Response\nfrom aspen.utils import utcnow\nfrom postgres.orm import Model\nfrom psycopg2 import IntegrityError\nimport xmltodict\n\nfrom liberapay.constants import AVATAR_QUERY\nfrom liberapay.security.crypto import constant_time_compare\nfrom liberapay.website import website\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(website.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        if thing == 'user_name':\n            thing = 'lower(user_name)'\n            value = value.lower()\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 = []\n        found = 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 = {a.user_id: a for a in found}\n        for i in user_infos:\n            if i.user_id in found:\n                accounts.append(found[i.user_id])\n            else:\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') or \\\n               netloc.endswith('libravatar.org'):\n                query = AVATAR_QUERY\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                id = cursor.one(\"\"\"\n                    INSERT INTO participants DEFAULT VALUES RETURNING id\n                \"\"\")\n                account = cursor.one(\"\"\"\n                    INSERT INTO elsewhere\n                                (participant, {0})\n                         VALUES (%s, {1})\n                      RETURNING elsewhere.*::elsewhere_with_participant\n                \"\"\".format(cols, placeholders), (id,)+vals)\n        except IntegrityError:\n            # The account is already in the DB, update it instead\n            account = cls.db.one(\"\"\"\n                UPDATE elsewhere\n                   SET ({0}) = ({1})\n                 WHERE platform=%s AND user_id=%s\n             RETURNING elsewhere.*::elsewhere_with_participant\n            \"\"\".format(cols, placeholders), vals+(i.platform, i.user_id))\n            if not account:\n                raise\n\n        # Return account after propagating avatar_url to participant\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 liberapay_slug(self):\n        return self.user_name or ('~' + self.user_id)\n\n    @property\n    def liberapay_url(self):\n        scheme = website.canonical_scheme\n        host = website.canonical_host\n        platform = self.platform\n        slug = self.liberapay_slug\n        return \"{scheme}://{host}/on/{platform}/{slug}/\".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    @property\n    def friendly_name(self):\n        if getattr(self.platform, 'optional_user_name', False):\n            return self.display_name or self.user_name or self.user_id\n        else:\n            return self.user_name or self.display_name or self.user_id\n\n    @property\n    def friendly_name_long(self):\n        r = self.friendly_name\n        display_name = self.display_name\n        if display_name and display_name != r:\n            return '%s (%s)' % (r, display_name)\n        user_name = self.user_name\n        if user_name and user_name != r:\n            return '%s (%s)' % (r, user_name)\n        return r\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, api_lookup=True):\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    uid = path['user_name']\n    if uid[:1] == '~':\n        key = 'user_id'\n        uid = uid[1:]\n    else:\n        key = 'user_name'\n        if uid[:1] == '@':\n            uid = uid[1:]\n    try:\n        account = AccountElsewhere._from_thing(key, platform.name, uid)\n    except UnknownAccountElsewhere:\n        account = None\n    if not account:\n        if not api_lookup:\n            raise Response(404)\n        try:\n            user_info = platform.get_user_info(key, uid)\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                        uid, platform.display_name)\n                raise Response(404, err)\n            raise\n        account = AccountElsewhere.upsert(user_info)\n    return platform, account\n", "path": "liberapay/models/account_elsewhere.py"}, {"content": "from __future__ import absolute_import, division, print_function, unicode_literals\n\nfrom liberapay.elsewhere._base import PlatformOAuth2\nfrom liberapay.elsewhere._extractors import key\n\n\nclass LinuxFr(PlatformOAuth2):\n\n    # Platform attributes\n    name = 'linuxfr'\n    display_name = 'LinuxFr.org'\n    account_url = 'https://linuxfr.org/users/{user_name}'\n\n    # Auth attributes\n    # LinuxFr uses https://github.com/doorkeeper-gem/doorkeeper\n    auth_url = 'https://linuxfr.org/api/oauth/authorize'\n    access_token_url = 'https://linuxfr.org/api/oauth/token'\n\n    # API attributes\n    # https://linuxfr.org/developpeur\n    api_format = 'json'\n    api_url = 'https://linuxfr.org/api/v1'\n    api_user_self_info_path = '/me'\n\n    # User info extractors\n    x_user_name = key('login')\n    x_email = key('email')\n", "path": "liberapay/elsewhere/linuxfr.py"}], "after_files": [{"content": "from __future__ import absolute_import, division, print_function, unicode_literals\n\nfrom datetime import timedelta\nimport json\nimport uuid\nimport xml.etree.ElementTree as ET\n\nfrom six.moves.urllib.parse import urlsplit, urlunsplit\n\nfrom aspen import Response\nfrom aspen.utils import utcnow\nfrom postgres.orm import Model\nfrom psycopg2 import IntegrityError\nimport xmltodict\n\nfrom liberapay.constants import AVATAR_QUERY\nfrom liberapay.security.crypto import constant_time_compare\nfrom liberapay.website import website\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(website.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        if thing == 'user_name':\n            thing = 'lower(user_name)'\n            value = value.lower()\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 = []\n        found = 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 = {a.user_id: a for a in found}\n        for i in user_infos:\n            if i.user_id in found:\n                accounts.append(found[i.user_id])\n            else:\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') or \\\n               netloc.endswith('libravatar.org'):\n                query = AVATAR_QUERY\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                id = cursor.one(\"\"\"\n                    INSERT INTO participants DEFAULT VALUES RETURNING id\n                \"\"\")\n                account = cursor.one(\"\"\"\n                    INSERT INTO elsewhere\n                                (participant, {0})\n                         VALUES (%s, {1})\n                      RETURNING elsewhere.*::elsewhere_with_participant\n                \"\"\".format(cols, placeholders), (id,)+vals)\n        except IntegrityError:\n            # The account is already in the DB, update it instead\n            account = cls.db.one(\"\"\"\n                UPDATE elsewhere\n                   SET ({0}) = ({1})\n                 WHERE platform=%s AND user_id=%s\n             RETURNING elsewhere.*::elsewhere_with_participant\n            \"\"\".format(cols, placeholders), vals+(i.platform, i.user_id))\n            if not account:\n                raise\n\n        # Return account after propagating avatar_url to participant\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 liberapay_slug(self):\n        return self.user_name or ('~' + self.user_id)\n\n    @property\n    def liberapay_url(self):\n        scheme = website.canonical_scheme\n        host = website.canonical_host\n        platform = self.platform\n        slug = self.liberapay_slug\n        return \"{scheme}://{host}/on/{platform}/{slug}/\".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            user_name_lower=self.user_name.lower(),\n            platform_data=self.platform_data\n        )\n\n    @property\n    def friendly_name(self):\n        if getattr(self.platform, 'optional_user_name', False):\n            return self.display_name or self.user_name or self.user_id\n        else:\n            return self.user_name or self.display_name or self.user_id\n\n    @property\n    def friendly_name_long(self):\n        r = self.friendly_name\n        display_name = self.display_name\n        if display_name and display_name != r:\n            return '%s (%s)' % (r, display_name)\n        user_name = self.user_name\n        if user_name and user_name != r:\n            return '%s (%s)' % (r, user_name)\n        return r\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, api_lookup=True):\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    uid = path['user_name']\n    if uid[:1] == '~':\n        key = 'user_id'\n        uid = uid[1:]\n    else:\n        key = 'user_name'\n        if uid[:1] == '@':\n            uid = uid[1:]\n    try:\n        account = AccountElsewhere._from_thing(key, platform.name, uid)\n    except UnknownAccountElsewhere:\n        account = None\n    if not account:\n        if not api_lookup:\n            raise Response(404)\n        try:\n            user_info = platform.get_user_info(key, uid)\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                        uid, platform.display_name)\n                raise Response(404, err)\n            raise\n        account = AccountElsewhere.upsert(user_info)\n    return platform, account\n", "path": "liberapay/models/account_elsewhere.py"}, {"content": "from __future__ import absolute_import, division, print_function, unicode_literals\n\nfrom liberapay.elsewhere._base import PlatformOAuth2\nfrom liberapay.elsewhere._extractors import key\n\n\nclass LinuxFr(PlatformOAuth2):\n\n    # Platform attributes\n    name = 'linuxfr'\n    display_name = 'LinuxFr.org'\n    account_url = 'https://linuxfr.org/users/{user_name_lower}'\n\n    # Auth attributes\n    # LinuxFr uses https://github.com/doorkeeper-gem/doorkeeper\n    auth_url = 'https://linuxfr.org/api/oauth/authorize'\n    access_token_url = 'https://linuxfr.org/api/oauth/token'\n\n    # API attributes\n    # https://linuxfr.org/developpeur\n    api_format = 'json'\n    api_url = 'https://linuxfr.org/api/v1'\n    api_user_self_info_path = '/me'\n\n    # User info extractors\n    x_user_name = key('login')\n    x_email = key('email')\n", "path": "liberapay/elsewhere/linuxfr.py"}]}