Skip to content

Commit

Permalink
Merge branch 'master' into power_level
Browse files Browse the repository at this point in the history
  • Loading branch information
maltee1 authored Jun 2, 2022
2 parents 08ed1ab + 38ff3e0 commit 053db40
Show file tree
Hide file tree
Showing 6 changed files with 150 additions and 99 deletions.
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ All setup and usage instructions are located on
[docs.mau.fi](https://docs.mau.fi/bridges/python/signal/index.html).
Some quick links:

* [Bridge setup](https://docs.mau.fi/bridges/python/setup/index.html?bridge=signal)
(or [with Docker](https://docs.mau.fi/bridges/python/signal/setup-docker.html))
* [Bridge setup](https://docs.mau.fi/bridges/python/setup.html?bridge=signal)
(or [with Docker](https://docs.mau.fi/bridges/python/signal/docker-setup.html))
* Basic usage: [Authentication](https://docs.mau.fi/bridges/python/signal/authentication.html)

### Features & Roadmap
Expand Down
1 change: 1 addition & 0 deletions mautrix_signal/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ def do_update(self, helper: ConfigUpdateHelper) -> None:
copy("bridge.private_chat_portal_meta")
copy("bridge.delivery_receipts")
copy("bridge.delivery_error_reports")
copy("bridge.message_status_events")
copy("bridge.resend_bridge_info")
copy("bridge.periodic_sync")

Expand Down
7 changes: 5 additions & 2 deletions mautrix_signal/example-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -158,8 +158,9 @@ bridge:
# Whether or not created rooms should have federation enabled.
# If false, created portal rooms will never be federated.
federate_rooms: true
# End-to-bridge encryption support options. You must install the e2be optional dependency for
# this to work. See https://docs.mau.fi/bridges/general/end-to-bridge-encryption.html
# End-to-bridge encryption support options.
#
# See https://docs.mau.fi/bridges/general/end-to-bridge-encryption.html for more info.
encryption:
# Allow encryption, work in group chat rooms with e2ee enabled
allow: false
Expand Down Expand Up @@ -188,6 +189,8 @@ bridge:
delivery_receipts: false
# Whether or not delivery errors should be reported as messages in the Matrix room. (not yet implemented)
delivery_error_reports: false
# Whether the bridge should send the message status as a custom com.beeper.message_send_status event.
message_status_events: false
# Set this to true to tell the bridge to re-send m.bridge events to all rooms on the next run.
# This field will automatically be changed back to false after it,
# except if the config file is not writable.
Expand Down
215 changes: 127 additions & 88 deletions mautrix_signal/portal.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@
from mautrix.errors import IntentError, MatrixError, MForbidden
from mautrix.types import (
AudioInfo,
BeeperMessageStatusEventContent,
ContentURI,
EncryptedEvent,
EncryptedFile,
Expand All @@ -66,8 +67,11 @@
Membership,
MessageEvent,
MessageEventContent,
MessageStatusReason,
MessageType,
PowerLevelStateEventContent,
RelatesTo,
RelationType,
RoomID,
TextMessageEventContent,
UserID,
Expand Down Expand Up @@ -320,12 +324,53 @@ async def handle_matrix_message(
status, event_id, self.mxid, EventType.ROOM_MESSAGE, message.msgtype, error=e
)
await sender.handle_auth_failure(e)
await self._send_message(
self.main_intent,
TextMessageEventContent(
msgtype=MessageType.NOTICE, body=f"\u26a0 Your message was not bridged: {e}"
),
)
await self._send_error_notice("message", e)
asyncio.create_task(self._send_message_status(event_id, e))

async def _send_error_notice(self, type_name: str, err: Exception) -> None:
if not self.config["bridge.delivery_error_reports"]:
return
message = f"{type(err).__name__}: {err}"
if isinstance(err, NotConnected):
message = "There was an error connecting to signald."
elif isinstance(err, UnknownReactionTarget):
message = "Could not find message to react to on Signal."
await self._send_message(
self.main_intent,
TextMessageEventContent(
msgtype=MessageType.NOTICE,
body=f"\u26a0 Your {type_name} was not bridged: {message}",
),
)

async def _send_message_status(self, event_id: EventID, err: Exception | None) -> None:
if not self.config["bridge.message_status_events"]:
return
intent = self.az.intent if self.encrypted else self.main_intent
status = BeeperMessageStatusEventContent(
network=self.bridge_info_state_key,
relates_to=RelatesTo(
rel_type=RelationType.REFERENCE,
event_id=event_id,
),
success=err is None,
)
if err:
status.reason = MessageStatusReason.GENERIC_ERROR
status.error = str(err)
status.is_certain = True
status.can_retry = True
if isinstance(err, AttachmentTooLargeError):
status.reason = MessageStatusReason.UNSUPPORTED
status.can_retry = False
elif isinstance(err, UnknownReactionTarget):
status.can_retry = False

await intent.send_message_event(
room_id=self.mxid,
event_type=EventType.BEEPER_MESSAGE_STATUS,
content=status,
)

async def _beeper_link_preview_to_signal(
self, beeper_link_preview: dict[str, Any]
Expand Down Expand Up @@ -406,58 +451,55 @@ async def _handle_matrix_message(
return

self.log.debug(f"Sending Matrix message {event_id} to Signal with timestamp {request_id}")
try:
retry_count = await self._signal_send_with_retries(
sender,
event_id,
message_type=message.msgtype,
send_fn=lambda *args, **kwargs: self.signal.send(**kwargs),
event_type=EventType.ROOM_MESSAGE,
username=sender.username,
recipient=self.chat_id,
body=text,
mentions=mentions,
previews=link_previews,
quote=quote,
attachments=attachments,
timestamp=request_id,
)
except Exception:
self.log.exception("Sending message failed")
raise
else:
sender.send_remote_checkpoint(
MessageSendCheckpointStatus.SUCCESS,
event_id,
self.mxid,
EventType.ROOM_MESSAGE,
message.msgtype,
retry_num=retry_count,
)
await self._send_delivery_receipt(event_id)
retry_count = await self._signal_send_with_retries(
sender,
event_id,
message_type=message.msgtype,
send_fn=lambda *args, **kwargs: self.signal.send(**kwargs),
event_type=EventType.ROOM_MESSAGE,
username=sender.username,
recipient=self.chat_id,
body=text,
mentions=mentions,
previews=link_previews,
quote=quote,
attachments=attachments,
timestamp=request_id,
)

msg = DBMessage(
mxid=event_id,
mx_room=self.mxid,
sender=sender.address,
timestamp=request_id,
signal_chat_id=self.chat_id,
signal_receiver=self.receiver,
)
await msg.insert()
self.log.debug(f"Handled Matrix message {event_id} -> {request_id}")
if attachment_path and self.config["signal.remove_file_after_handling"]:
try:
os.remove(attachment_path)
except FileNotFoundError:
pass
msg = DBMessage(
mxid=event_id,
mx_room=self.mxid,
sender=sender.address,
timestamp=request_id,
signal_chat_id=self.chat_id,
signal_receiver=self.receiver,
)
await msg.insert()
self.log.debug(f"Handled Matrix message {event_id} -> {request_id}")
if attachment_path and self.config["signal.remove_file_after_handling"]:
try:
os.remove(attachment_path)
except FileNotFoundError:
pass

# Handle disappearing messages
if self.expiration_time and self.disappearing_enabled:
dm = DisappearingMessage(self.mxid, event_id, self.expiration_time)
dm.start_timer()
await dm.insert()
await self._disappear_event(dm)
# Handle disappearing messages
if self.expiration_time and self.disappearing_enabled:
dm = DisappearingMessage(self.mxid, event_id, self.expiration_time)
dm.start_timer()
await dm.insert()
await self._disappear_event(dm)

sender.send_remote_checkpoint(
MessageSendCheckpointStatus.SUCCESS,
event_id,
self.mxid,
EventType.ROOM_MESSAGE,
message.msgtype,
retry_num=retry_count,
)
await self._send_delivery_receipt(event_id)
asyncio.create_task(self._send_message_status(event_id, err=None))

async def _signal_send_with_retries(
self,
Expand All @@ -468,24 +510,23 @@ async def _signal_send_with_retries(
message_type: MessageType | None = None,
**send_args,
) -> int:
retry_count = 7
retry_message_event_id = None
retry_count = 4
last_error_type = NotConnected
for retry_num in range(retry_count):
try:
req_id = uuid4()
self.log.info(
f"Send attempt {retry_num}. Attempting to send {event_id} with {req_id}"
)
await send_fn(sender, event_id, req_id=req_id, **send_args)

# It was successful.
if retry_message_event_id is not None:
await self.main_intent.redact(self.mxid, retry_message_event_id)
return retry_num
except (NotConnected, UnknownReactionTarget) as e:
if retry_num >= retry_count - 1:
break
last_error_type = type(e)
# Only handle NotConnected and UnknownReactionTarget exceptions so that other
# exceptions actually continue to error.
sleep_seconds = (retry_num + 1) ** 2
sleep_seconds = retry_num * 2 + 1
msg = (
f"Not connected to signald. Going to sleep for {sleep_seconds}s. Error: {e}"
if isinstance(e, NotConnected)
Expand All @@ -502,34 +543,15 @@ async def _signal_send_with_retries(
retry_num=retry_num,
)

if retry_num > 2:
# User has waited > ~15 seconds, send a notice that we are retrying.
user_friendly_message = (
"There was an error connecting to signald."
if isinstance(e, NotConnected)
else "Could not find message to react to on Signal."
)
event_content = TextMessageEventContent(
MessageType.NOTICE,
f"{user_friendly_message} Waiting for {sleep_seconds} before retrying.",
)
if retry_message_event_id is not None:
event_content.set_edit(retry_message_event_id)
new_event_id = await self.main_intent.send_message(self.mxid, event_content)
retry_message_event_id = retry_message_event_id or new_event_id

await asyncio.sleep(sleep_seconds)
except Exception as e:
await sender.handle_auth_failure(e)
raise

if retry_message_event_id is not None:
await self.main_intent.redact(self.mxid, retry_message_event_id)
event_type_name = {
EventType.ROOM_MESSAGE: "message",
EventType.REACTION: "reaction",
}.get(event_type, str(event_type))
raise NotConnected(f"Failed to send {event_type_name} after {retry_count} retries.")
raise last_error_type(f"Failed to send {event_type_name} after {retry_count} retries.")

async def handle_matrix_reaction(
self, sender: u.User, event_id: EventID, reacting_to: EventID, emoji: str
Expand All @@ -550,15 +572,17 @@ async def handle_matrix_reaction(
emoji=emoji,
)
except Exception as e:
self.log.exception("Sending reaction failed")
self.log.exception(f"Failed to handle Matrix reaction {event_id} to {reacting_to}")
sender.send_remote_checkpoint(
MessageSendCheckpointStatus.PERM_FAILURE,
event_id,
self.mxid,
EventType.REACTION,
error=e,
)
await self._send_error_notice("reaction", e)
await sender.handle_auth_failure(e)
asyncio.create_task(self._send_message_status(event_id, e))
else:
sender.send_remote_checkpoint(
MessageSendCheckpointStatus.SUCCESS,
Expand All @@ -568,6 +592,7 @@ async def handle_matrix_reaction(
retry_num=retry_count,
)
await self._send_delivery_receipt(event_id)
asyncio.create_task(self._send_message_status(event_id, err=None))

async def _handle_matrix_reaction(
self,
Expand Down Expand Up @@ -621,7 +646,10 @@ async def handle_matrix_redaction(
sender.username, recipient=self.chat_id, timestamp=message.timestamp
)
except Exception as e:
self.log.exception("Removing message failed")
self.log.exception(
f"Failed to handle Matrix redaction {redaction_event_id} of "
f"message {event_id} ({message.timestamp})"
)
sender.send_remote_checkpoint(
MessageSendCheckpointStatus.PERM_FAILURE,
redaction_event_id,
Expand All @@ -630,6 +658,8 @@ async def handle_matrix_redaction(
error=e,
)
await sender.handle_auth_failure(e)
asyncio.create_task(self._send_error_notice("message deletion", e))
asyncio.create_task(self._send_message_status(event_id, e))
else:
self.log.trace(f"Removed {message} after Matrix redaction")
sender.send_remote_checkpoint(
Expand All @@ -639,6 +669,7 @@ async def handle_matrix_redaction(
EventType.ROOM_REDACTION,
)
await self._send_delivery_receipt(redaction_event_id)
asyncio.create_task(self._send_message_status(redaction_event_id, err=None))
return

reaction = await DBReaction.get_by_mxid(event_id, self.mxid)
Expand All @@ -655,7 +686,10 @@ async def handle_matrix_redaction(
username=sender.username, recipient=self.chat_id, reaction=remove_reaction
)
except Exception as e:
self.log.exception("Removing reaction failed")
self.log.exception(
f"Failed to handle Matrix redaction {redaction_event_id} of "
f"reaction {event_id} to {reaction.msg_timestamp}"
)
sender.send_remote_checkpoint(
MessageSendCheckpointStatus.PERM_FAILURE,
redaction_event_id,
Expand All @@ -664,6 +698,8 @@ async def handle_matrix_redaction(
error=e,
)
await sender.handle_auth_failure(e)
asyncio.create_task(self._send_error_notice("reaction deletion", e))
asyncio.create_task(self._send_message_status(event_id, e))
else:
self.log.trace(f"Removed {reaction} after Matrix redaction")
sender.send_remote_checkpoint(
Expand All @@ -673,15 +709,18 @@ async def handle_matrix_redaction(
EventType.ROOM_REDACTION,
)
await self._send_delivery_receipt(redaction_event_id)
asyncio.create_task(self._send_message_status(redaction_event_id, err=None))
return

sender.send_remote_checkpoint(
MessageSendCheckpointStatus.PERM_FAILURE,
redaction_event_id,
self.mxid,
EventType.ROOM_REDACTION,
error=f"No message or reaction found for redaction",
error="No message or reaction found for redaction",
)
status_err = UnknownReactionTarget("No message or reaction found for redaction")
asyncio.create_task(self._send_message_status(redaction_event_id, err=status_err))

async def handle_matrix_join(self, user: u.User) -> None:
if self.is_direct or not await user.is_logged_in():
Expand Down Expand Up @@ -735,7 +774,7 @@ async def handle_matrix_invite(self, invited_by: u.User, user: u.User | p.Puppet
invited_by.username, self.chat_id, add_members=[user.address]
)
except RPCError as e:
raise RejectMatrixInvite(e.message) from e
raise RejectMatrixInvite(str(e)) from e
power_levels = await self.main_intent.get_power_levels(self.mxid)
invitee_pl = power_levels.get_user_level(user.mxid)
if invitee_pl >= 50:
Expand Down
Loading

0 comments on commit 053db40

Please sign in to comment.