Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: warn/mute/unmute command #155

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,9 @@ logs/*.log
# VsCode
.vscode

# JetBrains
.idea

# Dev
test.py

Expand Down
6 changes: 6 additions & 0 deletions src/spotted/config/db/post_db_del.sql
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,14 @@ DROP TABLE IF EXISTS published_post
-----
DROP TABLE IF EXISTS banned_users
-----
DROP TABLE IF EXISTS warned_users
-----
DROP TABLE IF EXISTS muted_users
-----
DROP TABLE IF EXISTS spot_report
-----
DROP TABLE IF EXISTS user_report
-----
DROP TABLE IF EXISTS user_follow
-----
DROP TRIGGER IF EXISTS drop_old_warns ON warned_users
23 changes: 23 additions & 0 deletions src/spotted/config/db/post_db_init.sql
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,22 @@ CREATE TABLE IF NOT EXISTS banned_users
PRIMARY KEY (user_id)
);
-----
CREATE TABLE IF NOT EXISTS warned_users
(
user_id BIGINT NOT NULL,
warn_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
valid_until_date TIMESTAMP NOT NULL,
PRIMARY KEY (user_id, warn_date)
);
-----
CREATE TABLE IF NOT EXISTS muted_users
(
user_id BIGINT NOT NULL,
mute_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
expire_date TIMESTAMP NOT NULL,
LightDestory marked this conversation as resolved.
Show resolved Hide resolved
PRIMARY KEY (user_id)
);
-----
CREATE TABLE IF NOT EXISTS spot_report
(
user_id BIGINT NOT NULL,
Expand Down Expand Up @@ -70,3 +86,10 @@ CREATE TABLE IF NOT EXISTS user_follow
follow_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (user_id, message_id)
);
-----
CREATE TRIGGER IF NOT EXISTS drop_old_warns
BEFORE INSERT ON warned_users
FOR EACH ROW
BEGIN
DELETE FROM warned_users WHERE user_id=NEW.user_id and valid_until_date < DATETIME('now');
END;
3 changes: 3 additions & 0 deletions src/spotted/config/yaml/settings.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -19,5 +19,8 @@ post:
delete_anonymous_comments: true
reject_after_autoreply: true
autoreplies_per_page: 6
max_n_warns: 3
warn_expiration_days: 60
mute_default_duration_days: 7
token: ""
bot_tag: "@bot_tag"
3 changes: 3 additions & 0 deletions src/spotted/config/yaml/settings.yaml.types
Original file line number Diff line number Diff line change
Expand Up @@ -18,5 +18,8 @@ post:
delete_anonymous_comments: bool
reject_after_autoreply: bool
autoreplies_per_page: int
max_n_warns: int
warn_expiration_days: int
mute_default_duration_days: int
token: str
bot_tag: str
3 changes: 3 additions & 0 deletions src/spotted/data/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@
"report_wait_mins",
"replace_anonymous_comments",
"delete_anonymous_comments",
"max_n_warns",
"warn_expiration_days",
"mute_default_duration_days",
]
SettingsKeysType = Literal[SettingsKeys, SettingsPostKeys, SettingsDebugKeys]
AutorepliesKeysType = Literal["autoreplies"]
Expand Down
78 changes: 76 additions & 2 deletions src/spotted/data/user.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
"""Users management"""
from dataclasses import dataclass
from datetime import datetime
from datetime import datetime, timedelta
from random import choice

from telegram import Bot
from telegram import Bot, ChatPermissions

from .config import Config
from .data_reader import read_md
from .db_manager import DbManager
from .pending_post import PendingPost
Expand All @@ -24,13 +25,20 @@ class User:
user_id: int
private_message_id: int | None = None
ban_date: datetime | None = None
mute_date: datetime | None = None
mute_expire_date: datetime | None = None
LightDestory marked this conversation as resolved.
Show resolved Hide resolved
follow_date: datetime | None = None

@property
def is_pending(self) -> bool:
"""If the user has a post already pending or not"""
return bool(PendingPost.from_user(self.user_id))

@property
def is_warn_bannable(self) -> bool:
"""If the user is bannable due to warns"""
return self.get_n_warns() >= Config.post_get("max_n_warns")

@property
def is_banned(self) -> bool:
"""If the user is banned or not"""
Expand All @@ -49,6 +57,14 @@ def banned_users(cls) -> "list[User]":
for row in DbManager.select_from(table_name="banned_users", select="user_id, ban_date")
]

@classmethod
def muted_users(cls) -> "list[User]":
LightDestory marked this conversation as resolved.
Show resolved Hide resolved
"""Returns a list of all the muted users"""
return [
cls(user_id=row["user_id"], mute_date=row["mute_date"], mute_expire_date=row["expire_date"])
for row in DbManager.select_from(table_name="muted_users", select="user_id, mute_date, expire_date")
]

@classmethod
def credited_users(cls) -> "list[User]":
"""Returns a list of all the credited users"""
Expand Down Expand Up @@ -78,6 +94,11 @@ def following_users(cls, message_id: int) -> "list[User]":
)
]

def get_n_warns(self) -> int:
"""Returns the count of consecutive warns of the user"""
count = DbManager.count_from(table_name="warned_users", where="user_id = %s", where_args=(self.user_id,))
return count if count else 0

def ban(self):
"""Adds the user to the banned list"""

Expand All @@ -92,9 +113,62 @@ def sban(self) -> bool:
"""
if self.is_banned:
DbManager.delete_from(table_name="banned_users", where="user_id = %s", where_args=(self.user_id,))
DbManager.delete_from(table_name="warned_users", where="user_id = %s", where_args=(self.user_id,))
return True
return False

async def mute(self, bot: Bot, days: int):
"""Mute a user restricting its actions inside the community group

Args:
bot: the telegram bot
days(optional): The number of days the user should be muted for.
"""
await bot.restrict_chat_member(
chat_id=Config.post_get("channel_id"),
user_id=self.user_id,
permissions=ChatPermissions(
can_send_messages=False,
can_send_other_messages=False,
can_add_web_page_previews=False,
),
)
expiration_date = datetime.now() + timedelta(days=days)
DbManager.insert_into(
table_name="muted_users",
columns=("user_id", "expire_date"),
values=(self.user_id, expiration_date),
)

async def unmute(self, bot: Bot):
"""Unmute a user taking back all restrictions

Args:
bot : the telegram bot
"""
await bot.restrict_chat_member(
chat_id=Config.post_get("channel_id"),
user_id=self.user_id,
permissions=ChatPermissions(
can_send_messages=True,
can_send_other_messages=True,
can_add_web_page_previews=True,
),
)
DbManager.delete_from(table_name="muted_users", where="user_id = %s", where_args=(self.user_id,))

def warn(self):
"""Increase the number of warns of a user
If this is number would reach 3 the user is banned
LightDestory marked this conversation as resolved.
Show resolved Hide resolved

Args:
bot: the telegram bot
"""
valid_until_date = datetime.now() + timedelta(days=Config.post_get("warn_expiration_days"))
DbManager.insert_into(
table_name="warned_users", columns=("user_id", "valid_until_date"), values=(self.user_id, valid_until_date)
)

def become_anonym(self) -> bool:
"""Removes the user from the credited list, if he was present

Expand Down
14 changes: 13 additions & 1 deletion src/spotted/handlers/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@
from .follow_spot import follow_spot_callback
from .forwarded_post import forwarded_post_msg
from .help import help_cmd
from .job_handlers import clean_pending_job, db_backup_job
from .job_handlers import clean_muted_users, clean_pending_job, db_backup_job
from .mute import mute_cmd
from .purge import purge_cmd
from .reload import reload_cmd
from .reply import reply_cmd
Expand All @@ -38,6 +39,8 @@
from .settings import settings_callback, settings_cmd
from .spot import spot_conv_handler
from .start import start_cmd
from .unmute import unmute_cmd
from .warn import warn_cmd


async def add_commands(app: Application):
Expand All @@ -61,6 +64,9 @@ async def add_commands(app: Application):
admin_commands = [
BotCommand("ban", "banna un utente"),
BotCommand("sban", "banna un utente"),
BotCommand("mute", "muta un utente"),
BotCommand("unmute", "smuta un utente"),
BotCommand("warn", "warna un utente"),
BotCommand("reply", "rispondi ad uno spot o un report"),
BotCommand("autoreply", "rispondi ad uno spot o un report con un messaggio automatico"),
BotCommand("reload", "ricarica la configurazione del bot"),
Expand Down Expand Up @@ -105,11 +111,16 @@ def add_handlers(app: Application):

# Command handlers: Admin commands
app.add_handler(CommandHandler("sban", sban_cmd, filters=admin_filter))
app.add_handler(CommandHandler("unmute", unmute_cmd, filters=admin_filter))
app.add_handler(CommandHandler("clean_pending", clean_pending_cmd, filters=admin_filter))
app.add_handler(CommandHandler("db_backup", db_backup_cmd, filters=admin_filter))
app.add_handler(CommandHandler("purge", purge_cmd, filters=admin_filter))
app.add_handler(CommandHandler("reload", reload_cmd, filters=admin_filter))

# Command handlers: Community-based commands
app.add_handler(CommandHandler("warn", warn_cmd, filters=filters.ChatType.PRIVATE | community_filter))
app.add_handler(CommandHandler("mute", mute_cmd, filters=filters.ChatType.PRIVATE | community_filter))

# MessageHandler
app.add_handler(MessageHandler(filters.REPLY & admin_filter & filters.Regex(r"^/ban$"), ban_cmd))
app.add_handler(MessageHandler(filters.REPLY & admin_filter & filters.Regex(r"^/reply"), reply_cmd))
Expand Down Expand Up @@ -144,3 +155,4 @@ def add_jobs(app: Application):
"""
app.job_queue.run_daily(clean_pending_job, time=time(hour=5, tzinfo=utc)) # run each day at 05:00 utc
app.job_queue.run_daily(db_backup_job, time=time(hour=5, tzinfo=utc)) # run each day at 05:00 utc
app.job_queue.run_daily(clean_muted_users, time=time(hour=5, tzinfo=utc)) # run each day at 05:00 utc
17 changes: 14 additions & 3 deletions src/spotted/handlers/ban.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
from telegram import Update
from telegram.ext import CallbackContext

from spotted.data import PendingPost, Report, User
from spotted.data import Config, PendingPost, Report, User
from spotted.utils import EventInfo


Expand All @@ -28,13 +28,24 @@ async def ban_cmd(update: Update, context: CallbackContext):
chat_id=info.chat_id, text="Per bannare qualcuno, rispondi con /ban al suo post o report"
)
return
await execute_ban(user_id, info)


async def execute_ban(user_id: int, info: EventInfo, from_warn: bool = False):
"""Execute the ban of a user by his user_id

Args:
user_id: The user_id of the user to ban
info: The EventInfo object
from_warn: A boolean indicating if the ban is executed from a warn
"""
user = User(user_id)
receipt_chat_id = info.chat_id if not from_warn else Config.post_get("admin_group_id")
if user.is_banned:
await info.bot.send_message(chat_id=info.chat_id, text="L'utente è già bannato")
await info.bot.send_message(chat_id=receipt_chat_id, text=f"L'utente {user_id} è già bannato")
return
user.ban()
await info.bot.send_message(chat_id=info.chat_id, text="L'utente è stato bannato")
await info.bot.send_message(chat_id=receipt_chat_id, text=f"L'utente {user_id} è stato bannato")
await info.bot.send_message(
chat_id=user.user_id,
text="Grazie per il tuo contributo alla community, a causa "
Expand Down
19 changes: 18 additions & 1 deletion src/spotted/handlers/job_handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from telegram.error import BadRequest, Forbidden
from telegram.ext import CallbackContext

from spotted.data import Config, PendingPost
from spotted.data import Config, DbManager, PendingPost, User
from spotted.debug import logger
from spotted.utils import EventInfo

Expand Down Expand Up @@ -75,3 +75,20 @@ async def db_backup_job(context: CallbackContext):
)
except BinasciiError as ex:
await context.bot.send_message(chat_id=admin_group_id, text=f"✖️ Impossibile effettuare il backup\n\n{ex}")


async def clean_muted_users(context: CallbackContext):
"""Job called each day at 05:00 utc.
Removed expired users mute records from the database

Args:
context: context passed by the jobqueue
"""
expired_muted = DbManager.select_from(
table_name="muted_users", select="user_id", where="expire_date < DATETIME('now')"
)
if len(expired_muted) == 0:
return
for user in expired_muted:
LightDestory marked this conversation as resolved.
Show resolved Hide resolved
DbManager.delete_from(table_name="muted_users", where="user_id = %s", where_args=(user,))
User(user).unmute(context.bot)
41 changes: 41 additions & 0 deletions src/spotted/handlers/mute.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
"""/mute command"""
from telegram import Update
from telegram.ext import CallbackContext

from spotted.data import Config, User
from spotted.utils import EventInfo


async def mute_cmd(update: Update, context: CallbackContext):
"""Handles the /mute command.
Mute a user by replying to one of his message in the comment group with /mute <n_days>
Args:
update: update event
context: context passed by the handler
"""
info = EventInfo.from_message(update, context)
admins = [admin.user.id for admin in await info.bot.get_chat_administrators(Config.post_get("community_group_id"))]
g_message = update.message.reply_to_message
if (info.user_id not in admins) or (g_message is None):
text = "Per mutare rispondi ad un commento con /mute <days>\nIl numero di giorni è opzionale, di default è 7"
if info.user_id not in admins:
text = "Non sei un admin"
await info.bot.send_message(chat_id=info.user_id, text=text)
await info.message.delete()
return
days = Config.post_get("mute_default_duration_days")
LightDestory marked this conversation as resolved.
Show resolved Hide resolved
if len(context.args) > 0:
try:
days = int(context.args[0])
except ValueError:
pass
user = User(g_message.from_user.id)
await user.mute(info.bot, days)
await info.bot.send_message(
chat_id=Config.post_get("admin_group_id"),
text=f"L'utente è stato mutato per {days} giorn{'o' if days == 1 else 'i'}.",
)
await info.bot.send_message(
chat_id=user.user_id, text=f"Sei stato mutato da Spotted DMI" f"per {days} giorn{'o' if days == 1 else 'i'}"
LightDestory marked this conversation as resolved.
Show resolved Hide resolved
)
await info.message.delete()
Loading
Loading