initial verification check dm logic
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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
@@ -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
@@ -1,6 +1,6 @@
|
||||
maubot: 0.1.0
|
||||
id: org.jobmachine.communitybot
|
||||
version: 0.2.0
|
||||
version: 0.2.1
|
||||
license: MIT
|
||||
modules:
|
||||
- community
|
||||
|
||||
Reference in New Issue
Block a user