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
|
algorithm that is perfect is impossible. consider configuring your community such that censorship need only be applied
|
||||||
in a limited subset of rooms.
|
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
|
# installation
|
||||||
|
|
||||||
install this like any other maubot plugin: zip the contents of this repo into a file and upload via the web interface,
|
install this like any other maubot plugin: zip the contents of this repo into a file and upload via the web interface,
|
||||||
|
|||||||
+23
-1
@@ -115,4 +115,26 @@ banlists:
|
|||||||
proactive_banning: true
|
proactive_banning: true
|
||||||
|
|
||||||
# should we redact messages when a user is banned?
|
# should we redact messages when a user is banned?
|
||||||
redact_on_ban: true
|
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.
|
# 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 json
|
||||||
import time
|
import time
|
||||||
import re
|
import re
|
||||||
import fnmatch
|
import fnmatch
|
||||||
import asyncio
|
import asyncio
|
||||||
|
import random
|
||||||
import asyncpg.exceptions
|
import asyncpg.exceptions
|
||||||
|
|
||||||
from mautrix.client import (
|
from mautrix.client import (
|
||||||
@@ -33,6 +34,7 @@ from mautrix.types import (
|
|||||||
SpaceParentStateEventContent,
|
SpaceParentStateEventContent,
|
||||||
JoinRulesStateEventContent,
|
JoinRulesStateEventContent,
|
||||||
JoinRule,
|
JoinRule,
|
||||||
|
RoomCreatePreset,
|
||||||
)
|
)
|
||||||
from mautrix.errors import MNotFound
|
from mautrix.errors import MNotFound
|
||||||
from mautrix.util.config import BaseProxyConfig, ConfigUpdateHelper
|
from mautrix.util.config import BaseProxyConfig, ConfigUpdateHelper
|
||||||
@@ -71,11 +73,15 @@ class Config(BaseProxyConfig):
|
|||||||
helper.copy("banlists")
|
helper.copy("banlists")
|
||||||
helper.copy("proactive_banning")
|
helper.copy("proactive_banning")
|
||||||
helper.copy("redact_on_ban")
|
helper.copy("redact_on_ban")
|
||||||
|
helper.copy("check_if_human")
|
||||||
|
helper.copy("verification_phrases")
|
||||||
|
helper.copy("verification_attempts")
|
||||||
|
|
||||||
|
|
||||||
class CommunityBot(Plugin):
|
class CommunityBot(Plugin):
|
||||||
|
|
||||||
_redaction_tasks: asyncio.Task = None
|
_redaction_tasks: asyncio.Task = None
|
||||||
|
_verification_states: Dict[str, Dict] = {}
|
||||||
|
|
||||||
async def start(self) -> None:
|
async def start(self) -> None:
|
||||||
await super().start()
|
await super().start()
|
||||||
@@ -754,8 +760,6 @@ class CommunityBot(Plugin):
|
|||||||
else:
|
else:
|
||||||
on_banlist = await self.check_if_banned(evt.sender)
|
on_banlist = await self.check_if_banned(evt.sender)
|
||||||
if on_banlist:
|
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)
|
await self.ban_this_user(evt.sender)
|
||||||
return
|
return
|
||||||
# passive sync of tracking db
|
# passive sync of tracking db
|
||||||
@@ -764,7 +768,6 @@ class CommunityBot(Plugin):
|
|||||||
# greeting activities
|
# greeting activities
|
||||||
room_id = str(evt.room_id)
|
room_id = str(evt.room_id)
|
||||||
if room_id in self.config["greeting_rooms"]:
|
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:
|
if on_banlist:
|
||||||
return
|
return
|
||||||
greeting_map = self.config["greetings"]
|
greeting_map = self.config["greetings"]
|
||||||
@@ -791,6 +794,132 @@ class CommunityBot(Plugin):
|
|||||||
self.config["notification_room"], html=notification_message
|
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)
|
@event.on(EventType.ROOM_MESSAGE)
|
||||||
async def update_message_timestamp(self, evt: MessageEvent) -> None:
|
async def update_message_timestamp(self, evt: MessageEvent) -> None:
|
||||||
power_levels = await self.client.get_state_event(
|
power_levels = await self.client.get_state_event(
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
maubot: 0.1.0
|
maubot: 0.1.0
|
||||||
id: org.jobmachine.communitybot
|
id: org.jobmachine.communitybot
|
||||||
version: 0.2.0
|
version: 0.2.1
|
||||||
license: MIT
|
license: MIT
|
||||||
modules:
|
modules:
|
||||||
- community
|
- community
|
||||||
|
|||||||
Reference in New Issue
Block a user