├── log.txt ├── MissCutie ├── Functions │ └── __init__.py ├── Handlers │ ├── admin_rights.py │ ├── __init__.py │ ├── telethon │ │ ├── __init__.py │ │ └── validations.py │ ├── alternate.py │ ├── filters.py │ ├── pyrogram │ │ └── admins.py │ ├── misc.py │ ├── managers.py │ ├── extraction.py │ └── msg_types.py ├── Plugins │ ├── Admin │ │ ├── __init__.py │ │ ├── anti_channel.py │ │ ├── bluser.py │ │ ├── rules.py │ │ ├── purge.py │ │ ├── muting.py │ │ └── log_channel.py │ ├── Tools │ │ ├── __init__.py │ │ ├── ud.py │ │ ├── wiki.py │ │ ├── image.py │ │ ├── gtranslator.py │ │ ├── text_to_speech.py │ │ ├── identity.py │ │ ├── telegraph.py │ │ └── music.py │ ├── User │ │ ├── __init__.py │ │ ├── github.py │ │ ├── paste.py │ │ ├── chatbot.py │ │ ├── math.py │ │ └── afk.py │ ├── __init__.py │ ├── get_common_chats.py │ ├── speed_test.py │ ├── error_handler.py │ ├── users.py │ └── modules.py ├── Database │ ├── __init__.py │ ├── chatbot_sql.py │ ├── forceSubscribe_sql.py │ ├── rules_sql.py │ ├── purges_sql.py │ ├── approve_sql.py │ ├── blacklistusers_sql.py │ ├── antichannel_sql.py │ ├── userinfo_sql.py │ ├── log_channel_sql.py │ ├── afk_sql.py │ ├── reporting_sql.py │ ├── disable_sql.py │ ├── antiflood_sql.py │ ├── global_bans_sql.py │ ├── notes_sql.py │ ├── blacklist_sql.py │ ├── users_sql.py │ └── connection_sql.py └── __init__.py ├── README.md ├── Procfile ├── heroku.yml ├── renovate.json ├── Dockerfile ├── requirements.txt ├── LICENSE └── app.json /log.txt: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /MissCutie/Functions/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /MissCutie/Handlers/admin_rights.py: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## UNMAINTAINED REPOSITORY 2 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | worker: python3 -m MissCutie 2 | ps:scale worker=1 3 | -------------------------------------------------------------------------------- /heroku.yml: -------------------------------------------------------------------------------- 1 | build: 2 | docker: 3 | worker: Dockerfile 4 | -------------------------------------------------------------------------------- /MissCutie/Handlers/__init__.py: -------------------------------------------------------------------------------- 1 | # BSD 2-Clause License 2 | 3 | # Copyright (c) 2022, Kushal 4 | # All rights reserved. -------------------------------------------------------------------------------- /MissCutie/Plugins/Admin/__init__.py: -------------------------------------------------------------------------------- 1 | # BSD 2-Clause License 2 | 3 | # Copyright (c) 2022, Kushal 4 | # All rights reserved. -------------------------------------------------------------------------------- /MissCutie/Plugins/Tools/__init__.py: -------------------------------------------------------------------------------- 1 | # BSD 2-Clause License 2 | 3 | # Copyright (c) 2022, Kushal 4 | # All rights reserved. -------------------------------------------------------------------------------- /MissCutie/Plugins/User/__init__.py: -------------------------------------------------------------------------------- 1 | # BSD 2-Clause License 2 | 3 | # Copyright (c) 2022, Kushal 4 | # All rights reserved. -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "config:base" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /MissCutie/Handlers/telethon/__init__.py: -------------------------------------------------------------------------------- 1 | from MissCutie import DEV_USERS, INSPECTOR, REQUESTER, telethn 2 | 3 | IMMUNE_USERS = INSPECTOR.union(REQUESTER).union(DEV_USERS) 4 | 5 | IMMUNE_USERS = ( 6 | list(INSPECTOR) + list(REQUESTER) + list(DEV_USERS) 7 | ) 8 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.10.4-slim-buster 2 | RUN apt update && apt upgrade -y 3 | RUN apt-get -y install git 4 | RUN apt-get install -y wget python3-pip curl bash neofetch ffmpeg software-properties-common 5 | COPY requirements.txt . 6 | RUN apt-get install -y nodejs 7 | COPY . . 8 | RUN pip install -U "pip < 22" setuptools wheel && pip install -U -r requirements.txt 9 | RUN python3 -m MissCutie 10 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | aiohttp 2 | cachetools==4.2.2 3 | python-telegram-bot==13.9 4 | Telethon==1.24.0 5 | pyrate-limiter 6 | sqlalchemy==1.4.11 7 | git+https://github.com/trentm/python-markdown2.git 8 | future 9 | psycopg2-binary 10 | bleach 11 | emoji==1.6.3 12 | requests 13 | humanize 14 | speedtest-cli 15 | pretty_errors 16 | alphabet_detector 17 | wget 18 | faker 19 | pyaztro 20 | psutil 21 | gpytranslate 22 | cloudscraper 23 | bs4 24 | lxml 25 | pillow 26 | telegraph 27 | wikipedia 28 | opencv-python--headless 29 | gtts 30 | pyrogram 31 | TgCrypto 32 | speedtest-cli 33 | tswift 34 | pynewtonmath 35 | -------------------------------------------------------------------------------- /MissCutie/Database/__init__.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import create_engine 2 | from sqlalchemy.ext.declarative import declarative_base 3 | from sqlalchemy.orm import sessionmaker, scoped_session 4 | 5 | from MissCutie import DB_URI, LOGGER as log 6 | 7 | if DB_URI and DB_URI.startswith("postgres://"): 8 | DB_URI = DB_URI.replace("postgres://", "postgresql://", 1) 9 | 10 | def start() -> scoped_session: 11 | engine = create_engine(DB_URI, client_encoding="utf8") 12 | log.info("[PostgreSQL] Connecting to database......") 13 | BASE.metadata.bind = engine 14 | BASE.metadata.create_all(engine) 15 | return scoped_session(sessionmaker(bind=engine, autoflush=False)) 16 | 17 | 18 | BASE = declarative_base() 19 | try: 20 | SESSION = start() 21 | except Exception as e: 22 | log.exception(f'[PostgreSQL] Failed to connect due to {e}') 23 | exit() 24 | 25 | log.info("[PostgreSQL] Connection successful, session started.") 26 | -------------------------------------------------------------------------------- /MissCutie/Plugins/Tools/ud.py: -------------------------------------------------------------------------------- 1 | import requests 2 | from MissCutie import dispatcher 3 | from MissCutie.Plugins.disable import DisableAbleCommandHandler 4 | from telegram import ParseMode, Update 5 | from telegram.ext import CallbackContext, run_async 6 | 7 | 8 | def ud(update: Update, context: CallbackContext): 9 | message = update.effective_message 10 | text = message.text[len("/ud ") :] 11 | results = requests.get( 12 | f"https://api.urbandictionary.com/v0/define?term={text}", 13 | ).json() 14 | try: 15 | reply_text = f'*{text}*\n\n{results["list"][0]["definition"]}\n\n_{results["list"][0]["example"]}_' 16 | except: 17 | reply_text = "No results found." 18 | message.reply_text(reply_text, parse_mode=ParseMode.MARKDOWN) 19 | 20 | 21 | UD_HANDLER = DisableAbleCommandHandler(["ud"], ud, run_async=True) 22 | 23 | dispatcher.add_handler(UD_HANDLER) 24 | 25 | __command_list__ = ["ud"] 26 | __handlers__ = [UD_HANDLER] 27 | -------------------------------------------------------------------------------- /MissCutie/Database/chatbot_sql.py: -------------------------------------------------------------------------------- 1 | import threading 2 | 3 | from sqlalchemy import Column, String 4 | 5 | from MissCutie.Database import BASE, SESSION 6 | 7 | 8 | class FallenChats(BASE): 9 | __tablename__ = "fallen_chats" 10 | chat_id = Column(String(14), primary_key=True) 11 | 12 | def __init__(self, chat_id): 13 | self.chat_id = chat_id 14 | 15 | 16 | FallenChats.__table__.create(checkfirst=True) 17 | INSERTION_LOCK = threading.RLock() 18 | 19 | 20 | def is_fallen(chat_id): 21 | try: 22 | chat = SESSION.query(FallenChats).get(str(chat_id)) 23 | return bool(chat) 24 | finally: 25 | SESSION.close() 26 | 27 | 28 | def set_fallen(chat_id): 29 | with INSERTION_LOCK: 30 | fallenchat = SESSION.query(FallenChats).get(str(chat_id)) 31 | if not fallenchat: 32 | fallenchat = FallenChats(str(chat_id)) 33 | SESSION.add(fallenchat) 34 | SESSION.commit() 35 | 36 | 37 | def rem_fallen(chat_id): 38 | with INSERTION_LOCK: 39 | if fallenchat := SESSION.query(FallenChats).get(str(chat_id)): 40 | SESSION.delete(fallenchat) 41 | SESSION.commit() 42 | -------------------------------------------------------------------------------- /MissCutie/Handlers/alternate.py: -------------------------------------------------------------------------------- 1 | from telegram.error import BadRequest 2 | from functools import wraps 3 | from telegram import ChatAction 4 | 5 | 6 | def send_message(message, text, *args, **kwargs): 7 | try: 8 | return message.reply_text(text, *args, **kwargs) 9 | except BadRequest as err: 10 | if str(err) == "Reply message not found": 11 | return message.reply_text(text, quote=False, *args, **kwargs) 12 | 13 | 14 | def typing_action(func): 15 | """Sends typing action while processing func command.""" 16 | 17 | @wraps(func) 18 | def command_func(update, context, *args, **kwargs): 19 | context.bot.send_chat_action( 20 | chat_id=update.effective_chat.id, action=ChatAction.TYPING) 21 | return func(update, context, *args, **kwargs) 22 | 23 | return command_func 24 | 25 | def send_action(action): 26 | """Sends `action` while processing func command.""" 27 | 28 | def decorator(func): 29 | @wraps(func) 30 | def command_func(update, context, *args, **kwargs): 31 | context.bot.send_chat_action( 32 | chat_id=update.effective_chat.id, action=action 33 | ) 34 | return func(update, context, *args, **kwargs) 35 | 36 | return command_func 37 | 38 | return decorator -------------------------------------------------------------------------------- /MissCutie/Database/forceSubscribe_sql.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2020-2021 by @saifalisew1508 2 | # This program is a part of MissCutie TG bot project 3 | 4 | 5 | 6 | 7 | from sqlalchemy import Column, Numeric, String 8 | 9 | from MissCutie.Database import BASE, SESSION 10 | 11 | 12 | class forceSubscribe(BASE): 13 | __tablename__ = "forceSubscribe" 14 | chat_id = Column(Numeric, primary_key=True) 15 | channel = Column(String) 16 | 17 | def __init__(self, chat_id, channel): 18 | self.chat_id = chat_id 19 | self.channel = channel 20 | 21 | 22 | forceSubscribe.__table__.create(checkfirst=True) 23 | 24 | 25 | def fs_settings(chat_id): 26 | try: 27 | return ( 28 | SESSION.query(forceSubscribe) 29 | .filter(forceSubscribe.chat_id == chat_id) 30 | .one() 31 | ) 32 | except: 33 | return None 34 | finally: 35 | SESSION.close() 36 | 37 | 38 | def add_channel(chat_id, channel): 39 | adder = SESSION.query(forceSubscribe).get(chat_id) 40 | if adder: 41 | adder.channel = channel 42 | else: 43 | adder = forceSubscribe(chat_id, channel) 44 | SESSION.add(adder) 45 | SESSION.commit() 46 | 47 | 48 | def disapprove(chat_id): 49 | rem = SESSION.query(forceSubscribe).get(chat_id) 50 | if rem: 51 | SESSION.delete(rem) 52 | SESSION.commit() 53 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 2-Clause License 2 | 3 | Copyright (c) 2022, Kushal 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 17 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 18 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 20 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 21 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 22 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 23 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 24 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 25 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | -------------------------------------------------------------------------------- /MissCutie/Database/rules_sql.py: -------------------------------------------------------------------------------- 1 | import threading 2 | 3 | from MissCutie.Database import BASE, SESSION 4 | from sqlalchemy import Column, String, UnicodeText, distinct, func 5 | 6 | 7 | class Rules(BASE): 8 | __tablename__ = "rules" 9 | chat_id = Column(String(14), primary_key=True) 10 | rules = Column(UnicodeText, default="") 11 | 12 | def __init__(self, chat_id): 13 | self.chat_id = chat_id 14 | 15 | def __repr__(self): 16 | return f"" 17 | 18 | 19 | Rules.__table__.create(checkfirst=True) 20 | 21 | INSERTION_LOCK = threading.RLock() 22 | 23 | 24 | def set_rules(chat_id, rules_text): 25 | with INSERTION_LOCK: 26 | rules = SESSION.query(Rules).get(str(chat_id)) 27 | if not rules: 28 | rules = Rules(str(chat_id)) 29 | rules.rules = rules_text 30 | 31 | SESSION.add(rules) 32 | SESSION.commit() 33 | 34 | 35 | def get_rules(chat_id): 36 | ret = rules.rules if (rules := SESSION.query(Rules).get(str(chat_id))) else "" 37 | SESSION.close() 38 | return ret 39 | 40 | 41 | def num_chats(): 42 | try: 43 | return SESSION.query(func.count(distinct(Rules.chat_id))).scalar() 44 | finally: 45 | SESSION.close() 46 | 47 | 48 | def migrate_chat(old_chat_id, new_chat_id): 49 | with INSERTION_LOCK: 50 | if chat := SESSION.query(Rules).get(str(old_chat_id)): 51 | chat.chat_id = str(new_chat_id) 52 | SESSION.commit() 53 | -------------------------------------------------------------------------------- /MissCutie/Handlers/filters.py: -------------------------------------------------------------------------------- 1 | from MissCutie import DEV_USERS, INSPECTOR, REQUESTER 2 | from telegram import Message 3 | from telegram.ext import MessageFilter 4 | 5 | 6 | class CustomFilters(object): 7 | class _Supporters(MessageFilter): 8 | def filter(self, message: Message): 9 | return bool(message.from_user and message.from_user.id in REQUESTER) 10 | 11 | support_filter = _Supporters() 12 | 13 | class _Sudoers(MessageFilter): 14 | def filter(self, message: Message): 15 | return bool(message.from_user and message.from_user.id in INSPECTOR) 16 | 17 | sudo_filter = _Sudoers() 18 | 19 | class _Developers(MessageFilter): 20 | def filter(self, message: Message): 21 | return bool(message.from_user and message.from_user.id in DEV_USERS) 22 | 23 | dev_filter = _Developers() 24 | 25 | class _MimeType(MessageFilter): 26 | def __init__(self, mimetype): 27 | self.mime_type = mimetype 28 | self.name = f"CustomFilters.mime_type({self.mime_type})" 29 | 30 | def filter(self, message: Message): 31 | return bool( 32 | message.document and message.document.mime_type == self.mime_type 33 | ) 34 | 35 | mime_type = _MimeType 36 | 37 | class _HasText(MessageFilter): 38 | def filter(self, message: Message): 39 | return bool( 40 | message.text 41 | or message.sticker 42 | or message.photo 43 | or message.document 44 | or message.video 45 | ) 46 | 47 | has_text = _HasText() 48 | -------------------------------------------------------------------------------- /MissCutie/Database/purges_sql.py: -------------------------------------------------------------------------------- 1 | import threading 2 | 3 | from MissCutie.Database import BASE, SESSION 4 | from sqlalchemy import (Column, BigInteger, String) 5 | 6 | 7 | class Purges(BASE): 8 | __tablename__ = "purges" 9 | chat_id = Column(String(14), primary_key=True) 10 | message_from = Column(BigInteger, primary_key=True) 11 | 12 | 13 | def __init__(self, chat_id, message_from): 14 | self.chat_id = str(chat_id) # ensure string 15 | self.message_from = message_from 16 | 17 | 18 | def __repr__(self): 19 | return f"" 20 | 21 | 22 | Purges.__table__.create(checkfirst=True) 23 | 24 | PURGES_INSERTION_LOCK = threading.RLock() 25 | 26 | def purgefrom(chat_id, message_from): 27 | with PURGES_INSERTION_LOCK: 28 | note = Purges(str(chat_id), message_from) 29 | SESSION.add(note) 30 | SESSION.commit() 31 | 32 | def is_purgefrom(chat_id, message_from): 33 | try: 34 | return SESSION.query(Purges).get((str(chat_id), message_from)) 35 | finally: 36 | SESSION.close() 37 | 38 | def clear_purgefrom(chat_id, message_from): 39 | with PURGES_INSERTION_LOCK: 40 | if note := SESSION.query(Purges).get((str(chat_id), message_from)): 41 | SESSION.delete(note) 42 | SESSION.commit() 43 | return True 44 | else: 45 | SESSION.close() 46 | return False 47 | 48 | def show_purgefrom(chat_id): 49 | try: 50 | return SESSION.query(Purges).filter(Purges.chat_id == str(chat_id)).order_by(Purges.message_from.asc()).all() 51 | finally: 52 | SESSION.close() 53 | -------------------------------------------------------------------------------- /MissCutie/Plugins/__init__.py: -------------------------------------------------------------------------------- 1 | # BSD 2-Clause License 2 | 3 | # Copyright (c) 2022, Kushal 4 | # All rights reserved. 5 | 6 | from MissCutie import LOAD, LOGGER, NO_LOAD 7 | 8 | 9 | def __list_all_modules(): 10 | import glob 11 | from os.path import basename, dirname, isfile 12 | import os 13 | path =r'./MissCutie/Plugins/' 14 | list_of_files = [] 15 | 16 | for root, dirs, files in os.walk(path): 17 | list_of_files.extend(os.path.join(root,file) for file in files) 18 | # This generates a list of modules in this folder for the * in __help__ to work. 19 | 20 | all_modules = [ 21 | basename(name)[:-3] 22 | for name in list_of_files 23 | if isfile(name) and name.endswith(".py") and not name.endswith("__init__.py") 24 | ] 25 | 26 | if LOAD or NO_LOAD: 27 | to_load = LOAD 28 | if to_load: 29 | if not all( 30 | any(mod == module_name for module_name in all_modules) 31 | for mod in to_load 32 | ): 33 | LOGGER.error("Invalid loadorder names. Quitting.") 34 | quit(1) 35 | 36 | all_modules = sorted(set(all_modules) - set(to_load)) 37 | to_load = list(all_modules) + to_load 38 | 39 | else: 40 | to_load = all_modules 41 | 42 | if NO_LOAD: 43 | LOGGER.info(f"Not loading: {NO_LOAD}") 44 | return [item for item in to_load if item not in NO_LOAD] 45 | 46 | return to_load 47 | 48 | return all_modules 49 | 50 | 51 | ALL_MODULES = __list_all_modules() 52 | LOGGER.info("Modules to load: %s", str(ALL_MODULES)) 53 | __all__ = ALL_MODULES + ["ALL_MODULES"] 54 | -------------------------------------------------------------------------------- /MissCutie/Database/approve_sql.py: -------------------------------------------------------------------------------- 1 | import threading 2 | 3 | from sqlalchemy import Column, String, BigInteger 4 | 5 | from MissCutie.Database import BASE, SESSION 6 | 7 | 8 | class Approvals(BASE): 9 | __tablename__ = "approval" 10 | chat_id = Column(String(14), primary_key=True) 11 | user_id = Column(BigInteger, primary_key=True) 12 | 13 | def __init__(self, chat_id, user_id): 14 | self.chat_id = str(chat_id) # ensure string 15 | self.user_id = user_id 16 | 17 | def __repr__(self): 18 | return f"" 19 | 20 | 21 | Approvals.__table__.create(checkfirst=True) 22 | 23 | APPROVE_INSERTION_LOCK = threading.RLock() 24 | 25 | 26 | def approve(chat_id, user_id): 27 | with APPROVE_INSERTION_LOCK: 28 | approve_user = Approvals(str(chat_id), user_id) 29 | SESSION.add(approve_user) 30 | SESSION.commit() 31 | 32 | 33 | def is_approved(chat_id, user_id): 34 | try: 35 | return SESSION.query(Approvals).get((str(chat_id), user_id)) 36 | finally: 37 | SESSION.close() 38 | 39 | 40 | def disapprove(chat_id, user_id): 41 | with APPROVE_INSERTION_LOCK: 42 | if disapprove_user := SESSION.query(Approvals).get( 43 | (str(chat_id), user_id) 44 | ): 45 | SESSION.delete(disapprove_user) 46 | SESSION.commit() 47 | return True 48 | else: 49 | SESSION.close() 50 | return False 51 | 52 | 53 | def list_approved(chat_id): 54 | try: 55 | return ( 56 | SESSION.query(Approvals) 57 | .filter(Approvals.chat_id == str(chat_id)) 58 | .order_by(Approvals.user_id.asc()) 59 | .all() 60 | ) 61 | finally: 62 | SESSION.close() 63 | -------------------------------------------------------------------------------- /MissCutie/Database/blacklistusers_sql.py: -------------------------------------------------------------------------------- 1 | import threading 2 | 3 | from MissCutie.Database import BASE, SESSION 4 | from sqlalchemy import Column, String, UnicodeText 5 | 6 | 7 | class BlacklistUsers(BASE): 8 | __tablename__ = "blacklistusers" 9 | user_id = Column(String(14), primary_key=True) 10 | reason = Column(UnicodeText) 11 | 12 | def __init__(self, user_id, reason=None): 13 | self.user_id = user_id 14 | self.reason = reason 15 | 16 | 17 | BlacklistUsers.__table__.create(checkfirst=True) 18 | 19 | BLACKLIST_LOCK = threading.RLock() 20 | BLACKLIST_USERS = set() 21 | 22 | 23 | def blacklist_user(user_id, reason=None): 24 | with BLACKLIST_LOCK: 25 | user = SESSION.query(BlacklistUsers).get(str(user_id)) 26 | if not user: 27 | user = BlacklistUsers(str(user_id), reason) 28 | else: 29 | user.reason = reason 30 | 31 | SESSION.add(user) 32 | SESSION.commit() 33 | __load_blacklist_userid_list() 34 | 35 | 36 | def unblacklist_user(user_id): 37 | with BLACKLIST_LOCK: 38 | if user := SESSION.query(BlacklistUsers).get(str(user_id)): 39 | SESSION.delete(user) 40 | 41 | SESSION.commit() 42 | __load_blacklist_userid_list() 43 | 44 | 45 | def get_reason(user_id): 46 | if user := SESSION.query(BlacklistUsers).get(str(user_id)): 47 | rep = user.reason 48 | else: 49 | rep = "" 50 | SESSION.close() 51 | return rep 52 | 53 | 54 | def is_user_blacklisted(user_id): 55 | return user_id in BLACKLIST_USERS 56 | 57 | 58 | def __load_blacklist_userid_list(): 59 | global BLACKLIST_USERS 60 | try: 61 | BLACKLIST_USERS = {int(x.user_id) for x in SESSION.query(BlacklistUsers).all()} 62 | finally: 63 | SESSION.close() 64 | 65 | 66 | __load_blacklist_userid_list() 67 | -------------------------------------------------------------------------------- /MissCutie/Plugins/get_common_chats.py: -------------------------------------------------------------------------------- 1 | import os 2 | from time import sleep 3 | from MissCutie import OWNER_ID, dispatcher 4 | from MissCutie.Handlers.extraction import extract_user 5 | from MissCutie.Database.users_sql import get_user_com_chats 6 | from telegram import Update 7 | from telegram.error import BadRequest, RetryAfter, Unauthorized 8 | from telegram.ext import CallbackContext, CommandHandler, Filters 9 | from telegram.ext.dispatcher import run_async 10 | 11 | 12 | def get_user_common_chats(update: Update, context: CallbackContext): 13 | bot, args = context.bot, context.args 14 | msg = update.effective_message 15 | user = extract_user(msg, args) 16 | if not user: 17 | msg.reply_text("I share no common chats with the void.") 18 | return 19 | common_list = get_user_com_chats(user) 20 | if not common_list: 21 | msg.reply_text("No common chats with this user!") 22 | return 23 | name = bot.get_chat(user).first_name 24 | text = f"Common chats with {name}\n" 25 | for chat in common_list: 26 | try: 27 | chat_name = bot.get_chat(chat).title 28 | sleep(0.3) 29 | text += f"• {chat_name}\n" 30 | except (BadRequest, Unauthorized): 31 | pass 32 | except RetryAfter as e: 33 | sleep(e.retry_after) 34 | 35 | if len(text) < 4096: 36 | msg.reply_text(text, parse_mode="HTML") 37 | else: 38 | with open("common_chats.txt", "w") as f: 39 | f.write(text) 40 | with open("common_chats.txt", "rb") as f: 41 | msg.reply_document(f) 42 | os.remove("common_chats.txt") 43 | 44 | 45 | COMMON_CHATS_HANDLER = CommandHandler( 46 | "getchats", get_user_common_chats, filters=Filters.user(OWNER_ID), run_async=True 47 | ) 48 | 49 | dispatcher.add_handler(COMMON_CHATS_HANDLER) 50 | -------------------------------------------------------------------------------- /MissCutie/Database/antichannel_sql.py: -------------------------------------------------------------------------------- 1 | import threading 2 | 3 | from sqlalchemy import Boolean 4 | from sqlalchemy.sql.sqltypes import String 5 | from sqlalchemy import Column 6 | 7 | from MissCutie.Database import BASE, SESSION 8 | 9 | 10 | class AntiChannelSettings(BASE): 11 | __tablename__ = "anti_channel_settings" 12 | 13 | chat_id = Column(String(14), primary_key=True) 14 | setting = Column(Boolean, default=False, nullable=False) 15 | 16 | def __init__(self, chat_id: int, disabled: bool): 17 | self.chat_id = str(chat_id) 18 | self.setting = disabled 19 | 20 | def __repr__(self): 21 | return f"" 22 | 23 | 24 | AntiChannelSettings.__table__.create(checkfirst=True) 25 | ANTICHANNEL_SETTING_LOCK = threading.RLock() 26 | 27 | 28 | def enable_antichannel(chat_id: int): 29 | with ANTICHANNEL_SETTING_LOCK: 30 | chat = SESSION.query(AntiChannelSettings).get(str(chat_id)) 31 | if not chat: 32 | chat = AntiChannelSettings(chat_id, True) 33 | 34 | chat.setting = True 35 | SESSION.add(chat) 36 | SESSION.commit() 37 | 38 | 39 | def disable_antichannel(chat_id: int): 40 | with ANTICHANNEL_SETTING_LOCK: 41 | chat = SESSION.query(AntiChannelSettings).get(str(chat_id)) 42 | if not chat: 43 | chat = AntiChannelSettings(chat_id, False) 44 | 45 | chat.setting = False 46 | SESSION.add(chat) 47 | SESSION.commit() 48 | 49 | 50 | def antichannel_status(chat_id: int) -> bool: 51 | with ANTICHANNEL_SETTING_LOCK: 52 | d = SESSION.query(AntiChannelSettings).get(str(chat_id)) 53 | return d.setting if d else False 54 | 55 | 56 | def migrate_chat(old_chat_id, new_chat_id): 57 | with ANTICHANNEL_SETTING_LOCK: 58 | if chat := SESSION.query(AntiChannelSettings).get(str(old_chat_id)): 59 | chat.chat_id = new_chat_id 60 | SESSION.add(chat) 61 | 62 | SESSION.commit() 63 | -------------------------------------------------------------------------------- /MissCutie/Database/userinfo_sql.py: -------------------------------------------------------------------------------- 1 | import threading 2 | 3 | from MissCutie.Database import BASE, SESSION 4 | from sqlalchemy import Column, BigInteger, UnicodeText 5 | 6 | 7 | class UserInfo(BASE): 8 | __tablename__ = "userinfo" 9 | user_id = Column(BigInteger, primary_key=True) 10 | info = Column(UnicodeText) 11 | 12 | def __init__(self, user_id, info): 13 | self.user_id = user_id 14 | self.info = info 15 | 16 | def __repr__(self): 17 | return "" % self.user_id 18 | 19 | 20 | class UserBio(BASE): 21 | __tablename__ = "userbio" 22 | user_id = Column(BigInteger, primary_key=True) 23 | bio = Column(UnicodeText) 24 | 25 | def __init__(self, user_id, bio): 26 | self.user_id = user_id 27 | self.bio = bio 28 | 29 | def __repr__(self): 30 | return "" % self.user_id 31 | 32 | 33 | UserInfo.__table__.create(checkfirst=True) 34 | UserBio.__table__.create(checkfirst=True) 35 | 36 | INSERTION_LOCK = threading.RLock() 37 | 38 | 39 | def get_user_me_info(user_id): 40 | userinfo = SESSION.query(UserInfo).get(user_id) 41 | SESSION.close() 42 | return userinfo.info if userinfo else None 43 | 44 | 45 | def set_user_me_info(user_id, info): 46 | with INSERTION_LOCK: 47 | userinfo = SESSION.query(UserInfo).get(user_id) 48 | if userinfo: 49 | userinfo.info = info 50 | else: 51 | userinfo = UserInfo(user_id, info) 52 | SESSION.add(userinfo) 53 | SESSION.commit() 54 | 55 | 56 | def get_user_bio(user_id): 57 | userbio = SESSION.query(UserBio).get(user_id) 58 | SESSION.close() 59 | return userbio.bio if userbio else None 60 | 61 | 62 | def set_user_bio(user_id, bio): 63 | with INSERTION_LOCK: 64 | userbio = SESSION.query(UserBio).get(user_id) 65 | if userbio: 66 | userbio.bio = bio 67 | else: 68 | userbio = UserBio(user_id, bio) 69 | 70 | SESSION.add(userbio) 71 | SESSION.commit() 72 | -------------------------------------------------------------------------------- /MissCutie/Handlers/pyrogram/admins.py: -------------------------------------------------------------------------------- 1 | from typing import Callable 2 | 3 | from pyrogram.enums import ChatMemberStatus 4 | from pyrogram.types import Message 5 | 6 | from MissCutie import OWNER_ID, DEV_USERS, pbot 7 | 8 | 9 | def can_change_info(func: Callable) -> Callable: 10 | async def non_admin(_, message: Message): 11 | if message.from_user.id in DEV_USERS: 12 | return await func(_, message) 13 | 14 | check = await pbot.get_chat_member(message.chat.id, message.from_user.id) 15 | if check.status not in [ChatMemberStatus.OWNER, ChatMemberStatus.ADMINISTRATOR]: 16 | return await message.reply_text( 17 | "You are not admin" 18 | ) 19 | 20 | admin = ( 21 | await pbot.get_chat_member(message.chat.id, message.from_user.id) 22 | ).privileges 23 | if admin.can_change_info: 24 | return await func(_, message) 25 | else: 26 | return await message.reply_text( 27 | "`You don't have permissions to change group info." 28 | ) 29 | 30 | return non_admin 31 | 32 | 33 | def can_restrict(func: Callable) -> Callable: 34 | async def non_admin(_, message: Message): 35 | if message.from_user.id in OWNER_ID: 36 | return await func(_, message) 37 | 38 | check = await pbot.get_chat_member(message.chat.id, message.from_user.id) 39 | if check.status not in [ChatMemberStatus.OWNER, ChatMemberStatus.ADMINISTRATOR]: 40 | return await message.reply_text( 41 | "You are not admin." 42 | ) 43 | 44 | admin = ( 45 | await pbot.get_chat_member(message.chat.id, message.from_user.id) 46 | ).privileges 47 | if admin.can_restrict_members: 48 | return await func(_, message) 49 | else: 50 | return await message.reply_text( 51 | "`You don't have permissions to restrict users in this chat." 52 | ) 53 | 54 | return non_admin 55 | -------------------------------------------------------------------------------- /MissCutie/Plugins/Tools/wiki.py: -------------------------------------------------------------------------------- 1 | import wikipedia 2 | from MissCutie import dispatcher 3 | from MissCutie.Plugins.disable import DisableAbleCommandHandler 4 | from telegram import ParseMode, Update 5 | from telegram.ext import CallbackContext, run_async 6 | from wikipedia.exceptions import DisambiguationError, PageError 7 | 8 | 9 | def wiki(update: Update, context: CallbackContext): 10 | msg = update.effective_message.reply_to_message or update.effective_message 11 | res = "" 12 | if msg == update.effective_message: 13 | search = msg.text.split(" ", maxsplit=1)[1] 14 | else: 15 | search = msg.text 16 | try: 17 | res = wikipedia.summary(search) 18 | except DisambiguationError as e: 19 | update.message.reply_text( 20 | f"Disambiguated pages found! Adjust your query accordingly.\n{e}", 21 | parse_mode=ParseMode.HTML, 22 | ) 23 | except PageError as e: 24 | update.message.reply_text( 25 | f"{e}", parse_mode=ParseMode.HTML, 26 | ) 27 | if res: 28 | result = f"{search}\n\n" 29 | result += f"{res}\n" 30 | result += f"""Read more...""" 31 | if len(result) > 4000: 32 | with open("result.txt", "w") as f: 33 | f.write(f"{result}\n\nUwU OwO OmO UmU") 34 | with open("result.txt", "rb") as f: 35 | context.bot.send_document( 36 | document=f, 37 | filename=f.name, 38 | reply_to_message_id=update.message.message_id, 39 | chat_id=update.effective_chat.id, 40 | parse_mode=ParseMode.HTML, 41 | ) 42 | else: 43 | update.message.reply_text( 44 | result, parse_mode=ParseMode.HTML, disable_web_page_preview=True, 45 | ) 46 | 47 | 48 | WIKI_HANDLER = DisableAbleCommandHandler("wiki", wiki, run_async=True) 49 | dispatcher.add_handler(WIKI_HANDLER) 50 | -------------------------------------------------------------------------------- /MissCutie/Plugins/User/github.py: -------------------------------------------------------------------------------- 1 | import requests 2 | from MissCutie import dispatcher 3 | from MissCutie.Plugins.disable import DisableAbleCommandHandler 4 | from telegram import Update, ParseMode 5 | from telegram.ext import CallbackContext, run_async 6 | 7 | __mod_name__ = "Github" 8 | 9 | __help__ = """ Get Your Github Profile information by using this Command - 10 | 11 | ‣ `/github saifalisew1508` - will send profile of your github account """ 12 | 13 | def github(update: Update, context: CallbackContext): 14 | bot = context.bot 15 | message = update.effective_message 16 | args = message.text.split(" ", 1) 17 | 18 | if len(args) == 1: 19 | message.reply_text('Provide me Username, Ex - /git Noob-Kittu') 20 | return 21 | username = args[1] 22 | URL = f'https://api.github.com/users/{username}' 23 | with requests.get(URL) as request: 24 | if request.status_code == 404: 25 | return message.reply_text("404") 26 | 27 | result = request.json() 28 | try: 29 | url = result['html_url'] 30 | name = result['name'] 31 | company = result['company'] 32 | bio = result['bio'] 33 | created_at = result['created_at'] 34 | avatar_url = result['avatar_url'] 35 | blog = result['blog'] 36 | location = result['location'] 37 | repositories = result['public_repos'] 38 | followers = result['followers'] 39 | following = result['following'] 40 | caption = f"""**Info Of {name}** 41 | **Username:** `{username}` 42 | **Bio:** `{bio}` 43 | **Profile Link:** [Here]({url}) 44 | **Company:** `{company}` 45 | **Created On:** `{created_at}` 46 | **Repositories:** `{repositories}` 47 | **Blog:** `{blog}` 48 | **Location:** `{location}` 49 | **Followers:** `{followers}` 50 | **Following:** `{following}`""" 51 | except Exception as e: 52 | print(e) 53 | message.reply_photo(photo=avatar_url, caption=caption, parse_mode=ParseMode.MARKDOWN) 54 | 55 | 56 | GIT_HANDLER = DisableAbleCommandHandler("github", github, run_async=True) 57 | dispatcher.add_handler(GIT_HANDLER) 58 | -------------------------------------------------------------------------------- /MissCutie/Plugins/User/paste.py: -------------------------------------------------------------------------------- 1 | import codecs 2 | import os 3 | 4 | import requests 5 | from telegram import InlineKeyboardButton, InlineKeyboardMarkup, ParseMode 6 | 7 | from MissCutie import dispatcher 8 | from MissCutie.Plugins.disable import DisableAbleCommandHandler 9 | 10 | 11 | def paste(update, context): 12 | msg = update.effective_message 13 | if msg.reply_to_message: 14 | if msg.reply_to_message.document: 15 | file = context.bot.get_file(msg.reply_to_message.document) 16 | file.download("file.txt") 17 | text = codecs.open("file.txt", "r+", encoding="utf-8") 18 | paste_text = text.read() 19 | else: 20 | paste_text = msg.reply_to_message.text 21 | else: 22 | msg.reply_text("What am I supposed to do with this?") 23 | return 24 | try: 25 | link = ( 26 | requests.post( 27 | "https://nekobin.com/api/documents", 28 | json={"content": paste_text}, 29 | ) 30 | .json() 31 | .get("result") 32 | .get("key") 33 | ) 34 | text = "**Pasted to Nekobin!!!**" 35 | buttons = [ 36 | [ 37 | InlineKeyboardButton( 38 | text="View Link", url=f"https://nekobin.com/{link}" 39 | ), 40 | InlineKeyboardButton( 41 | text="View Raw", 42 | url=f"https://nekobin.com/raw/{link}", 43 | ), 44 | ] 45 | ] 46 | msg.reply_text( 47 | text, 48 | reply_markup=InlineKeyboardMarkup(buttons), 49 | parse_mode=ParseMode.MARKDOWN, 50 | disable_web_page_preview=True, 51 | ) 52 | os.remove("file.txt") 53 | except Exception as excp: 54 | msg.reply_text(f"Failed. Error: {excp}") 55 | return 56 | 57 | 58 | __help__ = """ 59 | Copy Paste your Text on Nekobin 60 | 61 | × /paste: Saves replied content to nekobin.com and replies with a url 62 | """ 63 | 64 | __mod_name__ = "Paste" 65 | 66 | PASTE_HANDLER = DisableAbleCommandHandler("paste", paste) 67 | 68 | dispatcher.add_handler(PASTE_HANDLER) 69 | -------------------------------------------------------------------------------- /MissCutie/Plugins/Tools/image.py: -------------------------------------------------------------------------------- 1 | import os 2 | import cv2 3 | from io import BytesIO 4 | from MissCutie import dispatcher 5 | from MissCutie.Plugins.disable import DisableAbleCommandHandler 6 | from telegram import Update, ParseMode 7 | from telegram.ext import CallbackContext, run_async 8 | 9 | # Copyright - All Copyrights of this file is belong to kushal 10 | 11 | def sketch(update: Update, context: CallbackContext): 12 | bot = context.bot 13 | chat_id = update.effective_chat.id 14 | message = update.effective_message 15 | try: 16 | if message.reply_to_message and message.reply_to_message.photo: 17 | file_id = message.reply_to_message.photo[-1].file_id 18 | newFile = context.bot.get_file(file_id) 19 | newFile.download("getSketchfile.png") 20 | #reading image 21 | image = cv2.imread("getSketchfile.png") 22 | #converting BGR image to grayscale 23 | gray_image = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) 24 | #image inversion 25 | inverted_image = 255 - gray_image 26 | 27 | blurred = cv2.GaussianBlur(inverted_image, (21, 21), 0) 28 | inverted_blurred = 255 - blurred 29 | pencil_sketch = cv2.divide(gray_image, inverted_blurred, scale=120.0) 30 | 31 | 32 | filename = 'my_sketch.png' 33 | cv2.imwrite(filename, pencil_sketch) 34 | ofile = open(filename, "rb") 35 | bot.send_photo(chat_id, ofile) 36 | if os.path.exists("getSketchfile.png"): 37 | os.remove("getSketchfile.png") 38 | if os.path.exists(filename): 39 | os.remove(filename) 40 | 41 | else: 42 | update.effective_message.reply_text( 43 | "Please reply to an image to make a sketch.", 44 | ) 45 | 46 | except Exception as e: 47 | message.reply_text(f'Error Report @MissCutie_Support, {e}') 48 | 49 | 50 | 51 | __help__ = """ 52 | ‣ `/sktech `*:* Create your image sktech by replying picture 53 | 54 | """ 55 | 56 | __mod_name__ = "Image" 57 | 58 | SKETCH_HANDLER = DisableAbleCommandHandler("sketch", sketch, run_async=True) 59 | dispatcher.add_handler(SKETCH_HANDLER) 60 | -------------------------------------------------------------------------------- /MissCutie/Database/log_channel_sql.py: -------------------------------------------------------------------------------- 1 | import threading 2 | 3 | from MissCutie.Database import BASE, SESSION 4 | from sqlalchemy import Column, String, distinct, func 5 | 6 | 7 | class GroupLogs(BASE): 8 | __tablename__ = "log_channels" 9 | chat_id = Column(String(14), primary_key=True) 10 | log_channel = Column(String(14), nullable=False) 11 | 12 | def __init__(self, chat_id, log_channel): 13 | self.chat_id = str(chat_id) 14 | self.log_channel = str(log_channel) 15 | 16 | 17 | GroupLogs.__table__.create(checkfirst=True) 18 | 19 | LOGS_INSERTION_LOCK = threading.RLock() 20 | 21 | CHANNELS = {} 22 | 23 | 24 | def set_chat_log_channel(chat_id, log_channel): 25 | with LOGS_INSERTION_LOCK: 26 | if res := SESSION.query(GroupLogs).get(str(chat_id)): 27 | res.log_channel = log_channel 28 | else: 29 | res = GroupLogs(chat_id, log_channel) 30 | SESSION.add(res) 31 | 32 | CHANNELS[str(chat_id)] = log_channel 33 | SESSION.commit() 34 | 35 | 36 | def get_chat_log_channel(chat_id): 37 | return CHANNELS.get(str(chat_id)) 38 | 39 | 40 | def stop_chat_logging(chat_id): 41 | with LOGS_INSERTION_LOCK: 42 | if res := SESSION.query(GroupLogs).get(str(chat_id)): 43 | if str(chat_id) in CHANNELS: 44 | del CHANNELS[str(chat_id)] 45 | 46 | log_channel = res.log_channel 47 | SESSION.delete(res) 48 | SESSION.commit() 49 | return log_channel 50 | 51 | 52 | def num_logchannels(): 53 | try: 54 | return SESSION.query(func.count(distinct(GroupLogs.chat_id))).scalar() 55 | finally: 56 | SESSION.close() 57 | 58 | 59 | def migrate_chat(old_chat_id, new_chat_id): 60 | with LOGS_INSERTION_LOCK: 61 | if chat := SESSION.query(GroupLogs).get(str(old_chat_id)): 62 | chat.chat_id = str(new_chat_id) 63 | SESSION.add(chat) 64 | if str(old_chat_id) in CHANNELS: 65 | CHANNELS[str(new_chat_id)] = CHANNELS.get(str(old_chat_id)) 66 | 67 | SESSION.commit() 68 | 69 | 70 | def __load_log_channels(): 71 | global CHANNELS 72 | try: 73 | all_chats = SESSION.query(GroupLogs).all() 74 | CHANNELS = {chat.chat_id: chat.log_channel for chat in all_chats} 75 | finally: 76 | SESSION.close() 77 | 78 | 79 | __load_log_channels() 80 | -------------------------------------------------------------------------------- /MissCutie/Plugins/Tools/gtranslator.py: -------------------------------------------------------------------------------- 1 | from emoji import UNICODE_EMOJI 2 | #from google_trans_new import LANGUAGES, google_translator 3 | from telegram import Update, ParseMode 4 | from telegram.ext import run_async ,CallbackContext 5 | from gpytranslate import SyncTranslator 6 | from MissCutie import dispatcher 7 | from MissCutie.Plugins.disable import DisableAbleCommandHandler 8 | trans = SyncTranslator() 9 | 10 | def totranslate(update: Update, context: CallbackContext) -> None: 11 | message = update.effective_message 12 | reply_msg = message.reply_to_message 13 | if not reply_msg: 14 | message.reply_text( 15 | "Reply to messages or write messages from other languages ​​for translating into the intended language\n\n" 16 | "Example: `/tr en-ja` to translate from English to Japanese\n" 17 | "Or use: `/tr ja` for automatic detection and translating it into japanese.\n" 18 | "See [List of Language Codes](t.me/fateunionupdates/32) for a list of language codes.", 19 | parse_mode="markdown", 20 | disable_web_page_preview=True) 21 | return 22 | if reply_msg.caption: 23 | to_translate = reply_msg.caption 24 | elif reply_msg.text: 25 | to_translate = reply_msg.text 26 | try: 27 | args = message.text.split()[1].lower() 28 | if "//" in args: 29 | source = args.split("//")[0] 30 | dest = args.split("//")[1] 31 | else: 32 | source = trans.detect(to_translate) 33 | dest = args 34 | except IndexError: 35 | source = trans.detect(to_translate) 36 | dest = "en" 37 | translation = trans(to_translate, 38 | sourcelang=source, targetlang=dest) 39 | reply = f"Translated from {source} to {dest}:\n" \ 40 | f"{translation.text}" 41 | 42 | message.reply_text(reply, parse_mode=ParseMode.HTML) 43 | 44 | 45 | __help__ = """ You can translate messages on telegram in a simple way 46 | ‣ `/tr [List of Language Codes]`:- as reply to a long message. 47 | ‣ `/tl [List of Language Codes]`:- as reply to a long message. 48 | """ 49 | __mod_name__ = "Translator" 50 | 51 | TRANSLATE_HANDLER = DisableAbleCommandHandler(["tr", "tl"], totranslate, run_async=True) 52 | 53 | dispatcher.add_handler(TRANSLATE_HANDLER) 54 | 55 | __command_list__ = ["tr", "tl"] 56 | __handlers__ = [TRANSLATE_HANDLER] 57 | 58 | -------------------------------------------------------------------------------- /MissCutie/Plugins/speed_test.py: -------------------------------------------------------------------------------- 1 | import speedtest 2 | from MissCutie import DEV_USERS, dispatcher 3 | from MissCutie.Plugins.disable import DisableAbleCommandHandler 4 | from MissCutie.Handlers.validation import dev_plus 5 | from telegram import InlineKeyboardButton, InlineKeyboardMarkup, ParseMode, Update 6 | from telegram.ext import CallbackContext, CallbackQueryHandler, run_async 7 | 8 | 9 | def convert(speed): 10 | return round(int(speed) / 1048576, 2) 11 | 12 | 13 | @dev_plus 14 | def speedtestxyz(update: Update, context: CallbackContext): 15 | buttons = [ 16 | [ 17 | InlineKeyboardButton("Image", callback_data="speedtest_image"), 18 | InlineKeyboardButton("Text", callback_data="speedtest_text"), 19 | ] 20 | ] 21 | update.effective_message.reply_text( 22 | "Select SpeedTest Mode", reply_markup=InlineKeyboardMarkup(buttons) 23 | ) 24 | 25 | 26 | 27 | def speedtestxyz_callback(update: Update, context: CallbackContext): 28 | query = update.callback_query 29 | 30 | if query.from_user.id in DEV_USERS: 31 | msg = update.effective_message.edit_text("Running a speedtest....") 32 | speed = speedtest.Speedtest() 33 | speed.get_best_server() 34 | speed.download() 35 | speed.upload() 36 | replymsg = "SpeedTest Results:" 37 | 38 | if query.data == "speedtest_image": 39 | speedtest_image = speed.results.share() 40 | update.effective_message.reply_photo( 41 | photo=speedtest_image, caption=replymsg 42 | ) 43 | msg.delete() 44 | 45 | elif query.data == "speedtest_text": 46 | result = speed.results.dict() 47 | replymsg += f"\nDownload: `{convert(result['download'])}Mb/s`\nUpload: `{convert(result['upload'])}Mb/s`\nPing: `{result['ping']}`" 48 | update.effective_message.edit_text(replymsg, parse_mode=ParseMode.MARKDOWN) 49 | else: 50 | query.answer("You are required to join Heroes Association to use this command.") 51 | 52 | 53 | SPEED_TEST_HANDLER = DisableAbleCommandHandler("speedtest", speedtestxyz, run_async=True) 54 | SPEED_TEST_CALLBACKHANDLER = CallbackQueryHandler( 55 | speedtestxyz_callback, pattern="speedtest_.*", run_async=True 56 | ) 57 | 58 | dispatcher.add_handler(SPEED_TEST_HANDLER) 59 | dispatcher.add_handler(SPEED_TEST_CALLBACKHANDLER) 60 | 61 | __mod_name__ = "SpeedTest" 62 | __command_list__ = ["speedtest"] 63 | __handlers__ = [SPEED_TEST_HANDLER, SPEED_TEST_CALLBACKHANDLER] 64 | -------------------------------------------------------------------------------- /MissCutie/Database/afk_sql.py: -------------------------------------------------------------------------------- 1 | import threading 2 | 3 | from datetime import datetime 4 | 5 | from MissCutie.Database import BASE, SESSION 6 | from sqlalchemy import Boolean, Column, BigInteger, UnicodeText, DateTime 7 | 8 | 9 | class AFK(BASE): 10 | __tablename__ = "afk_users" 11 | 12 | user_id = Column(BigInteger, primary_key=True) 13 | is_afk = Column(Boolean) 14 | reason = Column(UnicodeText) 15 | time = Column(DateTime) 16 | 17 | def __init__(self, user_id: int, reason: str = "", is_afk: bool = True): 18 | self.user_id = user_id 19 | self.reason = reason 20 | self.is_afk = is_afk 21 | self.time = datetime.now() 22 | 23 | def __repr__(self): 24 | return f"afk_status for {self.user_id}" 25 | 26 | 27 | AFK.__table__.create(checkfirst=True) 28 | INSERTION_LOCK = threading.RLock() 29 | 30 | AFK_USERS = {} 31 | 32 | 33 | def is_afk(user_id): 34 | return user_id in AFK_USERS 35 | 36 | 37 | def check_afk_status(user_id): 38 | try: 39 | return SESSION.query(AFK).get(user_id) 40 | finally: 41 | SESSION.close() 42 | 43 | 44 | def set_afk(user_id, reason=""): 45 | with INSERTION_LOCK: 46 | curr = SESSION.query(AFK).get(user_id) 47 | if not curr: 48 | curr = AFK(user_id, reason, True) 49 | else: 50 | curr.is_afk = True 51 | 52 | AFK_USERS[user_id] = {"reason": reason, "time": curr.time} 53 | 54 | SESSION.add(curr) 55 | SESSION.commit() 56 | 57 | 58 | def rm_afk(user_id): 59 | with INSERTION_LOCK: 60 | if curr := SESSION.query(AFK).get(user_id): 61 | if user_id in AFK_USERS: # sanity check 62 | del AFK_USERS[user_id] 63 | 64 | SESSION.delete(curr) 65 | SESSION.commit() 66 | return True 67 | 68 | SESSION.close() 69 | return False 70 | 71 | 72 | def toggle_afk(user_id, reason=""): 73 | with INSERTION_LOCK: 74 | curr = SESSION.query(AFK).get(user_id) 75 | if not curr: 76 | curr = AFK(user_id, reason, True) 77 | elif curr.is_afk: 78 | curr.is_afk = False 79 | else: 80 | curr.is_afk = True 81 | SESSION.add(curr) 82 | SESSION.commit() 83 | 84 | 85 | def __load_afk_users(): 86 | global AFK_USERS 87 | try: 88 | all_afk = SESSION.query(AFK).all() 89 | AFK_USERS = { 90 | user.user_id: {"reason": user.reason, "time": user.time} for user in all_afk if user.is_afk 91 | } 92 | finally: 93 | SESSION.close() 94 | 95 | 96 | __load_afk_users() 97 | -------------------------------------------------------------------------------- /MissCutie/Plugins/Tools/text_to_speech.py: -------------------------------------------------------------------------------- 1 | from typing import Optional, List 2 | from gtts import gTTS 3 | import os 4 | import requests 5 | import json 6 | 7 | from telegram import ChatAction 8 | from telegram.ext import run_async 9 | 10 | from MissCutie import dispatcher 11 | from MissCutie.Plugins.disable import DisableAbleCommandHandler 12 | from MissCutie.Handlers.alternate import typing_action, send_action 13 | 14 | @send_action(ChatAction.RECORD_AUDIO) 15 | def gtts(update, context): 16 | msg = update.effective_message 17 | reply = " ".join(context.args) 18 | if not reply: 19 | if msg.reply_to_message: 20 | reply = msg.reply_to_message.text 21 | else: 22 | return msg.reply_text( 23 | "Reply to some message or enter some text to convert it into audio format!" 24 | ) 25 | for x in "\n": 26 | reply = reply.replace(x, "") 27 | try: 28 | tts = gTTS(reply, lang='en', tld='co.in') 29 | tts.save("k.mp3") 30 | with open("k.mp3", "rb") as speech: 31 | msg.reply_audio(speech) 32 | finally: 33 | if os.path.isfile("k.mp3"): 34 | os.remove("k.mp3") 35 | 36 | 37 | # Open API key 38 | API_KEY = "6ae0c3a0-afdc-4532-a810-82ded0054236" 39 | URL = "http://services.gingersoftware.com/Ginger/correct/json/GingerTheText" 40 | 41 | 42 | @typing_action 43 | def spellcheck(update, context): 44 | if update.effective_message.reply_to_message: 45 | msg = update.effective_message.reply_to_message 46 | 47 | params = dict(lang="US", clientVersion="2.0", apiKey=API_KEY, text=msg.text) 48 | 49 | res = requests.get(URL, params=params) 50 | changes = json.loads(res.text).get("LightGingerTheTextResult") 51 | curr_string = "" 52 | prev_end = 0 53 | 54 | for change in changes: 55 | start = change.get("From") 56 | end = change.get("To") + 1 57 | if suggestions := change.get("Suggestions"): 58 | sugg_str = suggestions[0].get("Text") # should look at this list more 59 | curr_string += msg.text[prev_end:start] + sugg_str 60 | prev_end = end 61 | 62 | curr_string += msg.text[prev_end:] 63 | update.effective_message.reply_text(curr_string) 64 | else: 65 | update.effective_message.reply_text( 66 | "Reply to some message to get grammar corrected text!" 67 | ) 68 | 69 | dispatcher.add_handler(DisableAbleCommandHandler("tts", gtts, pass_args=True, run_async=True)) 70 | dispatcher.add_handler(DisableAbleCommandHandler("splcheck", spellcheck, run_async=True)) 71 | 72 | __help__ = """ 73 | ‣ `/tts`: Convert Text in Bot Audio 74 | *Usage*: reply to text or write message with command. Example `/tts hello` 75 | ‣ `/slpcheck`: Check the right spelling of text 76 | """ 77 | __mod_name__ = "Speech Text" 78 | __command_list__ = ["tts"] 79 | -------------------------------------------------------------------------------- /MissCutie/Database/reporting_sql.py: -------------------------------------------------------------------------------- 1 | import threading 2 | from typing import Union 3 | 4 | from MissCutie.Database import BASE, SESSION 5 | from sqlalchemy import Boolean, Column, BigInteger, String 6 | 7 | 8 | class ReportingUserSettings(BASE): 9 | __tablename__ = "user_report_settings" 10 | user_id = Column(BigInteger, primary_key=True) 11 | should_report = Column(Boolean, default=True) 12 | 13 | def __init__(self, user_id): 14 | self.user_id = user_id 15 | 16 | def __repr__(self): 17 | return f"" 18 | 19 | 20 | class ReportingChatSettings(BASE): 21 | __tablename__ = "chat_report_settings" 22 | chat_id = Column(String(14), primary_key=True) 23 | should_report = Column(Boolean, default=True) 24 | 25 | def __init__(self, chat_id): 26 | self.chat_id = str(chat_id) 27 | 28 | def __repr__(self): 29 | return f"" 30 | 31 | 32 | ReportingUserSettings.__table__.create(checkfirst=True) 33 | ReportingChatSettings.__table__.create(checkfirst=True) 34 | 35 | CHAT_LOCK = threading.RLock() 36 | USER_LOCK = threading.RLock() 37 | 38 | 39 | def chat_should_report(chat_id: Union[str, int]) -> bool: 40 | try: 41 | if chat_setting := SESSION.query(ReportingChatSettings).get( 42 | str(chat_id) 43 | ): 44 | return chat_setting.should_report 45 | return False 46 | finally: 47 | SESSION.close() 48 | 49 | 50 | def user_should_report(user_id: int) -> bool: 51 | try: 52 | if user_setting := SESSION.query(ReportingUserSettings).get(user_id): 53 | return user_setting.should_report 54 | return True 55 | finally: 56 | SESSION.close() 57 | 58 | 59 | def set_chat_setting(chat_id: Union[int, str], setting: bool): 60 | with CHAT_LOCK: 61 | chat_setting = SESSION.query(ReportingChatSettings).get(str(chat_id)) 62 | if not chat_setting: 63 | chat_setting = ReportingChatSettings(chat_id) 64 | 65 | chat_setting.should_report = setting 66 | SESSION.add(chat_setting) 67 | SESSION.commit() 68 | 69 | 70 | def set_user_setting(user_id: int, setting: bool): 71 | with USER_LOCK: 72 | user_setting = SESSION.query(ReportingUserSettings).get(user_id) 73 | if not user_setting: 74 | user_setting = ReportingUserSettings(user_id) 75 | 76 | user_setting.should_report = setting 77 | SESSION.add(user_setting) 78 | SESSION.commit() 79 | 80 | 81 | def migrate_chat(old_chat_id, new_chat_id): 82 | with CHAT_LOCK: 83 | chat_notes = ( 84 | SESSION.query(ReportingChatSettings) 85 | .filter(ReportingChatSettings.chat_id == str(old_chat_id)) 86 | .all() 87 | ) 88 | for note in chat_notes: 89 | note.chat_id = str(new_chat_id) 90 | SESSION.commit() 91 | -------------------------------------------------------------------------------- /MissCutie/Plugins/Admin/anti_channel.py: -------------------------------------------------------------------------------- 1 | from telegram.ext.filters import Filters 2 | from telegram import Update, message 3 | from MissCutie import dispatcher 4 | from telegram.ext import CallbackContext, CommandHandler 5 | from telegram.ext.messagehandler import MessageHandler 6 | import html 7 | from MissCutie.Plugins.disable import DisableAbleCommandHandler 8 | from MissCutie.Database.antichannel_sql import antichannel_status, disable_antichannel, enable_antichannel 9 | from MissCutie.Handlers.validation import user_admin 10 | 11 | @user_admin 12 | def set_antichannel(update: Update, context: CallbackContext): 13 | message = update.effective_message 14 | chat = update.effective_chat 15 | args = context.args 16 | if len(args) > 0: 17 | s = args[0].lower() 18 | if s in ["yes", "on"]: 19 | enable_antichannel(chat.id) 20 | message.reply_html(f"Enabled antichannel in {html.escape(chat.title)}") 21 | elif s in ["off", "no"]: 22 | disable_antichannel(chat.id) 23 | message.reply_html(f"Disabled antichannel in {html.escape(chat.title)}") 24 | else: 25 | message.reply_text(f"Unrecognized arguments {s}") 26 | return 27 | message.reply_html( 28 | f"Antichannel setting is currently {antichannel_status(chat.id)} in {html.escape(chat.title)}" 29 | ) 30 | 31 | def eliminate(update: Update, context: CallbackContext): 32 | message = update.effective_message 33 | chat = update.effective_chat 34 | bot = context.bot 35 | if not antichannel_status(chat.id): 36 | return 37 | if message.sender_chat and message.sender_chat.type == "channel" and not message.is_automatic_forward: 38 | message.delete() 39 | sender_chat = message.sender_chat 40 | bot.ban_chat_sender_chat(sender_chat_id=sender_chat.id, chat_id=chat.id) 41 | 42 | dispatcher.add_handler(MessageHandler(filters=Filters.chat_type.groups, callback=eliminate), group=101) 43 | dispatcher.add_handler( 44 | CommandHandler(command="antichannel", callback=set_antichannel, run_async=True, filters=Filters.chat_type.groups), 45 | group=100) 46 | 47 | 48 | __help__ = """ 49 | Through this menu you can set a punishment for users who write in the group masquerading as a channel. 50 | 51 | ℹ️ Telegram allows each user to write to the group by hiding through a channel they own. 52 | 53 | 👮🏻‍♂️ It's not possible to know which user is writing via a channel and if it is an administrator: this block will apply to whoever writes via a channel. 54 | 55 | 🏃🏻 If this option is active, a user who was writing via a channel will only be able to continue writing to the group but only via his real identity and no longer via other channels. 56 | *Commands:* 57 | *Admins only:* 58 | ➢ `/antichannel on`*:* Anti-Channel service on 59 | ➢ `/antichannel off`*:* Anti-Channel service off 60 | """ 61 | 62 | __mod_name__ = "Anti-Channel" 63 | __command_list__ = ["antichannel", "antichannelon", "antichanneloff"] 64 | -------------------------------------------------------------------------------- /MissCutie/Database/disable_sql.py: -------------------------------------------------------------------------------- 1 | import threading 2 | 3 | from MissCutie.Database import BASE, SESSION 4 | from sqlalchemy import Column, String, UnicodeText, distinct, func 5 | 6 | 7 | class Disable(BASE): 8 | __tablename__ = "disabled_commands" 9 | chat_id = Column(String(14), primary_key=True) 10 | command = Column(UnicodeText, primary_key=True) 11 | 12 | def __init__(self, chat_id, command): 13 | self.chat_id = chat_id 14 | self.command = command 15 | 16 | def __repr__(self): 17 | return f"Disabled cmd {self.command} in {self.chat_id}" 18 | 19 | 20 | Disable.__table__.create(checkfirst=True) 21 | DISABLE_INSERTION_LOCK = threading.RLock() 22 | 23 | DISABLED = {} 24 | 25 | 26 | def disable_command(chat_id, disable): 27 | with DISABLE_INSERTION_LOCK: 28 | disabled = SESSION.query(Disable).get((str(chat_id), disable)) 29 | 30 | if not disabled: 31 | DISABLED.setdefault(str(chat_id), set()).add(disable) 32 | 33 | disabled = Disable(str(chat_id), disable) 34 | SESSION.add(disabled) 35 | SESSION.commit() 36 | return True 37 | 38 | SESSION.close() 39 | return False 40 | 41 | 42 | def enable_command(chat_id, enable): 43 | with DISABLE_INSERTION_LOCK: 44 | if disabled := SESSION.query(Disable).get((str(chat_id), enable)): 45 | if enable in DISABLED.get(str(chat_id)): # sanity check 46 | DISABLED.setdefault(str(chat_id), set()).remove(enable) 47 | 48 | SESSION.delete(disabled) 49 | SESSION.commit() 50 | return True 51 | 52 | SESSION.close() 53 | return False 54 | 55 | 56 | def is_command_disabled(chat_id, cmd): 57 | return str(cmd).lower() in DISABLED.get(str(chat_id), set()) 58 | 59 | 60 | def get_all_disabled(chat_id): 61 | return DISABLED.get(str(chat_id), set()) 62 | 63 | 64 | def num_chats(): 65 | try: 66 | return SESSION.query(func.count(distinct(Disable.chat_id))).scalar() 67 | finally: 68 | SESSION.close() 69 | 70 | 71 | def num_disabled(): 72 | try: 73 | return SESSION.query(Disable).count() 74 | finally: 75 | SESSION.close() 76 | 77 | 78 | def migrate_chat(old_chat_id, new_chat_id): 79 | with DISABLE_INSERTION_LOCK: 80 | chats = SESSION.query(Disable).filter(Disable.chat_id == str(old_chat_id)).all() 81 | for chat in chats: 82 | chat.chat_id = str(new_chat_id) 83 | SESSION.add(chat) 84 | 85 | if str(old_chat_id) in DISABLED: 86 | DISABLED[str(new_chat_id)] = DISABLED.get(str(old_chat_id), set()) 87 | 88 | SESSION.commit() 89 | 90 | 91 | def __load_disabled_commands(): 92 | global DISABLED 93 | try: 94 | all_chats = SESSION.query(Disable).all() 95 | for chat in all_chats: 96 | DISABLED.setdefault(chat.chat_id, set()).add(chat.command) 97 | 98 | finally: 99 | SESSION.close() 100 | 101 | 102 | __load_disabled_commands() 103 | -------------------------------------------------------------------------------- /MissCutie/Handlers/telethon/validations.py: -------------------------------------------------------------------------------- 1 | from MissCutie.Handlers.telethon import IMMUNE_USERS, telethn 2 | from MissCutie import REQUESTER 3 | from telethon.tl.types import ChannelParticipantsAdmins 4 | 5 | 6 | async def user_is_ban_protected(user_id: int, message): 7 | status = False 8 | if message.is_private or user_id in (IMMUNE_USERS): 9 | return True 10 | 11 | async for user in telethn.iter_participants( 12 | message.chat_id, filter=ChannelParticipantsAdmins 13 | ): 14 | if user_id == user.id: 15 | status = True 16 | break 17 | return status 18 | 19 | 20 | async def user_is_admin(user_id: int, message): 21 | status = False 22 | if message.is_private: 23 | return True 24 | 25 | async for user in telethn.iter_participants( 26 | message.chat_id, filter=ChannelParticipantsAdmins 27 | ): 28 | if user_id == user.id or user_id in REQUESTER: 29 | status = True 30 | break 31 | return status 32 | 33 | 34 | async def is_user_admin(user_id: int, chat_id): 35 | status = False 36 | async for user in telethn.iter_participants( 37 | chat_id, filter=ChannelParticipantsAdmins 38 | ): 39 | if user_id == user.id or user_id in REQUESTER: 40 | status = True 41 | break 42 | return status 43 | 44 | 45 | async def misscutie_is_admin(chat_id: int): 46 | status = False 47 | misscutie = await telethn.get_me() 48 | async for user in telethn.iter_participants( 49 | chat_id, filter=ChannelParticipantsAdmins 50 | ): 51 | if misscutie.id == user.id: 52 | status = True 53 | break 54 | return status 55 | 56 | 57 | async def is_user_in_chat(chat_id: int, user_id: int): 58 | status = False 59 | async for user in telethn.iter_participants(chat_id): 60 | if user_id == user.id: 61 | status = True 62 | break 63 | return status 64 | 65 | 66 | async def can_change_info(message): 67 | return ( 68 | message.chat.admin_rights.change_info 69 | if message.chat.admin_rights 70 | else False 71 | ) 72 | 73 | 74 | async def can_ban_users(message): 75 | return ( 76 | message.chat.admin_rights.ban_users 77 | if message.chat.admin_rights 78 | else False 79 | ) 80 | 81 | 82 | async def can_pin_messages(message): 83 | return ( 84 | message.chat.admin_rights.pin_messages 85 | if message.chat.admin_rights 86 | else False 87 | ) 88 | 89 | 90 | async def can_invite_users(message): 91 | return ( 92 | message.chat.admin_rights.invite_users 93 | if message.chat.admin_rights 94 | else False 95 | ) 96 | 97 | 98 | async def can_add_admins(message): 99 | return ( 100 | message.chat.admin_rights.add_admins 101 | if message.chat.admin_rights 102 | else False 103 | ) 104 | 105 | 106 | async def can_delete_messages(message): 107 | 108 | if message.is_private: 109 | return True 110 | elif message.chat.admin_rights: 111 | return message.chat.admin_rights.delete_messages 112 | else: 113 | return False 114 | -------------------------------------------------------------------------------- /MissCutie/Plugins/Tools/identity.py: -------------------------------------------------------------------------------- 1 | import os 2 | import wget 3 | import urllib.request 4 | from faker import Faker 5 | import pyaztro 6 | from faker.providers import internet 7 | from MissCutie import dispatcher 8 | from MissCutie.Plugins.disable import DisableAbleCommandHandler 9 | from telegram import Update, ParseMode 10 | from telegram.ext import CallbackContext, run_async 11 | 12 | 13 | def fakeid(update: Update, context: CallbackContext): 14 | message = update.effective_message 15 | dltmsg = message.reply_text("generating fake identity for you...") 16 | fake = Faker() 17 | print("FAKE DETAILS GENERATED\n") 18 | name = str(fake.name()) 19 | fake.add_provider(internet) 20 | address = str(fake.address()) 21 | ip = fake.ipv4_private() 22 | cc = fake.credit_card_full() 23 | email = fake.ascii_free_email() 24 | job = fake.job() 25 | android = fake.android_platform_token() 26 | pc = fake.chrome() 27 | message.reply_text( 28 | f" Fake Information Generated\nName :-{name}\n\nAddress:-{address}\n\nIP ADDRESS:-{ip}\n\ncredit card:-{cc}\n\nEmail Id:-{email}\n\nJob:-{job}\n\nandroid user agent:-{android}\n\nPc user agent:-{pc}", 29 | parse_mode=ParseMode.HTML, 30 | ) 31 | 32 | dltmsg.delete() 33 | 34 | 35 | 36 | 37 | def astro(update: Update, context: CallbackContext): 38 | message = update.effective_message 39 | args = message.text.split(" ", 1) 40 | 41 | if len(args) == 1: 42 | message.reply_text('Please choose your horoscope sign. List of all signs - aries, taurus, gemini, cancer, leo, virgo, libra, scorpio, sagittarius, capricorn, aquarius and pisces.') 43 | return 44 | msg = message.reply_text("Fetching data...") 45 | try: 46 | x = args[1] 47 | horoscope = pyaztro.Aztro(sign=x) 48 | mood = horoscope.mood 49 | lt = horoscope.lucky_time 50 | desc = horoscope.description 51 | col = horoscope.color 52 | com = horoscope.compatibility 53 | ln = horoscope.lucky_number 54 | 55 | result = ( 56 | f"**Horoscope for `{x}`**:\n" 57 | f"**Mood :** `{mood}`\n" 58 | f"**Lucky Time :** `{lt}`\n" 59 | f"**Lucky Color :** `{col}`\n" 60 | f"**Lucky Number :** `{ln}`\n" 61 | f"**Compatibility :** `{com}`\n" 62 | f"**Description :** `{desc}`\n" 63 | ) 64 | 65 | msg.edit_text(result, parse_mode=ParseMode.MARKDOWN) 66 | 67 | except Exception as e: 68 | msg.edit_text(f"Sorry i haven't found anything!\nmaybe you have given a wrong sign name please check help of horoscope.\nError - {e}") 69 | 70 | 71 | 72 | __help__ = """ 73 | ‣ `/hs `: 74 | Usage: it will show horoscope of daily of your sign. 75 | List of all signs - aries, taurus, gemini, cancer, leo, virgo, libra, scorpio, sagittarius, capricorn, aquarius and pisces. 76 | ‣ `/fakeid`: 77 | Usage: it will fake identity for you. 78 | """ 79 | 80 | __mod_name__ = "Identity" 81 | 82 | FAKER_HANDLER = DisableAbleCommandHandler("fakeid", fakeid, run_async=True) 83 | ASTRO_HANDLER = DisableAbleCommandHandler("hs", astro, run_async=True) 84 | dispatcher.add_handler(FAKER_HANDLER) 85 | dispatcher.add_handler(ASTRO_HANDLER) 86 | -------------------------------------------------------------------------------- /MissCutie/Plugins/Tools/telegraph.py: -------------------------------------------------------------------------------- 1 | from MissCutie import telethn as tbot 2 | TMP_DOWNLOAD_DIRECTORY = "./" 3 | from telethon import events 4 | import os 5 | from PIL import Image 6 | from datetime import datetime 7 | from telegraph import Telegraph, upload_file, exceptions 8 | babe = "MissCutie" 9 | telegraph = Telegraph() 10 | r = telegraph.create_account(short_name=babe) 11 | auth_url = r["auth_url"] 12 | 13 | 14 | @tbot.on(events.NewMessage(pattern="^/tg(m|t)(?!\S+)")) 15 | async def tgraph(event): 16 | if event.fwd_from: 17 | return 18 | if event.reply_to_msg_id: 19 | start = datetime.now() 20 | r_message = await event.get_reply_message() 21 | input_str = event.pattern_match.group(1) 22 | optional_title = "" 23 | if input_str == "m": 24 | downloaded_file_name = await tbot.download_media( 25 | r_message, 26 | TMP_DOWNLOAD_DIRECTORY 27 | ) 28 | end = datetime.now() 29 | ms = (end - start).seconds 30 | h = await event.reply(f"Downloaded to {downloaded_file_name} in {ms} seconds.") 31 | if downloaded_file_name.endswith((".webp")): 32 | resize_image(downloaded_file_name) 33 | try: 34 | start = datetime.now() 35 | media_urls = upload_file(downloaded_file_name) 36 | except exceptions.TelegraphException as exc: 37 | await h.edit(f"ERROR: {str(exc)}") 38 | os.remove(downloaded_file_name) 39 | else: 40 | end = datetime.now() 41 | ms_two = (end - start).seconds 42 | os.remove(downloaded_file_name) 43 | await h.edit( 44 | f"Uploaded to https://telegra.ph{media_urls[0]} in {ms + ms_two} seconds.", 45 | link_preview=True, 46 | ) 47 | elif input_str == "t": 48 | user_object = await tbot.get_entity(r_message.sender_id) 49 | title_of_page = user_object.first_name # + " " + user_object.last_name 50 | # apparently, all Users do not have last_name field 51 | if optional_title: 52 | title_of_page = optional_title 53 | page_content = r_message.message 54 | if r_message.media: 55 | if page_content != "": 56 | title_of_page = page_content 57 | downloaded_file_name = await tbot.download_media( 58 | r_message, 59 | TMP_DOWNLOAD_DIRECTORY 60 | ) 61 | m_list = None 62 | with open(downloaded_file_name, "rb") as fd: 63 | m_list = fd.readlines() 64 | for m in m_list: 65 | page_content += m.decode("UTF-8") + "\n" 66 | os.remove(downloaded_file_name) 67 | page_content = page_content.replace("\n", "
") 68 | response = telegraph.create_page( 69 | title_of_page, 70 | html_content=page_content 71 | ) 72 | end = datetime.now() 73 | ms = (end - start).seconds 74 | await event.reply( 75 | f'Pasted to https://telegra.ph/{response["path"]} in {ms} seconds.', 76 | link_preview=True, 77 | ) 78 | else: 79 | await event.reply("Reply to a message to get a permanent telegra.ph link.") 80 | 81 | 82 | def resize_image(image): 83 | im = Image.open(image) 84 | im.save(image, "PNG") 85 | 86 | file_help = os.path.basename(__file__) 87 | file_help = file_help.replace(".py", "") 88 | file_helpo = file_help.replace("_", " ") 89 | 90 | __help__ = """ 91 | ‣ `/tgm`: Get Telegraph Link Of Replied Media 92 | ‣ `/tgt`: Get Telegraph Link of Replied Text 93 | """ 94 | 95 | __mod_name__ = "Telegraph" 96 | 97 | 98 | -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "MissCutieRobot", 3 | "description": "Telegram group management bot.", 4 | "keywords": [ 5 | "telegram", 6 | "anime", 7 | "group", 8 | "manager", 9 | "misscutie" 10 | ], 11 | "repository": "https://github.com/saifalisew1508/MissCutieRobot", 12 | "addons": [ 13 | { 14 | "options": { 15 | "version": "12" 16 | }, 17 | "plan": "heroku-postgresql" 18 | } 19 | ], 20 | "buildpacks": [ 21 | { 22 | "url": "https://github.com/jonathanong/heroku-buildpack-ffmpeg-latest" 23 | }, 24 | { 25 | "url": "heroku/python" 26 | } 27 | ], 28 | "env": { 29 | "TOKEN": { 30 | "description": "Your bot token. Get one from @BotFather duh", 31 | "required": true, 32 | "value": "" 33 | }, 34 | "API_ID": { 35 | "description": "Get API_ID from my.telegram.org, used for telethon based modules.", 36 | "required": true, 37 | "value": "3160248" 38 | }, 39 | "API_HASH": { 40 | "description": "Get API_HASH from my.telegram.org, used for telethon based modules.", 41 | "required": true, 42 | "value": "da866bcad3446898c4d4a20fa10b9d09" 43 | }, 44 | "SQLALCHEMY_DATABASE_URI": { 45 | "description": "Your postgres sql db, empty this field if you dont have one.", 46 | "required": false, 47 | "value": "sqldbtype://username:pw@hostname:port/db_name" 48 | }, 49 | "OWNER_ID": { 50 | "description": "Your user ID as an integer.", 51 | "required": true, 52 | "value": "1697409878" 53 | }, 54 | 55 | "BOT_NAME": { 56 | "description": "Insert below your bot, it will appear everywhere", 57 | "required": true, 58 | "value": "" 59 | }, 60 | "OWNER_USERNAME": { 61 | "description": "Your username without the @", 62 | "value": "PrinceXofficial" 63 | }, 64 | "SUPPORT_CHAT": { 65 | "description": "Your Telegram support group chat username where your users will go and bother you with shit But be like: MyGroupChatUsernameBlah. If this ever points to masha support than consider you made an enemy.", 66 | "required": true, 67 | "value": "MissCutie_Support" 68 | }, 69 | "PHOTO": { 70 | "description": "Your Telegram Bot Image (Add Link Only).", 71 | "required": true, 72 | "value": "https://telegra.ph/file/b749b0e80e82291e85e10.jpg" 73 | }, 74 | "DEV_USERS": { 75 | "description": "ID of users who are Devs of your bot (can use /py etc.). If you are a noob and would come and bother Masha support then keep the current ID's here at they are and add yours.", 76 | "required": false, 77 | "value": "" 78 | }, 79 | "INSPECTOR": { 80 | "description": "A space separated list of user IDs who you want to assign as inspector users.", 81 | "required": false, 82 | "value": "" 83 | }, 84 | "REQUESTER": { 85 | "description": "A space separated list of user IDs who you wanna assign as requester(gban perms only).", 86 | "required": false, 87 | "value": "" 88 | }, 89 | "ENV": { 90 | "description": "Setting this to ANYTHING will enable environment variables. Leave it as it is", 91 | "value": "ANYTHING" 92 | }, 93 | "WEBHOOK": { 94 | "description": "Setting this to ANYTHING will enable webhooks. If you dont know how this works leave it as it is", 95 | "required": false, 96 | "value": "" 97 | }, 98 | "PORT": { 99 | "description": "Port to use for your webhooks. Better leave this as it is on heroku", 100 | "required": false, 101 | "value": "" 102 | }, 103 | "URL": { 104 | "description": "The Heroku App URL :- https://.herokuapp.com/", 105 | "required": false, 106 | "value": "" 107 | }, 108 | "No_LOAD": { 109 | "description": "Dont load these modules cause they shit, space separation", 110 | "required": false, 111 | "value": "rss" 112 | }, 113 | "ALLOW_EXCL": { 114 | "description": "Set this to True if you want ! to be a command prefix along with /. Recommended is True", 115 | "value": "True" 116 | } 117 | 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /MissCutie/Database/antiflood_sql.py: -------------------------------------------------------------------------------- 1 | import threading 2 | 3 | from sqlalchemy import String, Column, BigInteger, UnicodeText 4 | 5 | from MissCutie.Database import SESSION, BASE 6 | 7 | DEF_COUNT = 1 8 | DEF_LIMIT = 0 9 | DEF_OBJ = (None, DEF_COUNT, DEF_LIMIT) 10 | 11 | 12 | class FloodControl(BASE): 13 | __tablename__ = "antiflood" 14 | chat_id = Column(String(14), primary_key=True) 15 | user_id = Column(BigInteger) 16 | count = Column(BigInteger, default=DEF_COUNT) 17 | limit = Column(BigInteger, default=DEF_LIMIT) 18 | 19 | def __init__(self, chat_id): 20 | self.chat_id = str(chat_id) # ensure string 21 | 22 | def __repr__(self): 23 | return f"" 24 | 25 | 26 | class FloodSettings(BASE): 27 | __tablename__ = "antiflood_settings" 28 | chat_id = Column(String(14), primary_key=True) 29 | flood_type = Column(BigInteger, default=1) 30 | value = Column(UnicodeText, default="0") 31 | 32 | def __init__(self, chat_id, flood_type=1, value="0"): 33 | self.chat_id = str(chat_id) 34 | self.flood_type = flood_type 35 | self.value = value 36 | 37 | def __repr__(self): 38 | return f"<{self.chat_id} will executing {self.flood_type} for flood.>" 39 | 40 | 41 | FloodControl.__table__.create(checkfirst=True) 42 | FloodSettings.__table__.create(checkfirst=True) 43 | 44 | INSERTION_FLOOD_LOCK = threading.RLock() 45 | INSERTION_FLOOD_SETTINGS_LOCK = threading.RLock() 46 | 47 | CHAT_FLOOD = {} 48 | 49 | 50 | def set_flood(chat_id, amount): 51 | with INSERTION_FLOOD_LOCK: 52 | flood = SESSION.query(FloodControl).get(str(chat_id)) 53 | if not flood: 54 | flood = FloodControl(str(chat_id)) 55 | 56 | flood.user_id = None 57 | flood.limit = amount 58 | 59 | CHAT_FLOOD[str(chat_id)] = (None, DEF_COUNT, amount) 60 | 61 | SESSION.add(flood) 62 | SESSION.commit() 63 | 64 | 65 | def update_flood(chat_id: str, user_id) -> bool: 66 | if chat_id not in CHAT_FLOOD: 67 | return 68 | curr_user_id, count, limit = CHAT_FLOOD.get(chat_id, DEF_OBJ) 69 | 70 | if limit == 0: # no antiflood 71 | return False 72 | 73 | if user_id != curr_user_id or user_id is None: # other user 74 | CHAT_FLOOD[chat_id] = (user_id, DEF_COUNT, limit) 75 | return False 76 | 77 | count += 1 78 | if count > limit: # too many msgs, kick 79 | CHAT_FLOOD[chat_id] = (None, DEF_COUNT, limit) 80 | return True 81 | 82 | # default -> update 83 | CHAT_FLOOD[chat_id] = (user_id, count, limit) 84 | return False 85 | 86 | 87 | def get_flood_limit(chat_id): 88 | return CHAT_FLOOD.get(str(chat_id), DEF_OBJ)[2] 89 | 90 | 91 | def set_flood_strength(chat_id, flood_type, value): 92 | # for flood_type 93 | # 1 = ban 94 | # 2 = kick 95 | # 3 = mute 96 | # 4 = tban 97 | # 5 = tmute 98 | with INSERTION_FLOOD_SETTINGS_LOCK: 99 | curr_setting = SESSION.query(FloodSettings).get(str(chat_id)) 100 | if not curr_setting: 101 | curr_setting = FloodSettings( 102 | chat_id, flood_type=int(flood_type), value=value, 103 | ) 104 | 105 | curr_setting.flood_type = int(flood_type) 106 | curr_setting.value = str(value) 107 | 108 | SESSION.add(curr_setting) 109 | SESSION.commit() 110 | 111 | 112 | def get_flood_setting(chat_id): 113 | try: 114 | if setting := SESSION.query(FloodSettings).get(str(chat_id)): 115 | return setting.flood_type, setting.value 116 | else: 117 | return 1, "0" 118 | 119 | finally: 120 | SESSION.close() 121 | 122 | 123 | def migrate_chat(old_chat_id, new_chat_id): 124 | with INSERTION_FLOOD_LOCK: 125 | if flood := SESSION.query(FloodControl).get(str(old_chat_id)): 126 | CHAT_FLOOD[str(new_chat_id)] = CHAT_FLOOD.get(str(old_chat_id), DEF_OBJ) 127 | flood.chat_id = str(new_chat_id) 128 | SESSION.commit() 129 | 130 | SESSION.close() 131 | 132 | 133 | def __load_flood_settings(): 134 | global CHAT_FLOOD 135 | try: 136 | all_chats = SESSION.query(FloodControl).all() 137 | CHAT_FLOOD = {chat.chat_id: (None, DEF_COUNT, chat.limit) for chat in all_chats} 138 | finally: 139 | SESSION.close() 140 | 141 | 142 | __load_flood_settings() 143 | -------------------------------------------------------------------------------- /MissCutie/__init__.py: -------------------------------------------------------------------------------- 1 | import logging, os, sys, time 2 | import telegram.ext as tg 3 | from telethon.sessions import MemorySession 4 | from telethon import TelegramClient 5 | from aiohttp import ClientSession 6 | from pyrogram import Client, errors 7 | from pyrogram.errors.exceptions.bad_request_400 import PeerIdInvalid, ChannelInvalid 8 | from pyrogram.types import Chat, User 9 | 10 | 11 | StartTime = time.time() 12 | 13 | 14 | # enable logging 15 | logging.basicConfig( 16 | format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", 17 | handlers=[logging.FileHandler("log.txt"), logging.StreamHandler()], 18 | level=logging.INFO, 19 | ) 20 | 21 | LOGGER = logging.getLogger(__name__) 22 | 23 | 24 | # if version < 3.6, stop bot. 25 | if sys.version_info[0] < 3 or sys.version_info[1] < 6: 26 | LOGGER.error( 27 | "You must have a python version of at least 3.6! Multiple features depend on this. Bot quitting." 28 | ) 29 | quit(1) 30 | 31 | ENV = bool(os.environ.get("ENV", False)) 32 | 33 | if ENV: 34 | TOKEN = os.environ.get("TOKEN", None) 35 | 36 | try: 37 | OWNER_ID = os.environ.get("OWNER_ID", None) 38 | except ValueError: 39 | raise Exception("Your OWNER_ID env variable is not a valid integer.") 40 | try: 41 | INSPECTOR = {int(x) for x in os.environ.get("INSPECTOR", "").split()} 42 | DEV_USERS = {int(x) for x in os.environ.get("DEV_USERS", "").split()} 43 | except ValueError: 44 | raise Exception("Your inspector(sudo) or dev users list does not contain valid integers.") 45 | 46 | try: 47 | REQUESTER = {int(x) for x in os.environ.get("REQUESTER", "").split()} 48 | except ValueError: 49 | raise Exception("Your requester list does not contain valid integers.") 50 | try: 51 | API_ID = int(os.environ.get("API_ID", None)) 52 | except ValueError: 53 | raise Exception("Your API_ID env variable is not a valid integer.") 54 | 55 | try: 56 | API_HASH = os.environ.get("API_HASH", None) 57 | except ValueError: 58 | raise Exception("Please Add Hash Api key to start the bot") 59 | 60 | DB_URI = os.environ.get("DATABASE_URL") 61 | AI_API_KEY = os.environ.get("AI_API_KEY") 62 | PHOTO = os.environ.get("PHOTO") 63 | WORKERS = int(os.environ.get("WORKERS", 8)) 64 | ALLOW_EXCL = os.environ.get('ALLOW_EXCL', False) 65 | ALLOW_CHATS = os.environ.get("ALLOW_CHATS", True) 66 | MESSAGE_DUMP = os.environ.get("MESSAGE_DUMP", None) 67 | OWNER_USERNAME = os.environ.get("OWNER_USERNAME", None) 68 | SUPPORT_CHAT = os.environ.get("SUPPORT_CHAT", None) 69 | EVENT_LOGS = os.environ.get("EVENT_LOGS", None) 70 | JOIN_LOGGER = os.environ.get("JOIN_LOGGER", None) 71 | 72 | WEBHOOK = bool(os.environ.get("WEBHOOK", False)) 73 | CERT_PATH = os.environ.get("CERT_PATH") 74 | URL = os.environ.get("URL", "") # Does not contain token 75 | PORT = int(os.environ.get("PORT", 5000)) 76 | 77 | LOAD = os.environ.get("LOAD", "").split() 78 | NO_LOAD = os.environ.get("NO_LOAD", "translation").split() 79 | 80 | DEL_CMDS = bool(os.environ.get("DEL_CMDS", True)) 81 | INFOPIC = bool(os.environ.get("INFOPIC", False)) 82 | 83 | 84 | 85 | 86 | else: 87 | from MissCutie.config import Development as Config 88 | 89 | try: 90 | OWNER_ID = int(Config.OWNER_ID) 91 | except ValueError: 92 | raise Exception("Your OWNER_ID variable is not a valid integer.") 93 | # telegram bot requered things from telegram org 94 | API_ID = Config.API_ID 95 | API_HASH = Config.API_HASH 96 | TOKEN = Config.TOKEN 97 | DB_URI = Config.SQLALCHEMY_DATABASE_URI 98 | BOT_ID = int(os.environ.get("BOT_ID", None)) 99 | SUPPORT_CHAT = Config.SUPPORT_CHAT 100 | 101 | # WEBHOOK REQUERED THINGS 102 | WORKERS = Config.WORKERS 103 | ALLOW_EXCL = Config.ALLOW_EXCL 104 | WEBHOOK = Config.WEBHOOK 105 | CERT_PATH = Config.CERT_PATH 106 | PORT = Config.PORT 107 | URL = Config.URL 108 | 109 | 110 | updater = tg.Updater(TOKEN, workers=WORKERS, use_context=True) 111 | telethn = TelegramClient(MemorySession(), API_ID, API_HASH) 112 | dispatcher = updater.dispatcher 113 | 114 | 115 | pbot = Client("MissCutie", api_id=API_ID, api_hash=API_HASH, bot_token=TOKEN) 116 | dispatcher = updater.dispatcher 117 | aiohttpsession = ClientSession() 118 | 119 | BOT_ID = dispatcher.bot.id 120 | BOT_NAME = dispatcher.bot.first_name 121 | BOT_USERNAME = dispatcher.bot.username 122 | 123 | app = Client( 124 | ":memory:", 125 | api_id=API_ID, 126 | api_hash=API_HASH, 127 | bot_token=TOKEN, 128 | workers=min(32, os.cpu_count() + 4), 129 | ) 130 | apps = [app] 131 | 132 | 133 | 134 | -------------------------------------------------------------------------------- /MissCutie/Handlers/misc.py: -------------------------------------------------------------------------------- 1 | from math import ceil 2 | from typing import Dict, List 3 | from telegram import InlineKeyboardButton, MAX_MESSAGE_LENGTH, ParseMode, Bot 4 | from MissCutie import NO_LOAD 5 | from telegram.error import TelegramError 6 | 7 | 8 | 9 | class EqInlineKeyboardButton(InlineKeyboardButton): 10 | def __eq__(self, other): 11 | return self.text == other.text 12 | 13 | def __lt__(self, other): 14 | return self.text < other.text 15 | 16 | def __gt__(self, other): 17 | return self.text > other.text 18 | 19 | 20 | def paginate_modules(page_n: int, module_dict: Dict, prefix, chat=None) -> List: 21 | modules = ( 22 | sorted( 23 | [ 24 | EqInlineKeyboardButton( 25 | x.__mod_name__, 26 | callback_data=f"{prefix}_module({chat},{x.__mod_name__.lower()})", 27 | ) 28 | for x in module_dict.values() 29 | ] 30 | ) 31 | if chat 32 | else sorted( 33 | [ 34 | EqInlineKeyboardButton( 35 | x.__mod_name__, 36 | callback_data=f"{prefix}_module({x.__mod_name__.lower()})", 37 | ) 38 | for x in module_dict.values() 39 | ] 40 | ) 41 | ) 42 | pairs = [ 43 | modules[i * 3:(i + 1) * 3] for i in range((len(modules) + 3 - 1) // 3) 44 | ] 45 | 46 | round_num = len(modules) / 3 47 | calc = len(modules) - round(round_num) 48 | if calc in [1, 2]: 49 | pairs.append((modules[-1], )) 50 | max_num_pages = ceil(len(pairs) / 10) 51 | modulo_page = page_n % max_num_pages 52 | 53 | # can only have a certain amount of buttons side by side 54 | if len(pairs) > 10: 55 | pairs = pairs[modulo_page * 10 : 10 * (modulo_page + 1)] + [ 56 | ( 57 | EqInlineKeyboardButton( 58 | "⮜", callback_data=f"{prefix}_prev({modulo_page})" 59 | ), 60 | EqInlineKeyboardButton("Back", callback_data="misscutie_back"), 61 | EqInlineKeyboardButton( 62 | "⮞", callback_data=f"{prefix}_next({modulo_page})" 63 | ), 64 | ) 65 | ] 66 | 67 | else: 68 | pairs += [[EqInlineKeyboardButton("Back", callback_data="misscutie_back")]] 69 | 70 | return pairs 71 | 72 | 73 | def build_keyboard(buttons): 74 | keyb = [] 75 | for btn in buttons: 76 | if btn.same_line and keyb: 77 | keyb[-1].append(InlineKeyboardButton(btn.name, url=btn.url)) 78 | else: 79 | keyb.append([InlineKeyboardButton(btn.name, url=btn.url)]) 80 | 81 | return keyb 82 | 83 | 84 | def revert_buttons(buttons): 85 | return "".join( 86 | f"\n[{btn.name}](buttonurl://{btn.url}:same)" 87 | if btn.same_line 88 | else f"\n[{btn.name}](buttonurl://{btn.url})" 89 | for btn in buttons 90 | ) 91 | 92 | def build_keyboard_parser(bot, chat_id, buttons): 93 | keyb = [] 94 | for btn in buttons: 95 | if btn.url == "{rules}": 96 | btn.url = f"http://t.me/{bot.username}?start={chat_id}" 97 | if btn.same_line and keyb: 98 | keyb[-1].append(InlineKeyboardButton(btn.name, url=btn.url)) 99 | else: 100 | keyb.append([InlineKeyboardButton(btn.name, url=btn.url)]) 101 | 102 | return keyb 103 | 104 | def send_to_list( 105 | bot: Bot, send_to: list, message: str, markdown=False, html=False, 106 | ) -> None: 107 | if html and markdown: 108 | raise Exception("Can only send with either markdown or HTML!") 109 | for user_id in set(send_to): 110 | try: 111 | if markdown: 112 | bot.send_message(user_id, message, parse_mode=ParseMode.MARKDOWN) 113 | elif html: 114 | bot.send_message(user_id, message, parse_mode=ParseMode.HTML) 115 | else: 116 | bot.send_message(user_id, message) 117 | except TelegramError: 118 | pass # ignore users who fail 119 | 120 | def split_message(msg: str) -> List[str]: 121 | if len(msg) < MAX_MESSAGE_LENGTH: 122 | return [msg] 123 | 124 | lines = msg.splitlines(True) 125 | small_msg = "" 126 | result = [] 127 | for line in lines: 128 | if len(small_msg) + len(line) < MAX_MESSAGE_LENGTH: 129 | small_msg += line 130 | else: 131 | result.append(small_msg) 132 | small_msg = line 133 | # Else statement at the end of the for loop, so append the leftover string. 134 | result.append(small_msg) 135 | 136 | return result 137 | 138 | def is_module_loaded(name): 139 | return name not in NO_LOAD 140 | -------------------------------------------------------------------------------- /MissCutie/Plugins/error_handler.py: -------------------------------------------------------------------------------- 1 | import traceback 2 | import json 3 | import requests 4 | import html 5 | import random 6 | import sys 7 | import pretty_errors 8 | import io 9 | from telegram import Update, InlineKeyboardMarkup, InlineKeyboardButton 10 | from telegram.ext import CallbackContext, CommandHandler 11 | from MissCutie import dispatcher, DEV_USERS, OWNER_ID, API_HASH, API_ID 12 | from requests.structures import CaseInsensitiveDict 13 | 14 | pretty_errors.mono() 15 | 16 | 17 | class ErrorsDict(dict): 18 | "A custom dict to store errors and their count" 19 | 20 | def __init__(self, *args, **kwargs): 21 | self.raw = [] 22 | super().__init__(*args, **kwargs) 23 | 24 | def __contains__(self, error): 25 | self.raw.append(error) 26 | error.identifier = "".join(random.choices("ABCDEFGHIJKLMNOPQRSTUVWXYZ", k=5)) 27 | for e in self: 28 | if type(e) is type(error) and e.args == error.args: 29 | self[e] += 1 30 | return True 31 | self[error] = 0 32 | return False 33 | 34 | def __len__(self): 35 | return len(self.raw) 36 | 37 | 38 | errors = ErrorsDict() 39 | 40 | 41 | def error_callback(update: Update, context: CallbackContext): 42 | if not update: 43 | return 44 | if context.error in errors: 45 | return 46 | try: 47 | stringio = io.StringIO() 48 | pretty_errors.output_stderr = stringio 49 | output = pretty_errors.excepthook( 50 | type(context.error), context.error, context.error.__traceback__, 51 | ) 52 | pretty_errors.output_stderr = sys.stderr 53 | pretty_error = stringio.getvalue() 54 | stringio.close() 55 | except: 56 | pretty_error = "Failed to create pretty error." 57 | tb_list = traceback.format_exception( 58 | None, context.error, context.error.__traceback__, 59 | ) 60 | tb = "".join(tb_list) 61 | pretty_message = f'{pretty_error.replace(context.bot.token, "$TOKEN").replace(str(API_ID), "$API_ID").replace(API_HASH, "$API_HASH")}\n-------------------------------------------------------------------------------\nAn exception was raised while handling an update\nUser: {update.effective_user.id}\nChat: {update.effective_chat.title if update.effective_chat else ""} {update.effective_chat.id if update.effective_chat else ""}\nCallback data: {update.callback_query.data if update.callback_query else "None"}\nMessage: {update.effective_message.text if update.effective_message else "No message"}\n\nFull Traceback: {tb}' 62 | data = pretty_message 63 | uri = "https://spaceb.in/api/v1/documents" 64 | cont = {'content': data, 'Extension': 'py'} 65 | data = json.dumps(cont) 66 | 67 | headers = CaseInsensitiveDict() 68 | headers["Content-Type"] = "application/json" 69 | 70 | resp = requests.post(uri, headers=headers, data=data) 71 | result = resp.json()["payload"] 72 | key = result["id"] 73 | e = html.escape(f"{context.error}") 74 | e = e.replace(context.bot.token, "$TOKEN").replace(str(API_ID), "$API_ID").replace(API_HASH, "$API_HASH") #.replace(SPAMWATCH_API, "$SPAMWATCH_API") 75 | if not key: 76 | with open("error.txt", "w+") as f: 77 | f.write(pretty_message) 78 | context.bot.send_document( 79 | OWNER_ID, 80 | open("error.txt", "rb"), 81 | caption=f"#{context.error.identifier}\nAn unknown error occured:\n{e}", 82 | parse_mode="html", 83 | ) 84 | return 85 | url = f"https://spaceb.in/{key}" 86 | context.bot.send_message( 87 | OWNER_ID, 88 | text=f"#{context.error.identifier}\nAn unknown error occured:\n{e}", 89 | reply_markup=InlineKeyboardMarkup( 90 | [[InlineKeyboardButton("SpaceBin", url=url)]], 91 | ), 92 | parse_mode="html", 93 | ) 94 | 95 | 96 | def list_errors(update: Update, context: CallbackContext): 97 | if update.effective_user.id not in DEV_USERS: 98 | return 99 | e = dict(sorted(errors.items(), key=lambda item: item[1], reverse=True)) 100 | msg = "Errors List:\n" 101 | for x, value in e.items(): 102 | msg += f'• {x}: {value} #{x.identifier}\n' 103 | msg += f"{len(errors)} have occurred since startup." 104 | if len(msg) > 4096: 105 | with open("errors_msg.txt", "w+") as f: 106 | f.write(msg) 107 | context.bot.send_document( 108 | update.effective_chat.id, 109 | open("errors_msg.txt", "rb"), 110 | caption='Too many errors have occured..', 111 | parse_mode="html", 112 | ) 113 | 114 | return 115 | update.effective_message.reply_text(msg, parse_mode="html") 116 | 117 | 118 | dispatcher.add_error_handler(error_callback) 119 | dispatcher.add_handler(CommandHandler("errors", list_errors)) 120 | -------------------------------------------------------------------------------- /MissCutie/Database/global_bans_sql.py: -------------------------------------------------------------------------------- 1 | import threading 2 | 3 | from MissCutie.Database import BASE, SESSION 4 | from sqlalchemy import Boolean, Column, BigInteger, String, UnicodeText 5 | 6 | 7 | class GloballyBannedUsers(BASE): 8 | __tablename__ = "gbans" 9 | user_id = Column(BigInteger, primary_key=True) 10 | name = Column(UnicodeText, nullable=False) 11 | reason = Column(UnicodeText) 12 | 13 | def __init__(self, user_id, name, reason=None): 14 | self.user_id = user_id 15 | self.name = name 16 | self.reason = reason 17 | 18 | def __repr__(self): 19 | return f"" 20 | 21 | def to_dict(self): 22 | return { 23 | "user_id": self.user_id, 24 | "name": self.name, 25 | "reason": self.reason 26 | } 27 | 28 | 29 | class GbanSettings(BASE): 30 | __tablename__ = "gban_settings" 31 | chat_id = Column(String(14), primary_key=True) 32 | setting = Column(Boolean, default=True, nullable=False) 33 | 34 | def __init__(self, chat_id, enabled): 35 | self.chat_id = str(chat_id) 36 | self.setting = enabled 37 | 38 | def __repr__(self): 39 | return f"" 40 | 41 | 42 | GloballyBannedUsers.__table__.create(checkfirst=True) 43 | GbanSettings.__table__.create(checkfirst=True) 44 | 45 | GBANNED_USERS_LOCK = threading.RLock() 46 | GBAN_SETTING_LOCK = threading.RLock() 47 | GBANNED_LIST = set() 48 | GBANSTAT_LIST = set() 49 | 50 | 51 | def gban_user(user_id, name, reason=None): 52 | with GBANNED_USERS_LOCK: 53 | user = SESSION.query(GloballyBannedUsers).get(user_id) 54 | if not user: 55 | user = GloballyBannedUsers(user_id, name, reason) 56 | else: 57 | user.name = name 58 | user.reason = reason 59 | 60 | SESSION.merge(user) 61 | SESSION.commit() 62 | __load_gbanned_userid_list() 63 | 64 | 65 | def update_gban_reason(user_id, name, reason=None): 66 | with GBANNED_USERS_LOCK: 67 | user = SESSION.query(GloballyBannedUsers).get(user_id) 68 | if not user: 69 | return None 70 | old_reason = user.reason 71 | user.name = name 72 | user.reason = reason 73 | 74 | SESSION.merge(user) 75 | SESSION.commit() 76 | return old_reason 77 | 78 | 79 | def ungban_user(user_id): 80 | with GBANNED_USERS_LOCK: 81 | if user := SESSION.query(GloballyBannedUsers).get(user_id): 82 | SESSION.delete(user) 83 | 84 | SESSION.commit() 85 | __load_gbanned_userid_list() 86 | 87 | 88 | def is_user_gbanned(user_id): 89 | return user_id in GBANNED_LIST 90 | 91 | 92 | def get_gbanned_user(user_id): 93 | try: 94 | return SESSION.query(GloballyBannedUsers).get(user_id) 95 | finally: 96 | SESSION.close() 97 | 98 | 99 | def get_gban_list(): 100 | try: 101 | return [x.to_dict() for x in SESSION.query(GloballyBannedUsers).all()] 102 | finally: 103 | SESSION.close() 104 | 105 | 106 | def enable_gbans(chat_id): 107 | with GBAN_SETTING_LOCK: 108 | chat = SESSION.query(GbanSettings).get(str(chat_id)) 109 | if not chat: 110 | chat = GbanSettings(chat_id, True) 111 | 112 | chat.setting = True 113 | SESSION.add(chat) 114 | SESSION.commit() 115 | if str(chat_id) in GBANSTAT_LIST: 116 | GBANSTAT_LIST.remove(str(chat_id)) 117 | 118 | 119 | def disable_gbans(chat_id): 120 | with GBAN_SETTING_LOCK: 121 | chat = SESSION.query(GbanSettings).get(str(chat_id)) 122 | if not chat: 123 | chat = GbanSettings(chat_id, False) 124 | 125 | chat.setting = False 126 | SESSION.add(chat) 127 | SESSION.commit() 128 | GBANSTAT_LIST.add(str(chat_id)) 129 | 130 | 131 | def does_chat_gban(chat_id): 132 | return str(chat_id) not in GBANSTAT_LIST 133 | 134 | 135 | def num_gbanned_users(): 136 | return len(GBANNED_LIST) 137 | 138 | 139 | def __load_gbanned_userid_list(): 140 | global GBANNED_LIST 141 | try: 142 | GBANNED_LIST = { 143 | x.user_id for x in SESSION.query(GloballyBannedUsers).all() 144 | } 145 | finally: 146 | SESSION.close() 147 | 148 | 149 | def __load_gban_stat_list(): 150 | global GBANSTAT_LIST 151 | try: 152 | GBANSTAT_LIST = { 153 | x.chat_id 154 | for x in SESSION.query(GbanSettings).all() 155 | if not x.setting 156 | } 157 | finally: 158 | SESSION.close() 159 | 160 | 161 | def migrate_chat(old_chat_id, new_chat_id): 162 | with GBAN_SETTING_LOCK: 163 | if chat := SESSION.query(GbanSettings).get(str(old_chat_id)): 164 | chat.chat_id = new_chat_id 165 | SESSION.add(chat) 166 | 167 | SESSION.commit() 168 | 169 | 170 | # Create in memory userid to avoid disk access 171 | __load_gbanned_userid_list() 172 | __load_gban_stat_list() 173 | -------------------------------------------------------------------------------- /MissCutie/Plugins/Admin/bluser.py: -------------------------------------------------------------------------------- 1 | # Module to blacklist users and prevent them from using commands by @TheRealPhoenix 2 | import html 3 | import MissCutie.Database.blacklistusers_sql as sql 4 | from MissCutie import ( 5 | DEV_USERS, 6 | OWNER_ID, 7 | INSPECTOR, 8 | REQUESTER, 9 | dispatcher, 10 | ) 11 | from MissCutie.Handlers.validation import dev_plus 12 | from MissCutie.Handlers.extraction import ( 13 | extract_user, 14 | extract_user_and_text, 15 | ) 16 | from MissCutie.Plugins.Admin.log_channel import gloggable 17 | from telegram import ParseMode, Update 18 | from telegram.error import BadRequest 19 | from telegram.ext import CallbackContext, CommandHandler, run_async 20 | from telegram.utils.helpers import mention_html 21 | 22 | BLACKLISTWHITELIST = DEV_USERS.union(INSPECTOR).union(REQUESTER).add(OWNER_ID) 23 | BLABLEUSERS = DEV_USERS.add(OWNER_ID) 24 | 25 | 26 | 27 | @dev_plus 28 | @gloggable 29 | def bl_user(update: Update, context: CallbackContext) -> str: 30 | message = update.effective_message 31 | user = update.effective_user 32 | bot, args = context.bot, context.args 33 | user_id, reason = extract_user_and_text(message, args) 34 | 35 | if not user_id: 36 | message.reply_text("I doubt that's a user.") 37 | return "" 38 | 39 | if user_id == bot.id: 40 | message.reply_text("How am I supposed to do my work if I am ignoring myself?") 41 | return "" 42 | 43 | if user_id in BLACKLISTWHITELIST: 44 | message.reply_text("No!\nNoticing Disasters is my job.") 45 | return "" 46 | 47 | try: 48 | target_user = bot.get_chat(user_id) 49 | except BadRequest as excp: 50 | if excp.message != "User not found": 51 | raise 52 | 53 | message.reply_text("I can't seem to find this user.") 54 | return "" 55 | sql.blacklist_user(user_id, reason) 56 | message.reply_text("I shall ignore the existence of this user!") 57 | log_message = ( 58 | f"#BLACKLIST\n" 59 | f"Admin: {mention_html(user.id, html.escape(user.first_name))}\n" 60 | f"User: {mention_html(target_user.id, html.escape(target_user.first_name))}" 61 | ) 62 | if reason: 63 | log_message += f"\nReason: {reason}" 64 | 65 | return log_message 66 | 67 | 68 | 69 | @dev_plus 70 | @gloggable 71 | def unbl_user(update: Update, context: CallbackContext) -> str: 72 | message = update.effective_message 73 | user = update.effective_user 74 | bot, args = context.bot, context.args 75 | user_id = extract_user(message, args) 76 | 77 | if not user_id: 78 | message.reply_text("I doubt that's a user.") 79 | return "" 80 | 81 | if user_id == bot.id: 82 | message.reply_text("I always notice myself.") 83 | return "" 84 | 85 | try: 86 | target_user = bot.get_chat(user_id) 87 | except BadRequest as excp: 88 | if excp.message != "User not found": 89 | raise 90 | 91 | message.reply_text("I can't seem to find this user.") 92 | return "" 93 | if sql.is_user_blacklisted(user_id): 94 | 95 | sql.unblacklist_user(user_id) 96 | message.reply_text("*notices user*") 97 | return f"#UNBLACKLIST\nAdmin: {mention_html(user.id, html.escape(user.first_name))}\nUser: {mention_html(target_user.id, html.escape(target_user.first_name))}" 98 | else: 99 | message.reply_text("I am not ignoring them at all though!") 100 | return "" 101 | 102 | 103 | 104 | @dev_plus 105 | def bl_users(update: Update, context: CallbackContext): 106 | users = [] 107 | bot = context.bot 108 | for each_user in sql.BLACKLIST_USERS: 109 | user = bot.get_chat(each_user) 110 | if reason := sql.get_reason(each_user): 111 | users.append( 112 | f"• {mention_html(user.id, html.escape(user.first_name))} :- {reason}" 113 | ) 114 | else: 115 | users.append(f"• {mention_html(user.id, html.escape(user.first_name))}") 116 | 117 | message = "Blacklisted Users\n" + ( 118 | "\n".join(users) if users else "Noone is being ignored as of yet." 119 | ) 120 | update.effective_message.reply_text(message, parse_mode=ParseMode.HTML) 121 | 122 | 123 | def __user_info__(user_id): 124 | is_blacklisted = sql.is_user_blacklisted(user_id) 125 | 126 | text = "Blacklisted: {}" 127 | if user_id in [777000, 1087968824]: 128 | return "" 129 | if user_id == dispatcher.bot.id: 130 | return "" 131 | if int(user_id) in INSPECTOR: 132 | return "" 133 | if is_blacklisted: 134 | text = text.format("Yes") 135 | if reason := sql.get_reason(user_id): 136 | text += f"\nReason: {reason}" 137 | else: 138 | text = text.format("No") 139 | 140 | return text 141 | 142 | 143 | BL_HANDLER = CommandHandler("ignore", bl_user, run_async=True) 144 | UNBL_HANDLER = CommandHandler("notice", unbl_user, run_async=True) 145 | BLUSERS_HANDLER = CommandHandler("ignoredlist", bl_users, run_async=True) 146 | 147 | dispatcher.add_handler(BL_HANDLER) 148 | dispatcher.add_handler(UNBL_HANDLER) 149 | dispatcher.add_handler(BLUSERS_HANDLER) 150 | 151 | __mod_name__ = "Blacklisting Users" 152 | __handlers__ = [BL_HANDLER, UNBL_HANDLER, BLUSERS_HANDLER] 153 | -------------------------------------------------------------------------------- /MissCutie/Handlers/managers.py: -------------------------------------------------------------------------------- 1 | import MissCutie.Database.blacklistusers_sql as sql 2 | from MissCutie import ALLOW_EXCL 3 | from MissCutie import DEV_USERS, INSPECTOR, REQUESTER 4 | from itertools import chain 5 | from telegram import Update 6 | from telegram.ext import CommandHandler, MessageHandler, RegexHandler, Filters 7 | from pyrate_limiter import ( 8 | BucketFullException, 9 | Duration, 10 | RequestRate, 11 | Limiter, 12 | MemoryListBucket, 13 | ) 14 | 15 | CMD_STARTERS = ("/", "!") if ALLOW_EXCL else ("/", ) 16 | 17 | 18 | class AntiSpam: 19 | def __init__(self): 20 | self.whitelist = set() 21 | 22 | if DEV_USERS: 23 | self.whitelist.update(DEV_USERS) 24 | if INSPECTOR: 25 | self.whitelist.update(INSPECTOR) 26 | if REQUESTER: 27 | self.whitelist.update(REQUESTER) 28 | # Values are HIGHLY experimental, its recommended you pay attention to our commits as we will be adjusting the values over time with what suits best. 29 | Duration.CUSTOM = 15 # Custom duration, 15 seconds 30 | self.sec_limit = RequestRate(6, Duration.CUSTOM) # 6 / Per 15 Seconds 31 | self.min_limit = RequestRate(20, Duration.MINUTE) # 20 / Per minute 32 | self.hour_limit = RequestRate(100, Duration.HOUR) # 100 / Per hour 33 | self.daily_limit = RequestRate(1000, Duration.DAY) # 1000 / Per day 34 | self.limiter = Limiter( 35 | self.sec_limit, 36 | self.min_limit, 37 | self.hour_limit, 38 | self.daily_limit, 39 | bucket_class=MemoryListBucket, 40 | ) 41 | 42 | def check_user(self, user): 43 | """ 44 | Return True if user is to be ignored else False 45 | """ 46 | if user in self.whitelist: 47 | return False 48 | try: 49 | self.limiter.try_acquire(user) 50 | return False 51 | except BucketFullException: 52 | return True 53 | 54 | 55 | SpamChecker = AntiSpam() 56 | MessageHandlerChecker = AntiSpam() 57 | 58 | 59 | class CustomCommandHandler(CommandHandler): 60 | def __init__(self, command, callback, admin_ok=False, allow_edit=False, **kwargs): 61 | super().__init__(command, callback, **kwargs) 62 | 63 | if allow_edit is False: 64 | self.filters &= ~( 65 | Filters.update.edited_message | Filters.update.edited_channel_post 66 | ) 67 | 68 | def check_update(self, update): 69 | if not isinstance(update, Update) or not update.effective_message: 70 | return 71 | message = update.effective_message 72 | 73 | try: 74 | user_id = update.effective_user.id 75 | except: 76 | user_id = None 77 | 78 | if user_id and sql.is_user_blacklisted(user_id): 79 | return False 80 | 81 | if message.text and len(message.text) > 1: 82 | fst_word = message.text.split(None, 1)[0] 83 | if len(fst_word) > 1 and any( 84 | fst_word.startswith(start) for start in CMD_STARTERS 85 | ): 86 | 87 | args = message.text.split()[1:] 88 | command = fst_word[1:].split("@") 89 | command.append(message.bot.username) 90 | if user_id == 1087968824: 91 | user_id = update.effective_chat.id 92 | if ( 93 | command[0].lower() not in self.command 94 | or command[1].lower() != message.bot.username.lower() 95 | ): 96 | return None 97 | if SpamChecker.check_user(user_id): 98 | return None 99 | if filter_result := self.filters(update): 100 | return args, filter_result 101 | else: 102 | return False 103 | 104 | def handle_update(self, update, dispatcher, check_result, context=None): 105 | if context: 106 | self.collect_additional_context(context, update, dispatcher, check_result) 107 | return self.callback(update, context) 108 | else: 109 | optional_args = self.collect_optional_args(dispatcher, update, check_result) 110 | return self.callback(dispatcher.bot, update, **optional_args) 111 | 112 | def collect_additional_context(self, context, update, dispatcher, check_result): 113 | if isinstance(check_result, bool): 114 | context.args = update.effective_message.text.split()[1:] 115 | else: 116 | context.args = check_result[0] 117 | if isinstance(check_result[1], dict): 118 | context.update(check_result[1]) 119 | 120 | 121 | class CustomRegexHandler(RegexHandler): 122 | def __init__(self, pattern, callback, friendly="", **kwargs): 123 | super().__init__(pattern, callback, **kwargs) 124 | 125 | 126 | class CustomMessageHandler(MessageHandler): 127 | def __init__(self, filters, callback, friendly="", allow_edit=False, **kwargs): 128 | super().__init__(filters, callback, **kwargs) 129 | if allow_edit is False: 130 | self.filters &= ~( 131 | Filters.update.edited_message | Filters.update.edited_channel_post 132 | ) 133 | 134 | def check_update(self, update): 135 | if isinstance(update, Update) and update.effective_message: 136 | return self.filters(update) 137 | -------------------------------------------------------------------------------- /MissCutie/Plugins/User/chatbot.py: -------------------------------------------------------------------------------- 1 | import html 2 | import json 3 | import re 4 | from time import sleep 5 | 6 | import requests 7 | from telegram import ( 8 | CallbackQuery, 9 | Chat, 10 | InlineKeyboardButton, 11 | InlineKeyboardMarkup, 12 | ParseMode, 13 | Update, 14 | User, 15 | ) 16 | from telegram.error import BadRequest, RetryAfter, Unauthorized 17 | from telegram.ext import ( 18 | CallbackContext, 19 | CallbackQueryHandler, 20 | CommandHandler, 21 | Filters, 22 | MessageHandler, 23 | run_async, 24 | ) 25 | from telegram.utils.helpers import mention_html 26 | 27 | import MissCutie.Database.chatbot_sql as sql 28 | from MissCutie import dispatcher, BOT_ID, BOT_NAME, BOT_USERNAME 29 | from MissCutie.Handlers.validation import user_admin, user_admin_no_reply 30 | from MissCutie.Handlers.filters import CustomFilters 31 | from MissCutie.Plugins.Admin.log_channel import gloggable 32 | 33 | 34 | @user_admin_no_reply 35 | @gloggable 36 | def fallenrm(update: Update, context: CallbackContext) -> str: 37 | query: Optional[CallbackQuery] = update.callback_query 38 | user: Optional[User] = update.effective_user 39 | if match := re.match(r"rm_chat\((.+?)\)", query.data): 40 | user_id = match[1] 41 | chat: Optional[Chat] = update.effective_chat 42 | if is_fallen := sql.set_fallen(chat.id): 43 | is_fallen = sql.set_fallen(user_id) 44 | return ( 45 | f"{html.escape(chat.title)}:\n" 46 | f"AI_DISABLED\n" 47 | f"Admin : {mention_html(user.id, html.escape(user.first_name))}\n" 48 | ) 49 | else: 50 | update.effective_message.edit_text( 51 | f"{dispatcher.bot.first_name} ᴄʜᴀᴛʙᴏᴛ ᴅɪsᴀʙʟᴇᴅ ʙʏ {mention_html(user.id, user.first_name)}.", 52 | parse_mode=ParseMode.HTML, 53 | ) 54 | 55 | return "" 56 | 57 | 58 | @user_admin_no_reply 59 | @gloggable 60 | def fallenadd(update: Update, context: CallbackContext) -> str: 61 | query: Optional[CallbackQuery] = update.callback_query 62 | user: Optional[User] = update.effective_user 63 | if match := re.match(r"add_chat\((.+?)\)", query.data): 64 | user_id = match[1] 65 | chat: Optional[Chat] = update.effective_chat 66 | if is_fallen := sql.rem_fallen(chat.id): 67 | is_fallen = sql.rem_fallen(user_id) 68 | return ( 69 | f"{html.escape(chat.title)}:\n" 70 | f"AI_ENABLE\n" 71 | f"Admin : {mention_html(user.id, html.escape(user.first_name))}\n" 72 | ) 73 | else: 74 | update.effective_message.edit_text( 75 | f"{dispatcher.bot.first_name} ᴄʜᴀᴛʙᴏᴛ ᴇɴᴀʙʟᴇᴅ ʙʏ {mention_html(user.id, user.first_name)}.", 76 | parse_mode=ParseMode.HTML, 77 | ) 78 | 79 | return "" 80 | 81 | 82 | @user_admin 83 | @gloggable 84 | def fallen(update: Update, context: CallbackContext): 85 | message = update.effective_message 86 | msg = "• ᴄʜᴏᴏsᴇ ᴀɴ ᴏᴩᴛɪᴏɴ ᴛᴏ ᴇɴᴀʙʟᴇ/ᴅɪsᴀʙʟᴇ ᴄʜᴀᴛʙᴏᴛ" 87 | keyboard = InlineKeyboardMarkup( 88 | [ 89 | [ 90 | InlineKeyboardButton(text="ᴇɴᴀʙʟᴇ", callback_data="add_chat({})"), 91 | InlineKeyboardButton(text="ᴅɪsᴀʙʟᴇ", callback_data="rm_chat({})"), 92 | ], 93 | ] 94 | ) 95 | message.reply_text( 96 | text=msg, 97 | reply_markup=keyboard, 98 | parse_mode=ParseMode.HTML, 99 | ) 100 | 101 | 102 | def fallen_message(context: CallbackContext, message): 103 | reply_message = message.reply_to_message 104 | if message.text.lower() == "fallen": 105 | return True 106 | elif BOT_USERNAME in message.text.upper(): 107 | return True 108 | elif reply_message: 109 | if reply_message.from_user.id == BOT_ID: 110 | return True 111 | else: 112 | return False 113 | 114 | 115 | def chatbot(update: Update, context: CallbackContext): 116 | message = update.effective_message 117 | chat_id = update.effective_chat.id 118 | bot = context.bot 119 | if is_fallen := sql.is_fallen(chat_id): 120 | return 121 | 122 | if message.text and not message.document: 123 | if not fallen_message(context, message): 124 | return 125 | bot.send_chat_action(chat_id, action="typing") 126 | url = f"https://kora-api.vercel.app/chatbot/2d94e37d-937f-4d28-9196-bd5552cac68b/{BOT_NAME}/Anonymous/message={message.text}" 127 | request = requests.get(url) 128 | results = json.loads(request.text) 129 | sleep(0.5) 130 | message.reply_text(results["reply"]) 131 | 132 | 133 | __help__ = f""" 134 | *{BOT_NAME} has an chatbot whic provides you a seemingless chatting experience :* 135 | 136 | » /chatbot *:* Shows chatbot control panel 137 | """ 138 | 139 | __mod_name__ = "ChatBot" 140 | 141 | 142 | CHATBOTK_HANDLER = CommandHandler("chatbot", fallen, run_async=True) 143 | ADD_CHAT_HANDLER = CallbackQueryHandler(fallenadd, pattern=r"add_chat", run_async=True) 144 | RM_CHAT_HANDLER = CallbackQueryHandler(fallenrm, pattern=r"rm_chat", run_async=True) 145 | CHATBOT_HANDLER = MessageHandler( 146 | Filters.text 147 | & (~Filters.regex(r"^#[^\s]+") & ~Filters.regex(r"^!") & ~Filters.regex(r"^\/")), 148 | chatbot, 149 | run_async=True, 150 | ) 151 | 152 | dispatcher.add_handler(ADD_CHAT_HANDLER) 153 | dispatcher.add_handler(CHATBOTK_HANDLER) 154 | dispatcher.add_handler(RM_CHAT_HANDLER) 155 | dispatcher.add_handler(CHATBOT_HANDLER) 156 | 157 | __handlers__ = [ 158 | ADD_CHAT_HANDLER, 159 | CHATBOTK_HANDLER, 160 | RM_CHAT_HANDLER, 161 | CHATBOT_HANDLER, 162 | ] 163 | -------------------------------------------------------------------------------- /MissCutie/Handlers/extraction.py: -------------------------------------------------------------------------------- 1 | from typing import List, Optional 2 | 3 | from MissCutie import LOGGER 4 | from MissCutie.Plugins.users import get_user_id 5 | from telegram import Message, MessageEntity 6 | from telegram.error import BadRequest 7 | 8 | 9 | def id_from_reply(message): 10 | prev_message = message.reply_to_message 11 | if not prev_message: 12 | return None, None 13 | user_id = prev_message.from_user.id 14 | res = message.text.split(None, 1) 15 | return (user_id, "") if len(res) < 2 else (user_id, res[1]) 16 | 17 | 18 | def extract_user(message: Message, args: List[str]) -> Optional[int]: 19 | return extract_user_and_text(message, args)[0] 20 | 21 | 22 | def extract_user_and_text( 23 | message: Message, args: List[str] 24 | ) -> (Optional[int], Optional[str]): 25 | prev_message = message.reply_to_message 26 | split_text = message.text.split(None, 1) 27 | 28 | if len(split_text) < 2: 29 | return id_from_reply(message) # only option possible 30 | 31 | text_to_parse = split_text[1] 32 | 33 | text = "" 34 | 35 | entities = list(message.parse_entities([MessageEntity.TEXT_MENTION])) 36 | ent = entities[0] if entities else None 37 | # if entity offset matches (command end/text start) then all good 38 | if entities and ent and ent.offset == len(message.text) - len(text_to_parse): 39 | ent = entities[0] 40 | user_id = ent.user.id 41 | text = message.text[ent.offset + ent.length :] 42 | 43 | elif len(args) >= 1 and args[0][0] == "@": 44 | user = args[0] 45 | user_id = get_user_id(user) 46 | if not user_id: 47 | message.reply_text( 48 | "No idea who this user is. You'll be able to interact with them if " 49 | "you reply to that person's message instead, or forward one of that user's messages." 50 | ) 51 | return None, None 52 | 53 | else: 54 | user_id = user_id 55 | res = message.text.split(None, 2) 56 | if len(res) >= 3: 57 | text = res[2] 58 | 59 | elif len(args) >= 1 and args[0].isdigit(): 60 | user_id = int(args[0]) 61 | res = message.text.split(None, 2) 62 | if len(res) >= 3: 63 | text = res[2] 64 | 65 | elif prev_message: 66 | user_id, text = id_from_reply(message) 67 | 68 | else: 69 | return None, None 70 | 71 | try: 72 | message.bot.get_chat(user_id) 73 | except BadRequest as excp: 74 | if excp.message in ("User_id_invalid", "Chat not found"): 75 | message.reply_text( 76 | "I don't seem to have interacted with this user before - please forward a message from " 77 | "them to give me control! (like a voodoo doll, I need a piece of them to be able " 78 | "to execute certain commands...)" 79 | ) 80 | else: 81 | LOGGER.exception("Exception %s on user %s", excp.message, user_id) 82 | 83 | return None, None 84 | 85 | return user_id, text 86 | 87 | 88 | def extract_text(message) -> str: 89 | return ( 90 | message.text 91 | or message.caption 92 | or (message.sticker.emoji if message.sticker else None) 93 | ) 94 | 95 | 96 | def extract_unt_fedban( 97 | message: Message, args: List[str] 98 | ) -> (Optional[int], Optional[str]): 99 | prev_message = message.reply_to_message 100 | split_text = message.text.split(None, 1) 101 | 102 | if len(split_text) < 2: 103 | return id_from_reply(message) # only option possible 104 | 105 | text_to_parse = split_text[1] 106 | 107 | text = "" 108 | 109 | entities = list(message.parse_entities([MessageEntity.TEXT_MENTION])) 110 | ent = entities[0] if entities else None 111 | # if entity offset matches (command end/text start) then all good 112 | if entities and ent and ent.offset == len(message.text) - len(text_to_parse): 113 | ent = entities[0] 114 | user_id = ent.user.id 115 | text = message.text[ent.offset + ent.length :] 116 | 117 | elif len(args) >= 1 and args[0][0] == "@": 118 | user = args[0] 119 | user_id = get_user_id(user) 120 | if not user_id and not isinstance(user_id, int): 121 | message.reply_text( 122 | "I don't have that user in my db. " 123 | "You'll be able to interact with them if you reply to that person's message instead, or forward one of that user's messages." 124 | ) 125 | return None, None 126 | 127 | else: 128 | user_id = user_id 129 | res = message.text.split(None, 2) 130 | if len(res) >= 3: 131 | text = res[2] 132 | 133 | elif len(args) >= 1 and args[0].isdigit(): 134 | user_id = int(args[0]) 135 | res = message.text.split(None, 2) 136 | if len(res) >= 3: 137 | text = res[2] 138 | 139 | elif prev_message: 140 | user_id, text = id_from_reply(message) 141 | 142 | else: 143 | return None, None 144 | 145 | try: 146 | message.bot.get_chat(user_id) 147 | except BadRequest as excp: 148 | if excp.message in ("User_id_invalid", "Chat not found") and not isinstance( 149 | user_id, int 150 | ): 151 | message.reply_text( 152 | "I don't seem to have interacted with this user before " 153 | "please forward a message from them to give me control! " 154 | "(like a voodoo doll, I need a piece of them to be able to execute certain commands...)" 155 | ) 156 | return None, None 157 | elif excp.message != "Chat not found": 158 | LOGGER.exception("Exception %s on user %s", excp.message, user_id) 159 | return None, None 160 | elif not isinstance(user_id, int): 161 | return None, None 162 | 163 | return user_id, text 164 | 165 | 166 | def extract_user_fban(message: Message, args: List[str]) -> Optional[int]: 167 | return extract_unt_fedban(message, args)[0] 168 | -------------------------------------------------------------------------------- /MissCutie/Database/notes_sql.py: -------------------------------------------------------------------------------- 1 | # Note: chat_id's are stored as strings because the int is too large to be stored in a PSQL database. 2 | import threading 3 | 4 | from MissCutie.Handlers.msg_types import Types 5 | from MissCutie.Database import BASE, SESSION 6 | from sqlalchemy import Boolean, Column, BigInteger, String, UnicodeText, distinct, func 7 | 8 | 9 | class Notes(BASE): 10 | __tablename__ = "notes" 11 | chat_id = Column(String(14), primary_key=True) 12 | name = Column(UnicodeText, primary_key=True) 13 | value = Column(UnicodeText, nullable=False) 14 | file = Column(UnicodeText) 15 | is_reply = Column(Boolean, default=False) 16 | has_buttons = Column(Boolean, default=False) 17 | msgtype = Column(BigInteger, default=Types.BUTTON_TEXT.value) 18 | 19 | def __init__(self, chat_id, name, value, msgtype, file=None): 20 | self.chat_id = str(chat_id) # ensure string 21 | self.name = name 22 | self.value = value 23 | self.msgtype = msgtype 24 | self.file = file 25 | 26 | def __repr__(self): 27 | return f"" 28 | 29 | 30 | class Buttons(BASE): 31 | __tablename__ = "note_urls" 32 | id = Column(BigInteger, primary_key=True, autoincrement=True) 33 | chat_id = Column(String(14), primary_key=True) 34 | note_name = Column(UnicodeText, primary_key=True) 35 | name = Column(UnicodeText, nullable=False) 36 | url = Column(UnicodeText, nullable=False) 37 | same_line = Column(Boolean, default=False) 38 | 39 | def __init__(self, chat_id, note_name, name, url, same_line=False): 40 | self.chat_id = str(chat_id) 41 | self.note_name = note_name 42 | self.name = name 43 | self.url = url 44 | self.same_line = same_line 45 | 46 | 47 | Notes.__table__.create(checkfirst=True) 48 | Buttons.__table__.create(checkfirst=True) 49 | 50 | NOTES_INSERTION_LOCK = threading.RLock() 51 | BUTTONS_INSERTION_LOCK = threading.RLock() 52 | 53 | 54 | def add_note_to_db(chat_id, note_name, note_data, msgtype, buttons=None, file=None): 55 | if not buttons: 56 | buttons = [] 57 | 58 | with NOTES_INSERTION_LOCK: 59 | if prev := SESSION.query(Notes).get((str(chat_id), note_name)): 60 | with BUTTONS_INSERTION_LOCK: 61 | prev_buttons = ( 62 | SESSION.query(Buttons) 63 | .filter( 64 | Buttons.chat_id == str(chat_id), Buttons.note_name == note_name, 65 | ) 66 | .all() 67 | ) 68 | for btn in prev_buttons: 69 | SESSION.delete(btn) 70 | SESSION.delete(prev) 71 | note = Notes( 72 | str(chat_id), note_name, note_data or "", msgtype=msgtype.value, file=file, 73 | ) 74 | SESSION.add(note) 75 | SESSION.commit() 76 | 77 | for b_name, url, same_line in buttons: 78 | add_note_button_to_db(chat_id, note_name, b_name, url, same_line) 79 | 80 | 81 | def get_note(chat_id, note_name): 82 | try: 83 | return ( 84 | SESSION.query(Notes) 85 | .filter(func.lower(Notes.name) == note_name, Notes.chat_id == str(chat_id)) 86 | .first() 87 | ) 88 | finally: 89 | SESSION.close() 90 | 91 | 92 | def rm_note(chat_id, note_name): 93 | with NOTES_INSERTION_LOCK: 94 | if note := ( 95 | SESSION.query(Notes) 96 | .filter( 97 | func.lower(Notes.name) == note_name, 98 | Notes.chat_id == str(chat_id), 99 | ) 100 | .first() 101 | ): 102 | with BUTTONS_INSERTION_LOCK: 103 | buttons = ( 104 | SESSION.query(Buttons) 105 | .filter( 106 | Buttons.chat_id == str(chat_id), Buttons.note_name == note_name, 107 | ) 108 | .all() 109 | ) 110 | for btn in buttons: 111 | SESSION.delete(btn) 112 | 113 | SESSION.delete(note) 114 | SESSION.commit() 115 | return True 116 | 117 | else: 118 | SESSION.close() 119 | return False 120 | 121 | 122 | def get_all_chat_notes(chat_id): 123 | try: 124 | return ( 125 | SESSION.query(Notes) 126 | .filter(Notes.chat_id == str(chat_id)) 127 | .order_by(Notes.name.asc()) 128 | .all() 129 | ) 130 | finally: 131 | SESSION.close() 132 | 133 | 134 | def add_note_button_to_db(chat_id, note_name, b_name, url, same_line): 135 | with BUTTONS_INSERTION_LOCK: 136 | button = Buttons(chat_id, note_name, b_name, url, same_line) 137 | SESSION.add(button) 138 | SESSION.commit() 139 | 140 | 141 | def get_buttons(chat_id, note_name): 142 | try: 143 | return ( 144 | SESSION.query(Buttons) 145 | .filter(Buttons.chat_id == str(chat_id), Buttons.note_name == note_name) 146 | .order_by(Buttons.id) 147 | .all() 148 | ) 149 | finally: 150 | SESSION.close() 151 | 152 | 153 | def num_notes(): 154 | try: 155 | return SESSION.query(Notes).count() 156 | finally: 157 | SESSION.close() 158 | 159 | 160 | def num_chats(): 161 | try: 162 | return SESSION.query(func.count(distinct(Notes.chat_id))).scalar() 163 | finally: 164 | SESSION.close() 165 | 166 | 167 | def migrate_chat(old_chat_id, new_chat_id): 168 | with NOTES_INSERTION_LOCK: 169 | chat_notes = ( 170 | SESSION.query(Notes).filter(Notes.chat_id == str(old_chat_id)).all() 171 | ) 172 | for note in chat_notes: 173 | note.chat_id = str(new_chat_id) 174 | 175 | with BUTTONS_INSERTION_LOCK: 176 | chat_buttons = ( 177 | SESSION.query(Buttons).filter(Buttons.chat_id == str(old_chat_id)).all() 178 | ) 179 | for btn in chat_buttons: 180 | btn.chat_id = str(new_chat_id) 181 | 182 | SESSION.commit() 183 | -------------------------------------------------------------------------------- /MissCutie/Plugins/Admin/rules.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | 3 | import MissCutie.Database.rules_sql as sql 4 | from MissCutie import dispatcher 5 | from MissCutie.Handlers.validation import user_admin, connection_status 6 | from MissCutie.Handlers.string_handling import markdown_parser 7 | from telegram import ( 8 | InlineKeyboardButton, 9 | InlineKeyboardMarkup, 10 | Message, 11 | ParseMode, 12 | Update, 13 | User, 14 | ) 15 | from telegram.error import BadRequest 16 | from telegram.ext import CallbackContext, CommandHandler, run_async 17 | from telegram.utils.helpers import escape_markdown 18 | 19 | 20 | @connection_status 21 | def get_rules(update: Update, context: CallbackContext): 22 | args = context.args 23 | here = args and args[0] == 'here' 24 | chat_id = update.effective_chat.id 25 | # connection_status sets update.effective_chat 26 | real_chat = update.effective_message.chat 27 | dest_chat = real_chat.id if here else None 28 | send_rules(update, chat_id, real_chat.type == real_chat.PRIVATE or here, dest_chat) 29 | 30 | 31 | # Do not async - not from a handler 32 | def send_rules(update, chat_id, from_pm=False, dest_chat=None): 33 | bot = dispatcher.bot 34 | user = update.effective_user # type: Optional[User] 35 | reply_msg = update.message.reply_to_message 36 | dest_chat = dest_chat or user.id 37 | try: 38 | chat = bot.get_chat(chat_id) 39 | except BadRequest as excp: 40 | if excp.message != "Chat not found" or not from_pm: 41 | raise 42 | 43 | bot.send_message( 44 | dest_chat, 45 | "The rules shortcut for this chat hasn't been set properly! Ask admins to " 46 | "fix this.\nMaybe they forgot the hyphen in ID", 47 | ) 48 | return 49 | rules = sql.get_rules(chat_id) 50 | text = f"The rules for *{escape_markdown(chat.title)}* are:\n\n{rules}" 51 | 52 | if from_pm and rules: 53 | bot.send_message( 54 | dest_chat, text, parse_mode=ParseMode.MARKDOWN, disable_web_page_preview=True, 55 | ) 56 | elif from_pm: 57 | bot.send_message( 58 | dest_chat, 59 | "The group admins haven't set any rules for this chat yet. " 60 | "This probably doesn't mean it's lawless though...!", 61 | ) 62 | elif rules and reply_msg: 63 | reply_msg.reply_text( 64 | "Please click the button below to see the rules.", 65 | reply_markup=InlineKeyboardMarkup( 66 | [ 67 | [ 68 | InlineKeyboardButton( 69 | text="Rules", url=f"t.me/{bot.username}?start={chat_id}", 70 | ), 71 | ], 72 | ], 73 | ), 74 | ) 75 | elif rules: 76 | update.effective_message.reply_text( 77 | "Please click the button below to see the rules.", 78 | reply_markup=InlineKeyboardMarkup( 79 | [ 80 | [ 81 | InlineKeyboardButton( 82 | text="Rules", url=f"t.me/{bot.username}?start={chat_id}", 83 | ), 84 | ], 85 | ], 86 | ), 87 | ) 88 | else: 89 | update.effective_message.reply_text( 90 | "The group admins haven't set any rules for this chat yet. " 91 | "This probably doesn't mean it's lawless though...!", 92 | ) 93 | 94 | 95 | @connection_status 96 | @user_admin 97 | def set_rules(update: Update, context: CallbackContext): 98 | chat_id = update.effective_chat.id 99 | msg = update.effective_message # type: Optional[Message] 100 | raw_text = msg.text 101 | args = raw_text.split(None, 1) # use python's maxsplit to separate cmd and args 102 | txt = entities = None 103 | if len(args) == 2: 104 | txt = args[1] 105 | entities = msg.parse_entities() 106 | elif msg.reply_to_message: 107 | txt = msg.reply_to_message.text 108 | raw_txt = txt 109 | entities = msg.reply_to_message.parse_entities() 110 | if txt: 111 | offset = len(txt) - len(raw_text) # set correct offset relative to command 112 | markdown_rules = markdown_parser( 113 | txt, entities=entities, offset=offset, 114 | ) 115 | 116 | sql.set_rules(chat_id, markdown_rules) 117 | update.effective_message.reply_text("Successfully set rules for this group.") 118 | else: 119 | update.effective_message.reply_text("There's... no rules?") 120 | 121 | 122 | @connection_status 123 | @user_admin 124 | def clear_rules(update: Update, context: CallbackContext): 125 | chat_id = update.effective_chat.id 126 | sql.set_rules(chat_id, "") 127 | update.effective_message.reply_text("Successfully cleared rules!") 128 | 129 | 130 | def __stats__(): 131 | return f"• {sql.num_chats()} groups have rules." 132 | 133 | 134 | def __import_data__(chat_id, data): 135 | # set chat rules 136 | rules = data.get("info", {}).get("rules", "") 137 | sql.set_rules(chat_id, rules) 138 | 139 | 140 | def __migrate__(old_chat_id, new_chat_id): 141 | sql.migrate_chat(old_chat_id, new_chat_id) 142 | 143 | 144 | def __chat_settings__(chat_id, user_id): 145 | return f"This chat has had it's rules set: `{bool(sql.get_rules(chat_id))}`" 146 | 147 | 148 | __help__ = """ 149 | ‣ `/rules`*:* get the rules for this chat. 150 | ‣ `/rules here`*:* get the rules for this chat but send it in the chat. 151 | *Admins only:* 152 | ‣ `/setrules `*:* set the rules for this chat. 153 | ‣ `/clearrules`*:* clear the rules for this chat. 154 | """ 155 | 156 | __mod_name__ = "Rules" 157 | 158 | GET_RULES_HANDLER = CommandHandler("rules", get_rules, run_async=True) 159 | SET_RULES_HANDLER = CommandHandler("setrules", set_rules, run_async=True) 160 | RESET_RULES_HANDLER = CommandHandler("clearrules", clear_rules, run_async=True) 161 | 162 | dispatcher.add_handler(GET_RULES_HANDLER) 163 | dispatcher.add_handler(SET_RULES_HANDLER) 164 | dispatcher.add_handler(RESET_RULES_HANDLER) 165 | -------------------------------------------------------------------------------- /MissCutie/Database/blacklist_sql.py: -------------------------------------------------------------------------------- 1 | import threading 2 | 3 | from sqlalchemy import func, distinct, Column, String, UnicodeText, BigInteger 4 | 5 | from MissCutie.Database import SESSION, BASE 6 | 7 | 8 | class BlackListFilters(BASE): 9 | __tablename__ = "blacklist" 10 | chat_id = Column(String(14), primary_key=True) 11 | trigger = Column(UnicodeText, primary_key=True, nullable=False) 12 | 13 | def __init__(self, chat_id, trigger): 14 | self.chat_id = str(chat_id) # ensure string 15 | self.trigger = trigger 16 | 17 | def __repr__(self): 18 | return f"" 19 | 20 | def __eq__(self, other): 21 | return ( 22 | isinstance(other, BlackListFilters) 23 | and self.chat_id == other.chat_id 24 | and self.trigger == other.trigger 25 | ) 26 | 27 | 28 | class BlacklistSettings(BASE): 29 | __tablename__ = "blacklist_settings" 30 | chat_id = Column(String(14), primary_key=True) 31 | blacklist_type = Column(BigInteger, default=1) 32 | value = Column(UnicodeText, default="0") 33 | 34 | def __init__(self, chat_id, blacklist_type=1, value="0"): 35 | self.chat_id = str(chat_id) 36 | self.blacklist_type = blacklist_type 37 | self.value = value 38 | 39 | def __repr__(self): 40 | return f"<{self.chat_id} will executing {self.blacklist_type} for blacklist trigger.>" 41 | 42 | 43 | BlackListFilters.__table__.create(checkfirst=True) 44 | BlacklistSettings.__table__.create(checkfirst=True) 45 | 46 | BLACKLIST_FILTER_INSERTION_LOCK = threading.RLock() 47 | BLACKLIST_SETTINGS_INSERTION_LOCK = threading.RLock() 48 | 49 | CHAT_BLACKLISTS = {} 50 | CHAT_SETTINGS_BLACKLISTS = {} 51 | 52 | 53 | def add_to_blacklist(chat_id, trigger): 54 | with BLACKLIST_FILTER_INSERTION_LOCK: 55 | blacklist_filt = BlackListFilters(str(chat_id), trigger) 56 | 57 | SESSION.merge(blacklist_filt) # merge to avoid duplicate key issues 58 | SESSION.commit() 59 | global CHAT_BLACKLISTS 60 | if CHAT_BLACKLISTS.get(str(chat_id), set()) == set(): 61 | CHAT_BLACKLISTS[str(chat_id)] = {trigger} 62 | else: 63 | CHAT_BLACKLISTS.get(str(chat_id), set()).add(trigger) 64 | 65 | 66 | def rm_from_blacklist(chat_id, trigger): 67 | with BLACKLIST_FILTER_INSERTION_LOCK: 68 | if blacklist_filt := SESSION.query(BlackListFilters).get( 69 | (str(chat_id), trigger) 70 | ): 71 | if trigger in CHAT_BLACKLISTS.get(str(chat_id), set()): # sanity check 72 | CHAT_BLACKLISTS.get(str(chat_id), set()).remove(trigger) 73 | 74 | SESSION.delete(blacklist_filt) 75 | SESSION.commit() 76 | return True 77 | 78 | SESSION.close() 79 | return False 80 | 81 | 82 | def get_chat_blacklist(chat_id): 83 | return CHAT_BLACKLISTS.get(str(chat_id), set()) 84 | 85 | 86 | def num_blacklist_filters(): 87 | try: 88 | return SESSION.query(BlackListFilters).count() 89 | finally: 90 | SESSION.close() 91 | 92 | 93 | def num_blacklist_chat_filters(chat_id): 94 | try: 95 | return ( 96 | SESSION.query(BlackListFilters.chat_id) 97 | .filter(BlackListFilters.chat_id == str(chat_id)) 98 | .count() 99 | ) 100 | finally: 101 | SESSION.close() 102 | 103 | 104 | def num_blacklist_filter_chats(): 105 | try: 106 | return SESSION.query(func.count(distinct(BlackListFilters.chat_id))).scalar() 107 | finally: 108 | SESSION.close() 109 | 110 | 111 | def set_blacklist_strength(chat_id, blacklist_type, value): 112 | # for blacklist_type 113 | # 0 = nothing 114 | # 1 = delete 115 | # 2 = warn 116 | # 3 = mute 117 | # 4 = kick 118 | # 5 = ban 119 | # 6 = tban 120 | # 7 = tmute 121 | with BLACKLIST_SETTINGS_INSERTION_LOCK: 122 | global CHAT_SETTINGS_BLACKLISTS 123 | curr_setting = SESSION.query(BlacklistSettings).get(str(chat_id)) 124 | if not curr_setting: 125 | curr_setting = BlacklistSettings( 126 | chat_id, blacklist_type=int(blacklist_type), value=value 127 | ) 128 | 129 | curr_setting.blacklist_type = int(blacklist_type) 130 | curr_setting.value = str(value) 131 | CHAT_SETTINGS_BLACKLISTS[str(chat_id)] = { 132 | "blacklist_type": int(blacklist_type), 133 | "value": value, 134 | } 135 | 136 | SESSION.add(curr_setting) 137 | SESSION.commit() 138 | 139 | 140 | def get_blacklist_setting(chat_id): 141 | try: 142 | if setting := CHAT_SETTINGS_BLACKLISTS.get(str(chat_id)): 143 | return setting["blacklist_type"], setting["value"] 144 | else: 145 | return 1, "0" 146 | 147 | finally: 148 | SESSION.close() 149 | 150 | 151 | def __load_chat_blacklists(): 152 | global CHAT_BLACKLISTS 153 | try: 154 | chats = SESSION.query(BlackListFilters.chat_id).distinct().all() 155 | for (chat_id,) in chats: # remove tuple by ( ,) 156 | CHAT_BLACKLISTS[chat_id] = [] 157 | 158 | all_filters = SESSION.query(BlackListFilters).all() 159 | for x in all_filters: 160 | CHAT_BLACKLISTS[x.chat_id] += [x.trigger] 161 | 162 | CHAT_BLACKLISTS = {x: set(y) for x, y in CHAT_BLACKLISTS.items()} 163 | 164 | finally: 165 | SESSION.close() 166 | 167 | 168 | def __load_chat_settings_blacklists(): 169 | global CHAT_SETTINGS_BLACKLISTS 170 | try: 171 | chats_settings = SESSION.query(BlacklistSettings).all() 172 | for x in chats_settings: # remove tuple by ( ,) 173 | CHAT_SETTINGS_BLACKLISTS[x.chat_id] = { 174 | "blacklist_type": x.blacklist_type, 175 | "value": x.value, 176 | } 177 | 178 | finally: 179 | SESSION.close() 180 | 181 | 182 | def migrate_chat(old_chat_id, new_chat_id): 183 | with BLACKLIST_FILTER_INSERTION_LOCK: 184 | chat_filters = ( 185 | SESSION.query(BlackListFilters) 186 | .filter(BlackListFilters.chat_id == str(old_chat_id)) 187 | .all() 188 | ) 189 | for filt in chat_filters: 190 | filt.chat_id = str(new_chat_id) 191 | SESSION.commit() 192 | 193 | 194 | __load_chat_blacklists() 195 | __load_chat_settings_blacklists() 196 | -------------------------------------------------------------------------------- /MissCutie/Plugins/users.py: -------------------------------------------------------------------------------- 1 | from io import BytesIO 2 | from time import sleep 3 | 4 | from telegram import TelegramError, Update 5 | from telegram.error import BadRequest, Unauthorized 6 | from telegram.ext import ( 7 | CallbackContext, 8 | CommandHandler, 9 | Filters, 10 | MessageHandler, 11 | run_async, 12 | ) 13 | 14 | import MissCutie.Database.users_sql as sql 15 | from MissCutie import DEV_USERS, LOGGER, OWNER_ID, dispatcher 16 | from MissCutie.Handlers.validation import dev_plus, sudo_plus 17 | from MissCutie.Database.users_sql import get_all_users 18 | 19 | USERS_GROUP = 4 20 | CHAT_GROUP = 5 21 | DEV_AND_MORE = DEV_USERS.add(int(OWNER_ID)) 22 | 23 | 24 | def get_user_id(username): 25 | # ensure valid userid 26 | if len(username) <= 5: 27 | return None 28 | 29 | if username.startswith("@"): 30 | username = username[1:] 31 | 32 | users = sql.get_userid_by_name(username) 33 | 34 | if not users: 35 | return None 36 | 37 | elif len(users) == 1: 38 | return users[0].user_id 39 | 40 | else: 41 | for user_obj in users: 42 | try: 43 | userdat = dispatcher.bot.get_chat(user_obj.user_id) 44 | if userdat.username == username: 45 | return userdat.id 46 | 47 | except BadRequest as excp: 48 | if excp.message != "Chat not found": 49 | LOGGER.exception("Error extracting user ID") 50 | 51 | return None 52 | 53 | 54 | 55 | @dev_plus 56 | def broadcast(update: Update, context: CallbackContext): 57 | to_send = update.effective_message.text.split(None, 1) 58 | 59 | if len(to_send) >= 2: 60 | to_group = False 61 | to_user = False 62 | if to_send[0] == "/broadcastgroups": 63 | to_group = True 64 | if to_send[0] == "/broadcastusers": 65 | to_user = True 66 | else: 67 | to_group = to_user = True 68 | chats = sql.get_all_chats() or [] 69 | users = get_all_users() 70 | failed = 0 71 | failed_user = 0 72 | if to_group: 73 | for chat in chats: 74 | try: 75 | context.bot.sendMessage( 76 | int(chat.chat_id), 77 | to_send[1], 78 | parse_mode="MARKDOWN", 79 | disable_web_page_preview=True, 80 | ) 81 | sleep(0.1) 82 | except TelegramError: 83 | failed += 1 84 | if to_user: 85 | for user in users: 86 | try: 87 | context.bot.sendMessage( 88 | int(user.user_id), 89 | to_send[1], 90 | parse_mode="MARKDOWN", 91 | disable_web_page_preview=True, 92 | ) 93 | sleep(0.1) 94 | except TelegramError: 95 | failed_user += 1 96 | update.effective_message.reply_text( 97 | f"Broadcast complete.\nGroups failed: {failed}.\nUsers failed: {failed_user}." 98 | ) 99 | 100 | 101 | 102 | def log_user(update: Update, context: CallbackContext): 103 | chat = update.effective_chat 104 | msg = update.effective_message 105 | 106 | sql.update_user(msg.from_user.id, msg.from_user.username, chat.id, chat.title) 107 | 108 | if msg.reply_to_message: 109 | sql.update_user( 110 | msg.reply_to_message.from_user.id, 111 | msg.reply_to_message.from_user.username, 112 | chat.id, 113 | chat.title, 114 | ) 115 | 116 | if msg.forward_from: 117 | sql.update_user(msg.forward_from.id, msg.forward_from.username) 118 | 119 | 120 | 121 | @sudo_plus 122 | def chats(update: Update, context: CallbackContext): 123 | all_chats = sql.get_all_chats() or [] 124 | chatfile = "List of chats.\n0. Chat name | Chat ID | Members count\n" 125 | P = 1 126 | for chat in all_chats: 127 | try: 128 | curr_chat = context.bot.getChat(chat.chat_id) 129 | bot_member = curr_chat.get_member(context.bot.id) 130 | chat_members = curr_chat.get_member_count(context.bot.id) 131 | chatfile += f"{P}. {chat.chat_name} | {chat.chat_id} | {chat_members}\n" 132 | P = P + 1 133 | except: 134 | pass 135 | 136 | with BytesIO(str.encode(chatfile)) as output: 137 | output.name = "groups_list.txt" 138 | update.effective_message.reply_document( 139 | document=output, 140 | filename="groups_list.txt", 141 | caption="Here be the list of groups in my database.", 142 | ) 143 | 144 | 145 | 146 | def chat_checker(update: Update, context: CallbackContext): 147 | bot = context.bot 148 | try: 149 | if update.effective_message.chat.get_member(bot.id).can_send_messages is False: 150 | bot.leaveChat(update.effective_message.chat.id) 151 | except Unauthorized: 152 | pass 153 | 154 | 155 | def __user_info__(user_id): 156 | if user_id in [777000, 1087968824]: 157 | return """╘══「 Groups count: ??? 」""" 158 | if user_id == dispatcher.bot.id: 159 | return """╘══「 Groups count: ??? 」""" 160 | num_chats = sql.get_user_num_chats(user_id) 161 | return f"""╘══「 Groups count: {num_chats} 」""" 162 | 163 | 164 | def __stats__(): 165 | return f"• {sql.num_users()} users, across {sql.num_chats()} chats" 166 | 167 | 168 | def __migrate__(old_chat_id, new_chat_id): 169 | sql.migrate_chat(old_chat_id, new_chat_id) 170 | 171 | 172 | # __help__ = "" # no help string 173 | 174 | BROADCAST_HANDLER = CommandHandler( 175 | ["broadcastall", "broadcastusers", "broadcastgroups"], broadcast 176 | ) 177 | USER_HANDLER = MessageHandler(Filters.all & Filters.chat_type.groups, log_user, run_async=True) 178 | CHAT_CHECKER_HANDLER = MessageHandler(Filters.all & Filters.chat_type.groups, chat_checker, run_async=True) 179 | CHATLIST_HANDLER = CommandHandler("groups", chats, run_async=True) 180 | 181 | dispatcher.add_handler(USER_HANDLER, USERS_GROUP) 182 | dispatcher.add_handler(BROADCAST_HANDLER) 183 | dispatcher.add_handler(CHATLIST_HANDLER) 184 | dispatcher.add_handler(CHAT_CHECKER_HANDLER, CHAT_GROUP) 185 | 186 | __mod_name__ = "User" 187 | __handlers__ = [(USER_HANDLER, USERS_GROUP), BROADCAST_HANDLER, CHATLIST_HANDLER] 188 | -------------------------------------------------------------------------------- /MissCutie/Database/users_sql.py: -------------------------------------------------------------------------------- 1 | import threading 2 | 3 | from MissCutie import dispatcher 4 | from MissCutie.Database import BASE, SESSION 5 | from sqlalchemy import ( 6 | Column, 7 | ForeignKey, 8 | BigInteger, 9 | String, 10 | UnicodeText, 11 | UniqueConstraint, 12 | func, 13 | ) 14 | 15 | 16 | class Users(BASE): 17 | __tablename__ = "users" 18 | user_id = Column(BigInteger, primary_key=True) 19 | username = Column(UnicodeText) 20 | 21 | def __init__(self, user_id, username=None): 22 | self.user_id = user_id 23 | self.username = username 24 | 25 | def __repr__(self): 26 | return f"" 27 | 28 | 29 | class Chats(BASE): 30 | __tablename__ = "chats" 31 | chat_id = Column(String(14), primary_key=True) 32 | chat_name = Column(UnicodeText, nullable=False) 33 | 34 | def __init__(self, chat_id, chat_name): 35 | self.chat_id = str(chat_id) 36 | self.chat_name = chat_name 37 | 38 | def __repr__(self): 39 | return f"" 40 | 41 | 42 | class ChatMembers(BASE): 43 | __tablename__ = "chat_members" 44 | priv_chat_id = Column(BigInteger, primary_key=True) 45 | # NOTE: Use dual primary key instead of private primary key? 46 | chat = Column( 47 | String(20), 48 | ForeignKey("chats.chat_id", onupdate="CASCADE", ondelete="CASCADE"), 49 | nullable=False, 50 | ) 51 | user = Column( 52 | BigInteger, 53 | ForeignKey("users.user_id", onupdate="CASCADE", ondelete="CASCADE"), 54 | nullable=False, 55 | ) 56 | __table_args__ = (UniqueConstraint("chat", "user", name="_chat_members_uc"),) 57 | 58 | def __init__(self, chat, user): 59 | self.chat = chat 60 | self.user = user 61 | 62 | def __repr__(self): 63 | return f"" 64 | 65 | 66 | Users.__table__.create(checkfirst=True) 67 | Chats.__table__.create(checkfirst=True) 68 | ChatMembers.__table__.create(checkfirst=True) 69 | 70 | INSERTION_LOCK = threading.RLock() 71 | 72 | 73 | def ensure_bot_in_db(): 74 | with INSERTION_LOCK: 75 | bot = Users(dispatcher.bot.id, dispatcher.bot.username) 76 | SESSION.merge(bot) 77 | SESSION.commit() 78 | 79 | 80 | def update_user(user_id, username, chat_id=None, chat_name=None): 81 | with INSERTION_LOCK: 82 | user = SESSION.query(Users).get(user_id) 83 | if not user: 84 | user = Users(user_id, username) 85 | SESSION.add(user) 86 | SESSION.flush() 87 | else: 88 | user.username = username 89 | 90 | if not chat_id or not chat_name: 91 | SESSION.commit() 92 | return 93 | 94 | chat = SESSION.query(Chats).get(str(chat_id)) 95 | if not chat: 96 | chat = Chats(str(chat_id), chat_name) 97 | SESSION.add(chat) 98 | SESSION.flush() 99 | 100 | else: 101 | chat.chat_name = chat_name 102 | 103 | member = ( 104 | SESSION.query(ChatMembers) 105 | .filter(ChatMembers.chat == chat.chat_id, ChatMembers.user == user.user_id) 106 | .first() 107 | ) 108 | if not member: 109 | chat_member = ChatMembers(chat.chat_id, user.user_id) 110 | SESSION.add(chat_member) 111 | 112 | SESSION.commit() 113 | 114 | 115 | def get_userid_by_name(username): 116 | try: 117 | return ( 118 | SESSION.query(Users) 119 | .filter(func.lower(Users.username) == username.lower()) 120 | .all() 121 | ) 122 | finally: 123 | SESSION.close() 124 | 125 | 126 | def get_name_by_userid(user_id): 127 | try: 128 | return SESSION.query(Users).get(Users.user_id == int(user_id)).first() 129 | finally: 130 | SESSION.close() 131 | 132 | 133 | def get_chat_members(chat_id): 134 | try: 135 | return SESSION.query(ChatMembers).filter(ChatMembers.chat == str(chat_id)).all() 136 | finally: 137 | SESSION.close() 138 | 139 | 140 | def get_all_chats(): 141 | try: 142 | return SESSION.query(Chats).all() 143 | finally: 144 | SESSION.close() 145 | 146 | 147 | def get_all_users(): 148 | try: 149 | return SESSION.query(Users).all() 150 | finally: 151 | SESSION.close() 152 | 153 | 154 | def get_user_num_chats(user_id): 155 | try: 156 | return ( 157 | SESSION.query(ChatMembers).filter(ChatMembers.user == int(user_id)).count() 158 | ) 159 | finally: 160 | SESSION.close() 161 | 162 | 163 | def get_user_com_chats(user_id): 164 | try: 165 | chat_members = ( 166 | SESSION.query(ChatMembers).filter(ChatMembers.user == int(user_id)).all() 167 | ) 168 | return [i.chat for i in chat_members] 169 | finally: 170 | SESSION.close() 171 | 172 | 173 | def num_chats(): 174 | try: 175 | return SESSION.query(Chats).count() 176 | finally: 177 | SESSION.close() 178 | 179 | 180 | def num_users(): 181 | try: 182 | return SESSION.query(Users).count() 183 | finally: 184 | SESSION.close() 185 | 186 | 187 | def migrate_chat(old_chat_id, new_chat_id): 188 | with INSERTION_LOCK: 189 | if chat := SESSION.query(Chats).get(str(old_chat_id)): 190 | chat.chat_id = str(new_chat_id) 191 | SESSION.commit() 192 | 193 | chat_members = ( 194 | SESSION.query(ChatMembers) 195 | .filter(ChatMembers.chat == str(old_chat_id)) 196 | .all() 197 | ) 198 | for member in chat_members: 199 | member.chat = str(new_chat_id) 200 | SESSION.commit() 201 | 202 | 203 | ensure_bot_in_db() 204 | 205 | 206 | def del_user(user_id): 207 | with INSERTION_LOCK: 208 | if curr := SESSION.query(Users).get(user_id): 209 | SESSION.delete(curr) 210 | SESSION.commit() 211 | return True 212 | 213 | ChatMembers.query.filter(ChatMembers.user == user_id).delete() 214 | SESSION.commit() 215 | SESSION.close() 216 | return False 217 | 218 | 219 | def rem_chat(chat_id): 220 | with INSERTION_LOCK: 221 | if chat := SESSION.query(Chats).get(str(chat_id)): 222 | SESSION.delete(chat) 223 | SESSION.commit() 224 | else: 225 | SESSION.close() 226 | -------------------------------------------------------------------------------- /MissCutie/Plugins/User/math.py: -------------------------------------------------------------------------------- 1 | import math 2 | 3 | import pynewtonmath as newton 4 | from telegram import Update 5 | from telegram.ext import CallbackContext, run_async 6 | 7 | from MissCutie import dispatcher, BOT_ID, BOT_NAME, BOT_USERNAME 8 | from MissCutie.Plugins.disable import DisableAbleCommandHandler 9 | 10 | 11 | 12 | def simplify(update: Update, context: CallbackContext): 13 | args = context.args 14 | message = update.effective_message 15 | message.reply_text(newton.simplify(f"{args[0]}")) 16 | 17 | 18 | 19 | def factor(update: Update, context: CallbackContext): 20 | args = context.args 21 | message = update.effective_message 22 | message.reply_text(newton.factor(f"{args[0]}")) 23 | 24 | 25 | 26 | def derive(update: Update, context: CallbackContext): 27 | args = context.args 28 | message = update.effective_message 29 | message.reply_text(newton.derive(f"{args[0]}")) 30 | 31 | 32 | 33 | def integrate(update: Update, context: CallbackContext): 34 | args = context.args 35 | message = update.effective_message 36 | message.reply_text(newton.integrate(f"{args[0]}")) 37 | 38 | 39 | 40 | def zeroes(update: Update, context: CallbackContext): 41 | args = context.args 42 | message = update.effective_message 43 | message.reply_text(newton.zeroes(f"{args[0]}")) 44 | 45 | 46 | 47 | def tangent(update: Update, context: CallbackContext): 48 | args = context.args 49 | message = update.effective_message 50 | message.reply_text(newton.tangent(f"{args[0]}")) 51 | 52 | 53 | 54 | def area(update: Update, context: CallbackContext): 55 | args = context.args 56 | message = update.effective_message 57 | message.reply_text(newton.area(f"{args[0]}")) 58 | 59 | 60 | 61 | def cos(update: Update, context: CallbackContext): 62 | args = context.args 63 | message = update.effective_message 64 | message.reply_text(math.cos(int(args[0]))) 65 | 66 | 67 | 68 | def sin(update: Update, context: CallbackContext): 69 | args = context.args 70 | message = update.effective_message 71 | message.reply_text(math.sin(int(args[0]))) 72 | 73 | 74 | 75 | def tan(update: Update, context: CallbackContext): 76 | args = context.args 77 | message = update.effective_message 78 | message.reply_text(math.tan(int(args[0]))) 79 | 80 | 81 | 82 | def arccos(update: Update, context: CallbackContext): 83 | args = context.args 84 | message = update.effective_message 85 | message.reply_text(math.acos(int(args[0]))) 86 | 87 | 88 | 89 | def arcsin(update: Update, context: CallbackContext): 90 | args = context.args 91 | message = update.effective_message 92 | message.reply_text(math.asin(int(args[0]))) 93 | 94 | 95 | 96 | def arctan(update: Update, context: CallbackContext): 97 | args = context.args 98 | message = update.effective_message 99 | message.reply_text(math.atan(int(args[0]))) 100 | 101 | 102 | 103 | def abs(update: Update, context: CallbackContext): 104 | args = context.args 105 | message = update.effective_message 106 | message.reply_text(math.fabs(int(args[0]))) 107 | 108 | 109 | 110 | def log(update: Update, context: CallbackContext): 111 | args = context.args 112 | message = update.effective_message 113 | message.reply_text(math.log(int(args[0]))) 114 | 115 | 116 | __mod_name__ = "Maths" 117 | 118 | 119 | __help__ = """ 120 | Solves complex math problems using https://newton.now.sh 121 | - /math*:* Math `/math 2^2+2(2)` 122 | - /factor*:* Factor `/factor x^2 + 2x` 123 | - /derive*:* Derive `/derive x^2+2x` 124 | - /integrate*:* Integrate `/integrate x^2+2x` 125 | - /zeroes*:* Find 0's `/zeroes x^2+2x` 126 | - /tangent*:* Find Tangent `/tangent 2lx^3` 127 | - /area*:* Area Under Curve `/area 2:4lx^3` 128 | - /cos*:* Cosine `/cos pi` 129 | - /sin*:* Sine `/sin 0` 130 | - /tan*:* Tangent `/tan 0` 131 | - /arccos*:* Inverse Cosine `/arccos 1` 132 | - /arcsin*:* Inverse Sine `/arcsin 0` 133 | - /arctan*:* Inverse Tangent `/arctan 0` 134 | - /abs*:* Absolute Value `/abs -1` 135 | - /log*:* Logarithm `/log 2l8` 136 | 137 | _Keep in mind_: To find the tangent line of a function at a certain x value, send the request as c|f(x) where c is the given x value and f(x) is the function expression, the separator is a vertical bar '|'. See the table above for an example request. 138 | To find the area under a function, send the request as c:d|f(x) where c is the starting x value, d is the ending x value, and f(x) is the function under which you want the curve between the two x values. 139 | To compute fractions, enter expressions as numerator(over)denominator. For example, to process 2/4 you must send in your expression as 2(over)4. The result expression will be in standard math notation (1/2, 3/4). 140 | """ 141 | 142 | 143 | 144 | SIMPLIFY_HANDLER = DisableAbleCommandHandler("math", simplify, run_async=True) 145 | FACTOR_HANDLER = DisableAbleCommandHandler("factor", factor, run_async=True) 146 | DERIVE_HANDLER = DisableAbleCommandHandler("derive", derive, run_async=True) 147 | INTEGRATE_HANDLER = DisableAbleCommandHandler("integrate", integrate, run_async=True) 148 | ZEROES_HANDLER = DisableAbleCommandHandler("zeroes", zeroes, run_async=True) 149 | TANGENT_HANDLER = DisableAbleCommandHandler("tangent", tangent, run_async=True) 150 | AREA_HANDLER = DisableAbleCommandHandler("area", area, run_async=True) 151 | COS_HANDLER = DisableAbleCommandHandler("cos", cos, run_async=True) 152 | SIN_HANDLER = DisableAbleCommandHandler("sin", sin, run_async=True) 153 | TAN_HANDLER = DisableAbleCommandHandler("tan", tan, run_async=True) 154 | ARCCOS_HANDLER = DisableAbleCommandHandler("arccos", arccos, run_async=True) 155 | ARCSIN_HANDLER = DisableAbleCommandHandler("arcsin", arcsin, run_async=True) 156 | ARCTAN_HANDLER = DisableAbleCommandHandler("arctan", arctan, run_async=True) 157 | ABS_HANDLER = DisableAbleCommandHandler("abs", abs, run_async=True) 158 | LOG_HANDLER = DisableAbleCommandHandler("log", log, run_async=True) 159 | 160 | dispatcher.add_handler(SIMPLIFY_HANDLER) 161 | dispatcher.add_handler(FACTOR_HANDLER) 162 | dispatcher.add_handler(DERIVE_HANDLER) 163 | dispatcher.add_handler(INTEGRATE_HANDLER) 164 | dispatcher.add_handler(ZEROES_HANDLER) 165 | dispatcher.add_handler(TANGENT_HANDLER) 166 | dispatcher.add_handler(AREA_HANDLER) 167 | dispatcher.add_handler(COS_HANDLER) 168 | dispatcher.add_handler(SIN_HANDLER) 169 | dispatcher.add_handler(TAN_HANDLER) 170 | dispatcher.add_handler(ARCCOS_HANDLER) 171 | dispatcher.add_handler(ARCSIN_HANDLER) 172 | dispatcher.add_handler(ARCTAN_HANDLER) 173 | dispatcher.add_handler(ABS_HANDLER) 174 | dispatcher.add_handler(LOG_HANDLER) 175 | -------------------------------------------------------------------------------- /MissCutie/Plugins/User/afk.py: -------------------------------------------------------------------------------- 1 | import random 2 | import html 3 | from datetime import datetime 4 | import humanize 5 | 6 | from MissCutie import dispatcher 7 | from MissCutie.Plugins.disable import ( 8 | DisableAbleCommandHandler, 9 | DisableAbleMessageHandler, 10 | ) 11 | from MissCutie.Database import afk_sql as sql, disable_sql 12 | from MissCutie.Plugins.users import get_user_id 13 | from telegram import MessageEntity, Update 14 | from telegram.error import BadRequest 15 | from telegram.ext import CallbackContext, Filters, MessageHandler, run_async 16 | 17 | AFK_GROUP = 7 18 | AFK_REPLY_GROUP = 8 19 | 20 | 21 | 22 | def afk(update: Update, context: CallbackContext): 23 | args = update.effective_message.text.split(None, 1) 24 | user = update.effective_user 25 | 26 | if not user: # ignore channels 27 | return 28 | 29 | if user.id in [777000, 1087968824]: 30 | return 31 | 32 | notice = "" 33 | if len(args) >= 2: 34 | reason = args[1] 35 | if len(reason) > 100: 36 | reason = reason[:100] 37 | notice = "\nYour afk reason was shortened to 100 characters." 38 | else: 39 | reason = "" 40 | 41 | sql.set_afk(update.effective_user.id, reason) 42 | fname = update.effective_user.first_name 43 | try: 44 | update.effective_message.reply_text( 45 | f"{fname} is now away!{notice}", 46 | ) 47 | except BadRequest: 48 | pass 49 | 50 | 51 | 52 | def no_longer_afk(update: Update, context: CallbackContext): 53 | user = update.effective_user 54 | chat = update.effective_chat 55 | message = update.effective_message 56 | 57 | if not user: # ignore channels 58 | return 59 | 60 | res = sql.rm_afk(user.id) 61 | if res and not disable_sql.is_command_disabled(chat.id, 'afk'): 62 | if message.new_chat_members: # dont say msg 63 | return 64 | firstname = update.effective_user.first_name 65 | try: 66 | options = [ 67 | "{} is here!", 68 | "{} is back!", 69 | "{} is now in the chat!", 70 | "{} is awake!", 71 | "{} is back online!", 72 | "{} is finally here!", 73 | "Welcome back! {}", 74 | "Where is {}?\nIn the chat!", 75 | ] 76 | chosen_option = random.choice(options) 77 | update.effective_message.reply_text( 78 | chosen_option.format(firstname), 79 | ) 80 | except: 81 | return 82 | 83 | 84 | 85 | def reply_afk(update: Update, context: CallbackContext): 86 | bot = context.bot 87 | message = update.effective_message 88 | userc = update.effective_user 89 | userc_id = userc.id 90 | chat = update.effective_chat 91 | if chat and disable_sql.is_command_disabled(chat.id, 'afk'): 92 | return "" 93 | if message.entities and message.parse_entities( 94 | [MessageEntity.TEXT_MENTION, MessageEntity.MENTION], 95 | ): 96 | entities = message.parse_entities( 97 | [MessageEntity.TEXT_MENTION, MessageEntity.MENTION], 98 | ) 99 | 100 | chk_users = [] 101 | for ent in entities: 102 | if ent.type == MessageEntity.TEXT_MENTION: 103 | user_id = ent.user.id 104 | fst_name = ent.user.first_name 105 | 106 | if user_id in chk_users: 107 | return 108 | chk_users.append(user_id) 109 | 110 | if ent.type != MessageEntity.MENTION: 111 | return 112 | 113 | user_id = get_user_id( 114 | message.text[ent.offset: ent.offset + ent.length], 115 | ) 116 | if not user_id: 117 | # Should never happen, since for a user to become AFK they must have spoken. Maybe changed username? 118 | return 119 | 120 | if user_id in chk_users: 121 | return 122 | chk_users.append(user_id) 123 | 124 | try: 125 | chat = bot.get_chat(user_id) 126 | except BadRequest: 127 | print(f"Error: Could not fetch userid {user_id} for AFK module") 128 | return 129 | fst_name = chat.first_name 130 | 131 | check_afk(update, context, user_id, fst_name, userc_id) 132 | 133 | elif message.reply_to_message: 134 | user_id = message.reply_to_message.from_user.id 135 | fst_name = message.reply_to_message.from_user.first_name 136 | check_afk(update, context, user_id, fst_name, userc_id) 137 | 138 | 139 | def check_afk(update: Update, context: CallbackContext, user_id: int, fst_name: str, userc_id: int): 140 | if not sql.is_afk(user_id): 141 | return 142 | user = sql.check_afk_status(user_id) 143 | if not user: 144 | return 145 | 146 | if userc_id == user_id: 147 | return 148 | 149 | time = humanize.naturaldelta(datetime.now() - user.time) 150 | 151 | if not user.reason: 152 | res = f"{fst_name} is afk.\n\nLast seen {time} ago." 153 | update.effective_message.reply_text(res) 154 | else: 155 | res = f"{html.escape(fst_name)} is afk.\nReason: {html.escape(user.reason)}\n\nLast seen {time} ago." 156 | update.effective_message.reply_text(res, parse_mode="html") 157 | 158 | 159 | AFK_HANDLER = DisableAbleCommandHandler("afk", afk, run_async=True) 160 | AFK_REGEX_HANDLER = DisableAbleMessageHandler( 161 | Filters.regex(r"^(?i)brb(.*)$"), afk, friendly="afk", 162 | ) 163 | NO_AFK_HANDLER = MessageHandler(Filters.all & Filters.chat_type.group, no_longer_afk, run_async=True) 164 | AFK_REPLY_HANDLER = MessageHandler(Filters.all & Filters.chat_type.group, reply_afk, run_async=True) 165 | 166 | dispatcher.add_handler(AFK_HANDLER, AFK_GROUP) 167 | dispatcher.add_handler(AFK_REGEX_HANDLER, AFK_GROUP) 168 | dispatcher.add_handler(NO_AFK_HANDLER, AFK_GROUP) 169 | dispatcher.add_handler(AFK_REPLY_HANDLER, AFK_REPLY_GROUP) 170 | 171 | __help__ = """ 172 | ‣ `/afk `*:* mark yourself as AFK(away from keyboard). 173 | ‣ `brb `*:* same as the afk command - but not a command. 174 | When marked as AFK, any mentions will be replied to with a message to say you're not available! 175 | """ 176 | 177 | __mod_name__ = "AFK" 178 | __command_list__ = ["afk"] 179 | __handlers__ = [ 180 | (AFK_HANDLER, AFK_GROUP), 181 | (AFK_REGEX_HANDLER, AFK_GROUP), 182 | (NO_AFK_HANDLER, AFK_GROUP), 183 | (AFK_REPLY_HANDLER, AFK_REPLY_GROUP), 184 | ] 185 | -------------------------------------------------------------------------------- /MissCutie/Plugins/Admin/purge.py: -------------------------------------------------------------------------------- 1 | import time 2 | from telethon import events 3 | from telegram import Update 4 | from telegram.ext import CallbackContext, CommandHandler, run_async, Filters 5 | 6 | from MissCutie import telethn, dispatcher 7 | from MissCutie.Handlers.validation import ( 8 | can_delete, 9 | user_admin, 10 | 11 | ) 12 | from MissCutie.Handlers.telethon.validations import ( 13 | can_delete_messages, 14 | user_is_admin, 15 | ) 16 | 17 | import MissCutie.Database.purges_sql as sql 18 | 19 | async def purge_messages(event): 20 | start = time.perf_counter() 21 | if event.from_id is None: 22 | return 23 | 24 | if not await user_is_admin( 25 | user_id=event.sender_id, message=event, 26 | ) and event.from_id not in [1087968824]: 27 | await event.reply("Only Admins are allowed to use this command") 28 | return 29 | 30 | if not await can_delete_messages(message=event): 31 | await event.reply("Can't seem to purge the message") 32 | return 33 | 34 | reply_msg = await event.get_reply_message() 35 | if not reply_msg and len(event.message.text[7:]) == 0: 36 | await event.reply("Reply to a message to select where to start purging from.") 37 | return 38 | 39 | messages = [] 40 | delete_to = event.message.id 41 | 42 | if not reply_msg and len(event.message.text[7:]) > 0: 43 | message_id = delete_to - int(event.message.text[7:]) 44 | messages.append(message_id) 45 | else: 46 | message_id = reply_msg.id 47 | messages.append(event.reply_to_msg_id) 48 | 49 | for msg_id in range(message_id, delete_to + 1): 50 | messages.append(msg_id) 51 | if len(messages) == 100: 52 | await event.client.delete_messages(event.chat_id, messages) 53 | messages = [] 54 | print(messages) 55 | try: 56 | await event.client.delete_messages(event.chat_id, messages) 57 | except: 58 | pass 59 | time_ = time.perf_counter() - start 60 | text = f"Purged Successfully in {time_:0.2f}s" 61 | await event.respond(text, parse_mode="markdown") 62 | 63 | 64 | async def delete_messages(event): 65 | if event.from_id is None: 66 | return 67 | 68 | if not await user_is_admin( 69 | user_id=event.sender_id, message=event, 70 | ) and event.from_id not in [1087968824]: 71 | await event.reply("Only Admins are allowed to use this command") 72 | return 73 | 74 | if not await can_delete_messages(message=event): 75 | await event.reply("Can't seem to delete this") 76 | return 77 | 78 | message = await event.get_reply_message() 79 | if not message: 80 | await event.reply("Whadya want to delete?") 81 | return 82 | chat = await event.get_input_chat() 83 | del_message = [message, event.message] 84 | await event.client.delete_messages(chat, del_message) 85 | 86 | 87 | @user_admin 88 | def purgefrom(update: Update, context: CallbackContext): 89 | msg = update.effective_message 90 | user = update.effective_user 91 | chat = update.effective_chat 92 | bot = context.bot 93 | 94 | if can_delete(chat, bot.id): 95 | 96 | if msg.reply_to_message: 97 | 98 | message_id = msg.reply_to_message.message_id 99 | message_from = message_id - 1 100 | 101 | if sql.is_purgefrom(msg.chat_id, message_from): 102 | msg.reply_text("The source and target are same, give me a range.") 103 | return 104 | 105 | sql.purgefrom(msg.chat_id, message_from) 106 | msg.reply_to_message.reply_text("Message marked for deletion. Reply to another message with purgeto to delete all messages in between.") 107 | 108 | else: 109 | msg.reply_text("Reply to a message to let me know what to delete.") 110 | return "" 111 | 112 | return "" 113 | 114 | 115 | async def purgeto_messages(event): 116 | start = time.perf_counter() 117 | if event.from_id is None: 118 | return 119 | 120 | if not await user_is_admin( 121 | user_id=event.sender_id, message=event, 122 | ) and event.from_id not in [1087968824]: 123 | await event.reply("Only Admins are allowed to use this command") 124 | return 125 | 126 | if not await can_delete_messages(message=event): 127 | await event.reply("Can't seem to purge the message") 128 | return 129 | 130 | reply_msg = await event.get_reply_message() 131 | if not reply_msg: 132 | await event.reply("Reply to a message to select where to start purging from.") 133 | return 134 | 135 | x = sql.show_purgefrom(event.chat_id) 136 | for i in x: 137 | try: 138 | message_id = int(i.message_from) 139 | message_from_ids = [int(i.message_from)] 140 | for message_from in message_from_ids: 141 | sql.clear_purgefrom(msg.chat_id, message_from) 142 | except: 143 | pass 144 | messages = [message_id] 145 | delete_to = reply_msg.id 146 | 147 | for msg_id in range(message_id, delete_to + 1): 148 | messages.append(msg_id) 149 | if len(messages) == 100: 150 | await event.client.delete_messages(event.chat_id, messages) 151 | messages = [] 152 | print(messages) 153 | try: 154 | await event.client.delete_messages(event.chat_id, messages) 155 | except: 156 | pass 157 | time_ = time.perf_counter() - start 158 | text = f"Purged Successfully in {time_:0.2f}s" 159 | await event.respond(text, parse_mode="markdown") 160 | 161 | 162 | __help__ = """ 163 | *Admins only:* 164 | ‣ `/del`*:* deletes the message you replied to 165 | ‣ `/purge`*:* deletes all messages between this and the replied to message. 166 | ‣ `/purge `*:* if replied to with a number, deletes that many messages from target message, if sent normally in group then delete from current to previous messages 167 | ‣ `/purgefrom`*:* marks a start point to purge from 168 | ‣ `/purgeto`*:* marks the end point, messages bet to and from are deleted 169 | """ 170 | 171 | 172 | #Telethon CMDs 173 | PURGE_HANDLER = purge_messages, events.NewMessage(pattern=r"^[!/]purge(?!\S+)") 174 | PURGETO_HANDLER = purgeto_messages, events.NewMessage(pattern="^[!/]purgeto$") 175 | DEL_HANDLER = delete_messages, events.NewMessage(pattern="^[!/]del$") 176 | 177 | #PTB CMDs 178 | PURGEFROM_HANDLER = CommandHandler("purgefrom", purgefrom, filters=Filters.chat_type.group, run_async=True) 179 | dispatcher.add_handler(PURGEFROM_HANDLER) 180 | 181 | telethn.add_event_handler(*PURGE_HANDLER) 182 | telethn.add_event_handler(*PURGETO_HANDLER) 183 | telethn.add_event_handler(*DEL_HANDLER) 184 | 185 | __mod_name__ = "Purges" 186 | __command_list__ = ["del", "purge", "purgefrom", "purgeto"] 187 | __handlers__ = [PURGE_HANDLER, DEL_HANDLER] 188 | -------------------------------------------------------------------------------- /MissCutie/Database/connection_sql.py: -------------------------------------------------------------------------------- 1 | import threading 2 | import time 3 | from typing import Union 4 | 5 | from sqlalchemy import Column, String, Boolean, UnicodeText, BigInteger 6 | 7 | from MissCutie.Database import SESSION, BASE 8 | 9 | 10 | class ChatAccessConnectionSettings(BASE): 11 | __tablename__ = "access_connection" 12 | chat_id = Column(String(14), primary_key=True) 13 | allow_connect_to_chat = Column(Boolean, default=True) 14 | 15 | def __init__(self, chat_id, allow_connect_to_chat): 16 | self.chat_id = str(chat_id) 17 | self.allow_connect_to_chat = str(allow_connect_to_chat) 18 | 19 | def __repr__(self): 20 | return f"" 21 | 22 | 23 | class Connection(BASE): 24 | __tablename__ = "connection" 25 | user_id = Column(BigInteger, primary_key=True) 26 | chat_id = Column(String(14)) 27 | 28 | def __init__(self, user_id, chat_id): 29 | self.user_id = user_id 30 | self.chat_id = str(chat_id) # Ensure String 31 | 32 | 33 | class ConnectionHistory(BASE): 34 | __tablename__ = "connection_history" 35 | user_id = Column(BigInteger, primary_key=True) 36 | chat_id = Column(String(14), primary_key=True) 37 | chat_name = Column(UnicodeText) 38 | conn_time = Column(BigInteger) 39 | 40 | def __init__(self, user_id, chat_id, chat_name, conn_time): 41 | self.user_id = user_id 42 | self.chat_id = str(chat_id) 43 | self.chat_name = str(chat_name) 44 | self.conn_time = int(conn_time) 45 | 46 | def __repr__(self): 47 | return f"" 48 | 49 | 50 | ChatAccessConnectionSettings.__table__.create(checkfirst=True) 51 | Connection.__table__.create(checkfirst=True) 52 | ConnectionHistory.__table__.create(checkfirst=True) 53 | 54 | CHAT_ACCESS_LOCK = threading.RLock() 55 | CONNECTION_INSERTION_LOCK = threading.RLock() 56 | CONNECTION_HISTORY_LOCK = threading.RLock() 57 | 58 | HISTORY_CONNECT = {} 59 | 60 | 61 | def allow_connect_to_chat(chat_id: Union[str, int]) -> bool: 62 | try: 63 | if chat_setting := SESSION.query(ChatAccessConnectionSettings).get( 64 | str(chat_id) 65 | ): 66 | return chat_setting.allow_connect_to_chat 67 | return False 68 | finally: 69 | SESSION.close() 70 | 71 | 72 | def set_allow_connect_to_chat(chat_id: Union[int, str], setting: bool): 73 | with CHAT_ACCESS_LOCK: 74 | chat_setting = SESSION.query(ChatAccessConnectionSettings).get(str(chat_id)) 75 | if not chat_setting: 76 | chat_setting = ChatAccessConnectionSettings(chat_id, setting) 77 | 78 | chat_setting.allow_connect_to_chat = setting 79 | SESSION.add(chat_setting) 80 | SESSION.commit() 81 | 82 | 83 | def connect(user_id, chat_id): 84 | with CONNECTION_INSERTION_LOCK: 85 | if prev := SESSION.query(Connection).get((int(user_id))): 86 | SESSION.delete(prev) 87 | connect_to_chat = Connection(int(user_id), chat_id) 88 | SESSION.add(connect_to_chat) 89 | SESSION.commit() 90 | return True 91 | 92 | 93 | def get_connected_chat(user_id): 94 | try: 95 | return SESSION.query(Connection).get((int(user_id))) 96 | finally: 97 | SESSION.close() 98 | 99 | 100 | def curr_connection(chat_id): 101 | try: 102 | return SESSION.query(Connection).get((str(chat_id))) 103 | finally: 104 | SESSION.close() 105 | 106 | 107 | def disconnect(user_id): 108 | with CONNECTION_INSERTION_LOCK: 109 | if disconnect := SESSION.query(Connection).get((int(user_id))): 110 | SESSION.delete(disconnect) 111 | SESSION.commit() 112 | return True 113 | else: 114 | SESSION.close() 115 | return False 116 | 117 | 118 | def add_history_conn(user_id, chat_id, chat_name): 119 | global HISTORY_CONNECT 120 | with CONNECTION_HISTORY_LOCK: 121 | conn_time = int(time.time()) 122 | if HISTORY_CONNECT.get(int(user_id)): 123 | counting = ( 124 | SESSION.query(ConnectionHistory.user_id) 125 | .filter(ConnectionHistory.user_id == str(user_id)) 126 | .count() 127 | ) 128 | getchat_id = { 129 | HISTORY_CONNECT[int(user_id)][x]["chat_id"]: x 130 | for x in HISTORY_CONNECT[int(user_id)] 131 | } 132 | if chat_id in getchat_id: 133 | todeltime = getchat_id[str(chat_id)] 134 | if delold := SESSION.query(ConnectionHistory).get( 135 | (int(user_id), str(chat_id)) 136 | ): 137 | SESSION.delete(delold) 138 | HISTORY_CONNECT[int(user_id)].pop(todeltime) 139 | elif counting >= 5: 140 | todel = list(HISTORY_CONNECT[int(user_id)]) 141 | todel.reverse() 142 | todel = todel[4:] 143 | for x in todel: 144 | chat_old = HISTORY_CONNECT[int(user_id)][x]["chat_id"] 145 | if delold := SESSION.query(ConnectionHistory).get( 146 | (int(user_id), str(chat_old)) 147 | ): 148 | SESSION.delete(delold) 149 | HISTORY_CONNECT[int(user_id)].pop(x) 150 | else: 151 | HISTORY_CONNECT[int(user_id)] = {} 152 | if delold := SESSION.query(ConnectionHistory).get( 153 | (int(user_id), str(chat_id)) 154 | ): 155 | SESSION.delete(delold) 156 | history = ConnectionHistory(int(user_id), str(chat_id), chat_name, conn_time) 157 | SESSION.add(history) 158 | SESSION.commit() 159 | HISTORY_CONNECT[int(user_id)][conn_time] = { 160 | "chat_name": chat_name, 161 | "chat_id": str(chat_id), 162 | } 163 | 164 | 165 | def get_history_conn(user_id): 166 | if not HISTORY_CONNECT.get(int(user_id)): 167 | HISTORY_CONNECT[int(user_id)] = {} 168 | return HISTORY_CONNECT[int(user_id)] 169 | 170 | 171 | def clear_history_conn(user_id): 172 | global HISTORY_CONNECT 173 | todel = list(HISTORY_CONNECT[int(user_id)]) 174 | for x in todel: 175 | chat_old = HISTORY_CONNECT[int(user_id)][x]["chat_id"] 176 | if delold := SESSION.query(ConnectionHistory).get( 177 | (int(user_id), str(chat_old)) 178 | ): 179 | SESSION.delete(delold) 180 | HISTORY_CONNECT[int(user_id)].pop(x) 181 | SESSION.commit() 182 | return True 183 | 184 | 185 | def __load_user_history(): 186 | global HISTORY_CONNECT 187 | try: 188 | qall = SESSION.query(ConnectionHistory).all() 189 | HISTORY_CONNECT = {} 190 | for x in qall: 191 | check = HISTORY_CONNECT.get(x.user_id) 192 | if check is None: 193 | HISTORY_CONNECT[x.user_id] = {} 194 | HISTORY_CONNECT[x.user_id][x.conn_time] = { 195 | "chat_name": x.chat_name, 196 | "chat_id": x.chat_id, 197 | } 198 | finally: 199 | SESSION.close() 200 | 201 | 202 | __load_user_history() 203 | -------------------------------------------------------------------------------- /MissCutie/Plugins/modules.py: -------------------------------------------------------------------------------- 1 | import importlib 2 | import collections 3 | 4 | from MissCutie import dispatcher, telethn 5 | from MissCutie.__help__ import ( 6 | CHAT_SETTINGS, 7 | DATA_EXPORT, 8 | DATA_IMPORT, 9 | HELPABLE, 10 | IMPORTED, 11 | MIGRATEABLE, 12 | STATS, 13 | USER_INFO, 14 | USER_SETTINGS 15 | ) 16 | from MissCutie.Handlers.validation import dev_plus, sudo_plus 17 | from telegram import ParseMode, Update 18 | from telegram.ext import CallbackContext, CommandHandler, run_async 19 | 20 | 21 | 22 | @dev_plus 23 | def load(update: Update, context: CallbackContext): 24 | message = update.effective_message 25 | text = message.text.split(" ", 1)[1] 26 | load_messasge = message.reply_text( 27 | f"Attempting to load module : {text}", parse_mode=ParseMode.HTML 28 | ) 29 | 30 | try: 31 | imported_module = importlib.import_module(f"MissCutieRobot.modules.{text}") 32 | except: 33 | load_messasge.edit_text("Does that module even exist?") 34 | return 35 | 36 | if not hasattr(imported_module, "__mod_name__"): 37 | imported_module.__mod_name__ = imported_module.__name__ 38 | 39 | if imported_module.__mod_name__.lower() not in IMPORTED: 40 | IMPORTED[imported_module.__mod_name__.lower()] = imported_module 41 | else: 42 | load_messasge.edit_text("Module already loaded.") 43 | return 44 | if "__handlers__" in dir(imported_module): 45 | handlers = imported_module.__handlers__ 46 | for handler in handlers: 47 | if not isinstance(handler, tuple): 48 | dispatcher.add_handler(handler) 49 | elif isinstance(handler[0], collections.Callable): 50 | callback, telethon_event = handler 51 | telethn.add_event_handler(callback, telethon_event) 52 | else: 53 | handler_name, priority = handler 54 | dispatcher.add_handler(handler_name, priority) 55 | else: 56 | IMPORTED.pop(imported_module.__mod_name__.lower()) 57 | load_messasge.edit_text("The module cannot be loaded.") 58 | return 59 | 60 | if hasattr(imported_module, "__help__") and imported_module.__help__: 61 | HELPABLE[imported_module.__mod_name__.lower()] = imported_module 62 | 63 | # Chats to migrate on chat_migrated events 64 | if hasattr(imported_module, "__migrate__"): 65 | MIGRATEABLE.append(imported_module) 66 | 67 | if hasattr(imported_module, "__stats__"): 68 | STATS.append(imported_module) 69 | 70 | if hasattr(imported_module, "__user_info__"): 71 | USER_INFO.append(imported_module) 72 | 73 | if hasattr(imported_module, "__import_data__"): 74 | DATA_IMPORT.append(imported_module) 75 | 76 | if hasattr(imported_module, "__export_data__"): 77 | DATA_EXPORT.append(imported_module) 78 | 79 | if hasattr(imported_module, "__chat_settings__"): 80 | CHAT_SETTINGS[imported_module.__mod_name__.lower()] = imported_module 81 | 82 | if hasattr(imported_module, "__user_settings__"): 83 | USER_SETTINGS[imported_module.__mod_name__.lower()] = imported_module 84 | 85 | 86 | load_messasge.edit_text( 87 | f"Successfully loaded module : {text}", 88 | parse_mode=ParseMode.HTML, 89 | ) 90 | 91 | 92 | 93 | @dev_plus 94 | def unload(update: Update, context: CallbackContext): 95 | message = update.effective_message 96 | text = message.text.split(" ", 1)[1] 97 | unload_messasge = message.reply_text( 98 | f"Attempting to unload module : {text}", parse_mode=ParseMode.HTML 99 | ) 100 | 101 | try: 102 | imported_module = importlib.import_module(f"MissCutieRobot.modules.{text}") 103 | except: 104 | unload_messasge.edit_text("Does that module even exist?") 105 | return 106 | 107 | if not hasattr(imported_module, "__mod_name__"): 108 | imported_module.__mod_name__ = imported_module.__name__ 109 | if imported_module.__mod_name__.lower() in IMPORTED: 110 | IMPORTED.pop(imported_module.__mod_name__.lower()) 111 | else: 112 | unload_messasge.edit_text("Can't unload something that isn't loaded.") 113 | return 114 | if "__handlers__" in dir(imported_module): 115 | handlers = imported_module.__handlers__ 116 | for handler in handlers: 117 | if isinstance(handler, bool): 118 | unload_messasge.edit_text("This module can't be unloaded!") 119 | return 120 | elif not isinstance(handler, tuple): 121 | dispatcher.remove_handler(handler) 122 | elif isinstance(handler[0], collections.Callable): 123 | callback, telethon_event = handler 124 | telethn.remove_event_handler(callback, telethon_event) 125 | else: 126 | handler_name, priority = handler 127 | dispatcher.remove_handler(handler_name, priority) 128 | else: 129 | unload_messasge.edit_text("The module cannot be unloaded.") 130 | return 131 | 132 | if hasattr(imported_module, "__help__") and imported_module.__help__: 133 | HELPABLE.pop(imported_module.__mod_name__.lower()) 134 | 135 | # Chats to migrate on chat_migrated events 136 | if hasattr(imported_module, "__migrate__"): 137 | MIGRATEABLE.remove(imported_module) 138 | 139 | if hasattr(imported_module, "__stats__"): 140 | STATS.remove(imported_module) 141 | 142 | if hasattr(imported_module, "__user_info__"): 143 | USER_INFO.remove(imported_module) 144 | 145 | if hasattr(imported_module, "__import_data__"): 146 | DATA_IMPORT.remove(imported_module) 147 | 148 | if hasattr(imported_module, "__export_data__"): 149 | DATA_EXPORT.remove(imported_module) 150 | 151 | if hasattr(imported_module, "__chat_settings__"): 152 | CHAT_SETTINGS.pop(imported_module.__mod_name__.lower()) 153 | 154 | if hasattr(imported_module, "__user_settings__"): 155 | USER_SETTINGS.pop(imported_module.__mod_name__.lower()) 156 | 157 | unload_messasge.edit_text( 158 | f"Successfully unloaded module : {text}", parse_mode=ParseMode.HTML 159 | ) 160 | 161 | 162 | 163 | @sudo_plus 164 | def listmodules(update: Update, context: CallbackContext): 165 | message = update.effective_message 166 | module_list = [] 167 | 168 | for helpable_module in HELPABLE: 169 | helpable_module_info = IMPORTED[helpable_module] 170 | file_info = IMPORTED[helpable_module_info.__mod_name__.lower()] 171 | file_name = file_info.__name__.rsplit("MissCutieRobot.modules.", 1)[1] 172 | mod_name = file_info.__mod_name__ 173 | module_list.append(f"- {mod_name} ({file_name})\n") 174 | module_list = "Following modules are loaded : \n\n" + "".join(module_list) 175 | message.reply_text(module_list, parse_mode=ParseMode.HTML) 176 | 177 | 178 | LOAD_HANDLER = CommandHandler("load", load, run_async=True) 179 | UNLOAD_HANDLER = CommandHandler("unload", unload, run_async=True) 180 | LISTMODULES_HANDLER = CommandHandler("listmodules", listmodules, run_async=True) 181 | 182 | dispatcher.add_handler(LOAD_HANDLER) 183 | dispatcher.add_handler(UNLOAD_HANDLER) 184 | dispatcher.add_handler(LISTMODULES_HANDLER) 185 | 186 | -------------------------------------------------------------------------------- /MissCutie/Handlers/msg_types.py: -------------------------------------------------------------------------------- 1 | from enum import IntEnum, unique 2 | 3 | from MissCutie.Handlers.string_handling import button_markdown_parser 4 | from telegram import Message 5 | 6 | 7 | @unique 8 | class Types(IntEnum): 9 | TEXT = 0 10 | BUTTON_TEXT = 1 11 | STICKER = 2 12 | DOCUMENT = 3 13 | PHOTO = 4 14 | AUDIO = 5 15 | VOICE = 6 16 | VIDEO = 7 17 | 18 | 19 | def get_note_type(msg: Message): 20 | data_type = None 21 | content = None 22 | text = "" 23 | raw_text = msg.text or msg.caption 24 | args = raw_text.split(None, 2) # use python's maxsplit to separate cmd and args 25 | note_name = args[1] 26 | 27 | buttons = [] 28 | # determine what the contents of the filter are - text, image, sticker, etc 29 | if len(args) >= 3: 30 | offset = len(args[2]) - len( 31 | raw_text 32 | ) # set correct offset relative to command + notename 33 | text, buttons = button_markdown_parser( 34 | args[2], 35 | entities=msg.parse_entities() or msg.parse_caption_entities(), 36 | offset=offset, 37 | ) 38 | data_type = Types.BUTTON_TEXT if buttons else Types.TEXT 39 | elif msg.reply_to_message: 40 | entities = msg.reply_to_message.parse_entities() 41 | msgtext = msg.reply_to_message.text or msg.reply_to_message.caption 42 | if len(args) >= 2 and msg.reply_to_message.text: # not caption, text 43 | text, buttons = button_markdown_parser(msgtext, entities=entities) 44 | data_type = Types.BUTTON_TEXT if buttons else Types.TEXT 45 | elif msg.reply_to_message.sticker: 46 | content = msg.reply_to_message.sticker.file_id 47 | data_type = Types.STICKER 48 | 49 | elif msg.reply_to_message.document: 50 | content = msg.reply_to_message.document.file_id 51 | text, buttons = button_markdown_parser(msgtext, entities=entities) 52 | data_type = Types.DOCUMENT 53 | 54 | elif msg.reply_to_message.photo: 55 | content = msg.reply_to_message.photo[-1].file_id # last elem = best quality 56 | text, buttons = button_markdown_parser(msgtext, entities=entities) 57 | data_type = Types.PHOTO 58 | 59 | elif msg.reply_to_message.audio: 60 | content = msg.reply_to_message.audio.file_id 61 | text, buttons = button_markdown_parser(msgtext, entities=entities) 62 | data_type = Types.AUDIO 63 | 64 | elif msg.reply_to_message.voice: 65 | content = msg.reply_to_message.voice.file_id 66 | text, buttons = button_markdown_parser(msgtext, entities=entities) 67 | data_type = Types.VOICE 68 | 69 | elif msg.reply_to_message.video: 70 | content = msg.reply_to_message.video.file_id 71 | text, buttons = button_markdown_parser(msgtext, entities=entities) 72 | data_type = Types.VIDEO 73 | 74 | return note_name, text, data_type, content, buttons 75 | 76 | 77 | # note: add own args? 78 | def get_welcome_type(msg: Message): 79 | data_type = None 80 | content = None 81 | text = "" 82 | 83 | try: 84 | if msg.reply_to_message: 85 | args = msg.reply_to_message.text or msg.reply_to_message.caption 86 | else: 87 | args = msg.text.split( 88 | None, 1 89 | ) # use python's maxsplit to separate cmd and args 90 | except AttributeError: 91 | args = False 92 | 93 | if msg.reply_to_message: 94 | if msg.reply_to_message.sticker: 95 | content = msg.reply_to_message.sticker.file_id 96 | text = None 97 | data_type = Types.STICKER 98 | 99 | elif msg.reply_to_message.document: 100 | content = msg.reply_to_message.document.file_id 101 | text = msg.reply_to_message.caption 102 | data_type = Types.DOCUMENT 103 | 104 | elif msg.reply_to_message.photo: 105 | content = msg.reply_to_message.photo[-1].file_id # last elem = best quality 106 | text = msg.reply_to_message.caption 107 | data_type = Types.PHOTO 108 | 109 | elif msg.reply_to_message.audio: 110 | content = msg.reply_to_message.audio.file_id 111 | text = msg.reply_to_message.caption 112 | data_type = Types.AUDIO 113 | 114 | elif msg.reply_to_message.voice: 115 | content = msg.reply_to_message.voice.file_id 116 | text = msg.reply_to_message.caption 117 | data_type = Types.VOICE 118 | 119 | elif msg.reply_to_message.video: 120 | content = msg.reply_to_message.video.file_id 121 | text = msg.reply_to_message.caption 122 | data_type = Types.VIDEO 123 | 124 | elif msg.reply_to_message.video_note: 125 | content = msg.reply_to_message.video_note.file_id 126 | text = None 127 | data_type = Types.VIDEO_NOTE 128 | 129 | buttons = [] 130 | # determine what the contents of the filter are - text, image, sticker, etc 131 | if args: 132 | if msg.reply_to_message: 133 | argumen = msg.reply_to_message.caption or "" 134 | offset = 0 # offset is no need since target was in reply 135 | entities = msg.reply_to_message.parse_entities() 136 | else: 137 | argumen = args[1] 138 | offset = len(argumen) - len( 139 | msg.text 140 | ) # set correct offset relative to command + notename 141 | entities = msg.parse_entities() 142 | text, buttons = button_markdown_parser( 143 | argumen, entities=entities, offset=offset 144 | ) 145 | 146 | if not data_type and text: 147 | data_type = Types.BUTTON_TEXT if buttons else Types.TEXT 148 | return text, data_type, content, buttons 149 | 150 | 151 | def get_filter_type(msg: Message): 152 | 153 | if not msg.reply_to_message and msg.text and len(msg.text.split()) >= 3: 154 | content = None 155 | text = msg.text.split(None, 2)[2] 156 | data_type = Types.TEXT 157 | 158 | elif ( 159 | msg.reply_to_message 160 | and msg.reply_to_message.text 161 | and len(msg.text.split()) >= 2 162 | ): 163 | content = None 164 | text = msg.reply_to_message.text 165 | data_type = Types.TEXT 166 | 167 | elif msg.reply_to_message and msg.reply_to_message.sticker: 168 | content = msg.reply_to_message.sticker.file_id 169 | text = None 170 | data_type = Types.STICKER 171 | 172 | elif msg.reply_to_message and msg.reply_to_message.document: 173 | content = msg.reply_to_message.document.file_id 174 | text = msg.reply_to_message.caption 175 | data_type = Types.DOCUMENT 176 | 177 | elif msg.reply_to_message and msg.reply_to_message.photo: 178 | content = msg.reply_to_message.photo[-1].file_id # last elem = best quality 179 | text = msg.reply_to_message.caption 180 | data_type = Types.PHOTO 181 | 182 | elif msg.reply_to_message and msg.reply_to_message.audio: 183 | content = msg.reply_to_message.audio.file_id 184 | text = msg.reply_to_message.caption 185 | data_type = Types.AUDIO 186 | 187 | elif msg.reply_to_message and msg.reply_to_message.voice: 188 | content = msg.reply_to_message.voice.file_id 189 | text = msg.reply_to_message.caption 190 | data_type = Types.VOICE 191 | 192 | elif msg.reply_to_message and msg.reply_to_message.video: 193 | content = msg.reply_to_message.video.file_id 194 | text = msg.reply_to_message.caption 195 | data_type = Types.VIDEO 196 | 197 | elif msg.reply_to_message and msg.reply_to_message.video_note: 198 | content = msg.reply_to_message.video_note.file_id 199 | text = None 200 | data_type = Types.VIDEO_NOTE 201 | 202 | else: 203 | text = None 204 | data_type = None 205 | content = None 206 | 207 | return text, data_type, content 208 | -------------------------------------------------------------------------------- /MissCutie/Plugins/Tools/music.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import time 3 | import os 4 | import json 5 | import wget 6 | import textwrap 7 | 8 | from tswift import Song 9 | from MissCutie import dispatcher 10 | from MissCutie.Plugins.disable import DisableAbleCommandHandler 11 | from telegram import Bot, Update, Message, Chat, ParseMode 12 | from telegram.ext import CallbackContext, run_async 13 | 14 | try: 15 | from youtubesearchpython import SearchVideos 16 | from yt_dlp import YoutubeDL 17 | 18 | except: 19 | os.system("pip install pip install youtube-search-python") 20 | os.system("pip install pip install yt_dlp") 21 | from youtubesearchpython import SearchVideos 22 | from yt_dlp import YoutubeDL 23 | 24 | 25 | 26 | def music(update: Update, context: CallbackContext): 27 | bot = context.bot 28 | message = update.effective_message 29 | chat = update.effective_chat 30 | user = update.effective_user 31 | args = message.text.split(" ", 1) 32 | 33 | if len(args) == 1: 34 | message.reply_text('Provide Song Name also like `/song on my way`!') 35 | return 36 | urlissed = args[1] 37 | 38 | pablo = bot.send_message( 39 | chat.id, textwrap.dedent( 40 | f"`Getting {urlissed} From Youtube Servers. Please Wait.`") 41 | ) 42 | 43 | search = SearchVideos(f"{urlissed}", offset=1, mode="dict", max_results=1) 44 | mi = search.result() 45 | mio = mi["search_result"] 46 | mo = mio[0]["link"] 47 | mio[0]["duration"] 48 | thum = mio[0]["title"] 49 | fridayz = mio[0]["id"] 50 | thums = mio[0]["channel"] 51 | url = mo 52 | kekme = f"https://img.youtube.com/vi/{fridayz}/hqdefault.jpg" 53 | sedlyf = wget.download(kekme) 54 | opts = { 55 | "format": "bestaudio", 56 | "addmetadata": True, 57 | "key": "FFmpegMetadata", 58 | "writethumbnail": True, 59 | "prefer_ffmpeg": True, 60 | "geo_bypass": True, 61 | "nocheckcertificate": True, 62 | "postprocessors": [ 63 | { 64 | "key": "FFmpegExtractAudio", 65 | "preferredcodec": "mp3", 66 | "preferredquality": "720", 67 | } 68 | ], 69 | "outtmpl": "%(id)s.mp3", 70 | "quiet": True, 71 | "logtostderr": False, 72 | } 73 | 74 | try: 75 | is_downloading = True 76 | with YoutubeDL(opts) as ytdl: 77 | infoo = ytdl.extract_info(url, False) 78 | duration = round(infoo["duration"] / 60) 79 | 80 | if duration > 10: 81 | pablo.edit_text( 82 | f"❌ Videos longer than 10 minute(s) aren't allowed, the provided video is {duration} minute(s)" 83 | ) 84 | is_downloading = False 85 | return 86 | ytdl_data = ytdl.extract_info(url, download=True) 87 | 88 | except Exception as e: 89 | pablo.edit_text(f"*Failed To Download* \n*Error :* `{str(e)}`") 90 | is_downloading = False 91 | return 92 | c_time = time.time() 93 | capy = textwrap.dedent( 94 | f"*Song Name :* `{thum}` \n*Requested For :* `{urlissed}` \n*Channel :* `{thums}` \n*Link :* `{mo}`") 95 | file_stark = f"{ytdl_data['id']}.mp3" 96 | bot.send_audio( 97 | chat.id, 98 | audio=open(file_stark, "rb"), 99 | duration=int(ytdl_data["duration"]), 100 | title=str(ytdl_data["title"]), 101 | performer=str(ytdl_data["uploader"]), 102 | thumb=sedlyf, 103 | caption=capy, 104 | parse_mode=ParseMode.MARKDOWN, 105 | 106 | ) 107 | pablo.delete() 108 | for files in (sedlyf, file_stark): 109 | if files and os.path.exists(files): 110 | os.remove(files) 111 | 112 | 113 | def video(update: Update, context: CallbackContext): 114 | bot = context.bot 115 | message = update.effective_message 116 | chat = update.effective_chat 117 | user = update.effective_user 118 | args = message.text.split(" ", 1) 119 | 120 | if len(args) == 1: 121 | message.reply_text('Provide video Name also like `/video on my way`!') 122 | return 123 | urlissed = args[1] 124 | 125 | pablo = bot.send_message( 126 | message.chat.id, textwrap.dedent( 127 | f"`Getting {urlissed} From Youtube Servers. Please Wait.`") 128 | ) 129 | search = SearchVideos(f"{urlissed}", offset=1, mode="dict", max_results=1) 130 | mi = search.result() 131 | mio = mi["search_result"] 132 | mo = mio[0]["link"] 133 | thum = mio[0]["title"] 134 | fridayz = mio[0]["id"] 135 | thums = mio[0]["channel"] 136 | kekme = f"https://img.youtube.com/vi/{fridayz}/hqdefault.jpg" 137 | url = mo 138 | sedlyf = wget.download(kekme) 139 | opts = { 140 | "format": "best", 141 | "addmetadata": True, 142 | "key": "FFmpegMetadata", 143 | "prefer_ffmpeg": True, 144 | "geo_bypass": True, 145 | "nocheckcertificate": True, 146 | "postprocessors": [{"key": "FFmpegVideoConvertor", "preferedformat": "mp4"}], 147 | "outtmpl": "%(id)s.mp4", 148 | "logtostderr": False, 149 | "quiet": True, 150 | } 151 | 152 | try: 153 | is_downloading = True 154 | with YoutubeDL(opts) as ytdl: 155 | infoo = ytdl.extract_info(url, False) 156 | duration = round(infoo["duration"] / 60) 157 | 158 | if duration > 10: 159 | pablo.edit_text( 160 | f"❌ Videos longer than 10 minute(s) aren't allowed, the provided video is {duration} minute(s)" 161 | ) 162 | is_downloading = False 163 | return 164 | ytdl_data = ytdl.extract_info(url, download=True) 165 | 166 | except Exception as e: 167 | pablo.edit_text(f"*Failed To Download* \n*Error :* `{str(e)}`") 168 | is_downloading = False 169 | return 170 | 171 | c_time = time.time() 172 | file_stark = f"{ytdl_data['id']}.mp4" 173 | capy = textwrap.dedent( 174 | f"*Video Name ➠* `{thum}` \n*Requested For :* `{urlissed}` \n*Channel :* `{thums}` \n*Link :* `{mo}`") 175 | bot.send_video( 176 | chat.id, 177 | video=open(file_stark, "rb"), 178 | duration=int(ytdl_data["duration"]), 179 | # file_name=str(ytdl_data["title"]), 180 | thumb=sedlyf, 181 | caption=capy, 182 | supports_streaming=True, 183 | parse_mode=ParseMode.MARKDOWN, 184 | ) 185 | pablo.delete() 186 | for files in (sedlyf, file_stark): 187 | if files and os.path.exists(files): 188 | os.remove(files) 189 | 190 | 191 | 192 | def lyrics(bot: Bot, update: Update, args): 193 | msg = update.effective_message 194 | if query := " ".join(args): 195 | song = "" 196 | if song := Song.find_song(query): 197 | reply = ( 198 | song.format() 199 | if song.lyrics 200 | else "Couldn't find any lyrics for that song!" 201 | ) 202 | else: 203 | reply = "Song not found!" 204 | if len(reply) > 4090: 205 | with open("lyrics.txt", 'w') as f: 206 | f.write(f"{reply}\n\n\nOwO UwU OmO") 207 | with open("lyrics.txt", 'rb') as f: 208 | msg.reply_document(document=f, 209 | caption="Message length exceeded max limit! Sending as a text file.") 210 | else: 211 | msg.reply_text(reply) 212 | 213 | else: 214 | msg.reply_text("You haven't specified which song to look for!") 215 | return 216 | 217 | 218 | 219 | __help__ = """ *Now Donwload and hear/watch song on telegram 220 | ‣ `/song on my way`*:* it will down song from youtube server for you 221 | ‣ `/video born alone die alone` *:* download video from youtube 222 | ‣ `/lyrics besharam rang` *:* returns the lyrics of that song. 223 | You can either enter just the song name or both the artist and song name. 224 | """ 225 | 226 | __mod_name__ = "Music" 227 | 228 | 229 | SONG_HANDLER = DisableAbleCommandHandler(["song", "music"], music, run_async=True) 230 | VIDEO_HANDLER = DisableAbleCommandHandler("video", video, run_async=True) 231 | LYRICS_HANDLER = DisableAbleCommandHandler("lyrics", lyrics, run_async=True) 232 | 233 | 234 | 235 | dispatcher.add_handler(SONG_HANDLER) 236 | dispatcher.add_handler(VIDEO_HANDLER) 237 | dispatcher.add_handler(LYRICS_HANDLER) 238 | -------------------------------------------------------------------------------- /MissCutie/Plugins/Admin/muting.py: -------------------------------------------------------------------------------- 1 | import html 2 | from typing import Optional 3 | 4 | from MissCutie import LOGGER, REQUESTER, dispatcher 5 | from MissCutie.Handlers.validation import ( 6 | bot_admin, 7 | can_restrict, 8 | connection_status, 9 | is_user_admin, 10 | user_admin, 11 | ) 12 | from MissCutie.Handlers.extraction import ( 13 | extract_user, 14 | extract_user_and_text, 15 | ) 16 | from MissCutie.Handlers.string_handling import extract_time 17 | from MissCutie.Plugins.Admin.log_channel import loggable 18 | from telegram import Bot, Chat, ChatPermissions, ParseMode, Update 19 | from telegram.error import BadRequest 20 | from telegram.ext import CallbackContext, CommandHandler, run_async 21 | from telegram.utils.helpers import mention_html 22 | 23 | 24 | def check_user(user_id: int, bot: Bot, chat: Chat) -> Optional[str]: 25 | if not user_id: 26 | return "You don't seem to be referring to a user or the ID specified is incorrect.." 27 | try: 28 | member = chat.get_member(user_id) 29 | except BadRequest as excp: 30 | if excp.message == "User not found": 31 | return "I can't seem to find this user" 32 | else: 33 | raise 34 | 35 | if user_id == bot.id: 36 | return "I'm not gonna MUTE myself, How high are you?" 37 | if is_user_admin(chat, user_id, member) or user_id in REQUESTER: 38 | return "Can't. Find someone else to mute but not this one." 39 | return None 40 | 41 | 42 | 43 | @connection_status 44 | @bot_admin 45 | @user_admin 46 | @loggable 47 | def mute(update: Update, context: CallbackContext) -> str: 48 | bot = context.bot 49 | args = context.args 50 | 51 | chat = update.effective_chat 52 | user = update.effective_user 53 | message = update.effective_message 54 | 55 | user_id, reason = extract_user_and_text(message, args) 56 | if reply := check_user(user_id, bot, chat): 57 | message.reply_text(reply) 58 | return "" 59 | 60 | member = chat.get_member(user_id) 61 | 62 | log = ( 63 | f"{html.escape(chat.title)}:\n" 64 | f"#MUTE\n" 65 | f"Admin: {mention_html(user.id, user.first_name)}\n" 66 | f"User: {mention_html(member.user.id, member.user.first_name)}" 67 | ) 68 | 69 | if reason: 70 | log += f"\nReason: {reason}" 71 | 72 | if member.can_send_messages is None or member.can_send_messages: 73 | chat_permissions = ChatPermissions(can_send_messages=False) 74 | bot.restrict_chat_member(chat.id, user_id, chat_permissions) 75 | bot.sendMessage( 76 | chat.id, 77 | f"Muted {html.escape(member.user.first_name)} with no expiration date!", 78 | parse_mode=ParseMode.HTML, 79 | ) 80 | return log 81 | 82 | else: 83 | message.reply_text("This user is already muted!") 84 | 85 | return "" 86 | 87 | 88 | 89 | @connection_status 90 | @bot_admin 91 | @user_admin 92 | @loggable 93 | def unmute(update: Update, context: CallbackContext) -> str: 94 | bot, args = context.bot, context.args 95 | chat = update.effective_chat 96 | user = update.effective_user 97 | message = update.effective_message 98 | 99 | user_id = extract_user(message, args) 100 | if not user_id: 101 | message.reply_text( 102 | "You'll need to either give me a username to unmute, or reply to someone to be unmuted.", 103 | ) 104 | return "" 105 | 106 | member = chat.get_member(int(user_id)) 107 | 108 | if member.status in ["kicked", "left"]: 109 | message.reply_text( 110 | "This user isn't even in the chat, unmuting them won't make them talk more than they " 111 | "already do!", 112 | ) 113 | 114 | elif ( 115 | member.can_send_messages 116 | and member.can_send_media_messages 117 | and member.can_send_other_messages 118 | and member.can_add_web_page_previews 119 | ): 120 | message.reply_text("This user already has the right to speak.") 121 | else: 122 | chat_permissions = ChatPermissions( 123 | can_send_messages=True, 124 | can_invite_users=True, 125 | can_pin_messages=True, 126 | can_send_polls=True, 127 | can_change_info=True, 128 | can_send_media_messages=True, 129 | can_send_other_messages=True, 130 | can_add_web_page_previews=True, 131 | ) 132 | try: 133 | bot.restrict_chat_member(chat.id, int(user_id), chat_permissions) 134 | except BadRequest: 135 | pass 136 | bot.sendMessage( 137 | chat.id, 138 | f"I shall allow {html.escape(member.user.first_name)} to text!", 139 | parse_mode=ParseMode.HTML, 140 | ) 141 | return ( 142 | f"{html.escape(chat.title)}:\n" 143 | f"#UNMUTE\n" 144 | f"Admin: {mention_html(user.id, user.first_name)}\n" 145 | f"User: {mention_html(member.user.id, member.user.first_name)}" 146 | ) 147 | return "" 148 | 149 | 150 | 151 | @connection_status 152 | @bot_admin 153 | @can_restrict 154 | @user_admin 155 | @loggable 156 | def temp_mute(update: Update, context: CallbackContext) -> str: 157 | bot, args = context.bot, context.args 158 | chat = update.effective_chat 159 | user = update.effective_user 160 | message = update.effective_message 161 | 162 | user_id, reason = extract_user_and_text(message, args) 163 | if reply := check_user(user_id, bot, chat): 164 | message.reply_text(reply) 165 | return "" 166 | 167 | member = chat.get_member(user_id) 168 | 169 | if not reason: 170 | message.reply_text("You haven't specified a time to mute this user for!") 171 | return "" 172 | 173 | split_reason = reason.split(None, 1) 174 | 175 | time_val = split_reason[0].lower() 176 | reason = split_reason[1] if len(split_reason) > 1 else "" 177 | mutetime = extract_time(message, time_val) 178 | 179 | if not mutetime: 180 | return "" 181 | 182 | log = ( 183 | f"{html.escape(chat.title)}:\n" 184 | f"#TEMP MUTED\n" 185 | f"Admin: {mention_html(user.id, user.first_name)}\n" 186 | f"User: {mention_html(member.user.id, member.user.first_name)}\n" 187 | f"Time: {time_val}" 188 | ) 189 | if reason: 190 | log += f"\nReason: {reason}" 191 | 192 | try: 193 | if member.can_send_messages is None or member.can_send_messages: 194 | chat_permissions = ChatPermissions(can_send_messages=False) 195 | bot.restrict_chat_member( 196 | chat.id, user_id, chat_permissions, until_date=mutetime, 197 | ) 198 | bot.sendMessage( 199 | chat.id, 200 | f"Muted {html.escape(member.user.first_name)} for {time_val}!", 201 | parse_mode=ParseMode.HTML, 202 | ) 203 | return log 204 | else: 205 | message.reply_text("This user is already muted.") 206 | 207 | except BadRequest as excp: 208 | if excp.message == "Reply message not found": 209 | # Do not reply 210 | message.reply_text(f"Muted for {time_val}!", quote=False) 211 | return log 212 | else: 213 | LOGGER.warning(update) 214 | LOGGER.exception( 215 | "ERROR muting user %s in chat %s (%s) due to %s", 216 | user_id, 217 | chat.title, 218 | chat.id, 219 | excp.message, 220 | ) 221 | message.reply_text("Well damn, I can't mute that user.") 222 | 223 | return "" 224 | 225 | # Help moved to admins 226 | __help__ = """ 227 | *Admins only:* 228 | ‣ `/mute `*:* silences a user. Can also be used as a reply, muting the replied to user. 229 | ‣ `/tmute x(m/h/d)`*:* mutes a user for x time. (via handle, or reply). `m` = `minutes`, `h` = `hours`, `d` = `days`. 230 | ‣ `/unmute `*:* unmutes a user. Can also be used as a reply, muting the replied to user. 231 | """ 232 | 233 | MUTE_HANDLER = CommandHandler("mute", mute, run_async=True) 234 | UNMUTE_HANDLER = CommandHandler("unmute", unmute, run_async=True) 235 | TEMPMUTE_HANDLER = CommandHandler(["tmute", "tempmute"], temp_mute, run_async=True) 236 | 237 | dispatcher.add_handler(MUTE_HANDLER) 238 | dispatcher.add_handler(UNMUTE_HANDLER) 239 | dispatcher.add_handler(TEMPMUTE_HANDLER) 240 | 241 | __mod_name__ = "Mute" 242 | __handlers__ = [MUTE_HANDLER, UNMUTE_HANDLER, TEMPMUTE_HANDLER] 243 | -------------------------------------------------------------------------------- /MissCutie/Plugins/Admin/log_channel.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | from functools import wraps 3 | 4 | from telegram.ext import CallbackContext 5 | 6 | from MissCutie.Handlers.misc import is_module_loaded 7 | 8 | FILENAME = __name__.rsplit(".", 1)[-1] 9 | 10 | if is_module_loaded(FILENAME): 11 | from telegram import ParseMode, Update 12 | from telegram.error import BadRequest, Unauthorized 13 | from telegram.ext import CommandHandler, JobQueue, run_async 14 | from telegram.utils.helpers import escape_markdown 15 | 16 | from MissCutie import EVENT_LOGS, LOGGER, dispatcher 17 | from MissCutie.Handlers.validation import user_admin 18 | from MissCutie.Database import log_channel_sql as sql 19 | 20 | def loggable(func): 21 | @wraps(func) 22 | def log_action( 23 | update: Update, 24 | context: CallbackContext, 25 | job_queue: JobQueue = None, 26 | *args, 27 | **kwargs, 28 | ): 29 | result = ( 30 | func(update, context, job_queue, *args, **kwargs) 31 | if job_queue 32 | else func(update, context, *args, **kwargs) 33 | ) 34 | chat = update.effective_chat 35 | message = update.effective_message 36 | 37 | if result: 38 | datetime_fmt = "%H:%M - %d-%m-%Y" 39 | result += f"\nEvent Stamp: {datetime.utcnow().strftime(datetime_fmt)}" 40 | 41 | if message.chat.type == chat.SUPERGROUP and message.chat.username: 42 | result += f'\nLink: click here' 43 | log_chat = sql.get_chat_log_channel(chat.id) 44 | if log_chat: 45 | send_log(context, log_chat, chat.id, result) 46 | 47 | return result 48 | 49 | return log_action 50 | 51 | def gloggable(func): 52 | @wraps(func) 53 | def glog_action(update: Update, context: CallbackContext, *args, **kwargs): 54 | result = func(update, context, *args, **kwargs) 55 | chat = update.effective_chat 56 | message = update.effective_message 57 | 58 | if result: 59 | datetime_fmt = "%H:%M - %d-%m-%Y" 60 | result += "\nEvent Stamp: {}".format( 61 | datetime.utcnow().strftime(datetime_fmt) 62 | ) 63 | 64 | if message.chat.type == chat.SUPERGROUP and message.chat.username: 65 | result += f'\nLink: click here' 66 | log_chat = str(EVENT_LOGS) 67 | if log_chat: 68 | send_log(context, log_chat, chat.id, result) 69 | 70 | return result 71 | 72 | return glog_action 73 | 74 | def send_log( 75 | context: CallbackContext, log_chat_id: str, orig_chat_id: str, result: str 76 | ): 77 | bot = context.bot 78 | try: 79 | bot.send_message( 80 | log_chat_id, 81 | result, 82 | parse_mode=ParseMode.HTML, 83 | disable_web_page_preview=True, 84 | ) 85 | except BadRequest as excp: 86 | if excp.message == "Chat not found": 87 | bot.send_message( 88 | orig_chat_id, "This log channel has been deleted - unsetting." 89 | ) 90 | sql.stop_chat_logging(orig_chat_id) 91 | else: 92 | LOGGER.warning(excp.message) 93 | LOGGER.warning(result) 94 | LOGGER.exception("Could not parse") 95 | 96 | bot.send_message( 97 | log_chat_id, 98 | result 99 | + "\n\nFormatting has been disabled due to an unexpected error.", 100 | ) 101 | 102 | 103 | @user_admin 104 | def logging(update: Update, context: CallbackContext): 105 | bot = context.bot 106 | message = update.effective_message 107 | chat = update.effective_chat 108 | 109 | if log_channel := sql.get_chat_log_channel(chat.id): 110 | log_channel_info = bot.get_chat(log_channel) 111 | message.reply_text( 112 | f"This group has all it's logs sent to:" 113 | f" {escape_markdown(log_channel_info.title)} (`{log_channel}`)", 114 | parse_mode=ParseMode.MARKDOWN, 115 | ) 116 | 117 | else: 118 | message.reply_text("No log channel has been set for this group!") 119 | 120 | 121 | @user_admin 122 | def setlog(update: Update, context: CallbackContext): 123 | bot = context.bot 124 | message = update.effective_message 125 | chat = update.effective_chat 126 | if chat.type == chat.CHANNEL: 127 | message.reply_text( 128 | "Now, forward the /setlog to the group you want to tie this channel to!" 129 | ) 130 | 131 | elif message.forward_from_chat: 132 | sql.set_chat_log_channel(chat.id, message.forward_from_chat.id) 133 | try: 134 | message.delete() 135 | except BadRequest as excp: 136 | if excp.message != "Message to delete not found": 137 | LOGGER.exception( 138 | "Error deleting message in log channel. Should work anyway though." 139 | ) 140 | 141 | try: 142 | bot.send_message( 143 | message.forward_from_chat.id, 144 | f"This channel has been set as the log channel for {chat.title or chat.first_name}.", 145 | ) 146 | except Unauthorized as excp: 147 | if excp.message == "Forbidden: bot is not a member of the channel chat": 148 | bot.send_message(chat.id, "Successfully set log channel!") 149 | else: 150 | LOGGER.exception("ERROR in setting the log channel.") 151 | 152 | bot.send_message(chat.id, "Successfully set log channel!") 153 | 154 | else: 155 | message.reply_text( 156 | "The steps to set a log channel are:\n" 157 | " - add bot to the desired channel\n" 158 | " - send /setlog to the channel\n" 159 | " - forward the /setlog to the group\n" 160 | ) 161 | 162 | 163 | @user_admin 164 | def unsetlog(update: Update, context: CallbackContext): 165 | bot = context.bot 166 | message = update.effective_message 167 | chat = update.effective_chat 168 | 169 | if log_channel := sql.stop_chat_logging(chat.id): 170 | bot.send_message( 171 | log_channel, f"Channel has been unlinked from {chat.title}" 172 | ) 173 | message.reply_text("Log channel has been un-set.") 174 | 175 | else: 176 | message.reply_text("No log channel has been set yet!") 177 | 178 | def __stats__(): 179 | return f"• {sql.num_logchannels()} log channels set." 180 | 181 | def __migrate__(old_chat_id, new_chat_id): 182 | sql.migrate_chat(old_chat_id, new_chat_id) 183 | 184 | def __chat_settings__(chat_id, user_id): 185 | if log_channel := sql.get_chat_log_channel(chat_id): 186 | log_channel_info = dispatcher.bot.get_chat(log_channel) 187 | return f"This group has all it's logs sent to: {escape_markdown(log_channel_info.title)} (`{log_channel}`)" 188 | return "No log channel is set for this group!" 189 | 190 | __help__ = """ 191 | *Admins only:* 192 | ‣ `/logchannel`*:* get log channel info 193 | ‣ `/setlog`*:* set the log channel. 194 | ‣ `/unsetlog`*:* unset the log channel. 195 | Setting the log channel is done by: 196 | ‣ adding the bot to the desired channel (as an admin!) 197 | ‣ sending /setlog in the channel 198 | ‣ forwarding the /setlog to the group 199 | """ 200 | 201 | __mod_name__ = "Log Channel" 202 | 203 | LOG_HANDLER = CommandHandler("logchannel", logging, run_async=True) 204 | SET_LOG_HANDLER = CommandHandler("setlog", setlog, run_async=True) 205 | UNSET_LOG_HANDLER = CommandHandler("unsetlog", unsetlog, run_async=True) 206 | 207 | dispatcher.add_handler(LOG_HANDLER) 208 | dispatcher.add_handler(SET_LOG_HANDLER) 209 | dispatcher.add_handler(UNSET_LOG_HANDLER) 210 | 211 | else: 212 | # run anyway if module not loaded 213 | def loggable(func): 214 | return func 215 | 216 | def gloggable(func): 217 | return func 218 | --------------------------------------------------------------------------------