mirror of
https://github.com/williamkray/maubot-kickbot.git
synced 2024-09-28 16:55:36 +00:00
151 lines
6.7 KiB
Python
151 lines
6.7 KiB
Python
# kickbot - a maubot plugin to track user activity and remove inactive users from rooms/spaces.
|
|
|
|
from typing import Awaitable, Type, Optional, Tuple
|
|
import json
|
|
import time
|
|
|
|
from mautrix.client import Client
|
|
from mautrix.types import (Event, StateEvent, EventID, UserID, FileInfo, EventType,
|
|
MediaMessageEventContent, ReactionEvent, RedactionEvent)
|
|
from mautrix.util.config import BaseProxyConfig, ConfigUpdateHelper
|
|
from maubot import Plugin, MessageEvent
|
|
from maubot.handlers import command, event
|
|
|
|
# database table related things
|
|
from .db import upgrade_table
|
|
|
|
|
|
|
|
class Config(BaseProxyConfig):
|
|
def do_update(self, helper: ConfigUpdateHelper) -> None:
|
|
helper.copy("admins")
|
|
helper.copy("master_room")
|
|
helper.copy("track_reactions")
|
|
helper.copy("warn_threshold_days")
|
|
helper.copy("kick_threshold_days")
|
|
|
|
|
|
class KickBot(Plugin):
|
|
|
|
async def start(self) -> None:
|
|
await super().start()
|
|
self.config.load_and_update()
|
|
|
|
@event.on(EventType.ROOM_MESSAGE)
|
|
async def update_message_timestamp(self, evt: MessageEvent) -> None:
|
|
q = """
|
|
REPLACE INTO user_events(mxid, last_message_timestamp)
|
|
VALUES ($1, $2)
|
|
"""
|
|
await self.database.execute(q, evt.sender, evt.timestamp)
|
|
|
|
@event.on(EventType.REACTION)
|
|
async def update_reaction_timestamp(self, evt: MessageEvent) -> None:
|
|
if not self.config["track_reactions"]:
|
|
pass
|
|
else:
|
|
q = """
|
|
REPLACE INTO user_events(mxid, last_message_timestamp)
|
|
VALUES ($1, $2)
|
|
"""
|
|
await self.database.execute(q, evt.sender, evt.timestamp)
|
|
|
|
@command.new("activity", help="track active/inactive status of members of a space")
|
|
async def activity(self) -> None:
|
|
pass
|
|
|
|
@activity.subcommand("sync", help="update the activity tracker with the current space members \
|
|
in case they are missing")
|
|
async def sync_space_members(self, evt: MessageEvent) -> None:
|
|
if evt.sender in self.config["admins"]:
|
|
space_members_obj = await self.client.get_joined_members(self.config["master_room"])
|
|
space_members_list = space_members_obj.keys()
|
|
table_users = await self.database.fetch("SELECT mxid FROM user_events")
|
|
table_user_list = [ row["mxid"] for row in table_users ]
|
|
untracked_users = set(space_members_list) - set(table_user_list)
|
|
non_space_members = set(table_user_list) - set(space_members_list)
|
|
try:
|
|
for user in untracked_users:
|
|
now = int(time.time() * 1000)
|
|
q = """
|
|
INSERT INTO user_events (mxid, last_message_timestamp)
|
|
VALUES ($1, $2)
|
|
"""
|
|
await self.database.execute(q, user, now)
|
|
self.log.info(f"{user} inserted into activity tracking table")
|
|
for user in non_space_members:
|
|
await self.database.execute("DELETE FROM user_events WHERE mxid = $1", user)
|
|
self.log.info(f"{user} is not a space member, dropped from activity tracking table")
|
|
await evt.react("✅")
|
|
except Exception as e:
|
|
self.log.exception(e)
|
|
else:
|
|
await evt.reply("lol you don't have permission to do that")
|
|
|
|
|
|
@activity.subcommand("ignore", help="exclude a specific matrix ID from inactivity tracking until their next \
|
|
trackable event (temporary exemption from inactivity reporting)")
|
|
@command.argument("mxid", "full matrix ID", required=True)
|
|
async def ignore_inactivity(self, evt: MessageEvent, mxid: UserID) -> None:
|
|
if evt.sender in self.config["admins"]:
|
|
try:
|
|
Client.parse_user_id(mxid)
|
|
await self.database.execute("UPDATE user_events SET ignore_inactivity = 1 WHERE \
|
|
mxid = $1", mxid)
|
|
self.log.info(f"{mxid} set to ignore inactivity")
|
|
await evt.react("✅")
|
|
except Exception as e:
|
|
await evt.respond(f"{e}")
|
|
else:
|
|
await evt.reply("lol you don't have permission to set that")
|
|
|
|
@activity.subcommand("unignore", help="re-enable activity tracking for a specific matrix ID")
|
|
@command.argument("mxid", "full matrix ID", required=True)
|
|
async def ignore_inactivity(self, evt: MessageEvent, mxid: UserID) -> None:
|
|
if evt.sender in self.config["admins"]:
|
|
try:
|
|
Client.parse_user_id(mxid)
|
|
await self.database.execute("UPDATE user_events SET ignore_inactivity = 0 WHERE \
|
|
mxid = $1", mxid)
|
|
self.log.info(f"{mxid} set to track inactivity")
|
|
await evt.react("✅")
|
|
except Exception as e:
|
|
await evt.respond(f"{e}")
|
|
else:
|
|
await evt.reply("lol you don't have permission to set that")
|
|
|
|
@activity.subcommand("snitch", help='generate a list of matrix IDs that have been inactive')
|
|
async def generate_report(self, evt: MessageEvent) -> None:
|
|
now = int(time.time() * 1000)
|
|
warn_days_ago = (now - (1000 * 60 * 60 * 24 * self.config["warn_threshold_days"]))
|
|
kick_days_ago = (now - (1000 * 60 * 60 * 24 * self.config["kick_threshold_days"]))
|
|
warn_q = """
|
|
SELECT mxid FROM user_events WHERE last_message_timestamp <= $1 AND
|
|
last_message_timestamp >= $2
|
|
AND ignore_inactivity = 0
|
|
"""
|
|
kick_q = """
|
|
SELECT mxid FROM user_events WHERE last_message_timestamp <= $1
|
|
AND ignore_inactivity = 0
|
|
"""
|
|
warn_inactive_results = await self.database.fetch(warn_q, warn_days_ago, kick_days_ago)
|
|
kick_inactive_results = await self.database.fetch(kick_q, kick_days_ago)
|
|
warn_inactive = [ row["mxid"] for row in warn_inactive_results ] or ["none"]
|
|
kick_inactive = [ row["mxid"] for row in kick_inactive_results ] or ["none"]
|
|
await evt.respond(f"<b>Users inactive for {self.config['warn_threshold_days']} days:</b> \
|
|
{', '.join(warn_inactive)} <br>\
|
|
<b>Users inactive for {self.config['kick_threshold_days']} days:</b> \
|
|
{', '.join(kick_inactive)}", \
|
|
allow_html=True)
|
|
|
|
#need to somehow regularly fetch and update the list of room ids that are associated with a given space
|
|
#to track events within so that we are actually only paying attention to those rooms
|
|
|
|
@classmethod
|
|
def get_db_upgrade_table(cls) -> None:
|
|
return upgrade_table
|
|
|
|
@classmethod
|
|
def get_config_class(cls) -> Type[BaseProxyConfig]:
|
|
return Config
|