commit 6b93e9d6b17a65daa0a2f9b1ddb56c89cdefe596 Author: Nick80835 Date: Mon Oct 16 09:46:31 2023 -0400 squash diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8c2ae75 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +__pycache__ +*.ini +*.session +*.session-journal +*.db \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..5934a7f --- /dev/null +++ b/requirements.txt @@ -0,0 +1,6 @@ +aiohttp +databases[sqlite] +pillow +praw +telethon +speedtest-cli \ No newline at end of file diff --git a/ubot/__init__.py b/ubot/__init__.py new file mode 100644 index 0000000..82f5545 --- /dev/null +++ b/ubot/__init__.py @@ -0,0 +1 @@ +import ubot.micro_bot diff --git a/ubot/__main__.py b/ubot/__main__.py new file mode 100644 index 0000000..7986d11 --- /dev/null +++ b/ubot/__main__.py @@ -0,0 +1 @@ +#dummy diff --git a/ubot/command_handler.py b/ubot/command_handler.py new file mode 100644 index 0000000..7b2fdeb --- /dev/null +++ b/ubot/command_handler.py @@ -0,0 +1,51 @@ +# Copyright (C) 2019 Nick Filmer (nick80835@gmail.com) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +from re import escape, search + +from telethon import events + + +class CommandHandler(): + def __init__(self, client, logger): + self.outgoing_commands = {} + self.incoming_commands = {} + self.logger = logger + client.add_event_handler(self.handle_outgoing, events.NewMessage(outgoing=True)) + client.add_event_handler(self.handle_incoming, events.NewMessage(incoming=True)) + + async def handle_outgoing(self, event): + for cmd in self.outgoing_commands.keys(): + if search(cmd, event.text): + event.pattern_match = search(cmd, event.text) + + try: + await self.outgoing_commands.get(cmd)(event) + return + except Exception as exception: + self.logger.warn(f"{self.outgoing_commands.get(cmd).__name__} - {exception}") + await event.reply(f"`An error occurred in {self.outgoing_commands.get(cmd).__name__}: {exception}`") + + async def handle_incoming(self, event): + for cmd in self.incoming_commands.keys(): + if search(cmd, event.text): + event.pattern_match = search(cmd, event.text) + + try: + await self.incoming_commands.get(cmd)(event) + return + except Exception as exception: + self.logger.warn(f"{self.incoming_commands.get(cmd).__name__} - {exception}") + await event.reply(f"`An error occurred in {self.incoming_commands.get(cmd).__name__}: {exception}`") diff --git a/ubot/database.py b/ubot/database.py new file mode 100644 index 0000000..eee3340 --- /dev/null +++ b/ubot/database.py @@ -0,0 +1,46 @@ +# Copyright (C) 2019 Nick Filmer (nick80835@gmail.com) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +from databases import Database as db + + +class Database(): + def __init__(self, client): + self.db = db("sqlite:///database.db") + client.loop.run_until_complete(self.db.connect()) + + async def ensure_table(self, table, columns): + await self.db.execute(f"create table if not exists {table} ({' TEXT, '.join(columns) + ' TEXT'})") + return ", ".join(columns) + + async def single_row_write(self, table, columns, row, value): + column_string = await self.ensure_table(table, columns) + await self.db.execute(f"delete from {table} where {columns[0]} = '{row}'") + await self.db.execute(f"insert into {table}({column_string}) values ('{row}', '{value}')") + + async def single_row_delete(self, table, columns, row): + await self.ensure_table(table, columns) + await self.db.execute(f"delete from {table} where {columns[0]} = '{row}'") + + async def single_column_readall(self, table, columns, row): + await self.ensure_table(table, columns) + fetched_tuple = await self.db.fetch_all(f"select {row} from {table}") + fetched_list = [item[0] for item in fetched_tuple] + return fetched_list + + async def single_row_read(self, table, columns, row): + await self.ensure_table(table, columns) + content = await self.db.fetch_one(f"select {columns[1]} from {table} where {columns[0]} = '{row}'") + return content[0] diff --git a/ubot/loader.py b/ubot/loader.py new file mode 100644 index 0000000..36b224f --- /dev/null +++ b/ubot/loader.py @@ -0,0 +1,84 @@ +# Copyright (C) 2019 Nick Filmer (nick80835@gmail.com) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +import glob +from importlib import import_module, reload +from os.path import basename, dirname, isfile +from re import escape + +from telethon import events + +from .command_handler import CommandHandler + + +class Loader(): + def __init__(self, client, logger, settings): + self.loaded_modules = [] + self.all_modules = [] + self.client = client + self.logger = logger + self.settings = settings + self.command_handler = CommandHandler(client, logger) + self.botversion = "0.1.2" + + def load_all_modules(self): + self._find_all_modules() + + for module_name in self.all_modules: + self.loaded_modules.append(import_module("ubot.modules." + module_name)) + + def reload_all_modules(self): + self.command_handler.outgoing_commands = {} + self.command_handler.incoming_commands = {} + + errors = "" + + for module in self.loaded_modules: + try: + reload(module) + except Exception as exception: + errors += f"`Error while reloading {module.__name__} -> {exception}\n\n`" + raise exception + + return errors or None + + def add(self, **args): + prefix = escape(self.settings.get_config("cmd_prefix") or '.') + + if args.get('noprefix', None): + del args['noprefix'] + prefix = '' + + if not args.get('isfilter', False) and args.get('pattern', None) is not None: + args['pattern'] = f"(?is)^{prefix}{args['pattern']}(?: |$)(.*)" + else: + del args['isfilter'] + args['pattern'] = f"(?is)(.*){args['pattern']}(.*)" + + def decorator(func): + self.command_handler.incoming_commands[args['pattern']] = func + + return decorator + + def _find_all_modules(self): + mod_paths = glob.glob(dirname(__file__) + "/modules/*.py") + + self.all_modules = [ + basename(f)[:-3] for f in mod_paths + if isfile(f) and f.endswith(".py") + ] + + system_index = self.all_modules.index("system") + self.all_modules.insert(0, self.all_modules.pop(system_index)) diff --git a/ubot/micro_bot.py b/ubot/micro_bot.py new file mode 100644 index 0000000..b5f3a28 --- /dev/null +++ b/ubot/micro_bot.py @@ -0,0 +1,100 @@ +# Copyright (C) 2019 Nick Filmer (nick80835@gmail.com) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +from logging import INFO, basicConfig, getLogger +from sys import version_info + +import telethon as tt +from telethon.errors.rpcerrorlist import PhoneNumberInvalidError +from telethon.network.connection.tcpabridged import \ + ConnectionTcpAbridged as CTA + +from .database import Database +from .loader import Loader +from .settings import Settings + +if version_info[0] < 3 or version_info[1] < 6: + print("This program requires at least Python 3.6.0 to work correctly, exiting.") + quit(1) + + +class MicroBot(): + def __init__(self): + self.client = None + self.settings = Settings() + self.logger = None + self.loader = None + self.database = None + + def start_microbot(self): + self.start_logger() + self.start_client() + self.start_database() + self.start_loader() + self.loader.load_all_modules() + self.client.run_until_disconnected() + + def start_database(self): + self.database = Database(self.client) + + def start_loader(self): + self.loader = Loader(self.client, self.logger, self.settings) + + def start_logger(self): + basicConfig(format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", level=INFO) + self.logger = getLogger(__name__) + + def _check_config(self, api_key, api_hash, session_name): + while api_key is None or api_key == "": + api_key = input("Enter your API key: ") + + while api_hash is None or api_hash == "": + api_hash = input("Enter your API hash: ") + + self.settings.set_config("api_key", api_key) + self.settings.set_config("api_hash", api_hash) + + if session_name is None or session_name == "": + session_name = "user0" + self.settings.set_config("session_name", session_name) + + return api_key, api_hash, session_name + + def start_client(self): + session_name = self.settings.get_config("session_name") + api_key = self.settings.get_config("api_key") + api_hash = self.settings.get_config("api_hash") + + api_key, api_hash, session_name = self._check_config(api_key, api_hash, session_name) + + self.client = tt.TelegramClient(session_name, api_key, api_hash, connection=CTA) + + try: + self.client.start() + except PhoneNumberInvalidError: + self.logger.error("The phone number provided is invalid, exiting.") + exit(2) + + async def stop_client(self, reason=None): + if reason: + self.logger.info("Stopping client for reason: %s", reason) + else: + self.logger.info("Stopping client.") + + await self.client.disconnect() + + +micro_bot = MicroBot() +micro_bot.start_microbot() diff --git a/ubot/modules/catdog.py b/ubot/modules/catdog.py new file mode 100644 index 0000000..bcdbf7d --- /dev/null +++ b/ubot/modules/catdog.py @@ -0,0 +1,171 @@ +# Copyright (C) 2019 Nick Filmer (nick80835@gmail.com) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +from aiohttp import ClientSession + +from ubot.micro_bot import micro_bot + +ldr = micro_bot.loader + +CAT_URL = 'http://api.thecatapi.com/v1/images/search' +DOG_URL = 'http://api.thedogapi.com/v1/images/search' +SHIBE_URL = 'http://shibe.online/api/shibes' +BIRD_URL = 'http://shibe.online/api/birds' +CAT_API_KEY = 'e5a56813-be40-481c-9c8a-a6585c37c1fe' +DOG_API_KEY = '105555df-5c50-40fe-bd59-d15a17ce1c2e' +CAT_HEADERS = {"x-api-key": CAT_API_KEY} +DOG_HEADERS = {"x-api-key": DOG_API_KEY} +IMGPARAM = {"mime_types": "jpg,png"} +GIFPARAM = {"mime_types": "gif"} + + +async def neko_atsume(params): + session = ClientSession() + + async with session.get(CAT_URL, params=params, headers=CAT_HEADERS) as response: + if response.status == 200: + neko = await response.json() + else: + neko = response.status + + await session.close() + return neko + + +async def inu_atsume(params): + session = ClientSession() + + async with session.get(DOG_URL, params=params, headers=DOG_HEADERS) as response: + if response.status == 200: + inu = await response.json() + else: + inu = response.status + + await session.close() + return inu + + +async def shibe_inu_atsume(): + session = ClientSession() + + async with session.get(SHIBE_URL, params=None, headers=None) as response: + if response.status == 200: + shibe_inu = await response.json() + else: + shibe_inu = response.status + + await session.close() + return shibe_inu + + +async def tori_atsume(): + session = ClientSession() + + async with session.get(BIRD_URL, params=None, headers=None) as response: + if response.status == 200: + tori = await response.json() + else: + tori = response.status + + await session.close() + return tori + + +@ldr.add(pattern="shibe") +async def shibe(event): + shibe_inu = await shibe_inu_atsume() + + if isinstance(shibe_inu, int): + await event.reply(f"`There was an error finding the shibes! :( -> {shibe_inu}`") + return + + await event.reply(file=shibe_inu[0]) + + +@ldr.add(pattern="bird") +async def bird(event): + tori = await tori_atsume() + + if isinstance(tori, int): + await event.reply(f"`There was an error finding the birdies! :( -> {tori}`") + return + + await event.reply(file=tori[0]) + + +@ldr.add(pattern="cat") +async def cat(event): + neko = await neko_atsume(IMGPARAM) + + if isinstance(neko, int): + await event.reply(f"`There was an error finding the cats! :( -> {neko}`") + return + + await event.reply(file=neko[0]["url"]) + + +@ldr.add(pattern="cathd") +async def cathd(event): + neko = await neko_atsume(IMGPARAM) + + if isinstance(neko, int): + await event.reply(f"`There was an error finding the cats! :( -> {neko}`") + return + + await event.reply(file=neko[0]["url"], force_document=True) + + +@ldr.add(pattern="catgif") +async def catgif(event): + neko = await neko_atsume(GIFPARAM) + + if isinstance(neko, int): + await event.reply(f"`There was an error finding the cats! :( -> {neko}`") + return + + await event.reply(file=neko[0]["url"]) + + +@ldr.add(pattern="dog") +async def dog(event): + inu = await inu_atsume(IMGPARAM) + + if isinstance(inu, int): + await event.reply(f"`There was an error finding the dogs! :( -> {inu}`") + return + + await event.reply(file=inu[0]["url"]) + + +@ldr.add(pattern="doghd") +async def doghd(event): + inu = await inu_atsume(IMGPARAM) + + if isinstance(inu, int): + await event.reply(f"`There was an error finding the dogs! :( -> {inu}`") + return + + await event.reply(file=inu[0]["url"], force_document=True).delete() + + +@ldr.add(pattern="doggif") +async def doggif(event): + inu = await inu_atsume(GIFPARAM) + + if isinstance(inu, int): + await event.reply(f"`There was an error finding the dogs! :( -> {inu}`") + return + + await event.reply(file=inu[0]["url"]) diff --git a/ubot/modules/danbooru.py b/ubot/modules/danbooru.py new file mode 100644 index 0000000..bcf0807 --- /dev/null +++ b/ubot/modules/danbooru.py @@ -0,0 +1,74 @@ +# Copyright (C) 2019 Nick Filmer (nick80835@gmail.com) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +from aiohttp import ClientSession + +from ubot.micro_bot import micro_bot + +ldr = micro_bot.loader + +DAN_URL = "http://danbooru.donmai.us/posts.json" + + +@ldr.add(pattern="dan(s|x|q|)") +async def danbooru(event): + if "x" in event.pattern_match.group(0): + rating = "Rating:explicit" + elif "s" in event.pattern_match.group(0): + rating = "Rating:safe" + elif "q" in event.pattern_match.group(0): + rating = "Rating:questionable" + else: + rating = "" + + search_query = event.pattern_match.group(2) + + params = {"limit": 1, + "random": "true", + "tags": f"{rating} {search_query}".strip()} + + session = ClientSession() + + async with session.get(DAN_URL, params=params) as response: + if response.status == 200: + response = await response.json() + else: + await event.reply(f"`An error occurred, response code: `**{response.status}**") + return + + await session.close() + + if not response: + await event.reply(f"`No results for query: `**{search_query}**") + return + + valid_urls = [] + + for url in ['file_url', 'large_file_url', 'source']: + if url in response[0].keys(): + valid_urls.append(response[0][url]) + + if not valid_urls: + await event.reply(f"`Failed to find URLs for query: `**{search_query}**") + return + + for image_url in valid_urls: + try: + await event.reply(file=image_url) + return + except: + pass + + await event.reply(f"`Failed to fetch media for query: `**{search_query}**") diff --git a/ubot/modules/deepfry.py b/ubot/modules/deepfry.py new file mode 100644 index 0000000..82323b8 --- /dev/null +++ b/ubot/modules/deepfry.py @@ -0,0 +1,136 @@ +# Copyright (C) 2019 Nick Filmer (nick80835@gmail.com) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +# Original source for the deepfrying code (used under the following license): https://github.com/Ovyerus/deeppyer + +# MIT License +# +# Copyright (c) 2017 Ovyerus +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +import io +from random import randint, uniform + +from PIL import Image, ImageEnhance, ImageOps +from telethon.tl.types import DocumentAttributeFilename + +from ubot.micro_bot import micro_bot + +ldr = micro_bot.loader + + +@ldr.add(pattern="deepfry") +async def deepfryer(event): + try: + frycount = int(event.pattern_match.group(1)) + if frycount < 1: + frycount = 1 + elif frycount > 10: + frycount = 10 + except ValueError: + frycount = 1 + + replied_fry = True + + if event.is_reply: + reply_message = await event.get_reply_message() + data = await check_media(reply_message) + + if isinstance(data, bool): + await event.reply("`I can't deep fry that!`") + return + else: + data = await check_media(event) + replied_fry = False + + if isinstance(data, bool): + await event.reply("`Reply to an image or sticker or caption an image to deep fry it!`") + return + + # Download photo (highres) as byte array + image = io.BytesIO() + await event.client.download_media(data, image) + image = Image.open(image) + + # Fry the image + image = image.convert("RGB") + + for _ in range(frycount): + image = await deepfry(image) + + fried_io = io.BytesIO() + fried_io.name = "image.jpeg" + image.save(fried_io, "JPEG") + fried_io.seek(0) + + if replied_fry: + await reply_message.reply(file=fried_io) + else: + await event.reply(file=fried_io) + + +async def deepfry(img): + # Crush image to hell and back + img = ImageOps.posterize(img, randint(3, 7)) + + # Generate colour overlay + overlay = img.copy() + overlay = ImageEnhance.Contrast(overlay).enhance(uniform(0.7, 1.8)) + overlay = ImageEnhance.Brightness(overlay).enhance(uniform(0.8, 1.3)) + overlay = ImageEnhance.Color(overlay).enhance(uniform(0.7, 1.4)) + + # Blend random colors onto and sharpen the image + img = Image.blend(img, overlay, uniform(0.1, 0.4)) + img = ImageEnhance.Sharpness(img).enhance(randint(5, 200)) + + return img + + +async def check_media(reply_message): + if reply_message and reply_message.media: + if reply_message.photo: + data = reply_message.photo + elif reply_message.document: + if DocumentAttributeFilename(file_name='AnimatedSticker.tgs') in reply_message.media.document.attributes: + return False + if reply_message.gif or reply_message.video or reply_message.audio or reply_message.voice: + return False + data = reply_message.media.document + else: + return False + else: + return False + + if not data or data is None: + return False + else: + return data diff --git a/ubot/modules/evaluation.py b/ubot/modules/evaluation.py new file mode 100644 index 0000000..fb0e5c1 --- /dev/null +++ b/ubot/modules/evaluation.py @@ -0,0 +1,133 @@ +# Copyright (C) 2019 Nick Filmer (nick80835@gmail.com) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +import inspect +import io + +from PIL import Image +from speedtest import Speedtest + +from ubot.micro_bot import micro_bot + +ldr = micro_bot.loader + + +@ldr.add(pattern="speed") +async def iamspeed(event): + test_message = await event.reply("`Running speed test…`") + test = Speedtest() + + test.get_best_server() + test.download() + test.upload() + test.results.share() + result = test.results.dict() + + await test_message.edit( + f"`Started at: {result['timestamp']}\n" + f"Download: {speed_convert(result['download'])}\n" + f"Upload: {speed_convert(result['upload'])}\n" + f"Ping: {result['ping']} milliseconds\n" + f"ISP: {result['client']['isp']}`" + ) + + +def speed_convert(size): + power = 2**10 + zero = 0 + units = {0: '', 1: 'Kilobits/s', 2: 'Megabits/s', 3: 'Gigabits/s', 4: 'Terabits/s'} + while size > power: + size /= power + zero += 1 + return f"{round(size, 2)} {units[zero]}" + + +@ldr.add(pattern="chatid") +async def chatidgetter(event): + if event.is_reply: + reply = await event.get_reply_message() + if reply.forward and reply.forward.channel_id: + await event.reply(f"**Channel ID:**` {reply.forward.channel_id}`") + return + chat_id = reply.chat_id + else: + chat_id = event.chat_id + + await event.reply(f"**Chat ID:**` {chat_id}`") + + +@ldr.add(pattern="userid") +async def useridgetter(event): + if event.is_reply: + reply = await event.get_reply_message() + user_id = reply.from_id + else: + user_id = event.from_id + + await event.reply(f"**User ID:**` {user_id}`") + + +@ldr.add(pattern="profile") +async def userprofilegetter(event): + user_arg = event.pattern_match.group(1) + + if user_arg: + try: + user_entity = await event.client.get_entity(user_arg) + except (ValueError, TypeError): + await event.reply("`The ID or username you provided was invalid!`") + return + elif event.is_reply: + reply = await event.get_reply_message() + reply_id = reply.from_id + if reply_id: + try: + user_entity = await event.client.get_entity(reply_id) + except (ValueError, TypeError): + await event.reply("`There was an error getting the user!`") + return + else: + await event.reply("`The user may have super sneaky privacy settings enabled!`") + return + else: + await event.reply("`Give me a user ID, username or reply!`") + return + + userid = user_entity.id + username = user_entity.username + userfullname = f"{user_entity.first_name} {user_entity.last_name or ''}" + + await event.reply(f"**Full Name:** {userfullname}\n**Username:** @{username}\n**User ID:** {userid}") + + +@ldr.add(pattern="stickpng") +async def stickertopng(event): + reply = await event.get_reply_message() + + if reply.sticker: + sticker_webp_data = reply.sticker + else: + await event.reply("`Reply to a sticker to get it as a PNG file!`") + return + + sticker_webp_io = io.BytesIO() + await event.client.download_media(sticker_webp_data, sticker_webp_io) + sticker_webp = Image.open(sticker_webp_io) + sticker_png_io = io.BytesIO() + sticker_webp.save(sticker_png_io, "PNG") + sticker_png_io.name = "sticker.png" + sticker_png_io.seek(0) + + await event.reply(file=sticker_png_io, force_document=True) diff --git a/ubot/modules/memes.py b/ubot/modules/memes.py new file mode 100644 index 0000000..a9311b5 --- /dev/null +++ b/ubot/modules/memes.py @@ -0,0 +1,208 @@ +# Copyright (C) 2019 Nick Filmer (nick80835@gmail.com) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +from asyncio import sleep +from random import choice, randint + +from ubot.micro_bot import micro_bot + +ldr = micro_bot.loader + +emoji = list("😂😝🤪🤩😤🥵🤯🥶😱🤔😩🙄💀👻🤡😹👀👁👌💦🔥🌚🌝🌞🔫💯") +b_emoji = "🅱️" +a_emoji = "🅰️" +i_emoji = "ℹ️" + +owo_faces = "owo uwu owu uwo u-u o-o OwO UwU @-@ ;-; ;_; ._. (._.) (o-o) ('._.) (。◕‿‿◕。)" \ + " (。◕‿◕。) (─‿‿─) ◔⌣◔ ◉_◉".split(sep=" ") + +zal_chars = " ̷̡̛̮͇̝͉̫̭͈͗͂̎͌̒̉̋́͜ ̵̠͕͍̩̟͚͍̞̳̌́̀̑̐̇̎̚͝ ̸̻̠̮̬̻͇͈̮̯̋̄͛̊͋̐̇͝͠ ̵̧̟͎͈̪̜̫̪͖̎͛̀͋͗́̍̊͠ ̵͍͉̟͕͇͎̖̹̔͌̊̏̌̽́̈́͊ͅ ̷̥͚̼̬̦͓͇̗͕͊̏͂͆̈̀̚͘̚ ̵̢̨̗̝̳͉̱̦͖̔̾͒͊͒̎̂̎͝ ̵̞̜̭̦̖̺͉̞̃͂͋̒̋͂̈́͘̕͜ ̶̢̢͇̲̥̗̟̏͛̇̏̊̑̌̔̚ͅͅ ̷̮͖͚̦̦̞̱̠̰̍̆̐͆͆͆̈̌́ ̶̲͚̪̪̪͍̹̜̬͊̆͋̄͒̾͆͝͝ ̴̨̛͍͖͎̞͍̞͕̟͑͊̉͗͑͆͘̕ ̶͕̪̞̲̘̬͖̙̞̽͌͗̽̒͋̾̍̀ ̵̨̧̡̧̖͔̞̠̝̌̂̐̉̊̈́́̑̓ ̶̛̱̼̗̱̙͖̳̬͇̽̈̀̀̎̋͌͝ ̷̧̺͈̫̖̖͈̱͎͋͌̆̈̃̐́̀̈".replace(" ", "") + + +@ldr.add(pattern="cp") +async def copypasta(event): + text_arg, reply = await get_text_arg(event) + + text_arg = await shitpostify(text_arg) + text_arg = await mockify(text_arg) + text_arg = await emojify(text_arg) + cp_text = await vaporize(text_arg) + + if reply: + await reply.reply(cp_text) + else: + await event.reply(cp_text) + + +@ldr.add(pattern="mock") +async def mock(event): + text_arg, reply = await get_text_arg(event) + + mock_text = await mockify(text_arg) + + if reply: + await reply.reply(mock_text) + else: + await event.reply(mock_text) + + +@ldr.add(pattern="vap") +async def vapor(event): + text_arg, reply = await get_text_arg(event) + + vapor_text = await vaporize(text_arg) + + if reply: + await reply.reply(vapor_text) + else: + await event.reply(vapor_text) + + +@ldr.add(pattern="zal") +async def zalgo(event): + text_arg, reply = await get_text_arg(event) + + zalgo_text = await zalgofy(text_arg) + + if reply: + await reply.reply(zalgo_text) + else: + await event.reply(zalgo_text) + + +@ldr.add(pattern="owo") +async def owo(event): + text_arg, reply = await get_text_arg(event) + + owo_text = await owoify(text_arg) + + if reply: + await reply.reply(owo_text) + else: + await event.reply(owo_text) + + +async def get_text_arg(event): + text_arg = event.pattern_match.group(1) + reply = False + + if text_arg: + pass + elif event.is_reply: + reply = await event.get_reply_message() + text_arg = reply.text + else: + text_arg = "Give me some text to fuck it up!" + + return text_arg, reply + + +async def shitpostify(text): + text = text.replace("dick", "peepee") + text = text.replace("ck", "cc") + text = text.replace("lol", "honk honk") + text = text.replace("though", "tho") + text = text.replace("cat", "pussy") + text = text.replace("dark", "dank") + + return text + + +async def mockify(text): + mock_text = "" + + for letter in text: + if len(mock_text) >= 2: + if ''.join(mock_text[-2:-1]).islower(): + mock_text += letter.upper() + continue + + if ''.join(mock_text[-2:-1]).isupper(): + mock_text += letter.lower() + continue + + if randint(1, 2) == randint(1, 2): + mock_text += letter.lower() + else: + mock_text += letter.upper() + + return mock_text + + +async def emojify(text): + text = text.replace("ab", "🆎") + text = text.replace("cl", "🆑") + text = text.replace("b", "🅱️") + text = text.replace("a", "🅰️") + text = text.replace("i", "ℹ️") + text = text.replace("AB", "🆎") + text = text.replace("CL", "🆑") + text = text.replace("B", "🅱️") + text = text.replace("A", "🅰️") + text = text.replace("I", "ℹ️") + + emoji_text = "" + + for letter in text: + if letter == " ": + emoji_text += choice(emoji) + else: + emoji_text += letter + + return emoji_text + + +async def vaporize(text): + vapor_text = "" + char_distance = 65248 + + for letter in text: + ord_letter = ord(letter) + if ord('!') <= ord_letter <= ord('~'): + letter = chr(ord_letter + char_distance) + vapor_text += letter + + return vapor_text + + +async def owoify(text): + text = text.replace("r", "w") + text = text.replace("R", "W") + text = text.replace("n", "ny") + text = text.replace("N", "NY") + text = text.replace("ll", "w") + text = text.replace("LL", "W") + text = text.replace("l", "w") + text = text.replace("L", "W") + + text += f" {choice(owo_faces)}" + + return text + + +async def zalgofy(text): + zalgo_text = "" + + for letter in text: + if letter == " ": + zalgo_text += letter + continue + + letter += choice(zal_chars) + letter += choice(zal_chars) + letter += choice(zal_chars) + zalgo_text += letter + + return zalgo_text diff --git a/ubot/modules/notes.py b/ubot/modules/notes.py new file mode 100644 index 0000000..6dadb6f --- /dev/null +++ b/ubot/modules/notes.py @@ -0,0 +1,119 @@ +# Copyright (C) 2019 Nick Filmer (nick80835@gmail.com) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +from ubot.micro_bot import micro_bot + +ldr = micro_bot.loader +db = micro_bot.database + +note_columns = ["notename", "notecontent"] + + +@ldr.add(pattern="save") +async def savenote(event): + notename, notecontent = await get_text_arg(event) + + if not notename or not notecontent: + await event.edit("`Provide both a note name and content to save!`") + return + + await event.edit(f"`Saving to note `**{notename}**`…`") + await db.single_row_write("Notes", note_columns, notename, notecontent) + await event.edit(f"`Successfully saved to note `**{notename}**`!`") + + +@ldr.add(pattern="get") +async def getnote(event): + notename = event.pattern_match.group(1).replace(" ", "_") + + if not notename: + notelist = '\n'.join(await db.single_column_readall("Notes", note_columns, "notename")) + + if notelist: + await event.edit(f"`Provide a note name to get its content, saved notes:`\n**{notelist}**") + return + else: + await event.edit(f"`You haven't saved any notes!\nUse `**{micro_bot.settings.get_config('cmd_prefix') or '.'}save**` to save notes.`") + return + + notecontent = await db.single_row_read("Notes", note_columns, notename) + + if notecontent: + await event.edit(f"{notecontent}") + else: + notelist = '\n'.join(await db.single_column_readall("Notes", note_columns, "notename")) + + if notelist: + await event.edit(f"`Note `**{notename}**` not found, saved notes:`\n**{notelist}**") + else: + await event.edit(f"`You haven't saved any notes!\nUse `**{micro_bot.settings.get_config('cmd_prefix') or '.'}save**` to save notes.`") + + +@ldr.add(incoming=True, noprefix=True, pattern="#(.*)") +async def getnoteincoming(event): + if not micro_bot.settings.get_bool("incoming_allowed"): + return + + notename = event.pattern_match.group(0).replace(" ", "_").lstrip("#") + + if not notename: + notelist = '\n'.join(await db.single_column_readall("Notes", note_columns, "notename")) + + if notelist: + await event.reply(f"`Provide a note name to get its content, saved notes:`\n**{notelist}**") + return + else: + await event.reply(f"`There aren't any saved notes!`") + return + + notecontent = await db.single_row_read("Notes", note_columns, notename) + + if notecontent: + await event.reply(f"{notecontent}") + else: + notelist = '\n'.join(await db.single_column_readall("Notes", note_columns, "notename")) + + if notelist: + await event.reply(f"`Note `**{notename}**` not found, saved notes:`\n**{notelist}**") + else: + await event.reply(f"`There aren't any saved notes!`") + + +@ldr.add(pattern="del") +async def delnote(event): + notename = event.pattern_match.group(1).replace(" ", "_") + + if not notename: + await event.edit("`I need a notes name to delete it!`") + return + + await db.single_row_delete("Notes", note_columns, notename) + await event.edit(f"`Note `**{notename}**` successfully deleted!`") + + +async def get_text_arg(event): + text_arg = event.pattern_match.group(1) + notename = None + notecontent = None + + if event.is_reply and text_arg: + reply = await event.get_reply_message() + notename = text_arg.replace(" ", "_") + notecontent = reply.text + elif text_arg: + notename = text_arg.split(" ")[0] + notecontent = " ".join(text_arg.split(" ")[1:]) + + return notename, notecontent diff --git a/ubot/modules/reddit.py b/ubot/modules/reddit.py new file mode 100644 index 0000000..871f7fd --- /dev/null +++ b/ubot/modules/reddit.py @@ -0,0 +1,153 @@ +# Copyright (C) 2019 Nick Filmer (nick80835@gmail.com) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +import io +from random import choice + +import praw +from aiohttp import ClientSession + +from ubot.micro_bot import micro_bot + +ldr = micro_bot.loader + +REDDIT = praw.Reddit(client_id='-fmzwojFG6JkGg', + client_secret=None, + user_agent='TG_Userbot') + +VALID_ENDS = (".mp4", ".jpg", ".jpeg", ".png", ".gif") + + +async def imagefetcherfallback(sub): + hot = REDDIT.subreddit(sub).hot() + hot_list = list(hot.__iter__()) + + for _ in range(10): + post = choice(hot_list) + + if post.url: + if post.url.endswith(VALID_ENDS): + return post.url, post.title + + return None, None + + +async def titlefetcherfallback(sub): + hot = REDDIT.subreddit(sub).hot() + hot_list = list(hot.__iter__()) + + return choice(hot_list).title + + +async def imagefetcher(event, sub): + image_url = False + + for _ in range(10): + post = REDDIT.subreddit(sub).random() + + if not post: + image_url, title = await imagefetcherfallback(sub) + break + + if post.url: + for ending in VALID_ENDS: + if post.url.endswith(ending): + file_ending = ending + image_url = post.url + title = post.title + break + + if not image_url: + await event.reply(f"`Failed to find any valid content on `**r/{sub}**`!`") + return + + try: + image_io = io.BytesIO() + session = ClientSession() + + async with session.get(image_url) as response: + if response.status == 200: + image_io.write(await response.read()) + image_io.name = f"reddit_content{file_ending}" + image_io.seek(0) + else: + raise Exception + + await session.close() + await event.reply(title, file=image_io) + except: + await session.close() + await event.reply(f"`Failed to download content from `**r/{sub}**`!`\n`Title: `**{title}**\n`URL: `{image_url}") + + +async def titlefetcher(event, sub): + post = REDDIT.subreddit(sub).random() + + if not post: + title = await titlefetcherfallback(sub) + else: + title = post.title + + await event.reply(title) + + +@ldr.add(pattern="redi") +async def redimg(event): + sub = event.pattern_match.group(1) + + if sub: + await imagefetcher(event, sub) + else: + await event.reply("Syntax: .redi ") + + +@ldr.add(pattern="redt") +async def redtit(event): + sub = event.pattern_match.group(1) + + if sub: + await titlefetcher(event, sub) + else: + await event.reply("Syntax: .redt ") + + +@ldr.add(pattern="suffer") +async def makemesuffer(event): + await imagefetcher(event, "MakeMeSuffer") + + +@ldr.add(pattern="snafu") +async def coaxedintoasnafu(event): + await imagefetcher(event, "CoaxedIntoASnafu") + + +@ldr.add(pattern="aita") +async def amitheasshole(event): + await titlefetcher(event, "AmITheAsshole") + + +@ldr.add(pattern="jon(x|)") +async def imsorryjon(event): + if "x" in event.pattern_match.group(0): + sub = "ImReallySorryJon" + else: + sub = "ImSorryJon" + + await imagefetcher(event, sub) + + +@ldr.add(pattern="tihi") +async def thanksihateit(event): + await imagefetcher(event, "TIHI") diff --git a/ubot/modules/system.py b/ubot/modules/system.py new file mode 100644 index 0000000..f61c83a --- /dev/null +++ b/ubot/modules/system.py @@ -0,0 +1,94 @@ +# Copyright (C) 2019 Nick Filmer (nick80835@gmail.com) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +from platform import python_version, uname +from time import time_ns + +from telethon import version + +from ubot.micro_bot import micro_bot + +ldr = micro_bot.loader + + +#@ldr.add(pattern="reload") +async def reload_modules(event): + await event.edit("`Reloading modules…`") + + errors = ldr.reload_all_modules() + + if errors: + await event.edit(errors) + else: + try: + await event.delete() + except: + pass + + +@ldr.add(pattern="alive") +async def alive(event): + alive_format = "`μBot is running under {0}.\n\n" \ + "Version: {1}\n" \ + "Telethon: {2}\n" \ + "Python: {3}`" + + await event.reply(alive_format.format(uname().node, ldr.botversion, version.__version__, python_version())) + + +#@ldr.add(pattern="shutdown") +async def shutdown(event): + await event.edit("`Goodbye…`") + await micro_bot.stop_client() + + +@ldr.add(pattern="ping") +async def ping(event): + start = time_ns() + reply = await event.reply("`Ping…`") + time_taken_ms = (time_ns() - start) / 1000000 + await reply.edit(f"`Ping… Pong! -> `**{time_taken_ms}**`ms`") + + +#@ldr.add(pattern="prefix") +async def change_prefix(event): + new_prefix = event.pattern_match.group(1) + + if not new_prefix: + await event.edit("`Please specify a valid command prefix!`") + return + + micro_bot.settings.set_config("cmd_prefix", new_prefix) + errors = ldr.reload_all_modules() + + if errors: + await event.edit(f"`Command prefix successfully changed to `**{new_prefix}**` but there were errors:`\n\n{errors}") + else: + await event.edit(f"`Command prefix successfully changed to `**{new_prefix}**`!`") + + +@ldr.add(pattern="repo") +async def bot_repo(event): + await event.reply("https://github.com/Nick80835/microbot") + + +#@ldr.add(pattern="toggleincoming") +async def toggleincoming(event): + if micro_bot.settings.get_bool("incoming_allowed"): + micro_bot.settings.set_config("incoming_allowed", "False") + await event.edit("`Successfully disabled incoming commands!`") + else: + micro_bot.settings.set_config("incoming_allowed", "True") + await event.edit("`Successfully enabled incoming commands!`") diff --git a/ubot/modules/urbandict.py b/ubot/modules/urbandict.py new file mode 100644 index 0000000..95c3c82 --- /dev/null +++ b/ubot/modules/urbandict.py @@ -0,0 +1,70 @@ +# Copyright (C) 2019 Nick Filmer (nick80835@gmail.com) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +import io + +from aiohttp import ClientSession + +from ubot.micro_bot import micro_bot + +ldr = micro_bot.loader + +UD_QUERY_URL = 'http://api.urbandictionary.com/v0/define' +UD_RANDOM_URL = 'http://api.urbandictionary.com/v0/random' + + +@ldr.add(pattern="ud") +async def urban_dict(event): + udquery = event.pattern_match.group(1) + + if udquery: + params = {'term': udquery} + url = UD_QUERY_URL + else: + params = None + url = UD_RANDOM_URL + + session = ClientSession() + + async with session.get(url, params=params) as response: + if response.status == 200: + response = await response.json() + else: + await event.reply(f"`An error occurred, response code:` **{response.status}**") + return + + await session.close() + + if response['list']: + response_word = response['list'][0] + else: + await event.reply(f"`No results for query:` **{udquery}**") + return + + definition = f"**{response_word['word']}**: `{response_word['definition']}`" + + if response_word['example']: + definition += f"\n\n**Example**: `{response_word['example']}`" + + definition = definition.replace("[", "").replace("]", "") + + if len(definition) >= 4096: + file_io = io.BytesIO() + file_io.write(bytes(definition.encode('utf-8'))) + file_io.name = f"definition of {response_word['word']}.txt" + await event.reply(file=file_io, caption="`Output was too large, sent it as a file.`") + return + + await event.reply(definition) diff --git a/ubot/resources/RobotoMono-Regular.ttf b/ubot/resources/RobotoMono-Regular.ttf new file mode 100644 index 0000000..5919b5d Binary files /dev/null and b/ubot/resources/RobotoMono-Regular.ttf differ diff --git a/ubot/settings.py b/ubot/settings.py new file mode 100644 index 0000000..d5ce2c0 --- /dev/null +++ b/ubot/settings.py @@ -0,0 +1,35 @@ +# Copyright (C) 2019 Nick Filmer (nick80835@gmail.com) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +from configparser import SafeConfigParser + + +class Settings(): + def __init__(self): + self.config = SafeConfigParser() + self.config.read("settings.ini") + + def get_config(self, key): + return self.config.get("DEFAULT", key, fallback=None) + + def get_bool(self, key): + return bool(self.config.get("DEFAULT", key, fallback=False) == "True") + + def set_config(self, key, value): + self.config.set("DEFAULT", key, value) + + with open('settings.ini', 'w') as config_file: + self.config.write(config_file) + config_file.close()