Merge branch 'master' into doc-updates
This commit is contained in:
commit
3d10b13f32
@ -5,38 +5,44 @@ version number. Breaking changes will be indicated by a change in the minor
|
|||||||
v1.5.3 (in progress)
|
v1.5.3 (in progress)
|
||||||
--------------------
|
--------------------
|
||||||
* 3.1.3 support
|
* 3.1.3 support
|
||||||
* Add v2 media_post api
|
* Added v2 media_post api
|
||||||
* 3.1.4 support
|
* 3.1.4 support
|
||||||
* Add "remote", "local" and "only_media" parameter for timelines more broadly
|
* Added "remote", "local" and "only_media" parameter for timelines more broadly
|
||||||
* Document updates to instance information api return value
|
* Documented updates to instance information api return value
|
||||||
* 3.2.0 support
|
* 3.2.0 support
|
||||||
* Add account notes API
|
* Added account notes API
|
||||||
* Add thumbnail support to media_post / media_update
|
* Added thumbnail support to media_post / media_update
|
||||||
* Document new keys in media API
|
* Documented new keys in media API
|
||||||
* 3.3.0 support
|
* 3.3.0 support
|
||||||
* Add "notify" parameter for following.
|
* Added "notify" parameter for following.
|
||||||
* Add support for timed mutes
|
* Added support for timed mutes
|
||||||
* Add support for getting an accounts features tags via account_featured_tags
|
* Added support for getting an accounts features tags via account_featured_tags
|
||||||
|
* Miscelaneous additions
|
||||||
|
* Added support for paginating by date via converting dates to snowflake IDs (on Mastodon only - thanks to edent for the suggestion)
|
||||||
|
* Added a method to revoke oauth tokens (thanks fluffy-critter)
|
||||||
* Fixes
|
* Fixes
|
||||||
* Various small and big fixes, improving reliablity and test coverage
|
* Various small and big fixes, improving reliablity and test coverage
|
||||||
|
* Changed URLs from "tootsuite" to "mastodon" in several places (thanks andypiper)
|
||||||
|
* Fixed some fields not converting to datetimes (thanks SouthFox-D)
|
||||||
|
* Improved oauth web flow support
|
||||||
|
|
||||||
v1.5.2
|
v1.5.2
|
||||||
------
|
------
|
||||||
* BREAKING CHANGE (but to a representation that was intended to be internal): Greatly improve how pagination info is stored (arittner)
|
* BREAKING CHANGE (but to a representation that was intended to be internal): Greatly improve how pagination info is stored (arittner)
|
||||||
* Add "unknown event" handler for streaming (arittner)
|
* Added "unknown event" handler for streaming (arittner)
|
||||||
* Add support for exclude_types in notifications api (MicroCheapFx)
|
* Added support for exclude_types in notifications api (MicroCheapFx)
|
||||||
* Add pagination to bookmarks (arittner)
|
* Added pagination to bookmarks (arittner)
|
||||||
* Make connecting for streaming more resilient (arittner)
|
* Made connecting for streaming more resilient (arittner)
|
||||||
* Allow specifying a user agent header (arittner)
|
* Allowed specifying a user agent header (arittner)
|
||||||
* Add support for tagged and exclude_reblogs on account_statuses api (arittner)
|
* Addeded support for tagged and exclude_reblogs on account_statuses api (arittner)
|
||||||
* Add support for reports without attached statuses (arittner)
|
* Added support for reports without attached statuses (arittner)
|
||||||
* General fixes
|
* General fixes
|
||||||
* Fix a typo in __json_fruefalse_parse (zen-tools)
|
* Fixed a typo in __json_fruefalse_parse (zen-tools)
|
||||||
* Some non-mastodon related fixes
|
* Some non-mastodon related fixes
|
||||||
* Fix a typo in error message for content_type (rinpatch
|
* Fixed a typo in error message for content_type (rinpatch
|
||||||
* Add support for specifying file name when uploading (animeavi)
|
* Added support for specifying file name when uploading (animeavi)
|
||||||
* Fix several crashes related to gotosocials version string (fwaggle)
|
* Fixed several crashes related to gotosocials version string (fwaggle)
|
||||||
* Fix an issue related to hometowns version string
|
* Fixed an issue related to hometowns version string
|
||||||
|
|
||||||
v1.5.1
|
v1.5.1
|
||||||
------
|
------
|
||||||
|
@ -513,6 +513,8 @@ class Mastodon:
|
|||||||
|
|
||||||
def auth_request_url(self, client_id=None, redirect_uris="urn:ietf:wg:oauth:2.0:oob",
|
def auth_request_url(self, client_id=None, redirect_uris="urn:ietf:wg:oauth:2.0:oob",
|
||||||
scopes=__DEFAULT_SCOPES, force_login=False):
|
scopes=__DEFAULT_SCOPES, force_login=False):
|
||||||
|
|
||||||
|
def auth_request_url(self, client_id=None, redirect_uris="urn:ietf:wg:oauth:2.0:oob", scopes=__DEFAULT_SCOPES, force_login=False, state=None):
|
||||||
"""
|
"""
|
||||||
Returns the URL that a client needs to request an OAuth grant from the server.
|
Returns the URL that a client needs to request an OAuth grant from the server.
|
||||||
|
|
||||||
@ -526,6 +528,10 @@ class Mastodon:
|
|||||||
|
|
||||||
Pass force_login if you want the user to always log in even when already logged
|
Pass force_login if you want the user to always log in even when already logged
|
||||||
into web Mastodon (i.e. when registering multiple different accounts in an app).
|
into web Mastodon (i.e. when registering multiple different accounts in an app).
|
||||||
|
|
||||||
|
State is the oauth `state`parameter to pass to the server. It is strongly suggested
|
||||||
|
to use a random, nonguessable value (i.e. nothing meaningful and no incrementing ID)
|
||||||
|
to preserve security guarantees. It can be left out for non-web login flows.
|
||||||
"""
|
"""
|
||||||
if client_id is None:
|
if client_id is None:
|
||||||
client_id = self.client_id
|
client_id = self.client_id
|
||||||
@ -540,12 +546,11 @@ class Mastodon:
|
|||||||
params['redirect_uri'] = redirect_uris
|
params['redirect_uri'] = redirect_uris
|
||||||
params['scope'] = " ".join(scopes)
|
params['scope'] = " ".join(scopes)
|
||||||
params['force_login'] = force_login
|
params['force_login'] = force_login
|
||||||
|
params['state'] = state
|
||||||
formatted_params = urlencode(params)
|
formatted_params = urlencode(params)
|
||||||
return "".join([self.api_base_url, "/oauth/authorize?", formatted_params])
|
return "".join([self.api_base_url, "/oauth/authorize?", formatted_params])
|
||||||
|
|
||||||
def log_in(self, username=None, password=None,
|
def log_in(self, username=None, password=None, code=None, redirect_uri="urn:ietf:wg:oauth:2.0:oob", refresh_token=None, scopes=__DEFAULT_SCOPES, to_file=None):
|
||||||
code=None, redirect_uri="urn:ietf:wg:oauth:2.0:oob", refresh_token=None,
|
|
||||||
scopes=__DEFAULT_SCOPES, to_file=None):
|
|
||||||
"""
|
"""
|
||||||
Get the access token for a user.
|
Get the access token for a user.
|
||||||
|
|
||||||
@ -620,6 +625,26 @@ class Mastodon:
|
|||||||
|
|
||||||
return response['access_token']
|
return response['access_token']
|
||||||
|
|
||||||
|
|
||||||
|
def revoke_access_token(self):
|
||||||
|
"""
|
||||||
|
Revoke the oauth token the user is currently authenticated with, effectively removing
|
||||||
|
the apps access and requiring the user to log in again.
|
||||||
|
"""
|
||||||
|
if self.access_token is None:
|
||||||
|
raise MastodonIllegalArgumentError("Not logged in, do not have a token to revoke.")
|
||||||
|
if self.client_id is None or self.client_secret is None:
|
||||||
|
raise MastodonIllegalArgumentError("Client authentication (id + secret) is required to revoke tokens.")
|
||||||
|
params = collections.OrderedDict([])
|
||||||
|
params['client_id'] = self.client_id
|
||||||
|
params['client_secret'] = self.client_secret
|
||||||
|
params['token'] = self.access_token
|
||||||
|
self.__api_request('POST', '/oauth/revoke', params)
|
||||||
|
|
||||||
|
# We are now logged out, clear token and logged in id
|
||||||
|
self.access_token = None
|
||||||
|
self.__logged_in_id = None
|
||||||
|
|
||||||
@api_version("2.7.0", "2.7.0", "2.7.0")
|
@api_version("2.7.0", "2.7.0", "2.7.0")
|
||||||
def create_account(self, username, password, email, agreement=False, reason=None, locale="en", scopes=__DEFAULT_SCOPES, to_file=None):
|
def create_account(self, username, password, email, agreement=False, reason=None, locale="en", scopes=__DEFAULT_SCOPES, to_file=None):
|
||||||
"""
|
"""
|
||||||
|
253
tests/cassettes/test_revoke.yaml
Normal file
253
tests/cassettes/test_revoke.yaml
Normal file
@ -0,0 +1,253 @@
|
|||||||
|
interactions:
|
||||||
|
- request:
|
||||||
|
body: username=mastodonpy_test_2%40localhost%3A3000&password=5fc638e0e53eafd9c4145b6bb852667d&redirect_uri=urn%3Aietf%3Awg%3Aoauth%3A2.0%3Aoob&grant_type=password&client_id=__MASTODON_PY_TEST_CLIENT_ID&client_secret=__MASTODON_PY_TEST_CLIENT_SECRET&scope=read+write+follow+push
|
||||||
|
headers:
|
||||||
|
Accept:
|
||||||
|
- '*/*'
|
||||||
|
Accept-Encoding:
|
||||||
|
- gzip, deflate
|
||||||
|
Connection:
|
||||||
|
- keep-alive
|
||||||
|
Content-Length:
|
||||||
|
- '271'
|
||||||
|
Content-Type:
|
||||||
|
- application/x-www-form-urlencoded
|
||||||
|
User-Agent:
|
||||||
|
- tests/v311
|
||||||
|
method: POST
|
||||||
|
uri: http://localhost:3000/oauth/token
|
||||||
|
response:
|
||||||
|
body:
|
||||||
|
string: '{"access_token":"s3ZSxpaa2Uhe9EcHankvkfaQZQGiWpdEWIhX7GuhDlk","token_type":"Bearer","scope":"read
|
||||||
|
write follow push","created_at":1668370881}'
|
||||||
|
headers:
|
||||||
|
Cache-Control:
|
||||||
|
- no-store
|
||||||
|
Content-Security-Policy:
|
||||||
|
- 'base-uri ''none''; default-src ''none''; frame-ancestors ''none''; font-src
|
||||||
|
''self'' http://localhost:3000; img-src ''self'' https: data: blob: http://localhost:3000;
|
||||||
|
style-src ''self'' http://localhost:3000 ''nonce-pCi2AQ9aKYXwS29cp2OHAg=='';
|
||||||
|
media-src ''self'' https: data: http://localhost:3000; frame-src ''self''
|
||||||
|
https:; manifest-src ''self'' http://localhost:3000; connect-src ''self''
|
||||||
|
data: blob: http://localhost:3000 http://localhost:3000 ws://localhost:4000
|
||||||
|
ws://localhost:3035 http://localhost:3035; script-src ''self'' ''unsafe-inline''
|
||||||
|
''unsafe-eval'' http://localhost:3000; child-src ''self'' blob: http://localhost:3000;
|
||||||
|
worker-src ''self'' blob: http://localhost:3000'
|
||||||
|
Content-Type:
|
||||||
|
- application/json; charset=utf-8
|
||||||
|
ETag:
|
||||||
|
- W/"b4c5259bc2edbe94aab5df0f3b8ba79a"
|
||||||
|
Pragma:
|
||||||
|
- no-cache
|
||||||
|
Referrer-Policy:
|
||||||
|
- strict-origin-when-cross-origin
|
||||||
|
Transfer-Encoding:
|
||||||
|
- chunked
|
||||||
|
Vary:
|
||||||
|
- Accept, Origin
|
||||||
|
X-Content-Type-Options:
|
||||||
|
- nosniff
|
||||||
|
X-Download-Options:
|
||||||
|
- noopen
|
||||||
|
X-Frame-Options:
|
||||||
|
- SAMEORIGIN
|
||||||
|
X-Permitted-Cross-Domain-Policies:
|
||||||
|
- none
|
||||||
|
X-Request-Id:
|
||||||
|
- 086350f4-00fc-4d82-a5ce-2b557df59682
|
||||||
|
X-Runtime:
|
||||||
|
- '0.040539'
|
||||||
|
X-XSS-Protection:
|
||||||
|
- 1; mode=block
|
||||||
|
status:
|
||||||
|
code: 200
|
||||||
|
message: OK
|
||||||
|
- request:
|
||||||
|
body: client_id=__MASTODON_PY_TEST_CLIENT_ID&client_secret=__MASTODON_PY_TEST_CLIENT_SECRET&token=s3ZSxpaa2Uhe9EcHankvkfaQZQGiWpdEWIhX7GuhDlk
|
||||||
|
headers:
|
||||||
|
Accept:
|
||||||
|
- '*/*'
|
||||||
|
Accept-Encoding:
|
||||||
|
- gzip, deflate
|
||||||
|
Authorization:
|
||||||
|
- Bearer s3ZSxpaa2Uhe9EcHankvkfaQZQGiWpdEWIhX7GuhDlk
|
||||||
|
Connection:
|
||||||
|
- keep-alive
|
||||||
|
Content-Length:
|
||||||
|
- '135'
|
||||||
|
Content-Type:
|
||||||
|
- application/x-www-form-urlencoded
|
||||||
|
User-Agent:
|
||||||
|
- tests/v311
|
||||||
|
method: POST
|
||||||
|
uri: http://localhost:3000/oauth/revoke
|
||||||
|
response:
|
||||||
|
body:
|
||||||
|
string: '{}'
|
||||||
|
headers:
|
||||||
|
Cache-Control:
|
||||||
|
- max-age=0, private, must-revalidate
|
||||||
|
Content-Security-Policy:
|
||||||
|
- 'base-uri ''none''; default-src ''none''; frame-ancestors ''none''; font-src
|
||||||
|
''self'' http://localhost:3000; img-src ''self'' https: data: blob: http://localhost:3000;
|
||||||
|
style-src ''self'' http://localhost:3000 ''nonce-PqM4ChK427oGZ5jIKprkYQ=='';
|
||||||
|
media-src ''self'' https: data: http://localhost:3000; frame-src ''self''
|
||||||
|
https:; manifest-src ''self'' http://localhost:3000; connect-src ''self''
|
||||||
|
data: blob: http://localhost:3000 http://localhost:3000 ws://localhost:4000
|
||||||
|
ws://localhost:3035 http://localhost:3035; script-src ''self'' ''unsafe-inline''
|
||||||
|
''unsafe-eval'' http://localhost:3000; child-src ''self'' blob: http://localhost:3000;
|
||||||
|
worker-src ''self'' blob: http://localhost:3000'
|
||||||
|
Content-Type:
|
||||||
|
- application/json; charset=utf-8
|
||||||
|
ETag:
|
||||||
|
- W/"44136fa355b3678a1146ad16f7e8649e"
|
||||||
|
Referrer-Policy:
|
||||||
|
- strict-origin-when-cross-origin
|
||||||
|
Transfer-Encoding:
|
||||||
|
- chunked
|
||||||
|
Vary:
|
||||||
|
- Accept
|
||||||
|
X-Content-Type-Options:
|
||||||
|
- nosniff
|
||||||
|
X-Download-Options:
|
||||||
|
- noopen
|
||||||
|
X-Frame-Options:
|
||||||
|
- SAMEORIGIN
|
||||||
|
X-Permitted-Cross-Domain-Policies:
|
||||||
|
- none
|
||||||
|
X-Request-Id:
|
||||||
|
- 36ec7e63-b15b-487a-aa4a-c396db945794
|
||||||
|
X-Runtime:
|
||||||
|
- '0.010431'
|
||||||
|
X-XSS-Protection:
|
||||||
|
- 1; mode=block
|
||||||
|
status:
|
||||||
|
code: 200
|
||||||
|
message: OK
|
||||||
|
- request:
|
||||||
|
body: status=illegal+access+detected
|
||||||
|
headers:
|
||||||
|
Accept:
|
||||||
|
- '*/*'
|
||||||
|
Accept-Encoding:
|
||||||
|
- gzip, deflate
|
||||||
|
Connection:
|
||||||
|
- keep-alive
|
||||||
|
Content-Length:
|
||||||
|
- '30'
|
||||||
|
Content-Type:
|
||||||
|
- application/x-www-form-urlencoded
|
||||||
|
User-Agent:
|
||||||
|
- tests/v311
|
||||||
|
method: POST
|
||||||
|
uri: http://localhost:3000/api/v1/statuses
|
||||||
|
response:
|
||||||
|
body:
|
||||||
|
string: '{"error":"The access token is invalid"}'
|
||||||
|
headers:
|
||||||
|
Cache-Control:
|
||||||
|
- no-store
|
||||||
|
Content-Security-Policy:
|
||||||
|
- 'base-uri ''none''; default-src ''none''; frame-ancestors ''none''; font-src
|
||||||
|
''self'' http://localhost:3000; img-src ''self'' https: data: blob: http://localhost:3000;
|
||||||
|
style-src ''self'' http://localhost:3000 ''nonce-UClOh6+Y0zf3a4O/ysqT/w=='';
|
||||||
|
media-src ''self'' https: data: http://localhost:3000; frame-src ''self''
|
||||||
|
https:; manifest-src ''self'' http://localhost:3000; connect-src ''self''
|
||||||
|
data: blob: http://localhost:3000 http://localhost:3000 ws://localhost:4000
|
||||||
|
ws://localhost:3035 http://localhost:3035; script-src ''self'' ''unsafe-inline''
|
||||||
|
''unsafe-eval'' http://localhost:3000; child-src ''self'' blob: http://localhost:3000;
|
||||||
|
worker-src ''self'' blob: http://localhost:3000'
|
||||||
|
Content-Type:
|
||||||
|
- application/json; charset=utf-8
|
||||||
|
Pragma:
|
||||||
|
- no-cache
|
||||||
|
Referrer-Policy:
|
||||||
|
- strict-origin-when-cross-origin
|
||||||
|
Transfer-Encoding:
|
||||||
|
- chunked
|
||||||
|
Vary:
|
||||||
|
- Accept, Origin
|
||||||
|
WWW-Authenticate:
|
||||||
|
- Bearer realm="Doorkeeper", error="invalid_token", error_description="The access
|
||||||
|
token is invalid"
|
||||||
|
X-Content-Type-Options:
|
||||||
|
- nosniff
|
||||||
|
X-Download-Options:
|
||||||
|
- noopen
|
||||||
|
X-Frame-Options:
|
||||||
|
- SAMEORIGIN
|
||||||
|
X-Permitted-Cross-Domain-Policies:
|
||||||
|
- none
|
||||||
|
X-Request-Id:
|
||||||
|
- ab1f9d04-149b-431a-b2b0-79921d45f3bc
|
||||||
|
X-Runtime:
|
||||||
|
- '0.005292'
|
||||||
|
X-XSS-Protection:
|
||||||
|
- 1; mode=block
|
||||||
|
status:
|
||||||
|
code: 401
|
||||||
|
message: Unauthorized
|
||||||
|
- request:
|
||||||
|
body: status=illegal+access+detected
|
||||||
|
headers:
|
||||||
|
Accept:
|
||||||
|
- '*/*'
|
||||||
|
Accept-Encoding:
|
||||||
|
- gzip, deflate
|
||||||
|
Connection:
|
||||||
|
- keep-alive
|
||||||
|
Content-Length:
|
||||||
|
- '30'
|
||||||
|
Content-Type:
|
||||||
|
- application/x-www-form-urlencoded
|
||||||
|
User-Agent:
|
||||||
|
- tests/v311
|
||||||
|
method: POST
|
||||||
|
uri: http://localhost:3000/api/v1/statuses
|
||||||
|
response:
|
||||||
|
body:
|
||||||
|
string: '{"error":"The access token is invalid"}'
|
||||||
|
headers:
|
||||||
|
Cache-Control:
|
||||||
|
- no-store
|
||||||
|
Content-Security-Policy:
|
||||||
|
- 'base-uri ''none''; default-src ''none''; frame-ancestors ''none''; font-src
|
||||||
|
''self'' http://localhost:3000; img-src ''self'' https: data: blob: http://localhost:3000;
|
||||||
|
style-src ''self'' http://localhost:3000 ''nonce-ZTmQUUs9q7lX74Aa3bTgzA=='';
|
||||||
|
media-src ''self'' https: data: http://localhost:3000; frame-src ''self''
|
||||||
|
https:; manifest-src ''self'' http://localhost:3000; connect-src ''self''
|
||||||
|
data: blob: http://localhost:3000 http://localhost:3000 ws://localhost:4000
|
||||||
|
ws://localhost:3035 http://localhost:3035; script-src ''self'' ''unsafe-inline''
|
||||||
|
''unsafe-eval'' http://localhost:3000; child-src ''self'' blob: http://localhost:3000;
|
||||||
|
worker-src ''self'' blob: http://localhost:3000'
|
||||||
|
Content-Type:
|
||||||
|
- application/json; charset=utf-8
|
||||||
|
Pragma:
|
||||||
|
- no-cache
|
||||||
|
Referrer-Policy:
|
||||||
|
- strict-origin-when-cross-origin
|
||||||
|
Transfer-Encoding:
|
||||||
|
- chunked
|
||||||
|
Vary:
|
||||||
|
- Accept, Origin
|
||||||
|
WWW-Authenticate:
|
||||||
|
- Bearer realm="Doorkeeper", error="invalid_token", error_description="The access
|
||||||
|
token is invalid"
|
||||||
|
X-Content-Type-Options:
|
||||||
|
- nosniff
|
||||||
|
X-Download-Options:
|
||||||
|
- noopen
|
||||||
|
X-Frame-Options:
|
||||||
|
- SAMEORIGIN
|
||||||
|
X-Permitted-Cross-Domain-Policies:
|
||||||
|
- none
|
||||||
|
X-Request-Id:
|
||||||
|
- 9a7ad262-0d94-4df6-92a5-a670d6515979
|
||||||
|
X-Runtime:
|
||||||
|
- '0.004613'
|
||||||
|
X-XSS-Protection:
|
||||||
|
- 1; mode=block
|
||||||
|
status:
|
||||||
|
code: 401
|
||||||
|
message: Unauthorized
|
||||||
|
version: 1
|
@ -22,14 +22,37 @@ def test_log_in_none(api_anonymous):
|
|||||||
with pytest.raises(MastodonIllegalArgumentError):
|
with pytest.raises(MastodonIllegalArgumentError):
|
||||||
api_anonymous.log_in()
|
api_anonymous.log_in()
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.vcr()
|
@pytest.mark.vcr()
|
||||||
def test_log_in_password(api_anonymous):
|
def test_log_in_password(api_anonymous):
|
||||||
token = api_anonymous.log_in(
|
token = api_anonymous.log_in(
|
||||||
username='mastodonpy_test_2@localhost:3000',
|
username='mastodonpy_test_2@localhost:3000',
|
||||||
password='5fc638e0e53eafd9c4145b6bb852667d')
|
password='5fc638e0e53eafd9c4145b6bb852667d'
|
||||||
|
)
|
||||||
assert token
|
assert token
|
||||||
|
|
||||||
|
@pytest.mark.vcr()
|
||||||
|
def test_revoke(api_anonymous):
|
||||||
|
token = api_anonymous.log_in(
|
||||||
|
username='mastodonpy_test_2@localhost:3000',
|
||||||
|
password='5fc638e0e53eafd9c4145b6bb852667d'
|
||||||
|
)
|
||||||
|
api_anonymous.revoke_access_token()
|
||||||
|
|
||||||
|
try:
|
||||||
|
api_anonymous.toot("illegal access detected")
|
||||||
|
assert False
|
||||||
|
except Exception as e:
|
||||||
|
print(e)
|
||||||
|
pass
|
||||||
|
|
||||||
|
api_revoked_token = Mastodon(access_token = token)
|
||||||
|
try:
|
||||||
|
api_anonymous.toot("illegal access detected")
|
||||||
|
assert False
|
||||||
|
except Exception as e:
|
||||||
|
print(e)
|
||||||
|
pass
|
||||||
|
|
||||||
@pytest.mark.vcr()
|
@pytest.mark.vcr()
|
||||||
def test_log_in_password_incorrect(api_anonymous):
|
def test_log_in_password_incorrect(api_anonymous):
|
||||||
with pytest.raises(MastodonIllegalArgumentError):
|
with pytest.raises(MastodonIllegalArgumentError):
|
||||||
|
Loading…
x
Reference in New Issue
Block a user