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

Auto create group #294

Merged
merged 1 commit into from
Feb 12, 2023
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
2 changes: 1 addition & 1 deletion ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@
* [ ] Provisioning API for logging in
* [x] Linking as secondary device
* [ ] Registering as primary device
* [x] Private chat creation by inviting Matrix puppet of Signal user to new room
* [x] Private chat/group creation by inviting Matrix puppet of Signal user to new room
* [x] Option to use own Matrix account for messages sent from other Signal clients
* [x] Automatic login with shared secret
* [x] Manual login with `login-matrix`
Expand Down
42 changes: 1 addition & 41 deletions mautrix_signal/commands/signal.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@

from mausignald.errors import UnknownIdentityKey, UnregisteredUserError
from mausignald.types import Address, GroupID, TrustLevel
from mautrix.appservice import IntentAPI
from mautrix.bridge.commands import SECTION_ADMIN, HelpSection, command_handler
from mautrix.types import (
ContentURI,
Expand All @@ -37,6 +36,7 @@
from ..util import normalize_number, user_has_power_level
from .auth import make_qr
from .typehint import CommandEvent
from .util import get_initial_state

try:
import PIL as _
Expand Down Expand Up @@ -354,7 +354,6 @@ async def create(evt: CommandEvent) -> EventID:
await warn_missing_power(levels, evt)

await portal.create_signal_group(evt.sender, levels, join_rule)
await evt.reply(f"Signal chat created. ID: {portal.chat_id}")


@command_handler(
Expand Down Expand Up @@ -535,45 +534,6 @@ async def _locked_confirm_bridge(
return await evt.reply("Bridging complete. Portal synchronization should begin momentarily.")


async def get_initial_state(
intent: IntentAPI, room_id: RoomID
) -> tuple[
str | None,
str | None,
PowerLevelStateEventContent | None,
bool,
ContentURI | None,
JoinRule | None,
]:
state = await intent.get_state(room_id)
title: str | None = None
about: str | None = None
levels: PowerLevelStateEventContent | None = None
encrypted: bool = False
avatar_url: ContentURI | None = None
join_rule: JoinRule | None = None
for event in state:
try:
if event.type == EventType.ROOM_NAME:
title = event.content.name
elif event.type == EventType.ROOM_TOPIC:
about = event.content.topic
elif event.type == EventType.ROOM_POWER_LEVELS:
levels = event.content
elif event.type == EventType.ROOM_CANONICAL_ALIAS:
title = title or event.content.canonical_alias
elif event.type == EventType.ROOM_ENCRYPTION:
encrypted = True
elif event.type == EventType.ROOM_AVATAR:
avatar_url = event.content.url
elif event.type == EventType.ROOM_JOIN_RULES:
join_rule = event.content.join_rule
except KeyError:
# Some state event probably has empty content
pass
return title, about, levels, encrypted, avatar_url, join_rule


async def warn_missing_power(levels: PowerLevelStateEventContent, evt: CommandEvent) -> None:
bot_pl = levels.get_user_level(evt.az.bot_mxid)
if bot_pl < levels.get_event_level(EventType.ROOM_POWER_LEVELS):
Expand Down
58 changes: 58 additions & 0 deletions mautrix_signal/commands/util.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# mautrix-signal - A Matrix-Signal puppeting bridge
# Copyright (C) 2021 Tulir Asokan
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
from __future__ import annotations

from mautrix.appservice import IntentAPI
from mautrix.types import ContentURI, EventType, JoinRule, PowerLevelStateEventContent, RoomID


async def get_initial_state(
intent: IntentAPI, room_id: RoomID
) -> tuple[
str | None,
str | None,
PowerLevelStateEventContent | None,
bool,
ContentURI | None,
JoinRule | None,
]:
state = await intent.get_state(room_id)
title: str | None = None
about: str | None = None
levels: PowerLevelStateEventContent | None = None
encrypted: bool = False
avatar_url: ContentURI | None = None
join_rule: JoinRule | None = None
for event in state:
try:
if event.type == EventType.ROOM_NAME:
title = event.content.name
elif event.type == EventType.ROOM_TOPIC:
about = event.content.topic
elif event.type == EventType.ROOM_POWER_LEVELS:
levels = event.content
elif event.type == EventType.ROOM_CANONICAL_ALIAS:
title = title or event.content.canonical_alias
elif event.type == EventType.ROOM_ENCRYPTION:
encrypted = True
elif event.type == EventType.ROOM_AVATAR:
avatar_url = event.content.url
elif event.type == EventType.ROOM_JOIN_RULES:
join_rule = event.content.join_rule
except KeyError:
# Some state event probably has empty content
pass
return title, about, levels, encrypted, avatar_url, join_rule
1 change: 1 addition & 0 deletions mautrix_signal/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ def do_update(self, helper: ConfigUpdateHelper) -> None:
copy("bridge.sync_direct_chat_list")
copy("bridge.double_puppet_server_map")
copy("bridge.double_puppet_allow_discovery")
copy("bridge.create_group_on_invite")
if self["bridge.login_shared_secret"]:
base["bridge.login_shared_secret_map"] = {
base["homeserver.domain"]: self["bridge.login_shared_secret"]
Expand Down
3 changes: 3 additions & 0 deletions mautrix_signal/example-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,9 @@ bridge:
periodic_sync: 0
# Should leaving the room on Matrix make the user leave on Signal?
bridge_matrix_leave: true
# Should the bridge auto-create a group chat on Signal when a ghost is invited to a room?
# Requires the user to have sufficient power level and double puppeting enabled.
create_group_on_invite: true

# Provisioning API part of the web server for automated portal creation and fetching information.
# Used by things like mautrix-manager (https://github.com/tulir/mautrix-manager).
Expand Down
53 changes: 52 additions & 1 deletion mautrix_signal/matrix.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@

from typing import TYPE_CHECKING

from mausignald.types import Address
from mausignald.types import Address, GroupID
from mautrix.bridge import BaseMatrixHandler, RejectMatrixInvite
from mautrix.types import (
Event,
Expand All @@ -37,6 +37,7 @@
)

from . import portal as po, puppet as pu, signal as s, user as u
from .commands.util import get_initial_state
from .db import Message as DBMessage

if TYPE_CHECKING:
Expand All @@ -55,6 +56,56 @@ def __init__(self, bridge: "SignalBridge") -> None:

super().__init__(bridge=bridge)

async def handle_puppet_group_invite(
self,
room_id: RoomID,
puppet: pu.Puppet,
invited_by: u.User,
evt: StateEvent,
members: list[UserID],
) -> None:
double_puppet = await pu.Puppet.get_by_custom_mxid(invited_by.mxid)
if (
not double_puppet
or self.az.bot_mxid in members
or not self.config["bridge.create_group_on_invite"]
):
if self.az.bot_mxid not in members:
await puppet.default_mxid_intent.leave_room(
room_id,
reason="This ghost does not join multi-user rooms without the bridge bot.",
)
else:
await puppet.default_mxid_intent.send_notice(
room_id,
"This ghost will remain inactive "
"until a Signal Group is created for this room.",
)
return

await double_puppet.intent.invite_user(room_id, self.az.bot_mxid)

title, about, levels, encrypted, avatar_url, join_rule = await get_initial_state(
double_puppet.intent, room_id
)

portal = po.Portal(
chat_id=GroupID(""),
mxid=evt.room_id,
name=title,
topic=about or "",
encrypted=encrypted,
receiver="",
avatar_url=avatar_url,
)
await portal.az.intent.ensure_joined(room_id)
invited_by_level = levels.get_user_level(invited_by.mxid)
if invited_by_level > levels.get_user_level(self.az.bot_mxid):
levels.users[self.az.bot_mxid] = 100 if invited_by_level >= 100 else invited_by_level
await double_puppet.intent.set_power_levels(room_id, levels)

await portal.create_signal_group(invited_by, levels, join_rule)

async def handle_invite(
self, room_id: RoomID, user_id: UserID, inviter: u.User, event_id: EventID
) -> None:
Expand Down
1 change: 1 addition & 0 deletions mautrix_signal/portal.py
Original file line number Diff line number Diff line change
Expand Up @@ -1805,6 +1805,7 @@ async def create_signal_group(
await self.update_bridge_info()
if relaybot:
await self._handle_relaybot_invited(relaybot)
await self.main_intent.send_notice(self.mxid, f"Signal group created. ID: {self.chat_id}")

async def bridge_signal_group(
self, source: u.User, levels: PowerLevelStateEventContent
Expand Down