├── runtime.txt ├── Procfile ├── lynda ├── modules │ ├── helper_funcs │ │ ├── __init__.py │ │ ├── telethn │ │ │ ├── __init__.py │ │ │ └── chatstatus.py │ │ ├── alternate.py │ │ ├── filters.py │ │ ├── handlers.py │ │ ├── misc.py │ │ ├── msg_types.py │ │ └── extraction.py │ ├── sql │ │ ├── __init__.py │ │ ├── last_fm_sql.py │ │ ├── rules_sql.py │ │ ├── blacklistusers_sql.py │ │ ├── afk_sql.py │ │ ├── userinfo_sql.py │ │ ├── chatbot_sql.py │ │ ├── log_channel_sql.py │ │ ├── rss_sql.py │ │ ├── antiflood_sql.py │ │ ├── reporting_sql.py │ │ ├── disable_sql.py │ │ ├── blacklist_sql.py │ │ ├── global_bans_sql.py │ │ ├── notes_sql.py │ │ ├── users_sql.py │ │ ├── blsticker_sql.py │ │ ├── cleaner_sql.py │ │ ├── connection_sql.py │ │ └── locks_sql.py │ ├── wiki.py │ ├── dev.py │ ├── __init__.py │ ├── paste.py │ ├── speed_test.py │ ├── special.py │ ├── purge.py │ ├── backups.py │ ├── lastfm.py │ ├── afk.py │ ├── users.py │ ├── rules.py │ ├── sed.py │ ├── blacklistusers.py │ ├── chatbot.py │ ├── fun.py │ ├── gtranslator.py │ ├── userinfo.py │ ├── modules.py │ ├── antiflood.py │ └── dbcleanup.py ├── elevated_users.json ├── memorize.py ├── lyn.py ├── sample_config.py └── __init__.py ├── .idea ├── .gitignore ├── misc.xml ├── vcs.xml ├── inspectionProfiles │ └── profiles_settings.xml ├── dictionaries │ └── Aman.xml ├── modules.xml └── LyndaRobot.iml ├── heroku.yml ├── .deepsource.toml ├── .vscode └── settings.json ├── requirements.txt ├── .github ├── FUNDING.yml └── workflows │ └── greetings.yml ├── CONTRIBUTING.md ├── Dockerfile ├── README.md ├── app.json.sample └── .gitignore /runtime.txt: -------------------------------------------------------------------------------- 1 | python-3.8.3 -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | worker: python3 -m lynda 2 | -------------------------------------------------------------------------------- /lynda/modules/helper_funcs/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | -------------------------------------------------------------------------------- /heroku.yml: -------------------------------------------------------------------------------- 1 | build: 2 | docker: 3 | worker: Dockerfile 4 | run: 5 | worker: python3 -m lynda 6 | -------------------------------------------------------------------------------- /.deepsource.toml: -------------------------------------------------------------------------------- 1 | version = 1 2 | 3 | [[analyzers]] 4 | name = "python" 5 | enabled = true 6 | 7 | [analyzers.meta] 8 | runtime_version = "3.x.x" 9 | -------------------------------------------------------------------------------- /lynda/elevated_users.json: -------------------------------------------------------------------------------- 1 | { 2 | "devs": [], 3 | "supports": [], 4 | "whitelists": [], 5 | "sudos": [], 6 | "sardegnas": [], 7 | "spammers": [] 8 | } -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/profiles_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | -------------------------------------------------------------------------------- /.idea/dictionaries/Aman.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | adminlist 5 | kaizoku 6 | manga 7 | spamfilters 8 | 9 | 10 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "python.pythonPath":"/usr/bin/python3", 3 | "python.linting.enabled": true, 4 | "python.testing.pytestArgs": [ 5 | "env" 6 | ], 7 | "python.testing.unittestEnabled": false, 8 | "python.testing.nosetestsEnabled": false, 9 | "python.testing.pytestEnabled": true 10 | 11 | } -------------------------------------------------------------------------------- /lynda/modules/helper_funcs/telethn/__init__.py: -------------------------------------------------------------------------------- 1 | from lynda import telethn, SUDO_USERS, WHITELIST_USERS, SUPPORT_USERS, SARDEGNA_USERS, DEV_USERS 2 | 3 | IMMUNE_USERS = SUDO_USERS + WHITELIST_USERS + SUPPORT_USERS + SARDEGNA_USERS + DEV_USERS 4 | 5 | IMMUNE_USERS = list(SUDO_USERS) + list(WHITELIST_USERS) + list(SUPPORT_USERS) + list(SARDEGNA_USERS) + list(DEV_USERS) -------------------------------------------------------------------------------- /lynda/modules/helper_funcs/alternate.py: -------------------------------------------------------------------------------- 1 | from telegram import error 2 | 3 | 4 | def send_message(message, text, *args, **kwargs): 5 | try: 6 | return message.reply_text(text, *args, **kwargs) 7 | except error.BadRequest as err: 8 | if str(err) == "Reply message not found": 9 | return message.reply_text(text, quote=False, *args, **kwargs) 10 | -------------------------------------------------------------------------------- /.idea/LyndaRobot.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 12 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | future 2 | emoji 3 | beautifulsoup4 4 | requests 5 | sqlalchemy 6 | python-telegram-bot==12.1.0 7 | psycopg2-binary 8 | PyLyrics 9 | feedparser 10 | pynewtonmath 11 | spongemock 12 | zalgo-text 13 | geopy 14 | nltk 15 | tswift 16 | psutil 17 | fontTools 18 | aiohttp>=2.2.5 19 | Pillow>=4.2.0 20 | CurrencyConverter 21 | googletrans 22 | jikanpy 23 | speedtest-cli 24 | coffeehouse 25 | typing 26 | gtts 27 | wikipedia 28 | pyowm 29 | animu-cf 30 | uptime 31 | psutil 32 | py-cpuinfo 33 | nekos.py 34 | spamwatch 35 | telethon -------------------------------------------------------------------------------- /lynda/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 lynda 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 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 13 | -------------------------------------------------------------------------------- /lynda/modules/wiki.py: -------------------------------------------------------------------------------- 1 | from typing import Optional, List 2 | from telegram import Update 3 | from telegram.ext import CallbackContext 4 | from lynda import dispatcher 5 | from lynda.modules.disable import DisableAbleCommandHandler 6 | import wikipedia 7 | 8 | 9 | def wiki(update: Update, context: CallbackContext): 10 | args = context.args 11 | reply = " ".join(args) 12 | summary = '{} {}' 13 | update.message.reply_text( 14 | summary.format( 15 | wikipedia.summary( 16 | reply, 17 | sentences=3), 18 | wikipedia.page(reply).url)) 19 | 20 | 21 | __help__ = """ 22 | -> `/wiki` text 23 | Returns search from wikipedia for the input text 24 | """ 25 | __mod_name__ = "Wikipedia" 26 | WIKI_HANDLER = DisableAbleCommandHandler("wiki", wiki, pass_args=True) 27 | dispatcher.add_handler(WIKI_HANDLER) 28 | -------------------------------------------------------------------------------- /lynda/modules/dev.py: -------------------------------------------------------------------------------- 1 | import os 2 | import subprocess 3 | import sys 4 | from time import sleep 5 | from typing import List 6 | 7 | from telegram import Update, TelegramError 8 | from telegram.ext import CommandHandler, run_async, CallbackContext 9 | 10 | from lynda import dispatcher 11 | from lynda.modules.helper_funcs.chat_status import dev_plus 12 | 13 | 14 | @run_async 15 | @dev_plus 16 | def leave(update: Update, context: CallbackContext): 17 | args = context.args 18 | message = update.effective_message 19 | if args: 20 | chat_id = str(args[0]) 21 | try: 22 | context.bot.leave_chat(int(chat_id)) 23 | message.reply_text( 24 | "Beep boop, I left that soup!.") 25 | except TelegramError: 26 | message.reply_text( 27 | "Beep boop, I could not leave that group(dunno why tho).") 28 | else: 29 | message.reply_text("Send a valid chat ID") 30 | 31 | 32 | LEAVE_HANDLER = CommandHandler("leave", leave, pass_args=True) 33 | 34 | dispatcher.add_handler(LEAVE_HANDLER) 35 | 36 | __mod_name__ = "Dev" 37 | __handlers__ = [LEAVE_HANDLER] 38 | -------------------------------------------------------------------------------- /lynda/modules/sql/last_fm_sql.py: -------------------------------------------------------------------------------- 1 | import threading 2 | 3 | from sqlalchemy import Column, String 4 | 5 | from lynda.modules.sql import BASE, SESSION 6 | 7 | 8 | class LastFMUsers(BASE): 9 | __tablename__ = "last_fm" 10 | user_id = Column(String(14), primary_key=True) 11 | username = Column(String(15)) 12 | 13 | def __init__(self, user_id, username): 14 | self.user_id = user_id 15 | self.username = username 16 | 17 | 18 | LastFMUsers.__table__.create(checkfirst=True) 19 | 20 | INSERTION_LOCK = threading.RLock() 21 | 22 | def set_user(user_id, username): 23 | with INSERTION_LOCK: 24 | user = SESSION.query(LastFMUsers).get(str(user_id)) 25 | if not user: 26 | user = LastFMUsers(str(user_id), str(username)) 27 | else: 28 | user.username = str(username) 29 | 30 | SESSION.add(user) 31 | SESSION.commit() 32 | 33 | 34 | def get_user(user_id): 35 | user = SESSION.query(LastFMUsers).get(str(user_id)) 36 | rep = "" 37 | if user: 38 | rep = str(user.username) 39 | 40 | SESSION.close() 41 | return rep -------------------------------------------------------------------------------- /lynda/memorize.py: -------------------------------------------------------------------------------- 1 | import time 2 | 3 | 4 | class MWT(object): 5 | """Memorize With Timeout""" 6 | 7 | _caches = {} 8 | _timeouts = {} 9 | 10 | def __init__(self, timeout=2): 11 | self.timeout = timeout 12 | 13 | def collect(self): 14 | """Clear cache of results which have timed out""" 15 | for func in self._caches: 16 | cache = {} 17 | for key in self._caches[func]: 18 | if (time.time() - 19 | self._caches[func][key][1]) < self._timeouts[func]: 20 | cache[key] = self._caches[func][key] 21 | self._caches[func] = cache 22 | 23 | def __call__(self, f): 24 | self.cache = self._caches[f] = {} 25 | self._timeouts[f] = self.timeout 26 | 27 | def func(*args, **kwargs): 28 | kw = sorted(kwargs.items()) 29 | key = (args, tuple(kw)) 30 | try: 31 | v = self.cache[key] 32 | # print("cache") 33 | if (time.time() - v[1]) > self.timeout: 34 | raise KeyError 35 | except KeyError: 36 | # print("new") 37 | v = self.cache[key] = f(*args, **kwargs), time.time() 38 | return v[0] 39 | 40 | func.func_name = f.__name__ 41 | 42 | return func -------------------------------------------------------------------------------- /lynda/modules/__init__.py: -------------------------------------------------------------------------------- 1 | from lynda 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 loadorder names. Quitting.") 18 | quit(1) 19 | 20 | all_modules = sorted(set(all_modules) - set(to_load)) 21 | to_load = list(all_modules) + to_load 22 | 23 | else: 24 | to_load = all_modules 25 | 26 | if NO_LOAD: 27 | LOGGER.info("Not loading: {}".format(NO_LOAD)) 28 | return [item for item in to_load if item not in NO_LOAD] 29 | 30 | return to_load 31 | 32 | return all_modules 33 | 34 | 35 | ALL_MODULES = __list_all_modules() 36 | LOGGER.info("Modules to load: %s", str(ALL_MODULES)) 37 | __all__ = ALL_MODULES + ["ALL_MODULES"] 38 | -------------------------------------------------------------------------------- /lynda/modules/paste.py: -------------------------------------------------------------------------------- 1 | import requests 2 | from telegram import Update, ParseMode 3 | from telegram.ext import run_async, CallbackContext 4 | 5 | from lynda import dispatcher 6 | from lynda.modules.disable import DisableAbleCommandHandler 7 | 8 | 9 | @run_async 10 | def paste(update: Update, context: CallbackContext): 11 | args = context.args 12 | message = update.effective_message 13 | 14 | if message.reply_to_message: 15 | data = message.reply_to_message.text 16 | 17 | elif len(args) >= 1: 18 | data = message.text.split(None, 1)[1] 19 | else: 20 | message.reply_text("What am I supposed to do with this?") 21 | return 22 | 23 | key = requests.post('https://nekobin.com/api/documents', 24 | json={"content": data}).json().get('result').get('key') 25 | url = f'https://nekobin.com/{key}' 26 | reply_text = f'Nekofied to *Nekobin* : {url}' 27 | message.reply_text( 28 | reply_text, 29 | parse_mode=ParseMode.MARKDOWN, 30 | disable_web_page_preview=True) 31 | 32 | 33 | __help__ = """ 34 | -> `/paste` 35 | Do a paste at `neko.bin` 36 | """ 37 | 38 | PASTE_HANDLER = DisableAbleCommandHandler("paste", paste, pass_args=True) 39 | dispatcher.add_handler(PASTE_HANDLER) 40 | 41 | __mod_name__ = "Paste" 42 | __command_list__ = ["paste"] 43 | __handlers__ = [PASTE_HANDLER] 44 | -------------------------------------------------------------------------------- /lynda/modules/helper_funcs/filters.py: -------------------------------------------------------------------------------- 1 | from telegram import Message 2 | from telegram.ext import BaseFilter 3 | 4 | from lynda import SUPPORT_USERS, SUDO_USERS, DEV_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 _Developers(BaseFilter): 21 | def filter(self, message: Message): 22 | return bool(message.from_user and message.from_user.id in DEV_USERS) 23 | 24 | dev_filter = _Developers() 25 | 26 | class _MimeType(BaseFilter): 27 | def __init__(self, mimetype): 28 | self.mime_type = mimetype 29 | self.name = "CustomFilters.mime_type({})".format(self.mime_type) 30 | 31 | def filter(self, message: Message): 32 | return bool(message.document and message.document.mime_type == self.mime_type) 33 | 34 | mime_type = _MimeType 35 | 36 | class _HasText(BaseFilter): 37 | def filter(self, message: Message): 38 | return bool(message.text or message.sticker or message.photo or message.document or message.video) 39 | 40 | has_text = _HasText() 41 | -------------------------------------------------------------------------------- /lynda/modules/sql/rules_sql.py: -------------------------------------------------------------------------------- 1 | import threading 2 | 3 | from sqlalchemy import Column, String, UnicodeText, func, distinct 4 | 5 | from lynda.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 (in `@YorktownEagleUnion`)! 28 | -------------------------------------------------------------------------------- /lynda/lyn.py: -------------------------------------------------------------------------------- 1 | from telethon import events 2 | from lynda import telethn 3 | 4 | """Triggers start command in pm and in groupchats""" 5 | def lyndabot(**args): 6 | """New message.""" 7 | pattern = args.get('pattern', None) 8 | r_pattern = r'^[/!]' 9 | if pattern is not None and not pattern.startswith('(?i)'): 10 | args['pattern'] = '(?i)' + pattern 11 | args['pattern'] = pattern.replace('^/', r_pattern, 1) 12 | 13 | def decorator(func): 14 | telethn.add_event_handler(func, events.NewMessage(**args)) 15 | return func 16 | 17 | return decorator 18 | 19 | 20 | def inlinequery(**args): 21 | """Inline query.""" 22 | pattern = args.get('pattern', None) 23 | if pattern is not None and not pattern.startswith('(?i)'): 24 | args['pattern'] = '(?i)' + pattern 25 | 26 | def decorator(func): 27 | telethn.add_event_handler(func, events.InlineQuery(**args)) 28 | return func 29 | 30 | return decorator 31 | 32 | 33 | def userupdate(**args): 34 | """User updates.""" 35 | 36 | def decorator(func): 37 | telethn.add_event_handler(func, events.UserUpdate(**args)) 38 | return func 39 | 40 | return decorator 41 | 42 | 43 | def callbackquery(**args): 44 | """Callback query.""" 45 | 46 | def decorator(func): 47 | telethn.add_event_handler(func, events.CallbackQuery(**args)) 48 | return func 49 | 50 | return decorator 51 | 52 | 53 | def chataction(**args): 54 | """Chat actions.""" 55 | 56 | def decorator(func): 57 | telethn.add_event_handler(func, events.ChatAction(**args)) 58 | return func 59 | 60 | return decorator 61 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # We're using Debian Slim Buster image 2 | FROM python:3.8-slim-buster 3 | 4 | ENV PIP_NO_CACHE_DIR 1 5 | 6 | RUN sed -i.bak 's/us-west-2\.ec2\.//' /etc/apt/sources.list 7 | 8 | # Installing Required Packages 9 | RUN apt update && apt upgrade -y && \ 10 | apt install --no-install-recommends -y \ 11 | debian-keyring \ 12 | debian-archive-keyring \ 13 | bash \ 14 | bzip2 \ 15 | curl \ 16 | figlet \ 17 | gcc \ 18 | git \ 19 | sudo \ 20 | util-linux \ 21 | libffi-dev \ 22 | libjpeg-dev \ 23 | libjpeg62-turbo-dev \ 24 | libwebp-dev \ 25 | linux-headers-amd64 \ 26 | musl-dev \ 27 | musl \ 28 | neofetch \ 29 | php-pgsql \ 30 | python3-lxml \ 31 | postgresql \ 32 | postgresql-client \ 33 | python3-psycopg2 \ 34 | libpq-dev \ 35 | libcurl4-openssl-dev \ 36 | libxml2-dev \ 37 | libxslt1-dev \ 38 | python3-pip \ 39 | python3-requests \ 40 | python3-sqlalchemy \ 41 | python3-tz \ 42 | python3-aiohttp \ 43 | openssl \ 44 | pv \ 45 | jq \ 46 | wget \ 47 | python3 \ 48 | python3-dev \ 49 | libreadline-dev \ 50 | libyaml-dev \ 51 | sqlite3 \ 52 | libsqlite3-dev \ 53 | zlib1g \ 54 | libssl-dev \ 55 | libopus0 \ 56 | libopus-dev \ 57 | && rm -rf /var/lib/apt/lists /var/cache/apt/archives /tmp 58 | 59 | # Pypi package Repo upgrade 60 | RUN pip3 install --upgrade pip setuptools 61 | 62 | # Copy Python Requirements to /root/LyndaRobot 63 | RUN git clone https://github.com/pokurt/LyndaRobot.git /root/LyndaRobot 64 | WORKDIR /root/LyndaRobot 65 | 66 | ENV PATH="/home/lynda/bin:$PATH" 67 | 68 | # Install requirements 69 | RUN sudo pip3 install -U -r requirements.txt 70 | 71 | # Starting Worker 72 | CMD ["python3","-m","lynda"] 73 | -------------------------------------------------------------------------------- /lynda/modules/sql/blacklistusers_sql.py: -------------------------------------------------------------------------------- 1 | import threading 2 | 3 | from sqlalchemy import Column, String, UnicodeText 4 | 5 | from lynda.modules.sql import BASE, SESSION 6 | 7 | 8 | class BlacklistUsers(BASE): 9 | __tablename__ = "blacklistusers" 10 | user_id = Column(String(14), primary_key=True) 11 | reason = Column(UnicodeText) 12 | 13 | def __init__(self, user_id, reason=None): 14 | self.user_id = user_id 15 | self.reason = reason 16 | 17 | 18 | BlacklistUsers.__table__.create(checkfirst=True) 19 | 20 | BLACKLIST_LOCK = threading.RLock() 21 | BLACKLIST_USERS = set() 22 | 23 | 24 | def blacklist_user(user_id, reason=None): 25 | with BLACKLIST_LOCK: 26 | user = SESSION.query(BlacklistUsers).get(str(user_id)) 27 | if not user: 28 | user = BlacklistUsers(str(user_id), reason) 29 | else: 30 | user.reason = reason 31 | 32 | SESSION.add(user) 33 | SESSION.commit() 34 | __load_blacklist_userid_list() 35 | 36 | 37 | def unblacklist_user(user_id): 38 | with BLACKLIST_LOCK: 39 | user = SESSION.query(BlacklistUsers).get(str(user_id)) 40 | if user: 41 | SESSION.delete(user) 42 | 43 | SESSION.commit() 44 | __load_blacklist_userid_list() 45 | 46 | 47 | def get_reason(user_id): 48 | user = SESSION.query(BlacklistUsers).get(str(user_id)) 49 | rep = "" 50 | if user: 51 | rep = user.reason 52 | 53 | SESSION.close() 54 | return rep 55 | 56 | 57 | def is_user_blacklisted(user_id): 58 | return user_id in BLACKLIST_USERS 59 | 60 | 61 | def __load_blacklist_userid_list(): 62 | global BLACKLIST_USERS 63 | try: 64 | BLACKLIST_USERS = {int(x.user_id) for x in SESSION.query(BlacklistUsers).all()} 65 | finally: 66 | SESSION.close() 67 | 68 | 69 | __load_blacklist_userid_list() 70 | -------------------------------------------------------------------------------- /.github/workflows/greetings.yml: -------------------------------------------------------------------------------- 1 | name: Greetings 2 | 3 | on: [pull_request, issues] 4 | 5 | jobs: 6 | review-app-test: 7 | 8 | runs-on: ubuntu-latest 9 | 10 | steps: 11 | - name: Run review-app test 12 | id: review_app_test # `id` value is used to refer the outputs from the corresponding action 13 | uses: niteoweb/reviewapps-deploy-status@v1.3.0 14 | env: 15 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 16 | with: 17 | # Checks to be performed, default is all the checks 18 | checks: build, response 19 | 20 | # Delay for the application to be built in Heroku, default is 5 seconds 21 | build_time_delay: 5 22 | 23 | # Delay for the application to load and start serving, default is 5 seconds 24 | load_time_delay: 5 25 | 26 | # Interval for the repeating checks, default is 10 seconds 27 | interval: 10 28 | 29 | # Acceptable responses for the response check, default is 200 30 | accepted_responses: 200 31 | 32 | # Max time to be spent retrying for the build check, default is 120 33 | deployments_timeout: 120 34 | 35 | # Max time to be spent retrying for the response check, default is 120 36 | publish_timeout: 120 37 | 38 | # `steps.review_app_test.outputs.review_app_url` must be used in workflow to fetch the Review App URL 39 | - name: Check review_app_url 40 | run: | 41 | echo "Outputs - ${{ steps.review_app_test.outputs.review_app_url }}": 42 | 43 | greeting: 44 | runs-on: ubuntu-latest 45 | steps: 46 | - uses: actions/first-interaction@v1 47 | with: 48 | repo-token: ${{ secrets.GITHUB_TOKEN }} 49 | issue-message: 'Hi, welcome to Kigyōbot and its repo, as this is your first issuse here we suggest you visit us on telegrm @YorktownEagleUnion for faster resolutions and urgent reports' 50 | pr-message: 'Hi, welcome to your first PR here, we will catch up with you shortly.' 51 | -------------------------------------------------------------------------------- /lynda/modules/sql/afk_sql.py: -------------------------------------------------------------------------------- 1 | import threading 2 | 3 | from sqlalchemy import Column, UnicodeText, Boolean, Integer 4 | 5 | from lynda.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 | if user_id in AFK_USERS: 36 | return True, AFK_USERS[user_id] 37 | return False, "" 38 | 39 | 40 | def set_afk(user_id, reason=""): 41 | with INSERTION_LOCK: 42 | curr = SESSION.query(AFK).get(user_id) 43 | if not curr: 44 | curr = AFK(user_id, reason, True) 45 | else: 46 | curr.is_afk = True 47 | curr.reason = reason 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 __load_afk_users(): 71 | global AFK_USERS 72 | try: 73 | all_afk = SESSION.query(AFK).all() 74 | AFK_USERS = {user.user_id: user.reason for user in all_afk if user.is_afk} 75 | finally: 76 | SESSION.close() 77 | 78 | 79 | __load_afk_users() 80 | -------------------------------------------------------------------------------- /lynda/modules/sql/userinfo_sql.py: -------------------------------------------------------------------------------- 1 | import threading 2 | 3 | from sqlalchemy import Column, Integer, UnicodeText 4 | 5 | from lynda.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 | -------------------------------------------------------------------------------- /lynda/modules/sql/chatbot_sql.py: -------------------------------------------------------------------------------- 1 | import threading 2 | 3 | from sqlalchemy import Column, String 4 | 5 | from lynda.modules.sql import BASE, SESSION 6 | 7 | 8 | class ChatbotChats(BASE): 9 | __tablename__ = "chatbot_chats" 10 | chat_id = Column(String(14), primary_key=True) 11 | ses_id = Column(String(70)) 12 | expires = Column(String(15)) 13 | 14 | def __init__(self, chat_id, ses_id, expires): 15 | self.chat_id = chat_id 16 | self.ses_id = ses_id 17 | self.expires = expires 18 | 19 | 20 | ChatbotChats.__table__.create(checkfirst=True) 21 | 22 | INSERTION_LOCK = threading.RLock() 23 | 24 | 25 | def is_chat(chat_id): 26 | try: 27 | chat = SESSION.query(ChatbotChats).get(str(chat_id)) 28 | if chat: 29 | return True 30 | else: 31 | return False 32 | finally: 33 | SESSION.close() 34 | 35 | 36 | def set_ses(chat_id, ses_id, expires): 37 | with INSERTION_LOCK: 38 | autochat = SESSION.query(ChatbotChats).get(str(chat_id)) 39 | if not autochat: 40 | autochat = ChatbotChats(str(chat_id), str(ses_id), str(expires)) 41 | else: 42 | autochat.ses_id = str(ses_id) 43 | autochat.expires = str(expires) 44 | 45 | SESSION.add(autochat) 46 | SESSION.commit() 47 | 48 | 49 | def get_ses(chat_id): 50 | autochat = SESSION.query(ChatbotChats).get(str(chat_id)) 51 | sesh = "" 52 | exp = "" 53 | if autochat: 54 | sesh = str(autochat.ses_id) 55 | exp = str(autochat.expires) 56 | 57 | SESSION.close() 58 | return sesh, exp 59 | 60 | 61 | def rem_chat(chat_id): 62 | with INSERTION_LOCK: 63 | autochat = SESSION.query(ChatbotChats).get(str(chat_id)) 64 | if autochat: 65 | SESSION.delete(autochat) 66 | 67 | SESSION.commit() 68 | 69 | def get_all_chats(): 70 | try: 71 | return SESSION.query(ChatbotChats.chat_id).all() 72 | finally: 73 | SESSION.close() -------------------------------------------------------------------------------- /lynda/modules/sql/log_channel_sql.py: -------------------------------------------------------------------------------- 1 | import threading 2 | 3 | from sqlalchemy import Column, String, func, distinct 4 | 5 | from lynda.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 | -------------------------------------------------------------------------------- /lynda/modules/speed_test.py: -------------------------------------------------------------------------------- 1 | import speedtest 2 | from telegram import Update, ParseMode, InlineKeyboardMarkup, InlineKeyboardButton 3 | from telegram.ext import run_async, CallbackQueryHandler 4 | 5 | from lynda import dispatcher, DEV_USERS 6 | from lynda.modules.disable import DisableAbleCommandHandler 7 | from lynda.modules.helper_funcs.chat_status import dev_plus 8 | 9 | 10 | def convert(speed): 11 | return round(int(speed) / 1048576, 2) 12 | 13 | 14 | @dev_plus 15 | @run_async 16 | def speedtestxyz(update: Update, _): 17 | buttons = [[InlineKeyboardButton("Image", 18 | callback_data="speedtest_image"), 19 | InlineKeyboardButton("Text", 20 | callback_data="speedtest_text")]] 21 | update.effective_message.reply_text( 22 | "Select SpeedTest Mode", 23 | reply_markup=InlineKeyboardMarkup(buttons)) 24 | 25 | 26 | @run_async 27 | def speedtestxyz_callback(update: Update, _): 28 | query = update.callback_query 29 | 30 | if query.from_user.id in DEV_USERS: 31 | msg = update.effective_message.edit_text('Runing a speedtest....') 32 | speed = speedtest.Speedtest() 33 | speed.get_best_server() 34 | speed.download() 35 | speed.upload() 36 | replymsg = 'SpeedTest Results:' 37 | 38 | if query.data == 'speedtest_image': 39 | speedtest_image = speed.results.share() 40 | update.effective_message.reply_photo( 41 | photo=speedtest_image, caption=replymsg) 42 | msg.delete() 43 | 44 | elif query.data == 'speedtest_text': 45 | result = speed.results.dict() 46 | replymsg += f"\nDownload: `{convert(result['download'])}Mb/s`\nUpload: `{convert(result['upload'])}Mb/s`\nPing: `{result['ping']}`" 47 | update.effective_message.edit_text( 48 | replymsg, parse_mode=ParseMode.MARKDOWN) 49 | else: 50 | query.answer( 51 | "You are required to join Eagle Union to use this command.") 52 | 53 | 54 | SPEED_TEST_HANDLER = DisableAbleCommandHandler("speedtest", speedtestxyz) 55 | SPEED_TEST_CALLBACKHANDLER = CallbackQueryHandler( 56 | speedtestxyz_callback, pattern='speedtest_.*') 57 | 58 | dispatcher.add_handler(SPEED_TEST_HANDLER) 59 | dispatcher.add_handler(SPEED_TEST_CALLBACKHANDLER) 60 | 61 | __mod_name__ = "SpeedTest" 62 | __command_list__ = ["speedtest"] 63 | __handlers__ = [SPEED_TEST_HANDLER, SPEED_TEST_CALLBACKHANDLER] 64 | -------------------------------------------------------------------------------- /lynda/modules/sql/rss_sql.py: -------------------------------------------------------------------------------- 1 | import threading 2 | 3 | from sqlalchemy import Column, UnicodeText, Integer 4 | 5 | from lynda.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 | -------------------------------------------------------------------------------- /lynda/modules/special.py: -------------------------------------------------------------------------------- 1 | from time import sleep 2 | from typing import Optional, List 3 | from telegram import TelegramError 4 | from telegram import Update 5 | from telegram.error import BadRequest 6 | from telegram.ext import Filters, CommandHandler 7 | from telegram.ext.dispatcher import run_async, CallbackContext 8 | 9 | import random 10 | import lynda.modules.sql.users_sql as sql 11 | from lynda.modules.helper_funcs.filters import CustomFilters 12 | from lynda import dispatcher, OWNER_ID, LOGGER 13 | from lynda.modules.disable import DisableAbleCommandHandler 14 | USERS_GROUP = 4 15 | 16 | @run_async 17 | def banall(update: Update, context: CallbackContext): 18 | args = context.args 19 | bot = context.bot 20 | chat_id = str(args[0]) if args else str(update.effective_chat.id) 21 | all_mems = sql.get_chat_members(chat_id) 22 | for mems in all_mems: 23 | try: 24 | bot.kick_chat_member(chat_id, mems.user) 25 | update.effective_message.reply_text( 26 | "Tried banning " + str(mems.user)) 27 | sleep(0.1) 28 | except BadRequest as excp: 29 | update.effective_message.reply_text( 30 | excp.message + " " + str(mems.user)) 31 | continue 32 | 33 | 34 | @run_async 35 | def snipe(update: Update, context: CallbackContext): 36 | args = context.args 37 | bot = context.bot 38 | try: 39 | chat_id = str(args[0]) 40 | del args[0] 41 | except TypeError: 42 | update.effective_message.reply_text( 43 | "Please give me a chat to echo to!") 44 | to_send = " ".join(args) 45 | if len(to_send) >= 2: 46 | try: 47 | bot.sendMessage(int(chat_id), str(to_send)) 48 | except TelegramError: 49 | LOGGER.warning("Couldn't send to group %s", str(chat_id)) 50 | update.effective_message.reply_text( 51 | "Couldn't send the message. Perhaps I'm not part of that group?") 52 | 53 | 54 | __help__ = """ 55 | ──「 *Owner only:* 」── 56 | -> /banall 57 | Ban all members from a chat 58 | 59 | ──「 *Sudo only:* 」── 60 | -> /snipe 61 | Make me send a message to a specific chat. 62 | """ 63 | 64 | __mod_name__ = "Special" 65 | 66 | SNIPE_HANDLER = CommandHandler( 67 | "snipe", 68 | snipe, 69 | pass_args=True, 70 | filters=CustomFilters.sudo_filter) 71 | BANALL_HANDLER = CommandHandler( 72 | "banall", 73 | banall, 74 | pass_args=True, 75 | filters=Filters.user(OWNER_ID)) 76 | 77 | dispatcher.add_handler(SNIPE_HANDLER) 78 | dispatcher.add_handler(BANALL_HANDLER) 79 | -------------------------------------------------------------------------------- /lynda/modules/purge.py: -------------------------------------------------------------------------------- 1 | from asyncio import sleep 2 | from lynda.modules.helper_funcs.telethn.chatstatus import user_is_admin 3 | from lynda.modules.helper_funcs.telethn.chatstatus import can_delete_messages 4 | from lynda.lyn import lyndabot 5 | 6 | 7 | @lyndabot(pattern="^/purge") 8 | async def purge_messages(event): 9 | if event.from_id is None: 10 | return 11 | 12 | if not await user_is_admin(user_id=event.from_id, message=event): 13 | await event.reply("Only Admins are allowed to use this command") 14 | return 15 | 16 | if not await can_delete_messages(message=event): 17 | await event.reply("Can't seem to purge the message") 18 | return 19 | 20 | message = await event.get_reply_message() 21 | if not message: 22 | await event.reply("Reply to a message to select where to start purging from.") 23 | return 24 | messages = [] 25 | message_id = message.id 26 | delete_to = event.message.id - 1 27 | await event.client.delete_messages(event.chat_id, event.message.id) 28 | 29 | messages.append(event.reply_to_msg_id) 30 | for message_id in range(delete_to, message_id - 1, -1): 31 | messages.append(message_id) 32 | if len(messages) == 100: 33 | await event.client.delete_messages(event.chat_id, messages) 34 | messages = [] 35 | 36 | message_count = len(messages) 37 | await event.client.delete_messages(event.chat_id, messages) 38 | msg = await event.reply(f"Purged {message_count} messages successfully!", parse_mode='markdown') 39 | await sleep(5) 40 | await msg.delete() 41 | 42 | 43 | @lyndabot(pattern="^/del$") 44 | async def delete_messages(event): 45 | if event.from_id is None: 46 | return 47 | 48 | if not await user_is_admin(user_id=event.from_id, message=event): 49 | await event.reply("Only Admins are allowed to use this command") 50 | return 51 | 52 | if not await can_delete_messages(message=event): 53 | await event.reply("Can't seem to delete this?") 54 | return 55 | 56 | message = await event.get_reply_message() 57 | if not message: 58 | await event.reply("Whadya want to delete?") 59 | return 60 | chat = await event.get_input_chat() 61 | del_message = [message, event.message] 62 | await event.client.delete_messages(chat, del_message) 63 | 64 | 65 | __help__ = """ 66 | ──「 *Admin only:* 」── 67 | -> `/del` 68 | deletes the message you replied to 69 | -> `/purge` 70 | deletes all messages between this and the replied to message. 71 | -> `/purge` 72 | deletes the replied message, and X messages following it if replied to a message. 73 | """ 74 | 75 | __mod_name__ = "Purges" 76 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Lynda](https://img4.goodfon.com/original/2120x1348/3/58/ayanami-azur-lane-art-anime-devushka-strela.jpg) 2 | # LyndaRobot 3 | [![GPLv3 license](https://img.shields.io/badge/License-GPLv3-blue.svg)](http://perso.crans.org/besson/LICENSE.html) [![DeepSource](https://static.deepsource.io/deepsource-badge-light-mini.svg)](https://deepsource.io/gh/pokurt/LyndaRobot/?ref=repository-badge) 4 | [![Codacy Badge](https://app.codacy.com/project/badge/Grade/41ee9ac813a34042925a6b6fa92cf84e)](https://www.codacy.com?utm_source=github.com&utm_medium=referral&utm_content=AmaanAhmed/Lynda&utm_campaign=Badge_Grade) [![Join Support!](https://img.shields.io/badge/Join%20Channel-!-red)](https://t.me/LyndaEagleSupport) [![Open Source Love](https://badges.frapsoft.com/os/v2/open-source.png?v=103)](https://github.com/ellerbrock/open-source-badges/) [![Maintenance](https://img.shields.io/badge/Maintained%3F-yes-green.svg)](https://GitHub.com/Naereen/StrapDown.js/graphs/commit-activity) [![GitHub contributors](https://img.shields.io/github/contributors/Naereen/StrapDown.js.svg)](https://GitHub.com/pokurt/LyndaRobot/graphs/contributors/) 5 | 6 | A modular telegram Python bot running on python3 with an sqlalchemy database. 7 | 8 | Originally a [Kigyō](https://t.me/kigyorobot) fork - Lynda has evolved further and was built to be more useful for Anime Chats. 9 | 10 | Can be found on telegram as [Lynda](https://t.me/LyndaRobot). 11 | 12 | The Support group can be reached out to at [Eagle Union](https://t.me/YorktownEagleUnion), where you can ask for help setting up your bot, discover/request new features, report bugs, and stay in the loop whenever a new update is available. 13 | 14 | ## Credits 15 | The bot is based of on the original work done by [PaulSonOfLars](https://github.com/PaulSonOfLars) 16 | This repo was just reamped to suit an Anime-centric community. All original credits go to Paul and his dedication, Without his efforts, this fork would not have been possible! 17 | 18 | Most modules including Blacklists, Lyrics and much more are taken from [TheRealPhoenixBot](https://t.me/TheRealPhoenixBot) 19 | 20 | Thank you for contributing with me in this Project: 21 | + [TheRealPhoenix](https://github.com/rsktg) 22 | + [DragSama](https://github.com/DragSama) 23 | + [TsunayoshiSawada](https://github.com/TsunayoshiSawada) 24 | + [Athphane](https://github.com/athphane) 25 | + [Dank-del](https://github.com/Dank-del) 26 | 27 | Any other authorship/credits can be seen through the commits. 28 | 29 | [![forthebadge made-with-python](http://ForTheBadge.com/images/badges/made-with-python.svg)](https://www.python.org/) 30 | 31 | Should any be missing kindly let us know at [Eagle Union](https://t.me/YorktownEagleUnion) or simply submit a pull request on the readme. 32 | -------------------------------------------------------------------------------- /lynda/modules/sql/antiflood_sql.py: -------------------------------------------------------------------------------- 1 | import threading 2 | 3 | from sqlalchemy import Column, Integer, String 4 | 5 | from lynda.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 | -------------------------------------------------------------------------------- /lynda/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 lynda.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 | -------------------------------------------------------------------------------- /lynda/sample_config.py: -------------------------------------------------------------------------------- 1 | # Create a new config.py or rename this to config.py file in same dir and import, then extend this class. 2 | import json 3 | import os 4 | 5 | 6 | def get_user_list(config, key): 7 | with open('{}/lynda/{}'.format(os.getcwd(), config), 'r') as json_file: 8 | return json.load(json_file)[key] 9 | 10 | 11 | # Create a new config.py or rename this to config.py file in same dir and import, then extend this class. 12 | class Config(object): 13 | LOGGER = True 14 | 15 | # REQUIRED 16 | TOKEN = "" # Bot Token 17 | API_ID = "" # Your api id 18 | API_HASH = "" # Your api hash 19 | SW_API = "" # Spamwatch Api 20 | OWNER_ID = "" # If you dont know, run the bot and do /id in your private chat with it 21 | OWNER_USERNAME = "" 22 | 23 | # RECOMMENDED 24 | SQLALCHEMY_DATABASE_URI = 'sqldbtype://username:pw@hostname:port/db_name' # needed for any database modules 25 | MESSAGE_DUMP = None # needed to make sure 'save from' messages persist 26 | GBAN_LOGS = None # Channel ID here with - 27 | LOAD = [] 28 | NO_LOAD = ['translation', 'rss'] 29 | WEBHOOK = False 30 | URL = None 31 | 32 | # OPTIONAL 33 | # ID Seperation format [1,2,3,4] 34 | SUDO_USERS = get_user_list('elevated_users.json', 35 | 'sudos') # List of id's - (not usernames) for users which have sudo access to the bot. 36 | DEV_USERS = get_user_list('elevated_users.json', 37 | 'devs') # List of id's - (not usernames) for developers who will have the same perms as the owner 38 | SUPPORT_USERS = get_user_list('elevated_users.json', 39 | 'supports') # List of id's (not usernames) for users which are allowed to gban, but can also be banned. 40 | WHITELIST_USERS = get_user_list('elevated_users.json', 41 | 'whitelists') # List of id's (not usernames) for users which WONT be banned/kicked by the bot. 42 | DONATION_LINK = None # EG, paypal 43 | CERT_PATH = None 44 | PORT = 5000 45 | DEL_CMDS = False # Delete commands that users dont have access to, like delete /ban if a non admin uses it. 46 | STRICT_GBAN = False 47 | WORKERS = 8 # Number of subthreads to use. Set as number of threads your processor uses 48 | BAN_STICKER = 'CAADAgADOwADPPEcAXkko5EB3YGYAg' # banhammer marie sticker 49 | ALLOW_EXCL = False # Allow ! commands as well as / 50 | CASH_API_KEY = None # Get one from https://www.alphavantage.co/support/#api-key 51 | TIME_API_KEY = None # Get one from https://timezonedb.com/register 52 | AI_API_KEY = None # Coffeehouse chatbot api key, get one from https://coffeehouse.intellivoid.info/ 53 | WALL_API = None # Get one from https://wall.alphacoders.com/api.php 54 | LASTFM_API_KEY = None # Get one from https://last.fm/api/ 55 | DEEPFRY_TOKEN = None 56 | API_WEATHER = None 57 | 58 | 59 | class Production(Config): 60 | LOGGER = True 61 | 62 | 63 | class Development(Config): 64 | LOGGER = True 65 | -------------------------------------------------------------------------------- /lynda/modules/helper_funcs/telethn/chatstatus.py: -------------------------------------------------------------------------------- 1 | from telethon.tl.types import ChannelParticipantsAdmins 2 | 3 | from lynda.modules.helper_funcs.telethn import IMMUNE_USERS, telethn 4 | 5 | async def user_is_ban_protected(user_id: int, message): 6 | status = False 7 | if message.is_private or user_id in (IMMUNE_USERS): 8 | return True 9 | 10 | async for user in telethn.iter_participants(message.chat_id, filter=ChannelParticipantsAdmins): 11 | if user_id == user.id: 12 | status = True 13 | break 14 | return status 15 | 16 | 17 | async def user_is_admin(user_id: int, message): 18 | status = False 19 | if message.is_private: 20 | return True 21 | 22 | async for user in telethn.iter_participants(message.chat_id, filter=ChannelParticipantsAdmins): 23 | if user_id == user.id or user_id in IMMUNE_USERS: 24 | status = True 25 | break 26 | return status 27 | 28 | 29 | async def is_user_admin(user_id: int, chat_id): 30 | status = False 31 | async for user in telethn.iter_participants(chat_id, filter=ChannelParticipantsAdmins): 32 | if user_id == user.id or user_id in IMMUNE_USERS: 33 | status = True 34 | break 35 | return status 36 | 37 | 38 | async def haruka_is_admin(chat_id: int): 39 | status = False 40 | haruka = await telethn.get_me() 41 | async for user in telethn.iter_participants(chat_id, 42 | filter=ChannelParticipantsAdmins): 43 | if haruka.id == user.id: 44 | status = True 45 | break 46 | return status 47 | 48 | 49 | async def is_user_in_chat(chat_id: int, user_id: int): 50 | status = False 51 | async for user in telethn.iter_participants(chat_id): 52 | if user_id == user.id: 53 | status = True 54 | break 55 | return status 56 | 57 | 58 | async def can_change_info(message): 59 | status = False 60 | if message.chat.admin_rights: 61 | status = message.chat.admin_rights.change_info 62 | return status 63 | 64 | 65 | async def can_ban_users(message): 66 | status = False 67 | if message.chat.admin_rights: 68 | status = message.chat.admin_rights.ban_users 69 | return status 70 | 71 | 72 | async def can_pin_messages(message): 73 | status = False 74 | if message.chat.admin_rights: 75 | status = message.chat.admin_rights.pin_messages 76 | return status 77 | 78 | 79 | async def can_invite_users(message): 80 | status = False 81 | if message.chat.admin_rights: 82 | status = message.chat.admin_rights.invite_users 83 | return status 84 | 85 | 86 | async def can_add_admins(message): 87 | status = False 88 | if message.chat.admin_rights: 89 | status = message.chat.admin_rights.add_admins 90 | return status 91 | 92 | 93 | async def can_delete_messages(message): 94 | status = False 95 | if message.chat.admin_rights: 96 | status = message.chat.admin_rights.delete_messages 97 | return status -------------------------------------------------------------------------------- /lynda/modules/sql/disable_sql.py: -------------------------------------------------------------------------------- 1 | import threading 2 | 3 | from sqlalchemy import Column, String, UnicodeText, func, distinct 4 | 5 | from lynda.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 str(cmd).lower() 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 | -------------------------------------------------------------------------------- /lynda/modules/backups.py: -------------------------------------------------------------------------------- 1 | import json 2 | from io import BytesIO 3 | 4 | from telegram import Update 5 | from telegram.error import BadRequest 6 | from telegram.ext import CommandHandler, run_async, CallbackContext 7 | 8 | from lynda import dispatcher, LOGGER 9 | from lynda.__main__ import DATA_IMPORT 10 | from lynda.modules.helper_funcs.chat_status import user_admin 11 | 12 | 13 | @run_async 14 | @user_admin 15 | def import_data(update: Update, context: CallbackContext): 16 | msg = update.effective_message 17 | chat = update.effective_chat 18 | # TODO: allow uploading doc with command, not just as reply 19 | # only work with a doc 20 | if msg.reply_to_message and msg.reply_to_message.document: 21 | try: 22 | file_info = context.bot.get_file(msg.reply_to_message.document.file_id) 23 | except BadRequest: 24 | msg.reply_text( 25 | "Try downloading and reuploading the file as yourself before importing - this one seems " 26 | "to be iffy!") 27 | return 28 | 29 | with BytesIO() as file: 30 | file_info.download(out=file) 31 | file.seek(0) 32 | data = json.load(file) 33 | 34 | # only import one group 35 | if len(data) > 1 and str(chat.id) not in data: 36 | msg.reply_text( 37 | "Theres more than one group here in this file, and none have the same chat id as this group " 38 | "- how do I choose what to import?") 39 | return 40 | 41 | # Select data source 42 | if str(chat.id) in data: 43 | data = data[str(chat.id)]['hashes'] 44 | else: 45 | data = data[list(data.keys())[0]]['hashes'] 46 | 47 | try: 48 | for mod in DATA_IMPORT: 49 | mod.__import_data__(str(chat.id), data) 50 | except Exception: 51 | msg.reply_text( 52 | "An exception occured while restoring your data. The process may not be complete. If " 53 | "you're having issues with this, message @Aman_Ahmed with your backup file so the " 54 | "issue can be debugged. My owners would be happy to help, and every bug " 55 | "reported makes me better! Thanks! :)") 56 | LOGGER.exception( 57 | "Import for chatid %s with name %s failed.", str( 58 | chat.id), str( 59 | chat.title)) 60 | return 61 | 62 | # TODO: some of that link logic 63 | # NOTE: consider default permissions stuff? 64 | msg.reply_text("Backup fully imported. Welcome back! :D") 65 | 66 | 67 | @run_async 68 | @user_admin 69 | def export_data(update: Update, _): 70 | msg = update.effective_message 71 | msg.reply_text("Doesn't work yet.") 72 | 73 | 74 | __help__ = """ 75 | ──「 *Admin only:* 」── 76 | -> `/import` 77 | reply to a group butler backup file to import as much as possible, making the transfer super simple! Note \ 78 | that files/photos can't be imported due to telegram restrictions. 79 | -> `/export` 80 | This isn't a command yet, but should be coming soon! 81 | """ 82 | 83 | IMPORT_HANDLER = CommandHandler("import", import_data) 84 | EXPORT_HANDLER = CommandHandler("export", export_data) 85 | 86 | dispatcher.add_handler(IMPORT_HANDLER) 87 | dispatcher.add_handler(EXPORT_HANDLER) 88 | 89 | __mod_name__ = "Backups" 90 | __handlers__ = [IMPORT_HANDLER, EXPORT_HANDLER] 91 | -------------------------------------------------------------------------------- /lynda/modules/sql/blacklist_sql.py: -------------------------------------------------------------------------------- 1 | import threading 2 | 3 | from sqlalchemy import func, distinct, Column, String, UnicodeText 4 | 5 | from lynda.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 | -------------------------------------------------------------------------------- /lynda/modules/lastfm.py: -------------------------------------------------------------------------------- 1 | # Last.fm module by @TheRealPhoenix - https://github.com/rsktg 2 | 3 | import requests 4 | 5 | from telegram import Update, ParseMode 6 | from telegram.ext import run_async, CommandHandler, CallbackContext 7 | 8 | from lynda import dispatcher, LASTFM_API_KEY 9 | from lynda.modules.disable import DisableAbleCommandHandler 10 | 11 | import lynda.modules.sql.last_fm_sql as sql 12 | 13 | 14 | @run_async 15 | def set_user(update: Update, context: CallbackContext): 16 | args = context.args 17 | msg = update.effective_message 18 | if args: 19 | user = update.effective_user.id 20 | username = " ".join(args) 21 | sql.set_user(user, username) 22 | msg.reply_text(f"Username set as {username}!") 23 | else: 24 | msg.reply_text( 25 | "That's not how this works...\nRun /setuser followed by your username!") 26 | 27 | 28 | @run_async 29 | def clear_user(update: Update, _): 30 | user = update.effective_user.id 31 | sql.set_user(user, "") 32 | update.effective_message.reply_text( 33 | "Last.fm username successfully cleared from my database!") 34 | 35 | 36 | @run_async 37 | def last_fm(update: Update, _): 38 | msg = update.effective_message 39 | user = update.effective_user.first_name 40 | user_id = update.effective_user.id 41 | username = sql.get_user(user_id) 42 | if not username: 43 | msg.reply_text("You haven't set your username yet!") 44 | return 45 | 46 | base_url = "http://ws.audioscrobbler.com/2.0" 47 | res = requests.get( 48 | f"{base_url}?method=user.getrecenttracks&limit=3&extended=1&user={username}&api_key={LASTFM_API_KEY}&format=json") 49 | if res.status_code != 200: 50 | msg.reply_text( 51 | "Hmm... something went wrong.\nPlease ensure that you've set the correct username!") 52 | return 53 | 54 | try: 55 | first_track = res.json().get("recenttracks").get("track")[0] 56 | except IndexError: 57 | msg.reply_text("You don't seem to have scrobbled any songs...") 58 | return 59 | if first_track.get("@attr"): 60 | # Ensures the track is now playing 61 | image = first_track.get("image")[3].get( 62 | "#text") # Grab URL of 300x300 image 63 | artist = first_track.get("artist").get("name") 64 | song = first_track.get("name") 65 | loved = int(first_track.get("loved")) 66 | rep = f"{user} is currently listening to:\n" 67 | if not loved: 68 | rep += f"🎧 {artist} - {song}" 69 | else: 70 | rep += f"🎧 {artist} - {song} (♥️, loved)" 71 | if image: 72 | rep += f"\u200c" 73 | else: 74 | tracks = res.json().get("recenttracks").get("track") 75 | track_dict = {tracks[i].get("artist").get( 76 | "name"): tracks[i].get("name") for i in range(3)} 77 | rep = f"{user} was listening to:\n" 78 | for artist, song in track_dict.items(): 79 | rep += f"🎧 {artist} - {song}\n" 80 | last_user = requests.get( 81 | f"{base_url}?method=user.getinfo&user={username}&api_key={LASTFM_API_KEY}&format=json").json().get("user") 82 | scrobbles = last_user.get("playcount") 83 | rep += f"\n({scrobbles} scrobbles so far)" 84 | 85 | msg.reply_text(rep, parse_mode=ParseMode.HTML) 86 | 87 | 88 | __mod_name__ = "Last.FM" 89 | 90 | SET_USER_HANDLER = CommandHandler("setuser", set_user, pass_args=True) 91 | CLEAR_USER_HANDLER = CommandHandler("clearuser", clear_user) 92 | LASTFM_HANDLER = DisableAbleCommandHandler("lastfm", last_fm) 93 | 94 | dispatcher.add_handler(SET_USER_HANDLER) 95 | dispatcher.add_handler(CLEAR_USER_HANDLER) 96 | dispatcher.add_handler(LASTFM_HANDLER) 97 | -------------------------------------------------------------------------------- /lynda/modules/helper_funcs/handlers.py: -------------------------------------------------------------------------------- 1 | import lynda.modules.sql.blacklistusers_sql as sql 2 | from lynda import ALLOW_EXCL 3 | from telegram import MessageEntity, Update 4 | from telegram.ext import CommandHandler, MessageHandler, RegexHandler, Filters 5 | from time import sleep 6 | 7 | CMD_STARTERS = ('/', '!') if ALLOW_EXCL else ('/', ) 8 | 9 | 10 | class CustomCommandHandler(CommandHandler): 11 | 12 | def __init__( 13 | self, 14 | command, 15 | callback, 16 | admin_ok=False, 17 | # allow_edit=False, 18 | **kwargs): 19 | super().__init__(command, callback, **kwargs) 20 | 21 | # if allow_edit is False: 22 | self.filters &= ~( 23 | Filters.update.edited_message 24 | | Filters.update.edited_channel_post) 25 | 26 | def check_update(self, update): 27 | if not isinstance(update, Update) or not update.effective_message: 28 | return 29 | message = update.effective_message 30 | 31 | try: 32 | user_id = update.effective_user.id 33 | except: 34 | user_id = None 35 | 36 | if user_id and sql.is_user_blacklisted(user_id): 37 | return False 38 | 39 | if message.text and len(message.text) > 1: 40 | fst_word = message.text.split(None, 1)[0] 41 | if len(fst_word) > 1 and any( 42 | fst_word.startswith(start) for start in CMD_STARTERS): 43 | 44 | args = message.text.split()[1:] 45 | command = fst_word[1:].split("@") 46 | command.append(message.bot.username) 47 | 48 | if ( 49 | command[0].lower() not in self.command 50 | or command[1].lower() != message.bot.username.lower() 51 | ): 52 | return None 53 | 54 | filter_result = self.filters(update) 55 | if filter_result: 56 | return args, filter_result 57 | else: 58 | return False 59 | 60 | def handle_update(self, update, dispatcher, check_result, context=None): 61 | if context: 62 | self.collect_additional_context(context, update, dispatcher, 63 | check_result) 64 | return self.callback(update, context) 65 | else: 66 | optional_args = self.collect_optional_args(dispatcher, update, check_result) 67 | return self.callback(dispatcher.bot, update, **optional_args) 68 | 69 | def collect_additional_context(self, context, update, dispatcher, check_result): 70 | if isinstance(check_result, bool): 71 | context.args = update.effective_message.text.split()[1:] 72 | else: 73 | context.args = check_result[0] 74 | if isinstance(check_result[1], dict): 75 | context.update(check_result[1]) 76 | 77 | 78 | class CustomRegexHandler(RegexHandler): 79 | 80 | def __init__(self, pattern, callback, friendly="", **kwargs): 81 | super().__init__(pattern, callback, **kwargs) 82 | 83 | 84 | class CustomMessageHandler(MessageHandler): 85 | 86 | def __init__(self, 87 | filters, 88 | callback, 89 | friendly="", 90 | # allow_edit=False, 91 | **kwargs): 92 | super().__init__(filters, callback, **kwargs) 93 | # if allow_edit is False: 94 | self.filters &= ~( 95 | Filters.update.edited_message 96 | | Filters.update.edited_channel_post) 97 | 98 | def check_update(self, update): 99 | if isinstance(update, Update) and update.effective_message: 100 | return self.filters(update) -------------------------------------------------------------------------------- /lynda/modules/helper_funcs/misc.py: -------------------------------------------------------------------------------- 1 | # Taken from @ HarukaNetwork/HarukaAya 2 | # Copyright (C) 2017-2019 Paul Larsen 3 | # Copyright (C) 2019-2020 Akito Mizukito (Haruka Network Development) 4 | # Give a Star to the source and Follow: https://gitlab.com/HarukaNetwork/OSS/HarukaAya 5 | 6 | 7 | from typing import List, Dict 8 | from telegram import MAX_MESSAGE_LENGTH, InlineKeyboardButton, ParseMode, Update 9 | from telegram.error import TelegramError 10 | from telegram.ext import CallbackContext 11 | 12 | from lynda import NO_LOAD 13 | 14 | 15 | class EqInlineKeyboardButton(InlineKeyboardButton): 16 | def __eq__(self, other): 17 | return self.text == other.text 18 | 19 | def __lt__(self, other): 20 | return self.text < other.text 21 | 22 | def __gt__(self, other): 23 | return self.text > other.text 24 | 25 | 26 | def split_message(msg: str) -> List[str]: 27 | if len(msg) < MAX_MESSAGE_LENGTH: 28 | return [msg] 29 | 30 | else: 31 | lines = msg.splitlines(True) 32 | small_msg = "" 33 | result = [] 34 | for line in lines: 35 | if len(small_msg) + len(line) < MAX_MESSAGE_LENGTH: 36 | small_msg += line 37 | else: 38 | result.append(small_msg) 39 | small_msg = line 40 | # Else statement at the end of the for loop, so append the leftover string. 41 | result.append(small_msg) 42 | 43 | return result 44 | 45 | 46 | def paginate_modules(_page_n: int, module_dict: Dict, prefix, chat=None) -> List: 47 | if not chat: 48 | modules = sorted( 49 | [EqInlineKeyboardButton(x.__mod_name__, 50 | callback_data="{}_module({})".format(prefix, x.__mod_name__.lower())) for x 51 | in module_dict.values()]) 52 | else: 53 | modules = sorted( 54 | [EqInlineKeyboardButton(x.__mod_name__, 55 | callback_data="{}_module({},{})".format(prefix, chat, x.__mod_name__.lower())) for x 56 | in module_dict.values()]) 57 | pairs = [ 58 | modules[i * 3:(i + 1) * 3] for i in range((len(modules) + 3 - 1) // 3) 59 | ] 60 | round_num = len(modules) / 3 61 | calc = len(modules) - round(round_num) 62 | if calc == 1: 63 | pairs.append((modules[-1], )) 64 | elif calc == 2: 65 | pairs.append((modules[-1], )) 66 | 67 | return pairs 68 | 69 | 70 | def send_to_list(context: CallbackContext, send_to: list, message: str, markdown=False, html=False) -> None: 71 | if html and markdown: 72 | raise Exception("Can only send with either markdown or HTML!") 73 | for user_id in set(send_to): 74 | try: 75 | if markdown: 76 | context.bot.send_message(user_id, message, parse_mode=ParseMode.MARKDOWN) 77 | elif html: 78 | context.bot.send_message(user_id, message, parse_mode=ParseMode.HTML) 79 | else: 80 | context.bot.send_message(user_id, message) 81 | except TelegramError: 82 | pass # ignore users who fail 83 | 84 | 85 | def build_keyboard(buttons): 86 | keyb = [] 87 | for btn in buttons: 88 | if btn.same_line and keyb: 89 | keyb[-1].append(InlineKeyboardButton(btn.name, url=btn.url)) 90 | else: 91 | keyb.append([InlineKeyboardButton(btn.name, url=btn.url)]) 92 | 93 | return keyb 94 | 95 | 96 | def revert_buttons(buttons): 97 | res = "" 98 | for btn in buttons: 99 | if btn.same_line: 100 | res += "\n[{}](buttonurl://{}:same)".format(btn.name, btn.url) 101 | else: 102 | res += "\n[{}](buttonurl://{})".format(btn.name, btn.url) 103 | 104 | return res 105 | 106 | 107 | def is_module_loaded(name): 108 | return name not in NO_LOAD 109 | 110 | def sendMessage(text: str, context: CallbackContext, update: Update): 111 | return context.bot.send_message(update.message.chat_id, 112 | reply_to_message_id=update.message.message_id, 113 | text=text, parse_mode=ParseMode.HTML) 114 | -------------------------------------------------------------------------------- /lynda/modules/afk.py: -------------------------------------------------------------------------------- 1 | import random 2 | 3 | from telegram import Update, MessageEntity 4 | from telegram.ext import Filters, run_async, CallbackContext 5 | 6 | from lynda import dispatcher 7 | from lynda.modules.disable import ( 8 | DisableAbleCommandHandler, 9 | DisableAbleRegexHandler, 10 | DisableAbleMessageHandler, 11 | ) 12 | from lynda.modules.sql import afk_sql as sql 13 | from lynda.modules.users import get_user_id 14 | 15 | AFK_GROUP = 7 16 | AFK_REPLY_GROUP = 8 17 | 18 | 19 | @run_async 20 | def afk(update: Update, _): 21 | args = update.effective_message.text.split(None, 1) 22 | reason = "" 23 | if len(args) >= 2: 24 | reason = args[1] 25 | 26 | sql.set_afk(update.effective_user.id, reason) 27 | update.effective_message.reply_text( 28 | "{} is away from keyboard !".format(update.effective_user.first_name) 29 | ) 30 | 31 | 32 | @run_async 33 | def no_longer_afk(update: Update, _): 34 | user = update.effective_user 35 | 36 | if not user: 37 | return 38 | 39 | res = sql.rm_afk(user.id) 40 | if res: 41 | options = [ 42 | "{} is here!", 43 | "{} is back!", 44 | "{} is now in the chat!", 45 | "{} is awake!", 46 | "{} is back online!", 47 | "{} is finally here!", 48 | "Welcome back!, {}", 49 | "Where is {}?\nIn the chat!", 50 | ] 51 | chosen_option = random.choice(options) 52 | update.effective_message.reply_text( 53 | chosen_option.format(update.effective_user.first_name) 54 | ) 55 | 56 | 57 | @run_async 58 | def reply_afk(update: Update, context: CallbackContext): 59 | message = update.effective_message 60 | entities = message.parse_entities( 61 | [MessageEntity.TEXT_MENTION, MessageEntity.MENTION] 62 | ) 63 | 64 | if message.entities and entities: 65 | for ent in entities: 66 | if ent.type == MessageEntity.TEXT_MENTION: 67 | user_id = ent.user.id 68 | fst_name = ent.user.first_name 69 | 70 | elif ent.type == MessageEntity.MENTION: 71 | user_id = get_user_id( 72 | message.text[ent.offset : ent.offset + ent.length] 73 | ) 74 | if not user_id: 75 | return 76 | chat = context.bot.get_chat(user_id) 77 | fst_name = chat.first_name 78 | 79 | else: 80 | return 81 | 82 | if sql.is_afk(user_id): 83 | valid, reason = sql.check_afk_status(user_id) 84 | if valid: 85 | if not reason: 86 | res = "{} is AFK!".format(fst_name) 87 | else: 88 | res = "{} is AFK!\nReason:\n{}".format(fst_name, reason) 89 | message.reply_text(res) 90 | 91 | 92 | def __gdpr__(user_id): 93 | sql.rm_afk(user_id) 94 | 95 | 96 | __help__ = """ 97 | -> `/afk` 98 | mark yourself as AFK(away from keyboard). 99 | -> `brb` 100 | same as the afk command - but not a command. 101 | 102 | When marked as AFK, any mentions will be replied to with a message to say you're not available! 103 | """ 104 | 105 | AFK_HANDLER = DisableAbleCommandHandler("afk", afk) 106 | AFK_REGEX_HANDLER = DisableAbleRegexHandler(r"(?i)brb", afk, friendly="afk") 107 | NO_AFK_HANDLER = DisableAbleMessageHandler( 108 | Filters.all & Filters.group, no_longer_afk, friendly="afk" 109 | ) 110 | AFK_REPLY_HANDLER = DisableAbleMessageHandler( 111 | (Filters.entity(MessageEntity.MENTION) | Filters.entity(MessageEntity.TEXT_MENTION)) 112 | & Filters.group, 113 | reply_afk, 114 | friendly="afk", 115 | ) 116 | 117 | dispatcher.add_handler(AFK_HANDLER, AFK_GROUP) 118 | dispatcher.add_handler(AFK_REGEX_HANDLER, AFK_GROUP) 119 | dispatcher.add_handler(NO_AFK_HANDLER, AFK_GROUP) 120 | dispatcher.add_handler(AFK_REPLY_HANDLER, AFK_REPLY_GROUP) 121 | 122 | __mod_name__ = "AFK" 123 | __command_list__ = ["afk"] 124 | __handlers__ = [ 125 | (AFK_HANDLER, AFK_GROUP), 126 | (AFK_REGEX_HANDLER, AFK_GROUP), 127 | (NO_AFK_HANDLER, AFK_GROUP), 128 | (AFK_REPLY_HANDLER, AFK_REPLY_GROUP), 129 | ] 130 | -------------------------------------------------------------------------------- /lynda/modules/users.py: -------------------------------------------------------------------------------- 1 | from io import BytesIO 2 | from time import sleep 3 | 4 | from telegram import Update, TelegramError 5 | from telegram.error import BadRequest 6 | from telegram.ext import CommandHandler, MessageHandler, Filters, run_async, CallbackContext 7 | 8 | import lynda.modules.sql.users_sql as sql 9 | 10 | from lynda import dispatcher, OWNER_ID, LOGGER, DEV_USERS 11 | from lynda.modules.helper_funcs.chat_status import sudo_plus, dev_plus 12 | 13 | USERS_GROUP = 4 14 | DEV_AND_MORE = DEV_USERS.append(int(OWNER_ID)) 15 | 16 | 17 | def get_user_id(username): 18 | # ensure valid userid 19 | if len(username) <= 5: 20 | return None 21 | 22 | if username.startswith('@'): 23 | username = username[1:] 24 | 25 | users = sql.get_userid_by_name(username) 26 | 27 | if not users: 28 | return None 29 | 30 | elif len(users) == 1: 31 | return users[0].user_id 32 | 33 | else: 34 | for user_obj in users: 35 | try: 36 | userdat = dispatcher.bot.get_chat(user_obj.user_id) 37 | if userdat.username == username: 38 | return userdat.id 39 | 40 | except BadRequest as excp: 41 | if excp.message != 'Chat not found': 42 | LOGGER.exception("Error extracting user ID") 43 | 44 | return None 45 | @run_async 46 | @dev_plus 47 | def broadcast(update: Update, context: CallbackContext): 48 | bot = context.bot 49 | 50 | to_send = update.effective_message.text.split(None, 1) 51 | 52 | if len(to_send) >= 2: 53 | chats = sql.get_all_chats() or [] 54 | failed = 0 55 | for chat in chats: 56 | try: 57 | bot.sendMessage(int(chat.chat_id), to_send[1]) 58 | sleep(0.1) 59 | except TelegramError: 60 | failed += 1 61 | LOGGER.warning( 62 | "Couldn't send broadcast to %s, group name %s", str( 63 | chat.chat_id), str( 64 | chat.chat_name)) 65 | 66 | update.effective_message.reply_text( 67 | f"Broadcast complete. {failed} groups failed to receive the message, probably due to being kicked.") 68 | 69 | 70 | @run_async 71 | def log_user(update: Update, _): 72 | chat = update.effective_chat 73 | msg = update.effective_message 74 | 75 | sql.update_user(msg.from_user.id, 76 | msg.from_user.username, 77 | chat.id, 78 | chat.title) 79 | 80 | if msg.reply_to_message: 81 | sql.update_user(msg.reply_to_message.from_user.id, 82 | msg.reply_to_message.from_user.username, 83 | chat.id, 84 | chat.title) 85 | 86 | if msg.forward_from: 87 | sql.update_user(msg.forward_from.id, 88 | msg.forward_from.username) 89 | 90 | 91 | @run_async 92 | @sudo_plus 93 | def chats(update: Update, _): 94 | 95 | all_chats = sql.get_all_chats() or [] 96 | chatfile = 'List of chats.\n' 97 | for chat in all_chats: 98 | chatfile += f"{chat.chat_name} - ({chat.chat_id})\n" 99 | 100 | with BytesIO(str.encode(chatfile)) as output: 101 | output.name = "chatlist.txt" 102 | update.effective_message.reply_document( 103 | document=output, 104 | filename="chatlist.txt", 105 | caption="Here is the list of chats in my Hit List.") 106 | 107 | 108 | def __stats__(): 109 | return f"{sql.num_users()} users, across {sql.num_chats()} chats" 110 | 111 | 112 | def __migrate__(old_chat_id, new_chat_id): 113 | sql.migrate_chat(old_chat_id, new_chat_id) 114 | 115 | 116 | __help__ = "" # no help string 117 | 118 | BROADCAST_HANDLER = CommandHandler("broadcast", broadcast) 119 | USER_HANDLER = MessageHandler(Filters.all & Filters.group, log_user) 120 | CHATLIST_HANDLER = CommandHandler("chatlist", chats) 121 | 122 | dispatcher.add_handler(USER_HANDLER, USERS_GROUP) 123 | dispatcher.add_handler(BROADCAST_HANDLER) 124 | dispatcher.add_handler(CHATLIST_HANDLER) 125 | 126 | __mod_name__ = "Users" 127 | __handlers__ = [ 128 | (USER_HANDLER, 129 | USERS_GROUP), 130 | BROADCAST_HANDLER, 131 | CHATLIST_HANDLER] 132 | -------------------------------------------------------------------------------- /app.json.sample: -------------------------------------------------------------------------------- 1 | { 2 | "addons": [ 3 | { 4 | "options": { 5 | "version": "9.5" 6 | }, 7 | "plan": "heroku-postgresql" 8 | } 9 | ], 10 | "description": "An Anime themed Telegram group management bot.", 11 | "env": { 12 | "ALLOW_EXCL": { 13 | "description": "Set this to True if you want ! to be a command prefix along with /", 14 | "value": "True" 15 | }, 16 | "BAN_STICKER": { 17 | "description": "ID of the sticker you want to use when banning people.", 18 | "required": false, 19 | "value": "" 20 | }, 21 | "DEL_CMDS": { 22 | "description": "Set this to True if you want to delete command messages from users who don't have the perms to run that command.", 23 | "value": "True" 24 | }, 25 | "DONATION_LINK": { 26 | "description": "Optional: link where you would like to receive donations.", 27 | "required": false, 28 | "value": "https://www.paypal.me/PaulSonOfLars" 29 | }, 30 | "ENV": { 31 | "description": "Setting this to ANYTHING will enable environment variables.", 32 | "value": "ANYTHING" 33 | }, 34 | "SQLALCHEMY_DATABASE_URI": { 35 | "description": "Your postgres sql db, empty this field if you dont have one.", 36 | "required": false, 37 | "value": "sqldbtype://username:pw@hostname:port/db_name" 38 | }, 39 | "OWNER_ID": { 40 | "description": "Your user ID as an integer.", 41 | "value": "792109647" 42 | }, 43 | "OWNER_USERNAME": { 44 | "description": "Your username", 45 | "value": "AnimeKaizoku" 46 | }, 47 | "DEV_USERS": { 48 | "description": "ID of users who are Dev (can use /py etc.)", 49 | "required": false, 50 | "value": "660565862 792109647" 51 | }, 52 | "GBAN_LOGS": { 53 | "description": "Gban log channel, include the hyphen too: ex: -123456", 54 | "value": "-123" 55 | }, 56 | "CASH_API_KEY": { 57 | "description": "Required for currency converter", 58 | "value": "-xyz" 59 | }, 60 | "TIME_API_KEY": { 61 | "description": "Required for timezone information", 62 | "value": "-xyz" 63 | }, 64 | "PORT": { 65 | "description": "Port to use for your webhooks.", 66 | "required": false, 67 | "value": "" 68 | }, 69 | "STRICT_GBAN": { 70 | "description": "Enforce gbans across new groups as well as old groups. When a gbanned user talks, he will be banned.", 71 | "value": "True" 72 | }, 73 | "SUDO_USERS": { 74 | "description": "A space separated list of user IDs who you want to assign as sudo users.", 75 | "required": false, 76 | "value": "459034222 189005567 660565862 346981140 367222759" 77 | }, 78 | "SUPPORT_USERS": { 79 | "description": "A space separated list of user IDs who you wanna assign as support users(gban perms only).", 80 | "required": false, 81 | "value": "615304572" 82 | }, 83 | "SARDEGNA_USERS": { 84 | "description": "A space separated list of user IDs who you wanna assign as Sardegna users.", 85 | "required": false, 86 | "value": "" 87 | }, 88 | "TOKEN": { 89 | "description": "Your bot token.", 90 | "required": true, 91 | "value": "" 92 | }, 93 | "URL": { 94 | "description": "The Heroku App URL :- https://.herokuapp.com/", 95 | "required": false, 96 | "value": "" 97 | }, 98 | "WEBHOOK": { 99 | "description": "Setting this to ANYTHING will enable webhooks.", 100 | "required": false, 101 | "value": "" 102 | }, 103 | "WHITELIST_USERS": { 104 | "description": "A space separated list of user IDs who you want to assign as whitelisted - can't be banned with your bot.", 105 | "required": false, 106 | "value": "" 107 | } 108 | }, 109 | "keywords": [ 110 | "telegram", 111 | "weeb", 112 | "group", 113 | "manager", 114 | "Kigyō" 115 | ], 116 | "name": "Kigyō bot", 117 | "repository": "https://github.com/AnimeKaizoku/Kigyōbot" 118 | } 119 | -------------------------------------------------------------------------------- /lynda/modules/rules.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | 3 | from telegram import Message, Update, User, ParseMode, InlineKeyboardMarkup, InlineKeyboardButton 4 | from telegram.error import BadRequest 5 | from telegram.ext import CommandHandler, run_async, Filters, CallbackContext 6 | from telegram.utils.helpers import escape_markdown 7 | 8 | import lynda.modules.sql.rules_sql as sql 9 | from lynda import dispatcher 10 | from lynda.modules.helper_funcs.chat_status import user_admin 11 | from lynda.modules.helper_funcs.string_handling import markdown_parser 12 | 13 | 14 | @run_async 15 | def get_rules(_, update: Update): 16 | chat_id = update.effective_chat.id 17 | send_rules(update, chat_id) 18 | 19 | 20 | # Do not async - not from a handler 21 | def send_rules(update, chat_id, from_pm=False): 22 | bot = dispatcher.bot 23 | user = update.effective_user # type: Optional[User] 24 | try: 25 | chat = bot.get_chat(chat_id) 26 | except BadRequest as excp: 27 | if excp.message == "Chat not found" and from_pm: 28 | bot.send_message( 29 | user.id, 30 | "The rules shortcut for this chat hasn't been set properly! Ask admins to " 31 | "fix this.") 32 | return 33 | else: 34 | raise 35 | 36 | rules = sql.get_rules(chat_id) 37 | text = f"The rules for *{escape_markdown(chat.title)}* are:\n\n{rules}" 38 | 39 | if from_pm and rules: 40 | bot.send_message( 41 | user.id, 42 | text, 43 | parse_mode=ParseMode.MARKDOWN, 44 | disable_web_page_preview=True) 45 | elif from_pm: 46 | bot.send_message( 47 | user.id, "The group admins haven't set any rules for this chat yet. " 48 | "This probably doesn't mean it's lawless though...!") 49 | elif rules: 50 | update.effective_message.reply_text("Contact me in PM to get this group's rules.", 51 | reply_markup=InlineKeyboardMarkup( 52 | [[InlineKeyboardButton(text="Rules", 53 | url=f"t.me/{bot.username}?start={chat_id}")]])) 54 | else: 55 | update.effective_message.reply_text( 56 | "The group admins haven't set any rules for this chat yet. " 57 | "This probably doesn't mean it's lawless though...!") 58 | 59 | 60 | @run_async 61 | @user_admin 62 | def set_rules(_, update: Update): 63 | msg = update.effective_message # type: Optional[Message] 64 | raw_text = msg.text 65 | # use python's maxsplit to separate cmd and args 66 | args = raw_text.split(None, 1) 67 | if len(args) == 2: 68 | txt = args[1] 69 | # set correct offset relative to command 70 | offset = len(txt) - len(raw_text) 71 | markdown_rules = markdown_parser( 72 | txt, entities=msg.parse_entities(), offset=offset) 73 | 74 | chat_id = update.effective_chat.id 75 | sql.set_rules(chat_id, markdown_rules) 76 | update.effective_message.reply_text( 77 | "Successfully set rules for this group.") 78 | 79 | 80 | @run_async 81 | @user_admin 82 | def clear_rules(update: Update, _): 83 | chat_id = update.effective_chat.id 84 | sql.set_rules(chat_id, "") 85 | update.effective_message.reply_text("Successfully cleared rules!") 86 | 87 | 88 | def __stats__(): 89 | return f"{sql.num_chats()} chats have rules set." 90 | 91 | 92 | def __import_data__(chat_id, data): 93 | # set chat rules 94 | rules = data.get('info', {}).get('rules', "") 95 | sql.set_rules(chat_id, rules) 96 | 97 | 98 | def __migrate__(old_chat_id, new_chat_id): 99 | sql.migrate_chat(old_chat_id, new_chat_id) 100 | 101 | 102 | def __chat_settings__(chat_id, _user_id): 103 | return f"This chat has had it's rules set: `{bool(sql.get_rules(chat_id))}`" 104 | 105 | 106 | __help__ = """ 107 | -> `/rules` 108 | get the rules for this chat. 109 | 110 | ──「 *Admin only:* 」── 111 | -> `/setrules` 112 | set the rules for this chat. 113 | -> `/clearrules` 114 | clear the rules for this chat. 115 | """ 116 | 117 | __mod_name__ = "Rules" 118 | 119 | GET_RULES_HANDLER = CommandHandler("rules", get_rules, filters=Filters.group) 120 | SET_RULES_HANDLER = CommandHandler( 121 | "setrules", set_rules, filters=Filters.group) 122 | RESET_RULES_HANDLER = CommandHandler( 123 | "clearrules", clear_rules, filters=Filters.group) 124 | 125 | dispatcher.add_handler(GET_RULES_HANDLER) 126 | dispatcher.add_handler(SET_RULES_HANDLER) 127 | dispatcher.add_handler(RESET_RULES_HANDLER) 128 | -------------------------------------------------------------------------------- /lynda/modules/sql/global_bans_sql.py: -------------------------------------------------------------------------------- 1 | import threading 2 | 3 | from sqlalchemy import Column, UnicodeText, Integer, String, Boolean 4 | 5 | from lynda.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 GbanSettings(BASE): 29 | __tablename__ = "gban_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 | GloballyBannedUsers.__table__.create(checkfirst=True) 42 | GbanSettings.__table__.create(checkfirst=True) 43 | 44 | GBANNED_USERS_LOCK = threading.RLock() 45 | GBAN_SETTING_LOCK = threading.RLock() 46 | GBANNED_LIST = set() 47 | GBANSTAT_LIST = set() 48 | 49 | 50 | def gban_user(user_id, name, reason=None): 51 | with GBANNED_USERS_LOCK: 52 | user = SESSION.query(GloballyBannedUsers).get(user_id) 53 | if not user: 54 | user = GloballyBannedUsers(user_id, name, reason) 55 | else: 56 | user.name = name 57 | user.reason = reason 58 | 59 | SESSION.merge(user) 60 | SESSION.commit() 61 | __load_gbanned_userid_list() 62 | 63 | 64 | def update_gban_reason(user_id, name, reason=None): 65 | with GBANNED_USERS_LOCK: 66 | user = SESSION.query(GloballyBannedUsers).get(user_id) 67 | if not user: 68 | return None 69 | old_reason = user.reason 70 | user.name = name 71 | user.reason = reason 72 | 73 | SESSION.merge(user) 74 | SESSION.commit() 75 | return old_reason 76 | 77 | 78 | def ungban_user(user_id): 79 | with GBANNED_USERS_LOCK: 80 | user = SESSION.query(GloballyBannedUsers).get(user_id) 81 | if user: 82 | SESSION.delete(user) 83 | 84 | SESSION.commit() 85 | __load_gbanned_userid_list() 86 | 87 | 88 | def is_user_gbanned(user_id): 89 | return user_id in GBANNED_LIST 90 | 91 | 92 | def get_gbanned_user(user_id): 93 | try: 94 | return SESSION.query(GloballyBannedUsers).get(user_id) 95 | finally: 96 | SESSION.close() 97 | 98 | 99 | def get_gban_list(): 100 | try: 101 | return [x.to_dict() for x in SESSION.query(GloballyBannedUsers).all()] 102 | finally: 103 | SESSION.close() 104 | 105 | 106 | def enable_gbans(chat_id): 107 | with GBAN_SETTING_LOCK: 108 | chat = SESSION.query(GbanSettings).get(str(chat_id)) 109 | if not chat: 110 | chat = GbanSettings(chat_id, True) 111 | 112 | chat.setting = True 113 | SESSION.add(chat) 114 | SESSION.commit() 115 | if str(chat_id) in GBANSTAT_LIST: 116 | GBANSTAT_LIST.remove(str(chat_id)) 117 | 118 | 119 | def disable_gbans(chat_id): 120 | with GBAN_SETTING_LOCK: 121 | chat = SESSION.query(GbanSettings).get(str(chat_id)) 122 | if not chat: 123 | chat = GbanSettings(chat_id, False) 124 | 125 | chat.setting = False 126 | SESSION.add(chat) 127 | SESSION.commit() 128 | GBANSTAT_LIST.add(str(chat_id)) 129 | 130 | 131 | def does_chat_gban(chat_id): 132 | return str(chat_id) not in GBANSTAT_LIST 133 | 134 | 135 | def num_gbanned_users(): 136 | return len(GBANNED_LIST) 137 | 138 | 139 | def __load_gbanned_userid_list(): 140 | global GBANNED_LIST 141 | try: 142 | GBANNED_LIST = {x.user_id for x in SESSION.query(GloballyBannedUsers).all()} 143 | finally: 144 | SESSION.close() 145 | 146 | 147 | def __load_gban_stat_list(): 148 | global GBANSTAT_LIST 149 | try: 150 | GBANSTAT_LIST = {x.chat_id for x in SESSION.query(GbanSettings).all() if not x.setting} 151 | finally: 152 | SESSION.close() 153 | 154 | 155 | def migrate_chat(old_chat_id, new_chat_id): 156 | with GBAN_SETTING_LOCK: 157 | chat = SESSION.query(GbanSettings).get(str(old_chat_id)) 158 | if chat: 159 | chat.chat_id = new_chat_id 160 | SESSION.add(chat) 161 | 162 | SESSION.commit() 163 | 164 | 165 | # Create in memory userid to avoid disk access 166 | __load_gbanned_userid_list() 167 | __load_gban_stat_list() 168 | -------------------------------------------------------------------------------- /lynda/modules/sed.py: -------------------------------------------------------------------------------- 1 | import re 2 | import sre_constants 3 | 4 | import telegram 5 | from telegram import Update 6 | from telegram.ext import run_async, CallbackContext 7 | 8 | from lynda import dispatcher, LOGGER 9 | from lynda.modules.disable import DisableAbleRegexHandler 10 | 11 | DELIMITERS = ("/", ":", "|", "_") 12 | 13 | 14 | def separate_sed(sed_string): 15 | if ( 16 | len(sed_string) < 3 17 | or sed_string[1] not in DELIMITERS 18 | or sed_string.count(sed_string[1]) < 2 19 | ): 20 | return 21 | 22 | delim = sed_string[1] 23 | start = counter = 2 24 | while counter < len(sed_string): 25 | if sed_string[counter] == "\\": 26 | counter += 1 27 | 28 | elif sed_string[counter] == delim: 29 | replace = sed_string[start:counter] 30 | counter += 1 31 | start = counter 32 | break 33 | 34 | counter += 1 35 | 36 | else: 37 | return None 38 | 39 | while counter < len(sed_string): 40 | if sed_string[counter] == "\\" and counter + \ 41 | 1 < len(sed_string) and sed_string[counter + 1] == delim: 42 | sed_string = sed_string[:counter] + sed_string[counter + 1:] 43 | 44 | elif sed_string[counter] == delim: 45 | replace_with = sed_string[start:counter] 46 | counter += 1 47 | break 48 | 49 | counter += 1 50 | else: 51 | return replace, sed_string[start:], "" 52 | 53 | flags = "" 54 | if counter < len(sed_string): 55 | flags = sed_string[counter:] 56 | return replace, replace_with, flags.lower() 57 | 58 | 59 | @run_async 60 | def sed(update: Update, _): 61 | sed_result = separate_sed(update.effective_message.text) 62 | if sed_result and update.effective_message.reply_to_message: 63 | if update.effective_message.reply_to_message.text: 64 | to_fix = update.effective_message.reply_to_message.text 65 | elif update.effective_message.reply_to_message.caption: 66 | to_fix = update.effective_message.reply_to_message.caption 67 | else: 68 | return 69 | 70 | repl, repl_with, flags = sed_result 71 | 72 | if not repl: 73 | update.effective_message.reply_to_message.reply_text( 74 | "You're trying to replace... " "nothing with something?") 75 | return 76 | 77 | try: 78 | check = re.match(repl, to_fix, flags=re.IGNORECASE) 79 | 80 | if check and check.group(0).lower() == to_fix.lower(): 81 | update.effective_message.reply_to_message.reply_text( 82 | "Hey everyone, {} is trying to make " 83 | "me say stuff I don't wanna " 84 | "say!".format( 85 | update.effective_user.first_name)) 86 | return 87 | 88 | if 'i' in flags and 'g' in flags: 89 | text = re.sub(repl, repl_with, to_fix, flags=re.I).strip() 90 | elif 'i' in flags: 91 | text = re.sub( 92 | repl, 93 | repl_with, 94 | to_fix, 95 | count=1, 96 | flags=re.I).strip() 97 | elif 'g' in flags: 98 | text = re.sub(repl, repl_with, to_fix).strip() 99 | else: 100 | text = re.sub(repl, repl_with, to_fix, count=1).strip() 101 | except sre_constants.error: 102 | LOGGER.warning(update.effective_message.text) 103 | LOGGER.exception("SRE constant error") 104 | update.effective_message.reply_text( 105 | "Do you even sed? Apparently not.") 106 | return 107 | 108 | # empty string errors -_- 109 | if len(text) >= telegram.MAX_MESSAGE_LENGTH: 110 | update.effective_message.reply_text( 111 | "The result of the sed command was too long for \ 112 | telegram!") 113 | elif text: 114 | update.effective_message.reply_to_message.reply_text(text) 115 | 116 | 117 | __help__ = """ 118 | -> `s//(/)` 119 | Reply to a message with this to perform a sed operation on that message, replacing all \ 120 | occurrences of 'text1' with 'text2'. Flags are optional, and currently include 'i' for ignore case, 'g' for global, \ 121 | or nothing. Delimiters include `/`, `_`, `|`, and `:`. Text grouping is supported. The resulting message cannot be \ 122 | larger than {}. 123 | 124 | *Reminder:* Sed uses some special characters to make matching easier, such as these: `+*.?\\` 125 | If you want to use these characters, make sure you escape them! 126 | eg: \\?. 127 | """.format(telegram.MAX_MESSAGE_LENGTH) 128 | 129 | __mod_name__ = "Regex" 130 | 131 | 132 | SED_HANDLER = DisableAbleRegexHandler( 133 | r's([{}]).*?\1.*'.format("".join(DELIMITERS)), sed, friendly="sed") 134 | 135 | dispatcher.add_handler(SED_HANDLER) 136 | -------------------------------------------------------------------------------- /lynda/modules/helper_funcs/msg_types.py: -------------------------------------------------------------------------------- 1 | from enum import IntEnum, unique 2 | 3 | from telegram import Message 4 | 5 | from lynda.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 | 19 | 20 | def get_note_type(msg: Message): 21 | data_type = None 22 | content = None 23 | text = "" 24 | raw_text = msg.text or msg.caption 25 | args = raw_text.split(None, 2) # use python's maxsplit to separate cmd and args 26 | note_name = args[1] 27 | 28 | buttons = [] 29 | # determine what the contents of the filter are - text, image, sticker, etc 30 | if len(args) >= 3: 31 | offset = len(args[2]) - len(raw_text) # set correct offset relative to command + notename 32 | text, buttons = button_markdown_parser(args[2], entities=msg.parse_entities() or msg.parse_caption_entities(), 33 | offset=offset) 34 | data_type = Types.BUTTON_TEXT if buttons else Types.TEXT 35 | elif msg.reply_to_message: 36 | entities = msg.reply_to_message.parse_entities() 37 | msgtext = msg.reply_to_message.text or msg.reply_to_message.caption 38 | if len(args) >= 2 and msg.reply_to_message.text: # not caption, text 39 | text, buttons = button_markdown_parser(msgtext, 40 | entities=entities) 41 | data_type = Types.BUTTON_TEXT if buttons else Types.TEXT 42 | elif msg.reply_to_message.sticker: 43 | content = msg.reply_to_message.sticker.file_id 44 | data_type = Types.STICKER 45 | 46 | elif msg.reply_to_message.document: 47 | content = msg.reply_to_message.document.file_id 48 | text, buttons = button_markdown_parser(msgtext, entities=entities) 49 | data_type = Types.DOCUMENT 50 | 51 | elif msg.reply_to_message.photo: 52 | content = msg.reply_to_message.photo[-1].file_id # last elem = best quality 53 | text, buttons = button_markdown_parser(msgtext, entities=entities) 54 | data_type = Types.PHOTO 55 | 56 | elif msg.reply_to_message.audio: 57 | content = msg.reply_to_message.audio.file_id 58 | text, buttons = button_markdown_parser(msgtext, entities=entities) 59 | data_type = Types.AUDIO 60 | 61 | elif msg.reply_to_message.voice: 62 | content = msg.reply_to_message.voice.file_id 63 | text, buttons = button_markdown_parser(msgtext, entities=entities) 64 | data_type = Types.VOICE 65 | 66 | elif msg.reply_to_message.video: 67 | content = msg.reply_to_message.video.file_id 68 | text, buttons = button_markdown_parser(msgtext, entities=entities) 69 | data_type = Types.VIDEO 70 | 71 | return note_name, text, data_type, content, buttons 72 | 73 | 74 | # note: add own args? 75 | def get_welcome_type(msg: Message): 76 | data_type = None 77 | content = None 78 | text = "" 79 | 80 | args = msg.text.split(None, 1) # use python's maxsplit to separate cmd and args 81 | 82 | buttons = [] 83 | # determine what the contents of the filter are - text, image, sticker, etc 84 | if len(args) >= 2: 85 | offset = len(args[1]) - len(msg.text) # set correct offset relative to command + notename 86 | text, buttons = button_markdown_parser(args[1], entities=msg.parse_entities(), offset=offset) 87 | data_type = Types.BUTTON_TEXT if buttons else Types.TEXT 88 | elif msg.reply_to_message and msg.reply_to_message.sticker: 89 | content = msg.reply_to_message.sticker.file_id 90 | text = msg.reply_to_message.caption 91 | data_type = Types.STICKER 92 | 93 | elif msg.reply_to_message and msg.reply_to_message.document: 94 | content = msg.reply_to_message.document.file_id 95 | text = msg.reply_to_message.caption 96 | data_type = Types.DOCUMENT 97 | 98 | elif msg.reply_to_message and msg.reply_to_message.photo: 99 | content = msg.reply_to_message.photo[-1].file_id # last elem = best quality 100 | text = msg.reply_to_message.caption 101 | data_type = Types.PHOTO 102 | 103 | elif msg.reply_to_message and msg.reply_to_message.audio: 104 | content = msg.reply_to_message.audio.file_id 105 | text = msg.reply_to_message.caption 106 | data_type = Types.AUDIO 107 | 108 | elif msg.reply_to_message and msg.reply_to_message.voice: 109 | content = msg.reply_to_message.voice.file_id 110 | text = msg.reply_to_message.caption 111 | data_type = Types.VOICE 112 | 113 | elif msg.reply_to_message and msg.reply_to_message.video: 114 | content = msg.reply_to_message.video.file_id 115 | text = msg.reply_to_message.caption 116 | data_type = Types.VIDEO 117 | 118 | return text, data_type, content, buttons 119 | -------------------------------------------------------------------------------- /lynda/modules/blacklistusers.py: -------------------------------------------------------------------------------- 1 | # Module to blacklist users and prevent them from using commands by 2 | # @TheRealPhoenix 3 | from typing import List 4 | 5 | from telegram import Update, ParseMode 6 | from telegram.error import BadRequest 7 | from telegram.ext import CommandHandler, run_async, CallbackContext 8 | from telegram.utils.helpers import mention_html 9 | 10 | import lynda.modules.sql.blacklistusers_sql as sql 11 | from lynda import dispatcher, OWNER_ID, DEV_USERS, SUDO_USERS, WHITELIST_USERS, SUPPORT_USERS 12 | from lynda.modules.helper_funcs.chat_status import dev_plus 13 | from lynda.modules.helper_funcs.extraction import extract_user_and_text, extract_user 14 | from lynda.modules.log_channel import gloggable 15 | 16 | BLACKLISTWHITELIST = [OWNER_ID] + DEV_USERS + \ 17 | SUDO_USERS + WHITELIST_USERS + SUPPORT_USERS 18 | BLABLEUSERS = [OWNER_ID] + DEV_USERS 19 | 20 | 21 | @run_async 22 | @dev_plus 23 | @gloggable 24 | def bl_user(update: Update, context: CallbackContext) -> str: 25 | args = context.args 26 | message = update.effective_message 27 | user = update.effective_user 28 | 29 | user_id, reason = extract_user_and_text(message, args) 30 | 31 | if not user_id: 32 | message.reply_text("I doubt that's a user.") 33 | return "" 34 | 35 | if user_id == context.bot.id: 36 | message.reply_text( 37 | "How am I supposed to do my work if I am ignoring myself?") 38 | return "" 39 | 40 | if user_id in BLACKLISTWHITELIST: 41 | message.reply_text("No!\nNoticing Nations is my job.") 42 | return "" 43 | 44 | try: 45 | target_user = context.bot.get_chat(user_id) 46 | except BadRequest as excp: 47 | if excp.message == "User not found": 48 | message.reply_text("I can't seem to find this user.") 49 | return "" 50 | else: 51 | raise 52 | 53 | sql.blacklist_user(user_id, reason) 54 | message.reply_text("I shall ignore the existence of this user!") 55 | log_message = ( 56 | f"#BLACKLIST\n" 57 | f"Admin: {mention_html(user.id, user.first_name)}\n" 58 | f"User: {mention_html(target_user.id, target_user.first_name)}") 59 | if reason: 60 | log_message += f"\nReason: {reason}" 61 | 62 | return log_message 63 | 64 | 65 | @run_async 66 | @dev_plus 67 | @gloggable 68 | def unbl_user(update: Update, context: CallbackContext) -> str: 69 | args = context.args 70 | message = update.effective_message 71 | user = update.effective_user 72 | 73 | user_id = extract_user(message, args) 74 | 75 | if not user_id: 76 | message.reply_text("I doubt that's a user.") 77 | return "" 78 | 79 | if user_id == context.bot.id: 80 | message.reply_text("I always notice myself.") 81 | return "" 82 | 83 | try: 84 | target_user = context.bot.get_chat(user_id) 85 | except BadRequest as excp: 86 | if excp.message == "User not found": 87 | message.reply_text("I can't seem to find this user.") 88 | return "" 89 | else: 90 | raise 91 | 92 | if sql.is_user_blacklisted(user_id): 93 | 94 | sql.unblacklist_user(user_id) 95 | message.reply_text("*notices user*") 96 | log_message = ( 97 | f"#UNBLACKLIST\n" 98 | f"Admin: {mention_html(user.id, user.first_name)}\n" 99 | f"User: {mention_html(target_user.id, target_user.first_name)}") 100 | 101 | return log_message 102 | 103 | else: 104 | message.reply_text("I am not ignoring them at all though!") 105 | return "" 106 | 107 | 108 | @run_async 109 | @dev_plus 110 | def bl_users(update: Update, context: CallbackContext): 111 | users = [] 112 | 113 | for each_user in sql.BLACKLIST_USERS: 114 | 115 | user = context.bot.get_chat(each_user) 116 | reason = sql.get_reason(each_user) 117 | 118 | if reason: 119 | users.append( 120 | f"• {mention_html(user.id, user.first_name)} :- {reason}") 121 | else: 122 | users.append(f"• {mention_html(user.id, user.first_name)}") 123 | 124 | message = "Blacklisted Users\n" 125 | message += '\n'.join(users) if users else "Noone is being ignored as of yet." 126 | update.effective_message.reply_text(message, parse_mode=ParseMode.HTML) 127 | 128 | 129 | def __user_info__(user_id): 130 | is_blacklisted = sql.is_user_blacklisted(user_id) 131 | 132 | text = "Globally Ignored: {}" 133 | 134 | if is_blacklisted: 135 | text = text.format("Yes") 136 | reason = sql.get_reason(user_id) 137 | if reason: 138 | text += f"\nReason: {reason}" 139 | else: 140 | text = text.format("No") 141 | 142 | return text 143 | 144 | 145 | BL_HANDLER = CommandHandler("ignore", bl_user, pass_args=True) 146 | UNBL_HANDLER = CommandHandler("notice", unbl_user, pass_args=True) 147 | BLUSERS_HANDLER = CommandHandler("ignoredlist", bl_users) 148 | 149 | dispatcher.add_handler(BL_HANDLER) 150 | dispatcher.add_handler(UNBL_HANDLER) 151 | dispatcher.add_handler(BLUSERS_HANDLER) 152 | 153 | __mod_name__ = "Blacklisting Users" 154 | __handlers__ = [BL_HANDLER, UNBL_HANDLER, BLUSERS_HANDLER] 155 | -------------------------------------------------------------------------------- /lynda/modules/chatbot.py: -------------------------------------------------------------------------------- 1 | # AI module using Intellivoid's Coffeehouse API by @TheRealPhoenix 2 | from time import time, sleep 3 | 4 | from coffeehouse.lydia import LydiaAI 5 | from coffeehouse.api import API 6 | from coffeehouse.exception import CoffeeHouseError as CFError 7 | 8 | from telegram import Update 9 | from telegram.ext import CommandHandler, MessageHandler, Filters, run_async, CallbackContext 10 | from telegram.error import BadRequest, Unauthorized, RetryAfter 11 | 12 | from lynda import dispatcher, AI_API_KEY, OWNER_ID 13 | import lynda.modules.sql.chatbot_sql as sql 14 | from lynda.modules.helper_funcs.chat_status import user_admin 15 | from lynda.modules.helper_funcs.filters import CustomFilters 16 | 17 | CoffeeHouseAPI = API(AI_API_KEY) 18 | api_client = LydiaAI(CoffeeHouseAPI) 19 | 20 | 21 | @run_async 22 | @user_admin 23 | def add_chat(update: Update, _): 24 | global api_client 25 | chat_id = update.effective_chat.id 26 | msg = update.effective_message 27 | is_chat = sql.is_chat(chat_id) 28 | if not is_chat: 29 | ses = api_client.create_session() 30 | ses_id = str(ses.id) 31 | expires = str(ses.expires) 32 | sql.set_ses(chat_id, ses_id, expires) 33 | msg.reply_text("AI successfully enabled for this chat!") 34 | else: 35 | msg.reply_text("AI is already enabled for this chat!") 36 | 37 | 38 | @run_async 39 | @user_admin 40 | def remove_chat(update: Update, _): 41 | msg = update.effective_message 42 | chat_id = update.effective_chat.id 43 | is_chat = sql.is_chat(chat_id) 44 | if not is_chat: 45 | msg.reply_text("AI isn't enabled here in the first place!") 46 | else: 47 | sql.rem_chat(chat_id) 48 | msg.reply_text("AI disabled successfully!") 49 | 50 | 51 | def check_message(context: CallbackContext, message): 52 | reply_msg = message.reply_to_message 53 | if message.text.lower() == "lynda": 54 | return True 55 | if reply_msg: 56 | if reply_msg.from_user.id == context.bot.get_me().id: 57 | return True 58 | else: 59 | return False 60 | 61 | 62 | @run_async 63 | def chatbot(update: Update, context: CallbackContext): 64 | global api_client 65 | msg = update.effective_message 66 | chat_id = update.effective_chat.id 67 | is_chat = sql.is_chat(chat_id) 68 | if not is_chat: 69 | return 70 | if msg.text and not msg.document: 71 | if not check_message(context.bot, msg): 72 | return 73 | sesh, exp = sql.get_ses(chat_id) 74 | query = msg.text 75 | try: 76 | if int(exp) < time(): 77 | ses = api_client.create_session() 78 | ses_id = str(ses.id) 79 | expires = str(ses.expires) 80 | sql.set_ses(chat_id, ses_id, expires) 81 | sesh, exp = sql.get_ses(chat_id) 82 | except ValueError: 83 | pass 84 | try: 85 | context.bot.send_chat_action(chat_id, action='typing') 86 | rep = api_client.think_thought(sesh, query) 87 | sleep(0.3) 88 | msg.reply_text(rep, timeout=60) 89 | except CFError as e: 90 | context.bot.send_message( 91 | OWNER_ID, f"Chatbot error: {e} occurred in {chat_id}!") 92 | 93 | 94 | @run_async 95 | def list_chatbot(update: Update, context: CallbackContext): 96 | chats = sql.get_all_chats() 97 | text = "AI-Enabled Chats\n" 98 | for chat in chats: 99 | try: 100 | x = context.bot.get_chat(int(*chat)) 101 | name = x.title if x.title else x.first_name 102 | text += f"• {name}\n" 103 | except BadRequest: 104 | sql.rem_chat(*chat) 105 | except Unauthorized: 106 | sql.rem_chat(*chat) 107 | except RetryAfter as e: 108 | sleep(e.retry_after) 109 | update.effective_message.reply_text(text, parse_mode="HTML") 110 | 111 | 112 | __mod_name__ = "Chatbot" 113 | 114 | __help__ = """ 115 | Chatbot utilizes the CoffeeHouse API and allows Lynda to talk back making your chat more interactive. 116 | This is an ongoing upgrade and is only available in your chats if you reach out to @YorktownEagleUnion and ask for it. 117 | In future we might make it open for any chat and controllable by group admins. 118 | Powered by CoffeeHouse (https://coffeehouse.intellivoid.net/) from @Intellivoid 119 | 120 | ──「 *Commands* 」── 121 | -> `/addchat` 122 | Enables Chatbot mode in the chat. 123 | -> `/rmchat` 124 | Disables Chatbot mode in the chat. 125 | 126 | ──「 *Nation Level Required:* 」── 127 | -> `/listai` 128 | Lists the chats the chatmode is enabled in. 129 | """ 130 | 131 | ADD_CHAT_HANDLER = CommandHandler("addchat", add_chat) 132 | REMOVE_CHAT_HANDLER = CommandHandler("rmchat", remove_chat) 133 | CHATBOT_HANDLER = MessageHandler(Filters.text & (~Filters.regex( 134 | r"^#[^\s]+") & ~Filters.regex(r"^!") & ~Filters.regex(r"^s\/")), chatbot) 135 | CHATBOTLIST_HANDLER = CommandHandler("listai", list_chatbot, filters=CustomFilters.dev_filter) 136 | # Filters for ignoring #note messages, !commands and sed. 137 | 138 | dispatcher.add_handler(ADD_CHAT_HANDLER) 139 | dispatcher.add_handler(REMOVE_CHAT_HANDLER) 140 | dispatcher.add_handler(CHATBOT_HANDLER) 141 | dispatcher.add_handler(CHATBOTLIST_HANDLER) 142 | -------------------------------------------------------------------------------- /lynda/modules/fun.py: -------------------------------------------------------------------------------- 1 | # D A N K modules by @deletescape vvv 2 | # based on 3 | # https://github.com/wrxck/mattata/blob/master/plugins/copypasta.mattata 4 | import html 5 | import random 6 | import time 7 | import requests 8 | import re 9 | import string 10 | import asyncio 11 | from io import BytesIO 12 | import os 13 | from pathlib import Path 14 | 15 | from telegram import Update, ParseMode, Message 16 | from telegram.ext import run_async, CallbackContext 17 | 18 | import lynda.modules.fun_strings as fun_strings 19 | from lynda import dispatcher 20 | from lynda.modules.disable import DisableAbleCommandHandler 21 | from lynda.modules.helper_funcs.chat_status import is_user_admin 22 | from lynda.modules.helper_funcs.extraction import extract_user 23 | 24 | 25 | @run_async 26 | def slap(update: Update, context: CallbackContext): 27 | args = context.args 28 | message = update.effective_message 29 | chat = update.effective_chat 30 | reply_text = message.reply_to_message.reply_text if message.reply_to_message else message.reply_text 31 | curr_user = html.escape(message.from_user.first_name) 32 | user_id = extract_user(message, args) 33 | 34 | if user_id == context.bot.id: 35 | temp = random.choice(fun_strings.SLAP_LYNDA_TEMPLATES) 36 | 37 | if isinstance(temp, list): 38 | if temp[2] == "tmute": 39 | if is_user_admin(chat, message.from_user.id): 40 | reply_text(temp[1]) 41 | return 42 | 43 | mutetime = int(time.time() + 60) 44 | context.bot.restrict_chat_member( 45 | chat.id, 46 | message.from_user.id, 47 | until_date=mutetime, 48 | can_send_messages=False) 49 | reply_text(temp[0]) 50 | else: 51 | reply_text(temp) 52 | return 53 | 54 | if user_id: 55 | slapped_user = context.bot.get_chat(user_id) 56 | user1 = curr_user 57 | user2 = html.escape(slapped_user.first_name) 58 | else: 59 | user1 = context.bot.first_name 60 | user2 = curr_user 61 | 62 | temp = random.choice(fun_strings.SLAP_TEMPLATES) 63 | item = random.choice(fun_strings.ITEMS) 64 | hit = random.choice(fun_strings.HIT) 65 | throw = random.choice(fun_strings.THROW) 66 | 67 | reply = temp.format( 68 | user1=user1, 69 | user2=user2, 70 | item=item, 71 | hits=hit, 72 | throws=throw) 73 | 74 | reply_text(reply, parse_mode=ParseMode.HTML) 75 | 76 | 77 | @run_async 78 | def pat(update: Update, _): 79 | msg = update.effective_message 80 | pat = requests.get("https://some-random-api.ml/animu/pat").json() 81 | link = pat.get("link") 82 | if not link: 83 | msg.reply_text("No URL was received from the API!") 84 | return 85 | msg.reply_video(link) 86 | 87 | 88 | @run_async 89 | def hug(update: Update, _): 90 | msg = update.effective_message 91 | hug = requests.get("https://some-random-api.ml/animu/hug").json() 92 | link = hug.get("link") 93 | if not link: 94 | msg.reply_text("No URL was received from the API!") 95 | return 96 | msg.reply_video(link) 97 | 98 | 99 | @run_async 100 | def insult(update: Update, _): 101 | msg = update.effective_message 102 | reply_text = msg.reply_to_message.reply_text if msg.reply_to_message else msg.reply_text 103 | reply_text(random.choice(fun_strings.INSULT_STRINGS)) 104 | 105 | 106 | @run_async 107 | def shrug(update: Update, _): 108 | msg = update.effective_message 109 | reply_text = msg.reply_to_message.reply_text if msg.reply_to_message else msg.reply_text 110 | reply_text(r"¯\_(ツ)_/¯") 111 | 112 | 113 | @run_async 114 | def table(update: Update, _): 115 | reply_text = update.effective_message.reply_to_message.reply_text if update.effective_message.reply_to_message else update.effective_message.reply_text 116 | reply_text(random.choice(fun_strings.TABLE)) 117 | 118 | 119 | __help__ = """ 120 | -> `/slap` 121 | slap a user, or get slapped if not a reply. 122 | -> `/shrug` 123 | get shrug XD. 124 | -> `/table` 125 | get flip/unflip :v. 126 | -> `/insult` 127 | Insults the retar 128 | -> `/pat` 129 | pats a user by a reply to the message 130 | -> `/hug` 131 | hugs a user by a reply to the message 132 | """ 133 | 134 | 135 | PAT_HANDLER = DisableAbleCommandHandler("pat", pat) 136 | HUG_HANDLER = DisableAbleCommandHandler("hug", hug) 137 | SLAP_HANDLER = DisableAbleCommandHandler("slap", slap, pass_args=True) 138 | SHRUG_HANDLER = DisableAbleCommandHandler("shrug", shrug) 139 | TABLE_HANDLER = DisableAbleCommandHandler("table", table) 140 | INSULT_HANDLER = DisableAbleCommandHandler("insult", insult) 141 | 142 | dispatcher.add_handler(SLAP_HANDLER) 143 | dispatcher.add_handler(SHRUG_HANDLER) 144 | dispatcher.add_handler(TABLE_HANDLER) 145 | dispatcher.add_handler(INSULT_HANDLER) 146 | dispatcher.add_handler(PAT_HANDLER) 147 | dispatcher.add_handler(HUG_HANDLER) 148 | 149 | __mod_name__ = "Fun" 150 | 151 | __command_list__ = [ 152 | "slap", 153 | "shrug", 154 | "table", 155 | "insult", 156 | "pat", 157 | "hug"] 158 | 159 | __handlers__ = [ 160 | SLAP_HANDLER, 161 | SHRUG_HANDLER, 162 | TABLE_HANDLER, 163 | INSULT_HANDLER, 164 | PAT_HANDLER, 165 | HUG_HANDLER] 166 | -------------------------------------------------------------------------------- /lynda/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 | 4 | from sqlalchemy import Column, String, Boolean, UnicodeText, Integer, func, distinct 5 | 6 | from lynda.modules.helper_funcs.msg_types import Types 7 | from lynda.modules.sql import SESSION, BASE 8 | 9 | 10 | class Notes(BASE): 11 | __tablename__ = "notes" 12 | chat_id = Column(String(14), primary_key=True) 13 | name = Column(UnicodeText, primary_key=True) 14 | value = Column(UnicodeText, nullable=False) 15 | file = Column(UnicodeText) 16 | is_reply = Column(Boolean, default=False) 17 | has_buttons = Column(Boolean, default=False) 18 | msgtype = Column(Integer, default=Types.BUTTON_TEXT.value) 19 | 20 | def __init__(self, chat_id, name, value, msgtype, file=None): 21 | self.chat_id = str(chat_id) # ensure string 22 | self.name = name 23 | self.value = value 24 | self.msgtype = msgtype 25 | self.file = file 26 | 27 | def __repr__(self): 28 | return "" % self.name 29 | 30 | 31 | class Buttons(BASE): 32 | __tablename__ = "note_urls" 33 | id = Column(Integer, primary_key=True, autoincrement=True) 34 | chat_id = Column(String(14), primary_key=True) 35 | note_name = Column(UnicodeText, primary_key=True) 36 | name = Column(UnicodeText, nullable=False) 37 | url = Column(UnicodeText, nullable=False) 38 | same_line = Column(Boolean, default=False) 39 | 40 | def __init__(self, chat_id, note_name, name, url, same_line=False): 41 | self.chat_id = str(chat_id) 42 | self.note_name = note_name 43 | self.name = name 44 | self.url = url 45 | self.same_line = same_line 46 | 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 | 55 | def add_note_to_db(chat_id, note_name, note_data, msgtype, buttons=None, file=None): 56 | if not buttons: 57 | buttons = [] 58 | 59 | with NOTES_INSERTION_LOCK: 60 | prev = SESSION.query(Notes).get((str(chat_id), note_name)) 61 | if prev: 62 | with BUTTONS_INSERTION_LOCK: 63 | prev_buttons = SESSION.query(Buttons).filter(Buttons.chat_id == str(chat_id), 64 | Buttons.note_name == note_name).all() 65 | for btn in prev_buttons: 66 | SESSION.delete(btn) 67 | SESSION.delete(prev) 68 | note = Notes(str(chat_id), note_name, note_data or "", msgtype=msgtype.value, file=file) 69 | SESSION.add(note) 70 | SESSION.commit() 71 | 72 | for b_name, url, same_line in buttons: 73 | add_note_button_to_db(chat_id, note_name, b_name, url, same_line) 74 | 75 | 76 | def get_note(chat_id, note_name): 77 | try: 78 | return SESSION.query(Notes).get((str(chat_id), note_name)) 79 | finally: 80 | SESSION.close() 81 | 82 | 83 | def rm_note(chat_id, note_name): 84 | with NOTES_INSERTION_LOCK: 85 | note = SESSION.query(Notes).get((str(chat_id), note_name)) 86 | if note: 87 | with BUTTONS_INSERTION_LOCK: 88 | buttons = SESSION.query(Buttons).filter(Buttons.chat_id == str(chat_id), 89 | Buttons.note_name == note_name).all() 90 | for btn in buttons: 91 | SESSION.delete(btn) 92 | 93 | SESSION.delete(note) 94 | SESSION.commit() 95 | return True 96 | 97 | else: 98 | SESSION.close() 99 | return False 100 | 101 | 102 | def get_all_chat_notes(chat_id): 103 | try: 104 | return SESSION.query(Notes).filter(Notes.chat_id == str(chat_id)).order_by(Notes.name.asc()).all() 105 | finally: 106 | SESSION.close() 107 | 108 | 109 | def add_note_button_to_db(chat_id, note_name, b_name, url, same_line): 110 | with BUTTONS_INSERTION_LOCK: 111 | button = Buttons(chat_id, note_name, b_name, url, same_line) 112 | SESSION.add(button) 113 | SESSION.commit() 114 | 115 | 116 | def get_buttons(chat_id, note_name): 117 | try: 118 | return SESSION.query(Buttons).filter(Buttons.chat_id == str(chat_id), Buttons.note_name == note_name).order_by( 119 | Buttons.id).all() 120 | finally: 121 | SESSION.close() 122 | 123 | 124 | def num_notes(): 125 | try: 126 | return SESSION.query(Notes).count() 127 | finally: 128 | SESSION.close() 129 | 130 | 131 | def num_chats(): 132 | try: 133 | return SESSION.query(func.count(distinct(Notes.chat_id))).scalar() 134 | finally: 135 | SESSION.close() 136 | 137 | 138 | def migrate_chat(old_chat_id, new_chat_id): 139 | with NOTES_INSERTION_LOCK: 140 | chat_notes = SESSION.query(Notes).filter(Notes.chat_id == str(old_chat_id)).all() 141 | for note in chat_notes: 142 | note.chat_id = str(new_chat_id) 143 | 144 | with BUTTONS_INSERTION_LOCK: 145 | chat_buttons = SESSION.query(Buttons).filter(Buttons.chat_id == str(old_chat_id)).all() 146 | for btn in chat_buttons: 147 | btn.chat_id = str(new_chat_id) 148 | 149 | SESSION.commit() 150 | -------------------------------------------------------------------------------- /lynda/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 lynda import LOGGER 7 | from lynda.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 | ent = entities[0] if entities else None 38 | # if entity offset matches (command end/text start) then all good 39 | if entities and ent and ent.offset == len(message.text) - len(text_to_parse): 40 | ent = entities[0] 41 | user_id = ent.user.id 42 | text = message.text[ent.offset + ent.length:] 43 | 44 | elif len(args) >= 1 and args[0][0] == '@': 45 | user = args[0] 46 | user_id = get_user_id(user) 47 | if not user_id: 48 | message.reply_text("No idea who this user is. You'll be able to interact with them if " 49 | "you reply to that person's message instead, or forward one of that user's messages.") 50 | return None, None 51 | 52 | else: 53 | user_id = user_id 54 | res = message.text.split(None, 2) 55 | if len(res) >= 3: 56 | text = res[2] 57 | 58 | elif len(args) >= 1 and args[0].isdigit(): 59 | user_id = int(args[0]) 60 | res = message.text.split(None, 2) 61 | if len(res) >= 3: 62 | text = res[2] 63 | 64 | elif prev_message: 65 | user_id, text = id_from_reply(message) 66 | 67 | else: 68 | return None, None 69 | 70 | try: 71 | message.bot.get_chat(user_id) 72 | except BadRequest as excp: 73 | if excp.message in ("User_id_invalid", "Chat not found"): 74 | message.reply_text("I don't seem to have interacted with this user before - please forward a message from " 75 | "them to give me control! (like a voodoo doll, I need a piece of them to be able " 76 | "to execute certain commands...)") 77 | else: 78 | LOGGER.exception("Exception %s on user %s", excp.message, user_id) 79 | 80 | return None, None 81 | 82 | return user_id, text 83 | 84 | 85 | def extract_text(message) -> str: 86 | return message.text or message.caption or (message.sticker.emoji if message.sticker else None) 87 | 88 | 89 | def extract_unt_fedban(message: Message, args: List[str]) -> (Optional[int], Optional[str]): 90 | prev_message = message.reply_to_message 91 | split_text = message.text.split(None, 1) 92 | 93 | if len(split_text) < 2: 94 | return id_from_reply(message) # only option possible 95 | 96 | text_to_parse = split_text[1] 97 | 98 | text = "" 99 | 100 | entities = list(message.parse_entities([MessageEntity.TEXT_MENTION])) 101 | ent = entities[0] if entities else None 102 | # if entity offset matches (command end/text start) then all good 103 | if entities and ent and ent.offset == len(message.text) - len(text_to_parse): 104 | ent = entities[0] 105 | user_id = ent.user.id 106 | text = message.text[ent.offset + ent.length:] 107 | 108 | elif len(args) >= 1 and args[0][0] == '@': 109 | user = args[0] 110 | user_id = get_user_id(user) 111 | if not user_id and not isinstance(user_id, int): 112 | message.reply_text( 113 | "Saya tidak memiliki pengguna di db saya. Anda akan dapat berinteraksi dengan mereka jika " 114 | "Anda membalas pesan orang itu, atau meneruskan salah satu dari pesan pengguna itu.") 115 | return None, None 116 | 117 | else: 118 | user_id = user_id 119 | res = message.text.split(None, 2) 120 | if len(res) >= 3: 121 | text = res[2] 122 | 123 | elif len(args) >= 1 and args[0].isdigit(): 124 | user_id = int(args[0]) 125 | res = message.text.split(None, 2) 126 | if len(res) >= 3: 127 | text = res[2] 128 | 129 | elif prev_message: 130 | user_id, text = id_from_reply(message) 131 | 132 | else: 133 | return None, None 134 | 135 | try: 136 | message.bot.get_chat(user_id) 137 | except BadRequest as excp: 138 | if excp.message in ("User_id_invalid", "Chat not found") and not isinstance(user_id, int): 139 | message.reply_text("Saya sepertinya tidak pernah berinteraksi dengan pengguna ini " 140 | "sebelumnya - silakan meneruskan pesan dari mereka untuk memberi saya kontrol! " 141 | "(Seperti boneka voodoo, saya butuh sepotong untuk bisa" 142 | "untuk menjalankan perintah tertentu...)") 143 | return None, None 144 | elif excp.message != "Chat not found": 145 | LOGGER.exception("Exception %s on user %s", excp.message, user_id) 146 | return None, None 147 | elif not isinstance(user_id, int): 148 | return None, None 149 | 150 | return user_id, text 151 | 152 | 153 | def extract_user_fban(message: Message, args: List[str]) -> Optional[int]: 154 | return extract_unt_fedban(message, args)[0] 155 | -------------------------------------------------------------------------------- /lynda/modules/gtranslator.py: -------------------------------------------------------------------------------- 1 | from emoji import UNICODE_EMOJI 2 | from googletrans import Translator, LANGUAGES 3 | from telegram import Update, ParseMode 4 | from telegram.ext import run_async, CallbackContext 5 | 6 | from lynda import dispatcher 7 | from lynda.modules.disable import DisableAbleCommandHandler 8 | 9 | 10 | @run_async 11 | def totranslate(update: Update, _): 12 | msg = update.effective_message 13 | problem_lang_code = [key for key in LANGUAGES if "-" in key] 14 | try: 15 | if msg.reply_to_message and msg.reply_to_message.text: 16 | 17 | args = update.effective_message.text.split(None, 1) 18 | text = msg.reply_to_message.text 19 | message = update.effective_message 20 | dest_lang = None 21 | 22 | try: 23 | source_lang = args[1].split(None, 1)[0] 24 | except Exception: 25 | source_lang = "en" 26 | 27 | if source_lang.count('-') == 2: 28 | for lang in problem_lang_code: 29 | if lang in source_lang: 30 | if source_lang.startswith(lang): 31 | dest_lang = source_lang.rsplit("-", 1)[1] 32 | source_lang = source_lang.rsplit("-", 1)[0] 33 | else: 34 | dest_lang = source_lang.split("-", 1)[1] 35 | source_lang = source_lang.split("-", 1)[0] 36 | elif source_lang.count('-') == 1: 37 | for lang in problem_lang_code: 38 | if lang in source_lang: 39 | dest_lang = source_lang 40 | source_lang = None 41 | break 42 | if dest_lang is None: 43 | dest_lang = source_lang.split("-")[1] 44 | source_lang = source_lang.split("-")[0] 45 | else: 46 | dest_lang = source_lang 47 | source_lang = None 48 | 49 | exclude_list = UNICODE_EMOJI.keys() 50 | for emoji in exclude_list: 51 | if emoji in text: 52 | text = text.replace(emoji, '') 53 | 54 | trl = Translator() 55 | if source_lang is None: 56 | detection = trl.detect(text) 57 | tekstr = trl.translate(text, dest=dest_lang) 58 | return message.reply_text( 59 | f"Translated from `{detection.lang}` to `{dest_lang}`:\n`{tekstr.text}`", 60 | parse_mode=ParseMode.MARKDOWN) 61 | else: 62 | tekstr = trl.translate(text, dest=dest_lang, src=source_lang) 63 | message.reply_text( 64 | f"Translated from `{source_lang}` to `{dest_lang}`:\n`{tekstr.text}`", 65 | parse_mode=ParseMode.MARKDOWN) 66 | else: 67 | args = update.effective_message.text.split(None, 2) 68 | message = update.effective_message 69 | source_lang = args[1] 70 | text = args[2] 71 | exclude_list = UNICODE_EMOJI.keys() 72 | for emoji in exclude_list: 73 | if emoji in text: 74 | text = text.replace(emoji, '') 75 | dest_lang = None 76 | temp_source_lang = source_lang 77 | if temp_source_lang.count('-') == 2: 78 | for lang in problem_lang_code: 79 | if lang in temp_source_lang: 80 | if temp_source_lang.startswith(lang): 81 | dest_lang = temp_source_lang.rsplit("-", 1)[1] 82 | source_lang = temp_source_lang.rsplit("-", 1)[0] 83 | else: 84 | dest_lang = temp_source_lang.split("-", 1)[1] 85 | source_lang = temp_source_lang.split("-", 1)[0] 86 | elif temp_source_lang.count('-') == 1: 87 | for lang in problem_lang_code: 88 | if lang in temp_source_lang: 89 | dest_lang = None 90 | else: 91 | dest_lang = temp_source_lang.split("-")[1] 92 | source_lang = temp_source_lang.split("-")[0] 93 | trl = Translator() 94 | if dest_lang is None: 95 | detection = trl.detect(text) 96 | tekstr = trl.translate(text, dest=source_lang) 97 | return message.reply_text("Translated from `{}` to `{}`:\n`{}`".format( 98 | detection.lang, source_lang, tekstr.text), parse_mode=ParseMode.MARKDOWN) 99 | else: 100 | tekstr = trl.translate(text, dest=dest_lang, src=source_lang) 101 | message.reply_text( 102 | "Translated from `{}` to `{}`:\n`{}`".format( 103 | source_lang, 104 | dest_lang, 105 | tekstr.text), 106 | parse_mode=ParseMode.MARKDOWN) 107 | 108 | except IndexError: 109 | update.effective_message.reply_text( 110 | "Reply to messages or write messages from other languages ​​for translating into the intended language\n\n" 111 | "Example: `/tr en ml` to translate from English to Malayalam\n" 112 | "Or use: `/tr ml` for automatic detection and translating it into Malayalam.\n" 113 | "See [List of Language Codes](t.me/OnePunchSupport/12823) for a list of language codes.", 114 | parse_mode="markdown", disable_web_page_preview=True) 115 | except ValueError: 116 | update.effective_message.reply_text( 117 | "The intended language is not found!") 118 | else: 119 | return 120 | 121 | 122 | __help__ = """ 123 | -> `/tr` (language code) 124 | Translates Languages to a desired Language code. 125 | """ 126 | 127 | TRANSLATE_HANDLER = DisableAbleCommandHandler("tr", totranslate) 128 | 129 | dispatcher.add_handler(TRANSLATE_HANDLER) 130 | 131 | __mod_name__ = "Translate" 132 | __command_list__ = ["tr"] 133 | __handlers__ = [TRANSLATE_HANDLER] 134 | -------------------------------------------------------------------------------- /lynda/modules/sql/users_sql.py: -------------------------------------------------------------------------------- 1 | import threading 2 | 3 | from sqlalchemy import Column, Integer, UnicodeText, String, ForeignKey, UniqueConstraint, func 4 | 5 | from lynda import dispatcher 6 | from lynda.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 | 188 | 189 | def rem_chat(chat_id): 190 | with INSERTION_LOCK: 191 | chat = SESSION.query(Chats).get(str(chat_id)) 192 | if chat: 193 | SESSION.delete(chat) 194 | SESSION.commit() 195 | else: 196 | SESSION.close() 197 | -------------------------------------------------------------------------------- /lynda/modules/userinfo.py: -------------------------------------------------------------------------------- 1 | import html 2 | from typing import List 3 | 4 | from telegram import Update, ParseMode, MAX_MESSAGE_LENGTH 5 | from telegram.ext.dispatcher import run_async, CallbackContext 6 | from telegram.utils.helpers import escape_markdown 7 | 8 | import lynda.modules.sql.userinfo_sql as sql 9 | from lynda import dispatcher, SUDO_USERS, DEV_USERS 10 | from lynda.modules.disable import DisableAbleCommandHandler 11 | from lynda.modules.helper_funcs.extraction import extract_user 12 | 13 | 14 | @run_async 15 | def about_me(update: Update, context: CallbackContext): 16 | args = context.args 17 | bot = context.bot 18 | message = update.effective_message 19 | user_id = extract_user(message, args) 20 | 21 | user = bot.get_chat(user_id) if user_id else message.from_user 22 | info = sql.get_user_me_info(user.id) 23 | 24 | if info: 25 | update.effective_message.reply_text( 26 | f"*{user.first_name}*:\n{escape_markdown(info)}", 27 | parse_mode=ParseMode.MARKDOWN) 28 | elif message.reply_to_message: 29 | username = message.reply_to_message.from_user.first_name 30 | update.effective_message.reply_text( 31 | f"{username} hasn't set an info message about themselves yet!") 32 | else: 33 | update.effective_message.reply_text( 34 | "You haven't set an info message about yourself yet!") 35 | 36 | 37 | @run_async 38 | def set_about_me(update: Update, context: CallbackContext): 39 | bot = context.bot 40 | message = update.effective_message 41 | user_id = message.from_user.id 42 | if message.reply_to_message: 43 | repl_message = message.reply_to_message 44 | repl_user_id = repl_message.from_user.id 45 | if repl_user_id == bot.id and ( 46 | user_id in SUDO_USERS or user_id in DEV_USERS): 47 | user_id = repl_user_id 48 | 49 | text = message.text 50 | info = text.split(None, 1) 51 | 52 | if len(info) == 2: 53 | if len(info[1]) < MAX_MESSAGE_LENGTH // 4: 54 | sql.set_user_me_info(user_id, info[1]) 55 | if user_id == bot.id: 56 | message.reply_text("Updated my info!") 57 | else: 58 | message.reply_text("Updated your info!") 59 | else: 60 | message.reply_text("The info needs to be under {} characters! You have {}.".format( 61 | MAX_MESSAGE_LENGTH // 4, len(info[1]))) 62 | 63 | 64 | @run_async 65 | def about_bio(update: Update, context: CallbackContext): 66 | args = context.args 67 | bot = context.bot 68 | message = update.effective_message 69 | 70 | user_id = extract_user(message, args) 71 | user = bot.get_chat(user_id) if user_id else message.from_user 72 | info = sql.get_user_bio(user.id) 73 | 74 | if info: 75 | update.effective_message.reply_text( 76 | "*{}*:\n{}".format(user.first_name, escape_markdown(info)), parse_mode=ParseMode.MARKDOWN) 77 | elif message.reply_to_message: 78 | username = user.first_name 79 | update.effective_message.reply_text( 80 | f"{username} hasn't had a message set about themselves yet!") 81 | else: 82 | update.effective_message.reply_text( 83 | "You haven't had a bio set about yourself yet!") 84 | message = update.effective_message 85 | if message.reply_to_message: 86 | repl_message = message.reply_to_message 87 | user_id = repl_message.from_user.id 88 | 89 | if user_id == message.from_user.id: 90 | message.reply_text( 91 | "Ha, you can't set your own bio! You're at the mercy of others here...") 92 | return 93 | 94 | sender_id = update.effective_user.id 95 | 96 | if user_id == bot.id and sender_id not in SUDO_USERS and sender_id not in DEV_USERS: 97 | message.reply_text( 98 | "Erm... yeah, I only trust sudo users or developers to set my bio.") 99 | return 100 | 101 | text = message.text 102 | # use python's maxsplit to only remove the cmd, hence keeping newlines. 103 | bio = text.split(None, 1) 104 | 105 | if len(bio) == 2: 106 | if len(bio[1]) < MAX_MESSAGE_LENGTH // 4: 107 | sql.set_user_bio(user_id, bio[1]) 108 | message.reply_text( 109 | "Updated {}'s bio!".format( 110 | repl_message.from_user.first_name)) 111 | else: 112 | message.reply_text( 113 | "A bio needs to be under {} characters! You tried to set {}.".format( 114 | MAX_MESSAGE_LENGTH // 4, len(bio[1]))) 115 | else: 116 | message.reply_text("Reply to someone's message to set their bio!") 117 | 118 | 119 | def __user_info__(user_id): 120 | bio = html.escape(sql.get_user_bio(user_id) or "") 121 | me = html.escape(sql.get_user_me_info(user_id) or "") 122 | if bio and me: 123 | return f"\nAbout user:\n{me}\nWhat others say:\n{bio}\n" 124 | elif bio: 125 | return f"\nWhat others say:\n{bio}\n" 126 | elif me: 127 | return f"\nAbout user:\n{me}\n" 128 | else: 129 | return "\n" 130 | 131 | 132 | __help__ = """ 133 | -> `/bio` 134 | will get your or another user's bio. This cannot be set by yourself. 135 | -> `/setme` 136 | will set your info 137 | -> `/me` 138 | will get your or another user's info 139 | """ 140 | 141 | # SET_BIO_HANDLER = DisableAbleCommandHandler("setbio", set_about_bio) 142 | GET_BIO_HANDLER = DisableAbleCommandHandler("bio", about_bio, pass_args=True) 143 | 144 | SET_ABOUT_HANDLER = DisableAbleCommandHandler("setme", set_about_me) 145 | GET_ABOUT_HANDLER = DisableAbleCommandHandler("me", about_me, pass_args=True) 146 | 147 | # dispatcher.add_handler(SET_BIO_HANDLER) 148 | dispatcher.add_handler(GET_BIO_HANDLER) 149 | dispatcher.add_handler(SET_ABOUT_HANDLER) 150 | dispatcher.add_handler(GET_ABOUT_HANDLER) 151 | 152 | __mod_name__ = "Bios" 153 | __command_list__ = ["bio", "setme", "me"] 154 | __handlers__ = [ 155 | # SET_BIO_HANDLER, 156 | GET_BIO_HANDLER, 157 | SET_ABOUT_HANDLER, 158 | GET_ABOUT_HANDLER] 159 | -------------------------------------------------------------------------------- /lynda/modules/sql/blsticker_sql.py: -------------------------------------------------------------------------------- 1 | import threading 2 | 3 | from sqlalchemy import func, distinct, Column, String, UnicodeText, Integer 4 | 5 | from lynda.modules.sql import SESSION, BASE 6 | 7 | 8 | class StickersFilters(BASE): 9 | __tablename__ = "blacklist_stickers" 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, StickersFilters) 22 | and self.chat_id == other.chat_id 23 | and self.trigger == other.trigger) 24 | 25 | class StickerSettings(BASE): 26 | __tablename__ = "blsticker_settings" 27 | chat_id = Column(String(14), primary_key=True) 28 | blacklist_type = Column(Integer, default=1) 29 | value = Column(UnicodeText, default="0") 30 | 31 | def __init__(self, chat_id, blacklist_type=1, value="0"): 32 | self.chat_id = str(chat_id) 33 | self.blacklist_type = blacklist_type 34 | self.value = value 35 | 36 | def __repr__(self): 37 | return "<{} will executing {} for blacklist trigger.>".format(self.chat_id, self.blacklist_type) 38 | 39 | 40 | StickersFilters.__table__.create(checkfirst=True) 41 | StickerSettings.__table__.create(checkfirst=True) 42 | 43 | STICKERS_FILTER_INSERTION_LOCK = threading.RLock() 44 | STICKSET_FILTER_INSERTION_LOCK = threading.RLock() 45 | 46 | CHAT_STICKERS = {} 47 | CHAT_BLSTICK_BLACKLISTS = {} 48 | 49 | 50 | def add_to_stickers(chat_id, trigger): 51 | with STICKERS_FILTER_INSERTION_LOCK: 52 | stickers_filt = StickersFilters(str(chat_id), trigger) 53 | 54 | SESSION.merge(stickers_filt) # merge to avoid duplicate key issues 55 | SESSION.commit() 56 | global CHAT_STICKERS 57 | if CHAT_STICKERS.get(str(chat_id), set()) == set(): 58 | CHAT_STICKERS[str(chat_id)] = {trigger} 59 | else: 60 | CHAT_STICKERS.get(str(chat_id), set()).add(trigger) 61 | 62 | 63 | def rm_from_stickers(chat_id, trigger): 64 | with STICKERS_FILTER_INSERTION_LOCK: 65 | stickers_filt = SESSION.query(StickersFilters).get((str(chat_id), trigger)) 66 | if stickers_filt: 67 | if trigger in CHAT_STICKERS.get(str(chat_id), set()): # sanity check 68 | CHAT_STICKERS.get(str(chat_id), set()).remove(trigger) 69 | 70 | SESSION.delete(stickers_filt) 71 | SESSION.commit() 72 | return True 73 | 74 | SESSION.close() 75 | return False 76 | 77 | 78 | def get_chat_stickers(chat_id): 79 | return CHAT_STICKERS.get(str(chat_id), set()) 80 | 81 | 82 | def num_stickers_filters(): 83 | try: 84 | return SESSION.query(StickersFilters).count() 85 | finally: 86 | SESSION.close() 87 | 88 | 89 | def num_stickers_chat_filters(chat_id): 90 | try: 91 | return SESSION.query(StickersFilters.chat_id).filter(StickersFilters.chat_id == str(chat_id)).count() 92 | finally: 93 | SESSION.close() 94 | 95 | 96 | def num_stickers_filter_chats(): 97 | try: 98 | return SESSION.query(func.count(distinct(StickersFilters.chat_id))).scalar() 99 | finally: 100 | SESSION.close() 101 | 102 | 103 | def set_blacklist_strength(chat_id, blacklist_type, value): 104 | # for blacklist_type 105 | # 0 = nothing 106 | # 1 = delete 107 | # 2 = warn 108 | # 3 = mute 109 | # 4 = kick 110 | # 5 = ban 111 | # 6 = tban 112 | # 7 = tmute 113 | with STICKSET_FILTER_INSERTION_LOCK: 114 | global CHAT_BLSTICK_BLACKLISTS 115 | curr_setting = SESSION.query(StickerSettings).get(str(chat_id)) 116 | if not curr_setting: 117 | curr_setting = StickerSettings(chat_id, blacklist_type=int(blacklist_type), value=value) 118 | 119 | curr_setting.blacklist_type = int(blacklist_type) 120 | curr_setting.value = str(value) 121 | CHAT_BLSTICK_BLACKLISTS[str(chat_id)] = {'blacklist_type': int(blacklist_type), 'value': value} 122 | 123 | SESSION.add(curr_setting) 124 | SESSION.commit() 125 | 126 | def get_blacklist_setting(chat_id): 127 | try: 128 | setting = CHAT_BLSTICK_BLACKLISTS.get(str(chat_id)) 129 | if setting: 130 | return setting['blacklist_type'], setting['value'] 131 | else: 132 | return 1, "0" 133 | 134 | finally: 135 | SESSION.close() 136 | 137 | 138 | def __load_CHAT_STICKERS(): 139 | global CHAT_STICKERS 140 | try: 141 | chats = SESSION.query(StickersFilters.chat_id).distinct().all() 142 | for (chat_id,) in chats: # remove tuple by ( ,) 143 | CHAT_STICKERS[chat_id] = [] 144 | 145 | all_filters = SESSION.query(StickersFilters).all() 146 | for x in all_filters: 147 | CHAT_STICKERS[x.chat_id] += [x.trigger] 148 | 149 | CHAT_STICKERS = {x: set(y) for x, y in CHAT_STICKERS.items()} 150 | 151 | finally: 152 | SESSION.close() 153 | 154 | 155 | def __load_chat_stickerset_blacklists(): 156 | global CHAT_BLSTICK_BLACKLISTS 157 | try: 158 | chats_settings = SESSION.query(StickerSettings).all() 159 | for x in chats_settings: # remove tuple by ( ,) 160 | CHAT_BLSTICK_BLACKLISTS[x.chat_id] = {'blacklist_type': x.blacklist_type, 'value': x.value} 161 | 162 | finally: 163 | SESSION.close() 164 | 165 | def migrate_chat(old_chat_id, new_chat_id): 166 | with STICKERS_FILTER_INSERTION_LOCK: 167 | chat_filters = SESSION.query(StickersFilters).filter(StickersFilters.chat_id == str(old_chat_id)).all() 168 | for filt in chat_filters: 169 | filt.chat_id = str(new_chat_id) 170 | SESSION.commit() 171 | 172 | def add_to_blacklist(chat_id, trigger): 173 | with BLACKLIST_FILTER_INSERTION_LOCK: 174 | blacklist_filt = BlackListFilters(str(chat_id), trigger) 175 | 176 | SESSION.merge(blacklist_filt) # merge to avoid duplicate key issues 177 | SESSION.commit() 178 | CHAT_BLACKLISTS.setdefault(str(chat_id), set()).add(trigger) 179 | 180 | __load_CHAT_STICKERS() 181 | __load_chat_stickerset_blacklists() -------------------------------------------------------------------------------- /lynda/modules/sql/cleaner_sql.py: -------------------------------------------------------------------------------- 1 | import threading 2 | 3 | from sqlalchemy import Column, UnicodeText, Boolean 4 | 5 | from lynda.modules.sql import BASE, SESSION 6 | 7 | 8 | class CleanerBlueTextChatSettings(BASE): 9 | __tablename__ = "cleaner_bluetext_chat_setting" 10 | chat_id = Column(UnicodeText, primary_key=True) 11 | is_enable = Column(Boolean, default=False) 12 | def __init__(self, chat_id, is_enable): 13 | self.chat_id = chat_id 14 | self.is_enable = is_enable 15 | 16 | def __repr__(self): 17 | return "clean blue text for {}".format(self.chat_id) 18 | 19 | class CleanerBlueTextChat(BASE): 20 | __tablename__ = "cleaner_bluetext_chat_ignore_commands" 21 | chat_id = Column(UnicodeText, primary_key=True) 22 | command = Column(UnicodeText, primary_key=True) 23 | def __init__(self, chat_id, command): 24 | self.chat_id = chat_id 25 | self.command = command 26 | 27 | 28 | class CleanerBlueTextGlobal(BASE): 29 | __tablename__ = "cleaner_bluetext_global_ignore_commands" 30 | command = Column(UnicodeText, primary_key=True) 31 | 32 | def __init__(self, command): 33 | self.command = command 34 | 35 | 36 | CleanerBlueTextChatSettings.__table__.create(checkfirst=True) 37 | CleanerBlueTextChat.__table__.create(checkfirst=True) 38 | CleanerBlueTextGlobal.__table__.create(checkfirst=True) 39 | 40 | CLEANER_CHAT_SETTINGS = threading.RLock() 41 | CLEANER_CHAT_LOCK = threading.RLock() 42 | CLEANER_GLOBAL_LOCK = threading.RLock() 43 | 44 | CLEANER_CHATS = {} 45 | GLOBAL_IGNORE_COMMANDS = set() 46 | 47 | 48 | def set_cleanbt(chat_id, is_enable): 49 | with CLEANER_CHAT_SETTINGS: 50 | curr = SESSION.query(CleanerBlueTextChatSettings).get(str(chat_id)) 51 | 52 | if not curr: 53 | curr = CleanerBlueTextChatSettings(str(chat_id), is_enable) 54 | else: 55 | curr.is_enabled = is_enable 56 | 57 | if str(chat_id) not in CLEANER_CHATS: 58 | CLEANER_CHATS.setdefault(str(chat_id), {"setting": False, "commands": set()}) 59 | 60 | CLEANER_CHATS[str(chat_id)]["setting"] = is_enable 61 | 62 | SESSION.add(curr) 63 | SESSION.commit() 64 | 65 | 66 | def chat_ignore_command(chat_id, ignore): 67 | ignore = ignore.lower() 68 | with CLEANER_CHAT_LOCK: 69 | ignored = SESSION.query(CleanerBlueTextChat).get((str(chat_id), ignore)) 70 | 71 | if not ignored: 72 | 73 | if str(chat_id) not in CLEANER_CHATS: 74 | CLEANER_CHATS.setdefault(str(chat_id), {"setting": False, "commands": set()}) 75 | 76 | CLEANER_CHATS[str(chat_id)]["commands"].add(ignore) 77 | 78 | ignored = CleanerBlueTextChat(str(chat_id), ignore) 79 | SESSION.add(ignored) 80 | SESSION.commit() 81 | return True 82 | SESSION.close() 83 | return False 84 | 85 | 86 | def chat_unignore_command(chat_id, unignore): 87 | unignore = unignore.lower() 88 | with CLEANER_CHAT_LOCK: 89 | unignored = SESSION.query(CleanerBlueTextChat).get((str(chat_id), unignore)) 90 | 91 | if unignored: 92 | 93 | if str(chat_id) not in CLEANER_CHATS: 94 | CLEANER_CHATS.setdefault(str(chat_id), {"setting": False, "commands": set()}) 95 | if unignore in CLEANER_CHATS.get(str(chat_id)).get("commands"): 96 | CLEANER_CHATS[str(chat_id)]["commands"].remove(unignore) 97 | 98 | SESSION.delete(unignored) 99 | SESSION.commit() 100 | return True 101 | 102 | SESSION.close() 103 | return False 104 | 105 | 106 | def global_ignore_command(command): 107 | command = command.lower() 108 | with CLEANER_GLOBAL_LOCK: 109 | ignored = SESSION.query(CleanerBlueTextGlobal).get(str(command)) 110 | 111 | if not ignored: 112 | GLOBAL_IGNORE_COMMANDS.add(command) 113 | 114 | ignored = CleanerBlueTextGlobal(str(command)) 115 | SESSION.add(ignored) 116 | SESSION.commit() 117 | return True 118 | 119 | SESSION.close() 120 | return False 121 | 122 | 123 | def global_unignore_command(command): 124 | command = command.lower() 125 | with CLEANER_GLOBAL_LOCK: 126 | unignored = SESSION.query(CleanerBlueTextGlobal).get(str(command)) 127 | 128 | if unignored: 129 | if command in GLOBAL_IGNORE_COMMANDS: 130 | GLOBAL_IGNORE_COMMANDS.remove(command) 131 | 132 | SESSION.delete(command) 133 | SESSION.commit() 134 | return True 135 | 136 | SESSION.close() 137 | return False 138 | 139 | 140 | def is_command_ignored(chat_id, command): 141 | if command.lower() in GLOBAL_IGNORE_COMMANDS: 142 | return True 143 | 144 | if str(chat_id) in CLEANER_CHATS: 145 | if command.lower() in CLEANER_CHATS.get(str(chat_id)).get('commands'): 146 | return True 147 | 148 | return False 149 | 150 | 151 | def is_enabled(chat_id): 152 | if str(chat_id) in CLEANER_CHATS: 153 | settings = CLEANER_CHATS.get(str(chat_id)).get('setting') 154 | return settings 155 | 156 | return False 157 | 158 | 159 | def get_all_ignored(chat_id): 160 | if str(chat_id) in CLEANER_CHATS: 161 | LOCAL_IGNORE_COMMANDS = CLEANER_CHATS.get(str(chat_id)).get("commands") 162 | else: 163 | LOCAL_IGNORE_COMMANDS = set() 164 | 165 | return GLOBAL_IGNORE_COMMANDS, LOCAL_IGNORE_COMMANDS 166 | 167 | 168 | def __load_cleaner_list(): 169 | global GLOBAL_IGNORE_COMMANDS 170 | global CLEANER_CHATS 171 | 172 | try: 173 | GLOBAL_IGNORE_COMMANDS = {int(x.command) for x in SESSION.query(CleanerBlueTextGlobal).all()} 174 | finally: 175 | SESSION.close() 176 | 177 | try: 178 | for x in SESSION.query(CleanerBlueTextChatSettings).all(): 179 | CLEANER_CHATS.setdefault(x.chat_id, {"setting": False, "commands": set()}) 180 | CLEANER_CHATS[x.chat_id]["setting"] = x.is_enable 181 | finally: 182 | SESSION.close() 183 | 184 | try: 185 | for x in SESSION.query(CleanerBlueTextChat).all(): 186 | CLEANER_CHATS.setdefault(x.chat_id, {"setting": False, "commands": set()}) 187 | CLEANER_CHATS[x.chat_id]["commands"].add(x.command) 188 | finally: 189 | SESSION.close() 190 | 191 | 192 | __load_cleaner_list() 193 | -------------------------------------------------------------------------------- /lynda/modules/modules.py: -------------------------------------------------------------------------------- 1 | import importlib 2 | 3 | from telegram import Update, ParseMode 4 | from telegram.ext import CommandHandler, run_async 5 | 6 | from lynda import dispatcher 7 | from lynda.__main__ import ( 8 | IMPORTED, 9 | HELPABLE, 10 | MIGRATEABLE, 11 | STATS, 12 | USER_INFO, 13 | DATA_IMPORT, 14 | DATA_EXPORT, 15 | CHAT_SETTINGS, 16 | USER_SETTINGS) 17 | from lynda.modules.helper_funcs.chat_status import sudo_plus, dev_plus 18 | 19 | 20 | @run_async 21 | @dev_plus 22 | def load(update: Update, _): 23 | message = update.effective_message 24 | text = message.text.split(" ", 1)[1] 25 | load_messasge = message.reply_text( 26 | f"Attempting to load module : {text}", 27 | parse_mode=ParseMode.HTML) 28 | 29 | try: 30 | imported_module = importlib.import_module("lynda.modules." + text) 31 | except Exception: 32 | load_messasge.edit_text("Does that module even exist?") 33 | return 34 | 35 | if not hasattr(imported_module, "__mod_name__"): 36 | imported_module.__mod_name__ = imported_module.__name__ 37 | 38 | if imported_module.__mod_name__.lower() not in IMPORTED: 39 | IMPORTED[imported_module.__mod_name__.lower()] = imported_module 40 | else: 41 | load_messasge.edit_text("Module already loaded.") 42 | return 43 | if "__handlers__" in dir(imported_module): 44 | handlers = imported_module.__handlers__ 45 | for handler in handlers: 46 | if not isinstance(handler, tuple): 47 | dispatcher.add_handler(handler) 48 | else: 49 | handler_name, priority = handler 50 | dispatcher.add_handler(handler_name, priority) 51 | else: 52 | IMPORTED.pop(imported_module.__mod_name__.lower()) 53 | load_messasge.edit_text("The module cannot be loaded.") 54 | return 55 | 56 | if hasattr(imported_module, "__help__") and imported_module.__help__: 57 | HELPABLE[imported_module.__mod_name__.lower()] = imported_module 58 | 59 | # Chats to migrate on chat_migrated events 60 | if hasattr(imported_module, "__migrate__"): 61 | MIGRATEABLE.append(imported_module) 62 | 63 | if hasattr(imported_module, "__stats__"): 64 | STATS.append(imported_module) 65 | 66 | if hasattr(imported_module, "__user_info__"): 67 | USER_INFO.append(imported_module) 68 | 69 | if hasattr(imported_module, "__import_data__"): 70 | DATA_IMPORT.append(imported_module) 71 | 72 | if hasattr(imported_module, "__export_data__"): 73 | DATA_EXPORT.append(imported_module) 74 | 75 | if hasattr(imported_module, "__chat_settings__"): 76 | CHAT_SETTINGS[imported_module.__mod_name__.lower()] = imported_module 77 | 78 | if hasattr(imported_module, "__user_settings__"): 79 | USER_SETTINGS[imported_module.__mod_name__.lower()] = imported_module 80 | 81 | load_messasge.edit_text( 82 | "Successfully loaded module : {}".format(text), 83 | parse_mode=ParseMode.HTML) 84 | 85 | 86 | @run_async 87 | @dev_plus 88 | def unload(update: Update, _): 89 | message = update.effective_message 90 | text = message.text.split(" ", 1)[1] 91 | unload_messasge = message.reply_text( 92 | f"Attempting to unload module : {text}", 93 | parse_mode=ParseMode.HTML) 94 | 95 | try: 96 | imported_module = importlib.import_module("lynda.modules." + text) 97 | except Exception: 98 | unload_messasge.edit_text("Does that module even exist?") 99 | return 100 | 101 | if not hasattr(imported_module, "__mod_name__"): 102 | imported_module.__mod_name__ = imported_module.__name__ 103 | if imported_module.__mod_name__.lower() in IMPORTED: 104 | IMPORTED.pop(imported_module.__mod_name__.lower()) 105 | else: 106 | unload_messasge.edit_text("Can't unload something that isn't loaded.") 107 | return 108 | if "__handlers__" in dir(imported_module): 109 | handlers = imported_module.__handlers__ 110 | for handler in handlers: 111 | if isinstance(handler, bool): 112 | unload_messasge.edit_text("This module can't be unloaded!") 113 | return 114 | elif not isinstance(handler, tuple): 115 | dispatcher.remove_handler(handler) 116 | else: 117 | handler_name, priority = handler 118 | dispatcher.remove_handler(handler_name, priority) 119 | else: 120 | unload_messasge.edit_text("The module cannot be unloaded.") 121 | return 122 | 123 | if hasattr(imported_module, "__help__") and imported_module.__help__: 124 | HELPABLE.pop(imported_module.__mod_name__.lower()) 125 | 126 | # Chats to migrate on chat_migrated events 127 | if hasattr(imported_module, "__migrate__"): 128 | MIGRATEABLE.remove(imported_module) 129 | 130 | if hasattr(imported_module, "__stats__"): 131 | STATS.remove(imported_module) 132 | 133 | if hasattr(imported_module, "__user_info__"): 134 | USER_INFO.remove(imported_module) 135 | 136 | if hasattr(imported_module, "__import_data__"): 137 | DATA_IMPORT.remove(imported_module) 138 | 139 | if hasattr(imported_module, "__export_data__"): 140 | DATA_EXPORT.remove(imported_module) 141 | 142 | if hasattr(imported_module, "__chat_settings__"): 143 | CHAT_SETTINGS.pop(imported_module.__mod_name__.lower()) 144 | 145 | if hasattr(imported_module, "__user_settings__"): 146 | USER_SETTINGS.pop(imported_module.__mod_name__.lower()) 147 | 148 | unload_messasge.edit_text( 149 | f"Successfully unloaded module : {text}", 150 | parse_mode=ParseMode.HTML) 151 | 152 | 153 | @run_async 154 | @sudo_plus 155 | def listmodules(update: Update, _): 156 | message = update.effective_message 157 | module_list = [] 158 | 159 | for helpable_module in HELPABLE: 160 | helpable_module_info = IMPORTED[helpable_module] 161 | file_info = IMPORTED[helpable_module_info.__mod_name__.lower()] 162 | file_name = file_info.__name__.rsplit("lynda.modules.", 1)[1] 163 | mod_name = file_info.__mod_name__ 164 | module_list.append(f'- {mod_name} ({file_name})\n') 165 | module_list = "Following modules are loaded : \n\n" + ''.join(module_list) 166 | message.reply_text(module_list, parse_mode=ParseMode.HTML) 167 | 168 | 169 | LOAD_HANDLER = CommandHandler("load", load) 170 | UNLOAD_HANDLER = CommandHandler("unload", unload) 171 | LISTMODULES_HANDLER = CommandHandler("listmodules", listmodules) 172 | 173 | dispatcher.add_handler(LOAD_HANDLER) 174 | dispatcher.add_handler(UNLOAD_HANDLER) 175 | dispatcher.add_handler(LISTMODULES_HANDLER) 176 | 177 | __mod_name__ = "Modules" 178 | -------------------------------------------------------------------------------- /lynda/modules/antiflood.py: -------------------------------------------------------------------------------- 1 | import html 2 | from typing import List 3 | 4 | from telegram import Update, ParseMode 5 | from telegram.error import BadRequest 6 | from telegram.ext import MessageHandler, CommandHandler, Filters, run_async, CallbackContext 7 | from telegram.utils.helpers import mention_html 8 | 9 | from lynda import dispatcher, WHITELIST_USERS, SARDEGNA_USERS 10 | from lynda.modules.helper_funcs.chat_status import is_user_admin, user_admin, can_restrict, connection_status 11 | from lynda.modules.log_channel import loggable 12 | from lynda.modules.sql import antiflood_sql as sql 13 | 14 | FLOOD_GROUP = 3 15 | 16 | 17 | @run_async 18 | @loggable 19 | def check_flood(update: Update, context: CallbackContext) -> str: 20 | user = update.effective_user 21 | chat = update.effective_chat 22 | msg = update.effective_message 23 | log_message = "" 24 | 25 | if not user: # ignore channels 26 | return log_message 27 | 28 | # ignore admins and whitelists 29 | if (is_user_admin(chat, user.id) 30 | or user.id in WHITELIST_USERS 31 | or user.id in SARDEGNA_USERS): 32 | sql.update_flood(chat.id, None) 33 | return log_message 34 | 35 | should_ban = sql.update_flood(chat.id, user.id) 36 | if not should_ban: 37 | return log_message 38 | 39 | try: 40 | context.bot.restrict_chat_member(chat.id, user.id, can_send_messages=False) 41 | context.bot.send_message( 42 | chat.id, 43 | f"*mutes {mention_html(user.id, user.first_name)} permanently*\nStop flooding the group!", 44 | parse_mode=ParseMode.HTML) 45 | log_message = ( 46 | f"{html.escape(chat.title)}:\n" 47 | f"#MUTED\n" 48 | f"User: {mention_html(user.id, user.first_name)}\n" 49 | f"Flooded the group.\nMuted until an admin unmutes") 50 | 51 | return log_message 52 | 53 | except BadRequest: 54 | msg.reply_text( 55 | "I can't kick people here, give me permissions first! Until then, I'll disable antiflood.") 56 | sql.set_flood(chat.id, 0) 57 | log_message = ( 58 | "{chat.title}:\n" 59 | "#INFO\n" 60 | "Don't have kick permissions, so automatically disabled antiflood.") 61 | 62 | return log_message 63 | 64 | 65 | @run_async 66 | @connection_status 67 | @user_admin 68 | @can_restrict 69 | @loggable 70 | def set_flood(update: Update, context: CallbackContext) -> str: 71 | chat = update.effective_chat 72 | user = update.effective_user 73 | message = update.effective_message 74 | args = context.args 75 | log_message = "" 76 | 77 | update_chat_title = chat.title 78 | message_chat_title = update.effective_message.chat.title 79 | 80 | if update_chat_title == message_chat_title: 81 | chat_name = "" 82 | else: 83 | chat_name = f" in {update_chat_title}" 84 | 85 | if len(args) >= 1: 86 | 87 | val = args[0].lower() 88 | 89 | if val in ('off', 'no', '0'): 90 | sql.set_flood(chat.id, 0) 91 | message.reply_text( 92 | "Antiflood has been disabled{}.".format(chat_name), 93 | parse_mode=ParseMode.HTML) 94 | 95 | elif val.isdigit(): 96 | amount = int(val) 97 | if amount <= 0: 98 | sql.set_flood(chat.id, 0) 99 | message.reply_text( 100 | "Antiflood has been disabled{}.".format(chat_name), 101 | parse_mode=ParseMode.HTML) 102 | log_message = ( 103 | f"{html.escape(chat.title)}:\n" 104 | f"#SETFLOOD\n" 105 | f"Admin: {mention_html(user.id, user.first_name)}\n" 106 | f"Disabled antiflood.") 107 | 108 | elif amount < 3: 109 | message.reply_text( 110 | "Antiflood has to be either 0 (disabled), or a number bigger than 3!") 111 | else: 112 | sql.set_flood(chat.id, amount) 113 | message.reply_text( 114 | "Antiflood has been updated and set to {}{}".format( 115 | amount, chat_name), parse_mode=ParseMode.HTML) 116 | log_message = ( 117 | f"{html.escape(chat.title)}:\n" 118 | f"#SETFLOOD\n" 119 | f"Admin: {mention_html(user.id, user.first_name)}\n" 120 | f"Set antiflood to {amount}.") 121 | 122 | return log_message 123 | else: 124 | message.reply_text( 125 | "Unrecognised argument - please use a number, 'off', or 'no'.") 126 | 127 | return log_message 128 | 129 | 130 | @run_async 131 | @connection_status 132 | def flood(update: Update, _): 133 | chat = update.effective_chat 134 | update_chat_title = chat.title 135 | message_chat_title = update.effective_message.chat.title 136 | 137 | if update_chat_title == message_chat_title: 138 | chat_name = "" 139 | else: 140 | chat_name = f" in {update_chat_title}" 141 | 142 | limit = sql.get_flood_limit(chat.id) 143 | 144 | if limit == 0: 145 | update.effective_message.reply_text( 146 | f"I'm not currently enforcing flood control{chat_name}!", 147 | parse_mode=ParseMode.HTML) 148 | else: 149 | update.effective_message.reply_text( 150 | f"I'm currently punching users if they send " 151 | f"more than {limit} consecutive messages{chat_name}.", 152 | parse_mode=ParseMode.HTML) 153 | 154 | 155 | def __migrate__(old_chat_id, new_chat_id): 156 | sql.migrate_chat(old_chat_id, new_chat_id) 157 | 158 | 159 | def __chat_settings__(chat_id, _user_id): 160 | limit = sql.get_flood_limit(chat_id) 161 | if limit == 0: 162 | return "*Not* currently enforcing flood control." 163 | else: 164 | return "Antiflood is set to `{}` messages.".format(limit) 165 | 166 | 167 | __help__ = """ 168 | -> `/flood` 169 | Get the current flood control setting 170 | 171 | ──「 *Admin only:* 」── 172 | -> `/setflood` 173 | enables or disables flood control 174 | """ 175 | 176 | FLOOD_BAN_HANDLER = MessageHandler( 177 | Filters.all & ~Filters.status_update & Filters.group, 178 | check_flood) 179 | SET_FLOOD_HANDLER = CommandHandler("setflood", set_flood, pass_args=True) 180 | FLOOD_HANDLER = CommandHandler("flood", flood) 181 | 182 | dispatcher.add_handler(FLOOD_BAN_HANDLER, FLOOD_GROUP) 183 | dispatcher.add_handler(SET_FLOOD_HANDLER) 184 | dispatcher.add_handler(FLOOD_HANDLER) 185 | 186 | __mod_name__ = "AntiFlood" 187 | __handlers__ = [(FLOOD_BAN_HANDLER, FLOOD_GROUP), 188 | SET_FLOOD_HANDLER, FLOOD_HANDLER] 189 | -------------------------------------------------------------------------------- /lynda/modules/sql/connection_sql.py: -------------------------------------------------------------------------------- 1 | import threading 2 | import time 3 | from typing import Union 4 | 5 | from sqlalchemy import Column, String, Boolean, UnicodeText, Integer 6 | 7 | from lynda.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, allow_connect_to_chat): 16 | self.chat_id = str(chat_id) 17 | self.allow_connect_to_chat = str(allow_connect_to_chat) 18 | 19 | def __repr__(self): 20 | return "".format(self.chat_id, self.allow_connect_to_chat) 21 | 22 | 23 | class Connection(BASE): 24 | __tablename__ = "connection" 25 | user_id = Column(Integer, primary_key=True) 26 | chat_id = Column(String(14)) 27 | 28 | def __init__(self, user_id, chat_id): 29 | self.user_id = user_id 30 | self.chat_id = str(chat_id) # Ensure String 31 | 32 | 33 | class ConnectionHistory(BASE): 34 | __tablename__ = "connection_history" 35 | user_id = Column(Integer, primary_key=True) 36 | chat_id = Column(String(14), primary_key=True) 37 | chat_name = Column(UnicodeText) 38 | conn_time = Column(Integer) 39 | 40 | def __init__(self, user_id, chat_id, chat_name, conn_time): 41 | self.user_id = user_id 42 | self.chat_id = str(chat_id) 43 | self.chat_name = str(chat_name) 44 | self.conn_time = int(conn_time) 45 | 46 | def __repr__(self): 47 | return "".format(self.user_id, self.chat_id) 48 | 49 | 50 | ChatAccessConnectionSettings.__table__.create(checkfirst=True) 51 | Connection.__table__.create(checkfirst=True) 52 | ConnectionHistory.__table__.create(checkfirst=True) 53 | 54 | CHAT_ACCESS_LOCK = threading.RLock() 55 | CONNECTION_INSERTION_LOCK = threading.RLock() 56 | CONNECTION_HISTORY_LOCK = threading.RLock() 57 | 58 | HISTORY_CONNECT = {} 59 | 60 | 61 | def allow_connect_to_chat(chat_id: Union[str, int]) -> bool: 62 | try: 63 | chat_setting = SESSION.query(ChatAccessConnectionSettings).get(str(chat_id)) 64 | if chat_setting: 65 | return chat_setting.allow_connect_to_chat 66 | return False 67 | finally: 68 | SESSION.close() 69 | 70 | 71 | def set_allow_connect_to_chat(chat_id: Union[int, str], setting: bool): 72 | with CHAT_ACCESS_LOCK: 73 | chat_setting = SESSION.query(ChatAccessConnectionSettings).get(str(chat_id)) 74 | if not chat_setting: 75 | chat_setting = ChatAccessConnectionSettings(chat_id, setting) 76 | 77 | chat_setting.allow_connect_to_chat = setting 78 | SESSION.add(chat_setting) 79 | SESSION.commit() 80 | 81 | 82 | def connect(user_id, chat_id): 83 | with CONNECTION_INSERTION_LOCK: 84 | prev = SESSION.query(Connection).get((int(user_id))) 85 | if prev: 86 | SESSION.delete(prev) 87 | connect_to_chat = Connection(int(user_id), chat_id) 88 | SESSION.add(connect_to_chat) 89 | SESSION.commit() 90 | return True 91 | 92 | 93 | def get_connected_chat(user_id): 94 | try: 95 | return SESSION.query(Connection).get((int(user_id))) 96 | finally: 97 | SESSION.close() 98 | 99 | 100 | def curr_connection(chat_id): 101 | try: 102 | return SESSION.query(Connection).get((str(chat_id))) 103 | finally: 104 | SESSION.close() 105 | 106 | 107 | def disconnect(user_id): 108 | with CONNECTION_INSERTION_LOCK: 109 | disconnect = SESSION.query(Connection).get((int(user_id))) 110 | if disconnect: 111 | SESSION.delete(disconnect) 112 | SESSION.commit() 113 | return True 114 | else: 115 | SESSION.close() 116 | return False 117 | 118 | 119 | def add_history_conn(user_id, chat_id, chat_name): 120 | global HISTORY_CONNECT 121 | with CONNECTION_HISTORY_LOCK: 122 | conn_time = int(time.time()) 123 | if HISTORY_CONNECT.get(int(user_id)): 124 | counting = SESSION.query(ConnectionHistory.user_id).filter(ConnectionHistory.user_id == str(user_id)).count() 125 | getchat_id = {} 126 | for x in HISTORY_CONNECT[int(user_id)]: 127 | getchat_id[HISTORY_CONNECT[int(user_id)][x]['chat_id']] = x 128 | if chat_id in getchat_id: 129 | todeltime = getchat_id[str(chat_id)] 130 | delold = SESSION.query(ConnectionHistory).get((int(user_id), str(chat_id))) 131 | if delold: 132 | SESSION.delete(delold) 133 | HISTORY_CONNECT[int(user_id)].pop(todeltime) 134 | elif counting >= 5: 135 | todel = list(HISTORY_CONNECT[int(user_id)]) 136 | todel.reverse() 137 | todel = todel[4:] 138 | for x in todel: 139 | chat_old = HISTORY_CONNECT[int(user_id)][x]['chat_id'] 140 | delold = SESSION.query(ConnectionHistory).get((int(user_id), str(chat_old))) 141 | if delold: 142 | SESSION.delete(delold) 143 | HISTORY_CONNECT[int(user_id)].pop(x) 144 | else: 145 | HISTORY_CONNECT[int(user_id)] = {} 146 | delold = SESSION.query(ConnectionHistory).get((int(user_id), str(chat_id))) 147 | if delold: 148 | SESSION.delete(delold) 149 | history = ConnectionHistory(int(user_id), str(chat_id), chat_name, conn_time) 150 | SESSION.add(history) 151 | SESSION.commit() 152 | HISTORY_CONNECT[int(user_id)][conn_time] = {'chat_name': chat_name, 'chat_id': str(chat_id)} 153 | 154 | 155 | def get_history_conn(user_id): 156 | if not HISTORY_CONNECT.get(int(user_id)): 157 | HISTORY_CONNECT[int(user_id)] = {} 158 | return HISTORY_CONNECT[int(user_id)] 159 | 160 | 161 | def clear_history_conn(user_id): 162 | global HISTORY_CONNECT 163 | todel = list(HISTORY_CONNECT[int(user_id)]) 164 | for x in todel: 165 | chat_old = HISTORY_CONNECT[int(user_id)][x]['chat_id'] 166 | delold = SESSION.query(ConnectionHistory).get((int(user_id), str(chat_old))) 167 | if delold: 168 | SESSION.delete(delold) 169 | HISTORY_CONNECT[int(user_id)].pop(x) 170 | SESSION.commit() 171 | return True 172 | 173 | 174 | def __load_user_history(): 175 | global HISTORY_CONNECT 176 | try: 177 | qall = SESSION.query(ConnectionHistory).all() 178 | HISTORY_CONNECT = {} 179 | for x in qall: 180 | check = HISTORY_CONNECT.get(x.user_id) 181 | if check is None: 182 | HISTORY_CONNECT[x.user_id] = {} 183 | HISTORY_CONNECT[x.user_id][x.conn_time] = {'chat_name': x.chat_name, 'chat_id': x.chat_id} 184 | finally: 185 | SESSION.close() 186 | 187 | 188 | __load_user_history() 189 | -------------------------------------------------------------------------------- /lynda/__init__.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | import sys 4 | import time 5 | import telegram.ext as tg 6 | import spamwatch 7 | 8 | from telethon import TelegramClient 9 | 10 | StartTime = time.time() 11 | 12 | # enable logging 13 | logging.basicConfig( 14 | format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", 15 | level=logging.INFO) 16 | 17 | LOGGER = logging.getLogger(__name__) 18 | 19 | # if version < 3.6, stop bot. 20 | if sys.version_info[0] < 3 or sys.version_info[1] < 6: 21 | LOGGER.error("You MUST have a python version of at least 3.6! Multiple features depend on this. Bot quitting.") 22 | quit(1) 23 | 24 | ENV = bool(os.environ.get('ENV', False)) 25 | 26 | if ENV: 27 | TOKEN = os.environ.get('TOKEN', None) 28 | 29 | try: 30 | OWNER_ID = int(os.environ.get('OWNER_ID', None)) 31 | except ValueError: 32 | raise Exception("Your OWNER_ID env variable is not a valid integer.") 33 | 34 | MESSAGE_DUMP = os.environ.get('MESSAGE_DUMP', None) 35 | OWNER_USERNAME = os.environ.get("OWNER_USERNAME", None) 36 | 37 | try: 38 | SUDO_USERS = {int(x) for x in os.environ.get("SUDO_USERS", "").split()} 39 | DEV_USERS = {int(x) for x in os.environ.get("DEV_USERS", "").split()} 40 | except ValueError: 41 | raise Exception("Your sudo or dev users list does not contain valid integers.") 42 | 43 | try: 44 | SUPPORT_USERS = {int(x) for x in os.environ.get("SUPPORT_USERS", "").split()} 45 | except ValueError: 46 | raise Exception("Your support users list does not contain valid integers.") 47 | 48 | try: 49 | SPAMMERS = {int(x) for x in os.environ.get("SPAMMERS", "").split()} 50 | except ValueError: 51 | raise Exception("Your spammers users list does not contain valid integers.") 52 | 53 | try: 54 | WHITELIST_USERS = {int(x) for x in os.environ.get("WHITELIST_USERS", "").split()} 55 | except ValueError: 56 | raise Exception("Your whitelisted users list does not contain valid integers.") 57 | 58 | try: 59 | SARDEGNA_USERS = {int(x) for x in os.environ.get("SARDEGNA_USERS", "").split()} 60 | except ValueError: 61 | raise Exception("Your Sardegna users list does not contain valid integers.") 62 | 63 | GBAN_LOGS = os.environ.get('GBAN_LOGS', None) 64 | WEBHOOK = bool(os.environ.get('WEBHOOK', False)) 65 | URL = os.environ.get('URL', "") # Does not contain token 66 | PORT = int(os.environ.get('PORT', 5000)) 67 | API_ID = os.environ.get('API_ID', None) 68 | API_HASH = os.environ.get('API_HASH', None) 69 | CERT_PATH = os.environ.get("CERT_PATH") 70 | DB_URI = os.environ.get('SQLALCHEMY_DATABASE_URI') 71 | DONATION_LINK = os.environ.get('DONATION_LINK') 72 | LOAD = os.environ.get("LOAD", "").split() 73 | NO_LOAD = os.environ.get("NO_LOAD", "translation").split() 74 | DEL_CMDS = bool(os.environ.get('DEL_CMDS', False)) 75 | STRICT_GBAN = bool(os.environ.get('STRICT_GBAN', False)) 76 | WORKERS = int(os.environ.get('WORKERS', 8)) 77 | BAN_STICKER = os.environ.get('BAN_STICKER', 'CAADAgADOwADPPEcAXkko5EB3YGYAg') 78 | ALLOW_EXCL = os.environ.get('ALLOW_EXCL', False) 79 | CASH_API_KEY = os.environ.get('CASH_API_KEY', None) 80 | TIME_API_KEY = os.environ.get('TIME_API_KEY', None) 81 | AI_API_KEY = os.environ.get('AI_API_KEY', None) 82 | WALL_API = os.environ.get('WALL_API', None) 83 | LASTFM_API_KEY = os.environ.get('LASTFM_API_KEY', None) 84 | DEEPFRY_TOKEN = os.environ.get('DEEPFRY_TOKEN', None) 85 | API_WEATHER = os.environ.get('API_WEATHER', None) 86 | SW_API = os.environ.get('SW_API', None) 87 | 88 | else: 89 | from lynda.config import Development as Config 90 | 91 | TOKEN = Config.TOKEN 92 | 93 | try: 94 | OWNER_ID = int(Config.OWNER_ID) 95 | except ValueError: 96 | raise Exception("Your OWNER_ID variable is not a valid integer.") 97 | 98 | MESSAGE_DUMP = Config.MESSAGE_DUMP 99 | OWNER_USERNAME = Config.OWNER_USERNAME 100 | 101 | try: 102 | SUDO_USERS = {int(x) for x in Config.SUDO_USERS or []} 103 | DEV_USERS = {int(x) for x in Config.DEV_USERS or []} 104 | except ValueError: 105 | raise Exception("Your sudo or dev users list does not contain valid integers.") 106 | 107 | try: 108 | SUPPORT_USERS = {int(x) for x in Config.SUPPORT_USERS or []} 109 | except ValueError: 110 | raise Exception("Your support users list does not contain valid integers.") 111 | 112 | try: 113 | SPAMMERS = {int(x) for x in Config.SPAMMERS or []} 114 | except ValueError: 115 | raise Exception("Your spammers users list does not contain valid integers.") 116 | 117 | try: 118 | WHITELIST_USERS = {int(x) for x in Config.WHITELIST_USERS or []} 119 | except ValueError: 120 | raise Exception("Your whitelisted users list does not contain valid integers.") 121 | 122 | try: 123 | SARDEGNA_USERS = {int(x) for x in Config.SARDEGNA_USERS or []} 124 | except ValueError: 125 | raise Exception("Your Sardegna users list does not contain valid integers.") 126 | 127 | GBAN_LOGS = Config.GBAN_LOGS 128 | WEBHOOK = Config.WEBHOOK 129 | URL = Config.URL 130 | PORT = Config.PORT 131 | CERT_PATH = Config.CERT_PATH 132 | API_ID = Config.API_ID 133 | API_HASH = Config.API_HASH 134 | DB_URI = Config.SQLALCHEMY_DATABASE_URI 135 | DONATION_LINK = Config.DONATION_LINK 136 | LOAD = Config.LOAD 137 | NO_LOAD = Config.NO_LOAD 138 | DEL_CMDS = Config.DEL_CMDS 139 | STRICT_GBAN = Config.STRICT_GBAN 140 | WORKERS = Config.WORKERS 141 | BAN_STICKER = Config.BAN_STICKER 142 | ALLOW_EXCL = Config.ALLOW_EXCL 143 | CASH_API_KEY = Config.CASH_API_KEY 144 | TIME_API_KEY = Config.TIME_API_KEY 145 | AI_API_KEY = Config.AI_API_KEY 146 | WALL_API = Config.WALL_API 147 | LASTFM_API_KEY = Config.LASTFM_API_KEY 148 | DEEPFRY_TOKEN = Config.DEEPFRY_TOKEN 149 | API_WEATHER = Config.API_WEATHER 150 | SW_API = Config.SW_API 151 | SUDO_USERS.add(OWNER_ID) 152 | DEV_USERS.add(OWNER_ID) 153 | 154 | telethn = TelegramClient("lynda", API_ID, API_HASH) 155 | updater = tg.Updater(TOKEN, workers=WORKERS, use_context=True) 156 | dispatcher = updater.dispatcher 157 | 158 | SUDO_USERS = list(SUDO_USERS) + list(DEV_USERS) 159 | DEV_USERS = list(DEV_USERS) 160 | WHITELIST_USERS = list(WHITELIST_USERS) 161 | SUPPORT_USERS = list(SUPPORT_USERS) 162 | SARDEGNA_USERS = list(SARDEGNA_USERS) 163 | SPAMMERS = list(SPAMMERS) 164 | 165 | # SpamWatch 166 | if SW_API == "None": 167 | spam_watch = None 168 | LOGGER.warning("SpamWatch API key is missing! Check your config var") 169 | else: 170 | try: 171 | spam_watch = spamwatch.Client(SW_API) 172 | except Exception: 173 | spam_watch = None 174 | 175 | # Load at end to ensure all prev variables have been set 176 | from lynda.modules.helper_funcs.handlers import CustomCommandHandler, CustomRegexHandler, CustomMessageHandler 177 | 178 | # make sure the regex handler can take extra kwargs 179 | tg.RegexHandler = CustomRegexHandler 180 | tg.CommandHandler = CustomCommandHandler 181 | tg.MessageHandler = CustomMessageHandler 182 | 183 | 184 | def spamfilters(_text, user_id, _chat_id): 185 | if int(user_id) in SPAMMERS: 186 | print("This user is a spammer!") 187 | return True 188 | else: 189 | return False 190 | -------------------------------------------------------------------------------- /lynda/modules/dbcleanup.py: -------------------------------------------------------------------------------- 1 | from time import sleep 2 | 3 | from telegram import Update, InlineKeyboardMarkup, InlineKeyboardButton 4 | from telegram.error import BadRequest, Unauthorized 5 | from telegram.ext import CommandHandler, CallbackQueryHandler, run_async, CallbackContext 6 | 7 | import lynda.modules.sql.global_bans_sql as gban_sql 8 | import lynda.modules.sql.users_sql as user_sql 9 | from lynda import dispatcher, OWNER_ID, DEV_USERS 10 | from lynda.modules.helper_funcs.chat_status import dev_plus 11 | 12 | 13 | def get_invalid_chats(context: CallbackContext, update: Update, remove: bool = False): 14 | chat_id = update.effective_chat.id 15 | chats = user_sql.get_all_chats() 16 | kicked_chats, progress = 0, 0 17 | chat_list = [] 18 | progress_message = None 19 | 20 | for chat in chats: 21 | 22 | if ((100 * chats.index(chat)) / len(chats)) > progress: 23 | progress_bar = f"{progress}% completed in getting invalid chats." 24 | if progress_message: 25 | try: 26 | context.bot.editMessageText( 27 | progress_bar, chat_id, progress_message.message_id) 28 | except Exception: 29 | pass 30 | else: 31 | progress_message = context.bot.sendMessage(chat_id, progress_bar) 32 | progress += 5 33 | 34 | cid = chat.chat_id 35 | sleep(0.1) 36 | try: 37 | context.bot.get_chat(cid, timeout=60) 38 | except (BadRequest, Unauthorized): 39 | kicked_chats += 1 40 | chat_list.append(cid) 41 | except Exception as e: 42 | print(e) 43 | try: 44 | progress_message.delete() 45 | except Exception as e: 46 | print(e) 47 | if remove: 48 | for muted_chat in chat_list: 49 | sleep(0.1) 50 | user_sql.rem_chat(muted_chat) 51 | 52 | return kicked_chats 53 | 54 | 55 | def get_invalid_gban(context: CallbackContext, _, remove: bool = False): 56 | banned = gban_sql.get_gban_list() 57 | ungbanned_users = 0 58 | ungban_list = [] 59 | 60 | for user in banned: 61 | user_id = user["user_id"] 62 | sleep(0.1) 63 | try: 64 | context.bot.get_chat(user_id) 65 | except BadRequest: 66 | ungbanned_users += 1 67 | ungban_list.append(user_id) 68 | except Exception as e: 69 | print(e) 70 | if remove: 71 | for user_id in ungban_list: 72 | sleep(0.1) 73 | gban_sql.ungban_user(user_id) 74 | 75 | return ungbanned_users 76 | 77 | 78 | @run_async 79 | @dev_plus 80 | def dbcleanup(update: Update, context: CallbackContext): 81 | msg = update.effective_message 82 | msg.reply_text("Getting invalid chat count ...") 83 | invalid_chat_count = get_invalid_chats(context.bot, update) 84 | msg.reply_text("Getting invalid gbanned count ...") 85 | invalid_gban_count = get_invalid_gban(context.bot, update) 86 | reply = f"Total invalid chats - {invalid_chat_count}\n" 87 | reply += f"Total invalid gbanned users - {invalid_gban_count}" 88 | 89 | buttons = [ 90 | [InlineKeyboardButton("Cleanup DB", callback_data="db_cleanup")] 91 | ] 92 | 93 | update.effective_message.reply_text( 94 | reply, reply_markup=InlineKeyboardMarkup(buttons)) 95 | 96 | 97 | def get_muted_chats(context: CallbackContext, update: Update, leave: bool = False): 98 | chat_id = update.effective_chat.id 99 | chats = user_sql.get_all_chats() 100 | muted_chats, progress = 0, 0 101 | chat_list = [] 102 | progress_message = None 103 | for chat in chats: 104 | if ((100 * chats.index(chat)) / len(chats)) > progress: 105 | progress_bar = f"{progress}% completed in getting muted chats." 106 | if progress_message: 107 | try: 108 | context.bot.editMessageText( 109 | progress_bar, chat_id, progress_message.message_id) 110 | except Exception as e: 111 | print(e) 112 | else: 113 | progress_message = context.bot.sendMessage(chat_id, progress_bar) 114 | progress += 5 115 | 116 | cid = chat.chat_id 117 | sleep(0.1) 118 | 119 | try: 120 | context.bot.send_chat_action(cid, "TYPING", timeout=60) 121 | except (BadRequest, Unauthorized): 122 | muted_chats += +1 123 | chat_list.append(cid) 124 | except Exception as e: 125 | print(e) 126 | try: 127 | progress_message.delete() 128 | except Exception as e: 129 | print(e) 130 | if leave: 131 | for muted_chat in chat_list: 132 | sleep(0.1) 133 | try: 134 | context.bot.leaveChat(muted_chat, timeout=60) 135 | except Exception as e: 136 | print(e) 137 | user_sql.rem_chat(muted_chat) 138 | return muted_chats 139 | 140 | 141 | @run_async 142 | @dev_plus 143 | def leave_muted_chats(update: Update, context: CallbackContext): 144 | message = update.effective_message 145 | progress_message = message.reply_text("Getting chat count ...") 146 | muted_chats = get_muted_chats(context.bot, update) 147 | 148 | buttons = [ 149 | [InlineKeyboardButton("Leave chats", callback_data="db_leave_chat")] 150 | ] 151 | 152 | update.effective_message.reply_text( 153 | f"I am muted in {muted_chats} chats.", 154 | reply_markup=InlineKeyboardMarkup(buttons)) 155 | progress_message.delete() 156 | 157 | 158 | @run_async 159 | def callback_button(update: Update, context: CallbackContext): 160 | query = update.callback_query 161 | message = query.message 162 | chat_id = update.effective_chat.id 163 | query_type = query.data 164 | admin_list = [OWNER_ID] + DEV_USERS 165 | context.bot.answer_callback_query(query.id) 166 | 167 | if query_type == "db_leave_chat" and query.from_user.id in admin_list: 168 | context.bot.editMessageText( 169 | "Leaving chats ...", 170 | chat_id, 171 | message.message_id) 172 | chat_count = get_muted_chats(context.bot, update, True) 173 | context.bot.sendMessage(chat_id, f"Left {chat_count} chats.") 174 | elif ( 175 | query_type == "db_leave_chat" 176 | or query_type == "db_cleanup" 177 | and query.from_user.id not in admin_list 178 | ): 179 | query.answer("You are not allowed to use this.") 180 | elif query_type == "db_cleanup": 181 | context.bot.editMessageText( 182 | "Cleaning up DB ...", 183 | chat_id, 184 | message.message_id) 185 | invalid_chat_count = get_invalid_chats(context.bot, update, True) 186 | invalid_gban_count = get_invalid_gban(context.bot, update, True) 187 | reply = "Cleaned up {} chats and {} gbanned users from db.".format( 188 | invalid_chat_count, invalid_gban_count) 189 | context.bot.sendMessage(chat_id, reply) 190 | 191 | 192 | DB_CLEANUP_HANDLER = CommandHandler("dbcleanup", dbcleanup) 193 | LEAVE_MUTED_CHATS_HANDLER = CommandHandler( 194 | "leavemutedchats", leave_muted_chats) 195 | BUTTON_HANDLER = CallbackQueryHandler(callback_button, pattern='db_.*') 196 | 197 | dispatcher.add_handler(DB_CLEANUP_HANDLER) 198 | dispatcher.add_handler(LEAVE_MUTED_CHATS_HANDLER) 199 | dispatcher.add_handler(BUTTON_HANDLER) 200 | 201 | __mod_name__ = "DB Cleanup" 202 | __handlers__ = [DB_CLEANUP_HANDLER, LEAVE_MUTED_CHATS_HANDLER, BUTTON_HANDLER] 203 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.rsuser 8 | *.suo 9 | *.user 10 | *.userosscache 11 | *.sln.docstates 12 | 13 | # User-specific files (MonoDevelop/Xamarin Studio) 14 | *.userprefs 15 | 16 | # Mono auto generated files 17 | mono_crash.* 18 | 19 | # Build results 20 | [Dd]ebug/ 21 | [Dd]ebugPublic/ 22 | [Rr]elease/ 23 | [Rr]eleases/ 24 | x64/ 25 | x86/ 26 | [Aa][Rr][Mm]/ 27 | [Aa][Rr][Mm]64/ 28 | bld/ 29 | [Bb]in/ 30 | [Oo]bj/ 31 | [Ll]og/ 32 | [Ll]ogs/ 33 | 34 | # Visual Studio 2015/2017 cache/options directory 35 | .vs/ 36 | # Uncomment if you have tasks that create the project's static files in wwwroot 37 | #wwwroot/ 38 | 39 | # Visual Studio 2017 auto generated files 40 | Generated\ Files/ 41 | 42 | # MSTest test Results 43 | [Tt]est[Rr]esult*/ 44 | [Bb]uild[Ll]og.* 45 | 46 | # NUnit 47 | *.VisualState.xml 48 | TestResult.xml 49 | nunit-*.xml 50 | 51 | # Build Results of an ATL Project 52 | [Dd]ebugPS/ 53 | [Rr]eleasePS/ 54 | dlldata.c 55 | 56 | # Benchmark Results 57 | BenchmarkDotNet.Artifacts/ 58 | 59 | # .NET Core 60 | project.lock.json 61 | project.fragment.lock.json 62 | artifacts/ 63 | 64 | # StyleCop 65 | StyleCopReport.xml 66 | 67 | # Files built by Visual Studio 68 | *_i.c 69 | *_p.c 70 | *_h.h 71 | *.ilk 72 | *.meta 73 | *.obj 74 | *.iobj 75 | *.pch 76 | *.pdb 77 | *.ipdb 78 | *.pgc 79 | *.pgd 80 | *.rsp 81 | *.sbr 82 | *.tlb 83 | *.tli 84 | *.tlh 85 | *.tmp 86 | *.tmp_proj 87 | *_wpftmp.csproj 88 | *.log 89 | *.vspscc 90 | *.vssscc 91 | .builds 92 | *.pidb 93 | *.svclog 94 | *.scc 95 | 96 | # Chutzpah Test files 97 | _Chutzpah* 98 | 99 | # Visual C++ cache files 100 | ipch/ 101 | *.aps 102 | *.ncb 103 | *.opendb 104 | *.opensdf 105 | *.sdf 106 | *.cachefile 107 | *.VC.db 108 | *.VC.VC.opendb 109 | 110 | # Visual Studio profiler 111 | *.psess 112 | *.vsp 113 | *.vspx 114 | *.sap 115 | 116 | # Visual Studio Trace Files 117 | *.e2e 118 | 119 | # TFS 2012 Local Workspace 120 | $tf/ 121 | 122 | # Guidance Automation Toolkit 123 | *.gpState 124 | 125 | # ReSharper is a .NET coding add-in 126 | _ReSharper*/ 127 | *.[Rr]e[Ss]harper 128 | *.DotSettings.user 129 | 130 | # TeamCity is a build add-in 131 | _TeamCity* 132 | 133 | # DotCover is a Code Coverage Tool 134 | *.dotCover 135 | 136 | # AxoCover is a Code Coverage Tool 137 | .axoCover/* 138 | !.axoCover/settings.json 139 | 140 | # Visual Studio code coverage results 141 | *.coverage 142 | *.coveragexml 143 | 144 | # NCrunch 145 | _NCrunch_* 146 | .*crunch*.local.xml 147 | nCrunchTemp_* 148 | 149 | # MightyMoose 150 | *.mm.* 151 | AutoTest.Net/ 152 | 153 | # Web workbench (sass) 154 | .sass-cache/ 155 | 156 | # Installshield output folder 157 | [Ee]xpress/ 158 | 159 | # DocProject is a documentation generator add-in 160 | DocProject/buildhelp/ 161 | DocProject/Help/*.HxT 162 | DocProject/Help/*.HxC 163 | DocProject/Help/*.hhc 164 | DocProject/Help/*.hhk 165 | DocProject/Help/*.hhp 166 | DocProject/Help/Html2 167 | DocProject/Help/html 168 | 169 | # Click-Once directory 170 | publish/ 171 | 172 | # Publish Web Output 173 | *.[Pp]ublish.xml 174 | *.azurePubxml 175 | # Note: Comment the next line if you want to checkin your web deploy settings, 176 | # but database connection strings (with potential passwords) will be unencrypted 177 | *.pubxml 178 | *.publishproj 179 | 180 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 181 | # checkin your Azure Web App publish settings, but sensitive information contained 182 | # in these scripts will be unencrypted 183 | PublishScripts/ 184 | 185 | # NuGet Packages 186 | *.nupkg 187 | # NuGet Symbol Packages 188 | *.snupkg 189 | # The packages folder can be ignored because of Package Restore 190 | **/[Pp]ackages/* 191 | # except build/, which is used as an MSBuild target. 192 | !**/[Pp]ackages/build/ 193 | # Uncomment if necessary however generally it will be regenerated when needed 194 | #!**/[Pp]ackages/repositories.config 195 | # NuGet v3's project.json files produces more ignorable files 196 | *.nuget.props 197 | *.nuget.targets 198 | 199 | # Microsoft Azure Build Output 200 | csx/ 201 | *.build.csdef 202 | 203 | # Microsoft Azure Emulator 204 | ecf/ 205 | rcf/ 206 | 207 | # Windows Store app package directories and files 208 | AppPackages/ 209 | BundleArtifacts/ 210 | Package.StoreAssociation.xml 211 | _pkginfo.txt 212 | *.appx 213 | *.appxbundle 214 | *.appxupload 215 | 216 | # Visual Studio cache files 217 | # files ending in .cache can be ignored 218 | *.[Cc]ache 219 | # but keep track of directories ending in .cache 220 | !?*.[Cc]ache/ 221 | 222 | # Others 223 | ClientBin/ 224 | ~$* 225 | *~ 226 | *.dbmdl 227 | *.dbproj.schemaview 228 | *.jfm 229 | *.pfx 230 | *.publishsettings 231 | orleans.codegen.cs 232 | 233 | # Including strong name files can present a security risk 234 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 235 | #*.snk 236 | 237 | # Since there are multiple workflows, uncomment next line to ignore bower_components 238 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 239 | #bower_components/ 240 | 241 | # RIA/Silverlight projects 242 | Generated_Code/ 243 | 244 | # Backup & report files from converting an old project file 245 | # to a newer Visual Studio version. Backup files are not needed, 246 | # because we have git ;-) 247 | _UpgradeReport_Files/ 248 | Backup*/ 249 | UpgradeLog*.XML 250 | UpgradeLog*.htm 251 | ServiceFabricBackup/ 252 | *.rptproj.bak 253 | 254 | # SQL Server files 255 | *.mdf 256 | *.ldf 257 | *.ndf 258 | 259 | # Business Intelligence projects 260 | *.rdl.data 261 | *.bim.layout 262 | *.bim_*.settings 263 | *.rptproj.rsuser 264 | *- [Bb]ackup.rdl 265 | *- [Bb]ackup ([0-9]).rdl 266 | *- [Bb]ackup ([0-9][0-9]).rdl 267 | 268 | # Microsoft Fakes 269 | FakesAssemblies/ 270 | 271 | # GhostDoc plugin setting file 272 | *.GhostDoc.xml 273 | 274 | # Node.js Tools for Visual Studio 275 | .ntvs_analysis.dat 276 | node_modules/ 277 | 278 | # Visual Studio 6 build log 279 | *.plg 280 | 281 | # Visual Studio 6 workspace options file 282 | *.opt 283 | 284 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 285 | *.vbw 286 | 287 | # Visual Studio LightSwitch build output 288 | **/*.HTMLClient/GeneratedArtifacts 289 | **/*.DesktopClient/GeneratedArtifacts 290 | **/*.DesktopClient/ModelManifest.xml 291 | **/*.Server/GeneratedArtifacts 292 | **/*.Server/ModelManifest.xml 293 | _Pvt_Extensions 294 | 295 | # Paket dependency manager 296 | .paket/paket.exe 297 | paket-files/ 298 | 299 | # FAKE - F# Make 300 | .fake/ 301 | 302 | # CodeRush personal settings 303 | .cr/personal 304 | 305 | # Python Tools for Visual Studio (PTVS) 306 | __pycache__/ 307 | *.pyc 308 | 309 | # Cake - Uncomment if you are using it 310 | # tools/** 311 | # !tools/packages.config 312 | 313 | # Tabs Studio 314 | *.tss 315 | 316 | # Telerik's JustMock configuration file 317 | *.jmconfig 318 | 319 | # BizTalk build output 320 | *.btp.cs 321 | *.btm.cs 322 | *.odx.cs 323 | *.xsd.cs 324 | 325 | # OpenCover UI analysis results 326 | OpenCover/ 327 | 328 | # Azure Stream Analytics local run output 329 | ASALocalRun/ 330 | 331 | # MSBuild Binary and Structured Log 332 | *.binlog 333 | 334 | # NVidia Nsight GPU debugger configuration file 335 | *.nvuser 336 | 337 | # MFractors (Xamarin productivity tool) working folder 338 | .mfractor/ 339 | 340 | # Local History for Visual Studio 341 | .localhistory/ 342 | 343 | # BeatPulse healthcheck temp database 344 | healthchecksdb 345 | 346 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 347 | MigrationBackup/ 348 | lynda/config.py 349 | lynda.session-journal 350 | lynda.session 351 | lynda/elevated_users.json 352 | -------------------------------------------------------------------------------- /lynda/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 lynda.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 | document = Column(Boolean, default=False) 18 | photo = Column(Boolean, default=False) 19 | sticker = Column(Boolean, default=False) 20 | gif = Column(Boolean, default=False) 21 | url = Column(Boolean, default=False) 22 | bots = Column(Boolean, default=False) 23 | forward = Column(Boolean, default=False) 24 | game = Column(Boolean, default=False) 25 | location = Column(Boolean, default=False) 26 | 27 | def __init__(self, chat_id): 28 | self.chat_id = str(chat_id) # ensure string 29 | self.audio = False 30 | self.voice = False 31 | self.contact = False 32 | self.video = False 33 | self.document = False 34 | self.photo = False 35 | self.sticker = False 36 | self.gif = False 37 | self.url = False 38 | self.bots = False 39 | self.forward = False 40 | self.game = False 41 | self.location = False 42 | 43 | def __repr__(self): 44 | return "" % self.chat_id 45 | 46 | 47 | class Restrictions(BASE): 48 | __tablename__ = "restrictions" 49 | chat_id = Column(String(14), primary_key=True) 50 | # Booleans are for "is this restricted", _NOT_ "is this allowed" 51 | messages = Column(Boolean, default=False) 52 | media = Column(Boolean, default=False) 53 | other = Column(Boolean, default=False) 54 | preview = Column(Boolean, default=False) 55 | 56 | def __init__(self, chat_id): 57 | self.chat_id = str(chat_id) # ensure string 58 | self.messages = False 59 | self.media = False 60 | self.other = False 61 | self.preview = False 62 | 63 | def __repr__(self): 64 | return "" % self.chat_id 65 | 66 | 67 | Permissions.__table__.create(checkfirst=True) 68 | Restrictions.__table__.create(checkfirst=True) 69 | 70 | PERM_LOCK = threading.RLock() 71 | RESTR_LOCK = threading.RLock() 72 | 73 | 74 | def init_permissions(chat_id, reset=False): 75 | curr_perm = SESSION.query(Permissions).get(str(chat_id)) 76 | if reset: 77 | SESSION.delete(curr_perm) 78 | SESSION.flush() 79 | perm = Permissions(str(chat_id)) 80 | SESSION.add(perm) 81 | SESSION.commit() 82 | return perm 83 | 84 | 85 | def init_restrictions(chat_id, reset=False): 86 | curr_restr = SESSION.query(Restrictions).get(str(chat_id)) 87 | if reset: 88 | SESSION.delete(curr_restr) 89 | SESSION.flush() 90 | restr = Restrictions(str(chat_id)) 91 | SESSION.add(restr) 92 | SESSION.commit() 93 | return restr 94 | 95 | 96 | def update_lock(chat_id, lock_type, locked): 97 | with PERM_LOCK: 98 | curr_perm = SESSION.query(Permissions).get(str(chat_id)) 99 | if not curr_perm: 100 | curr_perm = init_permissions(chat_id) 101 | 102 | if lock_type == "audio": 103 | curr_perm.audio = locked 104 | elif lock_type == "voice": 105 | curr_perm.voice = locked 106 | elif lock_type == "contact": 107 | curr_perm.contact = locked 108 | elif lock_type == "video": 109 | curr_perm.video = locked 110 | elif lock_type == "document": 111 | curr_perm.document = locked 112 | elif lock_type == "photo": 113 | curr_perm.photo = locked 114 | elif lock_type == "sticker": 115 | curr_perm.sticker = locked 116 | elif lock_type == "gif": 117 | curr_perm.gif = locked 118 | elif lock_type == 'url': 119 | curr_perm.url = locked 120 | elif lock_type == 'bots': 121 | curr_perm.bots = locked 122 | elif lock_type == 'forward': 123 | curr_perm.forward = locked 124 | elif lock_type == 'game': 125 | curr_perm.game = locked 126 | elif lock_type == 'location': 127 | curr_perm.location = locked 128 | 129 | SESSION.add(curr_perm) 130 | SESSION.commit() 131 | 132 | 133 | def update_restriction(chat_id, restr_type, locked): 134 | with RESTR_LOCK: 135 | curr_restr = SESSION.query(Restrictions).get(str(chat_id)) 136 | if not curr_restr: 137 | curr_restr = init_restrictions(chat_id) 138 | 139 | if restr_type == "messages": 140 | curr_restr.messages = locked 141 | elif restr_type == "media": 142 | curr_restr.media = locked 143 | elif restr_type == "other": 144 | curr_restr.other = locked 145 | elif restr_type == "previews": 146 | curr_restr.preview = locked 147 | elif restr_type == "all": 148 | curr_restr.messages = locked 149 | curr_restr.media = locked 150 | curr_restr.other = locked 151 | curr_restr.preview = locked 152 | SESSION.add(curr_restr) 153 | SESSION.commit() 154 | 155 | 156 | def is_locked(chat_id, lock_type): 157 | curr_perm = SESSION.query(Permissions).get(str(chat_id)) 158 | SESSION.close() 159 | 160 | if not curr_perm: 161 | return False 162 | 163 | elif lock_type == "sticker": 164 | return curr_perm.sticker 165 | elif lock_type == "photo": 166 | return curr_perm.photo 167 | elif lock_type == "audio": 168 | return curr_perm.audio 169 | elif lock_type == "voice": 170 | return curr_perm.voice 171 | elif lock_type == "contact": 172 | return curr_perm.contact 173 | elif lock_type == "video": 174 | return curr_perm.video 175 | elif lock_type == "document": 176 | return curr_perm.document 177 | elif lock_type == "gif": 178 | return curr_perm.gif 179 | elif lock_type == "url": 180 | return curr_perm.url 181 | elif lock_type == "bots": 182 | return curr_perm.bots 183 | elif lock_type == "forward": 184 | return curr_perm.forward 185 | elif lock_type == "game": 186 | return curr_perm.game 187 | elif lock_type == "location": 188 | return curr_perm.location 189 | 190 | 191 | def is_restr_locked(chat_id, lock_type): 192 | curr_restr = SESSION.query(Restrictions).get(str(chat_id)) 193 | SESSION.close() 194 | 195 | if not curr_restr: 196 | return False 197 | 198 | if lock_type == "messages": 199 | return curr_restr.messages 200 | elif lock_type == "media": 201 | return curr_restr.media 202 | elif lock_type == "other": 203 | return curr_restr.other 204 | elif lock_type == "previews": 205 | return curr_restr.preview 206 | elif lock_type == "all": 207 | return curr_restr.messages and curr_restr.media and curr_restr.other and curr_restr.preview 208 | 209 | 210 | def get_locks(chat_id): 211 | try: 212 | return SESSION.query(Permissions).get(str(chat_id)) 213 | finally: 214 | SESSION.close() 215 | 216 | 217 | def get_restr(chat_id): 218 | try: 219 | return SESSION.query(Restrictions).get(str(chat_id)) 220 | finally: 221 | SESSION.close() 222 | 223 | 224 | def migrate_chat(old_chat_id, new_chat_id): 225 | with PERM_LOCK: 226 | perms = SESSION.query(Permissions).get(str(old_chat_id)) 227 | if perms: 228 | perms.chat_id = str(new_chat_id) 229 | SESSION.commit() 230 | 231 | with RESTR_LOCK: 232 | rest = SESSION.query(Restrictions).get(str(old_chat_id)) 233 | if rest: 234 | rest.chat_id = str(new_chat_id) 235 | SESSION.commit() 236 | --------------------------------------------------------------------------------