Skip to content
This repository has been archived by the owner on Jan 10, 2025. It is now read-only.

Commit

Permalink
Add config option to restrict auto accepting only from local users (#19)
Browse files Browse the repository at this point in the history
* Add config option to restrict auto accepting only from local users

* Update flake & mypy to newer versions

* Roll flake back a few versions to support 3.7

* Rollback mypy to support 3.7
  • Loading branch information
devonh authored Apr 30, 2024
1 parent d75438b commit dfe3aa8
Show file tree
Hide file tree
Showing 4 changed files with 150 additions and 28 deletions.
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

0 comments on commit dfe3aa8

Please sign in to comment.