├── runtime.txt ├── Procfile ├── haruka ├── modules │ ├── helper_funcs │ │ ├── __init__.py │ │ ├── filters.py │ │ ├── handlers.py │ │ ├── extraction.py │ │ ├── misc.py │ │ ├── chat_status.py │ │ ├── msg_types.py │ │ └── string_handling.py │ ├── sql │ │ ├── __init__.py │ │ ├── translation.py │ │ ├── rules_sql.py │ │ ├── admin_sql.py │ │ ├── urlblacklist_sql.py │ │ ├── afk_sql.py │ │ ├── log_channel_sql.py │ │ ├── rss_sql.py │ │ ├── userinfo_sql.py │ │ ├── antiflood_sql.py │ │ ├── reporting_sql.py │ │ ├── disable_sql.py │ │ ├── blacklist_sql.py │ │ ├── connection_sql.py │ │ ├── global_mutes_sql.py │ │ ├── notes_sql.py │ │ ├── users_sql.py │ │ ├── cust_filters_sql.py │ │ ├── locks_sql.py │ │ ├── antispam_sql.py │ │ └── warns_sql.py │ ├── rextester │ │ ├── api.py │ │ └── langs.py │ ├── __init__.py │ ├── translator.py │ ├── translations │ │ ├── strings.py │ │ └── list_locale.py │ ├── keyboard.py │ ├── language.py │ ├── afk.py │ ├── msg_deleting.py │ ├── rules.py │ ├── antiflood.py │ ├── userinfo.py │ ├── urlblacklist.py │ ├── reverseimage.py │ ├── disable.py │ └── log_channel.py ├── sample_config.py └── __init__.py ├── 4pda.jpg ├── bob.jpg ├── kim.jpg ├── hitler.jpg ├── mafia.jpg ├── deeppyer ├── flare.png ├── LICENSE ├── README.md └── __init__.py ├── .gitignore ├── TRANSLATION.md ├── requirements.txt ├── README.md ├── CONTRIBUTING.md └── app.json /runtime.txt: -------------------------------------------------------------------------------- 1 | python-3.9.0 2 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | worker: python3 -m haruka 2 | -------------------------------------------------------------------------------- /haruka/modules/helper_funcs/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /4pda.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xditya/GroupManager/master/4pda.jpg -------------------------------------------------------------------------------- /bob.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xditya/GroupManager/master/bob.jpg -------------------------------------------------------------------------------- /kim.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xditya/GroupManager/master/kim.jpg -------------------------------------------------------------------------------- /hitler.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xditya/GroupManager/master/hitler.jpg -------------------------------------------------------------------------------- /mafia.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xditya/GroupManager/master/mafia.jpg -------------------------------------------------------------------------------- /deeppyer/flare.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xditya/GroupManager/master/deeppyer/flare.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | haruka/config.py 2 | *.pyc 3 | .idea/ 4 | logfile 5 | backups/ 6 | .project 7 | .pydevproject 8 | .directory 9 | .vscode 10 | sticker.png 11 | mocked*.jpg 12 | kimed*.jpg 13 | hitlered*.jpg 14 | 4pdaed*.jpg 15 | mafiaed*.jpg 16 | kangsticker 17 | kangsticker.png 18 | okgoogle.png 19 | log.err 20 | log.log 21 | *.backup 22 | -------------------------------------------------------------------------------- /TRANSLATION.md: -------------------------------------------------------------------------------- 1 | # We are glad that you want to help us translating the bot into your own language! 2 | 3 | 4 | Kindly open this [link](https://raw.githubusercontent.com/peaktogoo/Akito_Playground/akito/English.py) 5 | Copy it into your preferred text editor and start translating! 6 | 7 | After you have finished, Kindly send it to [Haruka Support Group](https://t.me/HarukaAyaGroup) -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | future 2 | emoji==0.6.0 3 | requests 4 | sqlalchemy==1.3.23 5 | python-telegram-bot==11.1.0 6 | psycopg2 7 | feedparser 8 | pythonping 9 | pynewtonmath 10 | #memes from SkittBot 11 | spongemock 12 | zalgo-text 13 | geopy 14 | nltk 15 | aiohttp>=2.2.5 16 | Pillow>=4.2.0 17 | #Cool stuff by Peak 18 | PyLyrics 19 | #AEX 20 | hurry.filesize 21 | #translator 22 | googletrans==3.1.0a0 23 | 24 | tldextract 25 | PyYAML 26 | pyowm 27 | wikipedia 28 | bleach 29 | markdown2 30 | -------------------------------------------------------------------------------- /haruka/modules/sql/__init__.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import create_engine 2 | from sqlalchemy.ext.declarative import declarative_base 3 | from sqlalchemy.orm import sessionmaker, scoped_session 4 | 5 | from haruka import DB_URI 6 | 7 | 8 | def start() -> scoped_session: 9 | engine = create_engine(DB_URI, client_encoding="utf8") 10 | BASE.metadata.bind = engine 11 | BASE.metadata.create_all(engine) 12 | return scoped_session(sessionmaker(bind=engine, autoflush=False)) 13 | 14 | 15 | BASE = declarative_base() 16 | SESSION = start() 17 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # How to deploy? 2 | [Watch video tutorial on deploying](https://youtu.be/gXXFpTAk6Vo) 3 | 4 | # GroupManager 5 | 6 | [![Deploy To Heroku](https://www.herokucdn.com/deploy/button.svg)](https://dashboard.heroku.com/new?template=https%3A%2F%2Fgithub.com%2Fxditya%2Fgroupmanager) 7 | 8 | A modular Telegram Python bot running on python3 with an sqlalchemy database. 9 | 10 | Can be found on telegram as [GroupManager](https://t.me/tg_groupmanagerbot). 11 | 12 | Alternatively, [find me on telegram](https://t.me/xditya)! (Keep all support questions in the support chat, where more people can help you.) 13 | 14 | You can also join our support group [here!](https://t.me/tg_groupmanagerbot) 15 | 16 | # Group 17 | [Join Discuss Group](https://t.me/giveaways_24hrs) 18 | 19 | # Report error 20 | Report your problem along with your name to [this person](https://t.me/xditya) 21 | 22 | # Credits 23 | This repo is just a custom fork of [HarukaAya](https://gitlab.com/HarukaNetwork/OSS/HarukaAya) 24 | 25 | Thanks to the [real developer](https://t.me/RealAkito)! 26 | -------------------------------------------------------------------------------- /haruka/modules/sql/translation.py: -------------------------------------------------------------------------------- 1 | import threading 2 | from sqlalchemy import Column, String, UnicodeText 3 | from haruka.modules.sql import SESSION, BASE 4 | 5 | class Locales(BASE): 6 | __tablename__ = "Locales" 7 | chat_id = Column(String(14), primary_key=True) 8 | locale_name = Column(UnicodeText) 9 | 10 | def __init__(self, chat_id, locale_name): 11 | self.chat_id = str(chat_id) # ensure string 12 | self.locale_name = locale_name 13 | 14 | Locales.__table__.create(checkfirst=True) 15 | LOCALES_INSERTION_LOCK = threading.RLock() 16 | 17 | def switch_to_locale(chat_id, locale_name): 18 | with LOCALES_INSERTION_LOCK: 19 | prev = SESSION.query(Locales).get((str(chat_id))) 20 | if prev: 21 | SESSION.delete(prev) 22 | switch_locale = Locales(str(chat_id), locale_name) 23 | SESSION.add(switch_locale) 24 | SESSION.commit() 25 | 26 | def prev_locale(chat_id): 27 | try: 28 | return SESSION.query(Locales).get((str(chat_id))) 29 | finally : 30 | SESSION.close() 31 | -------------------------------------------------------------------------------- /deeppyer/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Ovyerus 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /haruka/modules/rextester/api.py: -------------------------------------------------------------------------------- 1 | import requests 2 | 3 | from haruka.modules.rextester.langs import languages 4 | 5 | ### API warper for rextester.com 6 | ## Author: Nitan Alexandru Marcel. 7 | # LICENSE and github repo at https://github.com/nitanmarcel/rextester_cli 8 | 9 | URL = "https://rextester.com/rundotnet/api" 10 | 11 | 12 | class Rextester: 13 | def __init__(self, lang, code, stdin): 14 | if lang not in languages: 15 | raise CompilerError("Unknown language") 16 | 17 | data = {"LanguageChoice": languages[lang], "Program": code, "Input": stdin} 18 | 19 | request = requests.post(URL, data=data) 20 | self.response = request.json() 21 | self.result = self.response["Result"] 22 | self.warnings = self.response["Warnings"] 23 | self.errors = self.response["Errors"] 24 | self.stats = self.response["Stats"] 25 | self.files = self.response["Files"] 26 | 27 | if not code: 28 | raise CompilerError("Invalid Query") 29 | 30 | elif not any([self.result, self.warnings, self.errors]): 31 | raise CompilerError("Did you forget to output something?") 32 | 33 | 34 | class CompilerError(Exception): 35 | pass 36 | -------------------------------------------------------------------------------- /haruka/modules/__init__.py: -------------------------------------------------------------------------------- 1 | from haruka import LOAD, NO_LOAD, LOGGER 2 | 3 | 4 | def __list_all_modules(): 5 | from os.path import dirname, basename, isfile 6 | import glob 7 | # This generates a list of modules in this folder for the * in __main__ to work. 8 | mod_paths = glob.glob(dirname(__file__) + "/*.py") 9 | all_modules = [basename(f)[:-3] for f in mod_paths if isfile(f) 10 | and f.endswith(".py") 11 | and not f.endswith('__init__.py')] 12 | 13 | if LOAD or NO_LOAD: 14 | to_load = LOAD 15 | if to_load: 16 | if not all(any(mod == module_name for module_name in all_modules) for mod in to_load): 17 | LOGGER.error("Invalid load order names. Quitting.") 18 | quit(1) 19 | 20 | else: 21 | to_load = all_modules 22 | 23 | if NO_LOAD: 24 | LOGGER.info("Not loading: {}".format(NO_LOAD)) 25 | return [item for item in to_load if item not in NO_LOAD] 26 | 27 | return to_load 28 | 29 | return all_modules 30 | 31 | 32 | ALL_MODULES = sorted(__list_all_modules()) 33 | LOGGER.info("Modules to load: %s", str(ALL_MODULES)) 34 | __all__ = ALL_MODULES + ["ALL_MODULES"] 35 | -------------------------------------------------------------------------------- /haruka/modules/translator.py: -------------------------------------------------------------------------------- 1 | from typing import Optional, List 2 | 3 | from telegram import Message, Update, Bot, User 4 | from telegram import MessageEntity 5 | from telegram.ext import Filters, MessageHandler, run_async 6 | 7 | from haruka import dispatcher, LOGGER 8 | from haruka.modules.disable import DisableAbleCommandHandler 9 | 10 | from googletrans import Translator 11 | 12 | 13 | @run_async 14 | def do_translate(bot: Bot, update: Update, args: List[str]): 15 | msg = update.effective_message # type: Optional[Message] 16 | lan = " ".join(args) 17 | try: 18 | to_translate_text = msg.reply_to_message.text 19 | except: 20 | return 21 | translator = Translator() 22 | try: 23 | translated = translator.translate(to_translate_text, dest=lan) 24 | src_lang = translated.src 25 | translated_text = translated.text 26 | msg.reply_text("Translated from {} to {}.\n {}".format(src_lang, lan, translated_text)) 27 | except : 28 | msg.reply_text("Error") 29 | 30 | 31 | __help__ = """- /tr (language code) as reply to a long message. 32 | """ 33 | __mod_name__ = "Translator" 34 | 35 | dispatcher.add_handler(DisableAbleCommandHandler("tr", do_translate, pass_args=True)) 36 | -------------------------------------------------------------------------------- /haruka/modules/helper_funcs/filters.py: -------------------------------------------------------------------------------- 1 | from telegram import Message 2 | from telegram.ext import BaseFilter 3 | 4 | from haruka import SUPPORT_USERS, SUDO_USERS 5 | 6 | 7 | class CustomFilters(object): 8 | class _Supporters(BaseFilter): 9 | def filter(self, message: Message): 10 | return bool(message.from_user and message.from_user.id in SUPPORT_USERS) 11 | 12 | support_filter = _Supporters() 13 | 14 | class _Sudoers(BaseFilter): 15 | def filter(self, message: Message): 16 | return bool(message.from_user and message.from_user.id in SUDO_USERS) 17 | 18 | sudo_filter = _Sudoers() 19 | 20 | class _MimeType(BaseFilter): 21 | def __init__(self, mimetype): 22 | self.mime_type = mimetype 23 | self.name = "CustomFilters.mime_type({})".format(self.mime_type) 24 | 25 | def filter(self, message: Message): 26 | return bool(message.document and message.document.mime_type == self.mime_type) 27 | 28 | mime_type = _MimeType 29 | 30 | class _HasText(BaseFilter): 31 | def filter(self, message: Message): 32 | return bool(message.text or message.sticker or message.photo or message.document or message.video) 33 | 34 | has_text = _HasText() 35 | -------------------------------------------------------------------------------- /haruka/modules/sql/rules_sql.py: -------------------------------------------------------------------------------- 1 | import threading 2 | 3 | from sqlalchemy import Column, String, UnicodeText, func, distinct 4 | 5 | from haruka.modules.sql import SESSION, BASE 6 | 7 | 8 | class Rules(BASE): 9 | __tablename__ = "rules" 10 | chat_id = Column(String(14), primary_key=True) 11 | rules = Column(UnicodeText, default="") 12 | 13 | def __init__(self, chat_id): 14 | self.chat_id = chat_id 15 | 16 | def __repr__(self): 17 | return "".format(self.chat_id, self.rules) 18 | 19 | 20 | Rules.__table__.create(checkfirst=True) 21 | 22 | INSERTION_LOCK = threading.RLock() 23 | 24 | 25 | def set_rules(chat_id, rules_text): 26 | with INSERTION_LOCK: 27 | rules = SESSION.query(Rules).get(str(chat_id)) 28 | if not rules: 29 | rules = Rules(str(chat_id)) 30 | rules.rules = rules_text 31 | 32 | SESSION.add(rules) 33 | SESSION.commit() 34 | 35 | 36 | def get_rules(chat_id): 37 | rules = SESSION.query(Rules).get(str(chat_id)) 38 | ret = "" 39 | if rules: 40 | ret = rules.rules 41 | 42 | SESSION.close() 43 | return ret 44 | 45 | 46 | def num_chats(): 47 | try: 48 | return SESSION.query(func.count(distinct(Rules.chat_id))).scalar() 49 | finally: 50 | SESSION.close() 51 | 52 | 53 | def migrate_chat(old_chat_id, new_chat_id): 54 | with INSERTION_LOCK: 55 | chat = SESSION.query(Rules).get(str(old_chat_id)) 56 | if chat: 57 | chat.chat_id = str(new_chat_id) 58 | SESSION.commit() 59 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Contributions are very welcome! Here are some guidelines on how the project is designed. 4 | 5 | ### CodeStyle 6 | 7 | - Adhere to PEP8 as much as possible. 8 | 9 | - Line lengths should be under 120 characters, use list comprehensions over map/filter, don't leave trailing whitespace. 10 | 11 | - More complex pieces of code should be commented for future reference. 12 | 13 | ### Structure 14 | 15 | There are a few self-imposed rules on the project structure, to keep the project as tidy as possible. 16 | - All modules should go into the `modules/` directory. 17 | - Any database accesses should be done in `modules/sql/` - no instances of SESSION should be imported anywhere else. 18 | - Make sure your database sessions are properly scoped! Always close them properly. 19 | - When creating a new module, there should be as few changes to other files as possible required to incorporate it. 20 | Removing the module file should result in a bot which is still in perfect working condition. 21 | - If a module is dependent on multiple other files, which might not be loaded, then create a list of at module 22 | load time, in `__main__`, by looking at attributes. This is how migration, /help, /stats, /info, and many other things 23 | are based off of. It allows the bot to work fine with the LOAD and NO_LOAD configurations. 24 | - Keep in mind that some things might clash; eg a regex handler could clash with a command handler - in this case, you 25 | should put them in different dispatcher groups. 26 | 27 | Might seem complicated, but it'll make sense when you get into it. Feel free to ask me for a hand/advice! 28 | -------------------------------------------------------------------------------- /haruka/modules/sql/admin_sql.py: -------------------------------------------------------------------------------- 1 | import threading 2 | from typing import Union 3 | 4 | from sqlalchemy import Column, Integer, String, Boolean 5 | 6 | from haruka.modules.sql import SESSION, BASE 7 | 8 | 9 | class CommandReactionChatSettings(BASE): 10 | __tablename__ = "comm_react_setting" 11 | chat_id = Column(String(14), primary_key=True) 12 | comm_reaction = Column(Boolean, default=True) 13 | 14 | def __init__(self, chat_id): 15 | self.chat_id = str(chat_id) 16 | 17 | def __repr__(self): 18 | return "".format(self.chat_id) 19 | 20 | CommandReactionChatSettings.__table__.create(checkfirst=True) 21 | 22 | CHAT_LOCK = threading.RLock() 23 | 24 | def command_reaction(chat_id: Union[str, int]) -> bool: 25 | try: 26 | chat_setting = SESSION.query(CommandReactionChatSettings).get(str(chat_id)) 27 | if chat_setting: 28 | return chat_setting.comm_reaction 29 | return False 30 | finally: 31 | SESSION.close() 32 | 33 | 34 | def set_command_reaction(chat_id: Union[int, str], setting: bool): 35 | with CHAT_LOCK: 36 | chat_setting = SESSION.query(CommandReactionChatSettings).get(str(chat_id)) 37 | if not chat_setting: 38 | chat_setting = CommandReactionChatSettings(chat_id) 39 | 40 | chat_setting.comm_reaction = setting 41 | SESSION.add(chat_setting) 42 | SESSION.commit() 43 | 44 | 45 | def migrate_chat(old_chat_id, new_chat_id): 46 | with CHAT_LOCK: 47 | chat_notes = SESSION.query(CommandReactionChatSettings).filter( 48 | CommandReactionChatSettings.chat_id == str(old_chat_id)).all() 49 | for note in chat_notes: 50 | note.chat_id = str(new_chat_id) 51 | SESSION.commit() 52 | -------------------------------------------------------------------------------- /haruka/modules/rextester/langs.py: -------------------------------------------------------------------------------- 1 | languages = { 2 | "c#": 1, 3 | "csharp": "c#", 4 | "vb.net": 2, 5 | "vb": 2, 6 | "visual_basic_dotnet": 2, 7 | "f#": 3, 8 | "fsharp": 3, 9 | "java": 4, 10 | "python2": 5, 11 | "py2": 5, 12 | "c_gcc": 6, 13 | "gcc": 6, 14 | "c": ["gcc", "clang", "visual_c"], 15 | "cplusplus_gcc": 7, 16 | "cplusplus": "c++", 17 | "g++": 7, 18 | "c++": ["cplusplus_gcc", "cplusplus_clang", "visual_cplusplus"], 19 | "cpp_gcc": 7, 20 | "cpp": "c++", 21 | "php": 8, 22 | "pascal": 9, 23 | "pas": 9, 24 | "fpc": 9, 25 | "objective_c": 10, 26 | "objc": 10, 27 | "haskell": 11, 28 | "ruby": 12, 29 | "perl": 13, 30 | "lua": 14, 31 | "nasm": 15, 32 | "asm": 15, 33 | "sql_server": 16, 34 | "v8": 17, 35 | "common_lisp": 18, 36 | "clisp": 18, 37 | "lisp": ["common_lisp", "scheme"], 38 | "prolog": 19, 39 | "golang": 20, 40 | "go": 20, 41 | "scala": 21, 42 | "scheme": 22, 43 | "node": 23, 44 | "javascript": 23, 45 | "js": "javascript", 46 | "python3": 24, 47 | "py3": 24, 48 | "python": ["python3", "python2"], 49 | "c_clang": 26, 50 | "clang": 26, 51 | "cplusplus_clang": 27, 52 | "cpp_clang": 27, 53 | "clangplusplus": 27, 54 | "clang++": 27, 55 | "visual_cplusplus": 28, 56 | "visual_cpp": 28, 57 | "vc++": 28, 58 | "msvc": 28, 59 | "visual_c": 29, 60 | "d": 30, 61 | "r": 31, 62 | "tcl": 32, 63 | "mysql": 33, 64 | "postgresql": 34, 65 | "oracle": 35, 66 | "swift": 37, 67 | "bash": 38, 68 | "ada": 39, 69 | "erlang": 40, 70 | "elixir": 41, 71 | "ocaml": 42, 72 | "kotlin": 43, 73 | "brainfuck": 44, 74 | "fortran": 45, 75 | } 76 | -------------------------------------------------------------------------------- /haruka/sample_config.py: -------------------------------------------------------------------------------- 1 | if not __name__.endswith("sample_config"): 2 | import sys 3 | print("The README is there to be read. Extend this sample config to a config file, don't just rename and change " 4 | "values here. Doing that WILL backfire on you.\nBot quitting.", file=sys.stderr) 5 | quit(1) 6 | 7 | 8 | # Create a new config.py file in same dir and import, then extend this class. 9 | class Config(object): 10 | LOGGER = True 11 | 12 | # REQUIRED 13 | API_KEY = "YOUR KEY HERE" 14 | OWNER_ID = "YOUR ID HERE" # If you dont know, run the bot and do /id in your private chat with it 15 | OWNER_USERNAME = "YOUR USERNAME HERE" 16 | 17 | # RECOMMENDED 18 | SQLALCHEMY_DATABASE_URI = 'sqldbtype://username:pw@hostname:port/db_name' # needed for any database modules 19 | MESSAGE_DUMP = None # needed to make sure 'save from' messages persist 20 | LOAD = [] 21 | NO_LOAD = ['translation', 'sed'] 22 | WEBHOOK = False 23 | URL = None 24 | 25 | # OPTIONAL 26 | SUDO_USERS = [] # List of id's (not usernames) for users which have sudo access to the bot. 27 | SUPPORT_USERS = [] # List of id's (not usernames) for users which are allowed to gban, but can also be banned. 28 | WHITELIST_USERS = [] # List of id's (not usernames) for users which WONT be banned/kicked by the bot. 29 | MAPS_API = '' 30 | CERT_PATH = None 31 | PORT = 5000 32 | DEL_CMDS = False # Whether or not you should delete "blue text must click" commands 33 | STRICT_ANTISPAM = False 34 | WORKERS = 8 # Number of subthreads to use. This is the recommended amount - see for yourself what works best! 35 | BAN_STICKER = 'CAADAgADOwADPPEcAXkko5EB3YGYAg' # banhammer marie sticker 36 | STRICT_GBAN = False 37 | STRICT_GMUTE = False 38 | ALLOW_EXCL = True # Allow ! commands as well as / 39 | API_OPENWEATHER = None # OpenWeather API 40 | 41 | # MEMES 42 | DEEPFRY_TOKEN = None 43 | 44 | class Production(Config): 45 | LOGGER = False 46 | 47 | 48 | class Development(Config): 49 | LOGGER = True 50 | -------------------------------------------------------------------------------- /haruka/modules/sql/urlblacklist_sql.py: -------------------------------------------------------------------------------- 1 | import threading 2 | 3 | from sqlalchemy import Column, String, UnicodeText 4 | 5 | from haruka.modules.sql import BASE, SESSION 6 | 7 | 8 | class URLBlackListFilters(BASE): 9 | __tablename__ = "url_blacklist" 10 | chat_id = Column(String(14), primary_key=True) 11 | domain = Column(UnicodeText, primary_key=True, nullable=False) 12 | 13 | def __init__(self, chat_id, domain): 14 | self.chat_id = str(chat_id) 15 | self.domain = str(domain) 16 | 17 | 18 | URLBlackListFilters.__table__.create(checkfirst=True) 19 | 20 | URL_BLACKLIST_FILTER_INSERTION_LOCK = threading.RLock() 21 | 22 | CHAT_URL_BLACKLISTS = {} 23 | 24 | 25 | def blacklist_url(chat_id, domain): 26 | with URL_BLACKLIST_FILTER_INSERTION_LOCK: 27 | domain_filt = URLBlackListFilters(str(chat_id), domain) 28 | 29 | SESSION.merge(domain_filt) 30 | SESSION.commit() 31 | CHAT_URL_BLACKLISTS.setdefault(str(chat_id), set()).add(domain) 32 | 33 | 34 | def rm_url_from_blacklist(chat_id, domain): 35 | with URL_BLACKLIST_FILTER_INSERTION_LOCK: 36 | domain_filt = SESSION.query( 37 | URLBlackListFilters).get((str(chat_id), domain)) 38 | if domain_filt: 39 | if domain in CHAT_URL_BLACKLISTS.get(str(chat_id), set()): 40 | CHAT_URL_BLACKLISTS.get(str(chat_id), set()).remove(domain) 41 | SESSION.delete(domain_filt) 42 | SESSION.commit() 43 | return True 44 | 45 | SESSION.close() 46 | return False 47 | 48 | 49 | def get_blacklisted_urls(chat_id): 50 | return CHAT_URL_BLACKLISTS.get(str(chat_id), set()) 51 | 52 | 53 | def _load_chat_blacklist(): 54 | global CHAT_URL_BLACKLISTS 55 | try: 56 | chats = SESSION.query(URLBlackListFilters.chat_id).distinct().all() 57 | for (chat_id,) in chats: 58 | CHAT_URL_BLACKLISTS[chat_id] = [] 59 | 60 | all_urls = SESSION.query(URLBlackListFilters).all() 61 | for url in all_urls: 62 | CHAT_URL_BLACKLISTS[url.chat_id] += [url.domain] 63 | CHAT_URL_BLACKLISTS = { 64 | k: set(v) for k, 65 | v in CHAT_URL_BLACKLISTS.items()} 66 | finally: 67 | SESSION.close() 68 | 69 | 70 | _load_chat_blacklist() 71 | -------------------------------------------------------------------------------- /haruka/modules/translations/strings.py: -------------------------------------------------------------------------------- 1 | from haruka.modules.sql.translation import prev_locale 2 | from haruka.modules.translations.English import EnglishStrings 3 | from haruka.modules.translations.Russian import RussianStrings 4 | from haruka.modules.translations.Ukraine import UkrainianStrings 5 | from haruka.modules.translations.Spanish import SpanishStrings 6 | from haruka.modules.translations.Turkish import TurkishStrings 7 | from haruka.modules.translations.Indonesian import IndonesianStrings 8 | 9 | def tld(chat_id, t, show_none=True): 10 | LANGUAGE = prev_locale(chat_id) 11 | print(chat_id, t) 12 | if LANGUAGE: 13 | LOCALE = LANGUAGE.locale_name 14 | if LOCALE in ('ru') and t in RussianStrings: 15 | return RussianStrings[t] 16 | elif LOCALE in ('ua') and t in UkrainianStrings: 17 | return UkrainianStrings[t] 18 | elif LOCALE in ('es') and t in SpanishStrings: 19 | return SpanishStrings[t] 20 | elif LOCALE in ('tr') and t in TurkishStrings: 21 | return TurkishStrings[t] 22 | elif LOCALE in ('id') and t in IndonesianStrings: 23 | return IndonesianStrings[t] 24 | else: 25 | if t in EnglishStrings: 26 | return EnglishStrings[t] 27 | else: 28 | return t 29 | elif show_none: 30 | if t in EnglishStrings: 31 | return EnglishStrings[t] 32 | else: 33 | return t 34 | 35 | 36 | 37 | def tld_help(chat_id, t): 38 | LANGUAGE = prev_locale(chat_id) 39 | print("tld_help ", chat_id, t) 40 | if LANGUAGE: 41 | LOCALE = LANGUAGE.locale_name 42 | 43 | t = t + "_help" 44 | 45 | print("Test2", t) 46 | 47 | if LOCALE in ('ru') and t in RussianStrings: 48 | return RussianStrings[t] 49 | elif LOCALE in ('ua') and t in UkrainianStrings: 50 | return UkrainianStrings[t] 51 | elif LOCALE in ('es') and t in SpanishStrings: 52 | return SpanishStrings[t] 53 | elif LOCALE in ('tr') and t in TurkishStrings: 54 | return TurkishStrings[t] 55 | elif LOCALE in ('id') and t in IndonesianStrings: 56 | return IndonesianStrings[t] 57 | else: 58 | return False 59 | else: 60 | return False 61 | -------------------------------------------------------------------------------- /haruka/modules/sql/afk_sql.py: -------------------------------------------------------------------------------- 1 | import threading 2 | 3 | from sqlalchemy import Column, UnicodeText, Boolean, Integer 4 | 5 | from haruka.modules.sql import BASE, SESSION 6 | 7 | 8 | class AFK(BASE): 9 | __tablename__ = "afk_users" 10 | 11 | user_id = Column(Integer, primary_key=True) 12 | is_afk = Column(Boolean) 13 | reason = Column(UnicodeText) 14 | 15 | def __init__(self, user_id, reason="", is_afk=True): 16 | self.user_id = user_id 17 | self.reason = reason 18 | self.is_afk = is_afk 19 | 20 | def __repr__(self): 21 | return "afk_status for {}".format(self.user_id) 22 | 23 | 24 | AFK.__table__.create(checkfirst=True) 25 | INSERTION_LOCK = threading.RLock() 26 | 27 | AFK_USERS = {} 28 | 29 | 30 | def is_afk(user_id): 31 | return user_id in AFK_USERS 32 | 33 | 34 | def check_afk_status(user_id): 35 | try: 36 | return SESSION.query(AFK).get(user_id) 37 | finally: 38 | SESSION.close() 39 | 40 | 41 | def set_afk(user_id, reason=""): 42 | with INSERTION_LOCK: 43 | curr = SESSION.query(AFK).get(user_id) 44 | if not curr: 45 | curr = AFK(user_id, reason, True) 46 | else: 47 | curr.is_afk = True 48 | 49 | AFK_USERS[user_id] = reason 50 | 51 | SESSION.add(curr) 52 | SESSION.commit() 53 | 54 | 55 | def rm_afk(user_id): 56 | with INSERTION_LOCK: 57 | curr = SESSION.query(AFK).get(user_id) 58 | if curr: 59 | if user_id in AFK_USERS: # sanity check 60 | del AFK_USERS[user_id] 61 | 62 | SESSION.delete(curr) 63 | SESSION.commit() 64 | return True 65 | 66 | SESSION.close() 67 | return False 68 | 69 | 70 | def toggle_afk(user_id, reason=""): 71 | with INSERTION_LOCK: 72 | curr = SESSION.query(AFK).get(user_id) 73 | if not curr: 74 | curr = AFK(user_id, reason, True) 75 | elif curr.is_afk: 76 | curr.is_afk = False 77 | elif not curr.is_afk: 78 | curr.is_afk = True 79 | SESSION.add(curr) 80 | SESSION.commit() 81 | 82 | 83 | def __load_afk_users(): 84 | global AFK_USERS 85 | try: 86 | all_afk = SESSION.query(AFK).all() 87 | AFK_USERS = {user.user_id: user.reason for user in all_afk if user.is_afk} 88 | finally: 89 | SESSION.close() 90 | 91 | 92 | __load_afk_users() -------------------------------------------------------------------------------- /haruka/modules/sql/log_channel_sql.py: -------------------------------------------------------------------------------- 1 | import threading 2 | 3 | from sqlalchemy import Column, String, func, distinct 4 | 5 | from haruka.modules.sql import BASE, SESSION 6 | 7 | 8 | class GroupLogs(BASE): 9 | __tablename__ = "log_channels" 10 | chat_id = Column(String(14), primary_key=True) 11 | log_channel = Column(String(14), nullable=False) 12 | 13 | def __init__(self, chat_id, log_channel): 14 | self.chat_id = str(chat_id) 15 | self.log_channel = str(log_channel) 16 | 17 | 18 | GroupLogs.__table__.create(checkfirst=True) 19 | 20 | LOGS_INSERTION_LOCK = threading.RLock() 21 | 22 | CHANNELS = {} 23 | 24 | 25 | def set_chat_log_channel(chat_id, log_channel): 26 | with LOGS_INSERTION_LOCK: 27 | res = SESSION.query(GroupLogs).get(str(chat_id)) 28 | if res: 29 | res.log_channel = log_channel 30 | else: 31 | res = GroupLogs(chat_id, log_channel) 32 | SESSION.add(res) 33 | 34 | CHANNELS[str(chat_id)] = log_channel 35 | SESSION.commit() 36 | 37 | 38 | def get_chat_log_channel(chat_id): 39 | return CHANNELS.get(str(chat_id)) 40 | 41 | 42 | def stop_chat_logging(chat_id): 43 | with LOGS_INSERTION_LOCK: 44 | res = SESSION.query(GroupLogs).get(str(chat_id)) 45 | if res: 46 | if str(chat_id) in CHANNELS: 47 | del CHANNELS[str(chat_id)] 48 | 49 | log_channel = res.log_channel 50 | SESSION.delete(res) 51 | SESSION.commit() 52 | return log_channel 53 | 54 | 55 | def num_logchannels(): 56 | try: 57 | return SESSION.query(func.count(distinct(GroupLogs.chat_id))).scalar() 58 | finally: 59 | SESSION.close() 60 | 61 | 62 | def migrate_chat(old_chat_id, new_chat_id): 63 | with LOGS_INSERTION_LOCK: 64 | chat = SESSION.query(GroupLogs).get(str(old_chat_id)) 65 | if chat: 66 | chat.chat_id = str(new_chat_id) 67 | SESSION.add(chat) 68 | if str(old_chat_id) in CHANNELS: 69 | CHANNELS[str(new_chat_id)] = CHANNELS.get(str(old_chat_id)) 70 | 71 | SESSION.commit() 72 | 73 | 74 | def __load_log_channels(): 75 | global CHANNELS 76 | try: 77 | all_chats = SESSION.query(GroupLogs).all() 78 | CHANNELS = {chat.chat_id: chat.log_channel for chat in all_chats} 79 | finally: 80 | SESSION.close() 81 | 82 | 83 | __load_log_channels() 84 | -------------------------------------------------------------------------------- /haruka/modules/keyboard.py: -------------------------------------------------------------------------------- 1 | from math import ceil 2 | from typing import List, Dict 3 | 4 | from telegram import Bot, ParseMode, ReplyKeyboardMarkup, KeyboardButton 5 | from telegram.error import TelegramError 6 | 7 | from haruka import dispatcher 8 | from haruka.modules.translations.strings import tld 9 | from telegram.ext import CommandHandler, Filters, MessageHandler, CallbackQueryHandler 10 | 11 | import haruka.modules.sql.connection_sql as con_sql 12 | 13 | 14 | def keyboard(bot, update): 15 | user = update.effective_user # type: Optional[User] 16 | conn_id = con_sql.get_connected_chat(user.id) 17 | if conn_id and not conn_id == False: 18 | btn1 = "/disconnect - Disconnect from chat" 19 | btn2 = "" 20 | btn3 = "" 21 | else: 22 | if con_sql.get_history(user.id): 23 | history = con_sql.get_history(user.id) 24 | try: 25 | chat_name1 = dispatcher.bot.getChat(history.chat_id1).title 26 | except: 27 | chat_name1 = "" 28 | 29 | try: 30 | chat_name2 = dispatcher.bot.getChat(history.chat_id2).title 31 | except: 32 | chat_name2 = "" 33 | 34 | try: 35 | chat_name3 = dispatcher.bot.getChat(history.chat_id3).title 36 | except: 37 | chat_name3 = "" 38 | 39 | if chat_name1: 40 | btn1 = "/connect {} - {}".format(history.chat_id1, chat_name1) 41 | else: 42 | btn1 = "/connect - Connect to the chat" 43 | if chat_name2: 44 | btn2 = "/connect {} - {}".format(history.chat_id2, chat_name2) 45 | else: 46 | btn2 = "" 47 | if chat_name3: 48 | btn3 = "/connect {} - {}".format(history.chat_id3, chat_name3) 49 | else: 50 | btn3 = "" 51 | 52 | #TODO: Remove except garbage 53 | 54 | update.effective_message.reply_text("Keyboard Updated", 55 | reply_markup=ReplyKeyboardMarkup([[ 56 | KeyboardButton("/help - Bot Help"), 57 | KeyboardButton("/notes - Notes")], 58 | [KeyboardButton(btn1)], 59 | [KeyboardButton(btn2)], 60 | [KeyboardButton(btn3)]])) 61 | 62 | 63 | KEYBOARD_HANDLER = CommandHandler(["keyboard"], keyboard) 64 | dispatcher.add_handler(KEYBOARD_HANDLER) 65 | -------------------------------------------------------------------------------- /haruka/modules/sql/rss_sql.py: -------------------------------------------------------------------------------- 1 | import threading 2 | 3 | from sqlalchemy import Column, UnicodeText, Integer 4 | 5 | from haruka.modules.sql import BASE, SESSION 6 | 7 | 8 | class RSS(BASE): 9 | __tablename__ = "rss_feed" 10 | id = Column(Integer, primary_key=True) 11 | chat_id = Column(UnicodeText, nullable=False) 12 | feed_link = Column(UnicodeText) 13 | old_entry_link = Column(UnicodeText) 14 | 15 | def __init__(self, chat_id, feed_link, old_entry_link): 16 | self.chat_id = chat_id 17 | self.feed_link = feed_link 18 | self.old_entry_link = old_entry_link 19 | 20 | def __repr__(self): 21 | return "".format(self.chat_id, 22 | self.feed_link, 23 | self.old_entry_link) 24 | 25 | 26 | RSS.__table__.create(checkfirst=True) 27 | INSERTION_LOCK = threading.RLock() 28 | 29 | 30 | def check_url_availability(tg_chat_id, tg_feed_link): 31 | try: 32 | return SESSION.query(RSS).filter(RSS.feed_link == tg_feed_link, 33 | RSS.chat_id == tg_chat_id).all() 34 | finally: 35 | SESSION.close() 36 | 37 | 38 | def add_url(tg_chat_id, tg_feed_link, tg_old_entry_link): 39 | with INSERTION_LOCK: 40 | action = RSS(tg_chat_id, tg_feed_link, tg_old_entry_link) 41 | 42 | SESSION.add(action) 43 | SESSION.commit() 44 | 45 | 46 | def remove_url(tg_chat_id, tg_feed_link): 47 | with INSERTION_LOCK: 48 | # this loops to delete any possible duplicates for the same TG User ID, TG Chat ID and link 49 | for row in check_url_availability(tg_chat_id, tg_feed_link): 50 | # add the action to the DB query 51 | SESSION.delete(row) 52 | 53 | SESSION.commit() 54 | 55 | 56 | def get_urls(tg_chat_id): 57 | try: 58 | return SESSION.query(RSS).filter(RSS.chat_id == tg_chat_id).all() 59 | finally: 60 | SESSION.close() 61 | 62 | 63 | def get_all(): 64 | try: 65 | return SESSION.query(RSS).all() 66 | finally: 67 | SESSION.close() 68 | 69 | 70 | def update_url(row_id, new_entry_links): 71 | with INSERTION_LOCK: 72 | row = SESSION.query(RSS).get(row_id) 73 | 74 | # set the new old_entry_link with the latest update from the RSS Feed 75 | row.old_entry_link = new_entry_links[0] 76 | 77 | # commit the changes to the DB 78 | SESSION.commit() 79 | -------------------------------------------------------------------------------- /haruka/modules/sql/userinfo_sql.py: -------------------------------------------------------------------------------- 1 | import threading 2 | 3 | from sqlalchemy import Column, Integer, UnicodeText 4 | 5 | from haruka.modules.sql import SESSION, BASE 6 | 7 | 8 | class UserInfo(BASE): 9 | __tablename__ = "userinfo" 10 | user_id = Column(Integer, primary_key=True) 11 | info = Column(UnicodeText) 12 | 13 | def __init__(self, user_id, info): 14 | self.user_id = user_id 15 | self.info = info 16 | 17 | def __repr__(self): 18 | return "" % self.user_id 19 | 20 | 21 | class UserBio(BASE): 22 | __tablename__ = "userbio" 23 | user_id = Column(Integer, primary_key=True) 24 | bio = Column(UnicodeText) 25 | 26 | def __init__(self, user_id, bio): 27 | self.user_id = user_id 28 | self.bio = bio 29 | 30 | def __repr__(self): 31 | return "" % self.user_id 32 | 33 | 34 | UserInfo.__table__.create(checkfirst=True) 35 | UserBio.__table__.create(checkfirst=True) 36 | 37 | INSERTION_LOCK = threading.RLock() 38 | 39 | 40 | def get_user_me_info(user_id): 41 | userinfo = SESSION.query(UserInfo).get(user_id) 42 | SESSION.close() 43 | if userinfo: 44 | return userinfo.info 45 | return None 46 | 47 | 48 | def set_user_me_info(user_id, info): 49 | with INSERTION_LOCK: 50 | userinfo = SESSION.query(UserInfo).get(user_id) 51 | if userinfo: 52 | userinfo.info = info 53 | else: 54 | userinfo = UserInfo(user_id, info) 55 | SESSION.add(userinfo) 56 | SESSION.commit() 57 | 58 | 59 | def get_user_bio(user_id): 60 | userbio = SESSION.query(UserBio).get(user_id) 61 | SESSION.close() 62 | if userbio: 63 | return userbio.bio 64 | return None 65 | 66 | 67 | def set_user_bio(user_id, bio): 68 | with INSERTION_LOCK: 69 | userbio = SESSION.query(UserBio).get(user_id) 70 | if userbio: 71 | userbio.bio = bio 72 | else: 73 | userbio = UserBio(user_id, bio) 74 | 75 | SESSION.add(userbio) 76 | SESSION.commit() 77 | 78 | 79 | def clear_user_info(user_id): 80 | with INSERTION_LOCK: 81 | curr = SESSION.query(UserInfo).get(user_id) 82 | if curr: 83 | SESSION.delete(curr) 84 | SESSION.commit() 85 | return True 86 | 87 | SESSION.close() 88 | return False 89 | 90 | 91 | def clear_user_bio(user_id): 92 | with INSERTION_LOCK: 93 | curr = SESSION.query(UserBio).get(user_id) 94 | if curr: 95 | SESSION.delete(curr) 96 | SESSION.commit() 97 | return True 98 | 99 | SESSION.close() 100 | return False 101 | -------------------------------------------------------------------------------- /haruka/modules/sql/antiflood_sql.py: -------------------------------------------------------------------------------- 1 | import threading 2 | 3 | from sqlalchemy import Column, Integer, String 4 | 5 | from haruka.modules.sql import BASE, SESSION 6 | 7 | DEF_COUNT = 0 8 | DEF_LIMIT = 0 9 | DEF_OBJ = (None, DEF_COUNT, DEF_LIMIT) 10 | 11 | 12 | class FloodControl(BASE): 13 | __tablename__ = "antiflood" 14 | chat_id = Column(String(14), primary_key=True) 15 | user_id = Column(Integer) 16 | count = Column(Integer, default=DEF_COUNT) 17 | limit = Column(Integer, default=DEF_LIMIT) 18 | 19 | def __init__(self, chat_id): 20 | self.chat_id = str(chat_id) # ensure string 21 | 22 | def __repr__(self): 23 | return "" % self.chat_id 24 | 25 | 26 | FloodControl.__table__.create(checkfirst=True) 27 | 28 | INSERTION_LOCK = threading.RLock() 29 | 30 | CHAT_FLOOD = {} 31 | 32 | 33 | def set_flood(chat_id, amount): 34 | with INSERTION_LOCK: 35 | flood = SESSION.query(FloodControl).get(str(chat_id)) 36 | if not flood: 37 | flood = FloodControl(str(chat_id)) 38 | 39 | flood.user_id = None 40 | flood.limit = amount 41 | 42 | CHAT_FLOOD[str(chat_id)] = (None, DEF_COUNT, amount) 43 | 44 | SESSION.add(flood) 45 | SESSION.commit() 46 | 47 | 48 | def update_flood(chat_id: str, user_id) -> bool: 49 | if str(chat_id) in CHAT_FLOOD: 50 | curr_user_id, count, limit = CHAT_FLOOD.get(str(chat_id), DEF_OBJ) 51 | 52 | if limit == 0: # no antiflood 53 | return False 54 | 55 | if user_id != curr_user_id or user_id is None: # other user 56 | CHAT_FLOOD[str(chat_id)] = (user_id, DEF_COUNT + 1, limit) 57 | return False 58 | 59 | count += 1 60 | if count > limit: # too many msgs, kick 61 | CHAT_FLOOD[str(chat_id)] = (None, DEF_COUNT, limit) 62 | return True 63 | 64 | # default -> update 65 | CHAT_FLOOD[str(chat_id)] = (user_id, count, limit) 66 | return False 67 | 68 | 69 | def get_flood_limit(chat_id): 70 | return CHAT_FLOOD.get(str(chat_id), DEF_OBJ)[2] 71 | 72 | 73 | def migrate_chat(old_chat_id, new_chat_id): 74 | with INSERTION_LOCK: 75 | flood = SESSION.query(FloodControl).get(str(old_chat_id)) 76 | if flood: 77 | CHAT_FLOOD[str(new_chat_id)] = CHAT_FLOOD.get(str(old_chat_id), DEF_OBJ) 78 | flood.chat_id = str(new_chat_id) 79 | SESSION.commit() 80 | 81 | SESSION.close() 82 | 83 | 84 | def __load_flood_settings(): 85 | global CHAT_FLOOD 86 | try: 87 | all_chats = SESSION.query(FloodControl).all() 88 | CHAT_FLOOD = {chat.chat_id: (None, DEF_COUNT, chat.limit) for chat in all_chats} 89 | finally: 90 | SESSION.close() 91 | 92 | 93 | __load_flood_settings() 94 | -------------------------------------------------------------------------------- /haruka/modules/sql/reporting_sql.py: -------------------------------------------------------------------------------- 1 | import threading 2 | from typing import Union 3 | 4 | from sqlalchemy import Column, Integer, String, Boolean 5 | 6 | from haruka.modules.sql import SESSION, BASE 7 | 8 | 9 | class ReportingUserSettings(BASE): 10 | __tablename__ = "user_report_settings" 11 | user_id = Column(Integer, primary_key=True) 12 | should_report = Column(Boolean, default=True) 13 | 14 | def __init__(self, user_id): 15 | self.user_id = user_id 16 | 17 | def __repr__(self): 18 | return "".format(self.user_id) 19 | 20 | 21 | class ReportingChatSettings(BASE): 22 | __tablename__ = "chat_report_settings" 23 | chat_id = Column(String(14), primary_key=True) 24 | should_report = Column(Boolean, default=True) 25 | 26 | def __init__(self, chat_id): 27 | self.chat_id = str(chat_id) 28 | 29 | def __repr__(self): 30 | return "".format(self.chat_id) 31 | 32 | 33 | ReportingUserSettings.__table__.create(checkfirst=True) 34 | ReportingChatSettings.__table__.create(checkfirst=True) 35 | 36 | CHAT_LOCK = threading.RLock() 37 | USER_LOCK = threading.RLock() 38 | 39 | 40 | def chat_should_report(chat_id: Union[str, int]) -> bool: 41 | try: 42 | chat_setting = SESSION.query(ReportingChatSettings).get(str(chat_id)) 43 | if chat_setting: 44 | return chat_setting.should_report 45 | return False 46 | finally: 47 | SESSION.close() 48 | 49 | 50 | def user_should_report(user_id: int) -> bool: 51 | try: 52 | user_setting = SESSION.query(ReportingUserSettings).get(user_id) 53 | if user_setting: 54 | return user_setting.should_report 55 | return True 56 | finally: 57 | SESSION.close() 58 | 59 | 60 | def set_chat_setting(chat_id: Union[int, str], setting: bool): 61 | with CHAT_LOCK: 62 | chat_setting = SESSION.query(ReportingChatSettings).get(str(chat_id)) 63 | if not chat_setting: 64 | chat_setting = ReportingChatSettings(chat_id) 65 | 66 | chat_setting.should_report = setting 67 | SESSION.add(chat_setting) 68 | SESSION.commit() 69 | 70 | 71 | def set_user_setting(user_id: int, setting: bool): 72 | with USER_LOCK: 73 | user_setting = SESSION.query(ReportingUserSettings).get(user_id) 74 | if not user_setting: 75 | user_setting = ReportingUserSettings(user_id) 76 | 77 | user_setting.should_report = setting 78 | SESSION.add(user_setting) 79 | SESSION.commit() 80 | 81 | 82 | def migrate_chat(old_chat_id, new_chat_id): 83 | with CHAT_LOCK: 84 | chat_notes = SESSION.query(ReportingChatSettings).filter( 85 | ReportingChatSettings.chat_id == str(old_chat_id)).all() 86 | for note in chat_notes: 87 | note.chat_id = str(new_chat_id) 88 | SESSION.commit() 89 | -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | 2 | { 3 | "name": "GroupManager Bot", 4 | "description": "Modular Telegram group management bot!", 5 | "keywords": [ 6 | "telegram", 7 | "best", 8 | "group", 9 | "manager", 10 | "3" 11 | ], 12 | "repository": "https://github.com/xditya/GroupManager", 13 | "env": { 14 | "ENV": { 15 | "description": "Setting this to ANYTHING will enable env variables", 16 | "value": "ANYTHING" 17 | }, 18 | "TOKEN": { 19 | "description": "Your bot token, as a string.", 20 | "value": "" 21 | }, 22 | "OWNER_ID": { 23 | "description": "An integer of consisting of your owner ID", 24 | "value": "719195224" 25 | }, 26 | "OWNER_USERNAME": { 27 | "description": "Your username", 28 | "value": "xditya" 29 | }, 30 | 31 | "URL": { 32 | "description": "The Heroku App URL similar to https://.herokuapp.com/", 33 | "value": "" 34 | }, 35 | "MESSAGE_DUMP": { 36 | "description": "optional: a chat where your replied saved messages are stored, to stop people deleting their old", 37 | "value": "-1007777777777" 38 | }, 39 | "SUDO_USERS": { 40 | "description": "A space separated list of user_ids which should be considered sudo users", 41 | "value": "254318997 683538773 719195224 570400686 466337795" 42 | }, 43 | "SUPPORT_USERS": { 44 | "description": "A space separated list of user_ids which should be considered support users (can gban/ungban, nothing else)", 45 | "value": "254318997 683538773 570400686 719195224 466337795" 46 | }, 47 | "WHITELIST_USERS": { 48 | "description": "A space separated list of user_ids which should be considered whitelisted - they can't be banned.", 49 | "value": "254318997 683538773 570400686 466337795" 50 | }, 51 | "DONATION_LINK": { 52 | "description": "Optional: link where you would like to receive donations.", 53 | "value": "https://www.paypal.me/Your_id_here" 54 | }, 55 | "PORT": { 56 | "description": "Port to use for your webhooks", 57 | "value": "8443" 58 | }, 59 | "DEL_CMDS": { 60 | "description": "Whether to delete commands from users which don't have rights to use that command", 61 | "value": "True" 62 | }, 63 | "STRICT_GBAN": { 64 | "description": "Enforce gbans across new groups as well as old groups. When a gbanned user talks, he will be banned.", 65 | "value": "True" 66 | }, 67 | "ALLOW_EXCL": { 68 | "description": "Whether to allow using exclamation marks ! for commands as well as /.", 69 | "value": "True" 70 | }, 71 | "BAN_STICKER": { 72 | "description": "Which sticker to use when banning people. Use https://telegram.dog/ShowJsonBot to get the file_id", 73 | "value": "CAADBQADNwEAAjZzSxRZAc49mnFSSAI" 74 | } 75 | }, 76 | "addons": [ 77 | { 78 | "plan": "heroku-postgresql", 79 | "options": { 80 | "version": "10" 81 | } 82 | } 83 | ] 84 | } 85 | -------------------------------------------------------------------------------- /deeppyer/README.md: -------------------------------------------------------------------------------- 1 | # deeppyer 2 | ![banner image](./banner.jpg) 3 | 4 | deeppyer is an image deepfryer written in Python using [Pillow](https://python-pillow.org/ 5 | ) and using the [Microsoft Facial Recognition API](https://azure.microsoft.com/services/cognitive-services/face/). 6 | 7 | NOTE: This *requires* at least Python v3.6 in order to run. 8 | 9 | ## How to use 10 | You can either use deeppyer as a module, or straight from the command line. 11 | 12 | ### Command line usage 13 | ``` 14 | $ python deeppyer.py -h 15 | 16 | usage: deeppyer.py [-h] [-v] [-t TOKEN] [-o OUTPUT] FILE 17 | 18 | Deepfry an image, optionally adding lens flares for eyes. 19 | 20 | positional arguments: 21 | FILE File to deepfry. 22 | 23 | optional arguments: 24 | -h, --help show this help message and exit 25 | -v, --version Display program version. 26 | -t TOKEN, --token TOKEN 27 | Token to use for facial recognition API. 28 | -o OUTPUT, --output OUTPUT 29 | Filename to output to. 30 | ``` 31 | 32 | When a token is supplied, the script will automatically try to add lens flares for the eyes, otherwise it won't. 33 | 34 | ### Program usage 35 | ```py 36 | from PIL import Image 37 | import deeppyer, asyncio 38 | 39 | async def main(): 40 | img = Image.open('./foo.jpg') 41 | img = await deeppyer.deepfry(img, token='optional token') 42 | img.save('./bar.jpg') 43 | 44 | loop = asyncio.get_event_loop() 45 | loop.run_until_complete(main()) 46 | ``` 47 | 48 | ## API Documentation 49 | #### `async deeppyer.deepfry(img: PIL.Image, *, token: str=None, url_base: str='westcentralus', session: aiohttp.ClientSession=None)` 50 | Deepfry a given image. 51 | 52 | **Arguments** 53 | - *img* (PIL.Image) - Image to apply the deepfry effect on. 54 | - *[token]* (str) - Token to use for the facial recognition API. Defining this will add lens flares to the eyes of a face in the image. 55 | - *[url_base]* (str='westcentralus') - URL base to use for the facial recognition API. Can either be `westus`, `eastus2`, `westcentralus`, `westeurope` or `southeastasia`. 56 | - *[session]* (aiohttp.ClientSession) - Optional session to use when making the request to the API. May make it a tad faster if you already have a created session, and allows you to give it your own options. 57 | 58 | Returns: 59 | `PIL.Image` - Deepfried image. 60 | 61 | ## Why? 62 | ¯\\\_(ツ)_/¯ Why not 63 | 64 | ## Contributing 65 | If you wish to contribute something to this, go ahead! Just please try to keep your code similar-ish to mine, and make sure that it works with the tests. 66 | 67 | ## Testing 68 | Create a file in [tests](./tests) called `token.json` with the following format: 69 | ```json 70 | { 71 | "token": "", 72 | "url_base": "" 73 | } 74 | ``` 75 | `token` is your token for the facial recognition API. 76 | `url_base` is optional, and is for if your token is from a different region. 77 | 78 | After that, simply run `test.py` and make sure that all the images output as you want. 79 | -------------------------------------------------------------------------------- /haruka/modules/sql/disable_sql.py: -------------------------------------------------------------------------------- 1 | import threading 2 | 3 | from sqlalchemy import Column, String, UnicodeText, func, distinct 4 | 5 | from haruka.modules.sql import SESSION, BASE 6 | 7 | 8 | class Disable(BASE): 9 | __tablename__ = "disabled_commands" 10 | chat_id = Column(String(14), primary_key=True) 11 | command = Column(UnicodeText, primary_key=True) 12 | 13 | def __init__(self, chat_id, command): 14 | self.chat_id = chat_id 15 | self.command = command 16 | 17 | def __repr__(self): 18 | return "Disabled cmd {} in {}".format(self.command, self.chat_id) 19 | 20 | 21 | Disable.__table__.create(checkfirst=True) 22 | DISABLE_INSERTION_LOCK = threading.RLock() 23 | 24 | DISABLED = {} 25 | 26 | 27 | def disable_command(chat_id, disable): 28 | with DISABLE_INSERTION_LOCK: 29 | disabled = SESSION.query(Disable).get((str(chat_id), disable)) 30 | 31 | if not disabled: 32 | DISABLED.setdefault(str(chat_id), set()).add(disable) 33 | 34 | disabled = Disable(str(chat_id), disable) 35 | SESSION.add(disabled) 36 | SESSION.commit() 37 | return True 38 | 39 | SESSION.close() 40 | return False 41 | 42 | 43 | def enable_command(chat_id, enable): 44 | with DISABLE_INSERTION_LOCK: 45 | disabled = SESSION.query(Disable).get((str(chat_id), enable)) 46 | 47 | if disabled: 48 | if enable in DISABLED.get(str(chat_id)): # sanity check 49 | DISABLED.setdefault(str(chat_id), set()).remove(enable) 50 | 51 | SESSION.delete(disabled) 52 | SESSION.commit() 53 | return True 54 | 55 | SESSION.close() 56 | return False 57 | 58 | 59 | def is_command_disabled(chat_id, cmd): 60 | return cmd in DISABLED.get(str(chat_id), set()) 61 | 62 | 63 | def get_all_disabled(chat_id): 64 | return DISABLED.get(str(chat_id), set()) 65 | 66 | 67 | def num_chats(): 68 | try: 69 | return SESSION.query(func.count(distinct(Disable.chat_id))).scalar() 70 | finally: 71 | SESSION.close() 72 | 73 | 74 | def num_disabled(): 75 | try: 76 | return SESSION.query(Disable).count() 77 | finally: 78 | SESSION.close() 79 | 80 | 81 | def migrate_chat(old_chat_id, new_chat_id): 82 | with DISABLE_INSERTION_LOCK: 83 | chats = SESSION.query(Disable).filter(Disable.chat_id == str(old_chat_id)).all() 84 | for chat in chats: 85 | chat.chat_id = str(new_chat_id) 86 | SESSION.add(chat) 87 | 88 | if str(old_chat_id) in DISABLED: 89 | DISABLED[str(new_chat_id)] = DISABLED.get(str(old_chat_id), set()) 90 | 91 | SESSION.commit() 92 | 93 | 94 | def __load_disabled_commands(): 95 | global DISABLED 96 | try: 97 | all_chats = SESSION.query(Disable).all() 98 | for chat in all_chats: 99 | DISABLED.setdefault(chat.chat_id, set()).add(chat.command) 100 | 101 | finally: 102 | SESSION.close() 103 | 104 | 105 | __load_disabled_commands() 106 | -------------------------------------------------------------------------------- /haruka/modules/helper_funcs/handlers.py: -------------------------------------------------------------------------------- 1 | import telegram.ext as tg 2 | from telegram import Update 3 | import haruka.modules.sql.antispam_sql as sql 4 | 5 | CMD_STARTERS = ('/', '!') 6 | 7 | 8 | class CustomCommandHandler(tg.CommandHandler): 9 | def __init__(self, command, callback, **kwargs): 10 | if "admin_ok" in kwargs: 11 | del kwargs["admin_ok"] 12 | super().__init__(command, callback, **kwargs) 13 | 14 | def check_update(self, update): 15 | if (isinstance(update, Update) 16 | and (update.message or update.edited_message and self.allow_edited)): 17 | message = update.message or update.edited_message 18 | 19 | if message.text and len(message.text) > 1: 20 | fst_word = message.text_html.split(None, 1)[0] 21 | if len(fst_word) > 1 and any(fst_word.startswith(start) for start in CMD_STARTERS): 22 | command = fst_word[1:].split('@') 23 | command.append(message.bot.username) # in case the command was sent without a username 24 | if self.filters is None: 25 | res = True 26 | elif isinstance(self.filters, list): 27 | res = any(func(message) for func in self.filters) 28 | else: 29 | res = self.filters(message) 30 | 31 | return res and (command[0].lower() in self.command 32 | and command[1].lower() == message.bot.username.lower()) 33 | 34 | return False 35 | 36 | 37 | class CustomRegexHandler(tg.RegexHandler): 38 | def __init__(self, pattern, callback, friendly="", **kwargs): 39 | super().__init__(pattern, callback, **kwargs) 40 | 41 | class GbanLockHandler(tg.CommandHandler): 42 | def __init__(self, command, callback, **kwargs): 43 | super().__init__(command, callback, **kwargs) 44 | def check_update(self, update): 45 | if (isinstance(update, Update) and (update.message or update.edited_message and self.allow_edited)): 46 | message = update.message or update.edited_message 47 | if sql.is_user_gbanned(update.effective_user.id): 48 | return False 49 | if message.text and message.text.startswith('/') and len(message.text) > 1: 50 | first_word = message.text_html.split(None, 1)[0] 51 | if len(first_word) > 1 and first_word.startswith('/'): 52 | command = first_word[1:].split('@') 53 | command.append(message.bot.username) # in case the command was sent without a username 54 | if not (command[0].lower() in self.command and command[1].lower() == message.bot.username.lower()): 55 | return False 56 | if self.filters is None: 57 | res = True 58 | elif isinstance(self.filters, list): 59 | res = any(func(message) for func in self.filters) 60 | else: 61 | res = self.filters(message) 62 | return res 63 | return False 64 | -------------------------------------------------------------------------------- /haruka/modules/helper_funcs/extraction.py: -------------------------------------------------------------------------------- 1 | from typing import List, Optional 2 | 3 | from telegram import Message, MessageEntity 4 | from telegram.error import BadRequest 5 | 6 | from haruka import LOGGER 7 | from haruka.modules.users import get_user_id 8 | 9 | 10 | def id_from_reply(message): 11 | prev_message = message.reply_to_message 12 | if not prev_message: 13 | return None, None 14 | user_id = prev_message.from_user.id 15 | res = message.text.split(None, 1) 16 | if len(res) < 2: 17 | return user_id, "" 18 | return user_id, res[1] 19 | 20 | 21 | def extract_user(message: Message, args: List[str]) -> Optional[int]: 22 | return extract_user_and_text(message, args)[0] 23 | 24 | 25 | def extract_user_and_text(message: Message, args: List[str]) -> (Optional[int], Optional[str]): 26 | prev_message = message.reply_to_message 27 | split_text = message.text.split(None, 1) 28 | 29 | if len(split_text) < 2: 30 | return id_from_reply(message) # only option possible 31 | 32 | text_to_parse = split_text[1] 33 | 34 | text = "" 35 | 36 | entities = list(message.parse_entities([MessageEntity.TEXT_MENTION])) 37 | if len(entities) > 0: 38 | ent = entities[0] 39 | else: 40 | ent = None 41 | 42 | # if entity offset matches (command end/text start) then all good 43 | if entities and ent and ent.offset == len(message.text) - len(text_to_parse): 44 | ent = entities[0] 45 | user_id = ent.user.id 46 | text = message.text[ent.offset + ent.length:] 47 | 48 | elif len(args) >= 1 and args[0][0] == '@': 49 | user = args[0] 50 | user_id = get_user_id(user) 51 | if not user_id: 52 | message.reply_text("I don't have that user in my db. You'll be able to interact with them if " 53 | "you reply to that person's message instead, or forward one of that user's messages.") 54 | return None, None 55 | 56 | else: 57 | user_id = user_id 58 | res = message.text.split(None, 2) 59 | if len(res) >= 3: 60 | text = res[2] 61 | 62 | elif len(args) >= 1 and args[0].isdigit(): 63 | user_id = int(args[0]) 64 | res = message.text.split(None, 2) 65 | if len(res) >= 3: 66 | text = res[2] 67 | 68 | elif prev_message: 69 | user_id, text = id_from_reply(message) 70 | 71 | else: 72 | return None, None 73 | 74 | try: 75 | message.bot.get_chat(user_id) 76 | except BadRequest as excp: 77 | if excp.message in ("User_id_invalid", "Chat not found"): 78 | message.reply_text("I don't seem to have interacted with this user before - please forward a message from " 79 | "them to give me control! (like a voodoo doll, I need a piece of them to be able " 80 | "to execute certain commands...)") 81 | else: 82 | LOGGER.exception("Exception %s on user %s", excp.message, user_id) 83 | 84 | return None, None 85 | 86 | return user_id, text 87 | 88 | 89 | def extract_text(message) -> str: 90 | return message.text or message.caption or (message.sticker.emoji if message.sticker else None) 91 | -------------------------------------------------------------------------------- /haruka/modules/sql/blacklist_sql.py: -------------------------------------------------------------------------------- 1 | import threading 2 | 3 | from sqlalchemy import func, distinct, Column, String, UnicodeText 4 | 5 | from haruka.modules.sql import SESSION, BASE 6 | 7 | 8 | class BlackListFilters(BASE): 9 | __tablename__ = "blacklist" 10 | chat_id = Column(String(14), primary_key=True) 11 | trigger = Column(UnicodeText, primary_key=True, nullable=False) 12 | 13 | def __init__(self, chat_id, trigger): 14 | self.chat_id = str(chat_id) # ensure string 15 | self.trigger = trigger 16 | 17 | def __repr__(self): 18 | return "" % (self.trigger, self.chat_id) 19 | 20 | def __eq__(self, other): 21 | return bool(isinstance(other, BlackListFilters) 22 | and self.chat_id == other.chat_id 23 | and self.trigger == other.trigger) 24 | 25 | 26 | BlackListFilters.__table__.create(checkfirst=True) 27 | 28 | BLACKLIST_FILTER_INSERTION_LOCK = threading.RLock() 29 | 30 | CHAT_BLACKLISTS = {} 31 | 32 | 33 | def add_to_blacklist(chat_id, trigger): 34 | with BLACKLIST_FILTER_INSERTION_LOCK: 35 | blacklist_filt = BlackListFilters(str(chat_id), trigger) 36 | 37 | SESSION.merge(blacklist_filt) # merge to avoid duplicate key issues 38 | SESSION.commit() 39 | CHAT_BLACKLISTS.setdefault(str(chat_id), set()).add(trigger) 40 | 41 | 42 | def rm_from_blacklist(chat_id, trigger): 43 | with BLACKLIST_FILTER_INSERTION_LOCK: 44 | blacklist_filt = SESSION.query(BlackListFilters).get((str(chat_id), trigger)) 45 | if blacklist_filt: 46 | if trigger in CHAT_BLACKLISTS.get(str(chat_id), set()): # sanity check 47 | CHAT_BLACKLISTS.get(str(chat_id), set()).remove(trigger) 48 | 49 | SESSION.delete(blacklist_filt) 50 | SESSION.commit() 51 | return True 52 | 53 | SESSION.close() 54 | return False 55 | 56 | 57 | def get_chat_blacklist(chat_id): 58 | return CHAT_BLACKLISTS.get(str(chat_id), set()) 59 | 60 | 61 | def num_blacklist_filters(): 62 | try: 63 | return SESSION.query(BlackListFilters).count() 64 | finally: 65 | SESSION.close() 66 | 67 | 68 | def num_blacklist_chat_filters(chat_id): 69 | try: 70 | return SESSION.query(BlackListFilters.chat_id).filter(BlackListFilters.chat_id == str(chat_id)).count() 71 | finally: 72 | SESSION.close() 73 | 74 | 75 | def num_blacklist_filter_chats(): 76 | try: 77 | return SESSION.query(func.count(distinct(BlackListFilters.chat_id))).scalar() 78 | finally: 79 | SESSION.close() 80 | 81 | 82 | def __load_chat_blacklists(): 83 | global CHAT_BLACKLISTS 84 | try: 85 | chats = SESSION.query(BlackListFilters.chat_id).distinct().all() 86 | for (chat_id,) in chats: # remove tuple by ( ,) 87 | CHAT_BLACKLISTS[chat_id] = [] 88 | 89 | all_filters = SESSION.query(BlackListFilters).all() 90 | for x in all_filters: 91 | CHAT_BLACKLISTS[x.chat_id] += [x.trigger] 92 | 93 | CHAT_BLACKLISTS = {x: set(y) for x, y in CHAT_BLACKLISTS.items()} 94 | 95 | finally: 96 | SESSION.close() 97 | 98 | 99 | def migrate_chat(old_chat_id, new_chat_id): 100 | with BLACKLIST_FILTER_INSERTION_LOCK: 101 | chat_filters = SESSION.query(BlackListFilters).filter(BlackListFilters.chat_id == str(old_chat_id)).all() 102 | for filt in chat_filters: 103 | filt.chat_id = str(new_chat_id) 104 | SESSION.commit() 105 | 106 | 107 | __load_chat_blacklists() 108 | -------------------------------------------------------------------------------- /haruka/modules/translations/list_locale.py: -------------------------------------------------------------------------------- 1 | list_locales = { 'ab': 'Abkhaz', 2 | 'aa': 'Afar', 3 | 'af': 'Afrikaans', 4 | 'ak': 'Akan', 5 | 'sq': 'Albanian', 6 | 'am': 'Amharic', 7 | 'ar': 'Arabic', 8 | 'an': 'Aragonese', 9 | 'hy': 'Armenian', 10 | 'as': 'Assamese', 11 | 'av': 'Avaric', 12 | 'ae': 'Avestan', 13 | 'ay': 'Aymara', 14 | 'az': 'Azerbaijani', 15 | 'bm': 'Bambara', 16 | 'ba': 'Bashkir', 17 | 'eu': 'Basque', 18 | 'be': 'Belarusian', 19 | 'bn': 'Bengali', 20 | 'bh': 'Bihari', 21 | 'bi': 'Bislama', 22 | 'bs': 'Bosnian', 23 | 'br': 'Breton', 24 | 'bg': 'Bulgarian', 25 | 'my': 'Burmese', 26 | 'ca': 'Catalan', 27 | 'ch': 'Chamorro', 28 | 'ce': 'Chechen', 29 | 'ny': 'Chichewa', 30 | 'zh': 'Chinese', 31 | 'cv': 'Chuvash', 32 | 'kw': 'Cornish', 33 | 'co': 'Corsican', 34 | 'cr': 'Cree', 35 | 'hr': 'Croatian', 36 | 'cs': 'Czech', 37 | 'da': 'Danish', 38 | 'dv': 'Divehi', 39 | 'nl': 'Nederlands', 40 | 'dz': 'Dzongkha', 41 | 'en': 'English', 42 | 'eo': 'Esperanto', 43 | 'et': 'Estonian', 44 | 'ee': 'Ewe', 45 | 'fo': 'Faroese', 46 | 'fj': 'Fijian', 47 | 'fi': 'Finnish', 48 | 'fr': 'French', 49 | 'ff': 'Fula', 50 | 'gl': 'Galician', 51 | 'ka': 'Georgian', 52 | 'de': 'Deutsch', 53 | 'el': 'Greek', 54 | 'gn': 'Guaraní', 55 | 'gu': 'Gujarati', 56 | 'ht': 'Haitian', 57 | 'ha': 'Hausa', 58 | 'he': 'Hebrew', 59 | 'hz': 'Herero', 60 | 'hi': 'Hindi', 61 | 'ho': 'Hiri Motu', 62 | 'hu': 'Hungarian', 63 | 'ia': 'Interlingua', 64 | 'id': 'Indonesian', 65 | 'ie': 'Interlingue', 66 | 'ga': 'Irish', 67 | 'ig': 'Igbo', 68 | 'ik': 'Inupiaq', 69 | 'io': 'Ido', 70 | 'is': 'Icelandic', 71 | 'it': 'Italian', 72 | 'iu': 'Inuktitut', 73 | 'ja': 'Japanese', 74 | 'jv': 'Javanese', 75 | 'kl': 'Kalaallisut', 76 | 'kn': 'Kannada', 77 | 'kr': 'Kanuri', 78 | 'ks': 'Kashmiri', 79 | 'kk': 'Kazakh', 80 | 'km': 'Khmer', 81 | 'ki': 'Kikuyu', 82 | 'rw': 'Kinyarwanda', 83 | 'ky': 'Kirghiz', 84 | 'kv': 'Komi', 85 | 'kg': 'Kongo', 86 | 'ko': 'Korean', 87 | 'ku': 'Kurdish', 88 | 'kj': 'Kwanyama', 89 | 'la': 'Latin', 90 | 'lb': 'Luxembourgish', 91 | 'lg': 'Luganda', 92 | 'li': 'Limburgish', 93 | 'ln': 'Lingala', 94 | 'lo': 'Lao', 95 | 'lt': 'Lithuanian', 96 | 'lu': 'Luba-Katanga', 97 | 'lv': 'Latvian', 98 | 'gv': 'Manx', 99 | 'mk': 'Macedonian', 100 | 'mg': 'Malagasy', 101 | 'ms': 'Malay', 102 | 'ml': 'Malayalam', 103 | 'mt': 'Maltese', 104 | 'mi': 'Māori', 105 | 'mr': 'Marathi', 106 | 'mh': 'Marshallese', 107 | 'mn': 'Mongolian', 108 | 'na': 'Nauru', 109 | 'nv': 'Navajo', 110 | 'nb': 'Norwegian Bokmål', 111 | 'nd': 'North Ndebele', 112 | 'ne': 'Nepali', 113 | 'ng': 'Ndonga', 114 | 'nn': 'Norwegian Nynorsk', 115 | 'no': 'Norwegian', 116 | 'ii': 'Nuosu', 117 | 'nr': 'South Ndebele', 118 | 'oc': 'Occitan', 119 | 'oj': 'Ojibwe', 120 | 'cu': 'Old Church Slavonic', 121 | 'om': 'Oromo', 122 | 'or': 'Oriya', 123 | 'os': 'Ossetian', 124 | 'pj': 'Punjabi', 125 | 'pi': 'Pāli', 126 | 'fa': 'Persian', 127 | 'pl': 'Polish', 128 | 'ps': 'Pashto', 129 | 'pt': 'Portuguese', 130 | 'pt-br' : 'Brazilian Portuguese', 131 | 'qu': 'Quechua', 132 | 'rm': 'Romansh', 133 | 'rn': 'Kirundi', 134 | 'ro': 'Moldavan', 135 | 'ru': 'Russian', 136 | 'sa': 'Sanskrit', 137 | 'sc': 'Sardinian', 138 | 'sd': 'Sindhi', 139 | 'se': 'Northern Sami', 140 | 'sm': 'Samoan', 141 | 'sg': 'Sango', 142 | 'sr': 'Serbian', 143 | 'gd': 'Scottish Gaelic', 144 | 'sn': 'Shona', 145 | 'si': 'Sinhalese', 146 | 'sk': 'Slovak', 147 | 'sl': 'Slovene', 148 | 'so': 'Somali', 149 | 'st': 'Southern Sotho', 150 | 'es': 'Spanish', 151 | 'su': 'Sundanese', 152 | 'sw': 'Swahili', 153 | 'ss': 'Swati', 154 | 'sv': 'Swedish', 155 | 'ta': 'Tamil', 156 | 'te': 'Telugu', 157 | 'tg': 'Tajik', 158 | 'th': 'Thai', 159 | 'ti': 'Tigrinya', 160 | 'bo': 'Tibetan', 161 | 'tk': 'Turkmen', 162 | 'tl': 'Tagalog', 163 | 'tn': 'Tswana', 164 | 'to': 'Tonga', 165 | 'tr': 'Turkish', 166 | 'ts': 'Tsonga', 167 | 'tt': 'Tatar', 168 | 'tw': 'Twi', 169 | 'ty': 'Tahitian', 170 | 'ug': 'Uyghur', 171 | 'ua': 'Ukrainian', 172 | 'ur': 'Urdu', 173 | 'uz': 'Uzbek', 174 | 've': 'Venda', 175 | 'vi': 'Vietnamese', 176 | 'vo': 'Volapük', 177 | 'wa': 'Walloon', 178 | 'cy': 'Welsh', 179 | 'wo': 'Wolof', 180 | 'fy': 'Western Frisian', 181 | 'xh': 'Xhosa', 182 | 'yi': 'Yiddish', 183 | 'yo': 'Yoruba', 184 | 'za': 'Chuang', 185 | 'zu': 'Zulu'} 186 | -------------------------------------------------------------------------------- /haruka/modules/language.py: -------------------------------------------------------------------------------- 1 | from haruka.modules.sql.translation import switch_to_locale, prev_locale 2 | from haruka.modules.translations.strings import tld 3 | from telegram.ext import CommandHandler 4 | from telegram import ParseMode, InlineKeyboardMarkup, InlineKeyboardButton 5 | from haruka import dispatcher 6 | from haruka.modules.translations.list_locale import list_locales 7 | from haruka.modules.helper_funcs.chat_status import user_admin 8 | from telegram.ext import CallbackQueryHandler 9 | import re 10 | 11 | from haruka.modules.connection import connected 12 | 13 | 14 | @user_admin 15 | def locale(bot, update, args): 16 | chat = update.effective_chat 17 | if len(args) > 0: 18 | locale = args[0].lower() 19 | if locale in list_locales: 20 | if locale in ('en', 'ru', 'ua', 'es', 'tr', 'id'): 21 | switch_to_locale(chat.id, locale) 22 | update.message.reply_text(tld(chat.id, 'Switched to {} successfully!').format(list_locales[locale])) 23 | else: 24 | update.message.reply_text("{} is not supported yet!".format(list_locales[locale])) 25 | else: 26 | update.message.reply_text("Is that even a valid language code? Use an internationally accepted ISO code!") 27 | else: 28 | LANGUAGE = prev_locale(chat.id) 29 | if LANGUAGE: 30 | locale = LANGUAGE.locale_name 31 | native_lang = list_locales[locale] 32 | update.message.reply_text("Current locale for this chat is: *{}*".format(native_lang), parse_mode = ParseMode.MARKDOWN) 33 | else: 34 | update.message.reply_text("Current locale for this chat is: *English*", parse_mode=ParseMode.MARKDOWN) 35 | 36 | @user_admin 37 | def locale_button(bot, update): 38 | chat = update.effective_chat 39 | user = update.effective_user # type: Optional[User] 40 | query = update.callback_query 41 | lang_match = re.findall(r"en|ru|ua|es|tr|id", query.data) 42 | if lang_match: 43 | if lang_match[0]: 44 | switch_to_locale(chat.id, lang_match[0]) 45 | query.answer(text="Language changed!") 46 | else: 47 | query.answer(text="Error!", show_alert=True) 48 | 49 | try: 50 | LANGUAGE = prev_locale(chat.id) 51 | locale = LANGUAGE.locale_name 52 | curr_lang = list_locales[locale] 53 | except: 54 | curr_lang = "English" 55 | 56 | text = "*Select language* \n" 57 | text += "User language : `{}`".format(curr_lang) 58 | 59 | conn = connected(bot, update, chat, user.id, need_admin=False) 60 | 61 | if not conn == False: 62 | try: 63 | chatlng = prev_locale(conn).locale_name 64 | chatlng = list_locales[chatlng] 65 | text += "\nConnected chat language : `{}`".format(chatlng) 66 | except: 67 | chatlng = "English" 68 | 69 | text += "*\n\nSelect new user language:*" 70 | 71 | query.message.reply_text(text, parse_mode=ParseMode.MARKDOWN, 72 | reply_markup=InlineKeyboardMarkup([[ 73 | InlineKeyboardButton("English 🇺🇸", callback_data="set_lang_en")]] + [[ 74 | InlineKeyboardButton("Russian 🇷🇺", callback_data="set_lang_ru"), 75 | InlineKeyboardButton("Ukrainian 🇺🇦", callback_data="set_lang_ua")]] + [[ 76 | InlineKeyboardButton("Spanish 🇪🇸", callback_data="set_lang_es"), 77 | InlineKeyboardButton("Turkish 🇹🇷", callback_data="set_lang_tr")]] + [[ 78 | InlineKeyboardButton("Indonesian 🇮🇩", callback_data="set_lang_id")]] + [[ 79 | InlineKeyboardButton("⬅️ Back", callback_data="bot_start")]])) 80 | 81 | print(lang_match) 82 | query.message.delete() 83 | bot.answer_callback_query(query.id) 84 | 85 | 86 | LOCALE_HANDLER = CommandHandler(["set_locale", "locale", "lang", "setlang"], locale, pass_args=True) 87 | locale_handler = CallbackQueryHandler(locale_button, pattern="chng_lang") 88 | set_locale_handler = CallbackQueryHandler(locale_button, pattern=r"set_lang_") 89 | 90 | dispatcher.add_handler(LOCALE_HANDLER) 91 | dispatcher.add_handler(locale_handler) 92 | dispatcher.add_handler(set_locale_handler) 93 | -------------------------------------------------------------------------------- /haruka/modules/sql/connection_sql.py: -------------------------------------------------------------------------------- 1 | import threading 2 | from typing import Union 3 | 4 | from sqlalchemy import Column, String, Boolean, UnicodeText, Integer, func, distinct 5 | 6 | from haruka.modules.helper_funcs.msg_types import Types 7 | from haruka.modules.sql import SESSION, BASE 8 | 9 | 10 | class ChatAccessConnectionSettings(BASE): 11 | __tablename__ = "access_connection" 12 | chat_id = Column(String(14), primary_key=True) 13 | allow_connect_to_chat = Column(Boolean, default=True) 14 | 15 | def __init__(self, chat_id): 16 | self.chat_id = str(chat_id) 17 | 18 | def __repr__(self): 19 | return "".format(self.chat_id) 20 | 21 | 22 | class Connection(BASE): 23 | __tablename__ = "connection" 24 | user_id = Column(Integer, primary_key=True) 25 | chat_id = Column(String(14)) 26 | def __init__(self, user_id, chat_id): 27 | self.user_id = user_id 28 | self.chat_id = str(chat_id) #Ensure String 29 | 30 | 31 | class ConnectionHistory(BASE): 32 | __tablename__ = "connection_history5" 33 | user_id = Column(Integer, primary_key=True) 34 | chat_id1 = Column(String(14)) 35 | chat_id2 = Column(String(14)) 36 | chat_id3 = Column(String(14)) 37 | updated = Column(Integer) 38 | def __init__(self, user_id, chat_id1, chat_id2, chat_id3, updated): 39 | self.user_id = user_id 40 | self.chat_id1 = str(chat_id1) #Ensure String 41 | self.chat_id2 = str(chat_id2) #Ensure String 42 | self.chat_id3 = str(chat_id3) #Ensure String 43 | self.updated = updated 44 | 45 | ChatAccessConnectionSettings.__table__.create(checkfirst=True) 46 | Connection.__table__.create(checkfirst=True) 47 | ConnectionHistory.__table__.create(checkfirst=True) 48 | 49 | CHAT_ACCESS_LOCK = threading.RLock() 50 | CONNECTION_INSERTION_LOCK = threading.RLock() 51 | HISTORY_LOCK = threading.RLock() 52 | 53 | 54 | def add_history(user_id, chat_id1, chat_id2, chat_id3, updated): 55 | with HISTORY_LOCK: 56 | prev = SESSION.query(ConnectionHistory).get((int(user_id))) 57 | if prev: 58 | SESSION.delete(prev) 59 | history = ConnectionHistory(user_id, chat_id1, chat_id2, chat_id3, updated) 60 | SESSION.add(history) 61 | SESSION.commit() 62 | 63 | def get_history(user_id): 64 | try: 65 | return SESSION.query(ConnectionHistory).get(str(user_id)) 66 | finally: 67 | SESSION.close() 68 | 69 | 70 | def allow_connect_to_chat(chat_id: Union[str, int]) -> bool: 71 | try: 72 | chat_setting = SESSION.query(ChatAccessConnectionSettings).get(str(chat_id)) 73 | if chat_setting: 74 | return chat_setting.allow_connect_to_chat 75 | return False 76 | finally: 77 | SESSION.close() 78 | 79 | 80 | def set_allow_connect_to_chat(chat_id: Union[int, str], setting: bool): 81 | with CHAT_ACCESS_LOCK: 82 | chat_setting = SESSION.query(ChatAccessConnectionSettings).get(str(chat_id)) 83 | if not chat_setting: 84 | chat_setting = ChatAccessConnectionSettings(chat_id) 85 | 86 | chat_setting.allow_connect_to_chat = setting 87 | SESSION.add(chat_setting) 88 | SESSION.commit() 89 | 90 | 91 | def connect(user_id, chat_id): 92 | with CONNECTION_INSERTION_LOCK: 93 | prev = SESSION.query(Connection).get((int(user_id))) 94 | if prev: 95 | SESSION.delete(prev) 96 | connect_to_chat = Connection(int(user_id), chat_id) 97 | SESSION.add(connect_to_chat) 98 | SESSION.commit() 99 | return True 100 | 101 | 102 | def get_connected_chat(user_id): 103 | try: 104 | return SESSION.query(Connection).get((int(user_id))) 105 | finally: 106 | SESSION.close() 107 | 108 | 109 | def curr_connection(chat_id): 110 | try: 111 | return SESSION.query(Connection).get((str(chat_id))) 112 | finally : 113 | SESSION.close() 114 | 115 | 116 | 117 | def disconnect(user_id): 118 | with CONNECTION_INSERTION_LOCK: 119 | disconnect = SESSION.query(Connection).get((int(user_id))) 120 | if disconnect: 121 | SESSION.delete(disconnect) 122 | SESSION.commit() 123 | return True 124 | else: 125 | SESSION.close() 126 | return False -------------------------------------------------------------------------------- /haruka/modules/afk.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | 3 | from telegram import Message, Update, Bot, User 4 | from telegram import MessageEntity, ParseMode 5 | from telegram.error import BadRequest 6 | from telegram.ext import Filters, MessageHandler, run_async 7 | 8 | from haruka import dispatcher 9 | from haruka.modules.disable import DisableAbleCommandHandler, DisableAbleRegexHandler 10 | from haruka.modules.sql import afk_sql as sql 11 | from haruka.modules.users import get_user_id 12 | 13 | from haruka.modules.translations.strings import tld 14 | 15 | AFK_GROUP = 7 16 | AFK_REPLY_GROUP = 8 17 | 18 | 19 | @run_async 20 | def afk(bot: Bot, update: Update): 21 | chat = update.effective_chat # type: Optional[Chat] 22 | args = update.effective_message.text.split(None, 1) 23 | if len(args) >= 2: 24 | reason = args[1] 25 | else: 26 | reason = "" 27 | 28 | sql.set_afk(update.effective_user.id, reason) 29 | fname = update.effective_user.first_name 30 | update.effective_message.reply_text(tld(chat.id, f"{fname} is now AFK!")) 31 | 32 | 33 | @run_async 34 | def no_longer_afk(bot: Bot, update: Update): 35 | user = update.effective_user # type: Optional[User] 36 | chat = update.effective_chat # type: Optional[Chat] 37 | 38 | if not user: # ignore channels 39 | return 40 | 41 | res = sql.rm_afk(user.id) 42 | if res: 43 | firstname = update.effective_user.first_name 44 | try: 45 | update.effective_message.reply_text(tld(chat.id, f"{firstname} is no longer AFK!")) 46 | except: 47 | return 48 | 49 | 50 | @run_async 51 | def reply_afk(bot: Bot, update: Update): 52 | message = update.effective_message # type: Optional[Message] 53 | if message.entities and message.parse_entities([MessageEntity.TEXT_MENTION, MessageEntity.MENTION]): 54 | entities = message.parse_entities([MessageEntity.TEXT_MENTION, MessageEntity.MENTION]) 55 | for ent in entities: 56 | if ent.type == MessageEntity.TEXT_MENTION: 57 | user_id = ent.user.id 58 | fst_name = ent.user.first_name 59 | 60 | elif ent.type == MessageEntity.MENTION: 61 | user_id = get_user_id(message.text[ent.offset:ent.offset + ent.length]) 62 | if not user_id: 63 | # Should never happen, since for a user to become AFK they must have spoken. Maybe changed username? 64 | return 65 | try: 66 | chat = bot.get_chat(user_id) 67 | except BadRequest: 68 | print("Error: Could not fetch userid {} for AFK module".format(user_id)) 69 | return 70 | fst_name = chat.first_name 71 | 72 | else: 73 | return 74 | 75 | check_afk(bot, update, user_id, fst_name) 76 | 77 | elif message.reply_to_message: 78 | user_id = message.reply_to_message.from_user.id 79 | fst_name = message.reply_to_message.from_user.first_name 80 | check_afk(bot, update, user_id, fst_name) 81 | 82 | 83 | def check_afk(bot, update, user_id, fst_name): 84 | chat = update.effective_chat # type: Optional[Chat] 85 | if sql.is_afk(user_id): 86 | user = sql.check_afk_status(user_id) 87 | if not user.reason: 88 | res = tld(chat.id, f"{fst_name} is AFK!") 89 | else: 90 | res = tld(chat.id, f"{fst_name} is AFK! says its because of:\n{user.reason}") 91 | update.effective_message.reply_text(res) 92 | 93 | 94 | __help__ = """ 95 | - /afk : mark yourself as AFK. 96 | - brb : same as the afk command - but not a command. 97 | 98 | When marked as AFK, any mentions will be replied to with a message to say that you're not available! 99 | """ 100 | 101 | __mod_name__ = "AFK" 102 | 103 | AFK_HANDLER = DisableAbleCommandHandler("afk", afk) 104 | AFK_REGEX_HANDLER = DisableAbleRegexHandler("(?i)brb", afk, friendly="afk") 105 | NO_AFK_HANDLER = MessageHandler(Filters.all & Filters.group, no_longer_afk) 106 | AFK_REPLY_HANDLER = MessageHandler(Filters.all & Filters.group, reply_afk) 107 | 108 | dispatcher.add_handler(AFK_HANDLER, AFK_GROUP) 109 | dispatcher.add_handler(AFK_REGEX_HANDLER, AFK_GROUP) 110 | dispatcher.add_handler(NO_AFK_HANDLER, AFK_GROUP) 111 | dispatcher.add_handler(AFK_REPLY_HANDLER, AFK_REPLY_GROUP) 112 | -------------------------------------------------------------------------------- /haruka/modules/msg_deleting.py: -------------------------------------------------------------------------------- 1 | import html 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 CommandHandler, Filters 7 | from telegram.ext.dispatcher import run_async 8 | from telegram.utils.helpers import mention_html 9 | 10 | from haruka import dispatcher, LOGGER 11 | from haruka.modules.helper_funcs.chat_status import user_admin, can_delete 12 | from haruka.modules.log_channel import loggable 13 | 14 | 15 | @run_async 16 | @user_admin 17 | @loggable 18 | def purge(bot: Bot, update: Update, args: List[str]) -> str: 19 | msg = update.effective_message # type: Optional[Message] 20 | if msg.reply_to_message: 21 | user = update.effective_user # type: Optional[User] 22 | chat = update.effective_chat # type: Optional[Chat] 23 | if can_delete(chat, bot.id): 24 | message_id = msg.reply_to_message.message_id 25 | if args and args[0].isdigit(): 26 | if int(args[0]) < int(1): 27 | return 28 | 29 | delete_to = message_id + int(args[0]) 30 | else: 31 | delete_to = msg.message_id - 1 32 | for m_id in range(delete_to, message_id - 1, -1): # Reverse iteration over message ids 33 | try: 34 | bot.deleteMessage(chat.id, m_id) 35 | except BadRequest as err: 36 | if err.message == "Message can't be deleted": 37 | bot.send_message(chat.id, "Cannot delete all messages. The messages may be too old, I might " 38 | "not have delete rights, or this might not be a supergroup.") 39 | 40 | elif err.message != "Message to delete not found": 41 | LOGGER.exception("Error while purging chat messages.") 42 | 43 | try: 44 | msg.delete() 45 | except BadRequest as err: 46 | if err.message == "Message can't be deleted": 47 | bot.send_message(chat.id, "Cannot delete all messages. The messages may be too old, I might " 48 | "not have delete rights, or this might not be a supergroup.") 49 | 50 | elif err.message != "Message to delete not found": 51 | LOGGER.exception("Error while purging chat messages.") 52 | 53 | bot.send_message(chat.id, "Purge complete.") 54 | return "{}:" \ 55 | "\n#PURGE" \ 56 | "\n• Admin: {}" \ 57 | "\nPurged {} messages.".format(html.escape(chat.title), 58 | mention_html(user.id, user.first_name), 59 | delete_to - message_id) 60 | 61 | else: 62 | msg.reply_text("Reply to a message to select where to start purging from.") 63 | 64 | return "" 65 | 66 | 67 | @run_async 68 | @user_admin 69 | @loggable 70 | def del_message(bot: Bot, update: Update) -> str: 71 | if update.effective_message.reply_to_message: 72 | user = update.effective_user # type: Optional[User] 73 | chat = update.effective_chat # type: Optional[Chat] 74 | if can_delete(chat, bot.id): 75 | update.effective_message.reply_to_message.delete() 76 | update.effective_message.delete() 77 | return "{}:" \ 78 | "\n#DEL" \ 79 | "\n• Admin: {}" \ 80 | "\nMessage deleted.".format(html.escape(chat.title), 81 | mention_html(user.id, user.first_name)) 82 | else: 83 | update.effective_message.reply_text("Whadya want to delete?") 84 | 85 | return "" 86 | 87 | 88 | __help__ = """ 89 | Deleting messages made easy with this command. Bot purges \ 90 | messages all together or individually. 91 | 92 | *Admin only:* 93 | - /del: deletes the message you replied to 94 | - /purge: deletes all messages between this and the replied to message. 95 | - /purge : deletes the replied message, and X messages following it. 96 | """ 97 | 98 | __mod_name__ = "Purges" 99 | 100 | DELETE_HANDLER = CommandHandler("del", del_message, filters=Filters.group) 101 | PURGE_HANDLER = CommandHandler("purge", purge, filters=Filters.group, pass_args=True) 102 | 103 | dispatcher.add_handler(DELETE_HANDLER) 104 | dispatcher.add_handler(PURGE_HANDLER) 105 | -------------------------------------------------------------------------------- /haruka/modules/helper_funcs/misc.py: -------------------------------------------------------------------------------- 1 | from math import ceil 2 | from typing import List, Dict 3 | 4 | from telegram import MAX_MESSAGE_LENGTH, InlineKeyboardButton, Bot, ParseMode 5 | from telegram.error import TelegramError 6 | 7 | from haruka import LOAD, NO_LOAD 8 | from haruka.modules.translations.strings import tld 9 | from telegram.ext import CommandHandler, Filters, MessageHandler, CallbackQueryHandler 10 | 11 | 12 | class EqInlineKeyboardButton(InlineKeyboardButton): 13 | def __eq__(self, other): 14 | return self.text == other.text 15 | 16 | def __lt__(self, other): 17 | return self.text < other.text 18 | 19 | def __gt__(self, other): 20 | return self.text > other.text 21 | 22 | 23 | def split_message(msg: str) -> List[str]: 24 | if len(msg) < MAX_MESSAGE_LENGTH: 25 | return [msg] 26 | 27 | else: 28 | lines = msg.splitlines(True) 29 | small_msg = "" 30 | result = [] 31 | for line in lines: 32 | if len(small_msg) + len(line) < MAX_MESSAGE_LENGTH: 33 | small_msg += line 34 | else: 35 | result.append(small_msg) 36 | small_msg = line 37 | else: 38 | # Else statement at the end of the for loop, so append the leftover string. 39 | result.append(small_msg) 40 | 41 | return result 42 | 43 | 44 | def paginate_modules(chat_id, page_n: int, module_dict: Dict, prefix, chat=None) -> List: 45 | if not chat: 46 | modules = sorted( 47 | [EqInlineKeyboardButton(tld(chat_id, x.__mod_name__), 48 | callback_data="{}_module({})".format(prefix, x.__mod_name__.lower())) for x 49 | in module_dict.values()]) 50 | else: 51 | modules = sorted( 52 | [EqInlineKeyboardButton(tld(chat_id, x.__mod_name__), 53 | callback_data="{}_module({},{})".format(prefix, chat, x.__mod_name__.lower())) for x 54 | in module_dict.values()]) 55 | 56 | pairs = list(zip(modules[::2], modules[1::2])) 57 | 58 | if len(modules) % 2 == 1: 59 | pairs.append((modules[-1],)) 60 | 61 | max_num_pages = ceil(len(pairs) / 7) 62 | modulo_page = page_n % max_num_pages 63 | 64 | # can only have a certain amount of buttons side by side 65 | if len(pairs) > 7: 66 | pairs = pairs[modulo_page * 7:7 * (modulo_page + 1)] + [ 67 | (EqInlineKeyboardButton("<<", callback_data="{}_prev({})".format(prefix, modulo_page)), 68 | EqInlineKeyboardButton("⬅️ Back", callback_data="bot_start"), 69 | EqInlineKeyboardButton(">>", callback_data="{}_next({})".format(prefix, modulo_page)))] 70 | else: 71 | pairs += [[EqInlineKeyboardButton("⬅️ Back", callback_data="bot_start")]] 72 | 73 | 74 | return pairs 75 | 76 | 77 | def send_to_list(bot: Bot, send_to: list, message: str, markdown=False, html=False) -> None: 78 | if html and markdown: 79 | raise Exception("Can only send with either markdown or HTML!") 80 | for user_id in set(send_to): 81 | try: 82 | if markdown: 83 | bot.send_message(user_id, message, parse_mode=ParseMode.MARKDOWN) 84 | elif html: 85 | bot.send_message(user_id, message, parse_mode=ParseMode.HTML) 86 | else: 87 | bot.send_message(user_id, message) 88 | except TelegramError: 89 | pass # ignore users who fail 90 | 91 | 92 | def build_keyboard(buttons): 93 | keyb = [] 94 | for btn in buttons: 95 | if btn.same_line and keyb: 96 | keyb[-1].append(InlineKeyboardButton(btn.name, url=btn.url)) 97 | else: 98 | keyb.append([InlineKeyboardButton(btn.name, url=btn.url)]) 99 | 100 | return keyb 101 | 102 | 103 | def revert_buttons(buttons): 104 | res = "" 105 | for btn in buttons: 106 | if btn.same_line: 107 | res += "\n[{}](buttonurl://{}:same)".format(btn.name, btn.url) 108 | else: 109 | res += "\n[{}](buttonurl://{})".format(btn.name, btn.url) 110 | 111 | return res 112 | 113 | 114 | def is_module_loaded(name): 115 | return (not LOAD or name in LOAD) and name not in NO_LOAD 116 | 117 | 118 | def user_bot_owner(func): 119 | @wraps(func) 120 | def is_user_bot_owner(bot: Bot, update: Update, *args, **kwargs): 121 | user = update.effective_user 122 | if user and user.id == OWNER_ID: 123 | return func(bot, update, *args, **kwargs) 124 | else: 125 | pass 126 | return is_user_bot_owner 127 | -------------------------------------------------------------------------------- /haruka/modules/rules.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | 3 | from telegram import Message, Update, Bot, User 4 | from telegram import ParseMode, InlineKeyboardMarkup, InlineKeyboardButton 5 | from telegram.error import BadRequest 6 | from telegram.ext import CommandHandler, run_async, Filters 7 | from telegram.utils.helpers import escape_markdown 8 | 9 | import haruka.modules.sql.rules_sql as sql 10 | from haruka import dispatcher 11 | from haruka.modules.helper_funcs.chat_status import user_admin 12 | from haruka.modules.helper_funcs.string_handling import markdown_parser 13 | 14 | 15 | @run_async 16 | def get_rules(bot: Bot, update: Update): 17 | chat_id = update.effective_chat.id 18 | send_rules(update, chat_id) 19 | 20 | 21 | # Do not async - not from a handler 22 | def send_rules(update, chat_id, from_pm=False): 23 | bot = dispatcher.bot 24 | user = update.effective_user # type: Optional[User] 25 | try: 26 | chat = bot.get_chat(chat_id) 27 | except BadRequest as excp: 28 | if excp.message == "Chat not found" and from_pm: 29 | bot.send_message(user.id, "The rules shortcut for this chat hasn't been set properly! Ask admins to " 30 | "fix this.") 31 | return 32 | else: 33 | raise 34 | 35 | rules = sql.get_rules(chat_id) 36 | text = "The rules for *{}* are:\n\n{}".format(escape_markdown(chat.title), rules) 37 | 38 | if from_pm and rules: 39 | bot.send_message(user.id, text, parse_mode=ParseMode.MARKDOWN) 40 | elif from_pm: 41 | bot.send_message(user.id, "The group admins haven't set any rules for this chat yet. " 42 | "This probably doesn't mean it's lawless though...!") 43 | elif rules: 44 | update.effective_message.reply_text("Click the button below to get this group's rules.", 45 | reply_markup=InlineKeyboardMarkup( 46 | [[InlineKeyboardButton(text="Rules", 47 | url="t.me/{}?start={}".format(bot.username, 48 | chat_id))]])) 49 | else: 50 | update.effective_message.reply_text("The group admins haven't set any rules for this chat yet. " 51 | "This probably doesn't mean it's lawless though...!") 52 | 53 | 54 | @run_async 55 | @user_admin 56 | def set_rules(bot: Bot, update: Update): 57 | chat_id = update.effective_chat.id 58 | msg = update.effective_message # type: Optional[Message] 59 | raw_text = msg.text 60 | args = raw_text.split(None, 1) # use python's maxsplit to separate cmd and args 61 | if len(args) == 2: 62 | txt = args[1] 63 | offset = len(txt) - len(raw_text) # set correct offset relative to command 64 | markdown_rules = markdown_parser(txt, entities=msg.parse_entities(), offset=offset) 65 | 66 | sql.set_rules(chat_id, markdown_rules) 67 | update.effective_message.reply_text("Successfully set rules for this group.") 68 | 69 | 70 | @run_async 71 | @user_admin 72 | def clear_rules(bot: Bot, update: Update): 73 | chat_id = update.effective_chat.id 74 | sql.set_rules(chat_id, "") 75 | update.effective_message.reply_text("Successfully cleared rules!") 76 | 77 | 78 | def __stats__(): 79 | return "{} chats have rules set.".format(sql.num_chats()) 80 | 81 | 82 | def __import_data__(chat_id, data): 83 | # set chat rules 84 | rules = data.get('info', {}).get('rules', "") 85 | sql.set_rules(chat_id, rules) 86 | 87 | 88 | def __migrate__(old_chat_id, new_chat_id): 89 | sql.migrate_chat(old_chat_id, new_chat_id) 90 | 91 | 92 | def __chat_settings__(bot, update, chat, chatP, user): 93 | return "This chat has had it's rules set: `{}`".format(bool(sql.get_rules(chat.id))) 94 | 95 | 96 | __help__ = """ 97 | - /rules: get the rules for this chat. 98 | 99 | *Admin only:* 100 | - /setrules : set the rules for this chat. 101 | - /clearrules: clear the rules for this chat. 102 | """ 103 | 104 | __mod_name__ = "Rules" 105 | 106 | GET_RULES_HANDLER = CommandHandler("rules", get_rules, filters=Filters.group) 107 | SET_RULES_HANDLER = CommandHandler("setrules", set_rules, filters=Filters.group) 108 | RESET_RULES_HANDLER = CommandHandler("clearrules", clear_rules, filters=Filters.group) 109 | 110 | dispatcher.add_handler(GET_RULES_HANDLER) 111 | dispatcher.add_handler(SET_RULES_HANDLER) 112 | dispatcher.add_handler(RESET_RULES_HANDLER) 113 | -------------------------------------------------------------------------------- /haruka/__init__.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | import sys 4 | 5 | import telegram.ext as tg 6 | 7 | # enable logging 8 | logging.basicConfig( 9 | format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", 10 | level=logging.INFO) 11 | 12 | LOGGER = logging.getLogger(__name__) 13 | 14 | # if version < 3.6, stop bot. 15 | if sys.version_info[0] < 3 or sys.version_info[1] < 6: 16 | LOGGER.error("You MUST have a python version of at least 3.6! Multiple features depend on this. Bot quitting.") 17 | quit(1) 18 | 19 | ENV = bool(os.environ.get('ENV', False)) 20 | 21 | if ENV: 22 | TOKEN = os.environ.get('TOKEN', None) 23 | try: 24 | OWNER_ID = int(os.environ.get('OWNER_ID', None)) 25 | except ValueError: 26 | raise Exception("Your OWNER_ID env variable is not a valid integer.") 27 | 28 | MESSAGE_DUMP = os.environ.get('MESSAGE_DUMP', None) 29 | OWNER_USERNAME = os.environ.get("OWNER_USERNAME", None) 30 | 31 | try: 32 | SUDO_USERS = set(int(x) for x in os.environ.get("SUDO_USERS", "").split()) 33 | except ValueError: 34 | raise Exception("Your sudo users list does not contain valid integers.") 35 | 36 | try: 37 | SUPPORT_USERS = set(int(x) for x in os.environ.get("SUPPORT_USERS", "").split()) 38 | except ValueError: 39 | raise Exception("Your support users list does not contain valid integers.") 40 | 41 | try: 42 | WHITELIST_USERS = set(int(x) for x in os.environ.get("WHITELIST_USERS", "").split()) 43 | except ValueError: 44 | raise Exception("Your whitelisted users list does not contain valid integers.") 45 | 46 | WEBHOOK = bool(os.environ.get('WEBHOOK', False)) 47 | URL = os.environ.get('URL', "") # Does not contain token 48 | PORT = int(os.environ.get('PORT', 5000)) 49 | CERT_PATH = os.environ.get("CERT_PATH") 50 | 51 | DB_URI = os.environ.get('DATABASE_URL') 52 | DONATION_LINK = os.environ.get('DONATION_LINK') 53 | LOAD = os.environ.get("LOAD", "").split() 54 | NO_LOAD = os.environ.get("NO_LOAD", "translation").split() 55 | DEL_CMDS = bool(os.environ.get('DEL_CMDS', False)) 56 | STRICT_ANTISPAM = bool(os.environ.get('STRICT_GBAN', False)) 57 | WORKERS = int(os.environ.get('WORKERS', 8)) 58 | BAN_STICKER = os.environ.get('BAN_STICKER', 'CAADAgADOwADPPEcAXkko5EB3YGYAg') 59 | ALLOW_EXCL = os.environ.get('ALLOW_EXCL', False) 60 | API_WEATHER = os.environ.get('API_WEATHER', None) 61 | 62 | 63 | else: 64 | from haruka.config import Development as Config 65 | TOKEN = Config.API_KEY 66 | try: 67 | OWNER_ID = int(Config.OWNER_ID) 68 | except ValueError: 69 | raise Exception("Your OWNER_ID variable is not a valid integer.") 70 | 71 | MESSAGE_DUMP = Config.MESSAGE_DUMP 72 | OWNER_USERNAME = Config.OWNER_USERNAME 73 | 74 | try: 75 | SUDO_USERS = set(int(x) for x in Config.SUDO_USERS or []) 76 | except ValueError: 77 | raise Exception("Your sudo users list does not contain valid integers.") 78 | 79 | try: 80 | SUPPORT_USERS = set(int(x) for x in Config.SUPPORT_USERS or []) 81 | except ValueError: 82 | raise Exception("Your support users list does not contain valid integers.") 83 | 84 | try: 85 | WHITELIST_USERS = set(int(x) for x in Config.WHITELIST_USERS or []) 86 | except ValueError: 87 | raise Exception("Your whitelisted users list does not contain valid integers.") 88 | 89 | WEBHOOK = bool(os.environ.get('WEBHOOK', False)) 90 | URL = os.environ.get('URL', "") # Does not contain token 91 | PORT = int(os.environ.get('PORT', 5000)) 92 | CERT_PATH = os.environ.get("CERT_PATH") 93 | 94 | DB_URI = os.environ.get('DATABASE_URL') 95 | DONATION_LINK = os.environ.get('DONATION_LINK') 96 | LOAD = os.environ.get("LOAD", "").split() 97 | NO_LOAD = os.environ.get("NO_LOAD", "translation").split() 98 | DEL_CMDS = bool(os.environ.get('DEL_CMDS', False)) 99 | STRICT_ANTISPAM = bool(os.environ.get('STRICT_GBAN', False)) 100 | WORKERS = int(os.environ.get('WORKERS', 8)) 101 | BAN_STICKER = os.environ.get('BAN_STICKER', 'CAADAgADOwADPPEcAXkko5EB3YGYAg') 102 | ALLOW_EXCL = os.environ.get('ALLOW_EXCL', False) 103 | API_WEATHER = os.environ.get('API_WEATHER', None) 104 | 105 | SUDO_USERS.add(OWNER_ID) 106 | SUDO_USERS.add(680915808) #Nitin's id 107 | 108 | updater = tg.Updater(TOKEN, workers=WORKERS) 109 | dispatcher = updater.dispatcher 110 | 111 | SUDO_USERS = list(SUDO_USERS) 112 | WHITELIST_USERS = list(WHITELIST_USERS) 113 | SUPPORT_USERS = list(SUPPORT_USERS) 114 | 115 | # Load at end to ensure all prev variables have been set 116 | from haruka.modules.helper_funcs.handlers import CustomCommandHandler, CustomRegexHandler 117 | 118 | # make sure the regex handler can take extra kwargs 119 | tg.RegexHandler = CustomRegexHandler 120 | 121 | if ALLOW_EXCL: 122 | tg.CommandHandler = CustomCommandHandler 123 | -------------------------------------------------------------------------------- /haruka/modules/sql/global_mutes_sql.py: -------------------------------------------------------------------------------- 1 | import threading 2 | 3 | from sqlalchemy import Column, UnicodeText, Integer, String, Boolean 4 | 5 | from haruka.modules.sql import BASE, SESSION 6 | 7 | 8 | class GloballyMutedUsers(BASE): 9 | __tablename__ = "gmutes" 10 | user_id = Column(Integer, primary_key=True) 11 | name = Column(UnicodeText, nullable=False) 12 | reason = Column(UnicodeText) 13 | 14 | def __init__(self, user_id, name, reason=None): 15 | self.user_id = user_id 16 | self.name = name 17 | self.reason = reason 18 | 19 | def __repr__(self): 20 | return "".format(self.name, self.user_id) 21 | 22 | def to_dict(self): 23 | return {"user_id": self.user_id, 24 | "name": self.name, 25 | "reason": self.reason} 26 | 27 | 28 | class GmuteSettings(BASE): 29 | __tablename__ = "gmute_settings" 30 | chat_id = Column(String(14), primary_key=True) 31 | setting = Column(Boolean, default=True, nullable=False) 32 | 33 | def __init__(self, chat_id, enabled): 34 | self.chat_id = str(chat_id) 35 | self.setting = enabled 36 | 37 | def __repr__(self): 38 | return "".format(self.chat_id, self.setting) 39 | 40 | 41 | GloballyMutedUsers.__table__.create(checkfirst=True) 42 | GmuteSettings.__table__.create(checkfirst=True) 43 | 44 | GMUTED_USERS_LOCK = threading.RLock() 45 | GMUTE_SETTING_LOCK = threading.RLock() 46 | GMUTED_LIST = set() 47 | GMUTESTAT_LIST = set() 48 | 49 | 50 | def gmute_user(user_id, name, reason=None): 51 | with GMUTED_USERS_LOCK: 52 | user = SESSION.query(GloballyMutedUsers).get(user_id) 53 | if not user: 54 | user = GloballyMutedUsers(user_id, name, reason) 55 | else: 56 | user.name = name 57 | user.reason = reason 58 | 59 | SESSION.merge(user) 60 | SESSION.commit() 61 | __load_gmuted_userid_list() 62 | 63 | 64 | def update_gmute_reason(user_id, name, reason=None): 65 | with GMUTED_USERS_LOCK: 66 | user = SESSION.query(GloballyMutedUsers).get(user_id) 67 | if not user: 68 | return False 69 | user.name = name 70 | user.reason = reason 71 | 72 | SESSION.merge(user) 73 | SESSION.commit() 74 | return True 75 | 76 | 77 | def ungmute_user(user_id): 78 | with GMUTED_USERS_LOCK: 79 | user = SESSION.query(GloballyMutedUsers).get(user_id) 80 | if user: 81 | SESSION.delete(user) 82 | 83 | SESSION.commit() 84 | __load_gmuted_userid_list() 85 | 86 | 87 | def is_user_gmuted(user_id): 88 | return user_id in GMUTED_LIST 89 | 90 | 91 | def get_gmuted_user(user_id): 92 | try: 93 | return SESSION.query(GloballyMutedUsers).get(user_id) 94 | finally: 95 | SESSION.close() 96 | 97 | 98 | def get_gmute_list(): 99 | try: 100 | return [x.to_dict() for x in SESSION.query(GloballyMutedUsers).all()] 101 | finally: 102 | SESSION.close() 103 | 104 | 105 | def enable_gmutes(chat_id): 106 | with GMUTE_SETTING_LOCK: 107 | chat = SESSION.query(GmuteSettings).get(str(chat_id)) 108 | if not chat: 109 | chat = GmuteSettings(chat_id, True) 110 | 111 | chat.setting = True 112 | SESSION.add(chat) 113 | SESSION.commit() 114 | if str(chat_id) in GMUTESTAT_LIST: 115 | GMUTESTAT_LIST.remove(str(chat_id)) 116 | 117 | 118 | def disable_gmutes(chat_id): 119 | with GMUTE_SETTING_LOCK: 120 | chat = SESSION.query(GmuteSettings).get(str(chat_id)) 121 | if not chat: 122 | chat = GmuteSettings(chat_id, False) 123 | 124 | chat.setting = False 125 | SESSION.add(chat) 126 | SESSION.commit() 127 | GMUTESTAT_LIST.add(str(chat_id)) 128 | 129 | 130 | def does_chat_gmute(chat_id): 131 | return str(chat_id) not in GMUTESTAT_LIST 132 | 133 | 134 | def num_gmuted_users(): 135 | return len(GMUTED_LIST) 136 | 137 | 138 | def __load_gmuted_userid_list(): 139 | global GMUTED_LIST 140 | try: 141 | GMUTED_LIST = {x.user_id for x in SESSION.query(GloballyMutedUsers).all()} 142 | finally: 143 | SESSION.close() 144 | 145 | 146 | def __load_gmute_stat_list(): 147 | global GMUTESTAT_LIST 148 | try: 149 | GMUTESTAT_LIST = {x.chat_id for x in SESSION.query(GmuteSettings).all() if not x.setting} 150 | finally: 151 | SESSION.close() 152 | 153 | 154 | def migrate_chat(old_chat_id, new_chat_id): 155 | with GMUTE_SETTING_LOCK: 156 | chat = SESSION.query(GmuteSettings).get(str(old_chat_id)) 157 | if chat: 158 | chat.chat_id = new_chat_id 159 | SESSION.add(chat) 160 | 161 | SESSION.commit() 162 | 163 | 164 | # Create in memory userid to avoid disk access 165 | __load_gmuted_userid_list() 166 | __load_gmute_stat_list() 167 | -------------------------------------------------------------------------------- /haruka/modules/sql/notes_sql.py: -------------------------------------------------------------------------------- 1 | # Note: chat_id's are stored as strings because the int is too large to be stored in a PSQL database. 2 | import threading 3 | from typing import Union 4 | 5 | from sqlalchemy import Column, String, Boolean, UnicodeText, Integer, func, distinct 6 | 7 | from haruka.modules.helper_funcs.msg_types import Types 8 | from haruka.modules.sql import SESSION, BASE 9 | 10 | 11 | class Notes(BASE): 12 | __tablename__ = "notes" 13 | chat_id = Column(String(14), primary_key=True) 14 | name = Column(UnicodeText, primary_key=True) 15 | value = Column(UnicodeText, nullable=False) 16 | file = Column(UnicodeText) 17 | is_reply = Column(Boolean, default=False) 18 | has_buttons = Column(Boolean, default=False) 19 | msgtype = Column(Integer, default=Types.BUTTON_TEXT.value) 20 | 21 | def __init__(self, chat_id, name, value, msgtype, file=None): 22 | self.chat_id = str(chat_id) # ensure string 23 | self.name = name 24 | self.value = value 25 | self.msgtype = msgtype 26 | self.file = file 27 | 28 | def __repr__(self): 29 | return "" % self.name 30 | 31 | 32 | class Buttons(BASE): 33 | __tablename__ = "note_urls" 34 | id = Column(Integer, primary_key=True, autoincrement=True) 35 | chat_id = Column(String(14), primary_key=True) 36 | note_name = Column(UnicodeText, primary_key=True) 37 | name = Column(UnicodeText, nullable=False) 38 | url = Column(UnicodeText, nullable=False) 39 | same_line = Column(Boolean, default=False) 40 | 41 | def __init__(self, chat_id, note_name, name, url, same_line=False): 42 | self.chat_id = str(chat_id) 43 | self.note_name = note_name 44 | self.name = name 45 | self.url = url 46 | self.same_line = same_line 47 | 48 | Notes.__table__.create(checkfirst=True) 49 | Buttons.__table__.create(checkfirst=True) 50 | 51 | NOTES_INSERTION_LOCK = threading.RLock() 52 | BUTTONS_INSERTION_LOCK = threading.RLock() 53 | 54 | def add_note_to_db(chat_id, note_name, note_data, msgtype, buttons=None, file=None): 55 | if not buttons: 56 | buttons = [] 57 | 58 | with NOTES_INSERTION_LOCK: 59 | prev = SESSION.query(Notes).get((str(chat_id), note_name)) 60 | if prev: 61 | with BUTTONS_INSERTION_LOCK: 62 | prev_buttons = SESSION.query(Buttons).filter(Buttons.chat_id == str(chat_id), 63 | Buttons.note_name == note_name).all() 64 | for btn in prev_buttons: 65 | SESSION.delete(btn) 66 | SESSION.delete(prev) 67 | note = Notes(str(chat_id), note_name, note_data or "", msgtype=msgtype.value, file=file) 68 | SESSION.add(note) 69 | SESSION.commit() 70 | 71 | for b_name, url, same_line in buttons: 72 | add_note_button_to_db(chat_id, note_name, b_name, url, same_line) 73 | 74 | 75 | def get_note(chat_id, note_name): 76 | try: 77 | return SESSION.query(Notes).get((str(chat_id), note_name)) 78 | finally: 79 | SESSION.close() 80 | 81 | 82 | def rm_note(chat_id, note_name): 83 | with NOTES_INSERTION_LOCK: 84 | note = SESSION.query(Notes).get((str(chat_id), note_name)) 85 | if note: 86 | with BUTTONS_INSERTION_LOCK: 87 | buttons = SESSION.query(Buttons).filter(Buttons.chat_id == str(chat_id), 88 | Buttons.note_name == note_name).all() 89 | for btn in buttons: 90 | SESSION.delete(btn) 91 | 92 | SESSION.delete(note) 93 | SESSION.commit() 94 | return True 95 | 96 | else: 97 | SESSION.close() 98 | return False 99 | 100 | 101 | def get_all_chat_notes(chat_id): 102 | try: 103 | return SESSION.query(Notes).filter(Notes.chat_id == str(chat_id)).order_by(Notes.name.asc()).all() 104 | finally: 105 | SESSION.close() 106 | 107 | 108 | def add_note_button_to_db(chat_id, note_name, b_name, url, same_line): 109 | with BUTTONS_INSERTION_LOCK: 110 | button = Buttons(chat_id, note_name, b_name, url, same_line) 111 | SESSION.add(button) 112 | SESSION.commit() 113 | 114 | 115 | def get_buttons(chat_id, note_name): 116 | try: 117 | return SESSION.query(Buttons).filter(Buttons.chat_id == str(chat_id), Buttons.note_name == note_name).order_by( 118 | Buttons.id).all() 119 | finally: 120 | SESSION.close() 121 | 122 | 123 | def num_notes(): 124 | try: 125 | return SESSION.query(Notes).count() 126 | finally: 127 | SESSION.close() 128 | 129 | 130 | def num_chats(): 131 | try: 132 | return SESSION.query(func.count(distinct(Notes.chat_id))).scalar() 133 | finally: 134 | SESSION.close() 135 | 136 | 137 | def migrate_chat(old_chat_id, new_chat_id): 138 | with NOTES_INSERTION_LOCK: 139 | chat_notes = SESSION.query(Notes).filter(Notes.chat_id == str(old_chat_id)).all() 140 | for note in chat_notes: 141 | note.chat_id = str(new_chat_id) 142 | 143 | with BUTTONS_INSERTION_LOCK: 144 | chat_buttons = SESSION.query(Buttons).filter(Buttons.chat_id == str(old_chat_id)).all() 145 | for btn in chat_buttons: 146 | btn.chat_id = str(new_chat_id) 147 | 148 | SESSION.commit() 149 | -------------------------------------------------------------------------------- /haruka/modules/sql/users_sql.py: -------------------------------------------------------------------------------- 1 | import threading 2 | 3 | from sqlalchemy import Column, Integer, UnicodeText, String, ForeignKey, UniqueConstraint, func 4 | 5 | from haruka import dispatcher 6 | from haruka.modules.sql import BASE, SESSION 7 | 8 | 9 | class Users(BASE): 10 | __tablename__ = "users" 11 | user_id = Column(Integer, primary_key=True) 12 | username = Column(UnicodeText) 13 | 14 | def __init__(self, user_id, username=None): 15 | self.user_id = user_id 16 | self.username = username 17 | 18 | def __repr__(self): 19 | return "".format(self.username, self.user_id) 20 | 21 | 22 | class Chats(BASE): 23 | __tablename__ = "chats" 24 | chat_id = Column(String(14), primary_key=True) 25 | chat_name = Column(UnicodeText, nullable=False) 26 | 27 | def __init__(self, chat_id, chat_name): 28 | self.chat_id = str(chat_id) 29 | self.chat_name = chat_name 30 | 31 | def __repr__(self): 32 | return "".format(self.chat_name, self.chat_id) 33 | 34 | 35 | class ChatMembers(BASE): 36 | __tablename__ = "chat_members" 37 | priv_chat_id = Column(Integer, primary_key=True) 38 | # NOTE: Use dual primary key instead of private primary key? 39 | chat = Column(String(14), 40 | ForeignKey("chats.chat_id", 41 | onupdate="CASCADE", 42 | ondelete="CASCADE"), 43 | nullable=False) 44 | user = Column(Integer, 45 | ForeignKey("users.user_id", 46 | onupdate="CASCADE", 47 | ondelete="CASCADE"), 48 | nullable=False) 49 | __table_args__ = (UniqueConstraint('chat', 'user', name='_chat_members_uc'),) 50 | 51 | def __init__(self, chat, user): 52 | self.chat = chat 53 | self.user = user 54 | 55 | def __repr__(self): 56 | return "".format(self.user.username, self.user.user_id, 57 | self.chat.chat_name, self.chat.chat_id) 58 | 59 | 60 | Users.__table__.create(checkfirst=True) 61 | Chats.__table__.create(checkfirst=True) 62 | ChatMembers.__table__.create(checkfirst=True) 63 | 64 | INSERTION_LOCK = threading.RLock() 65 | 66 | 67 | def ensure_bot_in_db(): 68 | with INSERTION_LOCK: 69 | bot = Users(dispatcher.bot.id, dispatcher.bot.username) 70 | SESSION.merge(bot) 71 | SESSION.commit() 72 | 73 | 74 | def update_user(user_id, username, chat_id=None, chat_name=None): 75 | with INSERTION_LOCK: 76 | user = SESSION.query(Users).get(user_id) 77 | if not user: 78 | user = Users(user_id, username) 79 | SESSION.add(user) 80 | SESSION.flush() 81 | else: 82 | user.username = username 83 | 84 | if not chat_id or not chat_name: 85 | SESSION.commit() 86 | return 87 | 88 | chat = SESSION.query(Chats).get(str(chat_id)) 89 | if not chat: 90 | chat = Chats(str(chat_id), chat_name) 91 | SESSION.add(chat) 92 | SESSION.flush() 93 | 94 | else: 95 | chat.chat_name = chat_name 96 | 97 | member = SESSION.query(ChatMembers).filter(ChatMembers.chat == chat.chat_id, 98 | ChatMembers.user == user.user_id).first() 99 | if not member: 100 | chat_member = ChatMembers(chat.chat_id, user.user_id) 101 | SESSION.add(chat_member) 102 | 103 | SESSION.commit() 104 | 105 | 106 | def get_userid_by_name(username): 107 | try: 108 | return SESSION.query(Users).filter(func.lower(Users.username) == username.lower()).all() 109 | finally: 110 | SESSION.close() 111 | 112 | 113 | def get_name_by_userid(user_id): 114 | try: 115 | return SESSION.query(Users).get(Users.user_id == int(user_id)).first() 116 | finally: 117 | SESSION.close() 118 | 119 | 120 | def get_chat_members(chat_id): 121 | try: 122 | return SESSION.query(ChatMembers).filter(ChatMembers.chat == str(chat_id)).all() 123 | finally: 124 | SESSION.close() 125 | 126 | 127 | def get_all_chats(): 128 | try: 129 | return SESSION.query(Chats).all() 130 | finally: 131 | SESSION.close() 132 | 133 | 134 | def get_user_num_chats(user_id): 135 | try: 136 | return SESSION.query(ChatMembers).filter(ChatMembers.user == int(user_id)).count() 137 | finally: 138 | SESSION.close() 139 | 140 | 141 | def num_chats(): 142 | try: 143 | return SESSION.query(Chats).count() 144 | finally: 145 | SESSION.close() 146 | 147 | 148 | def num_users(): 149 | try: 150 | return SESSION.query(Users).count() 151 | finally: 152 | SESSION.close() 153 | 154 | 155 | def migrate_chat(old_chat_id, new_chat_id): 156 | with INSERTION_LOCK: 157 | chat = SESSION.query(Chats).get(str(old_chat_id)) 158 | if chat: 159 | chat.chat_id = str(new_chat_id) 160 | SESSION.add(chat) 161 | 162 | SESSION.flush() 163 | 164 | chat_members = SESSION.query(ChatMembers).filter(ChatMembers.chat == str(old_chat_id)).all() 165 | for member in chat_members: 166 | member.chat = str(new_chat_id) 167 | SESSION.add(member) 168 | 169 | SESSION.commit() 170 | 171 | 172 | ensure_bot_in_db() 173 | 174 | 175 | def del_user(user_id): 176 | with INSERTION_LOCK: 177 | curr = SESSION.query(Users).get(user_id) 178 | if curr: 179 | SESSION.delete(curr) 180 | SESSION.commit() 181 | return True 182 | 183 | ChatMembers.query.filter(ChatMembers.user == user_id).delete() 184 | SESSION.commit() 185 | SESSION.close() 186 | return False 187 | -------------------------------------------------------------------------------- /haruka/modules/antiflood.py: -------------------------------------------------------------------------------- 1 | import html 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, MessageHandler, CommandHandler, run_async 7 | from telegram.utils.helpers import mention_html 8 | 9 | from haruka import dispatcher 10 | from haruka.modules.helper_funcs.chat_status import is_user_admin, user_admin, can_restrict 11 | from haruka.modules.log_channel import loggable 12 | from haruka.modules.sql import antiflood_sql as sql 13 | 14 | from haruka.modules.translations.strings import tld 15 | 16 | FLOOD_GROUP = 3 17 | 18 | 19 | @run_async 20 | @loggable 21 | def check_flood(bot: Bot, update: Update) -> str: 22 | user = update.effective_user # type: Optional[User] 23 | chat = update.effective_chat # type: Optional[Chat] 24 | msg = update.effective_message # type: Optional[Message] 25 | 26 | if not user: # ignore channels 27 | return "" 28 | 29 | # ignore admins 30 | if is_user_admin(chat, user.id): 31 | sql.update_flood(chat.id, None) 32 | return "" 33 | 34 | should_ban = sql.update_flood(chat.id, user.id) 35 | if not should_ban: 36 | return "" 37 | 38 | try: 39 | bot.restrict_chat_member(chat.id, user.id, can_send_messages=False) 40 | msg.reply_text(tld(chat.id, "I like to leave the flooding to natural disasters. But you, you were just a " 41 | "disappointment. *Muted*!")) 42 | 43 | return "{}:" \ 44 | "\n#MUTED" \ 45 | "\nUser: {}" \ 46 | "\nFlooded the group.".format(html.escape(chat.title), 47 | mention_html(user.id, user.first_name)) 48 | 49 | except BadRequest: 50 | msg.reply_text(tld(chat.id, "I can't mute people here, give me permissions first! Until then, I'll disable antiflood.")) 51 | sql.set_flood(chat.id, 0) 52 | return "{}:" \ 53 | "\n#INFO" \ 54 | "\nDon't have mute permissions, so automatically disabled antiflood.".format(chat.title) 55 | 56 | 57 | @run_async 58 | @user_admin 59 | @can_restrict 60 | @loggable 61 | def set_flood(bot: Bot, update: Update, args: List[str]) -> str: 62 | chat = update.effective_chat # type: Optional[Chat] 63 | user = update.effective_user # type: Optional[User] 64 | message = update.effective_message # type: Optional[Message] 65 | 66 | if len(args) >= 1: 67 | val = args[0].lower() 68 | if val == "off" or val == "no" or val == "0": 69 | sql.set_flood(chat.id, 0) 70 | message.reply_text(tld(chat.id, "Antiflood has been disabled.")) 71 | 72 | elif val.isdigit(): 73 | amount = int(val) 74 | if amount <= 0: 75 | sql.set_flood(chat.id, 0) 76 | message.reply_text(tld(chat.id, "Antiflood has been disabled.")) 77 | return "{}:" \ 78 | "\n#SETFLOOD" \ 79 | "\nAdmin: {}" \ 80 | "\nDisabled antiflood.".format(html.escape(chat.title), mention_html(user.id, user.first_name)) 81 | 82 | elif amount < 3: 83 | message.reply_text(tld(chat.id, "Antiflood has to be either 0 (disabled), or a number bigger than 3 (enabled)!")) 84 | return "" 85 | 86 | else: 87 | sql.set_flood(chat.id, amount) 88 | message.reply_text(tld(chat.id, "Antiflood has been updated and set to {}").format(amount)) 89 | return "{}:" \ 90 | "\n#SETFLOOD" \ 91 | "\nAdmin: {}" \ 92 | "\nSet antiflood to {}.".format(html.escape(chat.title), 93 | mention_html(user.id, user.first_name), amount) 94 | 95 | else: 96 | message.reply_text(tld(chat.id, "Unrecognized argument - please use a number, 'off', or 'no'.")) 97 | 98 | return "" 99 | 100 | 101 | @run_async 102 | def flood(bot: Bot, update: Update): 103 | chat = update.effective_chat # type: Optional[Chat] 104 | 105 | limit = sql.get_flood_limit(chat.id) 106 | if limit == 0: 107 | update.effective_message.reply_text(tld(chat.id, "I'm not currently enforcing flood control!")) 108 | else: 109 | update.effective_message.reply_text(tld(chat.id, 110 | "I'm currently muting users if they send more than {} consecutive messages.").format(limit)) 111 | 112 | 113 | def __migrate__(old_chat_id, new_chat_id): 114 | sql.migrate_chat(old_chat_id, new_chat_id) 115 | 116 | 117 | def __chat_settings__(bot, update, chat, chatP, user): 118 | chat_id = chat.id 119 | limit = sql.get_flood_limit(chat_id) 120 | if limit == 0: 121 | return "*Not* currently enforcing flood control." 122 | else: 123 | return "Antiflood is set to `{}` messages.".format(limit) 124 | 125 | 126 | __help__ = """ 127 | You know how sometimes, people join, send 100 messages, and ruin your chat? With antiflood, that happens no more! 128 | 129 | Antiflood allows you to take action on users that send more than x messages in a row. Actions are: ban/kick/mute/tban/tmute 130 | 131 | Available commands are: 132 | - /flood: gets the current antiflood settings. 133 | - /setflood : sets the number of messages at which to take action on a user. 134 | """ 135 | 136 | __mod_name__ = "AntiFlood" 137 | 138 | FLOOD_BAN_HANDLER = MessageHandler(Filters.all & ~Filters.status_update & Filters.group, check_flood) 139 | SET_FLOOD_HANDLER = CommandHandler("setflood", set_flood, pass_args=True, filters=Filters.group) 140 | FLOOD_HANDLER = CommandHandler("flood", flood, filters=Filters.group) 141 | 142 | dispatcher.add_handler(FLOOD_BAN_HANDLER, FLOOD_GROUP) 143 | dispatcher.add_handler(SET_FLOOD_HANDLER) 144 | dispatcher.add_handler(FLOOD_HANDLER) 145 | -------------------------------------------------------------------------------- /haruka/modules/userinfo.py: -------------------------------------------------------------------------------- 1 | import html 2 | from typing import Optional, List 3 | 4 | from telegram import Message, Update, Bot, User 5 | from telegram import ParseMode, MAX_MESSAGE_LENGTH 6 | from telegram.ext.dispatcher import run_async 7 | from telegram.utils.helpers import escape_markdown 8 | 9 | import haruka.modules.sql.userinfo_sql as sql 10 | from haruka import dispatcher, SUDO_USERS, OWNER_ID 11 | from haruka.modules.disable import DisableAbleCommandHandler 12 | from haruka.modules.helper_funcs.extraction import extract_user 13 | 14 | 15 | @run_async 16 | def about_me(bot: Bot, update: Update, args: List[str]): 17 | message = update.effective_message # type: Optional[Message] 18 | user_id = extract_user(message, args) 19 | 20 | if user_id: 21 | user = bot.get_chat(user_id) 22 | else: 23 | user = message.from_user 24 | 25 | info = sql.get_user_me_info(user.id) 26 | 27 | if info: 28 | update.effective_message.reply_text("*{}*:\n{}".format(user.first_name, escape_markdown(info)), 29 | parse_mode=ParseMode.MARKDOWN) 30 | elif message.reply_to_message: 31 | username = message.reply_to_message.from_user.first_name 32 | update.effective_message.reply_text(username + " hasn't set an info message about themselves yet!") 33 | else: 34 | update.effective_message.reply_text("You haven't set an info message about yourself yet!") 35 | 36 | 37 | @run_async 38 | def set_about_me(bot: Bot, update: Update): 39 | message = update.effective_message # type: Optional[Message] 40 | user_id = message.from_user.id 41 | text = message.text 42 | info = text.split(None, 1) # use python's maxsplit to only remove the cmd, hence keeping newlines. 43 | if len(info) == 2: 44 | if len(info[1]) < MAX_MESSAGE_LENGTH // 4: 45 | sql.set_user_me_info(user_id, info[1]) 46 | message.reply_text("Updated your info!") 47 | else: 48 | message.reply_text( 49 | "Your info needs to be under {} characters! You have {}.".format(MAX_MESSAGE_LENGTH // 4, len(info[1]))) 50 | 51 | 52 | @run_async 53 | def about_bio(bot: Bot, update: Update, args: List[str]): 54 | message = update.effective_message # type: Optional[Message] 55 | 56 | user_id = extract_user(message, args) 57 | if user_id: 58 | user = bot.get_chat(user_id) 59 | else: 60 | user = message.from_user 61 | 62 | info = sql.get_user_bio(user.id) 63 | 64 | if info: 65 | update.effective_message.reply_text("*{}*:\n{}".format(user.first_name, escape_markdown(info)), 66 | parse_mode=ParseMode.MARKDOWN) 67 | elif message.reply_to_message: 68 | username = user.first_name 69 | update.effective_message.reply_text("{} hasn't had a message set about themselves yet!".format(username)) 70 | else: 71 | update.effective_message.reply_text("You haven't had a bio set about yourself yet!") 72 | 73 | 74 | @run_async 75 | def set_about_bio(bot: Bot, update: Update): 76 | message = update.effective_message # type: Optional[Message] 77 | sender = update.effective_user # type: Optional[User] 78 | if message.reply_to_message: 79 | repl_message = message.reply_to_message 80 | user_id = repl_message.from_user.id 81 | if user_id == message.from_user.id: 82 | message.reply_text("Ha, you can't set your own bio! You're at the mercy of others here...") 83 | return 84 | elif user_id == bot.id and sender.id not in SUDO_USERS: 85 | message.reply_text("Erm... yeah, I only trust sudo users to set my bio LMAO.") 86 | return 87 | elif user_id in SUDO_USERS and sender.id not in SUDO_USERS: 88 | message.reply_text("Erm... yeah, I only trust sudo users to set sudo users bio LMAO.") 89 | return 90 | elif user_id == OWNER_ID: 91 | message.reply_text("You ain't setting my master bio LMAO.") 92 | return 93 | 94 | text = message.text 95 | bio = text.split(None, 1) # use python's maxsplit to only remove the cmd, hence keeping newlines. 96 | if len(bio) == 2: 97 | if len(bio[1]) < MAX_MESSAGE_LENGTH // 4: 98 | sql.set_user_bio(user_id, bio[1]) 99 | message.reply_text("Updated {}'s bio!".format(repl_message.from_user.first_name)) 100 | else: 101 | message.reply_text( 102 | "A bio needs to be under {} characters! You tried to set {}.".format( 103 | MAX_MESSAGE_LENGTH // 4, len(bio[1]))) 104 | else: 105 | message.reply_text("Reply to someone's message to set their bio!") 106 | 107 | 108 | def __user_info__(user_id, chat_id): 109 | bio = html.escape(sql.get_user_bio(user_id) or "") 110 | me = html.escape(sql.get_user_me_info(user_id) or "") 111 | if bio and me: 112 | return "About user:\n{me}\nWhat others say:\n{bio}".format(me=me, bio=bio) 113 | elif bio: 114 | return "What others say:\n{bio}\n".format(me=me, bio=bio) 115 | elif me: 116 | return "About user:\n{me}""".format(me=me, bio=bio) 117 | else: 118 | return "" 119 | 120 | 121 | def __gdpr__(user_id): 122 | sql.clear_user_info(user_id) 123 | sql.clear_user_bio(user_id) 124 | 125 | 126 | __help__ = """ 127 | - /setbio : while replying, will save another user's bio 128 | - /bio: will get your or another user's bio. This cannot be set by yourself. 129 | - /setme : will set your info 130 | - /me: will get your or another user's info 131 | """ 132 | 133 | __mod_name__ = "Bios and Abouts" 134 | 135 | SET_BIO_HANDLER = DisableAbleCommandHandler("setbio", set_about_bio) 136 | GET_BIO_HANDLER = DisableAbleCommandHandler("bio", about_bio, pass_args=True) 137 | 138 | SET_ABOUT_HANDLER = DisableAbleCommandHandler("setme", set_about_me) 139 | GET_ABOUT_HANDLER = DisableAbleCommandHandler("me", about_me, pass_args=True) 140 | 141 | dispatcher.add_handler(SET_BIO_HANDLER) 142 | dispatcher.add_handler(GET_BIO_HANDLER) 143 | dispatcher.add_handler(SET_ABOUT_HANDLER) 144 | dispatcher.add_handler(GET_ABOUT_HANDLER) 145 | 146 | -------------------------------------------------------------------------------- /deeppyer/__init__.py: -------------------------------------------------------------------------------- 1 | from PIL import Image, ImageOps, ImageEnhance 2 | from io import BytesIO 3 | from enum import Enum 4 | import aiohttp, asyncio, math, argparse 5 | 6 | class DeepfryTypes(Enum): 7 | """ 8 | Enum for the various possible effects added to the image. 9 | """ 10 | RED = 1 11 | BLUE = 2 12 | 13 | 14 | class Colours: 15 | RED = (254, 0, 2) 16 | YELLOW = (255, 255, 15) 17 | BLUE = (36, 113, 229) 18 | WHITE = (255,) * 3 19 | 20 | 21 | # TODO: Replace face recognition API with something like OpenCV. 22 | 23 | async def deepfry(img: Image, *, token: str=None, url_base: str='westcentralus', session: aiohttp.ClientSession=None, type=DeepfryTypes.RED) -> Image: 24 | """ 25 | Deepfry an image. 26 | 27 | img: PIL.Image - Image to deepfry. 28 | [token]: str - Token to use for Microsoft facial recognition API. If this is not supplied, lens flares will not be added. 29 | [url_base]: str = 'westcentralus' - API base to use. Only needed if your key's region is not `westcentralus`. 30 | [session]: aiohttp.ClientSession - Optional session to use with API requests. If provided, may provide a bit more speed. 31 | 32 | Returns: PIL.Image - Deepfried image. 33 | """ 34 | img = img.copy().convert('RGB') 35 | 36 | if type not in DeepfryTypes: 37 | raise ValueError(f'Unknown deepfry type "{type}", expected a value from deeppyer.DeepfryTypes') 38 | 39 | if token: 40 | req_url = f'https://{url_base}.api.cognitive.microsoft.com/face/v1.0/detect?returnFaceId=false&returnFaceLandmarks=true' # WHY THE FUCK IS THIS SO LONG 41 | headers = { 42 | 'Content-Type': 'application/octet-stream', 43 | 'Ocp-Apim-Subscription-Key': token, 44 | 'User-Agent': 'DeepPyer/1.0' 45 | } 46 | b = BytesIO() 47 | 48 | img.save(b, 'jpeg') 49 | b.seek(0) 50 | 51 | if session: 52 | async with session.post(req_url, headers=headers, data=b.read()) as r: 53 | face_data = await r.json() 54 | else: 55 | async with aiohttp.ClientSession() as s, s.post(req_url, headers=headers, data=b.read()) as r: 56 | face_data = await r.json() 57 | 58 | if 'error' in face_data: 59 | err = face_data['error'] 60 | code = err.get('code', err.get('statusCode')) 61 | msg = err['message'] 62 | 63 | raise Exception(f'Error with Microsoft Face Recognition API\n{code}: {msg}') 64 | 65 | if face_data: 66 | landmarks = face_data[0]['faceLandmarks'] 67 | 68 | # Get size and positions of eyes, and generate sizes for the flares 69 | eye_left_width = math.ceil(landmarks['eyeLeftInner']['x'] - landmarks['eyeLeftOuter']['x']) 70 | eye_left_height = math.ceil(landmarks['eyeLeftBottom']['y'] - landmarks['eyeLeftTop']['y']) 71 | eye_left_corner = (landmarks['eyeLeftOuter']['x'], landmarks['eyeLeftTop']['y']) 72 | flare_left_size = eye_left_height if eye_left_height > eye_left_width else eye_left_width 73 | flare_left_size *= 4 74 | eye_left_corner = tuple(math.floor(x - flare_left_size / 2.5 + 5) for x in eye_left_corner) 75 | 76 | eye_right_width = math.ceil(landmarks['eyeRightOuter']['x'] - landmarks['eyeRightInner']['x']) 77 | eye_right_height = math.ceil(landmarks['eyeRightBottom']['y'] - landmarks['eyeRightTop']['y']) 78 | eye_right_corner = (landmarks['eyeRightInner']['x'], landmarks['eyeRightTop']['y']) 79 | flare_right_size = eye_right_height if eye_right_height > eye_right_width else eye_right_width 80 | flare_right_size *= 4 81 | eye_right_corner = tuple(math.floor(x - flare_right_size / 2.5 + 5) for x in eye_right_corner) 82 | 83 | # Crush image to hell and back 84 | img = img.convert('RGB') 85 | width, height = img.width, img.height 86 | img = img.resize((int(width ** .75), int(height ** .75)), resample=Image.LANCZOS) 87 | img = img.resize((int(width ** .88), int(height ** .88)), resample=Image.BILINEAR) 88 | img = img.resize((int(width ** .9), int(height ** .9)), resample=Image.BICUBIC) 89 | img = img.resize((width, height), resample=Image.BICUBIC) 90 | img = ImageOps.posterize(img, 4) 91 | 92 | # Generate red and yellow overlay for classic deepfry effect 93 | r = img.split()[0] 94 | r = ImageEnhance.Contrast(r).enhance(2.0) 95 | r = ImageEnhance.Brightness(r).enhance(1.5) 96 | 97 | if type == DeepfryTypes.RED: 98 | r = ImageOps.colorize(r, Colours.RED, Colours.YELLOW) 99 | elif type == DeepfryTypes.BLUE: 100 | r = ImageOps.colorize(r, Colours.BLUE, Colours.WHITE) 101 | 102 | # Overlay red and yellow onto main image and sharpen the hell out of it 103 | img = Image.blend(img, r, 0.75) 104 | img = ImageEnhance.Sharpness(img).enhance(100.0) 105 | 106 | if token and face_data: 107 | # Copy and resize flares 108 | flare = Image.open('./deeppyer/flare.png') 109 | flare_left = flare.copy().resize((flare_left_size,) * 2, resample=Image.BILINEAR) 110 | flare_right = flare.copy().resize((flare_right_size,) * 2, resample=Image.BILINEAR) 111 | 112 | del flare 113 | 114 | img.paste(flare_left, eye_left_corner, flare_left) 115 | img.paste(flare_right, eye_right_corner, flare_right) 116 | 117 | return img 118 | 119 | if __name__ == '__main__': 120 | parser = argparse.ArgumentParser(description='Deepfry an image, optionally adding lens flares for eyes.') 121 | parser.add_argument('-v', '--version', action='version', version='%(prog)s 1.0', help='Display program version.') 122 | parser.add_argument('-t', '--token', help='Token to use for facial recognition API.') 123 | parser.add_argument('-o', '--output', help='Filename to output to.') 124 | parser.add_argument('file', metavar='FILE', help='File to deepfry.') 125 | args = parser.parse_args() 126 | 127 | token = args.token 128 | img = Image.open(args.file) 129 | out = args.output or './deepfried.jpg' 130 | 131 | loop = asyncio.get_event_loop() 132 | img = loop.run_until_complete(deepfry(img, token=token)) 133 | 134 | img.save(out, 'jpeg') 135 | -------------------------------------------------------------------------------- /haruka/modules/helper_funcs/chat_status.py: -------------------------------------------------------------------------------- 1 | from functools import wraps 2 | from typing import Optional 3 | 4 | from telegram import User, Chat, ChatMember, Update, Bot 5 | 6 | from haruka import DEL_CMDS, SUDO_USERS, WHITELIST_USERS 7 | import haruka.modules.sql.admin_sql as admin_sql 8 | from haruka.modules.translations.strings import tld 9 | 10 | import haruka.modules.sql.antispam_sql as sql 11 | 12 | def can_delete(chat: Chat, bot_id: int) -> bool: 13 | return chat.get_member(bot_id).can_delete_messages 14 | 15 | 16 | def is_user_ban_protected(chat: Chat, user_id: int, member: ChatMember = None) -> bool: 17 | if chat.type == 'private' \ 18 | or user_id in SUDO_USERS \ 19 | or user_id in WHITELIST_USERS \ 20 | or chat.all_members_are_administrators: 21 | return True 22 | 23 | if not member: 24 | member = chat.get_member(user_id) 25 | return member.status in ('administrator', 'creator') 26 | 27 | 28 | def is_user_admin(chat: Chat, user_id: int, member: ChatMember = None) -> bool: 29 | if chat.type == 'private' \ 30 | or user_id in SUDO_USERS \ 31 | or chat.all_members_are_administrators: 32 | return True 33 | 34 | if not member: 35 | member = chat.get_member(user_id) 36 | return member.status in ('administrator', 'creator') 37 | 38 | 39 | def is_bot_admin(chat: Chat, bot_id: int, bot_member: ChatMember = None) -> bool: 40 | if chat.type == 'private' \ 41 | or chat.all_members_are_administrators: 42 | return True 43 | 44 | if not bot_member: 45 | bot_member = chat.get_member(bot_id) 46 | return bot_member.status in ('administrator', 'creator') 47 | 48 | 49 | def is_user_in_chat(chat: Chat, user_id: int) -> bool: 50 | member = chat.get_member(user_id) 51 | return member.status not in ('left', 'kicked') 52 | 53 | 54 | def bot_can_delete(func): 55 | @wraps(func) 56 | def delete_rights(bot: Bot, update: Update, *args, **kwargs): 57 | if can_delete(update.effective_chat, bot.id): 58 | return func(bot, update, *args, **kwargs) 59 | else: 60 | update.effective_message.reply_text("I can't delete messages here! " 61 | "Make sure I'm admin and can delete other user's messages.") 62 | 63 | return delete_rights 64 | 65 | 66 | def can_pin(func): 67 | @wraps(func) 68 | def pin_rights(bot: Bot, update: Update, *args, **kwargs): 69 | if update.effective_chat.get_member(bot.id).can_pin_messages: 70 | return func(bot, update, *args, **kwargs) 71 | else: 72 | update.effective_message.reply_text("I can't pin messages here! " 73 | "Make sure I'm admin and can pin messages.") 74 | 75 | return pin_rights 76 | 77 | 78 | def can_promote(func): 79 | @wraps(func) 80 | def promote_rights(bot: Bot, update: Update, *args, **kwargs): 81 | if update.effective_chat.get_member(bot.id).can_promote_members: 82 | return func(bot, update, *args, **kwargs) 83 | else: 84 | update.effective_message.reply_text("I can't promote/demote people here! " 85 | "Make sure I'm admin and can appoint new admins.") 86 | 87 | return promote_rights 88 | 89 | 90 | def can_restrict(func): 91 | @wraps(func) 92 | def promote_rights(bot: Bot, update: Update, *args, **kwargs): 93 | if update.effective_chat.get_member(bot.id).can_restrict_members: 94 | return func(bot, update, *args, **kwargs) 95 | else: 96 | update.effective_message.reply_text("I can't restrict people here! " 97 | "Make sure I'm admin and can appoint new admins.") 98 | 99 | return promote_rights 100 | 101 | 102 | def bot_admin(func): 103 | @wraps(func) 104 | def is_admin(bot: Bot, update: Update, *args, **kwargs): 105 | if is_bot_admin(update.effective_chat, bot.id): 106 | return func(bot, update, *args, **kwargs) 107 | else: 108 | update.effective_message.reply_text("I'm not admin!") 109 | 110 | return is_admin 111 | 112 | 113 | def user_admin(func): 114 | @wraps(func) 115 | def is_admin(bot: Bot, update: Update, *args, **kwargs): 116 | user = update.effective_user # type: Optional[User] 117 | chat = update.effective_chat # type: Optional[Chat] 118 | if user and is_user_admin(update.effective_chat, user.id): 119 | return func(bot, update, *args, **kwargs) 120 | 121 | elif not user: 122 | pass 123 | 124 | elif DEL_CMDS and " " not in update.effective_message.text: 125 | update.effective_message.delete() 126 | 127 | elif (admin_sql.command_reaction(chat.id) == True): 128 | update.effective_message.reply_text("Who dis non-admin telling me what to do?") 129 | 130 | return is_admin 131 | 132 | 133 | def user_admin_no_reply(func): 134 | @wraps(func) 135 | def is_admin(bot: Bot, update: Update, *args, **kwargs): 136 | user = update.effective_user # type: Optional[User] 137 | if user and is_user_admin(update.effective_chat, user.id): 138 | return func(bot, update, *args, **kwargs) 139 | 140 | elif not user: 141 | pass 142 | 143 | elif DEL_CMDS and " " not in update.effective_message.text: 144 | update.effective_message.delete() 145 | 146 | return is_admin 147 | 148 | 149 | def user_not_admin(func): 150 | @wraps(func) 151 | def is_not_admin(bot: Bot, update: Update, *args, **kwargs): 152 | user = update.effective_user # type: Optional[User] 153 | if user and not is_user_admin(update.effective_chat, user.id): 154 | return func(bot, update, *args, **kwargs) 155 | 156 | return is_not_admin 157 | 158 | 159 | def user_is_gbanned(func): 160 | @wraps(func) 161 | def is_user_gbanned(bot: Bot, update: Update, *args, **kwargs): 162 | if not sql.is_user_gbanned(update.effective_user.id): 163 | return func(bot, update, *args, **kwargs) 164 | else: 165 | pass 166 | return is_user_gbanned 167 | 168 | -------------------------------------------------------------------------------- /haruka/modules/urlblacklist.py: -------------------------------------------------------------------------------- 1 | import html 2 | 3 | from telegram import Bot, ParseMode, Update 4 | from telegram.error import BadRequest 5 | from telegram.ext import CommandHandler, Filters, MessageHandler, run_async 6 | 7 | import tldextract 8 | from haruka import LOGGER, dispatcher 9 | from haruka.modules.disable import DisableAbleCommandHandler 10 | from haruka.modules.helper_funcs.chat_status import user_admin, user_not_admin 11 | from haruka.modules.sql import urlblacklist_sql as sql 12 | 13 | 14 | @run_async 15 | @user_admin 16 | def add_blacklist_url(bot: Bot, update: Update): 17 | chat = update.effective_chat 18 | message = update.effective_message 19 | urls = message.text.split(None, 1) 20 | if len(urls) > 1: 21 | urls = urls[1] 22 | to_blacklist = list(set(uri.strip() 23 | for uri in urls.split("\n") if uri.strip())) 24 | blacklisted = [] 25 | 26 | for uri in to_blacklist: 27 | extract_url = tldextract.extract(uri) 28 | if extract_url.domain and extract_url.suffix: 29 | blacklisted.append(extract_url.domain + "." + extract_url.suffix) 30 | sql.blacklist_url(chat.id, extract_url.domain + "." + extract_url.suffix) 31 | 32 | if len(to_blacklist) == 1: 33 | extract_url = tldextract.extract(to_blacklist[0]) 34 | if extract_url.domain and extract_url.suffix: 35 | message.reply_text( 36 | "Added {} domain to the blacklist!".format( 37 | html.escape( 38 | extract_url.domain + "." + extract_url.suffix)), 39 | parse_mode=ParseMode.HTML) 40 | else: 41 | message.reply_text( 42 | "You are trying to blacklist an invalid url") 43 | else: 44 | message.reply_text( 45 | "Added {} domains to the blacklist.".format( 46 | len(blacklisted)), parse_mode=ParseMode.HTML) 47 | else: 48 | message.reply_text( 49 | "Tell me which urls you would like to add to the blacklist.") 50 | 51 | 52 | @run_async 53 | @user_admin 54 | def rm_blacklist_url(bot: Bot, update: Update): 55 | chat = update.effective_chat 56 | message = update.effective_message 57 | urls = message.text.split(None, 1) 58 | 59 | if len(urls) > 1: 60 | urls = urls[1] 61 | to_unblacklist = list(set(uri.strip() 62 | for uri in urls.split("\n") if uri.strip())) 63 | unblacklisted = 0 64 | for uri in to_unblacklist: 65 | extract_url = tldextract.extract(uri) 66 | success = sql.rm_url_from_blacklist(chat.id, extract_url.domain + "." + extract_url.suffix) 67 | if success: 68 | unblacklisted += 1 69 | 70 | if len(to_unblacklist) == 1: 71 | if unblacklisted: 72 | message.reply_text( 73 | "Removed {} from the blacklist!".format( 74 | html.escape( 75 | to_unblacklist[0])), 76 | parse_mode=ParseMode.HTML) 77 | else: 78 | message.reply_text("This isn't a blacklisted domain...!") 79 | elif unblacklisted == len(to_unblacklist): 80 | message.reply_text( 81 | "Removed {} domains from the blacklist.".format( 82 | unblacklisted), parse_mode=ParseMode.HTML) 83 | elif not unblacklisted: 84 | message.reply_text( 85 | "None of these domains exist, so they weren't removed.", 86 | parse_mode=ParseMode.HTML) 87 | else: 88 | message.reply_text( 89 | "Removed {} domains from the blacklist. {} did not exist, " 90 | "so were not removed.".format( 91 | unblacklisted, 92 | len(to_unblacklist) - unblacklisted), 93 | parse_mode=ParseMode.HTML) 94 | else: 95 | message.reply_text( 96 | "Tell me which domains you would like to remove from the blacklist.") 97 | 98 | 99 | @run_async 100 | @user_not_admin 101 | def del_blacklist_url(bot: Bot, update: Update): 102 | chat = update.effective_chat 103 | message = update.effective_message 104 | parsed_entities = message.parse_entities(types=["url"]) 105 | extracted_domains = [] 106 | for obj, url in parsed_entities.items(): 107 | extract_url = tldextract.extract(url) 108 | extracted_domains.append(extract_url.domain + "." + extract_url.suffix) 109 | for url in sql.get_blacklisted_urls(chat.id): 110 | if url in extracted_domains: 111 | try: 112 | message.delete() 113 | except BadRequest as excp: 114 | if excp.message == "Message to delete not found": 115 | pass 116 | else: 117 | LOGGER.exception("Error while deleting blacklist message.") 118 | break 119 | 120 | 121 | @run_async 122 | def get_blacklisted_urls(bot: Bot, update: Update): 123 | chat = update.effective_chat 124 | message = update.effective_message 125 | 126 | base_string = "Current blacklisted domains:\n" 127 | blacklisted = sql.get_blacklisted_urls(chat.id) 128 | 129 | if not blacklisted: 130 | message.reply_text("There are no blacklisted domains here!") 131 | return 132 | for domain in blacklisted: 133 | base_string += "- {}\n".format(domain) 134 | 135 | message.reply_text(base_string, parse_mode=ParseMode.HTML) 136 | 137 | 138 | URL_BLACKLIST_HANDLER = DisableAbleCommandHandler( 139 | "blacklist", 140 | add_blacklist_url, 141 | filters=Filters.group, 142 | pass_args=True, 143 | admin_ok=True) 144 | ADD_URL_BLACKLIST_HANDLER = CommandHandler( 145 | "addurl", add_blacklist_url, filters=Filters.group) 146 | 147 | RM_BLACKLIST_URL_HANDLER = CommandHandler( 148 | "delurl", rm_blacklist_url, filters=Filters.group) 149 | 150 | GET_BLACKLISTED_URLS = CommandHandler( 151 | "geturl", 152 | get_blacklisted_urls, 153 | filters=Filters.group) 154 | 155 | URL_DELETE_HANDLER = MessageHandler( 156 | Filters.entity("url"), 157 | del_blacklist_url, 158 | edited_updates=True) 159 | 160 | 161 | __mod_name__ = "Domain Blacklists" 162 | 163 | __help__ = """ 164 | Domain blacklisting is used to stop certain domains from being mentioned in a group, Any time an url on that domain is mentioned, / 165 | the message will immediately be deleted. 166 | 167 | *NOTE:* domain blacklisting do not affect group admins. 168 | 169 | - /geturl: View the current blacklisted urls 170 | 171 | *Admin only:* 172 | 173 | - /addurl : Add a domain to the blacklist. The bot will automatically parse the url. 174 | - /delurl : Remove urls from the blacklist 175 | 176 | """ 177 | 178 | dispatcher.add_handler(URL_BLACKLIST_HANDLER) 179 | dispatcher.add_handler(ADD_URL_BLACKLIST_HANDLER) 180 | dispatcher.add_handler(RM_BLACKLIST_URL_HANDLER) 181 | dispatcher.add_handler(GET_BLACKLISTED_URLS) 182 | dispatcher.add_handler(URL_DELETE_HANDLER) 183 | 184 | -------------------------------------------------------------------------------- /haruka/modules/reverseimage.py: -------------------------------------------------------------------------------- 1 | import os 2 | import re 3 | import requests 4 | import urllib 5 | from urllib.request import urlopen 6 | from urllib.error import URLError, HTTPError 7 | from bs4 import BeautifulSoup 8 | 9 | from typing import List 10 | from telegram import ParseMode, InputMediaPhoto, Update, Bot, TelegramError 11 | from telegram.ext import run_async 12 | 13 | from haruka import dispatcher 14 | 15 | from haruka.modules.disable import DisableAbleCommandHandler 16 | 17 | 18 | opener = urllib.request.build_opener() 19 | useragent = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.38 Safari/537.36' 20 | #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' 21 | opener.addheaders = [('User-agent', useragent)] 22 | 23 | 24 | @run_async 25 | def reverse(bot: Bot, update: Update, args: List[str]): 26 | if os.path.isfile("okgoogle.png"): 27 | os.remove("okgoogle.png") 28 | 29 | msg = update.effective_message 30 | chat_id = update.effective_chat.id 31 | rtmid = msg.message_id 32 | imagename = "okgoogle.png" 33 | 34 | reply = msg.reply_to_message 35 | if reply: 36 | if reply.sticker: 37 | file_id = reply.sticker.file_id 38 | elif reply.photo: 39 | file_id = reply.photo[-1].file_id 40 | elif reply.document: 41 | file_id = reply.document.file_id 42 | else: 43 | msg.reply_text("Reply to an image or sticker to lookup.") 44 | return 45 | image_file = bot.get_file(file_id) 46 | image_file.download(imagename) 47 | if args: 48 | txt = args[0] 49 | try: 50 | lim = int(txt) 51 | except: 52 | lim = 2 53 | else: 54 | lim = 2 55 | elif args and not reply: 56 | splatargs = msg.text.split(" ") 57 | if len(splatargs) == 3: 58 | img_link = splatargs[1] 59 | try: 60 | lim = int(splatargs[2]) 61 | except: 62 | lim = 2 63 | elif len(splatargs) == 2: 64 | img_link = splatargs[1] 65 | lim = 2 66 | else: 67 | msg.reply_text("/reverse ") 68 | return 69 | try: 70 | urllib.request.urlretrieve(img_link, imagename) 71 | except HTTPError as HE: 72 | if HE.reason == 'Not Found': 73 | msg.reply_text("Image not found.") 74 | return 75 | elif HE.reason == 'Forbidden': 76 | msg.reply_text("Couldn't access the provided link, The website might have blocked accessing to the website by bot or the website does not existed.") 77 | return 78 | except URLError as UE: 79 | msg.reply_text(f"{UE.reason}") 80 | return 81 | except ValueError as VE: 82 | msg.reply_text(f"{VE}\nPlease try again using http or https protocol.") 83 | return 84 | else: 85 | msg.reply_markdown("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] `.") 86 | return 87 | 88 | try: 89 | searchUrl = 'https://www.google.com/searchbyimage/upload' 90 | multipart = {'encoded_image': (imagename, open(imagename, 'rb')), 'image_content': ''} 91 | response = requests.post(searchUrl, files=multipart, allow_redirects=False) 92 | fetchUrl = response.headers['Location'] 93 | 94 | if response != 400: 95 | xx = bot.send_message(chat_id, "Image was successfully uploaded to Google." 96 | "\nParsing source now. Maybe.", reply_to_message_id=rtmid) 97 | else: 98 | xx = bot.send_message(chat_id, "Google told me to go away.", reply_to_message_id=rtmid) 99 | return 100 | 101 | os.remove(imagename) 102 | match = ParseSauce(fetchUrl + "&hl=en") 103 | guess = match['best_guess'] 104 | if match['override'] and not match['override'] == '': 105 | imgspage = match['override'] 106 | else: 107 | imgspage = match['similar_images'] 108 | 109 | if guess and imgspage: 110 | xx.edit_text(f"[{guess}]({fetchUrl})\nLooking for images...", parse_mode='Markdown', disable_web_page_preview=True) 111 | else: 112 | xx.edit_text("Couldn't find anything.") 113 | return 114 | 115 | images = scam(imgspage, lim) 116 | if len(images) == 0: 117 | xx.edit_text(f"[{guess}]({fetchUrl})\n[Visually similar images]({imgspage})" 118 | "\nCouldn't fetch any images.", parse_mode='Markdown', disable_web_page_preview=True) 119 | return 120 | 121 | imglinks = [] 122 | for link in images: 123 | lmao = InputMediaPhoto(media=str(link)) 124 | imglinks.append(lmao) 125 | 126 | bot.send_media_group(chat_id=chat_id, media=imglinks, reply_to_message_id=rtmid) 127 | xx.edit_text(f"[{guess}]({fetchUrl})\n[Visually similar images]({imgspage})", parse_mode='Markdown', disable_web_page_preview=True) 128 | except TelegramError as e: 129 | print(e) 130 | except Exception as exception: 131 | print(exception) 132 | 133 | def ParseSauce(googleurl): 134 | """Parse/Scrape the HTML code for the info we want.""" 135 | 136 | source = opener.open(googleurl).read() 137 | soup = BeautifulSoup(source, 'html.parser') 138 | 139 | results = { 140 | 'similar_images': '', 141 | 'override': '', 142 | 'best_guess': '' 143 | } 144 | 145 | try: 146 | for bess in soup.findAll('a', {'class': 'PBorbe'}): 147 | url = 'https://www.google.com' + bess.get('href') 148 | results['override'] = url 149 | except: 150 | pass 151 | 152 | for similar_image in soup.findAll('input', {'class': 'gLFyf'}): 153 | url = 'https://www.google.com/search?tbm=isch&q=' + urllib.parse.quote_plus(similar_image.get('value')) 154 | results['similar_images'] = url 155 | 156 | for best_guess in soup.findAll('div', attrs={'class':'r5a77d'}): 157 | results['best_guess'] = best_guess.get_text() 158 | 159 | return results 160 | 161 | def scam(imgspage, lim): 162 | """Parse/Scrape the HTML code for the info we want.""" 163 | 164 | single = opener.open(imgspage).read() 165 | decoded = single.decode('utf-8') 166 | if int(lim) > 10: 167 | lim = 10 168 | 169 | imglinks = [] 170 | counter = 0 171 | 172 | pattern = r'^,\[\"(.*[.png|.jpg|.jpeg])\",[0-9]+,[0-9]+\]$' 173 | oboi = re.findall(pattern, decoded, re.I | re.M) 174 | 175 | for imglink in oboi: 176 | counter += 1 177 | imglinks.append(imglink) 178 | if counter >= int(lim): 179 | break 180 | 181 | return imglinks 182 | 183 | 184 | __help__ = """ 185 | - /reverse: Does a reverse image search of the media which it was replied to. 186 | """ 187 | 188 | __mod_name__ = "Image Lookup" 189 | 190 | REVERSE_HANDLER = DisableAbleCommandHandler("reverse", reverse, pass_args=True, admin_ok=True) 191 | 192 | dispatcher.add_handler(REVERSE_HANDLER) 193 | -------------------------------------------------------------------------------- /haruka/modules/disable.py: -------------------------------------------------------------------------------- 1 | from typing import Union, List, Optional 2 | 3 | from future.utils import string_types 4 | from telegram import ParseMode, Update, Bot, Chat, User 5 | from telegram.ext import CommandHandler, RegexHandler, Filters 6 | from telegram.utils.helpers import escape_markdown 7 | 8 | from haruka import dispatcher 9 | from haruka.modules.helper_funcs.handlers import CMD_STARTERS 10 | from haruka.modules.helper_funcs.misc import is_module_loaded 11 | 12 | FILENAME = __name__.rsplit(".", 1)[-1] 13 | 14 | # If module is due to be loaded, then setup all the magical handlers 15 | if is_module_loaded(FILENAME): 16 | from haruka.modules.helper_funcs.chat_status import user_admin, is_user_admin 17 | from telegram.ext.dispatcher import run_async 18 | 19 | from haruka.modules.sql import disable_sql as sql 20 | 21 | DISABLE_CMDS = [] 22 | DISABLE_OTHER = [] 23 | ADMIN_CMDS = [] 24 | 25 | class DisableAbleCommandHandler(CommandHandler): 26 | def __init__(self, command, callback, admin_ok=False, **kwargs): 27 | super().__init__(command, callback, **kwargs) 28 | self.admin_ok = admin_ok 29 | if isinstance(command, string_types): 30 | DISABLE_CMDS.append(command) 31 | if admin_ok: 32 | ADMIN_CMDS.append(command) 33 | else: 34 | DISABLE_CMDS.extend(command) 35 | if admin_ok: 36 | ADMIN_CMDS.extend(command) 37 | 38 | def check_update(self, update): 39 | chat = update.effective_chat # type: Optional[Chat] 40 | user = update.effective_user # type: Optional[User] 41 | if super().check_update(update): 42 | # Should be safe since check_update passed. 43 | command = update.effective_message.text_html.split(None, 1)[0][1:].split('@')[0] 44 | 45 | # disabled, admincmd, user admin 46 | if sql.is_command_disabled(chat.id, command): 47 | return command in ADMIN_CMDS and is_user_admin(chat, user.id) 48 | 49 | # not disabled 50 | else: 51 | return True 52 | 53 | return False 54 | 55 | 56 | class DisableAbleRegexHandler(RegexHandler): 57 | def __init__(self, pattern, callback, friendly="", **kwargs): 58 | super().__init__(pattern, callback, **kwargs) 59 | DISABLE_OTHER.append(friendly or pattern) 60 | self.friendly = friendly or pattern 61 | 62 | def check_update(self, update): 63 | chat = update.effective_chat 64 | return super().check_update(update) and not sql.is_command_disabled(chat.id, self.friendly) 65 | 66 | 67 | @run_async 68 | @user_admin 69 | def disable(bot: Bot, update: Update, args: List[str]): 70 | chat = update.effective_chat # type: Optional[Chat] 71 | if len(args) >= 1: 72 | disable_cmd = args[0] 73 | if disable_cmd.startswith(CMD_STARTERS): 74 | disable_cmd = disable_cmd[1:] 75 | 76 | if disable_cmd in set(DISABLE_CMDS + DISABLE_OTHER): 77 | sql.disable_command(chat.id, disable_cmd) 78 | update.effective_message.reply_text("Disabled the use of `{}`".format(disable_cmd), 79 | parse_mode=ParseMode.MARKDOWN) 80 | else: 81 | update.effective_message.reply_text("That command can't be disabled") 82 | 83 | else: 84 | update.effective_message.reply_text("What should I disable?") 85 | 86 | 87 | @run_async 88 | @user_admin 89 | def enable(bot: Bot, update: Update, args: List[str]): 90 | chat = update.effective_chat # type: Optional[Chat] 91 | if len(args) >= 1: 92 | enable_cmd = args[0] 93 | if enable_cmd.startswith(CMD_STARTERS): 94 | enable_cmd = enable_cmd[1:] 95 | 96 | if sql.enable_command(chat.id, enable_cmd): 97 | update.effective_message.reply_text("Enabled the use of `{}`".format(enable_cmd), 98 | parse_mode=ParseMode.MARKDOWN) 99 | else: 100 | update.effective_message.reply_text("Is that even disabled?") 101 | 102 | else: 103 | update.effective_message.reply_text("What should I enable?") 104 | 105 | 106 | @run_async 107 | @user_admin 108 | def list_cmds(bot: Bot, update: Update): 109 | if DISABLE_CMDS + DISABLE_OTHER: 110 | result = "" 111 | for cmd in set(DISABLE_CMDS + DISABLE_OTHER): 112 | result += " - `{}`\n".format(escape_markdown(cmd)) 113 | update.effective_message.reply_text("The following commands are toggleable:\n{}".format(result), 114 | parse_mode=ParseMode.MARKDOWN) 115 | else: 116 | update.effective_message.reply_text("No commands can be disabled.") 117 | 118 | 119 | # do not async 120 | def build_curr_disabled(chat_id: Union[str, int]) -> str: 121 | disabled = sql.get_all_disabled(chat_id) 122 | if not disabled: 123 | return "No commands are disabled!" 124 | 125 | result = "" 126 | for cmd in disabled: 127 | result += " - `{}`\n".format(escape_markdown(cmd)) 128 | return "The following commands are currently restricted:\n{}".format(result) 129 | 130 | 131 | @run_async 132 | def commands(bot: Bot, update: Update): 133 | chat = update.effective_chat 134 | update.effective_message.reply_text(build_curr_disabled(chat.id), parse_mode=ParseMode.MARKDOWN) 135 | 136 | 137 | def __stats__(): 138 | return "{} disabled items, across {} chats.".format(sql.num_disabled(), sql.num_chats()) 139 | 140 | 141 | def __migrate__(old_chat_id, new_chat_id): 142 | sql.migrate_chat(old_chat_id, new_chat_id) 143 | 144 | 145 | def __chat_settings__(chat_id, user_id): 146 | return build_curr_disabled(chat_id) 147 | 148 | 149 | def __import_data__(chat_id, data): 150 | disabled = data.get('disabled', {}) 151 | for disable_cmd in disabled: 152 | sql.disable_command(chat_id, disable_cmd) 153 | 154 | 155 | __mod_name__ = "Command disabling" 156 | 157 | __help__ = """ 158 | Not everyone wants every feature that the bot offers. Some commands are best \ 159 | left unused; to avoid spam and abuse. 160 | 161 | This allows you to disable some commonly used commands, so noone can use them. \ 162 | It'll also allow you to autodelete them, stopping people from bluetexting. 163 | 164 | - /cmds: check the current status of disabled commands 165 | 166 | *Admin only:* 167 | - /enable : enable that command 168 | - /disable : disable that command 169 | - /listcmds: list all possible toggleable commands 170 | """ 171 | 172 | DISABLE_HANDLER = CommandHandler("disable", disable, pass_args=True, filters=Filters.group) 173 | ENABLE_HANDLER = CommandHandler("enable", enable, pass_args=True, filters=Filters.group) 174 | COMMANDS_HANDLER = CommandHandler(["cmds", "disabled"], commands, filters=Filters.group) 175 | TOGGLE_HANDLER = CommandHandler("listcmds", list_cmds, filters=Filters.group) 176 | 177 | dispatcher.add_handler(DISABLE_HANDLER) 178 | dispatcher.add_handler(ENABLE_HANDLER) 179 | dispatcher.add_handler(COMMANDS_HANDLER) 180 | dispatcher.add_handler(TOGGLE_HANDLER) 181 | 182 | else: 183 | DisableAbleCommandHandler = CommandHandler 184 | DisableAbleRegexHandler = RegexHandler 185 | -------------------------------------------------------------------------------- /haruka/modules/log_channel.py: -------------------------------------------------------------------------------- 1 | from functools import wraps 2 | from typing import Optional 3 | 4 | from haruka.modules.helper_funcs.misc import is_module_loaded 5 | 6 | from haruka.modules.translations.strings import tld 7 | 8 | FILENAME = __name__.rsplit(".", 1)[-1] 9 | 10 | if is_module_loaded(FILENAME): 11 | from telegram import Bot, Update, ParseMode, Message, Chat 12 | from telegram.error import BadRequest, Unauthorized 13 | from telegram.ext import CommandHandler, run_async 14 | from telegram.utils.helpers import escape_markdown 15 | 16 | from haruka import dispatcher, LOGGER 17 | from haruka.modules.helper_funcs.chat_status import user_admin 18 | from haruka.modules.sql import log_channel_sql as sql 19 | 20 | 21 | def loggable(func): 22 | @wraps(func) 23 | def log_action(bot: Bot, update: Update, *args, **kwargs): 24 | result = func(bot, update, *args, **kwargs) 25 | chat = update.effective_chat # type: Optional[Chat] 26 | message = update.effective_message # type: Optional[Message] 27 | if result: 28 | if chat.type == chat.SUPERGROUP and chat.username: 29 | result += "\nLink: " \ 30 | "click here".format(chat.username, 31 | message.message_id) 32 | log_chat = sql.get_chat_log_channel(chat.id) 33 | if log_chat: 34 | send_log(bot, log_chat, chat.id, result) 35 | elif result == "": 36 | pass 37 | else: 38 | LOGGER.warning("%s was set as loggable, but had no return statement.", func) 39 | 40 | return result 41 | 42 | return log_action 43 | 44 | 45 | def send_log(bot: Bot, log_chat_id: str, orig_chat_id: str, result: str): 46 | try: 47 | bot.send_message(log_chat_id, result, parse_mode=ParseMode.HTML) 48 | except BadRequest as excp: 49 | if excp.message == "Chat not found": 50 | bot.send_message(orig_chat_id, "This log channel has been deleted - unsetting.") 51 | sql.stop_chat_logging(orig_chat_id) 52 | else: 53 | LOGGER.warning(excp.message) 54 | LOGGER.warning(result) 55 | LOGGER.exception("Could not parse") 56 | 57 | bot.send_message(log_chat_id, result + "\n\nFormatting has been disabled due to an unexpected error.") 58 | 59 | 60 | @run_async 61 | @user_admin 62 | def logging(bot: Bot, update: Update): 63 | message = update.effective_message # type: Optional[Message] 64 | chat = update.effective_chat # type: Optional[Chat] 65 | 66 | log_channel = sql.get_chat_log_channel(chat.id) 67 | if log_channel: 68 | try: 69 | log_channel_info = bot.get_chat(log_channel) 70 | message.reply_text( 71 | "This group has all it's logs sent to: {} (`{}`)".format(escape_markdown(log_channel_info.title), 72 | log_channel), 73 | parse_mode=ParseMode.MARKDOWN) 74 | except: 75 | print("Nut") 76 | else: 77 | message.reply_text("No log channel has been set for this group!") 78 | 79 | 80 | @run_async 81 | @user_admin 82 | def setlog(bot: Bot, update: Update): 83 | message = update.effective_message # type: Optional[Message] 84 | chat = update.effective_chat # type: Optional[Chat] 85 | if chat.type == chat.CHANNEL: 86 | message.reply_text(tld(chat.id, "Now, forward the /setlog to the group you want to tie this channel to!")) 87 | 88 | elif message.forward_from_chat: 89 | sql.set_chat_log_channel(chat.id, message.forward_from_chat.id) 90 | try: 91 | message.delete() 92 | except BadRequest as excp: 93 | if excp.message == "Message to delete not found": 94 | pass 95 | else: 96 | LOGGER.exception("Error deleting message in log channel. Should work anyway though.") 97 | 98 | try: 99 | bot.send_message(message.forward_from_chat.id, tld(chat.id, 100 | "This channel has been set as the log channel for {}.").format( 101 | chat.title or chat.first_name)) 102 | except Unauthorized as excp: 103 | if excp.message == "Forbidden: bot is not a member of the channel chat": 104 | bot.send_message(chat.id, tld(chat.id, "Successfully set log channel!")) 105 | else: 106 | LOGGER.exception("ERROR in setting the log channel.") 107 | 108 | bot.send_message(chat.id, tld(chat.id, "Successfully set log channel!")) 109 | 110 | else: 111 | message.reply_text(tld(chat.id, "*The steps to set a log channel are:*\n" 112 | " • add bot to the desired channel\n" 113 | " • send /setlog to the channel\n" 114 | " • forward the /setlog to the group\n"), ParseMode.MARKDOWN) 115 | 116 | 117 | @run_async 118 | @user_admin 119 | def unsetlog(bot: Bot, update: Update): 120 | message = update.effective_message # type: Optional[Message] 121 | chat = update.effective_chat # type: Optional[Chat] 122 | 123 | log_channel = sql.stop_chat_logging(chat.id) 124 | if log_channel: 125 | try: 126 | bot.send_message(log_channel, tld(chat.id, "Channel has been unlinked from {}").format(chat.title)) 127 | message.reply_text(tld(chat.id, "Log channel has been un-set.")) 128 | except: 129 | print("Nut") 130 | else: 131 | message.reply_text(tld(chat.id, "No log channel has been set yet!")) 132 | 133 | 134 | def __stats__(): 135 | return "{} log channels set.".format(sql.num_logchannels()) 136 | 137 | 138 | def __migrate__(old_chat_id, new_chat_id): 139 | sql.migrate_chat(old_chat_id, new_chat_id) 140 | 141 | 142 | def __chat_settings__(bot, update, chat, chatP, user): 143 | log_channel = sql.get_chat_log_channel(chat.id) 144 | if log_channel: 145 | log_channel_info = dispatcher.bot.get_chat(log_channel) 146 | return "This group has all it's logs sent to: {} (`{}`)".format(escape_markdown(log_channel_info.title), 147 | log_channel) 148 | return "No log channel is set for this group!" 149 | 150 | 151 | __help__ = """ 152 | *Admin only:* 153 | - /logchannel: get log channel info 154 | - /setlog: set the log channel. 155 | - /unsetlog: unset the log channel. 156 | 157 | Setting the log channel is done by: 158 | - adding the bot to the desired channel (as an admin!) 159 | - sending /setlog in the channel 160 | - forwarding the /setlog to the group 161 | """ 162 | 163 | __mod_name__ = "Log Channels" 164 | 165 | LOG_HANDLER = CommandHandler("logchannel", logging) 166 | SET_LOG_HANDLER = CommandHandler("setlog", setlog) 167 | UNSET_LOG_HANDLER = CommandHandler("unsetlog", unsetlog) 168 | 169 | dispatcher.add_handler(LOG_HANDLER) 170 | dispatcher.add_handler(SET_LOG_HANDLER) 171 | dispatcher.add_handler(UNSET_LOG_HANDLER) 172 | 173 | else: 174 | # run anyway if module not loaded 175 | def loggable(func): 176 | return func 177 | -------------------------------------------------------------------------------- /haruka/modules/sql/cust_filters_sql.py: -------------------------------------------------------------------------------- 1 | import threading 2 | 3 | from sqlalchemy import Column, String, UnicodeText, Boolean, Integer, distinct, func 4 | 5 | from haruka.modules.sql import BASE, SESSION 6 | 7 | 8 | class CustomFilters(BASE): 9 | __tablename__ = "cust_filters" 10 | chat_id = Column(String(14), primary_key=True) 11 | keyword = Column(UnicodeText, primary_key=True, nullable=False) 12 | reply = Column(UnicodeText, nullable=False) 13 | is_sticker = Column(Boolean, nullable=False, default=False) 14 | is_document = Column(Boolean, nullable=False, default=False) 15 | is_image = Column(Boolean, nullable=False, default=False) 16 | is_audio = Column(Boolean, nullable=False, default=False) 17 | is_voice = Column(Boolean, nullable=False, default=False) 18 | is_video = Column(Boolean, nullable=False, default=False) 19 | 20 | has_buttons = Column(Boolean, nullable=False, default=False) 21 | # NOTE: Here for legacy purposes, to ensure older filters don't mess up. 22 | has_markdown = Column(Boolean, nullable=False, default=False) 23 | 24 | def __init__(self, chat_id, keyword, reply, is_sticker=False, is_document=False, is_image=False, is_audio=False, 25 | is_voice=False, is_video=False, has_buttons=False): 26 | self.chat_id = str(chat_id) # ensure string 27 | self.keyword = keyword 28 | self.reply = reply 29 | self.is_sticker = is_sticker 30 | self.is_document = is_document 31 | self.is_image = is_image 32 | self.is_audio = is_audio 33 | self.is_voice = is_voice 34 | self.is_video = is_video 35 | self.has_buttons = has_buttons 36 | self.has_markdown = True 37 | 38 | def __repr__(self): 39 | return "" % self.chat_id 40 | 41 | def __eq__(self, other): 42 | return bool(isinstance(other, CustomFilters) 43 | and self.chat_id == other.chat_id 44 | and self.keyword == other.keyword) 45 | 46 | 47 | class Buttons(BASE): 48 | __tablename__ = "cust_filter_urls" 49 | id = Column(Integer, primary_key=True, autoincrement=True) 50 | chat_id = Column(String(14), primary_key=True) 51 | keyword = Column(UnicodeText, primary_key=True) 52 | name = Column(UnicodeText, nullable=False) 53 | url = Column(UnicodeText, nullable=False) 54 | same_line = Column(Boolean, default=False) 55 | 56 | def __init__(self, chat_id, keyword, name, url, same_line=False): 57 | self.chat_id = str(chat_id) 58 | self.keyword = keyword 59 | self.name = name 60 | self.url = url 61 | self.same_line = same_line 62 | 63 | 64 | CustomFilters.__table__.create(checkfirst=True) 65 | Buttons.__table__.create(checkfirst=True) 66 | 67 | CUST_FILT_LOCK = threading.RLock() 68 | BUTTON_LOCK = threading.RLock() 69 | CHAT_FILTERS = {} 70 | 71 | 72 | def get_all_filters(): 73 | try: 74 | return SESSION.query(CustomFilters).all() 75 | finally: 76 | SESSION.close() 77 | 78 | 79 | def add_filter(chat_id, keyword, reply, is_sticker=False, is_document=False, is_image=False, is_audio=False, 80 | is_voice=False, is_video=False, buttons=None): 81 | global CHAT_FILTERS 82 | 83 | if buttons is None: 84 | buttons = [] 85 | 86 | with CUST_FILT_LOCK: 87 | prev = SESSION.query(CustomFilters).get((str(chat_id), keyword)) 88 | if prev: 89 | with BUTTON_LOCK: 90 | prev_buttons = SESSION.query(Buttons).filter(Buttons.chat_id == str(chat_id), 91 | Buttons.keyword == keyword).all() 92 | for btn in prev_buttons: 93 | SESSION.delete(btn) 94 | SESSION.delete(prev) 95 | 96 | filt = CustomFilters(str(chat_id), keyword, reply, is_sticker, is_document, is_image, is_audio, is_voice, 97 | is_video, bool(buttons)) 98 | 99 | if keyword not in CHAT_FILTERS.get(str(chat_id), []): 100 | CHAT_FILTERS[str(chat_id)] = sorted(CHAT_FILTERS.get(str(chat_id), []) + [keyword], 101 | key=lambda x: (-len(x), x)) 102 | 103 | SESSION.add(filt) 104 | SESSION.commit() 105 | 106 | for b_name, url, same_line in buttons: 107 | add_note_button_to_db(chat_id, keyword, b_name, url, same_line) 108 | 109 | 110 | def remove_filter(chat_id, keyword): 111 | global CHAT_FILTERS 112 | with CUST_FILT_LOCK: 113 | filt = SESSION.query(CustomFilters).get((str(chat_id), keyword)) 114 | if filt: 115 | if keyword in CHAT_FILTERS.get(str(chat_id), []): # Sanity check 116 | CHAT_FILTERS.get(str(chat_id), []).remove(keyword) 117 | 118 | with BUTTON_LOCK: 119 | prev_buttons = SESSION.query(Buttons).filter(Buttons.chat_id == str(chat_id), 120 | Buttons.keyword == keyword).all() 121 | for btn in prev_buttons: 122 | SESSION.delete(btn) 123 | 124 | SESSION.delete(filt) 125 | SESSION.commit() 126 | return True 127 | 128 | SESSION.close() 129 | return False 130 | 131 | 132 | def get_chat_triggers(chat_id): 133 | return CHAT_FILTERS.get(str(chat_id), set()) 134 | 135 | 136 | def get_chat_filters(chat_id): 137 | try: 138 | return SESSION.query(CustomFilters).filter(CustomFilters.chat_id == str(chat_id)).order_by( 139 | func.length(CustomFilters.keyword).desc()).order_by(CustomFilters.keyword.asc()).all() 140 | finally: 141 | SESSION.close() 142 | 143 | 144 | def get_filter(chat_id, keyword): 145 | try: 146 | return SESSION.query(CustomFilters).get((str(chat_id), keyword)) 147 | finally: 148 | SESSION.close() 149 | 150 | 151 | def add_note_button_to_db(chat_id, keyword, b_name, url, same_line): 152 | with BUTTON_LOCK: 153 | button = Buttons(chat_id, keyword, b_name, url, same_line) 154 | SESSION.add(button) 155 | SESSION.commit() 156 | 157 | 158 | def get_buttons(chat_id, keyword): 159 | try: 160 | return SESSION.query(Buttons).filter(Buttons.chat_id == str(chat_id), Buttons.keyword == keyword).order_by( 161 | Buttons.id).all() 162 | finally: 163 | SESSION.close() 164 | 165 | 166 | def num_filters(): 167 | try: 168 | return SESSION.query(CustomFilters).count() 169 | finally: 170 | SESSION.close() 171 | 172 | 173 | def num_chats(): 174 | try: 175 | return SESSION.query(func.count(distinct(CustomFilters.chat_id))).scalar() 176 | finally: 177 | SESSION.close() 178 | 179 | 180 | def __load_chat_filters(): 181 | global CHAT_FILTERS 182 | try: 183 | chats = SESSION.query(CustomFilters.chat_id).distinct().all() 184 | for (chat_id,) in chats: # remove tuple by ( ,) 185 | CHAT_FILTERS[chat_id] = [] 186 | 187 | all_filters = SESSION.query(CustomFilters).all() 188 | for x in all_filters: 189 | CHAT_FILTERS[x.chat_id] += [x.keyword] 190 | 191 | CHAT_FILTERS = {x: sorted(set(y), key=lambda i: (-len(i), i)) for x, y in CHAT_FILTERS.items()} 192 | 193 | finally: 194 | SESSION.close() 195 | 196 | 197 | def migrate_chat(old_chat_id, new_chat_id): 198 | with CUST_FILT_LOCK: 199 | chat_filters = SESSION.query(CustomFilters).filter(CustomFilters.chat_id == str(old_chat_id)).all() 200 | for filt in chat_filters: 201 | filt.chat_id = str(new_chat_id) 202 | SESSION.commit() 203 | CHAT_FILTERS[str(new_chat_id)] = CHAT_FILTERS[str(old_chat_id)] 204 | del CHAT_FILTERS[str(old_chat_id)] 205 | 206 | with BUTTON_LOCK: 207 | chat_buttons = SESSION.query(Buttons).filter(Buttons.chat_id == str(old_chat_id)).all() 208 | for btn in chat_buttons: 209 | btn.chat_id = str(new_chat_id) 210 | SESSION.commit() 211 | 212 | 213 | __load_chat_filters() 214 | -------------------------------------------------------------------------------- /haruka/modules/sql/locks_sql.py: -------------------------------------------------------------------------------- 1 | # New chat added -> setup permissions 2 | import threading 3 | 4 | from sqlalchemy import Column, String, Boolean 5 | 6 | from haruka.modules.sql import SESSION, BASE 7 | 8 | 9 | class Permissions(BASE): 10 | __tablename__ = "permissions" 11 | chat_id = Column(String(14), primary_key=True) 12 | # Booleans are for "is this locked", _NOT_ "is this allowed" 13 | audio = Column(Boolean, default=False) 14 | voice = Column(Boolean, default=False) 15 | contact = Column(Boolean, default=False) 16 | video = Column(Boolean, default=False) 17 | videonote = Column(Boolean, default=False) 18 | document = Column(Boolean, default=False) 19 | photo = Column(Boolean, default=False) 20 | sticker = Column(Boolean, default=False) 21 | gif = Column(Boolean, default=False) 22 | url = Column(Boolean, default=False) 23 | bots = Column(Boolean, default=False) 24 | forward = Column(Boolean, default=False) 25 | game = Column(Boolean, default=False) 26 | location = Column(Boolean, default=False) 27 | 28 | def __init__(self, chat_id): 29 | self.chat_id = str(chat_id) # ensure string 30 | self.audio = False 31 | self.voice = False 32 | self.contact = False 33 | self.video = False 34 | self.videonote = False 35 | self.document = False 36 | self.photo = False 37 | self.sticker = False 38 | self.gif = False 39 | self.url = False 40 | self.bots = False 41 | self.forward = False 42 | self.game = False 43 | self.location = False 44 | 45 | def __repr__(self): 46 | return "" % self.chat_id 47 | 48 | 49 | class Restrictions(BASE): 50 | __tablename__ = "restrictions" 51 | chat_id = Column(String(14), primary_key=True) 52 | # Booleans are for "is this restricted", _NOT_ "is this allowed" 53 | messages = Column(Boolean, default=False) 54 | media = Column(Boolean, default=False) 55 | other = Column(Boolean, default=False) 56 | preview = Column(Boolean, default=False) 57 | 58 | def __init__(self, chat_id): 59 | self.chat_id = str(chat_id) # ensure string 60 | self.messages = False 61 | self.media = False 62 | self.other = False 63 | self.preview = False 64 | 65 | def __repr__(self): 66 | return "" % self.chat_id 67 | 68 | 69 | Permissions.__table__.create(checkfirst=True) 70 | Restrictions.__table__.create(checkfirst=True) 71 | 72 | 73 | PERM_LOCK = threading.RLock() 74 | RESTR_LOCK = threading.RLock() 75 | 76 | 77 | def init_permissions(chat_id, reset=False): 78 | curr_perm = SESSION.query(Permissions).get(str(chat_id)) 79 | if reset: 80 | SESSION.delete(curr_perm) 81 | SESSION.flush() 82 | perm = Permissions(str(chat_id)) 83 | SESSION.add(perm) 84 | SESSION.commit() 85 | return perm 86 | 87 | 88 | def init_restrictions(chat_id, reset=False): 89 | curr_restr = SESSION.query(Restrictions).get(str(chat_id)) 90 | if reset: 91 | SESSION.delete(curr_restr) 92 | SESSION.flush() 93 | restr = Restrictions(str(chat_id)) 94 | SESSION.add(restr) 95 | SESSION.commit() 96 | return restr 97 | 98 | 99 | def update_lock(chat_id, lock_type, locked): 100 | with PERM_LOCK: 101 | curr_perm = SESSION.query(Permissions).get(str(chat_id)) 102 | if not curr_perm: 103 | curr_perm = init_permissions(chat_id) 104 | 105 | if lock_type == "audio": 106 | curr_perm.audio = locked 107 | elif lock_type == "voice": 108 | curr_perm.voice = locked 109 | elif lock_type == "contact": 110 | curr_perm.contact = locked 111 | elif lock_type == "video": 112 | curr_perm.video = locked 113 | elif lock_type == "videonote": 114 | curr_perm.videonote = locked 115 | elif lock_type == "document": 116 | curr_perm.document = locked 117 | elif lock_type == "photo": 118 | curr_perm.photo = locked 119 | elif lock_type == "sticker": 120 | curr_perm.sticker = locked 121 | elif lock_type == "gif": 122 | curr_perm.gif = locked 123 | elif lock_type == 'url': 124 | curr_perm.url = locked 125 | elif lock_type == 'bots': 126 | curr_perm.bots = locked 127 | elif lock_type == 'forward': 128 | curr_perm.forward = locked 129 | elif lock_type == 'game': 130 | curr_perm.game = locked 131 | elif lock_type == 'location': 132 | curr_perm.location = locked 133 | 134 | SESSION.add(curr_perm) 135 | SESSION.commit() 136 | 137 | 138 | def update_restriction(chat_id, restr_type, locked): 139 | with RESTR_LOCK: 140 | curr_restr = SESSION.query(Restrictions).get(str(chat_id)) 141 | if not curr_restr: 142 | curr_restr = init_restrictions(chat_id) 143 | 144 | if restr_type == "messages": 145 | curr_restr.messages = locked 146 | elif restr_type == "media": 147 | curr_restr.media = locked 148 | elif restr_type == "other": 149 | curr_restr.other = locked 150 | elif restr_type == "previews": 151 | curr_restr.preview = locked 152 | elif restr_type == "all": 153 | curr_restr.messages = locked 154 | curr_restr.media = locked 155 | curr_restr.other = locked 156 | curr_restr.preview = locked 157 | SESSION.add(curr_restr) 158 | SESSION.commit() 159 | 160 | 161 | def is_locked(chat_id, lock_type): 162 | curr_perm = SESSION.query(Permissions).get(str(chat_id)) 163 | SESSION.close() 164 | 165 | if not curr_perm: 166 | return False 167 | 168 | elif lock_type == "sticker": 169 | return curr_perm.sticker 170 | elif lock_type == "photo": 171 | return curr_perm.photo 172 | elif lock_type == "audio": 173 | return curr_perm.audio 174 | elif lock_type == "voice": 175 | return curr_perm.voice 176 | elif lock_type == "contact": 177 | return curr_perm.contact 178 | elif lock_type == "video": 179 | return curr_perm.video 180 | elif lock_type == "videonote": 181 | return curr_perm.videonote 182 | elif lock_type == "document": 183 | return curr_perm.document 184 | elif lock_type == "gif": 185 | return curr_perm.gif 186 | elif lock_type == "url": 187 | return curr_perm.url 188 | elif lock_type == "bots": 189 | return curr_perm.bots 190 | elif lock_type == "forward": 191 | return curr_perm.forward 192 | elif lock_type == "game": 193 | return curr_perm.game 194 | elif lock_type == "location": 195 | return curr_perm.location 196 | 197 | 198 | def is_restr_locked(chat_id, lock_type): 199 | curr_restr = SESSION.query(Restrictions).get(str(chat_id)) 200 | SESSION.close() 201 | 202 | if not curr_restr: 203 | return False 204 | 205 | if lock_type == "messages": 206 | return curr_restr.messages 207 | elif lock_type == "media": 208 | return curr_restr.media 209 | elif lock_type == "other": 210 | return curr_restr.other 211 | elif lock_type == "previews": 212 | return curr_restr.preview 213 | elif lock_type == "all": 214 | return curr_restr.messages and curr_restr.media and curr_restr.other and curr_restr.preview 215 | 216 | 217 | def get_locks(chat_id): 218 | try: 219 | return SESSION.query(Permissions).get(str(chat_id)) 220 | finally: 221 | SESSION.close() 222 | 223 | 224 | def get_restr(chat_id): 225 | try: 226 | return SESSION.query(Restrictions).get(str(chat_id)) 227 | finally: 228 | SESSION.close() 229 | 230 | 231 | def migrate_chat(old_chat_id, new_chat_id): 232 | with PERM_LOCK: 233 | perms = SESSION.query(Permissions).get(str(old_chat_id)) 234 | if perms: 235 | perms.chat_id = str(new_chat_id) 236 | SESSION.commit() 237 | 238 | with RESTR_LOCK: 239 | rest = SESSION.query(Restrictions).get(str(old_chat_id)) 240 | if rest: 241 | rest.chat_id = str(new_chat_id) 242 | SESSION.commit() 243 | -------------------------------------------------------------------------------- /haruka/modules/sql/antispam_sql.py: -------------------------------------------------------------------------------- 1 | import threading 2 | 3 | from sqlalchemy import Column, UnicodeText, Integer, String, Boolean 4 | 5 | from haruka.modules.sql import BASE, SESSION 6 | 7 | 8 | class GloballyBannedUsers(BASE): 9 | __tablename__ = "gbans" 10 | user_id = Column(Integer, primary_key=True) 11 | name = Column(UnicodeText, nullable=False) 12 | reason = Column(UnicodeText) 13 | 14 | def __init__(self, user_id, name, reason=None): 15 | self.user_id = user_id 16 | self.name = name 17 | self.reason = reason 18 | 19 | def __repr__(self): 20 | return "".format(self.name, self.user_id) 21 | 22 | def to_dict(self): 23 | return {"user_id": self.user_id, 24 | "name": self.name, 25 | "reason": self.reason} 26 | 27 | 28 | class GloballyMutedUsers(BASE): 29 | __tablename__ = "gmutes" 30 | user_id = Column(Integer, primary_key=True) 31 | name = Column(UnicodeText, nullable=False) 32 | reason = Column(UnicodeText) 33 | 34 | def __init__(self, user_id, name, reason=None): 35 | self.user_id = user_id 36 | self.name = name 37 | self.reason = reason 38 | 39 | def __repr__(self): 40 | return "".format(self.name, self.user_id) 41 | 42 | def to_dict(self): 43 | return {"user_id": self.user_id, 44 | "name": self.name, 45 | "reason": self.reason} 46 | 47 | 48 | class AntispamSettings(BASE): 49 | __tablename__ = "antispam_settings" 50 | chat_id = Column(String(14), primary_key=True) 51 | setting = Column(Boolean, default=True, nullable=False) 52 | 53 | def __init__(self, chat_id, enabled): 54 | self.chat_id = str(chat_id) 55 | self.setting = enabled 56 | 57 | def __repr__(self): 58 | return "".format(self.chat_id, self.setting) 59 | 60 | 61 | GloballyBannedUsers.__table__.create(checkfirst=True) 62 | GloballyMutedUsers.__table__.create(checkfirst=True) 63 | AntispamSettings.__table__.create(checkfirst=True) 64 | 65 | GBANNED_USERS_LOCK = threading.RLock() 66 | ASPAM_SETTING_LOCK = threading.RLock() 67 | GBANNED_LIST = set() 68 | GBANSTAT_LIST = set() 69 | ANTISPAMSETTING = set() 70 | 71 | GMUTED_USERS_LOCK = threading.RLock() 72 | GMUTE_SETTING_LOCK = threading.RLock() 73 | GMUTED_LIST = set() 74 | GMUTESTAT_LIST = set() 75 | 76 | 77 | def gban_user(user_id, name, reason=None): 78 | with GBANNED_USERS_LOCK: 79 | user = SESSION.query(GloballyBannedUsers).get(user_id) 80 | if not user: 81 | user = GloballyBannedUsers(user_id, name, reason) 82 | else: 83 | user.name = name 84 | user.reason = reason 85 | 86 | SESSION.merge(user) 87 | SESSION.commit() 88 | __load_gbanned_userid_list() 89 | 90 | 91 | def update_gban_reason(user_id, name, reason=None): 92 | with GBANNED_USERS_LOCK: 93 | user = SESSION.query(GloballyBannedUsers).get(user_id) 94 | if not user: 95 | return None 96 | old_reason = user.reason 97 | user.name = name 98 | user.reason = reason 99 | 100 | SESSION.merge(user) 101 | SESSION.commit() 102 | return old_reason 103 | 104 | 105 | def ungban_user(user_id): 106 | with GBANNED_USERS_LOCK: 107 | user = SESSION.query(GloballyBannedUsers).get(user_id) 108 | if user: 109 | SESSION.delete(user) 110 | 111 | SESSION.commit() 112 | __load_gbanned_userid_list() 113 | 114 | 115 | def is_user_gbanned(user_id): 116 | return user_id in GBANNED_LIST 117 | 118 | 119 | def get_gbanned_user(user_id): 120 | try: 121 | return SESSION.query(GloballyBannedUsers).get(user_id) 122 | finally: 123 | SESSION.close() 124 | 125 | 126 | def get_gban_list(): 127 | try: 128 | return [x.to_dict() for x in SESSION.query(GloballyBannedUsers).all()] 129 | finally: 130 | SESSION.close() 131 | 132 | 133 | def enable_antispam(chat_id): 134 | with ASPAM_SETTING_LOCK: 135 | chat = SESSION.query(AntispamSettings).get(str(chat_id)) 136 | if not chat: 137 | chat = AntispamSettings(chat_id, True) 138 | 139 | chat.setting = True 140 | SESSION.add(chat) 141 | SESSION.commit() 142 | if str(chat_id) in GBANSTAT_LIST: 143 | GBANSTAT_LIST.remove(str(chat_id)) 144 | 145 | 146 | def disable_antispam(chat_id): 147 | with ASPAM_SETTING_LOCK: 148 | chat = SESSION.query(AntispamSettings).get(str(chat_id)) 149 | if not chat: 150 | chat = AntispamSettings(chat_id, False) 151 | 152 | chat.setting = False 153 | SESSION.add(chat) 154 | SESSION.commit() 155 | GBANSTAT_LIST.add(str(chat_id)) 156 | 157 | 158 | def does_chat_gban(chat_id): 159 | return str(chat_id) not in GBANSTAT_LIST 160 | 161 | 162 | def num_gbanned_users(): 163 | return len(GBANNED_LIST) 164 | 165 | 166 | def __load_gbanned_userid_list(): 167 | global GBANNED_LIST 168 | try: 169 | GBANNED_LIST = {x.user_id for x in SESSION.query(GloballyBannedUsers).all()} 170 | finally: 171 | SESSION.close() 172 | 173 | 174 | def __load_gban_stat_list(): 175 | global GBANSTAT_LIST 176 | try: 177 | GBANSTAT_LIST = {x.chat_id for x in SESSION.query(AntispamSettings).all() if not x.setting} 178 | finally: 179 | SESSION.close() 180 | 181 | #Gmute 182 | 183 | def gmute_user(user_id, name, reason=None): 184 | with GMUTED_USERS_LOCK: 185 | user = SESSION.query(GloballyMutedUsers).get(user_id) 186 | if not user: 187 | user = GloballyMutedUsers(user_id, name, reason) 188 | else: 189 | user.name = name 190 | user.reason = reason 191 | 192 | SESSION.merge(user) 193 | SESSION.commit() 194 | __load_gmuted_userid_list() 195 | 196 | 197 | def update_gmute_reason(user_id, name, reason=None): 198 | with GMUTED_USERS_LOCK: 199 | user = SESSION.query(GloballyMutedUsers).get(user_id) 200 | if not user: 201 | return False 202 | user.name = name 203 | user.reason = reason 204 | 205 | SESSION.merge(user) 206 | SESSION.commit() 207 | return True 208 | 209 | 210 | def ungmute_user(user_id): 211 | with GMUTED_USERS_LOCK: 212 | user = SESSION.query(GloballyMutedUsers).get(user_id) 213 | if user: 214 | SESSION.delete(user) 215 | 216 | SESSION.commit() 217 | __load_gmuted_userid_list() 218 | 219 | 220 | def is_user_gmuted(user_id): 221 | return user_id in GMUTED_LIST 222 | 223 | 224 | def get_gmuted_user(user_id): 225 | try: 226 | return SESSION.query(GloballyMutedUsers).get(user_id) 227 | finally: 228 | SESSION.close() 229 | 230 | 231 | def get_gmute_list(): 232 | try: 233 | return [x.to_dict() for x in SESSION.query(GloballyMutedUsers).all()] 234 | finally: 235 | SESSION.close() 236 | 237 | 238 | def does_chat_gmute(chat_id): 239 | return str(chat_id) not in GMUTESTAT_LIST 240 | 241 | 242 | def num_gmuted_users(): 243 | return len(GMUTED_LIST) 244 | 245 | 246 | def __load_gmuted_userid_list(): 247 | global GMUTED_LIST 248 | try: 249 | GMUTED_LIST = {x.user_id for x in SESSION.query(GloballyMutedUsers).all()} 250 | finally: 251 | SESSION.close() 252 | 253 | 254 | def __load_gmute_stat_list(): 255 | global GMUTESTAT_LIST 256 | try: 257 | GMUTESTAT_LIST = {x.chat_id for x in SESSION.query(AntispamSettings).all() if not x.setting} 258 | finally: 259 | SESSION.close() 260 | 261 | 262 | 263 | def migrate_chat(old_chat_id, new_chat_id): 264 | with ASPAM_SETTING_LOCK: 265 | gban = SESSION.query(AntispamSettings).get(str(old_chat_id)) 266 | if gban: 267 | gban.chat_id = new_chat_id 268 | SESSION.add(gban) 269 | 270 | gmute = SESSION.query(AntispamSettings).get(str(old_chat_id)) 271 | if gmute: 272 | gmute.chat_id = new_chat_id 273 | SESSION.add(gmute) 274 | 275 | SESSION.commit() 276 | 277 | 278 | # Create in memory userid to avoid disk access 279 | __load_gbanned_userid_list() 280 | __load_gban_stat_list() 281 | 282 | __load_gmuted_userid_list() 283 | __load_gmute_stat_list() 284 | -------------------------------------------------------------------------------- /haruka/modules/helper_funcs/msg_types.py: -------------------------------------------------------------------------------- 1 | from enum import IntEnum, unique 2 | 3 | from telegram import Message 4 | 5 | from haruka.modules.helper_funcs.string_handling import button_markdown_parser 6 | 7 | 8 | @unique 9 | class Types(IntEnum): 10 | TEXT = 0 11 | BUTTON_TEXT = 1 12 | STICKER = 2 13 | DOCUMENT = 3 14 | PHOTO = 4 15 | AUDIO = 5 16 | VOICE = 6 17 | VIDEO = 7 18 | VIDEO_NOTE = 8 19 | 20 | 21 | def get_note_type(msg: Message): 22 | data_type = None 23 | content = None 24 | text = "" 25 | raw_text = msg.text or msg.caption 26 | args = raw_text.split(None, 2) # use python's maxsplit to separate cmd and args 27 | note_name = args[1] 28 | 29 | buttons = [] 30 | # determine what the contents of the filter are - text, image, sticker, etc 31 | if len(args) >= 3: 32 | offset = len(args[2]) - len(raw_text) # set correct offset relative to command + notename 33 | text, buttons = button_markdown_parser(args[2], entities=msg.parse_entities() or msg.parse_caption_entities(), 34 | offset=offset) 35 | if buttons: 36 | data_type = Types.BUTTON_TEXT 37 | else: 38 | data_type = Types.TEXT 39 | 40 | elif msg.reply_to_message: 41 | entities = msg.reply_to_message.parse_entities() 42 | msgtext = msg.reply_to_message.text or msg.reply_to_message.caption 43 | if len(args) >= 2 and msg.reply_to_message.text: # not caption, text 44 | text, buttons = button_markdown_parser(msgtext, 45 | entities=entities) 46 | if buttons: 47 | data_type = Types.BUTTON_TEXT 48 | else: 49 | data_type = Types.TEXT 50 | 51 | elif msg.reply_to_message.sticker: 52 | content = msg.reply_to_message.sticker.file_id 53 | data_type = Types.STICKER 54 | 55 | elif msg.reply_to_message.document: 56 | content = msg.reply_to_message.document.file_id 57 | text, buttons = button_markdown_parser(msgtext, entities=entities) 58 | data_type = Types.DOCUMENT 59 | 60 | elif msg.reply_to_message.photo: 61 | content = msg.reply_to_message.photo[-1].file_id # last elem = best quality 62 | text, buttons = button_markdown_parser(msgtext, entities=entities) 63 | data_type = Types.PHOTO 64 | 65 | elif msg.reply_to_message.audio: 66 | content = msg.reply_to_message.audio.file_id 67 | text, buttons = button_markdown_parser(msgtext, entities=entities) 68 | data_type = Types.AUDIO 69 | 70 | elif msg.reply_to_message.voice: 71 | content = msg.reply_to_message.voice.file_id 72 | text, buttons = button_markdown_parser(msgtext, entities=entities) 73 | data_type = Types.VOICE 74 | 75 | elif msg.reply_to_message.video: 76 | content = msg.reply_to_message.video.file_id 77 | text, buttons = button_markdown_parser(msgtext, entities=entities) 78 | data_type = Types.VIDEO 79 | 80 | elif msg.reply_to_message.video_note: 81 | content = msg.reply_to_message.video_note.file_id 82 | text, buttons = button_markdown_parser(msgtext, entities=entities) 83 | data_type = Types.VIDEO_NOTE 84 | 85 | return note_name, text, data_type, content, buttons 86 | 87 | 88 | # note: add own args? 89 | def get_welcome_type(msg: Message): 90 | data_type = None 91 | content = None 92 | text = "" 93 | 94 | try: 95 | if msg.reply_to_message: 96 | if msg.reply_to_message.text: 97 | args = msg.reply_to_message.text 98 | else: 99 | args = msg.reply_to_message.caption 100 | else: 101 | args = msg.text.split(None, 1) # use python's maxsplit to separate cmd and args 102 | except AttributeError: 103 | args = False 104 | 105 | buttons = [] 106 | # determine what the contents of the filter are - text, image, sticker, etc 107 | if args: 108 | if msg.reply_to_message: 109 | if msg.reply_to_message.caption: 110 | argumen = msg.reply_to_message.caption 111 | elif msg.reply_to_message.text: 112 | argumen = msg.reply_to_message.text 113 | else: 114 | argumen = args[1] 115 | offset = len(argumen) - len(msg.text) # set correct offset relative to command + notename 116 | text, buttons = button_markdown_parser(argumen, entities=msg.parse_entities(), offset=offset) 117 | if buttons: 118 | data_type = Types.BUTTON_TEXT 119 | else: 120 | data_type = Types.TEXT 121 | 122 | if msg.reply_to_message and msg.reply_to_message.sticker: 123 | content = msg.reply_to_message.sticker.file_id 124 | text = None 125 | data_type = Types.STICKER 126 | 127 | elif msg.reply_to_message and msg.reply_to_message.document: 128 | content = msg.reply_to_message.document.file_id 129 | # text = msg.reply_to_message.caption 130 | data_type = Types.DOCUMENT 131 | 132 | elif msg.reply_to_message and msg.reply_to_message.photo: 133 | content = msg.reply_to_message.photo[-1].file_id # last elem = best quality 134 | # text = msg.reply_to_message.caption 135 | data_type = Types.PHOTO 136 | 137 | elif msg.reply_to_message and msg.reply_to_message.audio: 138 | content = msg.reply_to_message.audio.file_id 139 | # text = msg.reply_to_message.caption 140 | data_type = Types.AUDIO 141 | 142 | elif msg.reply_to_message and msg.reply_to_message.voice: 143 | content = msg.reply_to_message.voice.file_id 144 | text = None 145 | data_type = Types.VOICE 146 | 147 | elif msg.reply_to_message and msg.reply_to_message.video: 148 | content = msg.reply_to_message.video.file_id 149 | # text = msg.reply_to_message.caption 150 | data_type = Types.VIDEO 151 | 152 | elif msg.reply_to_message and msg.reply_to_message.video_note: 153 | content = msg.reply_to_message.video_note.file_id 154 | text = None 155 | data_type = Types.VIDEO_NOTE 156 | 157 | return text, data_type, content, buttons 158 | 159 | def get_message_type(msg: Message): 160 | data_type = None 161 | content = None 162 | text = "" 163 | raw_text = msg.text or msg.caption 164 | args = raw_text.split(None, 1) # use python's maxsplit to separate cmd and args 165 | 166 | buttons = [] 167 | # determine what the contents of the filter are - text, image, sticker, etc 168 | if len(args) >= 2: 169 | offset = len(args[1]) - len(raw_text) # set correct offset relative to command + notename 170 | text, buttons = button_markdown_parser(args[1], entities=msg.parse_entities() or msg.parse_caption_entities(), 171 | offset=offset) 172 | if buttons: 173 | data_type = Types.BUTTON_TEXT 174 | else: 175 | data_type = Types.TEXT 176 | 177 | elif msg.reply_to_message: 178 | entities = msg.reply_to_message.parse_entities() 179 | msgtext = msg.reply_to_message.text or msg.reply_to_message.caption 180 | if len(args) >= 1 and msg.reply_to_message.text: # not caption, text 181 | text, buttons = button_markdown_parser(msgtext, 182 | entities=entities) 183 | if buttons: 184 | data_type = Types.BUTTON_TEXT 185 | else: 186 | data_type = Types.TEXT 187 | 188 | elif msg.reply_to_message.sticker: 189 | content = msg.reply_to_message.sticker.file_id 190 | data_type = Types.STICKER 191 | 192 | elif msg.reply_to_message.document: 193 | content = msg.reply_to_message.document.file_id 194 | text, buttons = button_markdown_parser(msgtext, entities=entities) 195 | data_type = Types.DOCUMENT 196 | 197 | elif msg.reply_to_message.photo: 198 | content = msg.reply_to_message.photo[-1].file_id # last elem = best quality 199 | text, buttons = button_markdown_parser(msgtext, entities=entities) 200 | data_type = Types.PHOTO 201 | 202 | elif msg.reply_to_message.audio: 203 | content = msg.reply_to_message.audio.file_id 204 | text, buttons = button_markdown_parser(msgtext, entities=entities) 205 | data_type = Types.AUDIO 206 | 207 | elif msg.reply_to_message.voice: 208 | content = msg.reply_to_message.voice.file_id 209 | text, buttons = button_markdown_parser(msgtext, entities=entities) 210 | data_type = Types.VOICE 211 | 212 | elif msg.reply_to_message.video: 213 | content = msg.reply_to_message.video.file_id 214 | text, buttons = button_markdown_parser(msgtext, entities=entities) 215 | data_type = Types.VIDEO 216 | 217 | elif msg.reply_to_message.video_note: 218 | content = msg.reply_to_message.video_note.file_id 219 | text, buttons = button_markdown_parser(msgtext, entities=entities) 220 | data_type = Types.VIDEO_NOTE 221 | 222 | return text, data_type, content, buttons 223 | -------------------------------------------------------------------------------- /haruka/modules/sql/warns_sql.py: -------------------------------------------------------------------------------- 1 | import threading 2 | 3 | from sqlalchemy import Integer, Column, String, UnicodeText, func, distinct, Boolean 4 | from sqlalchemy.dialects import postgresql 5 | 6 | from haruka.modules.sql import SESSION, BASE 7 | 8 | 9 | class Warns(BASE): 10 | __tablename__ = "warns" 11 | 12 | user_id = Column(Integer, primary_key=True) 13 | chat_id = Column(String(14), primary_key=True) 14 | num_warns = Column(Integer, default=0) 15 | reasons = Column(postgresql.ARRAY(UnicodeText)) 16 | 17 | def __init__(self, user_id, chat_id): 18 | self.user_id = user_id 19 | self.chat_id = str(chat_id) 20 | self.num_warns = 0 21 | self.reasons = [] 22 | 23 | def __repr__(self): 24 | return "<{} warns for {} in {} for reasons {}>".format(self.num_warns, self.user_id, self.chat_id, self.reasons) 25 | 26 | 27 | class WarnFilters(BASE): 28 | __tablename__ = "warn_filters" 29 | chat_id = Column(String(14), primary_key=True) 30 | keyword = Column(UnicodeText, primary_key=True, nullable=False) 31 | reply = Column(UnicodeText, nullable=False) 32 | 33 | def __init__(self, chat_id, keyword, reply): 34 | self.chat_id = str(chat_id) # ensure string 35 | self.keyword = keyword 36 | self.reply = reply 37 | 38 | def __repr__(self): 39 | return "" % self.chat_id 40 | 41 | def __eq__(self, other): 42 | return bool(isinstance(other, WarnFilters) 43 | and self.chat_id == other.chat_id 44 | and self.keyword == other.keyword) 45 | 46 | 47 | class WarnSettings(BASE): 48 | __tablename__ = "warn_settings" 49 | chat_id = Column(String(14), primary_key=True) 50 | warn_limit = Column(Integer, default=3) 51 | soft_warn = Column(Boolean, default=False) 52 | 53 | def __init__(self, chat_id, warn_limit=3, soft_warn=False): 54 | self.chat_id = str(chat_id) 55 | self.warn_limit = warn_limit 56 | self.soft_warn = soft_warn 57 | 58 | def __repr__(self): 59 | return "<{} has {} possible warns.>".format(self.chat_id, self.warn_limit) 60 | 61 | 62 | Warns.__table__.create(checkfirst=True) 63 | WarnFilters.__table__.create(checkfirst=True) 64 | WarnSettings.__table__.create(checkfirst=True) 65 | 66 | WARN_INSERTION_LOCK = threading.RLock() 67 | WARN_FILTER_INSERTION_LOCK = threading.RLock() 68 | WARN_SETTINGS_LOCK = threading.RLock() 69 | 70 | WARN_FILTERS = {} 71 | 72 | 73 | def warn_user(user_id, chat_id, reason=None): 74 | with WARN_INSERTION_LOCK: 75 | warned_user = SESSION.query(Warns).get((user_id, str(chat_id))) 76 | if not warned_user: 77 | warned_user = Warns(user_id, str(chat_id)) 78 | 79 | warned_user.num_warns += 1 80 | if reason: 81 | warned_user.reasons = warned_user.reasons + [reason] # TODO:: double check this wizardry 82 | 83 | reasons = warned_user.reasons 84 | num = warned_user.num_warns 85 | 86 | SESSION.add(warned_user) 87 | SESSION.commit() 88 | 89 | return num, reasons 90 | 91 | 92 | def remove_warn(user_id, chat_id): 93 | with WARN_INSERTION_LOCK: 94 | removed = False 95 | warned_user = SESSION.query(Warns).get((user_id, str(chat_id))) 96 | 97 | if warned_user and warned_user.num_warns > 0: 98 | warned_user.num_warns -= 1 99 | 100 | SESSION.add(warned_user) 101 | SESSION.commit() 102 | removed = True 103 | 104 | SESSION.close() 105 | return removed 106 | 107 | 108 | def reset_warns(user_id, chat_id): 109 | with WARN_INSERTION_LOCK: 110 | warned_user = SESSION.query(Warns).get((user_id, str(chat_id))) 111 | if warned_user: 112 | warned_user.num_warns = 0 113 | warned_user.reasons = [] 114 | 115 | SESSION.add(warned_user) 116 | SESSION.commit() 117 | SESSION.close() 118 | 119 | 120 | def get_warns(user_id, chat_id): 121 | try: 122 | user = SESSION.query(Warns).get((user_id, str(chat_id))) 123 | if not user: 124 | return None 125 | reasons = user.reasons 126 | num = user.num_warns 127 | return num, reasons 128 | finally: 129 | SESSION.close() 130 | 131 | 132 | def add_warn_filter(chat_id, keyword, reply): 133 | with WARN_FILTER_INSERTION_LOCK: 134 | warn_filt = WarnFilters(str(chat_id), keyword, reply) 135 | 136 | if keyword not in WARN_FILTERS.get(str(chat_id), []): 137 | WARN_FILTERS[str(chat_id)] = sorted(WARN_FILTERS.get(str(chat_id), []) + [keyword], 138 | key=lambda x: (-len(x), x)) 139 | 140 | SESSION.merge(warn_filt) # merge to avoid duplicate key issues 141 | SESSION.commit() 142 | 143 | 144 | def remove_warn_filter(chat_id, keyword): 145 | with WARN_FILTER_INSERTION_LOCK: 146 | warn_filt = SESSION.query(WarnFilters).get((str(chat_id), keyword)) 147 | if warn_filt: 148 | if keyword in WARN_FILTERS.get(str(chat_id), []): # sanity check 149 | WARN_FILTERS.get(str(chat_id), []).remove(keyword) 150 | 151 | SESSION.delete(warn_filt) 152 | SESSION.commit() 153 | return True 154 | SESSION.close() 155 | return False 156 | 157 | 158 | def get_chat_warn_triggers(chat_id): 159 | return WARN_FILTERS.get(str(chat_id), set()) 160 | 161 | 162 | def get_chat_warn_filters(chat_id): 163 | try: 164 | return SESSION.query(WarnFilters).filter(WarnFilters.chat_id == str(chat_id)).all() 165 | finally: 166 | SESSION.close() 167 | 168 | 169 | def get_warn_filter(chat_id, keyword): 170 | try: 171 | return SESSION.query(WarnFilters).get((str(chat_id), keyword)) 172 | finally: 173 | SESSION.close() 174 | 175 | 176 | def set_warn_limit(chat_id, warn_limit): 177 | with WARN_SETTINGS_LOCK: 178 | curr_setting = SESSION.query(WarnSettings).get(str(chat_id)) 179 | if not curr_setting: 180 | curr_setting = WarnSettings(chat_id, warn_limit=warn_limit) 181 | 182 | curr_setting.warn_limit = warn_limit 183 | 184 | SESSION.add(curr_setting) 185 | SESSION.commit() 186 | 187 | 188 | def set_warn_strength(chat_id, soft_warn): 189 | with WARN_SETTINGS_LOCK: 190 | curr_setting = SESSION.query(WarnSettings).get(str(chat_id)) 191 | if not curr_setting: 192 | curr_setting = WarnSettings(chat_id, soft_warn=soft_warn) 193 | 194 | curr_setting.soft_warn = soft_warn 195 | 196 | SESSION.add(curr_setting) 197 | SESSION.commit() 198 | 199 | 200 | def get_warn_setting(chat_id): 201 | try: 202 | setting = SESSION.query(WarnSettings).get(str(chat_id)) 203 | if setting: 204 | return setting.warn_limit, setting.soft_warn 205 | else: 206 | return 3, False 207 | 208 | finally: 209 | SESSION.close() 210 | 211 | 212 | def num_warns(): 213 | try: 214 | return SESSION.query(func.sum(Warns.num_warns)).scalar() or 0 215 | finally: 216 | SESSION.close() 217 | 218 | 219 | def num_warn_chats(): 220 | try: 221 | return SESSION.query(func.count(distinct(Warns.chat_id))).scalar() 222 | finally: 223 | SESSION.close() 224 | 225 | 226 | def num_warn_filters(): 227 | try: 228 | return SESSION.query(WarnFilters).count() 229 | finally: 230 | SESSION.close() 231 | 232 | 233 | def num_warn_chat_filters(chat_id): 234 | try: 235 | return SESSION.query(WarnFilters.chat_id).filter(WarnFilters.chat_id == str(chat_id)).count() 236 | finally: 237 | SESSION.close() 238 | 239 | 240 | def num_warn_filter_chats(): 241 | try: 242 | return SESSION.query(func.count(distinct(WarnFilters.chat_id))).scalar() 243 | finally: 244 | SESSION.close() 245 | 246 | 247 | def __load_chat_warn_filters(): 248 | global WARN_FILTERS 249 | try: 250 | chats = SESSION.query(WarnFilters.chat_id).distinct().all() 251 | for (chat_id,) in chats: # remove tuple by ( ,) 252 | WARN_FILTERS[chat_id] = [] 253 | 254 | all_filters = SESSION.query(WarnFilters).all() 255 | for x in all_filters: 256 | WARN_FILTERS[x.chat_id] += [x.keyword] 257 | 258 | WARN_FILTERS = {x: sorted(set(y), key=lambda i: (-len(i), i)) for x, y in WARN_FILTERS.items()} 259 | 260 | finally: 261 | SESSION.close() 262 | 263 | 264 | def migrate_chat(old_chat_id, new_chat_id): 265 | with WARN_INSERTION_LOCK: 266 | chat_notes = SESSION.query(Warns).filter(Warns.chat_id == str(old_chat_id)).all() 267 | for note in chat_notes: 268 | note.chat_id = str(new_chat_id) 269 | SESSION.commit() 270 | 271 | with WARN_FILTER_INSERTION_LOCK: 272 | chat_filters = SESSION.query(WarnFilters).filter(WarnFilters.chat_id == str(old_chat_id)).all() 273 | for filt in chat_filters: 274 | filt.chat_id = str(new_chat_id) 275 | SESSION.commit() 276 | WARN_FILTERS[str(new_chat_id)] = WARN_FILTERS[str(old_chat_id)] 277 | del WARN_FILTERS[str(old_chat_id)] 278 | 279 | with WARN_SETTINGS_LOCK: 280 | chat_settings = SESSION.query(WarnSettings).filter(WarnSettings.chat_id == str(old_chat_id)).all() 281 | for setting in chat_settings: 282 | setting.chat_id = str(new_chat_id) 283 | SESSION.commit() 284 | 285 | 286 | __load_chat_warn_filters() 287 | -------------------------------------------------------------------------------- /haruka/modules/helper_funcs/string_handling.py: -------------------------------------------------------------------------------- 1 | import re 2 | import time 3 | from typing import Dict, List 4 | import bleach 5 | import markdown2 6 | 7 | import emoji 8 | from telegram import MessageEntity 9 | from telegram.utils.helpers import escape_markdown 10 | 11 | # NOTE: the url \ escape may cause double escapes 12 | # match * (bold) (don't escape if in url) 13 | # match _ (italics) (don't escape if in url) 14 | # match ` (code) 15 | # match []() (markdown link) 16 | # else, escape *, _, `, and [ 17 | MATCH_MD = re.compile(r'\*(.*?)\*|' 18 | r'_(.*?)_|' 19 | r'`(.*?)`|' 20 | r'(?[*_`\[])') 22 | 23 | # regex to find []() links -> hyperlinks/buttons 24 | LINK_REGEX = re.compile(r'(? str: 29 | """ 30 | Escape all invalid markdown 31 | 32 | :param to_parse: text to escape 33 | :return: valid markdown string 34 | """ 35 | offset = 0 # offset to be used as adding a \ character causes the string to shift 36 | for match in MATCH_MD.finditer(to_parse): 37 | if match.group('esc'): 38 | ent_start = match.start() 39 | to_parse = to_parse[:ent_start + offset] + '\\' + to_parse[ent_start + offset:] 40 | offset += 1 41 | return to_parse 42 | 43 | 44 | # This is a fun one. 45 | def _calc_emoji_offset(to_calc) -> int: 46 | # Get all emoji in text. 47 | emoticons = emoji.get_emoji_regexp().finditer(to_calc) 48 | # Check the utf16 length of the emoji to determine the offset it caused. 49 | # Normal, 1 character emoji don't affect; hence sub 1. 50 | # special, eg with two emoji characters (eg face, and skin col) will have length 2, so by subbing one we 51 | # know we'll get one extra offset, 52 | return sum(len(e.group(0).encode('utf-16-le')) // 2 - 1 for e in emoticons) 53 | 54 | 55 | def markdown_parser(txt: str, entities: Dict[MessageEntity, str] = None, offset: int = 0) -> str: 56 | """ 57 | Parse a string, escaping all invalid markdown entities. 58 | 59 | Escapes URL's so as to avoid URL mangling. 60 | Re-adds any telegram code entities obtained from the entities object. 61 | 62 | :param txt: text to parse 63 | :param entities: dict of message entities in text 64 | :param offset: message offset - command and notename length 65 | :return: valid markdown string 66 | """ 67 | if not entities: 68 | entities = {} 69 | if not txt: 70 | return "" 71 | 72 | prev = 0 73 | res = "" 74 | # Loop over all message entities, and: 75 | # reinsert code 76 | # escape free-standing urls 77 | for ent, ent_text in entities.items(): 78 | if ent.offset < -offset: 79 | continue 80 | 81 | start = ent.offset + offset # start of entity 82 | end = ent.offset + offset + ent.length - 1 # end of entity 83 | 84 | # we only care about code, url, text links 85 | if ent.type in ("code", "url", "text_link"): 86 | # count emoji to switch counter 87 | count = _calc_emoji_offset(txt[:start]) 88 | start -= count 89 | end -= count 90 | 91 | # URL handling -> do not escape if in [](), escape otherwise. 92 | if ent.type == "url": 93 | if any(match.start(1) <= start and end <= match.end(1) for match in LINK_REGEX.finditer(txt)): 94 | continue 95 | # else, check the escapes between the prev and last and forcefully escape the url to avoid mangling 96 | else: 97 | # TODO: investigate possible offset bug when lots of emoji are present 98 | res += _selective_escape(txt[prev:start] or "") + escape_markdown(ent_text) 99 | 100 | # code handling 101 | elif ent.type == "code": 102 | res += _selective_escape(txt[prev:start]) + '`' + ent_text + '`' 103 | 104 | # handle markdown/html links 105 | elif ent.type == "text_link": 106 | res += _selective_escape(txt[prev:start]) + "[{}]({})".format(ent_text, ent.url) 107 | 108 | end += 1 109 | 110 | # anything else 111 | else: 112 | continue 113 | 114 | prev = end 115 | 116 | res += _selective_escape(txt[prev:]) # add the rest of the text 117 | return res 118 | 119 | 120 | def button_markdown_parser(txt: str, entities: Dict[MessageEntity, str] = None, offset: int = 0) -> (str, List): 121 | markdown_note = markdown_parser(txt, entities, offset) 122 | prev = 0 123 | note_data = "" 124 | buttons = [] 125 | for match in BTN_URL_REGEX.finditer(markdown_note): 126 | # Check if btnurl is escaped 127 | n_escapes = 0 128 | to_check = match.start(1) - 1 129 | while to_check > 0 and markdown_note[to_check] == "\\": 130 | n_escapes += 1 131 | to_check -= 1 132 | 133 | # if even, not escaped -> create button 134 | if n_escapes % 2 == 0: 135 | # create a thruple with button label, url, and newline status 136 | buttons.append((match.group(2), match.group(3), bool(match.group(4)))) 137 | note_data += markdown_note[prev:match.start(1)] 138 | prev = match.end(1) 139 | # if odd, escaped -> move along 140 | else: 141 | note_data += markdown_note[prev:to_check] 142 | prev = match.start(1) - 1 143 | else: 144 | note_data += markdown_note[prev:] 145 | 146 | return note_data, buttons 147 | 148 | 149 | def escape_invalid_curly_brackets(text: str, valids: List[str]) -> str: 150 | new_text = "" 151 | idx = 0 152 | while idx < len(text): 153 | if text[idx] == "{": 154 | if idx + 1 < len(text) and text[idx + 1] == "{": 155 | idx += 2 156 | new_text += "{{{{" 157 | continue 158 | else: 159 | success = False 160 | for v in valids: 161 | if text[idx:].startswith('{' + v + '}'): 162 | success = True 163 | break 164 | if success: 165 | new_text += text[idx: idx + len(v) + 2] 166 | idx += len(v) + 2 167 | continue 168 | else: 169 | new_text += "{{" 170 | 171 | elif text[idx] == "}": 172 | if idx + 1 < len(text) and text[idx + 1] == "}": 173 | idx += 2 174 | new_text += "}}}}" 175 | continue 176 | else: 177 | new_text += "}}" 178 | 179 | else: 180 | new_text += text[idx] 181 | idx += 1 182 | 183 | return new_text 184 | 185 | 186 | SMART_OPEN = '“' 187 | SMART_CLOSE = '”' 188 | START_CHAR = ('\'', '"', SMART_OPEN) 189 | 190 | 191 | def split_quotes(text: str) -> List: 192 | if any(text.startswith(char) for char in START_CHAR): 193 | counter = 1 # ignore first char -> is some kind of quote 194 | while counter < len(text): 195 | if text[counter] == "\\": 196 | counter += 1 197 | elif text[counter] == text[0] or (text[0] == SMART_OPEN and text[counter] == SMART_CLOSE): 198 | break 199 | counter += 1 200 | else: 201 | return text.split(None, 1) 202 | 203 | # 1 to avoid starting quote, and counter is exclusive so avoids ending 204 | key = remove_escapes(text[1:counter].strip()) 205 | # index will be in range, or `else` would have been executed and returned 206 | rest = text[counter + 1:].strip() 207 | if not key: 208 | key = text[0] + text[0] 209 | return list(filter(None, [key, rest])) 210 | else: 211 | return text.split(None, 1) 212 | 213 | 214 | def remove_escapes(text: str) -> str: 215 | counter = 0 216 | res = "" 217 | is_escaped = False 218 | while counter < len(text): 219 | if is_escaped: 220 | res += text[counter] 221 | is_escaped = False 222 | elif text[counter] == "\\": 223 | is_escaped = True 224 | else: 225 | res += text[counter] 226 | counter += 1 227 | return res 228 | 229 | 230 | def escape_chars(text: str, to_escape: List[str]) -> str: 231 | to_escape.append("\\") 232 | new_text = "" 233 | for x in text: 234 | if x in to_escape: 235 | new_text += "\\" 236 | new_text += x 237 | return new_text 238 | 239 | 240 | def extract_time(message, time_val): 241 | if any(time_val.endswith(unit) for unit in ('m', 'h', 'd')): 242 | unit = time_val[-1] 243 | time_num = time_val[:-1] # type: str 244 | if not time_num.isdigit(): 245 | message.reply_text("Invalid time amount specified.") 246 | return "" 247 | 248 | if unit == 'm': 249 | bantime = int(time.time() + int(time_num) * 60) 250 | elif unit == 'h': 251 | bantime = int(time.time() + int(time_num) * 60 * 60) 252 | elif unit == 'd': 253 | bantime = int(time.time() + int(time_num) * 24 * 60 * 60) 254 | else: 255 | # how even...? 256 | return "" 257 | return bantime 258 | else: 259 | message.reply_text("Invalid time type specified. Expected m,h, or d, got: {}".format(time_val[-1])) 260 | return "" 261 | 262 | 263 | def markdown_to_html(text): 264 | text = text.replace("*", "**") 265 | text = text.replace("`", "```") 266 | _html = markdown2.markdown(text) 267 | return bleach.clean(_html, tags=['strong', 'em', 'a', 'code', 'pre'], strip=True)[:-1] 268 | --------------------------------------------------------------------------------