Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| edd3eee178 | |||
| 933865d80c | |||
| b2541c4054 | |||
| bc490bd084 | |||
| 1e653c60e3 |
+27
-3
@@ -10,6 +10,10 @@ parent_room: ''
|
|||||||
# leave blank to generate an acronym of your community name during initialization
|
# leave blank to generate an acronym of your community name during initialization
|
||||||
community_slug: ''
|
community_slug: ''
|
||||||
|
|
||||||
|
# use_community_slug
|
||||||
|
# whether to use the community slug as a suffix for room aliases
|
||||||
|
use_community_slug: true
|
||||||
|
|
||||||
# sleep time between actions. you can drop this to 0 if your bot has no
|
# sleep time between actions. you can drop this to 0 if your bot has no
|
||||||
# ratelimits imposed on its homeserver, otherwise you may want to increase this
|
# ratelimits imposed on its homeserver, otherwise you may want to increase this
|
||||||
# to avoid errors.
|
# to avoid errors.
|
||||||
@@ -54,8 +58,13 @@ invitees: []
|
|||||||
|
|
||||||
# auto-greet users in rooms with these messages
|
# auto-greet users in rooms with these messages
|
||||||
# map greeting messages to a room
|
# map greeting messages to a room
|
||||||
# you can use {user} to reference the joining user in this message using a
|
# available placeholders:
|
||||||
# matrix.to link (rendered as a "pill" in element clients)
|
# - {user}: display name of the joining user (falls back to localpart or user ID)
|
||||||
|
# - {user_id}: full Matrix user ID
|
||||||
|
# - {user_link}: clickable matrix.to-compatible link to the joining user
|
||||||
|
# - {room}: room name (or room ID if no name is set)
|
||||||
|
# - {room_link}: clickable matrix.to-compatible link to the room
|
||||||
|
# - {room_id}: raw room ID
|
||||||
# html formatting is supported
|
# html formatting is supported
|
||||||
# set to {} if you don't care about greetings
|
# set to {} if you don't care about greetings
|
||||||
greetings:
|
greetings:
|
||||||
@@ -83,8 +92,15 @@ welcome_sleep: 0
|
|||||||
notification_room:
|
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 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
|
||||||
|
# - {user_link}: clickable matrix.to-compatible link to the joining user
|
||||||
|
# - {room}: room name (or room ID if no name is set)
|
||||||
|
# - {room_link}: clickable matrix.to-compatible link to the room
|
||||||
|
# - {room_id}: raw room ID
|
||||||
join_notification_message: |
|
join_notification_message: |
|
||||||
{user} has joined {room_link}.
|
{user} has joined {room}.
|
||||||
|
|
||||||
# whether to censor files/messages
|
# whether to censor files/messages
|
||||||
# can be boolean (true/false) for all-or-nothing behavior,
|
# can be boolean (true/false) for all-or-nothing behavior,
|
||||||
@@ -173,3 +189,11 @@ verification_message: |
|
|||||||
Thank you for joining {room}. As an anti-spam measure, you must demonstrate that you are a real person before you can send messages in its rooms.
|
Thank you for joining {room}. As an anti-spam measure, you must demonstrate that you are a real person before you can send messages in its rooms.
|
||||||
|
|
||||||
Please send a message to this chat with the content: "{phrase}"
|
Please send a message to this chat with the content: "{phrase}"
|
||||||
|
|
||||||
|
# prefixes used for the clickable {user} and {room} placeholders in rendered HTML notices.
|
||||||
|
# set either value to "" for a cleaner look without a visible prefix.
|
||||||
|
|
||||||
|
# Base URL for Matrix permalink generation.
|
||||||
|
# This is used for placeholders such as {user_link} and {room_link}.
|
||||||
|
# Set this to your own matrix.to-compatible instance if you do not want to use https://matrix.to.
|
||||||
|
matrix_to_base_url: "https://matrix.to"
|
||||||
|
|||||||
+477
-378
File diff suppressed because it is too large
Load Diff
@@ -99,6 +99,14 @@ class ConfigManager:
|
|||||||
"""
|
"""
|
||||||
return self.config.get("community_slug")
|
return self.config.get("community_slug")
|
||||||
|
|
||||||
|
def get_use_community_slug(self) -> Optional[str]:
|
||||||
|
"""Get the community slug suffix setting.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: Whether to use the community slug as a room suffix
|
||||||
|
"""
|
||||||
|
return self.config.get("use_community_slug")
|
||||||
|
|
||||||
def get_parent_room(self) -> Optional[str]:
|
def get_parent_room(self) -> Optional[str]:
|
||||||
"""Get the parent room ID.
|
"""Get the parent room ID.
|
||||||
|
|
||||||
@@ -201,7 +209,12 @@ class ConfigManager:
|
|||||||
Returns:
|
Returns:
|
||||||
List[str]: List of missing required configuration keys
|
List[str]: List of missing required configuration keys
|
||||||
"""
|
"""
|
||||||
required_configs = ["parent_room", "room_version", "community_slug"]
|
required_configs = [
|
||||||
|
"parent_room",
|
||||||
|
"room_version",
|
||||||
|
"community_slug",
|
||||||
|
"use_community_slug",
|
||||||
|
]
|
||||||
|
|
||||||
missing = []
|
missing = []
|
||||||
for config_key in required_configs:
|
for config_key in required_configs:
|
||||||
@@ -231,6 +244,7 @@ class ConfigManager:
|
|||||||
return {
|
return {
|
||||||
"room_version": self.get_room_version(),
|
"room_version": self.get_room_version(),
|
||||||
"community_slug": self.get_community_slug(),
|
"community_slug": self.get_community_slug(),
|
||||||
|
"use_community_slug": self.get_use_community_slug(),
|
||||||
"invitees": self.get_invitees(),
|
"invitees": self.get_invitees(),
|
||||||
"invite_power_level": self.get_invite_power_level(),
|
"invite_power_level": self.get_invite_power_level(),
|
||||||
"encrypt": self.is_encryption_enabled(),
|
"encrypt": self.is_encryption_enabled(),
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ async def validate_room_creation_params(
|
|||||||
sanitized_name = re.sub(r"[^a-zA-Z0-9]", "", roomname).lower()
|
sanitized_name = re.sub(r"[^a-zA-Z0-9]", "", roomname).lower()
|
||||||
|
|
||||||
# Check if community slug is configured
|
# Check if community slug is configured
|
||||||
if not config.get("community_slug", ""):
|
if config.get("use_community_slug", True) and not config.get("community_slug", ""):
|
||||||
error_msg = "No community slug configured. Please run initialize command first."
|
error_msg = "No community slug configured. Please run initialize command first."
|
||||||
return sanitized_name, force_encryption, force_unencryption, error_msg, roomname
|
return sanitized_name, force_encryption, force_unencryption, error_msg, roomname
|
||||||
|
|
||||||
@@ -63,7 +63,10 @@ async def prepare_room_creation_data(
|
|||||||
Tuple of (alias_localpart, server, room_invitees, parent_room)
|
Tuple of (alias_localpart, server, room_invitees, parent_room)
|
||||||
"""
|
"""
|
||||||
# Create alias with community slug
|
# Create alias with community slug
|
||||||
alias_localpart = f"{sanitized_name}-{config.get('community_slug', '')}"
|
if config.get("use_community_slug", True):
|
||||||
|
alias_localpart = f"{sanitized_name}-{config.get('community_slug', '')}"
|
||||||
|
else:
|
||||||
|
alias_localpart = sanitized_name
|
||||||
|
|
||||||
# Get server and invitees
|
# Get server and invitees
|
||||||
server = client.parse_user_id(client.mxid)[1]
|
server = client.parse_user_id(client.mxid)[1]
|
||||||
|
|||||||
@@ -31,7 +31,11 @@ async def validate_room_alias(client, alias_localpart: str, server: str) -> bool
|
|||||||
|
|
||||||
|
|
||||||
async def validate_room_aliases(
|
async def validate_room_aliases(
|
||||||
client, room_names: list[str], community_slug: str, server: str
|
client,
|
||||||
|
room_names: list[str],
|
||||||
|
community_slug: str,
|
||||||
|
use_community_slug: bool,
|
||||||
|
server: str,
|
||||||
) -> Tuple[bool, List[str]]:
|
) -> Tuple[bool, List[str]]:
|
||||||
"""Validate that all room aliases are available.
|
"""Validate that all room aliases are available.
|
||||||
|
|
||||||
@@ -39,12 +43,13 @@ async def validate_room_aliases(
|
|||||||
client: Matrix client instance
|
client: Matrix client instance
|
||||||
room_names: List of room names to validate
|
room_names: List of room names to validate
|
||||||
community_slug: The community slug to append
|
community_slug: The community slug to append
|
||||||
|
use_community_slug: Whether to append a community slug
|
||||||
server: The server domain
|
server: The server domain
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
tuple: (is_valid, list_of_conflicting_aliases)
|
tuple: (is_valid, list_of_conflicting_aliases)
|
||||||
"""
|
"""
|
||||||
if not community_slug:
|
if use_community_slug and not community_slug:
|
||||||
return False, []
|
return False, []
|
||||||
|
|
||||||
conflicting_aliases = []
|
conflicting_aliases = []
|
||||||
@@ -54,7 +59,10 @@ async def validate_room_aliases(
|
|||||||
from .message_utils import sanitize_room_name
|
from .message_utils import sanitize_room_name
|
||||||
|
|
||||||
sanitized_name = sanitize_room_name(room_name)
|
sanitized_name = sanitize_room_name(room_name)
|
||||||
alias_localpart = f"{sanitized_name}-{community_slug}"
|
if use_community_slug:
|
||||||
|
alias_localpart = f"{sanitized_name}-{community_slug}"
|
||||||
|
else:
|
||||||
|
alias_localpart = sanitized_name
|
||||||
|
|
||||||
# Check if alias is available
|
# Check if alias is available
|
||||||
is_available = await validate_room_alias(client, alias_localpart, server)
|
is_available = await validate_room_alias(client, alias_localpart, server)
|
||||||
|
|||||||
@@ -67,6 +67,9 @@ plugin_config:
|
|||||||
# leave blank to generate an acronym of your community name during initialization
|
# leave blank to generate an acronym of your community name during initialization
|
||||||
community_slug: ''
|
community_slug: ''
|
||||||
|
|
||||||
|
# use_community_slug
|
||||||
|
# whether to use the community slug as a suffix for room aliases
|
||||||
|
use_community_slug: true
|
||||||
|
|
||||||
# sleep time between actions. you can drop this to 0 if your bot has no
|
# sleep time between actions. you can drop this to 0 if your bot has no
|
||||||
# ratelimits imposed on its homeserver, otherwise you may want to increase this
|
# ratelimits imposed on its homeserver, otherwise you may want to increase this
|
||||||
@@ -112,8 +115,13 @@ plugin_config:
|
|||||||
|
|
||||||
# auto-greet users in rooms with these messages
|
# auto-greet users in rooms with these messages
|
||||||
# map greeting messages to a room
|
# map greeting messages to a room
|
||||||
# you can use {user} to reference the joining user in this message using a
|
# available placeholders:
|
||||||
# matrix.to link (rendered as a "pill" in element clients)
|
# - {user}: display name of the joining user (falls back to localpart or user ID)
|
||||||
|
# - {user_id}: full Matrix user ID
|
||||||
|
# - {user_link}: clickable matrix.to-compatible link to the joining user
|
||||||
|
# - {room}: room name (or room ID if no name is set)
|
||||||
|
# - {room_link}: clickable matrix.to-compatible link to the room
|
||||||
|
# - {room_id}: raw room ID
|
||||||
# html formatting is supported
|
# html formatting is supported
|
||||||
# set to {} if you don't care about greetings
|
# set to {} if you don't care about greetings
|
||||||
greetings:
|
greetings:
|
||||||
@@ -142,7 +150,12 @@ plugin_config:
|
|||||||
|
|
||||||
# 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 one of the above rooms:
|
||||||
join_notification_message: |
|
join_notification_message: |
|
||||||
User <code>{user}</code> has joined <code>{room}</code>.
|
{user} has joined {room}.
|
||||||
|
|
||||||
|
# Base URL for Matrix permalink generation.
|
||||||
|
# This is used for placeholders such as {user_link} and {room_link}.
|
||||||
|
# Set this to your own matrix.to-compatible instance if you do not want to use https://matrix.to.
|
||||||
|
matrix_to_base_url: "https://matrix.to"
|
||||||
|
|
||||||
# whether to censor files/messages
|
# whether to censor files/messages
|
||||||
# can be boolean (true/false) for all-or-nothing behavior,
|
# can be boolean (true/false) for all-or-nothing behavior,
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
maubot: 0.1.0
|
maubot: 0.1.0
|
||||||
id: org.jobmachine.communitybot
|
id: org.jobmachine.communitybot
|
||||||
version: 0.4.1
|
version: 0.6.0
|
||||||
license: MIT
|
license: MIT
|
||||||
modules:
|
modules:
|
||||||
- community
|
- community
|
||||||
|
|||||||
Executable → Regular
Executable → Regular
@@ -264,6 +264,47 @@ class TestBotEvents:
|
|||||||
# Should update user timestamp
|
# Should update user timestamp
|
||||||
real_bot.upsert_user_timestamp.assert_called()
|
real_bot.upsert_user_timestamp.assert_called()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_newjoin_notification_supports_user_link(self, bot, mock_state_evt):
|
||||||
|
"""Test join notification formatting with user_link placeholder."""
|
||||||
|
from community.bot import CommunityBot
|
||||||
|
real_bot = CommunityBot()
|
||||||
|
real_bot.config = {
|
||||||
|
**bot.config,
|
||||||
|
"greeting_rooms": {"!room:example.com": "none"},
|
||||||
|
"greetings": {},
|
||||||
|
"notification_room": "!notifications:example.com",
|
||||||
|
"join_notification_message": "{user_link} joined {room_link} ({room_id})"
|
||||||
|
}
|
||||||
|
real_bot.client = bot.client
|
||||||
|
real_bot.database = bot.database
|
||||||
|
real_bot.log = bot.log
|
||||||
|
|
||||||
|
real_bot.client.parse_user_id = Mock(return_value=("alice", "example.com"))
|
||||||
|
room_name_state = Mock()
|
||||||
|
room_name_state.name = "Test Room"
|
||||||
|
real_bot.client.get_state_event = AsyncMock(return_value=room_name_state)
|
||||||
|
real_bot.client.send_notice = AsyncMock()
|
||||||
|
real_bot.database.execute = AsyncMock()
|
||||||
|
|
||||||
|
mock_state_evt.source = 0
|
||||||
|
|
||||||
|
with patch.object(real_bot, 'get_space_roomlist', return_value=["!room:example.com"]), \
|
||||||
|
patch.object(real_bot, 'check_if_banned', return_value=False), \
|
||||||
|
patch.object(real_bot, 'upsert_user_timestamp', return_value=None):
|
||||||
|
await real_bot.newjoin(mock_state_evt)
|
||||||
|
|
||||||
|
real_bot.client.send_notice.assert_called_once()
|
||||||
|
args, kwargs = real_bot.client.send_notice.call_args
|
||||||
|
assert args[0] == "!notifications:example.com"
|
||||||
|
assert args[1] == "https://matrix.to/#/@user:example.com joined https://matrix.to/#/!room:example.com (!room:example.com)"
|
||||||
|
assert "https://matrix.to/#/@user:example.com" in kwargs["html"]
|
||||||
|
assert ">alice</a>" in kwargs["html"]
|
||||||
|
assert "https://matrix.to/#/!room:example.com" in kwargs["html"]
|
||||||
|
assert ">Test Room</a>" in kwargs["html"]
|
||||||
|
assert "(!room:example.com)" in kwargs["html"]
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_update_message_timestamp_tracking_enabled(self, bot, mock_message_evt):
|
async def test_update_message_timestamp_tracking_enabled(self, bot, mock_message_evt):
|
||||||
"""Test message timestamp update with tracking enabled."""
|
"""Test message timestamp update with tracking enabled."""
|
||||||
@@ -450,3 +491,104 @@ class TestBotEvents:
|
|||||||
|
|
||||||
# Should not update user timestamp
|
# Should not update user timestamp
|
||||||
real_bot.upsert_user_timestamp.assert_not_called()
|
real_bot.upsert_user_timestamp.assert_not_called()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_sleep_if_configured_uses_asyncio_sleep(self):
|
||||||
|
"""Test that configured delays use asyncio.sleep without blocking."""
|
||||||
|
real_bot = CommunityBot()
|
||||||
|
with patch("community.bot.asyncio.sleep", new=AsyncMock()) as sleep_mock:
|
||||||
|
await real_bot._sleep_if_configured(1.5)
|
||||||
|
sleep_mock.assert_awaited_once_with(1.5)
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_newjoin_uses_async_delay_for_greeting(self, bot, mock_state_evt):
|
||||||
|
"""Test that join greetings use the async delay helper."""
|
||||||
|
real_bot = CommunityBot()
|
||||||
|
real_bot.client = Mock()
|
||||||
|
real_bot.client.parse_user_id.return_value = ("alice", "example.com")
|
||||||
|
real_bot.client.send_notice = AsyncMock()
|
||||||
|
real_bot.client.get_state_event = AsyncMock(return_value=Mock(name="Test Room"))
|
||||||
|
real_bot.database = bot.database
|
||||||
|
real_bot.log = bot.log
|
||||||
|
real_bot.config = {
|
||||||
|
**bot.config,
|
||||||
|
"parent_room": "!parent:example.com",
|
||||||
|
"greeting_rooms": {"!room:example.com": "default"},
|
||||||
|
"greetings": {"default": "Welcome {user}"},
|
||||||
|
"welcome_sleep": 2,
|
||||||
|
"notification_room": "!notif:example.com",
|
||||||
|
"join_notification_message": "{user_link} joined {room_link}",
|
||||||
|
}
|
||||||
|
real_bot._sleep_if_configured = AsyncMock()
|
||||||
|
real_bot.check_if_banned = AsyncMock(return_value=False)
|
||||||
|
real_bot.ban_this_user = AsyncMock()
|
||||||
|
real_bot.do_sync = AsyncMock()
|
||||||
|
real_bot.get_space_roomlist = AsyncMock(return_value=["!room:example.com"])
|
||||||
|
|
||||||
|
await real_bot.newjoin(mock_state_evt)
|
||||||
|
|
||||||
|
real_bot._sleep_if_configured.assert_awaited_once_with(2)
|
||||||
|
assert real_bot.client.send_notice.await_count == 2
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_user_kick_uses_async_delay(self, bot):
|
||||||
|
"""Test that user kicking uses the async delay helper between rooms."""
|
||||||
|
real_bot = CommunityBot()
|
||||||
|
real_bot.client = Mock()
|
||||||
|
real_bot.client.get_state_event = AsyncMock(side_effect=[
|
||||||
|
{"name": "Room One"},
|
||||||
|
{}, # membership lookup
|
||||||
|
])
|
||||||
|
real_bot.client.kick_user = AsyncMock()
|
||||||
|
real_bot.database = bot.database
|
||||||
|
real_bot.log = bot.log
|
||||||
|
real_bot.config = {**bot.config, "sleep": 0.5, "parent_room": "!room:example.com"}
|
||||||
|
real_bot._sleep_if_configured = AsyncMock()
|
||||||
|
|
||||||
|
evt = Mock(spec=MessageEvent)
|
||||||
|
evt.mark_read = AsyncMock()
|
||||||
|
evt.respond = AsyncMock()
|
||||||
|
evt.reply = AsyncMock()
|
||||||
|
|
||||||
|
with patch.object(real_bot, "get_space_roomlist", AsyncMock(return_value=[])):
|
||||||
|
await real_bot.user_kick(evt, "@user:example.com")
|
||||||
|
|
||||||
|
real_bot._sleep_if_configured.assert_awaited_once_with(0.5)
|
||||||
|
|
||||||
|
def test_render_message_template_uses_consistent_user_placeholders(self):
|
||||||
|
"""{user} should stay plain while {user_link} becomes clickable."""
|
||||||
|
real_bot = CommunityBot()
|
||||||
|
|
||||||
|
plain_text, html_text = real_bot._render_message_template(
|
||||||
|
"{user} | {user_link} | {room} | {room_link} | {room_id}",
|
||||||
|
"@alice:example.com",
|
||||||
|
"alice",
|
||||||
|
"!room:example.com",
|
||||||
|
"Test Room",
|
||||||
|
)
|
||||||
|
|
||||||
|
assert plain_text == (
|
||||||
|
"@alice:example.com | https://matrix.to/#/@alice:example.com | "
|
||||||
|
"Test Room | https://matrix.to/#/!room:example.com | !room:example.com"
|
||||||
|
)
|
||||||
|
assert "@alice:example.com" in html_text
|
||||||
|
assert "<a href='https://matrix.to/#/@alice:example.com'>alice</a>" in html_text
|
||||||
|
assert "<a href='https://matrix.to/#/!room:example.com'>Test Room</a>" in html_text
|
||||||
|
|
||||||
|
def test_render_message_template_without_room_keeps_room_placeholders_safe(self):
|
||||||
|
"""Greeting templates without room data should not break placeholder rendering."""
|
||||||
|
real_bot = CommunityBot()
|
||||||
|
|
||||||
|
plain_text, html_text = real_bot._render_message_template(
|
||||||
|
"Welcome {user} / {user_link}",
|
||||||
|
"@alice:example.com",
|
||||||
|
"alice",
|
||||||
|
)
|
||||||
|
|
||||||
|
assert plain_text == "Welcome @alice:example.com / https://matrix.to/#/@alice:example.com"
|
||||||
|
assert html_text == (
|
||||||
|
"Welcome @alice:example.com / "
|
||||||
|
"<a href='https://matrix.to/#/@alice:example.com'>alice</a>"
|
||||||
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,56 @@
|
|||||||
|
import html
|
||||||
|
from types import SimpleNamespace
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from community.bot import CommunityBot, DEFAULT_ROOM_PILL_PREFIX, DEFAULT_USER_PILL_PREFIX
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture()
|
||||||
|
def bot() -> CommunityBot:
|
||||||
|
plugin = CommunityBot.__new__(CommunityBot)
|
||||||
|
plugin.config = {}
|
||||||
|
return plugin
|
||||||
|
|
||||||
|
|
||||||
|
def test_user_uri_helper_strips_at_and_uses_chat_action(bot: CommunityBot) -> None:
|
||||||
|
assert bot._matrix_user_uri("@alice:example.org") == "matrix:u/alice:example.org?action=chat"
|
||||||
|
|
||||||
|
|
||||||
|
def test_room_uri_helper_prefers_alias(bot: CommunityBot) -> None:
|
||||||
|
assert bot._matrix_room_uri("!roomid:example.org", "#general:example.org") == "matrix:r/general:example.org"
|
||||||
|
|
||||||
|
|
||||||
|
def test_room_uri_helper_falls_back_to_room_id_without_bang(bot: CommunityBot) -> None:
|
||||||
|
assert bot._matrix_room_uri("!roomid:example.org", None) == "matrix:roomid/roomid:example.org"
|
||||||
|
|
||||||
|
|
||||||
|
def test_event_uri_helper_strips_prefixes(bot: CommunityBot) -> None:
|
||||||
|
assert bot._matrix_event_uri("!roomid:example.org", "$eventid") == "matrix:roomid/roomid:example.org/e/eventid"
|
||||||
|
|
||||||
|
|
||||||
|
def test_format_user_pill_uses_clean_default_prefix(bot: CommunityBot) -> None:
|
||||||
|
plain, formatted = bot._format_user_pill("@alice:example.org", "Alice")
|
||||||
|
assert plain == f"{DEFAULT_USER_PILL_PREFIX}Alice"
|
||||||
|
assert 'href="matrix:u/alice:example.org?action=chat"' in formatted
|
||||||
|
assert ">Alice<" in formatted
|
||||||
|
|
||||||
|
|
||||||
|
def test_format_room_pill_uses_alias_when_available(bot: CommunityBot) -> None:
|
||||||
|
plain, formatted = bot._format_room_pill("!roomid:example.org", "General", "#general:example.org")
|
||||||
|
assert plain == f"{DEFAULT_ROOM_PILL_PREFIX}General"
|
||||||
|
assert formatted == '<a href="matrix:r/general:example.org">General</a>'
|
||||||
|
|
||||||
|
|
||||||
|
def test_format_room_pill_falls_back_to_room_id(bot: CommunityBot) -> None:
|
||||||
|
plain, formatted = bot._format_room_pill("!roomid:example.org", "General", None)
|
||||||
|
assert plain == f"{DEFAULT_ROOM_PILL_PREFIX}General"
|
||||||
|
assert formatted == '<a href="matrix:roomid/roomid:example.org">General</a>'
|
||||||
|
|
||||||
|
|
||||||
|
def test_format_user_pill_escapes_displayname(bot: CommunityBot) -> None:
|
||||||
|
plain, formatted = bot._format_user_pill("@alice:example.org", '<Admin & Ops>')
|
||||||
|
assert plain == f"{DEFAULT_USER_PILL_PREFIX}<Admin & Ops>"
|
||||||
|
# Keep this broad enough to avoid coupling to quote style.
|
||||||
|
assert "matrix:u/alice:example.org?action=chat" in formatted
|
||||||
|
assert html.escape('<Admin & Ops>') in formatted
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
from community.bot import CommunityBot
|
||||||
|
|
||||||
|
|
||||||
|
def test_matrix_uri_wrappers_delegate_to_canonical_helpers() -> None:
|
||||||
|
bot = CommunityBot.__new__(CommunityBot)
|
||||||
|
bot.config = {}
|
||||||
|
assert bot._matrix_user_uri("@alice:example.org") == "matrix:u/alice:example.org?action=chat"
|
||||||
|
assert bot._matrix_room_uri("!roomid:example.org") == "matrix:roomid/roomid:example.org"
|
||||||
|
assert bot._matrix_room_uri("!roomid:example.org", "#general:example.org") == "matrix:r/general:example.org"
|
||||||
|
assert bot._matrix_event_uri("!roomid:example.org", "$eventid") == "matrix:roomid/roomid:example.org/e/eventid"
|
||||||
+49
-16
@@ -46,36 +46,69 @@ class TestRoomUtils:
|
|||||||
assert result == True
|
assert result == True
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_validate_room_aliases_no_slug(self):
|
async def test_validate_room_aliases_slug_not_required_with_no_slug(self):
|
||||||
"""Test alias validation without community slug."""
|
"""Test alias validation without community slug."""
|
||||||
client = Mock()
|
client = Mock()
|
||||||
|
|
||||||
result = await validate_room_aliases(client, ["room1", "room2"], "", "example.com")
|
|
||||||
assert result == (False, [])
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
result = await validate_room_aliases(client, ["room1", "room2"], "", False, "example.com")
|
||||||
async def test_validate_room_aliases_success(self):
|
|
||||||
"""Test successful alias validation."""
|
|
||||||
client = Mock()
|
|
||||||
client.resolve_room_alias = AsyncMock(side_effect=MNotFound("Room not found", 404))
|
|
||||||
|
|
||||||
result = await validate_room_aliases(client, ["room1", "room2"], "test", "example.com")
|
|
||||||
assert result == (True, [])
|
assert result == (True, [])
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_validate_room_aliases_conflicts(self):
|
async def test_validate_room_aliases_slug_not_required_with_slug(self):
|
||||||
|
"""Test successful alias validation."""
|
||||||
|
client = Mock()
|
||||||
|
client.resolve_room_alias = AsyncMock(side_effect=MNotFound("Room not found", 404))
|
||||||
|
|
||||||
|
result = await validate_room_aliases(client, ["room1", "room2"], "test", False, "example.com")
|
||||||
|
assert result == (True, [])
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_validate_room_aliases_slug_required_with_no_slug(self):
|
||||||
|
"""Test alias validation without community slug."""
|
||||||
|
client = Mock()
|
||||||
|
|
||||||
|
result = await validate_room_aliases(client, ["room1", "room2"], "", True, "example.com")
|
||||||
|
assert result == (False, [])
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_validate_room_aliases_slug_required_with_slug(self):
|
||||||
|
"""Test successful alias validation."""
|
||||||
|
client = Mock()
|
||||||
|
client.resolve_room_alias = AsyncMock(side_effect=MNotFound("Room not found", 404))
|
||||||
|
|
||||||
|
result = await validate_room_aliases(client, ["room1", "room2"], "test", True, "example.com")
|
||||||
|
assert result == (True, [])
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_validate_room_aliases_conflicts_slug_not_required(self):
|
||||||
"""Test alias validation with conflicts."""
|
"""Test alias validation with conflicts."""
|
||||||
client = Mock()
|
client = Mock()
|
||||||
|
|
||||||
def resolve_side_effect(alias):
|
def resolve_side_effect(alias):
|
||||||
if "room1" in alias:
|
if "room1" in alias:
|
||||||
return {"room_id": "!room1:example.com"} # Exists
|
return {"room_id": "!room1:example.com"} # Exists
|
||||||
else:
|
else:
|
||||||
raise MNotFound() # Doesn't exist
|
raise MNotFound() # Doesn't exist
|
||||||
|
|
||||||
client.resolve_room_alias = AsyncMock(side_effect=resolve_side_effect)
|
client.resolve_room_alias = AsyncMock(side_effect=resolve_side_effect)
|
||||||
|
|
||||||
result = await validate_room_aliases(client, ["room1", "room2"], "test", "example.com")
|
result = await validate_room_aliases(client, ["room1", "room2"], "", False, "example.com")
|
||||||
|
assert result == (False, ["#room1:example.com"])
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_validate_room_aliases_conflicts_slug_required(self):
|
||||||
|
"""Test alias validation with conflicts."""
|
||||||
|
client = Mock()
|
||||||
|
|
||||||
|
def resolve_side_effect(alias):
|
||||||
|
if "room1" in alias:
|
||||||
|
return {"room_id": "!room1:example.com"} # Exists
|
||||||
|
else:
|
||||||
|
raise MNotFound() # Doesn't exist
|
||||||
|
|
||||||
|
client.resolve_room_alias = AsyncMock(side_effect=resolve_side_effect)
|
||||||
|
|
||||||
|
result = await validate_room_aliases(client, ["room1", "room2"], "test", True, "example.com")
|
||||||
assert result == (False, ["#room1-test:example.com"])
|
assert result == (False, ["#room1-test:example.com"])
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
|
|||||||
@@ -0,0 +1,104 @@
|
|||||||
|
"""Tests for notification and greeting template rendering."""
|
||||||
|
|
||||||
|
from unittest.mock import AsyncMock, Mock
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from mautrix.types import EventType, RoomID
|
||||||
|
|
||||||
|
from community.bot import CommunityBot
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def bot():
|
||||||
|
bot = CommunityBot.__new__(CommunityBot)
|
||||||
|
bot.client = Mock()
|
||||||
|
bot.config = {
|
||||||
|
"matrix_to_base_url": "https://matrix.to",
|
||||||
|
"user_pill_prefix": "@",
|
||||||
|
"room_pill_prefix": "#",
|
||||||
|
}
|
||||||
|
return bot
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_get_user_display_name_uses_member_displayname(bot):
|
||||||
|
member_state = Mock()
|
||||||
|
member_state.displayname = "Alice"
|
||||||
|
bot.client.get_state_event = AsyncMock(return_value=member_state)
|
||||||
|
|
||||||
|
result = await bot._get_user_display_name(RoomID("!room:example.org"), "@alice:example.org")
|
||||||
|
|
||||||
|
assert result == "Alice"
|
||||||
|
bot.client.get_state_event.assert_awaited_once_with(
|
||||||
|
RoomID("!room:example.org"),
|
||||||
|
EventType.ROOM_MEMBER,
|
||||||
|
state_key="@alice:example.org",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_get_user_display_name_falls_back_to_localpart(bot):
|
||||||
|
bot.client.get_state_event = AsyncMock(side_effect=Exception("missing"))
|
||||||
|
bot.client.parse_user_id = Mock(return_value=("alice", "example.org"))
|
||||||
|
|
||||||
|
result = await bot._get_user_display_name(RoomID("!room:example.org"), "@alice:example.org")
|
||||||
|
|
||||||
|
assert result == "alice"
|
||||||
|
|
||||||
|
|
||||||
|
def test_render_message_template_supports_user_id_and_user_link(bot):
|
||||||
|
plain, html = bot._render_message_template(
|
||||||
|
"{user} / {user_id} / {user_link}",
|
||||||
|
"@alice:example.org",
|
||||||
|
"Alice",
|
||||||
|
"!room:example.org",
|
||||||
|
"General",
|
||||||
|
)
|
||||||
|
|
||||||
|
assert plain == "@Alice / @alice:example.org / https://matrix.to/#/@alice:example.org"
|
||||||
|
assert "@Alice / @alice:example.org / " in html
|
||||||
|
assert '<a href=' in html
|
||||||
|
assert '>Alice</a>' in html
|
||||||
|
|
||||||
|
|
||||||
|
def test_render_message_template_uses_configurable_user_and_room_pill_prefixes(bot):
|
||||||
|
bot.config["user_pill_prefix"] = ""
|
||||||
|
bot.config["room_pill_prefix"] = ""
|
||||||
|
|
||||||
|
plain, html = bot._render_message_template(
|
||||||
|
"{user} has joined {room}.",
|
||||||
|
"@alice:example.org",
|
||||||
|
"Alice",
|
||||||
|
"!room:example.org",
|
||||||
|
"General",
|
||||||
|
)
|
||||||
|
|
||||||
|
assert plain == "Alice has joined General."
|
||||||
|
assert "<a href='matrix:u/alice:example.org?action=chat'>Alice</a>" in html
|
||||||
|
assert "<a href='matrix:roomid/room:example.org'>General</a>" in html
|
||||||
|
|
||||||
|
|
||||||
|
def test_render_message_template_defaults_to_prefixed_user_and_room_pills(bot):
|
||||||
|
plain, html = bot._render_message_template(
|
||||||
|
"{user} has joined {room}.",
|
||||||
|
"@alice:example.org",
|
||||||
|
"Alice",
|
||||||
|
"!room:example.org",
|
||||||
|
"General",
|
||||||
|
)
|
||||||
|
|
||||||
|
assert plain == "@Alice has joined #General."
|
||||||
|
assert "<a href='matrix:u/alice:example.org?action=chat'>@Alice</a>" in html
|
||||||
|
assert "<a href='matrix:roomid/room:example.org'>#General</a>" in html
|
||||||
|
|
||||||
|
|
||||||
|
def test_matrix_uri_helpers_are_consistent():
|
||||||
|
from community.bot import CommunityBot
|
||||||
|
|
||||||
|
bot = CommunityBot.__new__(CommunityBot)
|
||||||
|
bot.config = {}
|
||||||
|
|
||||||
|
assert bot._matrix_user_uri("@alice:example.org") == "matrix:u/alice:example.org?action=chat"
|
||||||
|
assert bot._matrix_room_uri("!roomid:example.org", "#general:example.org") == "matrix:r/general:example.org"
|
||||||
|
assert bot._matrix_room_uri("!roomid:example.org", None) == "matrix:roomid/roomid:example.org"
|
||||||
|
assert bot._matrix_event_uri("!roomid:example.org", "$eventid") == "matrix:roomid/roomid:example.org/e/eventid"
|
||||||
Reference in New Issue
Block a user