class Twitter(PlatformOAuth1): # Platform attributes name = 'twitter' display_name = 'Twitter' account_url = 'https://twitter.com/{user_name}' # Auth attributes auth_url = 'https://api.twitter.com' authorize_path = '/oauth/authenticate' # API attributes api_format = 'json' api_paginator = query_param_paginator('cursor', prev='previous_cursor', next='next_cursor') api_url = 'https://api.twitter.com/1.1' api_user_info_path = '/users/show.json?user_id={user_id}' api_user_name_info_path = '/users/show.json?screen_name={user_name}' api_user_self_info_path = '/account/verify_credentials.json' api_friends_path = '/friends/list.json?user_id={user_id}&skip_status=true' ratelimit_headers_prefix = 'x-rate-limit-' # User info extractors x_user_id = key('id') x_user_name = key('screen_name') x_display_name = key('name') x_email = not_available x_avatar_url = key('profile_image_url_https', clean=lambda v: v.replace('_normal.', '.')) x_friends_count = key('friends_count')
class GitLab(PlatformOAuth2): # Platform attributes name = 'gitlab' display_name = 'GitLab' account_url = 'https://gitlab.com/u/{user_name}' # Auth attributes # GitLab uses https://github.com/doorkeeper-gem/doorkeeper auth_url = 'https://gitlab.com/oauth/authorize' access_token_url = 'https://gitlab.com/oauth/token' # API attributes # http://doc.gitlab.com/ce/api/ api_format = 'json' api_paginator = header_links_paginator() api_url = 'https://gitlab.com/api/v3' # api_user_info_path = '/users/{user_id}' # api_user_name_info_path = '/users?username={user_name}' api_user_self_info_path = '/user' # api_team_members_path = '/groups/{user_name}/members' # The commented out paths are because we need this: # https://gitlab.com/gitlab-org/gitlab-ce/issues/13795 # User info extractors x_user_id = key('id') x_user_name = key('username') x_display_name = key('name') x_email = key('email') x_avatar_url = key('avatar_url')
class Google(PlatformOAuth2): # Platform attributes name = 'google' display_name = 'Google' account_url = 'https://plus.google.com/{user_id}' optional_user_name = True # Auth attributes auth_url = 'https://accounts.google.com/o/oauth2/auth' access_token_url = 'https://accounts.google.com/o/oauth2/token' oauth_default_scope = ['https://www.googleapis.com/auth/userinfo.email', 'https://www.googleapis.com/auth/plus.login'] # API attributes api_format = 'json' api_paginator = query_param_paginator('pageToken', next='nextPageToken', page='items', total='totalItems') api_url = 'https://www.googleapis.com/plus/v1' api_user_info_path = '/people/{user_id}' api_user_self_info_path = '/people/me' api_friends_path = '/people/{user_id}/people/visible' api_friends_limited = True # User info extractors x_user_id = key('id') x_display_name = key('displayName') x_email = any_key(('emails', 0), clean=lambda d: d.get('value')) x_avatar_url = key('image', clean=lambda d: d.get('url')) def x_user_name(self, extracted, info, *default): url = info.get('url', '') return url[25:] if url.startswith('https://plus.google.com/+') else None
class Facebook(PlatformOAuth2): # Platform attributes name = 'facebook' display_name = 'Facebook' account_url = 'https://www.facebook.com/profile.php?id={user_id}' optional_user_name = True # Auth attributes auth_url = 'https://www.facebook.com/dialog/oauth' access_token_url = 'https://graph.facebook.com/oauth/access_token' oauth_default_scope = ['public_profile,email,user_friends'] # API attributes api_format = 'json' api_paginator = keys_paginator('data', paging='paging', prev='previous') api_url = 'https://graph.facebook.com' api_user_name_info_path = '/{user_name}' api_user_self_info_path = '/me' api_friends_path = '/v2.2/{user_id}/friends' api_friends_limited = True # User info extractors x_user_id = key('id') x_user_name = key('username') x_display_name = key('name') x_email = key('email') def x_avatar_url(self, extracted, info, default): return 'https://graph.facebook.com/' + extracted.user_id + '/picture?width=256&height=256'
class Facebook(PlatformOAuth2): # Platform attributes name = 'facebook' display_name = 'Facebook' fontawesome_name = 'facebook-square' account_url = 'https://www.facebook.com/app_scoped_user_id/{user_id}/' optional_user_name = True # Auth attributes auth_url = 'https://www.facebook.com/v2.10/dialog/oauth' access_token_url = 'https://graph.facebook.com/v2.10/oauth/access_token' refresh_token_url = None oauth_default_scope = ['public_profile'] oauth_email_scope = 'email' oauth_friends_scope = 'user_friends' # API attributes api_format = 'json' api_paginator = keys_paginator('data', paging='paging', prev='previous') api_url = 'https://graph.facebook.com/v2.10' api_user_self_info_path = '/me?fields=id,name,email' api_friends_path = '/me/friends' api_friends_limited = True # User info extractors x_user_id = key('id') x_display_name = key('name') x_email = key('email') x_description = key('bio') def x_avatar_url(self, extracted, info, default): return 'https://graph.facebook.com/' + extracted.user_id + '/picture?width=256&height=256'
class Bountysource(Platform): # Platform attributes name = 'bountysource' display_name = 'Bountysource' account_url = '{platform_data.auth_url}/people/{user_id}' optional_user_name = True # API attributes api_format = 'json' api_user_info_path = '/users/{user_id}' api_user_self_info_path = '/user' # User info extractors x_user_id = key('id') x_user_name = not_available x_display_name = key('display_name') x_email = key('email') x_avatar_url = key('image_url') def get_auth_session(self, token=None): sess = requests.Session() sess.auth = BountysourceAuth(token) return sess def get_auth_url(self, user): query_id = hexlify(os.urandom(10)) time_now = int(time()) raw = '%s.%s.%s' % (user.id, time_now, self.api_secret) h = hashlib.md5(raw).hexdigest() token = '%s.%s.%s' % (user.id, time_now, h) params = dict(redirect_url=self.callback_url + '?query_id=' + query_id, external_access_token=token) url = self.auth_url + '/auth/liberapay/confirm?' + urlencode(params) return url, query_id, '' def get_query_id(self, querystring): token = querystring['access_token'] i = token.rfind('.') data, data_hash = token[:i], token[i + 1:] if data_hash != hashlib.md5(data + '.' + self.api_secret).hexdigest(): raise Response(400, 'Invalid hash in access_token') return querystring['query_id'] def get_user_self_info(self, sess): querystring = urlparse(sess._callback_url).query info = { k: v[0] if len(v) == 1 else v for k, v in parse_qs(querystring).items() } info.pop('access_token') info.pop('query_id') return self.extract_user_info(info) def handle_auth_callback(self, url, query_id, unused_arg): sess = self.get_auth_session(token=query_id) sess._callback_url = url return sess
class Youtube(PlatformOAuth2): # Platform attributes based_on = 'google' name = 'youtube' display_name = 'Youtube' account_url = 'https://youtube.com/channel/{user_id}' optional_user_name = True user_type = 'channel' # Auth attributes auth_url = 'https://accounts.google.com/o/oauth2/auth?access_type=offline' access_token_url = 'https://accounts.google.com/o/oauth2/token' oauth_default_scope = ['https://www.googleapis.com/auth/youtube.readonly'] # API attributes api_format = 'json' api_paginator = query_param_paginator('pageToken', next='nextPageToken', page='items', total=('pageInfo', 'totalResults')) api_url = 'https://www.googleapis.com/youtube/v3' api_user_info_path = '/channels?part=snippet&id={user_id}' api_user_self_info_path = '/channels?part=snippet&mine=true' api_friends_path = '/subscriptions?part=snippet&mine=true' api_search_path = '/search?part=snippet&type=channel&q={query}' # User info extractors x_user_info = key('items') x_user_id = any_key(('snippet', 'resourceId', 'channelId'), 'id') x_display_name = any_key(('snippet', 'title')) x_avatar_url = any_key(('snippet', 'thumbnails', 'medium', 'url')) x_description = any_key(('snippet', 'description'))
class Bitbucket(PlatformOAuth1): # Platform attributes name = 'bitbucket' display_name = 'Bitbucket' fontawesome_name = name account_url = 'https://bitbucket.org/{user_name}' # Auth attributes auth_url = 'https://bitbucket.org/api/1.0' authorize_path = '/oauth/authenticate' # API attributes api_format = 'json' api_paginator = keys_paginator('values', prev='previous', total='size') api_url = 'https://bitbucket.org/api' api_user_info_path = '/2.0/users/{user_id}' api_user_name_info_path = '/2.0/users/{user_name}' api_user_self_info_path = '/2.0/user' api_team_members_path = '/2.0/teams/{user_name}/members' api_friends_path = '/2.0/users/{user_name}/following' # User info extractors x_user_info = key('user') x_user_id = key('uuid') x_user_name = key('username') x_display_name = key('display_name') x_email = not_available x_avatar_url = any_key('avatar', ('links', 'avatar', 'href')) x_is_team = key('type', lambda v: v == 'team') def api_get(self, domain, path, sess=None, **kw): """Extend to manually retry /users/pypy as /teams/pypy. Bitbucket gives us a 404 where a 30x would be more helpful. """ try: return PlatformOAuth1.api_get(self, domain, path, sess, **kw) except Response as response: if response.code == 404 and ' is a team account' in response.body: assert path.startswith('/2.0/users/') path = '/2.0/teams/' + path[11:] return PlatformOAuth1.api_get(self, domain, path, sess, **kw) else: raise
class Autour(PlatformOAuth2): # Platform attributes name = 'autour' display_name = 'Autour.com' account_url = 'https://autour.com/{user_name}' allows_team_connect = True # Auth attributes auth_url = 'https://api.autour.com/oauth2' access_token_url = 'https://api.autour.com/oauth2/access_token' oauth_email_scope = 'user:email' oauth_default_scope = ['read:org'] # API attributes api_format = 'json' api_paginator = header_links_paginator() api_url = 'https://api.autour.com' api_app_auth_params = 'client_id={api_key}&client_secret={api_secret}' api_friends_path = '/friends/{user_name}' api_user_info_path = '/users/{user_name}' api_user_name_info_path = '/users/{user_name}' api_user_self_info_path = '/oauth2/verify_credentials' api_team_members_path = '/orgs/{user_name}/public_members' api_friends_path = '/users/{user_id}/following' ratelimit_headers_prefix = 'x-ratelimit-' # User info extractors x_user_id = key('id') x_user_name = key('login') x_display_name = key('name') x_email = key('email') x_gravatar_id = key('gravatar_id') x_avatar_url = key('avatar_url') x_is_team = key('type', clean=lambda t: t.lower() == 'organization')
class Twitch(PlatformOAuth2): # Platform attributes name = 'twitch' display_name = 'Twitch' account_url = 'https://twitch.tv/{user_name}' user_type = 'channel' # Auth attributes auth_url = 'https://api.twitch.tv/kraken/oauth2/authorize' access_token_url = 'https://api.twitch.tv/kraken/oauth2/token' oauth_default_scope = ['channel_read'] session_class = TwitchOAuthSession # API attributes api_headers = {'Accept': 'application/vnd.twitchtv.v5+json'} api_format = 'json' api_paginator = query_param_paginator('cursor', next='_cursor', total='_total') api_url = 'https://api.twitch.tv/kraken' api_user_info_path = '/channels/{user_id}' api_user_self_info_path = '/channel' api_friends_path = '/users/{user_id}/follows/channels' api_search_path = '/search/channels?query={query}' # User info extractors x_user_info = key('channel') x_user_id = key('_id') x_user_name = key('name') x_display_name = key('display_name') x_email = key('email') x_avatar_url = key('logo') x_description = key('description')
class LinuxFr(PlatformOAuth2): # Platform attributes name = 'linuxfr' display_name = 'LinuxFr.org' account_url = 'https://linuxfr.org/users/{user_name_lower}' # Auth attributes # LinuxFr uses https://github.com/doorkeeper-gem/doorkeeper auth_url = 'https://linuxfr.org/api/oauth/authorize' access_token_url = 'https://linuxfr.org/api/oauth/token' # API attributes # https://linuxfr.org/developpeur api_format = 'json' api_url = 'https://linuxfr.org/api/v1' api_user_self_info_path = '/me' # User info extractors x_user_name = key('login') x_email = key('email')
class Google(PlatformOAuth2): # Platform attributes name = 'google' display_name = 'Google' fontawesome_name = name account_url = None optional_user_name = True # Auth attributes # https://developers.google.com/identity/protocols/OAuth2WebServer auth_url = 'https://accounts.google.com/o/oauth2/auth?access_type=offline&include_granted_scopes=true' access_token_url = 'https://accounts.google.com/o/oauth2/token' # https://developers.google.com/identity/protocols/googlescopes oauth_default_scope = ['https://www.googleapis.com/auth/userinfo.profile'] oauth_friends_scope = 'https://www.googleapis.com/auth/contacts.readonly' # https://developers.google.com/people/api/rest/v1/people/get person_fields = 'personFields=names,nicknames,photos,taglines' # API attributes api_requires_user_token = True api_format = 'json' api_paginator = query_param_paginator('pageToken', next='nextPageToken', total='totalItems') api_url = 'https://people.googleapis.com/v1' api_user_info_path = '/people/{user_id}?%s' % person_fields api_user_self_info_path = '/people/me?%s' % person_fields api_friends_path = '/people/me/connections?%s' % person_fields # User info extractors x_user_id = key('resourceName', clean=partial(_strip_prefix, 'people/')) x_display_name = any_key(('names', 'displayName')) x_avatar_url = any_key('coverPhotos', 'photos', clean=lambda d: None if d.get('default') else d['url']) x_description = any_key(('taglines', 'value')) def x_user_info(self, extracted, info, *default): """Reduce a Person object to its primary values. Docs: https://developers.google.com/people/api/rest/v1/people#Person """ for k, v in list(info.items()): if type(v) is list: info[k] = get_primary(v) return info
class Twitch(PlatformOAuth2): # Platform attributes name = 'twitch' display_name = 'Twitch' fontawesome_name = name account_url = 'https://twitch.tv/{user_name}' user_type = 'channel' # Auth attributes # https://dev.twitch.tv/docs/authentication/ auth_url = 'https://id.twitch.tv/oauth2/authorize' access_token_url = 'https://id.twitch.tv/oauth2/token' can_auth_with_client_credentials = True # API attributes api_format = 'json' api_paginator = cursor_paginator(('pagination', 'cursor'), page='data', next='after', prev='before') api_url = 'https://api.twitch.tv/helix' api_user_info_path = '/users?id={user_id}' api_user_name_info_path = '/users?login={user_name}' api_user_self_info_path = '/users' # api_friends_path = '/users/follows?from_id={user_id}' # This endpoint only returns user IDs, not a list of user info objects # User info extractors x_user_info = key('data') x_user_id = key('id') x_user_name = key('login') x_display_name = key('display_name') x_email = key('email') x_avatar_url = key('profile_image_url') x_description = key('description') def __init__(self, *args, **kw): super().__init__(*args, **kw) self.api_headers = {'Client-ID': self.api_key}
class GitHub(PlatformOAuth2): # Platform attributes name = 'github' display_name = 'GitHub' account_url = 'https://github.com/{user_name}' allows_team_connect = True # Auth attributes auth_url = 'https://github.com/login/oauth/authorize' access_token_url = 'https://github.com/login/oauth/access_token' oauth_email_scope = 'user:email' oauth_default_scope = ['read:org'] # API attributes api_format = 'json' api_paginator = header_links_paginator() api_url = 'https://api.github.com' api_user_info_path = '/user/{user_id}' api_user_name_info_path = '/users/{user_name}' api_user_self_info_path = '/user' api_team_members_path = '/orgs/{user_name}/public_members' api_friends_path = '/users/{user_name}/following' ratelimit_headers_prefix = 'x-ratelimit-' # User info extractors x_user_id = key('id') x_user_name = key('login') x_display_name = key('name') x_email = key('email') x_gravatar_id = key('gravatar_id') x_avatar_url = key('avatar_url') x_is_team = key('type', clean=lambda t: t.lower() == 'organization') def is_team_admin(self, team_name, sess): user_teams = self.api_parser(self.api_get('/user/teams', sess=sess)) return any( team.get('organization', {}).get('login') == team_name and team.get('permission') == 'admin' for team in user_teams)
class Twitch(PlatformOAuth2): # Platform attributes name = 'twitch' display_name = 'Twitch' account_url = 'https://twitch.tv/{user_name}' user_type = 'channel' # Auth attributes # https://dev.twitch.tv/docs/authentication/ auth_url = 'https://id.twitch.tv/oauth2/authorize' access_token_url = 'https://id.twitch.tv/oauth2/token' can_auth_with_client_credentials = True # API attributes api_format = 'json' api_paginator = cursor_paginator(('pagination', 'cursor'), page='data', next='after', prev='before') api_url = 'https://api.twitch.tv/helix' api_user_info_path = '/users?id={user_id}' api_user_name_info_path = '/users?login={user_name}' api_user_self_info_path = '/users' # api_friends_path = '/users/follows?from_id={user_id}' # This endpoint only returns user IDs, not a list of user info objects # User info extractors x_user_info = key('data', clean=lambda o: o[0] if isinstance(o, list) and len(o) == 1 else o) x_user_id = key('id') x_user_name = key('login') x_display_name = key('display_name') x_email = key('email') x_avatar_url = key('profile_image_url') x_description = key('description')
class GitHub(PlatformOAuth2): # Platform attributes name = 'github' display_name = 'GitHub' account_url = 'https://github.com/{user_name}' repo_url = 'https://github.com/{slug}' has_teams = True # Auth attributes auth_url = 'https://github.com/login/oauth/authorize' access_token_url = 'https://github.com/login/oauth/access_token' oauth_email_scope = 'user:email' oauth_default_scope = ['read:org'] # API attributes api_format = 'json' api_paginator = header_links_paginator() api_url = 'https://api.github.com' api_app_auth_params = 'client_id={api_key}&client_secret={api_secret}' api_user_info_path = '/user/{user_id}' api_user_name_info_path = '/users/{user_name}' api_user_self_info_path = '/user' api_team_members_path = '/orgs/{user_name}/public_members' api_friends_path = '/users/{user_name}/following' api_repos_path = '/users/{user_name}/repos?type=owner&sort=updated&per_page=100' api_starred_path = '/users/{user_name}/starred' ratelimit_headers_prefix = 'x-ratelimit-' # User info extractors x_user_id = key('id') x_user_name = key('login') x_display_name = key('name') x_email = key('email') x_gravatar_id = key('gravatar_id') x_avatar_url = key('avatar_url') x_is_team = key('type', clean=lambda t: t.lower() == 'organization') # Repo info extractors x_repo_id = key('id') x_repo_name = key('name') x_repo_slug = key('full_name') x_repo_description = key('description') x_repo_last_update = key('updated_at') x_repo_is_fork = key('fork') x_repo_stars_count = key('stargazers_count') x_repo_owner_id = key('owner', clean=lambda d: d['id']) x_repo_extra_info_drop = drop_keys(lambda k: k.endswith('_url')) def get_CantReadMembership_url(self, **kw): return 'https://github.com/settings/connections/applications/' + self.api_key def is_team_member(self, org_name, sess, account): org_name = org_name.lower() # Check public membership first response = self.api_get('', '/orgs/' + org_name + '/public_members/' + account.user_name, sess=sess, error_handler=None) if response.status_code == 204: return True elif response.status_code != 404: self.api_error_handler(response, True, self.domain) # Check private membership response = self.api_get('', '/user/memberships/orgs/' + org_name, sess=sess, error_handler=None) if response.status_code == 403: raise CantReadMembership elif response.status_code >= 400: self.api_error_handler(response, True, self.domain) membership = self.api_parser(response) if membership['state'] == 'active': return True # Try the endpoint we were using before user_orgs = self.api_parser(self.api_get('', '/user/orgs', sess=sess)) return any(org.get('login') == org_name for org in user_orgs)
class GitLab(PlatformOAuth2): # Platform attributes name = 'gitlab' display_name = 'GitLab' account_url = 'https://gitlab.com/u/{user_name}' repo_url = 'https://gitlab.com/{slug}' has_teams = True # Auth attributes # GitLab uses https://github.com/doorkeeper-gem/doorkeeper auth_url = 'https://gitlab.com/oauth/authorize' access_token_url = 'https://gitlab.com/oauth/token' can_auth_with_client_credentials = True # API attributes # http://doc.gitlab.com/ce/api/ api_format = 'json' api_paginator = header_links_paginator(total_header='X-Total') api_url = 'https://gitlab.com/api/v3' # api_user_info_path = '/users/{user_id}' # api_user_name_info_path = '/users?username={user_name}' api_user_self_info_path = '/user' # api_team_members_path = '/groups/{user_name}/members' api_repos_path = '/projects?owned=true&visibility=public&order_by=last_activity_at&per_page=100' api_starred_path = '/projects?starred=true&visibility=public' # The commented out paths are because we need this: # https://gitlab.com/gitlab-org/gitlab-ce/issues/13795 # User info extractors x_user_id = key('id') x_user_name = key('username') x_display_name = key('name') x_email = key('email') x_avatar_url = key('avatar_url') # Repo info extractors x_repo_id = key('id') x_repo_name = key('name') x_repo_slug = key('path_with_namespace') x_repo_description = key('description') x_repo_last_update = key('last_activity_at') x_repo_is_fork = key('forked_from_project', clean=bool) x_repo_stars_count = key('star_count') x_repo_owner_id = key('owner', clean=lambda d: d['id'])
class GitLab(PlatformOAuth2): # Platform attributes name = 'gitlab' display_name = 'GitLab' fontawesome_name = name account_url = 'https://gitlab.com/{user_name}' repo_url = 'https://gitlab.com/{slug}' has_teams = True # Auth attributes # GitLab uses https://github.com/doorkeeper-gem/doorkeeper auth_url = 'https://gitlab.com/oauth/authorize' access_token_url = 'https://gitlab.com/oauth/token' oauth_default_scope = ['read_user'] # can_auth_with_client_credentials = True # https://gitlab.com/gitlab-org/gitlab-ce/issues/13795 # API attributes # http://doc.gitlab.com/ce/api/ api_format = 'json' api_paginator = header_links_paginator(total_header='X-Total') api_url = 'https://gitlab.com/api/v4' api_user_info_path = '/users/{user_id}' api_user_name_info_path = '/users?username={user_name}' api_user_self_info_path = '/user' api_team_members_path = '/groups/{user_name}/members' api_repos_path = APIEndpoint( '/users/{user_id}/projects?owned=true&visibility=public&order_by=last_activity_at&per_page=100', use_session=False) api_starred_path = APIEndpoint( '/users/{user_id}/projects?starred=true&visibility=public', use_session=False) # User info extractors x_user_id = key('id') x_user_name = key('username') x_display_name = key('name') x_email = key('email') x_avatar_url = key('avatar_url') x_description = key('bio') # Repo info extractors x_repo_id = key('id') x_repo_name = key('name') x_repo_slug = key('path_with_namespace') x_repo_description = key('description') x_repo_last_update = key('last_activity_at') x_repo_is_fork = key('forked_from_project', clean=bool) x_repo_stars_count = key('star_count')
class Mastodon(PlatformOAuth2): # Platform attributes name = 'mastodon' display_name = 'Mastodon' account_url = 'https://{domain}/@{user_name}' single_domain = False def example_account_address(self, _): return _('*****@*****.**') # Auth attributes # Mastodon uses https://github.com/doorkeeper-gem/doorkeeper auth_url = 'https://{domain}/oauth/authorize' access_token_url = 'https://{domain}/oauth/token' can_auth_with_client_credentials = True # API attributes # https://github.com/tootsuite/documentation/blob/master/Using-the-API/API.md api_format = 'json' api_paginator = header_links_paginator() api_url = 'https://{domain}/api/v1' api_user_info_path = '/accounts/{user_id}' # api_user_name_info_path = '/search?q={user_name}@{domain}' api_user_self_info_path = '/accounts/verify_credentials' api_friends_path = '/accounts/{user_id}/following' ratelimit_headers_prefix = 'x-ratelimit-' # User info extractors x_domain = key('url', clean=extract_domain_from_url) x_user_id = key('id') x_user_name = key('username') x_display_name = key('display_name') x_avatar_url = key('avatar_static') def x_user_info(self, extracted, info, default): if 'accounts' in info: accounts = info.get('accounts') if accounts: return accounts[0] raise Response(404) return default def register_app(self, domain): data = { 'client_name': self.app_name, 'redirect_uris': self.callback_url.format(domain=domain), 'scopes': 'read', 'website': self.app_url, } r = requests.post('https://%s/api/v1/apps' % domain, data, timeout=self.api_timeout) status = r.status_code try: o = r.json() c_id, c_secret = o['client_id'], o['client_secret'] except (KeyError, ValueError): c_id, c_secret = None, None if status != 200 or not c_id or not c_secret: logger.error('{} responded with {}:\n{}'.format(domain, status, r.text)) msg = lambda _: _( "Is {0} really a {1} server? It is currently not acting like one.", domain, self.display_name, ) raise LazyResponse(502, msg) return c_id, c_secret