扫码登录,获取cookies
This commit is contained in:
@@ -0,0 +1,28 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from kombu.asynchronous import get_event_loop
|
||||
from kombu.asynchronous.http.base import Headers, Request, Response
|
||||
from kombu.asynchronous.hub import Hub
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from kombu.asynchronous.http.curl import CurlClient
|
||||
|
||||
__all__ = ('Client', 'Headers', 'Response', 'Request')
|
||||
|
||||
|
||||
def Client(hub: Hub | None = None, **kwargs: int) -> CurlClient:
|
||||
"""Create new HTTP client."""
|
||||
from .curl import CurlClient
|
||||
return CurlClient(hub, **kwargs)
|
||||
|
||||
|
||||
def get_client(hub: Hub | None = None, **kwargs: int) -> CurlClient:
|
||||
"""Get or create HTTP client bound to the current event loop."""
|
||||
hub = hub or get_event_loop()
|
||||
try:
|
||||
return hub._current_http_client
|
||||
except AttributeError:
|
||||
client = hub._current_http_client = Client(hub, **kwargs)
|
||||
return client
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
274
backend/venv/Lib/site-packages/kombu/asynchronous/http/base.py
Normal file
274
backend/venv/Lib/site-packages/kombu/asynchronous/http/base.py
Normal file
@@ -0,0 +1,274 @@
|
||||
"""Base async HTTP client implementation."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from http.client import responses
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from vine import Thenable, maybe_promise, promise
|
||||
|
||||
from kombu.exceptions import HttpError
|
||||
from kombu.utils.compat import coro
|
||||
from kombu.utils.encoding import bytes_to_str
|
||||
from kombu.utils.functional import maybe_list, memoize
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from types import TracebackType
|
||||
|
||||
__all__ = ('Headers', 'Response', 'Request')
|
||||
|
||||
PYPY = hasattr(sys, 'pypy_version_info')
|
||||
|
||||
|
||||
@memoize(maxsize=1000)
|
||||
def normalize_header(key):
|
||||
return '-'.join(p.capitalize() for p in key.split('-'))
|
||||
|
||||
|
||||
class Headers(dict):
|
||||
"""Represents a mapping of HTTP headers."""
|
||||
|
||||
# TODO: This is just a regular dict and will not perform normalization
|
||||
# when looking up keys etc.
|
||||
|
||||
#: Set when all of the headers have been read.
|
||||
complete = False
|
||||
|
||||
#: Internal attribute used to keep track of continuation lines.
|
||||
_prev_key = None
|
||||
|
||||
|
||||
@Thenable.register
|
||||
class Request:
|
||||
"""A HTTP Request.
|
||||
|
||||
Arguments:
|
||||
---------
|
||||
url (str): The URL to request.
|
||||
method (str): The HTTP method to use (defaults to ``GET``).
|
||||
|
||||
Keyword Arguments:
|
||||
-----------------
|
||||
headers (Dict, ~kombu.asynchronous.http.Headers): Optional headers for
|
||||
this request
|
||||
body (str): Optional body for this request.
|
||||
connect_timeout (float): Connection timeout in float seconds
|
||||
Default is 30.0.
|
||||
timeout (float): Time in float seconds before the request times out
|
||||
Default is 30.0.
|
||||
follow_redirects (bool): Specify if the client should follow redirects
|
||||
Enabled by default.
|
||||
max_redirects (int): Maximum number of redirects (default 6).
|
||||
use_gzip (bool): Allow the server to use gzip compression.
|
||||
Enabled by default.
|
||||
validate_cert (bool): Set to true if the server certificate should be
|
||||
verified when performing ``https://`` requests.
|
||||
Enabled by default.
|
||||
auth_username (str): Username for HTTP authentication.
|
||||
auth_password (str): Password for HTTP authentication.
|
||||
auth_mode (str): Type of HTTP authentication (``basic`` or ``digest``).
|
||||
user_agent (str): Custom user agent for this request.
|
||||
network_interface (str): Network interface to use for this request.
|
||||
on_ready (Callable): Callback to be called when the response has been
|
||||
received. Must accept single ``response`` argument.
|
||||
on_stream (Callable): Optional callback to be called every time body
|
||||
content has been read from the socket. If specified then the
|
||||
response body and buffer attributes will not be available.
|
||||
on_timeout (callable): Optional callback to be called if the request
|
||||
times out.
|
||||
on_header (Callable): Optional callback to be called for every header
|
||||
line received from the server. The signature
|
||||
is ``(headers, line)`` and note that if you want
|
||||
``response.headers`` to be populated then your callback needs to
|
||||
also call ``client.on_header(headers, line)``.
|
||||
on_prepare (Callable): Optional callback that is implementation
|
||||
specific (e.g. curl client will pass the ``curl`` instance to
|
||||
this callback).
|
||||
proxy_host (str): Optional proxy host. Note that a ``proxy_port`` must
|
||||
also be provided or a :exc:`ValueError` will be raised.
|
||||
proxy_username (str): Optional username to use when logging in
|
||||
to the proxy.
|
||||
proxy_password (str): Optional password to use when authenticating
|
||||
with the proxy server.
|
||||
ca_certs (str): Custom CA certificates file to use.
|
||||
client_key (str): Optional filename for client SSL key.
|
||||
client_cert (str): Optional filename for client SSL certificate.
|
||||
"""
|
||||
|
||||
body = user_agent = network_interface = \
|
||||
auth_username = auth_password = auth_mode = \
|
||||
proxy_host = proxy_port = proxy_username = proxy_password = \
|
||||
ca_certs = client_key = client_cert = None
|
||||
|
||||
connect_timeout = 30.0
|
||||
request_timeout = 30.0
|
||||
follow_redirects = True
|
||||
max_redirects = 6
|
||||
use_gzip = True
|
||||
validate_cert = True
|
||||
|
||||
if not PYPY: # pragma: no cover
|
||||
__slots__ = ('url', 'method', 'on_ready', 'on_timeout', 'on_stream',
|
||||
'on_prepare', 'on_header', 'headers',
|
||||
'__weakref__', '__dict__')
|
||||
|
||||
def __init__(self, url, method='GET', on_ready=None, on_timeout=None,
|
||||
on_stream=None, on_prepare=None, on_header=None,
|
||||
headers=None, **kwargs):
|
||||
self.url = url
|
||||
self.method = method or self.method
|
||||
self.on_ready = maybe_promise(on_ready) or promise()
|
||||
self.on_timeout = maybe_promise(on_timeout)
|
||||
self.on_stream = maybe_promise(on_stream)
|
||||
self.on_prepare = maybe_promise(on_prepare)
|
||||
self.on_header = maybe_promise(on_header)
|
||||
if kwargs:
|
||||
for k, v in kwargs.items():
|
||||
setattr(self, k, v)
|
||||
if not isinstance(headers, Headers):
|
||||
headers = Headers(headers or {})
|
||||
self.headers = headers
|
||||
|
||||
def then(self, callback, errback=None):
|
||||
self.on_ready.then(callback, errback)
|
||||
|
||||
def __repr__(self):
|
||||
return '<Request: {0.method} {0.url} {0.body}>'.format(self)
|
||||
|
||||
|
||||
class Response:
|
||||
"""HTTP Response.
|
||||
|
||||
Arguments
|
||||
---------
|
||||
request (~kombu.asynchronous.http.Request): See :attr:`request`.
|
||||
code (int): See :attr:`code`.
|
||||
headers (~kombu.asynchronous.http.Headers): See :attr:`headers`.
|
||||
buffer (bytes): See :attr:`buffer`
|
||||
effective_url (str): See :attr:`effective_url`.
|
||||
status (str): See :attr:`status`.
|
||||
|
||||
Attributes
|
||||
----------
|
||||
request (~kombu.asynchronous.http.Request): object used to
|
||||
get this response.
|
||||
code (int): HTTP response code (e.g. 200, 404, or 500).
|
||||
headers (~kombu.asynchronous.http.Headers): HTTP headers
|
||||
for this response.
|
||||
buffer (bytes): Socket read buffer.
|
||||
effective_url (str): The destination url for this request after
|
||||
following redirects.
|
||||
error (Exception): Error instance if the request resulted in
|
||||
a HTTP error code.
|
||||
status (str): Human equivalent of :attr:`code`,
|
||||
e.g. ``OK``, `Not found`, or 'Internal Server Error'.
|
||||
"""
|
||||
|
||||
if not PYPY: # pragma: no cover
|
||||
__slots__ = ('request', 'code', 'headers', 'buffer', 'effective_url',
|
||||
'error', 'status', '_body', '__weakref__')
|
||||
|
||||
def __init__(self, request, code, headers=None, buffer=None,
|
||||
effective_url=None, error=None, status=None):
|
||||
self.request = request
|
||||
self.code = code
|
||||
self.headers = headers if headers is not None else Headers()
|
||||
self.buffer = buffer
|
||||
self.effective_url = effective_url or request.url
|
||||
self._body = None
|
||||
|
||||
self.status = status or responses.get(self.code, 'Unknown')
|
||||
self.error = error
|
||||
if self.error is None and (self.code < 200 or self.code > 299):
|
||||
self.error = HttpError(self.code, self.status, self)
|
||||
|
||||
def raise_for_error(self):
|
||||
"""Raise if the request resulted in an HTTP error code.
|
||||
|
||||
Raises
|
||||
------
|
||||
:class:`~kombu.exceptions.HttpError`
|
||||
"""
|
||||
if self.error:
|
||||
raise self.error
|
||||
|
||||
@property
|
||||
def body(self):
|
||||
"""The full contents of the response body.
|
||||
|
||||
Note:
|
||||
----
|
||||
Accessing this property will evaluate the buffer
|
||||
and subsequent accesses will be cached.
|
||||
"""
|
||||
if self._body is None:
|
||||
if self.buffer is not None:
|
||||
self._body = self.buffer.getvalue()
|
||||
return self._body
|
||||
|
||||
# these are for compatibility with Requests
|
||||
@property
|
||||
def status_code(self):
|
||||
return self.code
|
||||
|
||||
@property
|
||||
def content(self):
|
||||
return self.body
|
||||
|
||||
|
||||
@coro
|
||||
def header_parser(keyt=normalize_header):
|
||||
while 1:
|
||||
(line, headers) = yield
|
||||
if line.startswith('HTTP/'):
|
||||
continue
|
||||
elif not line:
|
||||
headers.complete = True
|
||||
continue
|
||||
elif line[0].isspace():
|
||||
pkey = headers._prev_key
|
||||
headers[pkey] = ' '.join([headers.get(pkey) or '', line.lstrip()])
|
||||
else:
|
||||
key, value = line.split(':', 1)
|
||||
key = headers._prev_key = keyt(key)
|
||||
headers[key] = value.strip()
|
||||
|
||||
|
||||
class BaseClient:
|
||||
Headers = Headers
|
||||
Request = Request
|
||||
Response = Response
|
||||
|
||||
def __init__(self, hub, **kwargs):
|
||||
self.hub = hub
|
||||
self._header_parser = header_parser()
|
||||
|
||||
def perform(self, request, **kwargs):
|
||||
for req in maybe_list(request) or []:
|
||||
if not isinstance(req, self.Request):
|
||||
req = self.Request(req, **kwargs)
|
||||
self.add_request(req)
|
||||
|
||||
def add_request(self, request):
|
||||
raise NotImplementedError('must implement add_request')
|
||||
|
||||
def close(self):
|
||||
pass
|
||||
|
||||
def on_header(self, headers, line):
|
||||
try:
|
||||
self._header_parser.send((bytes_to_str(line), headers))
|
||||
except StopIteration:
|
||||
self._header_parser = header_parser()
|
||||
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def __exit__(
|
||||
self,
|
||||
exc_type: type[BaseException] | None,
|
||||
exc_val: BaseException | None,
|
||||
exc_tb: TracebackType | None
|
||||
) -> None:
|
||||
self.close()
|
||||
293
backend/venv/Lib/site-packages/kombu/asynchronous/http/curl.py
Normal file
293
backend/venv/Lib/site-packages/kombu/asynchronous/http/curl.py
Normal file
@@ -0,0 +1,293 @@
|
||||
"""HTTP Client using pyCurl."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections import deque
|
||||
from functools import partial
|
||||
from io import BytesIO
|
||||
from time import time
|
||||
|
||||
from kombu.asynchronous.hub import READ, WRITE, Hub, get_event_loop
|
||||
from kombu.exceptions import HttpError
|
||||
from kombu.utils.encoding import bytes_to_str
|
||||
|
||||
from .base import BaseClient
|
||||
|
||||
try:
|
||||
import pycurl
|
||||
except ImportError: # pragma: no cover
|
||||
pycurl = Curl = METH_TO_CURL = None
|
||||
else:
|
||||
from pycurl import Curl
|
||||
|
||||
METH_TO_CURL = {
|
||||
'GET': pycurl.HTTPGET,
|
||||
'POST': pycurl.POST,
|
||||
'PUT': pycurl.UPLOAD,
|
||||
'HEAD': pycurl.NOBODY,
|
||||
}
|
||||
|
||||
__all__ = ('CurlClient',)
|
||||
|
||||
DEFAULT_USER_AGENT = 'Mozilla/5.0 (compatible; pycurl)'
|
||||
EXTRA_METHODS = frozenset(['DELETE', 'OPTIONS', 'PATCH'])
|
||||
|
||||
|
||||
class CurlClient(BaseClient):
|
||||
"""Curl HTTP Client."""
|
||||
|
||||
Curl = Curl
|
||||
|
||||
def __init__(self, hub: Hub | None = None, max_clients: int = 10):
|
||||
if pycurl is None:
|
||||
raise ImportError('The curl client requires the pycurl library.')
|
||||
hub = hub or get_event_loop()
|
||||
super().__init__(hub)
|
||||
self.max_clients = max_clients
|
||||
|
||||
self._multi = pycurl.CurlMulti()
|
||||
self._multi.setopt(pycurl.M_TIMERFUNCTION, self._set_timeout)
|
||||
self._multi.setopt(pycurl.M_SOCKETFUNCTION, self._handle_socket)
|
||||
self._curls = [self.Curl() for i in range(max_clients)]
|
||||
self._free_list = self._curls[:]
|
||||
self._pending = deque()
|
||||
self._fds = {}
|
||||
|
||||
self._socket_action = self._multi.socket_action
|
||||
self._timeout_check_tref = self.hub.call_repeatedly(
|
||||
1.0, self._timeout_check,
|
||||
)
|
||||
|
||||
# pycurl 7.29.0 workaround
|
||||
dummy_curl_handle = pycurl.Curl()
|
||||
self._multi.add_handle(dummy_curl_handle)
|
||||
self._multi.remove_handle(dummy_curl_handle)
|
||||
|
||||
def close(self):
|
||||
self._timeout_check_tref.cancel()
|
||||
for _curl in self._curls:
|
||||
_curl.close()
|
||||
self._multi.close()
|
||||
|
||||
def add_request(self, request):
|
||||
self._pending.append(request)
|
||||
self._process_queue()
|
||||
self._set_timeout(0)
|
||||
return request
|
||||
|
||||
# the next two methods are used for linux/epoll workaround:
|
||||
# we temporarily remove all curl fds from hub, so curl cannot
|
||||
# close a fd which is still inside epoll
|
||||
def _pop_from_hub(self):
|
||||
for fd in self._fds:
|
||||
self.hub.remove(fd)
|
||||
|
||||
def _push_to_hub(self):
|
||||
for fd, events in self._fds.items():
|
||||
if events & READ:
|
||||
self.hub.add_reader(fd, self.on_readable, fd)
|
||||
if events & WRITE:
|
||||
self.hub.add_writer(fd, self.on_writable, fd)
|
||||
|
||||
def _handle_socket(self, event, fd, multi, data, _pycurl=pycurl):
|
||||
if event == _pycurl.POLL_REMOVE:
|
||||
if fd in self._fds:
|
||||
self._fds.pop(fd, None)
|
||||
else:
|
||||
if event == _pycurl.POLL_IN:
|
||||
self._fds[fd] = READ
|
||||
elif event == _pycurl.POLL_OUT:
|
||||
self._fds[fd] = WRITE
|
||||
elif event == _pycurl.POLL_INOUT:
|
||||
self._fds[fd] = READ | WRITE
|
||||
|
||||
def _set_timeout(self, msecs):
|
||||
self.hub.call_later(msecs, self._timeout_check)
|
||||
|
||||
def _timeout_check(self, _pycurl=pycurl):
|
||||
self._pop_from_hub()
|
||||
try:
|
||||
while 1:
|
||||
try:
|
||||
ret, _ = self._multi.socket_all()
|
||||
except pycurl.error as exc:
|
||||
ret = exc.args[0]
|
||||
if ret != _pycurl.E_CALL_MULTI_PERFORM:
|
||||
break
|
||||
finally:
|
||||
self._push_to_hub()
|
||||
self._process_pending_requests()
|
||||
|
||||
def on_readable(self, fd, _pycurl=pycurl):
|
||||
return self._on_event(fd, _pycurl.CSELECT_IN)
|
||||
|
||||
def on_writable(self, fd, _pycurl=pycurl):
|
||||
return self._on_event(fd, _pycurl.CSELECT_OUT)
|
||||
|
||||
def _on_event(self, fd, event, _pycurl=pycurl):
|
||||
self._pop_from_hub()
|
||||
try:
|
||||
while 1:
|
||||
try:
|
||||
ret, _ = self._socket_action(fd, event)
|
||||
except pycurl.error as exc:
|
||||
ret = exc.args[0]
|
||||
if ret != _pycurl.E_CALL_MULTI_PERFORM:
|
||||
break
|
||||
finally:
|
||||
self._push_to_hub()
|
||||
self._process_pending_requests()
|
||||
|
||||
def _process_pending_requests(self):
|
||||
while 1:
|
||||
q, succeeded, failed = self._multi.info_read()
|
||||
for curl in succeeded:
|
||||
self._process(curl)
|
||||
for curl, errno, reason in failed:
|
||||
self._process(curl, errno, reason)
|
||||
if q == 0:
|
||||
break
|
||||
self._process_queue()
|
||||
|
||||
def _process_queue(self):
|
||||
while 1:
|
||||
started = 0
|
||||
while self._free_list and self._pending:
|
||||
started += 1
|
||||
curl = self._free_list.pop()
|
||||
request = self._pending.popleft()
|
||||
headers = self.Headers()
|
||||
buf = BytesIO()
|
||||
curl.info = {
|
||||
'headers': headers,
|
||||
'buffer': buf,
|
||||
'request': request,
|
||||
'curl_start_time': time(),
|
||||
}
|
||||
self._setup_request(curl, request, buf, headers)
|
||||
self._multi.add_handle(curl)
|
||||
if not started:
|
||||
break
|
||||
|
||||
def _process(self, curl, errno=None, reason=None, _pycurl=pycurl):
|
||||
info, curl.info = curl.info, None
|
||||
self._multi.remove_handle(curl)
|
||||
self._free_list.append(curl)
|
||||
buffer = info['buffer']
|
||||
if errno:
|
||||
code = 599
|
||||
error = HttpError(code, reason)
|
||||
error.errno = errno
|
||||
effective_url = None
|
||||
buffer.close()
|
||||
buffer = None
|
||||
else:
|
||||
error = None
|
||||
code = curl.getinfo(_pycurl.HTTP_CODE)
|
||||
effective_url = curl.getinfo(_pycurl.EFFECTIVE_URL)
|
||||
buffer.seek(0)
|
||||
# try:
|
||||
request = info['request']
|
||||
request.on_ready(self.Response(
|
||||
request=request, code=code, headers=info['headers'],
|
||||
buffer=buffer, effective_url=effective_url, error=error,
|
||||
))
|
||||
|
||||
def _setup_request(self, curl, request, buffer, headers, _pycurl=pycurl):
|
||||
setopt = curl.setopt
|
||||
setopt(_pycurl.URL, bytes_to_str(request.url))
|
||||
|
||||
# see tornado curl client
|
||||
request.headers.setdefault('Expect', '')
|
||||
request.headers.setdefault('Pragma', '')
|
||||
|
||||
setopt(
|
||||
_pycurl.HTTPHEADER,
|
||||
['{}: {}'.format(*h) for h in request.headers.items()],
|
||||
)
|
||||
|
||||
setopt(
|
||||
_pycurl.HEADERFUNCTION,
|
||||
partial(request.on_header or self.on_header, request.headers),
|
||||
)
|
||||
setopt(
|
||||
_pycurl.WRITEFUNCTION, request.on_stream or buffer.write,
|
||||
)
|
||||
setopt(
|
||||
_pycurl.FOLLOWLOCATION, request.follow_redirects,
|
||||
)
|
||||
setopt(
|
||||
_pycurl.USERAGENT,
|
||||
bytes_to_str(request.user_agent or DEFAULT_USER_AGENT),
|
||||
)
|
||||
if request.network_interface:
|
||||
setopt(_pycurl.INTERFACE, request.network_interface)
|
||||
setopt(
|
||||
_pycurl.ENCODING, 'gzip,deflate' if request.use_gzip else 'none',
|
||||
)
|
||||
if request.proxy_host:
|
||||
if not request.proxy_port:
|
||||
raise ValueError('Request with proxy_host but no proxy_port')
|
||||
setopt(_pycurl.PROXY, request.proxy_host)
|
||||
setopt(_pycurl.PROXYPORT, request.proxy_port)
|
||||
if request.proxy_username:
|
||||
setopt(_pycurl.PROXYUSERPWD, '{}:{}'.format(
|
||||
request.proxy_username, request.proxy_password or ''))
|
||||
|
||||
setopt(_pycurl.SSL_VERIFYPEER, 1 if request.validate_cert else 0)
|
||||
setopt(_pycurl.SSL_VERIFYHOST, 2 if request.validate_cert else 0)
|
||||
if request.ca_certs is not None:
|
||||
setopt(_pycurl.CAINFO, request.ca_certs)
|
||||
|
||||
setopt(_pycurl.IPRESOLVE, pycurl.IPRESOLVE_WHATEVER)
|
||||
|
||||
for meth in METH_TO_CURL.values():
|
||||
setopt(meth, False)
|
||||
try:
|
||||
meth = METH_TO_CURL[request.method]
|
||||
except KeyError:
|
||||
curl.setopt(_pycurl.CUSTOMREQUEST, request.method)
|
||||
else:
|
||||
curl.unsetopt(_pycurl.CUSTOMREQUEST)
|
||||
setopt(meth, True)
|
||||
|
||||
if request.method in ('POST', 'PUT'):
|
||||
if not request.body:
|
||||
body = b''
|
||||
else:
|
||||
body = request.body if isinstance(request.body, bytes) else request.body.encode('utf-8')
|
||||
|
||||
reqbuffer = BytesIO(body)
|
||||
setopt(_pycurl.READFUNCTION, reqbuffer.read)
|
||||
if request.method == 'POST':
|
||||
|
||||
def ioctl(cmd):
|
||||
if cmd == _pycurl.IOCMD_RESTARTREAD:
|
||||
reqbuffer.seek(0)
|
||||
setopt(_pycurl.IOCTLFUNCTION, ioctl)
|
||||
setopt(_pycurl.POSTFIELDSIZE, len(body))
|
||||
else:
|
||||
setopt(_pycurl.INFILESIZE, len(body))
|
||||
elif request.method == 'GET':
|
||||
assert not request.body
|
||||
|
||||
if request.auth_username is not None:
|
||||
auth_mode = {
|
||||
'basic': _pycurl.HTTPAUTH_BASIC,
|
||||
'digest': _pycurl.HTTPAUTH_DIGEST
|
||||
}[request.auth_mode or 'basic']
|
||||
setopt(_pycurl.HTTPAUTH, auth_mode)
|
||||
userpwd = '{}:{}'.format(
|
||||
request.auth_username, request.auth_password or '',
|
||||
)
|
||||
setopt(_pycurl.USERPWD, userpwd)
|
||||
else:
|
||||
curl.unsetopt(_pycurl.USERPWD)
|
||||
|
||||
if request.client_cert is not None:
|
||||
setopt(_pycurl.SSLCERT, request.client_cert)
|
||||
if request.client_key is not None:
|
||||
setopt(_pycurl.SSLKEY, request.client_key)
|
||||
|
||||
if request.on_prepare is not None:
|
||||
request.on_prepare(curl)
|
||||
Reference in New Issue
Block a user