├── 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 |
4 |
5 |
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 |
10 |
11 |
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 | 
2 | # LyndaRobot
3 | [](http://perso.crans.org/besson/LICENSE.html) [](https://deepsource.io/gh/pokurt/LyndaRobot/?ref=repository-badge)
4 | [](https://www.codacy.com?utm_source=github.com&utm_medium=referral&utm_content=AmaanAhmed/Lynda&utm_campaign=Badge_Grade) [](https://t.me/LyndaEagleSupport) [](https://github.com/ellerbrock/open-source-badges/) [](https://GitHub.com/Naereen/StrapDown.js/graphs/commit-activity) [](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 | [](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 |
--------------------------------------------------------------------------------