diff --git a/base-config.yaml b/base-config.yaml
index 1f6de2f..509ba2e 100644
--- a/base-config.yaml
+++ b/base-config.yaml
@@ -91,7 +91,7 @@ welcome_sleep: 0
# (optional)
notification_room:
-# message to send to the notification room when someone joins one of the above rooms:
+# message to send to the notification room when someone joins or leave one of the above rooms:
# available placeholders:
# - {user}: display name of the joining user (falls back to localpart or user ID)
# - {user_id}: full Matrix user ID
@@ -101,6 +101,15 @@ notification_room:
# - {room_id}: raw room ID
join_notification_message: |
{user} has joined {room}.
+
+leave_notification_message: |
+ {user} has left {room}.
+
+kick_notification_message: |
+ {user} was kicked by {actor} from {room}.
+
+ban_notification_message: |
+ {user} was baned by {actor} from {room}.
# whether to censor files/messages
# can be boolean (true/false) for all-or-nothing behavior,
diff --git a/community/bot.py b/community/bot.py
index f7a1dc8..65fe80b 100644
--- a/community/bot.py
+++ b/community/bot.py
@@ -206,6 +206,8 @@ class CommunityBot(Plugin):
target_room_id: RoomID,
template: str,
context: RenderContext,
+ actor_id: Optional[str] = None,
+ actor_display: Optional[str] = None,
) -> None:
"""Render a template once and send plaintext + HTML variants."""
plain_text, html_message = self._render_message_template(
@@ -214,6 +216,8 @@ class CommunityBot(Plugin):
context.user_display,
context.room_id,
context.room_text,
+ actor_id,
+ actor_display,
)
await self.client.send_notice(target_room_id, plain_text, html=html_message)
@@ -237,6 +241,56 @@ class CommunityBot(Plugin):
self.config["join_notification_message"],
context,
)
+
+ async def _handle_leave_notifications(self, evt: StateEvent, event_type: str) -> None:
+ """Send notifications when a user leaves, is kicked, or banned."""
+
+ # Optional: nur relevante Räume (wie bei join)
+ space_rooms = await self.get_space_roomlist()
+ if evt.room_id not in space_rooms:
+ return
+
+ if not self.config["notification_room"]:
+ return
+
+ user_id = evt.state_key
+ sender = evt.sender
+
+ context = await self._build_render_context(evt.room_id, user_id)
+
+ # === Actor bestimmen ===
+ actor_id = None
+ actor_display = None
+
+ if sender != user_id:
+ actor_id = sender
+ actor_display = await self._get_user_display_name(evt.room_id, sender)
+
+ # === Event-Typ bestimmen ===
+ if event_type == "leave" and sender == user_id:
+ template = self.config["leave_notification_message"]
+
+ elif event_type == "kick":
+ template = self.config["kick_notification_message"]
+
+ elif event_type == "ban":
+ template = self.config["ban_notification_message"]
+
+ else:
+ return
+
+ # === Template cleanup wenn kein actor ===
+ if not actor_id:
+ template = template.replace(" von {actor}", "")
+ template = template.replace(" {actor}", "")
+
+ await self._send_rendered_notice(
+ self.config["notification_room"],
+ template,
+ context,
+ actor_id=actor_id,
+ actor_display=actor_display,
+ )
def _is_human_verification_enabled_for_room(self, room_id: RoomID) -> bool:
configured = self.config["check_if_human"]
@@ -407,6 +461,20 @@ class CommunityBot(Plugin):
href = self._matrix_uri_user(str(user_id))
return label, f"{escape(label)}"
+
+ def _format_actor_pill(
+ self,
+ actor_id: Optional[str],
+ actor_display: Optional[str] = None,
+ ) -> Tuple[str, str]:
+ """Return plaintext and HTML variants for the {actor} placeholder."""
+ if not actor_id:
+ return "", ""
+
+ safe_display = actor_display or actor_id
+ href = self._matrix_uri_user(str(actor_id))
+
+ return safe_display, f"{escape(safe_display)}"
def _format_room_pill(
self,
@@ -434,6 +502,8 @@ class CommunityBot(Plugin):
user_display: Optional[str] = None,
room_id: Optional[str] = None,
room_text: Optional[str] = None,
+ actor_id: Optional[str] = None,
+ actor_display: Optional[str] = None,
) -> Tuple[str, str]:
user_url = self._matrix_to_url(user_id)
safe_user_display = user_display or user_id
@@ -442,6 +512,7 @@ class CommunityBot(Plugin):
room_url = self._matrix_to_url(safe_room_id) if safe_room_id else ""
user_plain, user_html = self._format_user_pill(user_id, safe_user_display)
room_plain, room_html = self._format_room_pill(safe_room_id, safe_room_text)
+ actor_plain, actor_html = self._format_actor_pill(actor_id, actor_display)
plain_text = template.format(
user=user_plain,
@@ -450,6 +521,8 @@ class CommunityBot(Plugin):
room=room_plain,
room_link=room_url,
room_id=safe_room_id,
+ actor=actor_plain,
+ actor_display=actor_display or "",
)
html_message = template.format(
@@ -462,7 +535,9 @@ class CommunityBot(Plugin):
if room_url
else escape(safe_room_text)
),
- room_id=escape(safe_room_id),
+ room_id=safe_room_id,
+ actor=actor_html,
+ actor_display=escape(actor_display) if actor_display else "",
)
return plain_text, html_message
@@ -1331,16 +1406,19 @@ class CommunityBot(Plugin):
async def handle_leave(self, evt: StateEvent) -> None:
"""Handle voluntary leave events."""
await self.handle_leave_events(evt)
+ await self._handle_leave_notifications(evt, "leave")
@event.on(InternalEventType.KICK)
async def handle_kick(self, evt: StateEvent) -> None:
"""Handle kick events."""
await self.handle_leave_events(evt)
+ await self._handle_leave_notifications(evt, "kick")
@event.on(InternalEventType.BAN)
async def handle_ban(self, evt: StateEvent) -> None:
"""Handle ban events."""
await self.handle_leave_events(evt)
+ await self._handle_leave_notifications(evt, "ban")
@event.on(InternalEventType.JOIN)
async def newjoin(self, evt: StateEvent) -> None:
diff --git a/maubot.yaml b/maubot.yaml
index 9e78e18..8f87854 100644
--- a/maubot.yaml
+++ b/maubot.yaml
@@ -2,7 +2,7 @@ maubot: 0.1.0
id: advanced-community-bot
name: Advanced Community Bot
description: Advanced Community Bot is a Maubot Bot Plugin to manage Matrix servers.
-version: 1.0.0
+version: 1.0.1
license: MIT
modules: