Compare commits

..

7 Commits

Author SHA1 Message Date
shokollm
8db5ba0ba4 Merge fix/issue-52 with conflict resolution 2026-04-04 20:32:06 +07:00
2dd11a8b48 Merge pull request 'feat: remove "by user" from bounty list display' (#76) from fix/issue-55 into main 2026-04-04 12:53:36 +02:00
shokollm
2617d17e28 feat: remove "by user" from bounty list display
Removes created_by_user_id from format_bounty() output.
Fixes #55
2026-04-04 17:52:47 +07:00
b091153f10 Merge pull request 'feat: implement /admin add|remove @username command' (#75) from fix/issue-51-v2 into main 2026-04-04 12:18:31 +02:00
shokollm
ce864d9fdc feat: implement /admin add|remove @username command
- Add cmd_admin handler for /admin add|remove @username
- Add _find_user_id_by_username helper to resolve usernames from bounty creators
- Register admin command handler in bot.py
- Add 'admin' to bot command list
- Addresses issue #51
2026-04-04 08:20:35 +00:00
e805a6428a Merge pull request 'feat: implement /timezone command to get/set room timezone' (#72) from fix/issue-53 into main 2026-04-04 10:15:42 +02:00
shokollm
6da16e752b feat: implement /timezone command to get/set room timezone
Re-implement the timezone command that was reverted.

- Add cmd_timezone function with get/set functionality
- Validate timezone using zoneinfo (IANA format)
- Admin-only permission via service layer
- Update help text and bot command list
- Fix indentation bug in cmd_add (duplicate lines)

Fixes #53
2026-04-04 08:14:58 +00:00
2 changed files with 131 additions and 2 deletions

View File

@@ -16,6 +16,7 @@ from commands import (
cmd_my,
cmd_show,
cmd_start,
cmd_timezone,
cmd_track,
cmd_untrack,
cmd_update,
@@ -44,6 +45,8 @@ def build_app() -> Application:
app.add_handler(CommandHandler("track", cmd_track))
app.add_handler(CommandHandler("untrack", cmd_untrack))
app.add_handler(CommandHandler("show", cmd_show))
app.add_handler(CommandHandler("timezone", cmd_timezone))
app.add_handler(CommandHandler("admin", cmd_admin))
app.add_handler(CommandHandler("admin", cmd_admin))
app.add_handler(MessageHandler(filters.COMMAND, cmd_help))
@@ -61,6 +64,7 @@ async def post_init(app: Application) -> None:
("track", "Track a bounty"),
("untrack", "Stop tracking"),
("show", "Show bounty details"),
("timezone", "Get/set room timezone"),
("admin", "Manage admins"),
("help", "Show help"),
]

View File

@@ -3,6 +3,7 @@
import time
from functools import wraps
from typing import Optional
from zoneinfo import ZoneInfo, ZoneInfoNotFoundError
import dateparser
from telegram import Update
@@ -104,8 +105,6 @@ def format_bounty(b, show_id: bool = True, slice_length: int = 0) -> str:
parts.append(f"⏰ Today (OVERDUE)")
else:
parts.append(f"{due_str} ({days_left}d)")
if b.created_by_user_id:
parts.append(f"by {b.created_by_user_id}")
return " | ".join(parts)
@@ -393,6 +392,101 @@ async def cmd_untrack(update: Update, ctx: ContextTypes.DEFAULT_TYPE) -> None:
await update.message.reply_text("Not tracking bounty #{bounty_id}.")
async def cmd_admin(update: Update, ctx: ContextTypes.DEFAULT_TYPE) -> None:
args = extract_args(update.message.text)
if not args:
await update.message.reply_text(
"Usage: /admin <add|remove|list> [@username]\n"
"/admin list — list admins\n"
"/admin add @username — add admin\n"
"/admin remove @username — remove admin"
)
return
subcommand = args[0].lower()
username = args[1] if len(args) > 1 else None
requesting_user_id = get_user_id(update)
room_id = get_room_id(update)
if subcommand == "list":
admins = BOUNTY_SERVICE.list_admins(room_id)
if not admins:
await update.message.reply_text("No admins in this room.")
return
admin_mentions = " ".join(f"admin_id:{uid}" for uid in admins)
await update.message.reply_text(f"Admins: {admin_mentions}")
return
if subcommand == "remove":
if not username:
await update.message.reply_text("Usage: /admin remove @username")
return
if not username.startswith("@"):
await update.message.reply_text(
f"{username} is not a valid username (must start with @)."
)
return
target_username = username[1:]
try:
chat = await ctx.bot.get_chat(target_username)
target_user_id = chat.id
except Exception:
await update.message.reply_text(
f"⛔ Could not find user @{target_username}."
)
return
try:
BOUNTY_SERVICE.remove_admin(room_id, target_user_id, requesting_user_id)
await update.message.reply_text(
f"✅ @{target_username} is no longer an admin."
)
except PermissionError as e:
await update.message.reply_text(f"{e}")
except ValueError:
await update.message.reply_text(f"⛔ @{target_username} is not an admin.")
return
if subcommand == "add":
if not username:
await update.message.reply_text("Usage: /admin add @username")
return
if not username.startswith("@"):
await update.message.reply_text(
f"{username} is not a valid username (must start with @)."
)
return
target_username = username[1:]
try:
chat = await ctx.bot.get_chat(target_username)
target_user_id = chat.id
except Exception:
await update.message.reply_text(
f"⛔ Could not find user @{target_username}."
)
return
try:
BOUNTY_SERVICE.add_admin(room_id, target_user_id, requesting_user_id)
await update.message.reply_text(f"✅ @{target_username} is now an admin.")
except PermissionError as e:
await update.message.reply_text(f"{e}")
except ValueError:
await update.message.reply_text(
f"⛔ @{target_username} is already an admin."
)
return
await update.message.reply_text(
"Unknown subcommand. Use: /admin <add|remove|list> [@username]"
)
async def cmd_start(update: Update, ctx: ContextTypes.DEFAULT_TYPE) -> None:
if is_group(update):
await update.message.reply_text(
@@ -455,6 +549,35 @@ async def cmd_show(update: Update, ctx: ContextTypes.DEFAULT_TYPE) -> None:
await update.message.reply_text("\n".join(lines), disable_web_page_preview=True)
async def cmd_timezone(update: Update, ctx: ContextTypes.DEFAULT_TYPE) -> None:
args = extract_args(update.message.text)
room_id = get_room_id(update)
user_id = get_user_id(update)
if not args:
current_tz = BOUNTY_SERVICE.get_timezone(room_id)
await update.message.reply_text(f"Current timezone: {current_tz}")
return
timezone_str = args[0]
try:
ZoneInfo(timezone_str)
except (KeyError, ZoneInfoNotFoundError):
await update.message.reply_text(
"⛔ Invalid timezone. Use IANA format (e.g., Asia/Jakarta)"
)
return
try:
BOUNTY_SERVICE.set_timezone(room_id, timezone_str, user_id)
except PermissionError as e:
await update.message.reply_text(f"{e}")
return
await update.message.reply_text(f"✅ Timezone set to {timezone_str}.")
def _find_user_id_by_username(room_id: int, username: str) -> int | None:
"""Find user_id by username from bounty creators in the room."""
bounties = BOUNTY_SERVICE.list_bounties(room_id)
@@ -528,6 +651,8 @@ async def cmd_help(update: Update, ctx: ContextTypes.DEFAULT_TYPE) -> None:
"/track <id> — track a bounty (groups only)\n"
"/untrack <id> — stop tracking (groups only)\n"
"/show <id> — show bounty details\n"
"/timezone — get room timezone\n"
"/timezone <tz> — set room timezone (admin only)\n"
"/start — re-initialize\n"
"/help — this message",
disable_web_page_preview=True,