Revert shadow HS support (#104)
Revert shadow HS support added in matrix-org/synapse#4145 Fixes matrix-org/matrix-dinsic#803 Part of that PR isn't reverted because it relates to matrix-org/matrix-dinsic#793
This commit is contained in:
committed by
GitHub
parent
6b439a7d65
commit
95f29bce7f
1
changelog.d/104.misc
Normal file
1
changelog.d/104.misc
Normal file
@@ -0,0 +1 @@
|
||||
Remove shadow HS support.
|
||||
@@ -1343,15 +1343,6 @@ url_preview_accept_language:
|
||||
#
|
||||
#replicate_user_profiles_to: example.com
|
||||
|
||||
# If specified, attempt to replay registrations, profile changes & 3pid
|
||||
# bindings on the given target homeserver via the AS API. The HS is authed
|
||||
# via a given AS token.
|
||||
#
|
||||
#shadow_server:
|
||||
# hs_url: https://shadow.example.com
|
||||
# hs: shadow.example.com
|
||||
# as_token: 12u394refgbdhivsia
|
||||
|
||||
# If enabled, don't let users set their own display names/avatars
|
||||
# other than for the very first time (unless they are a server admin).
|
||||
# Useful when provisioning users based on the contents of a 3rd party
|
||||
|
||||
@@ -139,7 +139,6 @@ class RegistrationConfig(Config):
|
||||
if not isinstance(self.replicate_user_profiles_to, list):
|
||||
self.replicate_user_profiles_to = [self.replicate_user_profiles_to]
|
||||
|
||||
self.shadow_server = config.get("shadow_server", None)
|
||||
self.rewrite_identity_server_urls = (
|
||||
config.get("rewrite_identity_server_urls") or {}
|
||||
)
|
||||
@@ -312,15 +311,6 @@ class RegistrationConfig(Config):
|
||||
#
|
||||
#replicate_user_profiles_to: example.com
|
||||
|
||||
# If specified, attempt to replay registrations, profile changes & 3pid
|
||||
# bindings on the given target homeserver via the AS API. The HS is authed
|
||||
# via a given AS token.
|
||||
#
|
||||
#shadow_server:
|
||||
# hs_url: https://shadow.example.com
|
||||
# hs: shadow.example.com
|
||||
# as_token: 12u394refgbdhivsia
|
||||
|
||||
# If enabled, don't let users set their own display names/avatars
|
||||
# other than for the very first time (unless they are a server admin).
|
||||
# Useful when provisioning users based on the contents of a 3rd party
|
||||
|
||||
@@ -610,9 +610,7 @@ class RegistrationHandler(BaseHandler):
|
||||
"""
|
||||
await self._auto_join_rooms(user_id)
|
||||
|
||||
async def appservice_register(
|
||||
self, user_localpart: str, as_token: str, password_hash: str, display_name: str
|
||||
):
|
||||
async def appservice_register(self, user_localpart: str, as_token: str):
|
||||
# FIXME: this should be factored out and merged with normal register()
|
||||
user = UserID(user_localpart, self.hs.hostname)
|
||||
user_id = user.to_string()
|
||||
@@ -630,26 +628,12 @@ class RegistrationHandler(BaseHandler):
|
||||
|
||||
self.check_user_id_not_appservice_exclusive(user_id, allowed_appservice=service)
|
||||
|
||||
display_name = display_name or user.localpart
|
||||
|
||||
await self.register_with_store(
|
||||
user_id=user_id,
|
||||
password_hash=password_hash,
|
||||
password_hash="",
|
||||
appservice_id=service_id,
|
||||
create_profile_with_displayname=display_name,
|
||||
create_profile_with_displayname=user.localpart,
|
||||
)
|
||||
|
||||
requester = create_requester(user)
|
||||
await self.profile_handler.set_displayname(
|
||||
user, requester, display_name, by_admin=True
|
||||
)
|
||||
|
||||
if self.hs.config.user_directory_search_all_users:
|
||||
profile = await self.store.get_profileinfo(user_localpart)
|
||||
await self.user_directory_handler.handle_local_profile_change(
|
||||
user_id, profile
|
||||
)
|
||||
|
||||
return user_id
|
||||
|
||||
def check_user_id_not_appservice_exclusive(
|
||||
@@ -678,37 +662,6 @@ class RegistrationHandler(BaseHandler):
|
||||
errcode=Codes.EXCLUSIVE,
|
||||
)
|
||||
|
||||
async def shadow_register(self, localpart, display_name, auth_result, params):
|
||||
"""Invokes the current registration on another server, using
|
||||
shared secret registration, passing in any auth_results from
|
||||
other registration UI auth flows (e.g. validated 3pids)
|
||||
Useful for setting up shadow/backup accounts on a parallel deployment.
|
||||
"""
|
||||
|
||||
# TODO: retries
|
||||
shadow_hs_url = self.hs.config.shadow_server.get("hs_url")
|
||||
as_token = self.hs.config.shadow_server.get("as_token")
|
||||
|
||||
await self.http_client.post_json_get_json(
|
||||
"%s/_matrix/client/r0/register?access_token=%s" % (shadow_hs_url, as_token),
|
||||
{
|
||||
# XXX: auth_result is an unspecified extension for shadow registration
|
||||
"auth_result": auth_result,
|
||||
# XXX: another unspecified extension for shadow registration to ensure
|
||||
# that the displayname is correctly set by the masters erver
|
||||
"display_name": display_name,
|
||||
"username": localpart,
|
||||
"password": params.get("password"),
|
||||
"bind_msisdn": params.get("bind_msisdn"),
|
||||
"device_id": params.get("device_id"),
|
||||
"initial_device_display_name": params.get(
|
||||
"initial_device_display_name"
|
||||
),
|
||||
"inhibit_login": False,
|
||||
"access_token": as_token,
|
||||
},
|
||||
)
|
||||
|
||||
async def check_registration_ratelimit(self, address: Optional[str]) -> None:
|
||||
"""A simple helper method to check whether the registration rate limit has been hit
|
||||
for a given IP address
|
||||
|
||||
@@ -17,7 +17,7 @@ import logging
|
||||
import random
|
||||
import re
|
||||
from http import HTTPStatus
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
from typing import TYPE_CHECKING
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from synapse.api.constants import LoginType
|
||||
@@ -38,7 +38,6 @@ from synapse.http.servlet import (
|
||||
)
|
||||
from synapse.metrics import threepid_send_requests
|
||||
from synapse.push.mailer import Mailer
|
||||
from synapse.types import UserID
|
||||
from synapse.util.msisdn import phone_number_to_msisdn
|
||||
from synapse.util.stringutils import assert_valid_client_secret, random_string
|
||||
from synapse.util.threepids import check_3pid_allowed, validate_email
|
||||
@@ -196,31 +195,30 @@ class PasswordRestServlet(RestServlet):
|
||||
if self.auth.has_access_token(request):
|
||||
requester = await self.auth.get_user_by_req(request)
|
||||
# blindly trust ASes without UI-authing them
|
||||
if requester.app_service:
|
||||
params = body
|
||||
else:
|
||||
try:
|
||||
(
|
||||
params,
|
||||
session_id,
|
||||
) = await self.auth_handler.validate_user_via_ui_auth(
|
||||
requester,
|
||||
request,
|
||||
body,
|
||||
"modify your account password",
|
||||
try:
|
||||
(
|
||||
params,
|
||||
session_id,
|
||||
) = await self.auth_handler.validate_user_via_ui_auth(
|
||||
requester,
|
||||
request,
|
||||
body,
|
||||
"modify your account password",
|
||||
)
|
||||
except InteractiveAuthIncompleteError as e:
|
||||
# The user needs to provide more steps to complete auth, but
|
||||
# they're not required to provide the password again.
|
||||
#
|
||||
# If a password is available now, hash the provided password and
|
||||
# store it for later.
|
||||
if new_password:
|
||||
password_hash = await self.auth_handler.hash(new_password)
|
||||
await self.auth_handler.set_session_data(
|
||||
e.session_id,
|
||||
UIAuthSessionDataConstants.PASSWORD_HASH,
|
||||
password_hash,
|
||||
)
|
||||
except InteractiveAuthIncompleteError as e:
|
||||
# The user needs to provide more steps to complete auth, but
|
||||
# they're not required to provide the password again.
|
||||
#
|
||||
# If a password is available now, hash the provided password and
|
||||
# store it for later.
|
||||
if new_password:
|
||||
password_hash = await self.auth_handler.hash(new_password)
|
||||
await self.auth_handler.set_session_data(
|
||||
e.session_id, "password_hash", password_hash
|
||||
)
|
||||
raise
|
||||
raise
|
||||
user_id = requester.user.to_string()
|
||||
else:
|
||||
requester = None
|
||||
@@ -290,28 +288,11 @@ class PasswordRestServlet(RestServlet):
|
||||
user_id, password_hash, logout_devices, requester
|
||||
)
|
||||
|
||||
if self.hs.config.shadow_server:
|
||||
shadow_user = UserID(
|
||||
requester.user.localpart, self.hs.config.shadow_server.get("hs")
|
||||
)
|
||||
await self.shadow_password(params, shadow_user.to_string())
|
||||
|
||||
return 200, {}
|
||||
|
||||
def on_OPTIONS(self, _):
|
||||
return 200, {}
|
||||
|
||||
async def shadow_password(self, body, user_id):
|
||||
# TODO: retries
|
||||
shadow_hs_url = self.hs.config.shadow_server.get("hs_url")
|
||||
as_token = self.hs.config.shadow_server.get("as_token")
|
||||
|
||||
await self.http_client.post_json_get_json(
|
||||
"%s/_matrix/client/r0/account/password?access_token=%s&user_id=%s"
|
||||
% (shadow_hs_url, as_token, user_id),
|
||||
body,
|
||||
)
|
||||
|
||||
|
||||
class DeactivateAccountRestServlet(RestServlet):
|
||||
PATTERNS = client_patterns("/account/deactivate$")
|
||||
@@ -667,7 +648,6 @@ class ThreepidRestServlet(RestServlet):
|
||||
self.auth = hs.get_auth()
|
||||
self.auth_handler = hs.get_auth_handler()
|
||||
self.datastore = hs.get_datastore()
|
||||
self.http_client = hs.get_simple_http_client()
|
||||
|
||||
async def on_GET(self, request):
|
||||
requester = await self.auth.get_user_by_req(request)
|
||||
@@ -686,32 +666,6 @@ class ThreepidRestServlet(RestServlet):
|
||||
user_id = requester.user.to_string()
|
||||
body = parse_json_object_from_request(request)
|
||||
|
||||
# skip validation if this is a shadow 3PID from an AS
|
||||
if requester.app_service:
|
||||
# XXX: ASes pass in a validated threepid directly to bypass the IS.
|
||||
# This makes the API entirely change shape when we have an AS token;
|
||||
# it really should be an entirely separate API - perhaps
|
||||
# /account/3pid/replicate or something.
|
||||
threepid: Optional[dict] = body.get("threepid")
|
||||
|
||||
if not threepid:
|
||||
raise SynapseError(400, "Missing param 'threepid'")
|
||||
|
||||
await self.auth_handler.add_threepid(
|
||||
user_id,
|
||||
threepid["medium"],
|
||||
threepid["address"],
|
||||
threepid["validated_at"],
|
||||
)
|
||||
|
||||
if self.hs.config.shadow_server:
|
||||
shadow_user = UserID(
|
||||
requester.user.localpart, self.hs.config.shadow_server.get("hs")
|
||||
)
|
||||
await self.shadow_3pid({"threepid": threepid}, shadow_user.to_string())
|
||||
|
||||
return 200, {}
|
||||
|
||||
threepid_creds = body.get("threePidCreds") or body.get("three_pid_creds")
|
||||
if threepid_creds is None:
|
||||
raise SynapseError(
|
||||
@@ -733,35 +687,12 @@ class ThreepidRestServlet(RestServlet):
|
||||
validation_session["address"],
|
||||
validation_session["validated_at"],
|
||||
)
|
||||
|
||||
if self.hs.config.shadow_server:
|
||||
shadow_user = UserID(
|
||||
requester.user.localpart, self.hs.config.shadow_server.get("hs")
|
||||
)
|
||||
threepid = {
|
||||
"medium": validation_session["medium"],
|
||||
"address": validation_session["address"],
|
||||
"validated_at": validation_session["validated_at"],
|
||||
}
|
||||
await self.shadow_3pid({"threepid": threepid}, shadow_user.to_string())
|
||||
|
||||
return 200, {}
|
||||
|
||||
raise SynapseError(
|
||||
400, "No validated 3pid session found", Codes.THREEPID_AUTH_FAILED
|
||||
)
|
||||
|
||||
async def shadow_3pid(self, body, user_id):
|
||||
# TODO: retries
|
||||
shadow_hs_url = self.hs.config.shadow_server.get("hs_url")
|
||||
as_token = self.hs.config.shadow_server.get("as_token")
|
||||
|
||||
await self.http_client.post_json_get_json(
|
||||
"%s/_matrix/client/r0/account/3pid?access_token=%s&user_id=%s"
|
||||
% (shadow_hs_url, as_token, user_id),
|
||||
body,
|
||||
)
|
||||
|
||||
|
||||
class ThreepidAddRestServlet(RestServlet):
|
||||
PATTERNS = client_patterns("/account/3pid/add$")
|
||||
@@ -807,33 +738,12 @@ class ThreepidAddRestServlet(RestServlet):
|
||||
validation_session["address"],
|
||||
validation_session["validated_at"],
|
||||
)
|
||||
if self.hs.config.shadow_server:
|
||||
shadow_user = UserID(
|
||||
requester.user.localpart, self.hs.config.shadow_server.get("hs")
|
||||
)
|
||||
threepid = {
|
||||
"medium": validation_session["medium"],
|
||||
"address": validation_session["address"],
|
||||
"validated_at": validation_session["validated_at"],
|
||||
}
|
||||
await self.shadow_3pid({"threepid": threepid}, shadow_user.to_string())
|
||||
return 200, {}
|
||||
|
||||
raise SynapseError(
|
||||
400, "No validated 3pid session found", Codes.THREEPID_AUTH_FAILED
|
||||
)
|
||||
|
||||
async def shadow_3pid(self, body, user_id):
|
||||
# TODO: retries
|
||||
shadow_hs_url = self.hs.config.shadow_server.get("hs_url")
|
||||
as_token = self.hs.config.shadow_server.get("as_token")
|
||||
|
||||
await self.http_client.post_json_get_json(
|
||||
"%s/_matrix/client/r0/account/3pid?access_token=%s&user_id=%s"
|
||||
% (shadow_hs_url, as_token, user_id),
|
||||
body,
|
||||
)
|
||||
|
||||
|
||||
class ThreepidBindRestServlet(RestServlet):
|
||||
PATTERNS = client_patterns("/account/3pid/bind$")
|
||||
@@ -903,7 +813,6 @@ class ThreepidDeleteRestServlet(RestServlet):
|
||||
self.hs = hs
|
||||
self.auth = hs.get_auth()
|
||||
self.auth_handler = hs.get_auth_handler()
|
||||
self.http_client = hs.get_simple_http_client()
|
||||
|
||||
async def on_POST(self, request):
|
||||
if not self.hs.config.enable_3pid_changes:
|
||||
@@ -928,12 +837,6 @@ class ThreepidDeleteRestServlet(RestServlet):
|
||||
logger.exception("Failed to remove threepid")
|
||||
raise SynapseError(500, "Failed to remove threepid")
|
||||
|
||||
if self.hs.config.shadow_server:
|
||||
shadow_user = UserID(
|
||||
requester.user.localpart, self.hs.config.shadow_server.get("hs")
|
||||
)
|
||||
await self.shadow_3pid_delete(body, shadow_user.to_string())
|
||||
|
||||
if ret:
|
||||
id_server_unbind_result = "success"
|
||||
else:
|
||||
@@ -941,17 +844,6 @@ class ThreepidDeleteRestServlet(RestServlet):
|
||||
|
||||
return 200, {"id_server_unbind_result": id_server_unbind_result}
|
||||
|
||||
async def shadow_3pid_delete(self, body, user_id):
|
||||
# TODO: retries
|
||||
shadow_hs_url = self.hs.config.shadow_server.get("hs_url")
|
||||
as_token = self.hs.config.shadow_server.get("as_token")
|
||||
|
||||
await self.http_client.post_json_get_json(
|
||||
"%s/_matrix/client/r0/account/3pid/delete?access_token=%s&user_id=%s"
|
||||
% (shadow_hs_url, as_token, user_id),
|
||||
body,
|
||||
)
|
||||
|
||||
|
||||
class ThreepidLookupRestServlet(RestServlet):
|
||||
PATTERNS = [re.compile("^/_matrix/client/unstable/account/3pid/lookup$")]
|
||||
|
||||
@@ -13,7 +13,6 @@
|
||||
# limitations under the License.
|
||||
|
||||
""" This module contains REST servlets to do with profile: /profile/<paths> """
|
||||
from twisted.internet import defer
|
||||
|
||||
from synapse.api.errors import Codes, SynapseError
|
||||
from synapse.http.servlet import RestServlet, parse_json_object_from_request
|
||||
@@ -28,7 +27,6 @@ class ProfileDisplaynameRestServlet(RestServlet):
|
||||
super().__init__()
|
||||
self.hs = hs
|
||||
self.profile_handler = hs.get_profile_handler()
|
||||
self.http_client = hs.get_simple_http_client()
|
||||
self.auth = hs.get_auth()
|
||||
|
||||
async def on_GET(self, request, user_id):
|
||||
@@ -68,27 +66,11 @@ class ProfileDisplaynameRestServlet(RestServlet):
|
||||
|
||||
await self.profile_handler.set_displayname(user, requester, new_name, is_admin)
|
||||
|
||||
if self.hs.config.shadow_server:
|
||||
shadow_user = UserID(user.localpart, self.hs.config.shadow_server.get("hs"))
|
||||
self.shadow_displayname(shadow_user.to_string(), content)
|
||||
|
||||
return 200, {}
|
||||
|
||||
def on_OPTIONS(self, request, user_id):
|
||||
return 200, {}
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def shadow_displayname(self, user_id, body):
|
||||
# TODO: retries
|
||||
shadow_hs_url = self.hs.config.shadow_server.get("hs_url")
|
||||
as_token = self.hs.config.shadow_server.get("as_token")
|
||||
|
||||
yield self.http_client.put_json(
|
||||
"%s/_matrix/client/r0/profile/%s/displayname?access_token=%s&user_id=%s"
|
||||
% (shadow_hs_url, user_id, as_token, user_id),
|
||||
body,
|
||||
)
|
||||
|
||||
|
||||
class ProfileAvatarURLRestServlet(RestServlet):
|
||||
PATTERNS = client_patterns("/profile/(?P<user_id>[^/]*)/avatar_url", v1=True)
|
||||
@@ -97,7 +79,6 @@ class ProfileAvatarURLRestServlet(RestServlet):
|
||||
super().__init__()
|
||||
self.hs = hs
|
||||
self.profile_handler = hs.get_profile_handler()
|
||||
self.http_client = hs.get_simple_http_client()
|
||||
self.auth = hs.get_auth()
|
||||
|
||||
async def on_GET(self, request, user_id):
|
||||
@@ -136,27 +117,11 @@ class ProfileAvatarURLRestServlet(RestServlet):
|
||||
user, requester, new_avatar_url, is_admin
|
||||
)
|
||||
|
||||
if self.hs.config.shadow_server:
|
||||
shadow_user = UserID(user.localpart, self.hs.config.shadow_server.get("hs"))
|
||||
self.shadow_avatar_url(shadow_user.to_string(), content)
|
||||
|
||||
return 200, {}
|
||||
|
||||
def on_OPTIONS(self, request, user_id):
|
||||
return 200, {}
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def shadow_avatar_url(self, user_id, body):
|
||||
# TODO: retries
|
||||
shadow_hs_url = self.hs.config.shadow_server.get("hs_url")
|
||||
as_token = self.hs.config.shadow_server.get("as_token")
|
||||
|
||||
yield self.http_client.put_json(
|
||||
"%s/_matrix/client/r0/profile/%s/avatar_url?access_token=%s&user_id=%s"
|
||||
% (shadow_hs_url, user_id, as_token, user_id),
|
||||
body,
|
||||
)
|
||||
|
||||
|
||||
class ProfileRestServlet(RestServlet):
|
||||
PATTERNS = client_patterns("/profile/(?P<user_id>[^/]*)", v1=True)
|
||||
|
||||
@@ -481,8 +481,6 @@ class RegisterRestServlet(RestServlet):
|
||||
|
||||
result = await self._do_appservice_registration(
|
||||
desired_username,
|
||||
password,
|
||||
desired_display_name,
|
||||
access_token,
|
||||
body,
|
||||
should_issue_refresh_token=should_issue_refresh_token,
|
||||
@@ -738,14 +736,6 @@ class RegisterRestServlet(RestServlet):
|
||||
):
|
||||
await self.store.upsert_monthly_active_user(registered_user_id)
|
||||
|
||||
if self.hs.config.shadow_server:
|
||||
await self.registration_handler.shadow_register(
|
||||
localpart=desired_username,
|
||||
display_name=desired_display_name,
|
||||
auth_result=auth_result,
|
||||
params=params,
|
||||
)
|
||||
|
||||
# Remember that the user account has been registered (and the user
|
||||
# ID it was registered with, since it might not have been specified).
|
||||
await self.auth_handler.set_session_data(
|
||||
@@ -774,44 +764,20 @@ class RegisterRestServlet(RestServlet):
|
||||
async def _do_appservice_registration(
|
||||
self,
|
||||
username,
|
||||
password,
|
||||
display_name,
|
||||
as_token,
|
||||
body,
|
||||
should_issue_refresh_token: bool = False,
|
||||
):
|
||||
if password:
|
||||
# Hash the password
|
||||
#
|
||||
# In mainline hashing of the password was moved further on in the registration
|
||||
# flow, but we need it here for the AS use-case of shadow servers
|
||||
password = await self.auth_handler.hash(password)
|
||||
|
||||
user_id = await self.registration_handler.appservice_register(
|
||||
username, as_token, password, display_name
|
||||
username, as_token
|
||||
)
|
||||
result = await self._create_registration_details(
|
||||
return await self._create_registration_details(
|
||||
user_id,
|
||||
body,
|
||||
is_appservice_ghost=True,
|
||||
should_issue_refresh_token=should_issue_refresh_token,
|
||||
)
|
||||
|
||||
auth_result = body.get("auth_result")
|
||||
if auth_result and LoginType.EMAIL_IDENTITY in auth_result:
|
||||
threepid = auth_result[LoginType.EMAIL_IDENTITY]
|
||||
await self.registration_handler.register_email_threepid(
|
||||
user_id, threepid, result["access_token"]
|
||||
)
|
||||
|
||||
if auth_result and LoginType.MSISDN in auth_result:
|
||||
threepid = auth_result[LoginType.MSISDN]
|
||||
await self.registration_handler.register_msisdn_threepid(
|
||||
user_id, threepid, result["access_token"]
|
||||
)
|
||||
|
||||
return result
|
||||
|
||||
async def _create_registration_details(
|
||||
self,
|
||||
user_id: str,
|
||||
|
||||
29
synapse/test_module/__init__.py
Normal file
29
synapse/test_module/__init__.py
Normal file
@@ -0,0 +1,29 @@
|
||||
import time
|
||||
|
||||
from synapse.api.constants import EventTypes
|
||||
from synapse.events import EventBase
|
||||
from synapse.module_api import ModuleApi
|
||||
from synapse.types import StateMap
|
||||
|
||||
|
||||
class MySuperModule:
|
||||
def __init__(self, config: dict, api: ModuleApi):
|
||||
self.api = api
|
||||
|
||||
self.api.register_third_party_rules_callbacks(
|
||||
check_event_allowed=self.check_event_allowed,
|
||||
)
|
||||
|
||||
async def check_event_allowed(self, event: EventBase, state: StateMap[EventBase]):
|
||||
if event.is_state() and event.type == EventTypes.Member:
|
||||
await self.api.create_and_send_event_into_room(
|
||||
{
|
||||
"room_id": event.room_id,
|
||||
"sender": event.sender,
|
||||
"type": "bzh.abolivier.test3",
|
||||
"content": {"now": int(time.time())},
|
||||
"state_key": "",
|
||||
}
|
||||
)
|
||||
|
||||
return True, None
|
||||
11
synapse/test_module/test_account_validity/__init__.py
Normal file
11
synapse/test_module/test_account_validity/__init__.py
Normal file
@@ -0,0 +1,11 @@
|
||||
from typing import Optional
|
||||
|
||||
from synapse.module_api import ModuleApi
|
||||
|
||||
|
||||
class DummyAccountValidity:
|
||||
def __init__(self, config: dict, api: ModuleApi):
|
||||
api.register_account_validity_callbacks(is_user_expired=self.is_user_expired)
|
||||
|
||||
async def is_user_expired(self, user_id: str) -> Optional[bool]:
|
||||
return False
|
||||
Reference in New Issue
Block a user