├── runtime.txt ├── .deepsource.toml ├── .gitignore ├── requirements.txt ├── Dockerfile ├── ubotindo ├── modules │ ├── debugger.py │ ├── no_sql │ │ ├── __init__.py │ │ ├── afk_db.py │ │ ├── log_channel_db.py │ │ ├── disable_db.py │ │ ├── users_db.py │ │ ├── gban_db.py │ │ └── blacklist_db.py │ ├── sql │ │ ├── __init__.py │ │ ├── antiflood_sql.py │ │ ├── notes_sql.py │ │ ├── connection_sql.py │ │ └── locks_sql.py │ ├── helper_funcs │ │ ├── admin_rights.py │ │ ├── alternate.py │ │ ├── handlers.py │ │ ├── filters.py │ │ ├── misc.py │ │ ├── extraction.py │ │ ├── chat_status.py │ │ ├── msg_types.py │ │ └── string_handling.py │ ├── __init__.py │ ├── translator.py │ ├── regex.py │ ├── purge.py │ ├── weather.py │ ├── lastfm.py │ ├── users.py │ ├── rules.py │ ├── webtools.py │ ├── afk.py │ ├── userinfo.py │ ├── dbcleanup.py │ ├── reverse.py │ ├── log_channel.py │ └── muting.py └── __init__.py ├── Procfile ├── start ├── .github └── workflows │ └── pythonapp.yml ├── sample_config.env └── README.md /runtime.txt: -------------------------------------------------------------------------------- 1 | python-3.9.0 2 | -------------------------------------------------------------------------------- /.deepsource.toml: -------------------------------------------------------------------------------- 1 | version = 1 2 | 3 | [[analyzers]] 4 | name = "python" 5 | enabled = true 6 | 7 | [analyzers.meta] 8 | runtime_version = "3.x.x" -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ubotindo-log.txt 2 | logfile 3 | *.pyc 4 | .idea/ 5 | .project 6 | .pydevproject 7 | .directory 8 | .vscode 9 | okgoogle.png 10 | *.backup 11 | sticker.png 12 | kangsticker.png 13 | kangsticker.tgs 14 | kangsticker 15 | ubotindo.mp3 16 | gpic.png 17 | config.env -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | alphabet_detector 2 | bs4 3 | bleach 4 | cachetools 5 | covid 6 | demjson 7 | dnspython 8 | emoji 9 | feedparser 10 | future 11 | git+https://github.com/userbotindo/python-markdown2.git 12 | google_trans_new 13 | gtts 14 | lxml 15 | pillow 16 | psutil 17 | psycopg2-binary 18 | pymongo[srv] 19 | python-telegram-bot==13.1 20 | pytz 21 | python-dotenv 22 | requests 23 | spamwatch 24 | speedtest-cli>=2.0.2 25 | sqlalchemy 26 | telegraph 27 | wget 28 | wikipedia 29 | hurry.filesize 30 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # set base image (host OS) 2 | FROM python:3.8 3 | 4 | # set the working directory in the container 5 | WORKDIR /ubotindo/ 6 | 7 | RUN apt -qq update && apt -qq upgrade 8 | RUN apt -qq install -y --no-install-recommends \ 9 | curl \ 10 | git \ 11 | gnupg2 \ 12 | wget \ 13 | 14 | # copy the dependencies file to the working directory 15 | COPY requirements.txt . 16 | 17 | # install dependencies 18 | RUN pip install -r requirements.txt 19 | 20 | # copy the content of the local src directory to the working directory 21 | COPY . . 22 | 23 | # command to run on container start 24 | CMD [ "bash", "./start" ] 25 | -------------------------------------------------------------------------------- /ubotindo/modules/debugger.py: -------------------------------------------------------------------------------- 1 | import os 2 | import datetime 3 | from telegram.ext import CommandHandler 4 | from telegram import Update 5 | from ubotindo import dispatcher 6 | from ubotindo.modules.helper_funcs.filters import CustomFilters 7 | from ubotindo.modules.helper_funcs.alternate import typing_action 8 | 9 | 10 | @typing_action 11 | def logs(update, context): 12 | user = update.effective_user 13 | with open("ubotindo-log.txt", "rb") as f: 14 | context.bot.send_document( 15 | document=f, 16 | filename=f.name, 17 | chat_id=user.id, 18 | caption="This logs that I saved", 19 | ) 20 | update.effective_message.reply_text("I am send log to your pm 💌") 21 | 22 | 23 | LOG_HANDLER = CommandHandler( 24 | "logs", logs, filters=CustomFilters.dev_filter, run_async=True 25 | ) 26 | dispatcher.add_handler(LOG_HANDLER) -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | # UserindoBot 2 | # Copyright (C) 2020 UserindoBot Team, 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | 17 | # Notes below is how the application works, don't activate all 18 | # Choose one which you think is better 19 | 20 | # Turn on this if you not using Webhook 21 | worker: bash start 22 | 23 | # Turn on this if you using Webhook 24 | web: bash start -------------------------------------------------------------------------------- /start: -------------------------------------------------------------------------------- 1 | #!/usr/bin/bash 2 | # 3 | # UserindoBot 4 | # Copyright (C) 2020 UserindoBot Team, 5 | # 6 | # This program is free software: you can redistribute it and/or modify 7 | # it under the terms of the GNU General Public License as published by 8 | # the Free Software Foundation, either version 3 of the License, or 9 | # (at your option) any later version. 10 | # 11 | # This program is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | # GNU General Public License for more details. 15 | # 16 | # You should have received a copy of the GNU General Public License 17 | # along with this program. If not, see . 18 | 19 | # Attemp to Terminate and Remove previous session 20 | if [ ! -d .git ]; then 21 | git clone https://github.com/userbotindo/UserIndoBot -b master tmp_git 22 | mv tmp_git/.git . 23 | rm -rf tmp_git 24 | fi 25 | 26 | # start 27 | python3 -m ubotindo -------------------------------------------------------------------------------- /.github/workflows/pythonapp.yml: -------------------------------------------------------------------------------- 1 | name: ErrorChecker 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build: 7 | 8 | runs-on: ubuntu-latest 9 | strategy: 10 | max-parallel: 5 11 | matrix: 12 | python-version: [3.9] 13 | 14 | steps: 15 | - uses: actions/checkout@v1 16 | - name: Set up Python ${{ matrix.python-version }} 17 | uses: actions/setup-python@v1 18 | with: 19 | python-version: ${{ matrix.python-version }} 20 | - name: Install dependencies 21 | run: | 22 | sudo apt-get install libpq-dev 23 | python -m pip install --upgrade pip 24 | pip install -r requirements.txt 25 | pip install flake8 flake8-print flake8-quotes 26 | - name: Check for showstoppers 27 | run: | 28 | # stop the build if there are Python syntax errors 29 | flake8 . --count --select=E999 --show-source --statistics 30 | shellcheck: 31 | 32 | runs-on: ubuntu-latest 33 | 34 | steps: 35 | - uses: actions/checkout@v1 36 | - name: Check for install script errors 37 | uses: ludeeus/action-shellcheck@0.1.0 38 | -------------------------------------------------------------------------------- /ubotindo/modules/no_sql/__init__.py: -------------------------------------------------------------------------------- 1 | # UserindoBot 2 | # Copyright (C) 2020 UserindoBot Team, 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | """MongoDB database.""" 17 | 18 | from pymongo import MongoClient, collection 19 | 20 | from ubotindo import MONGO_URI, LOGGER 21 | 22 | 23 | LOGGER.info("Connecting to MongoDB") 24 | 25 | DB_CLIENT = MongoClient(MONGO_URI) 26 | 27 | _DB = DB_CLIENT["UbotIndo"] 28 | 29 | 30 | def get_collection(name: str) -> collection: 31 | """Get the collection from database.""" 32 | return _DB[name] 33 | -------------------------------------------------------------------------------- /ubotindo/modules/sql/__init__.py: -------------------------------------------------------------------------------- 1 | # UserindoBot 2 | # Copyright (C) 2020 UserindoBot Team, 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | 17 | from sqlalchemy import create_engine 18 | from sqlalchemy.ext.declarative import declarative_base 19 | from sqlalchemy.orm import sessionmaker, scoped_session 20 | 21 | from ubotindo import DB_URI 22 | 23 | 24 | def start() -> scoped_session: 25 | engine = create_engine(DB_URI, client_encoding="utf8") 26 | BASE.metadata.bind = engine 27 | BASE.metadata.create_all(engine) 28 | return scoped_session(sessionmaker(bind=engine, autoflush=False)) 29 | 30 | 31 | BASE = declarative_base() 32 | SESSION = start() 33 | -------------------------------------------------------------------------------- /ubotindo/modules/helper_funcs/admin_rights.py: -------------------------------------------------------------------------------- 1 | # UserindoBot 2 | # Copyright (C) 2020 UserindoBot Team, 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | 17 | from telegram import User, Chat 18 | 19 | 20 | def user_can_promote(chat: Chat, user: User, bot_id: int) -> bool: 21 | return chat.get_member(user.id).can_promote_members 22 | 23 | 24 | def user_can_ban(chat: Chat, user: User, bot_id: int) -> bool: 25 | return chat.get_member(user.id).can_restrict_members 26 | 27 | 28 | def user_can_pin(chat: Chat, user: User, bot_id: int) -> bool: 29 | return chat.get_member(user.id).can_pin_messages 30 | 31 | 32 | def user_can_changeinfo(chat: Chat, user: User, bot_id: int) -> bool: 33 | return chat.get_member(user.id).can_change_info 34 | 35 | 36 | def user_can_delete(chat: Chat, user: User, bot_id: int) -> bool: 37 | return chat.get_member(user.id).can_delete_messages -------------------------------------------------------------------------------- /ubotindo/modules/no_sql/afk_db.py: -------------------------------------------------------------------------------- 1 | # UserindoBot 2 | # Copyright (C) 2020 UserindoBot Team, 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | """User(s) AFK database.""" 17 | 18 | from ubotindo.modules.no_sql import get_collection 19 | 20 | 21 | AFK_USERS = get_collection("AFK_USERS") 22 | AFK_LIST = set() 23 | 24 | 25 | def is_afk(user_id) -> bool: 26 | return user_id in AFK_LIST 27 | 28 | 29 | def check_afk_status(user_id) -> dict: 30 | data = AFK_USERS.find_one({'_id': user_id}) 31 | return data 32 | 33 | 34 | def set_afk(user_id, reason: str="") -> None: 35 | AFK_USERS.update_one( 36 | {'_id': user_id}, 37 | {"$set": {'reason': reason}}, 38 | upsert=True) 39 | __load_afk_users() 40 | 41 | 42 | def rm_afk(user_id) -> bool: 43 | data = AFK_USERS.find_one_and_delete({'_id': user_id}) 44 | if data: 45 | AFK_LIST.remove(user_id) 46 | return True 47 | return False 48 | 49 | 50 | def __load_afk_users()-> None: 51 | global AFK_LIST 52 | data = AFK_USERS.find() 53 | AFK_LIST = { 54 | x["_id"] for x in data 55 | } 56 | 57 | 58 | __load_afk_users() 59 | -------------------------------------------------------------------------------- /sample_config.env: -------------------------------------------------------------------------------- 1 | # Remove this line plox before doing anything else 2 | _____REMOVE_____THIS_____LINE_____=True 3 | 4 | 5 | #REQUIRED 6 | TOKEN = "" 7 | OWNER_ID = "" # If you dont know, run the bot and do /id in your private chat with it 8 | OWNER_USERNAME = "" 9 | # RECOMMENDED 10 | # needed for any database modules 11 | DATABASE_URL = "sqldbtype://username:pw@hostname:port/db_name" 12 | MONGO_DB_URI = "mongodb+srv://username:pwd@host.port.mongodb.net/db_name" 13 | MESSAGE_DUMP = "" # chat_id to make sure 'save from' messages persist 14 | GBAN_LOGS = "" # chat_id for your global ban logs 15 | WEBHOOK = False 16 | URL = "" 17 | 18 | # OPTIONAL 19 | LOAD = "" # list of loaded modules (seperate with space) 20 | NO_LOAD = "" # list of unloaded modules (seperate with space) 21 | # List of id's (not usernames) for users which have access to dev's command. 22 | DEV_USERS = "" # seperate with space 23 | # List of id's (not usernames) for users which have sudo access to the bot. 24 | SUDO_USERS = "" # seperate with space 25 | # List of id's (not usernames) for users which are allowed to gban, but 26 | # can also be banned. 27 | SUPPORT_USERS = "" # seperate with space 28 | # List of id's (not usernames) for users which WONT be banned/kicked by 29 | # the bot. 30 | WHITELIST_USERS = "" # seperate with space 31 | WHITELIST_CHATS = "" # seperate with space 32 | BLACKLIST_CHATS = "" # seperate with space 33 | DONATION_LINK = "" # EG, paypal 34 | CERT_PATH = "" 35 | PORT = 5000 36 | DEL_CMDS = False # Whether or not you should delete "blue text must click" commands 37 | STRICT_GBAN = True 38 | WORKERS = 8 # Number of subthreads to use. This is the recommended amount - see for yourself what works best! 39 | BAN_STICKER = "" # banhammer marie sticker 40 | # Set to ('/', '!') or whatever to enable it 41 | CUSTOM_CMD = False 42 | API_OPENWEATHER = "" # OpenWeather API 43 | SPAMWATCH_API = "" # Your SpamWatch token 44 | WALL_API = "" 45 | LASTFM_API_KEY = "" 46 | -------------------------------------------------------------------------------- /ubotindo/modules/__init__.py: -------------------------------------------------------------------------------- 1 | # UserindoBot 2 | # Copyright (C) 2020 UserindoBot Team, 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | 17 | import sys 18 | 19 | from ubotindo import LOAD, LOGGER, NO_LOAD 20 | 21 | 22 | def __list_all_modules(): 23 | import glob 24 | from os.path import basename, dirname, isfile 25 | 26 | # This generates a list of modules in this folder for the * in __main__ to 27 | # work. 28 | mod_paths = glob.glob(dirname(__file__) + "/*.py") 29 | all_modules = [ 30 | basename(f)[:-3] 31 | for f in mod_paths 32 | if isfile(f) and f.endswith(".py") and not f.endswith("__init__.py") 33 | ] 34 | 35 | if LOAD or NO_LOAD: 36 | to_load = LOAD 37 | if to_load: 38 | if not all( 39 | any(mod == module_name for module_name in all_modules) 40 | for mod in to_load 41 | ): 42 | LOGGER.error("Invalid loadorder names. Quitting.") 43 | sys.exit(1) 44 | 45 | else: 46 | to_load = all_modules 47 | 48 | if NO_LOAD: 49 | LOGGER.info("Not loading: {}".format(NO_LOAD)) 50 | return [item for item in to_load if item not in NO_LOAD] 51 | 52 | return to_load 53 | 54 | return all_modules 55 | 56 | 57 | ALL_MODULES = sorted(__list_all_modules()) 58 | LOGGER.info("Modules to load: %s", str(ALL_MODULES)) 59 | __all__ = ALL_MODULES + ["ALL_MODULES"] 60 | -------------------------------------------------------------------------------- /ubotindo/modules/no_sql/log_channel_db.py: -------------------------------------------------------------------------------- 1 | # UserindoBot 2 | # Copyright (C) 2020 UserindoBot Team, 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | """Channel log database.""" 17 | 18 | from ubotindo.modules.no_sql import get_collection 19 | 20 | 21 | LOG_DATA = get_collection("LOG_CHANNELS") 22 | 23 | CHANNELS = {} 24 | 25 | 26 | def set_chat_log_channel(chat_id, log_channel): 27 | LOG_DATA.update_one( 28 | {'chat_id': chat_id}, 29 | {"$set": {'log_channel': log_channel}}, 30 | upsert=True 31 | ) 32 | CHANNELS[str(chat_id)] = log_channel 33 | 34 | 35 | def get_chat_log_channel(chat_id) -> int: 36 | return CHANNELS.get(str(chat_id)) 37 | 38 | 39 | def stop_chat_logging(chat_id) -> int: 40 | res = LOG_DATA.find_one_and_delete({'chat_id': chat_id}) 41 | if str(chat_id) in CHANNELS: 42 | del CHANNELS[str(chat_id)] 43 | return res["log_channel"] 44 | 45 | 46 | def num_logchannels() -> int: 47 | return LOG_DATA.count_documents({}) 48 | 49 | 50 | def migrate_chat(old_chat_id, new_chat_id): 51 | LOG_DATA.update_one( 52 | {'chat_id': old_chat_id}, 53 | {"$set": {'chat_id': new_chat_id}} 54 | ) 55 | if str(old_chat_id) in CHANNELS: 56 | CHANNELS[str(new_chat_id)] = CHANNELS.get(str(old_chat_id)) 57 | 58 | 59 | def __load_log_channels(): 60 | global CHANNELS 61 | CHANNELS = { 62 | str(chat['chat_id']):str(chat['log_channel']) 63 | for chat in LOG_DATA.find() 64 | } 65 | 66 | 67 | __load_log_channels() -------------------------------------------------------------------------------- /ubotindo/modules/helper_funcs/alternate.py: -------------------------------------------------------------------------------- 1 | # UserindoBot 2 | # Copyright (C) 2020 UserindoBot Team, 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | 17 | from functools import wraps 18 | from telegram import error, ChatAction 19 | from ubotindo import LOGGER 20 | 21 | 22 | def send_message(message, text, *args, **kwargs): 23 | try: 24 | return message.reply_text(text, *args, **kwargs) 25 | except error.BadRequest as err: 26 | if str(err) == "Reply message not found": 27 | return message.reply_text(text, quote=False, *args, **kwargs) 28 | raise 29 | 30 | 31 | def typing_action(func): 32 | """Sends typing action while processing func command.""" 33 | 34 | @wraps(func) 35 | def command_func(update, context, *args, **kwargs): 36 | try: 37 | context.bot.send_chat_action( 38 | chat_id=update.effective_chat.id, action=ChatAction.TYPING 39 | ) 40 | return func(update, context, *args, **kwargs) 41 | except error.BadRequest as err: 42 | if str(err) == "Have no rights to send a message": 43 | LOGGER.warning("Bot muted in {} {}".format( 44 | update.effective_message.chat.title, 45 | update.effective_message.chat.id 46 | ) 47 | ) 48 | except error.Unauthorized: 49 | return 50 | 51 | return command_func 52 | 53 | 54 | def send_action(action): 55 | """Sends `action` while processing func command.""" 56 | 57 | def decorator(func): 58 | @wraps(func) 59 | def command_func(update, context, *args, **kwargs): 60 | context.bot.send_chat_action( 61 | chat_id=update.effective_chat.id, action=action 62 | ) 63 | return func(update, context, *args, **kwargs) 64 | 65 | return command_func 66 | 67 | return decorator 68 | -------------------------------------------------------------------------------- /ubotindo/modules/helper_funcs/handlers.py: -------------------------------------------------------------------------------- 1 | # UserindoBot 2 | # Copyright (C) 2020 UserindoBot Team, 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | 17 | import telegram.ext as tg 18 | from telegram import Update 19 | 20 | from ubotindo import LOGGER 21 | 22 | try: 23 | from ubotindo import CUSTOM_CMD 24 | except BaseException: 25 | CUSTOM_CMD = False 26 | 27 | if CUSTOM_CMD: 28 | CMD_STARTERS = CUSTOM_CMD 29 | LOGGER.debug("Bot custom command handler = \"%s\"", CMD_STARTERS) 30 | else: 31 | CMD_STARTERS = "/" 32 | 33 | 34 | class CustomCommandHandler(tg.CommandHandler): 35 | def __init__(self, command, callback, **kwargs): 36 | if "admin_ok" in kwargs: 37 | del kwargs["admin_ok"] 38 | super().__init__(command, callback, **kwargs) 39 | 40 | def check_update(self, update): 41 | if isinstance(update, Update) and update.effective_message: 42 | message = update.effective_message 43 | 44 | if message.text and len(message.text) > 1: 45 | fst_word = message.text.split(None, 1)[0] 46 | if len(fst_word) > 1 and any( 47 | fst_word.startswith(start) for start in CMD_STARTERS 48 | ): 49 | args = message.text.split()[1:] 50 | command = fst_word[1:].split("@") 51 | command.append( 52 | message.bot.username 53 | ) # in case the command was sent without a username 54 | 55 | if not ( 56 | command[0].lower() in self.command 57 | and command[1].lower() == message.bot.username.lower() 58 | ): 59 | return None 60 | 61 | filter_result = self.filters(update) 62 | if filter_result: 63 | return args, filter_result 64 | else: 65 | return False 66 | -------------------------------------------------------------------------------- /ubotindo/modules/no_sql/disable_db.py: -------------------------------------------------------------------------------- 1 | # UserindoBot 2 | # Copyright (C) 2020 UserindoBot Team, 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | """Group disabled commands database.""" 17 | 18 | from ubotindo.modules.no_sql import get_collection 19 | 20 | DISABLED_COMMANDS = get_collection("DISABLED_COMMANDS") 21 | 22 | DISABLED = {} 23 | 24 | 25 | def disable_command(chat_id, disable) -> bool: 26 | data = DISABLED_COMMANDS.find_one( 27 | {'chat_id': chat_id, 'command': disable}) 28 | if not data: 29 | DISABLED.setdefault(str(chat_id), set()).add(disable) 30 | 31 | DISABLED_COMMANDS.insert_one( 32 | {'chat_id': chat_id, 'command': disable}) 33 | return True 34 | return False 35 | 36 | 37 | def enable_command(chat_id, enable) -> bool: 38 | data = DISABLED_COMMANDS.find_one( 39 | {'chat_id': chat_id, 'command': enable} 40 | ) 41 | if data: 42 | if enable in DISABLED.get(str(chat_id)): # sanity check 43 | DISABLED.setdefault(str(chat_id), set()).remove(enable) 44 | 45 | DISABLED_COMMANDS.delete_one( 46 | {'chat_id': chat_id, 'command': enable} 47 | ) 48 | return True 49 | return False 50 | 51 | 52 | def is_command_disabled(chat_id, cmd) -> bool: 53 | return cmd in DISABLED.get(str(chat_id), set()) 54 | 55 | 56 | def get_all_disabled(chat_id) -> dict: 57 | return DISABLED.get(str(chat_id), set()) 58 | 59 | 60 | def num_chats() -> int: 61 | chats = DISABLED_COMMANDS.distinct('chat_id') 62 | return len(chats) 63 | 64 | 65 | def num_disabled() -> int: 66 | return DISABLED_COMMANDS.count_documents({}) 67 | 68 | 69 | def migrate_chat(old_chat_id, new_chat_id) -> None: 70 | DISABLED_COMMANDS.update_many( 71 | {'chat_id': old_chat_id}, {"$set": {'chat_id': new_chat_id}} 72 | ) 73 | 74 | if str(old_chat_id) in DISABLED: 75 | DISABLED[str(old_chat_id)] = DISABLED.get(str(old_chat_id), set()) 76 | 77 | 78 | def __load_disabled_commands() -> None: 79 | global DISABLED 80 | all_chats = DISABLED_COMMANDS.find() 81 | for chat in all_chats: 82 | DISABLED.setdefault(chat["chat_id"], set()).add(chat["command"]) 83 | 84 | 85 | __load_disabled_commands() 86 | -------------------------------------------------------------------------------- /ubotindo/modules/helper_funcs/filters.py: -------------------------------------------------------------------------------- 1 | # UserindoBot 2 | # Copyright (C) 2020 UserindoBot Team, 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | 17 | from telegram import Message 18 | from telegram.ext import MessageFilter 19 | 20 | from ubotindo import SUPPORT_USERS, SUDO_USERS, DEV_USERS 21 | 22 | 23 | class CustomFilters(object): 24 | class _Supporters(MessageFilter): 25 | def filter(self, message: Message): 26 | return bool( 27 | message.from_user 28 | and message.from_user.id in SUPPORT_USERS 29 | or message.from_user 30 | and message.from_user.id in SUDO_USERS 31 | or message.from_user 32 | and message.from_user.id in DEV_USERS 33 | ) 34 | 35 | support_filter = _Supporters() 36 | 37 | class _Sudoers(MessageFilter): 38 | def filter(self, message: Message): 39 | return bool( 40 | message.from_user 41 | and message.from_user.id in SUDO_USERS 42 | or message.from_user 43 | and message.from_user.id in DEV_USERS 44 | ) 45 | 46 | sudo_filter = _Sudoers() 47 | 48 | class _Devs(MessageFilter): 49 | def filter(self, message: Message): 50 | return bool( 51 | message.from_user and message.from_user.id in DEV_USERS 52 | ) 53 | 54 | dev_filter = _Devs() 55 | 56 | class _MimeType(MessageFilter): 57 | def __init__(self, mimetype): 58 | self.mime_type = mimetype 59 | self.name = "CustomFilters.mime_type({})".format(self.mime_type) 60 | 61 | def filter(self, message: Message): 62 | return bool( 63 | message.document 64 | and message.document.mime_type == self.mime_type 65 | ) 66 | 67 | mime_type = _MimeType 68 | 69 | class _HasText(MessageFilter): 70 | def filter(self, message: Message): 71 | return bool( 72 | message.text 73 | or message.sticker 74 | or message.photo 75 | or message.document 76 | or message.video 77 | ) 78 | 79 | has_text = _HasText() 80 | -------------------------------------------------------------------------------- /ubotindo/modules/no_sql/users_db.py: -------------------------------------------------------------------------------- 1 | # UserindoBot 2 | # Copyright (C) 2020 UserindoBot Team, 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | """User database utils.""" 17 | 18 | from ubotindo import dispatcher 19 | from ubotindo.modules.no_sql import get_collection 20 | 21 | 22 | USERS_DB = get_collection("USERS") 23 | CHATS_DB = get_collection("CHATS") 24 | CHAT_MEMBERS_DB = get_collection("CHAT_MEMBERS") 25 | 26 | 27 | def ensure_bot_in_db(): 28 | USERS_DB.update_one( 29 | {'_id': dispatcher.bot.id}, 30 | {"$set": {'username': dispatcher.bot.username}}, 31 | upsert=True, 32 | ) 33 | 34 | 35 | def update_user(user_id, username, chat_id=None, chat_name=None): 36 | USERS_DB.update_one( 37 | {'_id': user_id}, 38 | {"$set": {'username': username}}, 39 | upsert=True) 40 | 41 | if not (chat_id or chat_name): 42 | return 43 | 44 | CHATS_DB.update_one( 45 | {'chat_id': chat_id}, 46 | {"$set": {'chat_name': chat_name}}, 47 | upsert=True) 48 | 49 | member = CHAT_MEMBERS_DB.find_one({'chat_id': chat_id, 'user_id': user_id}) 50 | if member is None: 51 | CHAT_MEMBERS_DB.insert_one({'chat_id': chat_id, 'user_id': user_id}) 52 | 53 | 54 | def get_userid_by_name(username) -> dict: 55 | return [user for user in USERS_DB.find({'username': username})] 56 | 57 | 58 | def get_name_by_userid(user_id) -> dict: 59 | return [user for user in USERS_DB.find_one({'_id': user_id})] 60 | 61 | 62 | def get_chat_members(chat_id) -> list: 63 | return [member for member in CHAT_MEMBERS_DB.find({'chat_id': chat_id})] 64 | 65 | 66 | def get_all_chats() -> list: 67 | return [chat for chat in CHATS_DB.find()] 68 | 69 | 70 | def get_all_users() -> list: 71 | return [user for user in USERS_DB.find()] 72 | 73 | 74 | def get_user_num_chats(user_id) -> int: 75 | return CHAT_MEMBERS_DB.count_documents( 76 | {'user_id': user_id}) 77 | 78 | 79 | def num_chats() -> int: 80 | return CHATS_DB.count_documents({}) 81 | 82 | 83 | def num_users() -> int: 84 | return USERS_DB.count_documents({}) 85 | 86 | 87 | def rem_chat(chat_id) -> None: 88 | CHATS_DB.delete_one({'chat_id': chat_id}) 89 | 90 | 91 | def migrate_chat(old_chat_id, new_chat_id) -> None: 92 | CHATS_DB.update_one({'chat_id': old_chat_id}, {"$set": {'chat_id': new_chat_id}}) 93 | 94 | chat_members = ( 95 | CHAT_MEMBERS_DB.find({'chat_id': old_chat_id}) 96 | ) 97 | for member in chat_members: 98 | CHAT_MEMBERS_DB.update_one({'chat_id': member["chat_id"]}, {"$set": {'chat_id': new_chat_id}}) 99 | 100 | 101 | ensure_bot_in_db() 102 | -------------------------------------------------------------------------------- /ubotindo/modules/no_sql/gban_db.py: -------------------------------------------------------------------------------- 1 | # UserindoBot 2 | # Copyright (C) 2020 UserindoBot Team, 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | """Global bans utils.""" 17 | 18 | from ubotindo.modules.no_sql import get_collection 19 | 20 | GBAN_USER = get_collection("GBANS") 21 | GBAN_SETTINGS = get_collection("GBAN_SETTINGS") 22 | GBANNED_LIST = set() 23 | GBANSTAT_LIST = set() 24 | 25 | 26 | def gban_user(user_id, name, reason=None) -> None: 27 | GBAN_USER.insert_one({ 28 | '_id': user_id, 29 | 'name': name, 30 | 'reason': reason, 31 | }) 32 | __load_gbanned_userid_list() 33 | 34 | 35 | def update_gban_reason(user_id, name, reason) -> str: 36 | data = GBAN_USER.find_one_and_update( 37 | {'_id': user_id}, 38 | {"$set": {'name': name, 'reason': reason}}, 39 | upsert=False) 40 | return data["reason"] 41 | 42 | 43 | def ungban_user(user_id) -> None: 44 | GBAN_USER.delete_one({'_id': user_id}) 45 | __load_gbanned_userid_list() 46 | 47 | 48 | def is_user_gbanned(user_id): 49 | return user_id in GBANNED_LIST 50 | 51 | 52 | def get_gbanned_user(user_id): 53 | return GBAN_USER.find_one({'_id': user_id}) 54 | 55 | 56 | def get_gban_list() -> dict: 57 | return [i for i in GBAN_USER.find()] 58 | 59 | 60 | def enable_gbans(chat_id) -> None: 61 | __gban_setting(chat_id, True) 62 | if str(chat_id) in GBANSTAT_LIST: 63 | GBANSTAT_LIST.remove(str(chat_id)) 64 | 65 | 66 | def disable_gbans(chat_id) -> None: 67 | __gban_setting(chat_id, False) 68 | GBANSTAT_LIST.add(str(chat_id)) 69 | 70 | 71 | def __gban_setting(chat_id, setting: bool=True) -> None: 72 | if GBAN_SETTINGS.find_one({'_id': chat_id}): 73 | GBAN_SETTINGS.update_one( 74 | {'_id': chat_id}, {"$set": {'setting': setting}}) 75 | else: 76 | GBAN_SETTINGS.insert_one({'_id': chat_id, 'setting': setting}) 77 | 78 | 79 | def does_chat_gban(chat_id) -> bool: 80 | return str(chat_id) not in GBANSTAT_LIST 81 | 82 | 83 | def num_gbanned_users() -> int: 84 | return len(GBANNED_LIST) 85 | 86 | 87 | def __load_gbanned_userid_list() -> None: 88 | global GBANNED_LIST 89 | GBANNED_LIST = {i["_id"] for i in GBAN_USER.find()} 90 | 91 | 92 | def __load_gban_stat_list() -> None: 93 | global GBANSTAT_LIST 94 | GBANSTAT_LIST = { 95 | str(i["_id"]) 96 | for i in GBAN_SETTINGS.find() 97 | if not i["setting"] 98 | } 99 | 100 | 101 | def migrate_chat(old_chat_id, new_chat_id) -> None: 102 | old = GBAN_SETTINGS.find_one_and_delete({'_id': old_chat_id}) 103 | if old: 104 | setting = old["setting"] 105 | else: 106 | setting = True 107 | GBAN_SETTINGS.update_one( 108 | {'_id': new_chat_id}, 109 | {"$set": {'setting': setting}}, 110 | upsert=True, 111 | ) 112 | 113 | 114 | # Create in memory userid to avoid disk access 115 | __load_gbanned_userid_list() 116 | __load_gban_stat_list() 117 | -------------------------------------------------------------------------------- /ubotindo/modules/no_sql/blacklist_db.py: -------------------------------------------------------------------------------- 1 | # UserindoBot 2 | # Copyright (C) 2020 UserindoBot Team, 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | """Chat blacklist database.""" 17 | 18 | 19 | from ubotindo.modules.no_sql import get_collection 20 | 21 | 22 | BL = get_collection("BLACKLIST") 23 | BL_SETTING = get_collection("BLACKLIST_SETTINGS") 24 | 25 | 26 | CHAT_BLACKLISTS = {} 27 | CHAT_SETTINGS_BLACKLISTS = {} 28 | 29 | 30 | def add_to_blacklist(chat_id, trigger): 31 | BL.find_one_and_update( 32 | {'chat_id': chat_id, 'trigger': trigger}, 33 | {"$set": {'chat_id': chat_id, 'trigger': trigger}}, 34 | upsert=True) 35 | global CHAT_BLACKLISTS 36 | if CHAT_BLACKLISTS.get(str(chat_id), set()) == set(): 37 | CHAT_BLACKLISTS[str(chat_id)] = {trigger} 38 | else: 39 | CHAT_BLACKLISTS.get(str(chat_id), set()).add(trigger) 40 | 41 | 42 | def rm_from_blacklist(chat_id, trigger) -> bool: 43 | data = BL.find_one_and_delete( 44 | {'chat_id': chat_id, 'trigger': trigger} 45 | ) 46 | if data: 47 | if trigger in CHAT_BLACKLISTS.get(str(chat_id), set()): 48 | CHAT_BLACKLISTS.get(str(chat_id), set()).remove(trigger) 49 | return True 50 | return False 51 | 52 | 53 | def get_chat_blacklist(chat_id) -> set: 54 | return CHAT_BLACKLISTS.get(str(chat_id), set()) 55 | 56 | 57 | def num_blacklist_filters() -> int: 58 | return BL.count_documents({}) 59 | 60 | 61 | def num_blacklist_chat_filters(chat_id) -> int: 62 | return BL.count_documents({'chat_id': chat_id}) 63 | 64 | 65 | def num_blacklist_filter_chats() -> int: 66 | data = BL.distinct('chat_id') 67 | return len(data) 68 | 69 | 70 | def set_blacklist_strength(chat_id, blacklist_type, value): 71 | """For blacklist type settings 72 | `blacklist_type` (int): 73 | - 0 = nothing 74 | - 1 = delete 75 | - 2 = warn 76 | - 3 = mute 77 | - 4 = kick 78 | - 5 = ban 79 | - 6 = tban 80 | - 7 = tmute. 81 | """ 82 | global CHAT_SETTINGS_BLACKLISTS 83 | BL_SETTING.update_one( 84 | {'chat_id': chat_id}, 85 | {"$set": {'blacklist_type': int(blacklist_type), 'value': str(value)}}, 86 | upsert=True 87 | ) 88 | CHAT_SETTINGS_BLACKLISTS[str(chat_id)] = { 89 | "blacklist_type": int(blacklist_type), 90 | "value": value, 91 | } 92 | 93 | 94 | def get_blacklist_setting(chat_id) -> [int, str]: 95 | setting = CHAT_SETTINGS_BLACKLISTS.get(str(chat_id)) 96 | if setting: 97 | return setting["blacklist_type"], setting["value"] 98 | else: 99 | return 1, "0" 100 | 101 | 102 | def __load_chat_blacklists(): 103 | global CHAT_BLACKLISTS 104 | for chat in BL.find(): 105 | CHAT_BLACKLISTS[chat["chat_id"]] = [] 106 | 107 | for x in BL.find(): 108 | CHAT_BLACKLISTS[x["chat_id"]] += [x["trigger"]] 109 | 110 | CHAT_BLACKLISTS = {str(x): set(y) for x, y in CHAT_BLACKLISTS.items()} 111 | 112 | 113 | def __load_chat_settings_blacklists(): 114 | global CHAT_SETTINGS_BLACKLISTS 115 | for x in BL_SETTING.find(): 116 | CHAT_SETTINGS_BLACKLISTS[x["chat_id"]] = { 117 | "blacklist_type": x["blacklist_type"], 118 | "value": x["value"], 119 | } 120 | 121 | 122 | def migrate_chat(old_chat_id, new_chat_id): 123 | BL.update_many( 124 | {'chat_id': old_chat_id}, 125 | {"$set": {'chat_id':new_chat_id}} 126 | ) 127 | __load_chat_blacklists() 128 | __load_chat_settings_blacklists() 129 | 130 | 131 | __load_chat_blacklists() 132 | __load_chat_settings_blacklists() -------------------------------------------------------------------------------- /ubotindo/__init__.py: -------------------------------------------------------------------------------- 1 | """Initial app framework""" 2 | # UserindoBot 3 | # Copyright (C) 2020 UserindoBot Team, 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program. If not, see . 17 | 18 | import logging 19 | import os 20 | import sys 21 | import spamwatch 22 | import telegram.ext as tg 23 | from dotenv import load_dotenv 24 | 25 | # enable logging 26 | logging.basicConfig( 27 | format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", 28 | handlers=[ 29 | logging.FileHandler("ubotindo-log.txt"), 30 | logging.StreamHandler(), 31 | ], 32 | level=logging.INFO, 33 | ) 34 | 35 | LOGGER = logging.getLogger(__name__) 36 | 37 | # if version < 3.6, stop bot. 38 | if sys.version_info[0] < 3 or sys.version_info[1] < 6: 39 | LOGGER.error( 40 | "You MUST have a python version of at least 3.6!!!." 41 | ) 42 | sys.exit(1) 43 | 44 | load_dotenv("config.env") 45 | 46 | CONFIG_CHECK = os.environ.get( 47 | "_____REMOVE_____THIS_____LINE_____") or None 48 | 49 | if CONFIG_CHECK: 50 | LOGGER.info( 51 | "Please remove the line mentioned in the first hashtag from the config.env file" 52 | ) 53 | sys.exit(1) 54 | 55 | TOKEN = os.environ.get("TOKEN") 56 | OWNER_ID = int(os.environ.get("OWNER_ID") or 0) 57 | MESSAGE_DUMP = os.environ.get("MESSAGE_DUMP") or None 58 | GBAN_LOGS = os.environ.get("GBAN_LOGS") or None 59 | OWNER_USERNAME = os.environ.get("OWNER_USERNAME") or None 60 | DEV_USERS = set(int(x) for x in os.environ.get("DEV_USERS", "").split()) 61 | SUDO_USERS = set(int(x) for x in os.environ.get("SUDO_USERS", "").split()) 62 | SUPPORT_USERS = set( 63 | int(x) for x in os.environ.get( 64 | "SUPPORT_USERS", 65 | "").split()) 66 | WHITELIST_USERS = set( 67 | int(x) for x in os.environ.get( 68 | "WHITELIST_USERS", 69 | "").split()) 70 | WHITELIST_CHATS = set( 71 | int(x) for x in os.environ.get( 72 | "WHITELIST_CHATS", 73 | "").split()) 74 | BLACKLIST_CHATS = set( 75 | int(x) for x in os.environ.get( 76 | "BLACKLIST_CHATS", 77 | "").split()) 78 | WEBHOOK = eval(os.environ.get("WEBHOOK") or "False") 79 | URL = os.environ.get("URL", "") 80 | PORT = int(os.environ.get("PORT", 5000)) 81 | CERT_PATH = os.environ.get("CERT_PATH") or None 82 | DB_URI = os.environ.get("DATABASE_URL") 83 | MONGO_URI = os.environ.get("MONGO_DB_URI") 84 | DONATION_LINK = os.environ.get("DONATION_LINK") or None 85 | LOAD = os.environ.get("LOAD", "").split() 86 | NO_LOAD = os.environ.get("NO_LOAD", "").split() 87 | DEL_CMDS = eval(os.environ.get("DEL_CMDS") or "False") 88 | STRICT_GBAN = eval(os.environ.get("STRICT_GBAN") or "True") 89 | WORKERS = int(os.environ.get("WORKERS", 8)) 90 | BAN_STICKER = os.environ.get("BAN_STICKER", "CAADAgADOwADPPEcAXkko5EB3YGYAg") 91 | CUSTOM_CMD = os.environ.get("CUSTOM_CMD") or False 92 | API_WEATHER = os.environ.get("API_OPENWEATHER") or None 93 | WALL_API = os.environ.get("WALL_API") or None 94 | SPAMWATCH = os.environ.get("SPAMWATCH_API") or None 95 | LASTFM_API_KEY = os.environ.get("LASTFM_API_KEY") or None 96 | 97 | # add owner to devusers 98 | DEV_USERS.add(OWNER_ID) 99 | 100 | # make the Var type bool 101 | if str(CUSTOM_CMD).lower() == "false": 102 | CUSTOM_CMD = False 103 | 104 | # Pass if SpamWatch token not set. 105 | if SPAMWATCH is None: 106 | spamwtc = None # pylint: disable=C0103 107 | LOGGER.warning("Invalid spamwatch api") 108 | else: 109 | spamwtc = spamwatch.Client(SPAMWATCH) 110 | 111 | # Everything Init with this 112 | updater = tg.Updater(TOKEN, workers=WORKERS) 113 | dispatcher = updater.dispatcher 114 | 115 | # Declare user rank 116 | DEV_USERS = list(DEV_USERS) 117 | SUDO_USERS = list(SUDO_USERS) 118 | SUPPORT_USERS = list(SUPPORT_USERS) 119 | 120 | STAFF = DEV_USERS + SUDO_USERS + SUPPORT_USERS 121 | STAFF_USERS = list(STAFF) 122 | 123 | WHITELIST_USERS = list(WHITELIST_USERS) 124 | 125 | # Load at end to ensure all prev variables have been set 126 | # pylint: disable=C0413 127 | from ubotindo.modules.helper_funcs.handlers import ( 128 | CustomCommandHandler, 129 | ) 130 | 131 | if CUSTOM_CMD and len(CUSTOM_CMD) >= 1: 132 | tg.CommandHandler = CustomCommandHandler 133 | -------------------------------------------------------------------------------- /ubotindo/modules/translator.py: -------------------------------------------------------------------------------- 1 | # UserindoBot 2 | # Copyright (C) 2020 UserindoBot Team, 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | 17 | import json 18 | import os 19 | 20 | import requests 21 | from emoji import UNICODE_EMOJI 22 | from google_trans_new import google_translator, LANGUAGES 23 | from gtts import gTTS 24 | from telegram import ChatAction 25 | 26 | from ubotindo import dispatcher 27 | from ubotindo.modules.disable import DisableAbleCommandHandler 28 | from ubotindo.modules.helper_funcs.alternate import send_action, typing_action 29 | 30 | 31 | @typing_action 32 | def gtrans(update, context): 33 | msg = update.effective_message 34 | args = context.args 35 | lang = " ".join(args) 36 | if not lang: 37 | lang = "en" 38 | try: 39 | translate_text = ( 40 | msg.reply_to_message.text or 41 | msg.reply_to_message.caption or 42 | msg.reply_to_message.poll.question 43 | ) 44 | except AttributeError: 45 | return msg.reply_text("Give me the text to translate!") 46 | 47 | ignore_text = UNICODE_EMOJI.keys() 48 | for emoji in ignore_text: 49 | if emoji in translate_text: 50 | translate_text = translate_text.replace(emoji, "") 51 | 52 | translator = google_translator() 53 | try: 54 | translated = translator.translate(translate_text, lang_tgt=lang) 55 | source_lan = translator.detect(translate_text)[1].title() 56 | des_lan = LANGUAGES.get(lang).title() 57 | msg.reply_text( 58 | "Translated from {} to {}.\n {}".format( 59 | source_lan, des_lan, translated 60 | ) 61 | ) 62 | except BaseException: 63 | msg.reply_text("Error! invalid language code.") 64 | 65 | 66 | @send_action(ChatAction.RECORD_AUDIO) 67 | def gtts(update, context): 68 | msg = update.effective_message 69 | reply = " ".join(context.args) 70 | if not reply: 71 | if msg.reply_to_message: 72 | reply = msg.reply_to_message.text 73 | else: 74 | return msg.reply_text( 75 | "Reply to some message or enter some text to convert it into audio format!" 76 | ) 77 | for x in "\n": 78 | reply = reply.replace(x, "") 79 | try: 80 | tts = gTTS(reply) 81 | tts.save("ubotindo.mp3") 82 | with open("ubotindo.mp3", "rb") as speech: 83 | msg.reply_audio(speech) 84 | finally: 85 | if os.path.isfile("ubotindo.mp3"): 86 | os.remove("ubotindo.mp3") 87 | 88 | 89 | # Open API key 90 | API_KEY = "6ae0c3a0-afdc-4532-a810-82ded0054236" 91 | URL = "http://services.gingersoftware.com/Ginger/correct/json/GingerTheText" 92 | 93 | 94 | @typing_action 95 | def spellcheck(update, context): 96 | if update.effective_message.reply_to_message: 97 | msg = update.effective_message.reply_to_message 98 | 99 | params = dict( 100 | lang="US", clientVersion="2.0", apiKey=API_KEY, text=msg.text 101 | ) 102 | 103 | res = requests.get(URL, params=params) 104 | changes = json.loads(res.text).get("LightGingerTheTextResult") 105 | curr_string = "" 106 | prev_end = 0 107 | 108 | for change in changes: 109 | start = change.get("From") 110 | end = change.get("To") + 1 111 | suggestions = change.get("Suggestions") 112 | if suggestions: 113 | # should look at this list more 114 | sugg_str = suggestions[0].get("Text") 115 | curr_string += msg.text[prev_end:start] + sugg_str 116 | prev_end = end 117 | 118 | curr_string += msg.text[prev_end:] 119 | update.effective_message.reply_text(curr_string) 120 | else: 121 | update.effective_message.reply_text( 122 | "Reply to some message to get grammar corrected text!" 123 | ) 124 | 125 | 126 | __help__ = """ 127 | × /tr or /tl: - To translate to your language, by default language is set to english, use `/tr ` for some other language! 128 | × /spell: - As a reply to get grammar corrected text of gibberish message. 129 | × /tts: - To some message to convert it into audio format! 130 | """ 131 | __mod_name__ = "Translate" 132 | 133 | dispatcher.add_handler( 134 | DisableAbleCommandHandler( 135 | ["tr", "tl"], gtrans, pass_args=True, run_async=True 136 | ) 137 | ) 138 | dispatcher.add_handler( 139 | DisableAbleCommandHandler("tts", gtts, pass_args=True, run_async=True) 140 | ) 141 | dispatcher.add_handler( 142 | DisableAbleCommandHandler("spell", spellcheck, run_async=True) 143 | ) 144 | -------------------------------------------------------------------------------- /ubotindo/modules/regex.py: -------------------------------------------------------------------------------- 1 | # UserindoBot 2 | # Copyright (C) 2020 UserindoBot Team, 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | 17 | import re 18 | import sre_constants 19 | 20 | import telegram 21 | from telegram.ext import Filters 22 | 23 | from ubotindo import LOGGER, dispatcher 24 | from ubotindo.modules.disable import DisableAbleMessageHandler 25 | 26 | DELIMITERS = ("/", ":", "|", "_") 27 | 28 | 29 | def infinite_checker(repl): 30 | regex = [ 31 | r"\((.{1,}[\+\*]){1,}\)[\+\*].", 32 | r"[\(\[].{1,}\{\d(,)?\}[\)\]]\{\d(,)?\}", 33 | r"\(.{1,}\)\{.{1,}(,)?\}\(.*\)(\+|\* |\{.*\})", 34 | ] 35 | for match in regex: 36 | status = re.search(match, repl) 37 | if status: 38 | return True 39 | else: 40 | return False 41 | 42 | 43 | def separate_sed(sed_string): 44 | if ( 45 | len(sed_string) >= 3 46 | and sed_string[1] in DELIMITERS 47 | and sed_string.count(sed_string[1]) >= 2 48 | ): 49 | delim = sed_string[1] 50 | start = counter = 2 51 | while counter < len(sed_string): 52 | if sed_string[counter] == "\\": 53 | counter += 1 54 | 55 | elif sed_string[counter] == delim: 56 | replace = sed_string[start:counter] 57 | counter += 1 58 | start = counter 59 | break 60 | 61 | counter += 1 62 | 63 | else: 64 | return None 65 | 66 | while counter < len(sed_string): 67 | if ( 68 | sed_string[counter] == "\\" 69 | and counter + 1 < len(sed_string) 70 | and sed_string[counter + 1] == delim 71 | ): 72 | sed_string = sed_string[:counter] + sed_string[counter + 1 :] 73 | 74 | elif sed_string[counter] == delim: 75 | replace_with = sed_string[start:counter] 76 | counter += 1 77 | break 78 | 79 | counter += 1 80 | else: 81 | return replace, sed_string[start:], "" 82 | 83 | flags = "" 84 | if counter < len(sed_string): 85 | flags = sed_string[counter:] 86 | return replace, replace_with, flags.lower() 87 | 88 | 89 | def sed(update, context): 90 | sed_result = separate_sed(update.effective_message.text) 91 | if sed_result and update.effective_message.reply_to_message: 92 | if update.effective_message.reply_to_message.text: 93 | to_fix = update.effective_message.reply_to_message.text 94 | elif update.effective_message.reply_to_message.caption: 95 | to_fix = update.effective_message.reply_to_message.caption 96 | else: 97 | return 98 | 99 | repl, repl_with, flags = sed_result 100 | 101 | if not repl: 102 | update.effective_message.reply_to_message.reply_text( 103 | "You're trying to replace... " "nothing with something?" 104 | ) 105 | return 106 | 107 | try: 108 | 109 | # Protects bot from retarded geys -_- 110 | if infinite_checker(repl): 111 | return update.effective_message.reply_text("Nice try -_-") 112 | 113 | if "i" in flags and "g" in flags: 114 | text = re.sub(repl, repl_with, to_fix, flags=re.I).strip() 115 | elif "i" in flags: 116 | text = re.sub( 117 | repl, repl_with, to_fix, count=1, flags=re.I 118 | ).strip() 119 | elif "g" in flags: 120 | text = re.sub(repl, repl_with, to_fix).strip() 121 | else: 122 | text = re.sub(repl, repl_with, to_fix, count=1).strip() 123 | except sre_constants.error: 124 | LOGGER.warning(update.effective_message.text) 125 | LOGGER.exception("SRE constant error") 126 | update.effective_message.reply_text( 127 | "Do you even sed? Apparently not." 128 | ) 129 | return 130 | 131 | # empty string errors -_- 132 | if len(text) >= telegram.MAX_MESSAGE_LENGTH: 133 | update.effective_message.reply_text( 134 | "The result of the sed command was too long for \ 135 | telegram!" 136 | ) 137 | elif text: 138 | update.effective_message.reply_to_message.reply_text(text) 139 | 140 | 141 | SED_HANDLER = DisableAbleMessageHandler( 142 | Filters.regex(r"s([{}]).*?\1.*".format("".join(DELIMITERS))), 143 | sed, 144 | friendly="sed", 145 | run_async=True, 146 | ) 147 | 148 | dispatcher.add_handler(SED_HANDLER) 149 | -------------------------------------------------------------------------------- /ubotindo/modules/sql/antiflood_sql.py: -------------------------------------------------------------------------------- 1 | # UserindoBot 2 | # Copyright (C) 2020 UserindoBot Team, 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | 17 | import threading 18 | 19 | from sqlalchemy import String, Column, Integer, UnicodeText 20 | 21 | from ubotindo.modules.sql import SESSION, BASE 22 | 23 | DEF_COUNT = 0 24 | DEF_LIMIT = 0 25 | DEF_OBJ = (None, DEF_COUNT, DEF_LIMIT) 26 | 27 | 28 | class FloodControl(BASE): 29 | __tablename__ = "antiflood" 30 | chat_id = Column(String(14), primary_key=True) 31 | user_id = Column(Integer) 32 | count = Column(Integer, default=DEF_COUNT) 33 | limit = Column(Integer, default=DEF_LIMIT) 34 | 35 | def __init__(self, chat_id): 36 | self.chat_id = str(chat_id) # ensure string 37 | 38 | def __repr__(self): 39 | return "" % self.chat_id 40 | 41 | 42 | class FloodSettings(BASE): 43 | __tablename__ = "antiflood_settings" 44 | chat_id = Column(String(14), primary_key=True) 45 | flood_type = Column(Integer, default=1) 46 | value = Column(UnicodeText, default="0") 47 | 48 | def __init__(self, chat_id, flood_type=1, value="0"): 49 | self.chat_id = str(chat_id) 50 | self.flood_type = flood_type 51 | self.value = value 52 | 53 | def __repr__(self): 54 | return "<{} will executing {} for flood.>".format( 55 | self.chat_id, self.flood_type 56 | ) 57 | 58 | 59 | FloodControl.__table__.create(checkfirst=True) 60 | FloodSettings.__table__.create(checkfirst=True) 61 | 62 | INSERTION_FLOOD_LOCK = threading.RLock() 63 | INSERTION_FLOOD_SETTINGS_LOCK = threading.RLock() 64 | 65 | CHAT_FLOOD = {} 66 | 67 | 68 | def set_flood(chat_id, amount): 69 | with INSERTION_FLOOD_LOCK: 70 | flood = SESSION.query(FloodControl).get(str(chat_id)) 71 | if not flood: 72 | flood = FloodControl(str(chat_id)) 73 | 74 | flood.user_id = None 75 | flood.limit = amount 76 | 77 | CHAT_FLOOD[str(chat_id)] = (None, DEF_COUNT, amount) 78 | 79 | SESSION.add(flood) 80 | SESSION.commit() 81 | 82 | 83 | def update_flood(chat_id: str, user_id) -> bool: 84 | if str(chat_id) in CHAT_FLOOD: 85 | curr_user_id, count, limit = CHAT_FLOOD.get(str(chat_id), DEF_OBJ) 86 | 87 | if limit == 0: # no antiflood 88 | return False 89 | 90 | if user_id != curr_user_id or user_id is None: # other user 91 | CHAT_FLOOD[str(chat_id)] = (user_id, DEF_COUNT, limit) 92 | return False 93 | 94 | count += 1 95 | if count > limit: # too many msgs, kick 96 | CHAT_FLOOD[str(chat_id)] = (None, DEF_COUNT, limit) 97 | return True 98 | 99 | # default -> update 100 | CHAT_FLOOD[str(chat_id)] = (user_id, count, limit) 101 | return False 102 | 103 | 104 | def get_flood_limit(chat_id): 105 | return CHAT_FLOOD.get(str(chat_id), DEF_OBJ)[2] 106 | 107 | 108 | def set_flood_strength(chat_id, flood_type, value): 109 | # for flood_type 110 | # 1 = ban 111 | # 2 = kick 112 | # 3 = mute 113 | # 4 = tban 114 | # 5 = tmute 115 | with INSERTION_FLOOD_SETTINGS_LOCK: 116 | curr_setting = SESSION.query(FloodSettings).get(str(chat_id)) 117 | if not curr_setting: 118 | curr_setting = FloodSettings( 119 | chat_id, flood_type=int(flood_type), value=value 120 | ) 121 | 122 | curr_setting.flood_type = int(flood_type) 123 | curr_setting.value = str(value) 124 | 125 | SESSION.add(curr_setting) 126 | SESSION.commit() 127 | 128 | 129 | def get_flood_setting(chat_id): 130 | try: 131 | setting = SESSION.query(FloodSettings).get(str(chat_id)) 132 | if setting: 133 | return setting.flood_type, setting.value 134 | else: 135 | return 1, "0" 136 | 137 | finally: 138 | SESSION.close() 139 | 140 | 141 | def migrate_chat(old_chat_id, new_chat_id): 142 | with INSERTION_FLOOD_LOCK: 143 | flood = SESSION.query(FloodControl).get(str(old_chat_id)) 144 | if flood: 145 | CHAT_FLOOD[str(new_chat_id)] = CHAT_FLOOD.get( 146 | str(old_chat_id), DEF_OBJ 147 | ) 148 | flood.chat_id = str(new_chat_id) 149 | SESSION.commit() 150 | 151 | SESSION.close() 152 | 153 | 154 | def __load_flood_settings(): 155 | global CHAT_FLOOD 156 | try: 157 | all_chats = SESSION.query(FloodControl).all() 158 | CHAT_FLOOD = { 159 | chat.chat_id: (None, DEF_COUNT, chat.limit) for chat in all_chats 160 | } 161 | finally: 162 | SESSION.close() 163 | 164 | 165 | __load_flood_settings() 166 | -------------------------------------------------------------------------------- /ubotindo/modules/purge.py: -------------------------------------------------------------------------------- 1 | import html, time 2 | from typing import Optional, List 3 | 4 | from telegram import Message, Chat, Update, Bot, User 5 | from telegram.error import BadRequest 6 | from telegram.ext import Filters 7 | from telegram.utils.helpers import mention_html 8 | 9 | from ubotindo import dispatcher, LOGGER 10 | from ubotindo.modules.disable import DisableAbleCommandHandler 11 | from ubotindo.modules.helper_funcs.chat_status import user_admin, can_delete 12 | from ubotindo.modules.helper_funcs.admin_rights import user_can_delete 13 | from ubotindo.modules.log_channel import loggable 14 | 15 | 16 | @user_admin 17 | @loggable 18 | def purge(update, context): 19 | args = context.args 20 | msg = update.effective_message # type: Optional[Message] 21 | if msg.reply_to_message: 22 | user = update.effective_user # type: Optional[User] 23 | chat = update.effective_chat # type: Optional[Chat] 24 | if user_can_delete(chat, user, context.bot.id) == False: 25 | msg.reply_text("You don't have enough rights to delete message!") 26 | return "" 27 | if can_delete(chat, context.bot.id): 28 | message_id = msg.reply_to_message.message_id 29 | delete_to = msg.message_id - 1 30 | if args and args[0].isdigit(): 31 | new_del = message_id + int(args[0]) 32 | # No point deleting messages which haven't been written yet. 33 | if new_del < delete_to: 34 | delete_to = new_del 35 | 36 | for m_id in range(delete_to, message_id - 1, -1): # Reverse iteration over message ids 37 | try: 38 | context.bot.deleteMessage(chat.id, m_id) 39 | except BadRequest as err: 40 | if err.message == "Message can't be deleted": 41 | context.bot.send_message(chat.id, "Cannot delete all messages. The messages may be too old, I might " 42 | "not have delete rights, or this might not be a supergroup.") 43 | 44 | elif err.message != "Message to delete not found": 45 | LOGGER.exception("Error while purging chat messages.") 46 | 47 | try: 48 | msg.delete() 49 | except BadRequest as err: 50 | if err.message == "Message can't be deleted": 51 | context.bot.send_message(chat.id, "Cannot delete all messages. The messages may be too old, I might " 52 | "not have delete rights, or this might not be a supergroup.") 53 | 54 | elif err.message != "Message to delete not found": 55 | LOGGER.exception("Error while purging chat messages.") 56 | 57 | del_msg = context.bot.send_message(chat.id, "Purge complete.") 58 | time.sleep(2) 59 | 60 | try: 61 | del_msg.delete() 62 | 63 | except BadRequest: 64 | pass 65 | 66 | return "{}:" \ 67 | "\n#PURGE" \ 68 | "\nAdmin: {}" \ 69 | "\nPurged {} messages.".format(html.escape(chat.title), 70 | mention_html(user.id, user.first_name), 71 | delete_to - message_id) 72 | 73 | else: 74 | msg.reply_text("Reply to a message to select where to start purging from.") 75 | 76 | return "" 77 | 78 | 79 | @user_admin 80 | @loggable 81 | def del_message(update, context) -> str: 82 | if update.effective_message.reply_to_message: 83 | user = update.effective_user # type: Optional[User] 84 | chat = update.effective_chat # type: Optional[Chat] 85 | message = update.effective_message # type: Optional[Message] 86 | if user_can_delete(chat, user, context.bot.id) == False: 87 | message.reply_text("You don't have enough rights to delete message!") 88 | return "" 89 | if can_delete(chat, context.bot.id): 90 | update.effective_message.reply_to_message.delete() 91 | update.effective_message.delete() 92 | return "{}:" \ 93 | "\n#DEL" \ 94 | "\nAdmin: {}" \ 95 | "\nMessage deleted.".format(html.escape(chat.title), 96 | mention_html(user.id, user.first_name)) 97 | else: 98 | update.effective_message.reply_text("Whadya want to delete?") 99 | 100 | return "" 101 | 102 | 103 | __help__ = """ 104 | Deleting messages made easy with this command. Bot purges \ 105 | messages all together or individually. 106 | 107 | *Admin only:* 108 | × /del: Deletes the message you replied to 109 | × /purge: Deletes all messages between this and the replied to message. 110 | × /purge : Deletes the replied message, and X messages following it. 111 | """ 112 | 113 | __mod_name__ = "Purges" 114 | 115 | DELETE_HANDLER = DisableAbleCommandHandler("del", del_message, filters=Filters.chat_type.groups, run_async=True) 116 | PURGE_HANDLER = DisableAbleCommandHandler("purge", purge, filters=Filters.chat_type.groups, pass_args=True, run_async=True) 117 | 118 | dispatcher.add_handler(DELETE_HANDLER) 119 | dispatcher.add_handler(PURGE_HANDLER) 120 | -------------------------------------------------------------------------------- /ubotindo/modules/weather.py: -------------------------------------------------------------------------------- 1 | # UserindoBot 2 | # Copyright (C) 2020 UserindoBot Team, 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | 17 | import json 18 | import time 19 | 20 | import requests 21 | from pytz import country_names as cname 22 | from telegram import ParseMode 23 | from telegram.error import BadRequest 24 | 25 | from ubotindo import API_WEATHER as APPID 26 | from ubotindo import dispatcher 27 | from ubotindo.modules.disable import DisableAbleCommandHandler 28 | from ubotindo.modules.helper_funcs.alternate import typing_action 29 | 30 | 31 | @typing_action 32 | def weather(update, context): 33 | args = context.args 34 | if len(args) == 0: 35 | reply = "Write a location to check the weather." 36 | del_msg = update.effective_message.reply_text( 37 | "{}".format(reply), 38 | parse_mode=ParseMode.MARKDOWN, 39 | disable_web_page_preview=True, 40 | ) 41 | time.sleep(5) 42 | try: 43 | del_msg.delete() 44 | update.effective_message.delete() 45 | except BadRequest as err: 46 | if (err.message == "Message to delete not found") or ( 47 | err.message == "Message can't be deleted" 48 | ): 49 | return 50 | 51 | return 52 | 53 | CITY = " ".join(args) 54 | url = f"https://api.openweathermap.org/data/2.5/weather?q={CITY}&appid={APPID}" 55 | request = requests.get(url) 56 | result = json.loads(request.text) 57 | if request.status_code != 200: 58 | reply = "Location not valid." 59 | del_msg = update.effective_message.reply_text( 60 | "{}".format(reply), 61 | parse_mode=ParseMode.MARKDOWN, 62 | disable_web_page_preview=True, 63 | ) 64 | time.sleep(5) 65 | try: 66 | del_msg.delete() 67 | update.effective_message.delete() 68 | except BadRequest as err: 69 | if (err.message == "Message to delete not found") or ( 70 | err.message == "Message can't be deleted" 71 | ): 72 | return 73 | return 74 | 75 | try: 76 | cityname = result["name"] 77 | curtemp = result["main"]["temp"] 78 | feels_like = result["main"]["feels_like"] 79 | humidity = result["main"]["humidity"] 80 | wind = result["wind"]["speed"] 81 | weath = result["weather"][0] 82 | icon = weath["id"] 83 | condmain = weath["main"] 84 | conddet = weath["description"] 85 | country_name = cname[f"{result['sys']['country']}"] 86 | except KeyError: 87 | update.effective_message.reply_text("Invalid Location!") 88 | return 89 | 90 | if icon <= 232: # Rain storm 91 | icon = "⛈" 92 | elif icon <= 321: # Drizzle 93 | icon = "🌧" 94 | elif icon <= 504: # Light rain 95 | icon = "🌦" 96 | elif icon <= 531: # Cloudy rain 97 | icon = "⛈" 98 | elif icon <= 622: # Snow 99 | icon = "❄️" 100 | elif icon <= 781: # Atmosphere 101 | icon = "🌪" 102 | elif icon <= 800: # Bright 103 | icon = "☀️" 104 | elif icon <= 801: # A little cloudy 105 | icon = "⛅️" 106 | elif icon <= 804: # Cloudy 107 | icon = "☁️" 108 | kmph = str(wind * 3.6).split(".") 109 | 110 | def celsius(c): 111 | k = 273.15 112 | c = k if (c > (k - 1)) and (c < k) else c 113 | temp = str(round((c - k))) 114 | return temp 115 | 116 | def fahr(c): 117 | c1 = 9 / 5 118 | c2 = 459.67 119 | tF = c * c1 - c2 120 | if tF < 0 and tF > -1: 121 | tF = 0 122 | temp = str(round(tF)) 123 | return temp 124 | 125 | reply = f"*Current weather for {cityname}, {country_name} is*:\n\n*Temperature:* `{celsius(curtemp)}°C ({fahr(curtemp)}ºF), feels like {celsius(feels_like)}°C ({fahr(feels_like)}ºF) \n`*Condition:* `{condmain}, {conddet}` {icon}\n*Humidity:* `{humidity}%`\n*Wind:* `{kmph[0]} km/h`\n" 126 | del_msg = update.effective_message.reply_text( 127 | "{}".format(reply), 128 | parse_mode=ParseMode.MARKDOWN, 129 | disable_web_page_preview=True, 130 | ) 131 | time.sleep(30) 132 | try: 133 | del_msg.delete() 134 | update.effective_message.delete() 135 | except BadRequest as err: 136 | if (err.message == "Message to delete not found") or ( 137 | err.message == "Message can't be deleted" 138 | ): 139 | return 140 | 141 | 142 | __help__ = """ 143 | Weather module: 144 | 145 | × /weather : Gets weather information of particular place! 146 | 147 | \* To prevent spams weather command and the output will be deleted after 30 seconds 148 | """ 149 | 150 | __mod_name__ = "Weather" 151 | 152 | WEATHER_HANDLER = DisableAbleCommandHandler( 153 | "weather", weather, pass_args=True, run_async=True 154 | ) 155 | 156 | dispatcher.add_handler(WEATHER_HANDLER) 157 | -------------------------------------------------------------------------------- /ubotindo/modules/helper_funcs/misc.py: -------------------------------------------------------------------------------- 1 | # UserindoBot 2 | # Copyright (C) 2020 UserindoBot Team, 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | 17 | from typing import List, Dict 18 | 19 | from telegram import MAX_MESSAGE_LENGTH, InlineKeyboardButton, Bot, ParseMode 20 | from telegram.error import TelegramError 21 | 22 | from ubotindo import LOAD, NO_LOAD 23 | 24 | 25 | class EqInlineKeyboardButton(InlineKeyboardButton): 26 | def __eq__(self, other): 27 | return self.text == other.text 28 | 29 | def __lt__(self, other): 30 | return self.text < other.text 31 | 32 | def __gt__(self, other): 33 | return self.text > other.text 34 | 35 | 36 | def split_message(msg: str) -> List[str]: 37 | if len(msg) < MAX_MESSAGE_LENGTH: 38 | return [msg] 39 | 40 | else: 41 | lines = msg.splitlines(True) 42 | small_msg = "" 43 | result = [] 44 | for line in lines: 45 | if len(small_msg) + len(line) < MAX_MESSAGE_LENGTH: 46 | small_msg += line 47 | else: 48 | result.append(small_msg) 49 | small_msg = line 50 | else: 51 | # Else statement at the end of the for loop, so append the leftover 52 | # string. 53 | result.append(small_msg) 54 | 55 | return result 56 | 57 | 58 | def paginate_modules( 59 | page_n: int, module_dict: Dict, prefix, chat=None 60 | ) -> List: 61 | if not chat: 62 | modules = sorted( 63 | [ 64 | EqInlineKeyboardButton( 65 | x.__mod_name__, 66 | callback_data="{}_module({})".format( 67 | prefix, x.__mod_name__.lower() 68 | ), 69 | ) 70 | for x in module_dict.values() 71 | ] 72 | ) 73 | else: 74 | modules = sorted( 75 | [ 76 | EqInlineKeyboardButton( 77 | x.__mod_name__, 78 | callback_data="{}_module({},{})".format( 79 | prefix, chat, x.__mod_name__.lower() 80 | ), 81 | ) 82 | for x in module_dict.values() 83 | ] 84 | ) 85 | 86 | pairs = [ 87 | modules[i * 3 : (i + 1) * 3] 88 | for i in range((len(modules) + 3 - 1) // 3) 89 | ] 90 | round_num = len(modules) / 3 91 | calc = len(modules) - round(round_num) 92 | if calc == 1: 93 | pairs.append((modules[-1],)) 94 | elif calc == 2: 95 | pairs.append((modules[-1],)) 96 | 97 | # can only have a certain amount of buttons side by side 98 | # if len(pairs) > 7: 99 | # pairs = pairs[modulo_page * 7:7 * (modulo_page + 1)] + [ 100 | # (EqInlineKeyboardButton("<<<", callback_data="{}_prev({})".format(prefix, modulo_page)), 101 | # EqInlineKeyboardButton(">>>", callback_data="{}_next({})".format(prefix, 102 | # modulo_page)))] 103 | 104 | return pairs 105 | 106 | 107 | def send_to_list( 108 | bot: Bot, send_to: list, message: str, markdown=False, html=False 109 | ) -> None: 110 | if html and markdown: 111 | raise Exception("Can only send with either markdown or HTML!") 112 | for user_id in set(send_to): 113 | try: 114 | if markdown: 115 | bot.send_message( 116 | user_id, message, parse_mode=ParseMode.MARKDOWN 117 | ) 118 | elif html: 119 | bot.send_message(user_id, message, parse_mode=ParseMode.HTML) 120 | else: 121 | bot.send_message(user_id, message) 122 | except TelegramError: 123 | pass # ignore users who fail 124 | 125 | 126 | def build_keyboard(buttons): 127 | keyb = [] 128 | for btn in buttons: 129 | if btn.same_line and keyb: 130 | keyb[-1].append(InlineKeyboardButton(btn.name, url=btn.url)) 131 | else: 132 | keyb.append([InlineKeyboardButton(btn.name, url=btn.url)]) 133 | 134 | return keyb 135 | 136 | 137 | def revert_buttons(buttons): 138 | res = "" 139 | for btn in buttons: 140 | if btn.same_line: 141 | res += "\n[{}](buttonurl://{}:same)".format(btn.name, btn.url) 142 | else: 143 | res += "\n[{}](buttonurl://{})".format(btn.name, btn.url) 144 | 145 | return res 146 | 147 | 148 | def is_module_loaded(name): 149 | return (not LOAD or name in LOAD) and name not in NO_LOAD 150 | 151 | 152 | def build_keyboard_parser(bot, chat_id, buttons): 153 | keyb = [] 154 | for btn in buttons: 155 | if btn.url == "{rules}": 156 | btn.url = "http://t.me/{}?start={}".format(bot.username, chat_id) 157 | if btn.same_line and keyb: 158 | keyb[-1].append(InlineKeyboardButton(btn.name, url=btn.url)) 159 | else: 160 | keyb.append([InlineKeyboardButton(btn.name, url=btn.url)]) 161 | 162 | return keyb 163 | -------------------------------------------------------------------------------- /ubotindo/modules/lastfm.py: -------------------------------------------------------------------------------- 1 | # UserindoBot 2 | # Copyright (C) 2020 UserindoBot Team, 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | 17 | import time 18 | 19 | import requests 20 | from telegram import ParseMode, error 21 | from telegram.ext import CommandHandler 22 | 23 | from ubotindo import LASTFM_API_KEY, dispatcher 24 | from ubotindo.modules.no_sql import get_collection 25 | from ubotindo.modules.disable import DisableAbleCommandHandler 26 | from ubotindo.modules.helper_funcs.alternate import typing_action 27 | 28 | 29 | LASTFM_USER = get_collection("LAST_FM") 30 | 31 | 32 | @typing_action 33 | def set_user(update, context): 34 | msg = update.effective_message 35 | args = context.args 36 | if args: 37 | user = update.effective_user.id 38 | username = " ".join(args) 39 | if LASTFM_USER.find_one({'_id': user}): 40 | LASTFM_USER.find_one_and_update( 41 | {'_id': user}, {"$set": {'username': username}}) 42 | del_msg = msg.reply_text(f"Username updated to {username}!") 43 | else: 44 | LASTFM_USER.insert_one({'_id': user, 'username': username}) 45 | del_msg = msg.reply_text(f"Username set as {username}!") 46 | 47 | else: 48 | del_msg = msg.reply_text( 49 | "That's not how this works...\nRun /setuser followed by your username!" 50 | ) 51 | time.sleep(10) 52 | try: 53 | del_msg.delete() 54 | except error.BadRequest: 55 | return 56 | 57 | 58 | @typing_action 59 | def clear_user(update, context): 60 | user = update.effective_user.id 61 | LASTFM_USER.delete_one({'_id': user}) 62 | clear = update.effective_message.reply_text( 63 | "Last.fm username successfully cleared from my database!" 64 | ) 65 | time.sleep(10) 66 | clear.delete() 67 | 68 | 69 | @typing_action 70 | def last_fm(update, context): 71 | msg = update.effective_message 72 | user = update.effective_user.first_name 73 | user_id = update.effective_user.id 74 | data = LASTFM_USER.find_one({'_id': user_id}) 75 | if data is None: 76 | msg.reply_text("You haven't set your username yet!") 77 | return 78 | username = data["username"] 79 | base_url = "http://ws.audioscrobbler.com/2.0" 80 | res = requests.get( 81 | f"{base_url}?method=user.getrecenttracks&limit=3&extended=1&user={username}&api_key={LASTFM_API_KEY}&format=json" 82 | ) 83 | if not res.status_code == 200: 84 | msg.reply_text( 85 | "Hmm... something went wrong.\nPlease ensure that you've set the correct username!" 86 | ) 87 | return 88 | 89 | try: 90 | first_track = res.json().get("recenttracks").get("track")[0] 91 | except IndexError: 92 | msg.reply_text("You don't seem to have scrobbled any songs...") 93 | return 94 | if first_track.get("@attr"): 95 | # Ensures the track is now playing 96 | image = first_track.get("image")[3].get( 97 | "#text") # Grab URL of 300x300 image 98 | artist = first_track.get("artist").get("name") 99 | song = first_track.get("name") 100 | loved = int(first_track.get("loved")) 101 | rep = f"{user} is currently listening to:\n" 102 | if not loved: 103 | rep += f"🎧 {artist} - {song}" 104 | else: 105 | rep += f"🎧 {artist} - {song} (♥️, loved)" 106 | if image: 107 | rep += f"\u200c" 108 | else: 109 | tracks = res.json().get("recenttracks").get("track") 110 | track_dict = {tracks[i].get("artist").get( 111 | "name"): tracks[i].get("name") for i in range(3)} 112 | rep = f"{user} was listening to:\n" 113 | for artist, song in track_dict.items(): 114 | rep += f"🎧 {artist} - {song}\n" 115 | last_user = ( 116 | requests.get( 117 | f"{base_url}?method=user.getinfo&user={username}&api_key={LASTFM_API_KEY}&format=json" 118 | ) 119 | .json() 120 | .get("user") 121 | ) 122 | scrobbles = last_user.get("playcount") 123 | rep += f"\n({scrobbles} scrobbles so far)" 124 | 125 | send = msg.reply_text(rep, parse_mode=ParseMode.HTML) 126 | time.sleep(60) 127 | try: 128 | send.delete() 129 | msg.delete() 130 | except error.BadRequest: 131 | return 132 | 133 | 134 | def __stats__(): 135 | return "× {} saved Last.FM username.".format( 136 | LASTFM_USER.count_documents({}) 137 | ) 138 | 139 | 140 | SET_USER_HANDLER = CommandHandler("setuser", set_user, pass_args=True, run_async=True) 141 | CLEAR_USER_HANDLER = CommandHandler("clearuser", clear_user, run_async=True) 142 | LASTFM_HANDLER = DisableAbleCommandHandler("lastfm", last_fm, run_async=True) 143 | 144 | dispatcher.add_handler(SET_USER_HANDLER) 145 | dispatcher.add_handler(CLEAR_USER_HANDLER) 146 | dispatcher.add_handler(LASTFM_HANDLER) -------------------------------------------------------------------------------- /ubotindo/modules/users.py: -------------------------------------------------------------------------------- 1 | # UserindoBot 2 | # Copyright (C) 2020 UserindoBot Team, 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | 17 | from io import BytesIO 18 | from time import sleep 19 | 20 | from telegram import TelegramError 21 | from telegram.error import BadRequest, TimedOut, Unauthorized 22 | from telegram.ext import CommandHandler, Filters, MessageHandler 23 | 24 | from ubotindo.modules.no_sql import users_db 25 | from ubotindo import LOGGER, OWNER_ID, dispatcher 26 | from ubotindo.modules.helper_funcs.filters import CustomFilters 27 | 28 | USERS_GROUP = 4 29 | CHAT_GROUP = 10 30 | 31 | 32 | def get_user_id(username): 33 | # ensure valid userid 34 | if len(username) <= 5: 35 | return None 36 | 37 | if username.startswith("@"): 38 | username = username[1:] 39 | 40 | users = users_db.get_userid_by_name(username) 41 | 42 | if not users: 43 | return None 44 | 45 | elif len(users) == 1: 46 | return users[0]["_id"] 47 | 48 | else: 49 | for user_obj in users: 50 | try: 51 | userdat = dispatcher.bot.get_chat(user_obj["_id"]) 52 | if userdat.username == username: 53 | return userdat.id 54 | 55 | except BadRequest as excp: 56 | if excp.message == "Chat not found": 57 | pass 58 | else: 59 | LOGGER.exception("Error extracting user ID") 60 | 61 | return None 62 | 63 | 64 | def broadcast(update, context): 65 | to_send = update.effective_message.text.split(None, 1) 66 | if len(to_send) >= 2: 67 | chats = users_db.get_all_chats() or [] 68 | failed = 0 69 | for chat in chats: 70 | try: 71 | context.bot.sendMessage(int(chat["chat_id"]), to_send[1]) 72 | sleep(0.1) 73 | except TelegramError: 74 | failed += 1 75 | LOGGER.warning( 76 | "Couldn't send broadcast to %s, group name %s", 77 | str(chat["chat_id"]), 78 | str(chat["chat_name"]), 79 | ) 80 | 81 | update.effective_message.reply_text( 82 | "Broadcast complete. {} groups failed to receive the message, probably " 83 | "due to being kicked.".format(failed) 84 | ) 85 | 86 | 87 | def log_user(update, context): 88 | chat = update.effective_chat 89 | msg = update.effective_message 90 | 91 | users_db.update_user( 92 | msg.from_user.id, msg.from_user.username, chat.id, chat.title 93 | ) 94 | 95 | if msg.reply_to_message: 96 | users_db.update_user( 97 | msg.reply_to_message.from_user.id, 98 | msg.reply_to_message.from_user.username, 99 | chat.id, 100 | chat.title, 101 | ) 102 | 103 | if msg.forward_from: 104 | users_db.update_user(msg.forward_from.id, msg.forward_from.username) 105 | 106 | 107 | def chats(update, context): 108 | all_chats = users_db.get_all_chats() or [] 109 | chatfile = "List of chats.\n" 110 | for chat in all_chats: 111 | chatfile += "{} - ({})\n".format(chat["chat_name"], chat["chat_id"]) 112 | 113 | with BytesIO(str.encode(chatfile)) as output: 114 | output.name = "chatlist.txt" 115 | update.effective_message.reply_document( 116 | document=output, 117 | filename="chatlist.txt", 118 | caption="Here is the list of chats in my database.", 119 | ) 120 | 121 | 122 | def chat_checker(update, context): 123 | try: 124 | if ( 125 | update.effective_message.chat.get_member( 126 | context.bot.id 127 | ).can_send_messages 128 | is False 129 | ): 130 | context.bot.leaveChat(update.effective_message.chat.id) 131 | except (TimedOut, Unauthorized, BadRequest): 132 | pass 133 | 134 | 135 | def __user_info__(user_id): 136 | if user_id == dispatcher.bot.id: 137 | return """I've seen them in... Wow. Are they stalking me? They're in all the same places I am... oh. It's me.""" 138 | num_chats = users_db.get_user_num_chats(user_id) 139 | return """I've seen them in {} chats in total.""".format( 140 | num_chats 141 | ) 142 | 143 | 144 | def __stats__(): 145 | return "× {} users, across {} chats".format( 146 | users_db.num_users(), users_db.num_chats() 147 | ) 148 | 149 | 150 | def __migrate__(old_chat_id, new_chat_id): 151 | users_db.migrate_chat(old_chat_id, new_chat_id) 152 | 153 | 154 | __help__ = "" # no help string 155 | 156 | __mod_name__ = "Users" 157 | 158 | BROADCAST_HANDLER = CommandHandler( 159 | "broadcast", broadcast, filters=Filters.user(OWNER_ID), run_async=True 160 | ) 161 | USER_HANDLER = MessageHandler( 162 | Filters.all & Filters.chat_type.groups, log_user, run_async=True 163 | ) 164 | CHATLIST_HANDLER = CommandHandler( 165 | "chatlist", chats, filters=CustomFilters.sudo_filter, run_async=True 166 | ) 167 | CHAT_CHECKER_HANDLER = MessageHandler( 168 | Filters.all & Filters.chat_type.groups, chat_checker, run_async=True 169 | ) 170 | 171 | dispatcher.add_handler(USER_HANDLER, USERS_GROUP) 172 | dispatcher.add_handler(BROADCAST_HANDLER) 173 | dispatcher.add_handler(CHATLIST_HANDLER) 174 | dispatcher.add_handler(CHAT_CHECKER_HANDLER, CHAT_GROUP) 175 | -------------------------------------------------------------------------------- /ubotindo/modules/rules.py: -------------------------------------------------------------------------------- 1 | # UserindoBot 2 | # Copyright (C) 2020 UserindoBot Team, 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | 17 | from typing import Optional 18 | 19 | from telegram import ( 20 | InlineKeyboardButton, 21 | InlineKeyboardMarkup, 22 | Message, 23 | ParseMode, 24 | User, 25 | ) 26 | from telegram.error import BadRequest 27 | from telegram.ext import CommandHandler, Filters 28 | from telegram.utils.helpers import escape_markdown 29 | 30 | from ubotindo import dispatcher 31 | from ubotindo.modules.no_sql import get_collection 32 | from ubotindo.modules.helper_funcs.alternate import typing_action 33 | from ubotindo.modules.helper_funcs.chat_status import user_admin 34 | from ubotindo.modules.helper_funcs.string_handling import markdown_parser 35 | 36 | 37 | RULES_DATA = get_collection("RULES") 38 | 39 | 40 | @typing_action 41 | def get_rules(update, context): 42 | chat_id = update.effective_chat.id 43 | send_rules(update, chat_id) 44 | 45 | 46 | # Do not async - not from a handler 47 | def send_rules(update, chat_id, from_pm=False): 48 | bot = dispatcher.bot 49 | user = update.effective_user # type: Optional[User] 50 | try: 51 | chat = bot.get_chat(chat_id) 52 | except BadRequest as excp: 53 | if excp.message == "Chat not found" and from_pm: 54 | bot.send_message( 55 | user.id, 56 | "The rules shortcut for this chat hasn't been set properly! Ask admins to " 57 | "fix this.", 58 | ) 59 | return 60 | else: 61 | raise 62 | 63 | rules = chat_rules(chat_id) 64 | text = "The rules for *{}* are:\n\n{}".format( 65 | escape_markdown(chat.title), rules 66 | ) 67 | 68 | if from_pm and rules: 69 | bot.send_message(user.id, text, parse_mode=ParseMode.MARKDOWN) 70 | elif from_pm: 71 | bot.send_message( 72 | user.id, 73 | "The group admins haven't set any rules for this chat yet. " 74 | "This probably doesn't mean it's lawless though...!", 75 | ) 76 | elif rules: 77 | update.effective_message.reply_text( 78 | "Contact me in PM to get this group's rules.", 79 | reply_markup=InlineKeyboardMarkup( 80 | [ 81 | [ 82 | InlineKeyboardButton( 83 | text="Rules", 84 | url="t.me/{}?start={}".format( 85 | bot.username, chat_id 86 | ), 87 | ) 88 | ] 89 | ] 90 | ), 91 | ) 92 | else: 93 | update.effective_message.reply_text( 94 | "The group admins haven't set any rules for this chat yet. " 95 | "This probably doesn't mean it's lawless though...!" 96 | ) 97 | 98 | 99 | @user_admin 100 | @typing_action 101 | def set_rules(update, context): 102 | chat_id = update.effective_chat.id 103 | msg = update.effective_message # type: Optional[Message] 104 | raw_text = msg.text 105 | # use python's maxsplit to separate cmd and args 106 | args = raw_text.split(None, 1) 107 | if len(args) == 2: 108 | txt = args[1] 109 | # set correct offset relative to command 110 | offset = len(txt) - len(raw_text) 111 | markdown_rules = markdown_parser( 112 | txt, entities=msg.parse_entities(), offset=offset 113 | ) 114 | 115 | RULES_DATA.find_one_and_update( 116 | {'_id': chat_id}, 117 | {"$set": {'rules': markdown_rules}}, 118 | upsert=True) 119 | update.effective_message.reply_text( 120 | "Successfully set rules for this group." 121 | ) 122 | 123 | 124 | @user_admin 125 | @typing_action 126 | def clear_rules(update, context): 127 | chat_id = update.effective_chat.id 128 | RULES_DATA.delete_one({'_id': chat_id}) 129 | update.effective_message.reply_text("Successfully cleared rules!") 130 | 131 | 132 | def chat_rules(chat_id): 133 | data = RULES_DATA.find_one({'_id': int(chat_id)}) # ensure integer 134 | if data: 135 | return data["rules"] 136 | else: 137 | return False 138 | 139 | 140 | def __stats__(): 141 | count = RULES_DATA.count_documents({}) 142 | return "× {} chats have rules set.".format(count) 143 | 144 | 145 | def __import_data__(chat_id, data): 146 | # set chat rules 147 | rules = data.get("info", {}).get("rules", "") 148 | RULES_DATA.find_one_and_update( 149 | {'_id': chat_id}, 150 | {"$set": {'rules': rules}}, 151 | upsert=True) 152 | 153 | 154 | def __migrate__(old_chat_id, new_chat_id): 155 | rules = RULES_DATA.find_one_and_delete({'_id':old_chat_id}) 156 | if rules: 157 | RULES_DATA.insert_one( 158 | {'_id': new_chat_id, 'rules': rules["rules"]}) 159 | 160 | 161 | def __chat_settings__(chat_id, user_id): 162 | return "This chat has had it's rules set: `{}`".format( 163 | bool(chat_rules(chat_id)) 164 | ) 165 | 166 | 167 | __help__ = """ 168 | Every chat works with different rules; this module will help make those rules clearer! 169 | 170 | × /rules: get the rules for this chat. 171 | 172 | *Admin only:* 173 | × /setrules : Sets rules for the chat. 174 | × /clearrules: Clears saved rules for the chat. 175 | """ 176 | 177 | __mod_name__ = "Rules" 178 | 179 | GET_RULES_HANDLER = CommandHandler( 180 | "rules", get_rules, filters=Filters.chat_type.groups, run_async=True 181 | ) 182 | SET_RULES_HANDLER = CommandHandler( 183 | "setrules", set_rules, filters=Filters.chat_type.groups, run_async=True 184 | ) 185 | RESET_RULES_HANDLER = CommandHandler( 186 | "clearrules", clear_rules, filters=Filters.chat_type.groups, run_async=True 187 | ) 188 | 189 | dispatcher.add_handler(GET_RULES_HANDLER) 190 | dispatcher.add_handler(SET_RULES_HANDLER) 191 | dispatcher.add_handler(RESET_RULES_HANDLER) 192 | -------------------------------------------------------------------------------- /ubotindo/modules/helper_funcs/extraction.py: -------------------------------------------------------------------------------- 1 | # UserindoBot 2 | # Copyright (C) 2020 UserindoBot Team, 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | 17 | from typing import List, Optional 18 | 19 | from telegram import Message, MessageEntity 20 | from telegram.error import BadRequest 21 | 22 | from ubotindo import LOGGER 23 | from ubotindo.modules.users import get_user_id 24 | 25 | 26 | def id_from_reply(message): 27 | prev_message = message.reply_to_message 28 | if not prev_message: 29 | return None, None 30 | user_id = prev_message.from_user.id 31 | res = message.text.split(None, 1) 32 | if len(res) < 2: 33 | return user_id, "" 34 | return user_id, res[1] 35 | 36 | 37 | def extract_user(message: Message, args: List[str]) -> Optional[int]: 38 | return extract_user_and_text(message, args)[0] 39 | 40 | 41 | def extract_user_and_text( 42 | message: Message, args: List[str] 43 | ) -> (Optional[int], Optional[str]): 44 | prev_message = message.reply_to_message 45 | split_text = message.text.split(None, 1) 46 | 47 | if len(split_text) < 2: 48 | return id_from_reply(message) # only option possible 49 | 50 | text_to_parse = split_text[1] 51 | 52 | text = "" 53 | 54 | entities = list(message.parse_entities([MessageEntity.TEXT_MENTION])) 55 | if len(entities) > 0: 56 | ent = entities[0] 57 | else: 58 | ent = None 59 | 60 | # if entity offset matches (command end/text start) then all good 61 | if ( 62 | entities 63 | and ent 64 | and ent.offset == len(message.text) - len(text_to_parse) 65 | ): 66 | ent = entities[0] 67 | user_id = ent.user.id 68 | text = message.text[ent.offset + ent.length :] 69 | 70 | elif len(args) >= 1 and args[0][0] == "@": 71 | user = args[0] 72 | user_id = get_user_id(user) 73 | if not user_id: 74 | message.reply_text( 75 | "I don't have that user in my db. You'll be able to interact with them if " 76 | "you reply to that person's message instead, or forward one of that user's messages." 77 | ) 78 | return None, None 79 | 80 | else: 81 | user_id = user_id 82 | res = message.text.split(None, 2) 83 | if len(res) >= 3: 84 | text = res[2] 85 | 86 | elif len(args) >= 1 and args[0].isdigit(): 87 | user_id = int(args[0]) 88 | res = message.text.split(None, 2) 89 | if len(res) >= 3: 90 | text = res[2] 91 | 92 | elif prev_message: 93 | user_id, text = id_from_reply(message) 94 | 95 | else: 96 | return None, None 97 | 98 | try: 99 | message.bot.get_chat(user_id) 100 | except BadRequest as excp: 101 | if excp.message in ("User_id_invalid", "Chat not found"): 102 | message.reply_text( 103 | "I don't seem to have interacted with this user before - please forward a message from " 104 | "them to give me control! (like a voodoo doll, I need a piece of them to be able " 105 | "to execute certain commands...)" 106 | ) 107 | else: 108 | LOGGER.exception("Exception %s on user %s", excp.message, user_id) 109 | 110 | return None, None 111 | 112 | return user_id, text 113 | 114 | 115 | def extract_text(message) -> str: 116 | return ( 117 | message.text 118 | or message.caption 119 | or (message.sticker.emoji if message.sticker else None) 120 | ) 121 | 122 | 123 | def extract_unt_fedban( 124 | message: Message, args: List[str] 125 | ) -> (Optional[int], Optional[str]): 126 | prev_message = message.reply_to_message 127 | split_text = message.text.split(None, 1) 128 | 129 | if len(split_text) < 2: 130 | return id_from_reply(message) # only option possible 131 | 132 | text_to_parse = split_text[1] 133 | 134 | text = "" 135 | 136 | entities = list(message.parse_entities([MessageEntity.TEXT_MENTION])) 137 | if len(entities) > 0: 138 | ent = entities[0] 139 | else: 140 | ent = None 141 | 142 | # if entity offset matches (command end/text start) then all good 143 | if ( 144 | entities 145 | and ent 146 | and ent.offset == len(message.text) - len(text_to_parse) 147 | ): 148 | ent = entities[0] 149 | user_id = ent.user.id 150 | text = message.text[ent.offset + ent.length :] 151 | 152 | elif len(args) >= 1 and args[0][0] == "@": 153 | user = args[0] 154 | user_id = get_user_id(user) 155 | if not user_id and not str(user_id).isdigit(): 156 | message.reply_text( 157 | "I don't have this user's information in my database so, you'll not be able to interact with them" 158 | "Try replying to that person's msg or forward their message so i can act upon them" 159 | ) 160 | return None, None 161 | 162 | else: 163 | user_id = user_id 164 | res = message.text.split(None, 2) 165 | if len(res) >= 3: 166 | text = res[2] 167 | 168 | elif len(args) >= 1 and args[0].isdigit(): 169 | user_id = int(args[0]) 170 | res = message.text.split(None, 2) 171 | if len(res) >= 3: 172 | text = res[2] 173 | 174 | elif prev_message: 175 | user_id, text = id_from_reply(message) 176 | 177 | else: 178 | return None, None 179 | 180 | try: 181 | message.bot.get_chat(user_id) 182 | except BadRequest as excp: 183 | if ( 184 | excp.message in ("User_id_invalid", "Chat not found") 185 | and not str(user_id).isdigit() 186 | ): 187 | message.reply_text( 188 | "I don't seem to have interacted with this user before - please forward a message from " 189 | "them to give me control! (like a voodoo doll, I need a piece of them to be able " 190 | "to execute certain commands...)" 191 | ) 192 | return None, None 193 | elif excp.message != "Chat not found": 194 | LOGGER.exception("Exception %s on user %s", excp.message, user_id) 195 | return None, None 196 | elif not str(user_id).isdigit(): 197 | return None, None 198 | 199 | return user_id, text 200 | 201 | 202 | def extract_user_fban(message: Message, args: List[str]) -> Optional[int]: 203 | return extract_unt_fedban(message, args)[0] 204 | -------------------------------------------------------------------------------- /ubotindo/modules/sql/notes_sql.py: -------------------------------------------------------------------------------- 1 | # UserindoBot 2 | # Copyright (C) 2020 UserindoBot Team, 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | 17 | # Note: chat_id's are stored as strings because the int is too large to be 18 | # stored in a PSQL database. 19 | import threading 20 | 21 | from sqlalchemy import ( 22 | Column, 23 | String, 24 | Boolean, 25 | UnicodeText, 26 | Integer, 27 | func, 28 | distinct, 29 | ) 30 | 31 | from ubotindo.modules.helper_funcs.msg_types import Types 32 | from ubotindo.modules.sql import SESSION, BASE 33 | 34 | 35 | class Notes(BASE): 36 | __tablename__ = "notes" 37 | chat_id = Column(String(14), primary_key=True) 38 | name = Column(UnicodeText, primary_key=True) 39 | value = Column(UnicodeText, nullable=False) 40 | file = Column(UnicodeText) 41 | is_reply = Column(Boolean, default=False) 42 | has_buttons = Column(Boolean, default=False) 43 | msgtype = Column(Integer, default=Types.BUTTON_TEXT.value) 44 | 45 | def __init__(self, chat_id, name, value, msgtype, file=None): 46 | self.chat_id = str(chat_id) # ensure string 47 | self.name = name 48 | self.value = value 49 | self.msgtype = msgtype 50 | self.file = file 51 | 52 | def __repr__(self): 53 | return "" % self.name 54 | 55 | 56 | class Buttons(BASE): 57 | __tablename__ = "note_urls" 58 | id = Column(Integer, primary_key=True, autoincrement=True) 59 | chat_id = Column(String(14), primary_key=True) 60 | note_name = Column(UnicodeText, primary_key=True) 61 | name = Column(UnicodeText, nullable=False) 62 | url = Column(UnicodeText, nullable=False) 63 | same_line = Column(Boolean, default=False) 64 | 65 | def __init__(self, chat_id, note_name, name, url, same_line=False): 66 | self.chat_id = str(chat_id) 67 | self.note_name = note_name 68 | self.name = name 69 | self.url = url 70 | self.same_line = same_line 71 | 72 | 73 | Notes.__table__.create(checkfirst=True) 74 | Buttons.__table__.create(checkfirst=True) 75 | 76 | NOTES_INSERTION_LOCK = threading.RLock() 77 | BUTTONS_INSERTION_LOCK = threading.RLock() 78 | 79 | 80 | def add_note_to_db( 81 | chat_id, note_name, note_data, msgtype, buttons=None, file=None 82 | ): 83 | if not buttons: 84 | buttons = [] 85 | 86 | with NOTES_INSERTION_LOCK: 87 | prev = SESSION.query(Notes).get((str(chat_id), note_name)) 88 | if prev: 89 | with BUTTONS_INSERTION_LOCK: 90 | prev_buttons = ( 91 | SESSION.query(Buttons) 92 | .filter( 93 | Buttons.chat_id == str(chat_id), 94 | Buttons.note_name == note_name, 95 | ) 96 | .all() 97 | ) 98 | for btn in prev_buttons: 99 | SESSION.delete(btn) 100 | SESSION.delete(prev) 101 | note = Notes( 102 | str(chat_id), 103 | note_name, 104 | note_data or "", 105 | msgtype=msgtype.value, 106 | file=file, 107 | ) 108 | SESSION.add(note) 109 | SESSION.commit() 110 | 111 | for b_name, url, same_line in buttons: 112 | add_note_button_to_db(chat_id, note_name, b_name, url, same_line) 113 | 114 | 115 | def get_note(chat_id, note_name): 116 | try: 117 | return ( 118 | SESSION.query(Notes) 119 | .filter( 120 | func.lower(Notes.name) == note_name, 121 | Notes.chat_id == str(chat_id), 122 | ) 123 | .first() 124 | ) 125 | finally: 126 | SESSION.close() 127 | 128 | 129 | def rm_note(chat_id, note_name): 130 | with NOTES_INSERTION_LOCK: 131 | note = ( 132 | SESSION.query(Notes) 133 | .filter( 134 | func.lower(Notes.name) == note_name, 135 | Notes.chat_id == str(chat_id), 136 | ) 137 | .first() 138 | ) 139 | if note: 140 | with BUTTONS_INSERTION_LOCK: 141 | buttons = ( 142 | SESSION.query(Buttons) 143 | .filter( 144 | Buttons.chat_id == str(chat_id), 145 | Buttons.note_name == note_name, 146 | ) 147 | .all() 148 | ) 149 | for btn in buttons: 150 | SESSION.delete(btn) 151 | 152 | SESSION.delete(note) 153 | SESSION.commit() 154 | return True 155 | 156 | else: 157 | SESSION.close() 158 | return False 159 | 160 | 161 | def get_all_chat_notes(chat_id): 162 | try: 163 | return ( 164 | SESSION.query(Notes) 165 | .filter(Notes.chat_id == str(chat_id)) 166 | .order_by(Notes.name.asc()) 167 | .all() 168 | ) 169 | finally: 170 | SESSION.close() 171 | 172 | 173 | def add_note_button_to_db(chat_id, note_name, b_name, url, same_line): 174 | with BUTTONS_INSERTION_LOCK: 175 | button = Buttons(chat_id, note_name, b_name, url, same_line) 176 | SESSION.add(button) 177 | SESSION.commit() 178 | 179 | 180 | def get_buttons(chat_id, note_name): 181 | try: 182 | return ( 183 | SESSION.query(Buttons) 184 | .filter( 185 | Buttons.chat_id == str(chat_id), Buttons.note_name == note_name 186 | ) 187 | .order_by(Buttons.id) 188 | .all() 189 | ) 190 | finally: 191 | SESSION.close() 192 | 193 | 194 | def num_notes(): 195 | try: 196 | return SESSION.query(Notes).count() 197 | finally: 198 | SESSION.close() 199 | 200 | 201 | def num_chats(): 202 | try: 203 | return SESSION.query(func.count(distinct(Notes.chat_id))).scalar() 204 | finally: 205 | SESSION.close() 206 | 207 | 208 | def migrate_chat(old_chat_id, new_chat_id): 209 | with NOTES_INSERTION_LOCK: 210 | chat_notes = ( 211 | SESSION.query(Notes) 212 | .filter(Notes.chat_id == str(old_chat_id)) 213 | .all() 214 | ) 215 | for note in chat_notes: 216 | note.chat_id = str(new_chat_id) 217 | 218 | with BUTTONS_INSERTION_LOCK: 219 | chat_buttons = ( 220 | SESSION.query(Buttons) 221 | .filter(Buttons.chat_id == str(old_chat_id)) 222 | .all() 223 | ) 224 | for btn in chat_buttons: 225 | btn.chat_id = str(new_chat_id) 226 | 227 | SESSION.commit() 228 | -------------------------------------------------------------------------------- /ubotindo/modules/webtools.py: -------------------------------------------------------------------------------- 1 | # UserindoBot 2 | # Copyright (C) 2020 UserindoBot Team, 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | 17 | import datetime 18 | import html 19 | import os 20 | import platform 21 | import subprocess 22 | import time 23 | import sys 24 | from platform import python_version 25 | 26 | import requests 27 | import speedtest 28 | from threading import Thread 29 | from psutil import boot_time, cpu_percent, disk_usage, virtual_memory 30 | from spamwatch import __version__ as __sw__ 31 | from telegram import ParseMode, __version__ 32 | from telegram.error import BadRequest, TelegramError 33 | from telegram.ext import CommandHandler, Filters 34 | 35 | from ubotindo import MESSAGE_DUMP, OWNER_ID, dispatcher, updater 36 | from ubotindo.modules.helper_funcs.alternate import typing_action 37 | from ubotindo.modules.helper_funcs.filters import CustomFilters 38 | 39 | 40 | @typing_action 41 | def leavechat(update, context): 42 | bot = context.bot 43 | args = context.args 44 | if args: 45 | chat_id = str(args[0]) 46 | del args[0] 47 | try: 48 | bot.leave_chat(int(chat_id)) 49 | update.effective_message.reply_text("Left the group successfully!") 50 | except TelegramError: 51 | update.effective_message.reply_text("Attempt failed.") 52 | else: 53 | update.effective_message.reply_text("Give me a valid chat id") 54 | 55 | 56 | @typing_action 57 | def ping(update, context): 58 | msg = update.effective_message 59 | start_time = time.time() 60 | message = msg.reply_text("Pinging...") 61 | end_time = time.time() 62 | ping_time = round((end_time - start_time) * 1000, 3) 63 | message.edit_text( 64 | "*Pong!!!*\n`{}ms`".format(ping_time), parse_mode=ParseMode.MARKDOWN 65 | ) 66 | 67 | 68 | @typing_action 69 | def get_bot_ip(update, context): 70 | """Sends the bot's IP address, so as to be able to ssh in if necessary. 71 | OWNER ONLY. 72 | """ 73 | res = requests.get("http://ipinfo.io/ip") 74 | update.message.reply_text(res.text) 75 | 76 | 77 | @typing_action 78 | def speedtst(update, context): 79 | message = update.effective_message 80 | ed_msg = message.reply_text("Running high speed test . . .") 81 | test = speedtest.Speedtest() 82 | test.get_best_server() 83 | test.download() 84 | test.upload() 85 | test.results.share() 86 | result = test.results.dict() 87 | context.bot.editMessageText( 88 | "Download " 89 | f"{speed_convert(result['download'])} \n" 90 | "Upload " 91 | f"{speed_convert(result['upload'])} \n" 92 | "Ping " 93 | f"{result['ping']} \n" 94 | "ISP " 95 | f"{result['client']['isp']}", 96 | update.effective_chat.id, 97 | ed_msg.message_id, 98 | ) 99 | 100 | 101 | @typing_action 102 | def system_status(update, context): 103 | uptime = datetime.datetime.fromtimestamp(boot_time()).strftime( 104 | "%Y-%m-%d %H:%M:%S" 105 | ) 106 | status = "======[ SYSTEM INFO ]======\n\n" 107 | status += "System uptime: " + str(uptime) + "\n" 108 | 109 | uname = platform.uname() 110 | status += "System: " + str(uname.system) + "\n" 111 | status += "Node name: " + str(uname.node) + "\n" 112 | status += "Release: " + str(uname.release) + "\n" 113 | status += "Version: " + str(uname.version) + "\n" 114 | status += "Machine: " + str(uname.machine) + "\n" 115 | status += "Processor: " + str(uname.processor) + "\n\n" 116 | 117 | mem = virtual_memory() 118 | cpu = cpu_percent() 119 | disk = disk_usage("/") 120 | status += "CPU usage: " + str(cpu) + " %\n" 121 | status += "Ram usage: " + str(mem[2]) + " %\n" 122 | status += "Storage used: " + str(disk[3]) + " %\n\n" 123 | status += "Python version: " + python_version() + "\n" 124 | status += "Library version: " + str(__version__) + "\n" 125 | status += "Spamwatch API: " + str(__sw__) + "\n" 126 | context.bot.sendMessage( 127 | update.effective_chat.id, status, parse_mode=ParseMode.HTML 128 | ) 129 | 130 | 131 | def speed_convert(size): 132 | """Hi human, you can't read bytes?""" 133 | power = 2 ** 10 134 | zero = 0 135 | units = {0: "", 1: "Kb/s", 2: "Mb/s", 3: "Gb/s", 4: "Tb/s"} 136 | while size > power: 137 | size /= power 138 | zero += 1 139 | return f"{round(size, 2)} {units[zero]}" 140 | 141 | 142 | @typing_action 143 | def gitpull(update, context): 144 | sent_msg = update.effective_message.reply_text( 145 | "Pulling all changes from remote..." 146 | ) 147 | subprocess.Popen("git reset --hard origin/master && git clean -fd && git pull", stdout=subprocess.PIPE, shell=True) 148 | 149 | sent_msg_text = ( 150 | sent_msg.text 151 | + "\n\nChanges pulled... I guess..\nContinue to restart with /reboot " 152 | ) 153 | sent_msg.edit_text(sent_msg_text) 154 | 155 | 156 | def stop_and_restart(): 157 | """Kill old instance, replace the new one""" 158 | updater.stop() 159 | os.execl(sys.executable, sys.executable, *sys.argv) 160 | 161 | 162 | def restart(update, context): 163 | update.message.reply_text('Bot is restarting...') 164 | Thread(target=stop_and_restart).start() 165 | 166 | 167 | IP_HANDLER = CommandHandler( 168 | "ip", get_bot_ip, filters=Filters.chat(OWNER_ID), run_async=True 169 | ) 170 | PING_HANDLER = CommandHandler( 171 | "ping", ping, filters=CustomFilters.sudo_filter, run_async=True 172 | ) 173 | SPEED_HANDLER = CommandHandler( 174 | "speedtest", speedtst, filters=CustomFilters.sudo_filter, run_async=True 175 | ) 176 | SYS_STATUS_HANDLER = CommandHandler( 177 | "sysinfo", system_status, filters=CustomFilters.dev_filter, run_async=True 178 | ) 179 | LEAVECHAT_HANDLER = CommandHandler( 180 | ["leavechat", "leavegroup", "leave"], 181 | leavechat, 182 | pass_args=True, 183 | filters=CustomFilters.dev_filter, 184 | run_async=True, 185 | ) 186 | GITPULL_HANDLER = CommandHandler( 187 | "gitpull", gitpull, filters=CustomFilters.dev_filter, run_async=True 188 | ) 189 | RESTART_HANDLER = CommandHandler( 190 | "reboot", restart, filters=CustomFilters.dev_filter, run_async=True 191 | ) 192 | 193 | dispatcher.add_handler(IP_HANDLER) 194 | dispatcher.add_handler(SPEED_HANDLER) 195 | dispatcher.add_handler(PING_HANDLER) 196 | dispatcher.add_handler(SYS_STATUS_HANDLER) 197 | dispatcher.add_handler(LEAVECHAT_HANDLER) 198 | dispatcher.add_handler(GITPULL_HANDLER) 199 | dispatcher.add_handler(RESTART_HANDLER) 200 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # USERINDOBOT 2 | 3 | [![forthebadge made-with-python](http://ForTheBadge.com/images/badges/made-with-python.svg)](https://www.python.org/) 4 | [![ForTheBadge built-with-love](http://ForTheBadge.com/images/badges/built-with-love.svg)](https://github.com/UserBotIndo/) 5 | 6 | [![License: GPL v3](https://img.shields.io/badge/License-GPLv3-blue.svg)](https://www.gnu.org/licenses/gpl-3.0) 7 | [![Codacy Badge](https://app.codacy.com/project/badge/Grade/8bfae649db3742a883e0ac1008755db3)](https://www.codacy.com/gh/userbotindo/UserIndoBot/dashboard?utm_source=github.com&utm_medium=referral&utm_content=userbotindo/UserIndoBot&utm_campaign=Badge_Grade) 8 | [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square)](https://github.com/userbotindo/UserIndoBot/pulls) 9 | [![Maintenance](https://img.shields.io/badge/Maintained%3F-no-red.svg)](https://github.com/userbotindo/UserIndoBot/graphs/commit-activity) 10 | ![logo](https://i.ibb.co/zJdLsyg/Userindobot.png) 11 | 12 | A modular telegram Python bot running on python3 with sqlalchemy database. 13 | 14 | Originally a simple group management bot with multiple admin features, it has evolved, becoming extremely modular and 15 | simple to use. Note that this project uses a well-known Telegram-bot of its time @BanhammerMarie_bot from Paul Larson as its base. 16 | 17 | Can be found on telegram as [UserbotIndo](https://t.me/userbotindobot). 18 | 19 | Join the [Group Support](https://t.me/userbotindo) if you just want to stay in the loop about new features or announcements. 20 | 21 | 22 | ## NOTICE 23 | 24 | This project is no longer under active maintenance. Bug fixes may still be released, but no new features will be added. We'll no longer be responding to an Issue or Pull Request unless they are related to any security or critical bug concerns. You can use [Anjani](https://github.com/userbotindo/anjani), our new group management bot, an improvement from this bot. 25 | 26 | ## Credits 27 | 28 | Skyleebot For Awesome Bot, And This Base in They 29 | 30 | Skittbot for Stickers module and memes module. 31 | 32 | 1maverick1 for many stuff. 33 | 34 | AyraHikari for weather modules and some other stuff. 35 | 36 | RealAkito for reverse search modules. 37 | 38 | MrYacha for connections module 39 | 40 | ATechnoHazard for many stuffs 41 | 42 | Corsicanu and Nunopenim for android modules 43 | 44 | Any other missing Credits can be seen in commits! 45 | 46 | ## Starting the bot 47 | 48 | Once you've set up your database and your configuration (see below) is complete, simply run: 49 | 50 | `python3 -m ubotindo` 51 | 52 | ## Setting up the bot Read this before trying to use 53 | 54 | Please make sure to use python3.6 above, as I cannot guarantee everything will work as expected on older Python versions! 55 | This is because markdown parsing is done by iterating through a dict, which is ordered by default in 3.6. 56 | 57 | ### Configuration 58 | 59 | There are two possible ways of configuring your bot: a config.py file, or ENV variables. 60 | 61 | The preferred version is to use a `config.py` file, as it makes it easier to see all your settings together. 62 | This file should be placed in your `UserindoBot` folder, alongside the `__main__.py` file. 63 | This is where your bot token will be loaded from, as well as your database URI (if you're using a database), and most of 64 | your other settings. 65 | 66 | It is recommended to import sample_config and extend the Config class, as this will ensure your config contains all 67 | defaults set in the sample_config, hence making it easier to upgrade. 68 | 69 | An example `config.env` file could be: 70 | 71 | ```python 72 | API_KEY = "" # your bot Token from BotFather 73 | OWNER_ID = "1234567" # If you dont know, run the bot and do /id in your private chat with it 74 | OWNER_USERNAME = "userbotindo" # your telegram username 75 | SQLALCHEMY_DATABASE_URI = "sqldbtype://username:pw@hostname:port/db_name" 76 | MONGO_DB_URI = "mongodb+srv://username:pwd@host.port.mongodb.net/db_name" 77 | MESSAGE_DUMP = "-100987654" # needed to make sure 'save from' messages persist 78 | LOAD = "" # list of loaded modules (seperate with space) 79 | NO_LOAD = "afk android" # list of unloaded modules (seperate with space) 80 | STRICT_GBAN = True 81 | ``` 82 | 83 | ### Python dependencies 84 | 85 | Install the necessary Python dependencies by moving to the project directory and running: 86 | 87 | `pip3 install -r requirements.txt`. 88 | 89 | This will install all the necessary python packages. 90 | 91 | ### Database 92 | 93 | #### MongoDB 94 | 95 | [MongoDB](https://cloud.mongodb.com/) here is used to store users, chats, afk status, blacklist, global bans, data. 96 | 97 | #### SQL 98 | 99 | If you wish to use a database-dependent module (eg: locks, notes, filters, welcomes), 100 | you'll need to have a database installed on your system. I use Postgres, so I recommend using it for optimal compatibility. 101 | 102 | In the case of Postgres, this is how you would set up a database on a Debian/Ubuntu system. Other distributions may vary. 103 | 104 | - install PostgreSQL: 105 | 106 | `sudo apt-get update && sudo apt-get install postgresql` 107 | 108 | - change to the Postgres user: 109 | 110 | `sudo su - postgres` 111 | 112 | - create a new database user (change YOUR_USER appropriately): 113 | 114 | `createuser -P -s -e YOUR_USER` 115 | 116 | This will be followed by you need to input your password. 117 | 118 | - create a new database table: 119 | 120 | `createdb -O YOUR_USER YOUR_DB_NAME` 121 | 122 | Change YOUR_USER and YOUR_DB_NAME appropriately. 123 | 124 | - finally: 125 | 126 | `psql YOUR_DB_NAME -h YOUR_HOST YOUR_USER` 127 | 128 | This will allow you to connect to your database via your terminal. 129 | By default, YOUR_HOST should be 0.0.0.0:5432. 130 | 131 | You should now be able to build your database URI. This will be: 132 | 133 | `sqldbtype://username:pw@hostname:port/db_name` 134 | 135 | Replace SqlDbType with whichever DB you're using (eg Postgres, MySQL, SQLite, etc) 136 | repeat for your username, password, hostname (localhost?), port (5432?), and DB name. 137 | 138 | ## Modules 139 | 140 | ### Setting load order 141 | 142 | The module load order can be changed via the `LOAD` and `NO_LOAD` configuration settings. 143 | These should both represent lists. 144 | 145 | If `LOAD` is an empty list, all modules in `modules/` will be selected for loading by default. 146 | 147 | If `NO_LOAD` is not present or is an empty list, all modules selected for loading will be loaded. 148 | 149 | If a module is in both `LOAD` and `NO_LOAD`, the module will not be loaded - `NO_LOAD` takes priority. 150 | 151 | ### Creating your own modules 152 | 153 | Creating a module has been simplified as much as possible - but do not hesitate to suggest further simplification. 154 | 155 | All that is needed is that your .py file is in the modules folder. 156 | 157 | To add commands, make sure to import the dispatcher via 158 | 159 | `from ubotindo import dispatcher`. 160 | 161 | You can then add commands using the usual 162 | 163 | `dispatcher.add_handler()`. 164 | 165 | Assigning the `__help__` variable to a string describing this modules' available 166 | commands will allow the bot to load it and add the documentation for 167 | your module to the `/help` command. Setting the `__mod_name__` variable will also allow you to use a nicer, 168 | user-friendly name for a module. 169 | 170 | The `__migrate__()` function is used for migrating chats - when a chat is upgraded to a supergroup, the ID changes, so 171 | it is necessary to migrate it in the DB. 172 | 173 | The `__stats__()` function is for retrieving module statistics, eg number of users, number of chats. This is accessed 174 | through the `/stats` command, which is only available to the bot owner. 175 | -------------------------------------------------------------------------------- /ubotindo/modules/helper_funcs/chat_status.py: -------------------------------------------------------------------------------- 1 | # UserindoBot 2 | # Copyright (C) 2020 UserindoBot Team, 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | 17 | from functools import wraps 18 | from telegram import User, Chat, ChatMember 19 | from telegram.error import BadRequest, Unauthorized 20 | 21 | from ubotindo import ( 22 | DEL_CMDS, 23 | DEV_USERS, 24 | SUDO_USERS, 25 | WHITELIST_USERS, 26 | dispatcher, 27 | ) 28 | from cachetools import TTLCache 29 | from threading import RLock 30 | 31 | # refresh cache 10m 32 | ADMIN_CACHE = TTLCache(maxsize=512, ttl=60 * 10) 33 | THREAD_LOCK = RLock() 34 | 35 | 36 | def can_delete(chat: Chat, bot_id: int) -> bool: 37 | return chat.get_member(bot_id).can_delete_messages 38 | 39 | 40 | def is_user_ban_protected( 41 | chat: Chat, user_id: int, member: ChatMember = None 42 | ) -> bool: 43 | if ( 44 | chat.type == "private" 45 | or user_id in DEV_USERS 46 | or user_id in SUDO_USERS 47 | or user_id in WHITELIST_USERS 48 | or chat.all_members_are_administrators 49 | or user_id in (777000, 1087968824) 50 | ): 51 | return True 52 | 53 | if not member: 54 | member = chat.get_member(user_id) 55 | return member.status in ("administrator", "creator") 56 | 57 | 58 | def is_user_admin(chat: Chat, user_id: int, member: ChatMember = None) -> bool: 59 | if ( 60 | chat.type == "private" 61 | or user_id in DEV_USERS 62 | or user_id in SUDO_USERS 63 | or user_id in (777000, 1087968824) 64 | or chat.all_members_are_administrators 65 | ): 66 | return True 67 | 68 | if not member: 69 | with THREAD_LOCK: 70 | # try to fetch from cache first. 71 | try: 72 | return user_id in ADMIN_CACHE[chat.id] 73 | except KeyError: 74 | # keyerror happend means cache is deleted, 75 | # so query bot api again and return user status 76 | # while saving it in cache for future useage... 77 | try: 78 | chat_admins = dispatcher.bot.getChatAdministrators(chat.id) 79 | admin_list = [x.user.id for x in chat_admins] 80 | ADMIN_CACHE[chat.id] = admin_list 81 | 82 | if user_id in admin_list: 83 | return True 84 | except Unauthorized: 85 | return False 86 | 87 | 88 | def is_bot_admin( 89 | chat: Chat, bot_id: int, bot_member: ChatMember = None 90 | ) -> bool: 91 | if chat.type == "private" or chat.all_members_are_administrators: 92 | return True 93 | 94 | if not bot_member: 95 | bot_member = chat.get_member(bot_id) 96 | return bot_member.status in ("administrator", "creator") 97 | 98 | 99 | def is_user_in_chat(chat: Chat, user_id: int) -> bool: 100 | member = chat.get_member(user_id) 101 | return member.status not in ("left", "kicked") 102 | 103 | 104 | def bot_can_delete(func): 105 | @wraps(func) 106 | def delete_rights(update, context, *args, **kwargs): 107 | if can_delete(update.effective_chat, context.bot.id): 108 | return func(update, context, *args, **kwargs) 109 | else: 110 | update.effective_message.reply_text( 111 | "I can't delete messages here! " 112 | "Make sure I'm admin and can delete other user's messages." 113 | ) 114 | 115 | return delete_rights 116 | 117 | 118 | def can_pin(func): 119 | @wraps(func) 120 | def pin_rights(update, context, *args, **kwargs): 121 | if update.effective_chat.get_member(context.bot.id).can_pin_messages: 122 | return func(update, context, *args, **kwargs) 123 | else: 124 | update.effective_message.reply_text( 125 | "I can't pin messages here! " 126 | "Make sure I'm admin and can pin messages." 127 | ) 128 | 129 | return pin_rights 130 | 131 | 132 | def can_promote(func): 133 | @wraps(func) 134 | def promote_rights(update, context, *args, **kwargs): 135 | if update.effective_chat.get_member( 136 | context.bot.id 137 | ).can_promote_members: 138 | return func(update, context, *args, **kwargs) 139 | else: 140 | update.effective_message.reply_text( 141 | "I can't promote/demote people here! " 142 | "Make sure I'm admin and can appoint new admins." 143 | ) 144 | 145 | return promote_rights 146 | 147 | 148 | def can_restrict(func): 149 | @wraps(func) 150 | def promote_rights(update, context, *args, **kwargs): 151 | if update.effective_chat.get_member( 152 | context.bot.id 153 | ).can_restrict_members: 154 | return func(update, context, *args, **kwargs) 155 | else: 156 | update.effective_message.reply_text( 157 | "I can't restrict people here! " 158 | "Make sure I'm admin and can appoint new admins." 159 | ) 160 | 161 | return promote_rights 162 | 163 | 164 | def bot_admin(func): 165 | @wraps(func) 166 | def is_admin(update, context, *args, **kwargs): 167 | if is_bot_admin(update.effective_chat, context.bot.id): 168 | return func(update, context, *args, **kwargs) 169 | else: 170 | try: 171 | update.effective_message.reply_text("I'm not admin!") 172 | except BadRequest: 173 | return 174 | 175 | return is_admin 176 | 177 | 178 | def user_admin(func): 179 | @wraps(func) 180 | def is_admin(update, context, *args, **kwargs): 181 | user = update.effective_user # type: Optional[User] 182 | if user and is_user_admin(update.effective_chat, user.id): 183 | return func(update, context, *args, **kwargs) 184 | 185 | elif not user: 186 | pass 187 | 188 | elif DEL_CMDS and " " not in update.effective_message.text: 189 | try: 190 | update.effective_message.delete() 191 | except BadRequest: 192 | pass 193 | 194 | else: 195 | update.effective_message.reply_text( 196 | "You're missing admin rights for using this command!" 197 | ) 198 | 199 | return is_admin 200 | 201 | 202 | def user_admin_no_reply(func): 203 | @wraps(func) 204 | def is_admin(update, context, *args, **kwargs): 205 | user = update.effective_user # type: Optional[User] 206 | if user and is_user_admin(update.effective_chat, user.id): 207 | return func(update, context, *args, **kwargs) 208 | 209 | elif not user: 210 | pass 211 | 212 | elif DEL_CMDS and " " not in update.effective_message.text: 213 | update.effective_message.delete() 214 | 215 | return is_admin 216 | 217 | 218 | def user_not_admin(func): 219 | @wraps(func) 220 | def is_not_admin(update, context, *args, **kwargs): 221 | user = update.effective_user # type: Optional[User] 222 | if user and not is_user_admin(update.effective_chat, user.id): 223 | return func(update, context, *args, **kwargs) 224 | 225 | return is_not_admin 226 | -------------------------------------------------------------------------------- /ubotindo/modules/afk.py: -------------------------------------------------------------------------------- 1 | # UserindoBot 2 | # Copyright (C) 2020 UserindoBot Team, 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | """Afk module: Tell anyone if you away from keyboard.""" 17 | import random 18 | from time import sleep 19 | 20 | from telegram import MessageEntity 21 | from telegram.error import BadRequest 22 | from telegram.ext import Filters, MessageHandler 23 | 24 | import ubotindo.modules.helper_funcs.fun_strings as fun 25 | from ubotindo import dispatcher 26 | from ubotindo.modules.disable import ( 27 | DisableAbleCommandHandler, 28 | DisableAbleMessageHandler, 29 | ) 30 | from ubotindo.modules.no_sql import afk_db 31 | from ubotindo.modules.users import get_user_id 32 | 33 | AFK_GROUP = 7 34 | AFK_REPLY_GROUP = 8 35 | 36 | 37 | """This Function to triger bot""" 38 | 39 | 40 | def afk(update, context): 41 | args = update.effective_message.text.split(None, 1) 42 | 43 | if not update.effective_user.id: 44 | return 45 | 46 | if update.effective_user.id in (777000, 1087968824): 47 | return 48 | 49 | notice = "" 50 | if len(args) >= 2: 51 | reason = args[1] 52 | if len(reason) > 100: 53 | reason = reason[:100] 54 | notice = "\nYour afk reason was shortened to 100 characters." 55 | else: 56 | reason = "" 57 | 58 | afk_db.set_afk(update.effective_user.id, reason) 59 | afkstr = random.choice(fun.AFK) 60 | msg = update.effective_message 61 | afksend = msg.reply_text( 62 | afkstr.format(update.effective_user.first_name, notice) 63 | ) 64 | sleep(5) 65 | try: 66 | afksend.delete() 67 | except BadRequest: 68 | return 69 | 70 | """This function to check user afk or not""" 71 | def no_longer_afk(update, context): 72 | user = update.effective_user 73 | message = update.effective_message 74 | 75 | if not user: # ignore channels 76 | return 77 | 78 | res = afk_db.rm_afk(user.id) 79 | if res: 80 | if message.new_chat_members: # dont say msg 81 | return 82 | firstname = update.effective_user.first_name 83 | try: 84 | options = [ 85 | "{} is here!", 86 | "{} is back!", 87 | "{} is now in the chat!", 88 | "{} is awake!", 89 | "{} is back online!", 90 | "{} is finally here!", 91 | "Welcome back! {}", 92 | "Where is {}?\nIn the chat!", 93 | ] 94 | chosen_option = random.choice(options) 95 | unafk = update.effective_message.reply_text( 96 | chosen_option.format(firstname) 97 | ) 98 | sleep(10) 99 | unafk.delete() 100 | except BaseException: 101 | return 102 | 103 | 104 | """This method to tell if user afk""" 105 | def reply_afk(update, context): 106 | bot = context.bot 107 | message = update.effective_message 108 | userc = update.effective_user 109 | userc_id = userc.id 110 | if message.entities and message.parse_entities( 111 | [MessageEntity.TEXT_MENTION, MessageEntity.MENTION] 112 | ): 113 | entities = message.parse_entities( 114 | [MessageEntity.TEXT_MENTION, MessageEntity.MENTION]) 115 | 116 | chk_users = [] 117 | for ent in entities: 118 | if ent.type == MessageEntity.TEXT_MENTION: 119 | user_id = ent.user.id 120 | fst_name = ent.user.first_name 121 | 122 | if user_id in chk_users: 123 | return 124 | chk_users.append(user_id) 125 | 126 | if ent.type == MessageEntity.MENTION: 127 | user_id = get_user_id( 128 | message.text[ent.offset: ent.offset + ent.length] 129 | ) 130 | if not user_id: 131 | # Should never happen, since for a user to become AFK they 132 | # must have spoken. Maybe changed username? 133 | return 134 | 135 | if user_id in chk_users: 136 | return 137 | chk_users.append(user_id) 138 | 139 | try: 140 | chat = bot.get_chat(user_id) 141 | except BadRequest: 142 | print( 143 | "Error: Could not fetch userid {} for AFK module".format( 144 | user_id 145 | ) 146 | ) 147 | return 148 | fst_name = chat.first_name 149 | 150 | else: 151 | return 152 | 153 | check_afk(update, context, user_id, fst_name, userc_id) 154 | 155 | elif message.reply_to_message: 156 | user_id = message.reply_to_message.from_user.id 157 | fst_name = message.reply_to_message.from_user.first_name 158 | check_afk(update, context, user_id, fst_name, userc_id) 159 | 160 | 161 | def check_afk(update, context, user_id, fst_name, userc_id): 162 | if afk_db.is_afk(user_id): 163 | user = afk_db.check_afk_status(user_id) 164 | if user is None: 165 | return # sanity check 166 | if not user["reason"]: 167 | if int(userc_id) == int(user_id): 168 | return 169 | res = "{} is afk".format(fst_name) 170 | replafk = update.effective_message.reply_text(res) 171 | else: 172 | if int(userc_id) == int(user_id): 173 | return 174 | res = "{} is away from keyboard! says it's because of Reason: {}".format( 175 | fst_name, user["reason"]) 176 | replafk = update.effective_message.reply_text( 177 | res, parse_mode="html" 178 | ) 179 | sleep(10) 180 | try: 181 | replafk.delete() 182 | except BadRequest: 183 | return 184 | 185 | 186 | def __gdpr__(user_id): 187 | afk_db.rm_afk(user_id) 188 | 189 | 190 | __help__ = """ 191 | When marked as AFK, any mentions will be replied to with a message to say you're not available! 192 | 193 | × /afk : Mark yourself as AFK. 194 | × brb : Same as the afk command - but not a command. 195 | """ 196 | 197 | 198 | AFK_HANDLER = DisableAbleCommandHandler("afk", afk, run_async=True) 199 | AFK_REGEX_HANDLER = DisableAbleMessageHandler( 200 | Filters.regex("(?i)brb"), afk, friendly="afk", run_async=True 201 | ) 202 | NO_AFK_HANDLER = MessageHandler( 203 | Filters.all, no_longer_afk, run_async=True 204 | ) 205 | AFK_REPLY_HANDLER = MessageHandler( 206 | Filters.all & Filters.chat_type.groups & ~Filters.update.edited_message, 207 | reply_afk, 208 | run_async=True, 209 | ) 210 | 211 | 212 | dispatcher.add_handler(AFK_HANDLER, AFK_GROUP) 213 | dispatcher.add_handler(AFK_REGEX_HANDLER, AFK_GROUP) 214 | dispatcher.add_handler(NO_AFK_HANDLER, AFK_GROUP) 215 | dispatcher.add_handler(AFK_REPLY_HANDLER, AFK_REPLY_GROUP) 216 | 217 | 218 | __mod_name__ = "AFK" 219 | __command_list__ = ["afk"] 220 | __handlers__ = [ 221 | (AFK_HANDLER, AFK_GROUP), 222 | (AFK_REGEX_HANDLER, AFK_GROUP), 223 | (NO_AFK_HANDLER, AFK_GROUP), 224 | (AFK_REPLY_HANDLER, AFK_REPLY_GROUP), 225 | ] 226 | -------------------------------------------------------------------------------- /ubotindo/modules/sql/connection_sql.py: -------------------------------------------------------------------------------- 1 | # UserindoBot 2 | # Copyright (C) 2020 UserindoBot Team, 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | 17 | import threading 18 | import time 19 | from typing import Union 20 | 21 | from sqlalchemy import Column, String, Boolean, UnicodeText, Integer 22 | 23 | from ubotindo.modules.sql import SESSION, BASE 24 | 25 | 26 | class ChatAccessConnectionSettings(BASE): 27 | __tablename__ = "access_connection" 28 | chat_id = Column(String(14), primary_key=True) 29 | allow_connect_to_chat = Column(Boolean, default=True) 30 | 31 | def __init__(self, chat_id, allow_connect_to_chat): 32 | self.chat_id = str(chat_id) 33 | self.allow_connect_to_chat = str(allow_connect_to_chat) 34 | 35 | def __repr__(self): 36 | return "".format( 37 | self.chat_id, self.allow_connect_to_chat 38 | ) 39 | 40 | 41 | class Connection(BASE): 42 | __tablename__ = "connection" 43 | user_id = Column(Integer, primary_key=True) 44 | chat_id = Column(String(14)) 45 | 46 | def __init__(self, user_id, chat_id): 47 | self.user_id = user_id 48 | self.chat_id = str(chat_id) # Ensure String 49 | 50 | 51 | class ConnectionHistory(BASE): 52 | __tablename__ = "connection_history" 53 | user_id = Column(Integer, primary_key=True) 54 | chat_id = Column(String(14), primary_key=True) 55 | chat_name = Column(UnicodeText) 56 | conn_time = Column(Integer) 57 | 58 | def __init__(self, user_id, chat_id, chat_name, conn_time): 59 | self.user_id = user_id 60 | self.chat_id = str(chat_id) 61 | self.chat_name = str(chat_name) 62 | self.conn_time = int(conn_time) 63 | 64 | def __repr__(self): 65 | return "".format( 66 | self.user_id, self.chat_id 67 | ) 68 | 69 | 70 | ChatAccessConnectionSettings.__table__.create(checkfirst=True) 71 | Connection.__table__.create(checkfirst=True) 72 | ConnectionHistory.__table__.create(checkfirst=True) 73 | 74 | CHAT_ACCESS_LOCK = threading.RLock() 75 | CONNECTION_INSERTION_LOCK = threading.RLock() 76 | CONNECTION_HISTORY_LOCK = threading.RLock() 77 | 78 | HISTORY_CONNECT = {} 79 | 80 | 81 | def allow_connect_to_chat(chat_id: Union[str, int]) -> bool: 82 | try: 83 | chat_setting = SESSION.query(ChatAccessConnectionSettings).get( 84 | str(chat_id) 85 | ) 86 | if chat_setting: 87 | return chat_setting.allow_connect_to_chat 88 | return False 89 | finally: 90 | SESSION.close() 91 | 92 | 93 | def set_allow_connect_to_chat(chat_id: Union[int, str], setting: bool): 94 | with CHAT_ACCESS_LOCK: 95 | chat_setting = SESSION.query(ChatAccessConnectionSettings).get( 96 | str(chat_id) 97 | ) 98 | if not chat_setting: 99 | chat_setting = ChatAccessConnectionSettings(chat_id, setting) 100 | 101 | chat_setting.allow_connect_to_chat = setting 102 | SESSION.add(chat_setting) 103 | SESSION.commit() 104 | 105 | 106 | def connect(user_id, chat_id): 107 | with CONNECTION_INSERTION_LOCK: 108 | prev = SESSION.query(Connection).get((int(user_id))) 109 | if prev: 110 | SESSION.delete(prev) 111 | connect_to_chat = Connection(int(user_id), chat_id) 112 | SESSION.add(connect_to_chat) 113 | SESSION.commit() 114 | return True 115 | 116 | 117 | def get_connected_chat(user_id): 118 | try: 119 | return SESSION.query(Connection).get((int(user_id))) 120 | finally: 121 | SESSION.close() 122 | 123 | 124 | def curr_connection(chat_id): 125 | try: 126 | return SESSION.query(Connection).get((str(chat_id))) 127 | finally: 128 | SESSION.close() 129 | 130 | 131 | def disconnect(user_id): 132 | with CONNECTION_INSERTION_LOCK: 133 | disconnect = SESSION.query(Connection).get((int(user_id))) 134 | if disconnect: 135 | SESSION.delete(disconnect) 136 | SESSION.commit() 137 | return True 138 | else: 139 | SESSION.close() 140 | return False 141 | 142 | 143 | def add_history_conn(user_id, chat_id, chat_name): 144 | global HISTORY_CONNECT 145 | with CONNECTION_HISTORY_LOCK: 146 | conn_time = int(time.time()) 147 | if HISTORY_CONNECT.get(int(user_id)): 148 | counting = ( 149 | SESSION.query(ConnectionHistory.user_id) 150 | .filter(ConnectionHistory.user_id == str(user_id)) 151 | .count() 152 | ) 153 | getchat_id = {} 154 | for x in HISTORY_CONNECT[int(user_id)]: 155 | getchat_id[HISTORY_CONNECT[int(user_id)][x]["chat_id"]] = x 156 | if chat_id in getchat_id: 157 | todeltime = getchat_id[str(chat_id)] 158 | delold = SESSION.query(ConnectionHistory).get( 159 | (int(user_id), str(chat_id)) 160 | ) 161 | if delold: 162 | SESSION.delete(delold) 163 | HISTORY_CONNECT[int(user_id)].pop(todeltime) 164 | elif counting >= 5: 165 | todel = list(HISTORY_CONNECT[int(user_id)]) 166 | todel.reverse() 167 | todel = todel[4:] 168 | for x in todel: 169 | chat_old = HISTORY_CONNECT[int(user_id)][x]["chat_id"] 170 | delold = SESSION.query(ConnectionHistory).get( 171 | (int(user_id), str(chat_old)) 172 | ) 173 | if delold: 174 | SESSION.delete(delold) 175 | HISTORY_CONNECT[int(user_id)].pop(x) 176 | else: 177 | HISTORY_CONNECT[int(user_id)] = {} 178 | delold = SESSION.query(ConnectionHistory).get( 179 | (int(user_id), str(chat_id)) 180 | ) 181 | if delold: 182 | SESSION.delete(delold) 183 | history = ConnectionHistory( 184 | int(user_id), str(chat_id), chat_name, conn_time 185 | ) 186 | SESSION.add(history) 187 | SESSION.commit() 188 | HISTORY_CONNECT[int(user_id)][conn_time] = { 189 | "chat_name": chat_name, 190 | "chat_id": str(chat_id), 191 | } 192 | 193 | 194 | def get_history_conn(user_id): 195 | if not HISTORY_CONNECT.get(int(user_id)): 196 | HISTORY_CONNECT[int(user_id)] = {} 197 | return HISTORY_CONNECT[int(user_id)] 198 | 199 | 200 | def clear_history_conn(user_id): 201 | global HISTORY_CONNECT 202 | todel = list(HISTORY_CONNECT[int(user_id)]) 203 | for x in todel: 204 | chat_old = HISTORY_CONNECT[int(user_id)][x]["chat_id"] 205 | delold = SESSION.query(ConnectionHistory).get( 206 | (int(user_id), str(chat_old)) 207 | ) 208 | if delold: 209 | SESSION.delete(delold) 210 | HISTORY_CONNECT[int(user_id)].pop(x) 211 | SESSION.commit() 212 | return True 213 | 214 | 215 | def __load_user_history(): 216 | global HISTORY_CONNECT 217 | try: 218 | qall = SESSION.query(ConnectionHistory).all() 219 | HISTORY_CONNECT = {} 220 | for x in qall: 221 | check = HISTORY_CONNECT.get(x.user_id) 222 | if check is None: 223 | HISTORY_CONNECT[x.user_id] = {} 224 | HISTORY_CONNECT[x.user_id][x.conn_time] = { 225 | "chat_name": x.chat_name, 226 | "chat_id": x.chat_id, 227 | } 228 | finally: 229 | SESSION.close() 230 | 231 | 232 | __load_user_history() 233 | -------------------------------------------------------------------------------- /ubotindo/modules/userinfo.py: -------------------------------------------------------------------------------- 1 | # UserindoBot 2 | # Copyright (C) 2020 UserindoBot Team, 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | 17 | import html 18 | from typing import Optional 19 | 20 | from telegram import MAX_MESSAGE_LENGTH, Message, ParseMode, User 21 | from telegram.utils.helpers import escape_markdown 22 | 23 | from ubotindo import DEV_USERS, dispatcher 24 | from ubotindo.modules.disable import DisableAbleCommandHandler 25 | from ubotindo.modules.no_sql import get_collection 26 | from ubotindo.modules.helper_funcs.alternate import typing_action 27 | from ubotindo.modules.helper_funcs.extraction import extract_user 28 | 29 | 30 | USER_INFO = get_collection("USER_INFO") 31 | USER_BIO = get_collection("USER_BIO") 32 | 33 | 34 | @typing_action 35 | def about_me(update, context): 36 | message = update.effective_message # type: Optional[Message] 37 | args = context.args 38 | user_id = extract_user(message, args) 39 | 40 | if user_id: 41 | user = context.bot.get_chat(user_id) 42 | else: 43 | user = message.from_user 44 | 45 | info = USER_INFO.find_one({'_id': user.id}) 46 | 47 | if info: 48 | update.effective_message.reply_text( 49 | "*{}*:\n{}".format(user.first_name, escape_markdown(info["info"])), 50 | parse_mode=ParseMode.MARKDOWN, 51 | ) 52 | elif message.reply_to_message: 53 | username = message.reply_to_message.from_user.first_name 54 | update.effective_message.reply_text( 55 | username + "Information about him is currently unavailable !" 56 | ) 57 | else: 58 | update.effective_message.reply_text( 59 | "You have not added any information about yourself yet !" 60 | ) 61 | 62 | 63 | @typing_action 64 | def set_about_me(update, context): 65 | message = update.effective_message # type: Optional[Message] 66 | user_id = message.from_user.id 67 | if user_id == 1087968824: 68 | message.reply_text( 69 | "You cannot set your own bio when you're in anonymous admin mode!" 70 | ) 71 | return 72 | 73 | text = message.text 74 | info = text.split( 75 | None, 1 76 | ) # use python's maxsplit to only remove the cmd, hence keeping newlines. 77 | if len(info) == 2: 78 | if len(info[1]) < MAX_MESSAGE_LENGTH // 4: 79 | USER_INFO.update_one( 80 | {'_id': user_id}, 81 | {"$set": {'info': info[1]}}, 82 | upsert=True) 83 | message.reply_text("Your bio has been saved successfully") 84 | else: 85 | message.reply_text( 86 | " About You{} To be confined to letters ".format( 87 | MAX_MESSAGE_LENGTH // 4, len(info[1]) 88 | ) 89 | ) 90 | 91 | 92 | @typing_action 93 | def about_bio(update, context): 94 | message = update.effective_message # type: Optional[Message] 95 | args = context.args 96 | 97 | user_id = extract_user(message, args) 98 | if user_id: 99 | user = context.bot.get_chat(user_id) 100 | else: 101 | user = message.from_user 102 | 103 | info = USER_BIO.find_one({'_id': user.id}) 104 | 105 | if info: 106 | update.effective_message.reply_text( 107 | "*{}*:\n{}".format(user.first_name, escape_markdown(info["bio"])), 108 | parse_mode=ParseMode.MARKDOWN, 109 | ) 110 | elif message.reply_to_message: 111 | username = user.first_name 112 | update.effective_message.reply_text( 113 | "{} No details about him have been saved yet !".format(username) 114 | ) 115 | else: 116 | update.effective_message.reply_text( 117 | " Your bio about you has been saved !" 118 | ) 119 | 120 | 121 | @typing_action 122 | def set_about_bio(update, context): 123 | message = update.effective_message # type: Optional[Message] 124 | sender = update.effective_user # type: Optional[User] 125 | if message.reply_to_message: 126 | repl_message = message.reply_to_message 127 | user_id = repl_message.from_user.id 128 | if user_id == message.from_user.id: 129 | message.reply_text( 130 | "Are you looking to change your own ... ?? That 's it." 131 | ) 132 | return 133 | elif user_id == context.bot.id and sender.id not in DEV_USERS: 134 | message.reply_text("Only DEV USERS can change my information.") 135 | return 136 | elif user_id == 1087968824: 137 | message.reply_text("You cannot set anonymous user bio!") 138 | return 139 | 140 | text = message.text 141 | # use python's maxsplit to only remove the cmd, hence keeping newlines. 142 | bio = text.split(None, 1) 143 | if len(bio) == 2: 144 | if len(bio[1]) < MAX_MESSAGE_LENGTH // 4: 145 | USER_BIO.update_one( 146 | {'_id': user_id}, 147 | {"$set": {'bio': bio[1]}}, 148 | upsert=True) 149 | message.reply_text( 150 | "{} bio has been successfully saved!".format( 151 | repl_message.from_user.first_name 152 | ) 153 | ) 154 | else: 155 | message.reply_text( 156 | "About you {} Must stick to the letter! The number of characters you have just tried {} hm .".format( 157 | MAX_MESSAGE_LENGTH // 4, len(bio[1]) 158 | ) 159 | ) 160 | else: 161 | message.reply_text( 162 | " His bio can only be saved if someone MESSAGE as a REPLY" 163 | ) 164 | 165 | 166 | def __user_info__(user_id): 167 | bdata = USER_BIO.find_one({'_id': user_id}) 168 | if bdata: 169 | bio = html.escape(bdata["bio"]) 170 | idata = USER_INFO.find_one({'_id': user_id}) 171 | if idata: 172 | me = html.escape(idata["info"]) 173 | 174 | if bdata and idata: 175 | return "About user:\n{me}\n\nWhat others say:\n{bio}".format( 176 | me=me, bio=bio 177 | ) 178 | elif bdata: 179 | return "What others say:\n{}\n".format(bio) 180 | elif idata: 181 | return "About user:\n{}".format(me) 182 | else: 183 | return "" 184 | 185 | 186 | __help__ = """ 187 | Writing something about yourself is cool, whether to make people know about yourself or \ 188 | promoting your profile. 189 | 190 | All bios are displayed on /info command. 191 | 192 | × /setbio : While replying, will save another user's bio 193 | × /bio: Will get your or another user's bio. This cannot be set by yourself. 194 | × /setme : Will set your info 195 | × /me: Will get your or another user's info 196 | 197 | An example of setting a bio for yourself: 198 | `/setme I work for Telegram`; Bio is set to yourself. 199 | 200 | An example of writing someone else' bio: 201 | Reply to user's message: `/setbio He is such cool person`. 202 | 203 | *Notice:* Do not use /setbio against yourself! 204 | """ 205 | 206 | __mod_name__ = "Bios/Abouts" 207 | 208 | SET_BIO_HANDLER = DisableAbleCommandHandler( 209 | "setbio", set_about_bio, run_async=True 210 | ) 211 | GET_BIO_HANDLER = DisableAbleCommandHandler( 212 | "bio", about_bio, pass_args=True, run_async=True 213 | ) 214 | 215 | SET_ABOUT_HANDLER = DisableAbleCommandHandler( 216 | "setme", set_about_me, run_async=True 217 | ) 218 | GET_ABOUT_HANDLER = DisableAbleCommandHandler( 219 | "me", about_me, pass_args=True, run_async=True 220 | ) 221 | 222 | dispatcher.add_handler(SET_BIO_HANDLER) 223 | dispatcher.add_handler(GET_BIO_HANDLER) 224 | dispatcher.add_handler(SET_ABOUT_HANDLER) 225 | dispatcher.add_handler(GET_ABOUT_HANDLER) 226 | -------------------------------------------------------------------------------- /ubotindo/modules/dbcleanup.py: -------------------------------------------------------------------------------- 1 | # UserindoBot 2 | # Copyright (C) 2020 UserindoBot Team, 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | 17 | from time import sleep 18 | 19 | from telegram import Bot, InlineKeyboardButton, InlineKeyboardMarkup, Update 20 | from telegram.error import BadRequest, Unauthorized 21 | from telegram.ext import CallbackQueryHandler, CommandHandler 22 | 23 | from ubotindo.modules.no_sql import gban_db 24 | from ubotindo.modules.no_sql import users_db 25 | from ubotindo import DEV_USERS, dispatcher 26 | from ubotindo.modules.helper_funcs.filters import CustomFilters 27 | 28 | 29 | def get_invalid_chats(bot: Bot, update: Update, remove: bool = False): 30 | chat_id = update.effective_chat.id 31 | chats = users_db.get_all_chats() 32 | kicked_chats, progress = 0, 0 33 | chat_list = [] 34 | progress_message = None 35 | 36 | for chat in chats: 37 | 38 | if ((100 * chats.index(chat)) / len(chats)) > progress: 39 | progress_bar = f"{progress}% completed in getting invalid chats." 40 | if progress_message: 41 | try: 42 | bot.editMessageText( 43 | progress_bar, chat_id, progress_message.message_id 44 | ) 45 | except BaseException: 46 | pass 47 | else: 48 | progress_message = bot.sendMessage(chat_id, progress_bar) 49 | progress += 5 50 | 51 | cid = chat["chat_id"] 52 | sleep(0.5) 53 | try: 54 | bot.get_chat(cid, timeout=120) 55 | except (BadRequest, Unauthorized): 56 | kicked_chats += 1 57 | chat_list.append(cid) 58 | except BaseException: 59 | pass 60 | 61 | try: 62 | progress_message.delete() 63 | except BaseException: 64 | pass 65 | 66 | if not remove: 67 | return kicked_chats 68 | else: 69 | for muted_chat in chat_list: 70 | sleep(0.5) 71 | users_db.rem_chat(muted_chat) 72 | return kicked_chats 73 | 74 | 75 | def get_invalid_gban(bot: Bot, update: Update, remove: bool = False): 76 | banned = gban_db.get_gban_list() 77 | ungbanned_users = 0 78 | ungban_list = [] 79 | 80 | for user in banned: 81 | user_id = user["_id"] 82 | sleep(0.5) 83 | try: 84 | bot.get_chat(user_id) 85 | except BadRequest: 86 | ungbanned_users += 1 87 | ungban_list.append(user_id) 88 | except BaseException: 89 | pass 90 | 91 | if not remove: 92 | return ungbanned_users 93 | else: 94 | for user_id in ungban_list: 95 | sleep(0.5) 96 | gban_db.ungban_user(user_id) 97 | return ungbanned_users 98 | 99 | 100 | def dbcleanup(update, context): 101 | msg = update.effective_message 102 | 103 | msg.reply_text("Getting invalid chat count ...") 104 | invalid_chat_count = get_invalid_chats(context.bot, update) 105 | 106 | msg.reply_text("Getting invalid gbanned count ...") 107 | invalid_gban_count = get_invalid_gban(context.bot, update) 108 | 109 | reply = f"Total invalid chats - {invalid_chat_count}\n" 110 | reply += f"Total invalid gbanned users - {invalid_gban_count}" 111 | 112 | buttons = [ 113 | [InlineKeyboardButton("Cleanup DB", callback_data="db_cleanup")] 114 | ] 115 | 116 | update.effective_message.reply_text( 117 | reply, reply_markup=InlineKeyboardMarkup(buttons) 118 | ) 119 | 120 | 121 | def get_muted_chats(bot: Bot, update: Update, leave: bool = False): 122 | chat_id = update.effective_chat.id 123 | chats = users_db.get_all_chats() 124 | muted_chats, progress = 0, 0 125 | chat_list = [] 126 | progress_message = None 127 | 128 | for chat in chats: 129 | 130 | if ((100 * chats.index(chat)) / len(chats)) > progress: 131 | progress_bar = f"{progress}% completed in getting muted chats." 132 | if progress_message: 133 | try: 134 | bot.editMessageText( 135 | progress_bar, chat_id, progress_message.message_id 136 | ) 137 | except BaseException: 138 | pass 139 | else: 140 | progress_message = bot.sendMessage(chat_id, progress_bar) 141 | progress += 5 142 | 143 | cid = chat["chat_id"] 144 | sleep(0.5) 145 | 146 | try: 147 | bot.send_chat_action(cid, "TYPING", timeout=120) 148 | except (BadRequest, Unauthorized): 149 | muted_chats += +1 150 | chat_list.append(cid) 151 | except BaseException: 152 | pass 153 | 154 | try: 155 | progress_message.delete() 156 | except BaseException: 157 | pass 158 | 159 | if not leave: 160 | return muted_chats 161 | else: 162 | for muted_chat in chat_list: 163 | sleep(0.5) 164 | try: 165 | bot.leaveChat(muted_chat, timeout=120) 166 | except BaseException: 167 | pass 168 | users_db.rem_chat(muted_chat) 169 | return muted_chats 170 | 171 | 172 | def leave_muted_chats(update, context): 173 | message = update.effective_message 174 | progress_message = message.reply_text("Getting chat count ...") 175 | muted_chats = get_muted_chats(context.bot, update) 176 | 177 | buttons = [ 178 | [InlineKeyboardButton("Leave chats", callback_data="db_leave_chat")] 179 | ] 180 | 181 | update.effective_message.reply_text( 182 | f"I am muted in {muted_chats} chats.", 183 | reply_markup=InlineKeyboardMarkup(buttons), 184 | ) 185 | progress_message.delete() 186 | 187 | 188 | def callback_button(update, context): 189 | bot = context.bot 190 | query = update.callback_query 191 | message = query.message 192 | chat_id = update.effective_chat.id 193 | query_type = query.data 194 | 195 | bot.answer_callback_query(query.id) 196 | 197 | if query_type == "db_leave_chat": 198 | if query.from_user.id in DEV_USERS: 199 | bot.editMessageText( 200 | "Leaving chats ...", chat_id, message.message_id 201 | ) 202 | chat_count = get_muted_chats(bot, update, True) 203 | bot.sendMessage(chat_id, f"Left {chat_count} chats.") 204 | else: 205 | query.answer("You are not allowed to use this.") 206 | elif query_type == "db_cleanup": 207 | if query.from_user.id in DEV_USERS: 208 | bot.editMessageText( 209 | "Cleaning up DB ...", chat_id, message.message_id 210 | ) 211 | invalid_chat_count = get_invalid_chats(bot, update, True) 212 | invalid_gban_count = get_invalid_gban(bot, update, True) 213 | reply = "Cleaned up {} chats and {} gbanned users from db.".format( 214 | invalid_chat_count, invalid_gban_count 215 | ) 216 | bot.sendMessage(chat_id, reply) 217 | else: 218 | query.answer("You are not allowed to use this.") 219 | 220 | 221 | DB_CLEANUP_HANDLER = CommandHandler( 222 | "dbcleanup", dbcleanup, filters=CustomFilters.dev_filter, run_async=True 223 | ) 224 | LEAVE_MUTED_CHATS_HANDLER = CommandHandler( 225 | "leavemutedchats", 226 | leave_muted_chats, 227 | filters=CustomFilters.dev_filter, 228 | run_async=True, 229 | ) 230 | BUTTON_HANDLER = CallbackQueryHandler( 231 | callback_button, pattern="db_.*", run_async=True 232 | ) 233 | 234 | dispatcher.add_handler(DB_CLEANUP_HANDLER) 235 | dispatcher.add_handler(LEAVE_MUTED_CHATS_HANDLER) 236 | dispatcher.add_handler(BUTTON_HANDLER) 237 | 238 | __mod_name__ = "DB Cleanup" 239 | __handlers__ = [DB_CLEANUP_HANDLER, LEAVE_MUTED_CHATS_HANDLER, BUTTON_HANDLER] 240 | -------------------------------------------------------------------------------- /ubotindo/modules/reverse.py: -------------------------------------------------------------------------------- 1 | # UserindoBot 2 | # Copyright (C) 2020 UserindoBot Team, 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | 17 | import os 18 | import re 19 | import urllib 20 | from urllib.error import HTTPError, URLError 21 | 22 | import requests 23 | from bs4 import BeautifulSoup 24 | from telegram import InputMediaPhoto, TelegramError 25 | 26 | from ubotindo import dispatcher 27 | from ubotindo.modules.disable import DisableAbleCommandHandler 28 | from ubotindo.modules.helper_funcs.alternate import typing_action 29 | 30 | opener = urllib.request.build_opener() 31 | useragent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.38 Safari/537.36" 32 | # useragent = 'Mozilla/5.0 (Linux; Android 6.0.1; SM-G920V Build/MMB29K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/52.0.2743.98 Mobile Safari/537.36' 33 | opener.addheaders = [("User-agent", useragent)] 34 | 35 | 36 | @typing_action 37 | def reverse(update, context): 38 | if os.path.isfile("okgoogle.png"): 39 | os.remove("okgoogle.png") 40 | 41 | msg = update.effective_message 42 | chat_id = update.effective_chat.id 43 | rtmid = msg.message_id 44 | args = context.args 45 | imagename = "okgoogle.png" 46 | 47 | reply = msg.reply_to_message 48 | if reply: 49 | if reply.sticker: 50 | file_id = reply.sticker.file_id 51 | elif reply.photo: 52 | file_id = reply.photo[-1].file_id 53 | elif reply.document: 54 | file_id = reply.document.file_id 55 | else: 56 | msg.reply_text("Reply to an image or sticker to lookup.") 57 | return 58 | image_file = context.bot.get_file(file_id) 59 | image_file.download(imagename) 60 | if args: 61 | txt = args[0] 62 | try: 63 | lim = int(txt) 64 | except BaseException: 65 | lim = 2 66 | else: 67 | lim = 2 68 | elif args and not reply: 69 | splatargs = msg.text.split(" ") 70 | if len(splatargs) == 3: 71 | img_link = splatargs[1] 72 | try: 73 | lim = int(splatargs[2]) 74 | except BaseException: 75 | lim = 2 76 | elif len(splatargs) == 2: 77 | img_link = splatargs[1] 78 | lim = 2 79 | else: 80 | msg.reply_text("/reverse ") 81 | return 82 | try: 83 | urllib.request.urlretrieve(img_link, imagename) 84 | except HTTPError as HE: 85 | if HE.reason == "Not Found": 86 | msg.reply_text("Image not found.") 87 | return 88 | elif HE.reason == "Forbidden": 89 | msg.reply_text( 90 | "Couldn't access the provided link, The website might have blocked accessing to the website by bot or the website does not existed." 91 | ) 92 | return 93 | except URLError as UE: 94 | msg.reply_text(f"{UE.reason}") 95 | return 96 | except ValueError as VE: 97 | msg.reply_text( 98 | f"{VE}\nPlease try again using http or https protocol." 99 | ) 100 | return 101 | else: 102 | msg.reply_markdown( 103 | "Please reply to a sticker, or an image to search it!\nDo you know that you can search an image with a link too? `/reverse [picturelink] `." 104 | ) 105 | return 106 | 107 | try: 108 | searchUrl = "https://www.google.com/searchbyimage/upload" 109 | multipart = { 110 | "encoded_image": (imagename, open(imagename, "rb")), 111 | "image_content": "", 112 | } 113 | response = requests.post( 114 | searchUrl, files=multipart, allow_redirects=False 115 | ) 116 | fetchUrl = response.headers["Location"] 117 | 118 | if response != 400: 119 | xx = context.bot.send_message( 120 | chat_id, 121 | "Image was successfully uploaded to Google." 122 | "\nParsing source now. Maybe.", 123 | reply_to_message_id=rtmid, 124 | ) 125 | else: 126 | xx = context.bot.send_message( 127 | chat_id, 128 | "Google told me to go away.", 129 | reply_to_message_id=rtmid, 130 | ) 131 | return 132 | 133 | os.remove(imagename) 134 | match = ParseSauce(fetchUrl + "&hl=en") 135 | guess = match["best_guess"] 136 | if match["override"] and not match["override"] == "": 137 | imgspage = match["override"] 138 | else: 139 | imgspage = match["similar_images"] 140 | 141 | if guess and imgspage: 142 | xx.edit_text( 143 | f"[{guess}]({fetchUrl})\nLooking for images...", 144 | parse_mode="Markdown", 145 | disable_web_page_preview=True, 146 | ) 147 | else: 148 | xx.edit_text("Couldn't find anything.") 149 | return 150 | 151 | images = scam(imgspage, lim) 152 | if len(images) == 0: 153 | xx.edit_text( 154 | f"[{guess}]({fetchUrl})\n[Visually similar images]({imgspage})" 155 | "\nCouldn't fetch any images.", 156 | parse_mode="Markdown", 157 | ) 158 | return 159 | 160 | imglinks = [] 161 | for link in images: 162 | lmao = InputMediaPhoto(media=str(link)) 163 | imglinks.append(lmao) 164 | 165 | context.bot.send_media_group( 166 | chat_id=chat_id, media=imglinks, reply_to_message_id=rtmid 167 | ) 168 | xx.edit_text( 169 | f"[{guess}]({fetchUrl})\n[Visually similar images]({imgspage})", 170 | parse_mode="Markdown", 171 | disable_web_page_preview=True, 172 | ) 173 | except TelegramError as e: 174 | print(e) 175 | except Exception as exception: 176 | print(exception) 177 | 178 | 179 | def ParseSauce(googleurl): 180 | source = opener.open(googleurl).read() 181 | soup = BeautifulSoup(source, "html.parser") 182 | 183 | results = {"similar_images": "", "override": "", "best_guess": ""} 184 | 185 | try: 186 | for bess in soup.findAll("a", {"class": "PBorbe"}): 187 | url = "https://www.google.com" + bess.get("href") 188 | results["override"] = url 189 | except BaseException: 190 | pass 191 | 192 | for similar_image in soup.findAll("input", {"class": "gLFyf"}): 193 | url = ( 194 | "https://www.google.com/search?tbm=isch&q=" 195 | + urllib.parse.quote_plus(similar_image.get("value")) 196 | ) 197 | results["similar_images"] = url 198 | 199 | for best_guess in soup.findAll("div", attrs={"class": "r5a77d"}): 200 | results["best_guess"] = best_guess.get_text() 201 | 202 | return results 203 | 204 | 205 | def scam(imgspage, lim): 206 | """Parse/Scrape the HTML code for the info we want.""" 207 | 208 | single = opener.open(imgspage).read() 209 | decoded = single.decode("utf-8") 210 | if int(lim) > 10: 211 | lim = 10 212 | 213 | imglinks = [] 214 | counter = 0 215 | 216 | pattern = r"^,\[\"(.*[.png|.jpg|.jpeg])\",[0-9]+,[0-9]+\]$" 217 | oboi = re.findall(pattern, decoded, re.I | re.M) 218 | 219 | for imglink in oboi: 220 | counter += 1 221 | imglinks.append(imglink) 222 | if counter >= int(lim): 223 | break 224 | 225 | return imglinks 226 | 227 | 228 | REVERSE_HANDLER = DisableAbleCommandHandler( 229 | "reverse", reverse, pass_args=True, admin_ok=True, run_async=True 230 | ) 231 | 232 | dispatcher.add_handler(REVERSE_HANDLER) 233 | -------------------------------------------------------------------------------- /ubotindo/modules/log_channel.py: -------------------------------------------------------------------------------- 1 | # UserindoBot 2 | # Copyright (C) 2020 UserindoBot Team, 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | 17 | from functools import wraps 18 | 19 | from ubotindo.modules.helper_funcs.misc import is_module_loaded 20 | 21 | FILENAME = __name__.rsplit(".", 1)[-1] 22 | 23 | if is_module_loaded(FILENAME): 24 | from telegram import Bot, ParseMode 25 | from telegram.error import BadRequest, Unauthorized 26 | from telegram.ext import CommandHandler 27 | from telegram.utils.helpers import escape_markdown 28 | 29 | from ubotindo import LOGGER, dispatcher 30 | from ubotindo.modules.helper_funcs.chat_status import user_admin 31 | from ubotindo.modules.no_sql import log_channel_db as db 32 | 33 | def loggable(func): 34 | @wraps(func) 35 | def log_action(update, context, *args, **kwargs): 36 | result = func(update, context, *args, **kwargs) 37 | chat = update.effective_chat 38 | message = update.effective_message 39 | if result: 40 | if chat.type == chat.SUPERGROUP and chat.username: 41 | result += ( 42 | "\nLink: " 43 | 'click here'.format( 44 | chat.username, message.message_id 45 | ) 46 | ) 47 | log_chat = db.get_chat_log_channel(chat.id) 48 | if log_chat: 49 | try: 50 | send_log(context.bot, log_chat, chat.id, result) 51 | except Unauthorized: 52 | db.stop_chat_logging(chat.id) 53 | 54 | elif result == "": 55 | pass 56 | else: 57 | LOGGER.warning( 58 | "%s was set as loggable, but had no return statement.", 59 | func, 60 | ) 61 | 62 | return result 63 | 64 | return log_action 65 | 66 | def send_log(bot: Bot, log_chat_id: str, orig_chat_id: str, result: str): 67 | try: 68 | bot.send_message(log_chat_id, result, parse_mode=ParseMode.HTML) 69 | except BadRequest as excp: 70 | if excp.message == "Chat not found": 71 | bot.send_message( 72 | orig_chat_id, 73 | "This log channel has been deleted - unsetting.", 74 | ) 75 | db.stop_chat_logging(orig_chat_id) 76 | else: 77 | LOGGER.warning(excp.message) 78 | LOGGER.warning(result) 79 | LOGGER.exception("Could not parse") 80 | 81 | bot.send_message( 82 | log_chat_id, 83 | result 84 | + "\n\nFormatting has been disabled due to an unexpected error.", 85 | ) 86 | 87 | @user_admin 88 | def logging(update, context): 89 | message = update.effective_message 90 | chat = update.effective_chat 91 | 92 | log_channel = db.get_chat_log_channel(chat.id) 93 | if log_channel: 94 | log_channel_info = context.bot.get_chat(log_channel) 95 | message.reply_text( 96 | "This group has all it's logs sent to: {} (`{}`)".format( 97 | escape_markdown(log_channel_info.title), log_channel 98 | ), 99 | parse_mode=ParseMode.MARKDOWN, 100 | ) 101 | 102 | else: 103 | message.reply_text("No log channel has been set for this group!") 104 | 105 | @user_admin 106 | def setlog(update, context): 107 | message = update.effective_message 108 | chat = update.effective_chat 109 | if chat.type == chat.CHANNEL: 110 | message.reply_text( 111 | "Now, forward the /setlog to the group you want to tie this channel to!" 112 | ) 113 | 114 | elif message.forward_from_chat: 115 | db.set_chat_log_channel(chat.id, message.forward_from_chat.id) 116 | try: 117 | message.delete() 118 | except BadRequest as excp: 119 | if excp.message == "Message to delete not found": 120 | pass 121 | else: 122 | LOGGER.exception( 123 | "Error deleting message in log channel. Should work anyway though." 124 | ) 125 | 126 | try: 127 | context.bot.send_message( 128 | message.forward_from_chat.id, 129 | "This channel has been set as the log channel for {}.".format( 130 | chat.title or chat.first_name 131 | ), 132 | ) 133 | except Unauthorized as excp: 134 | if ( 135 | excp.message 136 | == "Forbidden: bot is not a member of the channel chat" 137 | ): 138 | context.bot.send_message( 139 | chat.id, "Successfully set log channel!" 140 | ) 141 | else: 142 | LOGGER.exception("ERROR in setting the log channel.") 143 | 144 | context.bot.send_message(chat.id, "Successfully set log channel!") 145 | 146 | else: 147 | message.reply_text( 148 | "The steps to set a log channel are:\n" 149 | " - add bot to the desired channel\n" 150 | " - send /setlog to the channel\n" 151 | " - forward the /setlog to the group\n" 152 | ) 153 | 154 | @user_admin 155 | def unsetlog(update, context): 156 | message = update.effective_message 157 | chat = update.effective_chat 158 | 159 | log_channel = db.stop_chat_logging(chat.id) 160 | if log_channel: 161 | context.bot.send_message( 162 | log_channel, 163 | "Channel has been unlinked from {}".format(chat.title), 164 | ) 165 | message.reply_text("Log channel has been un-set.") 166 | 167 | else: 168 | message.reply_text("No log channel has been set yet!") 169 | 170 | def __stats__(): 171 | return "× {} log channels have been set.".format(db.num_logchannels()) 172 | 173 | def __migrate__(old_chat_id, new_chat_id): 174 | db.migrate_chat(old_chat_id, new_chat_id) 175 | 176 | def __chat_settings__(chat_id, user_id): 177 | log_channel = db.get_chat_log_channel(chat_id) 178 | if log_channel: 179 | log_channel_info = dispatcher.bot.get_chat(log_channel) 180 | return "This group has all it's logs sent to: {} (`{}`)".format( 181 | escape_markdown(log_channel_info.title), log_channel 182 | ) 183 | return "No log channel is set for this group!" 184 | 185 | __help__ = """ 186 | Recent actions are nice, but they don't help you log every action taken by the bot. This is why you need log channels! 187 | 188 | Log channels can help you keep track of exactly what the other admins are doing. \ 189 | Bans, Mutes, warns, notes - everything can be moderated. 190 | 191 | *Admin only:* 192 | × /logchannel: Get log channel info 193 | × /setlog: Set the log channel. 194 | × /unsetlog: Unset the log channel. 195 | 196 | Setting the log channel is done by: 197 | × Add the bot to your channel, as an admin. This is done via the "add administrators" tab. 198 | × Send /setlog to your channel. 199 | × Forward the /setlog command to the group you wish to be logged. 200 | × Congratulations! All is set! 201 | """ 202 | 203 | __mod_name__ = "Logger" 204 | 205 | LOG_HANDLER = CommandHandler("logchannel", logging, run_async=True) 206 | SET_LOG_HANDLER = CommandHandler("setlog", setlog, run_async=True) 207 | UNSET_LOG_HANDLER = CommandHandler("unsetlog", unsetlog, run_async=True) 208 | 209 | dispatcher.add_handler(LOG_HANDLER) 210 | dispatcher.add_handler(SET_LOG_HANDLER) 211 | dispatcher.add_handler(UNSET_LOG_HANDLER) 212 | 213 | else: 214 | # run anyway if module not loaded 215 | def loggable(func): 216 | return func 217 | -------------------------------------------------------------------------------- /ubotindo/modules/helper_funcs/msg_types.py: -------------------------------------------------------------------------------- 1 | # UserindoBot 2 | # Copyright (C) 2020 UserindoBot Team, 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | 17 | from enum import IntEnum, unique 18 | 19 | from telegram import Message 20 | 21 | from ubotindo.modules.helper_funcs.string_handling import ( 22 | button_markdown_parser, 23 | ) 24 | 25 | 26 | @unique 27 | class Types(IntEnum): 28 | TEXT = 0 29 | BUTTON_TEXT = 1 30 | STICKER = 2 31 | DOCUMENT = 3 32 | PHOTO = 4 33 | AUDIO = 5 34 | VOICE = 6 35 | VIDEO = 7 36 | 37 | 38 | def get_note_type(msg: Message): 39 | data_type = None 40 | content = None 41 | text = "" 42 | raw_text = msg.text or msg.caption 43 | # use python's maxsplit to separate cmd and args 44 | args = raw_text.split(None, 2) 45 | note_name = args[1] 46 | 47 | buttons = [] 48 | # determine what the contents of the filter are - text, image, sticker, etc 49 | if len(args) >= 3: 50 | offset = len(args[2]) - len( 51 | raw_text 52 | ) # set correct offset relative to command + notename 53 | text, buttons = button_markdown_parser( 54 | args[2], 55 | entities=msg.parse_entities() or msg.parse_caption_entities(), 56 | offset=offset, 57 | ) 58 | if buttons: 59 | data_type = Types.BUTTON_TEXT 60 | else: 61 | data_type = Types.TEXT 62 | 63 | elif msg.reply_to_message: 64 | entities = msg.reply_to_message.parse_entities() 65 | msgtext = msg.reply_to_message.text or msg.reply_to_message.caption 66 | if len(args) >= 2 and msg.reply_to_message.text: # not caption, text 67 | text, buttons = button_markdown_parser(msgtext, entities=entities) 68 | if buttons: 69 | data_type = Types.BUTTON_TEXT 70 | else: 71 | data_type = Types.TEXT 72 | 73 | elif msg.reply_to_message.sticker: 74 | content = msg.reply_to_message.sticker.file_id 75 | data_type = Types.STICKER 76 | 77 | elif msg.reply_to_message.document: 78 | content = msg.reply_to_message.document.file_id 79 | text, buttons = button_markdown_parser(msgtext, entities=entities) 80 | data_type = Types.DOCUMENT 81 | 82 | elif msg.reply_to_message.photo: 83 | # last elem = best quality 84 | content = msg.reply_to_message.photo[-1].file_id 85 | text, buttons = button_markdown_parser(msgtext, entities=entities) 86 | data_type = Types.PHOTO 87 | 88 | elif msg.reply_to_message.audio: 89 | content = msg.reply_to_message.audio.file_id 90 | text, buttons = button_markdown_parser(msgtext, entities=entities) 91 | data_type = Types.AUDIO 92 | 93 | elif msg.reply_to_message.voice: 94 | content = msg.reply_to_message.voice.file_id 95 | text, buttons = button_markdown_parser(msgtext, entities=entities) 96 | data_type = Types.VOICE 97 | 98 | elif msg.reply_to_message.video: 99 | content = msg.reply_to_message.video.file_id 100 | text, buttons = button_markdown_parser(msgtext, entities=entities) 101 | data_type = Types.VIDEO 102 | 103 | return note_name, text, data_type, content, buttons 104 | 105 | 106 | # note: add own args? 107 | def get_welcome_type(msg: Message): 108 | data_type = None 109 | content = None 110 | text = "" 111 | raw_text = msg.text or msg.caption 112 | args = raw_text.split( 113 | None, 1 114 | ) # use python's maxsplit to separate cmd and args 115 | 116 | buttons = [] 117 | # determine what the contents of the filter are - text, image, sticker, etc 118 | if len(args) >= 2: 119 | offset = len(args[1]) - len( 120 | raw_text 121 | ) # set correct offset relative to command + notename 122 | text, buttons = button_markdown_parser( 123 | args[1], 124 | entities=msg.parse_entities() or msg.parse_caption_entities(), 125 | offset=offset, 126 | ) 127 | if buttons: 128 | data_type = Types.BUTTON_TEXT 129 | else: 130 | data_type = Types.TEXT 131 | 132 | elif msg.reply_to_message: 133 | entities = msg.reply_to_message.parse_entities() 134 | msgtext = msg.reply_to_message.text or msg.reply_to_message.caption 135 | if len(args) >= 1 and msg.reply_to_message.text: # not caption, text 136 | text, buttons = button_markdown_parser(msgtext, entities=entities) 137 | if buttons: 138 | data_type = Types.BUTTON_TEXT 139 | else: 140 | data_type = Types.TEXT 141 | 142 | elif msg.reply_to_message.sticker: 143 | content = msg.reply_to_message.sticker.file_id 144 | data_type = Types.STICKER 145 | 146 | elif msg.reply_to_message.document: 147 | content = msg.reply_to_message.document.file_id 148 | text, buttons = button_markdown_parser(msgtext, entities=entities) 149 | data_type = Types.DOCUMENT 150 | 151 | elif msg.reply_to_message.photo: 152 | # last elem = best quality 153 | content = msg.reply_to_message.photo[-1].file_id 154 | text, buttons = button_markdown_parser(msgtext, entities=entities) 155 | data_type = Types.PHOTO 156 | 157 | elif msg.reply_to_message.audio: 158 | content = msg.reply_to_message.audio.file_id 159 | text, buttons = button_markdown_parser(msgtext, entities=entities) 160 | data_type = Types.AUDIO 161 | 162 | elif msg.reply_to_message.voice: 163 | content = msg.reply_to_message.voice.file_id 164 | text, buttons = button_markdown_parser(msgtext, entities=entities) 165 | data_type = Types.VOICE 166 | 167 | elif msg.reply_to_message.video: 168 | content = msg.reply_to_message.video.file_id 169 | text, buttons = button_markdown_parser(msgtext, entities=entities) 170 | data_type = Types.VIDEO 171 | 172 | elif msg.reply_to_message.video_note: 173 | content = msg.reply_to_message.video_note.file_id 174 | text, buttons = button_markdown_parser(msgtext, entities=entities) 175 | data_type = Types.VIDEO_NOTE 176 | 177 | return text, data_type, content, buttons 178 | 179 | 180 | def get_filter_type(msg: Message): 181 | 182 | if not msg.reply_to_message and msg.text and len(msg.text.split()) >= 3: 183 | content = None 184 | text = msg.text.split(None, 2)[2] 185 | data_type = Types.TEXT 186 | 187 | elif ( 188 | msg.reply_to_message 189 | and msg.reply_to_message.text 190 | and len(msg.text.split()) >= 2 191 | ): 192 | content = None 193 | text = msg.reply_to_message.text 194 | data_type = Types.TEXT 195 | 196 | elif msg.reply_to_message and msg.reply_to_message.sticker: 197 | content = msg.reply_to_message.sticker.file_id 198 | text = None 199 | data_type = Types.STICKER 200 | 201 | elif msg.reply_to_message and msg.reply_to_message.document: 202 | content = msg.reply_to_message.document.file_id 203 | text = msg.reply_to_message.caption 204 | data_type = Types.DOCUMENT 205 | 206 | elif msg.reply_to_message and msg.reply_to_message.photo: 207 | # last elem = best quality 208 | content = msg.reply_to_message.photo[-1].file_id 209 | text = msg.reply_to_message.caption 210 | data_type = Types.PHOTO 211 | 212 | elif msg.reply_to_message and msg.reply_to_message.audio: 213 | content = msg.reply_to_message.audio.file_id 214 | text = msg.reply_to_message.caption 215 | data_type = Types.AUDIO 216 | 217 | elif msg.reply_to_message and msg.reply_to_message.voice: 218 | content = msg.reply_to_message.voice.file_id 219 | text = msg.reply_to_message.caption 220 | data_type = Types.VOICE 221 | 222 | elif msg.reply_to_message and msg.reply_to_message.video: 223 | content = msg.reply_to_message.video.file_id 224 | text = msg.reply_to_message.caption 225 | data_type = Types.VIDEO 226 | 227 | elif msg.reply_to_message and msg.reply_to_message.video_note: 228 | content = msg.reply_to_message.video_note.file_id 229 | text = None 230 | data_type = Types.VIDEO_NOTE 231 | 232 | else: 233 | text = None 234 | data_type = None 235 | content = None 236 | 237 | return text, data_type, content 238 | -------------------------------------------------------------------------------- /ubotindo/modules/sql/locks_sql.py: -------------------------------------------------------------------------------- 1 | # UserindoBot 2 | # Copyright (C) 2020 UserindoBot Team, 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | 17 | # New chat added -> setup permissions 18 | import threading 19 | 20 | from sqlalchemy import Column, String, Boolean 21 | 22 | from ubotindo.modules.sql import SESSION, BASE 23 | 24 | 25 | class Permissions(BASE): 26 | __tablename__ = "permissions" 27 | chat_id = Column(String(14), primary_key=True) 28 | # Booleans are for "is this locked", _NOT_ "is this allowed" 29 | audio = Column(Boolean, default=False) 30 | voice = Column(Boolean, default=False) 31 | contact = Column(Boolean, default=False) 32 | video = Column(Boolean, default=False) 33 | document = Column(Boolean, default=False) 34 | photo = Column(Boolean, default=False) 35 | sticker = Column(Boolean, default=False) 36 | gif = Column(Boolean, default=False) 37 | url = Column(Boolean, default=False) 38 | bots = Column(Boolean, default=False) 39 | forward = Column(Boolean, default=False) 40 | game = Column(Boolean, default=False) 41 | location = Column(Boolean, default=False) 42 | rtl = Column(Boolean, default=False) 43 | button = Column(Boolean, default=False) 44 | egame = Column(Boolean, default=False) 45 | inline = Column(Boolean, default=False) 46 | 47 | def __init__(self, chat_id): 48 | self.chat_id = str(chat_id) # ensure string 49 | self.audio = False 50 | self.voice = False 51 | self.contact = False 52 | self.video = False 53 | self.document = False 54 | self.photo = False 55 | self.sticker = False 56 | self.gif = False 57 | self.url = False 58 | self.bots = False 59 | self.forward = False 60 | self.game = False 61 | self.location = False 62 | self.rtl = False 63 | self.button = False 64 | self.egame = False 65 | self.inline = False 66 | 67 | def __repr__(self): 68 | return "" % self.chat_id 69 | 70 | 71 | class Restrictions(BASE): 72 | __tablename__ = "restrictions" 73 | chat_id = Column(String(14), primary_key=True) 74 | # Booleans are for "is this restricted", _NOT_ "is this allowed" 75 | messages = Column(Boolean, default=False) 76 | media = Column(Boolean, default=False) 77 | other = Column(Boolean, default=False) 78 | preview = Column(Boolean, default=False) 79 | 80 | def __init__(self, chat_id): 81 | self.chat_id = str(chat_id) # ensure string 82 | self.messages = False 83 | self.media = False 84 | self.other = False 85 | self.preview = False 86 | 87 | def __repr__(self): 88 | return "" % self.chat_id 89 | 90 | 91 | Permissions.__table__.create(checkfirst=True) 92 | Restrictions.__table__.create(checkfirst=True) 93 | 94 | 95 | PERM_LOCK = threading.RLock() 96 | RESTR_LOCK = threading.RLock() 97 | 98 | 99 | def init_permissions(chat_id, reset=False): 100 | curr_perm = SESSION.query(Permissions).get(str(chat_id)) 101 | if reset: 102 | SESSION.delete(curr_perm) 103 | SESSION.flush() 104 | perm = Permissions(str(chat_id)) 105 | SESSION.add(perm) 106 | SESSION.commit() 107 | return perm 108 | 109 | 110 | def init_restrictions(chat_id, reset=False): 111 | curr_restr = SESSION.query(Restrictions).get(str(chat_id)) 112 | if reset: 113 | SESSION.delete(curr_restr) 114 | SESSION.flush() 115 | restr = Restrictions(str(chat_id)) 116 | SESSION.add(restr) 117 | SESSION.commit() 118 | return restr 119 | 120 | 121 | def update_lock(chat_id, lock_type, locked): 122 | with PERM_LOCK: 123 | curr_perm = SESSION.query(Permissions).get(str(chat_id)) 124 | if not curr_perm: 125 | curr_perm = init_permissions(chat_id) 126 | 127 | if lock_type == "audio": 128 | curr_perm.audio = locked 129 | elif lock_type == "voice": 130 | curr_perm.voice = locked 131 | elif lock_type == "contact": 132 | curr_perm.contact = locked 133 | elif lock_type == "video": 134 | curr_perm.video = locked 135 | elif lock_type == "document": 136 | curr_perm.document = locked 137 | elif lock_type == "photo": 138 | curr_perm.photo = locked 139 | elif lock_type == "sticker": 140 | curr_perm.sticker = locked 141 | elif lock_type == "gif": 142 | curr_perm.gif = locked 143 | elif lock_type == "url": 144 | curr_perm.url = locked 145 | elif lock_type == "bots": 146 | curr_perm.bots = locked 147 | elif lock_type == "forward": 148 | curr_perm.forward = locked 149 | elif lock_type == "game": 150 | curr_perm.game = locked 151 | elif lock_type == "location": 152 | curr_perm.location = locked 153 | elif lock_type == "rtl": 154 | curr_perm.rtl = locked 155 | elif lock_type == "button": 156 | curr_perm.button = locked 157 | elif lock_type == "egame": 158 | curr_perm.egame = locked 159 | elif lock_type == "inline": 160 | curr_perm.inline = locked 161 | 162 | SESSION.add(curr_perm) 163 | SESSION.commit() 164 | 165 | 166 | def update_restriction(chat_id, restr_type, locked): 167 | with RESTR_LOCK: 168 | curr_restr = SESSION.query(Restrictions).get(str(chat_id)) 169 | if not curr_restr: 170 | curr_restr = init_restrictions(chat_id) 171 | 172 | if restr_type == "messages": 173 | curr_restr.messages = locked 174 | elif restr_type == "media": 175 | curr_restr.media = locked 176 | elif restr_type == "other": 177 | curr_restr.other = locked 178 | elif restr_type == "previews": 179 | curr_restr.preview = locked 180 | elif restr_type == "all": 181 | curr_restr.messages = locked 182 | curr_restr.media = locked 183 | curr_restr.other = locked 184 | curr_restr.preview = locked 185 | SESSION.add(curr_restr) 186 | SESSION.commit() 187 | 188 | 189 | def is_locked(chat_id, lock_type): 190 | curr_perm = SESSION.query(Permissions).get(str(chat_id)) 191 | SESSION.close() 192 | 193 | if not curr_perm: 194 | return False 195 | 196 | elif lock_type == "sticker": 197 | return curr_perm.sticker 198 | elif lock_type == "photo": 199 | return curr_perm.photo 200 | elif lock_type == "audio": 201 | return curr_perm.audio 202 | elif lock_type == "voice": 203 | return curr_perm.voice 204 | elif lock_type == "contact": 205 | return curr_perm.contact 206 | elif lock_type == "video": 207 | return curr_perm.video 208 | elif lock_type == "document": 209 | return curr_perm.document 210 | elif lock_type == "gif": 211 | return curr_perm.gif 212 | elif lock_type == "url": 213 | return curr_perm.url 214 | elif lock_type == "bots": 215 | return curr_perm.bots 216 | elif lock_type == "forward": 217 | return curr_perm.forward 218 | elif lock_type == "game": 219 | return curr_perm.game 220 | elif lock_type == "location": 221 | return curr_perm.location 222 | elif lock_type == "rtl": 223 | return curr_perm.rtl 224 | elif lock_type == "button": 225 | return curr_perm.button 226 | elif lock_type == "egame": 227 | return curr_perm.egame 228 | elif lock_type == "inline": 229 | return curr_perm.inline 230 | 231 | 232 | def is_restr_locked(chat_id, lock_type): 233 | curr_restr = SESSION.query(Restrictions).get(str(chat_id)) 234 | SESSION.close() 235 | 236 | if not curr_restr: 237 | return False 238 | 239 | if lock_type == "messages": 240 | return curr_restr.messages 241 | elif lock_type == "media": 242 | return curr_restr.media 243 | elif lock_type == "other": 244 | return curr_restr.other 245 | elif lock_type == "previews": 246 | return curr_restr.preview 247 | elif lock_type == "all": 248 | return ( 249 | curr_restr.messages 250 | and curr_restr.media 251 | and curr_restr.other 252 | and curr_restr.preview 253 | ) 254 | 255 | 256 | def get_locks(chat_id): 257 | try: 258 | return SESSION.query(Permissions).get(str(chat_id)) 259 | finally: 260 | SESSION.close() 261 | 262 | 263 | def get_restr(chat_id): 264 | try: 265 | return SESSION.query(Restrictions).get(str(chat_id)) 266 | finally: 267 | SESSION.close() 268 | 269 | 270 | def migrate_chat(old_chat_id, new_chat_id): 271 | with PERM_LOCK: 272 | perms = SESSION.query(Permissions).get(str(old_chat_id)) 273 | if perms: 274 | perms.chat_id = str(new_chat_id) 275 | SESSION.commit() 276 | 277 | with RESTR_LOCK: 278 | rest = SESSION.query(Restrictions).get(str(old_chat_id)) 279 | if rest: 280 | rest.chat_id = str(new_chat_id) 281 | SESSION.commit() 282 | -------------------------------------------------------------------------------- /ubotindo/modules/muting.py: -------------------------------------------------------------------------------- 1 | # UserindoBot 2 | # Copyright (C) 2020 UserindoBot Team, 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | 17 | import html 18 | from typing import Optional 19 | 20 | from telegram import Chat, ChatPermissions, Message, User 21 | from telegram.error import BadRequest 22 | from telegram.ext import CommandHandler, Filters 23 | from telegram.utils.helpers import mention_html 24 | 25 | from ubotindo import LOGGER, dispatcher 26 | from ubotindo.modules.helper_funcs.admin_rights import user_can_ban 27 | from ubotindo.modules.helper_funcs.alternate import typing_action 28 | from ubotindo.modules.helper_funcs.chat_status import ( 29 | bot_admin, 30 | can_restrict, 31 | is_user_admin, 32 | user_admin, 33 | ) 34 | from ubotindo.modules.helper_funcs.extraction import ( 35 | extract_user, 36 | extract_user_and_text, 37 | ) 38 | from ubotindo.modules.helper_funcs.string_handling import extract_time 39 | from ubotindo.modules.log_channel import loggable 40 | 41 | 42 | @bot_admin 43 | @user_admin 44 | @loggable 45 | @typing_action 46 | def mute(update, context): 47 | chat = update.effective_chat # type: Optional[Chat] 48 | user = update.effective_user # type: Optional[User] 49 | message = update.effective_message # type: Optional[Message] 50 | args = context.args 51 | 52 | if user_can_ban(chat, user, context.bot.id) == False: 53 | message.reply_text( 54 | "You don't have enough rights to restrict someone from talking!" 55 | ) 56 | return "" 57 | 58 | user_id = extract_user(message, args) 59 | if not user_id: 60 | message.reply_text( 61 | "You'll need to either give me a username to mute, or reply to someone to be muted." 62 | ) 63 | return "" 64 | 65 | if user_id == context.bot.id: 66 | message.reply_text("Yeahh... I'm not muting myself!") 67 | return "" 68 | 69 | member = chat.get_member(int(user_id)) 70 | 71 | if member: 72 | if is_user_admin(chat, user_id, member=member): 73 | message.reply_text( 74 | "Well i'm not gonna stop an admin from talking!" 75 | ) 76 | 77 | elif member.can_send_messages is None or member.can_send_messages: 78 | context.bot.restrict_chat_member( 79 | chat.id, 80 | user_id, 81 | permissions=ChatPermissions(can_send_messages=False), 82 | ) 83 | message.reply_text("👍🏻 muted! 🤐") 84 | return ( 85 | "{}:" 86 | "\n#MUTE" 87 | "\nAdmin: {}" 88 | "\nUser: {}".format( 89 | html.escape(chat.title), 90 | mention_html(user.id, user.first_name), 91 | mention_html(member.user.id, member.user.first_name), 92 | ) 93 | ) 94 | 95 | else: 96 | message.reply_text("This user is already taped 🤐") 97 | else: 98 | message.reply_text("This user isn't in the chat!") 99 | 100 | return "" 101 | 102 | 103 | @bot_admin 104 | @user_admin 105 | @loggable 106 | @typing_action 107 | def unmute(update, context): 108 | chat = update.effective_chat # type: Optional[Chat] 109 | user = update.effective_user # type: Optional[User] 110 | message = update.effective_message # type: Optional[Message] 111 | args = context.args 112 | 113 | if user_can_ban(chat, user, context.bot.id) == False: 114 | message.reply_text("You don't have enough rights to unmute people") 115 | return "" 116 | 117 | user_id = extract_user(message, args) 118 | if not user_id: 119 | message.reply_text( 120 | "You'll need to either give me a username to unmute, or reply to someone to be unmuted." 121 | ) 122 | return "" 123 | 124 | member = chat.get_member(int(user_id)) 125 | 126 | if member.status != "kicked" and member.status != "left": 127 | if ( 128 | member.can_send_messages 129 | and member.can_send_media_messages 130 | and member.can_send_other_messages 131 | and member.can_add_web_page_previews 132 | ): 133 | message.reply_text("This user already has the right to speak.") 134 | else: 135 | context.bot.restrict_chat_member( 136 | chat.id, 137 | int(user_id), 138 | permissions=ChatPermissions( 139 | can_send_messages=True, 140 | can_invite_users=True, 141 | can_pin_messages=True, 142 | can_send_polls=True, 143 | can_change_info=True, 144 | can_send_media_messages=True, 145 | can_send_other_messages=True, 146 | can_add_web_page_previews=True, 147 | ), 148 | ) 149 | message.reply_text("Yep! this user can start talking again...") 150 | return ( 151 | "{}:" 152 | "\n#UNMUTE" 153 | "\nAdmin: {}" 154 | "\nUser: {}".format( 155 | html.escape(chat.title), 156 | mention_html(user.id, user.first_name), 157 | mention_html(member.user.id, member.user.first_name), 158 | ) 159 | ) 160 | else: 161 | message.reply_text( 162 | "This user isn't even in the chat, unmuting them won't make them talk more than they " 163 | "already do!" 164 | ) 165 | 166 | return "" 167 | 168 | 169 | @bot_admin 170 | @can_restrict 171 | @user_admin 172 | @loggable 173 | @typing_action 174 | def temp_mute(update, context): 175 | chat = update.effective_chat # type: Optional[Chat] 176 | user = update.effective_user # type: Optional[User] 177 | message = update.effective_message # type: Optional[Message] 178 | args = context.args 179 | 180 | if user_can_ban(chat, user, context.bot.id) == False: 181 | message.reply_text( 182 | "You don't have enough rights to restrict someone from talking!" 183 | ) 184 | return "" 185 | 186 | user_id, reason = extract_user_and_text(message, args) 187 | 188 | if not user_id: 189 | message.reply_text("You don't seem to be referring to a user.") 190 | return "" 191 | 192 | try: 193 | member = chat.get_member(user_id) 194 | except BadRequest as excp: 195 | if excp.message == "User not found": 196 | message.reply_text("I can't seem to find this user") 197 | return "" 198 | else: 199 | raise 200 | 201 | if is_user_admin(chat, user_id, member): 202 | message.reply_text("I really wish I could mute admins...") 203 | return "" 204 | 205 | if user_id == context.bot.id: 206 | message.reply_text("I'm not gonna MUTE myself, are you crazy?") 207 | return "" 208 | 209 | if not reason: 210 | message.reply_text( 211 | "You haven't specified a time to mute this user for!" 212 | ) 213 | return "" 214 | 215 | split_reason = reason.split(None, 1) 216 | 217 | time_val = split_reason[0].lower() 218 | if len(split_reason) > 1: 219 | reason = split_reason[1] 220 | else: 221 | reason = "" 222 | 223 | mutetime = extract_time(message, time_val) 224 | 225 | if not mutetime: 226 | return "" 227 | 228 | log = ( 229 | "{}:" 230 | "\n#TEMP MUTED" 231 | "\nAdmin: {}" 232 | "\nUser: {}" 233 | "\nTime: {}".format( 234 | html.escape(chat.title), 235 | mention_html(user.id, user.first_name), 236 | mention_html(member.user.id, member.user.first_name), 237 | time_val, 238 | ) 239 | ) 240 | if reason: 241 | log += "\nReason: {}".format(reason) 242 | 243 | try: 244 | if member.can_send_messages is None or member.can_send_messages: 245 | context.bot.restrict_chat_member( 246 | chat.id, 247 | user_id, 248 | until_date=mutetime, 249 | permissions=ChatPermissions(can_send_messages=False), 250 | ) 251 | message.reply_text("shut up! 🤐 Taped for {}!".format(time_val)) 252 | return log 253 | else: 254 | message.reply_text("This user is already muted.") 255 | 256 | except BadRequest as excp: 257 | if excp.message == "Reply message not found": 258 | # Do not reply 259 | message.reply_text( 260 | "shut up! 🤐 Taped for {}!".format(time_val), quote=False 261 | ) 262 | return log 263 | else: 264 | LOGGER.warning(update) 265 | LOGGER.exception( 266 | "ERROR muting user %s in chat %s (%s) due to %s", 267 | user_id, 268 | chat.title, 269 | chat.id, 270 | excp.message, 271 | ) 272 | message.reply_text("Well damn, I can't mute that user.") 273 | 274 | return "" 275 | 276 | 277 | __help__ = """ 278 | Some people need to be publicly muted; spammers, annoyances, or just trolls. 279 | 280 | This module allows you to do that easily, by exposing some common actions, so everyone will see! 281 | 282 | *Admin only:* 283 | × /mute : Silences a user. Can also be used as a reply, muting the replied to user. 284 | × /tmute x(m/h/d): Mutes a user for x time. (via handle, or reply). m = minutes, h = hours, d = days. 285 | × /unmute : Unmutes a user. Can also be used as a reply, muting the replied to user. 286 | An example of temporarily mute someone: 287 | `/tmute @username 2h`; This mutes a user for 2 hours. 288 | """ 289 | 290 | __mod_name__ = "Muting" 291 | 292 | MUTE_HANDLER = CommandHandler( 293 | "mute", mute, pass_args=True, filters=Filters.chat_type.groups, run_async=True 294 | ) 295 | UNMUTE_HANDLER = CommandHandler( 296 | "unmute", unmute, pass_args=True, filters=Filters.chat_type.groups, run_async=True 297 | ) 298 | TEMPMUTE_HANDLER = CommandHandler( 299 | ["tmute", "tempmute"], 300 | temp_mute, 301 | pass_args=True, 302 | filters=Filters.chat_type.groups, 303 | run_async=True, 304 | ) 305 | 306 | dispatcher.add_handler(MUTE_HANDLER) 307 | dispatcher.add_handler(UNMUTE_HANDLER) 308 | dispatcher.add_handler(TEMPMUTE_HANDLER) 309 | -------------------------------------------------------------------------------- /ubotindo/modules/helper_funcs/string_handling.py: -------------------------------------------------------------------------------- 1 | # UserindoBot 2 | # Copyright (C) 2020 UserindoBot Team, 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | 17 | import re 18 | import time 19 | from typing import Dict, List 20 | import bleach 21 | import markdown2 22 | 23 | import emoji 24 | from telegram import MessageEntity 25 | from telegram.utils.helpers import escape_markdown 26 | 27 | # NOTE: the url \ escape may cause double escapes 28 | # match * (bold) (don't escape if in url) 29 | # match _ (italics) (don't escape if in url) 30 | # match ` (code) 31 | # match []() (markdown link) 32 | # else, escape *, _, `, and [ 33 | MATCH_MD = re.compile( 34 | r"\*(.*?)\*|" 35 | r"_(.*?)_|" 36 | r"`(.*?)`|" 37 | r"(?[*_`\[])" 39 | ) 40 | 41 | # regex to find []() links -> hyperlinks/buttons 42 | LINK_REGEX = re.compile(r"(? str: 49 | """ 50 | Escape all invalid markdown 51 | 52 | :param to_parse: text to escape 53 | :return: valid markdown string 54 | """ 55 | offset = 0 # offset to be used as adding a \ character causes the string to shift 56 | for match in MATCH_MD.finditer(to_parse): 57 | if match.group("esc"): 58 | ent_start = match.start() 59 | to_parse = ( 60 | to_parse[: ent_start + offset] 61 | + "\\" 62 | + to_parse[ent_start + offset :] 63 | ) 64 | offset += 1 65 | return to_parse 66 | 67 | 68 | # This is a fun one. 69 | def _calc_emoji_offset(to_calc) -> int: 70 | # Get all emoji in text. 71 | emoticons = emoji.get_emoji_regexp().finditer(to_calc) 72 | # Check the utf16 length of the emoji to determine the offset it caused. 73 | # Normal, 1 character emoji don't affect; hence sub 1. 74 | # special, eg with two emoji characters (eg face, and skin col) will have length 2, so by subbing one we 75 | # know we'll get one extra offset, 76 | return sum(len(e.group(0).encode("utf-16-le")) // 2 - 1 for e in emoticons) 77 | 78 | 79 | def markdown_parser( 80 | txt: str, entities: Dict[MessageEntity, str] = None, offset: int = 0 81 | ) -> str: 82 | """ 83 | Parse a string, escaping all invalid markdown entities. 84 | 85 | Escapes URL's so as to avoid URL mangling. 86 | Re-adds any telegram code entities obtained from the entities object. 87 | 88 | :param txt: text to parse 89 | :param entities: dict of message entities in text 90 | :param offset: message offset - command and notename length 91 | :return: valid markdown string 92 | """ 93 | if not entities: 94 | entities = {} 95 | if not txt: 96 | return "" 97 | 98 | prev = 0 99 | res = "" 100 | # Loop over all message entities, and: 101 | # reinsert code 102 | # escape free-standing urls 103 | for ent, ent_text in entities.items(): 104 | if ent.offset < -offset: 105 | continue 106 | 107 | start = ent.offset + offset # start of entity 108 | end = ent.offset + offset + ent.length - 1 # end of entity 109 | 110 | # we only care about code, url, text links 111 | if ent.type in ("code", "url", "text_link"): 112 | # count emoji to switch counter 113 | count = _calc_emoji_offset(txt[:start]) 114 | start -= count 115 | end -= count 116 | 117 | # URL handling -> do not escape if in [](), escape otherwise. 118 | if ent.type == "url": 119 | if any( 120 | match.start(1) <= start and end <= match.end(1) 121 | for match in LINK_REGEX.finditer(txt) 122 | ): 123 | continue 124 | # else, check the escapes between the prev and last and 125 | # forcefully escape the url to avoid mangling 126 | else: 127 | # TODO: investigate possible offset bug when lots of emoji 128 | # are present 129 | res += _selective_escape( 130 | txt[prev:start] or "" 131 | ) + escape_markdown(ent_text) 132 | 133 | # code handling 134 | elif ent.type == "code": 135 | res += ( 136 | _selective_escape(txt[prev:start]) + "`" + ent_text + "`" 137 | ) 138 | 139 | # handle markdown/html links 140 | elif ent.type == "text_link": 141 | res += _selective_escape(txt[prev:start]) + "[{}]({})".format( 142 | ent_text, ent.url 143 | ) 144 | 145 | end += 1 146 | 147 | # anything else 148 | else: 149 | continue 150 | 151 | prev = end 152 | 153 | res += _selective_escape(txt[prev:]) # add the rest of the text 154 | return res 155 | 156 | 157 | def button_markdown_parser( 158 | txt: str, entities: Dict[MessageEntity, str] = None, offset: int = 0 159 | ) -> (str, List): 160 | markdown_note = markdown_parser(txt, entities, offset) 161 | prev = 0 162 | note_data = "" 163 | buttons = [] 164 | for match in BTN_URL_REGEX.finditer(markdown_note): 165 | # Check if btnurl is escaped 166 | n_escapes = 0 167 | to_check = match.start(1) - 1 168 | while to_check > 0 and markdown_note[to_check] == "\\": 169 | n_escapes += 1 170 | to_check -= 1 171 | 172 | # if even, not escaped -> create button 173 | if n_escapes % 2 == 0: 174 | # create a thruple with button label, url, and newline status 175 | buttons.append( 176 | (match.group(2), match.group(3), bool(match.group(4))) 177 | ) 178 | note_data += markdown_note[prev : match.start(1)] 179 | prev = match.end(1) 180 | # if odd, escaped -> move along 181 | else: 182 | note_data += markdown_note[prev:to_check] 183 | prev = match.start(1) - 1 184 | else: 185 | note_data += markdown_note[prev:] 186 | 187 | return note_data, buttons 188 | 189 | 190 | def escape_invalid_curly_brackets(text: str, valids: List[str]) -> str: 191 | new_text = "" 192 | idx = 0 193 | while idx < len(text): 194 | if text[idx] == "{": 195 | if idx + 1 < len(text) and text[idx + 1] == "{": 196 | idx += 2 197 | new_text += "{{{{" 198 | continue 199 | else: 200 | success = False 201 | for v in valids: 202 | if text[idx:].startswith("{" + v + "}"): 203 | success = True 204 | break 205 | if success: 206 | new_text += text[idx : idx + len(v) + 2] 207 | idx += len(v) + 2 208 | continue 209 | else: 210 | new_text += "{{" 211 | 212 | elif text[idx] == "}": 213 | if idx + 1 < len(text) and text[idx + 1] == "}": 214 | idx += 2 215 | new_text += "}}}}" 216 | continue 217 | else: 218 | new_text += "}}" 219 | 220 | else: 221 | new_text += text[idx] 222 | idx += 1 223 | 224 | return new_text 225 | 226 | 227 | SMART_OPEN = "“" 228 | SMART_CLOSE = "”" 229 | START_CHAR = ("'", '"', SMART_OPEN) 230 | 231 | 232 | def split_quotes(text: str) -> List: 233 | if any(text.startswith(char) for char in START_CHAR): 234 | counter = 1 # ignore first char -> is some kind of quote 235 | while counter < len(text): 236 | if text[counter] == "\\": 237 | counter += 1 238 | elif text[counter] == text[0] or ( 239 | text[0] == SMART_OPEN and text[counter] == SMART_CLOSE 240 | ): 241 | break 242 | counter += 1 243 | else: 244 | return text.split(None, 1) 245 | 246 | # 1 to avoid starting quote, and counter is exclusive so avoids ending 247 | key = remove_escapes(text[1:counter].strip()) 248 | # index will be in range, or `else` would have been executed and 249 | # returned 250 | rest = text[counter + 1 :].strip() 251 | if not key: 252 | key = text[0] + text[0] 253 | return list(filter(None, [key, rest])) 254 | else: 255 | return text.split(None, 1) 256 | 257 | 258 | def remove_escapes(text: str) -> str: 259 | counter = 0 260 | res = "" 261 | is_escaped = False 262 | while counter < len(text): 263 | if is_escaped: 264 | res += text[counter] 265 | is_escaped = False 266 | elif text[counter] == "\\": 267 | is_escaped = True 268 | else: 269 | res += text[counter] 270 | counter += 1 271 | return res 272 | 273 | 274 | def escape_chars(text: str, to_escape: List[str]) -> str: 275 | to_escape.append("\\") 276 | new_text = "" 277 | for x in text: 278 | if x in to_escape: 279 | new_text += "\\" 280 | new_text += x 281 | return new_text 282 | 283 | 284 | def extract_time(message, time_val): 285 | if any(time_val.endswith(unit) for unit in ("m", "h", "d")): 286 | unit = time_val[-1] 287 | time_num = time_val[:-1] # type: str 288 | if not time_num.isdigit(): 289 | message.reply_text("Invalid time amount specified.") 290 | return "" 291 | 292 | if unit == "m": 293 | bantime = int(time.time() + int(time_num) * 60) 294 | elif unit == "h": 295 | bantime = int(time.time() + int(time_num) * 60 * 60) 296 | elif unit == "d": 297 | bantime = int(time.time() + int(time_num) * 24 * 60 * 60) 298 | else: 299 | # how even...? 300 | return "" 301 | return bantime 302 | else: 303 | message.reply_text( 304 | "Invalid time type specified. Expected m,h, or d, got: {}".format( 305 | time_val[-1] 306 | ) 307 | ) 308 | return "" 309 | 310 | 311 | def markdown_to_html(text): 312 | text = text.replace("*", "**") 313 | text = text.replace("`", "```") 314 | text = text.replace("~", "~~") 315 | _html = markdown2.markdown(text, extras=["strike", "underline"]) 316 | return bleach.clean( 317 | _html, 318 | tags=["strong", "em", "a", "code", "pre", "strike", "u"], 319 | strip=True, 320 | )[:-1] 321 | --------------------------------------------------------------------------------