Clean up doc strings and unused imports
This commit is contained in:
@@ -14,12 +14,10 @@
|
|||||||
|
|
||||||
"""Client for discovery based APIs
|
"""Client for discovery based APIs
|
||||||
|
|
||||||
A client library for Google's discovery
|
A client library for Google's discovery based APIs.
|
||||||
based APIs.
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
__author__ = 'jcgregorio@google.com (Joe Gregorio)'
|
__author__ = 'jcgregorio@google.com (Joe Gregorio)'
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
'build', 'build_from_document'
|
'build', 'build_from_document'
|
||||||
]
|
]
|
||||||
@@ -48,8 +46,9 @@ DISCOVERY_URI = ('https://www.googleapis.com/discovery/v0.2beta1/describe/'
|
|||||||
|
|
||||||
|
|
||||||
def key2param(key):
|
def key2param(key):
|
||||||
"""
|
"""Converts key names into parameter names.
|
||||||
max-results -> max_results
|
|
||||||
|
For example, converting "max-results" -> "max_results"
|
||||||
"""
|
"""
|
||||||
result = []
|
result = []
|
||||||
key = list(key)
|
key = list(key)
|
||||||
@@ -106,10 +105,10 @@ def build(serviceName, version,
|
|||||||
resp, content = http.request(requested_url)
|
resp, content = http.request(requested_url)
|
||||||
service = simplejson.loads(content)
|
service = simplejson.loads(content)
|
||||||
|
|
||||||
fn = os.path.join(os.path.dirname(__file__), "contrib",
|
fn = os.path.join(os.path.dirname(__file__), 'contrib',
|
||||||
serviceName, "future.json")
|
serviceName, 'future.json')
|
||||||
try:
|
try:
|
||||||
f = file(fn, "r")
|
f = file(fn, 'r')
|
||||||
future = f.read()
|
future = f.read()
|
||||||
f.close()
|
f.close()
|
||||||
except IOError:
|
except IOError:
|
||||||
@@ -275,9 +274,9 @@ def createResource(http, baseUrl, model, requestBuilder,
|
|||||||
|
|
||||||
docs = ['A description of how to use this function\n\n']
|
docs = ['A description of how to use this function\n\n']
|
||||||
for arg in argmap.iterkeys():
|
for arg in argmap.iterkeys():
|
||||||
required = ""
|
required = ''
|
||||||
if arg in required_params:
|
if arg in required_params:
|
||||||
required = " (required)"
|
required = ' (required)'
|
||||||
docs.append('%s - A parameter%s\n' % (arg, required))
|
docs.append('%s - A parameter%s\n' % (arg, required))
|
||||||
|
|
||||||
setattr(method, '__doc__', ''.join(docs))
|
setattr(method, '__doc__', ''.join(docs))
|
||||||
@@ -363,7 +362,7 @@ def createResource(http, baseUrl, model, requestBuilder,
|
|||||||
if futureDesc and 'methods' in futureDesc:
|
if futureDesc and 'methods' in futureDesc:
|
||||||
for methodName, methodDesc in futureDesc['methods'].iteritems():
|
for methodName, methodDesc in futureDesc['methods'].iteritems():
|
||||||
if 'next' in methodDesc and methodName in resourceDesc['methods']:
|
if 'next' in methodDesc and methodName in resourceDesc['methods']:
|
||||||
createNextMethod(Resource, methodName + "_next",
|
createNextMethod(Resource, methodName + '_next',
|
||||||
resourceDesc['methods'][methodName],
|
resourceDesc['methods'][methodName],
|
||||||
methodDesc['next'])
|
methodDesc['next'])
|
||||||
|
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ class HttpRequest(object):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, http, postproc, uri,
|
def __init__(self, http, postproc, uri,
|
||||||
method="GET",
|
method='GET',
|
||||||
body=None,
|
body=None,
|
||||||
headers=None,
|
headers=None,
|
||||||
methodId=None):
|
methodId=None):
|
||||||
@@ -139,7 +139,7 @@ class RequestMockBuilder(object):
|
|||||||
"""
|
"""
|
||||||
self.responses = responses
|
self.responses = responses
|
||||||
|
|
||||||
def __call__(self, http, postproc, uri, method="GET", body=None,
|
def __call__(self, http, postproc, uri, method='GET', body=None,
|
||||||
headers=None, methodId=None):
|
headers=None, methodId=None):
|
||||||
"""Implements the callable interface that discovery.build() expects
|
"""Implements the callable interface that discovery.build() expects
|
||||||
of requestBuilder, which is to build an object compatible with
|
of requestBuilder, which is to build an object compatible with
|
||||||
@@ -169,7 +169,7 @@ class HttpMock(object):
|
|||||||
self.headers = headers
|
self.headers = headers
|
||||||
|
|
||||||
def request(self, uri,
|
def request(self, uri,
|
||||||
method="GET",
|
method='GET',
|
||||||
body=None,
|
body=None,
|
||||||
headers=None,
|
headers=None,
|
||||||
redirections=1,
|
redirections=1,
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ Utilities for making it easier to work with OAuth.
|
|||||||
__author__ = 'jcgregorio@google.com (Joe Gregorio)'
|
__author__ = 'jcgregorio@google.com (Joe Gregorio)'
|
||||||
|
|
||||||
import copy
|
import copy
|
||||||
import datetime
|
|
||||||
import httplib2
|
import httplib2
|
||||||
import logging
|
import logging
|
||||||
import oauth2 as oauth
|
import oauth2 as oauth
|
||||||
@@ -18,9 +17,9 @@ import urlparse
|
|||||||
from anyjson import simplejson
|
from anyjson import simplejson
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from urlparse import parse_qs, parse_qsl
|
from urlparse import parse_qsl
|
||||||
except ImportError:
|
except ImportError:
|
||||||
from cgi import parse_qs, parse_qsl
|
from cgi import parse_qsl
|
||||||
|
|
||||||
|
|
||||||
class Error(Exception):
|
class Error(Exception):
|
||||||
@@ -137,7 +136,7 @@ class OAuthCredentials(Credentials):
|
|||||||
req = oauth.Request.from_consumer_and_token(
|
req = oauth.Request.from_consumer_and_token(
|
||||||
self.consumer, self.token, http_method=method, http_url=uri)
|
self.consumer, self.token, http_method=method, http_url=uri)
|
||||||
req.sign_request(signer, self.consumer, self.token)
|
req.sign_request(signer, self.consumer, self.token)
|
||||||
if headers == None:
|
if headers is None:
|
||||||
headers = {}
|
headers = {}
|
||||||
headers.update(req.to_header())
|
headers.update(req.to_header())
|
||||||
if 'user-agent' in headers:
|
if 'user-agent' in headers:
|
||||||
@@ -210,7 +209,7 @@ class FlowThreeLegged(Flow):
|
|||||||
resp, content = client.request(uri, 'POST', headers=headers,
|
resp, content = client.request(uri, 'POST', headers=headers,
|
||||||
body=body)
|
body=body)
|
||||||
if resp['status'] != '200':
|
if resp['status'] != '200':
|
||||||
logging.error('Failed to retrieve temporary authorization: %s' % content)
|
logging.error('Failed to retrieve temporary authorization: %s', content)
|
||||||
raise RequestError('Invalid response %s.' % resp['status'])
|
raise RequestError('Invalid response %s.' % resp['status'])
|
||||||
|
|
||||||
self.request_token = dict(parse_qsl(content))
|
self.request_token = dict(parse_qsl(content))
|
||||||
@@ -247,7 +246,7 @@ class FlowThreeLegged(Flow):
|
|||||||
uri = _oauth_uri('access', self.discovery, self.params)
|
uri = _oauth_uri('access', self.discovery, self.params)
|
||||||
resp, content = client.request(uri, 'POST', headers=headers)
|
resp, content = client.request(uri, 'POST', headers=headers)
|
||||||
if resp['status'] != '200':
|
if resp['status'] != '200':
|
||||||
logging.error('Failed to retrieve access token: %s' % content)
|
logging.error('Failed to retrieve access token: %s', content)
|
||||||
raise RequestError('Invalid response %s.' % resp['status'])
|
raise RequestError('Invalid response %s.' % resp['status'])
|
||||||
|
|
||||||
oauth_params = dict(parse_qsl(content))
|
oauth_params = dict(parse_qsl(content))
|
||||||
|
|||||||
@@ -14,8 +14,7 @@
|
|||||||
|
|
||||||
"""Utilities for Google App Engine
|
"""Utilities for Google App Engine
|
||||||
|
|
||||||
Utilities for making it easier to use OAuth 2.0
|
Utilities for making it easier to use OAuth 2.0 on Google App Engine.
|
||||||
on Google App Engine.
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
__author__ = 'jcgregorio@google.com (Joe Gregorio)'
|
__author__ = 'jcgregorio@google.com (Joe Gregorio)'
|
||||||
@@ -29,8 +28,9 @@ from client import Storage
|
|||||||
|
|
||||||
|
|
||||||
class FlowProperty(db.Property):
|
class FlowProperty(db.Property):
|
||||||
"""Utility property that allows easy
|
"""App Engine datastore Property for Flow.
|
||||||
storage and retreival of an
|
|
||||||
|
Utility property that allows easy storage and retreival of an
|
||||||
oauth2client.Flow"""
|
oauth2client.Flow"""
|
||||||
|
|
||||||
# Tell what the user type is.
|
# Tell what the user type is.
|
||||||
@@ -60,8 +60,9 @@ class FlowProperty(db.Property):
|
|||||||
|
|
||||||
|
|
||||||
class CredentialsProperty(db.Property):
|
class CredentialsProperty(db.Property):
|
||||||
"""Utility property that allows easy
|
"""App Engine datastore Property for Credentials.
|
||||||
storage and retrieval of
|
|
||||||
|
Utility property that allows easy storage and retrieval of
|
||||||
oath2client.Credentials
|
oath2client.Credentials
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@@ -109,9 +110,9 @@ class StorageByKeyName(Storage):
|
|||||||
key_name: string, key name for the entity that has the credentials
|
key_name: string, key name for the entity that has the credentials
|
||||||
property_name: string, name of the property that is an CredentialsProperty
|
property_name: string, name of the property that is an CredentialsProperty
|
||||||
"""
|
"""
|
||||||
self.model = model
|
self._model = model
|
||||||
self.key_name = key_name
|
self._key_name = key_name
|
||||||
self.property_name = property_name
|
self._property_name = property_name
|
||||||
|
|
||||||
def get(self):
|
def get(self):
|
||||||
"""Retrieve Credential from datastore.
|
"""Retrieve Credential from datastore.
|
||||||
@@ -119,8 +120,8 @@ class StorageByKeyName(Storage):
|
|||||||
Returns:
|
Returns:
|
||||||
oauth2client.Credentials
|
oauth2client.Credentials
|
||||||
"""
|
"""
|
||||||
entity = self.model.get_or_insert(self.key_name)
|
entity = self._model.get_or_insert(self._key_name)
|
||||||
credential = getattr(entity, self.property_name)
|
credential = getattr(entity, self._property_name)
|
||||||
if credential and hasattr(credential, 'set_store'):
|
if credential and hasattr(credential, 'set_store'):
|
||||||
credential.set_store(self.put)
|
credential.set_store(self.put)
|
||||||
return credential
|
return credential
|
||||||
@@ -131,6 +132,6 @@ class StorageByKeyName(Storage):
|
|||||||
Args:
|
Args:
|
||||||
credentials: Credentials, the credentials to store.
|
credentials: Credentials, the credentials to store.
|
||||||
"""
|
"""
|
||||||
entity = self.model.get_or_insert(self.key_name)
|
entity = self._model.get_or_insert(self._key_name)
|
||||||
setattr(entity, self.property_name, credentials)
|
setattr(entity, self._property_name, credentials)
|
||||||
entity.put()
|
entity.put()
|
||||||
|
|||||||
@@ -113,12 +113,12 @@ class OAuth2Credentials(Credentials):
|
|||||||
the OAuth2WebServerFlow.
|
the OAuth2WebServerFlow.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
token_uri: string, URI of token endpoint
|
token_uri: string, URI of token endpoint.
|
||||||
client_id: string, client identifier
|
client_id: string, client identifier.
|
||||||
client_secret: string, client secret
|
client_secret: string, client secret.
|
||||||
access_token: string, access token
|
access_token: string, access token.
|
||||||
token_expiry: datetime, when the access_token expires
|
token_expiry: datetime, when the access_token expires.
|
||||||
refresh_token: string, refresh token
|
refresh_token: string, refresh token.
|
||||||
user_agent: string, The HTTP User-Agent to provide for this application.
|
user_agent: string, The HTTP User-Agent to provide for this application.
|
||||||
|
|
||||||
|
|
||||||
@@ -178,14 +178,16 @@ class OAuth2Credentials(Credentials):
|
|||||||
'user-agent': self.user_agent,
|
'user-agent': self.user_agent,
|
||||||
'content-type': 'application/x-www-form-urlencoded'
|
'content-type': 'application/x-www-form-urlencoded'
|
||||||
}
|
}
|
||||||
resp, content = http_request(self.token_uri, method='POST', body=body, headers=headers)
|
resp, content = http_request(
|
||||||
|
self.token_uri, method='POST', body=body, headers=headers)
|
||||||
if resp.status == 200:
|
if resp.status == 200:
|
||||||
# TODO(jcgregorio) Raise an error if loads fails?
|
# TODO(jcgregorio) Raise an error if loads fails?
|
||||||
d = simplejson.loads(content)
|
d = simplejson.loads(content)
|
||||||
self.access_token = d['access_token']
|
self.access_token = d['access_token']
|
||||||
self.refresh_token = d.get('refresh_token', self.refresh_token)
|
self.refresh_token = d.get('refresh_token', self.refresh_token)
|
||||||
if 'expires_in' in d:
|
if 'expires_in' in d:
|
||||||
self.token_expiry = datetime.timedelta(seconds = int(d['expires_in'])) + datetime.datetime.now()
|
self.token_expiry = datetime.timedelta(
|
||||||
|
seconds = int(d['expires_in'])) + datetime.datetime.now()
|
||||||
else:
|
else:
|
||||||
self.token_expiry = None
|
self.token_expiry = None
|
||||||
if self.store is not None:
|
if self.store is not None:
|
||||||
@@ -195,7 +197,8 @@ class OAuth2Credentials(Credentials):
|
|||||||
raise RequestError('Invalid response %s.' % resp['status'])
|
raise RequestError('Invalid response %s.' % resp['status'])
|
||||||
|
|
||||||
def authorize(self, http):
|
def authorize(self, http):
|
||||||
"""
|
"""Authorize an httplib2.Http instance with these credentials.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
http: An instance of httplib2.Http
|
http: An instance of httplib2.Http
|
||||||
or something that acts like it.
|
or something that acts like it.
|
||||||
@@ -232,7 +235,7 @@ class OAuth2Credentials(Credentials):
|
|||||||
else:
|
else:
|
||||||
headers['user-agent'] = self.user_agent
|
headers['user-agent'] = self.user_agent
|
||||||
resp, content = request_orig(uri, method, body, headers,
|
resp, content = request_orig(uri, method, body, headers,
|
||||||
redirections, connection_type)
|
redirections, connection_type)
|
||||||
if resp.status == 401:
|
if resp.status == 401:
|
||||||
logging.info("Refreshing because we got a 401")
|
logging.info("Refreshing because we got a 401")
|
||||||
self._refresh(request_orig)
|
self._refresh(request_orig)
|
||||||
@@ -252,18 +255,20 @@ class OAuth2WebServerFlow(Flow):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, client_id, client_secret, scope, user_agent,
|
def __init__(self, client_id, client_secret, scope, user_agent,
|
||||||
authorization_uri='https://www.google.com/accounts/o8/oauth2/authorization',
|
auth_uri='https://www.google.com/accounts/o8/oauth2/authorization',
|
||||||
token_uri='https://www.google.com/accounts/o8/oauth2/token',
|
token_uri='https://www.google.com/accounts/o8/oauth2/token',
|
||||||
**kwargs):
|
**kwargs):
|
||||||
"""Constructor for OAuth2WebServerFlow
|
"""Constructor for OAuth2WebServerFlow
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
client_id: string, client identifier
|
client_id: string, client identifier.
|
||||||
client_secret: string client secret
|
client_secret: string client secret.
|
||||||
scope: string, scope of the credentials being requested
|
scope: string, scope of the credentials being requested.
|
||||||
user_agent: string, HTTP User-Agent to provide for this application.
|
user_agent: string, HTTP User-Agent to provide for this application.
|
||||||
authorization_uri: string, URI for authorization endpoint
|
auth_uri: string, URI for authorization endpoint. For convenience
|
||||||
token_uri: string, URI for token endpoint
|
defaults to Google's endpoints but any OAuth 2.0 provider can be used.
|
||||||
|
token_uri: string, URI for token endpoint. For convenience
|
||||||
|
defaults to Google's endpoints but any OAuth 2.0 provider can be used.
|
||||||
**kwargs: dict, The keyword arguments are all optional and required
|
**kwargs: dict, The keyword arguments are all optional and required
|
||||||
parameters for the OAuth calls.
|
parameters for the OAuth calls.
|
||||||
"""
|
"""
|
||||||
@@ -271,7 +276,7 @@ class OAuth2WebServerFlow(Flow):
|
|||||||
self.client_secret = client_secret
|
self.client_secret = client_secret
|
||||||
self.scope = scope
|
self.scope = scope
|
||||||
self.user_agent = user_agent
|
self.user_agent = user_agent
|
||||||
self.authorization_uri = authorization_uri
|
self.auth_uri = auth_uri
|
||||||
self.token_uri = token_uri
|
self.token_uri = token_uri
|
||||||
self.params = kwargs
|
self.params = kwargs
|
||||||
self.redirect_uri = None
|
self.redirect_uri = None
|
||||||
@@ -298,7 +303,7 @@ class OAuth2WebServerFlow(Flow):
|
|||||||
'scope': self.scope,
|
'scope': self.scope,
|
||||||
}
|
}
|
||||||
query.update(self.params)
|
query.update(self.params)
|
||||||
parts = list(urlparse.urlparse(self.authorization_uri))
|
parts = list(urlparse.urlparse(self.auth_uri))
|
||||||
query.update(dict(parse_qsl(parts[4]))) # 4 is the index of the query part
|
query.update(dict(parse_qsl(parts[4]))) # 4 is the index of the query part
|
||||||
parts[4] = urllib.urlencode(query)
|
parts[4] = urllib.urlencode(query)
|
||||||
return urlparse.urlunparse(parts)
|
return urlparse.urlunparse(parts)
|
||||||
@@ -324,8 +329,8 @@ class OAuth2WebServerFlow(Flow):
|
|||||||
'scope': self.scope
|
'scope': self.scope
|
||||||
})
|
})
|
||||||
headers = {
|
headers = {
|
||||||
'user-agent': self.user_agent,
|
'user-agent': self.user_agent,
|
||||||
'content-type': 'application/x-www-form-urlencoded'
|
'content-type': 'application/x-www-form-urlencoded'
|
||||||
}
|
}
|
||||||
h = httplib2.Http()
|
h = httplib2.Http()
|
||||||
resp, content = h.request(self.token_uri, method='POST', body=body, headers=headers)
|
resp, content = h.request(self.token_uri, method='POST', body=body, headers=headers)
|
||||||
@@ -340,8 +345,8 @@ class OAuth2WebServerFlow(Flow):
|
|||||||
|
|
||||||
logging.info('Successfully retrieved access token: %s' % content)
|
logging.info('Successfully retrieved access token: %s' % content)
|
||||||
return OAuth2Credentials(access_token, self.client_id, self.client_secret,
|
return OAuth2Credentials(access_token, self.client_id, self.client_secret,
|
||||||
refresh_token, token_expiry, self.token_uri,
|
refresh_token, token_expiry, self.token_uri,
|
||||||
self.user_agent)
|
self.user_agent)
|
||||||
else:
|
else:
|
||||||
logging.error('Failed to retrieve access token: %s' % content)
|
logging.error('Failed to retrieve access token: %s' % content)
|
||||||
raise RequestError('Invalid response %s.' % resp['status'])
|
raise RequestError('Invalid response %s.' % resp['status'])
|
||||||
|
|||||||
@@ -1,3 +1,13 @@
|
|||||||
|
# Copyright 2010 Google Inc. All Rights Reserved.
|
||||||
|
|
||||||
|
"""OAuth 2.0 utilities for Django.
|
||||||
|
|
||||||
|
Utilities for using OAuth 2.0 in conjunction with
|
||||||
|
the Django datastore.
|
||||||
|
"""
|
||||||
|
|
||||||
|
__author__ = 'jcgregorio@google.com (Joe Gregorio)'
|
||||||
|
|
||||||
import oauth2client
|
import oauth2client
|
||||||
import base64
|
import base64
|
||||||
import pickle
|
import pickle
|
||||||
@@ -31,7 +41,6 @@ class FlowField(models.Field):
|
|||||||
return 'VARCHAR'
|
return 'VARCHAR'
|
||||||
|
|
||||||
def to_python(self, value):
|
def to_python(self, value):
|
||||||
print "In to_python", value
|
|
||||||
if value is None:
|
if value is None:
|
||||||
return None
|
return None
|
||||||
if isinstance(value, oauth2client.client.Flow):
|
if isinstance(value, oauth2client.client.Flow):
|
||||||
|
|||||||
@@ -17,21 +17,22 @@ class Storage(BaseStorage):
|
|||||||
"""Store and retrieve a single credential to and from a file."""
|
"""Store and retrieve a single credential to and from a file."""
|
||||||
|
|
||||||
def __init__(self, filename):
|
def __init__(self, filename):
|
||||||
self.filename = filename
|
self._filename = filename
|
||||||
|
|
||||||
def get(self):
|
def get(self):
|
||||||
"""Retrieve Credential from file.
|
"""Retrieve Credential from file.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
apiclient.oauth.Credentials
|
oauth2client.client.Credentials
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
f = open(self.filename, 'r')
|
f = open(self._filename, 'r')
|
||||||
credentials = pickle.loads(f.read())
|
credentials = pickle.loads(f.read())
|
||||||
f.close()
|
f.close()
|
||||||
credentials.set_store(self.put)
|
credentials.set_store(self.put)
|
||||||
except:
|
except:
|
||||||
credentials = None
|
credentials = None
|
||||||
|
|
||||||
return credentials
|
return credentials
|
||||||
|
|
||||||
def put(self, credentials):
|
def put(self, credentials):
|
||||||
@@ -40,6 +41,6 @@ class Storage(BaseStorage):
|
|||||||
Args:
|
Args:
|
||||||
credentials: Credentials, the credentials to store.
|
credentials: Credentials, the credentials to store.
|
||||||
"""
|
"""
|
||||||
f = open(self.filename, 'w')
|
f = open(self._filename, 'w')
|
||||||
f.write(pickle.dumps(credentials))
|
f.write(pickle.dumps(credentials))
|
||||||
f.close()
|
f.close()
|
||||||
|
|||||||
@@ -14,14 +14,13 @@
|
|||||||
|
|
||||||
"""Command-line tools for authenticating via OAuth 2.0
|
"""Command-line tools for authenticating via OAuth 2.0
|
||||||
|
|
||||||
Do the OAuth 2.0 Web Server dance for
|
Do the OAuth 2.0 Web Server dance for a command line application. Stores the
|
||||||
a command line application. Stores the generated
|
generated credentials in a common file that is used by other example apps in
|
||||||
credentials in a common file that is used by
|
the same directory.
|
||||||
other example apps in the same directory.
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
__author__ = 'jcgregorio@google.com (Joe Gregorio)'
|
__author__ = 'jcgregorio@google.com (Joe Gregorio)'
|
||||||
__all__ = ["run"]
|
__all__ = ['run']
|
||||||
|
|
||||||
|
|
||||||
def run(flow, storage):
|
def run(flow, storage):
|
||||||
|
|||||||
Reference in New Issue
Block a user