Add join confirm feature

This commit is contained in:
夜坂雅 2023-06-13 11:13:30 +08:00
parent b6c7ae2c5a
commit 44ee4f4372
7 changed files with 122 additions and 13 deletions

View File

@ -5,23 +5,30 @@ from nio import (
AsyncClient,
MatrixRoom,
PowerLevels,
RoomGetEventError,
RoomGetStateEventError,
RoomMemberEvent,
RoomMessageText,
RoomPutStateError,
UnknownEvent,
)
from nyx_bot.bot_commands import Command
from nyx_bot.chat_functions import send_text_to_room
from nyx_bot.config import Config
from nyx_bot.message_responses import Message
from nyx_bot.storage import MatrixMessage, MembershipUpdates
from nyx_bot.utils import (
get_bot_event_type,
get_replaces,
get_reply_to,
hash_user_id,
is_bot_event,
make_datetime,
should_enable_join_confirm,
should_record_message_content,
strip_beginning_quote,
user_name,
)
logger = logging.getLogger(__name__)
@ -124,12 +131,64 @@ class Callbacks:
reacted_to = relation_dict.get("event_id")
if reacted_to and relation_dict.get("rel_type") == "m.annotation":
await self._reaction(room, event, reacted_to)
return
logger.debug(
f"Got unknown event with type to {event.type} from {event.sender} in {room.room_id}."
)
async def _reaction(
self, room: MatrixRoom, event: UnknownEvent, reacted_to_id: str
) -> None:
"""A reaction was sent to one of our messages. Let's send a reply acknowledging it.
Args:
room: The room the reaction was sent in.
event: The reaction event.
reacted_to_id: The event ID that the reaction points to.
"""
logger.debug(f"Got reaction to {room.room_id} from {event.sender}.")
# Get the original event that was reacted to
event_response = await self.client.room_get_event(room.room_id, reacted_to_id)
if isinstance(event_response, RoomGetEventError):
logger.warning(
"Error getting event that was reacted to (%s)", reacted_to_id
)
return
reacted_to_event = event_response.event
if (
is_bot_event(reacted_to_event)
and get_bot_event_type(reacted_to_event) == "join_confirm"
):
content = reacted_to_event.source.get("content")
state_key = content.get("io.github.shadowrz.nyx_bot", {}).get("state_key")
required_reaction = hash_user_id(state_key)
reaction_content = (
event.source.get("content", {}).get("m.relates_to", {}).get("key")
)
if reaction_content == required_reaction:
state_resp = await self.client.room_get_state_event(
room.room_id, "m.room.power_levels"
)
if isinstance(state_resp, RoomGetStateEventError):
logger.debug(
f"Failed to get power level data in room {room.display_name} ({room.room_id}). Stop processing."
)
return
content = state_resp.content
events = content.get("events")
users = content.get("users")
del users[state_key]
await self.client.room_put_state(
room.room_id, "m.room.power_levels", {"events": events, "users": users}
)
async def membership(self, room: MatrixRoom, event: RoomMemberEvent) -> None:
timestamp = make_datetime(event.server_timestamp)
MembershipUpdates.update_membership(room, event, timestamp)
@ -138,6 +197,8 @@ class Callbacks:
"invite",
"leave",
):
if not should_enable_join_confirm(self.room_features, room.room_id):
return
content = event.content or {}
name = content.get("displayname")
logger.debug(
@ -152,10 +213,30 @@ class Callbacks:
)
return
content = state_resp.content
powers = PowerLevels(
events=content.get("events"), users=content.get("users")
)
events = content.get("events")
events["m.reaction"] = -1
users = content.get("users")
powers = PowerLevels(events=events, users=users)
if not powers.can_user_send_state(self.client.user, "m.room.power_levels"):
logger.debug(
f"Bot is unable to update power levels in {room.display_name} ({room.room_id}). Stop processing."
)
return
users[event.state_key] = -1
put_state_resp = await self.client.room_put_state(
room.room_id, "m.room.power_levels", {"events": events, "users": users}
)
if isinstance(put_state_resp, RoomPutStateError):
logger.warn(
f"Failed to reconfigure power level: {put_state_resp.message}"
)
return
await send_text_to_room(
self.client,
room.room_id,
f"新加群的用户 {user_name(room, event.state_key)} ({event.state_key}) 请用 Reaction {hash_user_id(event.state_key)} 回复本条消息",
notice=True,
markdown_convert=False,
literal_text=True,
extended_data={"type": "join_confirm", "state_key": event.state_key},
)

View File

@ -5,7 +5,7 @@ import re
import time
from html import escape
from io import BytesIO
from typing import Optional, Union
from typing import Any, Dict, Optional, Union
import magic
from markdown import markdown
@ -51,6 +51,7 @@ async def send_text_to_room(
reply_to_event_id: Optional[str] = None,
literal_text: Optional[bool] = False,
literal_text_substitute: Optional[str] = None,
extended_data: Optional[Dict[Any, Any]] = None,
) -> Union[RoomSendResponse, ErrorResponse]:
"""Send text to a matrix room.
@ -160,10 +161,10 @@ async def send_text_to_room(
content["m.relates_to"] = {"m.in_reply_to": {"event_id": reply_to_event_id}}
# Add custom data for tracking bot message.
content["io.github.shadowrz.nyx_bot"] = {
"in_reply_to": reply_to_event_id,
"type": "text",
}
content["io.github.shadowrz.nyx_bot"] = extended_data or {}
content["io.github.shadowrz.nyx_bot"]["in_reply_to"] = reply_to_event_id
if not content["io.github.shadowrz.nyx_bot"].get("type"):
content["io.github.shadowrz.nyx_bot"]["type"] = "text"
try:
return await client.room_send(

View File

@ -123,6 +123,7 @@ class Config:
"jerryxiao": False,
"randomdraw": False,
"record_messages": False,
"join_confirm": False,
}
if "jerryxiao" in room_features_dict:
room_features_default["jerryxiao"] = room_features_dict["jerryxiao"]
@ -135,6 +136,9 @@ class Config:
"record_messages"
]
del room_features_dict["record_messages"]
if "join_confirm" in room_features_dict:
room_features_default["join_confirm"] = room_features_dict["join_confirm"]
del room_features_dict["join_confirm"]
self.room_features = defaultdict(lambda: room_features_default)
for k, v in room_features_dict.items():
if isinstance(v, dict):

View File

@ -7,6 +7,7 @@ from random import Random
from typing import Dict, Optional, Tuple
from urllib.parse import unquote, urlparse
import xxhash
from nio import AsyncClient, Event, MatrixRoom, RoomGetEventError, RoomMessageText
from nyx_bot.errors import NyxBotRuntimeError
@ -82,12 +83,19 @@ def strip_beginning_quote(original: str) -> str:
def get_reply_to(event: Event) -> Optional[str]:
content = event.source.get("content")
reply_to = ((content.get("m.relates_to") or {}).get("m.in_reply_to") or {}).get(
"event_id"
)
reply_to = content.get("m.relates_to", {}).get("m.in_reply_to", {}).get("event_id")
return reply_to
def get_bot_event_type(event: Event) -> Optional[str]:
if is_bot_event(event):
content = event.source.get("content")
type = content.get("io.github.shadowrz.nyx_bot", {}).get("type")
return type
else:
return None
def is_bot_event(event: Event) -> bool:
content = event.source.get("content")
return "io.github.shadowrz.nyx_bot" in content
@ -95,7 +103,7 @@ def is_bot_event(event: Event) -> bool:
def get_replaces(event: Event) -> Optional[str]:
content = event.source.get("content")
relates_to = content.get("m.relates_to") or {}
relates_to = content.get("m.relates_to", {})
rel_type = relates_to.get("rel_type")
if rel_type == "m.replace":
event_id = relates_to.get("event_id")
@ -237,6 +245,10 @@ def should_enable_randomdraw(room_features, room_id: str) -> bool:
return room_features[room_id]["randomdraw"]
def should_enable_join_confirm(room_features, room_id: str) -> bool:
return room_features[room_id]["join_confirm"]
# A structure for a Matrix UID. It also supports legacy UID formats.
# First part: [\!-9\;-\~]+
# Matches legacy UIDs too.
@ -252,3 +264,11 @@ MATRIX_UID_RE = r"@([\!-9\;-\~]+):([0-9]{1,3}.[0-9]{1,3}.[0-9]{1,3}.[0-9]{1,3}|\
def get_user_id_parts(user_id: str) -> Tuple[str, str]:
uid, domain = re.match(MATRIX_UID_RE, user_id).groups()
return (uid, domain)
REACTIONS = ["🎉", "🤣", "😃", "😋", "🥳", "🤔", "😅"]
def hash_user_id(user_id: str):
hash = xxhash.xxh64_intdigest(user_id)
return REACTIONS[hash % len(REACTIONS)]

View File

@ -19,6 +19,7 @@ dependencies = [
"python-dateutil",
"wordcloud",
"aiohttp",
"xxhash",
]
classifiers=[
"License :: OSI Approved :: Apache Software License",

View File

@ -7,4 +7,5 @@ python-magic
peewee
python-dateutil
wordcloud
aiohttp
aiohttp
xxhash

View File

@ -60,6 +60,7 @@ encryption = false
jerryxiao = false # Controls Jerry Xiao like feature.
randomdraw = false
record_messages = false # Controls recording message content.
join_confirm = false # Enable join confirming.
# # Toogle this room's room features.
# # You don't need to specify all of them.