Guest User

Untitled

a guest
Jun 24th, 2026
22
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Python 30.05 KB | None | 0 0
  1. usr/bin/env python3
  2. import datetime
  3. import os
  4. import re
  5. import pathlib
  6. import logging
  7.  
  8. import arrow
  9. # import asyncio  # used by telegram
  10. import httpx  # used by telegram
  11. import ics
  12. import telegram.constants
  13. from ptbcontrib.roles import setup_roles, RolesHandler
  14. from telegram import Update, Bot, InlineKeyboardButton, InlineKeyboardMarkup
  15. # check out the telegram docs:
  16. # https://docs.python-telegram-bot.org/en/stable/telegram.bot.html
  17. from telegram.constants import (
  18.     ChatMemberStatus,
  19.     ParseMode,
  20.     ChatType,
  21.     MessageEntityType
  22. )
  23. from telegram.ext import (
  24.     Application,
  25.     ApplicationBuilder,
  26.     CallbackQueryHandler,
  27.     ChatJoinRequestHandler,
  28.     ChatMemberHandler,
  29.     CommandHandler,
  30.     ContextTypes,
  31.     ConversationHandler,
  32.     Defaults,
  33.     filters,
  34.     MessageHandler,
  35.     PersistenceInput,
  36.     PicklePersistence
  37. )
  38. from telegram.ext.filters import UpdateFilter, MessageFilter
  39.  
  40. # Setup and check environment variables #########################################
  41. _configVariables = dict([
  42.     ("BOT_TOKEN", os.getenv("BOT_TOKEN")),
  43.     ("ADMIN_GROUP_ID", os.getenv("ADMIN_GROUP_ID")),
  44.     ("MAIN_GROUP_ID", os.getenv("MAIN_GROUP_ID")),
  45.     ("WAITING_ROOM_GROUP_ID", os.getenv("WAITING_ROOM_GROUP_ID")),
  46.     ("WAITING_ROOM_TOPICS_ID", os.getenv("WAITING_ROOM_TOPICS_ID")),
  47.     ("NOTIFICATIONS_CHANNEL_ID", os.getenv("NOTIFICATION_CHANNEL_ID")),
  48.     ("ICAL_URL", os.getenv("ICAL_URL")),
  49. ])
  50.  
  51. logging.basicConfig(
  52.     format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
  53.     level=logging.INFO
  54. )
  55.  
  56. def check_config():
  57.     for index, value in _configVariables.items():
  58.         if value is not None and len(value) > 0:
  59.             print(f"{index} holds a value")
  60.             pass
  61.         else:
  62.             raise ValueError(index, value)
  63.     print("All config variables are not None or empty")
  64.  
  65.  
  66. LOCAL_TZ = 'Europe/London'
  67. locales = arrow.locales
  68. defaults = Defaults(
  69.     tzinfo=arrow.get(tzinfo="Europe/London").tzinfo,
  70. )
  71.  
  72. p = pathlib.Path.cwd() / "data" / "ottbot"
  73. ottbot_persistence_path = p.resolve()
  74. # Setup for persistence
  75. ottbot_persistence = PicklePersistence(
  76.     filepath="ottbot",
  77.     update_interval=60
  78. )
  79.  
  80. ban_list = set()
  81.  
  82. # Setup ########################################################################
  83.  
  84.  
  85. def main() -> None:
  86.     check_config()
  87.     app = ApplicationBuilder() \
  88.         .token(_configVariables.get("BOT_TOKEN")) \
  89.         .defaults(defaults) \
  90.         .persistence(ottbot_persistence) \
  91.         .post_init(initialize) \
  92.         .post_stop(finalize) \
  93.         .build()
  94.  
  95.     job_queue = app.job_queue
  96.     app.run_polling(allowed_updates=Update.ALL_TYPES)  # Update.ALL_TYPES required to get ChatMemberHandler events
  97.  
  98.  
  99. # Utilities ####################################################################
  100.  
  101.  
  102. class ForwardOrigin:
  103.     def __init__(self, user: telegram.User):
  104.         self.first_name = user.first_name
  105.         self.last_name = user.last_name
  106.         self.id = user.id
  107.         self.username = user.username
  108.  
  109.     @property
  110.     def get_id(self):
  111.         return self.id
  112.  
  113.  
  114. class FilterWaitingRoom(UpdateFilter):
  115.     def filter(self, update):
  116.         output = update.effective_chat.id == int(_configVariables.get("WAITING_ROOM_TOPICS_ID"))
  117.         return output
  118.  
  119.  
  120. filter_waiting_room = FilterWaitingRoom()
  121.  
  122.  
  123. class FilterBanList(UpdateFilter):
  124.     def filter(self, update: Update):
  125.         return update.effective_user.id in ban_list
  126.  
  127.  
  128. filter_ban_list = FilterBanList()
  129.  
  130.  
  131. class FilterDirect(UpdateFilter):
  132.     def filter(self, update):
  133.         return update.effective_chat.type == ChatType.PRIVATE
  134.  
  135.  
  136. filter_direct_message = FilterDirect()
  137.  
  138.  
  139. class FilterBanned(UpdateFilter):
  140.     def filter(self, update):
  141.         if update.effective_user.id in ban_list:
  142.             return True
  143.  
  144.  
  145. filter_banned = FilterBanned()
  146.  
  147.  
  148. async def add_to_sfw_member(update: Update, context: ContextTypes.DEFAULT_TYPE):
  149.     user_id = int(update.effective_user.id)
  150.  
  151.     result = await context.bot.get_chat_member(chat_id=_configVariables.get("MAIN_GROUP_ID"), user_id=user_id)
  152.     if result == ChatMemberStatus.MEMBER:
  153.         context.roles["sfw_member"].add_member(user_id)
  154.  
  155.  
  156. def set_topic_from_user(user) -> str:
  157.     name = f"{user.username} ({user.id})"
  158.     print(name)
  159.     return name
  160.  
  161.  
  162. async def respond(update: Update, context: ContextTypes.DEFAULT_TYPE, message: str, **kwargs) -> None:
  163.     await context.bot.send_message(chat_id=update.effective_chat.id, text=message, **kwargs)
  164.  
  165.  
  166. async def respond_success(update: Update, context: ContextTypes.DEFAULT_TYPE, message: str) -> None:
  167.     await respond(update, context, f"✅ {message}")
  168.  
  169.  
  170. async def respond_error(update: Update, context: ContextTypes.DEFAULT_TYPE, message: str) -> None:
  171.     await respond(update, context, f"❌ {message}")
  172.  
  173.  
  174. async def alert(bot: Bot, text: str) -> None:
  175.     await bot.send_message(chat_id=_configVariables.get("ADMIN_GROUP_ID"), text=text)
  176.  
  177.  
  178. async def announce(bot: Bot, lines: list[str], chat_id=_configVariables.get("MAIN_GROUP_ID"), **kwargs) -> None:
  179.     await bot.send_message(chat_id=chat_id,
  180.                            parse_mode=ParseMode.HTML,
  181.                            text="\n".join(lines),
  182.                            **kwargs)
  183.  
  184.  
  185. def sanitize(text: str | None) -> str:
  186.     # See: https://core.telegram.org/bots/api#html-style
  187.     if text is None:
  188.         return ""
  189.     else:
  190.         # NOTE: Order matters! "&" must be escaped first!
  191.         return text.replace("&", "&") \
  192.             .replace("<", "&lt;") \
  193.             .replace(">", "&gt;")
  194.  
  195.  
  196. def ordinal(n: int) -> str:
  197.     return f"{n}th" if n // 10 == 1 else \
  198.         f"{n}st" if n % 10 == 1 else \
  199.             f"{n}nd" if n % 10 == 2 else \
  200.                 f"{n}rd" if n % 10 == 3 else \
  201.                     f"{n}th"
  202.  
  203.  
  204. async def get_upcoming_meet_events(
  205.         ical_url: str = _configVariables.get("ICAL_URL"),
  206.         local_tz: str = LOCAL_TZ,
  207.         now=arrow.utcnow()) -> list[ics.Event]:
  208.     """returns sorted list of events that have not yet ended"""
  209.     async with httpx.AsyncClient() as client:
  210.         response = await client.get(ical_url)
  211.         text = response.raise_for_status().text
  212.     events = list(ics.Calendar(text).events)
  213.     events.sort(key=lambda e: e.begin)
  214.     ret = []
  215.     for event in filter(lambda e: now < e.end, events):
  216.         event.begin = event.begin.to(local_tz)
  217.         event.end = event.end.to(local_tz)
  218.         ret.append(event)
  219.     return ret
  220.  
  221.  
  222. # Events #######################################################################
  223.  
  224.  
  225. async def waiting_room_welcome(bot, user) -> None:
  226.     await alert(bot, f"🆕 {user.first_name} {user.last_name} (@{user.username} id:{user.id})")
  227.     await announce(bot, [
  228.         f"Hi {sanitize(user.first_name)}! An admin will be with you shortly to get you in the main chat.",
  229.         "",
  230.         "In the mean time, please let us know a bit about yourself, read <a href='https://rules.cambfurs.co.uk'> \
  231.        the rules</a> and let us know whether you agree."
  232.     ], chat_id=_configVariables.get("WAITING_ROOM_GROUP_ID"))
  233.  
  234.  
  235. async def main_group_welcome(bot, user) -> None:
  236.     await announce(bot, [
  237.         f"Everyone welcome <a href='tg://user?id={user.id}'>{sanitize(user.first_name)}</a> to the chat!",
  238.     ])
  239.  
  240.  
  241. async def meet_started(bot, event) -> None:
  242.     month_name = locales.EnglishLocale.month_names[event.begin.month]
  243.     await announce(bot, [f"The {month_name} meet has started!"])
  244.  
  245.  
  246. async def meet_tomorrow(bot, event) -> None:
  247.     month_name = locales.EnglishLocale.month_names[event.begin.month]
  248.     await announce(bot, [f"Reminder! The {month_name} meet is tomorrow!"])
  249.  
  250.  
  251. async def meet_next_week(bot, event) -> None:
  252.     month_name = locales.EnglishLocale.month_names[event.begin.month]
  253.     await announce(bot, [f"Reminder! the {month_name} meet is next week!"])
  254.  
  255.  
  256. # This callback gets called every hour at the top of the hour
  257. async def hourly_callback(bot):
  258.     now = arrow.utcnow()
  259.     next_events = await get_upcoming_meet_events(now=now)
  260.     for event in next_events:
  261.         if now.floor('hour') == event.begin.floor('hour'):
  262.             await meet_started(bot, event)
  263.         elif now.hour == 10 and now.shift(days=1).date() == event.begin.date():
  264.             await meet_tomorrow(bot, event)
  265.         elif now.hour == 10 and now.shift(days=7).date() == event.begin.date():
  266.             await meet_next_week(bot, event)
  267.  
  268.  
  269. # Telegram is a bit silly: we can't query the chat to see who is in it.
  270. # The solution is to listen for chat join (and leave) requests, and keep track
  271. # of this ourselves. This misses anyone that was in the waiting room before
  272. # catbot was live.
  273. SEEN_MEMBERS_IN_WAITING_ROOM = set()
  274.  
  275.  
  276. async def chat_member_updated(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
  277.     if update.chat_member.chat.id != _configVariables.get("WAITING_ROOM_GROUP_ID"):
  278.         return
  279.     old = update.chat_member.old_chat_member
  280.     new = update.chat_member.new_chat_member
  281.     if old.status == ChatMemberStatus.LEFT and new.status == ChatMemberStatus.MEMBER:
  282.         SEEN_MEMBERS_IN_WAITING_ROOM.add(new.user.id)
  283.         await waiting_room_welcome(context.bot, new.user)
  284.     elif old.status == ChatMemberStatus.MEMBER and new.status == ChatMemberStatus.LEFT:
  285.         SEEN_MEMBERS_IN_WAITING_ROOM.discard(old.user.id)
  286.     print(SEEN_MEMBERS_IN_WAITING_ROOM)
  287.  
  288.  
  289. async def schedule(update: Update, context: ContextTypes.DEFAULT_TYPE, date: datetime, message_id: int,
  290.                    channel_id: str) -> str:
  291.     print(f"Received schedule request for deletion of {message_id} in {channel_id} at {date}")
  292.     tz = arrow.get(tzinfo="Europe/London").tzinfo
  293.     if arrow.utcnow().astimezone(tz) < date:
  294.         try:
  295.             context.job_queue.run_once(callback=delete, when=date.datetime, chat_id=channel_id, name=f"{message_id}",
  296.                                        data=message_id)
  297.             print("job scheduled")
  298.             message = str(message_id) + " scheduled"
  299.             return message
  300.         except Exception as e:
  301.             await respond_error(update, context, f"Failed to schedule job: {e}")
  302.     else:
  303.         print(f"Date received is in the past, cancelling request for job")
  304.         await update.effective_chat.send_message("Hey\! I'm an otter not a time traveller\!")
  305.  
  306.  
  307. async def delete(_context: ContextTypes.DEFAULT_TYPE) -> None:
  308.     job = _context.job
  309.     try:
  310.         await _context.bot.deleteMessage(chat_id=job.chat_id, message_id=job.data)
  311.         await _context.bot.send_message(chat_id=_configVariables.get("ADMIN_GROUP_ID"),
  312.                                         text="Scheduled message deletion completed")
  313.     except Exception as e:
  314.         await _context.bot.send_message(chat_id=_configVariables.get("ADMIN_GROUP_ID"),
  315.                                         text=f"Failed to complete scheduled deletion job for {job.data}: {e}")
  316.  
  317.  
  318. # Commands #####################################################################
  319.  
  320. COMMANDS = []
  321.  
  322.  
  323. async def cmd_start(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
  324.     """/start: initiate a CatBot conversation"""
  325.     command_docs = '\n'.join([cmd.__doc__ for cmd in COMMANDS])
  326.     await respond(update, context, f"Hewwo! I'm 0ttB0t! These are the things I can do:\n{command_docs}",
  327.                   protect_content=True)
  328.  
  329.  
  330. COMMANDS.append(cmd_start)
  331.  
  332.  
  333. async def cmd_notify(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
  334.     """/notify {pin} {datetime}: send a message to the CambFurs Notifications channel
  335.        Modifiers:
  336.            pin: pins the message in the chat
  337.            Markdown formatted Date: Schedules the message for deletion at the set time"""
  338.  
  339.     print(update.message.text_markdown_v2)
  340.     if update.message.reply_to_message is None:
  341.         await respond_error(update, context, "Please respond to the message you wish to send")
  342.         return
  343.  
  344.     # Modifier Handlers
  345.     protocol = update.message.text_markdown_v2.split(" ")
  346.     pin_message = None
  347.     delete_time = None
  348.     for val in protocol:
  349.         if val == "pin":
  350.             pin_message = True
  351.             print("Set Pin True")
  352.         if re.search("((tg:)(\/{2})(time\?unix=)(\d{10,})\))$", val):
  353.             value = re.findall('\d{10,}', val)
  354.             value = int(value[0])
  355.             # offset = value - datetime.datetime.now()
  356.             tz = arrow.get(tzinfo="Europe/London")
  357.             delete_time = tz.fromtimestamp(value)
  358.  
  359.     # Message Handler
  360.     text = update.message.reply_to_message.text_html
  361.     try:
  362.         message = await context.bot.send_message(
  363.             chat_id=_configVariables.get("NOTIFICATIONS_CHANNEL_ID"),
  364.             text=text,
  365.             parse_mode=ParseMode.HTML)
  366.     except Exception as e:
  367.         await respond_error(update, context, f"Error in sending notification due to: {e}")
  368.         return
  369.     try:
  370.         forward = await context.bot.forwardMessage(chat_id=_configVariables.get("MAIN_GROUP_ID"),
  371.                                                    from_chat_id=_configVariables.get("NOTIFICATIONS_CHANNEL_ID"),
  372.                                                    message_id=message.id)
  373.         success_message = f"Forwarded id: {forward.id}"
  374.         if pin_message:
  375.             try:
  376.                 await context.bot.pinChatMessage(chat_id=_configVariables.get("MAIN_GROUP_ID"),
  377.                                                  message_id=forward.message_id)
  378.                 success_message.join("\rMessage Pinned")
  379.             except Exception as e:
  380.                 await respond_error(update, context, f"Error pinning message due to: {e}")
  381.                 return
  382.         if delete_time is not None:
  383.             try:
  384.                 print(delete_time)
  385.                 notification = await schedule(update, context, delete_time, message.id,
  386.                                               _configVariables.get("NOTIFICATIONS_CHANNEL_ID"))
  387.                 forward = await schedule(update, context, delete_time, forward.id,
  388.                                          _configVariables.get("MAIN_GROUP_ID"))
  389.                 success_message.join(f"\rMessage Scheduled for deletion: {delete_time}")
  390.                 print(notification, forward)
  391.             except Exception as e:
  392.                 print(e)
  393.                 await respond_error(update, context, f"Error scheduling deletion message due to: {e}")
  394.                 return
  395.         await respond_success(update, context, success_message)
  396.         print(success_message)
  397.     except Exception as e:
  398.         await respond_error(update, context, f"Error forwarding message due to: {e}")
  399.         return
  400.  
  401.  
  402. COMMANDS.append(cmd_notify)
  403.  
  404.  
  405. async def cmd_cancel_deletion(update: Update, context: ContextTypes.DEFAULT_TYPE) -> bool:
  406.     """/canceljob {message id}: Cancels the scheduled deletion of a specified message"""
  407.     job_id = re.findall(r"\d{10,}", update.message.text)
  408.  
  409.     current_jobs = context.job_queue.get_jobs_by_name(job_id[0])
  410.     if not current_jobs:
  411.         return False
  412.     for job in current_jobs:
  413.         job.schedule_removal()
  414.     return True
  415.  
  416.  
  417. COMMANDS.append(cmd_cancel_deletion)
  418.  
  419.  
  420. async def cmd_meet_dates(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
  421.     """/meet_dates: list upcoming meet dates"""
  422.     upcoming_events = await get_upcoming_meet_events()
  423.     ret = ["⭐ <b><u>Upcoming meet dates</u></b> ⭐"]
  424.     for event in upcoming_events:
  425.         month = arrow.locales.EnglishLocale.month_names[event.begin.month]
  426.         day = ordinal(event.begin.day)
  427.         maybe_description = ' ' + event.description if event.description is not None else ''
  428.         ret.append(f"➡️ {month} {day}{sanitize(maybe_description)}")
  429.     await respond(update, context, "\n".join(ret), parse_mode=ParseMode.HTML)
  430.  
  431.  
  432. COMMANDS.append(cmd_meet_dates)
  433.  
  434.  
  435. async def cmd_say(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
  436.     """/say: puts replied message into the main chat"""
  437.     if update.message.reply_to_message is None:
  438.         await respond_error(update, context, "Please respond to the message you wish to send")
  439.         return
  440.  
  441.     text = update.message.reply_to_message.text_markdown_v2_urled
  442.     message = await context.bot.send_message(chat_id=_configVariables.get("MAIN_GROUP_ID"), text=text)
  443.     await respond_success(update, context, f"Sent! id: {message.id}")
  444.  
  445.  
  446. COMMANDS.append(cmd_say)
  447.  
  448.  
  449. async def cmd_register_admin(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
  450.     """/registerasadmin: If you have admin role in the main chat, and are in the admin chat,
  451.     this will give you enhanced permissions"""
  452.     if update.effective_chat.type == ChatType.PRIVATE:
  453.         uid = update.effective_user.id
  454.         chat_members = await context.bot.get_chat_administrators(_configVariables.get("MAIN_GROUP_ID"))
  455.         admin_members = await context.bot.get_chat_member(chat_id=_configVariables.get("MAIN_GROUP_ID"), user_id=uid)
  456.         if uid in context.roles.admins.chat_ids:
  457.             context.bot.send_message(chat_id=uid, text="You're already an admin!")
  458.             return
  459.         elif uid in chat_members & uid in admin_members:
  460.             try:
  461.                 context.roles.add_admin(uid)
  462.                 context.bot.send_message(chat_id=uid,
  463.                                          text="You've been added as an admin! You can now use all available commands")
  464.                 return
  465.             except Exception as e:
  466.                 await update.message.reply_text(f"Error! Could not register you as an admin due to: {e}")
  467.                 return
  468.         else:
  469.             await context.bot.send_message(chat_id=uid,
  470.                                            text="Hey! Very sneaky but I can't add you as an admin unless you are one!")
  471.             return
  472.     context.bot.send_message(chat_id=update.effective_chat, text="This command can only be used privately!")
  473.  
  474.  
  475. COMMANDS.append(cmd_register_admin)
  476.  
  477.  
  478. # Waiting Room Handlers#########################################################
  479.  
  480. async def cmd_join(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
  481.     """/join:"""
  482.     print("Join")
  483.     user = update.effective_user
  484.     member_type = await context.bot.get_chat_member(_configVariables.get("MAIN_GROUP_ID"), user.id)
  485.     if member_type.status == ChatMemberStatus.MEMBER or member_type.status == ChatMemberStatus.ADMINISTRATOR:
  486.         await update.message.reply_text("You're already a member!")
  487.         return
  488.     if member_type.status == ChatMemberStatus.BANNED or user.id in ban_list:
  489.         await update.message.reply_text("You cannot apply to rejoin Cambridge Furs as you have previously been banned.")
  490.     if member_type.status == ChatMemberStatus.LEFT:
  491.         print("Hit LEFT")
  492.         name = set_topic_from_user(user)
  493.         print(name)
  494.         topic = await context.bot.create_forum_topic(chat_id=_configVariables.get("WAITING_ROOM_TOPICS_ID"), name=name)
  495.         print(f" Created {topic.name} as topic ID: {topic.message_thread_id}")
  496.         await context.bot.forwardMessage(
  497.             chat_id=_configVariables.get("WAITING_ROOM_TOPICS_ID"),
  498.             from_chat_id=user.id,
  499.             message_thread_id=topic.message_thread_id,
  500.             message_id=update.message.id
  501.         )
  502.         print(f"Forwareded message from {user.id} to Topic id {topic.message_thread_id}")
  503.         context.bot_data[topic.message_thread_id] = user.id
  504.         print(
  505.             f"Stored user id {context.bot_data.get(topic.message_thread_id)} for key topic ID {topic.message_thread_id}")
  506.         context.bot_data[user.id] = topic.message_thread_id
  507.         print(f"Stored for topic {context.bot_data.get(user.id)} for key user id {user.id}")
  508.  
  509.  
  510. COMMANDS.append(cmd_join)
  511.  
  512.  
  513. async def message_reply_from_admin(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
  514.     if update.effective_user.id == 8691458770:
  515.         return
  516.     topic = update.message.message_thread_id
  517.     user_id = context.bot_data.get(topic)
  518.     print(user_id)
  519.     if user_id is None:
  520.         print("No valid chat_data held for this topic. Attempting another validation step")
  521.         origin = update.message.reply_to_message.forward_origin
  522.         if origin is None:
  523.             print("Alternative validation step failed. Requesting new message with forwarded origin")
  524.             await update.message.reply_text("Please reply to a forwarded message to continue this conversation.")
  525.             return
  526.         else:
  527.             print("Alternative validation successful. Saving Chat_data for this topic")
  528.             user_id = ForwardOrigin(origin["sender_user"]).id
  529.  
  530.             context.bot_data[topic] = user_id
  531.             print(f"Stored user id {context.bot_data.get(topic)} for key topic ID {topic}")
  532.             context.bot_data[user_id] = topic
  533.             print(f"Stored for topic {context.bot_data.get(user_id)} for key user id {user_id}")
  534.  
  535.     await context.bot.send_message(chat_id=user_id, text=update.message.text_html, parse_mode=ParseMode.HTML)
  536.  
  537.  
  538. async def message_reply_from_joiner(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
  539.     print("message_reply_from_joiner triggered")
  540.     thread_id = int
  541.     user_id = update.effective_user.id
  542.     try:
  543.         thread_id = context.bot_data.get(user_id)
  544.         print(thread_id)
  545.     except KeyError:
  546.         thread_id = await handle_lost_joiner_mapping(update, context)
  547.     finally:
  548.         try:
  549.             await context.bot.forwardMessage(
  550.                 chat_id=_configVariables.get("WAITING_ROOM_TOPICS_ID"),
  551.                 from_chat_id=user_id,
  552.                 message_thread_id=thread_id,
  553.                 message_id=update.message.id
  554.             )
  555.         except Exception as e:
  556.             print(e)
  557.  
  558.  
  559. async def handle_lost_joiner_mapping(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
  560.     user = update.effective_user
  561.     name = set_topic_from_user(user)
  562.     print(f"Lost joiner mapping triggered for conversation with {name}. Creating new topic")
  563.     topic = await context.bot.create_forum_topic(chat_id=_configVariables.get("WAITING_ROOM_TOPICS_ID"), name=name)
  564.     context.bot_data[topic.message_thread_id] = user.id
  565.     context.bot_data[user.id] = topic.message_thread_id
  566.     return topic.message_thread_id
  567.  
  568.  
  569. async def cmd_topic_approve(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
  570.     # TODO: Not yet implemented, implement a keyboard/command button
  571.     minutes_valid = 5
  572.     invite_link = await context.bot.create_chat_invite_link(
  573.         _configVariables.get("MAIN_GROUP_ID"),
  574.         creates_join_request=True,
  575.         expire_date=datetime.datetime.now(datetime.UTC) + datetime.timedelta(minutes=minutes_valid))
  576.  
  577.     global APPROVED_USERS_IN_WAITING_ROOM
  578.     user = context.bot_data.get(update.message.message_thread_id)
  579.     APPROVED_USERS_IN_WAITING_ROOM.add(user)
  580.     context.bot.send_message(chat_id=user, text=f"{user} Here's your invite link to the CambFurs group!\
  581.                           This link is only valid for {minutes_valid} minutes\n\n{invite_link.invite_link}")
  582.  
  583.  
  584. async def cmd_topic_ban(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
  585.     try:
  586.         user_id = context.bot_data[update.message.message_thread_id]
  587.         ban_list.add(user_id)
  588.         await close_topic(update, context)
  589.     except Exception as e:
  590.         print(e)
  591.  
  592.  
  593. COMMANDS.append(cmd_topic_ban)
  594.  
  595.  
  596. async def close_topic(update: Update, context: ContextTypes.DEFAULT_TYPE):
  597.     user_id = update.chat_join_request.from_user.id
  598.     topic = context.bot_data[user_id]
  599.     try:
  600.         context.bot_data.pop(topic)
  601.         context.bot_data.pop(user_id)
  602.     except IndexError:
  603.         await respond_error(update, context, "Unable to find item to remove in bot_data store")
  604.     else:
  605.         await context.bot.deleteForumTopic(
  606.             chat_id=_configVariables.get("WAITING_ROOM_TOPICS_ID"),
  607.             message_thread_id=topic)
  608.  
  609.  
  610. # Authentication ###############################################################
  611. # NOTE: Matching on usernames is not bullet-proof in Telegram. One user can own
  612. # multiple 'vanity' @usernames they can switch between or even have no username
  613. # at all. I think it's fine to rely on usernames for the short duration between
  614. # /approving and them joining.
  615.  
  616. APPROVED_USERS_IN_WAITING_ROOM = set()
  617.  
  618.  
  619. async def cmd_approve(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
  620.     """/approve @username: create invite link for user"""
  621.  
  622.     if not update.message.chat.id == _configVariables.get(
  623.             "WAITING_ROOM_GROUP_ID") or update.message.chat_id == _configVariables.get("WAITING_ROOM_TOPICS_ID"):
  624.         return
  625.  
  626.     if not update.message.from_user.username == "GroupAnonymousBot":
  627.         return
  628.  
  629.     user = list(update.message.parse_entities(types=['mention']).values())
  630.     if len(user) != 1:
  631.         await respond_error(update, context, "Must specify a single user to approve")
  632.         return
  633.     user = user[0]
  634.  
  635.     minutes_valid = 5
  636.     invite_link = await context.bot.create_chat_invite_link(
  637.         _configVariables.get("MAIN_GROUP_ID"),
  638.         creates_join_request=True,
  639.         expire_date=datetime.datetime.now(datetime.UTC) + datetime.timedelta(minutes=minutes_valid))
  640.  
  641.     global APPROVED_USERS_IN_WAITING_ROOM
  642.     APPROVED_USERS_IN_WAITING_ROOM.add(user)
  643.  
  644.     await respond(update, context,
  645.                   f"{user} Here's your invite link to the CambFurs group!\
  646.                   This link is only valid for {minutes_valid} minutes\n\n{invite_link.invite_link}")
  647.  
  648.  
  649. COMMANDS.append(cmd_approve)
  650.  
  651.  
  652. async def join_request(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
  653.     user_id = update.chat_join_request.from_user.id
  654.     chat_id = update.chat_join_request.chat.id
  655.     username = f"@{update.chat_join_request.from_user.username}"
  656.     if chat_id != _configVariables.get("MAIN_GROUP_ID"):
  657.         await alert(context.bot,
  658.                     f"⛔ Declined join request from {username}: requested to join chat other than main group")
  659.         await context.bot.decline_chat_join_request(chat_id, user_id)
  660.         return
  661.  
  662.     global APPROVED_USERS_IN_WAITING_ROOM
  663.     if username not in APPROVED_USERS_IN_WAITING_ROOM:
  664.         await alert(context.bot, f"⛔ Declined join request from {username}: they were not approved")
  665.         await context.bot.decline_chat_join_request(chat_id, user_id)
  666.         return
  667.  
  668.     APPROVED_USERS_IN_WAITING_ROOM.discard(username)
  669.     await context.bot.approve_chat_join_request(_configVariables.get("MAIN_GROUP_ID"), user_id)
  670.     await context.bot.revoke_chat_invite_link(_configVariables.get("MAIN_GROUP_ID"),
  671.                                               update.chat_join_request.invite_link)
  672.     await context.roles["sfw_member"].add_member(user_id)
  673.     await main_group_welcome(context.bot, update.chat_join_request.from_user)
  674.     # To kick a user from the waiting room we "unban" them. This will kick a
  675.     # member if they're in the chat by default. Yes that's a weird API decision.
  676.     # see: https://core.telegram.org/bots/api#unbanchatmember
  677.     await context.bot.unban_chat_member(_configVariables.get("WAITING_ROOM_GROUP_ID"), user_id)
  678.     await close_topic(update, context)
  679.  
  680.  
  681. # Setup runners ###############################################################
  682.  
  683. async def role_setup(app: Application):
  684.     roles = setup_roles(app)
  685.     roles.add_admin(int(_configVariables.get("ADMIN_GROUP_ID")))
  686.     chat_admins = await app.bot.get_chat_administrators(_configVariables.get("MAIN_GROUP_ID"))
  687.     for value in chat_admins:
  688.         roles.add_admin(value.user.id)
  689.         print(f"Added {value.user.name} ({value.user.id}) as admin")
  690.  
  691.     if 'sfw_member' not in roles:
  692.         roles.add_role(name="sfw_member")
  693.     sfw_member = roles["sfw_member"]
  694.     print("Role set up: sfw_member")
  695.  
  696.     roles.admins.add_child_role(sfw_member)
  697.     print("Role set as parent: admin > sfw_member")
  698.     return roles
  699.  
  700.  
  701. # noinspection PyTypeChecker
  702. async def initialize(app: Application) -> None:
  703.     # Exceptions escaping from the initialize function result in a silent crash.
  704.     # It's therefore important to wrap everything in a try block
  705.     try:
  706.         print("Started Initialisation")
  707.         roles = await role_setup(app)
  708.         # General Commands
  709.         app.add_handler(CommandHandler("join", cmd_join, filters.Regex(WEBSITE)))
  710.         app.add_handler(ChatJoinRequestHandler(join_request), group=0)
  711.         app.add_handler(CommandHandler("meet_dates", cmd_meet_dates))
  712.         app.add_handler(ChatMemberHandler(chat_member_updated, ChatMemberHandler.CHAT_MEMBER))
  713.         app.add_handler(CommandHandler("registerasadmin", cmd_register_admin))
  714.         app.add_handler(CommandHandler("join", cmd_join))
  715.         app.add_handler(RolesHandler(MessageHandler(
  716.             filter_waiting_room & (~filters.COMMAND),
  717.             message_reply_from_admin),
  718.             roles=roles.admins))
  719.         app.add_handler(RolesHandler(MessageHandler(
  720.             filter_direct_message & (~filters.COMMAND),
  721.             message_reply_from_joiner),
  722.             roles=~roles["sfw_member"]))
  723.         # Admin Commands
  724.         app.add_handler(RolesHandler(CommandHandler("start", cmd_start), roles=roles.admins))
  725.         app.add_handler(RolesHandler(CommandHandler("say", cmd_say), roles=roles.admins))
  726.         app.add_handler(RolesHandler(CommandHandler("notify", cmd_notify), roles=roles.admins))
  727.         app.add_handler(RolesHandler(CommandHandler("canceljob", cmd_cancel_deletion), roles=roles.admins))
  728.         app.add_handler(RolesHandler(CommandHandler("approve", cmd_approve), roles=roles.admins))
  729.         app.add_handler(RolesHandler(CommandHandler("reject", cmd_topic_approve), roles=roles.admins))
  730.         app.add_handler(RolesHandler(CommandHandler("reject", cmd_topic_ban), roles=roles.admins))
  731.         job_hourly_callback = app.job_queue.run_repeating(
  732.             hourly_callback,
  733.             interval=3600,
  734.             first=arrow.utcnow().ceil("hour").datetime)
  735.  
  736.     except Exception as e:
  737.         await alert(app.bot, f"🆘 0ttbot failed to start {e}")
  738.         raise
  739.     else:
  740.         await alert(app.bot, "🟢 0ttbot started")
  741.         print(app.bot_data)
  742.  
  743.  
  744. async def finalize(app) -> None:
  745.     # await alert(app.bot, "🆘 CatBot stopped")
  746.     pass
  747.  
  748.  
  749. if __name__ == "__main__":
  750.     main()
  751.  
Advertisement
Add Comment
Please, Sign In to add comment