mirror of
https://mau.dev/maunium/synapse.git
synced 2024-10-01 01:36:05 -04:00
Add a shadow-banned flag to users. (#8092)
This commit is contained in:
parent
b069b78bb4
commit
ac77cdb64e
1
changelog.d/8092.feature
Normal file
1
changelog.d/8092.feature
Normal file
@ -0,0 +1 @@
|
|||||||
|
Add support for shadow-banning users (ignoring any message send requests).
|
@ -213,6 +213,7 @@ class Auth(object):
|
|||||||
user = user_info["user"]
|
user = user_info["user"]
|
||||||
token_id = user_info["token_id"]
|
token_id = user_info["token_id"]
|
||||||
is_guest = user_info["is_guest"]
|
is_guest = user_info["is_guest"]
|
||||||
|
shadow_banned = user_info["shadow_banned"]
|
||||||
|
|
||||||
# Deny the request if the user account has expired.
|
# Deny the request if the user account has expired.
|
||||||
if self._account_validity.enabled and not allow_expired:
|
if self._account_validity.enabled and not allow_expired:
|
||||||
@ -252,7 +253,12 @@ class Auth(object):
|
|||||||
opentracing.set_tag("device_id", device_id)
|
opentracing.set_tag("device_id", device_id)
|
||||||
|
|
||||||
return synapse.types.create_requester(
|
return synapse.types.create_requester(
|
||||||
user, token_id, is_guest, device_id, app_service=app_service
|
user,
|
||||||
|
token_id,
|
||||||
|
is_guest,
|
||||||
|
shadow_banned,
|
||||||
|
device_id,
|
||||||
|
app_service=app_service,
|
||||||
)
|
)
|
||||||
except KeyError:
|
except KeyError:
|
||||||
raise MissingClientTokenError()
|
raise MissingClientTokenError()
|
||||||
@ -297,6 +303,7 @@ class Auth(object):
|
|||||||
dict that includes:
|
dict that includes:
|
||||||
`user` (UserID)
|
`user` (UserID)
|
||||||
`is_guest` (bool)
|
`is_guest` (bool)
|
||||||
|
`shadow_banned` (bool)
|
||||||
`token_id` (int|None): access token id. May be None if guest
|
`token_id` (int|None): access token id. May be None if guest
|
||||||
`device_id` (str|None): device corresponding to access token
|
`device_id` (str|None): device corresponding to access token
|
||||||
Raises:
|
Raises:
|
||||||
@ -356,6 +363,7 @@ class Auth(object):
|
|||||||
ret = {
|
ret = {
|
||||||
"user": user,
|
"user": user,
|
||||||
"is_guest": True,
|
"is_guest": True,
|
||||||
|
"shadow_banned": False,
|
||||||
"token_id": None,
|
"token_id": None,
|
||||||
# all guests get the same device id
|
# all guests get the same device id
|
||||||
"device_id": GUEST_DEVICE_ID,
|
"device_id": GUEST_DEVICE_ID,
|
||||||
@ -365,6 +373,7 @@ class Auth(object):
|
|||||||
ret = {
|
ret = {
|
||||||
"user": user,
|
"user": user,
|
||||||
"is_guest": False,
|
"is_guest": False,
|
||||||
|
"shadow_banned": False,
|
||||||
"token_id": None,
|
"token_id": None,
|
||||||
"device_id": None,
|
"device_id": None,
|
||||||
}
|
}
|
||||||
@ -488,6 +497,7 @@ class Auth(object):
|
|||||||
"user": UserID.from_string(ret.get("name")),
|
"user": UserID.from_string(ret.get("name")),
|
||||||
"token_id": ret.get("token_id", None),
|
"token_id": ret.get("token_id", None),
|
||||||
"is_guest": False,
|
"is_guest": False,
|
||||||
|
"shadow_banned": ret.get("shadow_banned"),
|
||||||
"device_id": ret.get("device_id"),
|
"device_id": ret.get("device_id"),
|
||||||
"valid_until_ms": ret.get("valid_until_ms"),
|
"valid_until_ms": ret.get("valid_until_ms"),
|
||||||
}
|
}
|
||||||
|
@ -142,6 +142,7 @@ class RegistrationHandler(BaseHandler):
|
|||||||
address=None,
|
address=None,
|
||||||
bind_emails=[],
|
bind_emails=[],
|
||||||
by_admin=False,
|
by_admin=False,
|
||||||
|
shadow_banned=False,
|
||||||
):
|
):
|
||||||
"""Registers a new client on the server.
|
"""Registers a new client on the server.
|
||||||
|
|
||||||
@ -159,6 +160,7 @@ class RegistrationHandler(BaseHandler):
|
|||||||
bind_emails (List[str]): list of emails to bind to this account.
|
bind_emails (List[str]): list of emails to bind to this account.
|
||||||
by_admin (bool): True if this registration is being made via the
|
by_admin (bool): True if this registration is being made via the
|
||||||
admin api, otherwise False.
|
admin api, otherwise False.
|
||||||
|
shadow_banned (bool): Shadow-ban the created user.
|
||||||
Returns:
|
Returns:
|
||||||
str: user_id
|
str: user_id
|
||||||
Raises:
|
Raises:
|
||||||
@ -194,6 +196,7 @@ class RegistrationHandler(BaseHandler):
|
|||||||
admin=admin,
|
admin=admin,
|
||||||
user_type=user_type,
|
user_type=user_type,
|
||||||
address=address,
|
address=address,
|
||||||
|
shadow_banned=shadow_banned,
|
||||||
)
|
)
|
||||||
|
|
||||||
if self.hs.config.user_directory_search_all_users:
|
if self.hs.config.user_directory_search_all_users:
|
||||||
@ -224,6 +227,7 @@ class RegistrationHandler(BaseHandler):
|
|||||||
make_guest=make_guest,
|
make_guest=make_guest,
|
||||||
create_profile_with_displayname=default_display_name,
|
create_profile_with_displayname=default_display_name,
|
||||||
address=address,
|
address=address,
|
||||||
|
shadow_banned=shadow_banned,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Successfully registered
|
# Successfully registered
|
||||||
@ -529,6 +533,7 @@ class RegistrationHandler(BaseHandler):
|
|||||||
admin=False,
|
admin=False,
|
||||||
user_type=None,
|
user_type=None,
|
||||||
address=None,
|
address=None,
|
||||||
|
shadow_banned=False,
|
||||||
):
|
):
|
||||||
"""Register user in the datastore.
|
"""Register user in the datastore.
|
||||||
|
|
||||||
@ -546,6 +551,7 @@ class RegistrationHandler(BaseHandler):
|
|||||||
user_type (str|None): type of user. One of the values from
|
user_type (str|None): type of user. One of the values from
|
||||||
api.constants.UserTypes, or None for a normal user.
|
api.constants.UserTypes, or None for a normal user.
|
||||||
address (str|None): the IP address used to perform the registration.
|
address (str|None): the IP address used to perform the registration.
|
||||||
|
shadow_banned (bool): Whether to shadow-ban the user
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Awaitable
|
Awaitable
|
||||||
@ -561,6 +567,7 @@ class RegistrationHandler(BaseHandler):
|
|||||||
admin=admin,
|
admin=admin,
|
||||||
user_type=user_type,
|
user_type=user_type,
|
||||||
address=address,
|
address=address,
|
||||||
|
shadow_banned=shadow_banned,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
return self.store.register_user(
|
return self.store.register_user(
|
||||||
@ -572,6 +579,7 @@ class RegistrationHandler(BaseHandler):
|
|||||||
create_profile_with_displayname=create_profile_with_displayname,
|
create_profile_with_displayname=create_profile_with_displayname,
|
||||||
admin=admin,
|
admin=admin,
|
||||||
user_type=user_type,
|
user_type=user_type,
|
||||||
|
shadow_banned=shadow_banned,
|
||||||
)
|
)
|
||||||
|
|
||||||
async def register_device(
|
async def register_device(
|
||||||
|
@ -44,6 +44,7 @@ class ReplicationRegisterServlet(ReplicationEndpoint):
|
|||||||
admin,
|
admin,
|
||||||
user_type,
|
user_type,
|
||||||
address,
|
address,
|
||||||
|
shadow_banned,
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
Args:
|
Args:
|
||||||
@ -60,6 +61,7 @@ class ReplicationRegisterServlet(ReplicationEndpoint):
|
|||||||
user_type (str|None): type of user. One of the values from
|
user_type (str|None): type of user. One of the values from
|
||||||
api.constants.UserTypes, or None for a normal user.
|
api.constants.UserTypes, or None for a normal user.
|
||||||
address (str|None): the IP address used to perform the regitration.
|
address (str|None): the IP address used to perform the regitration.
|
||||||
|
shadow_banned (bool): Whether to shadow-ban the user
|
||||||
"""
|
"""
|
||||||
return {
|
return {
|
||||||
"password_hash": password_hash,
|
"password_hash": password_hash,
|
||||||
@ -70,6 +72,7 @@ class ReplicationRegisterServlet(ReplicationEndpoint):
|
|||||||
"admin": admin,
|
"admin": admin,
|
||||||
"user_type": user_type,
|
"user_type": user_type,
|
||||||
"address": address,
|
"address": address,
|
||||||
|
"shadow_banned": shadow_banned,
|
||||||
}
|
}
|
||||||
|
|
||||||
async def _handle_request(self, request, user_id):
|
async def _handle_request(self, request, user_id):
|
||||||
@ -87,6 +90,7 @@ class ReplicationRegisterServlet(ReplicationEndpoint):
|
|||||||
admin=content["admin"],
|
admin=content["admin"],
|
||||||
user_type=content["user_type"],
|
user_type=content["user_type"],
|
||||||
address=content["address"],
|
address=content["address"],
|
||||||
|
shadow_banned=content["shadow_banned"],
|
||||||
)
|
)
|
||||||
|
|
||||||
return 200, {}
|
return 200, {}
|
||||||
|
@ -304,7 +304,7 @@ class RegistrationWorkerStore(SQLBaseStore):
|
|||||||
|
|
||||||
def _query_for_auth(self, txn, token):
|
def _query_for_auth(self, txn, token):
|
||||||
sql = (
|
sql = (
|
||||||
"SELECT users.name, users.is_guest, access_tokens.id as token_id,"
|
"SELECT users.name, users.is_guest, users.shadow_banned, access_tokens.id as token_id,"
|
||||||
" access_tokens.device_id, access_tokens.valid_until_ms"
|
" access_tokens.device_id, access_tokens.valid_until_ms"
|
||||||
" FROM users"
|
" FROM users"
|
||||||
" INNER JOIN access_tokens on users.name = access_tokens.user_id"
|
" INNER JOIN access_tokens on users.name = access_tokens.user_id"
|
||||||
@ -952,6 +952,7 @@ class RegistrationStore(RegistrationBackgroundUpdateStore):
|
|||||||
create_profile_with_displayname=None,
|
create_profile_with_displayname=None,
|
||||||
admin=False,
|
admin=False,
|
||||||
user_type=None,
|
user_type=None,
|
||||||
|
shadow_banned=False,
|
||||||
):
|
):
|
||||||
"""Attempts to register an account.
|
"""Attempts to register an account.
|
||||||
|
|
||||||
@ -968,6 +969,8 @@ class RegistrationStore(RegistrationBackgroundUpdateStore):
|
|||||||
admin (boolean): is an admin user?
|
admin (boolean): is an admin user?
|
||||||
user_type (str|None): type of user. One of the values from
|
user_type (str|None): type of user. One of the values from
|
||||||
api.constants.UserTypes, or None for a normal user.
|
api.constants.UserTypes, or None for a normal user.
|
||||||
|
shadow_banned (bool): Whether the user is shadow-banned,
|
||||||
|
i.e. they may be told their requests succeeded but we ignore them.
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
StoreError if the user_id could not be registered.
|
StoreError if the user_id could not be registered.
|
||||||
@ -986,6 +989,7 @@ class RegistrationStore(RegistrationBackgroundUpdateStore):
|
|||||||
create_profile_with_displayname,
|
create_profile_with_displayname,
|
||||||
admin,
|
admin,
|
||||||
user_type,
|
user_type,
|
||||||
|
shadow_banned,
|
||||||
)
|
)
|
||||||
|
|
||||||
def _register_user(
|
def _register_user(
|
||||||
@ -999,6 +1003,7 @@ class RegistrationStore(RegistrationBackgroundUpdateStore):
|
|||||||
create_profile_with_displayname,
|
create_profile_with_displayname,
|
||||||
admin,
|
admin,
|
||||||
user_type,
|
user_type,
|
||||||
|
shadow_banned,
|
||||||
):
|
):
|
||||||
user_id_obj = UserID.from_string(user_id)
|
user_id_obj = UserID.from_string(user_id)
|
||||||
|
|
||||||
@ -1028,6 +1033,7 @@ class RegistrationStore(RegistrationBackgroundUpdateStore):
|
|||||||
"appservice_id": appservice_id,
|
"appservice_id": appservice_id,
|
||||||
"admin": 1 if admin else 0,
|
"admin": 1 if admin else 0,
|
||||||
"user_type": user_type,
|
"user_type": user_type,
|
||||||
|
"shadow_banned": shadow_banned,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
@ -1042,6 +1048,7 @@ class RegistrationStore(RegistrationBackgroundUpdateStore):
|
|||||||
"appservice_id": appservice_id,
|
"appservice_id": appservice_id,
|
||||||
"admin": 1 if admin else 0,
|
"admin": 1 if admin else 0,
|
||||||
"user_type": user_type,
|
"user_type": user_type,
|
||||||
|
"shadow_banned": shadow_banned,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -0,0 +1,18 @@
|
|||||||
|
/* Copyright 2020 The Matrix.org Foundation C.I.C
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
-- A shadow-banned user may be told that their requests succeeded when they were
|
||||||
|
-- actually ignored.
|
||||||
|
ALTER TABLE users ADD COLUMN shadow_banned BOOLEAN;
|
@ -51,7 +51,15 @@ JsonDict = Dict[str, Any]
|
|||||||
|
|
||||||
class Requester(
|
class Requester(
|
||||||
namedtuple(
|
namedtuple(
|
||||||
"Requester", ["user", "access_token_id", "is_guest", "device_id", "app_service"]
|
"Requester",
|
||||||
|
[
|
||||||
|
"user",
|
||||||
|
"access_token_id",
|
||||||
|
"is_guest",
|
||||||
|
"shadow_banned",
|
||||||
|
"device_id",
|
||||||
|
"app_service",
|
||||||
|
],
|
||||||
)
|
)
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
@ -62,6 +70,7 @@ class Requester(
|
|||||||
access_token_id (int|None): *ID* of the access token used for this
|
access_token_id (int|None): *ID* of the access token used for this
|
||||||
request, or None if it came via the appservice API or similar
|
request, or None if it came via the appservice API or similar
|
||||||
is_guest (bool): True if the user making this request is a guest user
|
is_guest (bool): True if the user making this request is a guest user
|
||||||
|
shadow_banned (bool): True if the user making this request has been shadow-banned.
|
||||||
device_id (str|None): device_id which was set at authentication time
|
device_id (str|None): device_id which was set at authentication time
|
||||||
app_service (ApplicationService|None): the AS requesting on behalf of the user
|
app_service (ApplicationService|None): the AS requesting on behalf of the user
|
||||||
"""
|
"""
|
||||||
@ -77,6 +86,7 @@ class Requester(
|
|||||||
"user_id": self.user.to_string(),
|
"user_id": self.user.to_string(),
|
||||||
"access_token_id": self.access_token_id,
|
"access_token_id": self.access_token_id,
|
||||||
"is_guest": self.is_guest,
|
"is_guest": self.is_guest,
|
||||||
|
"shadow_banned": self.shadow_banned,
|
||||||
"device_id": self.device_id,
|
"device_id": self.device_id,
|
||||||
"app_server_id": self.app_service.id if self.app_service else None,
|
"app_server_id": self.app_service.id if self.app_service else None,
|
||||||
}
|
}
|
||||||
@ -101,13 +111,19 @@ class Requester(
|
|||||||
user=UserID.from_string(input["user_id"]),
|
user=UserID.from_string(input["user_id"]),
|
||||||
access_token_id=input["access_token_id"],
|
access_token_id=input["access_token_id"],
|
||||||
is_guest=input["is_guest"],
|
is_guest=input["is_guest"],
|
||||||
|
shadow_banned=input["shadow_banned"],
|
||||||
device_id=input["device_id"],
|
device_id=input["device_id"],
|
||||||
app_service=appservice,
|
app_service=appservice,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def create_requester(
|
def create_requester(
|
||||||
user_id, access_token_id=None, is_guest=False, device_id=None, app_service=None
|
user_id,
|
||||||
|
access_token_id=None,
|
||||||
|
is_guest=False,
|
||||||
|
shadow_banned=False,
|
||||||
|
device_id=None,
|
||||||
|
app_service=None,
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
Create a new ``Requester`` object
|
Create a new ``Requester`` object
|
||||||
@ -117,6 +133,7 @@ def create_requester(
|
|||||||
access_token_id (int|None): *ID* of the access token used for this
|
access_token_id (int|None): *ID* of the access token used for this
|
||||||
request, or None if it came via the appservice API or similar
|
request, or None if it came via the appservice API or similar
|
||||||
is_guest (bool): True if the user making this request is a guest user
|
is_guest (bool): True if the user making this request is a guest user
|
||||||
|
shadow_banned (bool): True if the user making this request is shadow-banned.
|
||||||
device_id (str|None): device_id which was set at authentication time
|
device_id (str|None): device_id which was set at authentication time
|
||||||
app_service (ApplicationService|None): the AS requesting on behalf of the user
|
app_service (ApplicationService|None): the AS requesting on behalf of the user
|
||||||
|
|
||||||
@ -125,7 +142,9 @@ def create_requester(
|
|||||||
"""
|
"""
|
||||||
if not isinstance(user_id, UserID):
|
if not isinstance(user_id, UserID):
|
||||||
user_id = UserID.from_string(user_id)
|
user_id = UserID.from_string(user_id)
|
||||||
return Requester(user_id, access_token_id, is_guest, device_id, app_service)
|
return Requester(
|
||||||
|
user_id, access_token_id, is_guest, shadow_banned, device_id, app_service
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def get_domain_from_id(string):
|
def get_domain_from_id(string):
|
||||||
|
@ -38,7 +38,7 @@ class CleanupExtremBackgroundUpdateStoreTestCase(HomeserverTestCase):
|
|||||||
|
|
||||||
# Create a test user and room
|
# Create a test user and room
|
||||||
self.user = UserID("alice", "test")
|
self.user = UserID("alice", "test")
|
||||||
self.requester = Requester(self.user, None, False, None, None)
|
self.requester = Requester(self.user, None, False, False, None, None)
|
||||||
info, _ = self.get_success(self.room_creator.create_room(self.requester, {}))
|
info, _ = self.get_success(self.room_creator.create_room(self.requester, {}))
|
||||||
self.room_id = info["room_id"]
|
self.room_id = info["room_id"]
|
||||||
|
|
||||||
@ -260,7 +260,7 @@ class CleanupExtremDummyEventsTestCase(HomeserverTestCase):
|
|||||||
# Create a test user and room
|
# Create a test user and room
|
||||||
self.user = UserID.from_string(self.register_user("user1", "password"))
|
self.user = UserID.from_string(self.register_user("user1", "password"))
|
||||||
self.token1 = self.login("user1", "password")
|
self.token1 = self.login("user1", "password")
|
||||||
self.requester = Requester(self.user, None, False, None, None)
|
self.requester = Requester(self.user, None, False, False, None, None)
|
||||||
info, _ = self.get_success(self.room_creator.create_room(self.requester, {}))
|
info, _ = self.get_success(self.room_creator.create_room(self.requester, {}))
|
||||||
self.room_id = info["room_id"]
|
self.room_id = info["room_id"]
|
||||||
self.event_creator = homeserver.get_event_creation_handler()
|
self.event_creator = homeserver.get_event_creation_handler()
|
||||||
|
@ -27,7 +27,7 @@ class ExtremStatisticsTestCase(HomeserverTestCase):
|
|||||||
room_creator = self.hs.get_room_creation_handler()
|
room_creator = self.hs.get_room_creation_handler()
|
||||||
|
|
||||||
user = UserID("alice", "test")
|
user = UserID("alice", "test")
|
||||||
requester = Requester(user, None, False, None, None)
|
requester = Requester(user, None, False, False, None, None)
|
||||||
|
|
||||||
# Real events, forward extremities
|
# Real events, forward extremities
|
||||||
events = [(3, 2), (6, 2), (4, 6)]
|
events = [(3, 2), (6, 2), (4, 6)]
|
||||||
|
@ -187,7 +187,7 @@ class CurrentStateMembershipUpdateTestCase(unittest.HomeserverTestCase):
|
|||||||
|
|
||||||
# Now let's create a room, which will insert a membership
|
# Now let's create a room, which will insert a membership
|
||||||
user = UserID("alice", "test")
|
user = UserID("alice", "test")
|
||||||
requester = Requester(user, None, False, None, None)
|
requester = Requester(user, None, False, False, None, None)
|
||||||
self.get_success(self.room_creator.create_room(requester, {}))
|
self.get_success(self.room_creator.create_room(requester, {}))
|
||||||
|
|
||||||
# Register the background update to run again.
|
# Register the background update to run again.
|
||||||
|
@ -42,7 +42,7 @@ class MessageAcceptTests(unittest.HomeserverTestCase):
|
|||||||
)
|
)
|
||||||
|
|
||||||
user_id = UserID("us", "test")
|
user_id = UserID("us", "test")
|
||||||
our_user = Requester(user_id, None, False, None, None)
|
our_user = Requester(user_id, None, False, False, None, None)
|
||||||
room_creator = self.homeserver.get_room_creation_handler()
|
room_creator = self.homeserver.get_room_creation_handler()
|
||||||
room_deferred = ensureDeferred(
|
room_deferred = ensureDeferred(
|
||||||
room_creator.create_room(
|
room_creator.create_room(
|
||||||
|
@ -250,7 +250,11 @@ class HomeserverTestCase(TestCase):
|
|||||||
|
|
||||||
async def get_user_by_req(request, allow_guest=False, rights="access"):
|
async def get_user_by_req(request, allow_guest=False, rights="access"):
|
||||||
return create_requester(
|
return create_requester(
|
||||||
UserID.from_string(self.helper.auth_user_id), 1, False, None
|
UserID.from_string(self.helper.auth_user_id),
|
||||||
|
1,
|
||||||
|
False,
|
||||||
|
False,
|
||||||
|
None,
|
||||||
)
|
)
|
||||||
|
|
||||||
self.hs.get_auth().get_user_by_req = get_user_by_req
|
self.hs.get_auth().get_user_by_req = get_user_by_req
|
||||||
@ -540,7 +544,7 @@ class HomeserverTestCase(TestCase):
|
|||||||
"""
|
"""
|
||||||
event_creator = self.hs.get_event_creation_handler()
|
event_creator = self.hs.get_event_creation_handler()
|
||||||
secrets = self.hs.get_secrets()
|
secrets = self.hs.get_secrets()
|
||||||
requester = Requester(user, None, False, None, None)
|
requester = Requester(user, None, False, False, None, None)
|
||||||
|
|
||||||
event, context = self.get_success(
|
event, context = self.get_success(
|
||||||
event_creator.create_event(
|
event_creator.create_event(
|
||||||
|
Loading…
Reference in New Issue
Block a user