Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add config option to restrict auto accepting only from local users #19

Merged
merged 4 commits into from
Apr 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,15 @@ modules:
- module: synapse_auto_accept_invite.InviteAutoAccepter
config:
# Optional: if set to true, then only invites for direct messages (1:1 rooms)
# will be auto accepted. Otherwise, all room invites are accepted.
# will be auto accepted.
# Defaults to false.
accept_invites_only_for_direct_messages: false

# Optional: if set to true, then only invites from local users will be auto
# accepted.
# Defaults to false.
accept_invites_only_from_local_users: false

# (For workerised Synapse deployments)
#
# This module should only be active on a single worker process at once,
Expand Down
4 changes: 2 additions & 2 deletions setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,11 @@ dev =
attrs
frozendict
# for type checking
mypy == 0.931
mypy == 1.4.1
types-frozendict
# for linting
black == 22.3.0
flake8 == 4.0.1
flake8 == 5.0.4
isort == 5.9.3


Expand Down
66 changes: 42 additions & 24 deletions synapse_auto_accept_invite/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
@attr.s(auto_attribs=True, frozen=True)
class InviteAutoAccepterConfig:
accept_invites_only_for_direct_messages: bool = False
accept_invites_only_from_local_users: bool = False
worker_to_run_on: Optional[str] = None


Expand Down Expand Up @@ -66,11 +67,15 @@ def parse_config(config: Dict[str, Any]) -> InviteAutoAccepterConfig:
accept_invites_only_for_direct_messages = config.get(
"accept_invites_only_for_direct_messages", False
)
accept_invites_only_from_local_users = config.get(
"accept_invites_only_from_local_users", False
)

worker_to_run_on = config.get("worker_to_run_on", None)

return InviteAutoAccepterConfig(
accept_invites_only_for_direct_messages=accept_invites_only_for_direct_messages,
accept_invites_only_from_local_users=accept_invites_only_from_local_users,
worker_to_run_on=worker_to_run_on,
)

Expand All @@ -82,36 +87,49 @@ async def on_new_event(self, event: EventBase, *args: Any) -> None:
event: The incoming event.
"""
# Check if the event is an invite for a local user.
if (
is_invite_for_local_user = (
event.type == "m.room.member"
and event.is_state()
and event.membership == "invite"
and self._api.is_mine(event.state_key)
)

# Only accept invites for direct messages if the configuration mandates it.
is_direct_message = event.content.get("is_direct", False)
is_allowed_by_direct_message_rules = (
not self._config.accept_invites_only_for_direct_messages
or is_direct_message is True
)

# Only accept invites from remote users if the configuration mandates it.
is_from_local_user = self._api.is_mine(event.sender)
is_allowed_by_local_user_rules = (
not self._config.accept_invites_only_from_local_users
or is_from_local_user is True
)

if (
is_invite_for_local_user
and is_allowed_by_direct_message_rules
and is_allowed_by_local_user_rules
):
is_direct_message = event.content.get("is_direct", False)

# Only accept invites for direct messages if the configuration mandates it, otherwise accept all invites.
if (
not self._config.accept_invites_only_for_direct_messages
or is_direct_message is True
):
# Make the user join the room. We run this as a background process to circumvent a race condition
# that occurs when responding to invites over federation (see https://github.com/matrix-org/synapse-auto-accept-invite/issues/12)
run_as_background_process(
"retry_make_join",
self._retry_make_join,
event.state_key,
event.state_key,
event.room_id,
"join",
bg_start_span=False,
)
# Make the user join the room. We run this as a background process to circumvent a race condition
# that occurs when responding to invites over federation (see https://github.com/matrix-org/synapse-auto-accept-invite/issues/12)
run_as_background_process(
"retry_make_join",
self._retry_make_join,
event.state_key,
event.state_key,
event.room_id,
"join",
bg_start_span=False,
)

if is_direct_message:
# Mark this room as a direct message!
await self._mark_room_as_direct_message(
event.state_key, event.sender, event.room_id
)
if is_direct_message:
# Mark this room as a direct message!
await self._mark_room_as_direct_message(
event.state_key, event.sender, event.room_id
)

async def _mark_room_as_direct_message(
self, user_id: str, dm_user_id: str, room_id: str
Expand Down
101 changes: 100 additions & 1 deletion tests/test_accept_invite.py
Original file line number Diff line number Diff line change
Expand Up @@ -195,7 +195,7 @@ async def test_accept_invite_direct_message(self) -> None:
},
)

async def test_remote_user(self) -> None:
async def test_invite_remote_user(self) -> None:
"""Tests that receiving an invite for a remote user does nothing."""
invite = MockEvent(
sender=self.user_id,
Expand All @@ -210,6 +210,36 @@ async def test_remote_user(self) -> None:

self.mocked_update_membership.assert_not_called()

async def test_invite_from_remote_user(self) -> None:
"""Tests that receiving an invite for a local user, from a remote user, makes the
module attempt to make the invitee join the room."""
invite = MockEvent(
sender=self.remote_invitee,
state_key=self.invitee,
type="m.room.member",
content={"membership": "invite"},
)
join_event = MockEvent(
sender="someone",
state_key="someone",
type="m.room.member",
content={"membership": "join"},
)
self.mocked_update_membership.return_value = make_awaitable(join_event)

# Stop mypy from complaining that we give on_new_event a MockEvent rather than an
# EventBase.
await self.module.on_new_event(event=invite) # type: ignore[arg-type]

await self.retry_assertions(
self.mocked_update_membership,
1,
sender=invite.state_key,
target=invite.state_key,
room_id=invite.room_id,
new_membership="join",
)

async def test_not_state(self) -> None:
"""Tests that receiving an invite that's not a state event does nothing."""
invite = MockEvent(
Expand Down Expand Up @@ -320,14 +350,83 @@ async def test_ignore_invite_if_only_enabled_for_direct_messages(self) -> None:
mocked_update_membership: Mock = module._api.update_room_membership # type: ignore[assignment]
mocked_update_membership.assert_not_called()

async def test_accept_invite_local_user_if_only_enabled_from_local_users(
self,
) -> None:
"""Tests that, if the module is configured to only accept invites from local users, invites
from local users are still automatically accepted.
"""
module = create_module(
config_override={"accept_invites_only_from_local_users": True},
)

# Patch out the account data get and put methods with dummy awaitables.
account_data_put: Mock = cast(Mock, module._api.account_data_manager.put_global)
account_data_put.return_value = make_awaitable(None)

account_data_get: Mock = cast(Mock, module._api.account_data_manager.get_global)
account_data_get.return_value = make_awaitable({})

mocked_update_membership: Mock = module._api.update_room_membership # type: ignore[assignment]
join_event = MockEvent(
sender="someone",
state_key="someone",
type="m.room.member",
content={"membership": "join"},
)
mocked_update_membership.return_value = make_awaitable(join_event)

invite = MockEvent(
sender=self.user_id,
state_key=self.invitee,
type="m.room.member",
content={"membership": "invite", "is_direct": True},
)

# Stop mypy from complaining that we give on_new_event a MockEvent rather than an
# EventBase.
await module.on_new_event(event=invite) # type: ignore[arg-type]

await self.retry_assertions(
mocked_update_membership,
1,
sender=invite.state_key,
target=invite.state_key,
room_id=invite.room_id,
new_membership="join",
)

async def test_ignore_invite_if_only_enabled_from_local_users(self) -> None:
"""Tests that, if the module is configured to only accept invites from local users,
invites from non-local users are ignored."""
module = create_module(
config_override={"accept_invites_only_from_local_users": True},
)

invite = MockEvent(
sender=self.remote_invitee,
state_key=self.invitee,
type="m.room.member",
content={"membership": "invite"},
)

# Stop mypy from complaining that we give on_new_event a MockEvent rather than an
# EventBase.
await module.on_new_event(event=invite) # type: ignore[arg-type]

mocked_update_membership: Mock = module._api.update_room_membership # type: ignore[assignment]
mocked_update_membership.assert_not_called()

def test_config_parse(self) -> None:
"""Tests that a correct configuration passes parse_config."""
config = {
"accept_invites_only_for_direct_messages": True,
"accept_invites_only_from_local_users": True,
}
parsed_config = InviteAutoAccepter.parse_config(config)

self.assertTrue(parsed_config.accept_invites_only_for_direct_messages)
self.assertTrue(parsed_config.accept_invites_only_from_local_users)

def test_runs_on_only_one_worker(self) -> None:
"""
Expand Down
Loading