diff --git a/.gitignore b/.gitignore index b7faf40..1800114 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,6 @@ # Byte-compiled / optimized / DLL files __pycache__/ -*.py[codz] +*.py[cod] *$py.class # C extensions @@ -46,7 +46,7 @@ htmlcov/ nosetests.xml coverage.xml *.cover -*.py.cover +*.py,cover .hypothesis/ .pytest_cache/ cover/ @@ -106,24 +106,17 @@ ipython_config.py # commonly ignored for libraries. # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control #poetry.lock -#poetry.toml # pdm # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. -# pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python. -# https://pdm-project.org/en/latest/usage/project/#working-with-version-control #pdm.lock -#pdm.toml +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/latest/usage/project/#working-with-version-control +.pdm.toml .pdm-python .pdm-build/ -# pixi -# Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control. -#pixi.lock -# Pixi creates a virtual environment in the .pixi directory, just like venv module creates one -# in the .venv directory. It is recommended not to include this directory in version control. -.pixi - # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm __pypackages__/ @@ -136,7 +129,6 @@ celerybeat.pid # Environments .env -.envrc .venv env/ venv/ @@ -175,33 +167,8 @@ cython_debug/ # option (not recommended) you can uncomment the following to ignore the entire idea folder. #.idea/ -# Abstra -# Abstra is an AI-powered process automation framework. -# Ignore directories containing user credentials, local state, and settings. -# Learn more at https://abstra.io/docs -.abstra/ - -# Visual Studio Code -# Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore -# that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore -# and can be added to the global gitignore or merged into this file. However, if you prefer, -# you could uncomment the following to ignore the entire vscode folder -# .vscode/ - # Ruff stuff: .ruff_cache/ # PyPI configuration file -.pypirc - -# Cursor -# Cursor is an AI-powered code editor. `.cursorignore` specifies files/directories to -# exclude from AI features like autocomplete and code analysis. Recommended for sensitive data -# refer to https://docs.cursor.com/context/ignore-files -.cursorignore -.cursorindexingignore - -# Marimo -marimo/_static/ -marimo/_lsp/ -__marimo__/ +.pypirc \ No newline at end of file diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml new file mode 100644 index 0000000..ff71fcd --- /dev/null +++ b/.gitlab-ci.yml @@ -0,0 +1,160 @@ +stages: + - before + - update + - parse + - categories + - commit + - create_mr + - backup + - diff_after_merge + +variables: + BRANCH_NAME: "update-submodules_${CI_COMMIT_SHA}" + REPO_URL: "git.vsecoder.dev/root/limoka.git" + GITLAB_TOKEN: $GITLAB_TOKEN + GIT_DEPTH: 0 + +image: python:3.9 + +before_run: + stage: before + rules: + - if: '$CI_PIPELINE_SOURCE == "schedule"' + when: on_success + - when: never + script: + - pip install requests + - git config --global user.email "gitlab_admin_9dee57@example.com" + - git config --global user.name "Administrator" + - git fetch origin + - git checkout main + - git remote set-url origin "https://oauth2:${GITLAB_TOKEN}@${REPO_URL}" + - echo "Синхронизируем main с origin/main..." + - git reset --hard origin/main + - echo "Удаляем ветку ${BRANCH_NAME} из удалённого репозитория..." + - git push origin --delete ${BRANCH_NAME} || echo "Ветка ${BRANCH_NAME} не существовала или не удалось удалить" + - echo "Удаляем локальную ветку ${BRANCH_NAME}, если она существует..." + - git branch -D ${BRANCH_NAME} || echo "Локальная ветка ${BRANCH_NAME} не существовала" + - git checkout -b ${BRANCH_NAME} + - echo "Создаём новую ветку ${BRANCH_NAME}..." + - git push origin ${BRANCH_NAME} --force || echo "Ошибка при создании ветки" + +update_repos: + stage: update + rules: + - if: '$CI_PIPELINE_SOURCE == "schedule"' + when: on_success + - when: never + script: + - git checkout ${BRANCH_NAME} + - echo "Cloning and update repositories..." + - python3 clone_repos.py + - git add * + - git commit -m "Added and updated repositories $(date +'%Y-%m-%d %H:%M:%S')" || echo "No changes for commit" + - git remote set-url origin "https://oauth2:${GITLAB_TOKEN}@${REPO_URL}" + - git push origin ${BRANCH_NAME} --force + +parse: + stage: parse + rules: + - if: '($CI_PIPELINE_SOURCE == "merge_request_event" && $CI_MERGE_REQUEST_EVENT_TYPE == "merged") || $CI_COMMIT_BRANCH == "main"' + when: on_success + - when: never + script: + - echo "Запускаем parse после мержа MR..." + - git fetch origin + - git checkout main + - git reset --hard origin/main + - python3 parse.py + - python3 -m venv venv + - source venv/bin/activate + - pip install --upgrade pip + - pip install scikit-learn tqdm + - python3 categories.py + - git add modules.json + - git commit -m "Updated modules.json after merge $(date +'%Y-%m-%d %H:%M:%S')" || echo "No changes for modules.json" + - git remote set-url origin "https://oauth2:${GITLAB_TOKEN}@${REPO_URL}" + - git push origin main + +commit_changes: + stage: commit + rules: + - if: '$CI_PIPELINE_SOURCE == "schedule"' + when: on_success + - when: never + script: + - git checkout ${BRANCH_NAME} + - git add * || echo "No changes" + - git commit -m "Финальный коммит $(date +'%Y-%m-%d %H:%M:%S')" || echo "No changes for final commit" + - git remote set-url origin "https://oauth2:${GITLAB_TOKEN}@${REPO_URL}" + - git push origin ${BRANCH_NAME} --force + +create_merge_request: + stage: create_mr + rules: + - if: '$CI_PIPELINE_SOURCE == "schedule"' + when: on_success + - when: never + script: + - echo "Checking branch status before MR..." + - git fetch origin + - git log ${BRANCH_NAME} -1 + - git diff origin/main origin/${BRANCH_NAME} || echo "No diff between main and ${BRANCH_NAME}" + - echo "Creating Merge Request..." + - | + curl --request POST \ + --header "PRIVATE-TOKEN: ${GITLAB_TOKEN}" \ + --data "source_branch=${BRANCH_NAME}" \ + --data "target_branch=main" \ + --data "title=Update of repositories $(date +'%Y-%m-%d %H:%M:%S')" \ + "https://git.vsecoder.dev/api/v4/projects/root%2Flimoka/merge_requests" \ + || echo "MR creating failure" + +backup: + stage: backup + needs: ["parse"] + script: + - echo "$TELEGRAM_BOT_TOKEN" + - echo "Creating .zip file of the repository with maximum compression..." + - git archive --format=zip --output=$CI_PROJECT_DIR/repository-original.zip HEAD + - zip -9 $CI_PROJECT_DIR/repository.zip $CI_PROJECT_DIR/repository-original.zip + - rm $CI_PROJECT_DIR/repository-original.zip + - echo "File size of the created .zip file:" + - du -sh $CI_PROJECT_DIR/repository.zip + - echo "Splitting the .zip file into 8 parts..." + - split -b 49M $CI_PROJECT_DIR/repository.zip $CI_PROJECT_DIR/repository-part- + - echo "Files created after split:" + - ls $CI_PROJECT_DIR/repository-part-* + - | + COMMIT_MESSAGE="$(git log -1 --pretty=%B)" + COMMIT_DATE="$(date --date="$(git log -1 --pretty=%ci)" +'%Y-%m-%d %H:%M:%S')" + COMMIT_HASH="$(git rev-parse --short=6 HEAD)" + COMMIT_URL="https://git.vsecoder.dev/root/limoka/commit/$(git rev-parse HEAD)" + MESSAGE="Commit Date: $COMMIT_DATE, Commit Message: $COMMIT_MESSAGE, Commit Hash: [\`$COMMIT_HASH\`]($COMMIT_URL)" + + echo "Sending .zip file parts to Telegram..." + + FIRST_PART=true + for part in $(ls $CI_PROJECT_DIR/repository-part-* | sort); do + if $FIRST_PART; then + TELEGRAM_API_URL="https://api.telegram.org/bot$TELEGRAM_BOT_TOKEN/sendDocument" + echo "Отправка первого файла на Telegram: $TELEGRAM_API_URL" + curl -X POST "$TELEGRAM_API_URL" \ + -F chat_id=$TELEGRAM_CHAT_ID \ + -F document=@$part \ + -F caption="$MESSAGE" \ + -F parse_mode="Markdown" + FIRST_PART=false + else + TELEGRAM_API_URL="https://api.telegram.org/bot$TELEGRAM_BOT_TOKEN/sendDocument" + echo "Отправка файла на Telegram: $TELEGRAM_API_URL" + curl -X POST "$TELEGRAM_API_URL" \ + -F chat_id=$TELEGRAM_CHAT_ID \ + -F document=@$part + fi + done + + - echo "Files sent to Telegram successfully!" + only: + - main + diff --git a/1jpshiro/hikka-modules/Autotime.py b/1jpshiro/hikka-modules/Autotime.py new file mode 100644 index 0000000..e540c79 --- /dev/null +++ b/1jpshiro/hikka-modules/Autotime.py @@ -0,0 +1,125 @@ +# --------------------------------------------------------------------------------- +# Author: @shiro_hikka +# Name: Autotime +# Description: Automatic stuff for your profile +# Commands: autoname, autobio, cfgset +# --------------------------------------------------------------------------------- +# © Copyright 2025 +# +# 🔒 Licensed under the GNU AGPLv3 +# 🌐 https://www.gnu.org/licenses/agpl-3.0.html +# --------------------------------------------------------------------------------- +# scope: hikka_only +# meta developer: @shiro_hikka +# meta banner: https://0x0.st/s/FIR0RnhUN5pZV5CZ6sNFEw/8KBz.jpg +# --------------------------------------------------------------------------------- + +__version__ = (1, 0, 0) + +from telethon.tl.functions.account import UpdateProfileRequest +from telethon.utils import get_display_name +from telethon.tl.functions.users import GetFullUserRequest +from telethon.tl.types import Message + +from .. import loader, utils +import re +import datetime +import asyncio + +@loader.tds +class Autotime(loader.Module): + """Automatic stuff for your profile""" + + strings = { + "name": "Autotime", + "no_time": "😒 You didn't place a {time}", + "cfg": "Positive or negative integer from -12 to 12 inclusively" + } + + def __init__(self): + self.bio_on = False + self.name_on = False + + self.config = loader.ModuleConfig( + loader.ConfigValue( + "Timezone", + "0", + lambda: self.strings["cfg"], + validator=loader.validators.Integer() + ) + ) + + async def client_ready(self): + self.me = await self.client.get_me() + + def _time(self): + offset = datetime.timedelta(hours=self.config["Timezone"]) + tz = datetime.timezone(offset) + now = datetime.datetime.now(tz) + time = now.strftime("%H:%M") + return time + + + async def cfgsetcmd(self, message: Message): + """ - specify a timezone + Regarding to UTC+0""" + tz = utils.get_args_raw(message) + q = await self.invoke( + "fconfig", + f"{self.strings('name')} Timezone {tz}", + message.chat.id + ) + + await self.client.delete_messages(message.chat.id, [message, q]) + + async def autonamecmd(self, message: Message): + """ - autotime in nickname | {time} must be placed in the text + Write without argument to disable""" + args = utils.get_args_raw(message) + + if not args: + self.name_on = False + regex = r"\d\d:\d\d" + name = utils.escape_html(get_display_name(self.me)) + name = re.sub(regex, "", name) + name.replace(" ", "") + + await self.client(UpdateProfileRequest(first_name=name)) + return await message.delete() + + if "{time}" not in args: + return await utils.answer(message, self.strings["no_time"]) + + self.name_on = True + await message.delete() + + while self.name_on: + text = args.replace("{time}", self._time()) + await self.client(UpdateProfileRequest(first_name=text)) + await asyncio.sleep(180) + + async def autobiocmd(self, message: Message): + """ - autotime in bio | {time} must be placed in the text + Write without argument to disable""" + args = utils.get_args_raw(message) + + if not args: + self.bio_on = False + regex = r"\d\d:\d\d" + bio = (await self.client(GetFullUserRequest(self.tg_id))).full_user.about + bio = re.sub(regex, "", bio) + bio.replace(" ", " ") + + await self.client(UpdateProfileRequest(about=bio)) + return await message.delete() + + if "{time}" not in args: + return await utils.answer(message, self.strings["no_time"]) + + self.bio_on = True + await message.delete() + + while self.bio_on: + text = args.replace("{time}", self._time()) + await self.client(UpdateProfileRequest(about=text)) + await asyncio.sleep(180) diff --git a/1jpshiro/hikka-modules/ChannelImitator.py b/1jpshiro/hikka-modules/ChannelImitator.py new file mode 100644 index 0000000..c411b11 --- /dev/null +++ b/1jpshiro/hikka-modules/ChannelImitator.py @@ -0,0 +1,253 @@ +# --------------------------------------------------------------------------------- +# Author: @shiro_hikka +# Name: Channel Imitator +# Description: Imitates someone else's channel in yours +# Commands: imitate +# --------------------------------------------------------------------------------- +# © Copyright 2025 +# +# 🔒 Licensed under the GNU AGPLv3 +# 🌐 https://www.gnu.org/licenses/agpl-3.0.html +# --------------------------------------------------------------------------------- +# scope: hikka_only +# meta developer: @shiro_hikka +# meta banner: https://0x0.st/s/FIR0RnhUN5pZV5CZ6sNFEw/8KBz.jpg +# --------------------------------------------------------------------------------- + +__version__ = (1, 1, 0) + +from .. import loader, utils +from telethon.tl.functions.messages import EditChatAboutRequest +from telethon.tl.functions.channels import ( + ToggleSignaturesRequest, + EditPhotoRequest, + GetFullChannelRequest, + EditTitleRequest +) +from telethon.tl.types import ( + Message, + MessageMediaUnsupported, + MessageMediaPoll, + Channel, + Chat, + User +) +from telethon.tl.functions.account import UpdateProfileRequest +import asyncio +import io + +@loader.tds +class ChannelImitator(loader.Module): + """ + Imitates someone else's channel in yours + Make assured your channel doesn't include avatars before using otherwise stolen ones will be overlayed with them + !!!If your account is experiencing a frequent floodwait limitations specify at least 150 in the Cooldown config!!! + """ + + strings = { + "name": "ChannelImitator", + "start": "👨‍💻 It will take a few minutes.... probably much more", + "cfg_author": "Specify a text that will appear instead of an absencing real author name", + "cfg_forwarded": "Specify a text that will will appear instead of an absecing real forwarder name", + "cfg_cooldown": "Specify a cooldown time between every sending" + } + + def __init__(self): + self.me = await self.client.get_me() + + self.config = loader.ModuleConfig( + loader.ConfigValue( + "Your channel", + None, + lambda: "Specify your channel ID", + validator=loader.validators.TelegramID() + ), + loader.ConfigValue( + "Another channel", + None, + lambda: "Specify an another channel ID", + validator=loader.validators.TelegramID() + ), + loader.ConfigValue( + "Author replacer", + "No author", + lambda: self.strings["cfg_author"], + validator=loader.validators.String() + ), + loader.ConfigValue( + "Forwarded replacer", + "Unknown", + lambda: self.strings["cfg_forwarded"], + validator=loader.validators.String() + ), + loader.ConfigValue( + "Cooldown", + 60, + lambda: self.strings["cfg_cooldown"], + validator=loader.validators.Integer() + ) + ) + + async def checkData(self, iterList, item): + is_ignore = False + is_noneCaption = False + is_fwd = True if item.fwd_from else False + is_media = True if item.media else False + media = None + name = None + name_id = None + author = item.post_author if item.post_author else self.config["Author replacer"] + + if is_media and (isinstance(item.media, (MessageMediaUnsupported, MessageMediaPoll)) or hasattr(item.media, "months")): + is_ignore = True + + try: + text = item.text + except: + text = "§" + + if is_media and text == "": + is_noneCaption = True + media = io.BytesIO(await item.download_media(bytes)) + + if isinstance(media, MessageMediaUnsupported): + media = None + elif is_ignore: + pass + elif hasattr(item.media, "photo"): + media.name = "photo.png" + else: + media.name = item.media.document.mime_type.replace("/", ".") + + if is_fwd: + if item.fwd_from.from_id: + name_id = item.fwd_from.from_id + + try: + entity = await self.client.get_entity(name_id) + if isinstance(entity, (Channel, Chat)): + name = entity.title + + elif isinstance(entity, User): + if entity.first_name: + name = f"{entity.first_name} {entity.last_name if entity.lastname else ''}" + else: + name = "Deleted" + + except: + name = self.config["Forwarded replacer"] + else: + name = item.fwd_from.from_name if item.fwd_from.from_name else self.config["Forwarded replacer"] + + _dict = { + "media": media, + "text": text, + "author": author, + "name": name, + "id": name_id, + "is_media": is_media, + "is_noneCaption": is_noneCaption + } + iterList.append(_dict) + return iterList + + async def imitatecmd(self, message: Message): + """ [limit: int] [-save] - save all the media and messages from specified channel + -save - simply save without changing title, bio and/or avatars""" + args = (utils.get_args_raw(message)).split() + limit = None + if args: + limit = int(args[0]) if args[0].isdigit() else None + + yourChannel = self.config["Your channel"] + anotherChannel = self.config["Another channel"] + if not all(isinstance(i, Channel) for i in [ + (await self.client.get_entity(yourChannel)), + (await self.client.get_entity(anotherChannel)) + ]): + return await utils.answer(message, "Please specify a channel ID") + + initName = self.me.first_name + iterList = [] + + if not "-save" in args: + _photos = [] + entity = await self.client(GetFullChannelRequest(anotherChannel)) + title = entity.chats[0].title + bio = entity.full_chat.about + + photos = await self.client.get_profile_photos(anotherChannel) + if photos: + for photo in photos: + _photos.append(photo) + _photos = _photos[::-1] + + await utils.answer(message, self.strings["start"]) + + try: + if "-save" in args: + await self.client(EditChatAboutRequest(yourChannel, bio)) + await self.client(EditTitleRequest(yourChannel, title)) + if _photos: + for _photo in _photos: + await self.client(EditPhotoRequest(yourChannel, _photo)) + + await self.client(ToggleSignaturesRequest(yourChannel, enabled=True)) + except: + pass + + async for i in self.client.iter_messages(anotherChannel, limit=limit): + await self.checkData(iterList, item=i) + + iterList = iterList[::-1] + for i in iterList: + media = i["media"] + text = i["text"] + author = i["author"] + name = i["name"] + name_id = i["id"] + is_media = i["is_media"] + is_noneCaption = i["is_noneCaption"] + + if not is_media and text == "§": + continue + + if self.me.first_name != author: + await self.client(UpdateProfileRequest(first_name=author)) + + if is_media and media: + if is_noneCaption: + try: + await message.client.send_file(yourChannel, media) + except: + await message.client.send_message(yourChannel, "Just a poll") + + try: + await message.client.send_message( + yourChannel, + f"↑ forwarded from {name}" if name_id else f"forwarded from {name}" if name else "" + ) + except: + pass + else: + await message.client.send_file( + yourChannel, + media, + caption="".join(( + f"forwarded from {name}:\n\n" if name_id else f"forwarded from {name}:\n\n" if name else "", + text + )) + ) + await asyncio.sleep(self.config["Cooldown"]) + else: + await message.client.send_message( + yourChannel, + "".join(( + f"forwarded from {name}:\n\n" if name_id else f"forwarded from {name}:\n\n" if name else "", + text + )) + ) + await asyncio.sleep(self.config["Cooldown"]) + + await self.client(UpdateProfileRequest(first_name=initName)) + await utils.answer(message, "😎 Done") diff --git a/1jpshiro/hikka-modules/Counter.py b/1jpshiro/hikka-modules/Counter.py new file mode 100644 index 0000000..85e3aa8 --- /dev/null +++ b/1jpshiro/hikka-modules/Counter.py @@ -0,0 +1,95 @@ +# --------------------------------------------------------------------------------- +# Author: @shiro_hikka +# Name: Counter +# Description: Inline Clicks Counter +# Commands: count, creset +# --------------------------------------------------------------------------------- +# © Copyright 2025 +# +# 🔒 Licensed under the GNU AGPLv3 +# 🌐 https://www.gnu.org/licenses/agpl-3.0.html +# --------------------------------------------------------------------------------- +# scope: hikka_only +# meta developer: @shiro_hikka +# meta banner: https://0x0.st/s/FIR0RnhUN5pZV5CZ6sNFEw/8KBz.jpg +# --------------------------------------------------------------------------------- + +__version__ = (1, 0, 1) + +from .. import loader, utils +from telethon.tl.types import Message +from ..inline.types import InlineCall +import asyncio + +@loader.tds +class Counter(loader.Module): + """Inline Clicks Counter""" + + strings = { + "name": "Counter", + "count": "Counter: {}" + } + + async def client_ready(self): + counts = self.db.get(__name__, "c") + users = self.db.get(__name__, "u") + if not counts: + self.db.set(__name__, "c", 0) + if not users: + self.db.set(__name__, "u", []) + + + async def cresetcmd(self, message: Message): + """ [-u] [-c] - reset the counter\n-u (users list) -c (counts list)""" + args = (utils.get_args_raw(message)).split() + + if all(i not in ["-u", "-c"] for i in args): + return await utils.answer(message, "🤨 Incorrect flag") + + if "-u" in args: + self.db.set(__name__, "u", []) + if "-c" in args: + self.db.set(__name__, "c", 0) + + await message.delete() + + async def countcmd(self, message: Message): + """ Creates an inline button for counting a presses""" + counts = self.db.get(__name__, "c") + + await self.inline.form( + text=self.strings["count"].format(counts), + message=message, + reply_markup=[ + { + "text": "Click", + "callback": self.back + } + ], + disable_security=True + ) + + + async def back(self, call: InlineCall): + id = call.from_user.id + if id in self.db.get(__name__, "u"): + return + + counts = self.db.get(__name__, "c") + counts += 1 + self.db.set(__name__, "c", counts) + + users = self.db.get(__name__, "u") + users.append(id) + self.db.set(__name__, "u", users) + + await call.edit( + text=self.strings["count"].format(counts), + reply_markup=[ + { + "text": "Click", + "callback": self.back + } + ], + disable_security=True + ) diff --git a/1jpshiro/hikka-modules/LICENSE b/1jpshiro/hikka-modules/LICENSE new file mode 100644 index 0000000..0ad25db --- /dev/null +++ b/1jpshiro/hikka-modules/LICENSE @@ -0,0 +1,661 @@ + GNU AFFERO GENERAL PUBLIC LICENSE + Version 3, 19 November 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU Affero General Public License is a free, copyleft license for +software and other kinds of works, specifically designed to ensure +cooperation with the community in the case of network server software. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +our General Public Licenses are intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + Developers that use our General Public Licenses protect your rights +with two steps: (1) assert copyright on the software, and (2) offer +you this License which gives you legal permission to copy, distribute +and/or modify the software. + + A secondary benefit of defending all users' freedom is that +improvements made in alternate versions of the program, if they +receive widespread use, become available for other developers to +incorporate. Many developers of free software are heartened and +encouraged by the resulting cooperation. However, in the case of +software used on network servers, this result may fail to come about. +The GNU General Public License permits making a modified version and +letting the public access it on a server without ever releasing its +source code to the public. + + The GNU Affero General Public License is designed specifically to +ensure that, in such cases, the modified source code becomes available +to the community. It requires the operator of a network server to +provide the source code of the modified version running there to the +users of that server. Therefore, public use of a modified version, on +a publicly accessible server, gives the public access to the source +code of the modified version. + + An older license, called the Affero General Public License and +published by Affero, was designed to accomplish similar goals. This is +a different license, not a version of the Affero GPL, but Affero has +released a new version of the Affero GPL which permits relicensing under +this license. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU Affero General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Remote Network Interaction; Use with the GNU General Public License. + + Notwithstanding any other provision of this License, if you modify the +Program, your modified version must prominently offer all users +interacting with it remotely through a computer network (if your version +supports such interaction) an opportunity to receive the Corresponding +Source of your version by providing access to the Corresponding Source +from a network server at no charge, through some standard or customary +means of facilitating copying of software. This Corresponding Source +shall include the Corresponding Source for any work covered by version 3 +of the GNU General Public License that is incorporated pursuant to the +following paragraph. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the work with which it is combined will remain governed by version +3 of the GNU General Public License. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU Affero General Public License from time to time. Such new versions +will be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU Affero General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU Affero General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU Affero General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + 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 . + +Also add information on how to contact you by electronic and paper mail. + + If your software can interact with users remotely through a computer +network, you should also make sure that it provides a way for users to +get its source. For example, if your program is a web application, its +interface could display a "Source" link that leads users to an archive +of the code. There are many ways you could offer source, and different +solutions will be better for different programs; see section 13 for the +specific requirements. + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU AGPL, see +. diff --git a/1jpshiro/hikka-modules/MessageEraser.py b/1jpshiro/hikka-modules/MessageEraser.py new file mode 100644 index 0000000..d6cb52c --- /dev/null +++ b/1jpshiro/hikka-modules/MessageEraser.py @@ -0,0 +1,139 @@ +# --------------------------------------------------------------------------------- +# Author: @shiro_hikka +# Name: Message Eraser +# Description: Delete your messages in the current chat +# Commands: purge, stoppurge +# --------------------------------------------------------------------------------- +# © Copyright 2025 +# +# 🔒 Licensed under the GNU AGPLv3 +# 🌐 https://www.gnu.org/licenses/agpl-3.0.html +# --------------------------------------------------------------------------------- +# scope: hikka_only +# meta developer: @shiro_hikka +# meta banner: https://0x0.st/s/FIR0RnhUN5pZV5CZ6sNFEw/8KBz.jpg +# --------------------------------------------------------------------------------- + +__version__ = (1, 2, 3) + +from .. import loader, utils +from telethon.tl.types import Message +import asyncio +import random + +@loader.tds +class MessageEraser(loader.Module): + """Delete your messages in the current chat""" + + strings = { + "name": "MessageEraser", + "enabled": "😒 It's not operational now anyway", + "disabled": "❄️ Operation status changed to disabled", + "interrupted": "😀 The deletion was interrupted because you changed your mind", + "none": "👁️ You didn't even intend to delete anything here, but anyway it's disabled now" + } + + async def client_ready(self): + status = self.db.get(__name__, "status", None) + if status is None: + self.db.set(__name__, "status", {}) + + + async def stoppurgecmd(self, message: Message): + """ + Interrupt the deletion process + Use in the chat where you've previously started deletion + """ + chat_id = utils.get_chat_id(message) + + status = self.db.get(__name__, "status", {}) + _status = status.get(chat_id, None) + status[chat_id] = False + self.db.set(__name__, "status", status) + + if _status is True: + await utils.answer(message, self.strings["disabled"]) + elif _status is False: + await utils.answer(message, self.strings["enabled"]) + else: + await utils.answer(message, self.strings["none"]) + + async def purgecmd(self, message: Message): + """ + [reply] [10s / 10m / 10h / 10d] [-all] - delete all your messages in the current chat or only ones up to the message you replied to + Possible to do with a delay + -all - to delete messages from each topic if this is a forum otherwise flag'll just be ignored + Example: 10h 3d + """ + args = (utils.get_args_raw(message)).split() + if "-all" in args: + is_each = True + args.remove("-all") + else: + is_each = False + + reply = await message.get_reply_message() + chat_id = utils.get_chat_id(message) + delay = 0 + + is_last = False + is_forum = (await self.client.get_entity(chat_id)).forum + + status = self.db.get(__name__, "status", {}) + status[chat_id] = True + self.db.set(__name__, "status", status) + + if args: + for i in args: + if len(i) < 2 or not i[:-1].isdigit(): + continue + + delay += ( + {"d": 86400, "h": 3600, "m": 60, "s": 1}.get(i[-1], 0) * i[:-1] + ) + + + await asyncio.sleep(delay) + + batch = [] + async for _message in self.client.iter_messages(chat_id): + status = self.db.get(__name__, "status", {}) + if status.get(chat_id, None) is not True: + return await utils.answer(message, self.strings["interrupted"]) + + if _message.from_id != self.tg_id: + continue + + if is_forum and not is_each and utils.get_topic(message) != utils.get_topic(_message): + continue + + if len(batch) == 10: + await asyncio.sleep(self.getRandomDelay) + await message.client.delete_messages(chat_id, batch) + batch = [] + + if reply: + if is_last: + break + if _message.id == reply.id: + is_last = True + + batch.append(_message.id) + + if len(batch) != 0: + await message.client.delete_messages(chat_id, batch) + batch = [] + + await utils.answer(message, "🤩 Done") + + + def getRandomDelay(self): + """A self-made function, creatively designed for generating a random float""" + rangeList = random.choice([(2.1, 3.9), (4.4, 6.7), (7.5, 9.1), (9.4, 10.4)]) + randomRange = random.uniform(rangeList[0], rangeList[1]) + randomSubRange = random.uniform(0.800, 1.399) + + randomNum = randomRange * random.random() + (random.random() + 1.0) * randomSubRange + randomNum *= 3.8 if randomNum < 3 else 2.4 if randomNum < 5 else 1.3 + + return round(randomNum, 3) \ No newline at end of file diff --git a/1jpshiro/hikka-modules/PMStat.py b/1jpshiro/hikka-modules/PMStat.py new file mode 100644 index 0000000..677a016 --- /dev/null +++ b/1jpshiro/hikka-modules/PMStat.py @@ -0,0 +1,54 @@ +# --------------------------------------------------------------------------------- +# Author: @shiro_hikka +# Name: PMStat +# Description: Defines how many messages did you and your chat partner write +# Commands: stat +# --------------------------------------------------------------------------------- +# © Copyright 2025 +# +# 🔒 Licensed under the GNU AGPLv3 +# 🌐 https://www.gnu.org/licenses/agpl-3.0.html +# --------------------------------------------------------------------------------- +# scope: hikka_only +# meta developer: @shiro_hikka +# meta banner: https://0x0.st/s/FIR0RnhUN5pZV5CZ6sNFEw/8KBz.jpg +# --------------------------------------------------------------------------------- + +__version__ = (1, 0, 0) + +from .. import loader, utils +from telethon.tl.types import Message + +@loader.tds +class PMStat(loader.Module): + """Defines how many messages did you and your chat partner write""" + + strings = { + "name": "PMStat", + "q": "👨‍💻 All in all, {} messages were counted from {}", + "pm": "🤨 Use in PM only" + } + + async def statcmd(self, message: Message): + """ [-p] [-s] - (-p - counts your chat partner messages) (-s - send result to the saved messages)""" + args = utils.get_args_raw(message) + if not message.is_private: + return await utils.answer(message, self.strings["pm"]) + + await message.delete() + + chat = await self.client.get_entity(message.peer_id.user_id) + target = "you" if "-p" not in args else f"{chat.first_name}" + s = chat.id if "-s" not in args else self.tg_id + count = 0 + messagesList = [] + + async for i in self.client.iter_messages(chat.id): + if "-p" in args: + if i.from_id != self.tg_id: + messagesList.append(i) + else: + if i.from_id == self.tg_id: + messagesList.append(i) + + await message.client.send_message(s, self.strings["q"].format(len(messagesList), target)) diff --git a/1jpshiro/hikka-modules/StickerStealer.py b/1jpshiro/hikka-modules/StickerStealer.py new file mode 100644 index 0000000..86c25e5 --- /dev/null +++ b/1jpshiro/hikka-modules/StickerStealer.py @@ -0,0 +1,135 @@ +# --------------------------------------------------------------------------------- +# Author: @shiro_hikka +# Name: Sticker stealer +# Description: Emoji / Sticker pickpocket +# Commands: steal +# --------------------------------------------------------------------------------- +# © Copyright 2025 +# +# 🔒 Licensed under the GNU AGPLv3 +# 🌐 https://www.gnu.org/licenses/agpl-3.0.html +# --------------------------------------------------------------------------------- +# scope: hikka_only +# meta developer: @shiro_hikka +# meta banner: https://0x0.st/s/FIR0RnhUN5pZV5CZ6sNFEw/8KBz.jpg +# --------------------------------------------------------------------------------- + +__version__ = (1, 0, 1) + +from .. import loader, utils +from telethon.tl.types import Message +import asyncio + +@loader.tds +class StickerStealer(loader.Module): + """Emoji / Sticker pickpocket""" + + strings = { + "name": "StickerStealer", + "incorrect": "🤨 It's not a sticker or emoji" + } + + def __init__(self): + self.config = loader.ModuleConfig( + loader.ConfigValue( + "Emoji pack", + "emsaved", + lambda: "Specify a name of your emoji pack", + ), + loader.ConfigValue( + "Animated sticker pack", + "vssaved", + lambda: "Specify a name of your animated sticker pack" + ), + loader.ConfigValue( + "Static sticker pack", + "sssaved", + lambda: "Specify a name of your static sticker pack" + ) + ) + + + def checkType(self, reply, message): + if hasattr(reply, "media"): + if hasattr(reply.media, "document"): + mime_type = reply.media.document.mime_type.split('/') + if mime_type[1] == "webp": + return 3 + elif mime_type[1] == "webm": + return 2 + + if reply.entities: + return 1 + + else: + return 0 + + + async def stealcmd(self, message: Message): + """ - add an emoji or sticker to your pack + Emoji: one type of emoji only is possible to be used at time""" + await utils.answer(message, "....") + reply = await message.get_reply_message() + bot = "Stickers" + + cfg_ref = { + 1: self.config["Emoji pack"], + 2: self.config["Animated sticker pack"], + 3: self.config["Static sticker pack"] + } + entity_type = { + 1: "An emoji", + 2: "A sticker", + 3: "A sticker" + } + + async with self.client.conversation(bot) as bot: + _entity_type = self.checkType(reply, message) + if _entity_type == 0: + return await utils.answer(message, self.strings["incorrect"]) + + elif _entity_type == 1: + outgoing = await bot.send_message("/addemoji") + else: + outgoing = await bot.send_message("/addsticker") + response = await bot.get_response() + + await asyncio.sleep(2) + await outgoing.delete() + await response.delete() + + if _entity_type == 1: + outgoing = await bot.send_message(self.config["emoji"]) + elif _entity_type == 2: + outgoing = await bot.send_message(self.config["video_sticker"]) + else: + outgoing = await bot.send_message(self.config["static_sticker"]) + + response = await bot.get_response() + await asyncio.sleep(2) + await response.delete() + await outgoing.delete() + + if response.text == "Не выбран набор стикеров.": + return await utils.answer(message, f"Create {entity_type[_entity_type].lower()} pack with a public name {cfg_ref[_entity_type]}") + + if _entity_type == 1: + emoji = reply.message + toSend = reply + else: + emoji = reply.media.document.attributes[1].alt + toSend = reply + + outgoing = await bot.send_message(toSend) + response = await bot.get_response() + await asyncio.sleep(2) + await outgoing.delete() + await response.delete() + + outgoing = await bot.send_message(emoji) + response = await bot.get_response() + await asyncio.sleep(2) + await outgoing.delete() + await response.delete() + + await utils.answer(message, f"{entity_type[_entity_type]} added") diff --git a/1jpshiro/hikka-modules/Timer.py b/1jpshiro/hikka-modules/Timer.py new file mode 100644 index 0000000..d613dbb --- /dev/null +++ b/1jpshiro/hikka-modules/Timer.py @@ -0,0 +1,80 @@ +# --------------------------------------------------------------------------------- +# Author: @shiro_hikka +# Name: Timer +# Description: Creates fine adorned timer +# Commands: timer +# --------------------------------------------------------------------------------- +# © Copyright 2025 +# +# 🔒 Licensed under the GNU AGPLv3 +# 🌐 https://www.gnu.org/licenses/agpl-3.0.html +# --------------------------------------------------------------------------------- +# scope: hikka_only +# meta developer: @shiro_hikka +# meta banner: https://0x0.st/s/FIR0RnhUN5pZV5CZ6sNFEw/8KBz.jpg +# --------------------------------------------------------------------------------- + +__version__ = (1, 0, 0) + +from .. import loader, utils +from telethon.tl.types import Message +import re +import asyncio + +@loader.tds +class Timer(loader.Module): + """Creates fine adorned timer""" + + strings = { + "name": "Timer", + "q": "Current Timer for {}\n👾 {} left" + } + + async def parseArgs(self, message, args, parsed): + for arg in args: + if arg[-1] not in ["h", "m", "s"]: + args.remove(arg) + + for arg in args: + parsed[arg[-1]] = int(re.sub(r"[^0-9]", "", arg)) + return parsed + + + async def timercmd(self, message: Message): + """ [5h 5m 5s] - launch the timer""" + args = (utils.get_args_raw(message)).split() + parsed = {"h": None, "m": None, "s": None} + if not args: + return await utils.answer(message, "Specify time") + + _parsed = await self.parseArgs(message, args, parsed) + if all(_parsed[i] is None for i in parsed): + return await utils.answer(message, "Time isn't specified") + + hours = _parsed["h"] * 3600 if _parsed["h"] else 0 + mins = _parsed["m"] * 60 if _parsed["m"] else 0 + secs = _parsed["s"] if _parsed["s"] else 0 + _time = secs + mins + hours + + c = f"{hours}:{mins}:{secs}" + pretime = "{}:{}" + while _time > -1: + h = f"{_time//3600}" + m = f"{_time%3600//60}" + s = f"{_time%3600%60}" + if _time > 59: + q = self.strings["q"].format(c, pretime.format(h, m)) + else: + q = self.strings["q"].format(c, pretime.format(h, f"{m}:{s}")) + + try: + await utils.answer(message, q) + except: + pass + + _time -= 1 + await asyncio.sleep(1) + + regex = r"\..*\<.*?\>.*" + answer = re.sub(regex, "\n Time's over", q.replace("\n", ".")) + await utils.answer(message, answer) diff --git a/1jpshiro/hikka-modules/Tracker.py b/1jpshiro/hikka-modules/Tracker.py new file mode 100644 index 0000000..d7b331c --- /dev/null +++ b/1jpshiro/hikka-modules/Tracker.py @@ -0,0 +1,284 @@ +# --------------------------------------------------------------------------------- +# Author: @shiro_hikka +# Name: Tracker +# Description: Tracks the change history of usernames and nicknames of users +# Commands: track, addtrack, deltrack, trackstat +# --------------------------------------------------------------------------------- +# © Copyright 2025 +# +# 🔒 Licensed under the GNU AGPLv3 +# 🌐 https://www.gnu.org/licenses/agpl-3.0.html +# --------------------------------------------------------------------------------- +# scope: hikka_only +# meta developer: @shiro_hikka +# meta banner: https://0x0.st/s/FIR0RnhUN5pZV5CZ6sNFEw/8KBz.jpg +# --------------------------------------------------------------------------------- + +__version__ = (1, 1, 0) + +from .. import loader, utils +from telethon.tl.types import Message +from ..inline.types import InlineCall +import datetime +import time as t +import re + +@loader.tds +class Tracker(loader.Module): + """Tracks the change history of usernames and nicknames of users""" + + strings = { + "name": "Tracker", + "enabled": "The tracker successfully enabled", + "disabled": "The tracker successfully disabled", + "no_user": "It seems this user doesn't exist, try another ID/Username", + "change_status": "You just changed a status of tracking the user", + "new_user": "You've successfully added a new user to track", + "no_stat": "You're currently tracking no user", + "only_one": "You're currently tracking only one user", + "removed": "You've removed this user from the track list and each ID was descendingly replaced", + "not_removed": "This user isn't added to the list so there's nobody to remove", + "exists": "This user's already included in the track list, he's ID is {}", + "cfg": "Specify a period of the cooldown between checks" + } + + def __init__(self): + self.config = loader.ModuleConfig( + loader.ConfigValue( + "Cooldown", + 120, + lambda: self.strings["cfg"], + validator = loader.validators.Integer() + ) + ) + + async def client_ready(self): + if not self.db.get(__name__, "status"): + self.db.set(__name__, "status", False) + + if not self.db.get(__name__, "users"): + self.db.set(__name__, "users", {}) + + if not self.db.get(__name__, "time"): + self.db.set(__name__, "time", t.time()) + + + async def trackcmd(self, message: Message): + """ Enable / Disable the tracking""" + status = not(self.db.get(__name__, "status")) + self.db.set(__name__, "status", status) + + if status is True: + await utils.answer(message, self.strings["enabled"]) + + else: + await utils.answer(message, self.strings["disabled"]) + + async def addtrackcmd(self, message: Message): + """ - add a new user to track""" + args = utils.get_args_raw(message) + users = self.db.get(__name__, "users") + ID = len(users) + 1 + ID = str(ID) + + try: + user = await self.client.get_entity(int(args) if args.isdigit() else args) + + except Exception: + return await utils.answer(message, self.strings["no_user"]) + + for _user in users: + if users[_user]["user_id"] == user.id: + return await utils.answer(message, self.strings["exists"].format(_user)) + + UID = user.id + nick = f"{user.first_name} {user.last_name}" if user.last_name else user.first_name + username = f"@{user.username}" if user.username else "Empty" + + time = datetime.datetime.now() + date = str(time.date()).split('-') + hms = str(time.time()).split(':') + + users[ID] = { + "nicks": [ + "[{}.{}.{} - {}:{}:{}] {}".format( + date[2], date[1], date[0], hms[0], hms[1], hms[2].split('.')[0], nick + ) + ], + "unames": [ + "[{}.{}.{} - {}:{}:{}] {}".format( + date[2], date[1], date[0], hms[0], hms[1], hms[2].split('.')[0], username + ) + ], + "active": True, + "user_id": UID + } + + self.db.set(__name__, "users", users) + await utils.answer(message, self.strings["new_user"]) + + async def deltrackcmd(self, message: Message): + """ Remove user from the track list""" + args = utils.get_args_raw(message) + users = self.db.get(__name__, "users") + if not users: + return await utils.answer(message, self.strings["no_stat"]+"\nWho do you suppose to remove") + + try: + user = await self.client.get_entity(int(args) if args.isdigit() else args) + + except Exception: + return await utils.answer(message, self.strings["no_user"]) + + + for _user in users: + if users[_user]["user_id"] == user.id: + ID = int(_user) + del users[_user] + + for i in range(ID, len(users)+1): + if i == ID: + continue + + users[str(i-1)] = users.pop(str(i)) + + self.db.set(__name__, "users", users) + return await utils.answer(message, self.strings["removed"]) + + await utils.answer(message, self.strings["not_removed"]) + + async def trackstatcmd(self, message: Message): + """ View the statistic about users you're tracking""" + users = self.db.get(__name__, "users") + if not users: + return await utils.answer(message, self.strings["no_stat"]) + + ID = "1" + user = await self.client.get_entity(users[ID]["user_id"]) + status = "In progress" if users[ID]["active"] else "Inactive" + + text = ( + f"ID: {user.id}"+ + "\n\n Nicknames\n"+ + "\n".join(users[ID]["nicks"])+ + "\n\n Usernames\n"+ + "\n".join(users[ID]["unames"]) + ) + + await self.inline.form( + text=text, + message=message, + reply_markup=[ + [ + { + "text": f"Tracking status: {status}", + "callback": lambda call: self.showStat(call, int(ID), "change_status") + } + ], + [ + { + "text": "Previous user", + "callback": lambda call: self.showStat(call, int(ID), "previous") + }, + { + "text": "Next user", + "callback": lambda call: self.showStat(call, int(ID), "next") + } + ] + ] + ) + + + async def showStat(self, call: InlineCall, ID, action) -> None: + users = self.db.get(__name__, "users") + if not users: + return await call.answer(self.strings["no_stat"]) + + user = await self.client.get_entity(users[str(ID)]["user_id"]) + ID = ID + 1 if action == "next" else ID - 1 if action == "previous" else ID + + if ID == 0: + ID = len(users) + elif ID > len(users): + ID = 1 + + ID = str(ID) + if action == "change_status": + users[ID]["active"] = not(users[ID]["active"]) + await call.answer(self.strings["change_status"]) + + else: + if len(users) == 1: + return await call.answer(self.strings["only_one"]) + + status = "In progress" if users[ID]["active"] else "Inactive" + self.db.set(__name__, "users", users) + + text = ( + f"ID: {user.id}"+ + "\n\n Nicknames\n"+ + "\n".join(users[ID]["nicks"])+ + "\n\n Usernames\n"+ + "\n".join(users[ID]["unames"]) + ) + + await call.edit( + text=text, + reply_markup=[ + [ + { + "text": f"Tracking status: {status}", + "callback": lambda call: self.showStat(call, int(ID), "change_status") + } + ], + [ + { + "text": "Previous user", + "callback": lambda call: self.showStat(call, int(ID), "previous") + }, + { + "text": "Next user", + "callback": lambda call: self.showStat(call, int(ID), "next") + } + ] + ] + ) + + + async def watcher(self, message: Message): + diff = t.time() - self.db.get(__name__, "time") + if diff < self.config["Cooldown"]: + return + + users = self.db.get(__name__, "users") + if not users: + return + + for user in users: + if users[user]["active"] is False: + continue + + entity = await self.client.get_entity(users[user]["user_id"]) + nick = f"{entity.first_name} {entity.last_name}" if entity.last_name else entity.first_name + username = f"@{entity.username}" if entity.username else "Empty" + + time = datetime.datetime.now() + date = str(time.date()).split('-') + hms = str(time.time()).split(':') + + if nick != re.sub(r"\[.*\]", "", users[user]["nicks"][-1]).strip(): + users[user]["nicks"].append( + "[{}.{}.{} - {}:{}:{}] {}".format( + date[2], date[1], date[0], hms[0], hms[1], hms[2].split('.')[0], nick + ) + ) + + if username != re.sub(r"\[.*\]", "", users[user]["unames"][-1]).strip(): + users[user]["unames"].append( + "[{}.{}.{} - {}:{}:{}] {}".format( + date[2], date[1], date[0], hms[0], hms[1], hms[2].split(',')[0], username + ) + ) + + self.db.set(__name__, "users", users) + self.db.set(__name__, "time", t.time()) diff --git a/1jpshiro/hikka-modules/full.txt b/1jpshiro/hikka-modules/full.txt new file mode 100644 index 0000000..99fdb3e --- /dev/null +++ b/1jpshiro/hikka-modules/full.txt @@ -0,0 +1,8 @@ +Autotime +Counter +PMStat +ChannelImitator +StickerStealer +Timer +MessageEraser +Tracker diff --git a/AlpacaGang/ftg-modules/DND.py b/AlpacaGang/ftg-modules/DND.py new file mode 100644 index 0000000..31a472d --- /dev/null +++ b/AlpacaGang/ftg-modules/DND.py @@ -0,0 +1,454 @@ +# -*- coding: future_fstrings -*- + +# Friendly Telegram (telegram userbot) +# By Magical Unicorn (based on official Anti PM & AFK Friendly Telegram modules) +# Copyright (C) 2020 Magical Unicorn + +# 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 .. import loader, utils + +import logging +import datetime +import time + +from telethon import functions, types +logger = logging.getLogger(__name__) + + +def register(cb): + cb(DoNotDisturbMod()) + + +@loader.tds +class DoNotDisturbMod(loader.Module): + """ + DND (Do Not Disturb) : + -> Prevents people sending you unsolicited private messages. + -> Prevents disturbing when you are unavailable.\n + Commands : +   + """ + strings = {"name": "DND", + "afk": "I'm AFK right now (since {} ago).", + "afk_back": "I'm goin' BACK !", + "afk_gone": "I'm goin' AFK !", + "afk_no_group_off": "AFK status message enabled for group chats.", + "afk_no_group_on": "AFK status message disabled for group chats.", + "afk_no_pm_off": "AFK status message enabled for PMs.", + "afk_no_pm_on": "AFK status message disabled for PMs.", + "afk_notif_off": "Notifications are now disabled during AFK time.", + "afk_notif_on": "Notifications are now enabled during AFK time.", + "afk_rate_limit_off": "AFK status message rate limit disabled.", + "afk_rate_limit_on": ("AFK status message rate limit enabled." + "\n\nOne AFK status message max will be sent per chat."), + "afk_reason": ("I'm AFK right now (since {} ago)." + "\n\nReason : {}"), + "arg_on_off": "Argument must be 'off' or 'on' !", + "pm_off": ("Automatic answer for denied PMs disabled." + "\n\nUsers are now free to PM !"), + "pm_on": "An automatic answer is now sent for denied PMs.", + "pm_allowed": "I have allowed you to PM now.", + "pm_blocked": ("I don't want any PM from you, " + "so you have been blocked !"), + "pm_denied": "I have denied you to PM now.", + "pm_go_away": ("Hey there! Unfortunately, I don't accept private messages from strangers." + "\n\nPlease contact me in a group, or wait for me to approve you."), + "pm_reported": "You just got reported to spam !", + "pm_limit_arg": "Argument must be 'off', 'on' or a number between 10 and 1000 !", + "pm_limit_off": "Not allowed users are now free to PM without be automatically blocked.", + "pm_limit_on": "Not allowed users are now blocked after {} PMs.", + "pm_limit_current": "Current limit is {}.", + "pm_limit_current_no": "Automatic user blocking is currently disabled.", + "pm_limit_reset": "Limit reseted to {}.", + "pm_limit_set": "Limit set to {}.", + "pm_notif_off": "Notifications from denied PMs are now disabled.", + "pm_notif_on": "Notifications from denied PMs are now enabled.", + "pm_triggered": ("Hey! I don't appreciate you barging into my PM like this !" + "\nDid you even ask me for approving you to PM ? No ?" + "\nGoodbye then." + "\n\nPS: You've been reported as spam."), + "pm_unblocked": ("Alright fine! I'll forgive them this time. PM has been unblocked for " + "this user."), + "unknow": ("An unknow problem as occured." + "\n\nPlease report problem with logs on " + "Github."), + "who_to_allow": "Who shall I allow to PM ?", + "who_to_block": "Specify who to block.", + "who_to_deny": "Who shall I deny to PM ?", + "who_to_report": "Who shall I report ?", + "who_to_unblock": "Specify who to unblock."} + + def __init__(self): + self._me = None + self.default_pm_limit = 50 + + def config_complete(self): + self.name = self.strings["name"] + + async def client_ready(self, client, db): + self._db = db + self._client = client + self._me = await client.get_me(True) + + async def afkbackcmd(self, message): + """Remove the AFK status.\n """ + self._db.set(__name__, "afk", False) + self._db.set(__name__, "afk_gone", None) + self._db.set(__name__, "afk_rate", []) + await utils.answer(message, self.strings["afk_back"]) + + async def afkgocmd(self, message): + """ + .afkgo : Enable AFK status. + .afkgo [message] : Enable AFK status and add a reason. +   + """ + if utils.get_args_raw(message): + self._db.set(__name__, "afk", utils.get_args_raw(message)) + else: + self._db.set(__name__, "afk", True) + self._db.set(__name__, "afk_gone", time.time()) + self._db.set(__name__, "afk_rate", []) + await utils.answer(message, self.strings["afk_gone"]) + + async def afknogroupcmd(self, message): + """ + .afknogroup : Disable/Enable AFK status message for group chats. + .afknogroup off : Enable AFK status message for group chats. + .afknogroup on : Disable AFK status message for group chats. +   + """ + if utils.get_args_raw(message): + afknogroup_arg = utils.get_args_raw(message) + if afknogroup_arg == "off": + self._db.set(__name__, "afk_no_group", False) + await utils.answer(message, self.strings["afk_no_group_off"]) + elif afknogroup_arg == "on": + self._db.set(__name__, "afk_no_group", True) + await utils.answer(message, self.strings["afk_no_group_on"]) + else: + await utils.answer(message, self.strings["arg_on_off"]) + else: + afknogroup_current = self._db.get(__name__, "afk_no_group") + if afknogroup_current is None or afknogroup_current is False: + self._db.set(__name__, "afk_no_group", True) + await utils.answer(message, self.strings["afk_no_group_on"]) + elif afknogroup_current is True: + self._db.set(__name__, "afk_no_group", False) + await utils.answer(message, self.strings["afk_no_group_off"]) + else: + await utils.answer(message, self.strings["unknow"]) + + async def afknopmcmd(self, message): + """ + .afknopm : Disable/Enable AFK status message for PMs. + .afknopm off : Enable AFK status message for PMs. + .afknopm on : Disable AFK status message for PMs. +   + """ + if utils.get_args_raw(message): + afknopm_arg = utils.get_args_raw(message) + if afknopm_arg == "off": + self._db.set(__name__, "afk_no_pm", False) + await utils.answer(message, self.strings["afk_no_pm_off"]) + elif afknopm_arg == "on": + self._db.set(__name__, "afk_no_pm", True) + await utils.answer(message, self.strings["afk_no_pm_on"]) + else: + await utils.answer(message, self.strings["arg_on_off"]) + else: + afknopm_current = self._db.get(__name__, "afk_no_pm") + if afknopm_current is None or afknopm_current is False: + self._db.set(__name__, "afk_no_pm", True) + await utils.answer(message, self.strings["afk_no_pm_on"]) + elif afknopm_current is True: + self._db.set(__name__, "afk_no_pm", False) + await utils.answer(message, self.strings["afk_no_pm_off"]) + else: + await utils.answer(message, self.strings["unknow"]) + + async def afknotifcmd(self, message): + """ + .afknotif : Disable/Enable the notifications during AFK time. + .afknotif off : Disable the notifications during AFK time. + .afknotif on : Enable the notifications during AFK time. +   + """ + if utils.get_args_raw(message): + afknotif_arg = utils.get_args_raw(message) + if afknotif_arg == "off": + self._db.set(__name__, "afk_notif", False) + await utils.answer(message, self.strings["afk_notif_off"]) + elif afknotif_arg == "on": + self._db.set(__name__, "afk_notif", True) + await utils.answer(message, self.strings["afk_notif_on"]) + else: + await utils.answer(message, self.strings["arg_on_off"]) + else: + afknotif_current = self._db.get(__name__, "afk_notif") + if afknotif_current is None or afknotif_current is False: + self._db.set(__name__, "afk_notif", True) + await utils.answer(message, self.strings["afk_notif_on"]) + elif afknotif_current is True: + self._db.set(__name__, "afk_notif", False) + await utils.answer(message, self.strings["afk_notif_off"]) + else: + await utils.answer(message, self.strings["unknow"]) + + async def afkratecmd(self, message): + """ + .afkrate : Disable/Enable AFK rate limit. + .afkrate off : Disable AFK rate limit. + .afkrate on : Enable AFK rate limit. One AFK status message max will be sent per chat. +   + """ + if utils.get_args_raw(message): + afkrate_arg = utils.get_args_raw(message) + if afkrate_arg == "off": + self._db.set(__name__, "afk_rate_limit", False) + await utils.answer(message, self.strings["afk_rate_limit_off"]) + elif afkrate_arg == "on": + self._db.set(__name__, "afk_rate_limit", True) + await utils.answer(message, self.strings["afk_rate_limit_on"]) + else: + await utils.answer(message, self.strings["arg_on_off"]) + else: + afkrate_current = self._db.get(__name__, "afk_rate_limit") + if afkrate_current is None or afkrate_current is False: + self._db.set(__name__, "afk_rate_limit", True) + await utils.answer(message, self.strings["afk_rate_limit_on"]) + elif afkrate_current is True: + self._db.set(__name__, "afk_rate_limit", False) + await utils.answer(message, self.strings["afk_rate_limit_off"]) + else: + await utils.answer(message, self.strings["unknow"]) + + async def allowcmd(self, message): + """Allow this user to PM.\n """ + user = await utils.get_target(message) + if not user: + await utils.answer(message, self.strings["who_to_allow"]) + return + self._db.set(__name__, "allow", list(set(self._db.get(__name__, "allow", [])).union({user}))) + await utils.answer(message, self.strings["pm_allowed"].format(user)) + + async def blockcmd(self, message): + """Block this user to PM without being warned.\n """ + user = await utils.get_target(message) + if not user: + await utils.answer(message, self.strings["who_to_block"]) + return + await message.client(functions.contacts.BlockRequest(user)) + await utils.answer(message, self.strings["pm_blocked"].format(user)) + + async def denycmd(self, message): + """Deny this user to PM without being warned.\n """ + user = await utils.get_target(message) + if not user: + await utils.answer(message, self.strings["who_to_deny"]) + return + self._db.set(__name__, "allow", list(set(self._db.get(__name__, "allow", [])).difference({user}))) + await utils.answer(message, self.strings["pm_denied"].format(user)) + + async def pmcmd(self, message): + """ + .pm : Disable/Enable automatic answer for denied PMs. + .pm off : Disable automatic answer for denied PMs. + .pm on : Enable automatic answer for denied PMs. +   + """ + if utils.get_args_raw(message): + pm_arg = utils.get_args_raw(message) + if pm_arg == "off": + self._db.set(__name__, "pm", True) + await utils.answer(message, self.strings["pm_off"]) + elif pm_arg == "on": + self._db.set(__name__, "pm", False) + await utils.answer(message, self.strings["pm_on"]) + else: + await utils.answer(message, self.strings["arg_on_off"]) + else: + pm_current = self._db.get(__name__, "pm") + if pm_current is None or pm_current is False: + self._db.set(__name__, "pm", True) + await utils.answer(message, self.strings["pm_off"]) + elif pm_current is True: + self._db.set(__name__, "pm", False) + await utils.answer(message, self.strings["pm_on"]) + else: + await utils.answer(message, self.strings["unknow"]) + + async def pmlimitcmd(self, message): + """ + .pmlimit : Get current max number of PMs before automatically block not allowed user. + .pmlimit off : Disable automatic user blocking. + .pmlimit on : Enable automatic user blocking. + .pmlimit reset : Reset max number of PMs before automatically block not allowed user. + .pmlimit [number] : Modify max number of PMs before automatically block not allowed user. +   + """ + if utils.get_args_raw(message): + pmlimit_arg = utils.get_args_raw(message) + if pmlimit_arg == "off": + self._db.set(__name__, "pm_limit", False) + await utils.answer(message, self.strings["pm_limit_off"]) + return + elif pmlimit_arg == "on": + self._db.set(__name__, "pm_limit", True) + pmlimit_on = self.strings["pm_limit_on"].format(self.get_current_limit()) + await utils.answer(message, pmlimit_on) + return + elif pmlimit_arg == "reset": + self._db.set(__name__, "pm_limit_max", self.default_pm_limit) + pmlimit_reset = self.strings["pm_limit_reset"].format(self.get_current_pm_limit()) + await utils.answer(message, pmlimit_reset) + return + else: + try: + pmlimit_number = int(pmlimit_arg) + if pmlimit_number >= 10 and pmlimit_number <= 1000: + self._db.set(__name__, "pm_limit_max", pmlimit_number) + pmlimit_new = self.strings["pm_limit_set"].format(self.get_current_pm_limit()) + await utils.answer(message, pmlimit_new) + return + else: + await utils.answer(message, self.strings["pm_limit_arg"]) + return + except ValueError: + await utils.answer(message, self.strings["pm_limit_arg"]) + return + await utils.answer(message, self.strings["limit_arg"]) + else: + pmlimit = self._db.get(__name__, "pm_limit") + if pmlimit is None or pmlimit is False: + pmlimit_current = self.strings["pm_limit_current_no"] + elif pmlimit is True: + pmlimit_current = self.strings["pm_limit_current"].format(self.get_current_pm_limit()) + else: + await utils.answer(message, self.strings["unknow"]) + return + await utils.answer(message, pmlimit_current) + + async def pmnotifcmd(self, message): + """ + .pmnotif : Disable/Enable the notifications from denied PMs. + .pmnotif off : Disable the notifications from denied PMs. + .pmnotif on : Enable the notifications from denied PMs. +   + """ + if utils.get_args_raw(message): + pmnotif_arg = utils.get_args_raw(message) + if pmnotif_arg == "off": + self._db.set(__name__, "pm_notif", False) + await utils.answer(message, self.strings["pm_notif_off"]) + elif pmnotif_arg == "on": + self._db.set(__name__, "pm_notif", True) + await utils.answer(message, self.strings["pm_notif_on"]) + else: + await utils.answer(message, self.strings["arg_on_off"]) + else: + pmnotif_current = self._db.get(__name__, "pm_notif") + if pmnotif_current is None or pmnotif_current is False: + self._db.set(__name__, "pm_notif", True) + await utils.answer(message, self.strings["pm_notif_on"]) + elif pmnotif_current is True: + self._db.set(__name__, "pm_notif", False) + await utils.answer(message, self.strings["pm_notif_off"]) + else: + await utils.answer(message, self.strings["unknow"]) + + async def reportcmd(self, message): + """Report the user to spam. Use only in PM.\n """ + user = await utils.get_target(message) + if not user: + await utils.answer(message, self.strings["who_to_report"]) + return + self._db.set(__name__, "allow", list(set(self._db.get(__name__, "allow", [])).difference({user}))) + if message.is_reply and isinstance(message.to_id, types.PeerChannel): + await message.client(functions.messages.ReportRequest(peer=message.chat_id, + id=[message.reply_to_msg_id], + reason=types.InputReportReasonSpam())) + else: + await message.client(functions.messages.ReportSpamRequest(peer=message.to_id)) + await utils.answer(message, self.strings["pm_reported"]) + + async def unblockcmd(self, message): + """Unblock this user to PM.""" + user = await utils.get_target(message) + if not user: + await utils.answer(message, self.strings["who_to_unblock"]) + return + await message.client(functions.contacts.UnblockRequest(user)) + await utils.answer(message, self.strings["pm_unblocked"].format(user)) + + async def watcher(self, message): + user = await utils.get_user(message) + pm = self._db.get(__name__, "pm") + if getattr(message.to_id, "user_id", None) == self._me.user_id and (pm is None or pm is False): + if not user.is_self and not user.bot and not user.verified and not self.get_allowed(message.from_id): + await utils.answer(message, self.strings["pm_go_away"]) + if self._db.get(__name__, "pm_limit") is True: + pms = self._db.get(__name__, "pms", {}) + pm_limit = self._db.get(__name__, "pm_limit_max") + pm_user = pms.get(message.from_id, 0) + if isinstance(pm_limit, int) and pm_limit >= 10 and pm_limit <= 1000 and pm_user >= pm_limit: + await utils.answer(message, self.strings["pm_triggered"]) + await message.client(functions.contacts.BlockRequest(message.from_id)) + await message.client(functions.messages.ReportSpamRequest(peer=message.from_id)) + del pms[message.from_id] + self._db.set(__name__, "pms", pms) + else: + self._db.set(__name__, "pms", {**pms, message.from_id: pms.get(message.from_id, 0) + 1}) + pm_notif = self._db.get(__name__, "pm_notif") + if pm_notif is None or pm_notif is False: + await message.client.send_read_acknowledge(message.chat_id) + return + if message.mentioned or getattr(message.to_id, "user_id", None) == self._me.user_id: + afk_status = self._db.get(__name__, "afk") + if user.is_self or user.bot or user.verified or afk_status is False: + return + if message.mentioned and self._db.get(__name__, "afk_no_group") is True: + return + afk_no_pm = self._db.get(__name__, "afk_no_pm") + if getattr(message.to_id, "user_id", None) == self._me.user_id and afk_no_pm is True: + return + if self._db.get(__name__, "afk_rate_limit") is True: + afk_rate = self._db.get(__name__, "afk_rate", []) + if utils.get_chat_id(message) in afk_rate: + return + else: + self._db.setdefault(__name__, {}).setdefault("afk_rate", []).append(utils.get_chat_id(message)) + self._db.save() + now = datetime.datetime.now().replace(microsecond=0) + gone = datetime.datetime.fromtimestamp(self._db.get(__name__, "afk_gone")).replace(microsecond=0) + diff = now - gone + if afk_status is True: + afk_message = self.strings["afk"].format(diff) + elif afk_status is not False: + afk_message = self.strings["afk_reason"].format(diff, afk_status) + await utils.answer(message, afk_message) + _notif = self._db.get(__name__, "_notif") + if _notif is None or _notif is False: + await message.client.send_read_acknowledge(message.chat_id) + + def get_allowed(self, id): + return id in self._db.get(__name__, "allow", []) + + def get_current_pm_limit(self): + pm_limit = self._db.get(__name__, "pm_limit_max") + if not isinstance(pm_limit, int) or pm_limit < 10 or pm_limit > 1000: + pm_limit = self.default_pm_limit + self._db.set(__name__, "pm_limit_max", pm_limit) + return pm_limit diff --git a/AlpacaGang/ftg-modules/Tag.py b/AlpacaGang/ftg-modules/Tag.py new file mode 100644 index 0000000..7d6fe60 --- /dev/null +++ b/AlpacaGang/ftg-modules/Tag.py @@ -0,0 +1,125 @@ +# -*- coding: future_fstrings -*- + +# Friendly Telegram (telegram userbot) +# By Magical Unicorn (based on official Anti PM & AFK Friendly Telegram modules) +# Copyright (C) 2020 Magical Unicorn + +# 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 .. import loader, utils + +import logging + +from telethon import functions, types +from telethon.tl.types import PeerUser, PeerChat, PeerChannel, ChannelParticipantsAdmins +logger = logging.getLogger(__name__) + + +def register(cb): + cb(TagMod()) + + +@loader.tds +class TagMod(loader.Module): + """ + Tag : + -> Tag all admins (fast way to report). + -> Tag all bots (why not ?). + -> Tag all members (why not ?).\n + Commands : +   + """ + strings = {"name": "Tag", + "error_chat": "This command can be used in channels and group chats only.", + "unknow": ("An unknow problem as occured." + "\n\nPlease report problem with logs on " + "Github."), + "user_link": "\n• {}"} + + def config_complete(self): + self.name = self.strings["name"] + + async def admincmd(self, message): + """ + .admin : Tag all admins (excepted bots). + .admin [message] : Tag all admins (excepted bots) with message before tags. +   + """ + if isinstance(message.to_id, PeerUser): + await utils.answer(message, self.strings["error_chat"]) + return + if utils.get_args_raw(message): + rep = utils.get_args_raw(message) + else: + rep = "" + user = await utils.get_target(message) + if isinstance(message.to_id, PeerChat) or isinstance(message.to_id, PeerChannel): + async for user in message.client.iter_participants(message.to_id, filter=ChannelParticipantsAdmins): + if not user.bot: + user_name = user.first_name + if user.last_name is not None: + user_name += " " + user.last_name + rep += self.strings["user_link"].format(user.id, user_name) + await utils.answer(message, rep) + else: + await utils.answer(message, self.strings["unknow"]) + + async def allcmd(self, message): + """ + .all : Tag all members. + .all [message] : Tag all members with message before tags. +   + """ + if isinstance(message.to_id, PeerUser): + await utils.answer(message, self.strings["error_chat"]) + return + if utils.get_args_raw(message): + rep = utils.get_args_raw(message) + else: + rep = "" + user = await utils.get_target(message) + if isinstance(message.to_id, PeerChat) or isinstance(message.to_id, PeerChannel): + async for user in message.client.iter_participants(message.to_id): + user_name = user.first_name + if user.last_name is not None: + user_name += " " + user.last_name + rep += self.strings["user_link"].format(user.id, user_name) + await utils.answer(message, rep) + else: + await utils.answer(message, self.strings["unknow"]) + + async def botcmd(self, message): + """ + .bot : Tag all bots. + .bot [message] : Tag all bots with message before tags. +   + """ + if isinstance(message.to_id, PeerUser): + await utils.answer(message, self.strings["error_chat"]) + return + if utils.get_args_raw(message): + rep = utils.get_args_raw(message) + else: + rep = "" + user = await utils.get_target(message) + if isinstance(message.to_id, PeerChat) or isinstance(message.to_id, PeerChannel): + async for user in message.client.iter_participants(message.to_id): + if user.bot: + user_name = user.first_name + if user.last_name is not None: + user_name += " " + user.last_name + rep += self.strings["user_link"].format(user.id, user_name) + await utils.answer(message, rep) + else: + await utils.answer(message, self.strings["unknow"]) diff --git a/AlpacaGang/ftg-modules/contact.py b/AlpacaGang/ftg-modules/contact.py new file mode 100644 index 0000000..ef95c9c --- /dev/null +++ b/AlpacaGang/ftg-modules/contact.py @@ -0,0 +1,42 @@ +from .. import loader, utils + +import logging +import datetime +import time +import asyncio + +logger = logging.getLogger(__name__) + + +def register(cb): + cb(CONTACTMod()) + + +@loader.tds +class CONTACTMod(loader.Module): + """Это модуль для игры в \"контакт\"""" + strings = {"name": "contact"} + + def __init__(self): + self.name = self.strings["name"] + + def config_complete(self): + pass + + async def contactcmd(self, message): + """Эта команда пишет 10 сообщений для контакта""" + try: + await message.delete() + x = 10 + lst = str(x) + await message.respond(lst) + + dd = time.time() + + while time.time() - dd < x: + now = str(x - round(time.time() - dd)) + if now != lst: + await message.respond(now) + lst = now + except: + await message.respond("Упс, ошибочка вышла! Напшите @gerasikoff, он вам поможет") diff --git a/AlpacaGang/ftg-modules/cuttly.py b/AlpacaGang/ftg-modules/cuttly.py new file mode 100644 index 0000000..294ec81 --- /dev/null +++ b/AlpacaGang/ftg-modules/cuttly.py @@ -0,0 +1,95 @@ +import requests as rq +from urllib.parse import quote_plus as escape +import re + +from .. import loader, utils +import asyncio +import logging +logger = logging.getLogger(__name__) + +# simplified cuttly api +class CuttlyApi: + def __init__(self, token, api_url='https://cutt.ly/api/api.php'): + self.token = token + self.api_url = api_url + self.error_codes = { + 1: 'Link is already shortened', + 2: 'Link to short is not a link', + 3: 'Short link https://cutt.ly/{name} is taken', + 4: 'Invalid API key', + 5: 'Link preferred alias contains invalid characters', + 6: 'Link is from blocked domain' + } + self.ok_code = 7 + + def shorten(self, short: str, name: str=None) -> dict: + if not re.fullmatch(r'\w+://.+', short): # add scheme if needed + short = 'http://' + short # assume that it supports http + res = rq.get(self.api_url, params={ + "key": self.token, + "short": escape(short, ':/%._-'), + "name": name + }) + res = res.json()['url'] + return res + +@loader.tds +class CuttlyMod(loader.Module): + """URL shortener module""" + # make errors translatable + strings = { + "name": "Cutt.ly", + "error_1": "Link is already shortened", + "error_2": "It is not a link", + "error_3": "Short link https://cutt.ly/{name} is taken", + "error_4": "Invalid API key. Change it in config.", + "error_5": "Link preferred alias contains invalid characters", + "error_6": "Link is from blocked domain", + "unknown_error": "Unknown error {}. Check https://cutt.ly/cuttly-api for information.", + "ok": "Shorted!\nShort link: {short}\nFull link: {full}", + "ok_nofull": "Shorted!\nShort link: {short}", + "no_args": "At least 1 argument needed - the link you gonna to short", + "many_args": "At most 2 arguments - the link you gonna to short and preferred alias for it." + } + def __init__(self): + self.config = loader.ModuleConfig( + # name - default - description + "cuttly_api_url", "https://cutt.ly/api/api.php", "Cuttly API URL, took from https://cutt.ly/cuttly-api", + "api_key", None, "API key for cutt.ly. Register there and take one.", + "include_full_link", True, "Shall bot include full link into answer." + ) + + def config_complete(self): + self.name = self.strings['name'] + self.cl = CuttlyApi(self.config['api_key'], self.config['cuttly_api_url']) + + async def shortcmd(self, message): + '''usage: .short [preferred_alias]''' + args = utils.get_args(message) + if len(args) < 1: + await utils.answer(message, self.strings['no_args']) + return + elif len(args) > 2: + await utils.answer(message, self.strings['many_args']) + return + + if len(args) == 1: + args.append(None) + + res = self.cl.shorten(*args) + logger.debug(f'Got response from cutt.ly: {res}') + if res['status'] != self.cl.ok_code: + try: + msg = self.strings[f'error_{res["status"]}'] + except KeyError: # Unknown error, not in strings yet + msg = self.strings['unknown_error'].format(res['status']) + else: + if self.config['include_full_link']: + msg = self.strings['ok'] + else: + msg = self.strings['ok_nofull'] + await utils.answer(message, msg.format( + short = res.get('shortLink', None), # If we got an error + full = res.get('fullLink', None), + name = args[1] + )) diff --git a/AlpacaGang/ftg-modules/dogbin.py b/AlpacaGang/ftg-modules/dogbin.py new file mode 100644 index 0000000..5a04958 --- /dev/null +++ b/AlpacaGang/ftg-modules/dogbin.py @@ -0,0 +1,131 @@ +# Copyright (C) 2019 The Raphielscape Company LLC. +# +# Licensed under the Raphielscape Public License, Version 1.c (the "License"); +# you may not use this file except in compliance with the License. +# +""" Userbot module containing commands for interacting with dogbin(https://del.dog)""" + +from requests import get, post, exceptions +import asyncio +import os +from userbot import BOTLOG, BOTLOG_CHATID, CMD_HELP, LOGS, TEMP_DOWNLOAD_DIRECTORY +from userbot.events import register + +DOGBIN_URL = "https://dogbin.f0x1d.com/" + + +@register(outgoing=True, pattern=r"^.paste(?: |$)([\s\S]*)") +async def paste(pstl): + """ For .paste command, pastes the text directly to dogbin. """ + dogbin_final_url = "" + match = pstl.pattern_match.group(1).strip() + reply_id = pstl.reply_to_msg_id + + if not match and not reply_id: + await pstl.edit("`Elon Musk said I cannot paste void.`") + return + + if match: + message = match + elif reply_id: + message = (await pstl.get_reply_message()) + if message.media: + downloaded_file_name = await pstl.client.download_media( + message, + TEMP_DOWNLOAD_DIRECTORY, + ) + m_list = None + with open(downloaded_file_name, "rb") as fd: + m_list = fd.readlines() + message = "" + for m in m_list: + message += m.decode("UTF-8") + "\r" + os.remove(downloaded_file_name) + else: + message = message.message + + # Dogbin + await pstl.edit("`Pasting text . . .`") + resp = post(DOGBIN_URL + "documents", data=message.encode('utf-8')) + + if resp.status_code == 200: + response = resp.json() + key = response['key'] + dogbin_final_url = DOGBIN_URL + key + + if response['isUrl']: + reply_text = ("`Pasted successfully!`\n\n" + f"`Shortened URL:` {dogbin_final_url}\n\n" + "`Original(non-shortened) URLs`\n" + f"`Dogbin URL`: {DOGBIN_URL}v/{key}\n") + else: + reply_text = ("`Pasted successfully!`\n\n" + f"`Dogbin URL`: {dogbin_final_url}") + else: + reply_text = ("`Failed to reach Dogbin`") + + await pstl.edit(reply_text) + if BOTLOG: + await pstl.client.send_message( + BOTLOG_CHATID, + f"Paste query was executed successfully", + ) + + +@register(outgoing=True, pattern="^.getpaste(?: |$)(.*)") +async def get_dogbin_content(dog_url): + """ For .getpaste command, fetches the content of a dogbin URL. """ + textx = await dog_url.get_reply_message() + message = dog_url.pattern_match.group(1) + await dog_url.edit("`Getting dogbin content...`") + + if textx: + message = str(textx.message) + + format_normal = f'{DOGBIN_URL}' + format_view = f'{DOGBIN_URL}v/' + + if message.startswith(format_view): + message = message[len(format_view):] + elif message.startswith(format_normal): + message = message[len(format_normal):] + elif message.startswith("del.dog/"): + message = message[len("del.dog/"):] + else: + await dog_url.edit("`Is that even a dogbin url?`") + return + + resp = get(f'{DOGBIN_URL}raw/{message}') + + try: + resp.raise_for_status() + except exceptions.HTTPError as HTTPErr: + await dog_url.edit( + "Request returned an unsuccessful status code.\n\n" + str(HTTPErr)) + return + except exceptions.Timeout as TimeoutErr: + await dog_url.edit("Request timed out." + str(TimeoutErr)) + return + except exceptions.TooManyRedirects as RedirectsErr: + await dog_url.edit( + "Request exceeded the configured number of maximum redirections." + + str(RedirectsErr)) + return + + reply_text = "`Fetched dogbin URL content successfully!`\n\n`Content:` " + resp.text + + await dog_url.edit(reply_text) + if BOTLOG: + await dog_url.client.send_message( + BOTLOG_CHATID, + "Get dogbin content query was executed successfully", + ) + + +CMD_HELP.update({ + "dogbin": + ".paste \ +\nUsage: Create a paste or a shortened url using dogbin (https://dogbin.f0x1d.com/)\ +\n\n.getpaste\ +\nUsage: Gets the content of a paste or shortened url from dogbin (https://dogbin.f0x1d.com/)" +}) diff --git a/AlpacaGang/ftg-modules/inactive.py b/AlpacaGang/ftg-modules/inactive.py new file mode 100644 index 0000000..5e9873e --- /dev/null +++ b/AlpacaGang/ftg-modules/inactive.py @@ -0,0 +1,143 @@ +import logging +import json +# import telethon +from .. import loader, utils, security + +logger = logging.getLogger(__name__) + + +@loader.tds +class InactiveDetectorMod(loader.Module): + """Detects inactive users""" + strings = { + "name": "Inactivity detector", + "top_header": "These {un} users wrote {mn} messages or less since joining the group:\n\n", + "top_place": "[{name}](tg://user?id={uid}) ({nmsg})", # FIXME: mentions + "top_delimiter": ", ", # TODO: move to config + "not_int": "Most messages must be integer", + "recount_priv": "I can't recount stats in private messages!", + "recount_started": "Processing recount for chat {}. It may take a lot.", + "recount_db_dumped": "Dumped database to owners and/or saved messages", + "recount_dump": "Database dump for chat {cid}:\n\n
{dmp}
", + "recount_iter_done": "Iterated over {} messages in this chat", + "recount_finish": "Recount successful!" + } + + def __init__(self): + self.config = loader.ModuleConfig( + "default_chat_id", -1001457369532, "Chat ID to get top if command used in PM", + "top_delimiter", ', ', "Separates inactivity top members", + "dump_db_before_recount", False, "Dump database of chat before recounting. " + "Dump will be sent to saved or bot owners" + ) + self.name = self.strings['name'] + + async def client_ready(self, client, db): + self.client = client + self.db = db + self.me = await self.client.get_me() + + async def inactivecmd(self, message): + """.inactive """ + if message.is_private: + chat_id = self.config['default_chat_id'] + else: + chat_id = message.chat_id + args = utils.get_args(message) + if args: + if args[0].isdigit(): + most = int(args[0]) + else: + await utils.answer(message, self.strings("not_int", message)) + return + else: + most = 0 + users_db = self.db.get(__name__, str(chat_id), {}) + users = {} + + async for user in self.client.iter_participants(chat_id): + if not user.bot: + if str(user.id) not in users_db: + users_db[str(user.id)] = self.get_empty_user(user) + users[str(user.id)] = users_db[str(user.id)] + # We won't include users not CURRENTLY in chat, + # but their stats will remain in the database + + self.db.set(__name__, str(chat_id), users_db) + + def key(x): + return x[1]['cnt'] + + users = sorted(users.items(), key=key) + text = [] + for uid, u in users: + if u['cnt'] <= most: + text.append(self.strings('top_place', message).format( + name=u['name'], uid=uid, nmsg=u['cnt'] + )) + else: + break + msg = self.strings('top_header', message).format(un=len(text), mn=most)\ + + self.config['top_delimiter'].join(text) + + kw = {} + if self.me.id != message.from_id: + kw['silent'] = True + await utils.answer(message, msg, parse_mode="md", **kw) + + async def recountcmd(self, message): + if message.is_private: + await utils.answer(message, self.strings('recount_priv', message)) + return + + chat_id = message.chat_id + await utils.answer(message, self.strings('recount_started', message).format(chat_id)) + db = self.db.get(__name__, str(chat_id), {}) + json_db = json.dumps(db) + msg = self.strings("recount_dump", message).format(cid=chat_id, dmp=json_db) + logging.debug('Database dump (chat %d): %s', chat_id, json_db) + owners = self.db.get(security.__name__, "owner", ["me"]) + if owners: + for owner in owners: + try: + await self.client.send_message(owner, msg) + except Exception: + logger.warning("Dump of chat %d sending to %d failed", + chat_id, owner, exc_info=True) + new_db = {} + n = 0 + async for msg in self.client.iter_messages(chat_id, limit=None): + if not msg.sender.bot: + n += 1 + from_id = msg.from_id + # Ensure such user exists, or create him + new_db[str(from_id)] = new_db.get(str(from_id), + self.get_empty_user(msg.sender)) + new_db[str(from_id)]['cnt'] += 1 + await utils.answer(message, self.strings("recount_iter_done", message).format(n)) + self.db.set(__name__, str(chat_id), new_db) + await utils.answer(message, self.strings("recount_finish", message)) + + async def watcher(self, message): + if message.is_private: + return + else: + chat_id = str(message.chat_id) + users = self.db.get(__name__, chat_id, {}) + from_id = str(message.from_id) + # this creates user if not exists + if message.sender: + users[from_id] = users.get(from_id, self.get_empty_user(message.sender)) + users[from_id]["cnt"] += 1 + self.db.set(__name__, chat_id, users) + + def get_full_name(self, user): + fn, ln = '', '' + if user.first_name: + fn = user.first_name + if user.last_name: # Can be None, then we get an exception + ln = user.last_name + return (fn + ' ' + ln).strip() + + def get_empty_user(self, user): + return {"cnt": 0, "name": self.get_full_name(user)} diff --git a/AlpacaGang/ftg-modules/purge.py b/AlpacaGang/ftg-modules/purge.py new file mode 100644 index 0000000..6e2e918 --- /dev/null +++ b/AlpacaGang/ftg-modules/purge.py @@ -0,0 +1,154 @@ +# Copyright (C) 2019 The Raphielscape Company LLC. +# +# Licensed under the Raphielscape Public License, Version 1.c (the "License"); +# you may not use this file except in compliance with the License. +# +""" Userbot module for purging unneeded messages(usually spam or ot). """ + +from asyncio import sleep + +from telethon.errors import rpcbaseerrors + +from userbot import BOTLOG, BOTLOG_CHATID, CMD_HELP +from userbot.events import register + + +@register(outgoing=True, pattern="^.purge$") +async def fastpurger(purg): + """ For .purge command, purge all messages starting from the reply. """ + chat = await purg.get_input_chat() + msgs = [] + itermsg = purg.client.iter_messages(chat, min_id=purg.reply_to_msg_id) + count = 0 + + if purg.reply_to_msg_id is not None: + async for msg in itermsg: + msgs.append(msg) + count = count + 1 + msgs.append(purg.reply_to_msg_id) + if len(msgs) == 100: + await purg.client.delete_messages(chat, msgs) + msgs = [] + else: + await purg.edit("`I need a mesasge to start purging from.`") + return + + if msgs: + await purg.client.delete_messages(chat, msgs) + done = await purg.client.send_message( + purg.chat_id, f"`Fast purge complete!`\ + \nPurged {str(count)} messages") + + if BOTLOG: + await purg.client.send_message( + BOTLOG_CHATID, + "Purge of " + str(count) + " messages done successfully.") + await sleep(2) + await done.delete() + + +@register(outgoing=True, pattern="^.purgeme") +async def purgeme(delme): + """ For .purgeme, delete x count of your latest message.""" + message = delme.text + count = int(message[9:]) + i = 1 + + async for message in delme.client.iter_messages(delme.chat_id, + from_user='me'): + if i > count + 1: + break + i = i + 1 + await message.delete() + + smsg = await delme.client.send_message( + delme.chat_id, + "`Purge complete!` Purged " + str(count) + " messages.", + ) + if BOTLOG: + await delme.client.send_message( + BOTLOG_CHATID, + "Purge of " + str(count) + " messages done successfully.") + await sleep(2) + i = 1 + await smsg.delete() + + +@register(outgoing=True, pattern="^.del$") +async def delete_it(delme): + """ For .del command, delete the replied message. """ + msg_src = await delme.get_reply_message() + if delme.reply_to_msg_id: + try: + await msg_src.delete() + await delme.delete() + if BOTLOG: + await delme.client.send_message( + BOTLOG_CHATID, "Deletion of message was successful") + except rpcbaseerrors.BadRequestError: + if BOTLOG: + await delme.client.send_message( + BOTLOG_CHATID, "Well, I can't delete a message") + + +@register(outgoing=True, pattern="^.edit") +async def editer(edit): + """ For .editme command, edit your last message. """ + message = edit.text + chat = await edit.get_input_chat() + self_id = await edit.client.get_peer_id('me') + string = str(message[6:]) + i = 1 + async for message in edit.client.iter_messages(chat, self_id): + if i == 2: + await message.edit(string) + await edit.delete() + break + i = i + 1 + if BOTLOG: + await edit.client.send_message(BOTLOG_CHATID, + "Edit query was executed successfully") + + +@register(outgoing=True, pattern="^.sd") +async def selfdestruct(destroy): + """ For .sd command, make seflf-destructable messages. """ + message = destroy.text + counter = int(message[4:6]) + text = str(destroy.text[6:]) + await destroy.delete() + smsg = await destroy.client.send_message(destroy.chat_id, text) + await sleep(counter) + await smsg.delete() + if BOTLOG: + await destroy.client.send_message(BOTLOG_CHATID, + "sd query done successfully") + + +CMD_HELP.update({ + 'purge': + '.purge\ + \nUsage: Purges all messages starting from the reply.' +}) + +CMD_HELP.update({ + 'purgeme': + '.purgeme \ + \nUsage: Deletes x amount of your latest messages.' +}) + +CMD_HELP.update({"del": ".del\ +\nUsage: Deletes the message you replied to."}) + +CMD_HELP.update({ + 'edit': + ".edit \ +\nUsage: Replace your last message with ." +}) + +CMD_HELP.update({ + 'sd': + '.sd \ +\nUsage: Creates a message that selfdestructs in x seconds.\ +\nKeep the seconds under 100 since it puts your bot to sleep.' +}) diff --git a/AlpacaGang/ftg-modules/quotes.py b/AlpacaGang/ftg-modules/quotes.py new file mode 100644 index 0000000..c2fb986 --- /dev/null +++ b/AlpacaGang/ftg-modules/quotes.py @@ -0,0 +1,298 @@ +# -*- coding: future_fstrings -*- + +# Friendly Telegram (telegram userbot) +# Copyright (C) 2018-2019 The Authors + +# 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 logging +import requests +import base64 +import json +import telethon + +from .. import loader, utils +from PIL import Image +from io import BytesIO + +logger = logging.getLogger(__name__) + + +def register(cb): + cb(QuotesMod()) + + +@loader.tds +class QuotesMod(loader.Module): + """Quote a message.""" + strings = { + "name": "Quotes", + "api_token_cfg_doc": "API Key/Token for Quotes.", + "api_url_cfg_doc": "API URL for Quotes.", + "colors_cfg_doc": "Username colors", + "default_username_color_cfg_doc": "Default color for the username.", + "no_reply": "You didn't reply to a message.", + "no_template": "You didn't specify the template.", + "delimiter": ", ", + "server_error": "Server error. Please report to developer.", + "invalid_token": "You've set an invalid token.", + "unauthorized": "You're unauthorized to do this.", + "not_enough_permissions": "Wrong template. You can use only the default one.", + "templates": "Available Templates: {}", + "cannot_send_stickers": "You cannot send stickers in this chat.", + "admin": "admin", + "creator": "creator", + "hidden": "hidden", + "channel": "Channel", + "filename": "file.png" + } + + def __init__(self): + self.config = loader.ModuleConfig("api_token", None, lambda: self.strings["api_token_cfg_doc"], + "api_url", "http://api.antiddos.systems", + lambda: self.strings["api_url_cfg_doc"], + "username_colors", ["#fb6169", "#faa357", "#b48bf2", "#85de85", + "#62d4e3", "#65bdf3", "#ff5694"], + lambda: self.strings["colors_cfg_doc"], + "default_username_color", "#b48bf2", + lambda: self.strings["default_username_color_cfg_doc"]) + + def config_complete(self): + self.name = self.strings["name"] + + async def client_ready(self, client, db): + self.client = client + + async def quotecmd(self, message): # noqa: C901 + """Quote a message. + Usage: .quote [template] [file/force_file] + Or: .quote np [template] [file/force_file] + If template is missing, possible templates are fetched. + If no args provided, default template will be used, quote sent as sticker""" + args = utils.get_args(message) + if len(args) > 0 and args[0] == 'np': + no_picture = True + del args[0] + else: + no_picture = False + reply = await message.get_reply_message() + + if not reply: + return await utils.answer(message, self.strings["no_reply"]) + + username_color = username = admintitle = user_id = None + profile_photo_url = reply.from_id + + admintitle = "" + pfp = None + if isinstance(reply.to_id, telethon.tl.types.PeerChannel) and reply.fwd_from: + user = reply.forward.chat + elif isinstance(reply.to_id, telethon.tl.types.PeerChat): + chat = await self.client(telethon.tl.functions.messages.GetFullChatRequest(reply.to_id)) + participants = chat.full_chat.participants.participants + participant = next(filter(lambda x: x.user_id == reply.from_id, participants), None) + if isinstance(participant, telethon.tl.types.ChatParticipantCreator): + admintitle = self.strings["creator"] + elif isinstance(participant, telethon.tl.types.ChatParticipantAdmin): + admintitle = self.strings["admin"] + user = await reply.get_sender() + else: + user = await reply.get_sender() + + username = telethon.utils.get_display_name(user) + if reply.fwd_from is not None and reply.fwd_from.post_author is not None: + username += f" ({reply.fwd_from.post_author})" + user_id = reply.from_id + + if reply.fwd_from: + if reply.fwd_from.saved_from_peer: + profile_photo_url = reply.forward.chat + admintitle = self.strings["channel"] + elif reply.fwd_from.from_name: + username = reply.fwd_from.from_name + profile_photo_url = None + admintitle = "" + elif reply.forward.sender: + username = telethon.utils.get_display_name(reply.forward.sender) + profile_photo_url = reply.forward.sender.id + admintitle = "" + elif reply.forward.chat: + admintitle = self.strings["channel"] + profile_photo_url = user + else: + if isinstance(reply.to_id, telethon.tl.types.PeerUser) is False: + try: + user = await self.client(telethon.tl.functions.channels.GetParticipantRequest(message.chat_id, + user)) + if isinstance(user.participant, telethon.tl.types.ChannelParticipantCreator): + admintitle = user.participant.rank or self.strings["creator"] + elif isinstance(user.participant, telethon.tl.types.ChannelParticipantAdmin): + admintitle = user.participant.rank or self.strings["admin"] + user = user.users[0] + except telethon.errors.rpcerrorlist.UserNotParticipantError: + pass + + if no_picture: + profile_photo_url = '' + else: + if profile_photo_url is not None: + pfp = await self.client.download_profile_photo(profile_photo_url, bytes) + + if pfp is not None: + profile_photo_url = "data:image/png;base64, " + base64.b64encode(pfp).decode() + else: + profile_photo_url = "" + + if user_id is not None: + username_color = self.config["username_colors"][user_id % 7] + else: + username_color = self.config["default_username_color"] + + reply_username = "" + reply_text = "" + if reply.is_reply is True: + reply_to = await reply.get_reply_message() + reply_peer = None + if reply_to.fwd_from is not None: + if reply_to.forward.chat is not None: + reply_peer = reply_to.forward.chat + elif reply_to.fwd_from.from_id is not None: + try: + user_id = reply_to.fwd_from.from_id + user = await self.client(telethon.tl.functions.users.GetFullUserRequest(user_id)) + reply_peer = user.user + except ValueError: + pass + else: + reply_username = reply_to.fwd_from.from_name + elif reply_to.from_id is not None: + reply_user = await self.client(telethon.tl.functions.users.GetFullUserRequest(reply_to.from_id)) + reply_peer = reply_user.user + + if reply_username is None or reply_username == "": + reply_username = telethon.utils.get_display_name(reply_peer) + reply_text = reply_to.message + + date = "" + if reply.fwd_from is not None: + date = reply.fwd_from.date.strftime("%H:%M") + else: + date = reply.date.strftime("%H:%M") + + request = json.dumps({ + "ProfilePhotoURL": profile_photo_url, + "usernameColor": username_color, + "username": username, + "adminTitle": admintitle, + "Text": reply.message, + "Markdown": get_markdown(reply), + "ReplyUsername": reply_username, + "ReplyText": reply_text, + "Date": date, + "Template": args[0] if len(args) > 0 else 'default', + "APIKey": self.config["api_token"] + }) + + resp = await utils.run_sync(requests.post, self.config["api_url"] + "/api/v2/quote", data=request) + resp.raise_for_status() + resp = await utils.run_sync(resp.json) + + if resp["status"] == 500: + return await utils.answer(message, self.strings["server_error"]) + elif resp["status"] == 401: + if resp["message"] == "ERROR_TOKEN_INVALID": + return await utils.answer(message, self.strings["invalid_token"]) + else: + raise ValueError("Invalid response from server", resp) + elif resp["status"] == 403: + if resp["message"] == "ERROR_UNAUTHORIZED": + return await utils.answer(message, self.strings["unauthorized"]) + else: + raise ValueError("Invalid response from server", resp) + elif resp["status"] == 404: + if resp["message"] == "ERROR_TEMPLATE_NOT_FOUND": + newreq = await utils.run_sync(requests.post, self.config["api_url"] + "/api/v1/getalltemplates", data={ + "token": self.config["api_token"] + }) + newreq = await utils.run_sync(newreq.json) + + if newreq["status"] == "NOT_ENOUGH_PERMISSIONS": + return await utils.answer(message, self.strings["not_enough_permissions"]) + elif newreq["status"] == "SUCCESS": + templates = self.strings["delimiter"].join(newreq["message"]) + return await utils.answer(message, self.strings["templates"].format(templates)) + elif newreq["status"] == "INVALID_TOKEN": + return await utils.answer(message, self.strings["invalid_token"]) + else: + raise ValueError("Invalid response from server", newreq) + else: + raise ValueError("Invalid response from server", resp) + elif resp["status"] != 200: + raise ValueError("Invalid response from server", resp) + + req = await utils.run_sync(requests.get, self.config["api_url"] + "/cdn/" + resp["message"]) + req.raise_for_status() + file = BytesIO(req.content) + file.seek(0) + + if len(args) == 2: + if args[1] == "file": + await utils.answer(message, file) + elif args[1] == "force_file": + file.name = self.strings["filename"] + await utils.answer(message, file, force_document=True) + else: + img = await utils.run_sync(Image.open, file) + with BytesIO() as sticker: + await utils.run_sync(img.save, sticker, "webp") + sticker.name = "sticker.webp" + sticker.seek(0) + try: + await utils.answer(message, sticker) + except telethon.errors.rpcerrorlist.ChatSendStickersForbiddenError: + await utils.answer(message, self.strings["cannot_send_stickers"]) + file.close() + + +def get_markdown(reply): + if not reply.entities: + return [] + + markdown = [] + for entity in reply.entities: + md_item = { + "Type": None, + "Start": entity.offset, + "End": entity.offset + entity.length - 1 + } + if isinstance(entity, telethon.tl.types.MessageEntityBold): + md_item["Type"] = "bold" + elif isinstance(entity, telethon.tl.types.MessageEntityItalic): + md_item["Type"] = "italic" + elif isinstance(entity, (telethon.tl.types.MessageEntityMention, telethon.tl.types.MessageEntityTextUrl, + telethon.tl.types.MessageEntityMentionName, telethon.tl.types.MessageEntityHashtag, + telethon.tl.types.MessageEntityCashtag, telethon.tl.types.MessageEntityBotCommand, + telethon.tl.types.MessageEntityUrl)): + md_item["Type"] = "link" + elif isinstance(entity, telethon.tl.types.MessageEntityCode): + md_item["Type"] = "code" + elif isinstance(entity, telethon.tl.types.MessageEntityStrike): + md_item["Type"] = "stroke" + elif isinstance(entity, telethon.tl.types.MessageEntityUnderline): + md_item["Type"] = "underline" + else: + logger.warning("Unknown entity: " + str(entity)) + + markdown.append(md_item) + return markdown diff --git a/AlpacaGang/ftg-modules/range.py b/AlpacaGang/ftg-modules/range.py new file mode 100644 index 0000000..af411ea --- /dev/null +++ b/AlpacaGang/ftg-modules/range.py @@ -0,0 +1,147 @@ +# encoding: utf-8 +import asyncio +import time +import logging + +from .. import loader, utils +logger = logging.getLogger(__name__) + + +def register(cb): + cb(RangeMod()) + + +@loader.tds +class RangeMod(loader.Module): + """Provides numbers as in Python range with delay""" + strings = { + "name": "Range", + "no_args": "Not enough args (minimum {})", + "delay_num": "Delay must be a number", + "args_int": "All range args must be integers", + "many_args": "There must be no more than {} arguments" + } + + def __init__(self): + self.config = loader.ModuleConfig( + "msg_format", "{0}", "Format of each message. {0} replaces current number.", + "default_delay", 1.0, "Delay in all commands by default" + ) + self.name = self.strings['name'] + + def config_complete(self): + self.name = self.strings['name'] + + async def _do_range(self, range_args, delay, message): + """for internal usage; do range itself""" + await message.delete() + for now in range(*range_args): + before = time.time() + await message.respond(self.config['msg_format'].format(now)) + delta = time.time() - before + await asyncio.sleep(max(delay - delta, 0)) + + async def _get_args(self, message, minn, maxn): + args = utils.get_args(message) + if len(args) < minn: + logger.warning(f'Minimum {minn} {"args" if minn != 1 else "arg"}, {len(args)} provided') + await utils.answer(message, self.strings['no_args'].format(minn)) + return None + elif len(args) > maxn: + logger.warning(f'Maximum {maxn} {"args" if maxn != 1 else "arg"}, {len(args)} provided') + await utils.answer(message, self.strings['many_args'].format(maxn)) + return None + return args + + async def _check_range_args(self, range_args, message): + """for internal usage; check if range args are int""" + try: + range_args = [int(x) for x in range_args] + return range_args + except ValueError: + logger.warning(f'Impossible to convert all range args to int ({range_args})') + await utils.answer(message, self.strings['args_int']) + return None + + async def rangecmd(self, message): + """Iterates over the given range and returns each number in separate message. + Usage: .range """ + args = await self._get_args(message, 1, 3) + if args is None: + return # user done sth wrong + + delay = self.config['default_delay'] + range_args = await self._check_range_args(args, message) + if range_args is None: + return # user done sth wrong + + await self._do_range(range_args, delay, message) + + async def drangecmd(self, message): + """Iterates over the given range and returns each number in separate message. + Usage: .drange """ + args = await self._get_args(message, 2, 4) + if args is None: + return # user done sth wrong + + try: + delay = float(args[0]) + except ValueError: + logger.warning(f'Impossible to convert delay to float ({args[0]})') + await utils.answer(message, self.strings['delay_num']) + return + + range_args = await self._check_range_args(args[1:], message) + if range_args is None: + return + + await self._do_range(range_args, delay, message) + + async def countcmd(self, message): + """Count from 1 to N.\nUsage: .count or .count """ + args = await self._get_args(message, 1, 2) + if args is None: + return + + if len(args) == 1: + delay = self.config['default_delay'] + range_args = (1, args[0], 1) + elif len(args) == 2: + try: + delay = float(args[0]) + except ValueError: + logger.warning(f'Impossible to convert delay to float ({args[0]})') + await utils.answer(message, self.strings['delay_num']) + return + range_args = (1, args[1], 1) + + range_args = await self._check_range_args(range_args, message) + if range_args is None: + return + range_args[1] += 1 # so last number we print will be N itself + + await self._do_range(range_args, delay, message) + + async def rcountcmd(self, message): + """Count from N to 1.\nUsage: .rcount or .rcount """ + args = await self._get_args(message, 1, 2) + if args is None: + return + + if len(args) == 1: + delay = self.config['default_delay'] + range_args = (args[0], 0, -1) + elif len(args) == 2: + try: + delay = float(args[0]) + except ValueError: + logger.warning(f'Impossible to convert delay to float ({args[0]})') + await utils.answer(message, self.strings['delay_num']) + return + range_args = (args[1], 0, -1) + + range_args = await self._check_range_args(range_args, message) + if range_args is None: + return + + await self._do_range(range_args, delay, message) diff --git a/AlpacaGang/ftg-modules/repl.py b/AlpacaGang/ftg-modules/repl.py new file mode 100644 index 0000000..19429be --- /dev/null +++ b/AlpacaGang/ftg-modules/repl.py @@ -0,0 +1,66 @@ +from .. import loader, utils +# from telethon import* +from telethon import functions, types + +import logging +import datetime +import time +import asyncio + +logger = logging.getLogger(__name__) + + +def register(cb): + cb(REPLMod()) + + +@loader.tds +class REPLMod(loader.Module): + """REPLIED for selected users""" + strings = {"name": "REPL"} + + d = dict() + + def __init__(self): + self.name = self.strings["name"] + + def config_complete(self): + pass + + async def client_ready(self, client, db): + self._db = db + self._me = await client.get_me() + + async def addtxcmd(self, message): + """Select users\nFor example: .addtx used_id \"text when reply (Default: \'.\'\"""" + args = utils.get_args(message) + if not len(args): + await utils.answer(message, "Напиши .help REPL, там есть пример, как нужно делать.\nТы сделал неправильно!") + elif len(args) == 1: + self.d[int(args[0])] = '.' + else: + f = "" + for i in range(1, len(args)): + f += args[i] + if i != len(args) - 1: + f += " " + self.d[int(args[0])] = f + await utils.answer(message, "Done.\nP.S: 3 seconds later it's automatic delete") + await asyncio.sleep(3) + await message.delete() + + async def clrtxcmd(self, message): + """Unselect user\nFor example: `.clrtx used_id` for one user or `.clrtx` for all users""" + args = utils.get_args(message) + if not len(args): + self.d.clear() + else: + self.d.pop(int(args[0])) + await utils.answer(message, "Done.\nP.S: 3 seconds later it's automatic delete") + await asyncio.sleep(3) + await message.delete() + + async def watcher(self, message): + if (message.from_id in self.d) and ( + message.mentioned or getattr(message.to_id, "user_id", None) == self._me.id): + await message.respond(self.d[message.from_id], reply_to=message) diff --git a/AlpacaGang/ftg-modules/sirius.py b/AlpacaGang/ftg-modules/sirius.py new file mode 100644 index 0000000..9621e91 --- /dev/null +++ b/AlpacaGang/ftg-modules/sirius.py @@ -0,0 +1,81 @@ +# requires: pymongo dnspython + +# Забейте +import asyncio +import pymongo + +import logging +logger = logging.getLogger(__name__) +from .. import loader, utils + +class Student: + def __init__(self, id: int, last_name: str, first_name: str, + patronymic: str, grade: int, region: str, academ: bool, approved: int=None): + self.last_name = last_name + self.first_name = first_name + self.patronymic = patronymic + self.grade = int(grade) + self.region = region + self.academ = bool(academ) + self.approved = approved + self.id = int(id) + + def __str__(self): + p = 'Академ' if self.academ else 'Отбор' + # a = f'Уже вроде бы добавлен в чат (tg://user?id={self.approved})' if self.approved else 'Еще нет в чате' + return f'[{p}.{self.id}] {self.last_name} {self.first_name} {self.patronymic}, '\ + f'{self.grade} класс, из {self.region}' + +class SiriusMod(loader.Module): + """Ищем поступивших на ИЮ2020""" + strings = {"name": "Sirius"} + def __init__(self): + self.config = loader.ModuleConfig( + # name - default - description + "db_uri", None, "Database URI, if you dont know where to take it - nevermind", + "db_db", None, "database", + "db_coll", None, "collection", + "replace_ё", True, "replace ё with е in requests, incorrect usage may return incorrect result" + ) + self.name = self.strings['name'] + self.db = None + + def config_complete(self): + self.db = pymongo.MongoClient(self.config['db_uri'])\ + .get_database(self.config['db_db']).get_collection(self.config['db_coll']) + + async def findcmd(self, message): + arg = utils.get_args_raw(message).strip() + if self.config['replace_ё']: + arg = arg.replace('ё', 'е') + arg = arg.replace('Ё', 'Е') + logger.debug('Got: %s', arg) + if not arg: + await utils.answer(message, 'Только 1 аргумент - номер в списке или фамилия/имя') + if arg.isdigit(): + add = f'людей с номером {arg}' + arg = int(arg) + users = list(self.db.find({"id": arg})) + elif ' ' in arg or arg.lower() == 'янао': # Костыли костыли + add = f'людей из региона {arg}' + _users = list(self.db.find()) + users = [] + for user in _users: + if user['region'].lower() == arg.lower(): + users.append(user) + else: + add = f'людей, которых зовут {arg}' + arg = arg.capitalize() + users = list(self.db.find({'$or': [{"last_name": arg}, {"first_name": arg}, {"patronymic": arg}]})) + + msg = [f'{len(users)} всего {add}', '=='] + for user in users: + del user['_id'] + s = Student(**user) + msg += [str(s), '=='] + msg = msg[:-1] + msg = '\n'.join(msg) + await utils.answer(message, msg) + + + diff --git a/AlpacaGang/ftg-modules/spam.py b/AlpacaGang/ftg-modules/spam.py new file mode 100644 index 0000000..bba611e --- /dev/null +++ b/AlpacaGang/ftg-modules/spam.py @@ -0,0 +1,92 @@ +# Copyright (C) 2019 The Raphielscape Company LLC. +# +# Licensed under the Raphielscape Public License, Version 1.c (the "License"); +# you may not use this file except in compliance with the License. + +import asyncio +from asyncio import wait, sleep + +from userbot import BOTLOG, BOTLOG_CHATID, CMD_HELP +from userbot.events import register + + +@register(outgoing=True, pattern="^.cspam (.*)") +async def tmeme(e): + cspam = str(e.pattern_match.group(1)) + message = cspam.replace(" ", "") + await e.delete() + for letter in message: + await e.respond(letter) + if BOTLOG: + await e.client.send_message( + BOTLOG_CHATID, "#CSPAM\n" + "TSpam was executed successfully") + + +@register(outgoing=True, pattern="^.wspam (.*)") +async def tmeme(e): + wspam = str(e.pattern_match.group(1)) + message = wspam.split() + await e.delete() + for word in message: + await e.respond(word) + if BOTLOG: + await e.client.send_message( + BOTLOG_CHATID, "#WSPAM\n" + "WSpam was executed successfully") + + +@register(outgoing=True, pattern="^.spam (.*)") +async def spammer(e): + counter = int(e.pattern_match.group(1).split(' ', 1)[0]) + spam_message = str(e.pattern_match.group(1).split(' ', 1)[1]) + await e.delete() + await asyncio.wait([e.respond(spam_message) for i in range(counter)]) + if BOTLOG: + await e.client.send_message(BOTLOG_CHATID, "#SPAM\n" + "Spam was executed successfully") + + +@register(outgoing=True, pattern="^.picspam") +async def tiny_pic_spam(e): + message = e.text + text = message.split() + counter = int(text[1]) + link = str(text[2]) + await e.delete() + await asyncio.wait([e.client.send_file(e.chat_id, link) for i in range(counter)]) + if BOTLOG: + await e.client.send_message( + BOTLOG_CHATID, "#PICSPAM\n" + "PicSpam was executed successfully") + + +@register(outgoing=True, pattern="^.delayspam (.*)") +async def spammer(e): + spamDelay = float(e.pattern_match.group(1).split(' ', 2)[0]) + counter = int(e.pattern_match.group(1).split(' ', 2)[1]) + spam_message = str(e.pattern_match.group(1).split(' ', 2)[2]) + await e.delete() + for i in range(1, counter): + await e.respond(spam_message) + await sleep(spamDelay) + if BOTLOG: + await e.client.send_message( + BOTLOG_CHATID, "#DelaySPAM\n" + "DelaySpam was executed successfully") + + +CMD_HELP.update({ + "spam": + ".cspam \ +\nUsage: Spam the text letter by letter.\ +\n\n.spam \ +\nUsage: Floods text in the chat !!\ +\n\n.wspam \ +\nUsage: Spam the text word by word.\ +\n\n.picspam \ +\nUsage: As if text spam was not enough !!\ +\n\n.delayspam \ +\nUsage: .bigspam but with custom delay.\ +\n\n\nNOTE : Spam at your own risk !!" +}) diff --git a/AlpacaGang/ftg-modules/spf.py b/AlpacaGang/ftg-modules/spf.py new file mode 100644 index 0000000..cd15ec1 --- /dev/null +++ b/AlpacaGang/ftg-modules/spf.py @@ -0,0 +1,37 @@ +# fuck python the encoding: utf-8 +from .. import loader, utils + +import logging +import datetime +import time +import asyncio + +logger = logging.getLogger(__name__) + + +def register(cb): + cb(SPFMod()) + + +@loader.tds +class SPFMod(loader.Module): + """Этот модуль геи личку ваших друзей""" + strings = {"name": "ЖУЖАКА НАХУЙ"} + + def __init__(self): + self.name = self.strings["name"] + + def config_complete(self): + pass + + async def spfcmd(self, message): + """Чтобы использовать пишем так: .spf @ник_вашего_друга""" + args = utils.get_args(message) + if not args: + await utils.answer(message, "Вы не указали кому хотите писать\nЧтобы использовать напишите так: .spf @ник_вашего_друга") + return + who = args[0][1:] + conv = message.client.conversation("t.me/" + who, + timeout=5, exclusive=True) + for i in range(100): + await conv.send_message("Ты гей") diff --git a/AlpacaGang/ftg-modules/tralka.py b/AlpacaGang/ftg-modules/tralka.py new file mode 100644 index 0000000..7f9dc09 --- /dev/null +++ b/AlpacaGang/ftg-modules/tralka.py @@ -0,0 +1,245 @@ +from .. import loader, utils +import logging +import random + +logger = logging.getLogger(__name__) + +version = 4.8 +sentence_min = 3 +sentence_max = 10 +# paragraph_min = 10 +# paragraph_max = 20 +print_length = False + +m = ['некультурный', 'необразованный', + 'гороховый', 'мудовый', 'глупенький', + 'малолетний', 'ебучий', 'гнилой', + 'собачий', 'ссаный', 'моржовый', + 'вредный', 'прибабахнутый', 'ебаный', + 'волшебный', 'сказочный', 'маленький', + 'приёмный', 'сральный', 'пердёжный', + 'обоссанный', 'обосранный', 'чёртов', + 'грязный', 'тупой', 'нищий', 'родной', 'мусорный', + 'дегенеративный', + 'распроклятый', 'турецкий', 'блядский', + 'ёбаный', 'хуев', 'хуёвый', 'ебанутый', + 'ёбнутый', 'грязный', 'зелёный', 'сукин', + 'лысый', 'пожилой', 'вонючий', 'чокнутый'] + +f = ['некультурная', 'необразованная', + 'гороховая', 'мудовая', 'глупенькая', + 'малолетняя', 'ебучая', 'гнилая', + 'собачья', 'ссаная', 'моржовая', + 'вредная', 'прибабахнутая', 'ебаная', + 'волшебная', 'сказочная', 'маленькая', + 'приёмная', 'сральная', 'пердёжная', + 'обоссанная', 'обосранная', 'чёртова', + 'грязная', 'тупая', 'нищая', + 'родная', 'мусорная', 'дегенеративная', + 'распроклятая', 'турецкая', 'блядская', + 'ёбаная', 'хуева', 'хуёвая', 'ебанутая', + 'ёбнутая', 'грязная', 'зелёная', 'сукина', + 'лысая', 'пожилая', 'вонючая', 'чокнутая'] + +s = ['некультурное', 'необразованное', + 'гороховое', 'мудовое', 'глупенькое', + 'малолетнее', 'ебучее', 'гнилое', + 'собачье', 'ссаное', 'моржовое', + 'вредное', 'прибабахнутое', 'ебаное', + 'волшебное', 'сказочное', 'маленькое', + 'приёмное', 'сральное', 'пердёжное', + 'обоссанное', 'обосранное', 'чёртово', 'грязное', + 'тупое', 'нищее', 'родное', 'мусорное', 'дегенеративное', + 'распроклятое', 'турецкое', 'блядское', + 'ёбаное', 'хуево', 'хуёвое', 'ебанутое', + 'ёбнутое', 'грязное', 'зелёное', 'сукино', + 'лысое', 'пожилое', 'вонючее', 'чокнутое'] + +k = ['из жопы', 'в ловушке', 'в бане', + 'на хуе', 'в дурке', 'из стула', 'в дурке ебаной', + 'в хуипе', 'в запечатанной колоде'] + +n = ['негра', 'джокера', 'тупого говна', 'хуйни ебаной', + 'хуя', 'феминизма', 'говна', + 'от народа', 'хуйни', 'Навального', + 'ловушкера', 'Путина', 'русского народа', 'вонючки', 'с функцией жопа'] + +o = ['пиздец', 'блять', 'попался в ловушку', + 'тебя забайтили', 'ловушка джокера', 'тебе бан', + 'фак ю', 'убейся', 'соси', + 'ёбаный твой рот', 'срал я на тебя', + 'убейся об стену', 'соси пизду', 'у тебя хуй вместо носа', + 'купи мою подписку на ютубе', + 'хуй соси', 'губой тряси', 'я съел деда', + 'насрал в пизду', + '22 июня 1642 года Карл Первый поднял королевский штандарт (королевский флаг), что по английским традициям означало объявление войны', + 'мне этот мир абсолютно понятен', + 'я был на этой планете бесконечным множеством', + 'но тебе этого не понять', + 'иди преисполняться в гранях каких-то', + 'пиздуй - бороздуй', + 'бредишь', 'вот я какну и смываю, и ты так делай', + 'не надо шутить с войной', + 'твою дочку ебут', 'залупаешься', + 'хули ты пиздишь', 'поцелуй лошадиную сраку', + 'распронаёб тебя', 'ъеь', 'ьуь', 'аье', + 'какого хуя они в другом порядке разложены', + 'ай фак ю булщит щит', + 'он за углом сидит и тебе на голову дрочит', + 'армяне в нарды играют', 'жирняк гай', + 'иди сюда, попробуй меня трахнуть, я тебя сам трахну', + 'что ты там делаешь', 'беги за горизонт', + 'попал в дурку ебаную', 'был бы ты человек', 'нахуй', + 'запомни', 'хули ты сюда лезешь', + 'высрана твоя морда', 'возьми салфетку', + 'я бы никому не проиграл', + 'иди нахуй', 'иди', + 'я тебя ебал, гад, срать на нас говна', + 'я тебя ебал гадить нас срать так', + 'держи в курсе', 'несёшь хуйню какую-то', + 'русские вперёд'] + +d = ['бекон', 'сыр', 'пенис', 'член', + 'мудозвон', 'лицемер', 'лжец', + 'хуй', 'гомогей', 'чай', 'рукоблуд', + 'долбан', 'пидорас', 'сын', 'козёл', + 'газ', 'фашист', 'пососатель', + 'дегенерат', 'спермобак', 'долбоёб', + 'клоун', 'паразит', 'письколёт', + 'мудак', 'спидозник', 'пудж', 'кремлебот', + 'объебос', 'дурачок', 'хуебес', 'пиздолёт', + 'педик', 'педик - медведик', 'дебил', 'дифичент', + 'кок сакер', 'пиздабол', 'аутист', 'гадёныш', 'выблядок', + 'глиномес', 'даун', 'хер', 'булщит', 'засранец', + 'инвалид', 'дурак', 'болван', + 'минетчик', 'онанист', 'напёрдыш', + 'чилипиздрик', 'пиздюк', 'гей', 'ловушкер', + 'пендос', 'наркоман', 'алкаш', 'жиробас', + 'рак', 'укурок', 'крокодил', 'ебальник', + 'секс-раб', 'потомок', 'дрыщ', + 'урод', 'карлик', 'дед инсайд', 'волк', + 'калыван', 'либераст', 'шакал', + 'педофил', 'бомж', 'пингвин', 'жираф', + 'огурец', 'салат', 'лук', 'картофель', + 'деградант', 'спам', 'человек', 'гуманитарий', + 'язык', 'стол', 'PEP-8', 'ебалай', 'враг', 'недруг', 'супостат', + 'кретин', 'козолуп', 'свинарь', + 'униженец', 'опущенец', 'муравей', + 'дятел', 'козёл', 'жирняк', 'говноед', + 'чёрт', 'суслик', 'идиот', 'жлоб', 'мерзавец', + 'негодяй', 'подлец', 'ублюдок', 'гад', + 'гавкошмыг', 'чикибамбонатор', 'чикибамбог', + 'джокер', 'жмых', 'жмышок', 'жмышонок', + 'куколд', 'ебалай', 'ушлёпок', + 'хуесос', 'членосос', 'чикибамбонёнок', + 'чикибан', 'чикибомбастер', 'чайник', + 'чикибамбонизатор', 'чикибамбог'] + +dd = ['куколда', 'хуйолда', 'мудила', 'блядина', 'гнида', + 'пидрила', 'тварь', 'сука', 'сперма', 'пидорасина', + 'либераха', 'срака', 'жопа', 'петушара', 'залупа', + 'хуета', 'пупа', 'петька', 'блядь', 'елда', 'тряпка', + 'яма', 'хуемразь', 'срань', 'мошонка', 'ссанина', + 'вагина', 'пизда', 'пососательница', + 'ловушка', 'паста', 'макаронина', + 'жиробасина', 'радфемка', 'шлюха', 'прошмандовка', + 'жируха', 'доска', 'уродина', + 'плоскодонка', 'скотина', 'омега', + 'черешня', 'ватрушка', 'шишка', + 'ракушка', 'свинья', 'какашка', + 'гнилушка', 'лягушка', 'свинушка', + 'картошка', 'волчара', 'дочь', 'пешка', + 'давалка', 'пососательница', + 'колбаса', 'собака', 'мохнатка', 'жижа', + 'какашка', 'какуля', 'душа', 'вражина', + 'падла', 'болезнь', 'бумажка', 'вонючка', + 'тень', 'гадина', 'чикибамбони', + 'микробамбони', 'мышь', 'мразь', + 'мразина', 'мразота'] + +ddd = ['удобрение', 'уёбище', 'ебло', 'хуйло', + 'чудище', 'говно', 'яблоко', 'животное', + 'дерьмо', 'блядотище', 'дитя', 'порождение', + 'очко', 'растение', 'ебало', 'ведро', + 'мудило', 'хуепучило'] + +gens = ['03', '14', '25', '8', + '06', '16', '26', '30', + '41', '52', '303', '330', '0', + '414', '441', '1', '8', + '525', '552', '2', '067', + '167', '267', '306', '416', + '526', '07', '8', '8', '8', + '17', '27', '8', '8', '8', + '307', '417', '527', '8', '8', + '3067', '4167', '5267'] + +array = [d, dd, ddd, m, f, s, k, n, o] + + +def generate(word_count: int, caps_rate: int, name: str): + res = [] + priv = '' + if name: + priv += f'Привет, {name}! ' + caps_rate %= 100 + priv += 'Ты, ' + word_count_now = 0 + while word_count_now < word_count: + tempi = word_count + 1 + while word_count_now + tempi > word_count: + random.seed() + y = random.choice(gens) + x = [] + for j in y: + x.append(random.choice(array[int(j)])) + x = ' '.join(x) + tempi = len(x.split()) + res.append(x) + word_count_now += tempi + res = ', '.join(res) + res = res.split() + count = 0 + kek = random.randint(sentence_min, sentence_max) + for v in range(len(res)): + if res[v].endswith(','): + count += 1 + if count % kek == 0: + count = 0 + random.seed() + kek = random.randint(sentence_min, sentence_max) + res[v] = res[v][:-1] + '.' + if v < len(res) - 1: + res[v + 1] = res[v + 1][0].upper() + res[v + 1][1:] + res[0] = priv + res[0] + res = ' '.join(res).split() + for v in range(len(res)): + random.seed() + z = random.randint(0, 99) + if z < caps_rate: + res[v] = res[v].upper() + return ' '.join(res) + '.' +# КТО ПРОЧИТАЛ ТОТ ЗДОХНЕТ + +def register(cb): + cb(TralkaMod()) + + +@loader.tds +class TralkaMod(loader.Module): + """Generates pastes""" + strings = {"name": "Tralka"} + + def __init__(self): + self.config = loader.ModuleConfig() + self.name = self.strings['name'] + + async def tralkacmd(self, message): + """.tralka """ + args = utils.get_args(message) + if len(args) < 2: + await utils.answer(message, "Not enough arguments") + elif len(args) == 2: + await utils.answer(message, generate(int(args[0]), int(args[1]), None)) + else: + await utils.answer(message, generate(int(args[0]), int(args[1]), args[2])) diff --git a/AlpacaGang/ftg-modules/wait.py b/AlpacaGang/ftg-modules/wait.py new file mode 100644 index 0000000..11f2d65 --- /dev/null +++ b/AlpacaGang/ftg-modules/wait.py @@ -0,0 +1,94 @@ +from .. import loader, utils + +import logging +import datetime +import time +import asyncio + +logger = logging.getLogger(__name__) + + +def register(cb): + cb(WAITMod()) + + +@loader.tds +class WAITMod(loader.Module): + """Этот модуль поможет вам удалить сообщение через n секунд/минут""" + strings = {"name": "wait"} + + def __init__(self): + self.name = self.strings["name"] + + def config_complete(self): + pass + + async def wait5cmd(self, message): + """Эта команда удаляет сообхение черезе 5 секунд""" + await utils.answer(message, "Через 5 секунд это сообщение удалится") + + for i in range(4, -1, -1): + await asyncio.sleep(1) + await utils.answer(message, "Через " + str(i) + " секунд это сообщение удалится") + + await message.delete() + + async def waitcmd(self, message): + """Эта команда удаляет сообхение через n секунд, \nписать нужно так: .wait , если хотите секунды\nи так .wait m, если хотите ждать в минутах\n(например .wait 5m)""" + args = utils.get_args(message) + if not args or len(args) > 1: + await utils.answer(message, "Вы не указали число секунд или указали несколько параметров") + else: + try: + g = -1 + h = "" + try: + g = int(args[0][:len(args[0])]) + except: + try: + g = int(args[0][:len(args[0]) - 1]) + h = args[0][len(args[0]) - 1] + except: + await utils.answer(message, "Вы указали не число!") + if g > 0: + if h == 's' or h == '': + x = g + lst = "Через " + str(x) + " секунд это сообщение удалится" + await utils.answer(message, lst) + + dd = time.time() + + while time.time() - dd < x: + now = "Через " + str(x - round(time.time() - dd)) + " секунд это сообщение удалится" + if now != lst: + await utils.answer(message, now) + lst = now + await message.delete() + elif h == 'm': + x = g + lst = "Через " + str(x) + " минут это сообщение удалится" + await utils.answer(message, lst) + + dd = time.time() + + ff = x * 60 + + llst = x + while time.time() - dd < ff: + oo = round((ff - round(time.time() - dd)) / 60) + nw = oo + if nw == llst: + await asyncio.sleep(0.1) + continue + now = "Через " + str(nw) + " минут это сообщение удалится" + await utils.answer(message, now) + llst = nw + await message.delete() + else: + await utils.answer(message, "Вы указали не число!") + except: + await utils.answer(message, "Упс, ошибочка вышла! Напшите @gerasikoff, он вам поможет") + + async def tagcmd(self, message): + """Эта команда для троллинга друзей. \nЕй вы можете тегнуть друга, а сообщение само удалится!""" + await message.delete() diff --git a/AmoreForever/amoremods/README.md b/AmoreForever/amoremods/README.md new file mode 100644 index 0000000..58a5bad --- /dev/null +++ b/AmoreForever/amoremods/README.md @@ -0,0 +1,3 @@ +![amoremods](https://te.legra.ph/file/8600de18766a556b2f78e.jpg) +# amoremods +My mods for userbot diff --git a/AmoreForever/amoremods/abstract.py b/AmoreForever/amoremods/abstract.py new file mode 100644 index 0000000..63fd618 --- /dev/null +++ b/AmoreForever/amoremods/abstract.py @@ -0,0 +1,53 @@ +# █ █ █ █▄▀ ▄▀█ █▀▄▀█ █▀█ █▀█ █ █ +# █▀█ █ █ █ █▀█ █ ▀ █ █▄█ █▀▄ █▄█ + +# 🔒 Licensed under the GNU GPLv3 +# 🌐 https://www.gnu.org/licenses/agpl-3.0.html +# 👤 https://t.me/hikamoru + +# meta developer: @hikamorumods +# meta pic: https://te.legra.ph/file/868a280910e7f61f6ab0e.png +# meta banner: https://raw.githubusercontent.com/AmoreForever/assets/master/Abstract.jpg + + +from .. import utils, loader + +chat = "@aeabstractbot" + + +class AbstractMod(loader.Module): + """Write a beautiful summary on a notebook""" + + strings = { + "name": "Abstract", + "processing": ( + "🕔 Processing..." + ), + } + + @loader.owner + @loader.command(ru_doc="<текст> - Создать конспект") + async def konspcmd(self, message): + """ - Create summary""" + text = utils.get_args_raw(message) + message = await utils.answer(message, self.strings("processing")) + async with self._client.conversation(chat) as conv: + msgs = [] + msgs += [await conv.send_message("/start")] + msgs += [await conv.get_response()] + msgs += [await conv.send_message(text)] + m = await conv.get_response() + + await self._client.send_file( + message.peer_id, + m.media, + reply_to=message.reply_to_msg_id, + ) + + for msg in msgs + [m]: + await msg.delete() + + if message.out: + await message.delete() + + await self.client.delete_dialog(chat) diff --git a/AmoreForever/amoremods/activity.py b/AmoreForever/amoremods/activity.py new file mode 100644 index 0000000..eec04cc --- /dev/null +++ b/AmoreForever/amoremods/activity.py @@ -0,0 +1,33 @@ +# █ █ █ █▄▀ ▄▀█ █▀▄▀█ █▀█ █▀█ █ █ +# █▀█ █ █ █ █▀█ █ ▀ █ █▄█ █▀▄ █▄█ + +# 🔒 Licensed under the GNU GPLv3 +# 🌐 https://www.gnu.org/licenses/agpl-3.0.html +# 👤 https://t.me/hikamoru + +# meta developer: @hikamorumods +# meta banner: https://raw.githubusercontent.com/AmoreForever/assets/master/Activity.jpg +# requires: deep_translator + +import requests +import deep_translator +from .. import loader, utils + + +def generate_activity(): + return requests.get("http://api.farkhodovme.tk/activity/en").json()['activity'] + + +class Activity(loader.Module): + """Generate activity if you're bored""" + + strings = {"name": "Activity", "activity": "⛩ Activity: {}", "lang": "en"} + strings_ru = {"activity": "⛩ Занятие: {}", "lang": "ru"} + strings_uz = {"activity": "⛩ Harakat: {}", "lang": "uz"} + + @loader.command(ru_doc="Сгенерировать занятие", uz_doc="Harakat yaratish") + async def activity(self, message): + """Generate activity""" + res = (deep_translator.GoogleTranslator(source="auto", target=self.strings["lang"]).translate(generate_activity()) if self.strings["lang"] != "en" else generate_activity()) + txt = self.strings['activity'].format(res) + await utils.answer(message, txt) \ No newline at end of file diff --git a/AmoreForever/amoremods/aeconv.py b/AmoreForever/amoremods/aeconv.py new file mode 100644 index 0000000..658e7b2 --- /dev/null +++ b/AmoreForever/amoremods/aeconv.py @@ -0,0 +1,287 @@ +# █ █ █ █▄▀ ▄▀█ █▀▄▀█ █▀█ █▀█ █ █ +# █▀█ █ █ █ █▀█ █ ▀ █ █▄█ █▀▄ █▄█ + +# 🔒 Licensed under the GNU GPLv3 +# 🌐 https://www.gnu.org/licenses/agpl-3.0.html +# 👤 https://t.me/hikamoru + +# meta developer: @hikamorumods +# meta banner: https://github.com/AmoreForever/assets/blob/master/Aeconv.jpg?raw=true +# meta pic: https://cdn-icons-png.flaticon.com/512/5670/5670084.png + +import re +import logging + +from bs4 import BeautifulSoup as bs +from requests import get +from asyncio import sleep +from asyncio.exceptions import TimeoutError +from hikkatl.tl.types import Message +from hikkatl.errors.common import AlreadyInConversationError + +from .. import utils, loader +from ..inline.types import InlineCall + +logger = logging.getLogger(__name__) + +@loader.tds +class Aeconv(loader.Module): + """Easy and fast valute converter""" + + bot = "@exchange_rates_vsk_bot" + strings = { + "name": "Aeconv", + "wait": "💵 Converting...", + "no_args": "🖕 Where are the arguments?", + "unsupported": "🚫 Unsupported currency!", + "converted": "💸 Converted {}\n\n", + "already": "⚠️ Wait until the bot responds!", + "wrong_currency": "🤷‍♀️ Wrong currency", + "choose_currency": "📉 Choose currency", + "processing": "🕔 Processing...", + "done": "✅ Done!", + "already_in_conv": "⚠️ Already in conversation!", + } + + strings_ru = { + "wait": "💵 Конвертирую...", + "no_args": "🖕 Где аргументы?", + "unsupported": "🚫 Валюта не поддерживается!", + "converted": "💸 Сконвертирован {}\n\n", + "already": "⚠️ Подожди пока бот ответит!", + "wrong_currency": "🤷‍♀️ Неправильная валюта", + "choose_currency": "📉 Выберите валюту", + "processing": "🕔 Обрабатываю...", + "done": "[Aeconv] Готово!", + "already_in_conv": "⚠️ Жди пока закончится процесс!", + } + + strings_uz = { + "wait": "💵 Valyuta konvertatsiyasi...", + "no_args": "🖕 Argumetlar qayerda?", + "unsupported": "🚫 Valyuta qo'llab-quvvatlanmaydi!", + "converted": "💸 Konvertatsiya qilindi {}\n\n", + "already": "⚠️ Bot javob berishini kuting!", + "wrong_currency": "🤷‍♀️ Noto'g'ri valyuta", + "choose_currency": "📉 Valyutani tanlang", + "processing": "🕔 Qayta ishlayapman...", + "done": "[Aeconv]Tayyor!", + "already_in_conv": "⚠️ Protsess tugaguncha kuting!", + } + + strings_de = { # i'm really sorry for translations, i'm not good at it + "wait": "💵 Konvertiere...", + "no_args": "🖕 Wo sind die Argumente?", + "unsupported": "🚫 Nicht unterstützte Währung!", + "converted": "💸 Konvertiert {}\n\n", + "already": "⚠️ Warten Sie, bis der Bot antwortet!", + "wrong_currency": "🤷‍♀️ Falsche Währung", + "choose_currency": "📉 Währung auswählen", + "processing": "🕔 Verarbeitung...", + "done": "[Aeconv]Fertig!", + "already_in_conv": "⚠️ Warten Sie, bis der Prozess beendet ist!", + + } + + strings_tr = { # i'm really sorry for translations, i'm not good at it + "wait": "💵 Dönüştürülüyor...", + "no_args": "🖕 Argümanlar nerede?", + "unsupported": "🚫 Desteklenmeyen para birimi!", + "converted": "💸 Dönüştürüldü {}\n\n", + "already": "⚠️ Bot cevap verene kadar bekleyin!", + "wrong_currency": "🤷‍♀️ Yanlış para birimi", + "choose_currency": "📉 Para birimini seçin", + "processing": "🕔 İşleniyor...", + "done": "[Aeconv]Tamam!", + "already_in_conv": "⚠️ İşlem bitene kadar bekleyin!", + } + + strings_kk = { # i'm really sorry for translations, i'm not good at it + "wait": "💵 Валюта айырбасталуда...", + "no_args": "🖕 Аргументтер қайда?", + "unsupported": "🚫 Валюта қолдау көрсетілмейді!", + "converted": "💸 Айырбасталды {}\n\n", + "already": "⚠️ Бот жауап бергенге дейін күтіңіз!", + "wrong_currency": "🤷‍♀️ Дұрыс валюта емес", + "choose_currency": "📉 Валютаны таңдаңыз", + "processing": "🕔 Қайта өңдеу...", + "done": "[Aeconv]Тайық!", + "already_in_conv": "⚠️ Процесс аяқталғанда дейін күтіңіз!", + } + + custom_emojis = { + "🇬🇧": "🇬🇧", + "🇺🇿": "🇺🇿", + "🇺🇸": "🇺🇸", + "🇷🇺": "🇷🇺", + "🇰🇿": "🇰🇿", + "🇪🇺": "🇪🇺", + "🇺🇦": "🇺🇦", + "🇹🇷": "🇹🇷", + "🇵🇱": "🇵🇱", + "🇰🇬": "🇰🇬", + "bit": "💰", + "eth": "🔹", + "ton": "💰" + } + + currency_mapping = { + "EU": ("🇪🇺", "EUR"), + "GB": ("🇬🇧", "GBP"), + "UZ": ("🇺🇿", "UZS"), + "US": ("🇺🇸", "USD"), + "RU": ("🇷🇺", "RUB"), + "KZ": ("🇰🇿", "KZT"), + "UA": ("🇺🇦", "UAH"), + "PL": ("🇵🇱", "PLN"), + "TR": ("🇹🇷", "TRY"), + "KG": ("🇰🇬", "KGS") + } + + currencies = [ + "EUR", "GBP", "UZS", "USD", "RUB", "KZT", "UAH", "PLN", "TRY", "KGS", "TON", "ETH", "BTC" + ] + + currency_flags = { + "EUR": "🇪🇺", + "GBP": "🇬🇧", + "UZS": "🇺🇿", + "USD": "🇺🇸", + "RUB": "🇷🇺", + "KZT": "🇰🇿", + "UAH": "🇺🇦", + "PLN": "🇵🇱", + "TRY": "🇹🇷", + "KGS": "🇰🇬" + } + + letters_stashing = { + "E": "cur_df", + "G": "cur_gh", + "P": "cur_nq", + "R": "cur_rs", + "S": "cur_rs", + "T": "cur_tu", + "U": "cur_tu", + } + + def currencies_markup(self, argument: str = "") -> list: + return utils.chunks( + [ + { + "text": f"{self.currency_flags[cur]} {cur}", + "callback": self.callback_4_currency, + "args": (cur,), + } + for cur in [ + i + for i in self.currency_flags.keys() + if i.startswith(argument.upper()) + ] + if cur.startswith(argument.upper()) + ], + 5, + ) + + async def client_ready(self, client, db): + await utils.dnd(client, self.bot, archive=True) + + async def get_ton_in_rub(self, am, what: str = "uzs", cup: bool = False) -> str: + r = ( + get(f"https://coinchefs.com/{what}/ton/{am}/") + if cup + else get(f"https://coinchefs.com/ton/{what}/{am}/") + ) + soup = bs(r.text, "html.parser") + if result_div := soup.find('div', class_='convert-result'): + if result_text_div := result_div.find( + 'div', class_='col-xs-10 col-sm-10 text-center result-text' + ): + if value_element := result_text_div.b: + return value_element.get_text(strip=True) + else: + logger.debug("Value element not found") + else: + logger.debug("Result text div not found") + else: + logger.debug("Result div not found") + return None + + async def callback_4_currency(self, call: InlineCall, currency: str): + try: + first_letter = currency[0] + await call.answer(self.strings["processing"], show_alert=True) + await call.delete() + async with self.client.conversation(self.bot) as conv: + m = await conv.send_message("/settings") + r = await conv.get_response() + await r.click(data=b'cur_menu') + await r.click(data=b'cur_curmenu') + await r.click(data=self.letters_stashing[first_letter]) + await r.click(data=f"cur_{currency.upper()}") + await r.delete() + await m.delete() + await self.inline.bot.send_message(self.tg_id, self.strings["done"]) + except AlreadyInConversationError: + await call.answer(self.strings["already_in_conv"], show_alert=True) + + @loader.command(ru_doc="<количество> [валюта] должны быть разделены пробелом") + async def conv(self, message: Message): + """ [currency] should be separated by space""" + args = utils.get_args_raw(message) + if not args: + await utils.answer(message, self.strings["no_args"]) + return + # if args.split(" ")[1].upper() not in self.currencies: + # await utils.answer(message, self.strings["wrong_currency"]) + # return + await utils.answer(message, self.strings["wait"]) + if "ton".lower() in args.lower(): + li_args = args.split(" ") + ex_ = await self.get_ton_in_rub(li_args[0]) + try: + async with message.client.conversation(self.bot) as conv: + msg = await conv.send_message(args) if "ton".lower() not in args.lower() else await conv.send_message(ex_) + r = await conv.get_response() + res = r.text + text_ = "" + text_ += ( + self.strings["converted"].format(args) + if "ton".lower() not in args.lower() + else self.strings["converted"].format(f"{li_args[0]} TON") + ) + for emoji, currency, *_ in self.currency_mapping.values(): + if match := re.findall(f"{emoji} ?(.*) {currency}", res): + text_ += ( + f"{self.custom_emojis.get(emoji)} {currency}: " + f"{match[0]}\n" + ) + + if match := re.findall(r"(.*) BTC", res): + text_ += f"\n{self.custom_emojis['bit']} BTC: {match[0]}\n" + if match := re.findall(r"(.*) ETH", res): + text_ += f"{self.custom_emojis['eth']} ETH: {match[0]}\n" + + if ex_ := await self.get_ton_in_rub(args.split(" ")[0], args.split(" ")[1].lower(), True): + text_ += f"{self.custom_emojis['ton']} TON: {ex_.split(' = ')[1]}\n" + await utils.answer(message, text_) + await msg.delete() + await r.delete() + except AlreadyInConversationError: + await utils.answer(message, self.strings["already"]) + except TimeoutError: + await utils.answer(message, self.strings["unsupported"]) + except IndexError: + await utils.answer(message, self.strings["no_args"]) + + + @loader.command(ru_doc="[валюта] | без аргументов покажет список валют для включения/выключения") + async def controlvalute(self, message: Message): + """[currency] | without arguments will show list of currencies for enable/disable""" + if args := utils.get_args_raw(message): + await utils.answer(message, self.strings["choose_currency"], reply_markup=self.currencies_markup(args)) + else: + return await utils.answer(message, self.strings["choose_currency"], reply_markup=self.currencies_markup()) + + + \ No newline at end of file diff --git a/AmoreForever/amoremods/alarm.py b/AmoreForever/amoremods/alarm.py new file mode 100644 index 0000000..e4157c7 --- /dev/null +++ b/AmoreForever/amoremods/alarm.py @@ -0,0 +1,225 @@ +# █ █ █ █▄▀ ▄▀█ █▀▄▀█ █▀█ █▀█ █ █ +# █▀█ █ █ █ █▀█ █ ▀ █ █▄█ █▀▄ █▄█ + +# 🔒 Licensed under the GNU GPLv3 +# 🌐 https://www.gnu.org/licenses/agpl-3.0.html +# 👤 https://t.me/hikamoru + + +# meta developer: @hikamorumods +# meta banner: https://raw.githubusercontent.com/AmoreForever/assets/master/Alarm.jpg + +import re +import pytz +import random +import logging +import asyncio +from datetime import datetime + +from .. import utils, loader + +logger = logging.getLogger(__name__) + +day_to_weekday = { + "mon": 0, + "tue": 1, + "wed": 2, + "thu": 3, + "fri": 4, + "sat": 5, + "sun": 6, + "пн": 0, + "вт": 1, + "ср": 2, + "чт": 3, + "пт": 4, + "сб": 5, + "вс": 6, +} + + +@loader.tds +class AlarmMod(loader.Module): + """Alarm module for remind you about something""" + + strings = { + "name": "Alarm", + "set": " Alarm set for {}!", + "unset": " Alarm for {} unset!", + "unset_all": " All alarms unset!", + "list_item": ( + " Alarm for {}! #{}" + "\n🕔 Time: {}" + "\n🔊 Message: {}" + ), + "no_alarms": "🙅‍♂️ No alarms!", + "off_button": "✋ Off", + "notification": "⏰ Alarm!\n\n{}", + "turned_off": "✔️ Alarm turned off!", + "incorrect_time": "🖕 Incorrect time!", + "where_args": "🖕 Where arguments?", + "incorrect_args": "🖕 Incorrect arguments! Write like this: .setalarm mon 12:00 text", + "interval_doc": "Interval of sending notifications in seconds", + "time_zone_doc": "Time zone for alarms (for example, Europe/Moscow)", + } + strings_ru = { + "set": " Напоминание установлено на {}!", + "unset": " Напоминание для {} отменено!", + "unset_all": " Все напоминания отменены!", + "list_item": ( + " Напоминание для {}! #{}" + "\n🕔 Время: {}" + "\n🔊 Сообщение: {}" + ), + "no_alarms": "🙅‍♂️ Нет напоминаний!", + "off_button": "✋ Выключить", + "notification": "⏰ Напоминание!\n\n{}", + "turned_off": "✔️ Напоминание выключено!", + "incorrect_time": "🖕 Неправильное время!", + "where_args": "🖕 Где аргументы?", + "incorrect_args": "🖕 Неправильные аргументы! Пиши так: .setalarm пн 12:00 текст", + "interval_doc": "Интервал отправления напоминаний в секундах", + "time_zone_doc": "Часовой пояс для напоминаний (например, Europe/Moscow)", + } + + def __init__(self): + self.config = loader.ModuleConfig( + loader.ConfigValue( + "interval", + 5, + lambda: self.strings("interval_doc"), + validator=loader.validators.Integer(minimum=1, maximum=60), + ), + loader.ConfigValue( + "time_zone", + "Europe/Moscow", + lambda: self.strings("time_zone_doc"), + validator=loader.validators.RegExp( + r"^[\w/]+$", + ) + ), + ) + @loader.command(ru_doc="<день недели> <время> <сообщение> - установить напоминание") + async def setalarm(self, message): + """
.update", + "_cfg_cst_msg": "Custom message for info. May contain {me}, {version}, {build}, {prefix}, {platform}, {upd}, {time}, {uptime} keywords", + "_cfg_cst_btn": "Custom button for info. Leave empty to remove button", + "_cfg_cst_bnr": "Custom Banner for info.", + "_cfg_cst_frmt": "Custom fileformat for Banner info.", + "_cfg_banner": "Set `True` in order to disable an image banner", + "_cfg_time": "Use 1, -1, -3 etc.", + "_cfg_close": "Here you can change close button name", + } + + strings_ru = { + "owner": "Владелец", + "version": "Версия", + "build": "Сборка", + "prefix": "Префикс", + "uptime": "Аптайм", + "platform": "Платформа", + "time": "Время", + "up-to-date": "😌 Актуальная версия", + "update_required": "😕 Требуется обновление .update", + } + + strings_uz = { + "owner": "Egasi", + "version": "Versiya", + "build": "Yig'ish", + "prefix": "Prefix", + "uptime": "Uptime", + "platform": "Platforma", + "time": "Soat", + "up-to-date": "😌 Joriy versiya", + "update_required": "😕 Yangilanish talab qilinadi .update", + } + + strings_de = { + "owner": "Besitzer", + "version": "Version", + "build": "Zusammenbau", + "prefix": "Präfix", + "uptime": "Betriebszeit", + "platform": "Plattform", + "time": "Die Zeit", + "up-to-date": "😌 Aktuelle Version", + "update_required": "😕 Aktualisierung erforderlich .update", + } + + def __init__(self): + self.config = loader.ModuleConfig( + loader.ConfigValue( + "custom_message", + "no", + doc=lambda: self.strings("_cfg_cst_msg"), + ), + loader.ConfigValue( + "custom_button1", + ["🏡 Modules", "https://t.me/amoremods"], + lambda: self.strings("_cfg_cst_btn"), + validator=loader.validators.Series(min_len=0, max_len=2), + ), + loader.ConfigValue( + "custom_button2", + [], + lambda: self.strings("_cfg_cst_btn"), + validator=loader.validators.Series(min_len=0, max_len=2), + ), + loader.ConfigValue( + "custom_button3", + [], + lambda: self.strings("_cfg_cst_btn"), + validator=loader.validators.Series(min_len=0, max_len=2), + ), + loader.ConfigValue( + "custom_banner", + "https://te.legra.ph/file/64bde7bf6b8e377521134.mp4", + lambda: self.strings("_cfg_cst_bnr"), + ), + loader.ConfigValue( + "disable_banner", + False, + lambda: self.strings("_cfg_banner"), + validator=loader.validators.Boolean(), + ), + loader.ConfigValue( + "custom_format", + "gif", + lambda: self.strings("_cfg_cst_frmt"), + validator=loader.validators.Choice(["photo", "video", "gif"]), + ), + loader.ConfigValue( + "timezone", + "+5", + lambda: self.strings("_cfg_time"), + ), + loader.ConfigValue( + "close_btn", + "🔻Close", + lambda: self.strings("_cfg_close"), + ), + ) + + async def client_ready(self, client, db): + self._db = db + self._client = client + self._me = await client.get_me() + + def _render_info(self) -> str: + ver = utils.get_git_hash() or "Unknown" + + try: + repo = git.Repo() + diff = repo.git.log(["HEAD..origin/master", "--oneline"]) + upd = ( + self.strings("update_required") if diff else self.strings("up-to-date") + ) + except Exception: + upd = "" + + me = f'{utils.escape_html(get_display_name(self._me))}' + version = f'{".".join(list(map(str, list(main.__version__))))}' + build = f'#{ver[:8]}' # fmt: skip + prefix = f"«{utils.escape_html(self.get_prefix())}»" + platform = utils.get_named_platform() + uptime = utils.formatted_uptime() + offset = datetime.timedelta(hours=self.config["timezone"]) + tz = datetime.timezone(offset) + time1 = datetime.datetime.now(tz) + time = time1.strftime("%H:%M:%S") + + return ( + " \n" + + self.config["custom_message"].format( + me=me, + version=version, + build=build, + upd=upd, + prefix=prefix, + platform=platform, + uptime=uptime, + time=time, + ) + if self.config["custom_message"] != "no" + else ( + "🎢 AmoreInfo \n" + f'🤴 {self.strings("owner")}: {me}\n\n' + f"🕶 {self.strings('version')}: {version} {build}\n" + f"{upd}\n" + f"⏳ {self.strings('uptime')}: {uptime}\n\n" + f"⌚ {self.strings('time')}: {time}\n" + f"📼 {self.strings('prefix')}: {prefix}\n" + f"{platform}\n" + ) + ) + + def _get_mark(self, int): + if int == 1: + return ( + { + "text": self.config["custom_button1"][0], + "url": self.config["custom_button1"][1], + } + if self.config["custom_button1"] + else None + ) + + elif int == 2: + return ( + { + "text": self.config["custom_button2"][0], + "url": self.config["custom_button2"][1], + } + if self.config["custom_button2"] + else None + ) + + elif int == 3: + return ( + { + "text": self.config["custom_button3"][0], + "url": self.config["custom_button3"][1], + } + if self.config["custom_button3"] + else None + ) + + elif int == 4: + return ( + { + "text": self.config["close_btn"], + "action": "close", + } + if self.config["close_btn"] + else None + ) + + @loader.owner + async def ainfocmd(self, message: Message): + """Send userbot info""" + m1 = self._get_mark(1) + m2 = self._get_mark(2) + m3 = self._get_mark(3) + m4 = self._get_mark(4) + + await self.inline.form( + message=message, + text=self._render_info(), + reply_markup=[ + [ + *([m1] if m1 else []), + ], + [ + *([m2] if m2 else []), + *([m3] if m3 else []), + ], + [ + *([m4] if m4 else []), + ], + ], + **{} + if self.config["disable_banner"] + else {self.config["custom_format"]: self.config["custom_banner"]}, + ) diff --git a/AmoreForever/amoremods/animevoices.py b/AmoreForever/amoremods/animevoices.py new file mode 100644 index 0000000..3b3af2b --- /dev/null +++ b/AmoreForever/amoremods/animevoices.py @@ -0,0 +1,473 @@ +# █ █ █ █▄▀ ▄▀█ █▀▄▀█ █▀█ █▀█ █ █ +# █▀█ █ █ █ █▀█ █ ▀ █ █▄█ █▀▄ █▄█ + +# 🔒 Licensed under the GNU GPLv3 +# 🌐 https://www.gnu.org/licenses/agpl-3.0.html +# 👤 https://t.me/hikamoru + +# meta developer: @hikamorumods +# meta banner: https://raw.githubusercontent.com/AmoreForever/assets/master/AnimeVoices.jpg + +from .. import loader + +@loader.tds +class AnimeVoicesMod(loader.Module): + """🎤 Popular Anime Voices""" + + strings = {"name": "AnimeVoices"} + + async def smexkcmd(self, message): + """Смех Канеки""" + + reply = await message.get_reply_message() + await message.delete() + await message.client.send_file( + message.to_id, + "https://t.me/animevoicesbyamore/9", + voice_note=True, + reply_to=reply.id if reply else None, + ) + return + + async def smexycmd(self, message): + """Смех Ягами""" + + reply = await message.get_reply_message() + await message.delete() + await message.client.send_file( + message.to_id, + "https://t.me/animevoicesbyamore/7", + voice_note=True, + reply_to=reply.id if reply else None, + ) + return + + async def znaycmd(self, message): + """Знай свое место ничтожество""" + + reply = await message.get_reply_message() + await message.delete() + await message.client.send_file( + message.to_id, + "https://t.me/animevoicesbyamore/35", + voice_note=True, + reply_to=reply.id if reply else None, + ) + return + + async def madaracmd(self, message): + """Учиха Мадара""" + + reply = await message.get_reply_message() + await message.delete() + await message.client.send_file( + message.to_id, + "https://t.me/animevoicesbyamore/24", + voice_note=True, + reply_to=reply.id if reply else None, + ) + return + + async def sharingancmd(self, message): + """Итачи Шаринган""" + + reply = await message.get_reply_message() + await message.delete() + await message.client.send_file( + message.to_id, + "https://t.me/VoiceAmore/29", + voice_note=True, + reply_to=reply.id if reply else None, + ) + return + + async def itachicmd(self, message): + """Учиха Итачи""" + + reply = await message.get_reply_message() + await message.delete() + await message.client.send_file( + message.to_id, + "https://t.me/animevoicesbyamore/26", + voice_note=True, + reply_to=reply.id if reply else None, + ) + return + + async def imsasukecmd(self, message): + """Учиха Саске""" + + reply = await message.get_reply_message() + await message.delete() + await message.client.send_file( + message.to_id, + "https://t.me/VoiceAmore/30", + voice_note=True, + reply_to=reply.id if reply else None, + ) + return + + async def paincmd(self, message): + """Познайте боль""" + + reply = await message.get_reply_message() + await message.delete() + await message.client.send_file( + message.to_id, + "https://t.me/animevoicesbyamore/15", + voice_note=True, + reply_to=reply.id if reply else None, + ) + return + + async def rascmd(self, message): + """Расширение территории""" + + reply = await message.get_reply_message() + await message.delete() + await message.client.send_file( + message.to_id, + "https://t.me/animevoicesbyamore/17", + voice_note=True, + reply_to=reply.id if reply else None, + ) + return + + async def tenseicmd(self, message): + """Shinra tensei""" + + reply = await message.get_reply_message() + await message.delete() + await message.client.send_file( + message.to_id, + "https://t.me/animevoicesbyamore/18", + voice_note=True, + reply_to=reply.id if reply else None, + ) + return + + async def dazaicmd(self, message): + """Dazai""" + + reply = await message.get_reply_message() + await message.delete() + await message.client.send_file( + message.to_id, + "https://t.me/animevoicesbyamore/3", + voice_note=True, + reply_to=reply.id if reply else None, + ) + return + + async def gaycmd(self, message): + """I'm gay""" + + reply = await message.get_reply_message() + await message.delete() + await message.client.send_file( + message.to_id, + "https://t.me/animevoicesbyamore/20", + voice_note=True, + reply_to=reply.id if reply else None, + ) + return + + async def bankaicmd(self, message): + """Bankai""" + + reply = await message.get_reply_message() + await message.delete() + await message.client.send_file( + message.to_id, + "https://t.me/animevoicesbyamore/21", + voice_note=True, + reply_to=reply.id if reply else None, + ) + return + + async def satecmd(self, message): + """Sate sate sate""" + + reply = await message.get_reply_message() + await message.delete() + await message.client.send_file( + message.to_id, + "https://t.me/animevoicesbyamore/5", + voice_note=True, + reply_to=reply.id if reply else None, + ) + return + + async def yoaimocmd(self, message): + """Yoaimo""" + + reply = await message.get_reply_message() + await message.delete() + await message.client.send_file( + message.to_id, + "https://t.me/animevoicesbyamore/11", + voice_note=True, + reply_to=reply.id if reply else None, + ) + return + + async def ghoulcmd(self, message): + """Я гуль""" + + reply = await message.get_reply_message() + await message.delete() + await message.client.send_file( + message.to_id, + "https://t.me/animevoicesbyamore/12", + voice_note=True, + reply_to=reply.id if reply else None, + ) + return + + async def welawcmd(self, message): + """Мы закон""" + + reply = await message.get_reply_message() + await message.delete() + await message.client.send_file( + message.to_id, + "https://t.me/animevoicesbyamore/13", + voice_note=True, + reply_to=reply.id if reply else None, + ) + return + + async def dattebayocmd(self, message): + """Даттебайо""" + + reply = await message.get_reply_message() + await message.delete() + await message.client.send_file( + message.to_id, + "https://t.me/animevoicesbyamore/14", + voice_note=True, + reply_to=reply.id if reply else None, + ) + return + + async def hardlifecmd(self, message): + """Жизнь такова""" + + reply = await message.get_reply_message() + await message.delete() + await message.client.send_file( + message.to_id, + "https://t.me/animevoicesbyamore/16", + voice_note=True, + reply_to=reply.id if reply else None, + ) + return + + async def hanmacmd(self, message): + """Я Ханма Шужи""" + + reply = await message.get_reply_message() + await message.delete() + await message.client.send_file( + message.to_id, + "https://t.me/animevoicesbyamore/25", + voice_note=True, + reply_to=reply.id if reply else None, + ) + return + + async def surprisecmd(self, message): + """Surprise MxtherFxcker""" + + reply = await message.get_reply_message() + await message.delete() + await message.client.send_file( + message.to_id, + "https://t.me/animevoicesbyamore/30", + voice_note=True, + reply_to=reply.id if reply else None, + ) + return + + async def equalcmd(self, message): + """Мы созданы равными""" + + reply = await message.get_reply_message() + await message.delete() + await message.client.send_file( + message.to_id, + "https://t.me/animevoicesbyamore/31", + voice_note=True, + reply_to=reply.id if reply else None, + ) + return + + async def beautytreecmd(self, message): + """Красота леса""" + + reply = await message.get_reply_message() + await message.delete() + await message.client.send_file( + message.to_id, + "https://t.me/animevoicesbyamore/32", + voice_note=True, + reply_to=reply.id if reply else None, + ) + return + + async def bankaiicmd(self, message): + """Bankai remix""" + + reply = await message.get_reply_message() + await message.delete() + await message.client.send_file( + message.to_id, + "https://t.me/animevoicesbyamore/33", + voice_note=True, + reply_to=reply.id if reply else None, + ) + return + + async def yametecmd(self, message): + """Фулл ямете кудасай""" + + reply = await message.get_reply_message() + await message.delete() + await message.client.send_file( + message.to_id, + "https://t.me/animevoicesbyamore/47", + voice_note=True, + reply_to=reply.id if reply else None, + ) + return + + async def mafiacmd(self, message): + """Просыпается мафия""" + + reply = await message.get_reply_message() + await message.delete() + await message.client.send_file( + message.to_id, + "https://t.me/animevoicesbyamore/48", + voice_note=True, + reply_to=reply.id if reply else None, + ) + return + + async def sharinganncmd(self, message): + """Sharingan remix""" + + reply = await message.get_reply_message() + await message.delete() + await message.client.send_file( + message.to_id, + "https://t.me/animevoicesbyamore/49", + voice_note=True, + reply_to=reply.id if reply else None, + ) + return + + async def smexecmd(self, message): + """Смех Эрен""" + + reply = await message.get_reply_message() + await message.delete() + await message.client.send_file( + message.to_id, + "https://t.me/animevoicesbyamore/50", + voice_note=True, + reply_to=reply.id if reply else None, + ) + return + + async def narutocmd(self, message): + """Naruto heroes""" + + reply = await message.get_reply_message() + await message.delete() + await message.client.send_file( + message.to_id, + "https://t.me/animevoicesbyamore/51", + voice_note=True, + reply_to=reply.id if reply else None, + ) + return + + async def smexrcmd(self, message): + """Смех рюк""" + + reply = await message.get_reply_message() + await message.delete() + await message.client.send_file( + message.to_id, + "https://t.me/animevoicesbyamore/52", + voice_note=True, + reply_to=reply.id if reply else None, + ) + return + + async def ohayocmd(self, message): + """Охаё""" + + reply = await message.get_reply_message() + await message.delete() + await message.client.send_file( + message.to_id, + "https://t.me/animevoicesbyamore/53", + voice_note=True, + reply_to=reply.id if reply else None, + ) + return + + async def iamhungrycmd(self, message): + """Есть хочу""" + + reply = await message.get_reply_message() + await message.delete() + await message.client.send_file( + message.to_id, + "https://t.me/animevoicesbyamore/54", + voice_note=True, + reply_to=reply.id if reply else None, + ) + return + + async def amaterasucmd(self, message): + """Аматерасу remix""" + + reply = await message.get_reply_message() + await message.delete() + await message.client.send_file( + message.to_id, + "https://t.me/animevoicesbyamore/55", + voice_note=True, + reply_to=reply.id if reply else None, + ) + return + + async def owocmd(self, message): + """Full OwO""" + + reply = await message.get_reply_message() + await message.delete() + await message.client.send_file( + message.to_id, + "https://t.me/animevoicesbyamore/56", + voice_note=True, + reply_to=reply.id if reply else None, + ) + return + + async def ghoulrucmd(self, message): + """Русский Tokyo Ghoul""" + + reply = await message.get_reply_message() + await message.delete() + await message.client.send_file( + message.to_id, + "https://t.me/animevoicesbyamore/57", + voice_note=True, + reply_to=reply.id if reply else None, + ) + return + #voices by @dziru \ No newline at end of file diff --git a/AmoreForever/amoremods/autoprofile.py b/AmoreForever/amoremods/autoprofile.py new file mode 100644 index 0000000..1c3418d --- /dev/null +++ b/AmoreForever/amoremods/autoprofile.py @@ -0,0 +1,283 @@ +# Friendly Telegram (telegram userbot) +# Copyright (C) 2018-2019 The Authors + +# 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 . + +# █ █ █ █▄▀ ▄▀█ █▀▄▀█ █▀█ █▀█ █ █ +# █▀█ █ █ █ █▀█ █ ▀ █ █▄█ █▀▄ █▄█ + +# 🔒 Licensed under the GNU GPLv3 +# 🌐 https://www.gnu.org/licenses/agpl-3.0.html +# 👤 https://t.me/hikamoru + +# meta developer: @hikamorumods, FTG +__version__ = (1, 1, 0) + +import asyncio +import datetime + +from telethon.tl import functions +from telethon.utils import get_display_name + +from .. import loader, utils + + +@loader.tds +class AutoProfileMod(loader.Module): + """Automatic stuff for your profile :P""" + + strings = { + "name": "AutoProfile", + "invalid_args": ( + "Missing parameters, please read the .aguide ✔️" + ), + "missing_time": ( + "Time was not specified in bio " + ), + "enabled_bio": ( + "Enabled bio clock " + ), + "bio_not_enabled": ( + "Bio clock is not enabled " + ), + "disabled_bio": ( + "Disabled bio clock " + ), + "enabled_name": ( + "Enabled name clock " + ), + "name_not_enabled": ( + "Name clock is not enabled " + ), + "disabled_name": ( + "Name clock disabled " + ), + "_cfg_time": "Use timezone 1, -1, -3 etc.", + } + + strings_uz = { + "invalid_args": ( + "to'g'ri argumetlar emas, ni o'qing.aguide ✔️" + ), + "missing_time": ( + "vaqt bio-da o'rnatilmagan ❎< / emoji>" + ), + "enabled_bio": ( + "Bio soat muvaffaqiyatli o'rnatildi " + ), + "bio_not_enabled": ( + "soat bio-ga o'rnatilmagan ❎< / emoji > " + ), + "disabled_bio": ( + " Bio-dagi vaqt muvaffaqiyatli o'chirildi " + ), + "enabled_name": ( + "soat taxallusga muvaffaqiyatli o'rnatildi " + ), + "name_not_enabled": ( + "soat taxallusga o'rnatilmagan ❎< / emoji > " + ), + "disabled_name": ( + "taxallusdagi vaqt muvaffaqiyatli o'chirildi " + ), + "_cfg_time": "vaqt zonasidan foydalaning 1, -1, -3 va boshqalar.", + } + + strings_ru = { + "invalid_args": ( + "Не правильные аргуметы, прочитай .aguide ✔️" + ), + "missing_time": ( + "Время не было установлено в био" + ), + "enabled_bio": ( + "Био часы успешно установлены " + ), + "bio_not_enabled": ( + "Часы не установлено в био" + ), + "disabled_bio": ( + "Время в био успешно отключен " + ), + "enabled_name": ( + "Часы в ник успешно установлены " + ), + "name_not_enabled": ( + "Часы не установлены в ник" + ), + "disabled_name": ( + "Время в нике успешно отключен " + ), + "_cfg_time": "Используй таймзону 1, -1, -3 и тд.", + } + + strings_de = { + "invalid_args": ( + "Sind nicht die richtigen Argumente, lies .aguide ✔️" + ), + "missing_time": ( + "Die Zeit wurde nicht auf bio gesetzt" + ), + "enabled_bio": ( + "Bio-Uhr wurde erfolgreich installiert " + ), + "bio_not_enabled": ( + "Die Uhr ist nicht auf bio eingestellt" + ), + "disabled_bio": ( + "Zeit in bio erfolgreich deaktiviert " + ), + "enabled_name": ( + "Die Uhr wurde erfolgreich auf den Nickname gesetzt " + ), + "name_not_enabled": ( + "Die Uhr ist nicht auf den Spitznamen eingestellt" + ), + "disabled_name": ( + "Nickzeit wurde erfolgreich deaktiviert " + ), + "_cfg_time": "Benutze die Zeitzone 1, -1, -3 usw.", + } + + def __init__(self): + self.bio_enabled = False + self.name_enabled = False + self.raw_bio = None + self.raw_name = None + self.config = loader.ModuleConfig( + loader.ConfigValue( + "timezone", + "+5", + lambda: self.strings("_cfg_time"), + ), + ) + + async def client_ready(self, client, db): + self.client = client + self._me = await client.get_me() + + @loader.command(ru_doc="""Что-бы указать таймзону через конфиг""") + async def cfautoprofcmd(self, message): + """To specify the timezone via the config""" + name = self.strings("name") + await self.allmodules.commands["config"]( + await utils.answer(message, + f"{self.get_prefix()}config {name}") + ) + + @loader.command(ru_doc="""Автоматически изменяет биографию вашей учетной записи с учетом текущего времени, использования: .autobio 'сообщение, время как {time}'""") + async def autobiocmd(self, message): + """Automatically changes your account's bio with current time, usage: + .autobio 'message, time as {time}'""" + + msg = utils.get_args(message) + if len(msg) != 1: + return await utils.answer(message, self.strings("invalid_args", message)) + raw_bio = msg[0] + if "{time}" not in raw_bio: + return await utils.answer(message, self.strings("missing_time", message)) + + + self.bio_enabled = True + self.raw_bio = raw_bio + await self.allmodules.log("start_autobio") + await utils.answer(message, self.strings("enabled_bio", message)) + + while self.bio_enabled: + offset = datetime.timedelta(hours=self.config["timezone"]) + tz = datetime.timezone(offset) + time1 = datetime.datetime.now(tz) + current_time = time1.strftime("%H:%M") + bio = raw_bio.format(time=current_time) + await self.client(functions.account.UpdateProfileRequest(about=bio)) + await asyncio.sleep(60) + + @loader.command(ru_doc="""Что-бы остановить время в био введи .stopautobio""") + async def stopautobiocmd(self, message): + """Stop autobio cmd.""" + + if self.bio_enabled is False: + return await utils.answer(message, self.strings("bio_not_enabled", message)) + self.bio_enabled = False + + await self.allmodules.log("stop_autobio") + await utils.answer(message, self.strings("disabled_bio", message)) + await self.client( + functions.account.UpdateProfileRequest(about=self.raw_bio.format(time="")) + ) + + @loader.command(ru_doc="""Автоматически изменяет имя вашей учетной записи с учетом текущего времени, использования: .autoname 'сообщение, время как {time}'""") + async def autonamecmd(self, message): + """Automatically changes your Telegram name with current time, usage: + .autoname ''""" + + msg = utils.get_args(message) + if len(msg) != 1: + return await utils.answer(message, self.strings("invalid_args", message)) + raw_name = msg[0] + if "{time}" not in raw_name: + return await utils.answer(message, self.strings("missing_time", message)) + + self.name_enabled = True + self.raw_name = raw_name + await self.allmodules.log("start_autoname") + await utils.answer(message, self.strings("enabled_name", message)) + + while self.name_enabled: + offset = datetime.timedelta(hours=self.config["timezone"]) + tz = datetime.timezone(offset) + time1 = datetime.datetime.now(tz) + current_time = time1.strftime("%H:%M") + name = raw_name.format(time=current_time) + await self.client(functions.account.UpdateProfileRequest(first_name=name)) + await asyncio.sleep(60) + + @loader.command(ru_doc="""Что-бы остановить время в имени учетной записи введи .stopautoname""") + async def stopautonamecmd(self, message): + """just write .stopautoname""" + + if self.name_enabled is False: + return await utils.answer( + message, self.strings("name_not_enabled", message) + ) + + self.name_enabled = False + await self.allmodules.log("stop_autoname") + await utils.answer(message, self.strings("disabled_name", message)) + await self.client( + functions.account.UpdateProfileRequest( + first_name=self.raw_name.format(time="") + ) + ) + + @loader.command(ru_docs="""Доки ru/en""") + async def aguide(self, message): + "Just guide ru/en" + args = utils.get_args_raw(message) + args = args if args in {"en", "ru"} else "en" + + time = "{time}" + nick = f'{utils.escape_html(get_display_name(self._me))}' + pref = f"{utils.escape_html(self.get_prefix())}" + + await utils.answer( + message, + f"💸 For example:\n\n💸 AutoName: {pref}autoname '{nick} | {time}'\n" + f"💸 AutoBio: {pref}autobio 'smth | {time}'\n" + if args == "en" + else ( + f"💸 Например:\n\n💸 Авто Ник: {pref}autoname '{nick} | {time}'\n" + f"💸 Авто Био: {pref}autobio 'что-то | {time}'\n" + ), + ) diff --git a/AmoreForever/amoremods/besafe.py b/AmoreForever/amoremods/besafe.py new file mode 100644 index 0000000..74571db --- /dev/null +++ b/AmoreForever/amoremods/besafe.py @@ -0,0 +1,124 @@ +# █ █ █ █▄▀ ▄▀█ █▀▄▀█ █▀█ █▀█ █ █ +# █▀█ █ █ █ █▀█ █ ▀ █ █▄█ █▀▄ █▄█ + +# 🔒 Licensed under the GNU GPLv3 +# 🌐 https://www.gnu.org/licenses/agpl-3.0.html +# 👤 https://t.me/hikamoru + +# If you don't like the module don't use it + +# meta developer: @hikamorumods +# meta banner: https://github.com/AmoreForever/assets/blob/master/besafe.jpg?raw=true + +import logging +import requests +import ast +import re +from .. import loader, utils + +logger = logging.getLogger(__name__) + +__version__ = (1, 0, 0) + + +@loader.tds +class BeSafe(loader.Module): + """ + Check module before loading + """ + + strings = { + "name": "BeSafe", + "no_args_or_reply": "🤷‍♂️ [BeSafe] No link or reply to file", + "safe": "🛡 Module is safe", + "suspicious": "🔎 Module is suspicious\n\n Suspicious imports:\n", + 'sus_keywords': "\n🔑 Suspicous keywords:" + } + strings_ru = { + "no_args_or_reply": "🤷‍♂️ [BeSafe] Нет ссылки или реплея на модуль", + "safe": "🛡 Модуль безопасен", + "suspicious": "🔎 Модуль подозрительный\n\n Подозрительные импорты:\n", + 'sus_keywords': "\n🔑 Подозрительные ключевые слова:" + } + + def extract_imports(self, code): + code = code.lstrip('\ufeff') # крч удаление символа BOM, если он есть + + try: + tree = ast.parse(code) + except SyntaxError as e: + if "invalid non-printable character" not in str(e): + raise + code = code.encode('utf-8-sig').decode('utf-8') + tree = ast.parse(code) + imports = [] + + for node in ast.walk(tree): + if isinstance(node, ast.Import): + imports.extend(name.name for name in node.names) + elif isinstance(node, ast.ImportFrom): + module_name = node.module + imports.extend(f"{module_name}.{name.name}" for name in node.names) + return imports + + + + suspicious_imports = [ + 'glob', + 'os', + 'sys', + 'telethon.tl.TLRequest', + 'requests', + ] + suspicious_keywords = [ + r'0x418d4e0b', + r'0xf5b399ac', + r'w+z+mm+"A"+nk+u+h+lk', + r'b"\x0bN\x8dA"' + r'session', + r'TestingHikka_BOT' # временно будет тут + ] + + def extract_keywords(self, code): + words = [] + for word in self.suspicious_keywords: + if r := re.findall(word, code): + words.append(r[0]) + return words + + + + @loader.command() + async def bs(self, message): + """ + BeSafe - or + """ + args = utils.get_args_raw(message) + reply = await message.get_reply_message() + + if args: + r = await utils.run_sync(requests.get, args) + string = r.text + elif reply: + code = (await self._client.download_file(reply.media, bytes)).decode("utf-8") + string = code + else: + await utils.answer(message, self.strings["no_args_or_reply"]) + + imports = self.extract_imports(string) + sus_imports = [f"▫️ {imp}" for imp in self.suspicious_imports if imp in imports] + sus_keywords = [] + + if sus_imports: + kw = self.extract_keywords(string) + sus_keywords = [f"▫️ {k}" for k in self.suspicious_keywords if k in kw] + + if sus_imports or sus_keywords: + sus_list = sus_imports + [self.strings["sus_keywords"]] + sus_keywords + text = self.strings["suspicious"] + '\n'.join(sus_list) + else: + text = self.strings["safe"] + + await utils.answer(message, text) + + \ No newline at end of file diff --git a/AmoreForever/amoremods/birthdaywish.py b/AmoreForever/amoremods/birthdaywish.py new file mode 100644 index 0000000..22a7226 --- /dev/null +++ b/AmoreForever/amoremods/birthdaywish.py @@ -0,0 +1,229 @@ +# █ █ █ █▄▀ ▄▀█ █▀▄▀█ █▀█ █▀█ █ █ +# █▀█ █ █ █ █▀█ █ ▀ █ █▄█ █▀▄ █▄█ + +# 🔒 Licensed under the GNU GPLv3 +# 🌐 https://www.gnu.org/licenses/agpl-3.0.html +# 👤 https://t.me/hikamoru + +# meta developer: @hikamorumods + +import re +import asyncio +import random +from aiohttp import web +from .. import utils, loader + + +class WebCreator: + def __init__(self, name, tg_link, preview_name): + self.url = None + self.app = web.Application() + self.app.router.add_get("/", self.index) + self.name = name + self.tg_link = tg_link + self.preview_name = preview_name + + async def index(self, request): + html_content = f""" + + + + + For {self.name} + + + + + + + + + +
+

Happy Birthday, {self.name}!

+

Dear {self.name},
+ On this special day, I wish you all the very best, all the joy you can ever have, and may you be blessed + abundantly today, tomorrow, and the days to come! May you have a fantastic birthday and many more to come... + HAPPY BIRTHDAY!!!!
+ With love, {self.preview_name} +

+
+ + + + + +""" + + return web.Response(text=html_content, content_type="text/html") + + async def open_tunnel(self, port): + ssh_command = f"ssh -o StrictHostKeyChecking=no -R 80:localhost:{port} nokey@localhost.run" + process = await asyncio.create_subprocess_shell( + ssh_command, + stdin=asyncio.subprocess.PIPE, + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE, + ) + + url = await self._extract_tunnel_url(process.stdout) + self.url = url or f"https://localhost:{port}" + return self.url + + async def _extract_tunnel_url(self, stdout): + event = asyncio.Event() + url = None + + async def read_output(): + nonlocal url + while True: + line = await stdout.readline() + if not line: + break + decoded_line = line.decode() + match = re.search(r"tunneled.*?(https:\/\/.+)", decoded_line) + if match: + url = match[1] + break + event.set() + + await read_output() + await event.wait() + return url + + +@loader.tds +class BirthdayWish(loader.Module): + """Share warmth with your loved ones and give them this website to make their birthdays even more special and joyful.""" + + strings = { + "name": "BirthdayWish", + "provide_name": "🤷‍♂️ Please provide a name", + "web_url": "🌐 URL: {} | Expires in {} seconds", + "expired": " Url Expired", + } + + strings_ru = { + "provide_name": "🤷‍♂️ Пожалуйста, укажите имя", + "web_url": "🌐 URL: {} | Истекает через {} секунд", + "expired": " Url истек", + } + + def __init__(self): + self.wishes = {} + + async def tunnel_handler(self, port): + + creator = WebCreator( + name=self.name, tg_link=self.tg_link, preview_name=self.preview_name + ) + + runner = web.AppRunner(creator.app) + await runner.setup() + + global site + site = web.TCPSite(runner, "127.0.0.1", port) + await site.start() + + url = await creator.open_tunnel(port) + return url, runner + + async def wishcmd(self, message): + """Create Birthday web wishes args: """ + + args = utils.get_args_raw(message).split(" ") + + if args[0] == "": + return await utils.answer(message, self.strings("provide_name")) + + text = args[0] + + expiration_time = int(args[1]) if len(args) > 1 else 20 + + me = await message.client.get_me() + + self.tg_link = f"https://t.me/{me.username}" or "https://t.me/Unknown" + self.preview_name = me.first_name + self.name = text + + port = random.randint(1000, 9999) + + url, runner = await self.tunnel_handler(port) + await utils.answer( + message, self.strings("web_url").format(url, expiration_time) + ) + + await asyncio.sleep(expiration_time) + + await site.stop() + await runner.cleanup() + + await utils.answer(message, self.strings("expired")) diff --git a/AmoreForever/amoremods/bull.py b/AmoreForever/amoremods/bull.py new file mode 100644 index 0000000..a8d68c1 --- /dev/null +++ b/AmoreForever/amoremods/bull.py @@ -0,0 +1,100 @@ +# █ █ █ █▄▀ ▄▀█ █▀▄▀█ █▀█ █▀█ █ █ +# █▀█ █ █ █ █▀█ █ ▀ █ █▄█ █▀▄ █▄█ + +# 🔒 Licensed under the GNU GPLv3 +# 🌐 https://www.gnu.org/licenses/agpl-3.0.html +# 👤 https://t.me/hikamoru + +# meta developer: @hikamorumods +# meta pic: https://te.legra.ph/file/7772a7dae6290f0a612a6.png +# meta banner: https://raw.githubusercontent.com/AmoreForever/assets/master/Bull.jpg + +import random +from .. import loader, utils +from ..inline.types import InlineCall +from ..inline.types import InlineQuery + + + +bullr = ( + "ТЫ ПОНИМАЕШЬ ЧТО Я ПИЗДАК В ТЕОЙ МАТРЕИ НА СВОЙ УЙ КАК МАКАРОНИНУ НАМОТАЛ БЛЯДЬ И НАЧАЛ РАСКРУЧИВАТЬ ЕЁ, ПОСЛЕ ЧЕГО ВЫКИНУЛ В КОСМОС, ЧТОБ ЕЁ ТАМ ИНОПЛАНЕТЯНЫ ХУЯМИ РВАЛИ?)", + "ТЫ ПОНИМАЕШЬ ЧТО Я ТВОЮ МАТЬ ОТПРАВИЛ СО СВОЕГО ЪХУЯ В НЕБО, ЧТОБ ОНА СВОИМ ПИЗДАКОМ ПРИНИМАЛА МИТЕОРИТНУЮ АТАКУ?)", + "ЕСЛИ ТЫ СЕЙЧАС ТАК И БУДЕШЬ ПРОДОЛЖАТЬ ПРОТИВОРЕЧИТЬ МОЕМУ ХУЮ, КАК ИМ КАК БЛЯДЬ НА НЛО ЗАХВОЧУ ТВОЁ ОЧКО И НАЧНУ ОПЫТЫ ПРОВОДИТЬ", + "ТЫ ПОНИМАЕШЬЧ ТО ВТОЯ МАТЬ МОЙ ХУЙ ЗАВЕРНУЛА В ПАКЕТИК ПОТОМУ ЧТО У ЭТОЙ БОМЖИХИ НЕБЫЛО ДЕНЕГ НА ПРЕЗИКИ, И ПОКЕТИК ПОРВАЛСЯ, И РОДИЛОСЬ ТАКОЕ ХУЙЛО КАК ТЫ", + "TЫ ПОНИМАЕШЬ ЧТО Я ТВОЮ МАТЬ СЛУЧАЙНО СВОИМ ХУЁМ СМЁЛ НАХУЙ СО СВОЕГО ПУТИ, И ОНА УЛЕТЕЛА НА РАДИУС ОБСТРЕЛА МОЕЙ ЗАЛУПЫ", + "АМЕБА ЕБАНАЯ СУКА) МАМАШКУ ТВОЮ ДЫРЯВИЛ ЧЕТ ) НАХУЙ ТВОЯ МАМАША КРИЧИТ КОГДА Я НАЧИНАЮ ЕБАТЬ ЕЕ)", + "АМЕБА ИЛИ ТЫ ОЛЕНЬ?) СЛЫШЬ ЕСЛИ ТЫ ПРОЛЬЕШЬ НА МОЙ ХУЙ СЛЕЗЫ , ТО ТЫ НЕ РАССЧИТЫВАЙ НА ТО , ЧТО ПОТОМ К ТЕБЕ ПРИДЕТ ФЕЯ И ПООБЕЩАЕТ ТЕБЕ ДОЛГУЮ И СЧАСТЛИВУЮ ЖИЗНЬ)", + "ОЛЕНЬ ТЫ ЕБАНЫЙ) МАТЬ ТВОЮ ЕБУ ЧЕТ ) ДАВАЙ ТЫ ЩАС ВОЗЬМЕШЬ МОЙ ХУЙ КАК ПЕРО И СЛОВНО КАК ПИСАТЕЛЬ СЕРЕБРЯНОГО ВЕКА НАПИШЕШЬ КАКОЙ НИБУДЬ РОМАН КОТОРЫЙ БУДЕТ ПО РАЗМЕРУ ПРИМЕРНО КАК МАСТЕР И МАРГАРИТА )", + "твоя мамка блядоебская кобыла и лезби", + "у тебя мать сраная шлюха", + "Я ТОВЮ МАМАШУ СВОИМ ХУЁМ РАСПЛЮЩИЛ, И ТЕПЕРЬ ОНА КАК ХОЯЧАЮ ПРУЖИТНКА БЛЯДЬ, ОТ СЕБЯ ВСЕ ХУИ ОТТАЛКИВАЕТ КРОМЕ МОЕГО, ДЛЯ МОЕГО ХУЯ ВСЕГДА ОТКРЫТ ДОСТУП В ЕЁ ПИЗДАК", + "ТЫ ПОНИМАЕШЬ ЧТО Я ТВОЮ МАТЬ БЛЯДЬ НАТЯНУЛ ПИЗДАКОМ НА ВЫСОКОВОЛЬТНУЮ ЭЛЕКТРО ВЫШКУ, И ОНА В СЕБЯ НАХУЙ ВЕСЬ ТОК ВТЯНУЛА, ТЕПЕРЬ ОНА БЛЯДЬ КАК ЭЛЕКТРО", + "Я КОГДА ВЫЕБАЛ ТВОЮ МАТЬ Я СВОЙ ХУЙ ПОСТАВИЛ К ЕЁ УХУ, ЧТОБ ОНА СЛЫШАЛА ПРИБОЯ СПЕРМЫ, А ПОТОМ ОНА ШИРОКО РАСКРЫЛА РОТ МЫ В ЕЁ ЕБЛЯТНИКЕ УСТРОИЛИ ОКЕАН", + "ТЫ ПОНИМАЕШЬ ЧТО Я В ПИЗДАК ТВОЕЙ МАТРЕИ СВОЙ ХУЙ ЗАСУНУ КАК БЛЯДЬ ШТТЕККЕР И ЕЁ ЗАРЯД ПОВЫСИЛСЯ КАК ОТ ЭНЕРГЕТИКА)", + "ТЫ ПОНИМАЕШЬ ЧТО ТВОЯ МАТЬ НА МОЁМ ХУЮ УСТРОИЛА БЛЯДЬ ТАНЦПОЛ, И НАЧАЛА СВОИМ ПОДРУГАМ ПРОДАВАТЬ НА НЕГО БИЛЕТЫ", + " ЕСЛИ ТЫ СЕЙЧАС НЕ НАЧНЁШЬ МНЕ ОТВЕЧАТЬ, Я ТЕБЕ НАХУЙ ХЁМ ПАЛЬЦЫ ПЕРЕЛОМАЮ, ОБРАЗИНА ТЫ ЕБАНАЯ)", + "ТЫ ПОНИМАЕШЬ ЧТО ВТОЯ МАМАШКА КАШЁЛКА ЕБАНАЯ, НА МОЙ ХУЙ ВЕШАЕТСЯ СВОИМ ПИЗДАКОМ КАК МАГНИТИК НА ХОЛОДИЛЬНИК, ПИДОПР ТЫ БЛЯДЬ ЕБАНЫЙ", + "ТЫ ПОНИМАЕШЬ ЧТО Я ТВОЕЙ МАТЕРИ ГОЛОВУ ХУЁМ КАК КОПЬЁМ ПРОБИЛ БЛЯДЬ И ЕЁ КУРИНЫЙ МОЗГ УМЕР НАХУЙ)) ИЗ-ЗА ЭТОГО ОНА ТЕБЯ ДАЖЕ И НЕ ВСПОМИНАЕТ)", + "ТЫ ПОНИМАЕШЬ ЧТО Я ВЫСТАВИЛ СВОЙ ХУЙ НА АВИТО, А ТВОЯ МАТЬ ПРОШЛА БЛЯДЬ БЕЗ ОЧЕРЕДИ И ККУПИЛА ЕГО, ОТДАВ СВОЮ ГНИЛУЮ ПОЧКУ?)", + "ТЫ ПОНИМАЕШЬ ЧТО ТВОЯ МАТЬ МОЙ ХУЙ НА НОЧЬ СЕБЕ В ПИЗДАК СУЁТ КАК ОБОГРЕВАТЕЛЬ НАХУЙ?)", + "ТЫ ПОНИМАЕШЬ ЧТО Я ПОКА ЧТО ЕБАЛ ТВОЮ МАМАШУ В СРАКУ, У НЕЁ ТАМ ЗАСОР СПЕРМЫ БЛЯДЬ ОБРАЗОВАЛСЯ И ЗАСОХ, ТЕПЕРЬ ОНА СРАТЬ НОРМАЛЬНО НЕ МОЖЕТ, ИДИ НАХУЙ СПАСАЙ ЭТУ ШЛЮХУ", + "ТЫ ПОНИМАЕШЬ ЧТО КОГДА Я ЕБУ ТВОЮ МАТЬ ЧТОБЫ ОНА НЕ ОРАЛА Я ЕЙ КЛЯП В РОТ СУЮ, НО ОДИН РАЗ СЛУЧИЛОСЬ ТАКОЕ, ЧТО КОГДА Я СВОИМ ХУЁМ ДАЛ ЕЙ ПО ПИЗДЕ ОНА ЭТОТ КЛЯП ПРОГЛАТИЛА, И НАЧАЛА ЗАДЫХАТЬСЯ, СПАСИ СВОЮ ШЛЮХА МАМКУ)", + "ТЫ ПОНИМАЕШЬ ЧТО МОЙ ХУЙ ВЗЛАМЫВАЕТ ОЧКО ТВОЕЙ МАТЕРИ КАК СЕЙФ НАХУЙ, И ОТ ТУДА НАЧИНАЮТ ВАЛИТЬСЯ САМОРОДКИ СПЕРМЫ?)", + "ТЫ ПОНИМАЕШЬ ЧТО Я В ПИЗДАКЕ ТВОЕЙ МАТЕРИ УСТРОИЛ ИЗВЕРЖЕНИЕ СВОЕГО ХУЯ НАХУЙ?", + "ТЫ ПОНИМАЕШЬ ЧТО Я ХУЁМ НАЧАЛ МОТАТЬ ПЕРЕД ТВОИМ ЕБАЛОИ И ТЕБЯ СЛУЧАЙНО НАХУЙ ЗАГИПНОТЕЗИРОВАЛ, И ТЫ ОБ ХУИ СТАЛ ГОЛОВОЙ БИТЬСЯ?)", + "ТЫ ПОНИМАЕШЬ ЧТО КОГДА Я ТВОЮ МАТЬ ОНА КАК ШЛЮХА ЛОЖИТСЯ НА СПИНКУ И НАЧИНАЕТ ПОСАСЫВАТЬ МОИ ЯЙЦА", + "ТЫ ПОНИМАЕШЬ ЧТО Я В ПИЗДАКЕ ТВОЕЙ МАТЕРИ ИЗ ЕЁ КЛИТОРНЫЙ СТЕН ВЫРЕЗАЮ РАКЕТНИЦУ СВОИМ ХУЁМ?", + "Я СЕЙЧАС СВОЕЙ СПЕРМОЙ ОБОЛЬЮ ТВОЙ ПИЗДАК КАК КЕРАСИНОМ, И ПУЩУ НА НЕГО ИСКРУ, ДОБЫТАЯ КОТОРАЯ БУДЕТ О ТВОИ ГНИЛЫЕ ЗУБКИ, И ТЫ СГОРИШЬ НАХУЙ)", + "ТЫ ПОНИМАЕШЬ ЧТО ТЫ ОТ МОЕЙ ЗАЛУПЫ ПРЯЧШЬСЯ В ПИЗДАКЕ СВОЕЙ МАТЕРИ КАК НАХУЙ В БУНКЕРЕ, А Я СВОИМ ХУЁМ ЕГО НА СКВОЗЬ ПРОШИЛ И ТЕБЕ В ЕБАЛО ПОПАЛ)) ", + "ТЫ ПОНИМАЕШЬ ЧТО Я ХУЁМ СТАЛ КАТАЛИЗИРОВАТЬ ПИЗДАК ТВОЕЙ МАТЕРИ НА РАЗДВИЖЕНИЕ ЕЁ ЖИРНЫХ НОГ?)", + ) + +def bullme(): + iwfy = random.choice(bullr) + return iwfy + +@loader.tds +class BullMod(loader.Module): + """Bull пиз#а собеседнику""" + + strings = {"name": "BullMod"} + + @loader.inline_everyone + async def bull_inline_handler(self, query: InlineQuery): + """Забулить кого-то жесткими матами про мать""" + aoa = bullme() + + btn_a = [{"text": "🌀 Random", "callback": self.bulls}] + + return { + "title": "Пошутить про маму", + "thumb": "https://te.legra.ph/file/b2a6c8d20e0034a534ac4.jpg", + "description": "Отправить...", + "message": f"{aoa}", + "reply_markup": btn_a, + } + + async def bullcmd(self, message): + """Забулить кого-то жесткими матами про мать""" + aoa = bullme() + await utils.answer(message, aoa) + + async def bullicmd(self, message): + """Забулить кого-то жесткими матами про мать (inline)""" + aoa = bullme() + await self.inline.form( + message=message, + text=aoa, + reply_markup=[ + [{"text": "🌀 Random", "callback": self.bulls}], + ] + ) + + async def bulls(self, call: InlineCall): + aoa = bullme() + await call.edit( + text=aoa, + reply_markup=[ + [{"text": "🌀 Random", "callback": self.bulls}], + ] + ) diff --git a/AmoreForever/amoremods/createlinks.py b/AmoreForever/amoremods/createlinks.py new file mode 100644 index 0000000..936bc52 --- /dev/null +++ b/AmoreForever/amoremods/createlinks.py @@ -0,0 +1,158 @@ +# █ █ █ █▄▀ ▄▀█ █▀▄▀█ █▀█ █▀█ █ █ +# █▀█ █ █ █ █▀█ █ ▀ █ █▄█ █▀▄ █▄█ + +# 🔒 Licensed under the GNU GPLv3 +# 🌐 https://www.gnu.org/licenses/agpl-3.0.html +# 👤 https://t.me/hikamoru + +# meta developer: @hikamorumods +# meta pic: https://te.legra.ph/file/388e1b26a46a8c439e479.png +# meta banner: https://raw.githubusercontent.com/AmoreForever/assets/master/Createlinks.jpg + + +from .. import loader, utils, security + + + +@loader.tds +class AmorelinksMod(loader.Module): + """Create links""" + + strings = { + "name": "AmoreLinks", + "youtube": "🫂 YouTube link special for you.\n\n", + "google": "🫂 Google link special for you.\n\n", + "github": "🫂 Github link special for you.\n\n", + "pornhub": "🫂 Pornhub link special for you.\n\n", + "telegram": "🫂 Telegram link special for you.\n\n", + "4pda": "🫂 4pda link special for you.\n\n", + + } + + async def ytcmd(self, message): + """ create YouTube link""" + text = utils.get_args_raw(message) + s = f"✏ Input word: {text}" + if await self.allmodules.check_security( + message, + security.OWNER | security.SUDO, + ): + + try: + await self.inline.form( + self.strings("youtube", message) + s, + reply_markup=[ + [{"text": "♨️ Link", "url": f"https://m.youtube.com/results?sp=mAEA&search_query={text}"}], + [{"text": "🔻 Close", "action": f"close"}], + + ], + message=message, + ) + except Exception: + await utils.answer(message, self.strings("join", message)) + + + async def gugcmd(self, message): + """ create Google link""" + text = utils.get_args_raw(message) + s = f"✏ Input word: {text}" + if await self.allmodules.check_security( + message, + security.OWNER | security.SUDO, + ): + + try: + await self.inline.form( + self.strings("google", message) + s, + reply_markup=[ + [{"text": "🛰 Link", "url": f"https://www.google.com/search?q={text}"}], + [{"text": "🔻 Close", "action": f"close"}], + ], + message=message, + ) + except Exception: + await utils.answer(message, self.strings("join", message)) + + async def ghcmd(self, message): + """ create Github link""" + text = utils.get_args_raw(message) + s = f"✏ Input word: {text}" + if await self.allmodules.check_security( + message, + security.OWNER | security.SUDO, + ): + + try: + await self.inline.form( + self.strings("github", message) + s, + reply_markup=[ + [{"text": "🛰 Link", "url": f"https://github.com/search?q={text}"}], + [{"text": "🔻 Close", "action": f"close"}], + ], + message=message, + ) + except Exception: + await utils.answer(message, self.strings("join", message)) + + async def phcmd(self, message): + """ create PornHub link""" + text = utils.get_args_raw(message) + s = f"✏ Input word: {text}" + if await self.allmodules.check_security( + message, + security.OWNER | security.SUDO, + ): + + try: + await self.inline.form( + self.strings("pornhub", message) + s, + reply_markup=[ + [{"text": "🛰 Link", "url": f"https://rt.pornhub.com/video/search?search={text}"}], + [{"text": "🔻 Close", "action": f"close"}], + ], + message=message, + ) + except Exception: + await utils.answer(message, self.strings("join", message)) + + async def tgcmd(self, message): + """ create Telegram link""" + text = utils.get_args_raw(message) + s = f"✏ Input word: {text}" + if await self.allmodules.check_security( + message, + security.OWNER | security.SUDO, + ): + + try: + await self.inline.form( + self.strings("telegram", message) + s, + reply_markup=[ + [{"text": "🛰 Link", "url": f"tg://search?query={text}"}], + [{"text": "🔻 Close", "action": f"close"}], + ], + message=message, + ) + except Exception: + await utils.answer(message, self.strings("join", message)) + + async def pdacmd(self, message): + """ create 4pda link""" + text = utils.get_args_raw(message) + s = f"✏ Input word: {text}" + if await self.allmodules.check_security( + message, + security.OWNER | security.SUDO, + ): + + try: + await self.inline.form( + self.strings("4pda", message) + s, + reply_markup=[ + [{"text": "🛰 Link", "url": f"https://4pda.to/forum/index.php?act=search&source=all&forums=316&subforums=1&query={text}"}], + [{"text": "🔻 Close", "action": f"close"}], + ], + message=message, + ) + except Exception: + await utils.answer(message, self.strings("join", message)) diff --git a/AmoreForever/amoremods/dtwr.py b/AmoreForever/amoremods/dtwr.py new file mode 100644 index 0000000..28b1142 --- /dev/null +++ b/AmoreForever/amoremods/dtwr.py @@ -0,0 +1,73 @@ +# █ █ █ █▄▀ ▄▀█ █▀▄▀█ █▀█ █▀█ █ █ +# █▀█ █ █ █ █▀█ █ ▀ █ █▄█ █▀▄ █▄█ + +# 🔒 Licensed under the GNU GPLv3 +# 🌐 https://www.gnu.org/licenses/agpl-3.0.html +# 👤 https://t.me/hikamoru + +# meta developer: @hikamorumods +# meta banner: https://raw.githubusercontent.com/AmoreForever/assets/master/DTWR.jpg + +from .. import loader, utils +from telethon.tl.types import Message + + +@loader.tds +class DTWRMod(loader.Module): + """Module Don't tag wihout reason""" + + strings = { + "name": "DTWR", + "text": "Your custom text", + "username": "Input you username without '@'", + } + + strings_ru = { + "text": "Кастомный текст", + "username": "Введи свой юзернэйм без '@'", + } + + strings_uz = { + "text": "Kastom text", + "username": "Usernameingizni kiriting, '@' siz" + } + + def __init__(self): + self.config = loader.ModuleConfig( + loader.ConfigValue( + "Username", + "username", + doc=lambda: self.strings("username"), + ), + loader.ConfigValue( + "custom_text", + "😫 Please don't tag me without reason", + doc=lambda: self.strings("text"), + ), + ) + + @loader.command(ru_docs="Конфиг этого модуля") + async def cfgdtwrcmd(self, message): + """This module config""" + name = self.strings("name") + await self.allmodules.commands["config"]( + await utils.answer(message, f"{self.get_prefix()}config {name}") + ) + + @loader.tag("only_messages", "only_groups", "in") + async def watcher(self, message: Message): + + reply = await message.get_reply_message() + + tag = self.config['Username'] + if tag.startswith('@') is False: + tag = f"@{tag}" + + if reply: + return False + if message.text.lower() == tag: + await message.reply(self.config["custom_text"]) + await self._client.send_read_acknowledge( + message.chat_id, + clear_mentions=True, + ) diff --git a/AmoreForever/amoremods/facts.py b/AmoreForever/amoremods/facts.py new file mode 100644 index 0000000..2fdfd6f --- /dev/null +++ b/AmoreForever/amoremods/facts.py @@ -0,0 +1,48 @@ +# █ █ █ █▄▀ ▄▀█ █▀▄▀█ █▀█ █▀█ █ █ +# █▀█ █ █ █ █▀█ █ ▀ █ █▄█ █▀▄ █▄█ + +# 🔒 Licensed under the GNU GPLv3 +# 🌐 https://www.gnu.org/licenses/agpl-3.0.html +# 👤 https://t.me/hikamoru + +# meta developer: @hikamorumods +# meta banner: https://raw.githubusercontent.com/AmoreForever/assets/master/Facts.jpg +# channel @facti_p + +from .. import loader, utils +from telethon import functions +from asyncio import sleep +import random +import datetime +chat = "@faktiru" + + +class FactsMod(loader.Module): + """More Interesting Facts""" + + strings = { + "name": "Facts", + "wait": "💡 Searching..." + } + + strings_ru = { + "wait": "💡 Поиск..." + } + + @loader.command(ru_docs="Интересные Факты") + async def afactscmd(self, message): + """Intersting Facts""" + reply = await message.get_reply_message() + await utils.answer(message, self.strings["wait"]) + result = await message.client( + functions.messages.GetHistoryRequest( + peer=chat, offset_id=0, offset_date=datetime.datetime.now(), add_offset=random.randint(0, 1000), limit=1, max_id=0, min_id=0, hash=0, + ) + ) + await sleep(0.30) + await message.delete() + await message.client.send_message( + message.to_id, + result.messages[0], + reply_to=reply.id if reply else None, + ) diff --git a/AmoreForever/amoremods/figlet.py b/AmoreForever/amoremods/figlet.py new file mode 100644 index 0000000..22eed45 --- /dev/null +++ b/AmoreForever/amoremods/figlet.py @@ -0,0 +1,67 @@ +# █ █ █ █▄▀ ▄▀█ █▀▄▀█ █▀█ █▀█ █ █ +# █▀█ █ █ █ █▀█ █ ▀ █ █▄█ █▀▄ █▄█ + +# 🔒 Licensed under the GNU GPLv3 +# 🌐 https://www.gnu.org/licenses/agpl-3.0.html +# 👤 https://t.me/hikamoru + +# meta developer: @hikamorumods +# meta banner: https://github.com/AmoreForever/assets/blob/master/Figlet.jpg?raw=true + + + +import pyfiglet +import functools +from .. import loader, utils + + +class Figlet(loader.Module): + """Creates Figlet Text""" + + strings = {"name": "Figlet"} + style_to_font = { + "slant": "slant", + "3d": "3-d", + "5line": "5lineoblique", + "alpha": "alphabet", + "banner": "banner3-D", + "doh": "doh", + "iso": "isometric1", + "letter": "letters", + "allig": "alligator", + "dotm": "dotmatrix", + "bubble": "bubble", + "bulb": "bulbhead", + "digi": "digital" + } + + @loader.command() + async def listfig(self, message): + """List of figlet styles""" + keys_list = " , ".join(list(self.style_to_font.keys())) + await utils.answer(message, f"🚩 Available styles: {keys_list}") + + @loader.command() + async def figlet(self, message): + """Create figlet text, + + +

^deck_name^

+
+ Loading... +
+
+

+ +
+
Start test
+ + + + + """ + + +@loader.tds +class FlashCardsMod(loader.Module): + """Flash cards for learning""" + + strings = { + "name": "FlashCards", + "deck_not_found": "🚫 Deck not foundYou haven't provided deck name", + "deck_created": "#Deck #{} {} successfully created!", + "deck_removed": "🚫 Deck removed", + "save_deck_no_reply": ( + "🚫 This command should be used in reply to message with deck items." + ), + "deck_saved": "✅ Deck saved!", + "generating_page": "⚙️ Generating page, please wait ...", + "offline_testing": "📖 Offline testing, based on deck {}", + } + + strings_ru = { + "deck_not_found": "🚫 Дека не найденаТы не указал имя деки", + "deck_created": "#Deck #{} {} успешно создана!", + "deck_removed": "🚫 Дека удалена", + "save_deck_no_reply": ( + "🚫 Эта команда должна выполняться в ответ на измененную деку." + ), + "deck_saved": "✅ Дека сохранена!", + "generating_page": "⚙️ Генерирую страницу, секунду...", + "offline_testing": "📖 Оффлайн тестирование на основе деки {}", + "_cmd_doc_newdeck": " - Создать новую деку", + "_cmd_doc_decks": "Показать деки", + "_cmd_doc_deletedeck": " - Удалить деку", + "_cmd_doc_listdeck": " - Показать деку", + "_cmd_doc_editdeck": " - Редактировать деку", + "_cmd_doc_savedeck": " - Сохранить деку", + "_cmd_doc_htmldeck": " - Сгенерировать оффлайн-тестирование по деке", + "_cls_doc": "Флеш-карты для обучения", + } + + strings_de = { + "deck_not_found": "🚫 Deck nicht gefundenDu hast keinen Decknamen angegeben", + "deck_created": "#Deck #{} {} erfolgreich erstellt!", + "deck_removed": "🚫 Deck entfernt", + "save_deck_no_reply": ( + "🚫 Dieser Befehl sollte in Antwort auf eine Nachricht mit" + " Deck-Elementen" + " verwendet werden." + ), + "deck_saved": "✅ Deck gespeichert!", + "generating_page": "⚙️ Seite wird generiert, bitte warten ...", + "offline_testing": "📖 Offline-Testing basierend auf dem Deck {}", + "_cmd_doc_newdeck": " - Erstelle ein neues Deck", + "_cmd_doc_decks": "Zeige Decks", + "_cmd_doc_deletedeck": " - Deck löschen", + "_cmd_doc_listdeck": " - Deck anzeigen", + "_cmd_doc_editdeck": " - Deck bearbeiten", + "_cmd_doc_savedeck": " - Deck speichern", + "_cmd_doc_htmldeck": " - Offline-Testing basierend auf dem Deck", + "_cls_doc": "Flash-Karten für das Lernen", + } + + strings_tr = { + "deck_not_found": "🚫 Deck bulunamadıDeck adı belirtmedin", + "deck_created": "#Deck #{} {} başarıyla oluşturuldu!", + "deck_removed": "🚫 Deck kaldırıldı", + "save_deck_no_reply": "🚫 Bu komut, deck öğeleriyle yanıtlanmalıdır.", + "deck_saved": "✅ Deck kaydedildi!", + "generating_page": "⚙️ Sayfa oluşturuluyor, lütfen bekleyin ...", + "offline_testing": "📖 {} deckine dayalı çevrimdışı test", + "_cmd_doc_newdeck": " - Yeni bir deck oluştur", + "_cmd_doc_decks": "Deckleri göster", + "_cmd_doc_deletedeck": " - Deck sil", + "_cmd_doc_listdeck": " - Decki göster", + "_cmd_doc_editdeck": " - Decki düzenle", + "_cmd_doc_savedeck": " - Decki kaydet", + "_cmd_doc_htmldeck": " - Decke dayalı çevrimdışı test oluştur", + "_cls_doc": "Öğrenmek için flaş kartlar", + } + + strings_hi = { + "deck_not_found": "🚫 डेक नहीं मिलाआपने डेक का नाम नहीं दिया", + "deck_created": "#Deck #{} {} सफलतापूर्वक बनाया गया!", + "deck_removed": "🚫 डेक हटा दिया गया", + "save_deck_no_reply": ( + "🚫 यह कमांड डेक आइटम के साथ उत्तर देने के लिए उपयोग किया जाना चाहिए।" + ), + "deck_saved": "✅ डेक सहेज लिया गया!", + "generating_page": "⚙️ पेज उत्पन्न किया जा रहा है, कृपया प्रतीक्षा करें ...", + "offline_testing": "📖 {} डेक पर आधारित ऑफ़लाइन परीक्षण", + "_cmd_doc_newdeck": "<नाम> - एक नया डेक बनाएं", + "_cmd_doc_decks": "डेक दिखाएं", + "_cmd_doc_deletedeck": "<आईडी> - डेक हटाएं", + "_cmd_doc_listdeck": "<आईडी> - डेक दिखाएं", + "_cmd_doc_editdeck": "<आईडी> - डेक संपादित करें", + "_cmd_doc_savedeck": "<उत्तर> - डेक सहेजें", + "_cmd_doc_htmldeck": "<आईडी> - डेक पर आधारित ऑफ़लाइन परीक्षण बनाएं", + "_cls_doc": "फ्लैश कार्ड अध्ययन के लिए", + } + + strings_uz = { + "deck_not_found": "🚫 Deck topilmadiDeck nomini kiritmadingiz", + "deck_created": "#Deck #{} {} muvaffaqiyatli yaratildi!", + "deck_removed": "🚫 Deck o'chirildi", + "save_deck_no_reply": ( + "🚫 Bu buyruq deck elementlari bilan javob berilishi kerak." + ), + "deck_saved": "✅ Deck saqlandi!", + "generating_page": "⚙️ Sahifa yaratilmoqda, iltimos kuting ...", + "offline_testing": "📖 {} deckiga asoslangan oflayn test", + "_cmd_doc_newdeck": " - Yangi deck yaratish", + "_cmd_doc_decks": "Decklarni ko'rsatish", + "_cmd_doc_deletedeck": " - Deckni o'chirish", + "_cmd_doc_listdeck": " - Deckni ko'rsatish", + "_cmd_doc_editdeck": " - Deckni tahrirlash", + "_cmd_doc_savedeck": " - Deckni saqlash", + "_cmd_doc_htmldeck": " - Deckiga asoslangan oflayn test yaratish", + "_cls_doc": "O'rganish uchun flash kartalar", + } + + async def client_ready(self): + self.decks = self.get("decks", {}) + + def get_deck_from_reply(self, reply, limit=None): + if reply is None: + return False + + if "#Deck" in reply.text: + counter = 1 + + for line in reply.text.split("\n"): + line = line.split() + if len(line) > 1: + deck = ( + line[1] + .replace("", "") + .replace("", "") + .replace("#", "") + ) + try: + int(deck) + except Exception: + pass + + if deck in self.decks: + if ( + limit is None + or not limit + and "#Decks" not in reply.text + or counter == limit + ): + return deck + else: + counter += 1 + + return False + + async def get_from_message(self, message: Message): + args = utils.get_args_raw(message) + try: + args = args.split()[0] + except Exception: + pass + + if args.startswith("#"): + args = args[1:] + + try: + int_args = int(args) + except Exception: + args = False + int_args = False + + if int(int_args) < 1000: + args = self.get_deck_from_reply(await message.get_reply_message(), int_args) + + if not args or args not in self.decks: + await utils.answer(message, self.strings("deck_not_found")) + await asyncio.sleep(2) + await message.delete() + return False + + return args + + async def newdeckcmd(self, message: Message): + """ - New deck of cards""" + + args = utils.get_args_raw(message) + if args == "": + await utils.answer(message, self.strings("no_deck_name")) + await asyncio.sleep(2) + await message.delete() + return + + random_id = str(randint(10000, 99999)) + + self.decks[random_id] = {"name": args, "cards": [("sample", "sample")]} + + self.set("decks", self.decks) + await utils.answer( + message, + self.strings("deck_created").format(random_id, args), + ) + + async def deckscmd(self, message: Message): + """List decks""" + res = "#Decks:\n\n" + for counter, (item_id, item) in enumerate(self.decks.items(), start=1): + if len(item["cards"]) == 0: + items = "No cards" + else: + items = "".join( + f"\n {front} - {back}" for front, back in item["cards"][:2] + ) + if len(item["cards"]) > 2: + items += "\n <...>" + res += ( + f"🔸{counter}. {item_id} |" + f" {item['name']}{items}\n\n" + ) + await utils.answer(message, res) + + async def deletedeckcmd(self, message: Message): + """ - Delete deck""" + deck_id = await self.get_from_message(message) + if not deck_id: + return + + del self.decks[deck_id] + self.set("decks", self.decks) + reply = await message.get_reply_message() + if reply: + if "#Decks" in reply.text: + await self.deckscmd(reply) + elif "#Deck" in reply.text: + await reply.edit(reply.text + "\n" + self.strings("deck_removed")) + await utils.answer(message, self.strings("deck_removed")) + + async def listdeckcmd(self, message: Message): + """ - List deck items""" + deck_id = await self.get_from_message(message) + if not deck_id: + return + + deck = self.decks[deck_id] + res = f"📋#Deck #{deck_id} {deck['name']}:\n➖➖➖➖➖➖➖➖➖➖" + for i, (front, back) in enumerate(deck["cards"], start=1): + res += f"\n{i}. {front} - {back}" + await utils.answer(message, res) + + async def editdeckcmd(self, message: Message): + """ - Edit deck items""" + deck_id = await self.get_from_message(message) + if not deck_id: + return + + deck = self.decks[deck_id] + res = f"📋#Deck #{deck_id} \"{deck['name']}\":\n➖➖➖➖➖➖➖➖➖➖" + for front, back in deck["cards"]: + res += f"\n{front} - {back}" + + res += ( + "\n➖➖➖➖➖➖➖➖➖➖\nEdit and type .savedeck in reply to" + " this" + " message\nNote: you can edit title and cards, but other message should" + " stay untouched, otherwise it can be saved incorrectly! #Editing" + ) + + await utils.answer(message, res) + + def remove_html(self, text): + return re.sub(r"<.*?>", "", text) + + async def savedeckcmd(self, message: Message): + """ - Save deck. Do not use if you don't know what is this""" + reply = await message.get_reply_message() + if not reply or "#Editing" not in reply.text: + await utils.answer(message, self.strings("save_deck_no_reply")) + await asyncio.sleep(2) + await message.delete() + return False + + deck_id = await self.get_from_message(message) + if not deck_id: + return + + deck = self.decks[deck_id] + self.decks[deck_id]["cards"] = [] + items = reply.text.split("\n") + for item in items[2:-3]: + self.decks[deck_id]["cards"].append( + ( + self.remove_html(item.split(" - ")[0]), + self.remove_html(item.split(" - ")[1]), + ) + ) + + try: + self.decks[deck_id]["name"] = self.remove_html( + re.search(r""(.+?)"", items[0]).group(1) + ) + except Exception: + pass + + self.set("decks", self.decks) + + res = f"📋#Deck #{deck_id} {deck['name']}:\n➖➖➖➖➖➖➖➖➖➖" + for i, (front, back) in enumerate(deck["cards"], start=1): + res += f"\n{i}. {front} - {back}" + res += "\n➖➖➖➖➖➖➖➖➖➖\n" + self.strings("deck_saved") + + await utils.answer(reply, res) + await message.delete() + + async def htmldeckcmd(self, message: Message): + """ - Generates the page with specified deck""" + deck_id = await self.get_from_message(message) + if not deck_id: + return + + deck = self.decks[deck_id] + await utils.answer(message, self.strings("generating_page")) + deck_name = deck["name"] + loc_cards = deck["cards"].copy() + cards = dict(loc_cards) + json_cards = json.dumps(cards).replace('"', '\\"') + txt = io.BytesIO( + TEMPLATE.replace("^title_deck_name^", deck_name) + .replace("^deck_name^", deck_name) + .replace("^json_cards^", json_cards) + .encode("utf-8") + ) + txt.name = "testing.html" + await message.delete() + await message.client.send_file( + message.to_id, + txt, + caption=self.strings("offline_testing").format(deck_name), + ) diff --git a/hikariatama/ftg/forbid_joins.py b/hikariatama/ftg/forbid_joins.py new file mode 100644 index 0000000..7396007 --- /dev/null +++ b/hikariatama/ftg/forbid_joins.py @@ -0,0 +1,58 @@ +# █ █ ▀ █▄▀ ▄▀█ █▀█ ▀ +# █▀█ █ █ █ █▀█ █▀▄ █ +# © Copyright 2022 +# https://t.me/hikariatama +# +# 🔒 Licensed under the GNU AGPLv3 +# 🌐 https://www.gnu.org/licenses/agpl-3.0.html + +# meta pic: https://static.dan.tatar/forbid_joins_icon.png +# meta banner: https://mods.hikariatama.ru/badges/forbid_joins.jpg +# meta developer: @hikarimods +# scope: hikka_only +# scope: hikka_min 1.2.10 + +from .. import loader + + +@loader.tds +class ForbidJoinMod(loader.Module): + """Tired of trojans in modules, which join channels? Load this module!""" + + strings = { + "name": "ForbidJoin", + "welcome": ( + "⚔️ Unit «LAMBDA» will protect you from pesky" + " JoinChannelRequest\n\nAll you need is to keep this" + " module installed!\n\nIf any developer tries to bypass this" + " protection, he will be added to SCAM modules list.\n\n⚠️" + " Protection will be activated after you restart userbot!" + ), + } + + strings_ru = { + "welcome": ( + "⚔️ Юнит «LAMBDA» будет защищать тебя от надоедливых" + " JoinChannelRequest\n\nВсе, что требуется - держать" + " этот модуль установленным!\n\nЕсли какой-либо разработчик" + " попытается обойти эту защиту, он будет добавлен в список SCAM" + " модулей.\n\n⚠️ Защита станет активной только после" + " перезагрузки!" + ), + } + + async def on_dlmod(self, client, db): + await self.inline.bot.send_photo( + client._tg_id, + "https://github.com/hikariatama/assets/raw/master/unit_lambda.png", + caption=self.strings("welcome"), + ) + + +# ⚠️⚠️ WARNING! ⚠️⚠️ +# If you are a module developer, and you'll try to bypass this protection to +# force user join your channel, you will be added to SCAM modules +# list and you will be banned from Hikka federation. +# Let USER decide, which channel he will follow. Do not be so petty +# I hope, you understood me. +# Thank you diff --git a/hikariatama/ftg/forex_wss.py b/hikariatama/ftg/forex_wss.py new file mode 100644 index 0000000..d8a2bbf --- /dev/null +++ b/hikariatama/ftg/forex_wss.py @@ -0,0 +1,169 @@ +# █ █ ▀ █▄▀ ▄▀█ █▀█ ▀ +# █▀█ █ █ █ █▀█ █▀▄ █ +# © Copyright 2022 +# https://t.me/hikariatama +# +# 🔒 Licensed under the GNU AGPLv3 +# 🌐 https://www.gnu.org/licenses/agpl-3.0.html + +# meta pic: https://static.dan.tatar/forex_wss.png +# meta banner: https://mods.hikariatama.ru/badges/forex_wss.jpg +# meta developer: @hikarimods +# requires: websockets +# scope: inline +# scope: hikka_only +# scope: hikka_min 1.2.10 + +import asyncio +import datetime +import json +import time +from urllib.parse import quote_plus + +import requests +import websockets +from aiogram.utils.exceptions import MessageNotModified +from telethon.tl.types import Message + +from .. import loader, utils +from ..inline.types import InlineCall + + +@loader.tds +class RealTimeValutesMod(loader.Module): + """Track valutes in real time. Updates more than once a second""" + + strings = { + "name": "RealTimeValutes", + "loading": "😌 Loading the most actual info from Forex...", + "wss_error": "🚫 Socket connection error", + "exchanges": ( + "😌 Exchange rates by Forex\n\n💵 1 USD = {:.2f} RUB\n💶 1 EUR =" + " {:.2f} RUB\n\nThis info is relevant to {:%m/%d/%Y" + " %H:%M:%S}" + ), + } + + strings_ru = { + "loading": "😌 Загружаю информацию с Forex...", + "wss_error": "🚫 Ошибка подеключения к сокету", + "exchanges": ( + "😌 Курсы валют Forex\n\n💵 1 USD = {:.2f} RUB\n💶 1 EUR = {:.2f}" + " RUB\n\nИнформация актуальна на {:%m/%d/%Y %H:%M:%S}" + ), + "_cmd_doc_val": "Показать курсы валют", + "_cls_doc": ( + "Отслеживает курсы валют в режиме реального времени. Обновляется несколько" + " раз в секунду" + ), + } + + async def _connect(self): + r = await utils.run_sync( + requests.get, + ( + f"https://rates-live.efxnow.com/signalr/negotiate?clientProtocol=2.1&connectionData=%5B%7B%22name%22%3A%22ratesstreamer%22%7D%5D&_={time.time() * 1000:.0f}" + ), + ) + + token = quote_plus(r.json()["ConnectionToken"]) + base = f"wss://rates-live.efxnow.com/signalr/connect?transport=webSockets&clientProtocol=2.1&connectionToken={token}&connectionData=%5B%7B%22name%22%3A%22ratesstreamer%22%7D%5D&tid=8" + + async with websockets.connect(base) as wss: + await wss.send( + '{"H":"ratesstreamer","M":"SubscribeToPricesUpdates","A":[["401203106","401203109"]],"I":8}' + ) # USD/RUB | EUR/RUB + + self._restart_at = time.time() + 5 * 60 + + while time.time() < self._restart_at: + rates = json.loads(await wss.recv()) + if "M" not in rates or not rates["M"]: + continue + + for row in rates["M"]: + if "A" not in row: + continue + + rate = row["A"] + valute = rate[0].split("|")[1].split("/")[0] + rate = float(rate[0].split("|")[3]) + + self._rates[valute] = rate + self._upd_time = time.time() + + return await self._connect() + + async def client_ready(self, client, db): + self._rates = {} + self._upd_time = 0 + + self._ratelimit = 0 + + self._reload_markup = self.inline.generate_markup( + {"text": "🔄 Update", "data": "update_exchanges"} + ) + + self._task = asyncio.ensure_future(self._connect()) + + async def valcmd(self, message: Message): + """Show exchange rates""" + try: + m = self.strings("exchanges").format( + self._rates["USD"], + self._rates["EUR"], + getattr(datetime, "datetime", datetime).fromtimestamp(self._upd_time), + ) + except (KeyError, IndexError): + await utils.answer(message, self.strings("wss_error")) + return + + try: + await self.inline.form( + m, + message=message, + reply_markup={"text": "🔄 Update", "data": "update_exchanges"}, + disable_security=True, + silent=True, + ) + except Exception: + await utils.answer(message, m) + + @loader.inline_everyone + async def reload_callback_handler(self, call: InlineCall): + """Processes 'reload' button clicks""" + if call.data != "update_exchanges": + return + + if self._ratelimit and time.time() < self._ratelimit: + await call.answer("Do not spam this button") + return + + self._ratelimit = time.time() + 1 + + try: + await self.inline.bot.edit_message_text( + inline_message_id=call.inline_message_id, + text=self.strings("exchanges").format( + self._rates["USD"], + self._rates["EUR"], + getattr(datetime, "datetime", datetime).fromtimestamp( + self._upd_time + ), + ), + reply_markup=self._reload_markup, + parse_mode="HTML", + ) + + await call.answer("😌 Exchange rates update complete!", show_alert=True) + except (IndexError, KeyError): + await call.answer("Socket connection error", show_alert=True) + return + except MessageNotModified: + await call.answer( + "Exchange rates have not changes since last update", show_alert=True + ) + return + + async def on_unload(self): + self._task.cancel() diff --git a/hikariatama/ftg/fuck_tags.py b/hikariatama/ftg/fuck_tags.py new file mode 100644 index 0000000..94698f4 --- /dev/null +++ b/hikariatama/ftg/fuck_tags.py @@ -0,0 +1,139 @@ +# █ █ ▀ █▄▀ ▄▀█ █▀█ ▀ +# █▀█ █ █ █ █▀█ █▀▄ █ +# © Copyright 2022 +# https://t.me/hikariatama +# +# 🔒 Licensed under the GNU AGPLv3 +# 🌐 https://www.gnu.org/licenses/agpl-3.0.html + +# scope: hikka_min 1.2.10 + +# meta pic: https://img.icons8.com/emoji/256/000000/middle-finger-light-skin-tone.png +# meta banner: https://mods.hikariatama.ru/badges/fuck_tags.jpg +# meta developer: @hikarimods +# scope: hikka_only + +import asyncio + +from telethon.tl.types import Message + +from .. import loader, utils + + +@loader.tds +class FuckTagsMod(loader.Module): + """Auto-read tags and messages in selected chats""" + + strings = { + "name": "FuckTags", + "args": "🚫 Incorrect args specified", + "on": "✅ Now I ignore tags in this chat", + "off": "✅ Now I don't ignore tags in this chat", + "on_strict": "✅ Now I automatically read messages in this chat", + "off_strict": "✅ Now I don't automatically read messages in this chat", + "do_not_tag_me": "🦊 Please, do not tag me.", + } + + strings_ru = { + "args": "🚫 Указаны неверные аргументы", + "on": "✅ Теперь я буду игнорировать теги в этом чате", + "off": "✅ Теперь я не буду игнорировать теги в этом чате", + "on_strict": ( + "✅ Теперь я буду автоматически читать сообщения в этом чате" + ), + "off_strict": ( + "✅ Теперь я не буду автоматически читать сообщения в этом чате" + ), + "do_not_tag_me": "🦊 Пожалуйста, не тегайте меня.", + "_cmd_doc_fucktags": "[чат] - Включить\\выключить тихие теги", + "_cmd_doc_fuckall": "[чат] - Включить\\выключить авточтение", + "_cmd_doc_fuckchats": "Показать активные авточтения в чатах", + "_cls_doc": "Автоматически читает теги в выбранных чатах", + } + + async def client_ready(self, client, db): + self._ratelimit = [] + + async def fucktagscmd(self, message: Message): + """[chat] - Toggle notags""" + args = utils.get_args_raw(message) + try: + try: + args = int(args) + except Exception: + pass + cid = (await self._client.get_entity(args)).id + except Exception: + cid = utils.get_chat_id(message) + + self._ratelimit = list(set(self._ratelimit) - set([cid])) + + if cid not in self.get("tags", []): + self.set("tags", self.get("tags", []) + [cid]) + await utils.answer(message, self.strings("on")) + else: + self.set( + "tags", + list(set(self.get("tags", [])) - set([cid])), + ) + await utils.answer(message, self.strings("off")) + + async def fuckallcmd(self, message: Message): + """[chat] - Toggle autoread""" + args = utils.get_args_raw(message) + try: + if str(args).isdigit(): + args = int(args) + cid = (await self._client.get_entity(args)).id + except Exception: + cid = utils.get_chat_id(message) + + if cid not in self.get("strict", []): + self.set("strict", self.get("strict", []) + [cid]) + await utils.answer(message, self.strings("on_strict")) + return + + self.set( + "strict", + list(set(self.get("strict", [])) - set([cid])), + ) + await utils.answer(message, self.strings("off_strict")) + + async def fuckchatscmd(self, message: Message): + """Показать активные авточтения в чатах""" + res = "== FuckTags ==\n" + for chat in self.get("tags", []): + try: + c = await self._client.get_entity(chat) + res += (c.title if c.title is not None else c.first_name) + "\n" + except Exception: + res += str(chat) + "\n" + + res += "\n== FuckMessages ==\n" + for chat in self.get("strict", []): + try: + c = await self._client.get_entity(chat) + res += (c.title if c.title is not None else c.first_name) + "\n" + except Exception: + res += str(chat) + "\n" + + await utils.answer(message, res) + + async def watcher(self, message: Message): + if not hasattr(message, "text") or not isinstance(message, Message): + return + + if utils.get_chat_id(message) in self.get("tags", []) and message.mentioned: + await self._client.send_read_acknowledge( + message.peer_id, + message, + clear_mentions=True, + ) + + if utils.get_chat_id(message) not in self._ratelimit: + msg = await utils.answer(message, self.strings("do_not_tag_me")) + self._ratelimit += [utils.get_chat_id(message)] + await asyncio.sleep(2) + await msg.delete() + elif utils.get_chat_id(message) in self.get("strict", []): + await self._client.send_read_acknowledge(message.peer_id, message) diff --git a/hikariatama/ftg/git_pusher.py b/hikariatama/ftg/git_pusher.py new file mode 100644 index 0000000..ecdde3a --- /dev/null +++ b/hikariatama/ftg/git_pusher.py @@ -0,0 +1,84 @@ +# █ █ ▀ █▄▀ ▄▀█ █▀█ ▀ +# █▀█ █ █ █ █▀█ █▀▄ █ +# © Copyright 2022 +# https://t.me/hikariatama +# +# 🔒 Licensed under the GNU AGPLv3 +# 🌐 https://www.gnu.org/licenses/agpl-3.0.html + +# meta pic: https://static.dan.tatar/git_pusher.png +# meta banner: https://mods.hikariatama.ru/badges/git_pusher.jpg +# meta developer: @hikarimods +# scope: hikka_only +# scope: hikka_min 1.2.10 + +import os +from random import choice + +import requests +from telethon.tl.types import Message + +from .. import loader, utils + + +@loader.tds +class GitPusherMod(loader.Module): + """Easily push your repo from within the Telegram""" + + strings = { + "name": "GitPusher", + "bad_dir": "🚫 Invalid directory", + "no_dir": "🚫 Specify directory with .setghdir", + "dir_set": "🌳 Updated git directory to {}", + "terminal_required": "🚫 Terminal module is required", + } + + strings_ru = { + "bad_dir": "🚫 Неверная директория", + "no_dir": "🚫 Укажи директорию используя .setghdir", + "dir_set": "🌳 Директория обновлена на {}", + "terminal_required": "🚫 Необходими модуль Terminal", + "_cmd_doc_setghdir": " - Установить директорию в качестве основной", + "_cmd_doc_push": "[commit message] - Закоммитить установленную директорию", + "_cls_doc": "Быстро коммить изменения в директории не выходя из Телеграм", + } + + async def client_ready(self): + self.commits = ( + await utils.run_sync( + requests.get, + "https://gist.github.com/hikariatama/b0a7001306ebcc74535992c13cd33f99/raw/7a5e2c0439d31c4fedf2530ffae650ae1cb9dd0c/commit_msgs.json", + ) + ).json() + + async def setghdircmd(self, message: Message): + """ - Set directory as upstream""" + args = utils.get_args_raw(message) + if not args or not os.path.isdir(args.strip()): + await utils.answer(message, self.strings("bad_dir")) + return + + self.set("dir", args) + await utils.answer(message, self.strings("dir_set").format(args)) + + async def pushcmd(self, message: Message): + """[commit message] - Push current upstream directory""" + if not self.get("dir"): + await utils.answer(message, self.strings("no_dir")) + return + + if "terminal" not in self.allmodules.commands: + await utils.answer(message, self.strings("terminal_required")) + return + + args = (utils.get_args_raw(message) or choice(self.commits)).replace('"', '\\"') + + message = await utils.answer( + message, + ( + f".terminal cd {utils.escape_html(self.get('dir'))} && git commit" + f' -am "{utils.escape_html(args)}" && git push' + ), + ) + + await self.allmodules.commands["terminal"](message) diff --git a/hikariatama/ftg/grustnogram.py b/hikariatama/ftg/grustnogram.py new file mode 100644 index 0000000..058d551 --- /dev/null +++ b/hikariatama/ftg/grustnogram.py @@ -0,0 +1,395 @@ +# █ █ ▀ █▄▀ ▄▀█ █▀█ ▀ +# █▀█ █ █ █ █▀█ █▀▄ █ +# © Copyright 2022 +# https://t.me/hikariatama +# +# 🔒 Licensed under the GNU AGPLv3 +# 🌐 https://www.gnu.org/licenses/agpl-3.0.html + +# meta pic: https://static.dan.tatar/grustnogram_icon.png +# meta banner: https://mods.hikariatama.ru/badges/grustnogram.jpg +# meta developer: @hikarimods +# requires: Pillow requests_toolbelt +# scope: inline +# scope: hikka_only +# scope: hikka_min 1.2.10 + +__version__ = (1, 0, 1) + +import asyncio +import io +import json +import logging +import random +import string +import textwrap + +import requests +from PIL import Image, ImageDraw, ImageFont +from requests_toolbelt import MultipartEncoder +from telethon.tl.types import Message + +from .. import loader, utils +from ..inline.types import InlineCall + +logger = logging.getLogger(__name__) + +fnt = requests.get( + "https://github.com/hikariatama/assets/raw/master/EversonMono.ttf" +).content + +font = lambda size: ImageFont.truetype( + io.BytesIO(fnt), + size, + encoding="UTF-8", +) + + +async def create_badge(data) -> bytes: + SIZE = (1200, 300) + INNER_MARGIN = (30, 30) + + thumb = Image.open( + io.BytesIO((await utils.run_sync(requests.get, data["avatar"])).content) + ) + + im = Image.new("RGB", SIZE, (11, 11, 11)) + draw = ImageDraw.Draw(im) + + thumb_size = SIZE[1] - INNER_MARGIN[1] * 2 + + thumb = thumb.resize((thumb_size, thumb_size)) + # thumb = add_corners(thumb, 10) + + im.paste(thumb, INNER_MARGIN) + + tpos = ( + INNER_MARGIN[0] + thumb_size + INNER_MARGIN[0] + 8, + INNER_MARGIN[1], + ) + + draw.text(tpos, f'{data["name"]}', (255, 255, 255), font=font(64)) + link_pos = tpos[1] + 8 + font(64).getsize(data["name"])[1] + draw.text( + (tpos[0], link_pos), + f'https://grustnogram.ru/u/{data["nickname"]}', + (220, 220, 220), + font=font(32), + ) + + offset = link_pos + 16 + font(32).getsize(data["nickname"])[1] + for line in textwrap.wrap( + data["about"], width=(SIZE[0] - tpos[0]) // font(32).getsize("a")[0] + ): + draw.text( + ( + tpos[0], + offset, + ), + line, + (180, 180, 180), + font=font(32), + ) + offset += font(32).getsize(line)[1] + + offset += 16 + + draw.text( + (tpos[0], offset), + f'Followers: {data["followers"]} / Follow: {data["follow"]}', + (150, 150, 150), + font=font(26), + ) + + img = io.BytesIO() + im.save(img, format="PNG") + return img.getvalue() + + +@loader.tds +class GrustnoGramMod(loader.Module): + """Grustnogram.ru Telegram client""" + + strings = { + "name": "GrustnoGram", + "invalid_args": ( + "🚫 Invalid args. Pass email and password, separated by space" + ), + "api_error": "🚫 API error.\n
{}
", + "auth_successful": "🖤 Auth successful as {}", + "no_photo": "🚫 You need to reply to a photo", + "published": ( + '🖤 Post successfully' + " published" + ), + "delete": "🗑 Delete", + "deleted": "🖤 Post deleted", + "notif_follow": ( + '🖤 {0} is now sad with' + " you" + ), + "notif_like": ( + '🖤 {0} have broken heart' + " from" + ' your post' + ), + } + + async def client_ready(self, client, db): + if not self.get("email") or not self.get("password"): + self.sadauthcmd = self.sadauthcmd_ + else: + self._register() + + self._task = asyncio.ensure_future(self._poller()) + + async def on_unload(self): + if hasattr(self, "_task"): + self._task.cancel() + + def _register(self): + self.sadmecmd = self.sadmecmd_ + self.saduploadcmd = self.saduploadcmd_ + + async def _login(self, email: str, password: str) -> dict: + return ( + await utils.run_sync( + requests.post, + "https://api.grustnogram.ru/sessions", + headers={ + "accept": "application/json", + "content-type": "application/x-www-form-urlencoded", + "user-agent": "Hikka Userbot", + }, + data=json.dumps({"email": email, "password": password}).encode(), + ) + ).json() + + async def _get_self(self) -> dict: + return ( + await utils.run_sync( + requests.get, + "https://api.grustnogram.ru/users/self", + headers={ + "accept": "application/json", + "user-agent": "Hikka Userbot", + "access-token": self.get("token", "undefined"), + }, + ) + ).json() + + async def _publish(self, media: bytes, caption: str) -> dict: + boundary = "----WebKitFormBoundary" + "".join( + random.sample(string.ascii_letters + string.digits, 16) + ) + + m = MultipartEncoder( + fields={"file": ("image.jpg", io.BytesIO(media), "image/jpg")}, + boundary=boundary, + ) + + res = ( + await utils.run_sync( + requests.post, + "https://media.grustnogram.ru/cors.php", + headers={ + "accept": "application/json, text/plain, */*", + "user-agent": "Hikka Userbot", + "access-token": self.get("token", "undefined"), + "content-type": m.content_type, + }, + data=m, + ) + ).json() + + if any(res["err_msg"]): + raise RuntimeError(f"Can't upload image {json.dumps(res, indent=4)}") + + url = res["data"] + + return ( + await utils.run_sync( + requests.post, + "https://api.grustnogram.ru/posts", + headers={ + "accept": "application/json", + "user-agent": "Hikka Userbot", + "access-token": self.get("token", "undefined"), + }, + data=json.dumps( + {"filter": 1, "text": caption, "media": [url]} + ).encode(), + ) + ).json() + + async def _delete(self, id_: int) -> dict: + return ( + await utils.run_sync( + requests.delete, + f"https://api.grustnogram.ru/posts/{id_}", + headers={ + "accept": "application/json", + "user-agent": "Hikka Userbot", + "access-token": self.get("token", "undefined"), + }, + ) + ).json() + + async def sadauthcmd_(self, message: Message): + """ - Auth on grustnogram.ru""" + args = utils.get_args_raw(message) + try: + email, password = args.split(maxsplit=1) + except Exception: + await utils.answer(message, self.strings("invalid_args")) + return + + result = await self._login(email, password) + + if any(result["err_msg"]): + await self._api_error(message, result) + return + + token = result["data"]["access_token"] + + self.set("email", email) + self.set("password", password) + self.set("token", token) + + await utils.answer( + message, + self.strings("auth_successful").format( + (await self._get_self())["data"]["name"] + ), + ) + self._register() + + async def sadmecmd_(self, message: Message): + """Get sad banner""" + await message.delete() + me = (await self._get_self())["data"] + await self._client.send_file( + message.peer_id, + file=await create_badge(me), + caption=f"https://grustnogram.ru/u/{me['nickname']}", + ) + + async def _api_error(self, message: Message, result: dict): + await utils.answer( + message, + self.strings("api_error").format( + json.dumps( + result, + indent=4, + ), + ), + ) + + async def inline_delete(self, call: InlineCall, id_: int): + result = await self._delete(id_) + if any(result["err_msg"]): + await self._api_error(call, result) + return + + await call.edit(self.strings("deleted")) + await call.unload() + + async def _poller(self): + try: + while True: + if not self.get("token"): + await asyncio.sleep(10) + continue + + res = ( + await utils.run_sync( + requests.get, + "https://api.grustnogram.ru/status", + headers={ + "accept": "application/json", + "user-agent": "Hikka Userbot", + "access-token": self.get("token", "undefined"), + }, + ) + ).json() + + if not res["data"]["notifications_count"]: + await asyncio.sleep(30) + continue + + logger.debug( + f"Got {res['data']['notifications_count']} notification(-s) from" + " GrustnoGram" + ) + + res = ( + await utils.run_sync( + requests.get, + "https://api.grustnogram.ru/notifications", + headers={ + "accept": "application/json", + "user-agent": "Hikka Userbot", + "access-token": self.get("token", "undefined"), + }, + ) + ).json() + + if any(res["data"]): + for notification in res["data"]: + if int(notification["data"]["read"]): + continue + + if notification["type"] == "follow": + await self.inline.bot.send_message( + self._tg_id, + self.strings("notif_follow").format( + notification["data"]["nickname"] + ), + parse_mode="HTML", + disable_web_page_preview=True, + ) + elif notification["type"] == "like": + await self.inline.bot.send_message( + self._tg_id, + self.strings("notif_like").format( + notification["data"]["nickname"], + notification["data"]["post_url"], + ), + parse_mode="HTML", + disable_web_page_preview=True, + ) + else: + logger.warning( + "Unknown notification type" + f" {json.dumps(notification, indent=4)}" + ) + + await asyncio.sleep(10) + except Exception: + logger.exception("GrustnoGram poller got himself in trouble!") + + async def saduploadcmd_(self, message: Message): + """Upload image to Grustnogram""" + reply = await message.get_reply_message() + if not reply or not reply.photo: + await utils.answer(message, self.strings("no_photo")) + return + + media = await self._client.download_file(reply.media, bytes) + + caption = getattr(reply, "raw_text", None) or "" + result = await self._publish(media, caption) + + if any(result["err_msg"]): + await self._api_error(message, result) + return + + await self.inline.form( + message=message, + text=self.strings("published").format(result["data"]["url"]), + reply_markup={ + "text": self.strings("delete"), + "callback": self.inline_delete, + "args": (result["data"]["id"],), + }, + ) diff --git a/hikariatama/ftg/hikarichat.py b/hikariatama/ftg/hikarichat.py new file mode 100644 index 0000000..c3ee5df --- /dev/null +++ b/hikariatama/ftg/hikarichat.py @@ -0,0 +1,6216 @@ +__version__ = (13, 0, 3) + +# █ █ ▀ █▄▀ ▄▀█ █▀█ ▀ +# █▀█ █ █ █ █▀█ █▀▄ █ +# © Copyright 2022 +# https://t.me/hikariatama +# +# 🔒 Licensed under the GNU AGPLv3 +# 🌐 https://www.gnu.org/licenses/agpl-3.0.html + +# meta pic: https://static.dan.tatar/hikarichat_icon.png +# meta banner: https://mods.hikariatama.ru/badges/hikarichat.jpg +# meta desc: Chat administrator toolkit, now with powerful free version +# meta developer: @hikarimods + +# scope: disable_onload_docs +# scope: inline +# scope: hikka_only +# scope: hikka_min 1.3.0 +# requires: aiohttp websockets + +import abc +import asyncio +import contextlib +import functools +import imghdr +import io +import json +import logging +import random +import re +import time +import typing +from math import ceil +from types import FunctionType + +import aiohttp +import requests +import websockets +from aiogram.types import CallbackQuery, ChatPermissions +from aiogram.utils.exceptions import MessageCantBeDeleted, MessageToDeleteNotFound +from telethon.errors import ChatAdminRequiredError, UserAdminInvalidError +from telethon.errors.rpcerrorlist import WebpageCurlFailedError +from telethon.tl.functions.channels import ( + EditAdminRequest, + EditBannedRequest, + GetFullChannelRequest, + GetParticipantRequest, + InviteToChannelRequest, +) +from telethon.tl.functions.messages import EditChatDefaultBannedRightsRequest +from telethon.tl.types import ( + Channel, + ChannelParticipantCreator, + Chat, + ChatAdminRights, + ChatBannedRights, + DocumentAttributeAnimated, + Message, + MessageEntitySpoiler, + MessageMediaUnsupported, + User, + UserStatusOnline, +) + +from .. import loader, utils +from ..inline.types import InlineCall, InlineMessage + +try: + from PIL import Image, ImageDraw, ImageFont +except ImportError: + PIL_AVAILABLE = False +else: + PIL_AVAILABLE = True + +logger = logging.getLogger(__name__) + +version = f"v{__version__[0]}.{__version__[1]}.{__version__[2]}stable" +ver = f"HikariChat {version}" + +FLOOD_TIMEOUT = 0.8 +FLOOD_TRESHOLD = 4 + + +PROTECTS = { + "antinsfw": "🔞 AntiNSFW", + "antiarab": "🇵🇸 AntiArab", + "antitagall": "🐵 AntiTagAll", + "antihelp": "🐺 AntiHelp", + "antiflood": "⏱ AntiFlood", + "antichannel": "📯 AntiChannel", + "antispoiler": "👻 AntiSpoiler", + "report": "📣 Report", + "antiexplicit": "🤬 AntiExplicit", + "antiservice": "⚙️ AntiService", + "antigif": "🎑 AntiGIF", + "antizalgo": "🌀 AntiZALGO", + "antistick": "🎨 AntiStick", + "antilagsticks": "⚰️ AntiLagSticks", + "cas": "🛡 CAS", + "bnd": "💬 BND", + "antiraid": "🚪 AntiRaid", + "banninja": "🥷 BanNinja", + "welcome": "👋 Welcome", + "captcha": "🚥 Captcha", +} + + +def fit(line: str, max_size: int) -> str: + if len(line) >= max_size: + return line + + offsets_sum = max_size - len(line) + + return f"{' ' * ceil(offsets_sum / 2 - 1)}{line}{' ' * int(offsets_sum / 2 - 1)}" + + +def gen_table(t: typing.List[typing.List[str]]) -> bytes: + table = "" + header = t[0] + rows_sizes = [len(i) + 2 for i in header] + for row in t[1:]: + rows_sizes = [max(len(j) + 2, rows_sizes[i]) for i, j in enumerate(row)] + + rows_lines = ["━" * i for i in rows_sizes] + + table += f"┏{('┯'.join(rows_lines))}┓\n" + + for line in t: + table += ( + f"┃⁣⁣ {' ┃⁣⁣ '.join([fit(row, rows_sizes[k]) for k, row in enumerate(line)])} ┃⁣⁣\n" + ) + table += "┠" + + for row in rows_sizes: + table += f"{'─' * row}┼" + + table = table[:-1] + "┫\n" + + return "\n".join(table.splitlines()[:-1]) + "\n" + f"┗{('┷'.join(rows_lines))}┛\n" + + +def get_first_name(user: typing.Union[User, Channel]) -> str: + """Returns first name of user or channel title""" + return utils.escape_html( + user.first_name if isinstance(user, User) else user.title + ).strip() + + +def get_full_name(user: typing.Union[User, Channel]) -> str: + return utils.escape_html( + user.title + if isinstance(user, Channel) + else ( + f"{user.first_name} " + + (user.last_name if getattr(user, "last_name", False) else "") + ) + ).strip() + + +BANNED_RIGHTS = { + "view_messages": False, + "send_messages": False, + "send_media": False, + "send_stickers": False, + "send_gifs": False, + "send_games": False, + "send_inline": False, + "send_polls": False, + "change_info": False, + "invite_users": False, +} + + +class HikariChatAPI: + def __init__(self): + self._bot = "@hikka_userbot" + + self._queue = [] + self.feds = {} + self.chats = {} + self.variables = {} + self.init_done = asyncio.Event() + self._show_warning = True + self._connected = False + self._inited = False + self._local = False + + async def init( + self, + client: "CustomTelegramClient", # type: ignore + db: "Database", # type: ignore + module: loader.Module, + ): + """Entry point""" + self._client = client + self._db = db + self.module = module + + if not self.module.get("token"): + await self._get_token() + + self._task = asyncio.ensure_future(self._connect()) + await self.init_done.wait() + + async def _wss(self): + async with websockets.connect( + f"wss://hikarichat.hikariatama.ru/ws/{self.module.get('token')}" + ) as wss: + init = json.loads(await wss.recv()) + + logger.debug(f"HikariChat connection debug info {init}") + + if init["event"] == "startup": + self.variables = init["variables"] + elif init["event"] == "license_violation": + await wss.close() + raise Exception("local") + + self.init_done.set() + + logger.debug("HikariChat connected") + self._show_warning = True + self._connected = True + self._inited = True + + while True: + ans = json.loads(await wss.recv()) + + if ans["event"] == "update_info": + self.chats = ans["chats"] + self.feds = ans["feds"] + + await wss.send(json.dumps({"ok": True, "queue": self._queue})) + self._queue = [] + for chat in self.chats: + if str(chat) not in self.module._linked_channels: + channel = ( + await self._client(GetFullChannelRequest(int(chat))) + ).full_chat.linked_chat_id + self.module._linked_channels[str(chat)] = channel or False + + if ans["event"] == "queue_status": + await self._client.edit_message( + ans["chat_id"], + ans["message_id"], + ans["text"], + ) + + async def _connect(self): + while True: + try: + await self._wss() + except Exception: + logger.debug("HikariChat disconnection traceback", exc_info=True) + + if not self._inited: + self._local = True + self.variables = json.loads( + ( + await utils.run_sync( + requests.get, + "https://gist.githubusercontent.com/hikariatama/31a8246c9c6ad0b451324969d6ff2940/raw/608509efd7fee6fa876227e1c8c3c7dc0a952892/variables.json", + ) + ).text + ) + self._feds = self.module.get("feds", {}) + delattr(self, "feds") + self.chats = self.module.get("chats", {}) + self._processor_task = asyncio.ensure_future( + self._queue_processor() + ) + self.init_done.set() + self._task.cancel() + return + + self._connected = False + if self._show_warning: + logger.debug("HikariChat disconnected, retry in 5 sec") + self._show_warning = False + + await asyncio.sleep(5) + + def request(self, payload: dict, message: typing.Optional[Message] = None): + if isinstance(message, Message): + payload = { + **payload, + **{ + "chat_id": utils.get_chat_id(message), + "message_id": message.id, + }, + } + + self._queue += [payload] + + def should_protect(self, chat_id: typing.Union[str, int], protection: str) -> bool: + return ( + str(chat_id) in self.chats + and protection in self.chats[str(chat_id)] + and str(self.chats[str(chat_id)][protection][1]) == str(self.module._tg_id) + ) + + async def nsfw(self, photo: bytes) -> str: + if not self.module.get("token"): + logger.warning("Token is not sent, NSFW check forbidden") + return "sfw" + + async with aiohttp.ClientSession() as session: + async with session.request( + "POST", + "https://hikarichat.hikariatama.ru/check_nsfw", + headers={"Authorization": f"Bearer {self.module.get('token')}"}, + data={"file": photo}, + ) as resp: + r = await resp.text() + + try: + r = json.loads(r) + except Exception: + logger.exception("Failed to check NSFW") + return "sfw" + + if "error" in r and "Rate limit" in r["error"]: + logger.warning("NSFW checker ratelimit exceeded") + return "sfw" + + if "success" not in r: + logger.error(f"API error {json.dumps(r, indent=4)}") + return "sfw" + + return r["verdict"] + + async def _get_token(self): + async with self._client.conversation(self._bot) as conv: + m = await conv.send_message("/token") + r = await conv.get_response() + token = r.raw_text + await m.delete() + await r.delete() + + if not token.startswith("kirito_") and not token.startswith("asuna_"): + raise loader.LoadError("Can't get token") + + self.module.set("token", token) + + await self._client.delete_dialog(self._bot) + + def __getattr__(self, attribute: str): + if self._local and attribute == "feds": + return {fed["shortname"]: fed for fed in self._feds.values()} + + raise AttributeError + + async def _queue_processor(self): + while True: + if not self._queue: + await asyncio.sleep(1) + continue + + ERROR = ( + "🚫 API Error:" + " {}" + ) + + async def assert_arguments(args: set, item: dict) -> bool: + if any(i not in item.get("args", {}) for i in args): + if "chat_id" in item: + await self._client.edit_message( + item["chat_id"], + item["message_id"], + ( + "🚫" + " Bad API arguments, PROKAZNIK!" + ), + ) + return False + + return True + + async def error(msg: str, item: dict): + if "chat_id" in item: + await self._client.edit_message( + item["chat_id"], + item["message_id"], + ERROR.format(msg), + ) + + feds_copy = self._feds.copy() + chats_copy = self.chats.copy() + + item = self._queue.pop(0) + u = str(self._client._tg_id) + + if item["action"] == "create federation": + if not await assert_arguments({"shortname", "name"}, item): + continue + + t = "fed_" + "".join( + [ + random.choice(list("abcdefghijklmnopqrstuvwyz1234567890")) + for _ in range(32) + ] + ) + + self._feds[t] = { + "shortname": item["args"]["shortname"], + "name": item["args"]["name"], + "chats": [], + "warns": {}, + "admins": [u], + "owner": u, + "fdef": [], + "notes": {}, + "uid": t, + } + + if item["action"] == "add chat to federation": + if not await assert_arguments({"uid", "cid"}, item): + continue + + if item["args"]["uid"] not in self._feds: + await error("Federation doesn't exist", item) + continue + + if str(item["args"]["cid"]) in self._feds[item["args"]["uid"]]["chats"]: + await error("Chat is already in this federation", item) + continue + + self._feds[item["args"]["uid"]]["chats"] += [str(item["args"]["cid"])] + + if item["action"] == "remove chat from federation": + if not await assert_arguments({"uid", "cid"}, item): + continue + + if item["args"]["uid"] not in self._feds: + await error("Federation doesn't exist", item) + continue + + if ( + str(item["args"]["cid"]) + not in self._feds[item["args"]["uid"]]["chats"] + ): + await error("Chat is not in this federation", item) + continue + + self._feds[item["args"]["uid"]]["chats"].remove( + str(item["args"]["cid"]) + ) + + if item["action"] == "update protections": + if not await assert_arguments({"protection", "state", "chat"}, item): + continue + + chat, protection, state = ( + str(item["args"]["chat"]), + item["args"]["protection"], + item["args"]["state"], + ) + + if protection not in self.variables["protections"] + ["welcome"]: + await error("Unknown protection type", item) + continue + + if ( + protection in self.variables["argumented_protects"] + and state not in self.variables["protect_actions"] + or protection not in self.variables["argumented_protects"] + and protection in self.variables["protections"] + and state not in {"on", "off"} + ): + await error("Protection state invalid", item) + continue + + if chat not in self.chats: + self.chats[chat] = {} + + if state == "off": + if protection in self.chats[chat]: + del self.chats[chat][protection] + else: + self.chats[chat][protection] = [state, u] + + if item["action"] == "delete federation": + if not await assert_arguments({"uid"}, item): + continue + + uid = item["args"]["uid"] + + if uid not in self._feds: + await error("Federation doesn't exist", item) + continue + + del self._feds[uid] + + if item["action"] == "rename federation": + if not await assert_arguments({"uid", "name"}, item): + continue + + uid, name = item["args"]["uid"], item["args"]["name"] + + if uid not in self._feds: + await error("Federation doesn't exist", item) + continue + + self._feds[uid]["name"] = name + + if item["action"] == "protect user": + if not await assert_arguments({"uid", "user"}, item): + continue + + uid, user = item["args"]["uid"], item["args"]["user"] + user = str(user) + + if not user.isdigit(): + await error("Unexpected format for user", item) + continue + + if uid not in self._feds: + await error("Federation doesn't exist", item) + continue + + if user in self._feds[uid]["fdef"]: + self._feds[uid]["fdef"].remove(user) + else: + self._feds[uid]["fdef"] += [user] + + if item["action"] == "warn user": + if not await assert_arguments({"uid", "user", "reason"}, item): + continue + + uid, user, reason = ( + item["args"]["uid"], + item["args"]["user"], + item["args"]["reason"], + ) + user = str(user) + + if not user.isdigit(): + await error("Unexpected format for user", item) + continue + + if uid not in self._feds: + await error("Federation doesn't exist", item) + continue + + if user not in self._feds[uid]["warns"]: + self._feds[uid]["warns"][user] = [] + + self._feds[uid]["warns"][user] += [reason] + + if item["action"] == "forgive user warn": + if not await assert_arguments({"uid", "user"}, item): + continue + + uid, user = item["args"]["uid"], item["args"]["user"] + user = str(user) + + if not user.isdigit(): + await error("Unexpected format for user", item) + continue + + if uid not in self._feds: + await error("Federation doesn't exist", item) + continue + + if ( + user not in self._feds[uid]["warns"] + or not self._feds[uid]["warns"][user] + ): + await error("This user has no warns yet", item) + continue + + del self._feds[uid]["warns"][user][-1] + + if item["action"] == "clear all user warns": + if not await assert_arguments({"uid", "user"}, item): + continue + + uid, user = item["args"]["uid"], item["args"]["user"] + user = str(user) + + if not user.isdigit(): + if not item["args"].get("silent", False): + await error("Unexpected format for user", item) + continue + + if uid not in self._feds: + if not item["args"].get("silent", False): + await error("Federation doesn't exist", item) + continue + + if not self._feds[uid].get("warns", {}).get(user): + if not item["args"].get("silent", False): + await error("This user has no warns yet", item) + continue + + del self._feds[uid]["warns"][user] + + if item["action"] == "clear federation warns": + if not await assert_arguments({"uid"}, item): + continue + + uid = item["args"]["uid"] + + if uid not in self._feds: + await error("Federation doesn't exist", item) + continue + + if not self._feds[uid].get("warns"): + await error("This federation has no warns yet", item) + continue + + del self._feds[uid]["warns"] + + if item["action"] == "new note": + if not await assert_arguments({"uid", "shortname", "note"}, item): + continue + + uid, shortname, note = ( + item["args"]["uid"], + item["args"]["shortname"], + item["args"]["note"], + ) + + if uid not in self._feds: + await error("Federation doesn't exist", item) + continue + + self._feds[uid]["notes"][shortname] = {"creator": u, "text": note} + + if item["action"] == "delete note": + if not await assert_arguments({"uid", "shortname"}, item): + continue + + uid, shortname = item["args"]["uid"], item["args"]["shortname"] + + if uid not in self._feds: + await error("Federation doesn't exist", item) + continue + + if shortname not in self._feds[uid]["notes"]: + await error(f"Note not found ({uid=}, {shortname=})", item) + continue + + del self._feds[uid]["notes"][shortname] + + if feds_copy != self._feds: + self.module.set("feds", self._feds) + + if chats_copy != self.chats: + self.module.set("chats", self.chats) + + +api = HikariChatAPI() + + +def reverse_dict(d: dict) -> dict: + return {val: key for key, val in d.items()} + + +@loader.tds +class HikariChatMod(loader.Module): + """ + Advanced chat admin toolkit + """ + + __metaclass__ = abc.ABCMeta + + strings = { + "name": "HikariChat", + "args": ( + "🚫 Args are" + " incorrect" + ), + "no_reason": "Not specified", + "antitagall_on": ( + "🐵 AntiTagAll is now on" + " in this chat\nAction: {}" + ), + "antitagall_off": ( + "🐵 AntiTagAll is now off" + " in this chat" + ), + "antiarab_on": ( + "🇵🇸 AntiArab is now on in" + " this chat\nAction: {}" + ), + "antiarab_off": ( + "🇵🇸 AntiArab is now off" + " in this chat" + ), + "antilagsticks_on": ( + "💣 Destructive stickers" + " protection is now on in this chat" + ), + "antilagsticks_off": ( + "💣 Destructive stickers" + " protection is now off in this chat" + ), + "antizalgo_on": ( + "🌀 AntiZALGO is now" + " on in" + " this chat\nAction: {}" + ), + "antizalgo_off": ( + "🌀 AntiZALGO is now off" + " in this chat" + ), + "antistick_on": ( + "🎨 AntiStick is now" + " on in" + " this chat\nAction: {}" + ), + "antistick_off": ( + "🎨 AntiStick is now off" + " in this chat" + ), + "antihelp_on": ( + "🐺 AntiHelp is now on in" + " this chat" + ), + "antihelp_off": ( + "🐺 AntiHelp is now" + " off in" + " this chat" + ), + "antiraid_on": ( + "🚪 AntiRaid is now on" + " in this chat\nAction: {}" + ), + "antiraid_off": ( + "🚪 AntiRaid is now off" + " in this chat" + ), + "bnd_on": ( + "💬 Block-Non-Discussion" + " is now on in this chat\nAction: {}" + ), + "bnd_off": ( + "💬 Block-Non-Discussion" + " is now off in this chat" + ), + "antiraid": ( + "🚪 AntiRaid is On." + " I {}" + ' {} in chat {}' + ), + "antichannel_on": ( + "📯 AntiChannel is now on" + " in this chat" + ), + "antichannel_off": ( + "📯 AntiChannel is" + " now off" + " in this chat" + ), + "report_on": ( + "📣 Report is now on in" + " this chat" + ), + "report_off": ( + "📣 Report is now off in" + " this chat" + ), + "antiflood_on": ( + " AntiFlood is now on in" + " this chat\nAction: {}" + ), + "antiflood_off": ( + " AntiFlood is now off" + " in this chat" + ), + "antispoiler_on": ( + "👻 AntiSpoiler is now on" + " in this chat" + ), + "antispoiler_off": ( + "👻 AntiSpoiler is" + " now off" + " in this chat" + ), + "antigif_on": ( + "🎑 AntiGIF is now on in" + " this chat" + ), + "antigif_off": ( + "🎑 AntiGIF is now off in" + " this chat" + ), + "antiservice_on": ( + "⚙️ AntiService is now on" + " in this chat" + ), + "antiservice_off": ( + "⚙️ AntiService is now" + " off in this chat" + ), + "banninja_on": ( + "🥷 BanNinja is now on in" + " this chat" + ), + "banninja_off": ( + "🥷 BanNinja is now" + " off in" + " this chat" + ), + "antiexplicit_on": ( + "🤬 AntiExplicit is" + " now on" + " in this chat\nAction: {}" + ), + "antiexplicit_off": ( + "🤬 AntiExplicit is now" + " off in this chat" + ), + "captcha_on": ( + "🚥 Captcha is now on in" + " this chat\nAction: {}" + ), + "captcha_off": ( + "🚥 Captcha is now off in" + " this chat" + ), + "cas_on": ( + "🛡 CAS is now on in this" + " chat\nAction: {}" + ), + "cas_off": ( + "🛡 CAS is now off in this" + " chat" + ), + "antinsfw_on": ( + "🔞 AntiNSFW is now on in" + " this chat\nAction: {}" + ), + "antinsfw_off": ( + "🔞 AntiNSFW is now" + " off in" + " this chat" + ), + "arabic_nickname": ( + '🇵🇸 {}' + " has hieroglyphics in his nickname.\n👊 Action: I {}" + ), + "zalgo": ( + '🌀 {}' + " has ZALGO in his nickname.\n👊 Action: I {}" + ), + "bnd": ( + '💬 {}' + " sent a message to channel comments without being chat member.\n👊 Action:" + " I {}" + ), + "cas": ( + '🛡 {}' + " appears to be in Combat Anti Spam database.\n👊 Action: I {}" + ), + "stick": ( + "🎨 {} is' + " flooding stickers.\n👊 Action: I {}" + ), + "explicit": ( + '🤬 {}' + " sent explicit content.\n👊 Action: I {}" + ), + "destructive_stick": ( + '🚫 {}' + " sent destructive sticker.\n👊 Action: I {}" + ), + "nsfw_content": ( + '🔞 {}' + " sent NSFW content.\n👊 Action: I {}" + ), + "flood": ( + ' {} is' + " flooding.\n👊 Action: I {}" + ), + "tagall": ( + '🐵 {}' + " used TagAll.\n👊 Action: I {}" + ), + "fwarn": ( + "👮‍♀️💼 {} got' + " {}/{} federative warn\nReason: {}\n\n{}" + ), + "no_fed_warns": ( + "👮‍♀️ This federation has" + " no warns yet" + ), + "no_warns": ( + '👮‍♀️ {}' + " has no warns yet" + ), + "warns": ( + '👮‍♀️ {}' + " has {}/{} warns\n\n{}" + ), + "warns_adm_fed": ( + "👮‍♀️ Warns in this" + " federation:\n" + ), + "dwarn_fed": ( + "👮‍♀️ Forgave last" + ' federative warn of {}' + ), + "clrwarns_fed": ( + "👮‍♀️ Forgave all" + ' federative warns of {}' + ), + "warns_limit": ( + '👮‍♀️ {}' + " reached warns limit.\nAction: I {}" + ), + "welcome": ( + "👋 Now I will greet" + " people in this chat\n{}" + ), + "unwelcome": ( + "👋 Now I will not greet" + " people in this chat" + ), + "chat404": "🔓 I am not protecting this chat yet.\n", + "protections": ( + "🇵🇸" + " .AntiArab - Bans spammy arabs\n🐺 .AntiHelp -" + " Removes frequent userbot commands\n🐵 .AntiTagAll -" + " Restricts tagging all members\n👋 .Welcome - Greets" + " new members\n🚪 .AntiRaid" + " - Bans all new members\n📯 .AntiChannel -" + " Restricts writing on behalf of channels\n👻 .AntiSpoiler -" + " Restricts spoilers\n🎑" + " .AntiGIF - Restricts GIFs\n🍓 .AntiNSFW -" + " Restricts NSFW photos and stickers\n.AntiFlood -" + " Prevents flooding\n🤬" + " .AntiExplicit - Restricts explicit content\n⚙️ .AntiService -" + " Removes service messages\n🌀 .AntiZALGO -" + " Penalty for users with ZALGO in nickname\n🎨 .AntiStick -" + " Prevents stickers flood\n🚥 .Captcha -" + " Requires every new participant to complete captcha\n🛡 .CAS - Check every" + " new participant through Combat Anti Spam\n💬 .BND - Restricts" + " messages from users, which are not a participants of chat" + " (comments)\n🥷" + " .BanNinja - Automatic version of AntiRaid\n⚰️" + " .AntiLagSticks - Bans laggy stickers\n👾 Admin:" + " .ban .kick" + " .mute\n.unban .unmute - Admin" + " tools\n👮‍♀️" + " Warns: .warn .warns\n.dwarn" + " .clrwarns - Warning system\n💼 Federations:" + " .fadd .frm" + " .newfed\n.namefed .fban" + " .rmfed .feds\n.fpromote" + " .fdemote\n.fdef .fdeflist -" + " Controlling multiple chats\n🗒 Notes: .fsave" + " .fstop .fnotes - Federative notes" + ), + "not_admin": "🤷‍♂️ I'm not admin here, or don't have enough rights", + "mute": ( + '🤐 {}' + " was muted {}. Reason: {}\n\n{}" + ), + "mute_log": ( + '🤐 {}' + ' was muted {} in {}. Reason: {}\n\n{}' + ), + "ban": ( + '🔒 {}' + " was banned {}. Reason: {}\n\n{}" + ), + "ban_log": ( + '🔒 {}' + ' was banned {} in {}. Reason: {}\n\n{}' + ), + "kick": ( + '🚪 {}' + " was kicked. Reason: {}\n\n{}" + ), + "kick_log": ( + '🚪 {}' + ' was kicked in {}. Reason: {}\n\n{}' + ), + "unmuted": ( + '🎉 {}' + " was unmuted" + ), + "unmuted_log": ( + '🎉 {}' + ' was unmuted in {}' + ), + "unban": ( + '🪄 {}' + " was unbanned" + ), + "unban_log": ( + '🪄 {}' + ' was unbanned in {}' + ), + "defense": ( + "🛡 Shield for {} is now {}' + ), + "no_defense": ( + "🛡 Federative defense" + " list is empty" + ), + "defense_list": ( + "🛡 Federative defense" + " list:\n{}" + ), + "fadded": ( + "💼 Current chat added to" + ' federation "{}"' + ), + "newfed": ( + "💼 Created federation" + ' "{}"' + ), + "rmfed": ( + "💼 Removed federation" + ' "{}"' + ), + "fed404": ( + "💼 Federation not" + " found" + ), + "frem": ( + "💼 Current chat removed" + ' from federation "{}"' + ), + "f404": ( + "💼 Current chat is" + " not in" + ' federation "{}"' + ), + "fexists": ( + "💼 Current chat is" + ' already in federation "{}"' + ), + "fedexists": ( + "💼 Federation exists" + ), + "joinfed": ( + "💼 Federation joined" + ), + "namedfed": ( + "💼 Federation renamed to" + " {}" + ), + "nofed": ( + "💼 Current chat is" + " not in" + " any federation" + ), + "fban": ( + '💼 {}' + " was banned in federation {} {}\nReason: {}\n{}" + ), + "gban": ( + '🖕 {}' + " was gbanned.\nReason: {}\n\n{}" + ), + "gbanning": ( + "🖕 Gbanning {}...' + ), + "gunban": ( + '🤗 {}' + " was gunbanned.\n\n{}" + ), + "gunbanning": ( + "🤗 Gunbanning {}...' + ), + "in_n_chats": ( + "👎 Banned in {}" + " chat(-s)" + ), + "unbanned_in_n_chats": ( + "✋️ Unbanned in {}" + " chat(-s)" + ), + "fmute": ( + '💼 {}' + " muted in federation {} {}\nReason: {}\n{}" + ), + "funban": ( + '💼 {}' + " unbanned in federation {}\n" + ), + "funmute": ( + '💼 {}' + " unmuted in federation {}\n" + ), + "feds_header": ( + "💼 Federations:\n\n" + ), + "fed": ( + '💼 Federation "{}"' + " info:\n🔰 Chats:\n{}\n🔰 Channels:\n{}\n🔰" + " Admins:\n{}\n🔰 Warns: {}\n" + ), + "no_fed": ( + "💼 This chat is not in" + " any federation" + ), + "fpromoted": ( + '💼 {}' + " promoted in federation {}" + ), + "fdemoted": ( + '💼 {}' + " demoted in federation {}" + ), + "api_error": ( + "🚫 api.hikariatama.ru" + " Error!\n{}" + ), + "fsave_args": ( + "💼 Usage: .fsave" + " shortname <reply>" + ), + "fstop_args": ( + "💼 Usage: .fstop" + " shortname" + ), + "fsave": ( + "💼 Federative note" + " {} saved!" + ), + "fstop": ( + "💼 Federative note" + " {} removed!" + ), + "fnotes": ( + "💼 Federative" + " notes:\n{}" + ), + "usage": "ℹ️ Usage: .{} <on/off>", + "chat_only": "ℹ️ This command is for chats only", + "version": ( + "🎢 {}\n\n🤘 Author:" + " t.me/hikariatama\n☺️" + " Downloaded from @hikarimods\n{}" + ), + "error": ( + "💀 HikariChat Issued" + " error" + ), + "reported": ( + '💼 {}' + " reported this message to admins\nReason: {}" + ), + "no_federations": ( + "💼 You have no active" + " federations" + ), + "clrallwarns_fed": ( + "👮‍♀️ Forgave all" + " federative warns of federation" + ), + "cleaning": ( + "🫥 Looking for Deleted" + " accounts..." + ), + "deleted": ( + "🫥 Removed {} Deleted" + " accounts" + ), + "fcleaning": ( + "🫥 Looking for Deleted" + " accounts in federation..." + ), + "btn_unban": "🔓 Unban (ADM)", + "btn_unmute": "🔈 Unmute (ADM)", + "btn_unwarn": "♻️ De-Warn (ADM)", + "inline_unbanned": ( + '🔓 {} unbanned by {}' + ), + "inline_unmuted": ( + '🔈 {} unmuted by {}' + ), + "inline_unwarned": ( + '♻️ Forgave last warn of {} by {}' + ), + "inline_funbanned": ( + '🔓 {} unbanned in federation by {}' + ), + "inline_funmuted": ( + '🔈 {} unmuted in federation by {}' + ), + "btn_funmute": "🔈 Fed Unmute (ADM)", + "btn_funban": "🔓 Fed Unban (ADM)", + "btn_mute": "🙊 Mute", + "btn_ban": "🔒 Ban", + "btn_fban": "💼 Fed Ban", + "btn_del": "🗑 Delete", + "inline_fbanned": ( + '💼 {}' + ' banned in federation by {}' + ), + "inline_muted": '🙊 {} muted by {}', + "inline_banned": ( + '🔒 {}' + ' banned by {}' + ), + "inline_deleted": '🗑 Deleted by {}', + "sync": "🔄 Syncing chats and feds with server in force mode...", + "sync_complete": "😌 Successfully synced", + "rename_noargs": ( + "🚫 Specify new" + " federation" + " name" + ), + "rename_success": '😇 Federation renamed to "{}"', + "suffix_removed": "📼 Punishment suffix removed", + "suffix_updated": "📼 New punishment suffix saved\n\n{}", + "processing_myrights": "😌 Processing chats", + "logchat_removed": "📲 Log chat disabled", + "logchat_invalid": ( + "🚫 Log chat invalid" + ), + "logchat_set": "📲 Log chat updated to {}", + "clnraid_args": ( + "🥷 Example usage:" + " .clnraid 10" + ), + "clnraid_admin": ( + "🥷 Error occured while" + " promoting cleaner. Please, ensure you have enough rights in chat" + ), + "clnraid_started": ( + "🥷 RaidCleaner is in" + " progress... Found {} users to kick..." + ), + "clnraid_confirm": ( + "🥷 Please, confirm that" + " you want to start RaidCleaner on {} users" + ), + "clnraid_yes": "🥷 Start", + "clnraid_cancel": "🔻 Cancel", + "clnraid_stop": "🚨 Stop", + "clnraid_complete": ( + "🥷 RaidCleaner complete!" + " Removed: {} user(-s)" + ), + "clnraid_cancelled": ( + "🥷 RaidCleaner" + " cancelled." + " Removed: {} user(-s)" + ), + "smart_anti_raid_active": ( + "🥷 BanNinja is working" + " hard to prevent intrusion to this chat.\n\n{}Deleted {}" + " bot(-s)" + ), + "smart_anti_raid_off": "🚨 Stop", + "smart_anti_raid_stopped": ( + "🥷 BanNinja Stopped" + ), + "banninja_report": ( + "🥷 BanNinja has done his" + " job.\nDeleted {} bot(-s)\n\n🏹 «BanNinja can handle any" + " size" + " of attack» © @hikariatama" + ), + "forbid_messages": ( + "⚠️ I've forbidden sending messages until attack is fully" + " released\n\n" + ), + "confirm_rmfed": ( + "⚠️ Warning! This operation can't be reverted! Are you sure, " + "you want to delete federation {}?" + ), + "confirm_rmfed_btn": "🗑 Delete", + "decline_rmfed_btn": "🔻 Cancel", + "pil_unavailable": ( + "🚫 Pillow package" + " unavailable" + ), + "action": "", + "configure": "Configure", + "toggle": "Toggle", + "no_protects": ( + "🚫 This chat has no" + " active protections to show" + ), + "from_where": ( + "🚫 Reply to a message to" + " purge from" + ), + "no_notes": ( + "🚫 No notes found" + ), + "complete_captcha": ( + "🚥 {}, please, complete captcha within 5' + " minutes" + ), + "captcha_timeout": ( + '🚥 {}' + " have not completed captcha in time.\n👊 Action: I {}" + ), + "captcha_failed": ( + '🚥 {}' + " failed captcha.\n👊 Action: I {}" + ), + "fdef403": ( + "🛡 You can't {} this" + " user, because he is under federative protection" + ), + } + + strings_ru = { + "complete_captcha": ( + "🚥 {}, пожалуйста, пройди капчу в течение 5' + " минут" + ), + "captcha_timeout": ( + "🚥 {} не' + " прошел капчу вовремя.\n👊 Действие: {}" + ), + "captcha_failed": ( + "🚥 {} не' + " прошел капчу.\n👊 Действие: {}" + ), + "cas_on": ( + "🛡 CAS теперь включен в" + " этом чате\nДействие: {}" + ), + "cas_off": ( + "🛡 CAS теперь выключен в" + " этом чате" + ), + "cas": ( + '🛡 {}' + " appears to be in Combat Anti Spam database.\n👊 Action: I {}" + ), + "from_where": ( + "🚫 Ответь на сообщение," + " начиная с которого надо удалить." + ), + "smart_anti_raid_active": ( + "🥷 BanNinja работает в" + " поте лица, отбивая атаку на этот чат.\n\n{}Удалено {} бот(-ов)" + ), + "forbid_messages": ( + "⚠️ Я запретил отправку сообщений, пока атака не будет полностью" + " отражена\n\n" + ), + "smart_anti_raid_off": "🚨 Остановить", + "smart_anti_raid_stopped": ( + "🥷 BanNinja" + " остановлен" + ), + "error": "😵 Произошла ошибка HikariChat", + "args": ( + "🚫 Неверные" + " аргументы" + ), + "no_reason": "Не указана", + "antitagall_on": ( + "🐵 AntiTagAll теперь" + " включен в этом чате\nДействие: {}" + ), + "antitagall_off": ( + "🐵 AntiTagAll теперь" + " выключен в этом чате" + ), + "antiarab_on": ( + "🇵🇸 AntiArab теперь" + " включен в этом чате\nДействие: {}" + ), + "antiarab_off": ( + "🇵🇸 AntiArab теперь" + " выключен в этом чате" + ), + "antizalgo_on": ( + "🌀 AntiZALGO теперь" + " включен в этом чате\nДействие: {}" + ), + "antizalgo_off": ( + "🌀 AntiZALGO теперь" + " выключен в этом чате" + ), + "antistick_on": ( + "🎨 AntiStick теперь" + " включен в этом чате\nДействие: {}" + ), + "antistick_off": ( + "🎨 AntiStick теперь" + " выключен в этом чате" + ), + "antihelp_on": ( + "🐺 AntiHelp теперь" + " включен в этом чате" + ), + "antihelp_off": ( + "🐺 AntiHelp теперь" + " выключен в этом чате" + ), + "antiraid_on": ( + "🚪 AntiRaid теперь" + " включен в этом чате\nДействие: {}" + ), + "antiraid_off": ( + "🚪 AntiRaid теперь" + " выключен в этом чате" + ), + "bnd_on": ( + "💬 Block-Non-Discussion" + " теперь включен в этом чате\nДействие: {}" + ), + "bnd_off": ( + "💬 Block-Non-Discussion" + " теперь выключен в этом чате" + ), + "antichannel_on": ( + "📯 AntiChannel теперь" + " включен в этом чате" + ), + "antichannel_off": ( + "📯 AntiChannel теперь" + " выключен в этом чате" + ), + "report_on": ( + "📣 Report теперь включен" + " в этом чате" + ), + "report_off": ( + "📣 Report теперь" + " выключен" + " в этом чате" + ), + "antiflood_on": ( + " AntiFlood теперь" + " включен в этом чате\nДействие: {}" + ), + "antiflood_off": ( + " AntiFlood теперь" + " выключен в этом чате" + ), + "antispoiler_on": ( + "👻 AntiSpoiler теперь" + " включен в этом чате" + ), + "antispoiler_off": ( + "👻 AntiSpoiler теперь" + " выключен в этом чате" + ), + "antigif_on": ( + "🎑 AntiGIF теперь" + " включен" + " в этом чате" + ), + "antigif_off": ( + "🎑 AntiGIF теперь" + " выключен в этом чате" + ), + "antiservice_on": ( + "⚙️ AntiService теперь" + " включен в этом чате" + ), + "antiservice_off": ( + "⚙️ AntiService теперь" + " выключен в этом чате" + ), + "banninja_on": ( + "🥷 BanNinja теперь" + " включен в этом чате" + ), + "banninja_off": ( + "🥷 BanNinja теперь" + " выключен в этом чате" + ), + "antiexplicit_on": ( + "🤬 AntiExplicit теперь" + " включен в этом чате\nДействие: {}" + ), + "antiexplicit_off": ( + "🤬 AntiExplicit теперь" + " выключен в этом чате" + ), + "antinsfw_on": ( + "🔞 AntiNSFW теперь" + " включен в этом чате\nДействие: {}" + ), + "antinsfw_off": ( + "🔞 AntiNSFW теперь" + " выключен в этом чате" + ), + "captcha_on": ( + "🚥 Captcha теперь" + " включена в этом чате\nДействие: {}" + ), + "captcha_off": ( + "🚥 Captcha теперь" + " выключена в этом чате" + ), + "no_fed_warns": ( + "👮‍♀️ This federation has" + " no warns yet" + ), + "warns_adm_fed": ( + "👮‍♀️ Warns in this" + " federation:\n" + ), + "welcome": ( + "👋 Теперь я буду" + " приветствовать людей в этом чате\n{}" + ), + "unwelcome": ( + "👋 Я больше не буду" + " приветствовать людей в этом чате" + ), + "chat404": "🔓 Этот чат еще не защищен.\n", + "not_admin": "🤷‍♂️ Я здесь не админ, или у меня недостаточно прав", + "no_defense": ( + "🛡 Федеративный список" + " защиты пуст" + ), + "defense_list": ( + "🛡 Федеративный список" + " защиты:\n{}" + ), + "fed404": ( + "💼 Федерация не" + " найдена" + ), + "fedexists": ( + "💼 Федерация" + " существует" + ), + "joinfed": ( + "💼 Присоединился к" + " федерации" + ), + "namedfed": ( + "💼 Федерация" + " переименована в {}" + ), + "nofed": ( + "💼 Этот чат не находится" + " ни в одной из федераций" + ), + "feds_header": ( + "💼 Федерации:\n\n" + ), + "no_fed": ( + "💼 Этот чат не находится" + " ни в одной из федераций" + ), + "api_error": ( + "🚫 Ошибка" + " api.hikariatama.ru!\n{}" + ), + "fsave_args": ( + "💼 Пример: .fsave" + " shortname <reply>" + ), + "fstop_args": ( + "💼 Пример: .fstop" + " shortname" + ), + "fsave": ( + "💼 Федеративная заметка" + " {} сохранена!" + ), + "fstop": ( + "💼 Федеративная заметка" + " {} удалена!" + ), + "fnotes": ( + "💼 Федеративные" + " заметки:\n{}" + ), + "usage": "ℹ️ Пример: .{} <on/off>", + "chat_only": "ℹ️ Эта команда предназначена для чатов", + "no_federations": ( + "💼 Нет активных" + " федераций" + ), + "clrallwarns_fed": ( + "👮‍♀️ Прощены все" + " предупреждения в федерации" + ), + "cleaning": ( + "🫥 Поиск удаленных" + " аккаунтов..." + ), + "deleted": ( + "🫥 Удалено {} удаленных" + " аккаунтов" + ), + "fcleaning": ( + "🫥 Поиск удаленных" + " аккаунтов в федерации..." + ), + "btn_unban": "🔓 Разбанить (админ)", + "btn_unmute": "🔈 Размутить (админ)", + "btn_unwarn": "♻️ Удалить предупреждение (админ)", + "btn_funmute": "🔈 Размутить в федерации (админ)", + "btn_funban": "🔓 Разбанить в федерации (админ)", + "btn_mute": "🙊 Мут", + "btn_ban": "🔒 Бан", + "btn_fban": "💼 Фед. бан", + "btn_del": "🗑 Удалить", + "sync": ( + "🔄 Принудительная синхронизация федераций и чатов с сервером..." + ), + "sync_complete": "😌 Сихнронизирован", + "rename_noargs": ( + "🚫 Укажи имя" + " федерации" + ), + "suffix_removed": "📼 Суффикс предупреждения удален", + "suffix_updated": "📼 Установлен новый суффикс предупреждения\n\n{}", + "processing_myrights": "😌 Обработка чатов", + "logchat_removed": "📲 Логирование отключено", + "logchat_invalid": ( + "🚫 Неверный чат" + " логирования" + ), + "logchat_set": "📲 Чат логирования установлен на {}", + "clnraid_args": ( + "🥷 Пример:" + " .clnraid 10" + ), + "clnraid_admin": ( + "🥷 Ошибка выдачи прав" + " боту. Убедись, что у тебя достаточно прав" + ), + "clnraid_started": ( + "🥷 RaidCleaner" + " активен..." + " Найдено {} пользователей для бана..." + ), + "clnraid_confirm": ( + "🥷 Подтвердите запуск" + " RaidCleaner на {} пользователях" + ), + "clnraid_yes": "🥷 Начать", + "banninja_report": ( + "🥷 BanNinja закончил" + " работу.\nУдалено {} бот(-ов)\n\n🏹 «BanNinja can handle any" + " size of attack» © @hikariatama" + ), + "clnraid_cancel": "🔻 Отмена", + "clnraid_stop": "🚨 Остановить", + "clnraid_complete": ( + "🥷 RaidCleaner закончил" + " работу! Удалено: {} бот(-ов)" + ), + "clnraid_cancelled": ( + "🥷 RaidCleaner" + " остановлен. Удалено: {} бот(-ов)" + ), + "confirm_rmfed_btn": "🗑 Удалить", + "decline_rmfed_btn": "🔻 Отмена", + "pil_unavailable": ( + "🚫 Библиотека Pillow" + " недоступна" + ), + "_cmd_doc_version": "Получить информацию о модуле", + "_cmd_doc_deleted": "Очистка удалнных аккаунтов в чате", + "_cmd_doc_fclean": "Очистка удаленных аккаунтов в федерации", + "_cmd_doc_newfed": " <имя> - Создать новую федерацию", + "_cmd_doc_rmfed": " - Удалить федерацию", + "_cmd_doc_fpromote": "<пользователь> - Выдать пользователю права в федерации", + "_cmd_doc_fdemote": ( + " <пользователь> - Забрать у пользователя права в федерации" + ), + "_cmd_doc_fadd": "<федерация> - Добавить чат в федерацию", + "_cmd_doc_frm": "Удалить чат из федерации", + "_cmd_doc_fban": "<пользователь> [причина] - Забанить пользователя в федерации", + "_cmd_doc_punishsuff": "Установить новый суффикс наказания", + "_cmd_doc_sethclog": "Установить чат логирования", + "_cmd_doc_funban": ( + "<пользователь> [причина] - Разбанить пользователя в федерации" + ), + "_cmd_doc_fmute": ( + "<пользователь> [причина] - Замутить пользователя в федерации" + ), + "_cmd_doc_funmute": ( + "<пользователь> [причина] - Разбанить пользователя в федерации" + ), + "_cmd_doc_kick": "<пользователь> [причина] - Кикнуть пользователя", + "_cmd_doc_ban": "<пользователь> [причина] - Забанить пользователя", + "_cmd_doc_mute": "<пользователь> [время] [причина] - Замутить пользователя", + "_cmd_doc_unmute": "<пользователь> - Размутить пользователя", + "_cmd_doc_unban": "<пользователь> - Разбанить пользователя", + "_cmd_doc_protects": "Показать доступные защиты", + "_cmd_doc_feds": "Показать федерации", + "_cmd_doc_fed": " - Информация о федерации", + "_cmd_doc_pchat": "Показать защиты в чате", + "_cmd_doc_warn": "<пользователь> - Предупредить пользователя", + "_cmd_doc_warns": ( + "[пользователь] - Показать предупреждения в чате \\ у пользователя" + ), + "_cmd_doc_delwarn": "<пользователь> - Простить последнее предупреждение", + "_cmd_doc_clrwarns": ( + "<пользователь> - Простить все предупреждения пользователя" + ), + "_cmd_doc_clrallwarns": "Простить все предупреждения в федерации", + "_cmd_doc_welcome": " - Изменить текст приветствовия", + "_cmd_doc_fdef": ( + "<пользователь> - Включить\\выключить федеративную защиту пользователя" + ), + "_cmd_doc_fsave": " - Сохранить федеративную заметку", + "_cmd_doc_fstop": " - Удалить федеративную заметку", + "_cmd_doc_fnotes": "Показать федеративные заметки", + "_cmd_doc_fdeflist": "Показать федеративный список защиты", + "_cmd_doc_dmute": "Удалить и замутить", + "_cmd_doc_dban": "Удалить и забанить", + "_cmd_doc_dwarn": "Удалить и предупредить", + "_cmd_doc_fsync": "Принудительная синхронизация федераций и чатов с сервером", + "_cmd_doc_frename": "Переименовать федерацию", + "_cmd_doc_myrights": "Показать все права администратора во всех чатах", + "action": "<действие>", + "configure": "Настроить", + "toggle": "Включить\\выключить", + "fed": ( + "💼 Федерация" + ' "{}":\n🔰' + " Чаты:\n{}\n🔰 Каналы:\n{}\n🔰" + " Админы:\n{}\n🔰 Предупреждения: {}\n" + ), + "confirm_rmfed": ( + "⚠️ Внимание! Это действие нельзя отменить! Ты уверен, что хочешь" + " удалить федерацию {}?" + ), + "_cls_doc": "Must-have модуль администратора чата", + "no_notes": ( + "🚫 Нет заметок" + ), + } + + def __init__(self): + self._punish_queue = [] + self._raid_cleaners = [] + self._global_queue = [] + self._captcha_db = {} + self._captcha_messages = {} + self._ban_ninja = {} + self._ban_ninja_messages = [] + self._ban_ninja_forms = {} + self._ban_ninja_progress = {} + self._ban_ninja_tasks = {} + self._ban_ninja_default_rights = {} + self.flood_timeout = FLOOD_TIMEOUT + self.flood_threshold = FLOOD_TRESHOLD + self._my_protects = {} + self._linked_channels = {} + self._sticks_ratelimit = {} + self._flood_fw_protection = {} + self._ratelimit = {"notes": {}, "report": {}} + self._delete_soon = [] + self._gban_cache = {} + + self.config = loader.ModuleConfig( + loader.ConfigValue( + "silent", + False, + lambda: "Do not notify about protections actions", + validator=loader.validators.Boolean(), + ), + loader.ConfigValue( + "join_ratelimit", + 10, + lambda: ( + "How many users per minute need to join until BanNinja activates" + ), + validator=loader.validators.Integer(minimum=1), + ), + loader.ConfigValue( + "banninja_cooldown", + 300, + lambda: "How long is BanNinja supposed to be active in seconds", + validator=loader.validators.Integer(minimum=15), + ), + loader.ConfigValue( + "warns_limit", + 7, + lambda: "How many warns can be issued before ban", + validator=loader.validators.Integer(minimum=1), + ), + loader.ConfigValue( + "close_on_raid", + True, + lambda: "Close chat on raid with active BanNinja", + validator=loader.validators.Boolean(), + ), + ) + + def render_table(self, t: typing.List[typing.List[str]]) -> bytes: + table = gen_table(t) + + fnt = ImageFont.truetype(io.BytesIO(self.font), 20, encoding="utf-8") + + def get_t_size(text, fnt): + if "\n" not in text: + return fnt.getsize(text) + + w, h = 0, 0 + + for line in text.split("\n"): + line_size = fnt.getsize(line) + if line_size[0] > w: + w = line_size[0] + + h += line_size[1] + + w += 10 + h += 10 + return (w, h) + + t_size = get_t_size(table, fnt) + img = Image.new("RGB", t_size, (30, 30, 30)) + + d = ImageDraw.Draw(img) + d.text((5, 5), table, font=fnt, fill=(200, 200, 200)) + + imgByteArr = io.BytesIO() + img.save(imgByteArr, format="PNG") + imgByteArr = imgByteArr.getvalue() + + return imgByteArr + + async def on_unload(self): + with contextlib.suppress(Exception): + self.api._task.cancel() + + with contextlib.suppress(Exception): + self._pt_task.cancel() + + with contextlib.suppress(Exception): + self.api._processor_task.cancel() + + with contextlib.suppress(Exception): + for _, form in self._ban_ninja_forms.items(): + with contextlib.suppress(Exception): + await form.delete() + + def lookup(self, modname: str): + return next( + ( + mod + for mod in self.allmodules.modules + if mod.name.lower() == modname.lower() + ), + False, + ) + + async def check_admin( + self, + chat_id: typing.Union[Chat, Channel, int], + user_id: typing.Union[User, int], + ) -> bool: + """ + Checks if user is admin in target chat + """ + try: + return (await self._client.get_permissions(chat_id, user_id)).is_admin + # We could've ignored only ValueError to check + # entity for validity, but there are many errors + # possible to occur, so we ignore all of them, bc + # actually we don't give a fuck about 'em + except Exception: + return ( + user_id in self._client.dispatcher.security._owner + or user_id in self._client.dispatcher.security._sudo + ) + + def chat_command(function) -> FunctionType: + """ + Decorator to allow execution of certain commands in chat only + """ + + @functools.wraps(function) + async def wrapped(*args, **kwargs): + if len(args) < 2 or not isinstance(args[1], Message): + return await function(*args, **kwargs) + + if args[1].is_private: + await utils.answer(args[1], args[0].strings("chat_only")) + return + + return await function(*args, **kwargs) + + wrapped.__doc__ = function.__doc__ + wrapped.__module__ = function.__module__ + + return wrapped + + def error_handler(function) -> FunctionType: + """ + Decorator to handle functions' errors + """ + + @functools.wraps(function) + async def wrapped(*args, **kwargs): + try: + return await function(*args, **kwargs) + except Exception: + logger.exception("Exception caught in HikariChat") + + if function.__name__.startswith("p__"): + return + + if function.__name__ == "watcher": + return + + wrapped.__doc__ = function.__doc__ + wrapped.__module__ = function.__module__ + + return wrapped + + async def get_config(self, chat: typing.Union[str, int]) -> tuple: + info = self.api.chats[str(chat)] + cinfo = await self._client.get_entity(int(chat)) + + answer_message = ( + f"🪆 HikariChat protection\n{get_full_name(cinfo)}\n\n" + ) + + protections = { + key: value + for key, value in PROTECTS.items() + if key in self.api.variables["protections"] + } + + btns = [] + for protection, style in protections.items(): + answer_message += ( + f" {style}: {info[protection][0]}\n" + if protection in info + else "" + ) + style = style if protection in info else style[2:] + btns += [ + { + "text": style, + "callback": self._change_protection_state, + "args": (chat, protection), + } + ] + + fed = None + for info in self.api.feds.values(): + if str(chat) in info["chats"]: + fed = info + + answer_message += ( + f"\n💼 {fed['name']}" + if fed + else "" + ) + + btns = utils.chunks(btns, 3) + [[{"text": "❌ Close", "action": "close"}]] + + return {"text": answer_message, "reply_markup": btns} + + async def _inline_config(self, call: CallbackQuery, chat: typing.Union[str, int]): + await call.edit(**(await self.get_config(chat))) + + async def _change_protection_state( + self, + call: CallbackQuery, + chat: typing.Union[str, int], + protection: str, + state: typing.Optional[str] = None, + ): + if protection == "welcome": + await call.answer("Use .welcome to configure this option!", show_alert=True) + return + + if protection in self.api.variables["argumented_protects"]: + if state is None: + cinfo = await self._client.get_entity(int(chat)) + markup = utils.chunks( + [ + { + "text": "🔒 Ban", + "callback": self._change_protection_state, + "args": (chat, protection, "ban"), + }, + { + "text": "🙊 Mute", + "callback": self._change_protection_state, + "args": (chat, protection, "mute"), + }, + { + "text": "🤕 Warn", + "callback": self._change_protection_state, + "args": (chat, protection, "warn"), + }, + { + "text": "🚪 Kick", + "callback": self._change_protection_state, + "args": (chat, protection, "kick"), + }, + { + "text": "😶‍🌫️ Delmsg", + "callback": self._change_protection_state, + "args": (chat, protection, "delmsg"), + }, + { + "text": "🚫 Off", + "callback": self._change_protection_state, + "args": (chat, protection, "off"), + }, + ], + 3, + ) + [ + [ + { + "text": "🔙 Back", + "callback": self._inline_config, + "args": (chat,), + } + ] + ] + current_state = ( + "off" + if protection not in self.api.chats[str(chat)] + else self.api.chats[str(chat)][protection][0] + ) + await call.edit( + ( + f"🌁 {get_full_name(cinfo)}:" + f" {PROTECTS[protection]} (now: {current_state})" + ), + reply_markup=markup, + ) + else: + self.api.request( + { + "action": "update protections", + "args": { + "chat": chat, + "protection": protection, + "state": state, + }, + } + ) + await call.answer("Configuration value saved") + if state != "off": + self.api.chats[str(chat)][protection] = [state, str(self._tg_id)] + else: + del self.api.chats[str(chat)][protection] + + await self._inline_config(call, chat) + else: + current_state = protection in self.api.chats[str(chat)] + self.api.request( + { + "action": "update protections", + "args": { + "chat": chat, + "protection": protection, + "state": "off" if current_state else "on", + }, + } + ) + + await call.answer( + f"{PROTECTS[protection]} -> {'off' if current_state else 'on'}" + ) + + if current_state: + del self.api.chats[str(chat)][protection] + else: + self.api.chats[str(chat)][protection] = ["on", str(self._tg_id)] + await self._inline_config(call, chat) + + @error_handler + async def protect(self, message: Message, protection: str): + """ + Protection toggle handler + """ + args = utils.get_args_raw(message) + chat = utils.get_chat_id(message) + + await self._promote_bot(chat) + + if protection in self.api.variables["argumented_protects"]: + if args not in self.api.variables["protect_actions"] or args == "off": + args = "off" + await utils.answer(message, self.strings(f"{protection}_off")) + else: + await utils.answer( + message, + self.strings(f"{protection}_on").format(args), + ) + elif args == "on": + await utils.answer(message, self.strings(f"{protection}_on")) + elif args == "off": + await utils.answer( + message, + self.strings(f"{protection}_off").format(args), + ) + else: + await utils.answer(message, self.strings("usage").format(protection)) + return + + self.api.request( + { + "action": "update protections", + "args": {"protection": protection, "state": args, "chat": chat}, + }, + message, + ) + + def protection_template(self, protection: str) -> FunctionType: + """ + Template for protection toggler + For internal use only + """ + comments = self.api.variables["named_protects"] + func_name = f"{protection}cmd" + function = functools.partial(self.protect, protection=protection) + function.__module__ = self.__module__ + function.__name__ = func_name + function.__self__ = self + + args = ( + self.strings("action") + if protection in self.api.variables["argumented_protects"] + else "" + ) + + action = ( + self.strings("configure") + if protection in self.api.variables["argumented_protects"] + else self.strings("toggle") + ) + + function.__doc__ = f"{args} - {action} {comments[protection]}" + return function + + @staticmethod + def convert_time(t: str) -> int: + """ + Tries to export time from text + """ + try: + if not str(t)[:-1].isdigit(): + return 0 + + if "d" in str(t): + t = int(t[:-1]) * 60 * 60 * 24 + + if "h" in str(t): + t = int(t[:-1]) * 60 * 60 + + if "m" in str(t): + t = int(t[:-1]) * 60 + + if "s" in str(t): + t = int(t[:-1]) + + t = int(re.sub(r"[^0-9]", "", str(t))) + except ValueError: + return 0 + + return t + + async def ban( + self, + chat: typing.Union[Chat, int], + user: typing.Union[User, Channel, int], + period: int = 0, + reason: str = None, + message: typing.Optional[Message] = None, + silent: bool = False, + ): + """Ban user in chat""" + if str(user).isdigit(): + user = int(user) + + if reason is None: + reason = self.strings("no_reason") + + try: + await self.inline.bot.kick_chat_member( + int(f"-100{getattr(chat, 'id', chat)}"), + int(getattr(user, "id", user)), + ) + except Exception: + logger.debug("Can't ban with bot", exc_info=True) + + await self._client.edit_permissions( + chat, + user, + until_date=(time.time() + period) if period else 0, + **BANNED_RIGHTS, + ) + + if silent: + return + + msg = self.strings("ban").format( + utils.get_link(user), + get_full_name(user), + f"for {period // 60} min(-s)" if period else "forever", + reason, + self.get("punish_suffix", ""), + ) + + if self._is_inline: + if self.get("logchat"): + if not isinstance(chat, (Chat, Channel)): + chat = await self._client.get_entity(chat) + + await self.inline.form( + message=self.get("logchat"), + text=self.strings("ban_log").format( + utils.get_link(user), + get_full_name(user), + f"for {period // 60} min(-s)" if period else "forever", + utils.get_link(chat), + get_full_name(chat), + reason, + "", + ), + reply_markup={ + "text": self.strings("btn_unban"), + "data": ( + f"ub/{chat.id if isinstance(chat, (Chat, Channel)) else chat}/{user.id}" + ), + }, + silent=True, + ) + + if isinstance(message, Message): + await utils.answer(message, msg) + else: + await self._client.send_message(chat.id, msg) + else: + await self.inline.form( + message=( + message + if isinstance(message, Message) + else getattr(chat, "id", chat) + ), + text=msg, + reply_markup={ + "text": self.strings("btn_unban"), + "data": ( + f"ub/{chat.id if isinstance(chat, (Chat, Channel)) else chat}/{user.id}" + ), + }, + silent=True, + ) + else: + await (utils.answer if message else self._client.send_message)( + message or chat.id, msg + ) + + async def mute( + self, + chat: typing.Union[Chat, int], + user: typing.Union[User, Channel, int], + period: int = 0, + reason: str = None, + message: typing.Optional[Message] = None, + silent: bool = False, + ): + """Mute user in chat""" + if str(user).isdigit(): + user = int(user) + + if reason is None: + reason = self.strings("no_reason") + + try: + await self.inline.bot.restrict_chat_member( + int(f"-100{getattr(chat, 'id', chat)}"), + int(getattr(user, "id", user)), + permissions=ChatPermissions(can_send_messages=False), + until_date=time.time() + period, + ) + except Exception: + logger.debug("Can't mute with bot", exc_info=True) + + await self._client.edit_permissions( + chat, + user, + until_date=time.time() + period, + send_messages=False, + ) + + if silent: + return + + msg = self.strings("mute").format( + utils.get_link(user), + get_full_name(user), + f"for {period // 60} min(-s)" if period else "forever", + reason, + self.get("punish_suffix", ""), + ) + + if self._is_inline: + if self.get("logchat"): + if not isinstance(chat, (Chat, Channel)): + chat = await self._client.get_entity(chat) + + await self.inline.form( + message=self.get("logchat"), + text=self.strings("mute_log").format( + utils.get_link(user), + get_full_name(user), + f"for {period // 60} min(-s)" if period else "forever", + utils.get_link(chat), + get_full_name(chat), + reason, + "", + ), + reply_markup={ + "text": self.strings("btn_unmute"), + "data": ( + f"um/{chat.id if isinstance(chat, (Chat, Channel)) else chat}/{user.id}" + ), + }, + silent=True, + ) + + if isinstance(message, Message): + await utils.answer(message, msg) + else: + await self._client.send_message(chat.id, msg) + else: + await self.inline.form( + message=( + message + if isinstance(message, Message) + else getattr(chat, "id", chat) + ), + text=msg, + reply_markup={ + "text": self.strings("btn_unmute"), + "data": ( + f"um/{chat.id if isinstance(chat, (Chat, Channel)) else chat}/{user.id}" + ), + }, + silent=True, + ) + else: + await (utils.answer if message else self._client.send_message)( + message or chat.id, msg + ) + + @loader.inline_everyone + async def actions_callback_handler(self, call: CallbackQuery): + """ + Handles unmute, unban, unwarn etc. button clicks + """ + if not re.match(r"[fbmudw]{1,3}\/[-0-9]+\/[-#0-9]+", call.data): + return + + action, chat, user = call.data.split("/") + + msg_id = None + + try: + user, msg_id = user.split("#") + msg_id = int(msg_id) + except Exception: + pass + + chat, user = int(chat), int(user) + + if not await self.check_admin(chat, call.from_user.id): + await call.answer("You are not admin") + return + + try: + user = await self._client.get_entity(user) + except Exception: + await call.answer("Unable to resolve entity") + return + + try: + adm = await self._client.get_entity(call.from_user.id) + except Exception: + await call.answer("Unable to resolve admin entity") + return + + p = ( + await self._client(GetParticipantRequest(chat, call.from_user.id)) + ).participant + + owner = isinstance(p, ChannelParticipantCreator) + + if action == "ub": + if not owner and not p.admin_rights.ban_users: + await call.answer("Not enough rights!") + return + + await self._client.edit_permissions( + chat, + user, + until_date=0, + **{right: True for right in BANNED_RIGHTS.keys()}, + ) + msg = self.strings("inline_unbanned").format( + utils.get_link(user), + get_full_name(user), + utils.get_link(adm), + get_full_name(adm), + ) + try: + await self.inline.bot.edit_message_text( + msg, + inline_message_id=call.inline_message_id, + parse_mode="HTML", + disable_web_page_preview=False, + ) + except Exception: + await self._client.send_message(chat, msg) + elif action == "um": + if not owner and not p.admin_rights.ban_users: + await call.answer("Not enough rights!") + return + + await self._client.edit_permissions( + chat, + user, + until_date=0, + send_messages=True, + ) + msg = self.strings("inline_unmuted").format( + utils.get_link(user), + get_full_name(user), + utils.get_link(adm), + get_full_name(adm), + ) + try: + await self.inline.bot.edit_message_text( + msg, + inline_message_id=call.inline_message_id, + parse_mode="HTML", + disable_web_page_preview=False, + ) + except Exception: + await self._client.send_message(chat, msg) + elif action == "dw": + if not owner and not p.admin_rights.ban_users: + await call.answer("Not enough rights!") + return + + fed = await self.find_fed(chat) + + self.api.request( + { + "action": "forgive user warn", + "args": {"uid": self.api.feds[fed]["uid"], "user": user.id}, + } + ) + + msg = self.strings("inline_unwarned").format( + utils.get_link(user), + get_full_name(user), + utils.get_link(adm), + get_full_name(adm), + ) + + try: + await self.inline.bot.edit_message_text( + msg, + inline_message_id=call.inline_message_id, + parse_mode="HTML", + disable_web_page_preview=False, + ) + except Exception: + await self._client.send_message(chat, msg) + elif action == "ufb": + if not owner and not p.admin_rights.ban_users: + await call.answer("Not enough rights!") + return + + m = await self._client.send_message( + chat, f"{self.get_prefix()}funban {user.id}" + ) + await self.funbancmd(m) + await m.delete() + msg = self.strings("inline_funbanned").format( + utils.get_link(user), + get_full_name(user), + utils.get_link(adm), + get_full_name(adm), + ) + try: + await self.inline.bot.edit_message_text( + msg, + inline_message_id=call.inline_message_id, + parse_mode="HTML", + disable_web_page_preview=False, + ) + except Exception: + await self._client.send_message(chat, msg) + elif action == "ufm": + if not owner and not p.admin_rights.ban_users: + await call.answer("Not enough rights!") + return + + m = await self._client.send_message( + chat, f"{self.get_prefix()}funmute {user.id}" + ) + await self.funmutecmd(m) + await m.delete() + msg = self.strings("inline_funmuted").format( + utils.get_link(user), + get_full_name(user), + utils.get_link(adm), + get_full_name(adm), + ) + try: + await self.inline.bot.edit_message_text( + msg, + inline_message_id=call.inline_message_id, + parse_mode="HTML", + disable_web_page_preview=False, + ) + except Exception: + await self._client.send_message(chat, msg) + elif action == "fb": + if not owner and not p.admin_rights.ban_users: + await call.answer("Not enough rights!") + return + + m = await self._client.send_message( + chat, f"{self.get_prefix()}fban {user.id}" + ) + await self.fbancmd(m) + await m.delete() + msg = self.strings("inline_fbanned").format( + utils.get_link(user), + get_full_name(user), + utils.get_link(adm), + get_full_name(adm), + ) + try: + await self.inline.bot.edit_message_text( + msg, + inline_message_id=call.inline_message_id, + parse_mode="HTML", + disable_web_page_preview=False, + ) + except Exception: + await self._client.send_message(chat, msg) + elif action == "m": + if not owner and not p.admin_rights.ban_users: + await call.answer("Not enough rights!") + return + + await self.mute(chat, user, 0, silent=True) + msg = self.strings("inline_muted").format( + utils.get_link(user), + get_full_name(user), + utils.get_link(adm), + get_full_name(adm), + ) + try: + await self.inline.bot.edit_message_text( + msg, + inline_message_id=call.inline_message_id, + parse_mode="HTML", + disable_web_page_preview=False, + ) + except Exception: + await self._client.send_message(chat, msg) + elif action == "d": + if not owner and not p.admin_rights.delete_messages: + await call.answer("Not enough rights!") + return + + msg = self.strings("inline_deleted").format( + utils.get_link(adm), + get_full_name(adm), + ) + + await self.inline.bot.edit_message_text( + msg, + inline_message_id=call.inline_message_id, + parse_mode="HTML", + disable_web_page_preview=False, + ) + else: + return + + if msg_id is not None: + await self._client.delete_messages(chat, message_ids=[msg_id]) + + async def args_parser( + self, + message: Message, + include_force: bool = False, + include_silent: bool = False, + ) -> tuple: + """Get args from message""" + args = " " + utils.get_args_raw(message) + if include_force and " -f" in args: + force = True + args = args.replace(" -f", "") + else: + force = False + + if include_silent and " -s" in args: + silent = True + args = args.replace(" -s", "") + else: + silent = False + + args = args.strip() + + reply = await message.get_reply_message() + + if reply and not args: + return ( + (await self._client.get_entity(reply.sender_id)), + 0, + utils.escape_html(self.strings("no_reason")).strip(), + *((force,) if include_force else []), + *((silent,) if include_silent else []), + ) + + try: + a = args.split()[0] + if str(a).isdigit(): + a = int(a) + user = await self._client.get_entity(a) + except Exception: + try: + user = await self._client.get_entity(reply.sender_id) + except Exception: + return False + + t = ([arg for arg in args.split() if self.convert_time(arg)] or ["0"])[0] + args = args.replace(t, "").replace(" ", " ") + t = self.convert_time(t) + + if not reply: + try: + args = " ".join(args.split()[1:]) + except Exception: + pass + + if time.time() + t >= 2208978000: # 01.01.2040 00:00:00 + t = 0 + + return ( + user, + t, + utils.escape_html(args or self.strings("no_reason")).strip(), + *((force,) if include_force else []), + *((silent,) if include_silent else []), + ) + + async def find_fed(self, message: typing.Union[Message, int]) -> str: + """Find if chat belongs to any federation""" + return next( + ( + federation + for federation, info in self.api.feds.items() + if str( + utils.get_chat_id(message) + if isinstance(message, Message) + else message + ) + in list(map(str, info["chats"])) + ), + None, + ) + + @error_handler + async def punish( + self, + chat_id: int, + user: typing.Union[int, Channel, User], + violation: str, + action: str, + user_name: str, + fulltime: bool = False, + message: Message = None, + ): + """ + Callback, called if the protection is triggered + Queue is being used to prevent spammy behavior + It is being processed in a loop `_punish_queue_handler` + """ + self._punish_queue += [ + [chat_id, user, violation, action, user_name, fulltime, message] + ] + + @error_handler + async def purgecmd(self, message: Message): + """[user(-s)] - Clean message history starting from replied one""" + if not message.is_reply: + await utils.answer(message, self.strings("from_where", message)) + return + + from_users = set() + args = utils.get_args(message) + + for arg in args: + try: + entity = await message.client.get_entity(arg) + + if isinstance(entity, User): + from_users.add(entity.id) + except ValueError: + pass + + messages = [] + + async for msg in self._client.iter_messages( + entity=message.peer_id, + min_id=message.reply_to_msg_id - 1, + reverse=True, + ): + logger.debug(msg) + if (not from_users or msg.sender_id in from_users) and ( + not getattr(message.reply_to, "forum_topic", False) + or msg.reply_to + and (msg.reply_to.reply_to_top_id or msg.reply_to.reply_to_msg_id) + == ( + message.reply_to.reply_to_top_id or message.reply_to.reply_to_msg_id + ) + ): + messages += [msg.id] + + if len(messages) >= 99: + await self._client.delete_messages(message.peer_id, messages) + messages.clear() + + if messages: + await self._client.delete_messages(message.peer_id, messages) + + async def delcmd(self, message): + """Delete the replied message""" + await self._client.delete_messages( + message.peer_id, + [ + ( + ( + await self._client.iter_messages( + message.peer_id, 1, max_id=message.id + ).__anext__() + ) + if not message.is_reply + else (await message.get_reply_message()) + ).id, + message.id, + ], + ) + + @loader.loop(interval=0.5, autostart=True) + async def _punish_queue_handler(self): + while self._punish_queue: + ( + chat_id, + user, + violation, + action, + user_name, + fulltime, + message, + ) = self._punish_queue.pop() + if str(chat_id) not in self._flood_fw_protection: + self._flood_fw_protection[str(chat_id)] = {} + + if ( + self._flood_fw_protection[str(chat_id)].get(str(user.id), 0) + >= time.time() + ): + continue + + comment = None + + if action == "ban": + comment = "banned him" + await self.ban( + chat_id, + user, + 0, + violation, + silent=str(chat_id) in self._ban_ninja or self.config["silent"], + ) + elif action == "fban": + comment = "f-banned him" + await self.fbancmd( + await self._client.send_message( + chat_id, + f"{self.get_prefix()}fban {user.id} {violation}", + ) + ) + elif action == "delmsg": + # Do nothing... + ... + elif action == "kick": + comment = "kicked him" + await self._client.kick_participant(chat_id, user) + elif action == "mute": + if fulltime: + comment = "muted him forever" + await self.mute( + chat_id, + user, + 0, + violation, + silent=str(chat_id) in self._ban_ninja or self.config["silent"], + ) + else: + comment = "muted him for 1 hour" + await self.mute( + chat_id, + user, + 60 * 60, + violation, + silent=str(chat_id) in self._ban_ninja or self.config["silent"], + ) + elif action == "warn": + comment = "warned him" + warn_msg = await self._client.send_message( + chat_id, f".warn {user.id} {violation}" + ) + await self.allmodules.commands["warn"](warn_msg) + await warn_msg.delete() + + if message is not None: + try: + await self.inline.bot.delete_message( + int(f"-100{chat_id}"), + message.id, + ) + except Exception: + with contextlib.suppress(Exception): + await message.delete() + + if not comment: + continue + + if str(chat_id) not in self._ban_ninja and not self.config["silent"]: + self._flood_fw_protection[str(chat_id)][str(user.id)] = round( + time.time() + 10 + ) + await self._client.send_message( + chat_id, + self.strings(violation).format( + utils.get_link(user), + user_name, + comment, + ), + ) + + @error_handler + async def versioncmd(self, message: Message): + """Get module info""" + await utils.answer( + message, + self.strings("version").format( + ver, + ( + "✅ Connected" + if self.api._connected + else ("🔁 Connecting..." if self.api._inited else "🗃 Local") + ), + ), + ) + + @error_handler + @chat_command + async def deletedcmd(self, message: Message): + """Remove deleted accounts from chat""" + chat = await message.get_chat() + + if not chat.admin_rights and not chat.creator: + await utils.answer(message, self.strings("not_admin")) + return + + kicked = 0 + + message = await utils.answer(message, self.strings("cleaning")) + + async for user in self._client.iter_participants(chat): + if user.deleted: + try: + await self._client.kick_participant(chat, user) + await self._client.edit_permissions( + chat, + user, + until_date=0, + **{right: True for right in BANNED_RIGHTS.keys()}, + ) + kicked += 1 + except Exception: + pass + + await utils.answer(message, self.strings("deleted").format(kicked)) + + @error_handler + @chat_command + async def fcleancmd(self, message: Message): + """Remove deleted accounts from federation""" + fed = await self.find_fed(message) + + if not fed: + await utils.answer(message, self.strings("no_fed")) + return + + chats = self.api.feds[fed]["chats"] + cleaned_in = [] + cleaned_in_c = [] + + message = await utils.answer(message, self.strings("fcleaning")) + + overall = 0 + + for c in chats: + try: + if str(c).isdigit(): + c = int(c) + chat = await self._client.get_entity(c) + except Exception: + continue + + if not chat.admin_rights and not chat.creator: + continue + + try: + kicked = 0 + async for user in self._client.iter_participants(chat): + if user.deleted: + try: + await self._client.kick_participant(chat, user) + await self._client.edit_permissions( + chat, + user, + until_date=0, + **{right: True for right in BANNED_RIGHTS.keys()}, + ) + kicked += 1 + except Exception: + pass + + overall += kicked + cleaned_in += [ + "👥 {utils.escape_html(chat.title)}' + f" - {kicked}" + ] + except UserAdminInvalidError: + pass + + if str(c) in self._linked_channels and self._linked_channels[str(c)]: + channel = await self._client.get_entity(self._linked_channels[str(c)]) + kicked = 0 + try: + async for user in self._client.iter_participants( + self._linked_channels[str(c)] + ): + if user.deleted: + try: + await self._client.kick_participant( + self._linked_channels[str(c)], + user, + ) + await self._client.edit_permissions( + self._linked_channels[str(c)], + user, + until_date=0, + **{right: True for right in BANNED_RIGHTS.keys()}, + ) + kicked += 1 + except Exception: + pass + + overall += kicked + cleaned_in_c += [ + "📣 {utils.escape_html(channel.title)}' + f" - {kicked}" + ] + except ChatAdminRequiredError: + pass + + await utils.answer( + message, + self.strings("deleted").format(overall) + + "\n\n" + + "\n".join(cleaned_in) + + "" + + "\n\n" + + "\n".join(cleaned_in_c) + + "", + ) + + @error_handler + @chat_command + async def newfedcmd(self, message: Message): + """ - Create new federation""" + args = utils.get_args_raw(message) + if not args or args.count(" ") == 0: + await utils.answer(message, self.strings("args")) + return + + shortname, name = args.split(maxsplit=1) + if shortname in self.api.feds: + await utils.answer(message, self.strings("fedexists")) + return + + self.api.request( + { + "action": "create federation", + "args": {"shortname": shortname, "name": name}, + }, + message, + ) + + await utils.answer(message, self.strings("newfed").format(name)) + + async def inline__confirm_rmfed(self, call: CallbackQuery, args: str): + name = self.api.feds[args]["name"] + + self.api.request( + {"action": "delete federation", "args": {"uid": self.api.feds[args]["uid"]}} + ) + + await call.edit(self.strings("rmfed").format(name)) + + @error_handler + @chat_command + async def rmfedcmd(self, message: Message): + """ - Remove federation""" + args = utils.get_args_raw(message) + if not args: + await utils.answer(message, self.strings("args")) + return + + if args not in self.api.feds: + await utils.answer(message, self.strings("fed404")) + return + + await self.inline.form( + self.strings("confirm_rmfed").format( + utils.escape_html(self.api.feds[args]["name"]) + ), + message=message, + reply_markup=[ + { + "text": self.strings("confirm_rmfed_btn"), + "callback": self.inline__confirm_rmfed, + "args": (args,), + }, + { + "text": self.strings("decline_rmfed_btn"), + "action": "close", + }, + ], + silent=True, + ) + + @error_handler + @chat_command + async def fpromotecmd(self, message: Message): + """ - Promote user in federation""" + fed = await self.find_fed(message) + + if not fed: + await utils.answer(message, self.strings("no_fed")) + return + + reply = await message.get_reply_message() + args = utils.get_args_raw(message) + if not reply and not args: + await utils.answer(message, self.strings("args")) + return + + user = reply.sender_id if reply else args + try: + try: + if str(user).isdigit(): + user = int(user) + obj = await self._client.get_entity(user) + except Exception: + await utils.answer(message, self.strings("args")) + return + + name = get_full_name(obj) + except Exception: + await utils.answer(message, self.strings("args")) + return + + self.api.request( + { + "action": "promote user in federation", + "args": {"uid": self.api.feds[fed]["uid"], "user": obj.id}, + }, + message, + ) + + await utils.answer( + message, + self.strings("fpromoted").format( + utils.get_link(obj), + name, + self.api.feds[fed]["name"], + ), + ) + + @error_handler + @chat_command + async def fdemotecmd(self, message: Message): + """ - Demote user in federation""" + fed = await self.find_fed(message) + + if not fed: + await utils.answer(message, self.strings("no_fed")) + return + + reply = await message.get_reply_message() + args = utils.get_args_raw(message) + if not reply and not args: + await utils.answer(message, self.strings("args")) + return + + user = reply.sender_id if reply else args + try: + try: + if str(user).isdigit(): + user = int(user) + obj = await self._client.get_entity(user) + except Exception: + await utils.answer(message, self.strings("args")) + return + + user = obj.id + + name = get_full_name(obj) + except Exception: + logger.exception("Parsing entity exception") + name = "User" + + self.api.request( + { + "action": "demote user in federation", + "args": {"uid": self.api.feds[fed]["uid"], "user": obj.id}, + }, + message, + ) + + await utils.answer( + message, + self.strings("fdemoted").format( + user, + name, + self.api.feds[fed]["name"], + ), + ) + + @error_handler + @chat_command + async def faddcmd(self, message: Message): + """ - Add chat to federation""" + args = utils.get_args_raw(message) + if not args: + await utils.answer(message, self.strings("args")) + return + + if args not in self.api.feds: + await utils.answer(message, self.strings("fed404")) + return + + chat = utils.get_chat_id(message) + + self.api.request( + { + "action": "add chat to federation", + "args": {"uid": self.api.feds[args]["uid"], "cid": chat}, + }, + message, + ) + + await utils.answer( + message, + self.strings("fadded").format( + self.api.feds[args]["name"], + ), + ) + + @error_handler + @chat_command + async def frmcmd(self, message: Message): + """Remove chat from federation""" + fed = await self.find_fed(message) + if not fed: + await utils.answer(message, self.strings("fed404")) + return + + chat = utils.get_chat_id(message) + + self.api.request( + { + "action": "remove chat from federation", + "args": {"uid": self.api.feds[fed]["uid"], "cid": chat}, + }, + message, + ) + + await utils.answer( + message, + self.strings("frem").format( + self.api.feds[fed]["name"], + ), + ) + + @loader.command( + ru_doc=( + "<реплай | юзер> [причина] [-s] - Заблокировать пользователя во всех чатах," + " где ты админ" + ) + ) + async def gban(self, message: Message): + """ [reason] [-s] - Ban user in all chats where you are admin""" + reply = await message.get_reply_message() + args = utils.get_args_raw(message) + if not reply and not args: + await utils.answer(message, self.strings("args")) + return + + a = await self.args_parser(message, include_silent=True) + + if not a: + await utils.answer(message, self.strings("args")) + return + + user, t, reason, silent = a + + message = await utils.answer( + message, + self.strings("gbanning").format( + utils.get_entity_url(user), + utils.escape_html(get_full_name(user)), + ), + ) + + if not self._gban_cache or self._gban_cache["exp"] < time.time(): + self._gban_cache = { + "exp": int(time.time()) + 10 * 60, + "chats": [ + chat.entity.id + async for chat in self._client.iter_dialogs() + if ( + ( + isinstance(chat.entity, Chat) + or ( + isinstance(chat.entity, Channel) + and getattr(chat.entity, "megagroup", False) + ) + ) + and chat.entity.admin_rights + and chat.entity.participants_count > 5 + and chat.entity.admin_rights.ban_users + ) + ], + } + + chats = "" + counter = 0 + + for chat in self._gban_cache["chats"]: + try: + await self.ban(chat, user, 0, reason, silent=True) + except Exception: + pass + else: + chats += '▫️ {}\n'.format( + utils.get_entity_url(await self._client.get_entity(chat, exp=0)), + utils.escape_html( + get_full_name(await self._client.get_entity(chat, exp=0)) + ), + ) + counter += 1 + + await utils.answer( + message, + self.strings("gban").format( + utils.get_entity_url(user), + utils.escape_html(get_full_name(user)), + reason, + self.strings("in_n_chats").format(counter) if silent else chats, + ), + ) + + @loader.command( + ru_doc=( + "<реплай | юзер> [причина] [-s] - Разблокировать пользователя во всех" + " чатах, где ты админ" + ) + ) + async def gunban(self, message: Message): + """ [reason] [-s] - Unban user in all chats where you are admin""" + reply = await message.get_reply_message() + args = utils.get_args_raw(message) + if not reply and not args: + await utils.answer(message, self.strings("args")) + return + + a = await self.args_parser(message, include_silent=True) + + if not a: + await utils.answer(message, self.strings("args")) + return + + user, t, reason, silent = a + + message = await utils.answer( + message, + self.strings("gunbanning").format( + utils.get_entity_url(user), + utils.escape_html(get_full_name(user)), + ), + ) + + if not self._gban_cache or self._gban_cache["exp"] < time.time(): + self._gban_cache = { + "exp": int(time.time()) + 10 * 60, + "chats": [ + chat.entity.id + async for chat in self._client.iter_dialogs() + if ( + ( + isinstance(chat.entity, Chat) + or ( + isinstance(chat.entity, Channel) + and getattr(chat.entity, "megagroup", False) + ) + ) + and chat.entity.admin_rights + and chat.entity.participants_count > 5 + and chat.entity.admin_rights.ban_users + ) + ], + } + + chats = "" + counter = 0 + + for chat in self._gban_cache["chats"]: + try: + await self._client.edit_permissions( + chat, + user, + until_date=0, + **{right: True for right in BANNED_RIGHTS.keys()}, + ) + except Exception: + pass + else: + chats += '▫️ {}\n'.format( + utils.get_entity_url(await self._client.get_entity(chat, exp=0)), + utils.escape_html( + get_full_name(await self._client.get_entity(chat, exp=0)) + ), + ) + counter += 1 + + await utils.answer( + message, + self.strings("gunban").format( + utils.get_entity_url(user), + utils.escape_html(get_full_name(user)), + ( + self.strings("unbanned_in_n_chats").format(counter) + if silent + else chats + ), + ), + ) + + @error_handler + @chat_command + async def fbancmd(self, message: Message): + """ [reason] - Ban user in federation""" + fed = await self.find_fed(message) + + if not fed: + await utils.answer(message, self.strings("no_fed")) + return + + a = await self.args_parser(message, include_force=True) + + if not a: + await utils.answer(message, self.strings("args")) + return + + user, t, reason, force = a + + if not force and user.id in list(map(int, self.api.feds[fed]["fdef"])): + await utils.answer(message, self.strings("fdef403").format("fban")) + return + + chats = self.api.feds[fed]["chats"] + + banned_in = [] + + for c in chats: + try: + if str(c).isdigit(): + c = int(c) + chat = await self._client.get_entity(c) + except Exception: + continue + + if not chat.admin_rights and not chat.creator: + continue + + try: + await self.ban(chat, user, t, reason, message, silent=True) + banned_in += [ + f'{get_full_name(chat)}' + ] + except Exception: + pass + + msg = ( + self.strings("fban").format( + utils.get_link(user), + get_first_name(user), + self.api.feds[fed]["name"], + f"for {t // 60} min(-s)" if t else "forever", + reason, + self.get("punish_suffix", ""), + ) + + "\n\n" + + "\n".join(banned_in) + + "" + ) + + if self._is_inline: + punishment_info = { + "reply_markup": { + "text": self.strings("btn_funban"), + "data": f"ufb/{utils.get_chat_id(message)}/{user.id}", + }, + } + + if self.get("logchat"): + await utils.answer(message, msg) + await self.inline.form( + text=self.strings("fban").format( + utils.get_link(user), + get_first_name(user), + self.api.feds[fed]["name"], + f"for {t // 60} min(-s)" if t else "forever", + reason, + "", + ) + + "" + + "\n".join(banned_in) + + "", + message=self.get("logchat"), + **punishment_info, + silent=True, + ) + else: + await self.inline.form( + text=msg, message=message, **punishment_info, silent=True + ) + else: + await utils.answer(message, msg) + + self.api.request( + { + "action": "clear all user warns", + "args": { + "uid": self.api.feds[fed]["uid"], + "user": user.id, + "silent": True, + }, + }, + message, + ) + + reply = await message.get_reply_message() + if reply: + await reply.delete() + + @error_handler + @chat_command + async def punishsuffcmd(self, message: Message): + """Set new punishment suffix""" + if not utils.get_args_raw(message): + self.set("punish_suffix", "") + await utils.answer(message, self.strings("suffix_removed")) + else: + suffix = utils.get_args_raw(message) + self.set("punish_suffix", suffix) + await utils.answer(message, self.strings("suffix_updated").format(suffix)) + + @error_handler + @chat_command + async def sethclogcmd(self, message: Message): + """Set logchat""" + if not utils.get_args_raw(message): + self.set("logchat", "") + await utils.answer(message, self.strings("logchat_removed")) + return + + logchat = utils.get_args_raw(message) + if logchat.isdigit(): + logchat = int(logchat) + + try: + logchat = await self._client.get_entity(logchat) + except Exception: + await utils.answer(message, self.strings("logchat_invalid")) + return + + self.set("logchat", logchat.id) + await utils.answer( + message, + self.strings("logchat_set").format(utils.escape_html(logchat.title)), + ) + + @error_handler + @chat_command + async def funbancmd(self, message: Message): + """ [reason] - Unban user in federation""" + fed = await self.find_fed(message) + + if not fed: + await utils.answer(message, self.strings("no_fed")) + return + + a = await self.args_parser(message) + + if not a: + await utils.answer(message, self.strings("args")) + return + + user, _, _ = a + + chats = self.api.feds[fed]["chats"] + + unbanned_in = [] + + for c in chats: + try: + if str(c).isdigit(): + c = int(c) + chat = await self._client.get_entity(c) + except Exception: + continue + + if not chat.admin_rights and not chat.creator: + continue + + try: + await self._client.edit_permissions( + chat, + user, + until_date=0, + **{right: True for right in BANNED_RIGHTS.keys()}, + ) + unbanned_in += [chat.title] + except UserAdminInvalidError: + pass + + m = ( + self.strings("funban").format( + utils.get_link(user), + get_first_name(user), + self.api.feds[fed]["name"], + ) + + "" + + "\n".join(unbanned_in) + + "" + ) + + if self.get("logchat"): + await self._client.send_message(self.get("logchat"), m) + + await utils.answer(message, m) + + @error_handler + @chat_command + async def fmutecmd(self, message: Message): + """ [reason] - Mute user in federation""" + fed = await self.find_fed(message) + + if not fed: + await utils.answer(message, self.strings("no_fed")) + return + + a = await self.args_parser(message, include_force=True) + + if not a: + await utils.answer(message, self.strings("args")) + return + + user, t, reason, force = a + + if not force and user.id in list(map(int, self.api.feds[fed]["fdef"])): + await utils.answer(message, self.strings("fdef403").format("fmute")) + return + + chats = self.api.feds[fed]["chats"] + + muted_in = [] + + for c in chats: + try: + if str(c).isdigit(): + c = int(c) + chat = await self._client.get_entity(c) + except Exception: + continue + + if not chat.admin_rights and not chat.creator: + continue + + try: + await self.mute(chat, user, t, reason, message, silent=True) + muted_in += [ + f'{get_full_name(chat)}' + ] + except Exception: + pass + + msg = ( + self.strings("fmute").format( + utils.get_link(user), + get_first_name(user), + self.api.feds[fed]["name"], + f"for {t // 60} min(-s)" if t else "forever", + reason, + self.get("punish_suffix", ""), + ) + + "\n\n" + + "\n".join(muted_in) + + "" + ) + + if self._is_inline: + punishment_info = { + "reply_markup": { + "text": self.strings("btn_funmute"), + "data": f"ufm/{utils.get_chat_id(message)}/{user.id}", + }, + } + + if self.get("logchat"): + await utils.answer(message, msg) + await self.inline.form( + text=self.strings("fmute").format( + utils.get_link(user), + get_first_name(user), + self.api.feds[fed]["name"], + f"for {t // 60} min(-s)" if t else "forever", + reason, + "", + ) + + "\n\n" + + "\n".join(muted_in) + + "", + message=self.get("logchat"), + **punishment_info, + silent=True, + ) + else: + await self.inline.form( + text=msg, message=message, **punishment_info, silent=True + ) + else: + await utils.answer(message, msg) + + @error_handler + @chat_command + async def funmutecmd(self, message: Message): + """ [reason] - Unban user in federation""" + fed = await self.find_fed(message) + + if not fed: + await utils.answer(message, self.strings("no_fed")) + return + + a = await self.args_parser(message) + + if not a: + await utils.answer(message, self.strings("args")) + return + + user, _, _ = a + + chats = self.api.feds[fed]["chats"] + + unbanned_in = [] + + for c in chats: + try: + if str(c).isdigit(): + c = int(c) + chat = await self._client.get_entity(c) + except Exception: + continue + + if not chat.admin_rights and not chat.creator: + continue + + try: + await self._client.edit_permissions( + chat, + user, + until_date=0, + **{right: True for right in BANNED_RIGHTS.keys()}, + ) + unbanned_in += [chat.title] + except UserAdminInvalidError: + pass + + msg = ( + self.strings("funmute").format( + utils.get_link(user), + get_first_name(user), + self.api.feds[fed]["name"], + ) + + "\n\n" + + "\n".join(unbanned_in) + + "" + ) + + await utils.answer(message, msg) + + if self.get("logchat"): + await self._client.send_message(self.get("logchat"), msg) + + @error_handler + @chat_command + async def kickcmd(self, message: Message): + """ [reason] - Kick user""" + chat = await message.get_chat() + + if not chat.admin_rights and not chat.creator: + await utils.answer(message, self.strings("not_admin")) + return + + reply = await message.get_reply_message() + args = utils.get_args_raw(message) + user, reason = None, None + + try: + if reply: + user = await self._client.get_entity(reply.sender_id) + reason = args or self.strings + else: + uid = args.split(maxsplit=1)[0] + if str(uid).isdigit(): + uid = int(uid) + user = await self._client.get_entity(uid) + reason = ( + args.split(maxsplit=1)[1] + if len(args.split(maxsplit=1)) > 1 + else self.strings("no_reason") + ) + except Exception: + await utils.answer(message, self.strings("args")) + return + + try: + await self._client.kick_participant(utils.get_chat_id(message), user) + msg = self.strings("kick").format( + utils.get_link(user), + get_first_name(user), + reason, + self.get("punish_suffix", ""), + ) + await utils.answer(message, msg) + + if self.get("logchat"): + await self._client.send_message( + self.get("logchat"), + self.strings("kick_log").format( + utils.get_link(user), + get_first_name(user), + utils.get_link(chat), + get_first_name(chat), + reason, + "", + ), + ) + except UserAdminInvalidError: + await utils.answer(message, self.strings("not_admin")) + return + + @error_handler + @chat_command + async def bancmd(self, message: Message): + """ [reason] - Ban user""" + chat = await message.get_chat() + + a = await self.args_parser(message, include_force=True) + if not a: + await utils.answer(message, self.strings("args")) + return + + user, t, reason, force = a + + if not chat.admin_rights and not chat.creator: + await utils.answer(message, self.strings("not_admin")) + return + + fed = await self.find_fed(message) + if ( + not force + and fed in self.api.feds + and user.id in list(map(int, self.api.feds[fed]["fdef"])) + ): + await utils.answer(message, self.strings("fdef403").format("ban")) + return + + try: + await self.ban(chat, user, t, reason, message) + except UserAdminInvalidError: + await utils.answer(message, self.strings("not_admin")) + return + + @error_handler + @chat_command + async def mutecmd(self, message: Message): + """ [time] [reason] - Mute user""" + chat = await message.get_chat() + + a = await self.args_parser(message, include_force=True) + if not a: + await utils.answer(message, self.strings("args")) + return + + user, t, reason, force = a + + if not chat.admin_rights and not chat.creator: + await utils.answer(message, self.strings("not_admin")) + return + + fed = await self.find_fed(message) + if ( + not force + and fed in self.api.feds + and user.id in list(map(int, self.api.feds[fed]["fdef"])) + ): + await utils.answer(message, self.strings("fdef403").format("mute")) + return + + try: + await self.mute(chat, user, t, reason, message) + except UserAdminInvalidError: + await utils.answer(message, self.strings("not_admin")) + return + + @error_handler + @chat_command + async def unmutecmd(self, message: Message): + """ - Unmute user""" + chat = await message.get_chat() + + if not chat.admin_rights and not chat.creator: + await utils.answer(message, self.strings("not_admin")) + return + + reply = await message.get_reply_message() + args = utils.get_args_raw(message) + user = None + + try: + if args.isdigit(): + args = int(args) + user = await self._client.get_entity(args) + except Exception: + try: + user = await self._client.get_entity(reply.sender_id) + except Exception: + await utils.answer(message, self.strings("args")) + return + + try: + await self._client.edit_permissions( + chat, + user, + until_date=0, + send_messages=True, + ) + msg = self.strings("unmuted").format( + utils.get_link(user), get_first_name(user) + ) + await utils.answer(message, msg) + + if self.get("logchat"): + await self._client.send_message( + self.get("logchat"), + self.strings("unmuted_log").format( + utils.get_link(user), + get_first_name(user), + utils.get_link(chat), + get_first_name(chat), + ), + ) + except UserAdminInvalidError: + await utils.answer(message, self.strings("not_admin")) + return + + @error_handler + @chat_command + async def unbancmd(self, message: Message): + """ - Unban user""" + chat = await message.get_chat() + + if not chat.admin_rights and not chat.creator: + await utils.answer(message, self.strings("not_admin")) + return + + reply = await message.get_reply_message() + args = utils.get_args_raw(message) + user = None + + try: + if args.isdigit(): + args = int(args) + user = await self._client.get_entity(args) + except Exception: + try: + user = await self._client.get_entity(reply.sender_id) + except Exception: + await utils.answer(message, self.strings("args")) + return + + try: + await self._client.edit_permissions( + chat, + user, + until_date=0, + **{right: True for right in BANNED_RIGHTS.keys()}, + ) + msg = self.strings("unban").format( + utils.get_link(user), get_first_name(user) + ) + await utils.answer(message, msg) + + if self.get("logchat"): + await self._client.send_message( + self.get("logchat"), + self.strings("unban_log").format( + utils.get_link(user), + get_first_name(user), + utils.get_link(chat), + get_first_name(chat), + ), + ) + except UserAdminInvalidError: + await utils.answer(message, self.strings("not_admin")) + return + + @error_handler + async def protectscmd(self, message: Message): + """typing.List available filters""" + await utils.answer( + message, + ( + self.strings("protections") + if self.api._inited + else "\n".join( + [ + line + for line in self.strings("protections").splitlines() + if "antinsfw" not in line.lower() + and "report" not in line.lower() + ] + ) + ), + ) + + @error_handler + async def fedscmd(self, message: Message): + """typing.List federations""" + res = self.strings("feds_header") + + if not self.api.feds: + await utils.answer(message, self.strings("no_federations")) + return + + for shortname, config in self.api.feds.copy().items(): + res += f" ☮️ {config['name']} ({shortname})" + for chat in config["chats"]: + try: + if str(chat).isdigit(): + chat = int(chat) + c = await self._client.get_entity(chat) + except Exception: + continue + + res += ( + "\n - {c.title}" + ) + + res += ( + "\n 👮‍♀️" + f" {len(config.get('warns', []))} warns\n\n" + ) + + await utils.answer(message, res) + + @error_handler + @chat_command + async def fedcmd(self, message: Message): + """ - Info about federation""" + args = utils.get_args_raw(message) + chat = utils.get_chat_id(message) + + fed = await self.find_fed(message) + + if (not args or args not in self.api.feds) and not fed: + await utils.answer(message, self.strings("no_fed")) + return + + if not args or args not in self.api.feds: + args = fed + + res = self.strings("fed") + + fed = args + + admins = "" + for admin in self.api.feds[fed]["admins"]: + try: + if str(admin).isdigit(): + admin = int(admin) + user = await self._client.get_entity(admin) + except Exception: + continue + name = get_full_name(user) + status = ( + " 🧃 online" + if isinstance(getattr(user, "status", None), UserStatusOnline) + else "" + ) + admins += ( + f' 👤 {name}{status}\n' + ) + + chats = "" + channels = "" + for chat in self.api.feds[fed]["chats"]: + try: + if str(chat).isdigit(): + chat = int(chat) + c = await self._client.get_entity(chat) + except Exception: + continue + + if str(chat) in self._linked_channels: + try: + channel = await self._client.get_entity( + self._linked_channels[str(chat)] + ) + channels += ( + " 📣 {utils.escape_html(channel.title)}\n' + ) + except Exception: + pass + + chats += ( + " 🫂 {utils.escape_html(c.title)}\n' + ) + + await utils.answer( + message, + res.format( + self.api.feds[fed]["name"], + chats or "-", + channels or "-", + admins or "-", + len(self.api.feds[fed].get("warns", [])), + ), + ) + + @error_handler + @chat_command + async def pchatcmd(self, message: Message): + """typing.List protection for current chat""" + chat_id = utils.get_chat_id(message) + try: + await self.inline.form( + message=message, + **(await self.get_config(chat_id)), + manual_security=True, + silent=True, + ) + except KeyError: + await utils.answer(message, self.strings("no_protects")) + + @error_handler + @chat_command + async def warncmd(self, message: Message): + """ - Warn user""" + chat = await message.get_chat() + + if not chat.admin_rights and not chat.creator: + await utils.answer(message, self.strings("not_admin")) + return + + args = utils.get_args_raw(message) + + if " -f" in args: + args = args.replace(" -f", "") + force = True + else: + force = False + + reply = await message.get_reply_message() + user = None + if reply: + user = await self._client.get_entity(reply.sender_id) + reason = args or self.strings("no_reason") + else: + try: + u = args.split(maxsplit=1)[0] + if u.isdigit(): + u = int(u) + + user = await self._client.get_entity(u) + except IndexError: + await utils.answer(message, self.strings("args")) + return + + try: + reason = args.split(maxsplit=1)[1] + except IndexError: + reason = self.strings("no_reason") + + fed = await self.find_fed(message) + + if not fed: + await utils.answer(message, self.strings("no_fed")) + return + + if not force and user.id in list(map(int, self.api.feds[fed]["fdef"])): + await utils.answer(message, self.strings("fdef403").format("warn")) + return + + self.api.request( + { + "action": "warn user", + "args": { + "uid": self.api.feds[fed]["uid"], + "user": user.id, + "reason": reason, + }, + }, + message, + ) + warns = self.api.feds[fed].get("warns", {}).get(str(user.id), []) + [reason] + + if len(warns) >= self.config["warns_limit"]: + user_name = get_first_name(user) + chats = self.api.feds[fed]["chats"] + for c in chats: + if str(c).isdigit(): + c = int(str(c)) + + await self._client( + EditBannedRequest( + c, + user, + ChatBannedRights( + until_date=time.time() + 60**2 * 24 * 7, + send_messages=True, + ), + ) + ) + + if c == utils.get_chat_id(message): + await self._client.send_message( + c, + self.strings("warns_limit").format( + utils.get_link(user), + user_name, + "muted him in federation for 7 days", + ), + ) + + if message.out: + await message.delete() + + self.api.request( + { + "action": "clear all user warns", + "args": {"uid": self.api.feds[fed]["uid"], "user": user.id}, + }, + message, + ) + else: + msg = self.strings("fwarn", message).format( + utils.get_link(user), + get_first_name(user), + len(warns), + self.config["warns_limit"], + reason, + self.get("punish_suffix", ""), + ) + + if self._is_inline: + punishment_info = { + "reply_markup": { + "text": self.strings("btn_unwarn"), + "data": f"dw/{utils.get_chat_id(message)}/{user.id}", + }, + } + + if self.get("logchat"): + await utils.answer(message, msg) + await self.inline.form( + text=self.strings("fwarn", message).format( + utils.get_link(user), + get_first_name(user), + len(warns), + self.config["warns_limit"], + reason, + "", + ), + message=self.get("logchat"), + **punishment_info, + silent=True, + ) + else: + await self.inline.form( + text=msg, message=message, **punishment_info, silent=True + ) + else: + await utils.answer(message, msg) + + @error_handler + @chat_command + async def warnscmd(self, message: Message): + """[user] - Show warns in chat \\ of user""" + chat_id = utils.get_chat_id(message) + + fed = await self.find_fed(message) + + async def check_member(user_id): + try: + await self._client.get_permissions(chat_id, user_id) + return True + except Exception: + return False + + if not fed: + await utils.answer(message, self.strings("no_fed")) + return + + warns = self.api.feds[fed].get("warns", {}) + + if not warns: + await utils.answer(message, self.strings("no_fed_warns")) + return + + async def send_user_warns(usid): + try: + if int(usid) < 0: + usid = int(str(usid)[4:]) + except Exception: + pass + + if not warns: + await utils.answer(message, self.strings("no_fed_warns")) + return + + if str(usid) not in warns or not warns[str(usid)]: + user_obj = await self._client.get_entity(usid) + await utils.answer( + message, + self.strings("no_warns").format( + utils.get_link(user_obj), get_full_name(user_obj) + ), + ) + else: + user_obj = await self._client.get_entity(usid) + _warns = "" + processed = [] + for warn in warns[str(usid)].copy(): + if warn in processed: + continue + processed += [warn] + _warns += ( + "🛑 " + + warn + + ( + f" [x{warns[str(usid)].count(warn)}]" + if warns[str(usid)].count(warn) > 1 + else "" + ) + + "\n" + ) + await utils.answer( + message, + self.strings("warns").format( + utils.get_link(user_obj), + get_full_name(user_obj), + len(warns[str(usid)]), + self.config["warns_limit"], + _warns, + ), + ) + + if not await self.check_admin(chat_id, message.sender_id): + await send_user_warns(message.sender_id) + else: + reply = await message.get_reply_message() + args = utils.get_args_raw(message) + if not reply and not args: + res = self.strings("warns_adm_fed") + for user, _warns in warns.copy().items(): + try: + user_obj = await self._client.get_entity(int(user)) + except Exception: + continue + + if isinstance(user_obj, User): + try: + name = get_full_name(user_obj) + except TypeError: + continue + else: + name = user_obj.title + + res += ( + "🐺 ' + + name + + "\n" + ) + processed = [] + for warn in _warns.copy(): + if warn in processed: + continue + processed += [warn] + res += ( + " 🏴󠁧󠁢󠁥󠁮󠁧󠁿 " + + warn + + ( + f" [x{_warns.count(warn)}]" + if _warns.count(warn) > 1 + else "" + ) + + "\n" + ) + + await utils.answer(message, res) + return + elif reply: + await send_user_warns(reply.sender_id) + elif args: + await send_user_warns(args) + + @error_handler + @chat_command + async def delwarncmd(self, message: Message): + """ - Forgave last warn""" + args = utils.get_args_raw(message) + reply = await message.get_reply_message() + user = None + + if reply: + user = await self._client.get_entity(reply.sender_id) + else: + if args.isdigit(): + args = int(args) + + try: + user = await self._client.get_entity(args) + except IndexError: + await utils.answer(message, self.strings("args")) + return + + fed = await self.find_fed(message) + + if not fed: + await utils.answer(message, self.strings("no_fed")) + return + + self.api.request( + { + "action": "forgive user warn", + "args": {"uid": self.api.feds[fed]["uid"], "user": user.id}, + }, + message, + ) + + msg = self.strings("dwarn_fed").format( + utils.get_link(user), get_first_name(user) + ) + + await utils.answer(message, msg) + + if self.get("logchat", False): + await self._client.send_message(self.get("logchat"), msg) + + @error_handler + @chat_command + async def clrwarnscmd(self, message: Message): + """ - Remove all warns from user""" + args = utils.get_args_raw(message) + reply = await message.get_reply_message() + user = None + if reply: + user = await self._client.get_entity(reply.sender_id) + else: + if args.isdigit(): + args = int(args) + + try: + user = await self._client.get_entity(args) + except IndexError: + await utils.answer(message, self.strings("args")) + return + + fed = await self.find_fed(message) + + if not fed: + await utils.answer(message, self.strings("no_fed")) + return + + self.api.request( + { + "action": "clear all user warns", + "args": {"uid": self.api.feds[fed]["uid"], "user": user.id}, + }, + message, + ) + + await utils.answer( + message, + self.strings("clrwarns_fed").format( + utils.get_link(user), get_first_name(user) + ), + ) + + @error_handler + @chat_command + async def clrallwarnscmd(self, message: Message): + """Remove all warns from current federation""" + fed = await self.find_fed(message) + + if not fed: + await utils.answer(message, self.strings("no_fed")) + return + + self.api.request( + { + "action": "clear federation warns", + "args": {"uid": self.api.feds[fed]["uid"]}, + }, + message, + ) + + await utils.answer(message, self.strings("clrallwarns_fed")) + + @error_handler + @chat_command + async def welcomecmd(self, message: Message): + """ - Change welcome text""" + chat_id = utils.get_chat_id(message) + args = utils.get_args_raw(message) or "off" + + self.api.request( + { + "action": "update protections", + "args": {"protection": "welcome", "state": args, "chat": chat_id}, + }, + message, + ) + + if args and args != "off": + await utils.answer(message, self.strings("welcome").format(args)) + else: + await utils.answer(message, self.strings("unwelcome")) + + @error_handler + @chat_command + async def fdefcmd(self, message: Message): + """ - Toggle global user invulnerability""" + fed = await self.find_fed(message) + + if not fed: + await utils.answer(message, self.strings("no_fed")) + return + + args = utils.get_args_raw(message) + reply = await message.get_reply_message() + user = None + if reply: + user = await self._client.get_entity(reply.sender_id) + else: + if str(args).isdigit(): + args = int(args) + + try: + user = await self._client.get_entity(args) + except Exception: + await utils.answer(message, self.strings("args")) + return + + self.api.request( + { + "action": "protect user", + "args": {"uid": self.api.feds[fed]["uid"], "user": user.id}, + }, + message, + ) + + await utils.answer( + message, + self.strings("defense").format( + utils.get_link(user), + get_first_name(user), + "on" if str(user.id) not in self.api.feds[fed]["fdef"] else "off", + ), + ) + + @error_handler + @chat_command + async def fsavecmd(self, message: Message): + """ - Save federative note""" + fed = await self.find_fed(message) + + if not fed: + await utils.answer(message, self.strings("no_fed")) + return + + args = utils.get_args_raw(message) + reply = await message.get_reply_message() + if not reply or not args or not reply.text: + await utils.answer(message, self.strings("fsave_args")) + return + + self.api.request( + { + "action": "new note", + "args": { + "uid": self.api.feds[fed]["uid"], + "shortname": args, + "note": reply.text, + }, + }, + message, + ) + + await utils.answer(message, self.strings("fsave").format(args)) + + @error_handler + @chat_command + async def fstopcmd(self, message: Message): + """ - Remove federative note""" + fed = await self.find_fed(message) + + if not fed: + await utils.answer(message, self.strings("no_fed")) + return + + args = utils.get_args_raw(message) + if not args: + await utils.answer(message, self.strings("fstop_args")) + return + + self.api.request( + { + "action": "delete note", + "args": {"uid": self.api.feds[fed]["uid"], "shortname": args}, + }, + message, + ) + + await utils.answer(message, self.strings("fstop").format(args)) + + @error_handler + @chat_command + async def fnotescmd(self, message: Message, from_watcher: bool = False): + """Show federative notes""" + fed = await self.find_fed(message) + + if not fed: + await utils.answer(message, self.strings("no_fed")) + return + + res = {} + cache = {} + + for shortname, note in self.api.feds[fed].get("notes", {}).items(): + if int(note["creator"]) != self._tg_id and from_watcher: + continue + + try: + if int(note["creator"]) not in cache: + obj = await self._client.get_entity(int(note["creator"])) + cache[int(note["creator"])] = obj.first_name or obj.title + key = ( + f'{cache[int(note["creator"])]}' + ) + if key not in res: + res[key] = "" + res[key] += f" {shortname}\n" + except Exception: + key = "unknown" + if key not in res: + res[key] = "" + res[key] += f" {shortname}\n" + + notes = "".join(f"\nby {owner}:\n{note}" for owner, note in res.items()) + + if not notes and not from_watcher: + await utils.answer(message, self.strings("no_notes")) + return + + if not notes: + return + + await utils.answer(message, self.strings("fnotes").format(notes)) + + @error_handler + @chat_command + async def fdeflistcmd(self, message: Message): + """Show global invulnerable users""" + fed = await self.find_fed(message) + + if not fed: + await utils.answer(message, self.strings("no_fed")) + return + + if not self.api.feds[fed].get("fdef", []): + await utils.answer(message, self.strings("no_defense")) + return + + res = "" + for user in self.api.feds[fed].get("fdef", []).copy(): + try: + u = await self._client.get_entity(int(user), exp=0) + except Exception: + self.api.request( + { + "action": "protect user", + "args": {"uid": self.api.feds[fed]["uid"], "user": user}, + }, + message, + ) + await asyncio.sleep(0.2) + continue + + tit = get_full_name(u) + + res += f' 🇻🇦 {tit}\n' + + await utils.answer(message, self.strings("defense_list").format(res)) + return + + @error_handler + @chat_command + async def dmutecmd(self, message: Message): + """Delete and mute""" + reply = await message.get_reply_message() + await self.mutecmd(message) + await reply.delete() + + @error_handler + @chat_command + async def dbancmd(self, message: Message): + """Delete and ban""" + reply = await message.get_reply_message() + await self.bancmd(message) + await reply.delete() + + @error_handler + @chat_command + async def dwarncmd(self, message: Message): + """Delete and warn""" + reply = await message.get_reply_message() + await self.warncmd(message) + await reply.delete() + + @error_handler + @chat_command + async def frenamecmd(self, message: Message): + """Rename federation""" + args = utils.get_args_raw(message) + fed = await self.find_fed(message) + + if not fed: + await utils.answer(message, self.strings("no_fed")) + return + + if not args: + await utils.answer(message, self.strings("rename_noargs")) + return + + self.api.request( + { + "action": "rename federation", + "args": {"uid": self.api.feds[fed]["uid"], "name": args}, + }, + message, + ) + + await utils.answer( + message, + self.strings("rename_success").format(utils.escape_html(args)), + ) + + @error_handler + @chat_command + async def clnraidcmd(self, message: Message): + """ - Clean raid""" + args = utils.get_args_raw(message) + if not args or not args.isdigit(): + await utils.answer(message, self.strings("clnraid_args")) + return + + args = min(int(args), 10000) + + await self.inline.form( + message=message, + text=self.strings("clnraid_confirm").format(args), + reply_markup=[ + { + "text": self.strings("clnraid_yes"), + "callback": self._clnraid, + "args": (utils.get_chat_id(message), args), + }, + { + "text": self.strings("clnraid_cancel"), + "action": "close", + }, + ], + silent=True, + ) + + async def _clnraid( + self, + call: typing.Union[InlineCall, InlineMessage], + chat_id: int, + quantity: int, + ) -> InlineCall: + if call is not None: + await call.edit(self.strings("clnraid_started").format(quantity)) + + deleted = 0 + actually_deleted = 0 + async for log_msg in self._client.iter_admin_log(chat_id, join=True): + if deleted >= quantity: + break + + deleted += 1 + + try: + await self.inline.bot.kick_chat_member( + int(f"-100{chat_id}"), + log_msg.user.id, + ) + except Exception: + logger.debug("Can't kick member", exc_info=True) + else: + actually_deleted += 1 + + if call is not None: + await call.edit(self.strings("clnraid_complete").format(actually_deleted)) + + return call + + @error_handler + async def myrightscmd(self, message: Message): + """typing.List your admin rights in all chats""" + if not PIL_AVAILABLE: + await utils.answer(message, self.strings("pil_unavailable")) + return + + message = await utils.answer(message, self.strings("processing_myrights")) + + rights = [] + async for chat in self._client.iter_dialogs(): + ent = chat.entity + + if ( + not ( + isinstance(ent, Chat) + or (isinstance(ent, Channel) and getattr(ent, "megagroup", False)) + ) + or not ent.admin_rights + or ent.participants_count < 5 + ): + continue + + r = ent.admin_rights + + rights += [ + [ + ent.title if len(ent.title) < 30 else f"{ent.title[:30]}...", + "YES" if r.change_info else "-----", + "YES" if r.delete_messages else "-----", + "YES" if r.ban_users else "-----", + "YES" if r.invite_users else "-----", + "YES" if r.pin_messages else "-----", + "YES" if r.add_admins else "-----", + ] + ] + + await self._client.send_file( + message.peer_id, + self.render_table( + [ + [ + "Chat", + "change_info", + "delete_messages", + "ban_users", + "invite_users", + "pin_messages", + "add_admins", + ] + ] + + rights + ), + ) + + if message.out: + await message.delete() + + @error_handler + async def p__antiservice(self, chat_id: typing.Union[str, int], message: Message): + if ( + self.api.should_protect(chat_id, "antiservice") + and str(chat_id) not in self._ban_ninja + and getattr(message, "action_message", False) + ): + if self.api.should_protect(chat_id, "captcha") and ( + getattr(message, "user_joined", False) + or getattr(message, "user_added", False) + ): + self._delete_soon += [(message, time.time() + 5 * 60)] + return + + try: + await self.inline.bot.delete_message( + int(f"-100{chat_id}"), + message.action_message.id, + ) + except Exception: + await message.delete() + + async def _update_ban_ninja(self, chat_id: str): + while ( + chat_id in self._ban_ninja_forms and self._ban_ninja[chat_id] > time.time() + ): + try: + await self._ban_ninja_forms[chat_id].edit( + self.strings("smart_anti_raid_active").format( + ( + self.strings("forbid_messages") + if self.config["close_on_raid"] + else "" + ), + self._ban_ninja_progress[chat_id], + ), + { + "text": self.strings("smart_anti_raid_off"), + "callback": self.disable_smart_anti_raid, + "args": (chat_id,), + }, + ) + except Exception: + pass + + await asyncio.sleep(15) + + try: + await self.disable_smart_anti_raid(None, chat_id) + except Exception: + pass + + @error_handler + async def p__banninja( + self, + chat_id: typing.Union[str, int], + user_id: typing.Union[str, int], + message: Message, + ) -> bool: + if not ( + self.api.should_protect(chat_id, "banninja") + and ( + getattr(message, "user_joined", False) + or getattr(message, "user_added", False) + ) + ): + return False + + chat_id = str(chat_id) + + if chat_id in self._ban_ninja: + if self._ban_ninja[chat_id] > time.time(): + self._ban_ninja[chat_id] = time.time() + int( + self.config["banninja_cooldown"] + ) + await self.inline.bot.kick_chat_member(int(f"-100{chat_id}"), user_id) + + self._ban_ninja_progress[chat_id] += 1 + + try: + await self.inline.bot.delete_message( + int(f"-100{chat_id}"), + message.action_message.id, + ) + except MessageToDeleteNotFound: + pass + except MessageCantBeDeleted: + await self._promote_bot(chat_id) + await self.inline.bot.delete_message( + int(f"-100{chat_id}"), + message.action_message.id, + ) + logger.debug( + f"BanNinja is active in chat {chat_id=}, I kicked {user_id=}" + ) + return True + + await self.disable_smart_anti_raid(None, chat_id) + + if chat_id not in self._join_ratelimit: + self._join_ratelimit[chat_id] = [] + + self._join_ratelimit[chat_id] += [[user_id, round(time.time())]] + + processed = [] + + for u, t in self._join_ratelimit[chat_id].copy(): + if u in processed or t + 60 < time.time(): + self._join_ratelimit[chat_id].remove([u, t]) + else: + processed += [u] + + self.set("join_ratelimit", self._join_ratelimit) + + if len(self._join_ratelimit[chat_id]) >= self.config["join_ratelimit"]: + if chat_id in self._ban_ninja: + return False + + self._ban_ninja[chat_id] = ( + round(time.time()) + self.config["banninja_cooldown"] + ) + form = await self.inline.form( + self.strings("smart_anti_raid_active").format( + ( + self.strings("forbid_messages") + if self.config["close_on_raid"] + else "" + ), + self.config["join_ratelimit"], + ), + message=int(chat_id), + reply_markup={ + "text": self.strings("smart_anti_raid_off"), + "callback": self.disable_smart_anti_raid, + "args": (chat_id,), + }, + silent=True, + ) + + if self.config["close_on_raid"]: + try: + chat = await message.get_chat() + self._ban_ninja_default_rights[chat_id] = chat.default_banned_rights + await self._client( + EditChatDefaultBannedRightsRequest( + chat.id, + ChatBannedRights( + send_messages=True, until_date=2**31 - 1 + ), + ) + ) + except Exception: + pass + + self._ban_ninja_forms[chat_id] = form + self._ban_ninja_progress[chat_id] = self.config["join_ratelimit"] + self._ban_ninja_tasks[chat_id] = asyncio.ensure_future( + self._update_ban_ninja(chat_id) + ) + + await ( + await self._clnraid( + call=( + await self.inline.form( + self.strings("clnraid_started").format("*loading*"), + message=int(chat_id), + reply_markup={"text": ".", "action": "close"}, + silent=True, + ) + ), + chat_id=int(chat_id), + quantity=self.config["join_ratelimit"], + ) + ).delete() + + messages = [] + users = [] + for u, m in self._ban_ninja_messages: + if u not in users: + if len(users) > self.config["join_ratelimit"]: + break + + users += [u] + + messages += [m] + + for m in messages: + try: + await self.inline.bot.delete_message( + int(f"-100{utils.get_chat_id(m)}"), + m.id, + ) + except MessageToDeleteNotFound: + pass + except MessageCantBeDeleted: + await self._promote_bot(utils.get_chat_id(m)) + await self.inline.bot.delete_message( + int(f"-100{utils.get_chat_id(m)}"), + m.id, + ) + except Exception: + await m.delete() + + try: + await self._client.pin_message(int(chat_id), form.form["message_id"]) + except Exception: + pass + + return False + + async def disable_smart_anti_raid(self, call: InlineCall, chat_id: int): + chat_id = str(chat_id) + if chat_id in self._ban_ninja: + del self._ban_ninja[chat_id] + if call: + await call.edit(self.strings("smart_anti_raid_stopped")) + + if call: + await call.answer("Success") + + try: + await self._client.unpin_message( + int(chat_id), + self._ban_ninja_forms[str(chat_id)].form["message_id"], + ) + except Exception: + pass + + if self.config["close_on_raid"]: + try: + await self._client( + EditChatDefaultBannedRightsRequest( + int(chat_id), + self._ban_ninja_default_rights[chat_id], + ) + ) + del self._ban_ninja_default_rights[chat_id] + except Exception: + pass + + await self._client.send_message( + int(chat_id), + self.strings("banninja_report").format( + self._ban_ninja_progress[chat_id] + ), + ) + + if chat_id in self._ban_ninja_forms: + await self._ban_ninja_forms[chat_id].delete() + del self._ban_ninja_forms[chat_id] + + if chat_id in self._ban_ninja_progress: + del self._ban_ninja_progress[chat_id] + + if chat_id in self._ban_ninja_tasks: + self._ban_ninja_tasks[chat_id].cancel() + del self._ban_ninja_tasks[chat_id] + + return + + await call.answer("Already stopped") + + @error_handler + async def p__antiraid( + self, + chat_id: typing.Union[str, int], + user_id: typing.Union[str, int], + user: typing.Union[User, Channel], + message: Message, + chat: typing.Union[Chat, Channel], + ) -> bool: + if self.api.should_protect(chat_id, "antiraid") and ( + getattr(message, "user_joined", False) + or getattr(message, "user_added", False) + ): + action = self.api.chats[str(chat_id)]["antiraid"][0] + if action == "kick": + await self._client.send_message( + "me", + self.strings("antiraid").format( + "kicked", + user.id, + get_full_name(user), + utils.escape_html(chat.title), + ), + ) + + await self._client.kick_participant(chat_id, user) + elif action == "ban": + await self._client.send_message( + "me", + self.strings("antiraid").format( + "banned", + user.id, + get_full_name(user), + utils.escape_html(chat.title), + ), + ) + + await self.ban(chat, user, 0, "antiraid") + elif action == "mute": + await self._client.send_message( + "me", + self.strings("antiraid").format( + "muted", + user.id, + get_full_name(user), + utils.escape_html(chat.title), + ), + ) + + await self.mute(chat, user, 0, "antiraid") + + return True + + return False + + async def _captcha_invalid( + self, + call: InlineCall, + chat_id: int, + user: User, + ): + if call.from_user.id != user.id: + await call.answer("Not for you....") + return + + with contextlib.suppress(KeyError): + del self._captcha_db[chat_id][user.id] + + await call.answer("Sorry ☹️") + + await self.punish( + chat_id, + user, + "captcha_failed", + self.api.chats[str(chat_id)]["captcha"][0], + get_full_name(user), + fulltime=True, + message=None, + ) + + with contextlib.suppress(Exception): + await self._captcha_messages[chat_id][user.id].delete() + + async def _captcha_valid(self, call: InlineCall, chat_id: int, user_id: int): + if call.from_user.id != user_id: + await call.answer("Not for you....") + return + + if self._captcha_db[chat_id][user_id]["unmute"]: + await self._client.edit_permissions( + int(chat_id), + int(user_id), + until_date=0, + send_messages=True, + ) + + with contextlib.suppress(KeyError): + del self._captcha_db[chat_id][user_id] + + with contextlib.suppress(Exception): + await self._captcha_messages[chat_id][user_id].delete() + + await call.answer("Welcome!") + + @error_handler + async def p__captcha( + self, + chat_id: typing.Union[str, int], + user_id: typing.Union[str, int], + user: typing.Union[User, Channel], + message: Message, + chat: Chat, + ) -> bool: + if not ( + self.api.should_protect(chat_id, "captcha") + and str(chat_id) not in self._ban_ninja + and ( + getattr(message, "user_joined", False) + or getattr(message, "user_added", False) + ) + ): + return False + + valid = utils.rand(6) + invalid = [utils.rand(6) for _ in range(5)] + + markup = [ + { + "text": i, + "callback": self._captcha_invalid, + "args": (chat_id, user), + } + for i in invalid + ] + [ + {"text": valid, "callback": self._captcha_valid, "args": (chat_id, user_id)} + ] + + random.shuffle(markup) + markup = utils.chunks(markup, 2) + + unmute = False + + if not ( + await self._client.get_permissions(int(chat_id), int(user_id)) + ).is_banned: + unmute = True + await self.mute(chat, user, 15 * 60, "captcha_processing", silent=True) + + for _ in range(5): + try: + m = await self.inline.form( + message=(await message.reply("🪄 Loading captcha...")), + text=self.strings("complete_captcha").format( + user.id, + get_full_name(user), + ), + photo=f"https://hikarichat.hikariatama.ru/captcha/{valid}", + reply_markup=markup, + disable_security=True, + ) + except WebpageCurlFailedError: + await asyncio.sleep(0.5) + else: + break + + if chat_id not in self._captcha_db: + self._captcha_db[chat_id] = {} + + if chat_id not in self._captcha_messages: + self._captcha_messages[chat_id] = {} + + self._captcha_db[chat_id][user_id] = { + "time": time.time() + 5 * 60, + "user": user, + "unmute": unmute, + } + + self._captcha_messages[chat_id][user_id] = m + + self._ban_ninja_messages = [(user_id, m)] + self._ban_ninja_messages + + @error_handler + async def p__cas( + self, + chat_id: typing.Union[str, int], + user_id: typing.Union[str, int], + user: typing.Union[User, Channel], + message: Message, + chat: Chat, + ) -> bool: + if not ( + self.api.should_protect(chat_id, "cas") + and str(chat_id) not in self._ban_ninja + and ( + getattr(message, "user_joined", False) + or getattr(message, "user_added", False) + ) + ): + return False + + return ( + self.api.chats[str(chat_id)]["cas"][0] + if ( + ( + await utils.run_sync( + requests.get, + f"https://api.cas.chat/check?user_id={user_id}", + ) + ) + .json() + .get("result", {}) + .get("offenses", False) + ) + else False + ) + + @error_handler + async def p__welcome( + self, + chat_id: typing.Union[str, int], + user_id: typing.Union[str, int], + user: typing.Union[User, Channel], + message: Message, + chat: Chat, + ) -> bool: + if not ( + self.api.should_protect(chat_id, "welcome") + and str(chat_id) not in self._ban_ninja + and ( + getattr(message, "user_joined", False) + or getattr(message, "user_added", False) + ) + ): + return False + + m = await self._client.send_message( + chat_id, + self.api.chats[str(chat_id)]["welcome"][0] + .replace("{user}", get_full_name(user)) + .replace("{chat}", utils.escape_html(chat.title)) + .replace( + "{mention}", + f'{get_full_name(user)}', + ), + reply_to=message.action_message.id, + ) + + self._ban_ninja_messages = [(user_id, m)] + self._ban_ninja_messages + + return True + + @error_handler + async def p__report( + self, + chat_id: typing.Union[str, int], + user_id: typing.Union[str, int], + user: typing.Union[User, Channel], + message: Message, + ): + if not self.api.should_protect(chat_id, "report") or not getattr( + message, + "reply_to_msg_id", + False, + ): + return + + reply = await message.get_reply_message() + if ( + str(user_id) not in self._ratelimit["report"] + or self._ratelimit["report"][str(user_id)] < time.time() + ) and ( + ( + message.raw_text.startswith("#report") + or message.raw_text.startswith("/report") + ) + and reply + ): + fed = await self.find_fed(message) + if fed in self.api.feds and reply.sender_id in list( + map(int, self.api.feds[fed]["fdef"]) + ): + await utils.answer(message, self.strings("fdef403").format("report")) + return + + chat = await message.get_chat() + + reason = ( + message.raw_text.split(maxsplit=1)[1] + if message.raw_text.count(" ") >= 1 + else self.strings("no_reason") + ) + + self.api.request( + { + "action": "report", + "args": { + "chat": chat_id, + "reason": reason, + "link": await utils.get_message_link(reply, chat), + "user_link": utils.get_link(user), + "user_name": get_full_name(user), + "text_thumbnail": (getattr(reply, "raw_text", "") or "")[ + :1024 + ] or "", + }, + }, + message, + ) + + msg = self.strings("reported").format( + utils.get_link(user), + get_full_name(user), + reason, + ) + + if self._is_inline: + m = await self._client.send_message( + chat.id, + "🌘 Reporting message to admins...", + reply_to=message.reply_to_msg_id, + ) + await self.inline.form( + message=m, + text=msg, + reply_markup=[ + [ + { + "text": self.strings("btn_mute"), + "data": f"m/{chat.id}/{reply.sender_id}#{reply.id}", + }, + { + "text": self.strings("btn_ban"), + "data": f"b/{chat.id}/{reply.sender_id}#{reply.id}", + }, + ], + [ + { + "text": self.strings("btn_fban"), + "data": f"fb/{chat.id}/{reply.sender_id}#{reply.id}", + }, + { + "text": self.strings("btn_del"), + "data": f"d/{chat.id}/{reply.sender_id}#{reply.id}", + }, + ], + ], + silent=True, + ) + else: + await (utils.answer if message else self._client.send_message)( + message or chat.id, + msg, + ) + + self._ratelimit["report"][str(user_id)] = time.time() + 30 + + try: + await self.inline.bot.delete_message( + int(f"-100{chat_id}"), + getattr(message, "action_message", message).id, + ) + except MessageToDeleteNotFound: + pass + except MessageCantBeDeleted: + await self._promote_bot(chat_id) + await self.inline.bot.delete_message( + int(f"-100{chat_id}"), + getattr(message, "action_message", message).id, + ) + + @error_handler + async def _promote_bot(self, chat_id: int): + try: + await self._client( + InviteToChannelRequest( + int(chat_id), + [self.inline.bot_username], + ) + ) + except Exception: + logger.warning( + "Unable to invite cleaner to chat. Maybe he's already there?" + ) + + try: + await self._client( + EditAdminRequest( + channel=int(chat_id), + user_id=self.inline.bot_username, + admin_rights=ChatAdminRights(ban_users=True, delete_messages=True), + rank="HikariChat", + ) + ) + except Exception: + logger.exception("Cleaner promotion failed!") + + @error_handler + async def p__antiflood( + self, + chat_id: typing.Union[str, int], + user_id: typing.Union[str, int], + user: typing.Union[User, Channel], + message: Message, + ) -> typing.Union[bool, str]: + if self.api.should_protect(chat_id, "antiflood"): + if str(chat_id) not in self._flood_cache: + self._flood_cache[str(chat_id)] = {} + + if str(user_id) not in self._flood_cache[str(chat_id)]: + self._flood_cache[str(chat_id)][str(user_id)] = [] + + for item in self._flood_cache[str(chat_id)][str(user_id)].copy(): + if time.time() - item > self.flood_timeout: + self._flood_cache[str(chat_id)][str(user_id)].remove(item) + + self._flood_cache[str(chat_id)][str(user_id)].append( + round(time.mktime(message.date.timetuple())) + if getattr(message, "date", False) + else round(time.time()) + ) + self.set("flood_cache", self._flood_cache) + + if ( + len(self._flood_cache[str(chat_id)][str(user_id)]) + >= self.flood_threshold + ): + return self.api.chats[str(chat_id)]["antiflood"][0] + + return False + + @error_handler + async def p__antichannel( + self, + chat_id: typing.Union[str, int], + user_id: typing.Union[str, int], + user: typing.Union[User, Channel], + message: Message, + ) -> bool: + if ( + self.api.should_protect(chat_id, "antichannel") + and getattr(message, "sender_id", 0) < 0 + ): + await self.ban(chat_id, user_id, 0, "", None, True) + try: + await self.inline.bot.delete_message(int(f"-100{chat_id}"), message.id) + except Exception: + await message.delete() + + return True + + return False + + @error_handler + async def p__antigif( + self, + chat_id: typing.Union[str, int], + user_id: typing.Union[str, int], + user: typing.Union[User, Channel], + message: Message, + ) -> bool: + if self.api.should_protect(chat_id, "antigif"): + try: + if ( + message.media + and DocumentAttributeAnimated() in message.media.document.attributes + ): + await message.delete() + return True + except Exception: + pass + + return False + + @error_handler + async def p__antispoiler( + self, + chat_id: typing.Union[str, int], + user_id: typing.Union[str, int], + user: typing.Union[User, Channel], + message: Message, + ) -> bool: + if self.api.should_protect(chat_id, "antispoiler"): + try: + if any(isinstance(_, MessageEntitySpoiler) for _ in message.entities): + await message.delete() + return True + except Exception: + pass + + return False + + @error_handler + async def p__antiexplicit( + self, + chat_id: typing.Union[str, int], + user_id: typing.Union[str, int], + user: typing.Union[User, Channel], + message: Message, + ) -> typing.Union[bool, str]: + if self.api.should_protect(chat_id, "antiexplicit"): + text = getattr(message, "raw_text", "") + P = "пПnPp" + I = "иИiI1uІИ́Їіи́ї" # noqa: E741 + E = "еЕeEЕ́е́" + D = "дДdD" + Z = "зЗ3zZ3" + M = "мМmM" + U = "уУyYuUУ́у́" + O = "оОoO0О́о́" # noqa: E741 + L = "лЛlL1" + A = "аАaAА́а́@" + N = "нНhH" + G = "гГgG" + K = "кКkK" + R = "рРpPrR" + H = "хХxXhH" + YI = "йЙyуУY" + YA = "яЯЯ́я́" + YO = "ёЁ" + YU = "юЮЮ́ю́" + B = "бБ6bB" + T = "тТtT1" + HS = "ъЪ" + SS = "ьЬ" + Y = "ыЫ" + + occurrences = re.findall( + rf"""\b[0-9]*(\w*[{P}][{I}{E}][{Z}][{D}]\w*|(?:[^{I}{U}\s]+|{N}{I})?(? typing.Union[bool, str]: + if not self.api.should_protect(chat_id, "antinsfw"): + return False + + media = False + + if getattr(message, "sticker", False): + media = message.sticker + elif getattr(message, "media", False): + media = message.media + + if not media: + return False + + photo = io.BytesIO() + await self._client.download_media(message.media, photo) + photo.seek(0) + + if imghdr.what(photo) not in self.api.variables["image_types"]: + return False + + response = await self.api.nsfw(photo) + if response != "nsfw": + return False + + todel = [] + async for _ in self._client.iter_messages( + message.peer_id, + reverse=True, + offset_id=message.id - 1, + ): + todel += [_] + if _.sender_id != message.sender_id: + break + + await self._client.delete_messages( + message.peer_id, + message_ids=todel, + revoke=True, + ) + + return self.api.chats[str(chat_id)]["antinsfw"][0] + + @error_handler + async def p__antitagall( + self, + chat_id: typing.Union[str, int], + user_id: typing.Union[str, int], + user: typing.Union[User, Channel], + message: Message, + ) -> typing.Union[bool, str]: + return ( + self.api.chats[str(chat_id)]["antitagall"][0] + if self.api.should_protect(chat_id, "antitagall") + and getattr(message, "text", False) + and message.text.count("tg://user?id=") >= 5 + else False + ) + + @error_handler + async def p__antihelp( + self, + chat_id: typing.Union[str, int], + user_id: typing.Union[str, int], + user: typing.Union[User, Channel], + message: Message, + ) -> bool: + if not self.api.should_protect(chat_id, "antihelp") or not getattr( + message, "text", False + ): + return False + + search = message.text + if "@" in search: + search = search[: search.find("@")] + + if ( + not search.split() + or search.split()[0][1:] not in self.api.variables["blocked_commands"] + ): + return False + + await message.delete() + return True + + @error_handler + async def p__antiarab( + self, + chat_id: typing.Union[str, int], + user_id: typing.Union[str, int], + user: typing.Union[User, Channel], + message: Message, + ) -> typing.Union[bool, str]: + return ( + self.api.chats[str(chat_id)]["antiarab"][0] + if ( + self.api.should_protect(chat_id, "antiarab") + and ( + getattr(message, "user_joined", False) + or getattr(message, "user_added", False) + ) + and ( + len(re.findall("[\u4e00-\u9fff]+", get_full_name(user))) != 0 + or len(re.findall("[\u0621-\u064a]+", get_full_name(user))) != 0 + ) + ) + else False + ) + + @error_handler + async def p__antizalgo( + self, + chat_id: typing.Union[str, int], + user_id: typing.Union[str, int], + user: typing.Union[User, Channel], + message: Message, + ) -> typing.Union[bool, str]: + return ( + self.api.chats[str(chat_id)]["antizalgo"][0] + if ( + self.api.should_protect(chat_id, "antizalgo") + and len( + re.findall( + "[\u200f\u200e\u0300-\u0361\u0316-\u0362\u0334-\u0338\u0363-\u036f\u3164\ud83d\udd07\u0020\u00a0\u2000-\u2009\u200a\u2028\u205f\u1160\ufff4]", + get_full_name(user), + ) + ) + / len(get_full_name(user)) + >= 0.6 + ) + else False + ) + + @error_handler + async def p__bnd( + self, + chat_id: typing.Union[str, int], + user_id: typing.Union[str, int], + user: typing.Union[User, Channel], + message: Message, + ) -> typing.Union[bool, str]: + if not self.api.should_protect(chat_id, "bnd"): + return False + + if ( + self.get("bnd_cache", {}).get(str(chat_id), {}).get(str(user_id), 0) + >= time.time() + ): + return False + + try: + assert ( + ( + await self.inline.bot.get_chat_member( + int(f"-100{chat_id}"), + int(user_id), + ) + ).status + ) not in {"left", "kicked"} + except Exception: + return self.api.chats[str(chat_id)]["bnd"][0] + else: + bnd_cache = self.get("bnd_cache", {}) + bnd_cache.setdefault(str(chat_id), {}).update( + {str(user_id): round(time.time()) + 60} + ) + self.set("bnd_cache", bnd_cache) + return False + + @error_handler + async def p__antistick( + self, + chat_id: typing.Union[str, int], + user_id: typing.Union[str, int], + user: typing.Union[User, Channel], + message: Message, + ) -> typing.Union[bool, str]: + if not self.api.should_protect(chat_id, "antistick") or not ( + getattr(message, "sticker", False) + or getattr(message, "media", False) + and isinstance(message.media, MessageMediaUnsupported) + ): + return False + + sender = user.id + if sender not in self._sticks_ratelimit: + self._sticks_ratelimit[sender] = [] + + self._sticks_ratelimit[sender] += [round(time.time())] + + for timing in self._sticks_ratelimit[sender].copy(): + if time.time() - timing > 60: + self._sticks_ratelimit[sender].remove(timing) + + if len(self._sticks_ratelimit[sender]) > self._sticks_limit: + return self.api.chats[str(chat_id)]["antistick"][0] + + @error_handler + async def p__antilagsticks( + self, + chat_id: typing.Union[str, int], + user_id: typing.Union[str, int], + user: typing.Union[User, Channel], + message: Message, + ) -> typing.Union[bool, str]: + res = ( + self.api.should_protect(chat_id, "antilagsticks") + and getattr(message, "sticker", False) + and getattr(message.sticker, "id", False) + in self.api.variables["destructive_sticks"] + ) + if res: + await message.delete() + + return res + + @error_handler + async def watcher(self, message: Message): + self._global_queue += [message] + + @error_handler + async def _global_queue_handler(self): + while True: + while self._global_queue: + await self._global_queue_handler_process(self._global_queue.pop(0)) + + for chat_id, info in self._captcha_db.copy().items(): + for user_id, captcha in info.copy().items(): + if captcha["time"] < time.time(): + del self._captcha_db[chat_id][user_id] + await self.punish( + chat_id, + captcha["user"], + "captcha_timeout", + self.api.chats[str(chat_id)]["captcha"][0], + get_full_name(captcha["user"]), + fulltime=True, + message=None, + ) + with contextlib.suppress(Exception): + await self._captcha_messages[chat_id][user_id].delete() + + for message, deletion_ts in self._delete_soon.copy(): + if deletion_ts < time.time(): + with contextlib.suppress(Exception): + await message.delete() + + with contextlib.suppress(Exception): + self._delete_soon.remove((message, deletion_ts)) + + await asyncio.sleep(0.01) + + @error_handler + async def _global_queue_handler_process(self, message: Message): + if not isinstance(getattr(message, "chat", 0), (Chat, Channel)): + return + + chat_id = utils.get_chat_id(message) + + if ( + isinstance(getattr(message, "chat", 0), Channel) + and not getattr(message, "megagroup", False) + and int(chat_id) in reverse_dict(self._linked_channels) + ): + actual_chat = str(reverse_dict(self._linked_channels)[int(chat_id)]) + await self.p__antiservice(actual_chat, message) + return + + await self.p__antiservice(chat_id, message) + + try: + user_id = ( + getattr(message, "sender_id", False) + or message.action_message.action.users[0] + ) + except Exception: + try: + user_id = message.action_message.action.from_id.user_id + except Exception: + try: + user_id = message.from_id.user_id + except Exception: + try: + user_id = message.action_message.from_id.user_id + except Exception: + try: + user_id = message.action.from_user.id + except Exception: + try: + user_id = (await message.get_user()).id + except Exception: + logger.debug( + f"Can't extract entity from event {type(message)}" + ) + return + + user_id = ( + int(str(user_id)[4:]) if str(user_id).startswith("-100") else int(user_id) + ) + + if await self.p__banninja(chat_id, user_id, message): + return + + fed = await self.find_fed(message) + + if fed in self.api.feds: + if ( + getattr(message, "raw_text", False) + and ( + str(user_id) not in self._ratelimit["notes"] + or self._ratelimit["notes"][str(user_id)] < time.time() + ) + and not message.raw_text.startswith(self.get_prefix()) + ): + logger.debug("Checking message for notes...") + if message.raw_text.lower().strip() in ["#заметки", "#notes", "/notes"]: + self._ratelimit["notes"][str(user_id)] = time.time() + 3 + if any( + str(note["creator"]) == str(self._tg_id) + for _, note in self.api.feds[fed]["notes"].items() + ): + await self.fnotescmd( + await message.reply( + f"{self.get_prefix()}fnotes" + ), + True, + ) + + for note, note_info in self.api.feds[fed]["notes"].items(): + if str(note_info["creator"]) != str(self._tg_id): + continue + + if note.lower() in message.raw_text.lower(): + txt = note_info["text"] + self._ratelimit["notes"][str(user_id)] = time.time() + 3 + + if not txt.startswith("@inline"): + await utils.answer(message, txt) + break + + txt = "\n".join(txt.splitlines()[1:]) + buttons = [] + button_re = r"\[(.+)\]\((https?://.*)\)" + txt_r = [] + for line in txt.splitlines(): + if re.match(button_re, re.sub(r"<.*?>", "", line).strip()): + match = re.search( + button_re, re.sub(r"<.*?>", "", line).strip() + ) + buttons += [ + [{"text": match.group(1), "url": match.group(2)}] + ] + else: + txt_r += [line] + + if not buttons: + await utils.answer(message, txt) + break + + await self.inline.form( + message=message, + text="\n".join(txt_r), + reply_markup=buttons, + silent=True, + ) + + if int(user_id) in ( + list(map(int, self.api.feds[fed]["fdef"])) + + list(self._linked_channels.values()) + ): + return + + if str(chat_id) not in self.api.chats or not self.api.chats[str(chat_id)]: + return + + try: + user = await self._client.get_entity(user_id) + except ValueError: + return + + chat = await message.get_chat() + user_name = get_full_name(user) + + args = (chat_id, user_id, user, message) + + await self.p__report(*args) + + try: + if ( + await self._client.get_perms_cached(chat_id, message.sender_id) + ).is_admin: + return + except Exception: + pass + + if await self.p__antiraid(*args, chat): + return + + cas_result = False + if self.api.should_protect(chat_id, "cas"): + cas_result = await self.p__cas(*args, chat) + + if cas_result: + await self.punish( + chat_id, + user, + "cas", + cas_result, + user_name, + message=message, + ) + return + + r = await self.p__antiarab(*args) + if r: + await self.punish( + chat_id, + user, + "arabic_nickname", + r, + user_name, + message=message, + ) + return + + if await self.p__welcome(*args, chat) and not self.api.should_protect( + chat_id, + "captcha", + ): + return + + if await self.p__captcha(*args, chat): + return + + if getattr(message, "action", ""): + return + + await self.p__report(*args) + + r = await self.p__bnd(*args) + if r: + await self.punish(chat_id, user, "bnd", r, user_name, message=message) + return + + r = await self.p__antiflood(*args) + if r: + await self.punish(chat_id, user, "flood", r, user_name, message=message) + return + + if await self.p__antichannel(*args): + return + + r = await self.p__antizalgo(*args) + if r: + await self.punish(chat_id, user, "zalgo", r, user_name, message=message) + return + + if await self.p__antigif(*args): + return + + r = await self.p__antilagsticks(*args) + if r: + await self.punish( + chat_id, user, "destructive_stick", "ban", user_name, message=message + ) + return + + r = await self.p__antistick(*args) + if r: + await self.punish(chat_id, user, "stick", r, user_name, message=message) + return + + if await self.p__antispoiler(*args): + return + + r = await self.p__antiexplicit(*args) + if r: + await self.punish(chat_id, user, "explicit", r, user_name, message=message) + return + + r = await self.p__antinsfw(*args) + if r: + await self.punish( + chat_id, + user, + "nsfw_content", + r, + user_name, + message=message, + ) + return + + r = await self.p__antitagall(*args) + if r: + await self.punish(chat_id, user, "tagall", r, user_name, message=message) + return + + await self.p__antihelp(*args) + + async def client_ready( + self, + client: "CustomTelegramClient", # type: ignore + db: "hikka.database.Database", # type: ignore + ): + """Entry point""" + global api + + self._is_inline = self.inline.init_complete + + self._sticks_limit = 7 + + self._join_ratelimit = self.get("join_ratelimit", {}) + self._flood_cache = self.get("flood_cache", {}) + + self.api = api + await api.init(client, db, self) + + for protection in self.api.variables["protections"]: + setattr(self, f"{protection}cmd", self.protection_template(protection)) + + # We can override class docstings because of abc meta + self.__doc__ = ( + "Advanced chat admin toolkit\nNow became free...\n\n💻 Developer:" + " t.me/hikariatama\n📣" + " Downloaded from: @hikarimods\n\n" + + f"📦Version: {version}\n" + + ("🗃 Local" if not self.api._inited else "⭐️ Full") + ) + + self._pt_task = asyncio.ensure_future(self._global_queue_handler()) + + if PIL_AVAILABLE: + asyncio.ensure_future(self._download_font()) + + async def _download_font(self): + self.font = ( + await utils.run_sync( + requests.get, + "https://github.com/hikariatama/assets/raw/master/EversonMono.ttf", + ) + ).content diff --git a/hikariatama/ftg/hikkamods_socket.py b/hikariatama/ftg/hikkamods_socket.py new file mode 100644 index 0000000..162ec2b --- /dev/null +++ b/hikariatama/ftg/hikkamods_socket.py @@ -0,0 +1,195 @@ +# █ █ ▀ █▄▀ ▄▀█ █▀█ ▀ +# █▀█ █ █ █ █▀█ █▀▄ █ +# © Copyright 2022 +# https://t.me/hikariatama +# +# 🔒 Licensed under the GNU AGPLv3 +# 🌐 https://www.gnu.org/licenses/agpl-3.0.html + +__version__ = (2, 0, 1) + +# scope: hikka_min 1.2.10 + +# meta developer: @hikarimods +# requires: rsa base64 + +import asyncio +import base64 +import logging +import random +import re +from typing import Optional + +import rsa +from telethon.tl.types import Message + +from .. import loader, main, translations, utils + +logger = logging.getLogger(__name__) + +pubkey = rsa.PublicKey( + 7110455561671499155469672749235101198284219627796886527432331759773809536504953770286294224729310191037878347906574131955439231159825047868272932664151403, + 65537, +) + +REGEXES = [ + re.compile( + r"https:\/\/github\.com\/([^\/]+?)\/([^\/]+?)\/raw\/(?:main|master)\/([^\/]+\.py)" + ), + re.compile( + r"https:\/\/raw\.githubusercontent\.com\/([^\/]+?)\/([^\/]+?)\/(?:main|master)\/([^\/]+\.py)" + ), +] + + +@loader.tds +class HikkaModsSocketMod(loader.Module): + """Gives @hikkamods_bot a right to download modules from official modules aggregator and autoupdate them""" + + strings = {"name": "HikkaModsSocket"} + + def __init__(self): + self.config = loader.ModuleConfig( + loader.ConfigValue( + "autoupdate", + False, + ( + "Do you want to autoupdate modules? (Join @heta_updates in order" + " for this option to take effect) ⚠️ Use at your own risk!" + ), + validator=loader.validators.Boolean(), + ) + ) + + async def client_ready(self, *_): + if self.config["autoupdate"] and hasattr(self, "request_join"): + await self.request_join( + "@heta_updates", + "This channel is the source of update notifications", + ) + + if self.get("nomute"): + return + + await utils.dnd(self._client, "@hikkamods_bot", archive=False) + self.set("nomute", True) + + @loader.loop(interval=60 * 60 * 6, autostart=True, wait_before=True) + async def stats_collector(self): + if not self._db.get(main.__name__, "stats", True): + raise loader.StopLoop + + logger.debug("Sending additional stats") + for module in [ + mod.__origin__ + for mod in self.allmodules.modules + if utils.check_url(mod.__origin__) + ]: + try: + await self.lookup("loader")._send_stats(module) + except Exception: + logger.debug(f"Failed to send stats for {module}", exc_info=True) + + async def _load_module(self, url: str, message: Optional[Message] = None): + loader_m = self.lookup("loader") + + await loader_m.download_and_install(url, None) + + if getattr(loader_m, "_fully_loaded", getattr(loader_m, "fully_loaded", False)): + getattr( + loader_m, + "_update_modules_in_db", + getattr(loader_m, "update_modules_in_db", lambda: None), + )() + + if message: + if any(link == url for link in loader_m.get("loaded_modules", {}).values()): + await self._client.inline_query( + "@hikkamods_bot", + f"#confirm_load {message.raw_text.splitlines()[2].strip()}", + ) + else: + await self._client.inline_query( + "@hikkamods_bot", + f"#confirm_fload {message.raw_text.splitlines()[2].strip()}", + ) + + async def watcher(self, message: Message): + if not isinstance(message, Message): + return + + if message.sender_id == 5519484330 and message.raw_text.startswith("#install"): + await message.delete() + + fileref = ( + message.raw_text.split("#install:")[1].strip().splitlines()[0].strip() + ) + sig = base64.b64decode(message.raw_text.splitlines()[1].strip().encode()) + try: + rsa.verify( + rsa.compute_hash(fileref.encode("utf-8"), "SHA-1"), sig, pubkey + ) + except rsa.pkcs1.VerificationError: + logger.error(f"Got message with non-verified signature ({fileref=})") + return + + await self._load_module(f"https://heta.hikariatama.ru/{fileref}", message) + elif message.sender_id == 5519484330 and message.raw_text.startswith( + "#setlang" + ): + lang = message.raw_text.split()[1] + self._db.set(translations.__name__, "lang", lang) + await self.allmodules.reload_translations() + await self._client.inline_query("@hikkamods_bot", "#confirm_setlang") + elif ( + utils.get_chat_id(message) == 1688624566 + and "Heta url: " in message.raw_text + ): + url = message.raw_text.split("Heta url: ")[1].strip() + heta_dev, heta_repo, heta_mod = ( + url.lower().split("hikariatama.ru/")[1].split("/") + ) + + if heta_dev == "hikariatama" and heta_repo == "ftg": + urls = [f"https://mods.hikariatama.ru/{heta_mod}", url] + if any( + getattr(module, "__origin__", None).lower().strip("/") in urls + for module in self.allmodules.modules + ): + await self._load_module(urls[0]) + await asyncio.sleep(random.randint(1, 10)) + await self._client.inline_query( + "@hikkamods_bot", + f"#confirm_update_noheta {url.split('hikariatama.ru/')[1]}", + ) + return + + if any( + getattr(module, "__origin__", "").lower().strip("/") + == url.lower().strip("/") + for module in self.allmodules.modules + ): + await self._load_module(url) + await asyncio.sleep(random.randint(1, 10)) + await self._client.inline_query( + "@hikkamods_bot", + f"#confirm_update {url.split('hikariatama.ru/')[1]}", + ) + return + + for module in self.allmodules.modules: + link = getattr(module, "__origin__", "").lower().strip("/") + for regex in REGEXES: + if regex.search(link): + dev, repo, mod = regex.search(link).groups() + if dev == heta_dev and repo == heta_repo and mod == heta_mod: + await self._load_module(link) + await asyncio.sleep(random.randint(1, 10)) + await self._client.inline_query( + "@hikkamods_bot", + ( + "#confirm_update_noheta" + f" {url.split('hikariatama.ru/')[1]}" + ), + ) + return diff --git a/hikariatama/ftg/httpsc.py b/hikariatama/ftg/httpsc.py new file mode 100644 index 0000000..225da94 --- /dev/null +++ b/hikariatama/ftg/httpsc.py @@ -0,0 +1,112 @@ +# █ █ ▀ █▄▀ ▄▀█ █▀█ ▀ +# █▀█ █ █ █ █▀█ █▀▄ █ +# © Copyright 2022 +# https://t.me/hikariatama +# +# 🔒 Licensed under the GNU AGPLv3 +# 🌐 https://www.gnu.org/licenses/agpl-3.0.html + +# scope: hikka_min 1.2.10 + +# meta pic: https://img.icons8.com/external-flaticons-lineal-color-flat-icons/512/000000/external-status-agile-flaticons-lineal-color-flat-icons-2.png +# meta banner: https://mods.hikariatama.ru/badges/httpsc.jpg +# meta developer: @hikarimods + +from telethon.tl.types import Message + +from .. import loader, utils + +responses = { + 100: ("ℹ️ Continue", "Запрос принят, продолжай"), + 101: ("ℹ️ Switching Protocols", "Изменение протокола; подчинйся Upgrade хедеру"), + 200: ("✅ OK", "Запрос успешный, контент отображен"), + 201: ("✅ Created", "Запрос создан, url прилагается"), + 202: ("✅ Accepted", "Запрос принят и обрабатывается оффлайн"), + 203: ("✅ Non-Authoritative Information", "Загружено из кэша"), + 204: ("✅ No Content", "Запрос успешный, нет контента"), + 205: ("✅ Reset Content", "Очистить форму для продолжения"), + 206: ("✅ Partial Content", "Частичный контент прилагается"), + 300: ("↩️ Multiple Choices", "У объекта есть несколько источников"), + 301: ("↩️ Moved Permanently", "Адрес изменен навсегда"), + 302: ("↩️ Found", "Адрес изменен временно"), + 303: ("↩️ See Other", "Адрес и\\или объект изменен"), + 304: ("↩️ Not Modified", "Контент не изменился с предыдущего запроса"), + 305: ("↩️ Use Proxy", "Неверная локация"), + 307: ("↩️ Temporary Redirect", "Временное перенаправление"), + 400: ("🚫 Bad Request", "Ошибка формирования запроса со стороны клиента"), + 401: ("🚫 Unauthorized", "Не авторизован"), + 402: ("🚫 Payment Required", "Не оплачено"), + 403: ("🚫 Forbidden", "Доступ запрещен - бан / нехватка прав"), + 404: ("🚫 Not Found", "Не найдено"), + 405: ("🚫 Method Not Allowed", "Метод запрещен"), + 406: ("🚫 Not Acceptable", "Метод недоступен"), + 407: ("🚫 Proxy Authentication Required", "Не хватает авторизации прокси"), + 408: ("🚫 Request Timeout", "Время ожидания истекло"), + 409: ("🚫 Conflict", "Конфликт запросов"), + 410: ("🚫 Gone", "Адрес не существует и был перемещен"), + 411: ("🚫 Length Required", "Требуется указание длины контента запроса"), + 412: ("🚫 Precondition Failed", "Предусловие в хедерах неверно"), + 413: ("🚫 Request Entity Too Large", "Запрос слишком большой"), + 414: ("🚫 Request-URI Too Long", "Ссылка слишком большая"), + 415: ("🚫 Unsupported Media Type", "Неподдерживаеый формат контента"), + 416: ("🚫 Requested Range Not Satisfiable", "Не входит в разрешенный диапазон"), + 417: ("🚫 Expectation Failed", "Ожидания не выполняются"), + 500: ("💢 Internal Server Error", "Ошибка сервера"), + 501: ("💢 Not Implemented", "Операция не поддерживается"), + 502: ("💢 Bad Gateway", "Прокси \\ шлюз недоступен"), + 503: ("💢 Service Unavailable", "Перегрузка сервера"), + 504: ("💢 Gateway Timeout", "Таймаут прокси \\ шлюза"), + 505: ("💢 HTTP Version Not Supported", "Версия HTTP не соответствует требованиям"), +} + + +@loader.tds +class HttpErrorsMod(loader.Module): + """Dictionary of http status codes""" + + strings = { + "name": "HttpStatusCodes", + "args_incorrect": "Incorrect args", + "not_found": "Code not found", + "syntax_error": "Args are mandatory", + "scode": "{} {}\n⚜️ Описание кода: {}", + } + + strings_ru = { + "args_incorrect": "Неверные аргументы", + "not_found": "Код не найден", + "syntax_error": "Аргументы обязательны", + "_cmd_doc_httpsc": "<код> - Получить информацию о HTTP-коде", + "_cmd_doc_httpscs": "Показать все доступные коды", + "_cls_doc": "Словарь HTTP-кодов", + } + + @loader.unrestricted + async def httpsccmd(self, message: Message): + """ - Get status code info""" + args = utils.get_args(message) + if not args: + await utils.answer(message, self.strings("syntax_error", message)) + + try: + if int(args[0]) not in responses: + await utils.answer(message, self.strings("not_found", message)) + except ValueError: + await utils.answer(message, self.strings("args_incorrect", message)) + + await utils.answer( + message, + self.strings("scode", message).format( + responses[int(args[0])][0], args[0], responses[int(args[0])][1] + ), + ) + + @loader.unrestricted + async def httpscscmd(self, message: Message): + """Get all http status codes""" + await utils.answer( + message, + "\n".join( + [f"{str(sc)}: {_[0][0]} {_[1]}" for sc, _ in responses.items()] + ), + ) diff --git a/hikariatama/ftg/hw.py b/hikariatama/ftg/hw.py new file mode 100644 index 0000000..3c9e9cc --- /dev/null +++ b/hikariatama/ftg/hw.py @@ -0,0 +1,92 @@ +# █ █ ▀ █▄▀ ▄▀█ █▀█ ▀ +# █▀█ █ █ █ █▀█ █▀▄ █ +# © Copyright 2022 +# https://t.me/hikariatama +# +# 🔒 Licensed under the GNU AGPLv3 +# 🌐 https://www.gnu.org/licenses/agpl-3.0.html + +# meta pic: https://static.dan.tatar/hw_icon.png +# meta banner: https://mods.hikariatama.ru/badges/hw.jpg +# meta developer: @hikarimods +# scope: hikka_only +# scope: hikka_min 1.2.10 + +from random import randint + +from telethon.tl.types import Message + +from .. import loader, utils + + +@loader.tds +class HomeworkMod(loader.Module): + """Simple Homework planner""" + + strings = { + "name": "HomeWork", + "no_hometask": "🚫 You haven't provided hometask", + "new_hometask": "Hometask #{}:\n
{}
", + "not_found": "🚫 Hometask not found✅ Hometask removed", + } + + strings_ru = { + "no_hometask": "🚫 Укажи домашнее задание", + "new_hometask": "Домашнее задание #{}:\n
{}
", + "not_found": "🚫 Домашнее задание не найдено✅ Домашнее задание удалено", + "_cmd_doc_hw": " - Новое домашнее задание", + "_cmd_doc_hwl": "Список домашних заданий", + "_cmd_doc_uhw": " - Удалить домашнее задание", + "_cls_doc": "Простой планнер домашних заданий", + } + + async def client_ready(self, client, db): + self.hw = self.get("hw", {}) + + async def hwcmd(self, message: Message): + """ - New hometask""" + + args = utils.get_args_raw(message) + reply = await message.get_reply_message() + if args == "" and not reply: + await utils.answer(message, self.strings("no_hometask")) + return + + if args == "": + args = reply.text + + random_id = str(randint(10000, 99999)) + + self.hw[random_id] = args + + self.set("hw", self.hw) + await utils.answer( + message, + self.strings("new_hometask").format(random_id, str(args)), + ) + + @loader.unrestricted + async def hwlcmd(self, message: Message): + """List of hometasks""" + res = "#HW:\n\n" + + for item_id, item in self.hw.items(): + res += f"🔸 .uhw {item_id}: {item}" + "\n" + + await utils.answer(message, res) + + async def uhwcmd(self, message: Message): + """ - Remove hometask""" + args = utils.get_args_raw(message) + if args.startswith("#"): + args = args[1:] + + if args not in self.hw: + await utils.answer(message, self.strings("not_found")) + return + + del self.hw[args] + self.set("hw", self.hw) + await utils.answer(message, self.strings("removed")) diff --git a/hikariatama/ftg/img2pdf.py b/hikariatama/ftg/img2pdf.py new file mode 100644 index 0000000..f7a52da --- /dev/null +++ b/hikariatama/ftg/img2pdf.py @@ -0,0 +1,97 @@ +# █ █ ▀ █▄▀ ▄▀█ █▀█ ▀ +# █▀█ █ █ █ █▀█ █▀▄ █ +# © Copyright 2022 +# https://t.me/hikariatama +# +# 🔒 Licensed under the GNU AGPLv3 +# 🌐 https://www.gnu.org/licenses/agpl-3.0.html + +# scope: hikka_min 1.2.10 + +# meta pic: https://img.icons8.com/stickers/500/000000/pdf.png +# meta banner: https://mods.hikariatama.ru/badges/img2pdf.jpg +# meta developer: @hikarimods +# requires: Pillow + +import io + +from PIL import Image, UnidentifiedImageError +from telethon.tl.types import Message + +from .. import loader, utils + + +@loader.tds +class Img2PdfMod(loader.Module): + """Packs images to pdf""" + + strings = { + "name": "Img2Pdf", + "processing": ( + "🫥 Processing" + " files..." + ), + } + strings_ru = { + "processing": ( + "🫥 Обрабатываю" + " файлы..." + ) + } + strings_es = { + "processing": ( + "🫥 Procesando" + " archivos..." + ) + } + strings_de = { + "processing": ( + "🫥 Dateien werden" + " verarbeitet..." + ) + } + strings_tr = { + "processing": ( + "🫥 Dosyalar" + " işleniyor..." + ) + } + + @loader.unrestricted + async def img2pdfcmd(self, message: Message): + """ - Pack images into pdf""" + try: + start_offset = ( + message.id if message.media else (await message.get_reply_message()).id + ) + except Exception: + return await utils.answer(message, self.strings("no_file")) + + message = await utils.answer(message, self.strings("processing")) + + images = [] + + async for ms in self._client.iter_messages( + message.peer_id, offset_id=start_offset - 1, reverse=True + ): + if not ms.media: + break + im = await self._client.download_file(ms.media, bytes) + try: + images.append(Image.open(io.BytesIO(im))) + except UnidentifiedImageError: + break + + first_image, images = images[0], images[1:] + file = io.BytesIO() + first_image.save( + file, + "PDF", + resolution=100.0, + save_all=True, + append_images=images, + ) + f = io.BytesIO(file.getvalue()) + f.name = utils.get_args_raw(message) or "packed_images.pdf" + await self._client.send_file(message.peer_id, f) + await message.delete() diff --git a/hikariatama/ftg/inactive.py b/hikariatama/ftg/inactive.py new file mode 100644 index 0000000..7d8537f --- /dev/null +++ b/hikariatama/ftg/inactive.py @@ -0,0 +1,446 @@ +# █ █ ▀ █▄▀ ▄▀█ █▀█ ▀ +# █▀█ █ █ █ █▀█ █▀▄ █ +# © Copyright 2022 +# https://t.me/hikariatama +# +# 🔒 Licensed under the GNU AGPLv3 +# 🌐 https://www.gnu.org/licenses/agpl-3.0.html + +# meta pic: https://img.icons8.com/external-wanicon-flat-wanicon/344/external-dead-halloween-costume-avatar-wanicon-flat-wanicon.png +# meta developer: @hikarimods +# meta banner: https://mods.hikariatama.ru/badges/inactive.jpg +# scope: hikka_only +# scope: hikka_min 1.3.0 + +import asyncio +import contextlib +import logging +import time + +from telethon.tl.types import Message +from telethon.utils import get_display_name + +from .. import loader, utils +from ..inline.types import InlineCall + +logger = logging.getLogger(__name__) + + +@loader.tds +class Inactive(loader.Module): + """Blocks people who are inactive for a long time. Check .config""" + + strings = { + "name": "Inactive", + "config": ( + "🚫 You need to" + " configure module first: \n\n⚙️ {}config {}" + ), + "confirm": ( + "⚠️ Please, confirm that you want to start cleaning this chat from" + " inactive users with these parameters:\n\n⌚️ Inactive time:" + " {}\n💭 Minimal amount of messages: {}\n\n☝️ Please, note," + " that this operation might take a lot of API requests and cause" + " FloodWaits" + ), + "start": "🧹 Start", + "cancel": "🔻 Cancel", + "configure": "⚙️ Open config", + "started": "😼 Processing started! This message will update", + "processing": ( + "🫶 Processed {} messages from {} users. Already found {} users to" + " {} and" + " {} trusted\n\nStill processing..." + ), + "kick": "kick", + "ban": "ban", + "processing_complete": ( + "😻 Processing complete! Processed {} messages from {} users. Found {}" + " users to {}. Apply restrictions?\n" + ), + "processing_already": "😼 Processing already in progress!", + "restrictions_applied": "🔒 Action `{}` applied to {} inactive users!", + "cancelling_processing": "🔻 Cancelling processing...", + "processing_cancelled": "😼 Processing cancelled!", + "hrs": "hour(-s)", + "applying_restrictions": ( + "🔒 Applying restrictions. Found {} users to {}" + ), + "restrict": "🔒 Restrict", + "no_users": "😼 No inactive users found!", + "messages": "messages", + "waiting_lock": ( + "🛃 Processing is already active in other chat, waiting for lock to" + " release" + ), + } + + strings_ru = { + "config": ( + "🚫 Вам нужно вначале" + " настроить модуль: \n\n⚙️ {}config {}" + ), + "confirm": ( + "⚠️ Пожалуйста, подтвердите, что вы хотите начать очистку этого чата от" + " неактивных пользователей с этими параметрами:\n\n⌚️ Время" + " неактивности: {}\n💭 Минимальное количество сообщений: {}\n\n☝️" + " Пожалуйста, обратите внимание, что эта операция может занять много API" + " запросов и вызвать FloodWait'ы" + ), + "start": "🧹 Начать", + "cancel": "🔻 Отмена", + "configure": "⚙️ Открыть настройки", + "started": "😼 Обработка началась! Это сообщение будет обновляться", + "processing": ( + "🫶 Обработано {} сообщений от {} пользователей. Уже найдено {}" + " пользователей для {} и {} доверенных\n\nВсе еще обрабатываю..." + ), + "kick": "кика", + "ban": "бана", + "processing_complete": ( + "😻 Обработка завершена! Обработано {} сообщений от {} пользователей." + " Найдено {} пользователей для {}. Применять ограничения?\n" + ), + "processing_already": "😼 Обработка уже выполняется!", + "restrictions_applied": ( + "🔒 Действие `{}` применено к {} неактивным пользователям!" + ), + "cancelling_processing": "🔻 Отменяю обработку...", + "processing_cancelled": "😼 Обработка отменена!", + "hrs": "час(-ов)", + "applying_restrictions": ( + "🔒 Применяю ограничения. Найдено {} пользователей для {}" + ), + "restrict": "🔒 Ограничить", + "no_users": "😼 Не найдено неактивных пользователей!", + "messages": "сообщений", + "waiting_lock": ( + "🛃 Обработка уже выполняется в другом чате, жду освобождения" + " блокировки" + ), + } + + _lock = {} + _global_lock = asyncio.Lock() + + def __init__(self): + self.config = loader.ModuleConfig( + loader.ConfigValue( + "action", + "kick", + "Action to perform when user is inactive", + validator=loader.validators.Choice(["ban", "kick"]), + ), + loader.ConfigValue( + "inactive_time", + None, + ( + "If specified, any user, which sent no messages for this amount of" + " hours, will be blocked." + ), + validator=loader.validators.Union( + loader.validators.Integer(minimum=1), loader.validators.NoneType() + ), + ), + loader.ConfigValue( + "inactive_messages", + None, + ( + "If specified, any user, which sent less than this amount of" + " messages, will be blocked." + ), + validator=loader.validators.Union( + loader.validators.Integer(minimum=1), loader.validators.NoneType() + ), + ), + ) + + async def _configure(self, call: InlineCall): + await self.lookup("HikkaConfig").inline__configure( + call, + self.__class__.__name__, + obj_type=False, + ) + + async def _cancel(self, call: InlineCall, chat_id: int): + if chat_id in self._lock: + self._lock[chat_id].set() + await call.edit(self.strings("processing_cancelled")) + + async def _start(self, call: InlineCall, chat_id: int): + if chat_id in self._lock: + await call.edit(self.strings("processing_already")) + return + + self._lock[chat_id] = asyncio.Event() + + markup = { + "text": self.strings("cancel"), + "callback": self._cancel, + "args": (chat_id,), + } + + chat = await self._client.get_entity(chat_id) + data = {} + restrict = set() + processing_finished = asyncio.Event() + + async def _(): + nonlocal call, data, restrict + while True: + await asyncio.sleep(20) + if ( + processing_finished.is_set() + or chat_id not in self._lock + or self._lock[chat_id].is_set() + ): + break + + await call.edit( + self.strings("processing").format( + sum([len(user_messages) for user_messages in data.values()]), + len(data), + len(restrict), + self.strings(self.config["action"]), + len( + [ + user + for user, messages in data.items() + if ( + not self.config["inactive_messages"] + or len(messages) > self.config["inactive_messages"] + ) + and ( + not self.config["inactive_time"] + or messages + and time.time() - max(messages) + < self.config["inactive_time"] * 3600 + ) + ] + ), + ), + reply_markup=markup, + ) + + await call.edit( + ( + self.strings("waiting_lock") + if self._global_lock.locked() + else self.strings("started") + ), + reply_markup=markup, + ) + + async with self._global_lock: + if self._lock[chat_id].is_set(): + await call.edit(self.strings("processing_cancelled")) + self._lock.pop(chat_id) + return + + task = asyncio.ensure_future(_()) + + names = {} + + with contextlib.suppress(Exception): + await self._client.end_takeout(True) + + async with self._client.takeout( + **({"megagroups": True} if chat.megagroup else {"chats": True}) + ) as takeout: + async for user in takeout.iter_participants(chat): + data.setdefault(user.id, []) + names[user.id] = get_display_name(user) + + async for message in takeout.iter_messages(chat, wait_time=5): + sender = message.sender_id + if sender not in names: + continue + + date = time.mktime(message.date.timetuple()) + data.setdefault(sender, []).append(date) + if self.config["inactive_time"]: + if ( + time.time() - max(data[sender]) + > self.config["inactive_time"] * 3600 + ): + restrict.add(sender) + elif sender in restrict: + restrict.remove(sender) + + if self.config["inactive_messages"]: + if len(data[sender]) < self.config["inactive_messages"]: + restrict.add(sender) + elif sender in restrict: + restrict.remove(sender) + + if ( + self.config["inactive_messages"] + and all( + len(msgs) > self.config["inactive_messages"] + for msgs in data.values() + ) + and ( + not self.config["inactive_time"] + or all( + msgs + and time.time() - max(msgs) + > self.config["inactive_time"] * 3600 + for msgs in data.values() + ) + ) + ): + break + + if self._lock[chat_id].is_set(): + await call.edit(self.strings("processing_cancelled")) + self._lock.pop(chat_id) + return + + for user, messages in data.items(): + if ( + self.config["inactive_messages"] + and len(messages) < self.config["inactive_messages"] + or self.config["inactive_time"] + and time.time() - max(messages) > self.config["inactive_time"] * 3600 + ): + restrict.add(user) + elif user in restrict: + restrict.remove(user) + + processing_finished.set() + task.cancel() + + if not restrict: + await call.edit(self.strings("no_users")) + self._lock.pop(chat_id) + return + + m = self.strings("processing_complete").format( + sum([len(user_messages) for user_messages in data.values()]), + len(data), + len(restrict), + self.strings(self.config["action"]), + ) + + for user in restrict: + line = ( + "\n▫️ {utils.escape_html(names.get(user, user))}" + f" ({len(data[user])} {self.strings('messages')}," + f" {round((time.time() - max(data[user])) / 3600, 1) if data[user] else 'n/a'} {self.strings('hrs')})" + ) + if len(m + line) >= 4096: + m += "\n..." + break + + m += line + + await call.edit( + m, + reply_markup=[ + { + "text": self.strings("restrict"), + "callback": self._restrict, + "args": (chat_id, restrict, markup), + }, + { + "text": self.strings("cancel"), + "callback": self._im_cancel, + "args": (chat_id,), + }, + ], + ) + + async def _im_cancel(self, call: InlineCall, chat_id: int): + self._lock.pop(chat_id) + await call.edit(self.strings("processing_cancelled")) + + async def _restrict( + self, + call: InlineCall, + chat_id: int, + restrict: set, + markup: dict, + ): + await call.edit( + self.strings("applying_restrictions").format( + len(restrict), self.strings(self.config["action"]) + ), + reply_markup=markup, + ) + for user_id in restrict: + if self.config["action"] == "kick": + await self._client.kick_participant(chat_id, user_id) + else: + await self._client.edit_permissions( + chat_id, + user_id, + until_date=0, + view_messages=False, + send_messages=False, + send_media=False, + send_stickers=False, + send_gifs=False, + send_games=False, + send_inline=False, + send_polls=False, + change_info=False, + invite_users=False, + ) + + await asyncio.sleep(3) + + if self._lock[chat_id].is_set(): + await call.edit(self.strings("processing_cancelled")) + self._lock.pop(chat_id) + return + + await call.edit( + self.strings("restrictions_applied").format( + self.strings(self.config["action"]), + len(restrict), + ) + ) + self._lock.pop(chat_id) + + @loader.command(ru_doc="Запустить чистку неактивных юзеров") + async def inactive(self, message: Message): + """Start inactive users cleaner""" + if not self.config["inactive_time"] and not self.config["inactive_messages"]: + await utils.answer( + message, + self.strings("config").format( + self.get_prefix(), + self.__class__.__name__, + ), + ) + return + + if utils.get_chat_id(message) in self._lock: + await utils.answer(message, self.strings("processing_already")) + return + + await self.inline.form( + message=message, + text=self.strings("confirm").format( + ( + f'{self.config["inactive_time"]} {self.strings("hrs")}' + if self.config["inactive_time"] + else "-" + ), + self.config["inactive_messages"] or "-", + ), + reply_markup=[ + [ + { + "text": self.strings("start"), + "callback": self._start, + "args": (utils.get_chat_id(message),), + }, + {"text": self.strings("cancel"), "action": "close"}, + ], + [{"text": self.strings("configure"), "callback": self._configure}], + ], + ) diff --git a/hikariatama/ftg/inline_ghoul.py b/hikariatama/ftg/inline_ghoul.py new file mode 100644 index 0000000..2ae1621 --- /dev/null +++ b/hikariatama/ftg/inline_ghoul.py @@ -0,0 +1,42 @@ +# █ █ ▀ █▄▀ ▄▀█ █▀█ ▀ +# █▀█ █ █ █ █▀█ █▀▄ █ +# © Copyright 2022 +# https://t.me/hikariatama +# +# 🔒 Licensed under the GNU AGPLv3 +# 🌐 https://www.gnu.org/licenses/agpl-3.0.html + +# scope: hikka_min 1.2.10 + +# meta pic: https://img.icons8.com/color/480/000000/dota.png +# meta banner: https://mods.hikariatama.ru/badges/inline_ghoul.jpg +# meta developer: @hikarimods +# scope: inline +# scope: hikka_only + +from telethon.tl.types import Message + +from .. import loader + + +@loader.tds +class InlineGhoulMod(loader.Module): + """Non-spammy ghoul module""" + + strings = {"name": "InlineGhoul", "tired": "😾 Tired of counting!"} + + strings_ru = { + "tired": "😾 Я устал считать!", + "_cmd_doc_ghoul": "Отправляет сообщение Гуля", + "_cls_doc": "Неспамящий модуль Гуль", + } + + async def ghoulcmd(self, message: Message): + """Sends ghoul message""" + await self.animate( + message, + [f"👊 {x} - 7 = {x - 7}" for x in range(1000, 900, -7)] + + [self.strings("tired")], + interval=1, + inline=True, + ) diff --git a/hikariatama/ftg/inline_random.py b/hikariatama/ftg/inline_random.py new file mode 100644 index 0000000..63e02d3 --- /dev/null +++ b/hikariatama/ftg/inline_random.py @@ -0,0 +1,87 @@ +# █ █ ▀ █▄▀ ▄▀█ █▀█ ▀ +# █▀█ █ █ █ █▀█ █▀▄ █ +# © Copyright 2022 +# https://t.me/hikariatama +# +# 🔒 Licensed under the GNU AGPLv3 +# 🌐 https://www.gnu.org/licenses/agpl-3.0.html + +# scope: hikka_min 1.2.10 + +# meta pic: https://img.icons8.com/fluency/240/000000/shuffle.png +# meta banner: https://mods.hikariatama.ru/badges/inline_random.jpg +# meta developer: @hikarimods +# scope: inline +# scope: hikka_only + +from random import choice, randint + +from .. import loader, utils +from ..inline.types import InlineQuery + + +@loader.tds +class InlineRandomMod(loader.Module): + """Random tools for your userbot""" + + strings = {"name": "InlineRandom"} + + @loader.inline_everyone + async def coin_inline_handler(self, query: InlineQuery) -> dict: + """Heads or tails?""" + + r = "🦅 Heads" if randint(0, 1) else "🪙 Tails" + + return { + "title": "Toss a coin", + "description": "Trust in the God of luck, and he will be by your side!", + "message": f"The God of luck tells us... {r}", + "thumb": "https://img.icons8.com/external-justicon-flat-justicon/64/000000/external-coin-pirates-justicon-flat-justicon-1.png", + } + + @loader.inline_everyone + async def random_inline_handler(self, query: InlineQuery) -> dict: + """[number] - Send random number less than specified""" + + if not query.args: + return + + a = query.args + + if not str(a).isdigit(): + return + + return { + "title": f"Toss random number less or equal to {a}", + "description": "Trust in the God of luck, and he will be by your side!", + "message": f"The God of luck screams... {randint(1, int(a))}", + "thumb": "https://img.icons8.com/external-flaticons-flat-flat-icons/64/000000/external-numbers-auction-house-flaticons-flat-flat-icons.png", + } + + @loader.inline_everyone + async def choice_inline_handler(self, query: InlineQuery) -> dict: + """[args, separated by comma] - Make a choice""" + + if not query.args or not query.args.count(","): + return + + a = query.args + + return { + "title": "Choose one item from list", + "description": "Trust in the God of luck, and he will be by your side!", + "message": ( + "The God of luck whispers..." + f" {choice(a.split(',')).strip()}" + ), + "thumb": "https://img.icons8.com/external-filled-outline-geotatah/64/000000/external-choice-customer-satisfaction-filled-outline-filled-outline-geotatah.png", + } + + @loader.inline_everyone + async def person_inline_handler(self, query: InlineQuery) -> dict: + """This person doesn't exist""" + + return { + "photo": f"https://thispersondoesnotexist.com/image?id={utils.rand(10)}", + "title": "This person doesn't exist", + } diff --git a/hikariatama/ftg/inline_spotify.py b/hikariatama/ftg/inline_spotify.py new file mode 100644 index 0000000..eabbb63 --- /dev/null +++ b/hikariatama/ftg/inline_spotify.py @@ -0,0 +1,363 @@ +__version__ = (2, 1, 1) + +# █ █ ▀ █▄▀ ▄▀█ █▀█ ▀ +# █▀█ █ █ █ █▀█ █▀▄ █ +# © Copyright 2022 +# https://t.me/hikariatama +# +# 🔒 Licensed under the GNU AGPLv3 +# 🌐 https://www.gnu.org/licenses/agpl-3.0.html + +# meta pic: https://img.icons8.com/color/480/000000/playstation-buttons.png +# meta banner: https://mods.hikariatama.ru/badges/inline_spotify.jpg +# meta developer: @hikarimods +# scope: inline +# scope: hikka_only +# scope: hikka_min 1.5.3 + +import asyncio +import logging +import time +from math import ceil +from typing import Union + +from telethon.tl.types import Message + +from .. import loader, utils +from ..inline.types import InlineCall, InlineMessage + +logger = logging.getLogger(__name__) + + +def create_bar(pb): + try: + percentage = ceil(pb["progress_ms"] / pb["item"]["duration_ms"] * 100) + bar_filled = ceil(percentage / 10) + bar_empty = 10 - bar_filled + bar = "".join("─" for _ in range(bar_filled)) + bar += "🞆" + bar += "".join("─" for _ in range(bar_empty)) + + bar += ( + f' {pb["progress_ms"] // 1000 // 60:02}:{pb["progress_ms"] // 1000 % 60:02} /' + ) + bar += ( + f' {pb["item"]["duration_ms"] // 1000 // 60:02}:{pb["item"]["duration_ms"] // 1000 % 60:02}' + ) + except Exception: + bar = "──────🞆─── 0:00 / 0:00" + + return bar + + +@loader.tds +class InlineSpotifyMod(loader.Module): + """EXTENSION for SpotifyNow mod, that allows you to send interactive player.""" + + strings = { + "name": "InlineSpotify", + "input": "🎧 Enter the track name", + "search": "🔎 Search", + "listening_to": "I'm listening to", + "download": "📥 Download", + } + + strings_ru = { + "input": "🎧 Введи название трека", + "search": "🔎 Поиск", + "_cmd_doc_splayer": ( + "Отправляет интерактивный плеер Spotify (активен в течение 5 минут!)" + ), + "_cls_doc": ( + "Дополнение для модуля SpotifyNow, позволяющее вызвать интерактивный плеер." + ), + "listening_to": "Сейчас я слушаю", + "download": "📥 Скачать", + } + + strings_it = { + "input": "🎧 Inserisci il nome della traccia", + "search": "🔎 Cerca", + "_cmd_doc_splayer": ( + "Invia un player Spotify interattivo (attivo per 5 minuti!)" + ), + "_cls_doc": ( + "Estensione per il modulo SpotifyNow, che consente di inviare un player" + " interattivo." + ), + "listening_to": "Sto ascoltando", + "download": "📥 Scarica", + } + + strings_es = { + "input": "🎧 Introduzca el nombre de la pista", + "search": "🔎 Buscar", + "_cmd_doc_splayer": ( + "Envía un reproductor de Spotify interactivo (¡activo durante 5 minutos!)" + ), + "_cls_doc": ( + "Extensión para el módulo SpotifyNow, que permite enviar un reproductor" + " interactivo." + ), + "listening_to": "Estoy escuchando", + "download": "📥 Descargar", + } + + strings_uz = { + "input": "🎧 Ishora nomini kiriting", + "search": "🔎 Qidirish", + "_cmd_doc_splayer": ( + "Qo'llab-quvvatlash uchun Spotify interaktiv oynasini yuboring (5 daqiqada" + " faol!)" + ), + "_cls_doc": ( + "SpotifyNow moduli uchun kengaytma, interaktiv oynani yuborish mumkin." + ), + "listening_to": "Meni eshitib turaman", + "download": "📥 Yuklab oling", + } + + strings_tr = { + "input": "🎧 Parçanın adını girin", + "search": "🔎 Ara", + "_cmd_doc_splayer": ( + "Etkileşimli bir Spotify oynatıcı gönderir (5 dakika boyunca etkin!)" + ), + "_cls_doc": ( + "SpotifyNow modülü eklentisi, etkileşimli bir oynatıcı göndermenizi sağlar." + ), + "listening_to": "Şu anda dinliyorum", + "download": "📥 İndir", + } + + strings_kk = { + "input": "🎧 Тақырып атауын енгізіңіз", + "search": "🔎 іздеу", + "_cmd_doc_splayer": ( + "Spotify интерактивті ойынды жіберіңіз (5 минутта белсенді!)" + ), + "_cls_doc": ( + "SpotifyNow модулі қосымшасы, интерактивті ойынды жіберуге мүмкіндік" + " береді." + ), + "listening_to": "Ағымда маңызды болатындыңызды көрудіңіз керек", + "download": "📥 Жүктеу", + } + + strings_de = { + "input": "🎧 Geben Sie den Namen des Tracks ein", + "search": "🔎 Suche", + "_cmd_doc_splayer": ( + "Sendet einen interaktiven Spotify-Player (aktiv für 5 Minuten!)" + ), + "_cls_doc": ( + "Erweiterung für das SpotifyNow-Modul, das es ermöglicht, einen" + " interaktiven Player zu senden." + ), + "listening_to": "Ich höre zu", + "download": "📥 Herunterladen", + } + + async def _reload_sp(self, once: bool = False): + while True: + self.sp = getattr(self.lookup("SpotifyMod"), "sp", None) + if once: + break + + await asyncio.sleep(5) + + async def client_ready(self): + self.sp = None + + self._tasks = [asyncio.ensure_future(self._reload_sp())] + await self._reload_sp(True) + + self._active_forms = [] + + async def on_unload(self): + for task in self._tasks: + task.cancel() + + async def inline_close(self, call: InlineCall): + if any( + call.form.get("uid") == getattr(i, "unit_id", None) + for i in self._active_forms + ): + self._active_forms.remove( + next( + i + for i in self._active_forms + if call.form.get("uid") == getattr(i, "unit_id", None) + ) + ) + + await call.delete() + + async def sp_previous(self, call: InlineCall): + self.sp.previous_track() + await self.inline_iter(call, True) + + async def sp_next(self, call: InlineCall): + self.sp.next_track() + await self.inline_iter(call, True) + + async def sp_pause(self, call: InlineCall): + self.sp.pause_playback() + await self.inline_iter(call, True) + + async def sp_play(self, call: InlineCall): + self.sp.start_playback() + await self.inline_iter(call, True) + + async def sp_shuffle(self, call: InlineCall, state: bool): + self.sp.shuffle(state) + await self.inline_iter(call, True) + + async def sp_repeat(self, call: InlineCall, state: bool): + self.sp.repeat(state) + await self.inline_iter(call, True) + + async def sp_play_track(self, call: InlineCall, query: str): + try: + track = self.sp.track(query) + except Exception: + search = self.sp.search(q=query, type="track", limit=1) + try: + track = search["tracks"]["items"][0] + except Exception: + return + + self.sp.add_to_queue(track["id"]) + self.sp.next_track() + + async def inline_iter( + self, + call: Union[InlineCall, InlineMessage], + once: bool = False, + uid: str = False, + ): + try: + if not uid: + uid = getattr(call, "unit_id", call.form["id"]) + + until = time.time() + 5 * 60 + while ( + any(uid == i.unit_id for i in self._active_forms) + and until > time.time() + or once + ): + pb = self.sp.current_playback() + is_resuming = ( + "actions" in pb + and "disallows" in pb["actions"] + and "resuming" in pb["actions"]["disallows"] + and pb["actions"]["disallows"]["resuming"] + ) + + try: + artists = [artist["name"] for artist in pb["item"]["artists"]] + except Exception: + artists = [] + + try: + track = pb["item"]["name"] + track_id = pb["item"]["id"] + except Exception: + track = "" + track_id = "" + + full_name = f"{', '.join(artists)} - {track}" + + keyboard = [ + [ + ( + {"text": "🔁", "callback": self.sp_repeat, "args": (False,)} + if pb["repeat_state"] + else { + "text": "🔂", + "callback": self.sp_repeat, + "args": (True,), + } + ), + {"text": "⏮", "callback": self.sp_previous}, + ( + {"text": "⏸", "callback": self.sp_pause} + if is_resuming + else {"text": "▶️", "callback": self.sp_play} + ), + {"text": "⏭", "callback": self.sp_next}, + ( + { + "text": "↩️", + "callback": self.sp_shuffle, + "args": (False,), + } + if pb["shuffle_state"] + else { + "text": "🔀", + "callback": self.sp_shuffle, + "args": (True,), + } + ), + ], + [ + { + "text": self.strings("search"), + "input": self.strings("input"), + "handler": self.sp_play_track, + }, + { + "text": self.strings("download"), + "callback": self._download, + "args": (full_name,), + }, + {"text": "🔗 Link", "url": f"https://song.link/s/{track_id}"}, + ], + [{"text": "🚫 Close", "callback": self.inline_close}], + ] + + text = ( + f"🎧 {self.strings('listening_to')} {full_name}\n{create_bar(pb)}\u206f" + ) + + await call.edit( + text, + reply_markup=keyboard, + disable_web_page_preview=False, + ) + + if once: + break + + await asyncio.sleep(10) + except Exception: + logger.exception("BRUH") + + async def _download(self, call: InlineCall, track: str): + await call.answer(self.strings("download")) + await self.allmodules.commands["sfind"]( + await call.form["caller"].reply( + f"{self.get_prefix()}sfind {utils.escape_html(track)}" + ) + ) + + @loader.command( + ru_doc="Отправляет интерактивный плеер Spotify (активен в течение 5 минут!)", + it_doc="Invia un player interattivo di Spotify (attivo per 5 minuti!)", + de_doc="Sendet einen interaktiven Spotify-Player (aktiv für 5 Minuten!)", + tr_doc="Etkin Spotify oynatıcı gönderir (5 dakika boyunca aktif!)", + uz_doc="Faol Spotify oynatuvchisini yuboradi (5 daqiqada aktiv!)", + es_doc=( + "Envía un reproductor interactivo de Spotify (activo durante 5 minutos!)" + ), + kk_doc="Интерактивті Spotify ойындысын жібереді (5 минутта актив!)", + ) + async def splayer(self, message: Message): + """Send interactive Spotify player (active only for 5 minutes!)""" + form = await self.inline.form( + "🐻 Bear with us, while player is loading...", message=message + ) + + self._active_forms += [form] + self._tasks += [asyncio.ensure_future(self.inline_iter(form))] diff --git a/hikariatama/ftg/insult.py b/hikariatama/ftg/insult.py new file mode 100644 index 0000000..0103a1e --- /dev/null +++ b/hikariatama/ftg/insult.py @@ -0,0 +1,219 @@ +# █ █ ▀ █▄▀ ▄▀█ █▀█ ▀ +# █▀█ █ █ █ █▀█ █▀▄ █ +# © Copyright 2022 +# https://t.me/hikariatama +# +# 🔒 Licensed under the GNU AGPLv3 +# 🌐 https://www.gnu.org/licenses/agpl-3.0.html + +# meta pic: https://img.icons8.com/color/480/000000/angry--v1.png +# meta banner: https://mods.hikariatama.ru/badges/insult.jpg +# meta developer: @hikarimods +# scope: hikka_min 1.2.10 + +import random + +from telethon.tl.types import Message + +from .. import loader, utils + + +@loader.tds +class PoliteInsultMod(loader.Module): + """If you need to insult but to be intelligent""" + + strings = { + "name": "PoliteInsult", + "insult": ( + "🤬 {} you are {} {} {} {}" + ), + "adjectives_start": [ + "temperamental", + "rude", + "silly to me", + "arrogant", + "non-individualistic", + "undisciplined", + "unprofessional", + "irresponsible", + "reckless", + "indifferent to meser", + ], + "nouns": ["participant of this group chat", "this world citizen"], + "starts": [ + ( + "I don't want to jump to conclusions and I certainly can't claim, and" + " this is my subjective opinion, but" + ), + ( + "Having analyzed the situation, I can express my subjective opinion. It" + " lies in the fact that" + ), + ( + "Not trying to make anyone feel bad, but just expressing my humble" + " point of view, which does not affect other people's points of view, I" + " can say that" + ), + ( + "Without intending to affect any social minorities, I would like to say" + " that" + ), + ], + } + + strings_ru = { + "insult": ( + "🤬 {} ты - {} {} {} {}" + ), + "adjectives_start": [ + "вспыльчивый(-ая)", + "невоспитанный(-ая)", + "осточертевший(-ая) мне", + "глуповатый(-ая)", + "надменный(-ая)", + "неиндивидуалистичный(-ая)", + "индифферентный(-ая)", + "недисциплинированный(-ая)", + "непрофессиональный(-ая)", + "безответственный(-ая)", + "безрассудный(-ая)", + "безразличный(-ая) мне", + ], + "nouns": ["участник(-ца) данного чата", "житель(-ница) мира сего"], + "starts": [ + "Не хочу делать поспешных выводов, но", + "Я, конечно, не могу утверждать, и это мое субъективное мнение, но", + ( + "Проанализировав ситуацию, я могу высказать свое субъективное мнение." + " Оно заключается в том, что" + ), + ( + "Не пытаясь никого оскорбить, а лишь высказывая свою скромную точку" + " зрения, которая не влияет на точку зрения других людей, могу" + " сказать, что" + ), + ( + "Не преследуя попытку затронуть какие-либо социальные меньшинства, хочу" + " сказать, что" + ), + ], + } + + strings_de = { + "insult": ( + "🤬 {} du bist {} {} {} {}" + ), + "adjectives_start": [ + "launisch", + "hässlich", + "sinnlos", + "überheblich", + "nicht-individualistisch", + "unordentlich", + "unprofessionell", + "unverantwortlich", + "unvernünftig", + "uninteressiert", + ], + "nouns": ["Teilnehmer dieser Gruppe", "dieser Weltbürger"], + "starts": [ + ( + "Ich möchte nicht zu voreilig sein und kann nicht behaupten, und" + " dies ist meine subjektive Meinung, aber" + ), + ( + "Nachdem ich die Situation analysiert habe, kann ich meine subjektive" + " Meinung ausdrücken. Es liegt darin, dass" + ), + ( + "Ohne jemanden verletzen zu wollen, sondern nur meine bescheidene" + " Meinung auszudrücken, die die Meinungen anderer Menschen nicht" + " beeinflusst, kann ich sagen, dass" + ), + ( + "Ohne die Absicht, irgendwelche sozialen Minderheiten zu beeinflussen," + " möchte ich sagen, dass" + ), + ], + } + + strings_tr = { + "insult": ( + "🤬 {} sen {} {} {} {}" + ), + "adjectives_start": [ + "öfkeli", + "kaba", + "gözümü korkutmuş", + "kibirli", + "bireysel olmayan", + "düzensiz", + "profesyonel olmayan", + "sorumluluk almamış", + "akılsız", + "ilgisiz", + ], + "nouns": ["bu sohbet grubunun katılımcısı", "bu dünya vatandaşı"], + "starts": [ + ( + "Çabucak sonuçlara atlamak istemiyorum ve kesinlikle iddia edemem," + " ve bu benim kişisel görüşüm, ama" + ), + ( + "Durumu analiz ettiğimde, kişisel görüşümü ifade edebilirim. Bunun" + " içinde şu var ki" + ), + ( + "Herhangi biri duygulanmasını istememekle birlikte, sadece kibarca" + " bir görüş belirtmek, kişilerin görüşlerinin etkilenmediği, ki" + " söyleyebilirim ki" + ), + ( + "Herhangi bir sosyal azınlığı etkilemek için bir girişimde bulunmadan," + " söylemek istediğim şey budur" + ), + ], + } + + strings_hi = { + "insult": "🤬 {} तुम {} {} {} {}", + "adjectives_start": [ + "अशांत", + "अज्ञानी", + "अच्छी तरह से नहीं देखा", + "अपमानजनक", + "गैर-व्यक्तिगत", + "अनुचित", + "अप्रतिबंधी", + "अदायगी", + "असंवेदनशील", + "अव्यक्तिक", + ], + "nouns": ["इस चैट के भागीदार", "इस विश्व नागरिक"], + "starts": [ + ( + "मैं जल्दी निष्कर्षों को नहीं चाहता हूं और यह कहने से नहीं कि" + " यह मेरा व्यक्तिगत राय है, लेकिन" + ), + "अवस्था का विश्लेषण करके, मैं अपना व्यक्तिगत राय व्यक्त कर सकता हूं। इसमें यह है कि", + ( + "किसी को दुखाने की कोशिश न करते हुए, केवल मेरा बहुत छोटा राय" + " बताना, लोगों की रायों को प्रभावित न करने के लिए, जो" + " मैं कह सकता हूं कि" + ), + "किसी सामाजिक अनुकूलित समूह को प्रभावित न करने के लिए, मैं कहना चाहता हूं कि", + ], + } + + async def insultocmd(self, message: Message): + """Use when angry""" + await utils.answer( + message, + self.strings("insult").format( + random.choice(self.strings("starts")), + random.choice(self.strings("adjectives_start")), + random.choice(self.strings("adjectives_start")), + random.choice(self.strings("nouns")), + random.choice(["!!!!", "!", "."]), + ), + ) diff --git a/hikariatama/ftg/keyword.py b/hikariatama/ftg/keyword.py new file mode 100644 index 0000000..7c23d4a --- /dev/null +++ b/hikariatama/ftg/keyword.py @@ -0,0 +1,369 @@ +# █ █ ▀ █▄▀ ▄▀█ █▀█ ▀ +# █▀█ █ █ █ █▀█ █▀▄ █ +# © Copyright 2022 +# https://t.me/hikariatama +# +# 🔒 Licensed under the GNU AGPLv3 +# 🌐 https://www.gnu.org/licenses/agpl-3.0.html + +# scope: hikka_min 1.2.10 + +# meta pic: https://img.icons8.com/fluency/48/000000/macbook-chat.png +# meta banner: https://mods.hikariatama.ru/badges/keyword.jpg +# meta developer: @hikarimods +# scope: hikka_only + +import contextlib +import re + +from telethon.tl.types import Message + +from .. import loader, utils + + +@loader.tds +class KeywordMod(loader.Module): + """Allows you to create custom filters with regexes, commands and unlimited funcionality""" + + strings = { + "name": "Keyword", + "args": "🚫 Args are incorrect", + "kw_404": '🚫 Keyword "{}" not found', + "kw_added": "✅ New keyword:\nTrigger: {}\nMessage: {}\n{}{}{}{}{}", + "kw_removed": '✅ Keyword "{}" removed', + "kwbl_list": "🦊 Blacklisted chats:\n{}", + "bl_added": "✅ This chat is now blacklisted for Keywords", + "bl_removed": "✅ This chat is now whitelisted for Keywords", + "sent": "🦊 [Keywords]: Sent message to {}, triggered by {}:\n{}", + "kwords": "🦊 Current keywords:\n{}", + "no_command": ( + "🚫 Execution of command forbidden, because message contains reply" + ), + } + + strings_ru = { + "args": "🚫 Неверные аргументы", + "kw_404": '🚫 Кейворд "{}" не найден', + "kw_added": "✅ Новый кейворд:\nТриггер: {}\nСообщение: {}\n{}{}{}{}{}", + "kw_removed": '✅ Кейворд "{}" удален', + "kwbl_list": "🦊 Чаты в черном списке:\n{}", + "bl_added": "✅ Этот чат теперь в черном списке Кейвордов", + "bl_removed": "✅ Этот чат больше не в черном списке Кейвордов", + "sent": "🦊 [Кейворды]: Отправлено сообщение в {}, активировано {}:\n{}", + "kwords": "🦊 Текущие кейворды:\n{}", + "no_command": ( + "🚫 Команда не была выполнена, так как сообщение содержит реплай" + ), + "_cmd_doc_kword": ( + "<кейворд | можно в кавычках | & для нескольких слов, которые должны быть в" + " сообщении в любом порядке> <сообщение | оставь пустым для удаления" + " кейворда> [-r для полного совпадения] [-m для автопрочтения сообщения]" + " [-l для включения логирования] [-e для включения регулярных выражений]" + ), + "_cmd_doc_kwords": "Показать активные кейворды", + "_cmd_doc_kwbl": "Добавить чат в черный список кейвордов", + "_cmd_doc_kwbllist": "Показать чаты в черном списке", + "_cls_doc": "Создавай кастомные кейворды с регулярными выражениями и командами", + } + + strings_de = { + "args": "🚫 Falsche Argumente", + "kw_404": '🚫 Keyword "{}" nicht gefunden', + "kw_added": "✅ Neuer Keyword:\nTrigger: {}\nNachricht: {}\n{}{}{}{}{}", + "kw_removed": '✅ Keyword "{}" entfernt', + "kwbl_list": "🦊 Blacklisted Chats:\n{}", + "bl_added": "✅ Dieser Chat ist nun blacklisted für Keywords", + "bl_removed": "✅ Dieser Chat ist nun whitelisted für Keywords", + "sent": "🦊 [Keywords]: Nachricht an {}, getriggert durch {}:\n{}", + "kwords": "🦊 Aktuelle Keywords:\n{}", + "no_command": ( + "🚫 Kommando nicht ausgeführt, da die Nachricht einen Reply enthält" + ), + "_cmd_doc_kword": ( + " [-r für exakte Übereinstimmung] [-m für" + " automatische Nachrichtenlöschung] [-l für Logging] [-e für reguläre" + " Ausdrücke]" + ), + "_cmd_doc_kwords": "Zeige aktive Keywords", + "_cmd_doc_kwbl": "Füge Chat zur Keyword Blacklist hinzu", + "_cmd_doc_kwbllist": "Zeige Chats in der Keyword Blacklist", + "_cls_doc": "Erstelle eigene Keywords mit regulären Ausdrücken und Befehlen", + } + + strings_hi = { + "args": "🚫 गलत तर्क", + "kw_404": '🚫 "{}" कीवर्ड नहीं मिला', + "kw_added": "✅ नया कीवर्ड:\nट्रिगर: {}\nसंदेश: {}\n{}{}{}{}{}", + "kw_removed": '✅ "{}" कीवर्ड हटा दिया', + "kwbl_list": "🦊 ब्लैकलिस्टेड चैट्स:\n{}", + "bl_added": "✅ यह चैट अब कीवर्ड ब्लैकलिस्ट में है", + "bl_removed": "✅ यह चैट अब कीवर्ड व्हाइटलिस्ट में है", + "sent": "🦊 [कीवर्ड्स]: {} को, {} ने ट्रिगर किया:\n{}", + "kwords": "🦊 वर्तमान कीवर्ड्स:\n{}", + "no_command": "🚫 कमांड नहीं चलाया क्योंकि संदेश रिप्लाई का सामना कर रहा है", + "_cmd_doc_kword": ( + "<कीवर्ड | उदाहरण के लिए & | & के बाद एक से अधिक शब्द, जो संदेश में किसी भी" + " क्रम में होने चाहिए> <संदेश | खाली छोड़ने से कीवर्ड हट जाएगा> [-r बिल्कुल" + " मेल के लिए] [-m स्वचालित संदेश हटाने के लिए] [-l लॉगिंग के लिए] [-e" + " रेगुलर एक्सप्रेशन के लिए]" + ), + "_cmd_doc_kwords": "वर्तमान कीवर्ड्स दिखाएं", + "_cmd_doc_kwbl": "कीवर्ड ब्लैकलिस्ट में चैट जोड़ें", + "_cmd_doc_kwbllist": "कीवर्ड ब्लैकलिस्ट में चैट दिखाएं", + "_cls_doc": "रेगुलर एक्सप्रेशन और कमांड के साथ अपने कीवर्ड बनाएं", + } + + strings_uz = { + "args": "🚫 Noto'g'ri argument", + "kw_404": '🚫 "{}" kalit so\'z topilmadi', + "kw_added": "✅ Yangi kalit so'z:\nTriger: {}\nXabar: {}\n{}{}{}{}{}", + "kw_removed": "✅ \"{}\" kalit so'z o'chirildi", + "kwbl_list": "🦊 Qora ro'yxatli guruhlar:\n{}", + "bl_added": "✅ Bu guruh kalit so'zlarni qora ro'yxatga qo'shildi", + "bl_removed": "✅ Bu guruh kalit so'zlarni oq ro'yxatga qo'shildi", + "sent": "🦊 [Kalit so'zlarni]: {} ga, {} guruhga xabar jo'natdi:\n{}", + "kwords": "🦊 Hozirgi kalit so'zlarni:\n{}", + "no_command": "🚫 Komanda bajarilmadi chunki xabar javob qaytaradi", + "_cmd_doc_kword": ( + " [-r to'g'ri moslik uchun] [-m avtomatik xabar o'chirish" + " uchun] [-l yozuvni qayd etish uchun] [-e regular ifodalar uchun]" + ), + "_cmd_doc_kwords": "Hozirgi kalit so'zlarni ko'rsatish", + "_cmd_doc_kwbl": "Qora ro'yxatga guruh qo'shish", + "_cmd_doc_kwbllist": "Qora ro'yxatda guruhlar ro'yxatini ko'rsatish", + "_cls_doc": "Regular ifodalarni va buyruqlarni ishlatib kalit so'z yarating", + } + + strings_tr = { + "args": "🚫 Yanlış argüman", + "kw_404": '🚫 "{}" anahtar kelime bulunamadı', + "kw_added": "✅ Yeni anahtar kelime:\nTriger: {}\nMesaj: {}\n{}{}{}{}{}", + "kw_removed": '✅ "{}" anahtar kelime kaldırıldı', + "kwbl_list": "🦊 Kara liste sohbetler:\n{}", + "bl_added": "✅ Bu sohbet anahtar kelimeleri kara listeye eklendi", + "bl_removed": "✅ Bu sohbet anahtar kelimeleri açık listeye eklendi", + "sent": "🦊 [Anahtar Kelimeler]: {}'a, {} sohbetine mesaj gönderdi:\n{}", + "kwords": "🦊 Geçerli anahtar kelimeler:\n{}", + "no_command": "🚫 Komut yürütülemedi çünkü mesaj yanıt veriyor", + "_cmd_doc_kword": ( + " [-r tam eşleme için] [-m otomatik mesaj silmek için]" + " [-l kayıt için] [-e düzenli ifadeler için]" + ), + "_cmd_doc_kwords": "Geçerli anahtar kelimeleri göster", + "_cmd_doc_kwbl": "Sohbeti kara listeye ekle", + "_cmd_doc_kwbllist": "Kara listede sohbetleri göster", + "_cls_doc": ( + "Anahtar kelimeleri oluşturmak için düzenli ifadeleri ve komutları kullanın" + ), + } + + async def client_ready(self): + self.keywords = self.get("keywords", {}) + self.bl = self.get("bl", []) + + async def kwordcmd(self, message: Message): + """ [-r for full match] [-m for autoreading msg] [-l to log in pm] [-e for regular expressions]""" + args = utils.get_args_raw(message) + kw, ph, restrict, ar, l, e, c = "", "", False, False, False, False, False + if "-r" in args: + restrict = True + args = args.replace(" -r", "").replace("-r", "") + + if "-m" in args: + ar = True + args = args.replace(" -m", "").replace("-m", "") + + if "-l" in args: + l = True + args = args.replace(" -l", "").replace("-l", "") + + if "-e" in args: + e = True + args = args.replace(" -e", "").replace("-e", "") + + if "-c" in args: + c = True + args = args.replace(" -c", "").replace("-c", "") + + if args[0] == "'": + kw = args[1 : args.find("'", 1)] + args = args[args.find("'", 1) + 1 :] + elif args[0] == '"': + kw = args[1 : args.find('"', 1)] + args = args[args.find('"', 1) + 1 :] + else: + kw = args.split()[0] + try: + args = args.split(maxsplit=1)[1] + except Exception: + args = "" + + if ph := args: + ph = ph.strip() + kw = kw.strip() + self.keywords[kw] = [f"🤖 {ph}", restrict, ar, l, e, c] + self.set("keywords", self.keywords) + return await utils.answer( + message, + self.strings("kw_added").format( + kw, + utils.escape_html(ph), + ("Restrict: yes\n" if restrict else ""), + ("Auto-read: yes\n" if ar else ""), + ("Log: yes" if l else ""), + ("Regex: yes" if e else ""), + ("Command: yes" if c else ""), + ), + ) + else: + if kw not in self.keywords: + return await utils.answer(message, self.strings("kw_404").format(kw)) + + del self.keywords[kw] + + self.set("keywords", self.keywords) + return await utils.answer(message, self.strings("kw_removed").format(kw)) + + async def kwordscmd(self, message: Message): + """List current kwords""" + res = "" + for kw, ph in self.keywords.items(): + res += ( + "" + + kw + + "\nMessage: " + + utils.escape_html(ph[0]) + + "\n" + + ("Restrict: yes\n" if ph[1] else "") + + ("Auto-read: yes\n" if ph[2] else "") + + ("Log: yes" if ph[3] else "") + + ("Regex: yes" if len(ph) > 4 and ph[4] else "") + + ("Command: yes" if len(ph) > 5 and ph[5] else "") + + "" + ) + if res[-1] != "\n": + res += "\n" + + res += "\n" + + await utils.answer(message, self.strings("kwords").format(res)) + + @loader.group_admin_ban_users + async def kwblcmd(self, message: Message): + """Blacklist chat from answering keywords""" + cid = utils.get_chat_id(message) + if cid not in self.bl: + self.bl.append(cid) + self.set("bl", self.bl) + return await utils.answer(message, self.strings("bl_added")) + else: + self.bl.remove(cid) + self.set("bl", self.bl) + return await utils.answer(message, self.strings("bl_removed")) + + async def kwbllistcmd(self, message: Message): + """List blacklisted chats""" + chat = str(utils.get_chat_id(message)) + res = "" + for user in self.bl: + try: + u = await self._client.get_entity(user) + except Exception: + self.chats[chat]["defense"].remove(user) + continue + + tit = ( + u.first_name if getattr(u, "first_name", None) is not None else u.title + ) + res += ( + " 👺 {tit}{(' ' + u.last_name) if getattr(u, 'last_name', None) is not None else ''}\n" + ) + + if not res: + res = "No" + + return await utils.answer(message, self.strings("kwbl_list").format(res)) + + async def watcher(self, message: Message): + with contextlib.suppress(Exception): + cid = utils.get_chat_id(message) + if cid in self.bl: + return + + for kw, ph in self.keywords.copy().items(): + if len(ph) > 4 and ph[4]: + try: + if not re.match(kw, message.raw_text): + continue + except Exception: + continue + else: + kws = [ + _.strip() for _ in ([kw] if "&" not in kw else kw.split("&")) + ] + trigger = False + for k in kws: + if k.lower() in message.text.lower(): + trigger = True + if not ph[1]: + break + elif k.lower() not in message.text.lower() and ph[1]: + trigger = False + break + + if not trigger: + continue + + offset = 2 + + if ( + len(ph) > 5 + and ph[5] + and ph[0][offset:].startswith(self.get_prefix()) + ): + offset += 1 + + if ph[2]: + await self._client.send_read_acknowledge(cid, clear_mentions=True) + + if ph[3]: + chat = await message.get_chat() + ch = ( + message.first_name + if getattr(message, "first_name", None) is not None + else "" + ) + if not ch: + ch = ( + chat.title + if getattr(message, "title", None) is not None + else "" + ) + await self._client.send_message( + "me", self.strings("sent").format(ch, kw, ph[0]) + ) + + if not message.reply_to_msg_id: + ms = await utils.answer(message, ph[0]) + else: + ms = await message.respond(ph[0]) + + ms.text = ph[0][2:] + + if len(ph) > 5 and ph[5]: + if ph[0][offset:].split()[0] == "del": + await message.delete() + await ms.delete() + elif not message.reply_to_msg_id: + cmd = ph[0][offset:].split()[0] + if cmd in self.allmodules.commands: + await self.allmodules.commands[cmd](ms) + else: + await ms.respond(self.strings("no_command")) diff --git a/hikariatama/ftg/lastcommand.py b/hikariatama/ftg/lastcommand.py new file mode 100644 index 0000000..13f0fc9 --- /dev/null +++ b/hikariatama/ftg/lastcommand.py @@ -0,0 +1,61 @@ +# █ █ ▀ █▄▀ ▄▀█ █▀█ ▀ +# █▀█ █ █ █ █▀█ █▀▄ █ +# © Copyright 2022 +# https://t.me/hikariatama +# +# 🔒 Licensed under the GNU AGPLv3 +# 🌐 https://www.gnu.org/licenses/agpl-3.0.html + +# meta pic: https://static.dan.tatar/lastcommand_icon.png +# meta banner: https://mods.hikariatama.ru/badges/lastcommand.jpg +# meta developer: @hikarimods +# scope: hikka_only +# scope: hikka_min 1.2.10 + +from telethon.tl.types import Message + +from .. import loader + + +@loader.tds +class LastCommandMod(loader.Module): + """Execute last command""" + + strings = {"name": "LastCommand"} + strings_ru = { + "_cls_doc": "Выполняет последнюю команду", + "_cmd_doc_lc": "Выполнить последнюю команду", + } + strings_de = { + "_cls_doc": "Führt den letzten Befehl aus", + "_cmd_doc_lc": "Letzten Befehl ausführen", + } + strings_tr = { + "_cls_doc": "Son komutu çalıştırır", + "_cmd_doc_lc": "Son komutu çalıştır", + } + strings_hi = { + "_cls_doc": "अंतिम आदेश निष्पादित करें", + "_cmd_doc_lc": "अंतिम आदेश निष्पादित करें", + } + strings_uz = { + "_cls_doc": "Oxirgi buyruqni bajarish", + "_cmd_doc_lc": "Oxirgi buyruqni bajarish", + } + + async def client_ready(self): + orig_dispatch = self.allmodules.dispatch + + def _disp_wrap(command: callable) -> tuple: + txt, func = orig_dispatch(command) + + if "lc" not in txt: + self.allmodules.last_command = func + + return txt, func + + self.allmodules.dispatch = _disp_wrap + + async def lccmd(self, message: Message): + """Execute last command""" + await self.allmodules.last_command(message) diff --git a/hikariatama/ftg/latex.py b/hikariatama/ftg/latex.py new file mode 100644 index 0000000..eb1ed71 --- /dev/null +++ b/hikariatama/ftg/latex.py @@ -0,0 +1,85 @@ +# █ █ ▀ █▄▀ ▄▀█ █▀█ ▀ +# █▀█ █ █ █ █▀█ █▀▄ █ +# © Copyright 2022 +# https://t.me/hikariatama +# +# 🔒 Licensed under the GNU AGPLv3 +# 🌐 https://www.gnu.org/licenses/agpl-3.0.html + +# scope: hikka_min 1.2.10 + +# meta pic: https://img.icons8.com/fluency/452/texshop.png +# meta banner: https://mods.hikariatama.ru/badges/latex.jpg +# meta developer: @hikarimods + +import io +import logging + +import matplotlib.pyplot as plt +from telethon.tl.types import Message + +from .. import loader, utils + +logger = logging.getLogger(__name__) + + +@loader.tds +class LaTeXMod(loader.Module): + """Renders mathematical formulas in LaTeX pngs""" + + strings = { + "name": "LaTeX", + "no_args": "🚫 Specify a formula to render", + "cant_render": "🚫 Can't render formula", + } + + strings_ru = { + "no_args": "🚫 Укажи формулу для рендера", + "cant_render": "🚫 В формуле обнаружена ошибка", + } + + async def latexcmd(self, message: Message): + """ - Create LaTeX render""" + args = utils.get_args_raw(message) + if not args: + await utils.answer(message, self.strings("no_args")) + return + + try: + tex = f"${args}$" + + fig = plt.figure() + ax = fig.add_axes([0, 0, 1, 1]) + ax.set_axis_off() + + t = ax.text( + 0.5, + 0.5, + tex, + horizontalalignment="center", + verticalalignment="center", + fontsize=25, + color="black", + ) + + ax.figure.canvas.draw() + + bbox = t.get_window_extent() + fig.set_size_inches(bbox.width / 80, bbox.height / 80) + buf = io.BytesIO() + plt.savefig(buf) + buf.seek(0) + except Exception: + logger.exception("Can't render formula") + await utils.answer(message, self.strings("cant_render")) + return + + await self._client.send_file( + message.peer_id, + buf.getvalue(), + reply_to=message.reply_to_msg_id, + caption=f"🧮 LaTeX: {args}", + ) + + if message.out: + await message.delete() diff --git a/hikariatama/ftg/leomatch.py b/hikariatama/ftg/leomatch.py new file mode 100644 index 0000000..b006382 --- /dev/null +++ b/hikariatama/ftg/leomatch.py @@ -0,0 +1,374 @@ +# █ █ ▀ █▄▀ ▄▀█ █▀█ ▀ +# █▀█ █ █ █ █▀█ █▀▄ █ +# © Copyright 2022 +# https://t.me/hikariatama +# +# 🔒 Licensed under the GNU AGPLv3 +# 🌐 https://www.gnu.org/licenses/agpl-3.0.html + +# meta desc: Simplifies the interaction with @leomatchbot - Rejects slag, allows you to create filters by age, cities, blacklisted words. +# meta pic: https://static.dan.tatar/leomatch_icon.png +# meta banner: https://mods.hikariatama.ru/badges/leomatch.jpg +# meta developer: @hikarimods +# requires: russian-names +# scope: hikka_only +# scope: hikka_min 1.3.0 + +__version__ = (2, 0, 3) + +import asyncio +import logging +import re +import time +from typing import Iterable, Optional + +from russian_names import RussianNames +from telethon.tl.types import Message + +from .. import loader, utils + +logger = logging.getLogger(__name__) + + +@loader.tds +class LeomatchMod(loader.Module): + """Simplifies the interaction with @leomatchbot - Rejects slag, allows you to create filters by age, cities, blacklisted words. Check .config for more info""" + + strings = {"name": "Leomatch"} + + strings_ru = { + "_cls_doc": ( + "Упрощает взаимодействие с @leomatchbot - отклоняет шлак, позволяет" + " создавать фильтры по возрасту, городам, черному списку слов. Загляни в" + " .config для подробной информации" + ), + } + + _last_decline = 0 + _queue = [] + _groups = {} + + def __init__(self): + self.config = loader.ModuleConfig( + loader.ConfigValue( + "min_age", + 0, + ( + "Минимальный возраст собеседника - будет автоматически отклонять" + " всех, кто младше" + ), + validator=loader.validators.Integer(minimum=0), + ), + loader.ConfigValue( + "max_age", + 100, + ( + "Максимальный возраст собеседника - будет автоматически отклонять" + " всех, кто старше" + ), + validator=loader.validators.Integer(minimum=0), + ), + loader.ConfigValue( + "blacklist_cities", + [], + ( + "Список городов, пользователи из которых будут автоматически" + " отклоняться" + ), + validator=loader.validators.Series(), + ), + loader.ConfigValue( + "whitelist_cities", + [], + ( + "Список городов для белого списка. Пользователи из других городов" + " будут автоматически отклоняться" + ), + validator=loader.validators.Series(), + ), + loader.ConfigValue( + "blacklist_words", + [], + ( + "Если в анкете пользователя есть слово из этого списка, она будет" + " автоматически отклонена" + ), + validator=loader.validators.Series(), + ), + loader.ConfigValue( + "whitelist_words", + [], + ( + "Если в анкете пользователя есть нет слов из этого списка, она" + " будет автоматически отклонена" + ), + validator=loader.validators.Series(), + ), + loader.ConfigValue( + "decline_slag", + True, + "Отклонять ли шлак (Подпишитесь на наш ТикТок и др.)", + validator=loader.validators.Boolean(), + ), + loader.ConfigValue( + "enable", + True, + "Включить ли модуль", + validator=loader.validators.Boolean(), + ), + loader.ConfigValue( + "minimal_len", + 0, + "Минимальное количество слов в анкете", + validator=loader.validators.Integer(minimum=0), + ), + loader.ConfigValue( + "log", + True, + "Отправлять в логи информацию о причинах отклонения анкет", + validator=loader.validators.Boolean(), + ), + loader.ConfigValue( + "delay", + 5, + "Задержка между автоматическим отклонением анкет", + validator=loader.validators.Integer(minimum=3), + ), + loader.ConfigValue( + "no_female", + False, + "Автоматически отклонять девушек", + validator=loader.validators.Boolean(), + ), + loader.ConfigValue( + "no_male", + False, + "Автоматически отклонять парней", + validator=loader.validators.Boolean(), + ), + ) + + async def client_ready(self): + names = RussianNames() + await utils.run_sync(names._fill_base) + + self.female_names = map(lambda x: x.lower(), names._base["woman"]["name"]) + self.male_names = map(lambda x: x.lower(), names._base["man"]["name"]) + + @loader.loop(interval=1, autostart=True) + async def loop(self): + if not self._queue: + return + + if self._last_decline + self.config["delay"] > time.time(): + await asyncio.sleep(self._last_decline + self.config["delay"] - time.time()) + + self._last_decline = time.time() + + log, answer = self._queue.pop(0) + + async with self._client.conversation(1234060895) as conv: + m = await conv.send_message(answer) + await conv.get_response() + + await m.delete() + + if self.config["log"] and log: + logger.info(log) + + async def _decline( + self, + message: Message, + log: Optional[str] = None, + answer: Optional[str] = "👎", + ): + for m in [message] + ( + [m for m in self._groups[message.grouped_id]] + if message.grouped_id and message.grouped_id in self._groups + else [] + ): + await m.delete() + + self._queue += [(log, answer)] + + @staticmethod + def _in(needle: str, haystack: Iterable, alter: str) -> bool: + """ + Checks for occurence of needle in haystack using smart method + :param needle: string to search for + :param haystack: iterable to search in + :param alter: string to search in if needle is not a one word + :return: True if needle is found in haystack, False otherwise + """ + return ( + True + if needle.strip().lower() in map(lambda x: x.lower().strip(), haystack) + else " " in needle and needle.strip().lower() in alter.lower() + ) + + @loader.watcher(chat_id=1234060895, out=True) + async def out_watcher(self, _): + if self._queue: + self._queue = [] + logger.info("Останавливаюсь, т.к. ты отправил сообщение") + return + + @loader.watcher("in", from_id=1234060895) + async def watcher(self, message: Message): + if not self.config["enable"]: + return + + if ( + "Пригласи друзей и получи больше" in message.raw_text + and "Твоя статистика" not in message.raw_text + ): + self._queue = [] + logger.info("Останавливаюсь, т.к. закончились доступные лайки") + return + + if message.grouped_id: + self._groups.setdefault(message.grouped_id, []).append(message) + + if self.config["decline_slag"] and ( + ( + "Подпишись на канал Дайвинчика" in message.raw_text + and "https://t.me/leoday" in message.text + ) + or ( + "Бот не запрашивает личные данные и не идентифицирует пользователей по" + " паспортным данным" + in message.raw_text + ) + or ( + "хочешь больше просмотров в TikTok?" in message.raw_text + and "tiktok.com/tag/дайвинчик" in message.raw_text + ) + or ( + "Пришли свое расположение и увидишь анкеты рядом с тобой" + in message.raw_text + ) + ): + await self._decline( + message, + "Отклонил какой-то шлак", + "Продолжить просмотр анкет", + ) + return + + if self.config["decline_slag"] and message.raw_text == "Это все, идем дальше?": + await self._decline( + message, + "Автоматически продолжаю просмотр анкет", + "Смотреть анкеты", + ) + return + + if ( + message.raw_text.count(",") < 2 + or message.raw_text.startswith("Кому-то понравилась твоя анкета:") + or "Отлично! Надеюсь хорошо проведете время ;) Начинай общаться" + in message.raw_text + ): + return + + words = re.sub( + r" {2,}", + " ", + "".join( + ( + symbol + if symbol + in "abcdefghijklmnopqrstuvwxyzёйцукенгшщзхъфывапролджэячсмитьбю1234567890 " + else " " + ) + for symbol in ( + "" + if len(message.raw_text.lower().split(",", maxsplit=2)) < 3 + or "–" not in message.raw_text + else message.raw_text.lower() + .split(",", maxsplit=2)[2] + .split("–")[1] + ) + ), + ).split() + + user = ( + message.raw_text.split("–")[0].strip() + if "–" in message.raw_text + else message.raw_text + ) + + user_name = user.split(",")[0].strip().lower() + if ( + self.config["no_female"] + and user_name in self.female_names + or self.config["no_male"] + and user_name in self.male_names + ): + await self._decline(message, f"{user} отклонен по несовпадению пола") + + if self.config["minimal_len"] and len(list(words)) < self.config["minimal_len"]: + await self._decline( + message, + f"{user} отклонен из-за слишком короткой анкеты", + ) + return + + if ( + self.config["blacklist_cities"] + and len(message.raw_text.split(",")) >= 3 + and message.raw_text.split(",")[2].split()[0].lower().strip() + in map(lambda x: x.lower().strip(), self.config["blacklist_cities"]) + ): + await self._decline( + message, + f"{user} отклонен из-за наличия города в черном списке", + ) + return + + if ( + self.config["whitelist_cities"] + and len(message.raw_text.split(",")) >= 3 + and message.raw_text.split(",")[2].split()[0].lower().strip() + not in map(lambda x: x.lower().strip(), self.config["whitelist_cities"]) + ): + await self._decline( + message, + f"{user} отклонен из-за отсутствия города в белом списке", + ) + return + + if self.config["blacklist_words"] and any( + self._in(word, words, message.raw_text) + for word in self.config["blacklist_words"] + ): + await self._decline(message, f"{user} отклонен из-за слов в черном списке") + return + + if self.config["whitelist_words"] and not any( + self._in(word, words, message.raw_text) + for word in self.config["whitelist_words"] + ): + await self._decline( + message, + f"{user} отклонен из-за отсутствия в анкете слов из белого списка", + ) + return + + if ( + self.config["min_age"] + and len(message.raw_text.split(",")) >= 2 + and message.raw_text.split(",")[1].strip().isdigit() + and int(message.raw_text.split(",")[1].strip()) < self.config["min_age"] + ): + await self._decline(message, f"{user} отклонен из-за младшего возраста") + return + + if ( + self.config["max_age"] + and len(message.raw_text.split(",")) >= 2 + and message.raw_text.split(",")[1].strip().isdigit() + and int(message.raw_text.split(",")[1].strip()) > self.config["max_age"] + ): + await self._decline(message, f"{user} отклонен из-за старшего возраста") + return diff --git a/hikariatama/ftg/linter.py b/hikariatama/ftg/linter.py new file mode 100644 index 0000000..fe32c6a --- /dev/null +++ b/hikariatama/ftg/linter.py @@ -0,0 +1,88 @@ +# █ █ ▀ █▄▀ ▄▀█ █▀█ ▀ +# █▀█ █ █ █ █▀█ █▀▄ █ +# © Copyright 2022 +# https://t.me/hikariatama +# +# 🔒 Licensed under the GNU AGPLv3 +# 🌐 https://www.gnu.org/licenses/agpl-3.0.html + +# meta pic: https://static.dan.tatar/linter_icon.png +# meta banner: https://mods.hikariatama.ru/badges/linter.jpg +# meta developer: @hikarimods +# requires: black +# scope: hikka_only +# scope: hikka_min 1.2.10 + +import io +import logging +import re +from random import choice + +import black +import requests +from telethon.tl.types import Message + +from .. import loader, utils + +logging.getLogger("blib2to3.pgen2.driver").setLevel(logging.ERROR) + +URL = r"(?:(?:https?|ftp):\/\/)?[\w/\-?=%.]+\.[\w/\-&?=%.]+" + +captions = [ + "Here is your new fresh linted code! Enjoy", + "This was such a hard work to clean this code... Uff..", + "Here we go!", + "Glad to be your virtual code-cleaning-maid!", + "Take this, master!", +] + + +@loader.tds +class PyLinterMod(loader.Module): + """`Black` plugin wrapper for telegram""" + + strings = {"name": "PyLinter", "no_code": "🚫 Please, specify code to lint"} + + async def lintcmd(self, message: Message): + """[code|reply] - Perform automatic lint to python code""" + reply = await message.get_reply_message() + args = utils.get_args_raw(message) + media = message.media or (reply.media if reply else False) + + if media: + try: + args = (await self._client.download_file(media, bytes)).decode("utf-8") + except TypeError: + pass + + if not args: + if not reply: + await utils.answer(message, self.strings("no_code")) + return + + args = reply.raw_text + + if re.match(URL, args): + args = (await utils.run_sync(requests.get, args)).text + + lint_result = black.format_str(args, mode=black.Mode()) + + if len(lint_result) < 2048: + await utils.answer( + message, + f"{utils.escape_html(lint_result)}", + ) + return + + file = io.BytesIO(args.encode("utf-8")) + file.name = "lint_result.py" + await self._client.send_file( + message.peer_id, + file, + caption=( + f"{choice(captions)}" + f" {utils.escape_html(utils.ascii_face())}" + ), + ) + if message.out: + await message.delete() diff --git a/hikariatama/ftg/longread.py b/hikariatama/ftg/longread.py new file mode 100644 index 0000000..5de64c1 --- /dev/null +++ b/hikariatama/ftg/longread.py @@ -0,0 +1,87 @@ +__version__ = (1, 0, 2) + +# █ █ ▀ █▄▀ ▄▀█ █▀█ ▀ +# █▀█ █ █ █ █▀█ █▀▄ █ +# © Copyright 2022 +# https://t.me/hikariatama +# +# 🔒 Licensed under the GNU AGPLv3 +# 🌐 https://www.gnu.org/licenses/agpl-3.0.html + +# meta pic: https://static.dan.tatar/longread_icon.png +# meta banner: https://mods.hikariatama.ru/badges/longread.jpg +# meta developer: @hikarimods +# scope: inline +# scope: hikka_only +# scope: hikka_min 1.2.10 + +from telethon.tl.types import Message + +from .. import loader, utils +from ..inline.types import InlineCall, InlineQuery + + +@loader.tds +class LongReadMod(loader.Module): + """Pack longreads under button spoilers""" + + strings = { + "name": "LongRead", + "no_text": "🚫 Please, specify text to hide", + "longread": ( + "🗄 This is long read\nClick button to show text!\nThis button is" + " active within 6 hours" + ), + } + + strings_ru = { + "no_text": "🚫 Укажи текст, который надо спрятать", + "longread": ( + "🗄 Это - лонгрид\nНажми на кнопку, чтобы показать текст!\nОна" + " активна в течение 6 часов" + ), + "_cmd_doc_lr": " - Создать лонгрид", + "_cls_doc": "Пакует лонгриды под спойлеры", + } + + async def lrcmd(self, message: Message): + """ - Create new hidden message""" + args = utils.get_args_raw(message) + if not args: + return + + await self.inline.form( + self.strings("longread"), + message, + reply_markup={ + "text": "📖 Open spoiler", + "callback": self._handler, + "args": (args,), + }, + disable_security=True, + ) + + async def lr_inline_handler(self, query: InlineQuery): + """Create new hidden message""" + text = query.args + + if not text: + return await query.e400() + + return { + "title": "Create new longread", + "description": "ℹ This will create button-spoiler", + "thumb": "https://img.icons8.com/external-wanicon-flat-wanicon/64/000000/external-read-free-time-wanicon-flat-wanicon.png", + "message": self.strings("longread"), + "reply_markup": { + "text": "📖 Open spoiler", + "callback": self._handler, + "args": (text,), + "disable_security": True, + }, + } + + async def _handler(self, call: InlineCall, text: str): + """Process button presses""" + await call.edit(text) + await call.answer() diff --git a/hikariatama/ftg/lovemagic.py b/hikariatama/ftg/lovemagic.py new file mode 100644 index 0000000..4cef656 --- /dev/null +++ b/hikariatama/ftg/lovemagic.py @@ -0,0 +1,162 @@ +# █ █ ▀ █▄▀ ▄▀█ █▀█ ▀ +# █▀█ █ █ █ █▀█ █▀▄ █ +# © Copyright 2022 +# https://t.me/hikariatama +# +# 🔒 Licensed under the GNU AGPLv3 +# 🌐 https://www.gnu.org/licenses/agpl-3.0.html + +# meta pic: https://static.dan.tatar/lovemagic_icon.png +# meta banner: https://mods.hikariatama.ru/badges/lovemagic.jpg +# meta developer: @hikarimods +# scope: inline +# scope: hikka_only +# scope: hikka_min 1.3.0 + +import json +import logging +import random +from asyncio import sleep +from typing import Union + +import requests +from telethon.tl.types import Message + +from .. import loader, utils +from ..inline.types import InlineCall + +logger = logging.getLogger(__name__) + + +@loader.tds +class ILYMod(loader.Module): + """Famous TikTok hearts animation implemented in Hikka w/o logspam""" + + strings = { + "name": "LoveMagic", + "message": "❤️‍🔥 I want to tell you something...\n{}", + } + + strings_ru = { + "message": "❤️‍🔥 Я хочу тебе сказать кое-что...\n{}", + "_cls_doc": "Известная TikTok анимация сердечек без спама в логи и флудвейтов", + } + + async def client_ready(self): + self.classic_frames = ( + await utils.run_sync( + requests.get, + "https://gist.github.com/hikariatama/89d0246c72e5882e12af43be63f5bca5/raw/08a5df7255d5e925ab2ede1efc892d9dc93af8e1/ily_classic.json", + ) + ).json() + + self.gay_frames = ( + await utils.run_sync( + requests.get, + "https://gist.github.com/hikariatama/3596a7c4f273a41e5289586ccff53a71/raw/f680c04f5855dcb02645b603d84d2496a8ea3350/ily_gay.json", + ) + ).json() + + async def ily_handler( + self, + obj: Union[InlineCall, Message], + text: str, + inline: bool = False, + ): + frames = self.classic_frames + [ + f'{" ".join(text.split()[: i + 1])}' + for i in range(len(text.split())) + ] + + obj = await self.animate(obj, frames, interval=0.5, inline=inline) + + await sleep(10) + if not isinstance(obj, Message): + await obj.edit( + f"{text}", + reply_markup={ + "text": "💔 Хочу также!", + "url": "https://t.me/hikka_talks", + }, + ) + + await obj.unload() + + async def ily_handler_gay( + self, + obj: Union[InlineCall, Message], + text: str, + inline: bool = False, + ): + obj = await self.animate( + obj, + self.gay_frames + + [ + f'{" ".join(text.split()[: i + 1])}' + for i in range(len(text.split())) + ], + interval=0.5, + inline=inline, + ) + + await sleep(10) + if not isinstance(obj, Message): + await obj.edit( + f"{text}", + reply_markup={ + "text": "💔 Хочу также!", + "url": "https://t.me/hikka_talks", + }, + ) + + await obj.unload() + + @loader.command(ru_doc="Отправить анимацию сердец в инлайне") + async def ilyicmd(self, message: Message): + """Send inline message with animated hearts""" + args = utils.get_args_raw(message) + await self.inline.form( + self.strings("message").format("*" * (len(args) or 9)), + reply_markup={ + "text": "🧸 Open", + "callback": self.ily_handler, + "args": (args or "I ❤️ you!",), + "kwargs": {"inline": True}, + }, + message=message, + disable_security=True, + ) + + @loader.command(ru_doc="Отправить анимацию сердец") + async def ily(self, message: Message): + """Send message with animated hearts""" + await self.ily_handler( + message, + utils.get_args_raw(message) or "I ❤️ you!", + inline=False, + ) + + @loader.command(ru_doc="Отправить гейскую анимацию сердец в инлайне") + async def ilygayicmd(self, message: Message): + """Send inline message with animated hearts (gay)""" + args = utils.get_args_raw(message) + await self.inline.form( + self.strings("message").format("*" * (len(args) or 21)), + reply_markup={ + "text": "🧸 Open", + "callback": self.ily_handler_gay, + "args": (args or "I am gay and I 💙 you!",), + "kwargs": {"inline": True}, + }, + message=message, + disable_security=True, + ) + + @loader.command(ru_doc="Отправить гейскую анимацию сердец") + async def ilygay(self, message: Message): + """Send message with animated hearts (gay)""" + await self.ily_handler_gay( + message, + utils.get_args_raw(message) or "I am gay and I 💙 you!", + inline=False, + ) diff --git a/hikariatama/ftg/mindgame.py b/hikariatama/ftg/mindgame.py new file mode 100644 index 0000000..fa165d7 --- /dev/null +++ b/hikariatama/ftg/mindgame.py @@ -0,0 +1,105 @@ +# █ █ ▀ █▄▀ ▄▀█ █▀█ ▀ +# █▀█ █ █ █ █▀█ █▀▄ █ +# © Copyright 2022 +# https://t.me/hikariatama +# +# 🔒 Licensed under the GNU AGPLv3 +# 🌐 https://www.gnu.org/licenses/agpl-3.0.html + +# meta pic: https://static.dan.tatar/mindgame_icon.png +# meta banner: https://mods.hikariatama.ru/badges/mindgame.jpg +# meta developer: @hikarimods +# scope: hikka_only +# scope: hikka_min 1.2.10 + +import random + +import grapheme +from telethon.tl.types import Message + +from .. import loader, utils +from ..inline.types import InlineCall + +EMOJIES = utils.chunks( + list( + grapheme.graphemes( + "😌☺️😞😔🧑‍🏫👨‍🏫👨‍💻🧑‍💻" + "🤵‍♂️🤵👩‍🚀🧑‍🚀👨‍🚒🧑‍🚒👨‍⚖️🧑‍⚖️" + "🧟🧟‍♀️🦹🦹‍♀️🌇🌆🦸🦸‍♂️" + "🧙🧙‍♀️🧚🧚‍♂️👯‍♀️👯👭👫" + "👨‍👩‍👦👨‍👩‍👧👨‍🏭🧑‍🏭👳👳‍♂️🧑👨" + "🕵️🕵️‍♂️🧑‍🌾👨‍🌾👨‍⚕️🧑‍⚕️🕵️🕵️‍♂️" + "👨‍🍳🧑‍🍳🧑‍🔬👨‍🔬🧝‍♀️🧝‍♂️💏👨‍❤️‍💋‍👨" + ) + ), + 2, +) + + +@loader.tds +class MindGameMod(loader.Module): + """Train your brain and mind""" + + strings = { + "name": "MindGame", + "header": ( + "🎮 Find an emoji, that differs from others\nYou've completed {}" + " levels!" + ), + } + + strings_ru = { + "header": ( + "🎮 Найди эмодзи, который отличается от других\nТы прошел {}" + " уровней!" + ) + } + + _ratelimit = [] + + def generate_markup(self, counter: int) -> list: + fail_emoji, next_step_emoji = random.choice(EMOJIES) + markup = [ + {"text": fail_emoji, "callback": self._incorrect} for _ in range(8**2 - 1) + ] + [ + { + "text": next_step_emoji, + "callback": self._next_step_callback, + "args": (counter + 1,), + } + ] + random.shuffle(markup) + return utils.chunks(markup, 8) + + async def mindgamecmd(self, message: Message): + """Open a new mindgame""" + await self.inline.form( + message=message, + text=self.strings("header").format(0), + reply_markup=self.generate_markup(0), + disable_security=True, + ) + + async def _next_step_callback(self, call: InlineCall, counter: int): + if call.from_user.id != self._tg_id and call.from_user.id in self._ratelimit: + await call.answer("You've spent your chance...") + return + + await call.edit( + self.strings("header").format(counter), + self.generate_markup(counter), + ) + + await call.answer("Correct!") + + self._ratelimit = [] + + async def _incorrect(self, call: InlineCall): + if call.from_user.id != self._tg_id: + if call.from_user.id in self._ratelimit: + await call.answer("You've spent your chance...") + return + + self._ratelimit += [call.from_user.id] + + await call.answer("NO!") diff --git a/hikariatama/ftg/moonlove.py b/hikariatama/ftg/moonlove.py new file mode 100644 index 0000000..3af42a9 --- /dev/null +++ b/hikariatama/ftg/moonlove.py @@ -0,0 +1,65 @@ +# █ █ ▀ █▄▀ ▄▀█ █▀█ ▀ +# █▀█ █ █ █ █▀█ █▀▄ █ +# © Copyright 2022 +# https://t.me/hikariatama +# +# 🔒 Licensed under the GNU AGPLv3 +# 🌐 https://www.gnu.org/licenses/agpl-3.0.html + +# meta pic: https://static.dan.tatar/moonlove_icon.png +# meta banner: https://mods.hikariatama.ru/badges/moonlove.jpg +# meta developer: @hikarimods +# scope: hikka_only +# scope: hikka_min 1.2.10 + +from telethon.tl.types import Message + +from .. import loader, utils + +FRAMES = [ + "🌘🌗🌖🌕🌔🌓🌒\n🌙❤️❤️🌙❤️❤️🌙\n❤️💓💓❤️💓💓❤️\n❤️💓💓💓💓💓❤️\n🌙❤️💓💓💓❤️🌙\n🌙🌙❤️💓❤️🌙🌙\n🌙🌙🌙❤️🌙🌙🌙\n🌘🌗🌖🌕🌔🌓🌒", + "🌗🌖🌕🌔🌓🌒🌘\n🌙❤️❤️🌙❤️❤️🌙\n❤️💓💓❤️💓💓❤️\n❤️💓💓💗💓💓❤️\n🌙❤️💓💓💓❤️🌙\n🌙🌙❤️💓❤️🌙🌙\n🌙🌙🌙❤️🌙🌙🌙\n🌗🌖🌕🌔🌓🌒🌘", + "🌖🌕🌔🌓🌒🌘🌗\n🌙❤️❤️🌙❤️❤️🌙\n❤️💓💗❤️💗💓❤️\n❤️💓💗💗💗💓❤️\n🌙❤️💓💗💓❤️🌙\n🌙🌙❤️💓❤️🌙🌙\n🌙🌙🌙❤️🌙🌙🌙\n🌖🌕🌔🌓🌒🌘🌗", + "🌕🌔🌓🌒🌘🌗🌖\n🌙❤️❤️🌙❤️❤️🌙\n❤️💗💗❤️💗💗❤️\n❤️💗💗💗💗💗❤️\n🌙❤️💗💗💗❤️🌙\n🌙🌙❤️💗❤️🌙🌙\n🌙🌙🌙❤️🌙🌙🌙\n🌕🌔🌓🌒🌘🌗🌖", + "🌔🌓🌒🌘🌗🌖🌕\n🌙❤️❤️🌙❤️❤️🌙\n❤️💗💗❤️💗💗❤️\n❤️💗💗💖💗💗❤️\n🌙❤️💗💗💗❤️🌙\n🌙🌙❤️💗❤️🌙🌙\n🌙🌙🌙❤️🌙🌙🌙\n🌔🌓🌒🌘🌗🌖🌕", + "🌓🌒🌘🌗🌖🌕🌔\n🌙❤️❤️🌙❤️❤️🌙\n❤️💗💖❤️💖💗❤️\n❤️💗💖💖💖💗❤️\n🌙❤️💗💖💗❤️🌙\n🌙🌙❤️💗❤️🌙🌙\n🌙🌙🌙❤️🌙🌙🌙\n🌓🌒🌘🌗🌖🌕🌔", + "🌒🌘🌗🌖🌕🌔🌓\n🌙❤️❤️🌙❤️❤️🌙\n❤️💖💖❤️💖💖❤️\n❤️💖💖💖💖💖❤️\n🌙❤️💖💖💖❤️🌙\n🌙🌙❤️💖❤️🌙🌙\n🌙🌙🌙❤️🌙🌙🌙\n🌒🌘🌗🌖🌕🌔🌓", + "🌘🌗🌖🌕🌔🌓🌒\n🌙❤️❤️🌙❤️❤️🌙\n❤️💖💖❤️💖💖❤️\n❤️💖💖💓💖💖❤️\n🌙❤️💖💖💖❤️🌙\n🌙🌙❤️💖❤️🌙🌙\n🌙🌙🌙❤️🌙🌙🌙\n🌘🌗🌖🌕🌔🌓🌒", + "🌗🌖🌕🌔🌓🌒🌘\n🌙❤️❤️🌙❤️❤️🌙\n❤️💖💓❤️💓💖❤️\n❤️💖💓💓💓💖❤️\n🌙❤️💖💓💖❤️🌙\n🌙🌙❤️💖❤️🌙🌙\n🌙🌙🌙❤️🌙🌙🌙\n🌗🌖🌕🌔🌓🌒🌘", +] * 3 + [ # It's shit, I know. But it's the easiest solution tho + "💓", + "💗", + "💖", +] + + +@loader.tds +class MoonLoveMod(loader.Module): + """Animation with moon and hearts for your beloved""" + + strings = {"name": "MoonLove"} + strings_ru = { + "_cls_doc": "Анимация с лунами и сердечками для любимой", + "_cmd_doc_moonlove": "[текст] - Люблю тебя невообразимо", + "_cmd_doc_moonlovei": "[текст] - Люблю тебя невообразимо (инлайн)", + } + + async def moonlovecmd(self, message: Message): + """[text] - Love you to the moon""" + m = await self.animate( + message, + FRAMES, + interval=0.3, + inline=False, + ) + await m.edit(utils.get_args_raw(message) or "❤️") + + async def moonloveicmd(self, message: Message): + """[text] - Love you to the moon [Inline]""" + m = await self.animate( + message, + FRAMES, + interval=0.3, + inline=True, + ) + await m.edit(utils.get_args_raw(message) or "❤️") diff --git a/hikariatama/ftg/musicdl.py b/hikariatama/ftg/musicdl.py new file mode 100644 index 0000000..feac336 --- /dev/null +++ b/hikariatama/ftg/musicdl.py @@ -0,0 +1,65 @@ +# █ █ ▀ █▄▀ ▄▀█ █▀█ ▀ +# █▀█ █ █ █ █▀█ █▀▄ █ +# © Copyright 2022 +# https://t.me/hikariatama +# +# 🔒 Licensed under the GNU AGPLv3 +# 🌐 https://www.gnu.org/licenses/agpl-3.0.html + +# meta pic: https://static.dan.tatar/musicdl_icon.png +# meta banner: https://mods.hikariatama.ru/badges/musicdl.jpg +# meta developer: @hikarimods +# scope: hikka_only +# scope: hikka_min 1.3.0 + +from telethon.tl.types import Message + +from .. import loader, utils + + +@loader.tds +class MusicDLMod(loader.Module): + """Download music""" + + strings = { + "name": "MusicDL", + "args": "🚫 Arguments not specified", + "loading": "🔍 Loading...", + "404": "🚫 Music {} not found", + } + + strings_ru = { + "args": "🚫 Не указаны аргументы", + "loading": "🔍 Загрузка...", + "404": "🚫 Песня {} не найдена", + } + + async def client_ready(self, *_): + self.musicdl = await self.import_lib( + "https://libs.hikariatama.ru/musicdl.py", + suspend_on_error=True, + ) + + @loader.command(ru_doc="<название> - Скачать песню") + async def mdl(self, message: Message): + """ - Download track""" + args = utils.get_args_raw(message) + if not args: + await utils.answer(message, self.strings("args")) + return + + message = await utils.answer(message, self.strings("loading")) + result = await self.musicdl.dl(args, only_document=True) + + if not result: + await utils.answer(message, self.strings("404").format(args)) + return + + await self._client.send_file( + message.peer_id, + result, + caption=f"🎧 {utils.ascii_face()}", + reply_to=getattr(message, "reply_to_msg_id", None), + ) + if message.out: + await message.delete() diff --git a/hikariatama/ftg/neko.py b/hikariatama/ftg/neko.py new file mode 100644 index 0000000..c883866 --- /dev/null +++ b/hikariatama/ftg/neko.py @@ -0,0 +1,148 @@ +# █ █ ▀ █▄▀ ▄▀█ █▀█ ▀ +# █▀█ █ █ █ █▀█ █▀▄ █ +# © Copyright 2022 +# https://t.me/hikariatama +# +# 🔒 Licensed under the GNU AGPLv3 +# 🌐 https://www.gnu.org/licenses/agpl-3.0.html + +# meta pic: https://static.dan.tatar/neko_icon.png +# meta banner: https://mods.hikariatama.ru/badges/neko.jpg +# meta developer: @hikarimods +# scope: hikka_only +# scope: hikka_min 1.2.10 + +import asyncio +import functools +import json +import random +from urllib.parse import quote_plus + +import requests +from telethon.tl.types import Message + +from .. import loader, utils + +phrases = ["Uwu", "Senpai", "Uff", "Meow", "Bonk", "Ara-ara", "Hewwo", "You're cute!"] + + +async def photo(self, args: str) -> str: + return ( + await utils.run_sync(requests.get, f"{self.endpoints['img']}{args}") + ).json()["url"] + + +@loader.tds +class NekosLifeMod(loader.Module): + """NekosLife API Wrapper""" + + strings = {"name": "NekosLife"} + + strings_ru = { + "_cmd_doc_nk": "Отправить аниме арт", + "_cmd_doc_nkct": "Показать доступные категории", + "_cmd_doc_owoify": "OwOфицировать текст", + "_cmd_doc_why": "Почему?", + "_cmd_doc_fact": "А ты знал?", + "_cmd_doc_meow": "Отправляет ASCII-арт кошки", + "_cls_doc": "Обертка NekosLife API", + } + + async def client_ready(self, client, db): + ans = ( + await utils.run_sync(requests.get, "https://nekos.life/api/v2/endpoints") + ).json() + self.categories = json.loads( + "[" + + [_ for _ in ans if "/api" in _ and "/img/" in _][0] + .split("<")[1] + .split(">")[0] + .replace("'", '"') + + "]" + ) + self.endpoints = { + "img": "https://nekos.life/api/v2/img/", + "owoify": "https://nekos.life/api/v2/owoify?text=", + "why": "https://nekos.life/api/v2/why", + "cat": "https://nekos.life/api/v2/cat", + "fact": "https://nekos.life/api/v2/fact", + } + + @loader.pm + async def nkcmd(self, message: Message): + """Send anime pic""" + args = utils.get_args_raw(message) + args = "neko" if args not in self.categories else args + pic = functools.partial(photo, self=self, args=args) + await self.inline.gallery( + message=message, + next_handler=pic, + caption=lambda: f"{random.choice(phrases)} {utils.ascii_face()}", + ) + + @loader.pm + async def nkctcmd(self, message: Message): + """Show available categories""" + cats = "\n".join( + [" |
".join(_) for _ in utils.chunks(self.categories, 5)] + ) + await utils.answer( + message, + f"Available categories:\n{cats}", + ) + + @loader.unrestricted + async def owoifycmd(self, message: Message): + """OwOify text""" + args = utils.get_args_raw(message) + if not args: + args = await message.get_reply_message() + if not args: + await message.delete() + return + + args = args.text + + if len(args) > 180: + message = await utils.answer(message, "OwOifying...") + + args = quote_plus(args) + owo = "" + for chunk in utils.chunks(args, 180): + owo += ( + await utils.run_sync(requests.get, f"{self.endpoints['owoify']}{chunk}") + ).json()["owo"] + await asyncio.sleep(0.1) + await utils.answer(message, owo) + + @loader.unrestricted + async def whycmd(self, message: Message): + """Why?""" + await utils.answer( + message, + ( + "👾" + f" {(await utils.run_sync(requests.get, self.endpoints['why'])).json()['why']}" + ), + ) + + @loader.unrestricted + async def factcmd(self, message: Message): + """Did you know?""" + await utils.answer( + message, + ( + "🧐 Did you know, that" + f" {(await utils.run_sync(requests.get, self.endpoints['fact'])).json()['fact']}" + ), + ) + + @loader.unrestricted + async def meowcmd(self, message: Message): + """Sends cat ascii art""" + await utils.answer( + message, + ( + f"{(await utils.run_sync(requests.get, self.endpoints['cat'])).json()['cat']}" + ), + ) diff --git a/hikariatama/ftg/nekospy.py b/hikariatama/ftg/nekospy.py new file mode 100644 index 0000000..60f3728 --- /dev/null +++ b/hikariatama/ftg/nekospy.py @@ -0,0 +1,1212 @@ +__version__ = (1, 0, 28) + +# ©️ Dan Gazizullin, 2021-2022 +# This file is a part of Hikka Userbot +# 🌐 https://github.com/hikariatama/Hikka +# You can redistribute it and/or modify it under the terms of the GNU AGPLv3 +# 🔑 https://www.gnu.org/licenses/agpl-3.0.html + +# meta pic: https://0x0.st/oRer.webp +# meta banner: https://mods.hikariatama.ru/badges/nekospy.jpg +# meta developer: @hikarimods +# scope: hikka_only +# scope: hikka_min 1.6.0 + +import contextlib +import io +import logging +import time +import typing + +from telethon.tl.types import ( + DocumentAttributeFilename, + Message, + PeerChat, + UpdateDeleteChannelMessages, + UpdateDeleteMessages, + UpdateEditChannelMessage, + UpdateEditMessage, +) +from telethon.utils import get_display_name + +from .. import loader, utils + +logger = logging.getLogger(__name__) + + +@loader.tds +class NekoSpy(loader.Module): + """Sends you deleted and / or edited messages from selected users""" + + rei = "👩‍🎤" + groups = "👥" + pm = "👤" + + strings = { + "name": "NekoSpy", + "state": f"{rei} Spy mode is now {{}}", + "spybl": f"{rei} Current chat added to blacklist for spying", + "spybl_removed": f"{rei} Current chat removed from blacklist for spying", + "spybl_clear": f"{rei} Ignore list for spying cleared", + "spywl": f"{rei} Current chat added to whitelist for spying", + "spywl_removed": f"{rei} Current chat removed from whitelist for spying", + "spywl_clear": f"{rei} Include list for spying cleared", + "whitelist": f"\n{rei} Tracking only messages from:\n{{}}", + "always_track": f"\n{rei} Always tracking messages from:\n{{}}", + "blacklist": f"\n{rei} Ignoring messages from:\n{{}}", + "chat": f"{groups} Tracking messages in groups\n", + "pm": f"{pm} Tracking messages in personal messages\n", + "mode_off": f"{pm} Not tracking messages {{}}spymode\n", + "deleted_pm": ( + '🗑 {} deleted message in' + " pm. Content:\n{}" + ), + "deleted_chat": ( + '🗑 Message in chat {} by {} has been deleted. Content:\n{}' + ), + "edited_pm": ( + '🔏 {} edited message' + " in pm." + " Old content:\n{}" + ), + "edited_chat": ( + '🔏 Message in chat {}' + " by {} has been edited. Old content:\n{}' + ), + "on": "on", + "off": "off", + "cfg_enable_pm": "Enable spy mode in Personal messages", + "cfg_enable_groups": "Enable spy mode in Groups", + "cfg_whitelist": "List of chats to include messages from", + "cfg_blacklist": "List of chats to exclude messages from", + "cfg_always_track": ( + "List of chats to always track messages from, no matter what" + ), + "cfg_log_edits": "Log information about messages being edited", + "cfg_ignore_inline": "Ignore inline messages (sent using @via bots)", + "cfg_fw_protect": "Interval of messages sending to prevent floodwait", + "sd_media": ( + "🔥 {} sent you a self-destructing" + " media" + ), + "save_sd": ( + "🔥 Saving" + " self-destructing media\n" + ), + "cfg_save_sd": "Save self-destructing media", + } + + strings_ru = { + "on": "включен", + "off": "выключен", + "state": f"{rei} Режим слежения теперь {{}}", + "spybl": f"{rei} Текущий чат добавлен в черный список для слежения", + "spybl_removed": ( + f"{rei} Текущий чат удален из черного списка для слежения" + ), + "spybl_clear": f"{rei} Черный список для слежения очищен", + "spywl": f"{rei} Текущий чат добавлен в белый список для слежения", + "spywl_removed": ( + f"{rei} Текущий чат удален из белого списка для слежения" + ), + "spywl_clear": f"{rei} Белый список для слежения очищен", + "whitelist": ( + f"\n{rei} Слежу только" + " за сообщениями от пользователей / групп:\n{}" + ), + "always_track": ( + f"\n{rei} Всегда слежу за сообщениями от пользователей /" + " групп:\n{}" + ), + "blacklist": ( + f"\n{rei} Игнорирую сообщений от пользователей / групп:\n{{}}" + ), + "chat": f"{groups} Слежу за сообщениями в группах\n", + "pm": f"{pm} Слежу за сообщениями в личных сообщениях\n", + "deleted_pm": ( + '🗑 {} удалил сообщение в' + " личке. Содержимое:\n{}" + ), + "deleted_chat": ( + '🗑 Сообщение в чате {} от' + ' {} было удалено. Содержимое:\n{}' + ), + "edited_pm": ( + '🔏 {} отредактировал сообщение в личке. Старое содержимое:\n{}' + ), + "edited_chat": ( + '🔏 Сообщение в чате {} от' + ' {} было отредактировано. Старое содержимое:\n{}' + ), + "mode_off": f"{pm} Не отслеживаю сообщения {{}}spymode\n", + "cfg_enable_pm": "Включить режим шпиона в личных сообщениях", + "cfg_enable_groups": "Включить режим шпиона в группах", + "cfg_whitelist": "Список чатов, от которых нужно сохранять сообщения", + "cfg_blacklist": "Список чатов, от которых нужно игнорировать сообщения", + "cfg_always_track": ( + "Список чатов, от которых всегда следует отслеживать сообщения," + " несмотря ни на что" + ), + "cfg_log_edits": "Сохранять отредактированные сообщения", + "cfg_ignore_inline": "Игнорировать сообщения из инлайн-режима", + "cfg_fw_protect": "Защита от флудвейтов при пересылке", + "_cls_doc": ( + "Сохраняет удаленные и/или отредактированные сообщения от выбранных" + " пользователей" + ), + "sd_media": ( + "🔥 {} отправил вам самоуничтожающееся" + " медиа" + ), + "save_sd": ( + "🔥 Сохраняю" + " самоуничтожающиеся медиа\n" + ), + "cfg_save_sd": "Сохранять самоуничтожающееся медиа", + } + + strings_it = { + "on": "attivato", + "off": "disattivato", + "state": f"{rei} Modalità di tracciamento ora {{}}", + "spybl": ( + f"{rei} Il gruppo corrente è stato aggiunto alla lista nera di" + " tracciamento" + ), + "spybl_removed": ( + f"{rei} Il gruppo corrente è stato rimosso dalla lista nera di" + " tracciamento" + ), + "spybl_clear": f"{rei} Lista nera di tracciamento ripulita", + "spywl": ( + f"{rei} Il gruppo corrente è stato aggiunto alla lista bianca di" + " tracciamento" + ), + "spywl_removed": ( + f"{rei} Il gruppo corrente è stato rimosso dalla lista bianca di" + " tracciamento" + ), + "spywl_clear": f"{rei} Lista bianca di tracciamento ripulita", + "whitelist": ( + f"\n{rei} Sto tracciando solo messaggi da utenti / gruppi:\n{{}}" + ), + "always_track": ( + f"\n{rei} Sto tracciando sempre messaggi da utenti / gruppi:\n{{}}" + ), + "blacklist": f"\n{rei} Ignoro messaggi da utenti / gruppi:\n{{}}", + "chat": f"{groups} Sto tracciando i messaggi nei gruppi\n", + "pm": f"{pm} Sto tracciando i messaggi nei messaggi privati\n", + "deleted_pm": ( + '🗑 {} ha cancellato il' + " messaggio in privato. Contenuto:\n{}" + ), + "deleted_pm": ( + '🗑 {} ha eliminato un messaggio in privato. Contenuto:\n{}' + ), + "deleted_chat": ( + '🗑 Un messaggio nel gruppo {} da {} è stato eliminato.' + " Contenuto:\n{}" + ), + "edited_pm": ( + '🔏 {} ha modificato un' + " messaggio in privato. Vecchio contenuto:\n{}" + ), + "edited_chat": ( + '🔏 Un messaggio nel gruppo {} da {} è stato modificato. Vecchio' + " contenuto:\n{}" + ), + "mode_off": ( + f"{pm} Non sto tenendo traccia dei messaggi" + " {}spymode\n" + ), + "cfg_enable_pm": "Attiva modalità spia nei messaggi privati", + "cfg_enable_groups": "Attiva modalità spia nei gruppi", + "cfg_whitelist": "Lista dei gruppi da cui tenere traccia dei messaggi", + "cfg_blacklist": "Lista dei gruppi da cui ignorare i messaggi", + "cfg_always_track": ( + "Lista dei gruppi da cui tenere traccia dei messaggi," + " non importa quello che succede" + ), + "cfg_log_edits": "Salva i messaggi modificati", + "cfg_ignore_inline": "Ignora i messaggi in modalità inline", + "cfg_fw_protect": "Protezione contro floodwate ai messaggi inoltrati", + "_cls_doc": "Salva i messaggi eliminati e/o modificati da utenti selezionati", + "sd_media": ( + "🔥 {} ti ha inviato un media" + " che si autodistrugge" + ), + "save_sd": ( + "🔥 Salvo" + " i media che si autodistruggono\n" + ), + "cfg_save_sd": "Salva i media che si autodistruggono", + } + + strings_de = { + "on": "Aktiviert", + "off": "Deaktiviert", + "state": f"{rei} Der Tracking-Modus ist jetzt {{}}.", + "spybl": ( + f"{rei} Der aktuelle Chat wurde zur Spionage-Blacklist hinzugefügt." + ), + "spybl_removed": ( + f"{rei} Der aktuelle Chat wurde von der Spionage-Blacklist entfernt." + ), + "spybl_clear": f"{rei} Die Spionage-Blacklist wurde geleert.", + "spywl": ( + f"{rei} Der aktuelle Chat wurde zur Spionage-Whitelist hinzugefügt." + ), + "spywl_removed": ( + f"{rei} Der aktuelle Chat wurde von der Spionage-Whitelist entfernt." + ), + "spywl_clear": f"{rei} Die Spionage-Whitelist wurde geleert.", + "whitelist": f"\n{rei} Ich beobachte nur Nachrichten von:\n{{}}", + "always_track": f"\n{rei} Ich beobachte immer Nachrichten von:\n{{}}", + "blacklist": f"\n{rei} Ich ignoriere Nachrichten von:\n{{}}", + "chat": f"{groups} Ich beobachte Nachrichten in Gruppen.\n", + "pm": f"{pm} Ich beobachte Nachrichten in privaten Nachrichten.\n", + "deleted_pm": ( + '🗑 {} hat eine private Nachricht gelöscht. Inhalt:\n{}' + ), + "deleted_chat": ( + '🗑 Die Nachricht im Chat {}' + ' von {} wurde gelöscht. Inhalt:\n{}' + ), + "edited_pm": ( + '🔏 {} hat eine private Nachricht bearbeitet. Alte Nachricht:\n{}' + ), + "edited_chat": ( + '🔏 Die Nachricht im Chat {}' + ' von {} wurde bearbeitet. Alte Nachricht:\n{}' + ), + "mode_off": ( + f"{pm} Ich beobachte" + " Nachrichten nicht mehr. {}spymode\n" + ), + "cfg_enable_pm": "Aktivieren Sie den Spionage-Modus in privaten Nachrichten", + "cfg_enable_groups": "Aktivieren Sie den Spionage-Modus in Gruppen", + "cfg_whitelist": ( + "Liste der Gruppen, von denen Nachrichten gespeichert werden sollen" + ), + "cfg_blacklist": ( + "Liste der Gruppen, von denen Nachrichten ignoriert werden sollen" + ), + "cfg_always_track": ( + "Liste der Gruppen, von denen immer Nachrichten verfolgt werden sollen," + " egal was passiert" + ), + "cfg_log_edits": "Gespeicherte bearbeitete Nachrichten", + "cfg_ignore_inline": "Ignoriere Nachrichten aus Inline-Modus", + "cfg_fw_protect": "Schutz vor Floodwässern beim Weiterleiten", + "_cls_doc": ( + "Speichert gelöschte bearbeitete Nachrichten von ausgewählten Benutzern" + ), + "sd_media": ( + "🔥 {} hat Ihnen ein selbstzerstörendes" + " Medium gesendet" + ), + "save_sd": ( + "🔥 Speichere" + " selbstzerstörende Medien\n" + ), + "cfg_save_sd": "Speichern Sie selbstzerstörende Medien", + } + + strings_uz = { + "on": "yoqildi", + "off": "o'chirildi", + "state": f"{rei} Shu paytda spy rejimi {{}}", + "spybl": f"{rei} Ushbu chat spay rejimining qora ro'yxatiga qo'shildi", + "spybl_removed": ( + f"{rei} Ushbu chat spay rejimining qora ro'yxatidan olib tashlandi" + ), + "spybl_clear": f"{rei} Spay rejimining qora ro'yxati tozalandi", + "spywl": f"{rei} Ushbu chat spay rejimining oq ro'yxatiga qo'shildi", + "spywl_removed": ( + f"{rei} Ushbu chat spay rejimining oq ro'yxatidan olib tashlandi" + ), + "spywl_clear": f"{rei} Spay rejimining oq ro'yxati tozalandi", + "whitelist": f"\n{rei} Faqat kelgan xabarlarni kuzatish\n{{}}", + "always_track": f"\n{rei} Har doim kelgan xabarlarni kuzatish\n{{}}", + "blacklist": f"\n{rei} kelgan xabarlarni o'chirish\n{{}}", + "chat": f"{groups} Gruplardagi xabarlarimni kuzatish\n", + "pm": f"{pm} Shaxsiy xabarlarimni kuzatish\n", + "deleted_pm": ( + '🗑 {} shaxsiy xabarni' + " o'chirdi. Xabar:\n{}" + ), + "deleted_chat": ( + '🗑 {} guruhdan {} xabarni o\'chirdi. Xabar:\n{}' + ), + "edited_pm": ( + '🔏 {} shaxsiy xabarni' + " tahrirladi. Eski xabar:\n{}" + ), + "edited_chat": ( + '🔏 {} guruhdan {} xabarni tahrirladi. Eski xabar:\n{}' + ), + "mode_off": ( + f"{pm} Xabarlarimni kuzatishni to'xtatdim{{}}spymode\n" + ), + "cfg_enable_pm": "Shaxsiy xabarlarimni kuzatishni yoqish", + "cfg_enable_groups": "Guruh xabarlarimni kuzatishni yoqish", + "cfg_whitelist": "Xabarlarni saqlash kerak bo'lgan suhbatlar ro'yxati", + "cfg_blacklist": "Xabarlarni o'chirish kerak bo'lgan suhbatlar ro'yxati", + "cfg_always_track": ( + "Nima bo'lishidan qat'i nazar, har doim xabarlarni kuzatib boradigan" + " suhbatlar ro'yxati" + ), + "cfg_log_edits": "Saqlangan tahrirlangan xabarlarni", + "cfg_ignore_inline": "Inline rejimidan kelgan xabarlarni o'chirish", + "cfg_fw_protect": "Forwarding floodlardan himoyalash", + "_cls_doc": ( + "Tanlangan foydalanuvchilardan kelgan va/yoki o'chirilgan yoki tahrirlangan" + " xabarlarni saqlaydi" + ), + "sd_media": ( + "🔥 {} sizga o'chiriladigan media" + " yubordi" + ), + "save_sd": ( + "🔥 O'z-o'zini yo'q" + " qiladigan ommaviy axborot vositalarini saqlash\n" + ), + "cfg_save_sd": "O'chiriladigan media saqlash", + } + + strings_tr = { + "on": "açık", + "off": "kapalı", + "state": f"{rei} Şu anda gizli mod {{}}", + "spybl": f"{rei} Bu sohbet gizli modun siyah listesine eklendi", + "spybl_removed": ( + f"{rei} Bu sohbet gizli modun siyah listesinden kaldırıldı" + ), + "spybl_clear": f"{rei} Gizli modun siyah listesi temizlendi", + "spywl": f"{rei} Bu sohbet gizli modun beyaz listesine eklendi", + "spywl_removed": ( + f"{rei} Bu sohbet gizli modun beyaz listesinden kaldırıldı" + ), + "spywl_clear": f"{rei} Gizli modun beyaz listesi temizlendi", + "whitelist": f"\n{rei} Sadece belirtilen gelen mesajları kaydet\n{{}}", + "always_track": ( + f"\n{rei} Her zaman belirtilen gelen mesajları kaydet\n{{}}" + ), + "blacklist": f"\n{rei} Belirtilen gelen mesajları sil\n{{}}", + "chat": f"{groups} Grup mesajlarımı kaydet\n", + "pm": f"{pm} Özel mesajlarımı kaydet\n", + "deleted_pm": ( + '🗑 {} özel mesajı sildi.' + " Mesaj:\n{}" + ), + "deleted_chat": ( + '🗑 {} guruptan {} mesajı sildi. Mesaj:\n{}' + ), + "edited_pm": ( + '🔏 {} özel mesajı' + " düzenledi. Eski mesaj:\n{}" + ), + "edited_chat": ( + '🔏 {} guruptan {} mesajı düzenledi. Eski mesaj:\n{}' + ), + "mode_off": ( + f"{pm} Mesajlarımı kaydetmeyi kapattım{{}}spymode\n" + ), + "cfg_enable_pm": "Özel mesajlarımı kaydetmeyi aç", + "cfg_enable_groups": "Grup mesajlarımı kaydetmeyi aç", + "cfg_whitelist": "Kaydedilmesi gereken sohbetler listesi", + "cfg_blacklist": "Silinmesi gereken sohbetler listesi", + "cfg_always_track": ( + "Ne olursa olsun, iletileri her zaman izlenecek sohbetler listesi" + ), + "cfg_log_edits": "Kaydedilen düzenlenmiş mesajları", + "cfg_ignore_inline": "Inline modundan gelen mesajları sil", + "cfg_fw_protect": "Forwarding floodlarından korun", + "_cls_doc": ( + "Belirtilen kullanıcıların/sohbetlerin silinmiş, düzenlenmiş veya" + " kaydedilen mesajlarını kaydeder" + ), + "sd_media": ( + "🔥 {} sana silinebilir medya gönderdi" + ), + "save_sd": ( + "🔥 Kendi kendini imha" + " eden medyayı kaydetme\n" + ), + "cfg_save_sd": "Silinebilir medyayı kaydet", + } + + strings_es = { + "on": "activado", + "off": "desactivado", + "state": f"{rei} El modo espía está actualmente {{}}", + "spybl": ( + f"{rei} Este chat ha sido añadido a la lista negra del modo espía" + ), + "spybl_removed": ( + f"{rei} Este chat ha sido eliminado de la lista negra del modo espía" + ), + "spybl_clear": f"{rei} La lista negra del modo espía ha sido limpiada", + "spywl": ( + f"{rei} Este chat ha sido añadido a la lista blanca del modo espía" + ), + "spywl_removed": ( + f"{rei} Este chat ha sido" + " eliminado de la lista blanca del modo espía" + ), + "spywl_clear": f"{rei} La lista blanca del modo espía ha sido limpiada", + "whitelist": ( + f"\n{rei} Guardar solo los mensajes de los especificados\n{{}}" + ), + "always_track": ( + f"\n{rei} Guardar siempre los mensajes de los especificados\n{{}}" + ), + "blacklist": f"\n{rei} Borrar los mensajes de los especificados\n{{}}", + "chat": ( + "👥 Guardar mis" + " mensajes de grupo\n" + ), + "pm": f"{pm} Guardar mis mensajes privados\n", + "deleted_pm": ( + '🗑 {} eliminó un mensaje' + " privado. Mensaje:\n{}" + ), + "deleted_chat": ( + '🗑 {} eliminó un mensaje de' + ' {} en el grupo. Mensaje:\n{}' + ), + "edited_pm": ( + '🔏 {} editó un mensaje' + " privado. Mensaje anterior:\n{}" + ), + "edited_chat": ( + '🔏 {} editó un mensaje de' + ' {} en el grupo. Mensaje anterior:\n{}' + ), + "mode_off": ( + f"{pm} He desactivado el modo espía{{}}spymode\n" + ), + "cfg_enable_pm": "Guardar mensajes privados", + "cfg_enable_groups": "Guardar mensajes de grupo", + "cfg_whitelist": "Lista de Chats a guardar", + "cfg_blacklist": "Lista de Сhats a borrar", + "cfg_always_track": ( + "Lista de Chats para rastrear siempre los mensajes, pase lo que pase" + ), + "cfg_log_edits": "Guardar mensajes editados", + "cfg_ignore_inline": "Ignorar mensajes de inline", + "cfg_fw_protect": "Protegerse de forwarding floods", + "_cls_doc": ( + "Guarda los mensajes borrados, editados o enviados por un usuario" + " especificado" + ), + "sd_media": ( + "🔥 {} te ha enviado un mensaje de" + " contenido que se puede borrar" + ), + "save_sd": ( + "🔥 Guardar medios" + " autodestructivos\n" + ), + "cfg_save_sd": "Guardar contenido que se puede borrar", + } + + strings_kk = { + "on": "қосылған", + "off": "өшірілген", + "state": f"{rei} Шпион режимі ағымда {{}}", + "spybl": ( + f"{rei} Бұл сөйлесу қорытынды шпион режимінің қара тізіміне қосылды" + ), + "spybl_removed": ( + f"{rei} Бұл сөйлесу қорытынды шпион режимінің қара тізімінен алынды" + ), + "spybl_clear": f"{rei} Шпион режимінің қара тізімін тазалау", + "spywl": ( + f"{rei} Бұл сөйлесу қорытынды шпион режимінің ақ тізіміне қосылды" + ), + "spywl_removed": ( + f"{rei} Бұл сөйлесу қорытынды шпион режимінің ақ тізімінен алынды" + ), + "spywl_clear": f"{rei} Шпион режимінің ақ тізімін тазалау", + "whitelist": f"\n{rei} Тек хабарламаларды қадағалау:\n{{}}", + "always_track": f"\n{rei} Әрқашан хабарламаларды қадағалау:\n{{}}", + "blacklist": f"\n{rei} Хабарламаларды елемеу:\n{{}}", + "chat": f"{groups} Группадағы жазбаларымды сақтау\n", + "pm": f"{pm} Жеке жазбаларымды сақтау\n", + "deleted_pm": ( + '🗑 {} жеке жазбағын жойды.' + " Жазба:\n{}" + ), + "deleted_chat": ( + '🗑 {} {} топындағы' + ' жазбағын жойды. Жазба:\n{}' + ), + "edited_pm": ( + '🔏 {} жеке жазбағын' + " өзгертті. Алдындағы жазба:\n{}" + ), + "edited_chat": ( + '🔏 {} {} топындағы жазбағын өзгертті. Алдындағы жазба:\n{}' + ), + "mode_off": f"{pm} Спай режимін өшірдім{{}}spymode\n", + "cfg_enable_pm": "Жеке хабарламаларды сақтау", + "cfg_enable_groups": "Топтардың хабарламаларын сақтау", + "cfg_whitelist": "Сақталатын топтар тізімі", + "cfg_blacklist": "Жоюға мүмкіндік беретін топтар тізімі", + "cfg_always_track": ( + "Еш нәрсеге қарамастан, әрқашан хабарламаларды бақылайтын топтар тізімі" + ), + "cfg_log_edits": "Өңделген хабарламаларды сақтау", + "cfg_ignore_inline": "Inline режимінен келген хабарламаларды жою", + "cfg_fw_protect": "Forwarding flood-тен қорғау", + "_cls_doc": ( + "Көрсетілген пайдаланушы/топтардың жойылған, өңделген немесе сақталған" + " хабарламаларын сақтайды" + ), + "sd_media": ( + "🔥 {} сенің жойылған медиа-жазбаңың" + " болуы мүмкін" + ), + "save_sd": ( + "🔥 Жойылған" + " медиа-жазбаларды сақтау\n" + ), + "cfg_save_sd": "Жойылған медиа-жазбаларды сақтау", + } + + def __init__(self): + self._tl_channel = None + self.config = loader.ModuleConfig( + loader.ConfigValue( + "enable_pm", + True, + lambda: self.strings("cfg_enable_pm"), + validator=loader.validators.Boolean(), + ), + loader.ConfigValue( + "enable_groups", + False, + lambda: self.strings("cfg_enable_groups"), + validator=loader.validators.Boolean(), + ), + loader.ConfigValue( + "whitelist", + [], + lambda: self.strings("cfg_whitelist"), + validator=loader.validators.Series(), + ), + loader.ConfigValue( + "blacklist", + [], + lambda: self.strings("cfg_blacklist"), + validator=loader.validators.Series(), + ), + loader.ConfigValue( + "always_track", + [], + lambda: self.strings("cfg_always_track"), + validator=loader.validators.Series(), + ), + loader.ConfigValue( + "log_edits", + True, + lambda: self.strings("cfg_log_edits"), + validator=loader.validators.Boolean(), + ), + loader.ConfigValue( + "ignore_inline", + True, + lambda: self.strings("cfg_ignore_inline"), + validator=loader.validators.Boolean(), + ), + loader.ConfigValue( + "fw_protect", + 3.0, + lambda: self.strings("cfg_fw_protect"), + validator=loader.validators.Float(minimum=0.0), + ), + loader.ConfigValue( + "save_sd", + True, + lambda: self.strings("cfg_save_sd"), + validator=loader.validators.Boolean(), + ), + ) + + self._queue = [] + self._cache = {} + self._next = 0 + self._threshold = 10 + self._flood_protect_sample = 60 + + @loader.loop(interval=0.1, autostart=True) + async def sender(self): + if not self._queue or self._next > time.time(): + return + + item = self._queue.pop(0) + await item + self._next = int(time.time()) + self.config["fw_protect"] + + @staticmethod + def _int(value: typing.Union[str, int], /) -> typing.Union[str, int]: + return int(value) if str(value).isdigit() else value + + @property + def blacklist(self): + return list( + map( + self._int, + self.config["blacklist"] + + [777000, self._client.tg_id, self._tl_channel, self.inline.bot_id], + ) + ) + + @blacklist.setter + def blacklist(self, value: list): + self.config["blacklist"] = list( + set(value) + - {777000, self._client.tg_id, self._tl_channel, self.inline.bot_id} + ) + + @property + def whitelist(self): + return list(map(self._int, self.config["whitelist"])) + + @whitelist.setter + def whitelist(self, value: list): + self.config["whitelist"] = value + + @property + def always_track(self): + return list(map(self._int, self.config["always_track"])) + + async def client_ready(self): + channel, _ = await utils.asset_channel( + self._client, + "hikka-nekospy", + "Deleted and edited messages will appear there", + silent=True, + invite_bot=True, + avatar="https://pm1.narvii.com/6733/0e0380ca5cd7595de53f48c0ce541d3e2f2effc4v2_hq.jpg", + _folder="hikka", + ) + + self._channel = int(f"-100{channel.id}") + self._tl_channel = channel.id + + @loader.command( + ru_doc=( + "• Кто я? • Аянами Рей. • А кто ты? • Аянами Рей. • Ты тоже Аянами Рей? •" + " Да. Я та, кого знают как Аянами Рей. • Мы все те, кого знают, как Аянами" + " Рей. • Как они все могут быть мной? • Просто потому что другие зовут нас" + " Аянами Рей. Только и всё. У тебя ненастоящая душа, и тело твоё -" + " подделка. Знаешь почему? • Я не подделка и не фальшивка. Я - это я." + ), + tr_doc=( + "• Kimim? • Ayanami Rei. • Kimsin? • Ayanami Rei. • Sen de Ayanami Rei" + " misin? • Evet. Beni bilenler Ayanami Rei olarak bilir. • Hepimiz Ayanami" + " Rei olarak bilinenleriz. • Hepimiz nasıl Ayanami Rei olabiliriz? • Sadece" + " diğerleri bizi Ayanami Rei olarak adlandırıyor. Sadece bu. Ruhun gerçek" + " değil ve vücudun bir kopya. Biliyor musun neden? • Ben bir kopya değilim" + " ve sahte değilim. Ben benim." + ), + it_doc=( + "• Chi sono io? • Ayanami Rei. • Chi sei tu? • Ayanami Rei. • Tu sei anche" + " Ayanami Rei? • Sì. Io sono quella che conoscono come Ayanami Rei. • Tutti" + " noi siamo quelli che conoscono come Ayanami Rei. • Come possono tutti" + " essere io? • Solo perché gli altri ci chiamano Ayanami Rei. Solo questo." + " La tua anima non è vera e il tuo corpo è una copia. Lo sai perché? • Non" + " sono una copia e non sono una falsa. Io sono io." + ), + kk_doc=( + "• Мені кім? • Аянами Рей. • Сені кім? • Аянами Рей. • Сені де Аянами Рей?" + " • Иә. Мен Аянами Рей деп білінетін кім. • Барлығымыз Аянами Рей деп" + " білінетін кім. • Барлар мені қайсы бола алады? • Қатарынан, біздерді" + " Аянами Рей деп атайтын. Бірақ, бұл барлық. Сенің дуалың жарамсыз, және" + " телегің - бұл қате. Білесін бе? • Мен жарамсыз және қате емеспін. Мен -" + " бұл мен." + ), + de_doc=( + "• Wer bin ich? • Ayanami Rei. • Und wer bist du? • Ayanami Rei. • Bist du" + " auch Ayanami Rei? • Ja. Ich bin die, die als Ayanami Rei bekannt ist. •" + " Wir sind alle diejenigen, die als Ayanami Rei bekannt sind. • Wie können" + " alle mich sein? • Einfach nur, weil andere uns als Ayanami Rei nennen." + " Das ist alles. Du hast eine falsche Seele und deinen Körper gibt es" + " nicht. Weißt du, warum? • Ich bin nicht falsch und nicht falsch. Ich bin" + " ich." + ), + es_doc=( + "• ¿Quién soy? • Ayanami Rei. • ¿Y quién eres? • Ayanami Rei. • ¿Tú también" + " eres Ayanami Rei? • Sí. Soy la que se conoce como Ayanami Rei. • Todos" + " somos lo que se conoce como Ayanami Rei. • ¿Cómo pueden todos ser yo? •" + " Simplemente porque otros nos llaman Ayanami Rei. Eso es todo. Tienes un" + " alma falsa y tu cuerpo es una falsificación. ¿Sabes por qué? • No soy" + " falso ni falso. Soy yo." + ), + ) + async def spymode(self, message: Message): + """• Who am I? • Ayanami Rey. • Who are you? • Ayanami Rey. • Are you Ayanami Rey too? • Yes. I'm the one known as Ayanami Rey. • We're all what we know as Ayanami Rey. • How can they all be me? • Just because others call us Ayanami Rey. That's all. You have a fake soul and your body is a fake. You know why? • I'm not fake or fake. I am me.""" + await utils.answer( + message, + self.strings("state").format( + self.strings("off" if self.get("state", False) else "on") + ), + ) + self.set("state", not self.get("state", False)) + + @loader.command( + ru_doc="Добавить / удалить чат из списка игнора", + de_doc="Chat zur Ignorierliste hinzufügen / entfernen", + uz_doc="Chatni qo'shish / olib tashlash", + tr_doc="Sohbeti engelleme listesine ekle / kaldır", + es_doc="Agregar / eliminar chat de la lista de ignorados", + kk_doc="Чатты қосу / жою", + it_doc="Aggiungi / rimuovi chat dalla lista di ignorati", + ) + async def spybl(self, message: Message): + """Add / remove chat from blacklist""" + chat = utils.get_chat_id(message) + if chat in self.blacklist: + self.blacklist = list(set(self.blacklist) - {chat}) + await utils.answer(message, self.strings("spybl_removed")) + else: + self.blacklist = list(set(self.blacklist) | {chat}) + await utils.answer(message, self.strings("spybl")) + + @loader.command( + ru_doc="Очистить черный список", + de_doc="Schwarze Liste leeren", + uz_doc="Qora ro'yxatni tozalash", + tr_doc="Siyah listeyi temizle", + es_doc="Limpiar lista negra", + kk_doc="Қара тізімді тазалау", + it_doc="Cancella la lista nera", + ) + async def spyblclear(self, message: Message): + """Clear blacklist""" + self.blacklist = [] + await utils.answer(message, self.strings("spybl_clear")) + + @loader.command( + ru_doc="Добавить / удалить чат из белого списка", + de_doc="Chat zur Whitelist hinzufügen / entfernen", + uz_doc="Chatni o'qish ro'yxatiga qo'shish / olib tashlash", + tr_doc="Sohbeti beyaz listeye ekle / kaldır", + es_doc="Agregar / eliminar chat de la lista blanca", + kk_doc="Чатты оқыш тізіміне қосу / жою", + it_doc="Aggiungi / rimuovi chat dalla whitelist", + ) + async def spywl(self, message: Message): + """Add / remove chat from whitelist""" + chat = utils.get_chat_id(message) + if chat in self.whitelist: + self.whitelist = list(set(self.whitelist) - {chat}) + await utils.answer(message, self.strings("spywl_removed")) + else: + self.whitelist = list(set(self.whitelist) | {chat}) + await utils.answer(message, self.strings("spywl")) + + @loader.command( + ru_doc="Очистить белый список", + de_doc="Whitelist leeren", + uz_doc="O'qish ro'yxatini tozalash", + tr_doc="Beyaz listeyi temizle", + es_doc="Limpiar lista blanca", + kk_doc="Оқыш тізімін тазалау", + it_doc="Cancella la whitelist", + ) + async def spywlclear(self, message: Message): + """Clear whitelist""" + self.whitelist = [] + await utils.answer(message, self.strings("spywl_clear")) + + async def _get_entities_list(self, entities: list) -> str: + return "\n".join( + [ + "\u0020\u2800\u0020\u2800▫️ {}' + .format( + utils.get_entity_url(await self._client.get_entity(x, exp=0)), + utils.escape_html( + get_display_name(await self._client.get_entity(x, exp=0)) + ), + ) + for x in entities + ] + ) + + @loader.command( + ru_doc="Показать текущую конфигурацию спай-мода", + de_doc="Aktuelle Spy-Modus-Konfiguration anzeigen", + uz_doc="Spy rejimining hozirgi konfiguratsiyasini ko'rsatish", + tr_doc="Spy modu geçerli yapılandırmasını göster", + es_doc="Mostrar la configuración actual del modo espía", + kk_doc="Спай-режимдің ағымдағы конфигурациясын көрсету", + it_doc="Mostra la configurazione attuale della modalità spia", + ) + async def spyinfo(self, message: Message): + """Show current spy mode configuration""" + if not self.get("state"): + await utils.answer( + message, self.strings("mode_off").format(self.get_prefix()) + ) + return + + info = "" + + if self.config["save_sd"]: + info += self.strings("save_sd") + + if self.config["enable_groups"]: + info += self.strings("chat") + + if self.config["enable_pm"]: + info += self.strings("pm") + + if self.whitelist: + info += self.strings("whitelist").format( + await self._get_entities_list(self.whitelist) + ) + + if self.config["blacklist"]: + info += self.strings("blacklist").format( + await self._get_entities_list(self.config["blacklist"]) + ) + + if self.always_track: + info += self.strings("always_track").format( + await self._get_entities_list(self.always_track) + ) + + await utils.answer(message, info) + + async def _message_deleted(self, msg_obj: Message, caption: str): + caption = self.inline.sanitise_text(caption) + + if not msg_obj.photo and not msg_obj.video and not msg_obj.document: + self._queue += [ + self.inline.bot.send_message( + self._channel, + caption, + disable_web_page_preview=True, + ) + ] + return + + if msg_obj.sticker: + self._queue += [ + self.inline.bot.send_message( + self._channel, + caption + "\n\n<sticker>", + disable_web_page_preview=True, + ) + ] + return + + file = io.BytesIO(await self._client.download_media(msg_obj, bytes)) + args = (self._channel, file) + kwargs = {"caption": caption} + if msg_obj.photo: + file.name = "photo.jpg" + self._queue += [self.inline.bot.send_photo(*args, **kwargs)] + elif msg_obj.video: + file.name = "video.mp4" + self._queue += [self.inline.bot.send_video(*args, **kwargs)] + elif msg_obj.voice: + file.name = "audio.ogg" + self._queue += [self.inline.bot.send_voice(*args, **kwargs)] + elif msg_obj.document: + file.name = next( + attr.file_name + for attr in msg_obj.document.attributes + if isinstance(attr, DocumentAttributeFilename) + ) + self._queue += [self.inline.bot.send_document(*args, **kwargs)] + + async def _message_edited(self, caption: str, msg_obj: Message): + args = ( + self._channel, + await self._client.download_media(msg_obj, bytes), + ) + kwargs = {"caption": self.inline.sanitise_text(caption)} + if msg_obj.photo: + self._queue += [self.inline.bot.send_photo(*args, **kwargs)] + elif msg_obj.video: + self._queue += [self.inline.bot.send_video(*args, **kwargs)] + elif msg_obj.voice: + self._queue += [self.inline.bot.send_voice(*args, **kwargs)] + elif msg_obj.document: + self._queue += [self.inline.bot.send_document(*args, **kwargs)] + else: + self._queue += [ + self.inline.bot.send_message( + self._channel, + self.inline.sanitise_text(caption), + disable_web_page_preview=True, + ) + ] + + @loader.raw_handler(UpdateEditChannelMessage) + async def channel_edit_handler(self, update: UpdateEditChannelMessage): + if ( + not self.get("state", False) + or update.message.out + or (self.config["ignore_inline"] and update.message.via_bot_id) + ): + return + + key = f"{utils.get_chat_id(update.message)}/{update.message.id}" + if key in self._cache and ( + utils.get_chat_id(update.message) in self.always_track + or self._cache[key].sender_id in self.always_track + or ( + self.config["log_edits"] + and self.config["enable_groups"] + and utils.get_chat_id(update.message) not in self.blacklist + and ( + not self.whitelist + or utils.get_chat_id(update.message) in self.whitelist + ) + ) + ): + msg_obj = self._cache[key] + if not msg_obj.sender.bot and update.message.raw_text != msg_obj.raw_text: + await self._message_edited( + self.strings("edited_chat").format( + utils.get_entity_url(msg_obj.chat), + utils.escape_html(get_display_name(msg_obj.chat)), + utils.get_entity_url(msg_obj.sender), + utils.escape_html(get_display_name(msg_obj.sender)), + msg_obj.text, + message_url=await utils.get_message_link(msg_obj), + ), + msg_obj, + ) + + self._cache[key] = update.message + + def _should_capture(self, user_id: int, chat_id: int) -> bool: + return ( + chat_id not in self.blacklist + and user_id not in self.blacklist + and ( + not self.whitelist + or chat_id in self.whitelist + or user_id in self.whitelist + ) + ) + + @loader.raw_handler(UpdateEditMessage) + async def pm_edit_handler(self, update: UpdateEditMessage): + if ( + not self.get("state", False) + or update.message.out + or (self.config["ignore_inline"] and update.message.via_bot_id) + ): + return + + key = update.message.id + msg_obj = self._cache.get(key) + if ( + key in self._cache + and ( + self._cache[key].sender_id in self.always_track + or (utils.get_chat_id(self._cache[key]) in self.always_track) + or ( + self.config["log_edits"] + and self._should_capture( + self._cache[key].sender_id, + utils.get_chat_id(self._cache[key]), + ) + ) + and ( + ( + self.config["enable_pm"] + and not isinstance(msg_obj.peer_id, PeerChat) + or self.config["enable_groups"] + and isinstance(msg_obj.peer_id, PeerChat) + ) + ) + ) + and update.message.raw_text != msg_obj.raw_text + ): + sender = await self._client.get_entity(msg_obj.sender_id, exp=0) + if not sender.bot: + chat = ( + await self._client.get_entity( + msg_obj.peer_id.chat_id, + exp=0, + ) + if isinstance(msg_obj.peer_id, PeerChat) + else None + ) + await self._message_edited( + ( + self.strings("edited_chat").format( + utils.get_entity_url(chat), + utils.escape_html(get_display_name(chat)), + utils.get_entity_url(sender), + utils.escape_html(get_display_name(sender)), + msg_obj.text, + message_url=await utils.get_message_link(msg_obj), + ) + if isinstance(msg_obj.peer_id, PeerChat) + else self.strings("edited_pm").format( + utils.get_entity_url(sender), + utils.escape_html(get_display_name(sender)), + msg_obj.text, + message_url=await utils.get_message_link(msg_obj), + ) + ), + msg_obj, + ) + + self._cache[update.message.id] = update.message + + @loader.raw_handler(UpdateDeleteMessages) + async def pm_delete_handler(self, update: UpdateDeleteMessages): + if not self.get("state", False): + return + + for message in update.messages: + if message not in self._cache: + continue + + msg_obj = self._cache.pop(message) + + if ( + msg_obj.sender_id not in self.always_track + and utils.get_chat_id(msg_obj) not in self.always_track + and ( + not self._should_capture( + msg_obj.sender_id, utils.get_chat_id(msg_obj) + ) + or (self.config["ignore_inline"] and msg_obj.via_bot_id) + or ( + not self.config["enable_groups"] + and isinstance(msg_obj.peer_id, PeerChat) + ) + or ( + not self.config["enable_pm"] + and not isinstance(msg_obj.peer_id, PeerChat) + ) + ) + ): + continue + + sender = await self._client.get_entity(msg_obj.sender_id, exp=0) + + if sender.bot: + continue + + chat = ( + await self._client.get_entity(msg_obj.peer_id.chat_id, exp=0) + if isinstance(msg_obj.peer_id, PeerChat) + else None + ) + + await self._message_deleted( + msg_obj, + ( + self.strings("deleted_chat").format( + utils.get_entity_url(chat), + utils.escape_html(get_display_name(chat)), + utils.get_entity_url(sender), + utils.escape_html(get_display_name(sender)), + msg_obj.text, + message_url=await utils.get_message_link(msg_obj), + ) + if isinstance(msg_obj.peer_id, PeerChat) + else self.strings("deleted_pm").format( + utils.get_entity_url(sender), + utils.escape_html(get_display_name(sender)), + msg_obj.text, + message_url=await utils.get_message_link(msg_obj), + ) + ), + ) + + @loader.raw_handler(UpdateDeleteChannelMessages) + async def channel_delete_handler(self, update: UpdateDeleteChannelMessages): + if not self.get("state", False): + return + + for message in update.messages: + key = f"{update.channel_id}/{message}" + if key not in self._cache: + continue + + msg_obj = self._cache.pop(key) + + if ( + msg_obj.sender_id in self.always_track + or utils.get_chat_id(msg_obj) in self.always_track + or self.config["enable_groups"] + and ( + self._should_capture( + msg_obj.sender_id, + utils.get_chat_id(msg_obj), + ) + and (not self.config["ignore_inline"] or not msg_obj.via_bot_id) + and not msg_obj.sender.bot + ) + ): + await self._message_deleted( + msg_obj, + self.strings("deleted_chat").format( + utils.get_entity_url(msg_obj.chat), + utils.escape_html(get_display_name(msg_obj.chat)), + utils.get_entity_url(msg_obj.sender), + utils.escape_html(get_display_name(msg_obj.sender)), + msg_obj.text, + message_url=await utils.get_message_link(msg_obj), + ), + ) + + @loader.watcher("in") + async def watcher(self, message: Message): + if ( + self.config["save_sd"] + and getattr(message, "media", False) + and getattr(message.media, "ttl_seconds", False) + ): + media = io.BytesIO(await self.client.download_media(message.media, bytes)) + media.name = "sd.jpg" if message.photo else "sd.mp4" + sender = await self.client.get_entity(message.sender_id, exp=0) + await ( + self.inline.bot.send_photo + if message.photo + else self.inline.bot.send_video + )( + self._channel, + media, + caption=self.strings("sd_media").format( + utils.get_entity_url(sender), + utils.escape_html(get_display_name(sender)), + ), + ) + + with contextlib.suppress(AttributeError): + self._cache[ + ( + message.id + if message.is_private or isinstance(message.peer_id, PeerChat) + else f"{utils.get_chat_id(message)}/{message.id}" + ) + ] = message diff --git a/hikariatama/ftg/nekospy_beta.py b/hikariatama/ftg/nekospy_beta.py new file mode 100644 index 0000000..4b72b8a --- /dev/null +++ b/hikariatama/ftg/nekospy_beta.py @@ -0,0 +1,1193 @@ +__version__ = (2, 12, 3) + +# ©️ Dan Gazizullin, 2021-2023 +# This file is a part of Hikka Userbot +# Code is licensed under CC-BY-NC-ND 4.0 unless otherwise specified. +# 🌐 https://github.com/hikariatama/Hikka +# 🔑 https://creativecommons.org/licenses/by-nc-nd/4.0/ +# + attribution +# + non-commercial +# + no-derivatives + +# You CANNOT edit this file without direct permission from the author. +# You can redistribute this file without any changes. + +# meta pic: https://0x0.st/oRer.webp +# meta banner: https://mods.hikariatama.ru/badges/nekospy_beta.jpg + +# meta developer: @hikarimods +# scope: hikka_only +# scope: hikka_min 1.6.3 +# requires: python-magic + +# packurl: https://gist.github.com/hikariatama/a1bf9aa5aae566b9d07977fa55e18734/raw/368fdb3fe92508aee4eb7d68d3cb4311490b6aa8/nekospy.yml + +import asyncio +import contextlib +import datetime +import io +import json +import logging +import mimetypes +import os +import re +import time +import typing +import zlib +from abc import ABC, abstractmethod +from pathlib import Path + +import magic +from hikkatl.tl.types import ( + InputDocumentFileLocation, + InputPhotoFileLocation, + Message, + UpdateDeleteChannelMessages, + UpdateDeleteMessages, + UpdateEditChannelMessage, + UpdateEditMessage, +) +from hikkatl.utils import get_display_name + +from .. import loader, utils +from ..database import Database +from ..pointers import PointerList +from ..tl_cache import CustomTelegramClient + +logger = logging.getLogger(__name__) + + +def get_size(path: Path) -> int: + return sum(f.stat().st_size for f in path.glob("**/*") if f.is_file()) + + +def sizeof_fmt(num: int, suffix: str = "B") -> str: + for unit in ["", "Ki", "Mi", "Gi", "Ti", "Pi", "Ei", "Zi"]: + if abs(num) < 1024.0: + return f"{num:3.1f}{unit}{suffix}" + num /= 1024.0 + return f"{num:.1f}Yi{suffix}" + + +class RecentsItem(typing.NamedTuple): + timestamp: int + chat_id: int + message_id: int + action: str + + @classmethod + def from_edit(cls, message: Message) -> "RecentsItem": + return cls( + timestamp=int(time.time()), + chat_id=utils.get_chat_id(message), + message_id=message.id, + action=ACTION_EDIT, + ) + + @classmethod + def from_delete( + cls, + message_id: int, + chat_id: typing.Optional[int] = None, + ) -> "RecentsItem": + return cls( + timestamp=int(time.time()), + chat_id=chat_id, + message_id=message_id, + action=ACTION_DELETE, + ) + + +ACTION_EDIT = "edit" +ACTION_DELETE = "del" + + +class CacheManager(ABC): + @abstractmethod + def purge(self): + """Purge the cache""" + + @abstractmethod + def stats(self) -> tuple: + """Return cache statistics""" + + @abstractmethod + def gc(self, max_age: int, max_size: int) -> None: + """Clean the cache""" + + @abstractmethod + async def store_message( + self, + message: Message, + no_repeat: bool = False, + ) -> typing.Union[bool, typing.Dict[str, typing.Any]]: + """Store a message in the cache""" + + @abstractmethod + async def fetch_message( + self, + chat_id: typing.Optional[int], + message_id: int, + ) -> typing.Optional[dict]: + """Fetch a message from the cache""" + + +class CacheManagerDisc(CacheManager): + def __init__(self, client: CustomTelegramClient, db: Database): + self._client = client + self._db = db + self._cache_dir = Path.home().joinpath(".nekospy") + self._cache_dir.mkdir(parents=True, exist_ok=True) + + def purge(self): + for _file in self._cache_dir.iterdir(): + if _file.is_dir(): + for _child in _file.iterdir(): + _child.unlink() + + _file.rmdir() + + def stats(self) -> tuple: + dirsize = sizeof_fmt(get_size(self._cache_dir)) + messages_count = len(list(self._cache_dir.glob("**/*"))) + try: + oldest_message = datetime.datetime.fromtimestamp( + max(map(os.path.getctime, self._cache_dir.iterdir())) + ).strftime("%Y-%m-%d %H:%M:%S") + except ValueError: + oldest_message = "n/a" + + return dirsize, messages_count, oldest_message + + def gc(self, max_age: int, max_size: int) -> None: + """Clean the cache""" + for _file in self._cache_dir.iterdir(): + if _file.is_file(): + if _file.stat().st_mtime < time.time() - max_age: + _file.unlink() + else: + for _child in _file.iterdir(): + if ( + _child.is_file() + and _child.stat().st_mtime < time.time() - max_age + ): + _child.unlink() + + while get_size(self._cache_dir) > max_size: + min( + self._cache_dir.iterdir(), + key=lambda x: x.stat().st_mtime, + ).unlink() + + async def store_message( + self, + message: Message, + no_repeat: bool = False, + ) -> typing.Union[bool, typing.Dict[str, typing.Any]]: + """Store a message in the cache""" + if not hasattr(message, "id"): + return False + + _dir = self._cache_dir.joinpath(str(utils.get_chat_id(message))) + _dir.mkdir(parents=True, exist_ok=True) + _file = _dir.joinpath(str(message.id)) + + sender = None + + try: + if message.sender_id is not None: + try: + sender = await self._client.get_entity(message.sender_id, exp=0) + except Exception: + sender = await message.get_sender() + + try: + chat = await self._client.get_entity(utils.get_chat_id(message), exp=0) + except Exception: + chat = await message.get_chat() + + if message.sender_id is None: + sender = chat + except ValueError: + if no_repeat: + logger.debug("Failed to get sender/chat, skipping", exc_info=True) + return False + + await asyncio.sleep(5) + return await self.store_message(message, True) + + is_chat: bool = message.is_group or message.is_channel + + try: + text: str = message.text + except AttributeError: + text: str = message.raw_text + + message_data = { + "url": await utils.get_message_link(message), + "text": text, + "sender_id": sender.id if sender else None, + "sender_bot": not not getattr(sender, "bot", False), + "sender_name": utils.escape_html(get_display_name(sender)), + "sender_url": utils.get_entity_url(sender), + "chat_id": chat.id, + **( + { + "chat_name": utils.escape_html(get_display_name(chat)), + "chat_url": utils.get_entity_url(chat), + } + if is_chat + else {} + ), + "assets": await self._extract_assets(message), + "is_chat": is_chat, + "via_bot_id": not not message.via_bot_id, + } + + _file.write_bytes(zlib.compress(json.dumps(message_data).encode("utf-8"))) + return message_data + + async def fetch_message( + self, + chat_id: typing.Optional[int], + message_id: int, + ) -> typing.Optional[dict]: + """Fetch a message from the cache""" + _dir = None + if chat_id: + _dir = self._cache_dir.joinpath(str(chat_id)) + _file = _dir.joinpath(str(message_id)) + else: + for _dir in self._cache_dir.iterdir(): + _file = _dir.joinpath(str(message_id)) + if _file.exists(): + break + else: + _file = None + + if not _file or not _file.exists(): + return None + + data = json.loads(zlib.decompress(_file.read_bytes()).decode("utf-8")) + data["chat_id"] = data["chat_id"] or int(_dir.name if _dir else 0) + + return data + + async def _extract_assets(self, message: Message) -> typing.Dict[str, str]: + return { + attribute: { + "id": value.id, + "access_hash": value.access_hash, + "file_reference": bytearray(value.file_reference).hex(), + "thumb_size": getattr( + value, + "thumb_size", + value.sizes[-1].type if getattr(value, "sizes", None) else "", + ), + } + for attribute, value in filter( + lambda x: x[1], + { + arg: getattr(message, arg) + for arg in { + "photo", + "audio", + "document", + "sticker", + "video", + "voice", + "video_note", + "gif", + } + }.items(), + ) + } + + +@loader.tds +class NekoSpyBeta(loader.Module): + """Sends you deleted and / or edited messages from selected users""" + + strings = {"name": "NekoSpy"} + + def __init__(self): + self.config = loader.ModuleConfig( + loader.ConfigValue( + "enable_pm", + True, + lambda: self.strings("cfg_enable_pm"), + validator=loader.validators.Boolean(), + ), + loader.ConfigValue( + "enable_groups", + False, + lambda: self.strings("cfg_enable_groups"), + validator=loader.validators.Boolean(), + ), + loader.ConfigValue( + "whitelist", + [], + lambda: self.strings("cfg_whitelist"), + validator=loader.validators.Hidden(loader.validators.Series()), + ), + loader.ConfigValue( + "blacklist", + [], + lambda: self.strings("cfg_blacklist"), + validator=loader.validators.Hidden(loader.validators.Series()), + ), + loader.ConfigValue( + "always_track", + [], + lambda: self.strings("cfg_always_track"), + validator=loader.validators.Hidden(loader.validators.Series()), + ), + loader.ConfigValue( + "log_edits", + True, + lambda: self.strings("cfg_log_edits"), + validator=loader.validators.Boolean(), + ), + loader.ConfigValue( + "ignore_inline", + True, + lambda: self.strings("cfg_ignore_inline"), + validator=loader.validators.Boolean(), + ), + loader.ConfigValue( + "fw_protect", + 3.0, + lambda: self.strings("cfg_fw_protect"), + validator=loader.validators.Float(minimum=0.0), + ), + loader.ConfigValue( + "save_sd", + True, + lambda: self.strings("cfg_save_sd"), + validator=loader.validators.Boolean(), + ), + loader.ConfigValue( + "max_cache_size", + 1024 * 1024 * 1024, + lambda: self.strings("max_cache_size"), + validator=loader.validators.Integer(minimum=0), + ), + loader.ConfigValue( + "max_cache_age", + 7 * 24 * 60 * 60, + lambda: self.strings("max_cache_age"), + validator=loader.validators.Integer(minimum=0), + ), + loader.ConfigValue( + "recent_maximum", + 60 * 60, + lambda: self.strings("recent_maximum"), + validator=loader.validators.Integer(minimum=0), + ), + loader.ConfigValue( + "nocache_big_chats", + True, + lambda: self.strings("cfg_nocache_big_chats"), + validator=loader.validators.Boolean(), + ), + loader.ConfigValue( + "nocache_chats", + [], + lambda: self.strings("cfg_nocache_chats"), + validator=loader.validators.Hidden(loader.validators.Series()), + ), + loader.ConfigValue( + "ecospace_mode", + False, + lambda: self.strings("cfg_ecospace_mode"), + validator=loader.validators.Boolean(), + ), + loader.ConfigValue( + "very_important", + [], + "Very important chats go here", + validator=loader.validators.Hidden(loader.validators.Series()), + ), + ) + + self._queue: typing.List[asyncio.coroutine] = [] + self._next: int = 0 + self._threshold: int = 10 + self._flood_protect_sample: int = 60 + self._channel: int = None + self._tl_channel: int = None + self._ignore_cache: typing.List[int] = [] + self.METHOD_MAP: typing.Dict[str, callable] = None + self._cacher: CacheManager = None + self._recent: typing.Dict[int, int] = {} + + async def client_ready(self): + channel, _ = await utils.asset_channel( + self._client, + "hikka-nekospy", + "Deleted and edited messages will appear there", + silent=True, + invite_bot=True, + avatar="https://i.pinimg.com/originals/6c/1e/cf/6c1ecf3afca663a9ebc0b18788b337ee.jpg", + _folder="hikka", + ) + + self._channel = int(f"-100{channel.id}") + self._tl_channel = channel.id + self.METHOD_MAP = { + "photo": self.inline.bot.send_photo, + "video": self.inline.bot.send_video, + "voice": self.inline.bot.send_voice, + "document": self.inline.bot.send_document, + } + + self._cacher = CacheManagerDisc(self._client, self._db) + self._gc.start() + self._recent: PointerList = self.pointer( + "recent_msgs", + [], + item_type=RecentsItem, + ) + + @loader.loop(interval=15) + async def _gc(self): + self._cacher.gc(self.config["max_cache_age"], self.config["max_cache_size"]) + for item in self._recent: + if item.timestamp + self.config["recent_maximum"] < time.time(): + self._recent.remove(item) + + @loader.loop(interval=0.1, autostart=True) + async def _sender(self): + if not self._queue or self._next > time.time(): + return + + try: + await self._queue.pop(0) + except Exception: + logger.exception("Failed to send message") + + self._next = int(time.time()) + self.config["fw_protect"] + + @staticmethod + def _int(value: typing.Union[str, int], /) -> typing.Union[str, int]: + return int(value) if str(value).isdigit() else value + + @property + def blacklist(self): + return list( + map( + self._int, + self.config["blacklist"] + + [777000, self._client.tg_id, self._tl_channel, self.inline.bot_id], + ) + ) + + @property + def very_important(self): + return list( + map( + self._int, + self.config["very_important"], + ) + ) + + @blacklist.setter + def blacklist(self, value: list): + self.config["blacklist"] = list( + set(value) + - {777000, self._client.tg_id, self._tl_channel, self.inline.bot_id} + ) + + @property + def whitelist(self): + return list(map(self._int, self.config["whitelist"])) + + @whitelist.setter + def whitelist(self, value: list): + self.config["whitelist"] = value + + @property + def always_track(self): + return list(map(self._int, self.config["always_track"])) + + @loader.command() + async def spymode(self, message: Message): + """Toggle spymode""" + await utils.answer( + message, + self.strings("state").format( + self.strings("off" if self.get("state", False) else "on") + ), + ) + self.set("state", not self.get("state", False)) + + @loader.command() + async def spybl(self, message: Message): + """Add / remove chat from blacklist""" + chat = utils.get_chat_id(message) + if chat in self.blacklist: + self.blacklist = list(set(self.blacklist) - {chat}) + await utils.answer(message, self.strings("spybl_removed")) + else: + self.blacklist = list(set(self.blacklist) | {chat}) + await utils.answer(message, self.strings("spybl")) + + @loader.command() + async def spyblclear(self, message: Message): + """Clear blacklist""" + self.blacklist = [] + await utils.answer(message, self.strings("spybl_clear")) + + @loader.command() + async def spywl(self, message: Message): + """Add / remove chat from whitelist""" + chat = utils.get_chat_id(message) + if chat in self.whitelist: + self.whitelist = list(set(self.whitelist) - {chat}) + await utils.answer(message, self.strings("spywl_removed")) + else: + self.whitelist = list(set(self.whitelist) | {chat}) + await utils.answer(message, self.strings("spywl")) + + @loader.command() + async def spywlclear(self, message: Message): + """Clear whitelist""" + self.whitelist = [] + await utils.answer(message, self.strings("spywl_clear")) + + async def _get_entities_list(self, entities: list) -> str: + return "\n".join( + [ + "\u0020\u2800\u0020\u2800▫️ {}' + .format( + utils.get_entity_url(await self._client.get_entity(x, exp=0)), + utils.escape_html( + get_display_name(await self._client.get_entity(x, exp=0)) + ), + ) + for x in entities + ] + ) + + @loader.command() + async def spyinfo(self, message: Message): + """Show current spy mode configuration""" + if not self.get("state"): + await utils.answer( + message, + self.strings("mode_off").format(self.get_prefix()), + ) + return + + info = "" + + if self.config["save_sd"]: + info += self.strings("save_sd") + + if self.config["enable_groups"]: + info += self.strings("chat") + + if self.config["enable_pm"]: + info += self.strings("pm") + + if self.whitelist: + info += self.strings("whitelist").format( + await self._get_entities_list(self.whitelist) + ) + + if self.config["blacklist"]: + info += self.strings("blacklist").format( + await self._get_entities_list(self.config["blacklist"]) + ) + + if self.always_track: + info += self.strings("always_track").format( + await self._get_entities_list(self.always_track) + ) + + await utils.answer(message, info) + + async def _notify_sticker(self, file: dict, caption: str): + file["file_reference"] = bytes.fromhex(file["file_reference"]) + try: + file = await self._client.download_file( + InputDocumentFileLocation(**file), + bytes, + ) + except Exception: + file = None + + if file: + try: + ext = ( + mimetypes.guess_extension(magic.from_buffer(file, mime=True)) + or ".bin" + ) + except Exception: + ext = ".bin" + + if ext == ".gz": + ext = ".tgs" + + file = io.BytesIO(file) + file.name = f"restored{ext}" + + m = await ( + self.inline.bot.send_video + if ext == ".webm" + else self.inline.bot.send_sticker + )(self._channel, file) + else: + m = None + + await self.inline.bot.send_message( + self._channel, + caption + "\n<sticker>", + reply_to_message_id=m.message_id if m else None, + ) + + async def _notify(self, msg_obj: dict, caption: str): + caption = self.inline.sanitise_text(caption) + for username in set( + [username.username for username in (self._client.hikka_me.usernames or [])] + + [self._client.hikka_me.username] + ): + caption = caption.replace(f"@{username}", f"{username}") + + assets = msg_obj["assets"] + + file = next((x for x in assets.values() if x), None) + if not file: + self._queue += [ + self.inline.bot.send_message( + self._channel, + caption, + disable_web_page_preview=True, + ) + ] + return + + if assets.get("sticker"): + self._queue += [self._notify_sticker(file, caption)] + return + + file["file_reference"] = bytes(bytearray.fromhex(file["file_reference"])) + try: + file = await self._client.download_file( + ( + InputPhotoFileLocation + if assets.get("photo") or assets.get("sticker") + else InputDocumentFileLocation + )(**file), + bytes, + ) + except Exception: + logger.exception("Can't restore file") + self._queue += [ + self.inline.bot.send_message( + self._channel, + caption + "\n\n<unable to restore file>", + disable_web_page_preview=True, + ) + ] + return + + try: + if not ( + ext := mimetypes.guess_extension(magic.from_buffer(file, mime=True)) + ): + ext = ".bin" + except Exception: + ext = ".bin" + + file = io.BytesIO(file) + file.name = f"restored{ext}" + + self._queue += [ + next(func for name, func in self.METHOD_MAP.items() if assets.get(name))( + self._channel, + file, + caption=caption, + ) + ] + + @loader.raw_handler(UpdateEditChannelMessage) + async def channel_edit_handler(self, update: UpdateEditChannelMessage): + self._recent.append(RecentsItem.from_edit(update.message)) + + if ( + not self.get("state", False) + or update.message.out + or (self.config["ignore_inline"] and update.message.via_bot_id) + ): + return + + msg_obj = await self._cacher.fetch_message( + utils.get_chat_id(update.message), + update.message.id, + ) + if ( + msg_obj + and msg_obj["is_chat"] + and ( + int(msg_obj["chat_id"]) in self.always_track + or int(msg_obj["sender_id"]) in self.always_track + or ( + self.config["log_edits"] + and self.config["enable_groups"] + and utils.get_chat_id(update.message) not in self.blacklist + and ( + not self.whitelist + or utils.get_chat_id(update.message) in self.whitelist + ) + ) + ) + and not msg_obj["sender_bot"] + and update.message.raw_text != utils.remove_html(msg_obj["text"]) + ): + await self._notify( + msg_obj, + self.strings("edited_chat").format( + msg_obj["chat_url"], + msg_obj["chat_name"], + msg_obj["sender_url"], + msg_obj["sender_name"], + msg_obj["text"], + message_url=msg_obj["url"], + ), + ) + + await self._cacher.store_message(update.message) + + def _should_capture(self, user_id: int, chat_id: int) -> bool: + return ( + chat_id not in self.blacklist + and user_id not in self.blacklist + and ( + not self.whitelist + or chat_id in self.whitelist + or user_id in self.whitelist + ) + ) + + @loader.raw_handler(UpdateEditMessage) + async def pm_edit_handler(self, update: UpdateEditMessage): + self._recent.append(RecentsItem.from_edit(update.message)) + + if ( + not self.get("state", False) + or update.message.out + or (self.config["ignore_inline"] and update.message.via_bot_id) + ): + return + + msg_obj = await self._cacher.fetch_message( + utils.get_chat_id(update.message), + update.message.id, + ) + + if msg_obj: + sender_id, chat_id, is_chat = ( + int(msg_obj["sender_id"]), + int(msg_obj["chat_id"]), + msg_obj["is_chat"], + ) + + if ( + ( + sender_id in self.always_track + or chat_id in self.always_track + or ( + ( + self.config["log_edits"] + and self._should_capture(sender_id, chat_id) + ) + and ( + ( + self.config["enable_pm"] + and not is_chat + or self.config["enable_groups"] + and is_chat + ) + ) + ) + ) + and update.message.raw_text != utils.remove_html(msg_obj["text"]) + and not msg_obj["sender_bot"] + ): + await self._notify( + msg_obj, + ( + self.strings("edited_chat").format( + msg_obj["chat_url"], + msg_obj["chat_name"], + msg_obj["sender_url"], + msg_obj["sender_name"], + msg_obj["text"], + message_url=msg_obj["url"], + ) + if is_chat + else self.strings("edited_pm").format( + msg_obj["sender_url"], + msg_obj["sender_name"], + msg_obj["text"], + message_url=msg_obj["url"], + ) + ), + ) + + await self._cacher.store_message(update.message) + + @loader.raw_handler(UpdateDeleteMessages) + async def pm_delete_handler(self, update: UpdateDeleteMessages): + for message in update.messages: + self._recent.append(RecentsItem.from_delete(message)) + + if not self.get("state", False): + return + + for message in update.messages: + if not ( + msg_obj := await self._cacher.fetch_message( + chat_id=None, + message_id=message, + ) + ): + continue + + sender_id, chat_id, is_chat = ( + int(msg_obj["sender_id"]), + int(msg_obj["chat_id"]), + msg_obj["is_chat"], + ) + + if ( + sender_id not in self.always_track + and chat_id not in self.always_track + and ( + not self._should_capture(sender_id, chat_id) + or (self.config["ignore_inline"] and msg_obj["via_bot_id"]) + or (not self.config["enable_groups"] and is_chat) + or (not self.config["enable_pm"] and not is_chat) + ) + or msg_obj["sender_bot"] + ): + continue + + await self._notify( + msg_obj, + ( + self.strings("deleted_chat").format( + msg_obj["chat_url"], + msg_obj["chat_name"], + msg_obj["sender_url"], + msg_obj["sender_name"], + msg_obj["text"], + message_url=msg_obj["url"], + ) + if is_chat + else self.strings("deleted_pm").format( + msg_obj["sender_url"], + msg_obj["sender_name"], + msg_obj["text"], + message_url=msg_obj["url"], + ) + ), + ) + + def _is_always_track(self, user_id: int, chat_id: int) -> bool: + return chat_id in self.always_track or user_id in self.always_track + + @loader.raw_handler(UpdateDeleteChannelMessages) + async def channel_delete_handler(self, update: UpdateDeleteChannelMessages): + for message in update.messages: + self._recent.append(RecentsItem.from_delete(message, update.channel_id)) + + if not self.get("state", False): + return + + for message in update.messages: + if not message or not ( + msg_obj := await self._cacher.fetch_message(update.channel_id, message) + ): + continue + + sender_id, chat_id = ( + int(msg_obj["sender_id"]), + int(msg_obj["chat_id"]), + ) + + if ( + self._is_always_track(sender_id, chat_id) + or self.config["enable_groups"] + and ( + self._should_capture(sender_id, chat_id) + and (not self.config["ignore_inline"] or not msg_obj["via_bot_id"]) + and not msg_obj["sender_bot"] + ) + ): + await self._notify( + msg_obj, + self.strings("deleted_chat").format( + msg_obj["chat_url"], + msg_obj["chat_name"], + msg_obj["sender_url"], + msg_obj["sender_name"], + msg_obj["text"], + message_url=msg_obj["url"], + ), + ) + + @loader.watcher("in", only_messages=True) + async def watcher(self, message: Message): + if not hasattr(message, "sender_id"): + return + + if not hasattr(message, "sender"): + message.sender = await message.get_sender() + + if (chat_id := utils.get_chat_id(message)) in self._ignore_cache or ( + message.is_private + and self.config["ecospace_mode"] + and ( + self.config["enable_pm"] + and message.is_private + and ( + self._should_capture(message.sender_id, message.sender_id) + and (not self.config["ignore_inline"] or not message.via_bot_id) + and not message.sender.bot + ) + ) + ): + return + + for chat in self.config["nocache_chats"]: + with contextlib.suppress(ValueError): + if (await self._client.get_entity(chat, exp=0)).id == chat_id: + self._ignore_cache += [chat_id] + return + + if message.is_group: + if self.config["ecospace_mode"] and not ( + self._is_always_track(message.sender_id, chat_id) + or ( + self.config["enable_groups"] + and message.is_group + and ( + self._should_capture(message.sender_id, chat_id) + and (not self.config["ignore_inline"] or not message.via_bot_id) + and not message.sender.bot + ) + ) + ): + return + + if ( + self.config["nocache_big_chats"] + and (await self._client.get_participants(chat_id, limit=1)).total > 500 + ): + self._ignore_cache += [chat_id] + return + + msg_obj = await self._cacher.store_message(message) + + for chat in self.very_important: + with contextlib.suppress(ValueError): + if ( + message.sender_id in self.very_important + or (await self._client.get_entity(chat, exp=0)).id == chat_id + ): + if all(arg in msg_obj for arg in ("chat_url", "chat_name")): + await self._notify( + msg_obj, + self.strings("saved_chat").format( + msg_obj["chat_url"], + msg_obj["chat_name"], + msg_obj["sender_url"], + msg_obj["sender_name"], + msg_obj["text"], + message_url=msg_obj["url"], + ), + ) + else: + await self._notify( + msg_obj, + self.strings("saved_pm").format( + msg_obj["sender_url"], + msg_obj["sender_name"], + msg_obj["text"], + message_url=msg_obj["url"], + ), + ) + + break + + if ( + not self.config["save_sd"] + or not getattr(message, "media", False) + or not getattr(message.media, "ttl_seconds", False) + ): + return + + media = io.BytesIO(await self.client.download_media(message.media, bytes)) + media.name = "sd.jpg" if message.photo else "sd.mp4" + + try: + sender = await self.client.get_entity(message.sender_id, exp=0) + except Exception: + sender = await message.get_sender() + + await ( + self.inline.bot.send_photo if message.photo else self.inline.bot.send_video + )( + self._channel, + media, + caption=self.strings("sd_media").format( + utils.get_entity_url(sender), + utils.escape_html(get_display_name(sender)), + ), + ) + + @loader.command() + async def nssave(self, message: Message): + """Save replied message to the channel""" + + async def _save(_reply: Message): + msg_obj = await self._cacher.store_message(_reply) + if all(arg in msg_obj for arg in ("chat_url", "chat_name")): + await self._notify( + msg_obj, + self.strings("saved_chat").format( + msg_obj["chat_url"], + msg_obj["chat_name"], + msg_obj["sender_url"], + msg_obj["sender_name"], + msg_obj["text"], + message_url=msg_obj["url"], + ), + ) + else: + await self._notify( + msg_obj, + self.strings("saved_pm").format( + msg_obj["sender_url"], + msg_obj["sender_name"], + msg_obj["text"], + message_url=msg_obj["url"], + ), + ) + + if reply := await message.get_reply_message(): + await _save(reply) + + args = utils.get_args_raw(message) + links = re.findall(r"(https://t.me[^\s]+)", args) + + for link in links: + peer, msg = link.split("/")[-2:] + msg = int(msg) + if re.match(r"https://t.me/c/\d+/\d+", link): + peer = int(peer) + + try: + msg = (await self.client.get_messages(peer, ids=[msg]))[0] + if not msg: + raise RuntimeError + except Exception: + logger.exception("Can't save message from link %s", link) + continue + + await _save(msg) + + await utils.answer(message, self.strings("saved")) + + @loader.command() + async def stat(self, message: Message): + """Show stats for cached media and messages""" + dirsize, messages_count, oldest_message = self._cacher.stats() + await utils.answer( + message, + self.strings("stats").format( + dirsize, + messages_count, + oldest_message, + ), + ) + + @loader.command() + async def purgecache(self, message: Message): + """Empty cache storage from messages""" + self._cacher.purge() + self._recent.clear() + await utils.answer(message, self.strings("purged_cache")) + + @loader.command() + async def rest(self, message: Message): + """[time] [-current] - Restore all deleted and edited messages from [time]""" + args = utils.get_args_raw(message) or "5m" + + if "-current" in args: + args = args.replace("-current", "").strip() + from_chat = utils.get_chat_id(message) + else: + from_chat = None + + if args[-1].isdigit(): + args += "s" + + if args[-1] == "m": + args = int(args[:-1]) * 60 + elif args[-1] == "s": + args = int(args[:-1]) + elif args[-1] == "h": + args = int(args[:-1]) * 60 * 60 + + if args < 1: + await utils.answer(message, self.strings("invalid_time")) + return + + message = await utils.answer(message, self.strings("restoring")) + for recent in self._recent: + if ( + time.time() - recent.timestamp > args + or not ( + msg_obj := await self._cacher.fetch_message( + recent.chat_id, + recent.message_id, + ) + ) + or (from_chat and msg_obj.get("chat_id") != from_chat) + ): + continue + + if all(arg in msg_obj for arg in ("chat_url", "chat_name")): + await self._notify( + msg_obj, + self.strings( + "deleted_chat" + if recent.action == ACTION_DELETE + else "edited_chat" + ).format( + msg_obj["chat_url"], + msg_obj["chat_name"], + msg_obj["sender_url"], + msg_obj["sender_name"], + msg_obj["text"], + message_url=msg_obj["url"], + ), + ) + else: + await self._notify( + msg_obj, + self.strings( + "deleted_pm" if recent.action == ACTION_DELETE else "edited_pm" + ).format( + msg_obj["sender_url"], + msg_obj["sender_name"], + msg_obj["text"], + message_url=msg_obj["url"], + ), + ) + + await utils.answer(message, self.strings("restored")) diff --git a/hikariatama/ftg/nometa.py b/hikariatama/ftg/nometa.py new file mode 100644 index 0000000..8fed6bb --- /dev/null +++ b/hikariatama/ftg/nometa.py @@ -0,0 +1,87 @@ +# █ █ ▀ █▄▀ ▄▀█ █▀█ ▀ +# █▀█ █ █ █ █▀█ █▀▄ █ +# © Copyright 2022 +# https://t.me/hikariatama +# +# 🔒 Licensed under the GNU AGPLv3 +# 🌐 https://www.gnu.org/licenses/agpl-3.0.html + + +# meta pic: https://static.dan.tatar/nometa_icon.png +# meta banner: https://mods.hikariatama.ru/badges/nometa.jpg +# meta developer: @hikarimods +# scope: hikka_only +# scope: hikka_min 1.3.0 + +from telethon.tl.types import Message + +from .. import loader, utils + + +@loader.tds +class NoMetaMod(loader.Module): + """Warns people about Meta messages""" + + strings = { + "name": "NoMeta", + "no_meta": ( + "👾 Please!\nNoMeta aka 'Hello', 'Hi' etc.\nAsk" + " directly, what do you want from me." + ), + "no_meta_ru": ( + "👾 Пожалуйста!\nНе нужно лишних сообщений по типу" + " 'Привет', 'Хай' и др.\nСпрашивай(-те) конкретно, что от" + " меня нужно." + ), + } + + @loader.command(ru_doc="Показать сообщение с предупреждением о мете") + @loader.unrestricted + async def nometacmd(self, message: Message): + """Show message about NoMeta""" + await self._client.send_message( + message.peer_id, + self.strings("no_meta"), + reply_to=getattr(message, "reply_to_msg_id", None), + ) + if message.out: + await message.delete() + + @loader.tag("only_messages", "only_pm", "in") + async def watcher(self, message: Message): + meta = ["hi", "hello", "hey there", "konichiwa", "hey"] + + meta_ru = [ + "привет", + "хай", + "хелло", + "хеллоу", + "хэллоу", + "коничива", + "алоха", + "слушай", + "о", + "слуш", + "м?", + "а?", + "хей", + "хэй", + "йо", + "йоу", + "прив", + "yo", + "ку", + ] + + if message.raw_text.lower() in meta: + await utils.answer(message, self.strings("no_meta")) + await self._client.send_read_acknowledge( + message.chat_id, + clear_mentions=True, + ) + + if message.raw_text.lower() in meta_ru: + await utils.answer(message, self.strings("no_meta_ru")) + await self._client.send_read_acknowledge( + message.chat_id, clear_mentions=True + ) diff --git a/hikariatama/ftg/notes.py b/hikariatama/ftg/notes.py new file mode 100644 index 0000000..94b428f --- /dev/null +++ b/hikariatama/ftg/notes.py @@ -0,0 +1,260 @@ +# █ █ ▀ █▄▀ ▄▀█ █▀█ ▀ +# █▀█ █ █ █ █▀█ █▀▄ █ +# © Copyright 2022 +# https://t.me/hikariatama +# +# 🔒 Licensed under the GNU AGPLv3 +# 🌐 https://www.gnu.org/licenses/agpl-3.0.html + +# meta pic: https://static.dan.tatar/notes_icon.png +# meta banner: https://mods.hikariatama.ru/badges/notes.jpg +# meta developer: @hikarimods +# scope: hikka_only +# scope: hikka_min 1.2.10 + +import logging + +from telethon.tl.types import Message + +from .. import loader, utils + +logger = logging.getLogger(__name__) + + +@loader.tds +class NotesMod(loader.Module): + """Advanced notes module with folders and other features""" + + strings = { + "name": "Notes", + "saved": ( + "💾 Saved note with name {}.\nFolder:" + " {}." + ), + "no_reply": "🚫 Reply and note name are required.", + "no_name": "🚫 Specify note name.", + "no_note": "🚫 Note not found.", + "available_notes": "💾 Current notes:\n", + "no_notes": "😔 You have no notes yet", + "deleted": "🙂 Deleted note {}", + } + + strings_ru = { + "saved": ( + "💾 Заметка с именем {} сохранена.\nПапка:" + " {}." + ), + "no_reply": "🚫 Требуется реплай на контент заметки.", + "no_name": "🚫 Укажи имя заметки.", + "no_note": "🚫 Заметка не найдена.", + "available_notes": "💾 Текущие заметки:\n", + "no_notes": "😔 У тебя пока что нет заметок", + "deleted": "🙂 Заметка с именем {} удалена", + "_cmd_doc_hsave": "[папка] <имя> - Сохранить заметку", + "_cmd_doc_hget": "<имя> - Показать заметку", + "_cmd_doc_hdel": "<имя> - Удалить заметку", + "_cmd_doc_hlist": "[папка] - Показать все заметки", + "_cls_doc": "Модуль заметок с расширенным функционалом. Папки и категории", + } + + strings_de = { + "saved": ( + "💾 Notiz mit dem Namen {} gespeichert.\nOrdner:" + " {}." + ), + "no_reply": "🚫 Antworte auf den Inhalt der Notiz.", + "no_name": "🚫 Gib einen Namen für die Notiz an.", + "no_note": "🚫 Notiz nicht gefunden.", + "available_notes": "💾 Aktuelle Notizen:\n", + "no_notes": "😔 Du hast noch keine Notizen", + "deleted": "🙂 Notiz mit dem Namen {} gelöscht", + "_cmd_doc_hsave": "[Ordner] - Speichert eine neue Notiz", + "_cmd_doc_hget": " - Zeigt eine Notiz an", + "_cmd_doc_hdel": " - Löscht eine Notiz", + "_cmd_doc_hlist": "[Ordner] - Zeigt alle Notizen an", + "_cls_doc": "Notizenmodul mit erweiterten Funktionen. Ordner und Kategorien", + } + + strings_tr = { + "saved": ( + "💾 Notu adı {} kaydedildi.\nKlasör:" + " {}." + ), + "no_reply": "🚫 Not içeriğine yanıt verin.", + "no_name": "🚫 Bir not adı belirtin.", + "no_note": "🚫 Not bulunamadı.", + "available_notes": "💾 Mevcut notlar:\n", + "no_notes": "😔 Henüz notunuz yok", + "deleted": "🙂 Not adı {} silindi", + "_cmd_doc_hsave": "[Klasör] - Yeni bir not kaydedin", + "_cmd_doc_hget": " - Bir notu göster", + "_cmd_doc_hdel": " - Bir notu sil", + "_cmd_doc_hlist": "[Klasör] - Tüm notları göster", + "_cls_doc": "Gelişmiş not modülü. Klasörler ve diğer özellikler", + } + + strings_uz = { + "saved": ( + "💾 Qayd nomi {} saqlandi.\nJild:" + " {}." + ), + "no_reply": "🚫 Qayd tarkibiga javob bering.", + "no_name": "🚫 Qayd nomini kiriting.", + "no_note": "🚫 Qayd topilmadi.", + "available_notes": "💾 Mavjud qaydlar:\n", + "no_notes": "😔 Hozircha sizda qayd yo'q", + "deleted": "🙂 Qayd nomi {} o'chirildi", + "_cmd_doc_hsave": "[Jild] - Yangi qayd saqlash", + "_cmd_doc_hget": " - Qaydni ko'rsatish", + "_cmd_doc_hdel": " - Qaydni o'chirish", + "_cmd_doc_hlist": "[Jild] - Barcha qaydlarni ko'rsatish", + "_cls_doc": "Kengaytirilgan qayd moduli. Jildlar va kategoriyalar", + } + + strings_hi = { + "saved": ( + "💾 नोट का नाम {} सहेजा गया.\nफ़ोल्डर:" + " {}." + ), + "no_reply": "🚫 नोट की अंतर्दृष्टि पर जवाब दें।", + "no_name": "🚫 एक नोट नाम दर्ज करें।", + "no_note": "🚫 नोट नहीं मिला।", + "available_notes": "💾 उपलब्ध नोट्स:\n", + "no_notes": "😔 आपके पास अभी तक कोई नोट नहीं है", + "deleted": "🙂 नोट नाम {} हटा दिया गया", + "_cmd_doc_hsave": "[फ़ोल्डर] <नाम> - एक नया नोट सहेजें", + "_cmd_doc_hget": "<नाम> - एक नोट दिखाएं", + "_cmd_doc_hdel": "<नाम> - एक नोट हटाएं", + "_cmd_doc_hlist": "[फ़ोल्डर] - सभी नोट्स दिखाएं", + "_cls_doc": "उन्नत नोट्स मॉड्यूल। फ़ोल्डर और श्रेणियाँ", + } + + async def client_ready(self): + self._notes = self.get("notes", {}) + + async def hsavecmd(self, message: Message): + """[folder] - Save new note""" + args = utils.get_args_raw(message) + + if len(args.split()) >= 2: + folder = args.split()[0] + args = args.split(maxsplit=1)[1] + else: + folder = "global" + + reply = await message.get_reply_message() + + if not (reply and args): + await utils.answer(message, self.strings("no_reply")) + return + + if folder not in self._notes: + self._notes[folder] = {} + logger.warning(f"Created new folder {folder}") + + asset = await self._db.store_asset(reply) + + if getattr(reply, "video", False): + type_ = "🎞" + elif getattr(reply, "photo", False): + type_ = "🖼" + elif getattr(reply, "voice", False): + type_ = "🗣" + elif getattr(reply, "audio", False): + type_ = "🎧" + elif getattr(reply, "file", False): + type_ = "📝" + else: + type_ = "🔹" + + self._notes[folder][args] = {"id": asset, "type": type_} + + self.set("notes", self._notes) + + await utils.answer(message, self.strings("saved").format(args, folder)) + + def _get_note(self, name): + for category, notes in self._notes.items(): + for note, asset in notes.items(): + if note == name: + return asset + + def _del_note(self, name): + for category, notes in self._notes.copy().items(): + for note, asset in notes.copy().items(): + if note == name: + del self._notes[category][note] + + if not self._notes[category]: + del self._notes[category] + + self.set("notes", self._notes) + return True + + return False + + async def hgetcmd(self, message: Message): + """ - Show specified note""" + args = utils.get_args_raw(message) + if not args: + await utils.answer(message, self.strings("no_name")) + return + + asset = self._get_note(args) + if not asset: + await utils.answer(message, self.strings("no_note")) + return + + await self._client.send_message( + message.peer_id, + await self._db.fetch_asset(asset["id"]), + reply_to=getattr(message, "reply_to_msg_id", False), + ) + + if message.out: + await message.delete() + + async def hdelcmd(self, message: Message): + """ - Delete specified note""" + args = utils.get_args_raw(message) + if not args: + await utils.answer(message, self.strings("no_name")) + return + + asset = self._get_note(args) + if not asset: + await utils.answer(message, self.strings("no_note")) + return + + try: + await (await self._db.fetch_asset(asset["id"])).delete() + except Exception: + pass + + self._del_note(args) + + await utils.answer(message, self.strings("deleted").format(args)) + + async def hlistcmd(self, message: Message): + """[folder] - List all notes""" + args = utils.get_args_raw(message) + + if not self._notes: + await utils.answer(message, self.strings("no_notes")) + return + + result = self.strings("available_notes") + + if not args or args not in self._notes: + for category, notes in self._notes.items(): + result += f"\n🔸 {category}\n" + for note, asset in notes.items(): + result += f" {asset['type']} {note}\n" + + await utils.answer(message, result) + return + + for note, asset in self._notes[args].items(): + result += f"{asset['type']} {note}\n" + + await utils.answer(message, result) diff --git a/hikariatama/ftg/onload.py b/hikariatama/ftg/onload.py new file mode 100644 index 0000000..665ae3e --- /dev/null +++ b/hikariatama/ftg/onload.py @@ -0,0 +1,51 @@ +# █ █ ▀ █▄▀ ▄▀█ █▀█ ▀ +# █▀█ █ █ █ █▀█ █▀▄ █ +# © Copyright 2022 +# https://t.me/hikariatama +# +# 🔒 Licensed under the GNU AGPLv3 +# 🌐 https://www.gnu.org/licenses/agpl-3.0.html + +# meta pic: https://static.dan.tatar/onload_icon.png +# meta banner: https://mods.hikariatama.ru/badges/onload.jpg +# meta developer: @hikarimods +# scope: hikka_only +# scope: hikka_min 1.2.10 + +import logging + +from .. import loader, utils + +logger = logging.getLogger(__name__) + + +@loader.tds +class OnloadExecutorMod(loader.Module): + """Executes selected commands after every userbot restart""" + + strings = {"name": "OnloadExecutor"} + + async def client_ready(self, client, _): + self.c, _ = await utils.asset_channel( + client, + "hikka-onload", + ( + "All commands from this chat will be executed once Hikka is started, be" + " careful!" + ), + archive=True, + avatar="https://raw.githubusercontent.com/hikariatama/assets/master/hikka-onload.png", + _folder="hikka", + ) + + async for message in client.iter_messages(self.c): + if (getattr(message, "raw_text", "") or "").startswith(self.get_prefix()): + try: + m = await client.send_message("me", message.raw_text) + await self.allmodules.commands[message.raw_text[1:].split()[0]](m) + logger.debug("Registered onload command") + await m.delete() + except Exception: + logger.exception( + f"Exception while executing command {message.raw_text[:15]}..." + ) diff --git a/hikariatama/ftg/oxford.py b/hikariatama/ftg/oxford.py new file mode 100644 index 0000000..8a46198 --- /dev/null +++ b/hikariatama/ftg/oxford.py @@ -0,0 +1,191 @@ +# █ █ ▀ █▄▀ ▄▀█ █▀█ ▀ +# █▀█ █ █ █ █▀█ █▀▄ █ +# © Copyright 2022 +# https://t.me/hikariatama +# +# 🔒 Licensed under the GNU AGPLv3 +# 🌐 https://www.gnu.org/licenses/agpl-3.0.html + +# meta pic: https://static.dan.tatar/oxford_icon.png +# meta banner: https://mods.hikariatama.ru/badges/oxford.jpg +# meta developer: @hikarimods +# requires: bs4 +# scope: inline +# scope: hikka_only +# scope: hikka_min 1.3.0 + +import random +from urllib.parse import quote_plus + +import grapheme +import requests +from bs4 import BeautifulSoup +from telethon.tl.types import Message + +from .. import loader, utils +from ..inline.types import InlineCall + +DEFAULT_HEADERS = { + "Connection": "keep-alive", + "Pragma": "no-cache", + "Cache-Control": "no-cache", + "Upgrade-Insecure-Requests": "1", + "User-Agent": ( + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like" + " Gecko) Chrome/92.0.4515.131 Safari/537.36" + ), + "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9", + "Referer": "https://www.oxfordlearnersdictionaries.com", + "Accept-Encoding": "gzip, deflate, br", + "Accept-Language": "en-US,en;q=0.9,ru;q=0.8", +} + + +async def search(term: str) -> str: + res = await utils.run_sync( + requests.get, + f"https://www.oxfordlearnersdictionaries.com/search/english/direct/?q={quote_plus(term)}", + headers=DEFAULT_HEADERS, + ) + + soup = BeautifulSoup(res.text, "html.parser") + + if "spellcheck" in res.url: + try: + possible = [ + a.get("href").split("?q=")[1] + for a in soup.find("ul", {"class": "result-list"}).find_all("a") + ] + except Exception: + return {"ok": False, "possible": ["emptiness"]} + + return {"ok": False, "possible": possible} + + try: + soup.find("div", {"class": "idioms"}).clear() + except AttributeError: + pass + + return { + "ok": True, + "definitions": [ + definition.get_text() + for definition in soup.find_all("span", {"class": "def"}) + ], + "part_of_speech": soup.find("span", {"class": "pos"}).get_text(), + "pronunciation": soup.find("span", {"class": "phon"}).get_text(), + "term": term, + } + + +@loader.tds +class OxfordMod(loader.Module): + """Quickly access word definitions in Oxford Learners dictionary""" + + parts_of_speech = { + "noun": "существительное", + "pronoun": "местоимение", + "verb": "глагол", + "adjective": "прилагательное", + "adverb": "наречие", + "preposition": "предлог", + "conjunction": "союз", + "interjection": "междометие", + "determiner": "определитель", + "auxiliary verb": "вспомогательный глагол", + "modal verb": "модальный глагол", + "phrasal verb": "фразеологизм", + "idiom": "идиома", + "phrase": "фраза", + "abbreviation": "аббревиатура", + "article": "артикль", + "collocation": "коллокация", + "exclamation": "восклицание", + "expression": "выражение", + } + + strings = { + "name": "Oxford", + "no_exact": ( + "😔 There is no definition for {}\nMaybe, you" + " meant:" + ), + "match": '{} {} [{}] ({})\n\n{}', + **{key: key for key in parts_of_speech}, + } + + strings_ru = { + "_cls_doc": ( + "Быстрый доступ к определениям слов в образовательном Оксфордском словаре" + ), + "no_exact": ( + "😔 Нет определения для {}\nВозможно, вы имели в" + " виду:" + ), + **parts_of_speech, + } + + async def _search(self, call: InlineCall, term: str): + result = await search(term) + await call.edit(self.format_match(result)) + + def format_match(self, match: dict) -> str: + return self.strings("match").format( + random.choice( + [ + "{}", + "{}", + "{}", + "{}", + "{}", + "{}", + "{}", + "{}", + ] + ).format( + random.choice( + list( + grapheme.graphemes( + "👩‍🎓🧑‍🎓👨‍🎓👨‍🏫🧑‍🏫👩‍🏫🤵‍♀️🤵🤵‍♂️💁‍♀️💁‍♂️🙋‍♂️🙋‍♀️🙍‍♀️🙎‍♂️" + ) + ) + ) + ), + f"https://www.oxfordlearnersdictionaries.com/search/english/direct/?q={match['term']}", + utils.escape_html(match["term"]), + utils.escape_html(match["pronunciation"]), + utils.escape_html(self.strings(match["part_of_speech"])), + "\n\n".join( + [ + "▫️" + f" {utils.escape_html(definition)}" + for definition in match["definitions"] + ] + ), + ) + + @loader.command( + ru_doc="<слово> - Поиск слова в образовательном Оксфордском словаре" + ) + async def oxford(self, message: Message): + """ - Search word in Oxford Learner's Dictionary""" + args = utils.get_args_raw(message) + if not args: + args = "emptiness" + + result = await search(args) + if not result["ok"]: + await self.inline.form( + self.strings("no_exact").format(utils.escape_html(args)), + message, + reply_markup=utils.chunks( + [ + {"text": term, "callback": self._search, "args": (term,)} + for term in result["possible"] + ], + 2, + ), + ) + return + + await utils.answer(message, self.format_match(result)) diff --git a/hikariatama/ftg/pmbl.py b/hikariatama/ftg/pmbl.py new file mode 100644 index 0000000..7c6ba8d --- /dev/null +++ b/hikariatama/ftg/pmbl.py @@ -0,0 +1,697 @@ +__version__ = (3, 0, 4) + +# █ █ ▀ █▄▀ ▄▀█ █▀█ ▀ +# █▀█ █ █ █ █▀█ █▀▄ █ +# © Copyright 2022 +# https://t.me/hikariatama +# +# 🔒 Licensed under the GNU AGPLv3 +# 🌐 https://www.gnu.org/licenses/agpl-3.0.html + +# meta title: PM->BL +# meta pic: https://img.icons8.com/external-dreamcreateicons-flat-dreamcreateicons/512/000000/external-death-halloween-dreamcreateicons-flat-dreamcreateicons.png +# meta banner: https://mods.hikariatama.ru/badges/pmbl.jpg +# meta developer: @hikarimods +# scope: hikka_only +# scope: hikka_min 1.5.0 + +import contextlib +import logging +import time +from typing import Optional + +from telethon.tl.functions.contacts import BlockRequest +from telethon.tl.functions.messages import DeleteHistoryRequest, ReportSpamRequest +from telethon.tl.types import Message, PeerUser, User +from telethon.utils import get_display_name, get_peer_id + +from .. import loader, utils + +logger = logging.getLogger(__name__) + + +def format_(state: Optional[bool]) -> str: + if state is None: + return "❔" + + return "🫡" if state else "🙅‍♂️ Not" + + +@loader.tds +class PMBLMod(loader.Module): + """Bans and reports incoming messages from unknown users""" + + strings = { + "name": "PMBL", + "state": ( + "🛡 PM->BL is now" + " {}\nReport spam? - {}\nDelete dialog? - {}" + ), + "args": ( + "🚫 Usage example:" + " .pmblsett 0 0" + ), + "args_pmban": ( + "🚫 Usage example:" + " .pmbanlast 5" + ), + "banned": ( + "😃 Hey there" + " •ᴗ•\nUnit «SIGMA», the guardian of this account. You are" + " not approved! You can contact my owner in chat, if you need" + " help.\nI need to ban you in terms of security" + ), + "removing": ( + "🚮 Removing {} last" + " dialogs..." + ), + "removed": ( + "🚮 Removed {} last" + " dialogs!" + ), + "user_not_specified": ( + "🚫 You haven't specified" + " user" + ), + "approved": ( + " {} approved in pm' + ), + "banned_log": ( + '👮 I banned {}.\n\n{} Reported' + " spam\n{} Deleted dialog\n\n" + " 📝 Message\n{}" + ), + "hello": ( + "🔏 Unit «SIGMA» protects your personal messages from intrusions. It" + " will block everyone, who's trying to invade you.\n\nUse" + " .pmbl to enable protection, .pmblsett to" + " configure it and .pmbanlast if you've already been" + " pm-raided." + ), + } + + strings_ru = { + "state": ( + "🛡 Текущее состояние" + " PM->BL: {}\nСообщать о спаме? - {}\nУдалять диалог? - {}" + ), + "args": ( + "🚫 Пример:" + " .pmblsett 0 0" + ), + "args_pmban": ( + "🚫 Пример:" + " .pmbanlast 5" + ), + "banned": ( + "😃 Добрый день" + " •ᴗ•\nЮнит «SIGMA», защитник этого аккаунта. Вы не" + " потверждены! Вы можете связаться с моим владельцем в чате," + " если нужна помощь.\nЯ вынужден заблокировать вас из соображений" + " безопасности" + ), + "hello": ( + "🔏 Юнит «SIGMA» защищает твои личные сообщенния от неизвестных" + " пользователей. Он будет блокировать всех, кто не соответствует" + " настройкам.\n\nВведи .pmbl для активации защиты," + " .pmblsett для ее настройки и .pmbanlast если" + " нужно очистить уже прошедший рейд на личные сообщения." + ), + "removing": ( + "🚮 Удаляю {} последних" + " диалогов..." + ), + "removed": ( + "🚮 Удалил {} последних" + " диалогов!" + ), + "user_not_specified": ( + "🚫 Укажи" + " пользователя" + ), + "_cmd_doc_pmbl": "Включить или выключить защиту", + "_cmd_doc_pmbanlast": ( + "<количество> - Забанить и удалить n последних диалогов с пользователями" + ), + "_cmd_doc_allowpm": "<пользователь> - Разрешить пользователю писать тебе в ЛС", + "_cls_doc": "Блокирует и репортит входящие сообщения от незнакомцев", + "approved": ( + " {} одобрен в лс' + ), + "banned_log": ( + '👮 Я заблокировал {}.\n\n{}' + " Сообщил" + " о спаме\n{} Удалил диалог\n\n📝" + " Сообщение\n{}" + ), + } + + strings_de = { + "state": ( + "🛡 Aktueller PM->BL" + " Status: {}\nSpam melden? - {}\nDialoge löschen? - {}" + ), + "args": ( + "🚫 Beispiel:" + " .pmblsett 0 0" + ), + "args_pmban": ( + "🚫 Beispiel:" + " .pmbanlast 5" + ), + "banned": ( + "😃 Hallo" + " •ᴗ•\nEinheit «SIGMA», der Schutz dieses Accounts. Sie" + " sind nicht autorisiert! Sie können sich an den Besitzer meines" + " Accounts wenden, wenn Sie Hilfe benötigen.\nIch bin gezwungen, Sie aus" + " Sicherheitsgründen zu sperren" + ), + "hello": ( + "🔏 Einheit «SIGMA» schützt Ihre persönlichen Nachrichten vor" + " unbekannten Benutzern. Es wird alle blockieren, die nicht den" + " Einstellungen entsprechen.\n\nGeben Sie .pmbl ein, um die" + " Schutzfunktion zu aktivieren, .pmblsett zum Konfigurieren" + " und .pmbanlast, wenn Sie bereits einen Raid auf Ihre" + " persönlichen Nachrichten durchgeführt haben." + ), + "removing": ( + "🚮 Entferne {} letzte" + " Dialoge..." + ), + "removed": ( + "🚮 Entfernt {} letzte" + " Dialoge!" + ), + "user_not_specified": ( + "🚫 Du hast keinen" + " Benutzer angegeben" + ), + "_cmd_doc_pmbl": "Aktiviert oder deaktiviert den Schutz", + "_cmd_doc_pmbanlast": ( + " - Bannt und löscht n letzte Dialoge mit Benutzern" + ), + "_cmd_doc_allowpm": ( + " - Erlaubt dem Benutzer, dir eine private Nachricht zu senden" + ), + "_cls_doc": "Blockiert und meldet eingehende Nachrichten von Unbekannten", + "approved": ( + " {} wurde in den Ls genehmigt' + ), + "banned_log": ( + '👮 Ich habe {} geblockt.\n\n{} Hat' + " über Spam berichtet\n{} Hat den Dialog gelöscht\n\n📝" + " Nachricht\n{}" + ), + } + + strings_tr = { + "state": ( + "🛡 Şu anki PM->BL durumu:" + " {}\nSpam rapor edilsin mi? - {}\nSohbetler silinsin mi? - {}" + ), + "args": ( + "🚫 Örnek:" + " .pmblsett 0 0" + ), + "args_pmban": ( + "🚫 Örnek:" + " .pmbanlast 5" + ), + "banned": ( + "😃 Merhaba" + " •ᴗ•\n«SIGMA» birimi, hesabınızın koruması. Yetkili" + " değilsiniz! Yardım için hesabımın sahibi ile iletişime" + " geçebilirsiniz.\nGüvenlik nedeniyle sizi zorunlu olarak" + " engelliyorum" + ), + "hello": ( + "🔏 «SIGMA» birimi, tanımadığınız kullanıcılarla kişisel" + " mesajlarınızı korur. Ayarlara uygun olmayanları tümünü engeller.\n\n" + ".pmbl yazarak koruma özelliğini etkinleştirebilir, " + ".pmblsett yazarak yapılandırabilir ve zaten kişisel" + " mesajlarınıza bir raid gerçekleştirdiyseniz .pmbanlast" + " yazarak bunu gerçekleştirebilirsiniz." + ), + "removing": ( + "🚮 Son {} sohbet" + " siliniyor..." + ), + "removed": ( + "🚮 Son {} sohbet" + " silindi!" + ), + "user_not_specified": ( + "🚫 Bir kullanıcı" + " belirtmediniz" + ), + "_cmd_doc_pmbl": "Korumayı etkinleştirir veya devre dışı bırakır", + "_cmd_doc_pmbanlast": " - Kullanıcılarla son n sohbeti yasaklar ve siler", + "_cmd_doc_allowpm": ( + " - Kullanıcıya kişisel mesaj göndermeye izin verir" + ), + "_cls_doc": ( + "Tanımadığınız kullanıcıların gelen mesajlarını engeller ve rapor eder" + ), + "approved": ( + " {} Ls listesine eklendi' + ), + "banned_log": ( + '👮 {} engellendi.\n\n{} Spam rapor' + " etti\n{} Sohbeti sildi\n\n📝 Mesaj\n{}" + ), + } + + strings_uz = { + "state": ( + "🛡 Joriy PM->BL holati:" + " {}\nSpam haqida xabar berilsinmi? - {}\nSuhbatlar o'chirilsinmi? -" + " {}" + ), + "args": ( + "🚫 Misol:" + " .pmblsett 0 0" + ), + "args_pmban": ( + "🚫 Misol:" + " .pmbanlast 5" + ), + "banned": ( + "😃 Salom" + " •ᴗ•\n«SIGMA» birimi, hisobingizni himoya. Ruxsat" + " berilmaganingiz! Yordam kerak bo'lsa hisobimning egasi bilan" + " bog'lanishingiz mumkin.\nXavfsizlik sababli sizni majbur qilishim" + " kerak" + ), + "hello": ( + "🔏 «SIGMA» birimi, tanimaydigan foydalanuvchilar bilan" + " shaxsiy xabarlarini himoya qiladi. Sozlamalarga mos bo'lmasa" + " barchasini bloklashadi.\n\n.pmbl yozib himoya" + " imkoniyatini yoqish, .pmblsett yozib konfiguratsiyani" + " o'zgartirish va agar sizda shaxsiy xabarlariga raid bormi bo'lsa" + " .pmbanlast yozib uni bajarishingiz mumkin." + ), + "removing": ( + "🚮 Son {} suhbat" + " o'chirilmoqda..." + ), + "removed": ( + "🚮 Son {} suhbat" + " o'chirildi!" + ), + "user_not_specified": ( + "🚫 Siz foydalanuvchi" + " belgilamadingiz" + ), + "_cmd_doc_pmbl": "Himoyani yoqadi yoki o'chiradi", + "_cmd_doc_pmbanlast": ( + " - Foydalanuvchilar bilan son n suhbatni yasaklaydi" + ), + "_cmd_doc_allowpm": ( + " - Foydalanuvchiga shaxsiy xabar yuborishga ruxsat beradi" + ), + "_cls_doc": "Tanimaydigan foydalanuvchilar gelen xabarlarini bloklashadi", + "approved": ( + " {} Ls ro'yxatiga qo'shildi" + ), + "banned_log": ( + '👮 {} bloklandi.\n\n{} Spam xabar' + " berdi\n{} Suhbat o'chirildi\n\n📝" + " Xabar\n{}" + ), + } + + strings_hi = { + "state": ( + "🛡 वर्तमान PM->BL स्थिति:" + " {}\nस्पैम रिपोर्ट करें? - {}\nडायलॉगहटाएं? - {}" + ), + "args": ( + "🚫 उदाहरण:" + " .pmblsett 0 0" + ), + "args_pmban": ( + "🚫 उदाहरण:" + " .pmbanlast 5" + ), + "banned": ( + "😃 नमस्ते" + " •ᴗ•\nयूनिट «SIGMA», इस खाते की सुरक्षा. आप" + " अनधिकृत हैं! आप मेरे खाते के मालिक को अपनी मदद के लिए या आपको" + " सहायता की आवश्यकता है तो उसे संपर्क कर सकते हैं।\nमैं आपको सुरक्षा के" + " कारण बंद करने के लिए बाधित कर दूंगा" + ), + "hello": ( + "🔏 यूनिट «SIGMA» अपने निजी संदेशों को अज्ञात उपयोगकर्ताओं से" + " सुरक्षित करता है। इसे सेटिंग्स के अनुसार सभी ब्लॉक करेगा।\n\n" + ".pmbl दर्ज करें, ताकि सुरक्षा कार्यक्षमता सक्रिय हो, " + ".pmblsett कॉन्फ़िगर करने के लिए और .pmbanlast, जब आपने" + " अपने निजी संदेशों पर एक रैड किया है।" + ), + "removing": ( + "🚮 {} अंतिम डायलॉग हटा" + " रहा है..." + ), + "removed": ( + "🚮 {} अंतिम डायलॉग हटा" + " दिया!" + ), + "user_not_specified": ( + "🚫 आपने किसी उपयोगकर्ता" + " को नहीं निर्दिष्ट किया" + ), + "_cmd_doc_pmbl": "सुरक्षा को सक्षम या अक्षम करता है", + "_cmd_doc_pmbanlast": "<अंक> - उपयोगकर्ताओं के साथ निजी संदेशों को ब्लॉक और हटाता है", + "_cmd_doc_allowpm": ( + "<उपयोगकर्ता> - उपयोगकर्ता को आपको एक निजी संदेश भेजने की अनुमति देता है" + ), + "_cmd_doc_pmblsett": ( + "<ब्लॉक> <अनुमति> - ब्लॉक और अनुमति को सेट करता है, जब आपके पास एक निजी संदेश आता है" + ), + "_cls_doc": "एक निजी संदेश भेजने की अनुमति देता है", + } + + def __init__(self): + self._queue = [] + self._ban_queue = [] + self.config = loader.ModuleConfig( + loader.ConfigValue( + "ignore_contacts", + True, + lambda: "Ignore contacts?", + validator=loader.validators.Boolean(), + ), + loader.ConfigValue( + "ignore_active", + True, + lambda: "Ignore peers, where you participated?", + validator=loader.validators.Boolean(), + ), + loader.ConfigValue( + "active_threshold", + 5, + lambda: "What number of your messages is required to trust peer", + validator=loader.validators.Integer(minimum=1), + ), + loader.ConfigValue( + "custom_message", + doc=lambda: "Custom message to notify untrusted peers. Leave empty for default one", + ), + loader.ConfigValue( + "photo", + "https://github.com/hikariatama/assets/raw/master/unit_sigma.png", + lambda: "Photo, which is sent along with banned notification", + validator=loader.validators.Link(), + ), + loader.ConfigValue( + "report_spam", + False, + lambda: "Report spam?", + validator=loader.validators.Boolean(), + ), + loader.ConfigValue( + "delete_dialog", + False, + lambda: "Delete dialog?", + validator=loader.validators.Boolean(), + ), + loader.ConfigValue( + "silent", + False, + lambda: "Do not send anything to banned user", + validator=loader.validators.Boolean(), + ), + ) + + async def client_ready(self): + self._whitelist = self.get("whitelist", []) + self._ratelimit = [] + self._ratelimit_timeout = 5 * 60 + self._ratelimit_threshold = 10 + if not self.get("ignore_hello", False): + await self.inline.bot.send_photo( + self._tg_id, + photo=( + r"https://github.com/hikariatama/assets/raw/master/unit_sigma.png" + ), + caption=self.strings("hello"), + parse_mode="HTML", + ) + + self.set("ignore_hello", True) + + async def pmblcmd(self, message: Message): + """Toggle PMBL""" + current = self.get("state", False) + new = not current + self.set("state", new) + await utils.answer( + message, + self.strings("state").format( + "on" if new else "off", + "yes" if self.config["report_spam"] else "no", + "yes" if self.config["delete_dialog"] else "no", + ), + ) + + async def pmbanlastcmd(self, message: Message): + """ - Ban and delete dialogs with n most new users""" + n = utils.get_args_raw(message) + if not n or not n.isdigit(): + await utils.answer(message, self.strings("args_pmban")) + return + + n = int(n) + + await utils.answer(message, self.strings("removing").format(n)) + + dialogs = [] + async for dialog in self._client.iter_dialogs(ignore_pinned=True): + try: + if not isinstance(dialog.message.peer_id, PeerUser): + continue + except AttributeError: + continue + + m = ( + await self._client.get_messages( + dialog.message.peer_id, + limit=1, + reverse=True, + ) + )[0] + + dialogs += [ + ( + get_peer_id(dialog.message.peer_id), + int(time.mktime(m.date.timetuple())), + ) + ] + + dialogs.sort(key=lambda x: x[1]) + to_ban = [d for d, _ in dialogs[::-1][:n]] + + for d in to_ban: + await self._client(BlockRequest(id=d)) + + await self._client(DeleteHistoryRequest(peer=d, just_clear=True, max_id=0)) + + await utils.answer(message, self.strings("removed").format(n)) + + def _approve(self, user: int, reason: str = "unknown"): + self._whitelist += [user] + self._whitelist = list(set(self._whitelist)) + self.set("whitelist", self._whitelist) + logger.debug(f"User approved in pm {user}, filter: {reason}") + return + + async def allowpmcmd(self, message: Message): + """ - Allow user to pm you""" + args = utils.get_args_raw(message) + reply = await message.get_reply_message() + + user = None + + try: + user = await self._client.get_entity(args) + except Exception: + with contextlib.suppress(Exception): + user = await self._client.get_entity(reply.sender_id) if reply else None + + if not user: + chat = await message.get_chat() + if not isinstance(chat, User): + await utils.answer(message, self.strings("user_not_specified")) + return + + user = chat + + self._approve(user.id, "manual_approve") + await utils.answer( + message, self.strings("approved").format(user.id, get_display_name(user)) + ) + + async def watcher(self, message: Message): + if ( + getattr(message, "out", False) + or not isinstance(message, Message) + or not isinstance(message.peer_id, PeerUser) + or not self.get("state", False) + or utils.get_chat_id(message) + in { + 1271266957, # @replies + 777000, # Telegram Notifications + self._tg_id, # Self + } + ): + return + + self._queue += [message] + + @loader.loop(interval=0.05, autostart=True) + async def ban_loop(self): + if not self._ban_queue: + return + + message = self._ban_queue.pop(0) + self._ratelimit = list( + filter( + lambda x: x + self._ratelimit_timeout < time.time(), + self._ratelimit, + ) + ) + + dialog = None + + if len(self._ratelimit) < self._ratelimit_threshold: + if not self.config["silent"]: + try: + await self._client.send_file( + message.peer_id, + self.config["photo"], + caption=self.config["custom_message"] or self.strings("banned"), + ) + except Exception: + await utils.answer( + message, + self.config["custom_message"] or self.strings("banned"), + ) + + self._ratelimit += [round(time.time())] + + try: + dialog = await self._client.get_entity(message.peer_id) + except ValueError: + pass + + await self.inline.bot.send_message( + self._client.tg_id, + self.strings("banned_log").format( + dialog.id if dialog is not None else message.sender_id, + ( + utils.escape_html(dialog.first_name) + if dialog is not None + else ( + getattr(getattr(message, "sender", None), "username", None) + or message.sender_id + ) + ), + format_(self.config["report_spam"]), + format_(self.config["delete_dialog"]), + utils.escape_html( + "" + if message.photo + else ( + "