initial verification check dm logic

This commit is contained in:
William Kray
2025-04-06 21:07:01 -07:00
parent 49ae7bd66d
commit 5b973920ec
4 changed files with 177 additions and 6 deletions
+20
View File
@@ -148,6 +148,26 @@ please keep in mind that wordlist-based censorship is problematic and may redact
algorithm that is perfect is impossible. consider configuring your community such that censorship need only be applied
in a limited subset of rooms.
# user verification
configure your rooms (all, or a list of room-ids) to use the `check_if_human` setting. use this in conjunction with a room power-level configuration that
requires elevated permission to send messages. for example, a "waiting-room"
with a default power level of -1 for new users, while the power-level required
to send messages in that room remains 0.
enabling this and associated configuration will perform the following
validation:
1. when a user joins one of these rooms, the bot will check to see if they have
permission to send messages.
2. if not, the bot will start a DM with that user and ask them to repeat a phrase,
randomly chosen from your list of verification phrases. they have three tries.
3. when they send the matching verification phrase, the bot will bump their power
level up to that required to send messages in your room, and leave the DM.
not the most user-friendly experience, but may help cut down if you are experiencing
significant spam in your rooms.
# installation
install this like any other maubot plugin: zip the contents of this repo into a file and upload via the web interface,
+22
View File
@@ -116,3 +116,25 @@ proactive_banning: true
# should we redact messages when a user is banned?
redact_on_ban: true
# should we verify that users are human before allowing them to send messages?
# can be boolean (true/false) for all-or-nothing behavior,
# or pass a list of room IDs to only verify users in certain rooms
# use this in conjunction with room power-levels that require elevated permission
# to send messages in a room.
check_if_human: false
# list of phrases that users must type to verify they are human
# if check_if_human is true but this list is empty, verification will be skipped
# make these your favorite movie quotes, core values of your community, or
# whatever you want. the more unique and obscure, the better.
verification_phrases:
- Yes, I am a human!
- I am a robot, but I'm nice.
- My name is Inigo Montoya.
- The wet bird flies at night.
- Be excellent to each other.
- Party on, dudes.
# number of attempts a user has to enter the correct verification phrase
verification_attempts: 3
+133 -4
View File
@@ -1,11 +1,12 @@
# kickbot - a maubot plugin to track user activity and remove inactive users from rooms/spaces.
from typing import Awaitable, Type, Optional, Tuple
from typing import Awaitable, Type, Optional, Tuple, Dict
import json
import time
import re
import fnmatch
import asyncio
import random
import asyncpg.exceptions
from mautrix.client import (
@@ -33,6 +34,7 @@ from mautrix.types import (
SpaceParentStateEventContent,
JoinRulesStateEventContent,
JoinRule,
RoomCreatePreset,
)
from mautrix.errors import MNotFound
from mautrix.util.config import BaseProxyConfig, ConfigUpdateHelper
@@ -71,11 +73,15 @@ class Config(BaseProxyConfig):
helper.copy("banlists")
helper.copy("proactive_banning")
helper.copy("redact_on_ban")
helper.copy("check_if_human")
helper.copy("verification_phrases")
helper.copy("verification_attempts")
class CommunityBot(Plugin):
_redaction_tasks: asyncio.Task = None
_verification_states: Dict[str, Dict] = {}
async def start(self) -> None:
await super().start()
@@ -754,8 +760,6 @@ class CommunityBot(Plugin):
else:
on_banlist = await self.check_if_banned(evt.sender)
if on_banlist:
# self.log.debug(f"DEBUG user is on banlist!")
# ban this account in managed rooms, don't bother with anything else
await self.ban_this_user(evt.sender)
return
# passive sync of tracking db
@@ -764,7 +768,6 @@ class CommunityBot(Plugin):
# greeting activities
room_id = str(evt.room_id)
if room_id in self.config["greeting_rooms"]:
# just in case we got here even if the person is on the banlists
if on_banlist:
return
greeting_map = self.config["greetings"]
@@ -791,6 +794,132 @@ class CommunityBot(Plugin):
self.config["notification_room"], html=notification_message
)
# Human verification logic
if self.config["check_if_human"] and self.config["verification_phrases"]:
try:
# Check if verification is enabled for this room
verification_enabled = False
if isinstance(self.config["check_if_human"], bool):
verification_enabled = self.config["check_if_human"]
elif isinstance(self.config["check_if_human"], list):
verification_enabled = evt.room_id in self.config["check_if_human"]
if not verification_enabled:
return
# Get room name for greeting
roomname = "this room"
try:
roomnamestate = await self.client.get_state_event(evt.room_id, "m.room.name")
roomname = roomnamestate["name"]
except:
pass
# Check if user already has sufficient power level
try:
power_levels = await self.client.get_state_event(
evt.room_id, EventType.ROOM_POWER_LEVELS
)
user_level = power_levels.get_user_level(evt.sender)
events_default = power_levels.events_default
events = power_levels.events
# Get the required power level for sending messages
required_level = events.get(str(EventType.ROOM_MESSAGE), events_default)
# If user already has sufficient power level, skip verification
if user_level >= required_level:
self.log.debug(f"User {evt.sender} already has sufficient power level ({user_level} >= {required_level})")
return
except Exception as e:
self.log.error(f"Failed to check user power level: {e}")
return
# Create DM room with name
dm_room = await self.client.create_room(
preset=RoomCreatePreset.PRIVATE,
invitees=[evt.sender],
is_direct=True,
initial_state=[
{
"type": str(EventType.ROOM_NAME),
"content": {"name": f"{roomname} join verification check"}
}
]
)
# Select random verification phrase
verification_phrase = random.choice(self.config["verification_phrases"])
# Store verification state
self._verification_states[dm_room] = {
"user": evt.sender,
"target_room": evt.room_id,
"phrase": verification_phrase,
"attempts": self.config["verification_attempts"],
"required_level": required_level
}
# Send greeting
greeting = f"""Thank you for joining {roomname}. 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 phrase: "{verification_phrase}" """
await self.client.send_notice(dm_room, greeting)
except Exception as e:
self.log.error(f"Failed to start verification process: {e}")
@event.on(EventType.ROOM_MESSAGE)
async def handle_verification(self, evt: MessageEvent) -> None:
# Ignore messages from the bot itself
if evt.sender == self.client.mxid:
return
if evt.room_id not in self._verification_states:
return
state = self._verification_states[evt.room_id]
user_phrase = evt.content.body.strip().lower()
expected_phrase = state["phrase"].lower()
# Remove punctuation and compare
user_phrase = re.sub(r'[^\w\s]', '', user_phrase)
expected_phrase = re.sub(r'[^\w\s]', '', expected_phrase)
if user_phrase == expected_phrase:
try:
# Update power levels in target room
power_levels = await self.client.get_state_event(
state["target_room"], EventType.ROOM_POWER_LEVELS
)
power_levels.users[state["user"]] = state["required_level"]
await self.client.send_state_event(
state["target_room"], EventType.ROOM_POWER_LEVELS, power_levels
)
await self.client.send_notice(evt.room_id, "Success! My work here is done. You can leave this room now.")
except Exception as e:
await self.client.send_notice(
evt.room_id,
f"Something went wrong: {str(e)}. Please report this to the room moderators."
)
finally:
await self.client.leave_room(evt.room_id)
del self._verification_states[evt.room_id]
else:
state["attempts"] -= 1
if state["attempts"] <= 0:
await self.client.send_notice(
evt.room_id,
"You have run out of attempts. Please contact a room moderator for assistance."
)
await self.client.leave_room(evt.room_id)
del self._verification_states[evt.room_id]
else:
await self.client.send_notice(
evt.room_id,
f"Phrase does not match, you have {state['attempts']} tries remaining."
)
@event.on(EventType.ROOM_MESSAGE)
async def update_message_timestamp(self, evt: MessageEvent) -> None:
power_levels = await self.client.get_state_event(
+1 -1
View File
@@ -1,6 +1,6 @@
maubot: 0.1.0
id: org.jobmachine.communitybot
version: 0.2.0
version: 0.2.1
license: MIT
modules:
- community