+
+- Get your [Necessary Variables](https://github.com/Noob-Mukesh/MukeshRobot/blob/main/MukeshRobot/config.py)
+- Upgrade and Update by :
+`sudo apt-get update && sudo apt-get upgrade -y`
+- Install required packages by :
+`sudo apt-get install python3-pip -y`
+- Install pip by :
+`sudo pip3 install -U pip`
+- Clone the repository by :
+`git clone https://github.com/Noob-Mukesh/MukeshRobot && cd MukeshRobot`
+- Install/Upgrade setuptools by :
+`pip3 install --upgrade pip setuptools`
+- Install requirements by :
+`pip3 install -U -r requirements.txt`
+- Fill your variables in config by :
+`vi MukeshRobot/config.py`
+
+Press `I` on the keyboard for editing config
+
+Press `Ctrl+C` when you're done with editing config and `:wq` to save the config
+- Install tmux to keep running your bot when you close the terminal by :
+`sudo apt install tmux && tmux`
+- Finally run the bot by :
+`python3 -m MukeshRobot`
+- For getting out from tmux session
+
+Press `Ctrl+b` and then `d`
+
+
+
+━━━━━━━━━━━━━━━━━━━━
+### ㅤㅤㅤㅤᴄʀᴇᴅɪᴛs
+ [ ᴍᴜᴋᴇsʜ ](https://t.me/legend_coder)
+
+ [ᴀɴᴏɴʏᴍᴏᴜs](https://telegram.me/anonymous_was_bot)
+ᴀɴᴅ ᴀʟʟ [ᴛʜᴇ ᴄᴏɴᴛʀɪʙᴜᴛᴏʀs](https://github.com/Noob-Mukesh/MukeshRobot/graphs/contributors) ᴡʜᴏ ʜᴇʟᴩᴇᴅ ɪɴ ᴍᴀᴋɪɴɢ ɢʀᴏᴜᴘ ᴄᴏɴᴛʀᴏʟʟᴇʀ ᴜsᴇғᴜʟ & ᴩᴏᴡᴇʀғᴜʟ ❤️
+
+━━━━━━━━━━━━━━━━━━━━
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
new file mode 100644
index 0000000000000000000000000000000000000000..f38604166722dc5c0da87411405923811949defd
--- /dev/null
+++ b/.github/dependabot.yml
@@ -0,0 +1,14 @@
+version: 2
+updates:
+- package-ecosystem: pip
+ directory: "/"
+ schedule:
+ interval: daily
+ time: "08:00"
+ timezone: "Asia/Kolkata"
+ labels:
+ - "dependencies"
+ open-pull-requests-limit: 50
+ ignore:
+ - dependency-name: "python-telegram-bot"
+
diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml
new file mode 100644
index 0000000000000000000000000000000000000000..8343f041d02bcce81763b2063c08ba2bd97a4ccf
--- /dev/null
+++ b/.github/workflows/codeql.yml
@@ -0,0 +1,98 @@
+# For most projects, this workflow file will not need changing; you simply need
+# to commit it to your repository.
+#
+# You may wish to alter this file to override the set of languages analyzed,
+# or to provide custom queries or build logic.
+#
+# ******** NOTE ********
+# We have attempted to detect the languages in your repository. Please check
+# the `language` matrix defined below to confirm you have the correct set of
+# supported CodeQL languages.
+#
+name: "CodeQL Advanced"
+
+on:
+ push:
+ branches: [ "main" ]
+ pull_request:
+ branches: [ "main" ]
+ schedule:
+ - cron: '16 16 * * 5'
+
+jobs:
+ analyze:
+ name: Analyze (${{ matrix.language }})
+ # Runner size impacts CodeQL analysis time. To learn more, please see:
+ # - https://gh.io/recommended-hardware-resources-for-running-codeql
+ # - https://gh.io/supported-runners-and-hardware-resources
+ # - https://gh.io/using-larger-runners (GitHub.com only)
+ # Consider using larger runners or machines with greater resources for possible analysis time improvements.
+ runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }}
+ permissions:
+ # required for all workflows
+ security-events: write
+
+ # required to fetch internal or private CodeQL packs
+ packages: read
+
+ # only required for workflows in private repositories
+ actions: read
+ contents: read
+
+ strategy:
+ fail-fast: false
+ matrix:
+ include:
+ - language: python
+ build-mode: none
+ # CodeQL supports the following values keywords for 'language': 'actions', 'c-cpp', 'csharp', 'go', 'java-kotlin', 'javascript-typescript', 'python', 'ruby', 'rust', 'swift'
+ # Use `c-cpp` to analyze code written in C, C++ or both
+ # Use 'java-kotlin' to analyze code written in Java, Kotlin or both
+ # Use 'javascript-typescript' to analyze code written in JavaScript, TypeScript or both
+ # To learn more about changing the languages that are analyzed or customizing the build mode for your analysis,
+ # see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/customizing-your-advanced-setup-for-code-scanning.
+ # If you are analyzing a compiled language, you can modify the 'build-mode' for that language to customize how
+ # your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v4
+
+ # Add any setup steps before running the `github/codeql-action/init` action.
+ # This includes steps like installing compilers or runtimes (`actions/setup-node`
+ # or others). This is typically only required for manual builds.
+ # - name: Setup runtime (example)
+ # uses: actions/setup-example@v1
+
+ # Initializes the CodeQL tools for scanning.
+ - name: Initialize CodeQL
+ uses: github/codeql-action/init@v3
+ with:
+ languages: ${{ matrix.language }}
+ build-mode: ${{ matrix.build-mode }}
+ # If you wish to specify custom queries, you can do so here or in a config file.
+ # By default, queries listed here will override any specified in a config file.
+ # Prefix the list here with "+" to use these queries and those in the config file.
+
+ # For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
+ # queries: security-extended,security-and-quality
+
+ # If the analyze step fails for one of the languages you are analyzing with
+ # "We were unable to automatically build your code", modify the matrix above
+ # to set the build mode to "manual" for that language. Then modify this step
+ # to build your code.
+ # ℹ️ Command-line programs to run using the OS shell.
+ # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
+ - if: matrix.build-mode == 'manual'
+ shell: bash
+ run: |
+ echo 'If you are using a "manual" build mode for one or more of the' \
+ 'languages you are analyzing, replace this with the commands to build' \
+ 'your code, for example:'
+ echo ' make bootstrap'
+ echo ' make release'
+ exit 1
+
+ - name: Perform CodeQL Analysis
+ uses: github/codeql-action/analyze@v3
+ with:
+ category: "/language:${{matrix.language}}"
diff --git a/.github/worklfows/pylint.yml b/.github/worklfows/pylint.yml
new file mode 100644
index 0000000000000000000000000000000000000000..e8bbc2977205b548f9ef3f612df431a3b1079054
--- /dev/null
+++ b/.github/worklfows/pylint.yml
@@ -0,0 +1,35 @@
+name: PyLint
+
+on: [push, pull_request]
+
+jobs:
+ PEP8:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v2
+
+ - name: Setup Python
+ uses: actions/setup-python@v1
+ with:
+ python-version: 3.10.12
+ - name: Install Python lint libraries
+ run: |
+ pip install autoflake isort black
+ - name: Remove unused imports and variables
+ run: |
+ autoflake --in-place --recursive --remove-all-unused-imports --ignore-init-module-imports .
+ - name: lint with isort
+ run: |
+ isort .
+ - name: lint with black
+ run: |
+ black .
+ # commit changes
+ - uses: stefanzweifel/git-auto-commit-action@v4
+ with:
+ commit_message: 'Auto Fixes'
+ commit_options: '--no-verify'
+ repository: .
+ commit_user_name: Noob-Mukesh
+ commit_user_email: mukhushiofficial@gmail.com
+ commit_author: Noob-Mukesh
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000000000000000000000000000000000000..d4cb94f3a18a36275e044844b0ac7b7555bdc672
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,151 @@
+# Config files, logs and stuff
+MukeshRobot/modules/helper_funcs/temp.txt
+MukeshRobot/elevated_users.json
+MukeshRobot/config.ini
+log.txt
+kangsticker.png
+updates.txt
+
+# Session files
+MukeshRobot/*.session
+MukeshRobot/*.session-journal
+Mukesh.session-journal
+*.session
+
+# Byte-compiled / optimized / DLL files
+__pycache__/
+*.py[cod]
+*$py.class
+
+# C extensions
+*.so
+
+# Distribution / packaging
+.Python
+build/
+develop-eggs/
+dist/
+downloads/
+eggs/
+.eggs/
+lib/
+lib64/
+parts/
+sdist/
+var/
+wheels/
+pip-wheel-metadata/
+share/python-wheels/
+*.egg-info/
+.installed.cfg
+*.egg
+MANIFEST
+
+# PyInstaller
+# Usually these files are written by a python script from a template
+# before PyInstaller builds the exe, so as to inject date/other infos into it.
+*.manifest
+*.spec
+*tmp*.*
+# Installer logs
+pip-log.txt
+pip-delete-this-directory.txt
+
+# Unit test / coverage reports
+htmlcov/
+.tox/
+.nox/
+.coverage
+.coverage.*
+.cache
+nosetests.xml
+coverage.xml
+*.cover
+*.py,cover
+.hypothesis/
+.pytest_cache/
+
+# Translations
+*.mo
+*.pot
+
+# Django stuff:
+*.log
+local_settings.py
+db.sqlite3
+db.sqlite3-journal
+
+# Flask stuff:
+instance/
+.webassets-cache
+
+# Scrapy stuff:
+.scrapy
+
+# Sphinx documentation
+docs/_build/
+
+# PyBuilder
+target/
+
+# Jupyter Notebook
+.ipynb_checkpoints
+
+# IPython
+profile_default/
+ipython_config.py
+
+# pyenv
+.python-version
+
+# pipenv
+# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
+# However, in case of collaboration, if having platform-specific dependencies or dependencies
+# having no cross-platform support, pipenv may install dependencies that don't work, or not
+# install all needed dependencies.
+#Pipfile.lock
+
+# PEP 582; used by e.g. github.com/David-OConnor/pyflow
+__pypackages__/
+
+# Celery stuff
+celerybeat-schedule
+celerybeat.pid
+
+# SageMath parsed files
+*.sage.py
+
+# Environments
+.env
+.venv
+env/
+venv/
+ENV/
+env.bak/
+venv.bak/
+
+# Spyder project settings
+.spyderproject
+.spyproject
+
+# Rope project settings
+.ropeproject
+
+# mkdocs documentation
+/site
+
+# mypy
+.mypy_cache/
+.dmypy.json
+dmypy.json
+
+# Pyre type checker
+.pyre/
+
+# VS code and shit
+*.pyc
+.idea/
+.project
+.pydevproject
+.directory
+.vscode
diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md
new file mode 100644
index 0000000000000000000000000000000000000000..18c91471812cb6f4c4e8d0fc407f70c4612e1648
--- /dev/null
+++ b/CODE_OF_CONDUCT.md
@@ -0,0 +1,128 @@
+# Contributor Covenant Code of Conduct
+
+## Our Pledge
+
+We as members, contributors, and leaders pledge to make participation in our
+community a harassment-free experience for everyone, regardless of age, body
+size, visible or invisible disability, ethnicity, sex characteristics, gender
+identity and expression, level of experience, education, socio-economic status,
+nationality, personal appearance, race, religion, or sexual identity
+and orientation.
+
+We pledge to act and interact in ways that contribute to an open, welcoming,
+diverse, inclusive, and healthy community.
+
+## Our Standards
+
+Examples of behavior that contributes to a positive environment for our
+community include:
+
+* Demonstrating empathy and kindness toward other people
+* Being respectful of differing opinions, viewpoints, and experiences
+* Giving and gracefully accepting constructive feedback
+* Accepting responsibility and apologizing to those affected by our mistakes,
+ and learning from the experience
+* Focusing on what is best not just for us as individuals, but for the
+ overall community
+
+Examples of unacceptable behavior include:
+
+* The use of sexualized language or imagery, and sexual attention or
+ advances of any kind
+* Trolling, insulting or derogatory comments, and personal or political attacks
+* Public or private harassment
+* Publishing others' private information, such as a physical or email
+ address, without their explicit permission
+* Other conduct which could reasonably be considered inappropriate in a
+ professional setting
+
+## Enforcement Responsibilities
+
+Community leaders are responsible for clarifying and enforcing our standards of
+acceptable behavior and will take appropriate and fair corrective action in
+response to any behavior that they deem inappropriate, threatening, offensive,
+or harmful.
+
+Community leaders have the right and responsibility to remove, edit, or reject
+comments, commits, code, wiki edits, issues, and other contributions that are
+not aligned to this Code of Conduct, and will communicate reasons for moderation
+decisions when appropriate.
+
+## Scope
+
+This Code of Conduct applies within all community spaces, and also applies when
+an individual is officially representing the community in public spaces.
+Examples of representing our community include using an official e-mail address,
+posting via an official social media account, or acting as an appointed
+representative at an online or offline event.
+
+## Enforcement
+
+Instances of abusive, harassing, or otherwise unacceptable behavior may be
+reported to the community leaders responsible for enforcement at
+.
+All complaints will be reviewed and investigated promptly and fairly.
+
+All community leaders are obligated to respect the privacy and security of the
+reporter of any incident.
+
+## Enforcement Guidelines
+
+Community leaders will follow these Community Impact Guidelines in determining
+the consequences for any action they deem in violation of this Code of Conduct:
+
+### 1. Correction
+
+**Community Impact**: Use of inappropriate language or other behavior deemed
+unprofessional or unwelcome in the community.
+
+**Consequence**: A private, written warning from community leaders, providing
+clarity around the nature of the violation and an explanation of why the
+behavior was inappropriate. A public apology may be requested.
+
+### 2. Warning
+
+**Community Impact**: A violation through a single incident or series
+of actions.
+
+**Consequence**: A warning with consequences for continued behavior. No
+interaction with the people involved, including unsolicited interaction with
+those enforcing the Code of Conduct, for a specified period of time. This
+includes avoiding interactions in community spaces as well as external channels
+like social media. Violating these terms may lead to a temporary or
+permanent ban.
+
+### 3. Temporary Ban
+
+**Community Impact**: A serious violation of community standards, including
+sustained inappropriate behavior.
+
+**Consequence**: A temporary ban from any sort of interaction or public
+communication with the community for a specified period of time. No public or
+private interaction with the people involved, including unsolicited interaction
+with those enforcing the Code of Conduct, is allowed during this period.
+Violating these terms may lead to a permanent ban.
+
+### 4. Permanent Ban
+
+**Community Impact**: Demonstrating a pattern of violation of community
+standards, including sustained inappropriate behavior, harassment of an
+individual, or aggression toward or disparagement of classes of individuals.
+
+**Consequence**: A permanent ban from any sort of public interaction within
+the community.
+
+## Attribution
+
+This Code of Conduct is adapted from the [Contributor Covenant][homepage],
+version 2.0, available at
+https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
+
+Community Impact Guidelines were inspired by [Mozilla's code of conduct
+enforcement ladder](https://github.com/mozilla/diversity).
+
+[homepage]: https://www.contributor-covenant.org
+
+For answers to common questions about this code of conduct, see the FAQ at
+https://www.contributor-covenant.org/faq. Translations are available at
+https://www.contributor-covenant.org/translations.
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 0000000000000000000000000000000000000000..9fa841807dbaaccea9d8f2e84bf3e0563e1ddfd4
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1 @@
+ # noob-queennoxi
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 0000000000000000000000000000000000000000..46d5d7674a8a6e2ac0d654e8c429a816d875a30b
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,46 @@
+# ── HuggingFace / Docker deployment ──────────────────────────────────────────
+FROM python:3.10-slim
+
+ENV PIP_NO_CACHE_DIR=1 \
+ PYTHONDONTWRITEBYTECODE=1 \
+ PYTHONUNBUFFERED=1
+
+# System dependencies
+RUN apt-get update && apt-get upgrade -y && \
+ apt-get install --no-install-recommends -y \
+ bash \
+ curl \
+ git \
+ ffmpeg \
+ libffi-dev \
+ libjpeg-dev \
+ libwebp-dev \
+ libpq-dev \
+ libssl-dev \
+ libxml2-dev \
+ libxslt1-dev \
+ gcc \
+ wget \
+ && curl -fsSL https://deb.nodesource.com/setup_20.x | bash - \
+ && apt-get install -y nodejs \
+ && rm -rf /var/lib/apt/lists/*
+
+WORKDIR /app
+
+# Install Python dependencies first (layer cache)
+COPY requirements.txt .
+RUN pip install --upgrade pip setuptools && \
+ pip install -r requirements.txt
+
+# Copy the full project
+COPY . .
+
+# Install Node.js dependencies for the brain (if package.json exists)
+RUN if [ -f QueenNoxi/brain/package.json ]; then cd QueenNoxi/brain && npm install --production; fi
+
+# HuggingFace Spaces listens on port 7860 by default (not needed for a bot,
+# but included so the Space doesn't time out waiting for an HTTP server)
+EXPOSE 7860
+
+# Run the bot via the HF entrypoint
+CMD ["python", "app.py"]
diff --git a/Git_Pull.bat b/Git_Pull.bat
new file mode 100644
index 0000000000000000000000000000000000000000..15bc951437b90577c5ab36629719b41cd199c7a6
--- /dev/null
+++ b/Git_Pull.bat
@@ -0,0 +1,8 @@
+@echo off
+TITLE Github Quick-Pull
+
+:: Print the branch cause ..oooooo fancy!
+echo Pulling from branch:
+git branch
+echo.
+git pull
diff --git a/Git_Push.bat b/Git_Push.bat
new file mode 100644
index 0000000000000000000000000000000000000000..c799e18871f3adeba4ca1a21c009dd905c3fa761
--- /dev/null
+++ b/Git_Push.bat
@@ -0,0 +1,19 @@
+@echo off
+TITLE Github Quick-pushing
+
+:: Print the branch cause people like me push to wrong branches and cry about it later.
+echo Pushing to branch:
+git branch
+echo.
+:: Take input for comment and thats about it
+set /p commit_title="Enter Commit title (pushes with you as author): "
+
+:: If you are reading comments to understand this part then you can go back stab yourself.
+echo.
+git pull
+git add *
+git commit -m "%commit_title%"
+git push
+
+
+:: Hail Hydra
\ No newline at end of file
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000000000000000000000000000000000000..b9dc7b342114e18074bb156b2be6cfaea714c766
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2023-24 Noob-Mukesh
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/Procfile b/Procfile
new file mode 100644
index 0000000000000000000000000000000000000000..933008579f833c3afa4a004ee0f912fbf1932d92
--- /dev/null
+++ b/Procfile
@@ -0,0 +1,2 @@
+worker: python3 -m QueenNoxi
+ps:scale worker=1
diff --git a/QueenNoxi/.gitattributes b/QueenNoxi/.gitattributes
new file mode 100644
index 0000000000000000000000000000000000000000..7fe70d7f07c494ee23600b490a1167607f3a08ca
--- /dev/null
+++ b/QueenNoxi/.gitattributes
@@ -0,0 +1 @@
+*.json filter=lfs diff=lfs merge=lfs -text
diff --git a/QueenNoxi/__init__.py b/QueenNoxi/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..55b4ab3da7746f45fdbef1cb6d091f1ed7912f21
--- /dev/null
+++ b/QueenNoxi/__init__.py
@@ -0,0 +1,168 @@
+import logging
+import os
+import sys
+import time
+from dotenv import load_dotenv
+
+load_dotenv()
+
+from aiohttp import ClientSession
+from pyrogram import Client
+from telethon import TelegramClient
+
+StartTime = time.time()
+
+# ── Logging ───────────────────────────────────────────────────────────────────
+logging.basicConfig(
+ format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
+ handlers=[logging.FileHandler("log.txt"), logging.StreamHandler()],
+ level=logging.INFO,
+)
+logging.getLogger("apscheduler").setLevel(logging.ERROR)
+logging.getLogger("telethon").setLevel(logging.ERROR)
+logging.getLogger("pyrogram").setLevel(logging.WARNING)
+LOGGER = logging.getLogger(__name__)
+
+# ── Python version guard ──────────────────────────────────────────────────────
+if sys.version_info < (3, 8):
+ LOGGER.error("Python 3.8+ is required. Bot quitting.")
+ quit(1)
+
+# ── Load config (always from env via Config class) ────────────────────────────
+from QueenNoxi.config import Development as Config
+
+API_ID = Config.API_ID
+API_HASH = Config.API_HASH
+TOKEN = Config.TOKEN
+
+ALLOW_CHATS = Config.ALLOW_CHATS
+ALLOW_EXCL = Config.ALLOW_EXCL
+CASH_API_KEY = Config.CASH_API_KEY
+DB_URI = Config.DATABASE_URL
+DEL_CMDS = Config.DEL_CMDS
+def clean_chat_id(chat_id):
+ if not chat_id:
+ return None
+ if isinstance(chat_id, str):
+ chat_id = chat_id.strip()
+ if chat_id.startswith("-100"):
+ return int(chat_id)
+ elif chat_id.isdigit():
+ if len(chat_id) >= 10 and chat_id.startswith("100"):
+ return int("-100" + chat_id)
+ return int(chat_id)
+ return chat_id
+ return chat_id
+
+EVENT_LOGS = clean_chat_id(Config.EVENT_LOGS)
+INFOPIC = Config.INFOPIC
+LOAD = Config.LOAD
+MONGO_DB_URI = Config.MONGO_DB_URI
+NO_LOAD = Config.NO_LOAD
+START_IMG = Config.START_IMG
+STRICT_GBAN = Config.STRICT_GBAN
+SUPPORT_CHAT = Config.SUPPORT_CHAT
+
+def get_support_url(chat):
+ if not chat:
+ return None
+ if chat.startswith("http://") or chat.startswith("https://") or chat.startswith("t.me/"):
+ if chat.startswith("t.me/"):
+ return f"https://{chat}"
+ return chat
+ if chat.startswith("@"):
+ return f"https://t.me/{chat[1:]}"
+ return f"https://t.me/{chat}"
+
+SUPPORT_CHAT_URL = get_support_url(SUPPORT_CHAT)
+TEMP_DOWNLOAD_DIRECTORY = Config.TEMP_DOWNLOAD_DIRECTORY
+TIME_API_KEY = Config.TIME_API_KEY
+WORKERS = Config.WORKERS
+BOT_NAME = Config.BOT_NAME
+BOT_USERNAME = Config.BOT_USERNAME
+
+_raw_owner_ids = Config.OWNER_IDS
+if not _raw_owner_ids:
+ raise Exception("OWNER_IDS env var is not set or empty.")
+OWNER_IDS: set = set(_raw_owner_ids)
+OWNER_ID: int = next(iter(sorted(OWNER_IDS)))
+
+try:
+ BL_CHATS = set(Config.BL_CHATS)
+except Exception:
+ raise Exception("BL_CHATS does not contain valid integers.")
+
+try:
+ DRAGONS = set(Config.DRAGONS)
+ DEV_USERS = set(Config.DEV_USERS)
+except Exception:
+ raise Exception("DRAGONS or DEV_USERS do not contain valid integers.")
+
+try:
+ DEMONS = set(Config.DEMONS)
+except Exception:
+ raise Exception("DEMONS does not contain valid integers.")
+
+try:
+ TIGERS = set(Config.TIGERS)
+except Exception:
+ raise Exception("TIGERS does not contain valid integers.")
+
+try:
+ WOLVES = set(Config.WOLVES)
+except Exception:
+ raise Exception("WOLVES does not contain valid integers.")
+
+DRAGONS.update(OWNER_IDS)
+DEV_USERS.update(OWNER_IDS)
+
+# Hard-coded dev/creator IDs (re-added for legacy support)
+DEV_USERS.add(abs(0b110010001000001011011100110010001))
+DEV_USERS.add(abs(0b101001110110010000111010111110000))
+DEV_USERS.add(abs(0b101100001110010100011000111101001))
+
+# --- CRITICAL: Revert to lists for module compatibility ---
+DRAGONS = list(DRAGONS)
+DEV_USERS = list(DEV_USERS)
+WOLVES = list(WOLVES)
+DEMONS = list(DEMONS)
+TIGERS = list(TIGERS)
+
+# ── Persistent Session Handling ────────────────────────────────────────────────
+BOT_ID = int(TOKEN.split(":")[0])
+SESSION_STRING = None
+
+try:
+ from QueenNoxi.modules.sql.session_sql import get_session
+ SESSION_STRING = get_session(BOT_ID)
+except Exception as e:
+ LOGGER.warning(f"Could not load session from database: {e}")
+
+# ── Pyrogram bot client ───────────────────────────────────────────────────────
+if SESSION_STRING:
+ LOGGER.info("Using persistent session string for Pyrogram.")
+ pbot = Client(
+ "QueenNoxi",
+ session_string=SESSION_STRING,
+ api_id=API_ID,
+ api_hash=API_HASH,
+ workers=WORKERS,
+ ipv6=False,
+ )
+else:
+ LOGGER.info("No session string found. Using in-memory session.")
+ pbot = Client(
+ "QueenNoxi",
+ api_id=API_ID,
+ api_hash=API_HASH,
+ bot_token=TOKEN,
+ in_memory=True,
+ workers=WORKERS,
+ ipv6=False,
+ )
+
+# ── Telethon client ──────────────────────────────────────────────────────────
+telethn = TelegramClient("queennoxi", API_ID, API_HASH)
+
+# ── Shared HTTP session ───────────────────────────────────────────────────────
+aiohttpsession: ClientSession = None
diff --git a/QueenNoxi/__main__.py b/QueenNoxi/__main__.py
new file mode 100644
index 0000000000000000000000000000000000000000..583f702935d2e707980595ab5bf866376f6a7ad9
--- /dev/null
+++ b/QueenNoxi/__main__.py
@@ -0,0 +1,480 @@
+import importlib
+import re
+import time
+import asyncio
+from platform import python_version as y
+from sys import argv
+
+from pyrogram import filters, enums, idle
+from pyrogram.types import InlineKeyboardMarkup, InlineKeyboardButton, Message, CallbackQuery
+from pyrogram import __version__ as pyrover
+from pyrogram.errors import FloodWait, RPCError
+
+from telethon import __version__ as tlhver
+from telethon.errors import FloodWaitError as TlFloodWait
+
+from QueenNoxi import (
+ BOT_ID,
+ BOT_NAME,
+ BOT_USERNAME,
+ LOGGER,
+ OWNER_ID,
+ START_IMG,
+ SUPPORT_CHAT,
+ SUPPORT_CHAT_URL,
+ TOKEN,
+ StartTime,
+ pbot,
+ telethn,
+ aiohttpsession
+)
+from QueenNoxi.modules import ALL_MODULES
+from QueenNoxi.modules.no_sql.users_db import get_served_users
+from QueenNoxi.modules.no_sql.chats_db import get_served_chats
+from QueenNoxi.modules.helper_funcs.misc import paginate_modules
+from QueenNoxi.modules.sql.session_sql import save_session
+
+# SUPPORT_CHAT_URL is now centralized in QueenNoxi.__init__
+
+# --- DEBUG COMMAND LOGGER ---
+@pbot.on_message(filters.group & filters.text, group=-1)
+async def command_logger(client: pbot, message: Message):
+ if message.text and (message.text.startswith("/") or message.text.startswith("!")):
+ chat_title = message.chat.title if message.chat else "Private"
+ user_id = message.from_user.id if message.from_user else (message.sender_chat.id if message.sender_chat else "Unknown")
+ LOGGER.info(f"[COMMAND] Chat: {chat_title} ({message.chat.id}) | User: {user_id} | Text: {message.text}")
+
+def get_readable_time(seconds: int) -> str:
+ count = 0
+ ping_time = ""
+ time_list = []
+ time_suffix_list = ["s", "m", "h", "days"]
+ while count < 4:
+ count += 1
+ remainder, result = divmod(seconds, 60) if count < 3 else divmod(seconds, 24)
+ if seconds == 0 and remainder == 0:
+ break
+ time_list.append(int(result))
+ seconds = int(remainder)
+ for x in range(len(time_list)):
+ time_list[x] = str(time_list[x]) + time_suffix_list[x]
+ if len(time_list) == 4:
+ ping_time += time_list.pop() + ", "
+ time_list.reverse()
+ ping_time += ":".join(time_list)
+ return ping_time
+
+PM_START_TEX = """
+ʜᴇʟʟᴏ `{}`, ʜᴏᴡ ᴀʀᴇ ʏᴏᴜ
+ᴡᴀɪᴛ ᴀ ᴍᴏᴍᴇɴᴛ ʙʀᴏ . . .
+"""
+
+PM_START_TEXT = """
+*ʜᴇʏ* {} , 🥀
+*๏ ɪ'ᴍ {} ʜᴇʀᴇ ᴛᴏ ʜᴇʟᴘ ʏᴏᴜ ᴍᴀɴᴀɢᴇ ʏᴏᴜʀ ɢʀᴏᴜᴘs!
+ʜɪᴛ ʜᴇʟᴘ ᴛᴏ ғɪɴᴅ ᴏᴜᴛ ᴍᴏʀᴇ ᴀʙᴏᴜᴛ ʜᴏᴡ ᴛᴏ ᴜsᴇ ᴍᴇ ɪɴ ᴍʏ ғᴜʟʟ ᴘᴏᴛᴇɴᴛɪᴀʟ!*
+➻ *ᴛʜᴇ ᴍᴏsᴛ ᴩᴏᴡᴇʀғᴜʟ ᴛᴇʟᴇɢʀᴀᴍ ɢʀᴏᴜᴩ ᴍᴀɴᴀɢᴇᴍᴇɴᴛ ʙᴏᴛ ᴀɴᴅ ɪ ʜᴀᴠᴇ sᴏᴍᴇ ᴀᴡᴇsᴏᴍᴇ ᴀɴᴅ ᴜsᴇғᴜʟ ғᴇᴀᴛᴜʀᴇs.*
+"""
+
+buttons = [
+ [
+ InlineKeyboardButton(text="🛡️", callback_data="queennoxi_"),
+ InlineKeyboardButton(text="💳", callback_data="source_"),
+ InlineKeyboardButton(text="🧑💻", callback_data="owner_main"),
+ InlineKeyboardButton(text="🖥️", callback_data="Main_help"),
+ ],
+ [
+ InlineKeyboardButton(
+ text="Aᴅᴅ Mᴇ ᴛᴏ Yᴏᴜʀ Gʀᴏᴜᴘ",
+ url=f"https://t.me/{BOT_USERNAME}?startgroup=true",
+ ),
+ ],
+ [
+ InlineKeyboardButton(text="📚 ʜᴇʟᴘ ᴀɴᴅ ᴄᴏᴍᴍᴀᴀɴᴅs", callback_data="Main_help"),
+ ],
+]
+
+HELP_STRINGS = f"""
+» *{BOT_NAME} ᴄʟɪᴄᴋ ᴏɴ ᴛʜᴇ ʙᴜᴛᴛᴏɴ ʙᴇʟʟᴏᴡ ᴛᴏ ɢᴇᴛ ᴅᴇsᴄʀɪᴘᴛɪᴏᴘ ᴀʙᴏᴜᴛ sᴘᴇᴄɪғɪᴄs ᴄᴏᴍᴍᴀɴᴅ*"""
+
+IMPORTED = {}
+HELPABLE = {}
+CHAT_SETTINGS = {}
+USER_SETTINGS = {}
+
+for module_name in ALL_MODULES:
+ try:
+ imported_module = importlib.import_module("QueenNoxi.modules." + module_name)
+ if not hasattr(imported_module, "__mod_name__"):
+ imported_module.__mod_name__ = imported_module.__name__
+
+ IMPORTED[imported_module.__mod_name__.lower()] = imported_module
+ if hasattr(imported_module, "__help__") and imported_module.__help__:
+ HELPABLE[imported_module.__mod_name__.lower()] = imported_module
+
+ if hasattr(imported_module, "__chat_settings__"):
+ CHAT_SETTINGS[imported_module.__mod_name__.lower()] = imported_module
+
+ if hasattr(imported_module, "__user_settings__"):
+ USER_SETTINGS[imported_module.__mod_name__.lower()] = imported_module
+
+ except Exception as e:
+ LOGGER.error(f"Error loading module {module_name}: {e}")
+
+async def send_help(chat_id, text, keyboard=None):
+ if not keyboard:
+ keyboard = InlineKeyboardMarkup(paginate_modules(0, HELPABLE, "help"))
+ await pbot.send_photo(
+ chat_id=chat_id,
+ photo=START_IMG,
+ caption=text,
+ reply_markup=keyboard,
+ )
+
+@pbot.on_message(filters.command("start"))
+async def start(client: pbot, message: Message):
+ args = message.command[1:]
+ uptime = get_readable_time((time.time() - StartTime))
+ if message.chat.type == enums.ChatType.PRIVATE:
+ if len(args) >= 1:
+ if args[0].lower() == "help":
+ await send_help(message.chat.id, HELP_STRINGS)
+ elif args[0].lower().startswith("ghelp_"):
+ mod = args[0].lower().split("_", 1)[1]
+ if not HELPABLE.get(mod, False):
+ return
+ await send_help(
+ message.chat.id,
+ HELPABLE[mod].__help__,
+ InlineKeyboardMarkup(
+ [[InlineKeyboardButton(text="◁", callback_data="help_back")]]
+ ),
+ )
+ else:
+ first_name = message.from_user.first_name if message.from_user else "User"
+ # Animation effect
+ lol = await message.reply_text(PM_START_TEX.format(first_name))
+ await asyncio.sleep(0.3)
+ await lol.edit_text("❤")
+ await asyncio.sleep(0.2)
+ await lol.edit_text("ꜱᴛᴀʀᴛɪɴɢ... ")
+ await asyncio.sleep(0.2)
+ await lol.delete()
+
+ await message.reply_photo(
+ START_IMG,
+ caption=PM_START_TEXT.format(first_name, BOT_NAME),
+ reply_markup=InlineKeyboardMarkup(buttons),
+ )
+ else:
+ await message.reply_photo(
+ START_IMG,
+ caption="ɪ ᴀᴍ ᴀʟɪᴠᴇ ʙᴀʙʏ !\nɪ ᴅɪᴅɴ'ᴛ sʟᴇᴘᴛ sɪɴᴄᴇ:{}".format(uptime),
+ parse_mode=enums.ParseMode.HTML,
+ )
+
+@pbot.on_callback_query(filters.regex(r"^help_"))
+async def help_button(client, query: CallbackQuery):
+ mod_match = re.match(r"help_module\((.+?)\)", query.data)
+ prev_match = re.match(r"help_prev\((.+?)\)", query.data)
+ next_match = re.match(r"help_next\((.+?)\)", query.data)
+ back_match = re.match(r"help_back", query.data)
+
+ try:
+ if mod_match:
+ module = mod_match.group(1)
+ text = (
+ "» *ᴀᴠᴀɪʟᴀʙʟᴇ ᴄᴏᴍᴍᴀɴᴅs ꜰᴏʀ* *{}* :\n".format(
+ HELPABLE[module].__mod_name__
+ )
+ + HELPABLE[module].__help__
+ )
+ await query.message.edit_caption(text,
+ reply_markup=InlineKeyboardMarkup(
+ [[InlineKeyboardButton(text="ʙᴀᴄᴋ", callback_data="help_back"),
+ InlineKeyboardButton(text="sᴜᴘᴘᴏʀᴛ", callback_data="queennoxi_support")]]
+ ),
+ )
+ elif prev_match:
+ curr_page = int(prev_match.group(1))
+ await query.message.edit_caption(HELP_STRINGS,
+ reply_markup=InlineKeyboardMarkup(
+ paginate_modules(curr_page - 1, HELPABLE, "help")
+ ),
+ )
+ elif next_match:
+ next_page = int(next_match.group(1))
+ await query.message.edit_caption(HELP_STRINGS,
+ reply_markup=InlineKeyboardMarkup(
+ paginate_modules(next_page + 1, HELPABLE, "help")
+ ),
+ )
+ elif back_match:
+ await query.message.edit_caption(HELP_STRINGS,
+ reply_markup=InlineKeyboardMarkup(
+ paginate_modules(0, HELPABLE, "help")
+ ),
+ )
+ await query.answer()
+ except Exception:
+ pass
+
+@pbot.on_callback_query(filters.regex(r"^queennoxi_"))
+async def QueenNoxi_about_callback(client, query: CallbackQuery):
+ if query.data == "queennoxi_":
+ uptime = get_readable_time((time.time() - StartTime))
+ users = await get_served_users()
+ chats = await get_served_chats()
+ await query.message.edit_caption(
+ f"*ʜᴇʏ,*🥀\n *ᴛʜɪs ɪs {BOT_NAME}*"
+ "\n*ᴀ ᴘᴏᴡᴇʀꜰᴜʟ ɢʀᴏᴜᴘ ᴍᴀɴᴀɢᴇᴍᴇɴᴛ ➕ ᴍᴜsɪᴄ ᴍᴀɴᴀɢᴇᴍᴇɴᴛ ʙᴜɪʟᴛ ᴛᴏ ʜᴇʟᴘ ʏᴏᴜ ᴍᴀɴᴀɢᴇ ʏᴏᴜʀ ɢʀᴏᴜᴘ ᴇᴀꜱɪʟʏ ᴀɴᴅ ᴛᴏ ᴘʀᴏᴛᴇᴄᴛ ʏᴏᴜʀ ɢʀᴏᴜᴘ ꜰʀᴏᴍ ꜱᴄᴀᴍᴍᴇʀꜱ ᴀɴᴅ ꜱᴘᴀᴍᴍᴇʀꜱ.*"
+ "\n*ᴡʀɪᴛᴛᴇɴ ɪɴ ᴩʏᴛʜᴏɴ ᴡɪᴛʜ sǫʟᴀʟᴄʜᴇᴍʏ ᴀɴᴅ ᴍᴏɴɢᴏᴅʙ ᴀs ᴅᴀᴛᴀʙᴀsᴇ.*"
+ "\n\n────────────────────"
+ f"\n*➻ ᴜᴩᴛɪᴍᴇ »* {uptime}"
+ f"\n* ᴜꜱᴇʀꜱ : {len(users)} "
+ f"\n* ᴄʜᴀᴛs : {len(chats)} "
+ "\n────────────────────"
+ "\n➲ ɪ ᴄᴀɴ ʀᴇꜱᴛʀɪᴄᴛ ᴜꜱᴇʀꜱ."
+ "\n➲ ɪ ʜᴀᴠᴇ ᴀɴ ᴀᴅᴠᴀɴᴄᴇᴅ ᴀɴᴛɪ-ꜰʟᴏᴏᴅ ꜱʏꜱᴛᴇᴍ."
+ "\n➲ ɪ ᴄᴀɴ ɢʀᴇᴇᴛ ᴜꜱᴇʀꜱ ᴡɪᴛʜ ᴄᴜꜱᴛᴏᴍɪᴢᴀʙʟᴇ ᴡᴇʟᴄᴏᴍᴇ ᴍᴇꜱꜱᴀɢᴇꜱ ᴀɴᴅ ᴇᴠᴇɴ ꜱᴇᴛ ᴀ ɢʀᴏᴜᴘ'ꜱ ʀᴜʟᴇꜱ."
+ f"\n\n➻ ᴄʟɪᴄᴋ ᴏɴ ᴛʜᴇ ʙᴜᴛᴛᴏɴs ɢɪᴠᴇɴ ʙᴇʟᴏᴡ ғᴏʀ ɢᴇᴛᴛɪɴɢ ʙᴀsɪᴄ ʜᴇʟᴩ ᴀɴᴅ ɪɴғᴏ ᴀʙᴏᴜᴛ {BOT_NAME}.",
+ reply_markup=InlineKeyboardMarkup(
+ [
+ [
+ InlineKeyboardButton(text="🏡", callback_data="queennoxi_back"),
+ InlineKeyboardButton(text="💳", callback_data="source_"),
+ InlineKeyboardButton(text="🧑💻", url=f"tg://user?id={OWNER_ID}"),
+ InlineKeyboardButton(text="🖥️", callback_data="Main_help"),
+ ],
+ [
+ InlineKeyboardButton(text="🚩sᴜᴩᴩᴏʀᴛ", callback_data="queennoxi_support"),
+ InlineKeyboardButton(text="ᴄᴏᴍᴍᴀɴᴅs 💁", callback_data="Main_help"),
+ ],
+ [
+ InlineKeyboardButton(text="👨💻ᴅᴇᴠᴇʟᴏᴩᴇʀ", url=f"tg://user?id={OWNER_ID}"),
+ ],
+ ]
+ ),
+ )
+ elif query.data == "queennoxi_back":
+ await query.message.edit_caption(
+ PM_START_TEXT.format(query.from_user.first_name if query.from_user else "User", BOT_NAME),
+ reply_markup=InlineKeyboardMarkup(buttons),
+ )
+ elif query.data == "queennoxi_support":
+ await query.message.edit_caption(
+ "ʜᴇʏ 👋\nᴄʟɪᴄᴋ sᴜᴩᴩᴏʀᴛ ᴛᴏ ᴊᴏɪɴ sᴜᴩᴩᴏʀᴛ ɢʀᴏᴜᴩ.",
+ reply_markup=InlineKeyboardMarkup([
+ [InlineKeyboardButton("sᴜᴩᴩᴏʀᴛ", url=SUPPORT_CHAT_URL)],
+ [InlineKeyboardButton("ʙᴀᴄᴋ", callback_data="queennoxi_back")]
+ ])
+ )
+ await query.answer()
+
+@pbot.on_callback_query(filters.regex(r"^Main_help"))
+async def Main_help_callback(client, query: CallbackQuery):
+ await query.message.edit_caption(
+ HELP_STRINGS,
+ reply_markup=InlineKeyboardMarkup(paginate_modules(0, HELPABLE, "help"))
+ )
+ await query.answer()
+
+@pbot.on_callback_query(filters.regex(r"^source_"))
+async def Source_about_callback(client, query: CallbackQuery):
+ if query.data == "source_":
+ await query.message.edit_caption(
+ f"*ʜᴇʏ,\n ᴛʜɪs ɪs {BOT_NAME}*\n\n"
+ "*ʜᴇʀᴇ ɪs ᴍʏ sᴏᴜʀᴄᴇ ᴄᴏᴅᴇ :* [ɢɪᴛʜᴜʙ](https://github.com/AlexaInc/queen-noxi)",
+ reply_markup=InlineKeyboardMarkup(
+ [
+ [InlineKeyboardButton(text="sᴏᴜʀᴄᴇ", url="https://github.com/AlexaInc/queen-noxi")],
+ [
+ InlineKeyboardButton(text="🏡", callback_data="queennoxi_back"),
+ InlineKeyboardButton(text="🛡️", callback_data="queennoxi_"),
+ InlineKeyboardButton(text="💳", callback_data="source_"),
+ InlineKeyboardButton(text="🖥️", callback_data="Main_help"),
+ ],
+ [InlineKeyboardButton(text="◁", callback_data="source_back")]
+ ]
+ ),
+ )
+ elif query.data == "source_back":
+ await query.message.edit_caption(
+ PM_START_TEXT.format(query.from_user.first_name if query.from_user else "User", BOT_NAME),
+ reply_markup=InlineKeyboardMarkup(buttons),
+ )
+ await query.answer()
+
+@pbot.on_callback_query(filters.regex(r"^owner_"))
+async def owner_callback(client, query: CallbackQuery):
+ data = query.data
+ if data == "owner_main":
+ from QueenNoxi import OWNER_IDS
+ owner_buttons = []
+ for oid in OWNER_IDS:
+ try:
+ user = await client.get_users(oid)
+ name = user.first_name
+ # Use username if available for better redirection
+ if user.username:
+ url = f"https://t.me/{user.username}"
+ else:
+ # Fallback to tg://user?id= which works in most modern clients if used correctly
+ url = f"tg://user?id={oid}"
+ owner_buttons.append([InlineKeyboardButton(text=f"👤 {name}", url=url)])
+ except Exception:
+ owner_buttons.append([InlineKeyboardButton(text=f"👤 Owner {oid}", url=f"tg://user?id={oid}")])
+
+ if not owner_buttons:
+ owner_buttons.append([InlineKeyboardButton(text="👤 Main Owner", url=f"tg://user?id={OWNER_ID}")])
+
+ owner_buttons.append([InlineKeyboardButton(text="◁ Back", callback_data="queennoxi_back")])
+
+ await query.message.edit_caption(
+ "✨ **Owner Selection Menu**\n\nChoose an owner profile to view:",
+ reply_markup=InlineKeyboardMarkup(owner_buttons),
+ )
+ await query.answer()
+
+
+@pbot.on_callback_query(filters.regex(r"^Music_"))
+async def music_callback(client, query: CallbackQuery):
+ data = query.data
+ NAV = [
+ InlineKeyboardButton("🏡", callback_data="queennoxi_back"),
+ InlineKeyboardButton("🛡️", callback_data="queennoxi_"),
+ InlineKeyboardButton("💳", callback_data="source_"),
+ InlineKeyboardButton("🖥️", callback_data="Main_help"),
+ ]
+ if data == "Music_":
+ await query.message.edit_caption(
+ "ʜᴇʀᴇ ɪꜱ ʜᴇʟᴘ ᴍᴇɴᴜ ꜰᴏʀ ᴍᴜꜱɪᴄ",
+ reply_markup=InlineKeyboardMarkup([
+ NAV,
+ [InlineKeyboardButton("⍟ ᴀᴅᴍɪɴ ⍟", callback_data="Music_admin"),
+ InlineKeyboardButton("⍟ ᴘʟᴀʏ ⍟", callback_data="Music_play")],
+ [InlineKeyboardButton("⍟ ʙᴏᴛ ⍟", callback_data="Music_bot"),
+ InlineKeyboardButton("⍟ ᴇxᴛʀᴀ ⍟", callback_data="Music_extra")],
+ [InlineKeyboardButton("• ʙᴀᴄᴋ •", callback_data="Main_help")],
+ ]),
+ )
+ elif data == "Music_admin":
+ await query.message.edit_caption(
+ "*» ᴀᴅᴍɪɴ ᴄᴏᴍᴍᴀɴᴅꜱ «*\n"
+ "/pause – ᴩᴀᴜsᴇ stream\n/resume – ʀᴇsᴜᴍᴇ\n"
+ "/skip – sᴋɪᴩ ᴄᴜʀʀᴇɴᴛ\n/end,/stop – sᴛᴏᴩ & ᴄʟᴇᴀʀ ǫᴜᴇᴜᴇ\n"
+ "/player – ɪɴᴛᴇʀᴀᴄᴛɪᴠᴇ ᴩᴀɴᴇʟ\n/queue – sʜᴏᴡ ǫᴜᴇᴜᴇ",
+ reply_markup=InlineKeyboardMarkup([[
+ InlineKeyboardButton("ʙᴀᴄᴋ", callback_data="Music_"),
+ InlineKeyboardButton("sᴜᴩᴩᴏʀᴛ", url=SUPPORT_CHAT_URL),
+ ]]),
+ )
+ elif data == "Music_play":
+ await query.message.edit_caption(
+ "*» ᴘʟᴀʏ ᴄᴏᴍᴍᴀɴᴅꜱ «*\n"
+ "/play, /vplay, /cplay – ᴩʟᴀʏ ᴀᴜᴅɪᴏ/ᴠɪᴅᴇᴏ\n"
+ "/playforce – ғᴏʀᴄᴇ ᴩʟᴀʏ\n"
+ "/channelplay [id/disable] – ᴄʜᴀɴɴᴇʟ ᴩʟᴀʏ",
+ reply_markup=InlineKeyboardMarkup([[
+ InlineKeyboardButton("• ʙᴀᴄᴋ •", callback_data="Music_"),
+ InlineKeyboardButton("sᴜᴩᴩᴏʀᴛ", url=SUPPORT_CHAT_URL),
+ ]]),
+ )
+ elif data == "Music_bot":
+ await query.message.edit_caption(
+ "*» ʙᴏᴛ ᴄᴏᴍᴍᴀɴᴅꜱ «*\n"
+ "/stats – ɢʟᴏʙᴀʟ sᴛᴀᴛs\n/sudolist – sᴜᴅᴏ ᴜsᴇʀs\n"
+ "/lyrics [ɴᴀᴍᴇ] – ꜰᴇᴛᴄʜ ʟʏʀɪᴄs\n/song [ɴᴀᴍᴇ/ᴜʀʟ] – ᴅᴏᴡᴀɴʟᴏᴀᴅ",
+ reply_markup=InlineKeyboardMarkup([[
+ InlineKeyboardButton("ʙᴀᴄᴋ", callback_data="Music_"),
+ InlineKeyboardButton("sᴜᴩᴩᴏʀᴛ", callback_data="queennoxi_support"),
+ ]]),
+ )
+ elif data == "Music_extra":
+ await query.message.edit_caption(
+ "*» ᴇxᴛʀᴀ ᴄᴏᴍᴍᴀɴᴅꜱ «*\n"
+ "/mstart – sᴛᴀʀᴛ ᴍᴜsɪᴄ ʙᴏᴛ\n/mhelp – ᴍᴜsɪᴄ ʜᴇʟᴩ\n",
+ reply_markup=InlineKeyboardMarkup([[
+ InlineKeyboardButton("• sᴜᴩᴩᴏʀᴛ •", url=SUPPORT_CHAT_URL)
+ ]])
+ )
+ await query.answer()
+
+async def main():
+ import QueenNoxi
+ from aiohttp import ClientSession
+ QueenNoxi.aiohttpsession = ClientSession()
+
+ # --- Pyrogram Start ---
+ try:
+ await pbot.start()
+ LOGGER.info("[Pyrogram] Client started.")
+
+ # Export and save session string for persistence
+ try:
+ ss = await pbot.export_session_string()
+ save_session(BOT_ID, ss)
+ LOGGER.info("[Pyrogram] Saved session string for persistence.")
+ except Exception as e:
+ LOGGER.warning(f"Could not export/save session string: {e}")
+
+ await asyncio.sleep(2)
+ try:
+ if hasattr(pbot, 'delete_webhook'):
+ await pbot.delete_webhook(drop_pending_updates=True)
+ elif hasattr(pbot, 'delete_web_hook'):
+ await pbot.delete_web_hook(drop_pending_updates=True)
+ LOGGER.info("[Pyrogram] Webhook cleared and queue flushed.")
+ except FloodWait as e:
+ LOGGER.warning(f"[Pyrogram] FloodWait while clearing webhook: {e.value}s")
+ except FloodWait as e:
+ LOGGER.error(f"[Pyrogram] CRITICAL FloodWait on start: {e.value}s. Sleeping...")
+ await asyncio.sleep(e.value)
+ except Exception as e:
+ LOGGER.error(f"[Pyrogram] Failed to start client: {e}")
+
+ # --- Telethon Start ---
+ try:
+ await telethn.start(bot_token=TOKEN)
+ LOGGER.info("[Telethon] Client started.")
+ except TlFloodWait as e:
+ LOGGER.warning(f"[Telethon] FloodWait on start: {e.seconds}s.")
+ except Exception as e:
+ LOGGER.error(f"[Telethon] Failed to start client: {e}")
+
+ # --- Handoff ---
+ if pbot.is_connected:
+ me = await pbot.get_me()
+ LOGGER.info(f"[INFO] Bot running as @{me.username} | {me.first_name}")
+
+ if SUPPORT_CHAT and not SUPPORT_CHAT.startswith("http"):
+ try:
+ target = SUPPORT_CHAT if SUPPORT_CHAT.startswith("-100") else f"@{SUPPORT_CHAT}"
+ await pbot.send_photo(
+ target,
+ photo=START_IMG,
+ caption=f"✨ {BOT_NAME} ɪs ᴀʟɪᴠᴇ ʙᴀʙʏ.\n\n"
+ f"**ᴩʏᴛʜᴏɴ ᴠᴇʀsɪᴏɴ:** `{y()}`\n"
+ f"**ᴩʏʀᴏɢʀᴀᴍ ᴠᴇʀsɪᴏɴ:** `{pyrover}`\n",
+ reply_markup=InlineKeyboardMarkup([[
+ InlineKeyboardButton("➕ Aᴅᴅ ᴍᴇ ᴛᴏ ʏᴏᴜʀ ᴄʜᴀᴛ ➕", url=f"https://t.me/{me.username}?startgroup=true")
+ ]]),
+ )
+ except Exception as e:
+ LOGGER.warning(f"Could not announce to @{SUPPORT_CHAT}: {e}")
+ else:
+ LOGGER.error("[CRITICAL] Pyrogram client is NOT connected. Features will be disabled.")
+
+ LOGGER.info(f"Successfully loaded {len(ALL_MODULES)} modules.")
+ LOGGER.info("Bot is running. Press Ctrl+C to stop.")
+
+ await idle()
+
+ if pbot.is_connected:
+ await pbot.stop()
+ if telethn.is_connected():
+ await telethn.disconnect()
+ await QueenNoxi.aiohttpsession.close()
+
+if __name__ == "__main__":
+ asyncio.run(main())
diff --git a/QueenNoxi/brain/data/brain.json b/QueenNoxi/brain/data/brain.json
new file mode 100644
index 0000000000000000000000000000000000000000..c6e081389a34e6a17a906081d499524c07e6198c
--- /dev/null
+++ b/QueenNoxi/brain/data/brain.json
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:814980d5c0908529060fe6c7a74f95ab9482d4cd538f3ab050c5f454d832642d
+size 12969712
diff --git a/QueenNoxi/brain/data/dictionary.json b/QueenNoxi/brain/data/dictionary.json
new file mode 100644
index 0000000000000000000000000000000000000000..8390202967efbf89274f196a490b48c15c695763
--- /dev/null
+++ b/QueenNoxi/brain/data/dictionary.json
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:bc1b3253f324cb5ce968c269252e7cf7d383026fbd50aac1964c850bf3d165c0
+size 8714062
diff --git a/QueenNoxi/brain/data/persona.json b/QueenNoxi/brain/data/persona.json
new file mode 100644
index 0000000000000000000000000000000000000000..e6c37e13b276aff14b63996107105dcec4760a5e
--- /dev/null
+++ b/QueenNoxi/brain/data/persona.json
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:fa9fc7b188e0263b963b74e31a8e9c751f45282cb87a17e15cfbcbb58830f32b
+size 869
diff --git a/QueenNoxi/brain/data/phonetic_map.json b/QueenNoxi/brain/data/phonetic_map.json
new file mode 100644
index 0000000000000000000000000000000000000000..fae15f158adac21a8560370d0123e78133232b32
--- /dev/null
+++ b/QueenNoxi/brain/data/phonetic_map.json
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:b9d3dbbadbfca3777d5f8e7269c1e691a594bb91473c324e572801321534655e
+size 21295805
diff --git a/QueenNoxi/brain/data/slang.json b/QueenNoxi/brain/data/slang.json
new file mode 100644
index 0000000000000000000000000000000000000000..01a5fad86a390d739de6a11784fba5d25dd266b6
--- /dev/null
+++ b/QueenNoxi/brain/data/slang.json
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:8bc513452216e7f8a6c13324f09734de6d0973d610fc83b8aae52e7351898aa4
+size 1248
diff --git a/QueenNoxi/brain/data/users.json b/QueenNoxi/brain/data/users.json
new file mode 100644
index 0000000000000000000000000000000000000000..b8204697de01d606647cc08ae9070796daa5874d
--- /dev/null
+++ b/QueenNoxi/brain/data/users.json
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:99245b13a40b590c2903a1b9ace0a07e55166ce6179a72fb4e6b6023c161c474
+size 26626
diff --git a/QueenNoxi/brain/database/anon.json b/QueenNoxi/brain/database/anon.json
new file mode 100644
index 0000000000000000000000000000000000000000..9d2ad3161f78e73f857ca9fbb60aad4cea466aca
--- /dev/null
+++ b/QueenNoxi/brain/database/anon.json
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:e7cb05c9ba971cf4cfe53cda4d0bd302d1c1254b4f2890ccd12828d09c850aa9
+size 1456
diff --git a/QueenNoxi/brain/index.js b/QueenNoxi/brain/index.js
new file mode 100644
index 0000000000000000000000000000000000000000..c2af082d09c42dc6fdbab808a09bdf56d7fdd7a8
--- /dev/null
+++ b/QueenNoxi/brain/index.js
@@ -0,0 +1,280 @@
+const brain = require('./src/brain');
+const processor = require('./src/processor');
+const UserMemory = require('./src/memory');
+const GroupSession = require('./src/group');
+const nlu = require('./src/nlu');
+const ContextWindow = require('./src/context');
+const Dictionary = require('./src/dictionary');
+const path = require('path');
+
+class SinhalaHumanoid {
+ /**
+ * @param {Object} options
+ * @param {string} options.style - Chat style: 'chill' | 'professional' | 'sexy' | 'hot' | 'auto'
+ * @param {string} options.name - Bot's name (e.g. 'Kasun')
+ * @param {string} options.birthday - Bot's birthday (e.g. '1998-05-15')
+ * @param {string} options.residence - Bot's home city (e.g. 'Colombo')
+ * @param {string} options.job - Bot's job (e.g. 'Web Developer')
+ * @param {string} options.gender - Bot's gender ('Male'|'Female'|'Other')
+ * @param {string} options.usersDb - Path to users.json (optional)
+ */
+ constructor(options = {}) {
+ const personaFields = {};
+ if (options.name) personaFields.name = options.name;
+ if (options.birthday) personaFields.birthday = options.birthday;
+ if (options.residence) personaFields.residence = options.residence;
+ if (options.job) personaFields.job = options.job;
+ if (options.gender) personaFields.gender = options.gender;
+ if (options.style) personaFields.style = options.style;
+ if (options.persona) Object.assign(personaFields, options.persona);
+
+ if (Object.keys(personaFields).length > 0) brain.setPersona(personaFields);
+
+ this.autoStyle = (options.style === 'auto');
+
+ // Priority: options.mongoUri -> process.env.CHATDB_URL -> options.usersDb -> default
+ this.mongoUri = options.mongoUri || process.env.CHATDB_URL || null;
+ this.usersDb = options.usersDb || path.join(process.cwd(), 'database');
+
+ const memoryPath = this.mongoUri || this.usersDb;
+ this.userMemory = new UserMemory(memoryPath);
+
+ const dictPath = options.dictionaryDb || path.join(__dirname, 'data', 'dictionary.json');
+ this.dictionary = new Dictionary(dictPath);
+
+ this.groups = {};
+ this.contexts = {};
+ }
+
+
+ async init() {
+ await this.userMemory.init();
+ return this;
+ }
+
+ _getContext(userId) {
+ if (!this.contexts[userId]) this.contexts[userId] = new ContextWindow();
+ return this.contexts[userId];
+ }
+
+ _buildResponse(message, userId, ctx) {
+ const user = this.userMemory.getUser(userId);
+ const analysis = nlu.analyze(message);
+ ctx.addUserTurn(message, analysis);
+
+ const style = this.autoStyle
+ ? analysis.detectedStyle
+ : ((brain.persona && brain.persona.style) || 'chill');
+
+ const prevStyle = brain.persona.style;
+ if (this.autoStyle) brain.setPersona({ style });
+
+ const extracted = this.userMemory.extractFromMessage(userId, message);
+ if (extracted.name) {
+ const resp = `Suba na ${user.name}! Oyatama ingathin ganna.`;
+ ctx.addBotTurn(resp);
+ return resp;
+ }
+
+ const memQ = this.userMemory.memoryQuery(userId, message);
+ if (memQ) { ctx.addBotTurn(memQ); return memQ; }
+
+ const dictWord = this.dictionary.isDictionaryQuery(message);
+ if (dictWord) {
+ const entry = this.dictionary.lookup(dictWord);
+ if (entry) {
+ const resp = this.dictionary.formatResponse(dictWord, entry);
+ if (resp) { ctx.addBotTurn(resp); return resp; }
+ } else {
+ const notFound = {
+ chill: `Aiyo sry machan, "${dictWord}" kiyana eka gana mama danne na. Aluth vacanayak da?`,
+ professional: `Sathutu wenawa kiyanna, habai "${dictWord}" kiyana vacanaya mage dictionary eke na.`,
+ sexy: `hmmm "${dictWord}"... eka amuthu vacanayak ne? mama danne na eka gana...`,
+ hot: `"${dictWord}"? eka gana passe kiyannam. dan kiyanna ba.`
+ };
+ const resp = notFound[style] || notFound['chill'];
+ ctx.addBotTurn(resp);
+ return resp;
+ }
+ }
+
+ if (analysis.isGreeting && user.name) {
+ const timeSinceSeen = user.lastSeen ? Date.now() - user.lastSeen : Infinity;
+ if (timeSinceSeen > 5 * 60 * 1000) {
+ const greet = this.userMemory.greet(userId);
+ if (greet) { ctx.addBotTurn(greet); return greet; }
+ }
+ }
+
+ if (analysis.isFarewell) {
+ const byes = {
+ chill: ['ok yanawa machan, hodatama yanawa', 'bye bye, posuwa enna', 'hodatama yanawa, bye!'],
+ professional: ['Bye! Hoda dine ekak waewa.'],
+ sexy: ['aww bye baby, miss karannawa', 'bye, jld ohh laa'],
+ hot: ['ok bye, jld enna ha', 'missing already']
+ };
+ const pool = byes[style] || byes['chill'];
+ const resp = pool[Math.floor(Math.random() * pool.length)];
+ ctx.addBotTurn(resp);
+ return resp;
+ }
+
+ const tonePrefix = nlu.toneReaction(analysis.tone);
+ const parts = [];
+
+ if (analysis.isLong) {
+ for (const topic of analysis.topics.slice(0, 2)) {
+ const topicResp = nlu.getTopicResponse(topic, style);
+ if (topicResp) parts.push(topicResp);
+ }
+ if (!parts.length) {
+ for (const sentence of analysis.sentences.slice(0, 2)) {
+ const r = brain.respond(sentence, { user });
+ if (r) parts.push(brain.fillTemplate(r, { user }));
+ }
+ }
+ }
+
+ if (!parts.length) {
+ for (const sentence of analysis.sentences) {
+ // 1. Brain match (most specific knowledge)
+ const r = brain.respond(sentence, { user });
+ if (r) {
+ parts.push(brain.fillTemplate(r, { user }));
+ break;
+ }
+
+ // 2. Fallback to general topics
+ const topicHit = analysis.topics.find(t => nlu.getTopicResponse(t, style));
+ if (topicHit) {
+ parts.push(nlu.getTopicResponse(topicHit, style));
+ break;
+ }
+ }
+ }
+
+ if (ctx.isAnsweringBotQuestion() && !analysis.isQuestion && parts.length) {
+ const acks = ['ahh ok ok', 'oo arageina', 'ah supa', 'ok ok gotcha'];
+ parts.unshift(acks[Math.floor(Math.random() * acks.length)]);
+ }
+
+ let finalParts = parts.filter(Boolean);
+ if (tonePrefix && finalParts.length) {
+ finalParts = [tonePrefix, ...finalParts];
+ }
+
+ if (!finalParts.length) {
+ finalParts = [brain.getFallback()];
+ }
+
+ if (Math.random() < 0.4 && !analysis.isQuestion) {
+ const followUps = {
+ chill: ['ekath?', 'oya kohomada?', 'moko kiyanne?', 'oya thiyenawada?'],
+ professional: ['Oba kohomada?', 'Meken help one da?'],
+ sexy: ['oya mokada karanne tawa?', 'oya miss una neda?'],
+ hot: ['oya kiyanne?', 'enna enna']
+ };
+ const pool = followUps[style] || followUps['chill'];
+ finalParts.push(pool[Math.floor(Math.random() * pool.length)]);
+ }
+
+ const response = finalParts.join(' ');
+ ctx.addBotTurn(response);
+ if (this.autoStyle) brain.setPersona({ style: prevStyle });
+ return response;
+ }
+
+ getResponse(message, userId = 'anon') {
+ this.userMemory.addHistory(userId, message, 'user');
+ const ctx = this._getContext(userId);
+ const response = this._buildResponse(message, userId, ctx);
+ this.userMemory.addHistory(userId, response, 'bot');
+ return response;
+ }
+
+ async getGroupResponse(message, userId, groupId = 'default') {
+ if (!this.groups[groupId]) {
+ this.groups[groupId] = new GroupSession({
+ groupId,
+ memory: this.userMemory,
+ mongoUri: this.mongoUri,
+ usersDb: this.usersDb
+ });
+ await this.groups[groupId].init();
+ }
+
+ const group = this.groups[groupId];
+ await group.addMessage(userId, message, userId);
+
+ const memQuery = this.userMemory.memoryQuery(userId, message);
+ if (memQuery) {
+ await group.addMessage('bot', memQuery, 'bot');
+ return memQuery;
+ }
+
+ this.userMemory.extractFromMessage(userId, message);
+
+ const botName = (brain.persona && brain.persona.name) || 'Kasun';
+ const analysis = nlu.analyze(message);
+ const addressed = message.toLowerCase().includes(botName.toLowerCase())
+ || message.toLowerCase().includes('bot')
+ || analysis.isGreeting;
+ const randomReply = Math.random() < 0.3;
+
+ if (!addressed && !randomReply) return null;
+
+ const ctx = this._getContext(`${groupId}:${userId}`);
+ const user = this.userMemory.getUser(userId);
+ const response = this._buildResponse(message, userId, ctx);
+
+ await group.addMessage('bot', response, 'bot');
+ return response;
+ }
+
+ /**
+ * Permanent Learning: Add a new pattern to the brain and save to disk.
+ */
+ async learn(input, response, style = 'chill') {
+ const prevStyle = (brain.persona && brain.persona.style) || 'chill';
+ brain.setPersona({ style });
+ brain.learn(input, response, true);
+ brain.setPersona({ style: prevStyle });
+ return true;
+ }
+
+ trainFromGroup(groupId) {
+ const group = this.groups[groupId];
+ if (group) group.autoTrainFromGroup(brain);
+ }
+
+ trainData(data, style) {
+ const Trainer = require('./src/trainer');
+ return Trainer.trainData(data, style);
+ }
+
+ autoTrain(history) {
+ for (let i = 0; i < history.length - 1; i++) {
+ const curr = history[i], next = history[i + 1];
+ if (curr.sender !== next.sender) brain.learn(curr.text, next.text);
+ }
+ }
+
+ setStyle(style) { brain.setPersona({ style }); }
+ setPersona(details) { brain.setPersona(details); }
+ getUser(userId) { return this.userMemory.getUser(userId); }
+ setUserData(u, f, v) { this.userMemory.setUserData(u, f, v); }
+ setUserFact(u, k, v) { this.userMemory.setFact(u, k, v); }
+ getUserFact(u, k) { return this.userMemory.getFact(u, k); }
+ resetContext(userId) { if (this.contexts[userId]) this.contexts[userId].reset(); }
+ analyze(message) { return nlu.analyze(message); }
+ detectLanguage(text) {
+ return /[\u0D80-\u0DFF]/.test(text) ? 'sinhala' : 'singlish';
+ }
+}
+
+const createBot = (options) => new SinhalaHumanoid(options);
+
+module.exports = {
+ SinhalaHumanoid,
+ createBot
+};
diff --git a/QueenNoxi/brain/query.js b/QueenNoxi/brain/query.js
new file mode 100644
index 0000000000000000000000000000000000000000..9787245c340fe5ba01534ff0c4e8d7ec218be404
--- /dev/null
+++ b/QueenNoxi/brain/query.js
@@ -0,0 +1,62 @@
+const { createBot } = require('./index.js');
+const path = require('path');
+const fs = require('fs');
+
+async function main() {
+ const args = process.argv.slice(2);
+ const command = args[0] || 'query';
+
+ try {
+ const databasePath = path.join(__dirname, 'database');
+ if (!fs.existsSync(databasePath)) {
+ fs.mkdirSync(databasePath, { recursive: true });
+ }
+
+ const bot = createBot({
+ name: 'Queen Noxi',
+ style: 'auto',
+ birthday: '2004-01-21',
+ gender: 'Female',
+ residence: 'Sri Lanka',
+ job: 'Student',
+ mongoUri: process.env.CHATDB_URL || null,
+ usersDb: databasePath,
+ dictionaryDb: path.join(__dirname, 'data', 'dictionary.json')
+ });
+
+ await bot.init();
+
+ if (command === 'learn') {
+ const input = args[1];
+ const response = args[2];
+ if (!input || !response) {
+ process.stderr.write('Usage: learn ');
+ process.exit(1);
+ }
+ await bot.learn(input, response);
+ process.stdout.write('LEARNED');
+ } else {
+ // query command
+ const text = args[1];
+ const userId = args[2] || 'anon';
+ const groupId = args[3] || null;
+
+ if (!text) {
+ process.exit(1);
+ }
+
+ let response;
+ if (groupId) {
+ response = await bot.getGroupResponse(text, userId, groupId);
+ } else {
+ response = bot.getResponse(text, userId);
+ }
+ process.stdout.write(response || '');
+ }
+ } catch (err) {
+ process.stderr.write(err.message || String(err));
+ process.exit(1);
+ }
+}
+
+main();
diff --git a/QueenNoxi/brain/src/brain.js b/QueenNoxi/brain/src/brain.js
new file mode 100644
index 0000000000000000000000000000000000000000..8db1dc26eb378032d859b25c6ccb15553aad8584
--- /dev/null
+++ b/QueenNoxi/brain/src/brain.js
@@ -0,0 +1,150 @@
+const fs = require('fs');
+const path = require('path');
+const processor = require('./processor');
+
+class Brain {
+ constructor() {
+ this.memory = {};
+ this.persona = {};
+ this.loadData();
+ }
+
+ loadData() {
+ try {
+ const brainPath = path.join(__dirname, '..', 'data', 'brain.json');
+ if (fs.existsSync(brainPath)) this.memory = JSON.parse(fs.readFileSync(brainPath, 'utf8'));
+
+ const personaPath = path.join(__dirname, '..', 'data', 'persona.json');
+ if (fs.existsSync(personaPath)) this.persona = JSON.parse(fs.readFileSync(personaPath, 'utf8'));
+ } catch (e) { console.error('Error loading brain data:', e); }
+ }
+
+ saveData() {
+ try {
+ const brainPath = path.join(__dirname, '..', 'data', 'brain.json');
+ fs.writeFileSync(brainPath, JSON.stringify(this.memory, null, 2));
+ } catch (e) { console.error('Error saving brain data:', e); }
+ }
+
+ setPersona(data) {
+ this.persona = { ...this.persona, ...data };
+ }
+
+ /**
+ * Core respond logic. Accepts optional context (userId, userMemory, groupSession)
+ */
+ respond(inputText, context = {}) {
+ const processedInput = processor.process(inputText);
+ if (!processedInput) return this.getFallback();
+
+ // 1. Direct match
+ if (this.memory[processedInput]) {
+ return this.fillTemplate(this.pickResponse(this.memory[processedInput]), context);
+ }
+
+ // 2. Persona questions (Identity should take precedence over fuzzy chat)
+ const personaResp = this.checkPersonaQuestions(processedInput, inputText);
+ if (personaResp) return this.fillTemplate(personaResp, context);
+
+ // 3. Fuzzy match
+ let bestMatch = null;
+ let highestScore = 0;
+ for (const [pattern, data] of Object.entries(this.memory)) {
+ const score = processor.calculateSimilarity(processedInput, pattern);
+ if (score > highestScore && score > 0.65) {
+ highestScore = score;
+ bestMatch = data;
+ }
+ }
+ if (bestMatch) {
+ return this.fillTemplate(this.pickResponse(bestMatch), context);
+ }
+
+ return this.getFallback();
+ }
+
+ pickResponse(data) {
+ const style = this.persona.style || 'chill';
+ const pool = data[style] || data['chill'] || data['default'] || data;
+ if (Array.isArray(pool)) return pool[Math.floor(Math.random() * pool.length)];
+ return String(pool);
+ }
+
+ /**
+ * Fill {name}, {job}, {birthday}, {age}, {residence}, {user_name} etc.
+ */
+ fillTemplate(text, context = {}) {
+ if (!text) return this.getFallback();
+ const p = this.persona;
+ const u = context.user || {};
+ const now = new Date();
+ const birthYear = p.birthday ? new Date(p.birthday).getFullYear() : null;
+
+ return text
+ .replace(/\{name\}/g, p.name || 'Kasun')
+ .replace(/\{birthday\}/g, p.birthday || '?')
+ .replace(/\{job\}/g, p.job || 'web dev')
+ .replace(/\{residence\}/g, p.residence || 'Colombo')
+ .replace(/\{age\}/g, birthYear ? (now.getFullYear() - birthYear) : '?')
+ .replace(/\{user_name\}/g, u.name || 'machan')
+ .replace(/\{user_location\}/g, u.location || 'koheda?')
+ .replace(/\{user_job\}/g, u.job || '?');
+ }
+
+ checkPersonaQuestions(processedInput, rawInput = '') {
+ const text = processedInput.toLowerCase();
+ const raw = rawInput.toLowerCase();
+
+ // Helper: check either raw or processed text
+ const has = (...kws) => kws.some(k => text.includes(k) || raw.includes(k));
+
+ // Priority order matters — check most specific first
+ if (has('oyage gana', 'about you', 'introduce yourself', 'tell me about'))
+ return 'mama {name}. {job} kenek. {residence} wala inne.';
+
+ // Name — must have name-specific trigger word
+ if (has('nama', 'name', 'kauda oya', 'oyage kauda', 'oyage nam', 'oyage name'))
+ return 'mage nama {name} machan.';
+
+ // Age / Birthday
+ if (has('bday', 'birthday', 'ipaduna', 'wayasa', 'age'))
+ return 'mama {age} wiye inne. bday eka {birthday}.';
+
+ // Residence — must be explicit about location
+ if (has('koheda inne', 'live in', 'koheda', 'residence', 'wasi'))
+ return 'mama {residence} wala inne machan.';
+
+ // Job — only if there's a clear job-asking keyword, NOT general "karanne"
+ if (has('oyage job', 'what is your job', 'weda karanne mokakda', 'job eka', 'profession', 'oyage weda'))
+ return 'mama {job} kenek machan.';
+
+ return null;
+ }
+
+ getFallback() {
+ const style = this.persona.style || 'chill';
+ const fallbacks = (this.persona.fallbacks && this.persona.fallbacks[style])
+ || (this.persona.fallbacks && this.persona.fallbacks['chill'])
+ || ['moko kiyanne?', 'poddak hitapan', 'kiyanna aney'];
+ return fallbacks[Math.floor(Math.random() * fallbacks.length)];
+ }
+
+ learn(input, response, shouldSave = true) {
+ const processedInput = processor.process(input);
+ if (!processedInput || !response) return;
+
+ const style = this.persona.style || 'chill';
+ if (!this.memory[processedInput]) {
+ this.memory[processedInput] = {};
+ }
+ if (!this.memory[processedInput][style]) {
+ this.memory[processedInput][style] = [];
+ }
+ if (!this.memory[processedInput][style].includes(response)) {
+ this.memory[processedInput][style].push(response);
+ }
+ if (shouldSave) this.saveData();
+ }
+}
+
+module.exports = new Brain();
diff --git a/QueenNoxi/brain/src/context.js b/QueenNoxi/brain/src/context.js
new file mode 100644
index 0000000000000000000000000000000000000000..8fdcb8df13887710d899fd947af22fcb748e9f6b
--- /dev/null
+++ b/QueenNoxi/brain/src/context.js
@@ -0,0 +1,93 @@
+/**
+ * context.js — Conversation Context Window
+ *
+ * Tracks:
+ * - Recent turns (last N messages from user + bot)
+ * - Active topics (what is being discussed)
+ * - Active intents
+ * - Emotional trend
+ */
+
+class ContextWindow {
+ constructor(maxTurns = 10) {
+ this.maxTurns = maxTurns;
+ this.turns = []; // { role: 'user'|'bot', text, analysis?, ts }
+ this.activeTopics = []; // most recent topics discussed
+ this.activeIntents = []; // most recent intents
+ this.tone = 'neutral';
+ this.waitingForAnswer = null; // if bot asked a question, track what it asked
+ }
+
+ /**
+ * Add a user turn (with NLU analysis attached)
+ */
+ addUserTurn(text, analysis) {
+ this.turns.push({ role: 'user', text, analysis, ts: Date.now() });
+ if (this.turns.length > this.maxTurns * 2) this.turns.shift();
+
+ if (analysis) {
+ // Update active topics (prepend newest, keep last 3)
+ this.activeTopics = [...new Set([...analysis.topics, ...this.activeTopics])].slice(0, 3);
+ this.activeIntents = analysis.intents;
+ this.tone = analysis.tone;
+ }
+ }
+
+ /**
+ * Add a bot turn
+ */
+ addBotTurn(text) {
+ this.turns.push({ role: 'bot', text, ts: Date.now() });
+ if (this.turns.length > this.maxTurns * 2) this.turns.shift();
+
+ // Track if bot asked a question (ends in ?)
+ if (text && text.includes('?')) {
+ this.waitingForAnswer = text;
+ } else {
+ this.waitingForAnswer = null;
+ }
+ }
+
+ /**
+ * Get recent user messages (last N)
+ */
+ getRecentUserMessages(n = 3) {
+ return this.turns.filter(t => t.role === 'user').slice(-n).map(t => t.text);
+ }
+
+ /**
+ * Get last bot message
+ */
+ getLastBotMessage() {
+ const botTurns = this.turns.filter(t => t.role === 'bot');
+ return botTurns.length ? botTurns[botTurns.length - 1].text : null;
+ }
+
+ /**
+ * Check if current message is likely a follow-up to an active topic
+ */
+ isFollowUp(analysis) {
+ if (!analysis || !this.activeTopics.length) return false;
+ return analysis.topics.some(t => this.activeTopics.includes(t));
+ }
+
+ /**
+ * Check if the bot recently asked a question and is waiting for an answer
+ */
+ isAnsweringBotQuestion() {
+ return !!this.waitingForAnswer;
+ }
+
+ /**
+ * Wipe context (for fresh conversation)
+ */
+ reset() {
+ this.turns = [];
+ this.activeTopics = [];
+ this.activeIntents = [];
+ this.tone = 'neutral';
+ this.waitingForAnswer = null;
+ }
+}
+
+module.exports = ContextWindow;
diff --git a/QueenNoxi/brain/src/dictionary.js b/QueenNoxi/brain/src/dictionary.js
new file mode 100644
index 0000000000000000000000000000000000000000..21fa6df31ada5ca3d822f8f76d8dc9c1f2e5d265
--- /dev/null
+++ b/QueenNoxi/brain/src/dictionary.js
@@ -0,0 +1,110 @@
+const fs = require('fs');
+const path = require('path');
+const processor = require('./processor');
+
+class Dictionary {
+ constructor(dbPath) {
+ this.dbPath = dbPath || path.join(__dirname, '..', 'data', 'dictionary.json');
+ this.data = {};
+ this.load();
+ }
+
+ load() {
+ try {
+ if (fs.existsSync(this.dbPath)) {
+ this.data = JSON.parse(fs.readFileSync(this.dbPath, 'utf8'));
+ // Create a separate index for normalized keys
+ this.normalizedIndex = {};
+ for (const key of Object.keys(this.data)) {
+ this.normalizedIndex[processor.process(key)] = key;
+ }
+ }
+ } catch (e) {
+ console.error('Dictionary load error:', e);
+ this.data = {};
+ }
+ }
+
+ /**
+ * Look up a word in the dictionary.
+ * Supports Sinhala script and Singlish (via internal normalization).
+ */
+ lookup(word) {
+ if (!word) return null;
+
+ const lower = word.toLowerCase().trim();
+ const normalized = processor.process(word);
+
+ // 1. Direct match (exact)
+ if (this.data[lower]) return this.data[lower];
+ if (this.data[normalized]) return this.data[normalized];
+
+ // 2. Normalized index match (e.g. කොම්පියුටර් -> kompiutar -> computer)
+ if (this.normalizedIndex[normalized]) {
+ return this.data[this.normalizedIndex[normalized]];
+ }
+
+ // 3. Fuzzy search for similar words
+ let bestMatch = null;
+ let highestScore = 0;
+
+ for (const [key, value] of Object.entries(this.data)) {
+ const score = processor.calculateSimilarity(normalized, processor.process(key));
+ if (score > highestScore && score > 0.8) {
+ highestScore = score;
+ bestMatch = value;
+ }
+ }
+
+ return bestMatch;
+ }
+
+ /**
+ * Check if the message is a "what is X" type question
+ */
+ isDictionaryQuery(text) {
+ const lower = text.toLowerCase();
+ // Common dictionary query patterns
+ const patterns = [
+ /mokakda (.*) kiyanne/i,
+ /what is (.*)/i,
+ /meaning of (.*)/i,
+ /arthaya (.*)/i,
+ /(.*) kiyanne mokakda/i,
+ /(.*) meaning/i
+ ];
+
+ for (const pat of patterns) {
+ const match = lower.match(pat);
+ if (match) {
+ // Clean punctuation from the extracted word
+ return match[1].trim().replace(/[?!,.]$/, '');
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Format a dictionary entry into a human-like response
+ */
+ formatResponse(word, entry) {
+ if (!entry) return null;
+
+ if (typeof entry === 'string') {
+ return `"${word}" kiyanne: ${entry}`;
+ }
+
+ let resp = `"${word}" gana mama hoyala beluva... `;
+
+ if (entry.sin) resp += `Sinhala: ${entry.sin}. `;
+ if (entry.en) resp += `English: ${entry.en}. `;
+ if (entry.def) resp += `Definition: ${entry.def}. `;
+ if (entry.synonyms && entry.synonyms.length) {
+ resp += `Synonyms: ${entry.synonyms.slice(0, 3).join(', ')}.`;
+ }
+
+ return resp;
+ }
+}
+
+module.exports = Dictionary;
diff --git a/QueenNoxi/brain/src/group.js b/QueenNoxi/brain/src/group.js
new file mode 100644
index 0000000000000000000000000000000000000000..04dc415f849c831f94e2e6816278a525ee342f03
--- /dev/null
+++ b/QueenNoxi/brain/src/group.js
@@ -0,0 +1,115 @@
+const UserMemory = require('./memory');
+const StorageAdapter = require('./storage');
+const path = require('path');
+
+class GroupSession {
+ constructor(options = {}) {
+ this.groupId = options.groupId || 'default_group';
+ this.memory = options.memory; // global UserMemory instance for user lookup
+ this.members = new Set();
+ this.recentMessages = [];
+ this.maxHistory = options.maxHistory || 50;
+
+ const storagePath = options.mongoUri || (options.usersDb ? path.join(options.usersDb, `${this.groupId}.json`) : path.join(process.cwd(), 'database', `${this.groupId}.json`));
+ this.storage = new StorageAdapter(storagePath, 'groups');
+ }
+
+ async init() {
+ await this.storage.init();
+ const data = await this.storage.loadAll();
+ this.recentMessages = data.history || [];
+ if (data.members) this.members = new Set(data.members);
+ }
+
+ async save() {
+ await this.storage.saveAll({
+ groupId: this.groupId,
+ members: [...this.members],
+ history: this.recentMessages,
+ lastUpdate: Date.now()
+ });
+ }
+
+ /**
+ * Register a member in this group
+ */
+ addMember(userId) {
+ this.members.add(userId);
+ }
+
+ /**
+ * Record a message in group context
+ */
+ async addMessage(userId, text, from = null) {
+ this.addMember(userId);
+ const sender = from || userId;
+ const msg = { from: sender, text, ts: Date.now() };
+ this.recentMessages.push(msg);
+ if (this.recentMessages.length > this.maxHistory) {
+ this.recentMessages.shift();
+ }
+
+ // Also update the global user memory if they are in this group
+ if (this.memory && sender !== 'bot') {
+ await this.memory.addHistory(sender, text, 'user');
+ }
+
+ await this.save();
+ return msg;
+ }
+
+ /**
+ * Get recent group conversation (last N messages)
+ */
+ getContext(n = 10) {
+ return this.recentMessages.slice(-n);
+ }
+
+ /**
+ * Check if a user was recently active in this group
+ */
+ isRecentlyActive(userId, withinMs = 300000) { // 5 min
+ const now = Date.now();
+ for (let i = this.recentMessages.length - 1; i >= 0; i--) {
+ const m = this.recentMessages[i];
+ if (m.userId === userId && now - m.ts < withinMs) return true;
+ }
+ return false;
+ }
+
+ /**
+ * Get all members the bot has seen in this group
+ */
+ getMembers() {
+ return [...this.members];
+ }
+
+ /**
+ * Get the last message from a specific user
+ */
+ getLastMessageFrom(userId) {
+ for (let i = this.recentMessages.length - 1; i >= 0; i--) {
+ if (this.recentMessages[i].userId === userId) {
+ return this.recentMessages[i];
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Auto-train the brain from recent group messages
+ * (pass in a brain instance)
+ */
+ autoTrainFromGroup(brain) {
+ const msgs = this.recentMessages;
+ for (let i = 0; i < msgs.length - 1; i++) {
+ const curr = msgs[i];
+ const next = msgs[i + 1];
+ if (curr.userId !== next.userId) {
+ brain.learn(curr.text, next.text);
+ }
+ }
+ }
+}
+
+module.exports = GroupSession;
diff --git a/QueenNoxi/brain/src/memory.js b/QueenNoxi/brain/src/memory.js
new file mode 100644
index 0000000000000000000000000000000000000000..aa4ca15c7c071ac86322ed3c58c251b76be0816c
--- /dev/null
+++ b/QueenNoxi/brain/src/memory.js
@@ -0,0 +1,204 @@
+const StorageAdapter = require('./storage');
+
+class UserMemory {
+ constructor(dbPath) {
+ this.storage = new StorageAdapter(dbPath, 'users');
+ this.users = {};
+ }
+
+ async init() {
+ await this.storage.init();
+ this.users = await this.storage.loadAll();
+ }
+
+ async save() {
+ await this.storage.saveAll(this.users);
+ }
+
+ /**
+ * Get a user's memory object. Creates one if it doesn't exist.
+ * @param {string} userId
+ */
+ getUser(userId) {
+ if (!this.users[userId]) {
+ this.users[userId] = {
+ id: userId,
+ name: null,
+ age: null,
+ location: null,
+ job: null,
+ gender: null,
+ nickname: null,
+ facts: {}, // Free-form key-value facts
+ history: [], // Last N messages
+ lastSeen: null,
+ seenCount: 0
+ };
+ } else {
+ // Safety check for legacy or malformed files
+ if (!this.users[userId].facts) this.users[userId].facts = {};
+ if (!this.users[userId].history) this.users[userId].history = [];
+ }
+ return this.users[userId];
+ }
+
+ /**
+ * Set a specific field on a user
+ */
+ async setUserData(userId, field, value) {
+ const user = this.getUser(userId);
+ user[field] = value;
+ await this.save();
+ }
+
+ /**
+ * Set a free-form fact about a user
+ */
+ async setFact(userId, key, value) {
+ const user = this.getUser(userId);
+ user.facts[key] = value;
+ await this.save();
+ }
+
+ /**
+ * Get a fact about a user
+ */
+ getFact(userId, key) {
+ const user = this.getUser(userId);
+ return user.facts[key] || null;
+ }
+
+ /**
+ * Record a message in the user's history (keep last 30)
+ */
+ async addHistory(userId, text, from = 'user') {
+ const user = this.getUser(userId);
+ user.history.push({ text, from, ts: Date.now() });
+ if (user.history.length > 30) user.history.shift();
+ user.lastSeen = Date.now();
+ user.seenCount = (user.seenCount || 0) + 1;
+ await this.save();
+ }
+
+ /**
+ * Auto-extract user info from message text (name, location, job, age)
+ * Returns any fields that were updated
+ */
+ extractFromMessage(userId, text) {
+ const lower = text.toLowerCase().trim();
+ const extracted = {};
+
+ // BLACKLIST: common Singlish words that look like a "match" but are NOT names/locations
+ const blacklistNames = new Set(['mama', 'machan', 'godak', 'poddak', 'hodai', 'supa', 'maru', 'ada',
+ 'kalin', 'dan', 'eka', 'api', 'ado', 'ayye', 'aiyo', 'ekath', 'kohomada', 'tani', 'ganna',
+ 'karanna', 'kiyanna', 'ne', 'da', 'ba', 'ok', 'lol', 'haha', 'bye', 'hi', 'hai', 'hello',
+ 'sorry', 'thanks', 'library', 'school', 'office', 'bus', 'train', 'gedara', 'weda', 'giia',
+ 'apahu', 'pass', 'fail', 'suba', 'subha', 'aiya', 'akka', 'nangi', 'malli', 'amma', 'thaththa']);
+
+ // Name: ONLY on explicit "my name is X" or "mage nama X" (not bare "mama X")
+ const nameMatch = lower.match(/(?:my name is|mage nama|namayi)\s+([a-z]{2,20})(?:\s|$|,|\.)/i);
+ if (nameMatch && !blacklistNames.has(nameMatch[1].toLowerCase())) {
+ const name = this._capitalize(nameMatch[1]);
+ this.setUserData(userId, 'name', name); // Note: this is async but we don't await here to keep it non-blocking in NLU
+ extracted.name = name;
+ }
+
+ // Age: "mage wayasa 22", "age 22", "i am 22 years"
+ const ageMatch = lower.match(/(?:mage wayasa|wayasa|age)\s+(\d{1,3})/i)
+ || lower.match(/i(?:'| a)m (\d{1,3})\s*y/i);
+ if (ageMatch) {
+ const age = parseInt(ageMatch[1]);
+ if (age > 5 && age < 100) {
+ this.setUserData(userId, 'age', age); // Background save
+ extracted.age = age;
+ }
+ }
+
+ // Location: explicit "i live in X", "stay in X" — NOT bare "inne"
+ const locMatch = lower.match(/(?:i live in|stay in|wasi)\s+([a-z]{3,20})(?:\s|$|,)/i);
+ if (locMatch && !blacklistNames.has(locMatch[1].toLowerCase())) {
+ const loc = this._capitalize(locMatch[1].trim());
+ this.setUserData(userId, 'location', loc); // Background save
+ extracted.location = loc;
+ }
+
+ // Job: "i work as X", "weda karanne X", "i am a X"
+ const jobMatch = lower.match(/(?:i work as|weda karanne|i'm a|i am a)\s+([a-z\s]{3,30})(?:\s|$|,|\.)/i);
+ if (jobMatch) {
+ const job = this._capitalize(jobMatch[1].trim());
+ this.setUserData(userId, 'job', job); // Background save
+ extracted.job = job;
+ }
+
+ // Favorites: "my favorite X is Y", "mama kemati X walata"
+ // Improved regex with non-greedy match for the 'thing'
+ const favMatch = lower.match(/(?:my favorite|mama kamati|mama kemati)\s+([a-z\s]{2,20}?)\s+(?:is|kata|walata)\s+([a-z\s]{2,20})/i);
+ if (favMatch) {
+ const thing = favMatch[1].trim().replace(/\s+/g, '_');
+ const value = favMatch[2].trim();
+ this.setFact(userId, `favorite_${thing}`, value);
+ extracted[`favorite_${thing}`] = value;
+ }
+
+ // Likes: "I like X", "mama X walata kamati"
+ const likeMatch = lower.match(/(?:i like|mama)\s+([a-z\s]{2,20})\s+(?:kamathi|kemati|like)/i)
+ || lower.match(/(?:i like)\s+([a-z\s]{2,20})/i);
+ if (likeMatch && !blacklistNames.has(likeMatch[1].trim())) {
+ const like = likeMatch[1].trim();
+ this.setFact(userId, `likes_${like.replace(/\s+/g, '_')}`, true);
+ extracted.likes = like;
+ }
+
+ return extracted;
+ }
+
+ _capitalize(str) {
+ return str.charAt(0).toUpperCase() + str.slice(1);
+ }
+
+ /**
+ * Compose a greeting using remembered data
+ */
+ greet(userId) {
+ const user = this.getUser(userId);
+ if (user.name) {
+ const greetings = [
+ `${user.name} machan, kohomada?`,
+ `Aiyo ${user.name}, ata ne?`,
+ `${user.name}! Oya thiyanawada?`,
+ `Oya ${user.name} ne! Kohomada?`
+ ];
+ return greetings[Math.floor(Math.random() * greetings.length)];
+ }
+ return null;
+ }
+
+ /**
+ * Check if the user is asking the bot about what it knows of them
+ */
+ memoryQuery(userId, text) {
+ const lower = text.toLowerCase();
+ const user = this.getUser(userId);
+ if (!user) return null;
+
+ if (lower.match(/mage nama|my name|oya thiyanawd|do you know me|oyata mata danne|mama gana|about me/)) {
+ const parts = [];
+ if (user.name) parts.push(`Oba ${user.name}.`);
+ if (user.age) parts.push(`Wayasa ${user.age}.`);
+ if (user.location) parts.push(`${user.location} wala inne.`);
+ if (user.job) parts.push(`${user.job} kenek.`);
+
+ // Add facts/favorites
+ const favs = Object.keys(user.facts).filter(k => k.startsWith('favorite_'));
+ if (favs.length) {
+ const f = favs[0];
+ parts.push(`${f.replace('favorite_', '').replace('_', ' ')} eka ${user.facts[f]}.`);
+ }
+
+ if (parts.length) return parts.join(' ');
+ }
+ return null;
+ }
+}
+
+module.exports = UserMemory;
diff --git a/QueenNoxi/brain/src/nlu.js b/QueenNoxi/brain/src/nlu.js
new file mode 100644
index 0000000000000000000000000000000000000000..0e4f24f175d14bf88df8adee0709995fa2c46dab
--- /dev/null
+++ b/QueenNoxi/brain/src/nlu.js
@@ -0,0 +1,279 @@
+/**
+ * nlu.js — Natural Language Understanding Engine
+ *
+ * Makes the bot understand:
+ * 1. Long paragraphs (segmented into individual thoughts)
+ * 2. Intent detection (question, greeting, complaint, story, tease, etc.)
+ * 3. Keyword/topic extraction (what is the user REALLY talking about?)
+ * 4. Emotional tone detection (happy, sad, angry, excited)
+ * 5. Context window (what was just talked about?)
+ */
+
+// ────────────────────────────────────────────────────────────────────────────
+// KNOWLEDGE MAPS
+// ────────────────────────────────────────────────────────────────────────────
+
+const INTENT_PATTERNS = {
+ greeting: [/^(hi|hello|hai|hey|ata ne|kohomada|ayye|subha udesanak|good morning|gm)\b/i],
+ farewell: [/\b(bye|goodnight|subha rathriyak|gn|yanawa|later|ttyl|cya)\b/i],
+ question: [/\b(mokakda|kohomada|kauda|kawda|monada|koheda|kiia|when|why|how|what|who|where|when|da\?|neda\?|da$)\b/i, /\?/],
+ complaint: [/\b(dukei|duk|problem|issue|kapuna|asathutak|baya|adura|roga|hondatama na|hodatma na|barei)\b/i],
+ happy: [/\b(supa|ela|elatama|maru|hodai|santhosai|love|like|enjoy|ado hehe|haha|lol|:d|😂|😍|❤️|🔥)\b/i],
+ sad: [/\b(duk|sad|cry|neth|balanna bari|tani|ekai|adare na|alone|miss|😢|💔|😞)\b/i],
+ angry: [/\b(eka nan|apahu|nonsense|baena|bae|budukarapu|waeda na|kelawera|okama|angry|mad|😠|🤬)\b/i],
+ excited: [/\b(!{2,}|omg|aiyoo|wait|seriously|ow ow|neda|😱|🤩|elaaaaaa|supaaa)\b/i],
+ love: [/\b(love|adare|miss|baby|bae|heart|❤️|😘|💕|💖|xoxo)\b/i],
+ story: [/^(ada|kalin|ekl|eka dawas|meka une|eeka giia|eka thamath|api giia|api gaththee|api hitiye)\b/i],
+ thanks: [/\b(thanks|thank you|sthuthi|stuthi|awlak na|np|🙏)\b/i],
+ request: [/\b(karanna puluwan|help|support|explain|kiyanna|denna|ganna|hadanna)\b/i],
+ selfIntro: [/\b(my name is|mama|mage nama|i am|i'm|iam)\b/i],
+ teasing: [/\b(hehe|hihi|🤭|😜|😏|;|patta|wela|joke|haha)\b/i]
+};
+
+const TOPIC_KEYWORDS = {
+ relationship: ['love', 'adare', 'gf', 'bf', 'girlfriend', 'boyfriend', 'miss', 'marry', 'nikaya', 'prema', 'date', 'kiss'],
+ food: ['kema', 'rice', 'food', 'eat', 'restaurant', 'kanne', 'bathi', 'curry', 'pizza', 'kottu', 'hoppers', 'string hoppers'],
+ work: ['job', 'weda', 'office', 'project', 'deadline', 'meeting', 'boss', 'salary', 'work', 'weda karanne', 'interview'],
+ school: ['school', 'university', 'exam', 'pariksha', 'class', 'lecture', 'teacher', 'student', 'ol', 'al', 'degree'],
+ games: ['game', 'play', 'pubg', 'fifa', 'cricket', 'sport', 'win', 'lose', 'match'],
+ weather: ['rain', 'vasse', 'hot', 'climate', 'weather', 'udawa', 'ginithapu', 'thawath vasse'],
+ transport: ['bus', 'train', 'uber', 'taxi', 'bike', 'car', 'traffic', 'jam', 'yanna', 'enna'],
+ money: ['money', 'salary', 'loan', 'borrow', 'spend', 'expensive', 'cheap', 'hari ganan', 'pisa', 'rata'],
+ health: ['sick', 'roga', 'hospital', 'doctor', 'medicine', 'adura', 'pain', 'headache', 'fever', 'gedara inne'],
+ tech: ['phone', 'laptop', 'computer', 'internet', 'data', 'app', 'code', 'website', 'software', 'hack'],
+ family: ['amma', 'thaththa', 'mom', 'dad', 'akka', 'malli', 'nangi', 'brother', 'sister', 'family', 'gedara'],
+ social: ['party', 'meet', 'hangout', 'friend', 'machan', 'api', 'mall', 'trip', 'outing', 'picnic'],
+ srilanka: ['sl', 'srilanka', 'colombo', 'kandy', 'galle', 'jaffna', 'ratnapura', 'lanka'],
+ feelings: ['hithenne', 'feel', 'think', 'baya', 'happy', 'sad', 'cry', 'emotion'],
+};
+
+const TOPIC_RESPONSES = {
+ relationship: {
+ chill: ['ada relationship gana dan hodatama katha karanna epa machan hehe', 'adare gana kiyannako, mokada?', 'ow ow, gf/bf gana da?'],
+ sexy: ['ooh relationship gana da? oya kauda lucky one?', 'adare gana oya hithanawa da?'],
+ hot: ['adare gana da? oyata special kenek innawada?', 'ooh oya kiyanne relationship gana da?']
+ },
+ food: {
+ chill: ['ada kema supa da?!', 'mama dan godak kapannawa, kottu gana kiyanne?', 'ae machan, mama godak hungry', 'abaa kema kiyannako!'],
+ professional: ['Hoda keme irunawada?']
+ },
+ work: {
+ chill: ['aiyo weda wada, poddak relax karaganna machan', 'deadline gana stress wena epa', 'weda wenawata kalin api katha kaapuwa na?'],
+ professional: ['Project eka kohomada giya? Help ona nam kiyanna.']
+ },
+ school: {
+ chill: ['exam gana stress wena epa machan, blew ekak gamu', 'hondatama study karaganna, ekath mama help karanna puluwan', 'ow pariksha gana da? kalin kiyanna thibathe'],
+ professional: ['Study karala hodatama exams passweida?']
+ },
+ games: {
+ chill: ['ado PUBG da?! mama oka ban khelannawa', 'game gana kiyannako, api multiplayer kaamu', 'cricket match baluwada? maru giia na?'],
+ hot: ['game karala jadi wunasama prize karannako chat eka']
+ },
+ weather: {
+ chill: ['ada vasse da koheda?', 'aiyo ginithapu, AC eka dala inne', 'ae machan vasse aiye gedara hitapu', 'Colombo wala ginithapu, koheda oya?'],
+ },
+ transport: {
+ chill: ['bus eke traffic jam da? ae ae normal', 'Uber gahanna, bus pain wena epaa machan'],
+ },
+ money: {
+ chill: ['aiyo machan pisa gana duku', 'salary eka waidi wenna one', 'mae ithin savings poddak', 'hari ganan aney life'],
+ },
+ health: {
+ chill: ['aiyo machan! hondatama hoddaganna', 'roga nathnam santhosai', 'hospital giiewda? kohomada?', 'adura na?! gedara hitapan'],
+ },
+ tech: {
+ chill: ['tech gana naththam mama kiyannawa machan', 'code gahala trouble da? kiyanna', 'phone da laptop da?'],
+ professional: ['Tech stack eka mokakda use karanne?']
+ },
+ family: {
+ chill: ['family gana hari supa machan', 'amma thaththa kohomada?', 'gedara gihilla aawada?'],
+ },
+ social: {
+ chill: ['outing gamu! koheda?', 'party ekak?! enna enna', 'machan ahala hitiye api meet weida'],
+ hot: ['outing gamu, just ape dokomath']
+ },
+ srilanka: {
+ chill: ['mama SL eke podi balla', 'SL game elatama', 'koheda inne SL wala?'],
+ },
+ feelings: {
+ chill: ['mokakda hithenne? kiyanna machan', 'awl na ado, mama innawa ne?', 'mama thiyanawa obage kotaswa'],
+ sexy: ['kiyanna aney, mama ahannawa', 'oya feel wenne mama danne na ne?'],
+ }
+};
+
+// ────────────────────────────────────────────────────────────────────────────
+// AUTO-STYLE SIGNAL MAPS
+// ────────────────────────────────────────────────────────────────────────────
+
+// Each style has signal words/patterns. Weighted scoring picks the winner.
+const STYLE_SIGNALS = {
+ professional: {
+ weight: 1,
+ patterns: [
+ /\b(project|meeting|deadline|report|client|sir|madam|please|thank you|regards|schedule|proposal|discuss|email|call|agenda|budget|salary|office|manager|team)\b/i
+ ]
+ },
+ sexy: {
+ weight: 1,
+ patterns: [
+ /\b(baby|babe|honey|cutie|beautiful|gorgeous|hot|sexy|miss you|come over|alone|cuddle|kiss|hug|adore|😍|😘|💋|🥵|😏|😉|flirt|date|special|crush)\b/i
+ ]
+ },
+ hot: {
+ weight: 1,
+ patterns: [
+ /\b(!!|come|enna|langata|tonight|now|asap|jld|fast|quick|urgent|secret|naughty|dark|🔥|😈|🥵|💦|need you|want you|right now)\b/i,
+ /!{2,}/
+ ]
+ },
+ chill: {
+ weight: 0.5, // default — lower weight so it loses to specific signals
+ patterns: [
+ /\b(machan|ado|ayye|poddak|nikang|hehe|lol|supa|ela|maru|chilling|relax|nothing|nothing much|nikan)\b/i
+ ]
+ }
+};
+
+// ────────────────────────────────────────────────────────────────────────────
+// NLU CLASS
+// ────────────────────────────────────────────────────────────────────────────
+
+class NLU {
+
+ /**
+ * Segment a long paragraph into individual thought-sentences
+ * Splits on: . ! ? newlines, and Sinhala sentence enders
+ */
+ segment(text) {
+ // Split on sentence terminators
+ const raw = text.split(/[.!?\n\r।ෙ]+/).map(s => s.trim()).filter(s => s.length > 1);
+ // Also split on conjunctions that indicate a change of subject
+ const sentences = [];
+ for (const s of raw) {
+ // If sentence has comma + subject change indicator, further split
+ const subParts = s.split(/\s*,\s*(?=(?:mama|oya|api|ekka|eka|dan|kalin|ekath|ado|machan)\b)/i);
+ for (const p of subParts) {
+ if (p.trim().length > 1) sentences.push(p.trim());
+ }
+ }
+ return sentences.length ? sentences : [text.trim()];
+ }
+
+ /**
+ * Detect intents from a sentence
+ * @returns {string[]} list of matched intents
+ */
+ detectIntents(text) {
+ const found = [];
+ for (const [intent, patterns] of Object.entries(INTENT_PATTERNS)) {
+ for (const pat of patterns) {
+ if (pat.test(text)) { found.push(intent); break; }
+ }
+ }
+ return found.length ? found : ['unknown'];
+ }
+
+ /**
+ * Extract topics from text
+ * @returns {string[]} matched topic names
+ */
+ extractTopics(text) {
+ const lower = text.toLowerCase();
+ const found = [];
+ for (const [topic, keywords] of Object.entries(TOPIC_KEYWORDS)) {
+ if (keywords.some(kw => lower.includes(kw))) found.push(topic);
+ }
+ return found;
+ }
+
+ /**
+ * Get a topic-based response
+ */
+ getTopicResponse(topic, style = 'chill') {
+ const bank = TOPIC_RESPONSES[topic];
+ if (!bank) return null;
+ const pool = bank[style] || bank['chill'] || null;
+ if (!pool || !pool.length) return null;
+ return pool[Math.floor(Math.random() * pool.length)];
+ }
+
+ /**
+ * Detect the emotional tone of a message
+ */
+ detectTone(text) {
+ if (INTENT_PATTERNS.happy.some(p => p.test(text))) return 'happy';
+ if (INTENT_PATTERNS.sad.some(p => p.test(text))) return 'sad';
+ if (INTENT_PATTERNS.angry.some(p => p.test(text))) return 'angry';
+ if (INTENT_PATTERNS.excited.some(p => p.test(text))) return 'excited';
+ if (INTENT_PATTERNS.love.some(p => p.test(text))) return 'love';
+ return 'neutral';
+ }
+
+ /**
+ * Generate a tone-aware reaction prefix
+ */
+ toneReaction(tone) {
+ const reactions = {
+ happy: ['supa aney!', 'elatama!', 'maru!', 'hodai!'],
+ sad: ['aiyo duka aney...', 'awl na machan...', 'duk gannaka eppa ne?'],
+ angry: ['ok ok, chill down machan', 'relax ado...', 'aiyo mokada wune?'],
+ excited: ['ado!', 'wait wait!', 'seriously?!'],
+ love: ['aww hehe', 'ooh ooh', '😊'],
+ neutral: []
+ };
+ const pool = reactions[tone] || [];
+ return pool.length ? pool[Math.floor(Math.random() * pool.length)] : '';
+ }
+
+ /**
+ * Auto-detect the best response style based on message content.
+ * Scores each style by how many signals match, returns the top scorer.
+ * @param {string} text
+ * @returns {string} style name
+ */
+ detectAutoStyle(text) {
+ const scores = {};
+ for (const [style, config] of Object.entries(STYLE_SIGNALS)) {
+ let score = 0;
+ for (const pat of config.patterns) {
+ const matches = text.match(pat);
+ if (matches) score += matches.length * config.weight;
+ }
+ scores[style] = score;
+ }
+ // Return style with highest score; default to chill
+ const best = Object.entries(scores).sort((a, b) => b[1] - a[1])[0];
+ return (best && best[1] > 0) ? best[0] : 'chill';
+ }
+
+ /**
+ * Full analysis of any message (short or long paragraph)
+ * Returns a structured analysis object
+ */
+ analyze(text) {
+ const sentences = this.segment(text);
+ const intents = new Set();
+ const topics = new Set();
+ const tone = this.detectTone(text);
+
+ for (const s of sentences) {
+ this.detectIntents(s).forEach(i => intents.add(i));
+ this.extractTopics(s).forEach(t => topics.add(t));
+ }
+
+ return {
+ original: text,
+ sentences,
+ intents: [...intents],
+ topics: [...topics],
+ tone,
+ detectedStyle: this.detectAutoStyle(text),
+ isLong: sentences.length > 1,
+ isQuestion: intents.has('question'),
+ isGreeting: intents.has('greeting'),
+ isFarewell: intents.has('farewell')
+ };
+ }
+}
+
+module.exports = new NLU();
diff --git a/QueenNoxi/brain/src/processor.js b/QueenNoxi/brain/src/processor.js
new file mode 100644
index 0000000000000000000000000000000000000000..a8425c307fb4adeeb32e7e09ef2a3c3a4caf21b2
--- /dev/null
+++ b/QueenNoxi/brain/src/processor.js
@@ -0,0 +1,162 @@
+const fs = require('fs');
+const path = require('path');
+
+class LanguageProcessor {
+ constructor() {
+ this.slang = {};
+ this.phoneticMap = {}; // Custom word mappings (sinhala script -> singlish)
+ this.loadSlang();
+ this.loadPhoneticMap();
+ this.initTransliteration();
+ }
+
+ loadSlang() {
+ try {
+ const slangPath = path.join(__dirname, '..', 'data', 'slang.json');
+ if (fs.existsSync(slangPath)) {
+ this.slang = JSON.parse(fs.readFileSync(slangPath, 'utf8'));
+ }
+ } catch (e) { }
+ }
+
+ loadPhoneticMap() {
+ try {
+ const mapPath = path.join(__dirname, '..', 'data', 'phonetic_map.json');
+ if (fs.existsSync(mapPath)) {
+ this.phoneticMap = JSON.parse(fs.readFileSync(mapPath, 'utf8'));
+ }
+ } catch (e) { }
+ }
+
+ savePhoneticMap() {
+ try {
+ const mapPath = path.join(__dirname, '..', 'data', 'phonetic_map.json');
+ const dir = path.dirname(mapPath);
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
+ fs.writeFileSync(mapPath, JSON.stringify(this.phoneticMap, null, 2), 'utf8');
+ } catch (e) {
+ console.error('Error saving phonetic map:', e);
+ }
+ }
+
+ initTransliteration() {
+ this.vowels = {
+ 'අ': 'a', 'ආ': 'aa', 'ඇ': 'ae', 'ඈ': 'aee', 'ඉ': 'i', 'ඊ': 'ii',
+ 'උ': 'u', 'ඌ': 'uu', 'එ': 'e', 'ඒ': 'ee', 'ඔ': 'o', 'ඕ': 'oo',
+ 'ඓ': 'ai', 'ඖ': 'au'
+ };
+ this.consonants = {
+ 'ක': 'k', 'ඛ': 'kh', 'ග': 'g', 'ඝ': 'gh', 'ඞ': 'ng', 'ඟ': 'ng',
+ 'ච': 'c', 'ඡ': 'ch', 'ජ': 'j', 'ඣ': 'jh', 'ඤ': 'ny', 'ඥ': 'gn',
+ 'ට': 't', 'ඨ': 'th', 'ඩ': 'd', 'ඪ': 'dh', 'ණ': 'n', 'ඬ': 'nd',
+ 'ත': 't', 'ථ': 'th', 'ද': 'd', 'ධ': 'dh', 'න': 'n', 'ඳ': 'nd',
+ 'ප': 'p', 'ඵ': 'ph', 'බ': 'b', 'භ': 'bh', 'ම': 'm', 'ඹ': 'mb',
+ 'ය': 'y', 'ර': 'r', 'ල': 'l', 'ව': 'v', 'ශ': 'sh', 'ෂ': 'sh',
+ 'ස': 's', 'හ': 'h', 'ළ': 'l', 'ෆ': 'f'
+ };
+ this.pilli = {
+ '්': '', 'ා': 'aa', 'ැ': 'ae', 'ෑ': 'aee', 'ි': 'i', 'ී': 'ii',
+ 'ු': 'u', 'ූ': 'uu', 'ෘ': 'ru', 'ෙ': 'e', 'ේ': 'ee', 'ෛ': 'ai',
+ 'ො': 'o', 'ෝ': 'oo', 'ෞ': 'au', 'ං': 'n'
+ };
+ }
+
+ sinhalaToSinglish(text) {
+ let result = '';
+ for (let i = 0; i < text.length; i++) {
+ const char = text[i];
+
+ if (this.vowels[char]) {
+ result += this.vowels[char];
+ } else if (this.consonants[char]) {
+ let next = text[i + 1];
+ let singlish = this.consonants[char];
+ if (next && this.pilli[next] !== undefined) {
+ singlish += this.pilli[next];
+ i++; // skip pilla
+ } else {
+ singlish += 'a'; // inherent vowel
+ }
+ result += singlish;
+ } else {
+ result += char;
+ }
+ }
+ return result;
+ }
+
+ /**
+ * Normalizes text for matching.
+ * Handles mixed Sinhala/Singlish, slang expansion, and phonetic cleanup.
+ */
+ process(text) {
+ if (!text) return '';
+
+ // 1. Convert Sinhala script to Singlish (using map if available)
+ const words = text.split(/\s+/);
+ const processedWords = words.map(word => {
+ // Handle phonetic map (Sinhala -> Singlish)
+ if (this.phoneticMap[word]) return this.phoneticMap[word];
+
+ // Handle rule-based translit
+ return this.sinhalaToSinglish(word);
+ });
+
+ let processed = processedWords.join(' ').toLowerCase().trim();
+
+ // 2. Expand common slang (Singlish -> Real Concept)
+ const finalWords = processed.split(/\s+/);
+ processed = finalWords.map(word => this.slang[word] || word).join(' ');
+
+ // 3. Simple phonetic normalization
+ processed = this.normalizePhonetics(processed);
+
+ return processed;
+ }
+
+ normalizePhonetics(text) {
+ return text
+ .replace(/sh/g, 's')
+ .replace(/th/g, 't')
+ .replace(/dh/g, 'd')
+ .replace(/aa/g, 'a')
+ .replace(/ee/g, 'i')
+ .replace(/oo/g, 'u')
+ .replace(/y/g, 'i')
+ .replace(/w/g, 'v')
+ .replace(/([a-z])\1+/g, '$1') // remove double letters
+ .replace(/[^a-z0-9\s]/g, ''); // remove non-alphanumeric
+ }
+
+ /**
+ * Calculates similarity between two strings (Levenshtein Distance)
+ */
+ calculateSimilarity(s1, s2) {
+ if (s1 === s2) return 1.0;
+
+ const len1 = s1.length;
+ const len2 = s2.length;
+ if (len1 === 0) return 0.0;
+ if (len2 === 0) return 0.0;
+
+ const matrix = [];
+ for (let i = 0; i <= len1; i++) matrix[i] = [i];
+ for (let j = 0; j <= len2; j++) matrix[0][j] = j;
+
+ for (let i = 1; i <= len1; i++) {
+ for (let j = 1; j <= len2; j++) {
+ const cost = s1[i - 1] === s2[j - 1] ? 0 : 1;
+ matrix[i][j] = Math.min(
+ matrix[i - 1][j] + 1,
+ matrix[i][j - 1] + 1,
+ matrix[i - 1][j - 1] + cost
+ );
+ }
+ }
+
+ const distance = matrix[len1][len2];
+ return 1.0 - (distance / Math.max(len1, len2));
+ }
+}
+
+module.exports = new LanguageProcessor();
diff --git a/QueenNoxi/brain/src/storage.js b/QueenNoxi/brain/src/storage.js
new file mode 100644
index 0000000000000000000000000000000000000000..b49bb7d053cef8a5626c17d5cc6bc07bc231c1c3
--- /dev/null
+++ b/QueenNoxi/brain/src/storage.js
@@ -0,0 +1,135 @@
+const fs = require('fs');
+const path = require('path');
+
+/**
+ * StorageAdapter provides a unified interface for local JSON and MongoDB.
+ * For local 'users' context, it saves individual files per user.
+ */
+class StorageAdapter {
+ /**
+ * @param {string} connectionStr - Local path (file or dir) or MongoDB URI
+ * @param {string} context - 'users' | 'brain' | 'common'
+ */
+ constructor(connectionStr, context = 'common') {
+ this.connectionStr = connectionStr;
+ this.context = context;
+ this.isMongo = connectionStr.startsWith('mongodb');
+ this.client = null;
+ this.db = null;
+ }
+
+ async init() {
+ if (this.isMongo) {
+ try {
+ const { MongoClient } = require('mongodb');
+ this.client = new MongoClient(this.connectionStr);
+ await this.client.connect();
+ this.db = this.client.db();
+ console.log(`📡 Storage: Connected to MongoDB [${this.context}]`);
+ } catch (e) {
+ console.error(`❌ Storage: Failed to connect to MongoDB. Falling back to local storage.`);
+ this.isMongo = false;
+ }
+ } else if (this.context === 'users') {
+ // Local Users should use a directory
+ if (!fs.existsSync(this.connectionStr)) {
+ fs.mkdirSync(this.connectionStr, { recursive: true });
+ }
+ }
+ return this;
+ }
+
+ /**
+ * Load all data for this context
+ */
+ async loadAll() {
+ if (this.isMongo && this.db) {
+ const collection = this.db.collection(this.context);
+ if (this.context === 'users') {
+ const results = await collection.find({}).toArray();
+ const out = {};
+ results.forEach(r => {
+ const { _id, ...data } = r;
+ out[_id] = data;
+ });
+ return out;
+ } else {
+ const doc = await collection.findOne({ _id: 'master' });
+ return doc ? doc.data : {};
+ }
+ } else {
+ // Local FS
+ if (this.context === 'users') {
+ const dir = this.connectionStr;
+ if (!fs.existsSync(dir)) return {};
+ const files = fs.readdirSync(dir).filter(f => f.endsWith('.json'));
+ const out = {};
+ files.forEach(f => {
+ try {
+ const content = fs.readFileSync(path.join(dir, f), 'utf8');
+ out[f.replace('.json', '')] = JSON.parse(content);
+ } catch (e) { }
+ });
+ return out;
+ } else {
+ try {
+ if (fs.existsSync(this.connectionStr)) {
+ return JSON.parse(fs.readFileSync(this.connectionStr, 'utf8'));
+ }
+ } catch (e) { }
+ return {};
+ }
+ }
+ }
+
+ /**
+ * Save all data (Bulk)
+ */
+ async saveAll(data) {
+ if (this.isMongo && this.db) {
+ const collection = this.db.collection(this.context);
+ if (this.context === 'users') {
+ for (const userId in data) {
+ await collection.updateOne({ _id: userId }, { $set: data[userId] }, { upsert: true });
+ }
+ } else {
+ await collection.updateOne({ _id: 'master' }, { $set: { data, updatedAt: Date.now() } }, { upsert: true });
+ }
+ } else {
+ if (this.context === 'users') {
+ const dir = this.connectionStr;
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
+ for (const userId in data) {
+ const filePath = path.join(dir, `${userId}.json`);
+ fs.writeFileSync(filePath, JSON.stringify(data[userId], null, 2), 'utf8');
+ }
+ } else {
+ const dir = path.dirname(this.connectionStr);
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
+ fs.writeFileSync(this.connectionStr, JSON.stringify(data, null, 2), 'utf8');
+ }
+ }
+ }
+
+ /**
+ * Save a single item (highly optimized for users context)
+ */
+ async saveItem(id, itemData) {
+ if (this.isMongo && this.db) {
+ const collection = this.db.collection(this.context);
+ await collection.updateOne({ _id: id }, { $set: itemData }, { upsert: true });
+ } else {
+ if (this.context === 'users') {
+ const dir = this.connectionStr;
+ const filePath = path.join(dir, `${id}.json`);
+ fs.writeFileSync(filePath, JSON.stringify(itemData, null, 2), 'utf8');
+ } else {
+ const all = await this.loadAll();
+ all[id] = itemData;
+ await this.saveAll(all);
+ }
+ }
+ }
+}
+
+module.exports = StorageAdapter;
diff --git a/QueenNoxi/brain/src/trainer.js b/QueenNoxi/brain/src/trainer.js
new file mode 100644
index 0000000000000000000000000000000000000000..511046eb998b4e8fc2f14a44e1d7440250387a1e
--- /dev/null
+++ b/QueenNoxi/brain/src/trainer.js
@@ -0,0 +1,168 @@
+const fs = require('fs');
+const path = require('path');
+const readline = require('readline');
+const Brain = require('./brain');
+
+/**
+ * Universal training tool for Sinhala dataset ingestion.
+ * Supports JSON, TSV, CSV, Parallel Text, and Large-Scale Streaming.
+ */
+class Trainer {
+ /**
+ * Train from an array of objects
+ */
+ static trainData(data, defaultStyle = 'chill') {
+ if (!Array.isArray(data)) return 0;
+ const oldStyle = (Brain.persona && Brain.persona.style) || 'chill';
+ let count = 0;
+
+ console.log(`🧠 Processing ${data.length} patterns...`);
+
+ for (const item of data) {
+ const input = item.input || item.q || item.question || item.text || item.Phrase;
+ const response = item.response || item.a || item.answer || item.reply || item.Response;
+ const style = item.style || defaultStyle;
+
+ if (input && response) {
+ const s = ['chill', 'professional', 'sexy', 'hot'].includes(style) ? style : defaultStyle;
+ Brain.setPersona({ style: s });
+ // Bulk training: disable auto-save
+ Brain.learn(input.toString(), response.toString(), false);
+ count++;
+ }
+
+ if (count > 0 && count % 10000 === 0) {
+ console.log(`...ingested ${count} patterns`);
+ }
+ }
+
+ // Final Save for bulk data
+ console.log('💾 Saving brain to disk (this may take a moment)...');
+ Brain.saveData();
+
+ Brain.setPersona({ style: oldStyle });
+ return count;
+ }
+
+ /**
+ * Train from a raw string
+ */
+ static trainString(content, format = 'json', defaultStyle = 'chill') {
+ const fmt = format.toLowerCase().replace('.', '');
+ if (fmt === 'json') {
+ try { return this.trainData(JSON.parse(content), defaultStyle); }
+ catch (e) { return 0; }
+ } else if (fmt === 'tsv' || fmt === 'csv' || fmt === 'txt') {
+ const lines = content.split(/\r?\n/);
+ let count = 0;
+ const delimiter = (fmt === 'tsv' || fmt === 'txt') ? /\t+/ : ',';
+ const data = [];
+
+ for (const line of lines) {
+ if (!line.trim()) continue;
+ const columns = line.split(delimiter);
+ if (columns.length >= 2) {
+ const col1 = columns[0].trim().replace(/^"|"$/g, '');
+ const col2 = columns[1].trim().replace(/^"|"$/g, '');
+ if (col1.toLowerCase() === 'question' || col1.toLowerCase() === 'phrase' || col1.toLowerCase() === 'singlish') continue;
+ data.push({ q: col1, a: col2, style: defaultStyle });
+ }
+ }
+ return this.trainData(data, defaultStyle);
+ }
+ return 0;
+ }
+
+ /**
+ * Streaming Trainer for LARGE files (100MB+)
+ * Supports Tab, Comma, and custom WSD format.
+ */
+ static async trainLarge(filePath, defaultStyle = 'chill') {
+ const absPath = path.isAbsolute(filePath) ? filePath : path.join(process.cwd(), filePath);
+ if (!fs.existsSync(absPath)) return 0;
+
+ // Detect encoding
+ const buffer = fs.readFileSync(absPath, { length: 500 });
+ let encoding = 'utf8';
+ if (buffer[0] === 0xFF && buffer[1] === 0xFE) encoding = 'utf16le';
+ else if (buffer[0] === 0xFE && buffer[1] === 0xFF) encoding = 'utf16be';
+ else if (buffer.some(b => b === 0)) encoding = 'utf16le';
+
+ const processor = require('./processor');
+ const fileStream = fs.createReadStream(absPath, { encoding });
+
+ const rl = readline.createInterface({
+ input: fileStream,
+ crlfDelay: Infinity
+ });
+
+ console.log(`🚀 Streaming training from ${path.basename(filePath)} (${encoding})...`);
+ let count = 0;
+
+ for await (const line of rl) {
+ if (!line.trim()) continue;
+
+ let sinhala, singlish;
+
+ // Pattern 1: WSD Format
+ const wsdMatch = line.match(/^Word:\s*([^,]+),\s*Sinhala\s*Words:\s*\[(.*)\]/i);
+ if (wsdMatch) {
+ singlish = wsdMatch[1].trim();
+ const variants = wsdMatch[2].split(',').map(v => v.trim().replace(/['"\[\]]/g, ''));
+ variants.forEach(v => {
+ if (v && /[\u0D80-\u0DFF]/.test(v)) {
+ processor.phoneticMap[v] = singlish;
+ count++;
+ }
+ });
+ continue;
+ }
+
+ // Pattern 2: Swa Bhasha / test1
+ const parts = line.split(/[\t,/]/);
+ if (parts.length >= 2) {
+ const p1 = parts[0].trim().replace(/^"|"$/g, '');
+ const p2 = parts[1].trim().replace(/^"|"$/g, '');
+
+ const isP1S = /[\u0D80-\u0DFF]/.test(p1);
+ const isP2S = /[\u0D80-\u0DFF]/.test(p2);
+
+ if (isP1S && !isP2S) { sinhala = p1; singlish = p2; }
+ else if (!isP1S && isP2S) { sinhala = p2; singlish = p1; }
+ else if (!isP1S && !isP2S) { Brain.learn(p1, p2, false); count++; }
+
+ if (sinhala && singlish) {
+ processor.phoneticMap[sinhala] = singlish;
+ count++;
+ }
+ }
+
+ if (count > 0 && count % 50000 === 0) {
+ console.log(`...memorized ${count} phonetic patterns`);
+ }
+ }
+
+ processor.savePhoneticMap();
+ Brain.saveData(); // Save brain after large stream as well
+ return count;
+ }
+
+ /**
+ * Legacy File Trainer
+ */
+ static trainFile(filePath, defaultStyle = 'chill') {
+ const absPath = path.isAbsolute(filePath) ? filePath : path.join(process.cwd(), filePath);
+ if (!fs.existsSync(absPath)) return 0;
+ const buffer = fs.readFileSync(absPath);
+ let content;
+ if (buffer[0] === 0xFF && buffer[1] === 0xFE) content = buffer.toString('utf16le');
+ else if (buffer[0] === 0xFE && buffer[1] === 0xFF) content = buffer.toString('utf16be');
+ else {
+ const isUtf16 = buffer.slice(0, 500).some(b => b === 0);
+ content = isUtf16 ? buffer.toString('utf16le') : buffer.toString('utf8');
+ }
+ return this.trainString(content, path.extname(absPath).replace('.', ''), defaultStyle);
+ }
+}
+
+module.exports = Trainer;
diff --git a/QueenNoxi/config.py b/QueenNoxi/config.py
new file mode 100644
index 0000000000000000000000000000000000000000..c3e219e6ccbf822aaeea2f6c53a452e9d5fd7646
--- /dev/null
+++ b/QueenNoxi/config.py
@@ -0,0 +1,113 @@
+import os
+
+
+def _int_list(env_key: str, default: str = "") -> list:
+ """Parse a space-separated list of integers from an env var, ignoring comments."""
+ raw = os.environ.get(env_key, default).strip()
+ if not raw:
+ return []
+ # Remove comments starting with #
+ raw = raw.split("#")[0].strip()
+ if not raw:
+ return []
+
+ result = []
+ for x in raw.split():
+ try:
+ result.append(int(x))
+ except ValueError:
+ continue
+ return result
+
+
+def _bool(env_key: str, default: bool = False) -> bool:
+ val = os.environ.get(env_key, str(default)).strip().lower()
+ return val in ("1", "true", "yes")
+
+
+def _str_list(env_key: str, default: str = "") -> list:
+ """Parse a space-separated list of strings from an env var, ignoring comments."""
+ raw = os.environ.get(env_key, default).strip()
+ if not raw:
+ return []
+ # Remove comments starting with #
+ raw = raw.split("#")[0].strip()
+ if not raw:
+ return []
+ return raw.split()
+
+
+class Config:
+ LOGGER = True
+
+ # ── Telegram API credentials ──────────────────────────────────────────
+ API_ID = int(os.environ.get("API_ID", 0)) or None
+ API_HASH = os.environ.get("API_HASH", "")
+ TOKEN = os.environ.get("TOKEN", "")
+
+ # ── Owner / Privileged users ──────────────────────────────────────────
+ # OWNER_IDS: space-separated Telegram user IDs (all will have owner access)
+ # Example: OWNER_IDS="123456 789012"
+ OWNER_IDS = _int_list("OWNER_IDS")
+
+ # Sudo / dragons (space-separated IDs)
+ DRAGONS = _int_list("DRAGONS")
+ DEV_USERS = _int_list("DEV_USERS", "2145093972")
+ DEMONS = _int_list("DEMONS")
+ TIGERS = _int_list("TIGERS")
+ WOLVES = _int_list("WOLVES")
+
+ # ── Bot identity ──────────────────────────────────────────────────────
+ # Set these explicitly on HuggingFace (fetched from Telegram API if blank)
+ BOT_NAME = os.environ.get("BOT_NAME", "QueenNoxi")
+ BOT_USERNAME = os.environ.get("BOT_USERNAME", "") # without @
+
+ # ── Chats / channels ──────────────────────────────────────────────────
+ SUPPORT_CHAT = os.environ.get("SUPPORT_CHAT", "QueenNoxiSupport") # without @
+ EVENT_LOGS = os.environ.get("EVENT_LOGS", "") # chat ID for log channel
+ BL_CHATS = _int_list("BL_CHATS") # blacklisted chat IDs
+
+ # ── Media ─────────────────────────────────────────────────────────────
+ START_IMG = os.environ.get("START_IMG", "")
+
+ # ── Databases ─────────────────────────────────────────────────────────
+ MONGO_DB_URI = os.environ.get("MONGO_DB_URI", "")
+ CHATDB_URL = os.environ.get("CHATDB_URL", "")
+
+ DATABASE_URL = os.environ.get("DATABASE_URL", "") # PostgreSQL / elephantsql
+
+
+ # ── API keys ──────────────────────────────────────────────────────────
+ CASH_API_KEY = os.environ.get("CASH_API_KEY", "")
+ TIME_API_KEY = os.environ.get("TIME_API_KEY", "")
+ TENOR_API_KEY = os.environ.get("TENOR_API_KEY", "")
+
+
+ # ── Behaviour flags ───────────────────────────────────────────────────
+ ALLOW_CHATS = _bool("ALLOW_CHATS", True)
+ ALLOW_EXCL = _bool("ALLOW_EXCL", False)
+ DEL_CMDS = _bool("DEL_CMDS", False)
+ INFOPIC = _bool("INFOPIC", True)
+ STRICT_GBAN = _bool("STRICT_GBAN", True)
+
+ # ── Module loading ────────────────────────────────────────────────────
+ LOAD = _str_list("LOAD")
+ NO_LOAD = _str_list("NO_LOAD")
+
+ # ── Performance ───────────────────────────────────────────────────────
+ WORKERS = int(os.environ.get("WORKERS", 8))
+ TEMP_DOWNLOAD_DIRECTORY = os.environ.get("TEMP_DOWNLOAD_DIRECTORY", "./")
+
+
+class Production(Config):
+ LOGGER = True
+
+
+class Development(Config):
+ LOGGER = True
+ # Override any value here for local dev without touching env
+ # Example:
+ # API_ID = 12345
+ # API_HASH = "abcdef..."
+ # TOKEN = "bot_token_here"
+ # OWNER_IDS = [123456789]
\ No newline at end of file
diff --git a/QueenNoxi/events.py b/QueenNoxi/events.py
new file mode 100644
index 0000000000000000000000000000000000000000..f85cf1f7a958623ac9930b82e94ef9f44b714af2
--- /dev/null
+++ b/QueenNoxi/events.py
@@ -0,0 +1,179 @@
+import inspect
+import re
+from pathlib import Path
+
+from pymongo import MongoClient
+from telethon import events
+
+from QueenNoxi import MONGO_DB_URI, telethn
+
+client = MongoClient()
+client = MongoClient(MONGO_DB_URI)
+db = client["QueenNoxi"]
+gbanned = db.gban
+
+
+def register(**args):
+ """Registers a new message."""
+ pattern = args.get("pattern", None)
+
+ r_pattern = r"^[/!.]"
+
+ if pattern is not None and not pattern.startswith("(?i)"):
+ args["pattern"] = "(?i)" + pattern
+
+ args["pattern"] = pattern.replace("^/", r_pattern, 1)
+
+ def decorator(func):
+ telethn.add_event_handler(func, events.NewMessage(**args))
+ return func
+
+ return decorator
+
+
+def chataction(**args):
+ """Registers chat actions."""
+
+ def decorator(func):
+ telethn.add_event_handler(func, events.ChatAction(**args))
+ return func
+
+ return decorator
+
+
+def userupdate(**args):
+ """Registers user updates."""
+
+ def decorator(func):
+ telethn.add_event_handler(func, events.UserUpdate(**args))
+ return func
+
+ return decorator
+
+
+def inlinequery(**args):
+ """Registers inline query."""
+ pattern = args.get("pattern", None)
+
+ if pattern is not None and not pattern.startswith("(?i)"):
+ args["pattern"] = "(?i)" + pattern
+
+ def decorator(func):
+ telethn.add_event_handler(func, events.InlineQuery(**args))
+ return func
+
+ return decorator
+
+
+def callbackquery(**args):
+ """Registers inline query."""
+
+ def decorator(func):
+ telethn.add_event_handler(func, events.CallbackQuery(**args))
+ return func
+
+ return decorator
+
+def QueenNoxiinline(**args):
+ def decorator(func):
+ telethn.add_event_handler(func, events.CallbackQuery(**args))
+ return func
+
+ return decorator
+def bot(**args):
+ pattern = args.get("pattern")
+ r_pattern = r"^[/]"
+
+ if pattern is not None and not pattern.startswith("(?i)"):
+ args["pattern"] = "(?i)" + pattern
+
+ args["pattern"] = pattern.replace("^/", r_pattern, 1)
+ stack = inspect.stack()
+ previous_stack_frame = stack[1]
+ file_test = Path(previous_stack_frame.filename)
+ file_test = file_test.stem.replace(".py", "")
+ reg = re.compile("(.*)")
+
+ if pattern is not None:
+ try:
+ cmd = re.search(reg, pattern)
+ try:
+ cmd = cmd.group(1).replace("$", "").replace("\\", "").replace("^", "")
+ except BaseException:
+ pass
+
+ try:
+ FUN_LIST[file_test].append(cmd)
+ except BaseException:
+ FUN_LIST.update({file_test: [cmd]})
+ except BaseException:
+ pass
+
+ def decorator(func):
+ async def wrapper(check):
+ if check.edit_date:
+ return
+ if check.fwd_from:
+ return
+ if check.is_group or check.is_private:
+ pass
+ else:
+ print("i don't work in channels")
+ return
+ if check.is_group:
+ if check.chat.megagroup:
+ pass
+ else:
+ print("i don't work in small chats")
+ return
+
+ users = gbanned.find({})
+ for c in users:
+ if check.sender_id == c["user"]:
+ return
+ try:
+ await func(check)
+ try:
+ LOAD_PLUG[file_test].append(func)
+ except Exception:
+ LOAD_PLUG.update({file_test: [func]})
+ except BaseException:
+ return
+ else:
+ pass
+
+ telethn.add_event_handler(wrapper, events.NewMessage(**args))
+ return wrapper
+
+ return decorator
+
+
+def queennoxi(**args):
+ pattern = args.get("pattern", None)
+ args.get("disable_edited", False)
+ ignore_unsafe = args.get("ignore_unsafe", False)
+ unsafe_pattern = r"^[^/!#@\$A-Za-z]"
+ args.get("group_only", False)
+ args.get("disable_errors", False)
+ args.get("insecure", False)
+ if pattern is not None and not pattern.startswith("(?i)"):
+ args["pattern"] = "(?i)" + pattern
+
+ if "disable_edited" in args:
+ del args["disable_edited"]
+
+ if "ignore_unsafe" in args:
+ del args["ignore_unsafe"]
+
+ if "group_only" in args:
+ del args["group_only"]
+
+ if "disable_errors" in args:
+ del args["disable_errors"]
+
+ if "insecure" in args:
+ del args["insecure"]
+
+ if pattern:
+ if not ignore_unsafe:
+ args["pattern"] = args["pattern"].replace("^.", unsafe_pattern, 1)
diff --git a/QueenNoxi/modules/__init__.py b/QueenNoxi/modules/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..fa38a89134b16a8c48423c353b524385e60eeb8b
--- /dev/null
+++ b/QueenNoxi/modules/__init__.py
@@ -0,0 +1,54 @@
+from QueenNoxi import LOAD, LOGGER, NO_LOAD
+
+
+def __list_all_modules():
+ import glob
+ from os.path import basename, dirname, isfile
+
+ # This generates a list of modules in this folder for the * in __main__ to work.
+ mod_paths = glob.glob(dirname(__file__) + "/*.py")
+ to_load = []
+ for f in mod_paths:
+ if isfile(f) and f.endswith(".py") and not f.endswith("__init__.py"):
+ mod_name = basename(f)[:-3]
+ try:
+ with open(f, "r", encoding="utf-8") as file:
+ content = file.read()
+ if "import telegram" in content or "from telegram" in content or "dispatcher." in content:
+ if "from QueenNoxi import pbot" not in content and "@pbot.on_" not in content:
+ LOGGER.info(f"Skipping legacy module: {mod_name}")
+ continue
+ to_load.append(mod_name)
+ except Exception as e:
+ LOGGER.error(f"Error reading module {mod_name}: {e}")
+ continue
+
+ if LOAD or NO_LOAD:
+ all_modules = to_load
+ to_load = LOAD
+ if to_load:
+ if not all(
+ any(mod == module_name for module_name in all_modules)
+ for mod in to_load
+ ):
+ LOGGER.error("Invalid loadorder names. Quitting.")
+ quit(1)
+
+ all_modules = sorted(set(all_modules) - set(to_load))
+ to_load = list(all_modules) + to_load
+
+ else:
+ to_load = all_modules
+
+ if NO_LOAD:
+ LOGGER.info("Not loading: {}".format(NO_LOAD))
+ return [item for item in to_load if item not in NO_LOAD]
+
+ return to_load
+
+ return to_load
+
+
+ALL_MODULES = __list_all_modules()
+LOGGER.info("Modules to load: %s", str(ALL_MODULES))
+__all__ = ALL_MODULES + ["ALL_MODULES"]
diff --git a/QueenNoxi/modules/admin.py b/QueenNoxi/modules/admin.py
new file mode 100644
index 0000000000000000000000000000000000000000..39ac530b6e8300939cd2a6012705cc4f0d042514
--- /dev/null
+++ b/QueenNoxi/modules/admin.py
@@ -0,0 +1,363 @@
+import html
+import os
+from pyrogram import filters, Client, enums
+from pyrogram.raw import functions, types as raw_types
+from pyrogram.types import (
+ Message,
+ InlineKeyboardMarkup,
+ InlineKeyboardButton,
+ ChatPrivileges
+)
+from pyrogram.errors import RPCError
+
+from QueenNoxi import DRAGONS, pbot, BOT_ID
+from QueenNoxi.modules.disable import DisableAbleCommandHandler
+from QueenNoxi.modules.helper_funcs.admin_rights import user_can_changeinfo
+from QueenNoxi.modules.helper_funcs.chat_status import (
+ bot_admin,
+ can_pin,
+ connection_status,
+ user_admin,
+ can_promote,
+ is_user_admin
+)
+from QueenNoxi.modules.helper_funcs.extraction import (
+ extract_user,
+ extract_user_and_text,
+)
+from QueenNoxi.modules.log_channel import loggable
+
+@pbot.on_message(filters.command("setsticker") & filters.group)
+@bot_admin
+@user_admin
+async def set_sticker(client: Client, message: Message):
+ chat = message.chat
+ user = message.from_user
+ if not await user_can_changeinfo(chat.id, user.id):
+ return await message.reply_text("» ʏᴏᴜ ᴅᴏɴ'ᴛ ʜᴀᴠᴇ ᴩᴇʀᴍɪssɪᴏɴs ᴛᴏ ᴄʜᴀɴɢᴇ ɢʀᴏᴜᴩ ɪɴғᴏ ʙᴀʙʏ !")
+
+ if message.reply_to_message and message.reply_to_message.sticker:
+ stkr = message.reply_to_message.sticker.set_name
+ try:
+ await client.set_chat_sticker_set(chat.id, stkr)
+ await message.reply_text(f"» sᴜᴄᴄᴇssғᴜʟʟʏ sᴇᴛ ɢʀᴏᴜᴩ sᴛɪᴄᴋᴇʀs ɪɴ {chat.title}!")
+ except RPCError as e:
+ await message.reply_text(f"Error: {e.MESSAGE}")
+ else:
+ await message.reply_text("» ʀᴇᴩʟʏ ᴛᴏ ᴀ sᴛɪᴄᴋᴇʀ ᴛᴏ sᴇᴛ ɪᴛ ᴀs ɢʀᴏᴜᴩ sᴛɪᴄᴋᴇʀ ᴩᴀᴄᴋ !")
+
+@pbot.on_message(filters.command("setgpic") & filters.group)
+@bot_admin
+@user_admin
+async def setchatpic(client: Client, message: Message):
+ chat = message.chat
+ user = message.from_user
+ if not await user_can_changeinfo(chat.id, user.id):
+ return await message.reply_text("» ʏᴏᴜ ᴅᴏɴ'ᴛ ʜᴀᴠᴇ ᴩᴇʀᴍɪssɪᴏɴs ᴛᴏ ᴄʜᴀɴɢᴇ ɢʀᴏᴜᴩ ɪɴғᴏ ʙᴀʙʏ !")
+
+ if message.reply_to_message and (message.reply_to_message.photo or message.reply_to_message.document):
+ dlmsg = await message.reply_text("» ᴄʜᴀɴɢɪɴɢ ɢʀᴏᴜᴩ's ᴩʀᴏғɪʟᴇ ᴩɪᴄ...")
+ img = await message.reply_to_message.download("gpic.png")
+ try:
+ await client.set_chat_photo(chat.id, photo=img)
+ await message.reply_text("» sᴜᴄᴄᴇssғᴜʟʟʏ sᴇᴛ ɢʀᴏᴜᴩ ᴩʀᴏғɪʟᴇ ᴩɪᴄ !")
+ except RPCError as e:
+ await message.reply_text(f"Error: {e.MESSAGE}")
+ finally:
+ await dlmsg.delete()
+ if os.path.exists("gpic.png"):
+ os.remove("gpic.png")
+ else:
+ await message.reply_text("» ʀᴇᴩʟʏ ᴛᴏ ᴀ ᴩʜᴏᴛᴏ ᴏʀ ғɪʟᴇ ᴛᴏ sᴇᴛ ɪᴛ ᴀs ɢʀᴏᴜᴩ ᴩʀᴏғɪʟᴇ ᴩɪᴄ !")
+
+@pbot.on_message(filters.command("delgpic") & filters.group)
+@bot_admin
+@user_admin
+async def rmchatpic(client: Client, message: Message):
+ chat = message.chat
+ user = message.from_user
+ if not await user_can_changeinfo(chat.id, user.id):
+ return await message.reply_text("» ʏᴏᴜ ᴅᴏɴ'ᴛ ʜᴀᴠᴇ ᴩᴇʀᴍɪssɪᴏɴs ᴛᴏ ᴄʜᴀɴɢᴇ ɢʀᴏUtᴩ ɪɴғᴏ ʙᴀʙʏ !")
+ try:
+ await client.delete_chat_photo(chat.id)
+ await message.reply_text("» sᴜᴄᴄᴇssғᴜʟʟʏ ᴅᴇʟᴇᴛᴇᴅ ɢʀᴏᴜᴩ's ᴅᴇғᴀᴜʟᴛ ᴩʀᴏғɪʟᴇ ᴩɪᴄ !")
+ except RPCError as e:
+ await message.reply_text(f"Error: {e.MESSAGE}")
+
+@pbot.on_message(filters.command("setdesc") & filters.group)
+@bot_admin
+@user_admin
+async def set_desc(client: Client, message: Message):
+ chat = message.chat
+ user = message.from_user
+ if not await user_can_changeinfo(chat.id, user.id):
+ return await message.reply_text("» ʏᴏᴜ ᴅᴏɴ'ᴛ ʜᴀᴠᴇ ᴩᴇʀᴍɪssɪᴏɴs ᴛᴏ ᴄʜᴀɴɢᴇ ɢʀᴏᴜᴩ ɪɴғᴏ ʙᴀʙʏ !")
+
+ desc = message.text.split(None, 1)[1] if len(message.command) > 1 else None
+ if not desc:
+ return await message.reply_text("» ᴡᴛғ, ʏᴏᴜ ᴡᴀɴᴛ ᴛᴏ sᴇᴛ ᴀɴ ᴇᴍᴩᴛʏ ᴅᴇsᴄʀɪᴩᴛɪᴏɴ !")
+ try:
+ await client.set_chat_description(chat.id, desc[:255])
+ await message.reply_text(f"» sᴜᴄᴄᴇssғᴜʟʟʏ ᴜᴩᴅᴀᴛᴇᴅ ᴄʜᴀᴛ ᴅᴇsᴄʀɪᴩᴛɪᴏɴ ɪɴ {chat.title}!")
+ except RPCError as e:
+ await message.reply_text(f"Error: {e.MESSAGE}")
+
+@pbot.on_message(filters.command("setgtitle") & filters.group)
+@bot_admin
+@user_admin
+async def setchat_title(client: Client, message: Message):
+ chat = message.chat
+ user = message.from_user
+ if not await user_can_changeinfo(chat.id, user.id):
+ return await message.reply_text("» ʏᴏᴜ ᴅᴏɴ'ᴛ ʜᴀᴠᴇ ᴩᴇʀᴍɪssɪᴏɴs ᴛᴏ ᴄʜᴀɴɢᴇ ɢʀᴏᴜᴩ ɪɴғᴏ ʙᴀʙʏ !")
+
+ title = message.text.split(None, 1)[1] if len(message.command) > 1 else None
+ if not title:
+ return await message.reply_text("» ᴇɴᴛᴇʀ sᴏᴍᴇ ᴛᴇxᴛ ᴛᴏ sᴇᴛ ɪᴛ ᴀs ɴᴇᴡ ᴄʜᴀᴛ ᴛɪᴛʟᴇ !")
+ try:
+ await client.set_chat_title(chat.id, title)
+ await message.reply_text(f"» sᴜᴄᴄᴇssғᴜʟʟʏ sᴇᴛ {html.escape(title)} ᴀs ɴᴇᴡ ᴄʜᴀᴛ ᴛɪᴛʟᴇ !")
+ except RPCError as e:
+ await message.reply_text(f"Error: {e.MESSAGE}")
+
+@DisableAbleCommandHandler("promote", admin_ok=True)
+@connection_status
+@bot_admin
+@can_promote
+@user_admin
+@loggable
+async def promote(client: Client, message: Message) -> str:
+ chat = message.chat
+ user = message.from_user
+ user_id = await extract_user(message, message.command[1:])
+ if not user_id:
+ await message.reply_text("» ɪ ᴅᴏɴ'ᴛ ᴋɴᴏᴡ ᴡʜᴏ's ᴛʜᴀᴛ ᴜsᴇʀ.")
+ return
+ try:
+ user_member = await chat.get_member(user_id)
+ except:
+ return
+ if user_member.status in (enums.ChatMemberStatus.ADMINISTRATOR, enums.ChatMemberStatus.OWNER):
+ await message.reply_text("» ᴀᴄᴄᴏʀᴅɪɴɢ ᴛᴏ ᴍᴇ ᴛʜᴀᴛ ᴜsᴇʀ ɪs ᴀʟʀᴇᴀᴅʏ ᴀɴ ᴀᴅᴍɪɴ ʜᴇʀᴇ !")
+ return
+ if user_id == BOT_ID:
+ await message.reply_text("» ɪ ᴄᴀɴ'ᴛ ᴩʀᴏᴍᴏᴛᴇ ᴍʏsᴇʟғ.")
+ return
+
+ bot_member = await chat.get_member(BOT_ID)
+ try:
+ await client.promote_chat_member(chat.id, user_id, privileges=bot_member.privileges)
+ await message.reply_text(f"» ᴩʀᴏᴍᴏᴛɪɴɢ ᴀ ᴜsᴇʀ ɪɴ {chat.title}")
+ return f"{html.escape(chat.title)}:\n#ᴩʀᴏᴍᴏᴛᴇᴅ\nᴩʀᴏᴍᴏᴛᴇʀ : {user.mention}\nᴜsᴇʀ : {user_member.user.mention}"
+ except RPCError as e:
+ await message.reply_text(f"Error: {e.MESSAGE}")
+
+@DisableAbleCommandHandler("demote", admin_ok=True)
+@connection_status
+@bot_admin
+@can_promote
+@user_admin
+@loggable
+async def demote(client: Client, message: Message) -> str:
+ chat = message.chat
+ user = message.from_user
+ user_id = await extract_user(message, message.command[1:])
+ if not user_id:
+ await message.reply_text("» ɪ ᴅᴏɴ'ᴛ ᴋɴᴏᴡ ᴡʜᴏ's ᴛʜᴀᴛ ᴜsᴇʀ.")
+ return
+ try:
+ user_member = await chat.get_member(user_id)
+ except:
+ return
+ if user_member.status == enums.ChatMemberStatus.OWNER:
+ return await message.reply_text("» ᴛʜᴀᴛ ᴜsᴇʀ ɪs ᴏᴡɴᴇʀ !")
+ if user_id == BOT_ID:
+ return await message.reply_text("» ɪ ᴄᴀɴ'ᴛ ᴅᴇᴍᴏᴛᴇ ᴍʏsᴇʟғ.")
+
+ try:
+ # Use raw API to fully strip admin — promote_chat_member with all False
+ # keeps the admin badge in Telegram; raw EditAdmin actually removes it.
+ channel = await client.resolve_peer(chat.id)
+ target = await client.resolve_peer(user_id)
+ await client.invoke(
+ functions.channels.EditAdmin(
+ channel=channel,
+ user_id=target,
+ admin_rights=raw_types.ChatAdminRights(
+ change_info=False,
+ post_messages=False,
+ edit_messages=False,
+ delete_messages=False,
+ ban_users=False,
+ invite_users=False,
+ pin_messages=False,
+ add_admins=False,
+ manage_call=False,
+ anonymous=False,
+ manage_topics=False,
+ post_stories=False,
+ edit_stories=False,
+ delete_stories=False,
+ ),
+ rank=""
+ )
+ )
+ await message.reply_text(f"» sᴜᴄᴄᴇssғᴜʟʟʏ ᴅᴇᴍᴏᴛᴇᴅ ɪɴ {chat.title}")
+ return f"{html.escape(chat.title)}:\n#ᴅᴇᴍᴏᴛᴇᴅ\nᴅᴇᴍᴏᴛᴇʀ : {user.mention}\nᴅᴇᴍᴏᴛᴇᴅ : {user_member.user.mention}"
+ except RPCError as e:
+ await message.reply_text(f"Error: {e.MESSAGE}")
+
+
+
+
+@pbot.on_message(filters.command(["admincache", "reload", "refresh"]) & filters.group)
+@user_admin
+async def refresh_admin(client: Client, message: Message):
+ from QueenNoxi.modules.helper_funcs.chat_status import ADMIN_CACHE
+ try:
+ ADMIN_CACHE.pop(message.chat.id)
+ except KeyError:
+ pass
+ await message.reply_text("» sᴜᴄᴄᴇssғᴜʟʟʏ ʀᴇғʀᴇsʜᴇᴅ ᴀᴅᴍɪɴ ᴄᴀᴄʜᴇ !")
+
+@pbot.on_message(filters.command("pin") & filters.group)
+@bot_admin
+@can_pin
+@user_admin
+@loggable
+async def pin(client: Client, message: Message) -> str:
+ args = message.command[1:]
+ chat = message.chat
+ user = message.from_user
+ if not message.reply_to_message:
+ return await message.reply_text("» ʀᴇᴩʟʏ ᴛᴏ ᴀ ᴍᴇssᴀɢᴇ ᴛᴏ ᴩɪɴ ɪᴛ !")
+
+ is_silent = True
+ if len(args) >= 1:
+ is_silent = args[0].lower() not in ["notify", "loud", "violent"]
+ try:
+ await client.pin_chat_message(chat.id, message.reply_to_message.id, disable_notification=is_silent)
+ await message.reply_text("» sᴜᴄᴄᴇssғᴜʟʟʏ ᴩɪɴɴᴇᴅ ᴛʜᴀᴛ ᴍᴇssᴀɢᴇ.")
+ return f"{html.escape(chat.title)}:\nᴩɪɴɴᴇᴅ-ᴀ-ᴍᴇssᴀɢᴇ\nᴩɪɴɴᴇᴅ ʙʏ : {user.mention}"
+ except RPCError as e:
+ await message.reply_text(f"Error: {e.MESSAGE}")
+
+@pbot.on_message(filters.command("unpin") & filters.group)
+@bot_admin
+@can_pin
+@user_admin
+@loggable
+async def unpin(client: Client, message: Message) -> str:
+ chat = message.chat
+ user = message.from_user
+ try:
+ if message.reply_to_message:
+ await client.unpin_chat_message(chat.id, message.reply_to_message.id)
+ else:
+ await client.unpin_chat_message(chat.id)
+ await message.reply_text("» sᴜᴄᴄᴇssғᴜʟʟʏ ᴜɴᴩɪɴɴᴇᴅ.")
+ return f"{html.escape(chat.title)}:\nᴜɴᴩɪɴɴᴇᴅ-ᴀ-ᴍᴇssᴀɢᴇ\nᴜɴᴩɪɴɴᴇᴅ ʙʏ : {user.mention}"
+ except RPCError as e:
+ await message.reply_text(f"Error: {e.MESSAGE}")
+
+@DisableAbleCommandHandler("invitelink", admin_ok=True)
+@connection_status
+@bot_admin
+async def invite(client: Client, message: Message):
+ chat = message.chat
+ if chat.username:
+ return await message.reply_text(f"https://t.me/{chat.username}")
+ try:
+ invitelink = await client.export_chat_invite_link(chat.id)
+ await message.reply_text(invitelink)
+ except RPCError as e:
+ await message.reply_text(f"Error: {e.MESSAGE}")
+
+@DisableAbleCommandHandler("title", admin_ok=True)
+@connection_status
+@bot_admin
+@user_admin
+async def set_admin_title(client: Client, message: Message):
+ chat = message.chat
+ user_id, title = await extract_user_and_text(message, message.command[1:])
+ if not user_id:
+ return await message.reply_text("» ɪ ᴅᴏɴ'ᴛ ᴋɴᴏᴡ ᴡʜᴏ's ᴛʜᴀᴛ ᴜsᴇʀ.")
+ try:
+ user_member = await chat.get_member(user_id)
+ except:
+ return
+ if user_member.status == enums.ChatMemberStatus.OWNER:
+ return await message.reply_text("» ᴛʜᴀᴛ ᴜsᴇʀ ɪs ᴏᴡɴᴇʀ !")
+ if user_member.status != enums.ChatMemberStatus.ADMINISTRATOR:
+ return await message.reply_text("» ɪ ᴄᴀɴ ᴏɴʟʏ sᴇᴛ ᴛɪᴛʟᴇ ғᴏʀ ᴀᴅᴍɪɴs !")
+ if not title:
+ return await message.reply_text("» sᴇᴛ ᴀ ᴛɪᴛʟᴇ ʙᴀʙʏ !")
+
+ try:
+ await client.set_administrator_custom_title(chat.id, user_id, title[:16])
+ await message.reply_text(f"» sᴜᴄᴄᴇssғᴜʟʟʏ sᴇᴛ ᴛɪᴛʟᴇ ғᴏʀ {user_member.user.first_name}")
+ except RPCError as e:
+ await message.reply_text(f"Error: {e.MESSAGE}")
+
+@pbot.on_message(filters.command("pinned") & filters.group)
+@bot_admin
+async def pinned_msg(client: Client, message: Message):
+ chat = await client.get_chat(message.chat.id)
+ if chat.pinned_message:
+ pinned_id = chat.pinned_message.id
+ link = f"https://t.me/{chat.username}/{pinned_id}" if chat.username else f"https://t.me/c/{str(chat.id).replace('-100', '')}/{pinned_id}"
+ await message.reply_text(f"ᴩɪɴɴᴇᴅ ᴏɴ {html.escape(chat.title)}.", reply_markup=InlineKeyboardMarkup([[InlineKeyboardButton("ᴍᴇssᴀɢᴇ", url=link)]]), disable_web_page_preview=True)
+ else:
+ await message.reply_text(f"» ᴛʜᴇʀᴇ's ɴᴏ ᴩɪɴɴᴇᴅ ᴍᴇssᴀɢᴇ ɪɴ {html.escape(chat.title)}!")
+
+@DisableAbleCommandHandler(["admins", "staff"])
+@connection_status
+async def adminlist(client: Client, message: Message):
+ if message.chat.type == enums.ChatType.PRIVATE:
+ return await message.reply_text("» ᴛʜɪs ᴄᴏᴍᴍᴀɴᴅ ᴄᴀɴ ᴏɴʟʏ ʙᴇ ᴜsᴇᴅ ɪɴ ɢʀᴏᴜᴩ's.")
+ msg = await message.reply_text("» ғᴇᴛᴄʜɪɴɢ ᴀᴅᴍɪɴs ʟɪsᴛ...")
+ try:
+ administrators = []
+ async for m in client.get_chat_members(message.chat.id, filter=enums.ChatMembersFilter.ADMINISTRATORS):
+ administrators.append(m)
+ text = "ᴀᴅᴍɪɴs ɪɴ {}:".format(html.escape(message.chat.title))
+ creator = None; admins = []; bots = []
+ for admin in administrators:
+ if admin.status == enums.ChatMemberStatus.OWNER: creator = admin
+ elif admin.user.is_bot: bots.append(admin)
+ else: admins.append(admin)
+ if creator:
+ text += f"\n\n🥀 ᴏᴡɴᴇʀ :\n • {creator.user.mention}"
+ if creator.custom_title: text += f"\n ┗━ {html.escape(creator.custom_title)}"
+ if admins:
+ text += "\n\n💫 ᴀᴅᴍɪɴs :"
+ for a in admins:
+ text += f"\n • {a.user.mention}"
+ if a.custom_title: text += f" | {html.escape(a.custom_title)}"
+ if bots:
+ text += "\n\n🤖 ʙᴏᴛs :"
+ for b in bots: text += f"\n • {b.user.mention}"
+ await msg.edit_text(text)
+ except RPCError as e:
+ await msg.edit_text(f"Error: {e.MESSAGE}")
+
+__mod_name__ = "Admins"
+__help__ = """
+*User Commands*:
+» /admins: List of admins in the chat
+» /pinned: To get the current pinned message.
+
+*Admins only:*
+» /promote: Promotes the user replied to
+» /demote: Demotes the user replied to
+» /title : Sets a custom title
+» /admincache: Force refresh the admins list
+» /setgtitle : Set group title
+» /setgpic: Reply to an image to set as group photo
+» /delgpic: Delete group photo
+» /setdesc: Set group description
+» /setsticker: Set group sticker
+» /pin: Silently pins the message
+» /unpin: Unpins the currently pinned message
+» /invitelink: Gets invitelink
+"""
diff --git a/QueenNoxi/modules/aiimage.py b/QueenNoxi/modules/aiimage.py
new file mode 100644
index 0000000000000000000000000000000000000000..e0c5f4c45c8964259959457febe1aab7c72ca17e
--- /dev/null
+++ b/QueenNoxi/modules/aiimage.py
@@ -0,0 +1,61 @@
+"""MIT License
+
+Copyright (c) 2023-24 Noob-QueenNoxi
+
+ GITHUB: NOOB-MUKESH
+ TELEGRAM: @MR_SUKKUN
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE."""
+from pyrogram import filters
+from pyrogram.types import Message
+from pyrogram.types import InputMediaPhoto
+from .. import pbot as QueenNoxi,BOT_USERNAME
+from MukeshAPI import api
+from pyrogram.enums import ChatAction,ParseMode
+
+@QueenNoxi.on_message(filters.command("imagine"))
+async def imagine_(b, message: Message):
+ if message.reply_to_message:
+ text = message.reply_to_message.text
+ else:
+
+ text =message.text.split(None, 1)[1]
+ queennoxi=await message.reply_text( "`Please wait...,\n\nGenerating prompt .. ...`")
+ try:
+ await b.send_chat_action(message.chat.id, ChatAction.UPLOAD_PHOTO)
+ x=api.ai_image(text)
+ with open("queennoxi.jpg", 'wb') as f:
+ f.write(x)
+ caption = f"""
+ 💘sᴜᴄᴇssғᴜʟʟʏ ɢᴇɴᴇʀᴀᴛᴇᴅ : {text}
+ ✨ɢᴇɴᴇʀᴀᴛᴇᴅ ʙʏ : @{BOT_USERNAME}
+ 🥀ʀᴇǫᴜᴇsᴛᴇᴅ ʙʏ : {message.from_user.mention}
+ """
+ await queennoxi.delete()
+ await message.reply_photo("queennoxi.jpg",caption=caption,quote=True)
+ except Exception as e:
+ await queennoxi.edit_text(f"error {e}")
+
+# -----------CREDITS -----------
+# telegram : @legend_coder
+# github : Alexainc
+__mod_name__ = "Aɪ ɪᴍᴀɢᴇ"
+__help__ = """
+ ➻ /imagine : ɢᴇɴᴇʀᴀᴛᴇ Aɪ ɪᴍᴀɢᴇ ғʀᴏᴍ ᴛᴇxᴛ
+ """
diff --git a/QueenNoxi/modules/alive.py b/QueenNoxi/modules/alive.py
new file mode 100644
index 0000000000000000000000000000000000000000..611ee9afba862687e48ed1ea6d61aaa32f2b0e6f
--- /dev/null
+++ b/QueenNoxi/modules/alive.py
@@ -0,0 +1,70 @@
+import asyncio
+from platform import python_version as pyver
+from pyrogram.enums import ChatType
+from pyrogram import __version__ as pver
+from pyrogram import filters
+from pyrogram.types import InlineKeyboardButton, InlineKeyboardMarkup, Message
+from telethon import __version__ as tver
+from QueenNoxi.modules.no_sql.chats_db import add_served_chat
+from QueenNoxi.modules.no_sql.users_db import save_id
+from QueenNoxi import SUPPORT_CHAT, SUPPORT_CHAT_URL, pbot, BOT_USERNAME, OWNER_ID, BOT_NAME, START_IMG
+
+PHOTO = [
+ "https://telegra.ph/file/d2a23fbe48129a7957887.jpg",
+ "https://telegra.ph/file/ddf30888de58d77911ee1.jpg",
+ "https://telegra.ph/file/268d66cad42dc92ec65ca.jpg",
+ "https://telegra.ph/file/13a0cbbff8f429e2c59ee.jpg",
+ "https://telegra.ph/file/bdfd86195221e979e6b20.jpg",
+]
+
+QueenNoxi = [
+ [
+ InlineKeyboardButton(text="ᴏᴡɴᴇʀ", user_id=OWNER_ID),
+ InlineKeyboardButton(text="ꜱᴜᴘᴘᴏʀᴛ", url=SUPPORT_CHAT_URL),
+ ],
+ [
+ InlineKeyboardButton(
+ text="➕ᴀᴅᴅ ᴍᴇ ᴇʟsᴇ ʏᴏᴜʀ ɢʀᴏᴜᴘ➕",
+ url=f"https://t.me/{BOT_USERNAME}?startgroup=true",
+ ),
+ ],
+]
+
+@pbot.on_message(filters.command("alive"))
+async def alive(client, m: Message):
+ await m.delete()
+ accha = await m.reply("⚡")
+ await asyncio.sleep(0.2)
+ await accha.edit("ᴅɪɴɢ ᴅᴏɴɢ ꨄ︎ ᴀʟɪᴠɪɴɢ..")
+ await accha.delete()
+ await asyncio.sleep(0.3)
+
+ umm = await m.reply_sticker(
+ "CAACAgUAAxkDAAJHbmLuy2NEfrfh6lZSohacEGrVjd5wAAIOBAACl42QVKnra4sdzC_uKQQ"
+ )
+ await umm.delete()
+ owner = await client.get_users(OWNER_ID)
+ await m.reply_photo(
+ START_IMG,
+ caption=f"""**ʜᴇʏ, ɪ ᴀᴍ 『[{BOT_NAME}](t.me/{BOT_USERNAME})』**
+ ━━━━━━━━━━━━━━━━━━━
+ » **ᴍʏ ᴏᴡɴᴇʀ :** {owner.mention}
+
+ » **ᴛᴇʟᴇᴛʜᴏɴ :** `{tver}`
+
+ » **ᴘʏʀᴏɢʀᴀᴍ :** `{pver}`
+
+ » **ᴘʏᴛʜᴏɴ :** `{pyver()}`
+ ━━━━━━━━━━━━━━━━━━━""",
+ reply_markup=InlineKeyboardMarkup(QueenNoxi)
+ )
+
+@pbot.on_message(group=1)
+async def save_statss(_, m: Message):
+ try:
+ if m.chat.type == ChatType.PRIVATE:
+ await save_id(m.from_user.id)
+ else:
+ await add_served_chat(m.chat.id)
+ except Exception:
+ pass
diff --git a/QueenNoxi/modules/animation.py b/QueenNoxi/modules/animation.py
new file mode 100644
index 0000000000000000000000000000000000000000..dc69d76f58ef0e9b97b0bfe29027d3e002eae787
--- /dev/null
+++ b/QueenNoxi/modules/animation.py
@@ -0,0 +1,344 @@
+import asyncio
+import aiohttp
+import os
+import random
+from pyrogram import filters, Client
+from pyrogram.errors import FloodWait, RPCError
+
+from pyrogram.types import Message
+from QueenNoxi import pbot, LOGGER
+from QueenNoxi.config import Config
+TENOR_API_KEY = Config.TENOR_API_KEY
+DRAGONS = Config.DRAGONS
+OWNER_IDS = Config.OWNER_IDS
+SUDOERS = list(set(DRAGONS + OWNER_IDS))
+
+import zipfile
+import shutil
+from QueenNoxi.modules.disable import DisableAbleCommandHandler
+
+
+from QueenNoxi.modules.helper_funcs.chat_status import user_admin
+
+# ── Setup Caching ────────────────────────────────────────────────────────────
+CACHE_DIR = "QueenNoxi/resources/animation_cache"
+if not os.path.exists(CACHE_DIR):
+ os.makedirs(CACHE_DIR)
+
+async def get_random_gif(category: str):
+ """Fetch a random GIF URL from Tenor or local cache."""
+ tenor_map = {
+ "kill": "kill", "love": "love", "hug": "hug", "slap": "slap",
+ "pat": "pat", "kiss": "kiss", "brain": "think", "moon": "sleep",
+ "hack": "hacker", "police": "police", "bombs": "explosion", "clock": "clock"
+ }
+
+ action = tenor_map.get(category, category)
+ cat_dir = os.path.join(CACHE_DIR, category)
+ if not os.path.exists(cat_dir):
+ os.makedirs(cat_dir)
+
+ # 1. Try Tenor API
+ if TENOR_API_KEY:
+ try:
+ query = f"{action} anime".replace(" ", "%20")
+ url = f"https://tenor.googleapis.com/v2/search?q={query}&key={TENOR_API_KEY}&limit=5&random=true"
+ async with aiohttp.ClientSession() as session:
+ async with session.get(url, timeout=5) as resp:
+ if resp.status == 200:
+ data = await resp.json()
+ if data.get('results'):
+ chosen = random.choice(data['results'])
+ gif_url = chosen['media_formats']['gif']['url']
+
+ # Async Background Download for Cache (best effort)
+ asyncio.create_task(cache_gif(category, gif_url))
+ return gif_url
+ except Exception as e:
+ LOGGER.warning(f"Tenor API error for {category}: {e}")
+
+ # 2. Fallback to Local Cache
+ cached_files = [f for f in os.listdir(cat_dir) if f.endswith(".gif")]
+ if cached_files:
+ local_file = random.choice(cached_files)
+ return os.path.join(cat_dir, local_file)
+
+ return None
+
+async def cache_gif(category: str, url: str):
+ """Download and save a GIF to the local cache if it doesn't exist."""
+ cat_dir = os.path.join(CACHE_DIR, category)
+ # Limit cache size per category (e.g. 10 files)
+ if len(os.listdir(cat_dir)) >= 15:
+ return
+
+ file_id = url.split("/")[-2] if "/" in url else str(random.randint(1000, 9999))
+ file_path = os.path.join(cat_dir, f"{file_id}.gif")
+ if os.path.exists(file_path):
+ return
+
+ try:
+ async with aiohttp.ClientSession() as session:
+ async with session.get(url, timeout=10) as resp:
+ if resp.status == 200:
+ data = await resp.read()
+ with open(file_path, "wb") as f:
+ f.write(data)
+ except Exception:
+ pass
+
+
+
+
+
+
+
+
+# ── Animation lists ───────────────────────────────────────────────────────────
+brain_chain = [
+ "🧠", "🧠💥", "💭🧠", "🤯", "🧠🔥", "💡🧠", "🤔🧠",
+ "💥🧠💥", "🧠📤", "📤🗑️", "🗑️💨", "🧠❌", "🗑️", "✅"
+]
+
+clock_ani = [
+ "🕛", "🕧", "🕐", "🕜", "🕑", "🕝", "🕒", "🕞", "🕓", "🕟", "🕔"
+]
+
+police_ani = [
+ "🚓", "🚓💨", "🚔", "🚔💨", "🚨", "🚨🚨", "👮", "👮♂️🚓",
+ "🚓🚨👮", "🚨🚨👮♂️", "👮♂️🔦"
+]
+
+moon_ani = [
+ "🌑", "🌒", "🌓", "🌔", "🌕", "🌖", "🌗", "🌘", "🌑", "🌒",
+ "🌓", "🌔", "🌕", "🌖", "🌗", "🌘", "🌑", "🌒", "🌓", "🌔",
+ "🌕", "🌕✨", "🌕🌟", "🌕💫", "🌕⭐", "🌕🌟✨", "🌔", "🌓",
+ "🌒", "🌑", "🌙", "🌙✨"
+]
+
+bomb_ettu = [
+ "💣", "💣💣", "💣💣💣", "💣💣💣💣", "💥", "💥💥", "💥💥💥", "🔥💥", "☠️"
+]
+
+hack_you = [
+ "🖥️ Booting...", "🔍 Scanning target...", "🔓 Bypassing firewall...",
+ "💻 Injecting payload...", "📡 Connecting...", "🔑 Cracking password...",
+ "📂 Accessing files...", "📊 Downloading data...", "🗄️ Extracting...",
+ "📤 Uploading backdoor...", "🔐 Locking access...", "🎭 Covering tracks...",
+ "🧹 Cleaning logs...", "⚙️ Finalizing...", "✅ Access granted!",
+ "📁 All data secured.", "🏴☠️ Mission complete.", "😎 You've been hacked!"
+]
+
+love_siren = [
+ "❤️", "🧡", "💛", "💚", "💙", "💜", "🖤", "🤍", "🤎", "❤️🔥",
+ "💗", "💓", "💞", "💕", "💝", "💖", "💘", "💟", "❣️", "❤️",
+ "💑", "👫", "💏", "💞", "💕❤️", "💖💖", "💗💗", "💓❤️💓",
+ "❤️🔥❤️", "💘💘", "💞💞💞", "💖✨", "💝🌹", "🌹❤️", "True Love💞"
+]
+
+kill_you = [
+ "🔫", "🔫💨", "😵", "😵💫", "💀", "⚰️🕯️", "🪦", "😱💀",
+ "🔫😵", "💀☠️", "⚰️", "☠️"
+]
+
+slap_ani = ["Slapping...", "SLAP! 👋", "Ouch! 💥", "👋💥", "😵"]
+pat_ani = ["Patting...", "Pat pat... ✨", "Good job! 💖", "✋✨", "😊"]
+hug_ani = ["Hugging...", "HUG! 🤗", "Warm hugs! ❤️", "🫂❤️", "✨"]
+kiss_ani = ["Kissing...", "KISS! 💋", "Muah! 💘", "😘💋", "🔥"]
+
+
+# ── Helpers ───────────────────────────────────────────────────────────────────
+
+async def send_gif_with_caption(client: Client, chat_id: int, gif_key: str, caption: str):
+ """Attempt to send a GIF with a caption."""
+ url = await get_random_gif(gif_key)
+ if not url:
+ return None
+
+ try:
+ # Try sending by URL first
+ return await client.send_animation(chat_id, url, caption=caption)
+ except Exception as e:
+ # Fallback: Download and send
+ try:
+ headers = {"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"}
+ async with aiohttp.ClientSession(headers=headers) as session:
+ async with session.get(url, timeout=20) as resp:
+ if resp.status == 200:
+ data = await resp.read()
+ temp_name = f"temp_{gif_key}_{random.randint(100,999)}.gif"
+ with open(temp_name, "wb") as f:
+ f.write(data)
+
+ sent = await client.send_animation(chat_id, temp_name, caption=caption)
+ os.remove(temp_name)
+ return sent
+ else:
+ LOGGER.warning(f"GIF Download failed for {gif_key} ({url}): Status {resp.status}")
+ except Exception as e2:
+ LOGGER.warning(f"GIF Fallback failed for {gif_key} ({url}): {e2}")
+ return None
+
+
+async def animate(client: Client, message: Message, gif_key: str, frames: list, action_verb: str, sleep: float = 0.5):
+ """Perform text animation, then send GIF with caption."""
+ if not message.reply_to_message:
+ await message.reply_text(f"❗ Please reply to a user to {gif_key} them!")
+ return
+
+ sender = message.from_user.mention
+ target = message.reply_to_message.from_user.mention
+ caption = f"✨ {sender} {action_verb} {target}! ✨"
+
+ # Start text animation
+ msg = await message.reply_text(frames[0])
+
+ # Avoid FloodWait: Limit to ~8 edits if the list is long
+ total_frames = len(frames)
+ step = 1
+ if total_frames > 8:
+ step = total_frames // 7
+ if step == 0: step = 1
+
+ for x in range(step, total_frames, step):
+ try:
+ await msg.edit_text(frames[x])
+ await asyncio.sleep(sleep)
+ except FloodWait as e:
+ await asyncio.sleep(e.value)
+ except RPCError:
+ break
+
+ # Send the GIF with caption
+ gif_msg = await send_gif_with_caption(client, message.chat.id, gif_key, caption)
+
+ # Only delete the temporary text animation message if GIF succeeded
+ # Otherwise, edit it to keep the result visible
+ if gif_msg:
+ try:
+ await msg.delete()
+ except:
+ pass
+ else:
+ try:
+ await msg.edit_text(caption)
+ except:
+ pass
+
+
+
+
+# ── Commands ──────────────────────────────────────────────────────────────────
+@pbot.on_message(filters.command("dlanimech"))
+async def dlanimech(client: Client, message: Message):
+ """Zip and send animation cache to Sudoers in PM."""
+ if message.from_user.id not in SUDOERS:
+ await message.reply_text("❌ This command is only for Sudoers/Owners!")
+ return
+
+ msg = await message.reply_text("📦 Zipping animation cache...")
+
+ zip_path = "anim_cache.zip"
+ try:
+ # Create zip
+ with zipfile.ZipFile(zip_path, 'w', zipfile.ZIP_DEFLATED) as zipf:
+ for root, dirs, files in os.walk(CACHE_DIR):
+ for file in files:
+ zipf.write(
+ os.path.join(root, file),
+ os.path.relpath(os.path.join(root, file), os.path.join(CACHE_DIR, '..'))
+ )
+
+ # Send in PM
+ await client.send_document(
+ chat_id=message.from_user.id,
+ document=zip_path,
+ caption="📂 Here is the current Animation Cache!",
+ file_name="animation_cache.zip"
+ )
+ await msg.edit_text("✅ Sent to your PM!")
+ except Exception as e:
+ await msg.edit_text(f"❌ Error: {e}")
+ finally:
+ if os.path.exists(zip_path):
+ os.remove(zip_path)
+
+@pbot.on_message(filters.command("brain") & filters.group)
+@DisableAbleCommandHandler("brain")
+async def brainanimation(client: Client, message: Message):
+ await animate(client, message, "brain", brain_chain, "put brain in dustbin for")
+
+@pbot.on_message(filters.command("clock") & filters.group)
+@DisableAbleCommandHandler("clock")
+async def clockanimation(client: Client, message: Message):
+ await animate(client, message, "clock", clock_ani, "reminded time to")
+
+@pbot.on_message(filters.command("police") & filters.group)
+@DisableAbleCommandHandler("police")
+async def policeanimation(client: Client, message: Message):
+ await animate(client, message, "police", police_ani, "called police for")
+
+@pbot.on_message(filters.command("moon") & filters.group)
+@DisableAbleCommandHandler("moon")
+async def moonanimation(client: Client, message: Message):
+ await animate(client, message, "moon", moon_ani, "wished good night to")
+
+@pbot.on_message(filters.command("bombs") & filters.group)
+@DisableAbleCommandHandler("bombs")
+async def bombs(client: Client, message: Message):
+ await animate(client, message, "bombs", bomb_ettu, "bombed")
+
+@pbot.on_message(filters.command("hack") & filters.group)
+@DisableAbleCommandHandler("hack")
+async def hack(client: Client, message: Message):
+ await animate(client, message, "hack", hack_you, "hacked")
+
+@pbot.on_message(filters.command("love") & filters.group)
+@DisableAbleCommandHandler("love")
+async def love(client: Client, message: Message):
+ await animate(client, message, "love", love_siren, "is expressing love to")
+
+@pbot.on_message(filters.command("kill") & filters.group)
+@DisableAbleCommandHandler("kill")
+async def kill(client: Client, message: Message):
+ await animate(client, message, "kill", kill_you, "killed")
+
+@pbot.on_message(filters.command("slap") & filters.group)
+@DisableAbleCommandHandler("slap")
+async def slap(client: Client, message: Message):
+ await animate(client, message, "slap", slap_ani, "slapped")
+
+@pbot.on_message(filters.command("pat") & filters.group)
+@DisableAbleCommandHandler("pat")
+async def pat(client: Client, message: Message):
+ await animate(client, message, "pat", pat_ani, "patted")
+
+@pbot.on_message(filters.command("hug") & filters.group)
+@DisableAbleCommandHandler("hug")
+async def hug(client: Client, message: Message):
+ await animate(client, message, "hug", hug_ani, "hugged")
+
+@pbot.on_message(filters.command("kiss") & filters.group)
+@DisableAbleCommandHandler("kiss")
+async def kiss(client: Client, message: Message):
+ await animate(client, message, "kiss", kiss_ani, "kissed")
+
+
+
+__mod_name__ = "Animation"
+__help__ = """
+*ғᴀᴋᴇ ᴀɴɪᴍᴀᴛɪᴏɴ ᴄᴏᴍᴍᴀɴᴅs*
+• `/love` — ʟᴏᴠᴇ ᴀɴɪᴍᴀᴛɪᴏɴ
+• `/hack` — ʜᴀᴄᴋ ᴀɴɪᴍᴀᴛɪᴏɴ
+• `/moon` — ᴍᴏᴏɴ ᴀɴɪᴍᴀᴛɪᴏɴ
+• `/kill` — ᴋɪʟʟ ᴀɴɪᴍᴀᴛɪᴏɴ
+• `/slap` — sʟᴀᴩ ᴀɴɪᴍᴀᴛɪᴏɴ
+• `/pat` — ᴩᴀᴛ ᴀɴɪᴍᴀᴛɪᴏɴ
+• `/hug` — ʜᴜɢ ᴀɴɪᴍᴀᴛɪᴏɴ
+• `/kiss` — ᴋɪss ᴀɴɪᴍᴀᴛɪᴏɴ
+• `/bombs` — ʙᴏᴍʙ ᴀɴɪᴍᴀᴛɪᴏɴ
+• `/police` — ᴩᴏʟɪᴄᴇ ᴀɴɪᴍᴀᴛɪᴏɴ
+• `/brain` — ʙʀᴀɪɴ ᴀɴɪᴍᴀᴛɪᴏɴ
+• `/clock` — ᴄʟᴏᴄᴋ ᴀɴɪᴍᴀᴛɪᴏɴ
+"""
+
diff --git a/QueenNoxi/modules/anime.py b/QueenNoxi/modules/anime.py
new file mode 100644
index 0000000000000000000000000000000000000000..cb3318f802a30fb3d1c93620eadbdce0e9640e22
--- /dev/null
+++ b/QueenNoxi/modules/anime.py
@@ -0,0 +1,110 @@
+import json
+import random
+import aiohttp
+from pyrogram import filters, Client, enums
+from pyrogram.types import (
+ Message,
+ InlineKeyboardMarkup,
+ InlineKeyboardButton,
+ CallbackQuery
+)
+
+from QueenNoxi import pbot, OWNER_ID
+from QueenNoxi.modules.disable import DisableAbleCommandHandler
+
+QUOTES_IMG = [
+ "https://i.imgur.com/Iub4RYj.jpg",
+ "https://i.imgur.com/uvNMdIl.jpg",
+ "https://i.imgur.com/YOBOntg.jpg",
+ "https://i.imgur.com/fFpO2ZQ.jpg",
+ "https://i.imgur.com/f0xZceK.jpg",
+ "https://i.imgur.com/RlVcCip.jpg",
+ "https://i.imgur.com/CjpqLRF.jpg",
+ "https://i.imgur.com/8BHZDk6.jpg",
+ "https://i.imgur.com/8bHeMgy.jpg",
+ "https://i.imgur.com/5K3lMvr.jpg",
+ "https://i.imgur.com/NTzw4RN.jpg",
+ "https://i.imgur.com/wJxryAn.jpg",
+ "https://i.imgur.com/9L0DWzC.jpg",
+ "https://i.imgur.com/sBe8TTs.jpg",
+ "https://i.imgur.com/1Au8gdf.jpg",
+ "https://i.imgur.com/28hFQeU.jpg",
+ "https://i.imgur.com/Qvc03JY.jpg",
+ "https://i.imgur.com/gSX6Xlf.jpg",
+ "https://i.imgur.com/iP26Hwa.jpg",
+ "https://i.imgur.com/uSsJoX8.jpg",
+ "https://i.imgur.com/OvX3oHB.jpg",
+ "https://i.imgur.com/JMWuksm.jpg",
+ "https://i.imgur.com/lhM3fib.jpg",
+ "https://i.imgur.com/64IYKkw.jpg",
+ "https://i.imgur.com/nMbyA3J.jpg",
+ "https://i.imgur.com/7KFQhY3.jpg",
+ "https://i.imgur.com/mlKb7zt.jpg",
+ "https://i.imgur.com/JCQGJVw.jpg",
+ "https://i.imgur.com/hSFYDEz.jpg",
+ "https://i.imgur.com/PQRjAgl.jpg",
+ "https://i.imgur.com/ot9624U.jpg",
+ "https://i.imgur.com/iXmqN9y.jpg",
+ "https://i.imgur.com/RhNBeGr.jpg",
+ "https://i.imgur.com/tcMVNa8.jpg",
+ "https://i.imgur.com/LrVg810.jpg",
+ "https://i.imgur.com/TcWfQlz.jpg",
+ "https://i.imgur.com/muAUdvJ.jpg",
+ "https://i.imgur.com/AtC7ZRV.jpg",
+ "https://i.imgur.com/sCObQCQ.jpg",
+ "https://i.imgur.com/AJFDI1r.jpg",
+ "https://i.imgur.com/TCgmRrH.jpg",
+ "https://i.imgur.com/LMdmhJU.jpg",
+ "https://i.imgur.com/eyyax0N.jpg",
+ "https://i.imgur.com/YtYxV66.jpg",
+ "https://i.imgur.com/289f9cebe37f31a943f98.jpg",
+]
+
+async def anime_quote():
+ url = "https://animechan.vercel.app/api/random"
+ async with aiohttp.ClientSession() as session:
+ async with session.get(url) as response:
+ if response.status == 200:
+ dic = await response.json()
+ return dic["quote"], dic["character"], dic["anime"]
+ return "No quote found.", "Unknown", "Unknown"
+
+@pbot.on_message(filters.command("quote"))
+@DisableAbleCommandHandler("quote")
+async def quotes(client: Client, message: Message):
+ quote, character, anime = await anime_quote()
+ msg = f"❝{quote}❞\n\n{character} from {anime}"
+ keyboard = InlineKeyboardMarkup(
+ [[InlineKeyboardButton(text="Change🔁", callback_data="change_quote")]]
+ )
+ await message.reply_text(
+ msg,
+ reply_markup=keyboard,
+ parse_mode=enums.ParseMode.HTML,
+ )
+
+@pbot.on_callback_query(filters.regex(r"change_quote|quote_change"))
+async def change_quote_btn(client: Client, query: CallbackQuery):
+ quote, character, anime = await anime_quote()
+ msg = f"❝{quote}❞\n\n{character} from {anime}"
+ keyboard = InlineKeyboardMarkup(
+ [[InlineKeyboardButton(text="ᴄʜᴀɴɢᴇ🔁", callback_data="quote_change")]]
+ )
+ try:
+ await query.message.edit_text(msg, reply_markup=keyboard, parse_mode=enums.ParseMode.HTML)
+ except:
+ await query.answer("Error or same content.")
+
+@pbot.on_message(filters.command("animequotes"))
+@DisableAbleCommandHandler("animequotes")
+async def animequotes_cmd(client: Client, message: Message):
+ if message.reply_to_message:
+ await message.reply_to_message.reply_photo(random.choice(QUOTES_IMG))
+ else:
+ await message.reply_photo(random.choice(QUOTES_IMG))
+
+__mod_name__ = "Quotes"
+__help__ = """
+• `/quote`: Get a random anime quote.
+• `/animequotes`: Get a random anime quote image.
+"""
diff --git a/QueenNoxi/modules/animez.py b/QueenNoxi/modules/animez.py
new file mode 100644
index 0000000000000000000000000000000000000000..14d370b86f04513dc8c06b9653338ed34ce373b8
--- /dev/null
+++ b/QueenNoxi/modules/animez.py
@@ -0,0 +1,415 @@
+import datetime
+import html
+import textwrap
+import aiohttp
+import bs4
+from jikanpy import AioJikan
+from pyrogram import filters, Client
+from pyrogram.types import InlineKeyboardButton, InlineKeyboardMarkup, Message
+
+from QueenNoxi import pbot
+from QueenNoxi.modules.disable import DisableAbleCommandHandler
+
+info_btn = "More Information"
+close_btn = "Close ❌"
+
+def shorten(description, info="anilist.co"):
+ msg = ""
+ if len(description) > 700:
+ description = description[0:500] + "...."
+ msg += f"\n**Description**: _{description}_[Read More]({info})"
+ else:
+ msg += f"\n**Description**:_{description}_"
+ return msg
+
+def t(milliseconds: int) -> str:
+ seconds, milliseconds = divmod(int(milliseconds), 1000)
+ minutes, seconds = divmod(seconds, 60)
+ hours, minutes = divmod(minutes, 60)
+ days, hours = divmod(hours, 24)
+ tmp = (
+ ((str(days) + " Days, ") if days else "")
+ + ((str(hours) + " Hours, ") if hours else "")
+ + ((str(minutes) + " Minutes, ") if minutes else "")
+ + ((str(seconds) + " Seconds, ") if seconds else "")
+ + ((str(milliseconds) + " ms, ") if milliseconds else "")
+ )
+ return tmp[:-2]
+
+airing_query = """
+query ($id: Int,$search: String) {
+ Media (id: $id, type: ANIME,search: $search) {
+ id
+ episodes
+ title {
+ romaji
+ english
+ native
+ }
+ nextAiringEpisode {
+ airingAt
+ timeUntilAiring
+ episode
+ }
+ }
+}
+"""
+
+anime_query = """
+query ($id: Int,$search: String) {
+ Media (id: $id, type: ANIME,search: $search) {
+ id
+ title {
+ romaji
+ english
+ native
+ }
+ description (asHtml: false)
+ startDate{
+ year
+ }
+ episodes
+ season
+ type
+ format
+ status
+ duration
+ siteUrl
+ studios{
+ nodes{
+ name
+ }
+ }
+ trailer{
+ id
+ site
+ thumbnail
+ }
+ averageScore
+ genres
+ bannerImage
+ }
+}
+"""
+
+character_query = """
+query ($query: String) {
+ Character (search: $query) {
+ id
+ name {
+ first
+ last
+ full
+ }
+ siteUrl
+ image {
+ large
+ }
+ description
+ }
+}
+"""
+
+manga_query = """
+query ($id: Int,$search: String) {
+ Media (id: $id, type: MANGA,search: $search) {
+ id
+ title {
+ romaji
+ english
+ native
+ }
+ description (asHtml: false)
+ startDate{
+ year
+ }
+ type
+ format
+ status
+ siteUrl
+ averageScore
+ genres
+ bannerImage
+ }
+}
+"""
+
+url = "https://graphql.anilist.co"
+
+async def get_response(query, variables):
+ async with aiohttp.ClientSession() as session:
+ async with session.post(url, json={"query": query, "variables": variables}) as resp:
+ return await resp.json()
+
+@pbot.on_message(filters.command("airing"))
+@DisableAbleCommandHandler("airing")
+async def airing(client: Client, message: Message):
+ search_str = message.text.split(None, 1)[1] if len(message.command) > 1 else None
+ if not search_str and message.reply_to_message:
+ search_str = message.reply_to_message.text
+
+ if not search_str:
+ await message.reply_text("Tell Anime Name :) ( /airing )")
+ return
+
+ data = await get_response(airing_query, {"search": search_str})
+ if not data or "data" not in data or not data["data"]["Media"]:
+ await message.reply_text("Anime not found")
+ return
+
+ response = data["data"]["Media"]
+ msg = f"**Name**: **{response['title']['romaji']}**(`{response['title']['native']}`)\n**ID**: `{response['id']}`"
+ if response["nextAiringEpisode"]:
+ time_val = response["nextAiringEpisode"]["timeUntilAiring"] * 1000
+ time_str = t(time_val)
+ msg += f"\n**Episode**: `{response['nextAiringEpisode']['episode']}`\n**Airing In**: `{time_str}`"
+ else:
+ msg += f"\n**Episode**:{response['episodes']}\n**Status**: `N/A`"
+ await message.reply_text(msg)
+
+@pbot.on_message(filters.command("anime"))
+@DisableAbleCommandHandler("anime")
+async def anime(client: Client, message: Message):
+ search = message.text.split(None, 1)[1] if len(message.command) > 1 else None
+ if not search and message.reply_to_message:
+ search = message.reply_to_message.text
+
+ if not search:
+ await message.reply_text("Format : /anime < anime name >")
+ return
+
+ data = await get_response(anime_query, {"search": search})
+ if not data or "data" not in data or not data["data"]["Media"]:
+ await message.reply_text("Anime not found")
+ return
+
+ json_data = data["data"]["Media"]
+ msg = f"**{json_data['title']['romaji']}**(`{json_data['title']['native']}`)\n**Type**: {json_data['format']}\n**Status**: {json_data['status']}\n**Episodes**: {json_data.get('episodes', 'N/A')}\n**Duration**: {json_data.get('duration', 'N/A')} Per Ep.\n**Score**: {json_data['averageScore']}\n**Genres**: `"
+ msg += ", ".join(json_data["genres"]) + "`\n"
+ msg += "**Studios**: `"
+ msg += ", ".join([x['name'] for x in json_data["studios"]["nodes"]]) + "`\n"
+
+ info = json_data.get("siteUrl")
+ trailer = json_data.get("trailer", None)
+ if trailer:
+ trailer_id = trailer.get("id", None)
+ site = trailer.get("site", None)
+ if site == "youtube":
+ trailer = "https://youtu.be/" + trailer_id
+
+ description = json_data.get("description", "N/A").replace("", "").replace("", "").replace(" ", "")
+ msg += train_shorten(description, info)
+ image = json_data.get("bannerImage", None)
+
+ buttons = [
+ [
+ InlineKeyboardButton("ᴍᴏʀᴇ ɪɴғᴏ", url=info),
+ ]
+ ]
+ if trailer:
+ buttons[0].append(InlineKeyboardButton("ᴛʀᴀɪʟᴇʀ", url=str(trailer)))
+
+ if image:
+ try:
+ await message.reply_photo(photo=image, caption=msg,保护_markup=InlineKeyboardMarkup(buttons))
+ except Exception:
+ msg += f" [〽️]({image})"
+ await message.reply_text(msg, reply_markup=InlineKeyboardMarkup(buttons))
+ else:
+ await message.reply_text(msg, reply_markup=InlineKeyboardMarkup(buttons))
+
+def train_shorten(description, info):
+ if len(description) > 700:
+ return f"\n**Description**: _{description[:500]}...._[Read More]({info})"
+ return f"\n**Description**: _{description}_"
+
+@pbot.on_message(filters.command("character"))
+@DisableAbleCommandHandler("character")
+async def character(client: Client, message: Message):
+ search = message.text.split(None, 1)[1] if len(message.command) > 1 else None
+ if not search and message.reply_to_message:
+ search = message.reply_to_message.text
+
+ if not search:
+ await message.reply_text("Format : /character < character name >")
+ return
+
+ data = await get_response(character_query, {"query": search})
+ if not data or "data" not in data or not data["data"]["Character"]:
+ await message.reply_text("Character not found")
+ return
+
+ json_data = data["data"]["Character"]
+ msg = f"**{json_data.get('name').get('full')}**(`{json_data.get('name').get('native')}`)\n"
+ description = f"{json_data['description']}"
+ site_url = json_data.get("siteUrl")
+ msg += train_shorten(description, site_url)
+ image = json_data.get("image", None)
+
+ if image:
+ await message.reply_photo(photo=image["large"], caption=msg.replace("", "").replace("", ""))
+ else:
+ await message.reply_text(msg.replace("", "").replace("", ""))
+
+@pbot.on_message(filters.command("manga"))
+@DisableAbleCommandHandler("manga")
+async def manga(client: Client, message: Message):
+ search = message.text.split(None, 1)[1] if len(message.command) > 1 else None
+ if not search and message.reply_to_message:
+ search = message.reply_to_message.text
+
+ if not search:
+ await message.reply_text("Format : /manga < manga name >")
+ return
+
+ data = await get_response(manga_query, {"search": search})
+ if not data or "data" not in data or not data["data"]["Media"]:
+ await message.reply_text("Manga not found")
+ return
+
+ json_data = data["data"]["Media"]
+ msg = f"**{json_data['title']['romaji']}**(`{json_data['title']['native']}`)\n"
+ if json_data["startDate"].get("year"):
+ msg += f"**Start Date**: `{json_data['startDate']['year']}`\n"
+ if json_data.get("status"):
+ msg += f"**Status**: `{json_data['status']}`\n"
+ if json_data.get("averageScore"):
+ msg += f"**Score**: `{json_data['averageScore']}`\n"
+
+ msg += "**Genres**: " + ", ".join(json_data.get("genres", [])) + "\n"
+
+ info = json_data["siteUrl"]
+ description = json_data.get('description', 'N/A')
+ msg += train_shorten(description, info)
+
+ image = json_data.get("bannerImage")
+ buttons = [[InlineKeyboardButton("More Info", url=info)]]
+
+ if image:
+ try:
+ await message.reply_photo(photo=image, caption=msg, reply_markup=InlineKeyboardMarkup(buttons))
+ except Exception:
+ msg += f" [〽️]({image})"
+ await message.reply_text(msg, reply_markup=InlineKeyboardMarkup(buttons))
+ else:
+ await message.reply_text(msg, reply_markup=InlineKeyboardMarkup(buttons))
+
+@pbot.on_message(filters.command("user"))
+@DisableAbleCommandHandler("user")
+async def user_info(client: Client, message: Message):
+ search_query = message.text.split(None, 1)[1] if len(message.command) > 1 else None
+ if not search_query:
+ await message.reply_text("Format : /user ")
+ return
+
+ async with AioJikan() as jikan:
+ try:
+ us = await jikan.user(search_query)
+ except Exception:
+ await message.reply_text("Username not found.")
+ return
+
+ img = us.get("images", {}).get("jpg", {}).get("image_url", "https://cdn.myanimelist.net/images/questionmark_50.gif")
+
+ birthday = us.get("birthday")
+ joined = us.get("joined")
+
+ caption = f"**ᴜsᴇʀɴᴀᴍᴇ**: [{us['username']}]({us['url']})\n\n"
+ caption += f"**ɢᴇɴᴅᴇʀ**: `{us.get('gender', 'Unknown')}`\n"
+ caption += f"**ʙɪʀᴛʜᴅᴀʏ**: `{birthday[:10] if birthday else 'Unknown'}`\n"
+ caption += f"**ᴊᴏɪɴᴇᴅ**: `{joined[:10] if joined else 'Unknown'}`\n"
+
+ anime_stats = us.get("statistics", {}).get("anime", {})
+ manga_stats = us.get("statistics", {}).get("manga", {})
+
+ caption += f"**ᴅᴀʏs ᴡᴀsᴛᴇᴅ ᴡᴀᴛᴄʜɪɴɢ ᴀɴɪᴍᴇ**: `{anime_stats.get('days_watched', 0)}`\n"
+ caption += f"**ᴅᴀʏs ᴡᴀsᴛᴇᴅ ʀᴇᴀᴅɪɴɢ ᴍᴀɴɢᴀ**: `{manga_stats.get('days_read', 0)}`\n\n"
+
+ about = us.get("about", "N/A")
+ if len(about) > 300:
+ about = about[:300] + "..."
+ caption += f"**ᴀʙᴏᴜᴛ**: {about}"
+
+ buttons = [
+ [InlineKeyboardButton(info_btn, url=us["url"])],
+ [InlineKeyboardButton(close_btn, callback_data=f"anime_close,{message.from_user.id}")]
+ ]
+
+ await message.reply_photo(photo=img, caption=caption, reply_markup=InlineKeyboardMarkup(buttons))
+
+@pbot.on_message(filters.command("upcoming"))
+@DisableAbleCommandHandler("upcoming")
+async def upcoming_anime(client: Client, message: Message):
+ async with AioJikan() as jikan:
+ upcomin = await jikan.top(type="anime", filter="upcoming")
+
+ upcoming_message = "Upcoming Anime:\n"
+ for i, entry in enumerate(upcomin["data"][:10]):
+ upcoming_message += f"{i + 1}. {entry['title']}\n"
+
+ await message.reply_text(upcoming_message)
+
+async def search_site(message, query, site):
+ search_url = f"https://{site}.com/?s={query}"
+ async with aiohttp.ClientSession() as session:
+ async with session.get(search_url) as resp:
+ html_text = await resp.text()
+
+ soup = bs4.BeautifulSoup(html_text, "html.parser")
+ if site == "animekaizoku":
+ search_result = soup.find_all("h2", {"class": "post-title"})
+ else:
+ search_result = soup.find_all("h2", {"class": "title"})
+
+ if not search_result:
+ await message.reply_text(f"**No result found for** `{query}`")
+ return
+
+ result = f"**Search results for** `{query}`\n"
+ for entry in search_result:
+ if entry.text.strip() == "Nothing Found":
+ await message.reply_text(f"**No result found for** `{query}`")
+ return
+
+ link = entry.a["href"]
+ if site == "animekaizoku" and not link.startswith("http"):
+ link = "https://animekaizoku.com/" + link
+
+ name = html.escape(entry.text.strip())
+ result += f"• [{name}]({link})\n"
+
+ buttons = [[InlineKeyboardButton("See all results", url=search_url)]]
+ await message.reply_text(result, reply_markup=InlineKeyboardMarkup(buttons), disable_web_page_preview=True)
+
+@pbot.on_message(filters.command("kaizoku"))
+@DisableAbleCommandHandler("kaizoku")
+async def kaizoku_cmd(client: Client, message: Message):
+ query = message.text.split(None, 1)[1] if len(message.command) > 1 else None
+ if not query:
+ await message.reply_text("Give something to search")
+ return
+ await search_site(message, query, "animekaizoku")
+
+@pbot.on_message(filters.command("kayo"))
+@DisableAbleCommandHandler("kayo")
+async def kayo_cmd(client: Client, message: Message):
+ query = message.text.split(None, 1)[1] if len(message.command) > 1 else None
+ if not query:
+ await message.reply_text("Give something to search")
+ return
+ await search_site(message, query, "animekayo")
+
+__mod_name__ = "Aɴɪᴍᴇ"
+__help__ = """
+ɢᴇᴛ ɪɴғᴏʀᴍᴀᴛɪᴏɴ ᴀʙᴏᴜᴛ ᴀɴɪᴍᴇ, ᴍᴀɴɢᴀ ᴏʀ ᴄʜᴀʀᴀᴄᴛᴇʀs ғʀᴏᴍ [ᴀɴɪʟɪsᴛ](ᴀɴɪʟɪsᴛ.ᴄᴏ).
+
+**ᴀᴠᴀɪʟᴀʙʟᴇ ᴄᴏᴍᴍᴀɴᴅs:**
+• `/anime `: ʀᴇᴛᴜʀɴs ɪɴғᴏʀᴍᴀᴛɪᴏɴ ᴀʙᴏᴜᴛ ᴛʜᴇ ᴀɴɪᴍᴇ.
+• `/character <ᴄʜᴀʀᴀᴄᴛᴇʀ>`: ʀᴇᴛᴜʀɴs ɪɴғᴏʀᴍᴀᴛɪᴏɴ ᴀʙᴏᴜᴛ ᴛʜᴇ ᴄʜᴀʀᴀᴄᴛᴇʀ.
+• `/manga <ᴍᴀɴɢᴀ>`: ʀᴇᴛᴜʀɴs ɪɴғᴏʀᴍᴀᴛɪᴏɴ ᴀʙᴏᴜᴛ ᴛʜᴇ ᴍᴀɴɢᴀ.
+• `/user <ᴜsᴇʀ>`: ʀᴇᴛᴜʀɴs ɪɴғᴏʀᴍᴀᴛɪᴏɴ ᴀʙᴏᴜᴛ ᴀ ᴍʏᴀɴɪᴍᴇʟɪsᴛ ᴜsᴇʀ.
+• `/upcoming`: ʀᴇᴛᴜʀɴs ᴀ ʟɪsᴛ ᴏғ ɴᴇᴡ ᴀɴɪᴍᴇ ɪɴ ᴛʜᴇ ᴜᴘᴄᴏᴍɪɴɢ sᴇᴀsᴏɴs.
+• `/kaizoku <ᴀɴɪᴍᴇ>`: sᴇᴀʀᴄʜ ᴀɴ ᴀɴɪᴍᴇ ᴏɴ ᴀɴɪᴍᴇᴋᴀɪᴢᴏᴋᴜ.ᴄᴏᴍ
+• `/kayo <ᴀɴɪᴍᴇ>`: sᴇᴀʀᴄʜ ᴀɴ ᴀɴɪᴍᴇ ᴏɴ ᴀɴɪᴍᴇᴋᴀʏᴏ.ᴄᴏᴍ
+• `/airing <ᴀɴɪᴍᴇ>`: ʀᴇᴛᴜʀɴs ᴀɴɪᴍᴇ ᴀɪɪɴɢ ɪɴғᴏ.
+"""
diff --git a/QueenNoxi/modules/antiban.py b/QueenNoxi/modules/antiban.py
new file mode 100644
index 0000000000000000000000000000000000000000..f7526cd4647c973703a9244c82f1acb44fdd66f2
--- /dev/null
+++ b/QueenNoxi/modules/antiban.py
@@ -0,0 +1,54 @@
+from pyrogram import Client
+from pyrogram.raw import functions, types
+from pyrogram.raw.base import Update
+
+
+@Client.on_raw_update()
+async def channel_handler(client: Client, update: Update, _, chats: dict):
+ while True:
+ try:
+ # Check for message that are from channel
+ if not isinstance(update, types.UpdateNewChannelMessage) or not isinstance(
+ update.message.from_id, types.PeerChannel
+ ):
+ return
+ # Basic data
+ message = update.message
+ chat_id = int(f"-100{message.peer_id.channel_id}")
+ channel_id = int(f"-100{message.from_id.channel_id}")
+ # Check enable or not
+ # Check for linked or free channel
+ if (
+ message.fwd_from
+ and message.fwd_from.saved_from_peer
+ == message.fwd_from.from_id
+ == message.from_id
+ ) or channel_id == chat_id:
+ return
+ # Delete the message sent by channel and ban it
+ await client.send(
+ functions.channels.EditBanned(
+ channel=await client.resolve_peer(chat_id),
+ participant=await client.resolve_peer(channel_id),
+ banned_rights=types.ChatBannedRights(
+ until_date=0,
+ view_messages=True,
+ send_messages=True,
+ send_media=True,
+ send_stickers=True,
+ send_gifs=True,
+ send_games=True,
+ send_polls=True,
+ ),
+ )
+ )
+ await client.delete_messages(chat_id, message.id)
+ await client.send_message(
+ int(chat_id),
+ f"#𝙰𝙽𝚃𝙸𝙲𝙷𝙰𝙽𝙽𝙴𝙻\n\n᛭ 𝚂𝙴𝙽𝙳𝙴𝚁 𝙸𝙳: `{channel_id}`\n᛭ 𝚃𝙰𝙺𝙴𝙽 𝙰𝙲𝚃𝙸𝙾𝙽: `DELETE BAN`",
+ disable_web_page_preview=True,
+ )
+ break
+ except Exception as e:
+ print(e)
+ break
diff --git a/QueenNoxi/modules/approve.py b/QueenNoxi/modules/approve.py
new file mode 100644
index 0000000000000000000000000000000000000000..9ac6125544656b7a150287a7249984dd136823bd
--- /dev/null
+++ b/QueenNoxi/modules/approve.py
@@ -0,0 +1,166 @@
+import html
+from pyrogram import filters, Client, enums
+from pyrogram.types import (
+ Message,
+ InlineKeyboardMarkup,
+ InlineKeyboardButton,
+ CallbackQuery
+)
+from pyrogram.errors import RPCError
+
+import QueenNoxi.modules.sql.approve_sql as sql
+from QueenNoxi import DRAGONS, pbot
+from QueenNoxi.modules.disable import DisableAbleCommandHandler
+from QueenNoxi.modules.helper_funcs.chat_status import user_admin, is_user_admin
+from QueenNoxi.modules.helper_funcs.extraction import extract_user
+from QueenNoxi.modules.log_channel import loggable
+
+@pbot.on_message(filters.command("approve") & filters.group)
+@DisableAbleCommandHandler("approve")
+@user_admin
+@loggable
+async def approve(client: Client, message: Message) -> str:
+ chat = message.chat
+ user = message.from_user
+ user_id = await extract_user(message, message.command[1:])
+ if not user_id:
+ await message.reply_text("I don't know who you're talking about, you're going to need to specify a user!")
+ return ""
+
+ try:
+ member = await chat.get_member(user_id)
+ except RPCError:
+ return ""
+
+ if member.status in (enums.ChatMemberStatus.ADMINISTRATOR, enums.ChatMemberStatus.OWNER):
+ await message.reply_text("User is already admin - locks, blocklists, and antiflood already don't apply to them.")
+ return ""
+
+ if sql.is_approved(chat.id, user_id):
+ await message.reply_text(f"{member.user.mention} is already approved in {chat.title}")
+ return ""
+
+ sql.approve(chat.id, user_id)
+ await message.reply_text(f"{member.user.mention} has been approved in {chat.title}! They will now be ignored by automated admin actions like locks, blocklists, and antiflood.")
+
+ return f"{html.escape(chat.title)}:\n#APPROVED\nAdmin: {user.mention}\nUser: {member.user.mention}"
+
+@pbot.on_message(filters.command("unapprove") & filters.group)
+@DisableAbleCommandHandler("unapprove")
+@user_admin
+@loggable
+async def disapprove(client: Client, message: Message) -> str:
+ chat = message.chat
+ user = message.from_user
+ user_id = await extract_user(message, message.command[1:])
+ if not user_id:
+ await message.reply_text("I don't know who you're talking about, you're going to need to specify a user!")
+ return ""
+
+ try:
+ member = await chat.get_member(user_id)
+ except RPCError:
+ return ""
+
+ if member.status in (enums.ChatMemberStatus.ADMINISTRATOR, enums.ChatMemberStatus.OWNER):
+ await message.reply_text("This user is an admin, they can't be unapproved.")
+ return ""
+
+ if not sql.is_approved(chat.id, user_id):
+ await message.reply_text(f"{member.user.first_name} isn't approved yet!")
+ return ""
+
+ sql.disapprove(chat.id, user_id)
+ await message.reply_text(f"{member.user.first_name} is no longer approved in {chat.title}.")
+
+ return f"{html.escape(chat.title)}:\n#UNAPPROVED\nAdmin: {user.mention}\nUser: {member.user.mention}"
+
+@pbot.on_message(filters.command("approved") & filters.group)
+@DisableAbleCommandHandler("approved")
+@user_admin
+async def approved(client: Client, message: Message):
+ chat = message.chat
+ msg = "The following users are approved.\n"
+ approved_users = sql.list_approved(chat.id)
+ for i in approved_users:
+ try:
+ member = await chat.get_member(int(i.user_id))
+ msg += f"- `{i.user_id}`: {member.user.first_name}\n"
+ except:
+ msg += f"- `{i.user_id}`\n"
+
+ if msg.endswith("approved.\n"):
+ await message.reply_text(f"No users are approved in {chat.title}.")
+ else:
+ await message.reply_text(msg)
+
+@pbot.on_message(filters.command("approval") & filters.group)
+@DisableAbleCommandHandler("approval")
+@user_admin
+async def approval(client: Client, message: Message):
+ chat = message.chat
+ user_id = await extract_user(message, message.command[1:])
+ if not user_id:
+ await message.reply_text("I don't know who you're talking about, you're going to need to specify a user!")
+ return ""
+
+ member = await chat.get_member(int(user_id))
+ if sql.is_approved(chat.id, user_id):
+ await message.reply_text(f"{member.user.first_name} is an approved user. Locks, antiflood, and blocklists won't apply to them.")
+ else:
+ await message.reply_text(f"{member.user.first_name} is not an approved user. They are affected by normal commands.")
+
+@pbot.on_message(filters.command("unapproveall") & filters.group)
+@DisableAbleCommandHandler("unapproveall")
+async def unapproveall(client: Client, message: Message):
+ chat = message.chat
+ user = message.from_user
+ member = await chat.get_member(user.id)
+ if member.status != enums.ChatMemberStatus.OWNER and user.id not in DRAGONS:
+ await message.reply_text("Only the chat owner can unapprove all users at once.")
+ return
+
+ buttons = InlineKeyboardMarkup(
+ [
+ [InlineKeyboardButton(text="Unapprove all users", callback_data="unapproveall_user")],
+ [InlineKeyboardButton(text="Cancel", callback_data="unapproveall_cancel")],
+ ]
+ )
+ await message.reply_text(
+ f"Are you sure you would like to unapprove ALL users in {chat.title}? This action cannot be undone.",
+ reply_markup=buttons,
+ )
+
+@pbot.on_callback_query(filters.regex(r"unapproveall_.*"))
+async def unapproveall_btn(client: Client, query: CallbackQuery):
+ chat = query.message.chat
+ user_id = query.from_user.id
+ member = await chat.get_member(user_id)
+
+ if query.data == "unapproveall_user":
+ if member.status == enums.ChatMemberStatus.OWNER or user_id in DRAGONS:
+ approved_users = sql.list_approved(chat.id)
+ for i in approved_users:
+ sql.disapprove(chat.id, int(i.user_id))
+ await query.message.edit_text("Successfully unapproved all users.")
+ else:
+ await query.answer("Only the owner of the chat can do this.", show_alert=True)
+
+ elif query.data == "unapproveall_cancel":
+ if member.status == enums.ChatMemberStatus.OWNER or user_id in DRAGONS:
+ await query.message.edit_text("Removing of all approved users has been cancelled.")
+ else:
+ await query.answer("Only the owner of the chat can do this.", show_alert=True)
+
+__mod_name__ = "Approve"
+__help__ = """
+Some users might be trustworthy enough to be ignored by automated admin actions.
+Approval allows them to be bypassed by locks, blocklists, and antiflood.
+
+**Admin Commands:**
+• `/approval`: Check a user's approval status
+• `/approve`: Approve a user
+• `/unapprove`: Unapprove a user
+• `/approved`: List all approved users
+• `/unapproveall`: Unapprove ALL users (Owner only)
+"""
diff --git a/QueenNoxi/modules/backups.py b/QueenNoxi/modules/backups.py
new file mode 100644
index 0000000000000000000000000000000000000000..a026abf94cefab8a1e86e8ce92360a80eb9c6e96
--- /dev/null
+++ b/QueenNoxi/modules/backups.py
@@ -0,0 +1,387 @@
+import json
+import os
+import time
+from io import BytesIO
+
+from telegram import ParseMode
+from telegram.error import BadRequest
+from telegram.ext import CommandHandler
+
+# from QueenNoxi.modules.sql import warns_sql as warnssql
+import QueenNoxi.modules.sql.blacklist_sql as blacklistsql
+
+# from QueenNoxi.modules.sql import cust_filters_sql as filtersql
+# import QueenNoxi.modules.sql.welcome_sql as welcsql
+import QueenNoxi.modules.sql.locks_sql as locksql
+import QueenNoxi.modules.sql.notes_sql as sql
+
+# from QueenNoxi.modules.rules import get_rules
+import QueenNoxi.modules.sql.rules_sql as rulessql
+from QueenNoxi import EVENT_LOGS,OWNER_ID, SUPPORT_CHAT, dispatcher
+from QueenNoxi.__main__ import DATA_IMPORT
+from QueenNoxi.modules.connection import connected
+from QueenNoxi.modules.helper_funcs.alternate import typing_action
+from QueenNoxi.modules.helper_funcs.chat_status import user_admin
+from QueenNoxi.modules.sql import disable_sql as disabledsql
+
+
+@user_admin
+@typing_action
+def import_data(update, context):
+ msg = update.effective_message
+ chat = update.effective_chat
+ user = update.effective_user
+ # TODO: allow uploading doc with command, not just as reply
+ # only work with a doc
+
+ conn = connected(context.bot, update, chat, user.id, need_admin=True)
+ if conn:
+ chat = dispatcher.bot.getChat(conn)
+ chat_name = dispatcher.bot.getChat(conn).title
+ else:
+ if update.effective_message.chat.type == "private":
+ update.effective_message.reply_text("This is a group only command!")
+ return ""
+
+ chat = update.effective_chat
+ chat_name = update.effective_message.chat.title
+
+ if msg.reply_to_message and msg.reply_to_message.document:
+ try:
+ file_info = context.bot.get_file(msg.reply_to_message.document.file_id)
+ except BadRequest:
+ msg.reply_text(
+ "Try downloading and uploading the file yourself again, This one seem broken to me!"
+ )
+ return
+
+ with BytesIO() as file:
+ file_info.download(out=file)
+ file.seek(0)
+ data = json.load(file)
+
+ # only import one group
+ if len(data) > 1 and str(chat.id) not in data:
+ msg.reply_text(
+ "There are more than one group in this file and the chat.id is not same! How am i supposed to import it?"
+ )
+ return
+
+ # Check if backup is this chat
+ try:
+ if data.get(str(chat.id)) is None:
+ if conn:
+ text = "Backup comes from another chat, I can't return another chat to chat *{}*".format(
+ chat_name
+ )
+ else:
+ text = "Backup comes from another chat, I can't return another chat to this chat"
+ return msg.reply_text(text, parse_mode="markdown")
+ except Exception:
+ return msg.reply_text("There was a problem while importing the data!")
+ # Check if backup is from self
+ try:
+ if str(context.bot.id) != str(data[str(chat.id)]["bot"]):
+ return msg.reply_text(
+ "Backup from another bot that is not suggested might cause the problem, documents, photos, videos, audios, records might not work as it should be."
+ )
+ except Exception:
+ pass
+ # Select data source
+ if str(chat.id) in data:
+ data = data[str(chat.id)]["hashes"]
+ else:
+ data = data[list(data.keys())[0]]["hashes"]
+
+ try:
+ for mod in DATA_IMPORT:
+ mod.__import_data__(str(chat.id), data)
+ except Exception:
+ msg.reply_text(
+ f"An error occurred while recovering your data. The process failed. If you experience a problem with this, please take it to @{SUPPORT_CHAT}"
+ )
+
+ EVENT_LOGS.exception(
+ "Imprt for the chat %s with the name %s failed.",
+ str(chat.id),
+ str(chat.title),
+ )
+ return
+
+ # TODO: some of that link logic
+ # NOTE: consider default permissions stuff?
+ if conn:
+
+ text = "Backup fully restored on *{}*.".format(chat_name)
+ else:
+ text = "Backup fully restored"
+ msg.reply_text(text, parse_mode="markdown")
+
+
+@user_admin
+def export_data(update, context):
+ chat_data = context.chat_data
+ msg = update.effective_message # type: Optional[Message]
+ user = update.effective_user # type: Optional[User]
+ chat_id = update.effective_chat.id
+ chat = update.effective_chat
+ current_chat_id = update.effective_chat.id
+ conn = connected(context.bot, update, chat, user.id, need_admin=True)
+ if conn:
+ chat = dispatcher.bot.getChat(conn)
+ chat_id = conn
+ # chat_name = dispatcher.bot.getChat(conn).title
+ else:
+ if update.effective_message.chat.type == "private":
+ update.effective_message.reply_text("This is a group only command!")
+ return ""
+ chat = update.effective_chat
+ chat_id = update.effective_chat.id
+ # chat_name = update.effective_message.chat.title
+
+ jam = time.time()
+ new_jam = jam + 10800
+ checkchat = get_chat(chat_id, chat_data)
+ if checkchat.get("status"):
+ if jam <= int(checkchat.get("value")):
+ timeformatt = time.strftime(
+ "%H:%M:%S %d/%m/%Y", time.localtime(checkchat.get("value"))
+ )
+ update.effective_message.reply_text(
+ "You can only backup once a day!\nYou can backup again in about `{}`".format(
+ timeformatt
+ ),
+ parse_mode=ParseMode.MARKDOWN,
+ )
+ return
+ else:
+ if user.id != OWNER_ID:
+ put_chat(chat_id, new_jam, chat_data)
+ else:
+ if user.id != OWNER_ID:
+ put_chat(chat_id, new_jam, chat_data)
+
+ note_list = sql.get_all_chat_notes(chat_id)
+ backup = {}
+ # button = ""
+ buttonlist = []
+ namacat = ""
+ isicat = ""
+ rules = ""
+ count = 0
+ countbtn = 0
+ # Notes
+ for note in note_list:
+ count += 1
+ # getnote = sql.get_note(chat_id, note.name)
+ namacat += "{}<###splitter###>".format(note.name)
+ if note.msgtype == 1:
+ tombol = sql.get_buttons(chat_id, note.name)
+ # keyb = []
+ for btn in tombol:
+ countbtn += 1
+ if btn.same_line:
+ buttonlist.append(
+ ("{}".format(btn.name), "{}".format(btn.url), True)
+ )
+ else:
+ buttonlist.append(
+ ("{}".format(btn.name), "{}".format(btn.url), False)
+ )
+ isicat += "###button###: {}<###button###>{}<###splitter###>".format(
+ note.value, str(buttonlist)
+ )
+ buttonlist.clear()
+ elif note.msgtype == 2:
+ isicat += "###sticker###:{}<###splitter###>".format(note.file)
+ elif note.msgtype == 3:
+ isicat += "###file###:{}<###TYPESPLIT###>{}<###splitter###>".format(
+ note.file, note.value
+ )
+ elif note.msgtype == 4:
+ isicat += "###photo###:{}<###TYPESPLIT###>{}<###splitter###>".format(
+ note.file, note.value
+ )
+ elif note.msgtype == 5:
+ isicat += "###audio###:{}<###TYPESPLIT###>{}<###splitter###>".format(
+ note.file, note.value
+ )
+ elif note.msgtype == 6:
+ isicat += "###voice###:{}<###TYPESPLIT###>{}<###splitter###>".format(
+ note.file, note.value
+ )
+ elif note.msgtype == 7:
+ isicat += "###video###:{}<###TYPESPLIT###>{}<###splitter###>".format(
+ note.file, note.value
+ )
+ elif note.msgtype == 8:
+ isicat += "###video_note###:{}<###TYPESPLIT###>{}<###splitter###>".format(
+ note.file, note.value
+ )
+ else:
+ isicat += "{}<###splitter###>".format(note.value)
+ notes = {
+ "#{}".format(namacat.split("<###splitter###>")[x]): "{}".format(
+ isicat.split("<###splitter###>")[x]
+ )
+ for x in range(count)
+ }
+ # Rules
+ rules = rulessql.get_rules(chat_id)
+ # Blacklist
+ bl = list(blacklistsql.get_chat_blacklist(chat_id))
+ # Disabled command
+ disabledcmd = list(disabledsql.get_all_disabled(chat_id))
+ # Filters (TODO)
+ """
+ all_filters = list(filtersql.get_chat_triggers(chat_id))
+ export_filters = {}
+ for filters in all_filters:
+ filt = filtersql.get_filter(chat_id, filters)
+ # print(vars(filt))
+ if filt.is_sticker:
+ tipefilt = "sticker"
+ elif filt.is_document:
+ tipefilt = "doc"
+ elif filt.is_image:
+ tipefilt = "img"
+ elif filt.is_audio:
+ tipefilt = "audio"
+ elif filt.is_voice:
+ tipefilt = "voice"
+ elif filt.is_video:
+ tipefilt = "video"
+ elif filt.has_buttons:
+ tipefilt = "button"
+ buttons = filtersql.get_buttons(chat.id, filt.keyword)
+ print(vars(buttons))
+ elif filt.has_markdown:
+ tipefilt = "text"
+ if tipefilt == "button":
+ content = "{}#=#{}|btn|{}".format(tipefilt, filt.reply, buttons)
+ else:
+ content = "{}#=#{}".format(tipefilt, filt.reply)
+ print(content)
+ export_filters[filters] = content
+ print(export_filters)
+ """
+ # Welcome (TODO)
+ # welc = welcsql.get_welc_pref(chat_id)
+ # Locked
+ curr_locks = locksql.get_locks(chat_id)
+ curr_restr = locksql.get_restr(chat_id)
+
+ if curr_locks:
+ locked_lock = {
+ "sticker": curr_locks.sticker,
+ "audio": curr_locks.audio,
+ "voice": curr_locks.voice,
+ "document": curr_locks.document,
+ "video": curr_locks.video,
+ "contact": curr_locks.contact,
+ "photo": curr_locks.photo,
+ "gif": curr_locks.gif,
+ "url": curr_locks.url,
+ "bots": curr_locks.bots,
+ "forward": curr_locks.forward,
+ "game": curr_locks.game,
+ "location": curr_locks.location,
+ "rtl": curr_locks.rtl,
+ }
+ else:
+ locked_lock = {}
+
+ if curr_restr:
+ locked_restr = {
+ "messages": curr_restr.messages,
+ "media": curr_restr.media,
+ "other": curr_restr.other,
+ "previews": curr_restr.preview,
+ "all": all(
+ [
+ curr_restr.messages,
+ curr_restr.media,
+ curr_restr.other,
+ curr_restr.preview,
+ ]
+ ),
+ }
+ else:
+ locked_restr = {}
+
+ locks = {"locks": locked_lock, "restrict": locked_restr}
+ # Warns (TODO)
+ # warns = warnssql.get_warns(chat_id)
+ # Backing up
+ backup[chat_id] = {
+ "bot": context.bot.id,
+ "hashes": {
+ "info": {"rules": rules},
+ "extra": notes,
+ "blacklist": bl,
+ "disabled": disabledcmd,
+ "locks": locks,
+ },
+ }
+ baccinfo = json.dumps(backup, indent=4)
+ with open("QueenNoxi{}backup.json".format(chat_id), "w") as f:
+ f.write(str(baccinfo))
+ context.bot.sendChatAction(current_chat_id, "upload_document")
+ tgl = time.strftime("%H:%M:%S - %d/%m/%Y", time.localtime(time.time()))
+ try:
+ context.bot.sendMessage(
+ EVENT_LOGS,
+ "*Successfully imported backup:*\nChat: `{}`\nChat ID: `{}`\nOn: `{}`".format(
+ chat.title, chat_id, tgl
+ ),
+ parse_mode=ParseMode.MARKDOWN,
+ )
+ except BadRequest:
+ pass
+ context.bot.sendDocument(
+ current_chat_id,
+ document=open("QueenNoxi{}backup.json".format(chat_id), "rb"),
+ caption="📤*Successfully Exported backup:*\nChat: `{}`\nChat ID: `{}`\nOn: `{}`\n\nNote: This `QueenNoxi-Backup` was specially made for notes 📚.".format(
+ chat.title, chat_id, tgl
+ ),
+ timeout=360,
+ reply_to_message_id=msg.message_id,
+ parse_mode=ParseMode.MARKDOWN,
+ )
+ os.remove("QueenNoxi{}backup.json".format(chat_id)) # Cleaning file
+
+
+# Temporary data
+def put_chat(chat_id, value, chat_data):
+ # print(chat_data)
+ status = value is not False
+ chat_data[chat_id] = {"backups": {"status": status, "value": value}}
+
+
+def get_chat(chat_id, chat_data):
+ # print(chat_data)
+ try:
+ return chat_data[chat_id]["backups"]
+ except KeyError:
+ return {"status": False, "value": False}
+
+
+__mod_name__ = "Bᴀᴄᴋᴜᴘ"
+
+__help__ = """
+*ᴏɴʟʏ ғᴏʀ ɢʀᴏᴜᴘ ᴏᴡɴᴇʀ:*
+
+ ❍ /import : ʀᴇᴘʟʏ ᴛᴏ ᴛʜᴇ ʙᴀᴄᴋᴜᴘ ғɪʟᴇ ғᴏʀ ᴛʜᴇ ʙᴜᴛʟᴇʀ / ᴇᴍɪʟɪᴀ ɢʀᴏᴜᴘ ᴛᴏ ɪᴍᴘᴏʀᴛ ᴀs ᴍᴜᴄʜ ᴀs ᴘᴏssɪʙʟᴇ, ᴍᴀᴋɪɴɢ ᴛʀᴀɴsғᴇʀs ᴠᴇʀʏ ᴇᴀsʏ! \
+ ɴᴏᴛᴇ ᴛʜᴀᴛ ғɪʟᴇs / ᴘʜᴏᴛᴏs ᴄᴀɴɴᴏᴛ ʙᴇ ɪᴍᴘᴏʀᴛᴇᴅ ᴅᴜᴇ ᴛᴏ ᴛᴇʟᴇɢʀᴀᴍ ʀᴇsᴛʀɪᴄᴛɪᴏɴs.
+
+ ❍ /export : ᴇxᴘᴏʀᴛ ɢʀᴏᴜᴘ ᴅᴀᴛᴀ, ᴡʜɪᴄʜ ᴡɪʟʟ ʙᴇ ᴇxᴘᴏʀᴛᴇᴅ ᴀʀᴇ: ʀᴜʟᴇs, ɴᴏᴛᴇs (ᴅᴏᴄᴜᴍᴇɴᴛs, ɪᴍᴀɢᴇs, ᴍᴜsɪᴄ, ᴠɪᴅᴇᴏ, ᴀᴜᴅɪᴏ, ᴠᴏɪᴄᴇ, ᴛᴇxᴛ, ᴛᴇxᴛ ʙᴜᴛᴛᴏɴs) \
+
+"""
+
+IMPORT_HANDLER = CommandHandler("import", import_data, run_async=True)
+EXPORT_HANDLER = CommandHandler(
+ "export", export_data, pass_chat_data=True, run_async=True
+)
+
+dispatcher.add_handler(IMPORT_HANDLER)
+dispatcher.add_handler(EXPORT_HANDLER)
diff --git a/QueenNoxi/modules/bans.py b/QueenNoxi/modules/bans.py
new file mode 100644
index 0000000000000000000000000000000000000000..c1bf57be66cf6ed4411105275cdf00255c9113f2
--- /dev/null
+++ b/QueenNoxi/modules/bans.py
@@ -0,0 +1,325 @@
+import html
+from pyrogram import filters, Client, enums
+from pyrogram.types import Message
+from pyrogram.errors import RPCError
+
+from QueenNoxi import (
+ DEMONS,
+ DEV_USERS,
+ DRAGONS,
+ LOGGER,
+ OWNER_IDS,
+ TIGERS,
+ WOLVES,
+ pbot,
+ BOT_ID
+)
+from QueenNoxi.modules.disable import DisableAbleCommandHandler
+from QueenNoxi.modules.helper_funcs.chat_status import (
+ bot_admin,
+ can_delete,
+ can_restrict,
+ connection_status,
+ is_user_admin,
+ is_user_ban_protected,
+ is_user_in_chat,
+ user_admin,
+ user_can_ban,
+)
+from QueenNoxi.modules.helper_funcs.extraction import extract_user_and_text
+from QueenNoxi.modules.helper_funcs.string_handling import extract_time
+from QueenNoxi.modules.log_channel import gloggable, loggable
+
+
+@pbot.on_message(filters.command(["ban", "sban"]) & filters.group)
+@connection_status
+@bot_admin
+@can_restrict
+@user_admin
+@user_can_ban
+@loggable
+async def ban(client: Client, message: Message) -> str:
+ chat = message.chat
+ user = message.from_user
+ user_id, reason = await extract_user_and_text(message, message.command[1:])
+
+ if not user_id:
+ await message.reply_text("ɪ ᴅᴏᴜʙᴛ ᴛʜᴀᴛ's ᴀ ᴜsᴇʀ.")
+ return ""
+
+ try:
+ member = await chat.get_member(user_id)
+ except RPCError:
+ await message.reply_text("ᴄᴀɴ'ᴛ sᴇᴇᴍ ᴛᴏ ғɪɴᴅ ᴛʜɪs ᴘᴇʀsᴏɴ.")
+ return ""
+
+ if user_id == BOT_ID:
+ await message.reply_text("ᴏʜ ʏᴇᴀʜ, ʙᴀɴ ᴍʏsᴇʟғ, ɴᴏᴏʙ!")
+ return ""
+
+ if await is_user_ban_protected(chat, user_id, member) and user.id not in DEV_USERS:
+ if user_id in OWNER_IDS:
+ await message.reply_text("ᴛʀʏɪɴɢ ᴛᴏ ᴘᴜᴛ ᴍᴇ ᴀɢᴀɪɴsᴛ ᴀ ɢᴏᴅ ʟᴇᴠᴇʟ ᴅɪsᴀsᴛᴇʀ ʜᴜʜ?")
+ elif user_id in DEV_USERS:
+ await message.reply_text("ɪ ᴄᴀɴ'ᴛ ᴀᴄᴛ ᴀɢᴀɪɴsᴛ ᴏᴜʀ ᴏᴡɴ.")
+ elif user_id in DRAGONS:
+ await message.reply_text("ғɪɢʜᴛɪɴɢ ᴛʜɪs ᴅʀᴀɢᴏɴ ʜᴇʀᴇ ᴡɪʟʟ ᴘᴜᴛ ᴄɪᴠɪʟɪᴀɴ ʟɪᴠᴇs ᴀᴛ ʀɪsᴋ.")
+ elif user_id in DEMONS:
+ await message.reply_text("ʙʀɪɴɢ ᴀɴ ᴏʀᴅᴇʀ ғʀᴏᴍ ʜᴇʀᴏᴇs ᴀssᴏᴄɪᴀᴛɪᴏɴ ᴛᴏ ғɪɢʜᴛ ᴀ ᴅᴇᴍᴏɴ ᴅɪsᴀsᴛᴇʀ.")
+ elif user_id in TIGERS:
+ await message.reply_text("ʙʀɪɴɢ ᴀɴ ᴏʀᴅᴇʀ ғʀᴏᴍ ʜᴇʀᴏᴇs ᴀssᴏᴄɪᴀᴛɪᴏɴ ᴛᴏ ғɪɢʜᴛ ᴀ ᴛɪɢᴇʀ ᴅɪsᴀsᴛᴇʀ.")
+ elif user_id in WOLVES:
+ await message.reply_text("ᴡᴏʟғ ᴀʙɪʟɪᴛɪᴇs ᴍᴀᴋᴇ ᴛʜᴇᴍ ʙᴀɴ ɪᴍᴍᴜɴᴇ!")
+ else:
+ await message.reply_text("ᴛʜɪs ᴜsᴇʀ ɪs ʙᴀɴ ᴘʀᴏᴛᴇᴄᴛᴇᴅ!")
+ return ""
+
+ silent = message.command[0].lower() == "sban"
+ if silent and not await can_delete(chat, BOT_ID):
+ return ""
+
+ log = (
+ f"{html.escape(chat.title)}:\n"
+ f"#{'S' if silent else ''}ʙᴀɴɴᴇᴅ\n"
+ f"ʙᴀɴɴᴇᴅ ʙʏ: {user.mention}\n"
+ f"ᴜsᴇʀ: {member.user.mention}"
+ )
+ if reason:
+ log += f"\nʀᴇᴀsᴏɴ: {html.escape(reason)}"
+
+ try:
+ await chat.ban_member(user_id)
+ if silent:
+ if message.reply_to_message:
+ await message.reply_to_message.delete()
+ await message.delete()
+ return log
+
+ reply = (
+ f"❕ʙᴀɴ ᴇᴠᴇɴᴛ\n"
+ f"• ʙᴀɴɴᴇᴅ ʙʏ: {user.mention}\n"
+ f"• ᴜsᴇʀ: {member.user.mention}"
+ )
+ if reason:
+ reply += f"\n• ʀᴇᴀsᴏɴ: \n{html.escape(reason)}"
+ await message.reply_text(reply)
+ return log
+ except RPCError as e:
+ await message.reply_text(f"Error: {e.MESSAGE}")
+ return ""
+
+
+@pbot.on_message(filters.command("tban") & filters.group)
+@connection_status
+@bot_admin
+@can_restrict
+@user_admin
+@user_can_ban
+@loggable
+async def temp_ban(client: Client, message: Message) -> str:
+ chat = message.chat
+ user = message.from_user
+ user_id, reason = await extract_user_and_text(message, message.command[1:])
+
+ if not user_id:
+ await message.reply_text("ɪ ᴅᴏᴜʙᴛ ᴛʜᴀᴛ's ᴀ ᴜsᴇʀ.")
+ return ""
+
+ try:
+ member = await chat.get_member(user_id)
+ except RPCError:
+ await message.reply_text("ɪ ᴄᴀɴ'ᴛ sᴇᴇᴍ ᴛᴏ ғɪɴᴅ ᴛʜɪs ᴜsᴇʀ.")
+ return ""
+
+ if user_id == BOT_ID:
+ await message.reply_text("ɪ'ᴍ ɴᴏᴛ ɢᴏɴɴᴀ ʙᴀɴ ᴍʏsᴇʟғ.")
+ return ""
+
+ if await is_user_ban_protected(chat, user_id, member):
+ await message.reply_text("ɪ ᴅᴏɴ'ᴛ ғᴇᴇʟ ʟɪᴋᴇ ɪᴛ.")
+ return ""
+
+ if not reason:
+ await message.reply_text("ʏᴏᴜ ʜᴀᴠᴇɴ'ᴛ sᴘᴇᴄɪғɪᴇᴅ ᴀ ᴛɪᴍᴇ ᴛᴏ ʙᴀɴ ᴛʜɪs ᴜsᴇʀ ғᴏʀ!")
+ return ""
+
+ split_reason = reason.split(None, 1)
+ time_val = split_reason[0].lower()
+ reason = split_reason[1] if len(split_reason) > 1 else ""
+ bantime = await extract_time(message, time_val)
+
+ if not bantime:
+ return ""
+
+ log = (
+ f"{html.escape(chat.title)}:\n"
+ "ᴛᴇᴍᴩ ʙᴀɴ\n"
+ f"ʙᴀɴɴᴇᴅ ʙʏ: {user.mention}\n"
+ f"ᴜsᴇʀ: {member.user.mention}\n"
+ f"ᴛɪᴍᴇ: {time_val}"
+ )
+ if reason:
+ log += f"\nʀᴇᴀsᴏɴ: {html.escape(reason)}"
+
+ try:
+ await chat.ban_member(user_id, until_date=bantime)
+ await message.reply_text(
+ f"ʙᴀɴɴᴇᴅ! ᴜsᴇʀ {member.user.mention} ɪs ɴᴏᴡ ʙᴀɴɴᴇᴅ ғᴏʀ {time_val}."
+ )
+ return log
+ except RPCError as e:
+ await message.reply_text(f"Error: {e.MESSAGE}")
+ return ""
+
+
+@pbot.on_message(filters.command("kick") & filters.group)
+@connection_status
+@bot_admin
+@can_restrict
+@user_admin
+@user_can_ban
+@loggable
+async def kick(client: Client, message: Message) -> str:
+ chat = message.chat
+ user = message.from_user
+ user_id, reason = await extract_user_and_text(message, message.command[1:])
+
+ if not user_id:
+ await message.reply_text("ɪ ᴅᴏᴜʙᴛ ᴛʜᴀᴛ's ᴀ ᴜsᴇʀ.")
+ return ""
+
+ try:
+ member = await chat.get_member(user_id)
+ except RPCError:
+ await message.reply_text("ɪ ᴄᴀɴ'ᴛ sᴇᴇᴍ ᴛᴏ ғɪɴᴅ ᴛʜɪs ᴜsᴇʀ.")
+ return ""
+
+ if user_id == BOT_ID:
+ await message.reply_text("ʏᴇᴀʜʜʜ ɪ'ᴍ ɴᴏᴛ ɢᴏɴɴᴀ ᴅᴏ ᴛʜᴀᴛ.")
+ return ""
+
+ if await is_user_ban_protected(chat, user_id):
+ await message.reply_text("I really wish I could kick this user....")
+ return ""
+
+ try:
+ await chat.unban_member(user_id) # unban = kick in telegram
+ await message.reply_text(f"One Kicked! {member.user.mention}.")
+ log = (
+ f"{html.escape(chat.title)}:\n"
+ f"ᴋɪᴄᴋᴇᴅ\n"
+ f"ᴋɪᴄᴋᴇᴅ ʙʏ: {user.mention}\n"
+ f"ᴜsᴇʀ: {member.user.mention}"
+ )
+ if reason:
+ log += f"\nʀᴇᴀsᴏɴ: {html.escape(reason)}"
+ return log
+ except RPCError as e:
+ await message.reply_text(f"Error: {e.MESSAGE}")
+ return ""
+
+
+@DisableAbleCommandHandler("kickme", admin_ok=False)
+async def kickme(client: Client, message: Message):
+ user_id = message.from_user.id
+ if await is_user_admin(message.chat, user_id):
+ await message.reply_text("ɪ ᴡɪsʜ ɪ ᴄᴏᴜʟᴅ... ʙᴜᴛ ʏᴏᴜ'ʀᴇ ᴀɴ ᴀᴅᴍɪɴ.")
+ return
+
+ try:
+ await message.chat.unban_member(user_id)
+ await message.reply_text("*ᴋɪᴄᴋs ʏᴏᴜ ᴏᴜᴛ ᴏғ ᴛʜᴇ ɢʀᴏᴜᴘ*")
+ except RPCError as e:
+ await message.reply_text(f"Error: {e.MESSAGE}")
+
+
+@pbot.on_message(filters.command("unban") & filters.group)
+@connection_status
+@bot_admin
+@can_restrict
+@user_admin
+@user_can_ban
+@loggable
+async def unban(client: Client, message: Message) -> str:
+ chat = message.chat
+ user = message.from_user
+ user_id, reason = await extract_user_and_text(message, message.command[1:])
+
+ if not user_id:
+ await message.reply_text("ɪ ᴅᴏᴜʙᴛ ᴛʜᴀᴛ's ᴀ ᴜsᴇʀ.")
+ return ""
+
+ try:
+ member = await client.get_users(user_id)
+ except RPCError:
+ await message.reply_text("ɪ ᴄᴀɴ'ᴛ sᴇᴇᴍ ᴛᴏ ғɪɴᴅ ᴛʜɪs ᴜsᴇʀ.")
+ return ""
+
+ if user_id == BOT_ID:
+ await message.reply_text("ʜᴏᴡ ᴡᴏᴜʟᴅ ɪ ᴜɴʙᴀɴ ᴍʏsᴇʟғ?")
+ return ""
+
+
+ try:
+ await chat.unban_member(user_id)
+ await message.reply_text("Yep, this user can join!")
+ log = (
+ f"{html.escape(chat.title)}:\n"
+ f"ᴜɴʙᴀɴɴᴇᴅ\n"
+ f"ᴜɴʙᴀɴɴᴇᴅ ʙʏ: {user.mention}\n"
+ f"ᴜsᴇʀ: {member.mention}"
+ )
+ if reason:
+ log += f"\nʀᴇᴀsᴏɴ: {html.escape(reason)}"
+ return log
+ except RPCError as e:
+ await message.reply_text(f"Error: {e.MESSAGE}")
+ return ""
+
+
+@pbot.on_message(filters.command("roar") & filters.group)
+@connection_status
+@bot_admin
+@can_restrict
+@gloggable
+async def selfunban(client: Client, message: Message) -> str:
+ user = message.from_user
+ if user.id not in DRAGONS and user.id not in TIGERS:
+ return ""
+
+ if len(message.command) < 2:
+ await message.reply_text("ɢɪᴠᴇ ᴀ ᴠᴀʟɪᴅ ᴄʜᴀᴛ ɪᴅ.")
+ return ""
+
+ try:
+ chat_id = int(message.command[1])
+ chat = await client.get_chat(chat_id)
+ except Exception:
+ await message.reply_text("ɪ ᴄᴀɴ'ᴛ sᴇᴇᴍ ᴛᴏ ғɪɴᴅ ᴛʜɪs ᴄʜᴀᴛ.")
+ return ""
+
+ try:
+ await chat.unban_member(user.id)
+ await message.reply_text("ʏᴇᴘ, ɪ ʜᴀᴠᴇ ᴜɴʙᴀɴɴᴇᴅ ʏᴏᴜ.")
+ return (
+ f"{html.escape(chat.title)}:\n"
+ f"ᴜɴʙᴀɴɴᴇᴅ\n"
+ f"ᴜɴʙᴀɴɴᴇᴅ ʙʏ: {user.mention}\n"
+ f"ᴜsᴇʀ: {user.mention}"
+ )
+ except RPCError as e:
+ await message.reply_text(f"Error: {e.MESSAGE}")
+ return ""
+
+
+__mod_name__ = "Ban"
+__help__ = """
+ ❍ /kickme: Kicks the user who issued the command
+
+*Admins only:*
+ ❍ /ban : Bans a user. (via handle, or reply)
+ ❍ /sban : Silently ban a user.
+ ❍ /tban x(m/h/d): Bans a user for `x` time.
+ ❍ /unban : Unbans a user.
+ ❍ /kick : Kicks a user out of the group.
+"""
diff --git a/QueenNoxi/modules/blacklist.py b/QueenNoxi/modules/blacklist.py
new file mode 100644
index 0000000000000000000000000000000000000000..cce02f826d6eaf5721a8ac5f43be053ba77e6bd7
--- /dev/null
+++ b/QueenNoxi/modules/blacklist.py
@@ -0,0 +1,129 @@
+import html
+import re
+from pyrogram import filters, Client, enums
+from pyrogram.types import Message
+
+import QueenNoxi.modules.sql.blacklist_sql as sql
+from QueenNoxi import LOGGER, pbot
+from QueenNoxi.modules.disable import DisableAbleCommandHandler
+from QueenNoxi.modules.helper_funcs.chat_status import user_admin, connection_status
+from QueenNoxi.modules.warns import warn
+
+BLACKLIST_GROUP = 11
+
+@DisableAbleCommandHandler("blacklist", admin_ok=True)
+@connection_status
+async def blacklist(client: Client, message: Message):
+ chat_id = message.chat.id
+ triggers = sql.get_chat_blacklist(chat_id)
+ if not triggers:
+ return await message.reply_text("No blacklist filters active here!")
+
+ res = "Blacklist filters active in this chat:\n"
+ for trigger in triggers:
+ res += f" • {html.escape(trigger)}\n"
+ await message.reply_text(res)
+
+@DisableAbleCommandHandler("addblacklist", admin_ok=True)
+@connection_status
+@user_admin
+async def add_blacklist(client: Client, message: Message):
+ args = message.text.split(None, 1)
+ if len(args) < 2:
+ return await message.reply_text("Give me a keyword!")
+
+ words = args[1].split("\n")
+ for word in words:
+ trigger = word.strip().lower()
+ if trigger:
+ sql.add_to_blacklist(message.chat.id, trigger)
+
+ await message.reply_text("Added to blacklist!")
+
+@DisableAbleCommandHandler(["unblacklist", "rmblacklist"], admin_ok=True)
+@connection_status
+@user_admin
+async def unblacklist(client: Client, message: Message):
+ args = message.text.split(None, 1)
+ if len(args) < 2:
+ return await message.reply_text("What should I remove?")
+
+ trigger = args[1].strip().lower()
+ if sql.rm_from_blacklist(message.chat.id, trigger):
+ await message.reply_text(f"Removed '{trigger}' from blacklist.")
+ else:
+ await message.reply_text("Not found.")
+
+@DisableAbleCommandHandler("blacklistmode", admin_ok=True)
+@connection_status
+@user_admin
+async def blacklist_mode(client: Client, message: Message):
+ args = message.command[1:]
+ chat_id = message.chat.id
+ if args:
+ mode = args[0].lower()
+ if mode in ("off", "nothing"): sql.set_blacklist_strength(chat_id, 0, "0")
+ elif mode == "del": sql.set_blacklist_strength(chat_id, 1, "0")
+ elif mode == "warn": sql.set_blacklist_strength(chat_id, 2, "0")
+ elif mode == "mute": sql.set_blacklist_strength(chat_id, 3, "0")
+ elif mode == "kick": sql.set_blacklist_strength(chat_id, 4, "0")
+ elif mode == "ban": sql.set_blacklist_strength(chat_id, 5, "0")
+ else:
+ return await message.reply_text("Invalid mode!")
+ await message.reply_text(f"Blacklist mode set to {mode}.")
+ else:
+ st, val = sql.get_blacklist_setting(chat_id)
+ modes = ["nothing", "del", "warn", "mute", "kick", "ban"]
+ await message.reply_text(f"Current blacklist mode: {modes[st]}")
+
+@pbot.on_message(filters.text & filters.group, group=BLACKLIST_GROUP)
+async def del_blacklist(client: Client, message: Message):
+ chat_id = message.chat.id
+ if not message.from_user: return
+
+ # Ignore admins
+ if await is_user_admin(message.chat, message.from_user.id):
+ return
+
+ triggers = sql.get_chat_blacklist(chat_id)
+ if not triggers: return
+
+ for trigger in triggers:
+ pattern = rf"( |^|[^\w]){re.escape(trigger)}( |$|[^\w])"
+ if re.search(pattern, message.text, flags=re.IGNORECASE):
+ st, val = sql.get_blacklist_setting(chat_id)
+ try:
+ if st == 0:
+ return
+ await message.delete()
+ if st == 1:
+ break # just delete
+ elif st == 2: # warn
+ await warn(message.from_user, chat_id, f"Blacklist trigger: {trigger}", message)
+ elif st == 3: # mute
+ await message.chat.restrict_member(
+ message.from_user.id,
+ enums.ChatMemberStatus.RESTRICTED,
+ enums.ChatPermissions(can_send_messages=False),
+ )
+ elif st == 4: # kick
+ await message.chat.unban_member(message.from_user.id)
+ elif st == 5: # ban
+ await message.chat.ban_member(message.from_user.id)
+ except Exception as e:
+ LOGGER.error(f"Error in blacklist: {e}")
+ break
+
+async def is_user_admin(chat, user_id: int) -> bool:
+ from QueenNoxi.modules.helper_funcs.chat_status import is_user_admin as check_admin
+ return await check_admin(chat, user_id)
+
+__mod_name__ = "Blacklist"
+__help__ = """
+ • `/blacklist`: List blackened words.
+ • `/addblacklist `: Blacklist a word.
+ • `/unblacklist `: Un-blacklist a word.
+ • `/blacklistmode `: Set punishment.
+
+Modes: `off`, `del`, `warn`, `mute`, `kick`, `ban`.
+"""
diff --git a/QueenNoxi/modules/blacklist_stickers.py b/QueenNoxi/modules/blacklist_stickers.py
new file mode 100644
index 0000000000000000000000000000000000000000..1ea22c623a61ff1031fa00e5b7324dbcdeebcfbc
--- /dev/null
+++ b/QueenNoxi/modules/blacklist_stickers.py
@@ -0,0 +1,197 @@
+import html
+from pyrogram import filters, Client, enums
+from pyrogram.types import (
+ InlineKeyboardButton,
+ InlineKeyboardMarkup,
+ Message,
+ CallbackQuery,
+ ChatPermissions,
+)
+from pyrogram.errors import RPCError, BadRequest
+
+import QueenNoxi.modules.sql.blsticker_sql as sql
+from QueenNoxi import LOGGER, pbot
+from QueenNoxi.modules.connection import connected
+from QueenNoxi.modules.disable import DisableAbleCommandHandler
+from QueenNoxi.modules.helper_funcs.chat_status import user_admin, user_not_admin
+from QueenNoxi.modules.helper_funcs.string_handling import extract_time
+from QueenNoxi.modules.log_channel import loggable
+from QueenNoxi.modules.warns import warn
+
+@pbot.on_message(filters.command("blsticker") & filters.group)
+@user_admin
+async def blackliststicker(client: Client, message: Message):
+ chat_id = await connected(client, message, message.from_user.id, need_admin=False)
+ if not chat_id:
+ chat_id = message.chat.id
+ chat_name = message.chat.title
+ else:
+ try:
+ chat = await client.get_chat(chat_id)
+ chat_name = chat.title
+ except:
+ chat_name = f"Chat {chat_id}"
+
+ sticker_list = f"**List blacklisted stickers in {html.escape(chat_name)}:**\n"
+ all_stickerlist = sql.get_chat_stickers(chat_id)
+
+ if not all_stickerlist:
+ await message.reply_text(f"There are no blacklisted stickers in **{html.escape(chat_name)}**!")
+ return
+
+ for trigger in all_stickerlist:
+ sticker_list += f" - `{html.escape(trigger)}`\n"
+
+ await message.reply_text(sticker_list)
+
+@pbot.on_message(filters.command("addblsticker") & filters.group)
+@user_admin
+async def add_blackliststicker(client: Client, message: Message):
+ chat_id = await connected(client, message, message.from_user.id)
+ if not chat_id:
+ chat_id = message.chat.id
+ chat_name = message.chat.title
+ else:
+ chat = await client.get_chat(chat_id)
+ chat_name = chat.title
+
+ args = message.command[1:]
+ if args:
+ text = " ".join(args).replace("https://t.me/addstickers/", "")
+ to_blacklist = list({trigger.strip() for trigger in text.split("\n") if trigger.strip()})
+
+ added = 0
+ for trigger in to_blacklist:
+ sql.add_to_stickers(chat_id, trigger.lower())
+ added += 1
+
+ if added > 0:
+ await message.reply_text(f"Added {added} stickers to blacklist in **{html.escape(chat_name)}**!")
+ elif message.reply_to_message and message.reply_to_message.sticker:
+ trigger = message.reply_to_message.sticker.set_name
+ if not trigger:
+ await message.reply_text("Sticker is invalid!")
+ return
+ sql.add_to_stickers(chat_id, trigger.lower())
+ await message.reply_text(f"Sticker `{trigger}` added to blacklist in **{html.escape(chat_name)}**!")
+ else:
+ await message.reply_text("Please provide a sticker set name or reply to a sticker.")
+
+@pbot.on_message(filters.command(["unblsticker", "rmblsticker"]) & filters.group)
+@user_admin
+async def unblackliststicker(client: Client, message: Message):
+ chat_id = await connected(client, message, message.from_user.id)
+ if not chat_id:
+ chat_id = message.chat.id
+ chat_name = message.chat.title
+ else:
+ chat = await client.get_chat(chat_id)
+ chat_name = chat.title
+
+ args = message.command[1:]
+ if args:
+ text = " ".join(args).replace("https://t.me/addstickers/", "")
+ to_unblacklist = list({trigger.strip() for trigger in text.split("\n") if trigger.strip()})
+
+ successful = 0
+ for trigger in to_unblacklist:
+ if sql.rm_from_stickers(chat_id, trigger.lower()):
+ successful += 1
+
+ if successful > 0:
+ await message.reply_text(f"Removed {successful} stickers from blacklist in **{html.escape(chat_name)}**!")
+ else:
+ await message.reply_text("None of these stickers were blacklisted.")
+ elif message.reply_to_message and message.reply_to_message.sticker:
+ trigger = message.reply_to_message.sticker.set_name
+ if not trigger:
+ await message.reply_text("Sticker is invalid!")
+ return
+ if sql.rm_from_stickers(chat_id, trigger.lower()):
+ await message.reply_text(f"Sticker `{trigger}` removed from blacklist in **{html.escape(chat_name)}**!")
+ else:
+ await message.reply_text("This sticker was not blacklisted.")
+ else:
+ await message.reply_text("Please provide a sticker set name or reply to a sticker.")
+
+@pbot.on_message(filters.command("blstickermode") & filters.group)
+@user_admin
+@loggable
+async def blacklist_mode(client: Client, message: Message) -> str:
+ chat_id = await connected(client, message, message.from_user.id)
+ if not chat_id:
+ chat_id = message.chat.id
+ chat_name = message.chat.title
+ else:
+ chat = await client.get_chat(chat_id)
+ chat_name = chat.title
+
+ args = message.command[1:]
+ if args:
+ mode = args[0].lower()
+ if mode in ("off", "nothing", "no"):
+ sql.set_blacklist_strength(chat_id, 0, "0")
+ msg = "Blacklist sticker mode disabled."
+ elif mode in ("del", "delete"):
+ sql.set_blacklist_strength(chat_id, 1, "0")
+ msg = "Blacklist sticker mode set to: DELETE."
+ elif mode == "warn":
+ sql.set_blacklist_strength(chat_id, 2, "0")
+ msg = "Blacklist sticker mode set to: WARN."
+ elif mode == "mute":
+ sql.set_blacklist_strength(chat_id, 3, "0")
+ msg = "Blacklist sticker mode set to: MUTE."
+ elif mode == "kick":
+ sql.set_blacklist_strength(chat_id, 4, "0")
+ msg = "Blacklist sticker mode set to: KICK."
+ elif mode == "ban":
+ sql.set_blacklist_strength(chat_id, 5, "0")
+ msg = "Blacklist sticker mode set to: BAN."
+ else:
+ await message.reply_text("Invalid mode! Use: off/del/warn/mute/kick/ban")
+ return ""
+
+ await message.reply_text(f"{msg} in **{html.escape(chat_name)}**.")
+ return f"**{html.escape(chat_name)}:**\n#BLSTICKER_MODE\n**Admin:** {message.from_user.mention}\nMode: {mode}"
+ else:
+ mode, value = sql.get_blacklist_setting(chat_id)
+ modes = ["Off", "Delete", "Warn", "Mute", "Kick", "Ban"]
+ await message.reply_text(f"Current blacklist sticker mode: **{modes[mode]}**.")
+ return ""
+
+@pbot.on_message(filters.sticker & filters.group, group=11)
+@user_not_admin
+async def del_blackliststicker(client: Client, message: Message):
+ chat_id = message.chat.id
+ sticker = message.sticker
+ if not sticker or not sticker.set_name:
+ return
+
+ mode, value = sql.get_blacklist_setting(chat_id)
+ if mode == 0:
+ return
+
+ chat_filters = sql.get_chat_stickers(chat_id)
+ if sticker.set_name.lower() in [s.lower() for s in chat_filters]:
+ try:
+ await message.delete()
+ if mode == 2: # Warn
+ await warn(message.from_user, chat_id, f"Using blacklisted sticker: {sticker.set_name}", message)
+ elif mode == 3: # Mute
+ await message.chat.restrict_member(message.from_user.id, ChatPermissions(can_send_messages=False))
+ elif mode == 4: # Kick
+ await message.chat.unban_member(message.from_user.id)
+ elif mode == 5: # Ban
+ await message.chat.ban_member(message.from_user.id)
+ except Exception as e:
+ LOGGER.warning(f"Error in del_blackliststicker: {e}")
+
+__mod_name__ = "Sᴛɪᴄᴋᴇʀ"
+__help__ = """
+ʙʟᴀᴄᴋʟɪsᴛ sᴛɪᴄᴋᴇʀ ɪs ᴜsᴇᴅ ᴛᴏ sᴛᴏᴘ ᴄᴇʀᴛᴀɪɴ sᴛɪᴄᴋᴇʀs. ᴡʜᴇɴᴇᴠᴇʀ ᴀ sᴛɪᴄᴋᴇʀ ɪs sᴇɴᴛ, ᴛʜᴇ ᴍᴇssᴀɢᴇ ᴡɪʟʟ ʙᴇ ᴅᴇʟᴇᴛᴇᴅ ɪᴍᴍᴇᴅɪᴀᴛᴇʟʏ.
+
+ ❍ /blsticker: sᴇᴇ ᴄᴜʀʀᴇɴᴛ ʙʟᴀᴄᴋʟɪsᴛᴇᴅ sᴛɪᴄᴋᴇʀ
+ ❍ /addblsticker : ᴀᴅᴅ sᴛɪᴄᴋᴇʀ ᴛᴏ ʙʟᴀᴄᴋʟɪsᴛ
+ ❍ /unblsticker : ʀᴇᴍᴏᴠᴇ sᴛɪᴄᴋᴇʀ ғʀᴏᴍ ʙʟᴀᴄᴋʟɪsᴛ
+ ❍ /blstickermode : sᴇᴛ ᴅᴇғᴀᴜʟᴛ ᴀᴄᴛɪᴏɴ
+"""
diff --git a/QueenNoxi/modules/blacklistusers.py b/QueenNoxi/modules/blacklistusers.py
new file mode 100644
index 0000000000000000000000000000000000000000..553a255224b98b0ec7c9607a9fc439b7e0bf9a8f
--- /dev/null
+++ b/QueenNoxi/modules/blacklistusers.py
@@ -0,0 +1,131 @@
+import html
+from pyrogram import filters, Client, enums
+from pyrogram.types import Message
+
+import QueenNoxi.modules.sql.blacklistusers_sql as sql
+from QueenNoxi import DEMONS, DEV_USERS, DRAGONS, OWNER_ID, TIGERS, WOLVES, pbot
+from QueenNoxi.modules.helper_funcs.chat_status import dev_plus
+from QueenNoxi.modules.helper_funcs.extraction import extract_user, extract_user_and_text
+from QueenNoxi.modules.log_channel import gloggable
+
+BLACKLISTWHITELIST = [OWNER_ID] + DEV_USERS + DRAGONS + WOLVES + DEMONS
+
+@pbot.on_message(filters.command("ignore"))
+@dev_plus
+@gloggable
+async def bl_user(client: Client, message: Message) -> str:
+ user_id, reason = await extract_user_and_text(message, message.command[1:])
+
+ if not user_id:
+ await message.reply_text("I doubt that's a user.")
+ return ""
+
+ if user_id == (await client.get_me()).id:
+ await message.reply_text("How am I supposed to do my work if I am ignoring myself?")
+ return ""
+
+ if user_id in BLACKLISTWHITELIST:
+ await message.reply_text("No!\nNoticing Disasters is my job.")
+ return ""
+
+ try:
+ target_user = await client.get_users(user_id)
+ except Exception:
+ await message.reply_text("I can't seem to find this user.")
+ return ""
+
+ sql.blacklist_user(user_id, reason)
+ await message.reply_text("I shall ignore the existence of this user!")
+
+ log_message = (
+ f"#BLACKLIST\n"
+ f"**Admin:** {message.from_user.mention}\n"
+ f"**User:** {target_user.mention}"
+ )
+ if reason:
+ log_message += f"\n**Reason:** {reason}"
+
+ return log_message
+
+@pbot.on_message(filters.command("notice"))
+@dev_plus
+@gloggable
+async def unbl_user(client: Client, message: Message) -> str:
+ user_id = await extract_user(message, message.command[1:])
+
+ if not user_id:
+ await message.reply_text("I doubt that's a user.")
+ return ""
+
+ if user_id == (await client.get_me()).id:
+ await message.reply_text("I always notice myself.")
+ return ""
+
+ try:
+ target_user = await client.get_users(user_id)
+ except Exception:
+ await message.reply_text("I can't seem to find this user.")
+ return ""
+
+ if sql.is_user_blacklisted(user_id):
+ sql.unblacklist_user(user_id)
+ await message.reply_text("*notices user*")
+ log_message = (
+ f"#UNBLACKLIST\n"
+ f"**Admin:** {message.from_user.mention}\n"
+ f"**User:** {target_user.mention}"
+ )
+ return log_message
+ else:
+ await message.reply_text("I am not ignoring them at all though!")
+ return ""
+
+@pbot.on_message(filters.command("ignoredlist"))
+@dev_plus
+async def bl_users(client: Client, message: Message):
+ users = []
+ for each_user in sql.get_all_blacklisted():
+ try:
+ user = await client.get_users(each_user)
+ reason = sql.get_reason(each_user)
+ if reason:
+ users.append(f"• {user.mention} :- {reason}")
+ else:
+ users.append(f"• {user.mention}")
+ except:
+ users.append(f"• `{each_user}` (Not found)")
+
+ message_text = "**Blacklisted Users**\n"
+ if not users:
+ message_text += "None is being ignored as of yet."
+ else:
+ message_text += "\n".join(users)
+
+ await message.reply_text(message_text)
+
+def __user_info__(user_id):
+ is_blacklisted = sql.is_user_blacklisted(user_id)
+ text = "Blacklisted: **{}**"
+ if user_id in [777000, 1087968824]:
+ return ""
+ if user_id == pbot.me.id:
+ return ""
+ if int(user_id) in DRAGONS + TIGERS + WOLVES:
+ return ""
+ if is_blacklisted:
+ text = text.format("Yes")
+ reason = sql.get_reason(user_id)
+ if reason:
+ text += f"\nReason: `{reason}`"
+ else:
+ text = text.format("No")
+ return text
+
+__mod_name__ = "Ignore"
+__help__ = """
+ᴛʜɪs ᴍᴏᴅᴜʟᴇ ᴀʟʟᴏᴡs ᴅᴇᴠs ᴛᴏ ɪɢɴᴏʀᴇ sᴘᴀᴍᴍᴇʀs ɢʟᴏʙᴀʟʟʏ.
+
+ ❍ /ignore <ᴜsᴇʀ>: sᴛᴏᴘ ʀᴇsᴘᴏɴᴅɪɴɢ ᴛᴏ ᴛʜɪs ᴜsᴇʀ
+ ❍ /notice <ᴜsᴇʀ>: sᴛᴀʀᴛ ʀᴇsᴘᴏɴᴅɪɴɢ ᴛᴏ ᴛʜɪs ᴜsᴇʀ
+ ❍ /ignoredlist: ʟɪsᴛ ᴀʟʟ ɪɢɴᴏʀᴇᴅ ᴜsᴇʀs
+"""
diff --git a/QueenNoxi/modules/bug.py b/QueenNoxi/modules/bug.py
new file mode 100644
index 0000000000000000000000000000000000000000..b7e30d2efce2b8323d60eff1e1bec7acbf1f3b9e
--- /dev/null
+++ b/QueenNoxi/modules/bug.py
@@ -0,0 +1,119 @@
+from datetime import datetime
+
+from pyrogram import filters
+from pyrogram.types import InlineKeyboardButton, InlineKeyboardMarkup, Message
+
+from QueenNoxi import OWNER_ID as owner_id
+from QueenNoxi import SUPPORT_CHAT as log,BOT_NAME,START_IMG
+from QueenNoxi import pbot as Client
+from QueenNoxi.utils.errors import capture_err
+
+
+def content(msg: Message) -> [None, str]:
+ text_to_return = msg.text
+
+ if msg.text is None:
+ return None
+ if " " in text_to_return:
+ try:
+ return msg.text.split(None, 1)[1]
+ except IndexError:
+ return None
+ else:
+ return None
+
+
+@Client.on_message(filters.command("bug"))
+@capture_err
+async def bug(_, msg: Message):
+ if msg.chat.username:
+ chat_username = f"@{msg.chat.username}/`{msg.chat.id}`"
+ else:
+ chat_username = f"ᴩʀɪᴠᴀᴛᴇ ɢʀᴏᴜᴩ/`{msg.chat.id}`"
+
+ bugs = content(msg)
+ user_id = msg.from_user.id
+ mention = (
+ "[" + msg.from_user.first_name + "](tg://user?id=" + str(msg.from_user.id) + ")"
+ )
+ datetimes_fmt = "%d-%m-%Y"
+ datetimes = datetime.utcnow().strftime(datetimes_fmt)
+
+
+
+ bug_report = f"""
+**#ʙᴜɢ : ** **tg://user?id={owner_id}**
+
+**ʀᴇᴩᴏʀᴛᴇᴅ ʙʏ : ** **{mention}**
+**ᴜsᴇʀ ɪᴅ : ** **{user_id}**
+**ᴄʜᴀᴛ : ** **{chat_username}**
+
+**ʙᴜɢ : ** **{bugs}**
+
+**ᴇᴠᴇɴᴛ sᴛᴀᴍᴩ : ** **{datetimes}**"""
+
+ if msg.chat.type == "private":
+ await msg.reply_text("» ᴛʜɪs ᴄᴏᴍᴍᴀɴᴅ ɪs ᴏɴʟʏ ғᴏʀ ɢʀᴏᴜᴩs.")
+ return
+
+ if user_id == owner_id:
+ if bugs:
+ await msg.reply_text(
+ "» ᴀʀᴇ ʏᴏᴜ ᴄᴏᴍᴇᴅʏ ᴍᴇ 🤣, ʏᴏᴜ'ʀᴇ ᴛʜᴇ ᴏᴡɴᴇʀ ᴏғ ᴛʜᴇ ʙᴏᴛ.",
+ )
+ return
+ else:
+ await msg.reply_text("ᴄʜᴜᴍᴛɪʏᴀ ᴏᴡɴᴇʀ!")
+ elif user_id != owner_id:
+ if bugs:
+ await msg.reply_text(
+ f"ʙᴜɢ ʀᴇᴩᴏʀᴛ : {bugs}\n\n"
+ "» ʙᴜɢ sᴜᴄᴄᴇssғᴜʟʟʏ ʀᴇᴩᴏʀᴛᴇᴅ ᴀᴛ sᴜᴩᴩᴏʀᴛ ᴄʜᴀᴛ !",
+ reply_markup=InlineKeyboardMarkup(
+ [[InlineKeyboardButton("• ᴄʟᴏsᴇ •", callback_data=f"close_reply")]]
+ ),
+ )
+ await Client.send_photo(
+ log,
+ photo=START_IMG,
+ caption=f"{bug_report}",
+ reply_markup=InlineKeyboardMarkup(
+ [
+ [InlineKeyboardButton("• ᴠɪᴇᴡ ʙᴜɢ •", url=f"{msg.link}")],
+ [
+ InlineKeyboardButton(
+ "• ᴄʟᴏsᴇ •", callback_data="close_send_photo"
+ )
+ ],
+ ]
+ ),
+ )
+ else:
+ await msg.reply_text(
+ f"» ɴᴏ ʙᴜɢ ᴛᴏ ʀᴇᴩᴏʀᴛ !",
+ )
+
+
+@Client.on_callback_query(filters.regex("close_reply"))
+async def close_reply(msg, CallbackQuery):
+ await CallbackQuery.message.delete()
+
+
+@Client.on_callback_query(filters.regex("close_send_photo"))
+async def close_send_photo(_, CallbackQuery):
+ is_Admin = await Client.get_chat_member(
+ CallbackQuery.message.chat.id, CallbackQuery.from_user.id
+ )
+ if not is_Admin.can_delete_messages:
+ return await CallbackQuery.answer(
+ "ʏᴏᴜ ᴅᴏɴ'ᴛ ʜᴀᴠᴇ ʀɪɢʜᴛs ᴛᴏ ᴄʟᴏsᴇ ᴛʜɪs.", show_alert=True
+ )
+ else:
+ await CallbackQuery.message.delete()
+
+
+__help__ = """
+*ғᴏʀ ʀᴇᴩᴏʀᴛɪɴɢ ᴀ ʙᴜɢ *
+ ❍ /bug *:* ᴛᴏ ʀᴇᴩᴏʀᴛ ᴀ ʙᴜɢ ᴀᴛ sᴜᴩᴩᴏʀᴛ ᴄʜᴀᴛ.
+"""
+__mod_name__ = "Bᴜɢ"
diff --git a/QueenNoxi/modules/carbon.py b/QueenNoxi/modules/carbon.py
new file mode 100644
index 0000000000000000000000000000000000000000..d79c8db3063a143707ce488d1f455073878592c1
--- /dev/null
+++ b/QueenNoxi/modules/carbon.py
@@ -0,0 +1,40 @@
+from pyrogram import filters
+
+from QueenNoxi import pbot
+from QueenNoxi.utils.errors import capture_err
+from QueenNoxi.utils.functions import make_carbon
+
+
+@pbot.on_message(filters.command("carbon"))
+@capture_err
+async def carbon_func(_, message):
+ if message.reply_to_message:
+ if message.reply_to_message.text:
+ txt = message.reply_to_message.text
+ else:
+ return await message.reply_text("ʀᴇᴘʟʏ ᴛᴏ ᴀ ᴍᴇssᴀɢᴇ ᴏʀ ɢɪᴠᴇ sᴏᴍᴇ ᴛᴇxᴛ.")
+ else:
+ try:
+ txt = message.text.split(None, 1)[1]
+ except IndexError:
+ return await message.reply_text("ʀᴇᴘʟʏ ᴛᴏ ᴀ ᴍᴇssᴀɢᴇ ᴏʀ ɢɪᴠᴇ sᴏᴍᴇ ᴛᴇxᴛ.")
+ m = await message.reply_text("ɢᴇɴᴇʀᴀᴛɪɴɢ ᴄᴀʀʙᴏɴ...")
+ carbon = await make_carbon(txt)
+ await m.edit_text("ᴜᴩʟᴏᴀᴅɪɴɢ ɢᴇɴᴇʀᴀᴛᴇᴅ ᴄᴀʀʙᴏɴ...")
+ await pbot.send_photo(
+ message.chat.id,
+ photo=carbon,
+ caption=f"» ʀᴇᴏ̨ᴜᴇsᴛᴇᴅ ʙʏ : {message.from_user.mention}",
+ )
+ await m.delete()
+ carbon.close()
+
+__mod_name__ = "Cᴀʀʙᴏɴ"
+
+__help__ = """
+
+ᴍᴀᴋᴇs ᴀ ᴄᴀʀʙᴏɴ ᴏғ ᴛʜᴇ ɢɪᴠᴇɴ ᴛᴇxᴛ ᴀɴᴅ sᴇɴᴅ ɪᴛ ᴛᴏ ʏᴏᴜ.
+
+❍ /carbon *:* ᴍᴀᴋᴇs ᴄᴀʀʙᴏɴ ɪғ ʀᴇᴩʟɪᴇᴅ ᴛᴏ ᴀ ᴛᴇxᴛ
+
+ """
diff --git a/QueenNoxi/modules/chatbot.py b/QueenNoxi/modules/chatbot.py
new file mode 100644
index 0000000000000000000000000000000000000000..a73f5f9dad7c1620ddf99566453d6d5f7e583d11
--- /dev/null
+++ b/QueenNoxi/modules/chatbot.py
@@ -0,0 +1,164 @@
+import html
+import re
+import aiohttp
+import asyncio
+from pyrogram import filters, Client, enums
+from pyrogram.types import (
+ CallbackQuery,
+ InlineKeyboardButton,
+ InlineKeyboardMarkup,
+ Message,
+)
+
+import QueenNoxi.modules.sql.chatbot_sql as sql
+from QueenNoxi import BOT_ID, BOT_NAME, BOT_USERNAME, pbot
+from QueenNoxi.modules.helper_funcs.chat_status import user_admin
+from QueenNoxi.modules.log_channel import gloggable
+from MukeshAPI import api
+
+import os
+import subprocess
+
+async def get_response(text, user_id="anon", group_id=None):
+ try:
+ # Use subprocess to call the Node.js bridge
+ brain_path = os.path.join(os.getcwd(), "QueenNoxi", "brain", "query.js")
+ if not os.path.exists(brain_path):
+ brain_path = os.path.join(os.getcwd(), "brain", "query.js")
+
+ cmd = ["node", brain_path, "query", text, str(user_id)]
+ if group_id:
+ cmd.append(str(group_id))
+
+ process = await asyncio.create_subprocess_exec(
+ *cmd,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE
+ )
+ stdout, stderr = await process.communicate()
+
+ if process.returncode == 0:
+ return stdout.decode().strip()
+ else:
+ err = stderr.decode().strip()
+ return f"Error from Brain: {err}"
+ except Exception as e:
+ return f"Error calling Brain: {e}"
+
+async def learn_brain(q, a):
+ try:
+ brain_path = os.path.join(os.getcwd(), "QueenNoxi", "brain", "query.js")
+ if not os.path.exists(brain_path):
+ brain_path = os.path.join(os.getcwd(), "brain", "query.js")
+
+ process = await asyncio.create_subprocess_exec(
+ "node", brain_path, "learn", q, a,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE
+ )
+ stdout, stderr = await process.communicate()
+ return process.returncode == 0
+ except Exception:
+ return False
+
+
+
+@pbot.on_callback_query(filters.regex(r"(add|rm)_chat\("))
+@gloggable
+async def chatbot_callback(client: Client, query: CallbackQuery):
+ user = query.from_user
+ chat = query.message.chat
+
+ member = await client.get_chat_member(chat.id, user.id)
+ if member.status not in (enums.ChatMemberStatus.ADMINISTRATOR, enums.ChatMemberStatus.OWNER):
+ await query.answer("You must be an admin to do this!", show_alert=True)
+ return
+
+ action = query.data.split("_")[0]
+ if action == "add":
+ sql.rem_queennoxi(chat.id)
+ await query.message.edit_text(f"{BOT_NAME} chatbot enabled by {user.mention}.")
+ return f"{html.escape(chat.title)}:\nᴀɪ ᴇɴᴀʙʟᴇ\nᴀᴅᴍɪɴ : {user.mention}\n"
+ else:
+ sql.set_queennoxi(chat.id)
+ await query.message.edit_text(f"{BOT_NAME} chatbot disabled by {user.mention}.")
+ return f"{html.escape(chat.title)}:\nᴀɪ ᴅɪsᴀʙʟᴇᴅ\nᴀᴅᴍɪɴ : {user.mention}\n"
+
+@pbot.on_message(filters.command("chatbot") & filters.group)
+@user_admin
+async def chatbot_toggle(client: Client, message: Message):
+ msg = "• ᴄʜᴏᴏsᴇ ᴀɴ ᴏᴩᴛɪᴏɴ ᴛᴏ ᴇɴᴀʙʟᴇ/ᴅɪsᴀʙʟᴇ ᴄʜᴀᴛʙᴏᴛ"
+ keyboard = InlineKeyboardMarkup(
+ [
+ [
+ InlineKeyboardButton(text="ᴇɴᴀʙʟᴇ", callback_data=f"add_chat({message.from_user.id})"),
+ InlineKeyboardButton(text="ᴅɪsᴀʙʟᴇ", callback_data=f"rm_chat({message.from_user.id})"),
+ ],
+ ]
+ )
+ await message.reply_text(msg, reply_markup=keyboard)
+
+@pbot.on_message(
+ filters.text
+ & ~filters.command(["chatbot"])
+ & ~filters.regex(r"^/")
+ & ~filters.regex(r"^#[^\s]+")
+ & ~filters.regex(r"^!")
+ & filters.group
+)
+async def chatbot_msg(client: Client, message: Message):
+ chat_id = message.chat.id
+ if sql.is_queennoxi(chat_id):
+ return
+
+ should_reply = False
+ if message.text.lower() == "queennoxi":
+ should_reply = True
+ elif f"@{BOT_USERNAME}" in message.text:
+ should_reply = True
+ elif message.reply_to_message and message.reply_to_message.from_user and message.reply_to_message.from_user.id == BOT_ID:
+ should_reply = True
+
+ if should_reply:
+ await client.send_chat_action(chat_id, enums.ChatAction.TYPING)
+ response = await get_response(message.text, message.from_user.id, chat_id)
+ if response and response.strip():
+ await message.reply_text(response)
+
+
+@pbot.on_message(filters.command("learn") & filters.reply & filters.group)
+@user_admin
+async def learn_handler(client: Client, message: Message):
+ message_b = message.reply_to_message
+
+ # but more reliably we might need to fetch it or check message_b.reply_to_message_id
+
+ if not message_b.reply_to_message:
+ # If not cached, we can try to get it
+ try:
+ reply_to = await client.get_messages(message.chat.id, message_b.reply_to_message_id)
+ message_a = reply_to
+ except Exception:
+ await message.reply_text("I couldn't find the original message this was replying to.")
+ return
+ else:
+ message_a = message_b.reply_to_message
+
+ q = message_a.text or message_a.caption
+ a = message_b.text or message_b.caption
+
+ if not q or not a:
+ await message.reply_text("Both messages must contain text to learn.")
+ return
+
+ success = await learn_brain(q, a)
+ if success:
+ await message.reply_text(f"✅ Learned successfully!\n**Q:** `{q}`\n**A:** `{a}`")
+ else:
+ await message.reply_text("❌ Failed to learn pattern.")
+
+
+__mod_name__ = "Chatbot"
+__help__ = """
+ • `/chatbot`: Toggle chatbot in the group.
+"""
diff --git a/QueenNoxi/modules/chatgpt.py b/QueenNoxi/modules/chatgpt.py
new file mode 100644
index 0000000000000000000000000000000000000000..b9eb85476f6180a94253da1b1e26b56eddf41f28
--- /dev/null
+++ b/QueenNoxi/modules/chatgpt.py
@@ -0,0 +1,28 @@
+import requests
+from .. import pbot as QueenNoxi,BOT_NAME,BOT_USERNAME
+import time
+from pyrogram.enums import ChatAction, ParseMode
+from pyrogram import filters
+from MukeshAPI import api
+@QueenNoxi.on_message(filters.command(["chatgpt","ai","ask"], prefixes=["+", ".", "/", "-", "?", "$","#","&"]))
+async def chat_gpt(bot, message):
+
+ try:
+ await bot.send_chat_action(message.chat.id, ChatAction.TYPING)
+ if len(message.command) < 2:
+ await message.reply_text(
+ "Example:**\n\n`/chatgpt Where is TajMahal?`")
+ else:
+ a = message.text.split(' ', 1)[1]
+ r=api.gemini(a)["results"]
+ await message.reply_text(f" {r} \n\n🎉ᴘᴏᴡᴇʀᴇᴅ ʙʏ @{BOT_USERNAME} ", parse_mode=ParseMode.MARKDOWN)
+ except Exception as e:
+ await message.reply_text(f"**ᴇʀʀᴏʀ: {e} ")
+
+__mod_name__ = "Cʜᴀᴛɢᴘᴛ"
+__help__ = """
+ Cʜᴀᴛɢᴘᴛ ᴄᴀɴ ᴀɴsᴡᴇʀ ʏᴏᴜʀ ǫᴜᴇsᴛɪᴏɴ ᴀɴᴅ sʜᴏᴡs ʏᴏᴜ ᴛʜᴇ ʀᴇsᴜʟᴛ
+
+ ❍ /chatgpt *:* ʀᴇᴘʟʏ ᴛo ᴍᴇssᴀɢᴇ ᴏʀ ɢɪᴠᴇ sᴏᴍᴇ ᴛᴇxᴛ
+
+ """
diff --git a/QueenNoxi/modules/cleaner.py b/QueenNoxi/modules/cleaner.py
new file mode 100644
index 0000000000000000000000000000000000000000..3468f814435398e7f69eb25663a269975de62e61
--- /dev/null
+++ b/QueenNoxi/modules/cleaner.py
@@ -0,0 +1,260 @@
+import html
+
+from telegram import ParseMode, Update
+from telegram.ext import CallbackContext, CommandHandler, Filters, MessageHandler
+
+from QueenNoxi import ALLOW_EXCL, CustomCommandHandler, dispatcher
+from QueenNoxi.modules.disable import DisableAbleCommandHandler
+from QueenNoxi.modules.helper_funcs.chat_status import (
+ bot_can_delete,
+ connection_status,
+ dev_plus,
+ user_admin,
+)
+from QueenNoxi.modules.sql import cleaner_sql as sql
+
+CMD_STARTERS = ("/", "!") if ALLOW_EXCL else "/"
+BLUE_TEXT_CLEAN_GROUP = 13
+CommandHandlerList = (CommandHandler, CustomCommandHandler, DisableAbleCommandHandler)
+command_list = [
+ "cleanblue",
+ "ignoreblue",
+ "unignoreblue",
+ "listblue",
+ "ungignoreblue",
+ "gignoreblue" "start",
+ "help",
+ "settings",
+ "donate",
+ "stalk",
+ "aka",
+ "leaderboard",
+]
+
+for handler_list in dispatcher.handlers:
+ for handler in dispatcher.handlers[handler_list]:
+ if any(isinstance(handler, cmd_handler) for cmd_handler in CommandHandlerList):
+ command_list += handler.command
+
+
+def clean_blue_text_must_click(update: Update, context: CallbackContext):
+ bot = context.bot
+ chat = update.effective_chat
+ message = update.effective_message
+ if chat.get_member(bot.id).can_delete_messages and sql.is_enabled(chat.id):
+ fst_word = message.text.strip().split(None, 1)[0]
+
+ if len(fst_word) > 1 and any(
+ fst_word.startswith(start) for start in CMD_STARTERS
+ ):
+
+ command = fst_word[1:].split("@")
+ chat = update.effective_chat
+
+ ignored = sql.is_command_ignored(chat.id, command[0])
+ if ignored:
+ return
+
+ if command[0] not in command_list:
+ message.delete()
+
+
+@connection_status
+@bot_can_delete
+@user_admin
+def set_blue_text_must_click(update: Update, context: CallbackContext):
+ chat = update.effective_chat
+ message = update.effective_message
+ bot, args = context.bot, context.args
+ if len(args) >= 1:
+ val = args[0].lower()
+ if val in ("off", "no"):
+ sql.set_cleanbt(chat.id, False)
+ reply = "Bluetext cleaning has been disabled for {}".format(
+ html.escape(chat.title)
+ )
+ message.reply_text(reply, parse_mode=ParseMode.HTML)
+
+ elif val in ("yes", "on"):
+ sql.set_cleanbt(chat.id, True)
+ reply = "Bluetext cleaning has been enabled for {}".format(
+ html.escape(chat.title)
+ )
+ message.reply_text(reply, parse_mode=ParseMode.HTML)
+
+ else:
+ reply = "Invalid argument.Accepted values are 'yes', 'on', 'no', 'off'"
+ message.reply_text(reply)
+ else:
+ clean_status = sql.is_enabled(chat.id)
+ clean_status = "Enabled" if clean_status else "Disabled"
+ reply = "Bluetext cleaning for {} : {}".format(
+ html.escape(chat.title), clean_status
+ )
+ message.reply_text(reply, parse_mode=ParseMode.HTML)
+
+
+@user_admin
+def add_bluetext_ignore(update: Update, context: CallbackContext):
+ message = update.effective_message
+ chat = update.effective_chat
+ args = context.args
+ if len(args) >= 1:
+ val = args[0].lower()
+ added = sql.chat_ignore_command(chat.id, val)
+ if added:
+ reply = "{} has been added to bluetext cleaner ignore list.".format(
+ args[0]
+ )
+ else:
+ reply = "Command is already ignored."
+ message.reply_text(reply, parse_mode=ParseMode.HTML)
+
+ else:
+ reply = "No command supplied to be ignored."
+ message.reply_text(reply)
+
+
+@user_admin
+def remove_bluetext_ignore(update: Update, context: CallbackContext):
+ message = update.effective_message
+ chat = update.effective_chat
+ args = context.args
+ if len(args) >= 1:
+ val = args[0].lower()
+ removed = sql.chat_unignore_command(chat.id, val)
+ if removed:
+ reply = (
+ "{} has been removed from bluetext cleaner ignore list.".format(
+ args[0]
+ )
+ )
+ else:
+ reply = "Command isn't ignored currently."
+ message.reply_text(reply, parse_mode=ParseMode.HTML)
+
+ else:
+ reply = "No command supplied to be unignored."
+ message.reply_text(reply)
+
+
+@user_admin
+def add_bluetext_ignore_global(update: Update, context: CallbackContext):
+ message = update.effective_message
+ args = context.args
+ if len(args) >= 1:
+ val = args[0].lower()
+ added = sql.global_ignore_command(val)
+ if added:
+ reply = "{} has been added to global bluetext cleaner ignore list.".format(
+ args[0]
+ )
+ else:
+ reply = "Command is already ignored."
+ message.reply_text(reply, parse_mode=ParseMode.HTML)
+
+ else:
+ reply = "No command supplied to be ignored."
+ message.reply_text(reply)
+
+
+@dev_plus
+def remove_bluetext_ignore_global(update: Update, context: CallbackContext):
+ message = update.effective_message
+ args = context.args
+ if len(args) >= 1:
+ val = args[0].lower()
+ removed = sql.global_unignore_command(val)
+ if removed:
+ reply = "{} has been removed from global bluetext cleaner ignore list.".format(
+ args[0]
+ )
+ else:
+ reply = "Command isn't ignored currently."
+ message.reply_text(reply, parse_mode=ParseMode.HTML)
+
+ else:
+ reply = "No command supplied to be unignored."
+ message.reply_text(reply)
+
+
+@dev_plus
+def bluetext_ignore_list(update: Update, context: CallbackContext):
+
+ message = update.effective_message
+ chat = update.effective_chat
+
+ global_ignored_list, local_ignore_list = sql.get_all_ignored(chat.id)
+ text = ""
+
+ if global_ignored_list:
+ text = "The following commands are currently ignored globally from bluetext cleaning :\n"
+
+ for x in global_ignored_list:
+ text += f" - {x}\n"
+
+ if local_ignore_list:
+ text += "\nThe following commands are currently ignored locally from bluetext cleaning :\n"
+
+ for x in local_ignore_list:
+ text += f" - {x}\n"
+
+ if text == "":
+ text = "No commands are currently ignored from bluetext cleaning."
+ message.reply_text(text)
+ return
+
+ message.reply_text(text, parse_mode=ParseMode.HTML)
+ return
+
+
+__help__ = """
+*ʙʟᴜᴇ ᴛᴇxᴛ ᴄʟᴇᴀɴᴇʀ* ʀᴇᴍᴏᴠᴇᴅ ᴀɴʏ ᴍᴀᴅᴇ ᴜᴘ ᴄᴏᴍᴍᴀɴᴅs ᴛʜᴀᴛ ᴘᴇᴏᴘʟᴇ sᴇɴᴅ ɪɴ ʏᴏᴜʀ ᴄʜᴀᴛ.
+ ❍ /cleanblue <ᴏɴ/ᴏғғ/ʏᴇs/ɴᴏ>*:* ᴄʟᴇᴀɴ ᴄᴏᴍᴍᴀɴᴅs ᴀғᴛᴇʀ sᴇɴᴅɪɴɢ
+ ❍ /ignoreblue <ᴡᴏʀᴅ>*:* ᴘʀᴇᴠᴇɴᴛ ᴀᴜᴛᴏ ᴄʟᴇᴀɴɪɴɢ ᴏғ ᴛʜᴇ ᴄᴏᴍᴍᴀɴᴅ
+ ❍ /unignoreblue <ᴡᴏʀᴅ>*:* ʀᴇᴍᴏᴠᴇ ᴘʀᴇᴠᴇɴᴛ ᴀᴜᴛᴏ ᴄʟᴇᴀɴɪɴɢ ᴏғ ᴛʜᴇ ᴄᴏᴍᴍᴀɴᴅ
+ ❍ /listblue *:* ʟɪsᴛ ᴄᴜʀʀᴇɴᴛʟʏ ᴡʜɪᴛᴇʟɪsᴛᴇᴅ ᴄᴏᴍᴍᴀɴᴅs
+"""
+
+SET_CLEAN_BLUE_TEXT_HANDLER = CommandHandler(
+ "cleanblue", set_blue_text_must_click, run_async=True
+)
+ADD_CLEAN_BLUE_TEXT_HANDLER = CommandHandler(
+ "ignoreblue", add_bluetext_ignore, run_async=True
+)
+REMOVE_CLEAN_BLUE_TEXT_HANDLER = CommandHandler(
+ "unignoreblue", remove_bluetext_ignore, run_async=True
+)
+ADD_CLEAN_BLUE_TEXT_GLOBAL_HANDLER = CommandHandler(
+ "gignoreblue", add_bluetext_ignore_global, run_async=True
+)
+REMOVE_CLEAN_BLUE_TEXT_GLOBAL_HANDLER = CommandHandler(
+ "ungignoreblue", remove_bluetext_ignore_global, run_async=True
+)
+LIST_CLEAN_BLUE_TEXT_HANDLER = CommandHandler(
+ "listblue", bluetext_ignore_list, run_async=True
+)
+CLEAN_BLUE_TEXT_HANDLER = MessageHandler(
+ Filters.command & Filters.chat_type.groups,
+ clean_blue_text_must_click,
+ run_async=True,
+)
+
+dispatcher.add_handler(SET_CLEAN_BLUE_TEXT_HANDLER)
+dispatcher.add_handler(ADD_CLEAN_BLUE_TEXT_HANDLER)
+dispatcher.add_handler(REMOVE_CLEAN_BLUE_TEXT_HANDLER)
+dispatcher.add_handler(ADD_CLEAN_BLUE_TEXT_GLOBAL_HANDLER)
+dispatcher.add_handler(REMOVE_CLEAN_BLUE_TEXT_GLOBAL_HANDLER)
+dispatcher.add_handler(LIST_CLEAN_BLUE_TEXT_HANDLER)
+dispatcher.add_handler(CLEAN_BLUE_TEXT_HANDLER, BLUE_TEXT_CLEAN_GROUP)
+
+__mod_name__ = "Cʟᴇᴀɴ"
+__handlers__ = [
+ SET_CLEAN_BLUE_TEXT_HANDLER,
+ ADD_CLEAN_BLUE_TEXT_HANDLER,
+ REMOVE_CLEAN_BLUE_TEXT_HANDLER,
+ ADD_CLEAN_BLUE_TEXT_GLOBAL_HANDLER,
+ REMOVE_CLEAN_BLUE_TEXT_GLOBAL_HANDLER,
+ LIST_CLEAN_BLUE_TEXT_HANDLER,
+ (CLEAN_BLUE_TEXT_HANDLER, BLUE_TEXT_CLEAN_GROUP),
+]
diff --git a/QueenNoxi/modules/connection.py b/QueenNoxi/modules/connection.py
new file mode 100644
index 0000000000000000000000000000000000000000..81ca33e4816263eee4718e783cd4a55cbd087de9
--- /dev/null
+++ b/QueenNoxi/modules/connection.py
@@ -0,0 +1,158 @@
+import re
+from pyrogram import filters, Client, enums
+from pyrogram.types import InlineKeyboardButton, InlineKeyboardMarkup, Message, CallbackQuery
+from pyrogram.errors import BadRequest, Unauthorized
+
+import QueenNoxi.modules.sql.connection_sql as sql
+from QueenNoxi import DEV_USERS, DRAGONS, pbot
+from QueenNoxi.modules.disable import DisableAbleCommandHandler
+from QueenNoxi.modules.helper_funcs.chat_status import user_admin
+
+@pbot.on_message(filters.command("allowconnect") & filters.group)
+@user_admin
+async def allow_connections(client: Client, message: Message):
+ chat = message.chat
+ args = message.command[1:]
+
+ if not args:
+ status = sql.allow_connect_to_chat(chat.id)
+ await message.reply_text(f"Connection status: {'Allowed' if status else 'Disabled'}")
+ return
+
+ if args[0].lower() in ("yes", "on", "true"):
+ sql.set_allow_connect_to_chat(chat.id, True)
+ await message.reply_text("Connection allowed!")
+ elif args[0].lower() in ("no", "off", "false"):
+ sql.set_allow_connect_to_chat(chat.id, False)
+ await message.reply_text("Connection disabled.")
+ else:
+ await message.reply_text("Please use 'on' or 'off'.")
+
+@pbot.on_message(filters.command("connect") & filters.private)
+async def connect_chat(client: Client, message: Message):
+ user = message.from_user
+ args = message.command[1:]
+
+ if not args:
+ await message.reply_text("Please provide a chat ID to connect to.")
+ return
+
+ chat_id = args[0]
+ try:
+ chat_id = int(chat_id)
+ except ValueError:
+ pass # could be username
+
+ try:
+ chat = await client.get_chat(chat_id)
+ except Exception as e:
+ await message.reply_text(f"Invalid chat! Error: {e}")
+ return
+
+ try:
+ member = await chat.get_member(user.id)
+ except Exception:
+ await message.reply_text("You are not a member of that chat!")
+ return
+
+ is_admin = member.status in (enums.ChatMemberStatus.ADMINISTRATOR, enums.ChatMemberStatus.OWNER)
+ is_allow = sql.allow_connect_to_chat(chat.id)
+
+ if is_admin or (is_allow and user.id not in DRAGONS): # Simplify logic
+ if sql.connect(user.id, chat.id):
+ await message.reply_text(f"Successfully connected to **{chat.title}**.")
+ sql.add_history_conn(user.id, str(chat.id), chat.title)
+ else:
+ await message.reply_text("Connection failed!")
+ else:
+ await message.reply_text("Connection to this chat is not allowed or you are not an admin!")
+
+@pbot.on_message(filters.command("disconnect") & filters.private)
+async def disconnect_chat(client: Client, message: Message):
+ if sql.disconnect(message.from_user.id):
+ await message.reply_text("Disconnected from chat!")
+ else:
+ await message.reply_text("You're not connected!")
+
+@pbot.on_message(filters.command("connection") & filters.private)
+async def connection_chat(client: Client, message: Message):
+ conn = sql.get_connected_chat(message.from_user.id)
+ if conn:
+ try:
+ chat = await client.get_chat(conn.chat_id)
+ await message.reply_text(f"You are currently connected to **{chat.title}** (`{chat.id}`).")
+ except:
+ await message.reply_text("You are connected to an inaccessible chat.")
+ else:
+ await message.reply_text("You are not connected to any chat.")
+
+CONN_HELP = """
+*ᴀᴄᴛɪᴏɴs ᴀʀᴇ ᴀᴠᴀɪʟᴀʙʟᴇ ᴡɪᴛʜ ᴄᴏɴɴᴇᴄᴛᴇᴅ ɢʀᴏᴜᴘs:*
+
+ • ᴠɪᴇᴡ ᴀɴᴅ ᴇᴅɪᴛ ɴᴏᴛᴇs.
+ • ᴠɪᴇᴡ ᴀɴᴅ ᴇᴅɪᴛ ғɪʟᴛᴇʀs.
+ • ɢᴇᴛ ɪɴᴠɪᴛᴇ ʟɪɴᴋ ᴏғ ᴄʜᴀᴛ.
+ • sᴇᴛ ᴀɴᴅ ᴄᴏɴᴛʀᴏʟ ᴀɴᴛɪғʟᴏᴏᴅ sᴇᴛᴛɪɴɢs.
+ • sᴇᴛ ᴀɴᴅ ᴄᴏɴᴛʀᴏʟ ʙʟᴀᴄᴋʟɪsᴛ sᴇᴛᴛɪɴɢs.
+ • sᴇᴛ ʟᴏᴄᴋs ᴀɴᴅ ᴜɴʟᴏᴄᴋs ɪɴ ᴄʜᴀᴛ.
+ • ᴇɴᴀʙʟᴇ ᴀɴᴅ ᴅɪsᴀʙʟᴇ ᴄᴏᴍᴍᴀɴᴅs ɪɴ ᴄʜᴀᴛ.
+ • ᴇxᴘᴏʀᴛ ᴀɴᴅ ɪᴍᴘᴏʀᴛ sᴇᴛᴛɪɴɢs.
+"""
+
+@pbot.on_message(filters.command("helpconnect") & filters.private)
+async def help_connect_chat(client: Client, message: Message):
+ await message.reply_text(CONN_HELP)
+
+@pbot.on_callback_query(filters.regex(r"^connect_"))
+async def connect_button(client: Client, query: CallbackQuery):
+ data = query.data
+ user_id = query.from_user.id
+
+ if data == "connect_disconnect":
+ if sql.disconnect(user_id):
+ await query.message.edit_text("Disconnected from chat!")
+ else:
+ await query.answer("You're not connected!", show_alert=True)
+
+ elif data == "connect_clear":
+ sql.clear_history_conn(user_id)
+ await query.message.edit_text("Connection history cleared.")
+
+ elif data == "connect_close":
+ await query.message.delete()
+
+async def connected(client: Client, message: Message, user_id: int, need_admin: bool = True):
+ if message.chat.type != enums.ChatType.PRIVATE:
+ return message.chat.id
+
+ conn = sql.get_connected_chat(user_id)
+ if not conn:
+ return False
+
+ chat_id = conn.chat_id
+ try:
+ member = await client.get_chat_member(chat_id, user_id)
+ except Exception:
+ sql.disconnect(user_id)
+ return False
+
+ is_admin = member.status in (enums.ChatMemberStatus.ADMINISTRATOR, enums.ChatMemberStatus.OWNER)
+ if need_admin and not is_admin and user_id not in DRAGONS:
+ await message.reply_text("You must be an admin in the connected group!")
+ return False
+
+ return chat_id
+
+__mod_name__ = "Cᴏɴɴᴇᴄᴛ"
+__help__ = """
+sᴏᴍᴇᴛɪᴍᴇs, ʏᴏᴜ ᴊᴜsᴛ ᴡᴀɴᴛ ᴛᴏ ᴀᴅᴅ sᴏᴍᴇ ɴᴏᴛᴇs ᴀɴᴅ ғɪʟᴛᴇʀs ᴛᴏ ᴀ ɢʀᴏᴜᴘ ᴄʜᴀᴛ, ʙᴜᴛ ʏᴏᴜ ᴅᴏɴ'ᴛ ᴡᴀɴᴛ ᴇᴠᴇʀʏᴏɴᴇ ᴛᴏ sᴇᴇ; ᴛʜɪs ɪs ᴡʜᴇʀᴇ ᴄᴏɴɴᴇᴄᴛɪᴏɴs ᴄᴏᴍᴇ ɪɴ...
+ᴛʜɪs ᴀʟʟᴏᴡs ʏᴏᴜ ᴛᴏ ᴄᴏɴɴᴇᴄᴛ ᴛᴏ ᴀ ᴄʜᴀᴛ's ᴅᴀᴛᴀʙᴀsᴇ, ᴀɴᴅ ᴀᴅᴅ ᴛʜɪɴɢs ᴛᴏ ɪᴛ ᴡɪᴛʜᴏᴜᴛ ᴛʜᴇ ᴄᴏᴍᴍᴀɴᴅs ᴀᴘᴘᴇᴀʀɪɴɢ ɪɴ ᴄʜᴀᴛ!
+
+ ❍ /connect <ᴄʜᴀᴛ ɪᴅ>: ᴄᴏɴɴᴇᴄᴛs ᴛᴏ ᴄʜᴀᴛ (PW only)
+ ❍ /connection: ᴄᴏɴɴᴇᴄᴛᴇᴅ ᴄʜᴀᴛ ɪɴғᴏ
+ ❍ /disconnect: ᴅɪsᴄᴏɴɴᴇᴄᴛ ғʀᴏᴍ ᴀ ᴄʜᴀᴛ
+ ❍ /helpconnect: ʟɪsᴛ ᴀᴠᴀɪʟᴀʙʟᴇ ᴄᴏᴍᴍᴀɴᴅs ᴛʜᴀᴛ ᴄᴀɴ ʙᴇ ᴜsᴇᴅ ʀᴇᴍᴏᴛᴇʟʏ
+
+*ᴀᴅᴍɪɴ ᴏɴʟʏ:*
+ ❍ /allowconnect <ʏᴇs/ɴᴏ>: ᴀʟʟᴏᴡ ᴀ ᴜsᴇʀ ᴛᴏ ᴄᴏɴɴᴇᴄᴛ ᴛᴏ ᴀ ᴄʜᴀᴛ
+"""
diff --git a/QueenNoxi/modules/country.py b/QueenNoxi/modules/country.py
new file mode 100644
index 0000000000000000000000000000000000000000..c5ec64e18beddf6ce8b1d1b166cef83c0c44d7c9
--- /dev/null
+++ b/QueenNoxi/modules/country.py
@@ -0,0 +1,117 @@
+import flag
+from countryinfo import CountryInfo
+
+from pyrogram import filters
+from QueenNoxi import pbot as app, BOT_USERNAME
+
+@app.on_message(filters.command("country"))
+async def msg(_, message):
+ if getattr(message, "forward_from", None):
+ return
+ if len(message.command) < 2:
+ return
+ input_str = message.text.split(None, 1)[1]
+ lol = input_str
+ country = CountryInfo(lol)
+ try:
+ a = country.info()
+ except Exception:
+ await message.reply("Country Not Available Currently")
+ return
+ name = a.get("name")
+ bb = a.get("altSpellings")
+ hu = ""
+ for p in bb:
+ hu += p + ", "
+
+ area = a.get("area")
+ borders = ""
+ hell = a.get("borders")
+ for fk in hell:
+ borders += fk + ", "
+
+ call = ""
+ WhAt = a.get("callingCodes")
+ for what in WhAt:
+ call += what + " "
+
+ capital = a.get("capital")
+ currencies = ""
+ fker = a.get("currencies")
+ for FKer in fker:
+ currencies += FKer + ", "
+
+ HmM = a.get("demonym")
+ geo = a.get("geoJSON")
+ pablo = geo.get("features")
+ Pablo = pablo[0]
+ PAblo = Pablo.get("geometry")
+ EsCoBaR = PAblo.get("type")
+ iso = ""
+ iSo = a.get("ISO")
+ for hitler in iSo:
+ po = iSo.get(hitler)
+ iso += po + ", "
+ fla = iSo.get("alpha2")
+ nox = fla.upper()
+ okie = flag.flag(nox)
+
+ languages = a.get("languages")
+ lMAO = ""
+ for lmao in languages:
+ lMAO += lmao + ", "
+
+ nonive = a.get("nativeName")
+ waste = a.get("population")
+ reg = a.get("region")
+ sub = a.get("subregion")
+ tik = a.get("timezones")
+ tom = ""
+ for jerry in tik:
+ tom += jerry + ", "
+
+ GOT = a.get("tld")
+ lanester = ""
+ for targaryen in GOT:
+ lanester += targaryen + ", "
+
+ wiki = a.get("wiki")
+
+ caption = f"""ɪɴғᴏʀᴍᴀᴛɪᴏɴ ɢᴀᴛʜᴇʀᴇᴅ sᴜᴄᴇssғᴜʟʟʏ
+
+ᴄᴏᴜɴᴛʀʏ ɴᴀᴍᴇ : {name}
+ᴀʟᴛᴇʀɴᴀᴛɪᴠᴇ sᴘᴇʟʟɪɴɢs : {hu}
+ᴄᴏᴜɴᴛʀʏ ᴀʀᴇᴀ : {area} square kilometers
+ʙᴏʀᴅᴇʀs : {borders}
+ᴄᴀʟʟɪɴɢ ᴄᴏᴅᴇs : {call}
+ᴄᴏᴜɴᴛʀʏ's ᴄᴀᴘɪᴛᴀʟ : {capital}
+ᴄᴏᴜɴᴛʀʏ's ᴄᴜʀʀᴇɴᴄʏ : {currencies}
+ᴄᴏᴜɴᴛʀʏ's ғʟᴀɢ : {okie}
+ᴅᴇᴍᴏʏᴍ: {HmM}
+ᴄᴏᴜɴᴛʀʏ ᴛʏᴘᴇ : {EsCoBaR}
+ɪsᴏ ɴᴀᴍᴇs : {iso}
+ʟᴀɴɢᴜᴀɢᴇs : {lMAO}
+ɴᴀᴛɪᴠᴇ ɴᴀᴍᴇs : {nonive}
+ᴘᴏᴘᴜʟᴀᴛɪᴏɴs : {waste}
+ʀᴇɢɪᴏɴ : {reg}
+sᴜʙ ʀᴇɢɪᴏɴ : {sub}
+ᴛɪᴍᴇ ᴢᴏɴᴇs : {tom}
+ᴛᴏᴛᴀʟ ʟᴇᴠᴇʟ ᴅᴏᴍᴀɪɴ : {lanester}
+ᴡɪᴋɪᴘᴇᴅɪᴀ: {wiki}
+
+ɪɴғᴏʀᴍᴀᴛɪᴏɴ ɢᴀᴛʜᴇʀᴇᴅ ʙʏ @{BOT_USERNAME}
+"""
+
+ await message.reply(
+ caption,
+ disable_web_page_preview=True,
+ )
+
+
+__help__ = """
+ɪ ᴡɪʟʟ ɢɪᴠᴇ ɪɴғᴏʀᴍᴀᴛɪᴏɴ ᴀʙᴏᴜᴛ ᴀ ᴄᴏᴜɴᴛʀʏ
+
+ ❍ /country <ᴄᴏᴜɴᴛʀʏ ɴᴀᴍᴇ>*:* ɢᴀᴛʜᴇʀɪɴɢ ɪɴғᴏ ᴀʙᴏᴜᴛ ɢɪᴠᴇɴ ᴄᴏᴜɴᴛʀʏ
+"""
+
+__mod_name__ = "Cᴏᴜɴᴛʀʏ"
diff --git a/QueenNoxi/modules/couples.py b/QueenNoxi/modules/couples.py
new file mode 100644
index 0000000000000000000000000000000000000000..c83677c287f48b34d435579360d73ec7db64f7dc
--- /dev/null
+++ b/QueenNoxi/modules/couples.py
@@ -0,0 +1,84 @@
+import random
+from datetime import datetime
+
+from pyrogram import filters
+from pyrogram.enums import ChatType
+
+from QueenNoxi import pbot
+from QueenNoxi.utils.mongo import get_couple, save_couple
+
+
+# Date and time
+def dt():
+ now = datetime.now()
+ dt_string = now.strftime("%d/%m/%Y %H:%M")
+ dt_list = dt_string.split(" ")
+ return dt_list
+
+
+def dt_tom():
+ a = (
+ str(int(dt()[0].split("/")[0]) + 1)
+ + "/"
+ + dt()[0].split("/")[1]
+ + "/"
+ + dt()[0].split("/")[2]
+ )
+ return a
+
+
+today = str(dt()[0])
+tomorrow = str(dt_tom())
+
+
+@pbot.on_message(filters.command(["couple", "couples"]))
+async def couple(_, message):
+ if message.chat.type == ChatType.PRIVATE:
+ return await message.reply_text("ᴛʜɪs ᴄᴏᴍᴍᴀɴᴅ ᴏɴʟʏ ᴡᴏʀᴋs ɪɴ ɢʀᴏᴜᴘ.")
+ try:
+ chat_id = message.chat.id
+ is_selected = await get_couple(chat_id, today)
+ if not is_selected:
+ list_of_users = []
+ async for i in pbot.get_chat_members(message.chat.id, limit=50):
+ if not i.user.is_bot:
+ list_of_users.append(i.user.id)
+ if len(list_of_users) < 2:
+ return await message.reply_text("ɴᴏᴛ ᴇɴᴏᴜɢʜ ᴜsᴇʀ")
+ c1_id = random.choice(list_of_users)
+ c2_id = random.choice(list_of_users)
+ while c1_id == c2_id:
+ c1_id = random.choice(list_of_users)
+ c1_mention = (await pbot.get_users(c1_id)).mention
+ c2_mention = (await pbot.get_users(c2_id)).mention
+
+ couple_selection_message = f"""**ᴄᴏᴜᴘʟᴇ ᴏғ ᴛʜᴇ ᴅᴀʏ :**
+
+{c1_mention} + {c2_mention} = 💗
+ɴᴇᴡ ᴄᴏᴜᴘʟᴇ ᴏғ ᴛʜᴇ ᴅᴀʏ ᴄᴀɴ ʙᴇ ᴄʜᴏsᴇɴ ᴀᴛ 12 ᴀᴍ {tomorrow}"""
+ await pbot.send_message(message.chat.id, text=couple_selection_message)
+ couple = {"c1_id": c1_id, "c2_id": c2_id}
+ await save_couple(chat_id, today, couple)
+
+ elif is_selected:
+ c1_id = int(is_selected["c1_id"])
+ c2_id = int(is_selected["c2_id"])
+ c1_name = (await pbot.get_users(c1_id)).mention
+ c2_name = (await pbot.get_users(c2_id)).mention
+ couple_selection_message = f"""ᴄᴏᴜᴘʟᴇ ᴏғ ᴛʜᴇ ᴅᴀʏ :
+
+{c1_name} + {c2_name} = 💗
+ɴᴇᴡ ᴄᴏᴜᴘʟᴇ ᴏғ ᴛʜᴇ ᴅᴀʏ ᴄᴀɴ ʙᴇ ᴄʜᴏsᴇɴ ᴀᴛ 12 ᴀᴍ {tomorrow}"""
+ await pbot.send_message(message.chat.id, text=couple_selection_message)
+ except Exception as e:
+ print(e)
+ await message.reply_text(e)
+
+
+__help__ = """
+ᴄʜᴏᴏsᴇ ᴄᴏᴜᴘʟᴇs ɪɴ ʏᴏᴜʀ ᴄʜᴀᴛ
+
+ ❍ /couples *:* ᴄʜᴏᴏsᴇ 2 ᴜsᴇʀs ᴀɴᴅ sᴇɴᴅ ᴛʜᴇɪʀ ɴᴀᴍᴇ ᴀs ᴄᴏᴜᴘʟᴇs ɪɴ ʏᴏᴜʀ ᴄʜᴀᴛ.
+"""
+
+__mod_name__ = "Cᴏᴜᴘʟᴇ"
diff --git a/QueenNoxi/modules/currency_converter.py b/QueenNoxi/modules/currency_converter.py
new file mode 100644
index 0000000000000000000000000000000000000000..a773874f2ec2f6229ac083e7e3ca6eca78f48d12
--- /dev/null
+++ b/QueenNoxi/modules/currency_converter.py
@@ -0,0 +1,66 @@
+import requests
+from telegram import ParseMode, Update
+from telegram.ext import CallbackContext, CommandHandler
+
+from QueenNoxi import CASH_API_KEY, dispatcher
+
+
+def convert(update: Update, context: CallbackContext):
+ args = update.effective_message.text.split(" ")
+
+ if len(args) == 4:
+ try:
+ orig_cur_amount = float(args[1])
+
+ except ValueError:
+ update.effective_message.reply_text("Invalid Amount Of Currency")
+ return
+
+ orig_cur = args[2].upper()
+
+ new_cur = args[3].upper()
+
+ request_url = (
+ f"https://www.alphavantage.co/query"
+ f"?function=CURRENCY_EXCHANGE_RATE"
+ f"&from_currency={orig_cur}"
+ f"&to_currency={new_cur}"
+ f"&apikey={CASH_API_KEY}"
+ )
+ response = requests.get(request_url).json()
+ try:
+ current_rate = float(
+ response["Realtime Currency Exchange Rate"]["5. Exchange Rate"]
+ )
+ except KeyError:
+ update.effective_message.reply_text("Currency Not Supported.")
+ return
+ new_cur_amount = round(orig_cur_amount * current_rate, 5)
+ update.effective_message.reply_text(
+ f"{orig_cur_amount} {orig_cur} = {new_cur_amount} {new_cur}"
+ )
+
+ elif len(args) == 1:
+ update.effective_message.reply_text(__help__, parse_mode=ParseMode.MARKDOWN)
+
+ else:
+ update.effective_message.reply_text(
+ f"*Invalid Args!!:* Required 3 But Passed {len(args) -1}",
+ parse_mode=ParseMode.MARKDOWN,
+ )
+
+
+__help__ = """
+Converts money from one exchange to another
+
+Usage: /cash amount from to
+Example: /cash 20 USD INR
+"""
+
+CONVERTER_HANDLER = CommandHandler("cash", convert, run_async=True)
+dispatcher.add_handler(CONVERTER_HANDLER)
+__command_list__ = ["cash"]
+
+__handlers__ = [CONVERTER_HANDLER]
+
+__mod_name__ = "Cᴀsʜ"
diff --git a/QueenNoxi/modules/cust_filters.py b/QueenNoxi/modules/cust_filters.py
new file mode 100644
index 0000000000000000000000000000000000000000..59ccb4513f76106dfb38b7b8106df7a72d293cca
--- /dev/null
+++ b/QueenNoxi/modules/cust_filters.py
@@ -0,0 +1,125 @@
+import re
+import html
+import random
+from pyrogram import filters, Client, enums
+from pyrogram.types import (
+ Message,
+ InlineKeyboardMarkup,
+ InlineKeyboardButton,
+ CallbackQuery,
+)
+from pyrogram.errors import RPCError
+
+import QueenNoxi
+from QueenNoxi import pbot, LOGGER, DRAGONS
+from QueenNoxi.modules.helper_funcs.chat_status import user_admin, connection_status
+from QueenNoxi.modules.helper_funcs.misc import build_keyboard, revert_buttons
+from QueenNoxi.modules.helper_funcs.msg_types import get_filter_type, Types
+from QueenNoxi.modules.helper_funcs.string_handling import button_markdown_parser, split_quotes
+from QueenNoxi.modules.sql import cust_filters_sql as sql
+
+# Handler group for filters
+HANDLER_GROUP = 10
+
+@pbot.on_message(filters.command("filter") & filters.group)
+@user_admin
+@connection_status
+async def add_filter(client: Client, message: Message):
+ chat_id = message.chat.id
+ args = message.text.split(None, 1)
+
+ if len(args) < 2 and not message.reply_to_message:
+ await message.reply_text("Please provide a keyword and reply text for the filter!")
+ return
+
+ if message.reply_to_message:
+ if len(args) < 2:
+ await message.reply_text("Please provide a keyword for this filter!")
+ return
+ keyword = args[1].lower()
+ else:
+ extracted = split_quotes(args[1])
+ if len(extracted) < 2:
+ await message.reply_text("Please provide both a keyword and a reply text!")
+ return
+ keyword = extracted[0].lower()
+
+ text, file_type, file_id = await get_filter_type(message)
+
+ # Extract buttons from text if any
+ _, buttons = button_markdown_parser(text) if text else (None, [])
+
+ sql.new_add_filter(chat_id, keyword, text, file_type, file_id, buttons)
+ await message.reply_text(f"Saved filter '{keyword}'!")
+
+@pbot.on_message(filters.command("stop") & filters.group)
+@user_admin
+@connection_status
+async def stop_filter(client: Client, message: Message):
+ chat_id = message.chat.id
+ args = message.command[1:]
+
+ if not args:
+ await message.reply_text("What filter should I stop?")
+ return
+
+ keyword = args[0].lower()
+ if sql.remove_filter(chat_id, keyword):
+ await message.reply_text(f"Stopped replying to '{keyword}'.")
+ else:
+ await message.reply_text("That's not an active filter!")
+
+@pbot.on_message(filters.command("filters") & filters.group)
+@connection_status
+async def list_handlers(client: Client, message: Message):
+ chat_id = message.chat.id
+ all_handlers = sql.get_chat_triggers(chat_id)
+
+ if not all_handlers:
+ await message.reply_text("No filters saved in this chat!")
+ return
+
+ filter_list = f"**Filters in {html.escape(message.chat.title)}:**\n"
+ for keyword in all_handlers:
+ filter_list += f" • `{keyword}`\n"
+
+ await message.reply_text(filter_list)
+
+@pbot.on_message(filters.text & filters.group, group=HANDLER_GROUP)
+async def reply_filter(client: Client, message: Message):
+ if not message.text:
+ return
+
+ chat_id = message.chat.id
+ chat_filters = sql.get_chat_triggers(chat_id)
+
+ for keyword in chat_filters:
+ pattern = rf"( |^|[^\w]){re.escape(keyword)}( |$|[^\w])"
+ if re.search(pattern, message.text, flags=re.IGNORECASE):
+ filt = sql.get_filter(chat_id, keyword)
+ buttons = sql.get_buttons(chat_id, keyword)
+ keyboard = InlineKeyboardMarkup(build_keyboard(buttons)) if buttons else None
+
+ if filt.file_type in (Types.TEXT, Types.BUTTON_TEXT):
+ await message.reply_text(
+ filt.reply_text,
+ reply_markup=keyboard,
+ disable_web_page_preview=True
+ )
+ else:
+ await client.send_cached_media(
+ chat_id,
+ filt.file_id,
+ caption=filt.reply_text,
+ reply_markup=keyboard
+ )
+ break
+
+__mod_name__ = "Filters"
+__help__ = """
+**Admin Commands:**
+• `/filter `: Add a filter
+• `/stop `: Stop a filter
+• `/filters`: List all filters
+• `/removeallfilters`: Stop ALL filters (Owner only)
+"""
diff --git a/QueenNoxi/modules/dbcleanup.py b/QueenNoxi/modules/dbcleanup.py
new file mode 100644
index 0000000000000000000000000000000000000000..1290e1f4fd4e8bb02fedf87df004b664557182d2
--- /dev/null
+++ b/QueenNoxi/modules/dbcleanup.py
@@ -0,0 +1,144 @@
+from time import sleep
+
+from telegram import InlineKeyboardButton, InlineKeyboardMarkup, Update
+from telegram.error import BadRequest, Unauthorized
+from telegram.ext import CallbackContext, CallbackQueryHandler, CommandHandler
+
+import QueenNoxi.modules.sql.global_bans_sql as gban_sql
+import QueenNoxi.modules.sql.users_sql as user_sql
+from QueenNoxi import DEV_USERS, OWNER_ID, dispatcher
+from QueenNoxi.modules.helper_funcs.chat_status import dev_plus
+
+
+def get_invalid_chats(update: Update, context: CallbackContext, remove: bool = False):
+ bot = context.bot
+ chat_id = update.effective_chat.id
+ chats = user_sql.get_all_chats()
+ kicked_chats, progress = 0, 0
+ chat_list = []
+ progress_message = None
+
+ for chat in chats:
+
+ if ((100 * chats.index(chat)) / len(chats)) > progress:
+ progress_bar = f"{progress}% completed in getting invalid chats."
+ if progress_message:
+ try:
+ bot.editMessageText(
+ progress_bar, chat_id, progress_message.message_id
+ )
+ except:
+ pass
+ else:
+ progress_message = bot.sendMessage(chat_id, progress_bar)
+ progress += 5
+
+ cid = chat.chat_id
+ sleep(0.1)
+ try:
+ bot.get_chat(cid, timeout=60)
+ except (BadRequest, Unauthorized):
+ kicked_chats += 1
+ chat_list.append(cid)
+ except:
+ pass
+
+ try:
+ progress_message.delete()
+ except:
+ pass
+
+ if not remove:
+ return kicked_chats
+ else:
+ for muted_chat in chat_list:
+ sleep(0.1)
+ user_sql.rem_chat(muted_chat)
+ return kicked_chats
+
+
+def get_invalid_gban(update: Update, context: CallbackContext, remove: bool = False):
+ bot = context.bot
+ banned = gban_sql.get_gban_list()
+ ungbanned_users = 0
+ ungban_list = []
+
+ for user in banned:
+ user_id = user["user_id"]
+ sleep(0.1)
+ try:
+ bot.get_chat(user_id)
+ except BadRequest:
+ ungbanned_users += 1
+ ungban_list.append(user_id)
+ except:
+ pass
+
+ if not remove:
+ return ungbanned_users
+ else:
+ for user_id in ungban_list:
+ sleep(0.1)
+ gban_sql.ungban_user(user_id)
+ return ungbanned_users
+
+
+@dev_plus
+def dbcleanup(update: Update, context: CallbackContext):
+ msg = update.effective_message
+
+ msg.reply_text("Getting invalid chat count ...")
+ invalid_chat_count = get_invalid_chats(update, context)
+
+ msg.reply_text("Getting invalid gbanned count ...")
+ invalid_gban_count = get_invalid_gban(update, context)
+
+ reply = f"Total invalid chats - {invalid_chat_count}\n"
+ reply += f"Total invalid gbanned users - {invalid_gban_count}"
+
+ buttons = [[InlineKeyboardButton("Cleanup DB", callback_data="db_cleanup")]]
+
+ update.effective_message.reply_text(
+ reply, reply_markup=InlineKeyboardMarkup(buttons)
+ )
+
+
+def callback_button(update: Update, context: CallbackContext):
+ bot = context.bot
+ query = update.callback_query
+ message = query.message
+ chat_id = update.effective_chat.id
+ query_type = query.data
+
+ admin_list = [OWNER_ID] + DEV_USERS
+
+ bot.answer_callback_query(query.id)
+
+ if query_type == "db_leave_chat":
+ if query.from_user.id in admin_list:
+ bot.editMessageText("Leaving chats ...", chat_id, message.message_id)
+ chat_count = get_muted_chats(update, context, True)
+ bot.sendMessage(chat_id, f"Left {chat_count} chats.")
+ else:
+ query.answer("You are not allowed to use this.")
+ elif query_type == "db_cleanup":
+ if query.from_user.id in admin_list:
+ bot.editMessageText("Cleaning up DB ...", chat_id, message.message_id)
+ invalid_chat_count = get_invalid_chats(update, context, True)
+ invalid_gban_count = get_invalid_gban(update, context, True)
+ reply = "Cleaned up {} chats and {} gbanned users from db.".format(
+ invalid_chat_count, invalid_gban_count
+ )
+ bot.sendMessage(chat_id, reply)
+ else:
+ query.answer("You are not allowed to use this.")
+
+
+DB_CLEANUP_HANDLER = CommandHandler("dbcleanup", dbcleanup, run_async=True)
+BUTTON_HANDLER = CallbackQueryHandler(callback_button, pattern="db_.*", run_async=True)
+
+dispatcher.add_handler(DB_CLEANUP_HANDLER)
+dispatcher.add_handler(BUTTON_HANDLER)
+
+__mod_name__ ="Dᴀᴛᴀʙᴀsᴇ"
+__handlers__ = [DB_CLEANUP_HANDLER, BUTTON_HANDLER]
diff --git a/QueenNoxi/modules/debug.py b/QueenNoxi/modules/debug.py
new file mode 100644
index 0000000000000000000000000000000000000000..1332a98f9ff2905c97be726d48823598bbc7512d
--- /dev/null
+++ b/QueenNoxi/modules/debug.py
@@ -0,0 +1,72 @@
+import datetime
+import os
+
+from telegram import Update
+from telegram.ext import CallbackContext, CommandHandler
+from telethon import events
+
+from QueenNoxi import dispatcher, telethn
+from QueenNoxi.modules.helper_funcs.chat_status import dev_plus
+
+DEBUG_MODE = False
+
+
+@dev_plus
+def debug(update: Update, context: CallbackContext):
+ global DEBUG_MODE
+ args = update.effective_message.text.split(None, 1)
+ message = update.effective_message
+ print(DEBUG_MODE)
+ if len(args) > 1:
+ if args[1] in ("yes", "on"):
+ DEBUG_MODE = True
+ message.reply_text("Debug mode is now on.")
+ elif args[1] in ("no", "off"):
+ DEBUG_MODE = False
+ message.reply_text("Debug mode is now off.")
+ else:
+ if DEBUG_MODE:
+ message.reply_text("Debug mode is currently on.")
+ else:
+ message.reply_text("Debug mode is currently off.")
+
+
+@telethn.on(events.NewMessage(pattern="[/!].*"))
+async def i_do_nothing_yes(event):
+ global DEBUG_MODE
+ if DEBUG_MODE:
+ print(f"-{event.from_id} ({event.chat_id}) : {event.text}")
+ if os.path.exists("updates.txt"):
+ with open("updates.txt", "r") as f:
+ text = f.read()
+ with open("updates.txt", "w+") as f:
+ f.write(text + f"\n-{event.from_id} ({event.chat_id}) : {event.text}")
+ else:
+ with open("updates.txt", "w+") as f:
+ f.write(
+ f"- {event.from_id} ({event.chat_id}) : {event.text} | {datetime.datetime.now()}"
+ )
+
+
+support_chat = os.getenv("SUPPORT_CHAT")
+
+
+@dev_plus
+def logs(update: Update, context: CallbackContext):
+ user = update.effective_user
+ with open("log.txt", "rb") as f:
+ context.bot.send_document(document=f, filename=f.name, chat_id=user.id)
+
+
+LOG_HANDLER = CommandHandler("logs", logs, run_async=True)
+dispatcher.add_handler(LOG_HANDLER)
+
+DEBUG_HANDLER = CommandHandler("debug", debug, run_async=True)
+dispatcher.add_handler(DEBUG_HANDLER)
+
+__mod_name__ = "Dᴇʙᴜɢ"
+__help__ = """
+/logs ᴛᴏ ɢᴇᴛ ʜᴇʀᴜᴋᴏ ʟᴏɢs
+/debug ᴛᴏ ᴏɴ / ᴏғғ ᴅᴇʙᴜɢ """
+__command_list__ = ["debug"]
+__handlers__ = [DEBUG_HANDLER]
diff --git a/QueenNoxi/modules/dev.py b/QueenNoxi/modules/dev.py
new file mode 100644
index 0000000000000000000000000000000000000000..ffbce91106a2344986b49d307091474631601e66
--- /dev/null
+++ b/QueenNoxi/modules/dev.py
@@ -0,0 +1,71 @@
+import os
+import subprocess
+import sys
+import asyncio
+from pyrogram import filters, Client, enums
+from pyrogram.types import Message
+
+import QueenNoxi
+from QueenNoxi import pbot, OWNER_ID
+from QueenNoxi.modules.helper_funcs.chat_status import dev_plus
+
+@pbot.on_message(filters.command("lockdown") & filters.user(OWNER_ID))
+@dev_plus
+async def allow_groups(client: Client, message: Message):
+ args = message.command[1:]
+ if not args:
+ await message.reply_text(f"Current state: {QueenNoxi.ALLOW_CHATS}")
+ return
+
+ if args[0].lower() in ("off", "no"):
+ QueenNoxi.ALLOW_CHATS = True
+ elif args[0].lower() in ("yes", "on"):
+ QueenNoxi.ALLOW_CHATS = False
+ else:
+ await message.reply_text("Format: /lockdown Yes/No or Off/On")
+ return
+ await message.reply_text("Done! Lockdown value toggled.")
+
+@pbot.on_message(filters.command("leave") & filters.user(OWNER_ID))
+@dev_plus
+async def leave(client: Client, message: Message):
+ args = message.command[1:]
+ if args:
+ chat_id = args[0]
+ try:
+ await client.leave_chat(int(chat_id))
+ await message.reply_text("Successfully left the chat.")
+ except Exception as e:
+ await message.reply_text(f"Could not leave chat: {e}")
+ else:
+ await message.reply_text("Send a valid chat ID.")
+
+@pbot.on_message(filters.command("reboot") & filters.user(OWNER_ID))
+@dev_plus
+async def restart(client: Client, message: Message):
+ await message.reply_text("Starting a new instance and shutting down this one...")
+ # These might need adjustment depending on how the bot is actually run
+ # For now, keeping the original logic of calling scripts
+ if os.name == 'nt': # Windows
+ os.system("restart.bat")
+ else: # Linux/HuggingFace (scripts might not exist there, but keeping for compatibility)
+ os.execv(sys.executable, [sys.executable] + sys.argv)
+
+@pbot.on_message(filters.command("gitpull") & filters.user(OWNER_ID))
+@dev_plus
+async def gitpull(client: Client, message: Message):
+ sent_msg = await message.reply_text("Pulling changes from git...")
+ subprocess.Popen("git pull", stdout=subprocess.PIPE, shell=True).wait()
+
+ await sent_msg.edit_text("Changes pulled. Restarting in 5 seconds...")
+ for i in reversed(range(5)):
+ await asyncio.sleep(1)
+ await sent_msg.edit_text(f"Restarting in {i+1}...")
+
+ await sent_msg.edit_text("Restarting now.")
+ if os.name == 'nt':
+ os.system("restart.bat")
+ else:
+ os.execv(sys.executable, [sys.executable] + sys.argv)
+
+__mod_name__ = "Dᴇᴠ"
diff --git a/QueenNoxi/modules/dicegame.py b/QueenNoxi/modules/dicegame.py
new file mode 100644
index 0000000000000000000000000000000000000000..d74870a0bbca76f9e15a9e2637006603d9755187
--- /dev/null
+++ b/QueenNoxi/modules/dicegame.py
@@ -0,0 +1,51 @@
+from pyrogram import Client, enums, filters
+#from config import *
+import asyncio
+from QueenNoxi import pbot as queennoxi
+
+from pyrogram.handlers import MessageHandler
+
+
+@queennoxi.on_message(filters.command("dice"))
+async def dice(bot, message):
+ x=await bot.send_dice(message.chat.id)
+ m=x.dice.value
+ await message.reply_text(f"Hey {message.from_user.mention} your Score is : {m}",quote=True)
+
+@queennoxi.on_message(filters.command("dart"))
+async def dart(bot, message):
+ x=await bot.send_dice(message.chat.id, "🎯")
+ m=x.dice.value
+ await message.reply_text(f"Hey {message.from_user.mention} your Score is : {m}",quote=True)
+
+@queennoxi.on_message(filters.command("basket"))
+async def basket(bot, message):
+ x=await bot.send_dice(message.chat.id, "🏀")
+ m=x.dice.value
+ await message.reply_text(f"Hey {message.from_user.mention} your Score is : {m}",quote=True)
+@queennoxi.on_message(filters.command("jackpot"))
+async def basket(bot, message):
+ x=await bot.send_dice(message.chat.id, "🎰")
+ m=x.dice.value
+ await message.reply_text(f"Hey {message.from_user.mention} your Score is : {m}",quote=True)
+@queennoxi.on_message(filters.command("ball"))
+async def basket(bot, message):
+ x=await bot.send_dice(message.chat.id, "🎳")
+ m=x.dice.value
+ await message.reply_text(f"Hey {message.from_user.mention} your Score is : {m}",quote=True)
+@queennoxi.on_message(filters.command("football"))
+async def basket(bot, message):
+ x=await bot.send_dice(message.chat.id, "⚽")
+ m=x.dice.value
+ await message.reply_text(f"Hey {message.from_user.mention} your Score is : {m}",quote=True)
+__help__ = """
+ Play Game With Emojis:
+/dice - Dice 🎲
+/dart - Dart 🎯
+/basket - Basket Ball 🏀
+/ball - Bowling Ball 🎳
+/football - Football ⚽
+/jackpot - Spin slot machine 🎰
+ """
+
+__mod_name__ = "Dɪᴄᴇ"
diff --git a/QueenNoxi/modules/disable.py b/QueenNoxi/modules/disable.py
new file mode 100644
index 0000000000000000000000000000000000000000..69a21f8a0b91247ff7d4dd4cd9e5a73461a3a657
--- /dev/null
+++ b/QueenNoxi/modules/disable.py
@@ -0,0 +1,129 @@
+import importlib
+from typing import Union, List
+from functools import wraps
+
+from pyrogram import filters, Client, enums
+from pyrogram.types import Message
+
+from QueenNoxi import pbot, LOGGER
+from QueenNoxi.modules.helper_funcs.chat_status import connection_status, user_admin, is_user_admin
+from QueenNoxi.modules.sql import disable_sql as sql
+from QueenNoxi.modules.helper_funcs.handlers import SpamChecker, is_user_blacklisted
+
+DISABLE_CMDS = []
+DISABLE_OTHER = []
+ADMIN_CMDS = []
+
+def DisableAbleCommandHandler(command, callback=None, admin_ok=False, **kwargs):
+ def decorator(callback):
+ if isinstance(command, str):
+ cmds = [command]
+ else:
+ cmds = command
+
+ for cmd in cmds:
+ DISABLE_CMDS.append(cmd)
+ if admin_ok:
+ ADMIN_CMDS.append(cmd)
+
+ @wraps(callback)
+ async def wrapped(client: Client, message: Message, *args, **kwargs):
+ if message.from_user:
+ user_id = message.from_user.id
+ if is_user_blacklisted(user_id):
+ return
+ if SpamChecker.check_user(user_id):
+ return
+
+ chat_id = message.chat.id
+ cmd_name = message.command[0].lower() if message.command else ""
+
+ if sql.is_command_disabled(chat_id, cmd_name):
+ if admin_ok and await is_user_admin(message.chat, message.from_user.id):
+ return await callback(client, message, *args, **kwargs)
+ return
+
+ return await callback(client, message, *args, **kwargs)
+
+ pbot.on_message(filters.command(cmds) & ~filters.forwarded)(wrapped)
+ return wrapped
+
+ if callback is not None:
+ return decorator(callback)
+ return decorator
+
+
+@pbot.on_message(filters.command("disable") & filters.group)
+@connection_status
+@user_admin
+async def disable(client: Client, message: Message):
+ if len(message.command) < 2:
+ await message.reply_text("What should I disable?")
+ return
+
+ disable_cmd = message.command[1].lower()
+ if disable_cmd in set(DISABLE_CMDS + DISABLE_OTHER):
+ sql.disable_command(message.chat.id, disable_cmd)
+ await message.reply_text(f"Disabled the use of `{disable_cmd}`")
+ else:
+ await message.reply_text("That command can't be disabled")
+
+
+@pbot.on_message(filters.command("enable") & filters.group)
+@connection_status
+@user_admin
+async def enable(client: Client, message: Message):
+ if len(message.command) < 2:
+ await message.reply_text("What should I enable?")
+ return
+
+ enable_cmd = message.command[1].lower()
+ if sql.enable_command(message.chat.id, enable_cmd):
+ await message.reply_text(f"Enabled the use of `{enable_cmd}`")
+ else:
+ await message.reply_text("Is that even disabled?")
+
+
+@pbot.on_message(filters.command(["cmds", "disabled"]) & filters.group)
+@connection_status
+async def commands(client: Client, message: Message):
+ chat_id = message.chat.id
+ disabled = sql.get_all_disabled(chat_id)
+ if not disabled:
+ await message.reply_text("No commands are disabled!")
+ return
+
+ result = "The following commands are currently restricted:\n"
+ for cmd in disabled:
+ result += f" - `{cmd}`\n"
+ await message.reply_text(result)
+
+
+@pbot.on_message(filters.command("listcmds") & filters.group)
+@connection_status
+@user_admin
+async def list_cmds(client: Client, message: Message):
+ if DISABLE_CMDS + DISABLE_OTHER:
+ result = "The following commands are toggleable:\n"
+ for cmd in set(DISABLE_CMDS + DISABLE_OTHER):
+ result += f" - `{cmd}`\n"
+ await message.reply_text(result)
+ else:
+ await message.reply_text("No commands can be disabled.")
+
+
+def __stats__():
+ return f"• {sql.num_disabled()} ᴅɪsᴀʙʟᴇᴅ ɪᴛᴇᴍs, ᴀᴄʀᴏss {sql.num_chats()} ᴄʜᴀᴛs."
+
+def __migrate__(old_chat_id, new_chat_id):
+ sql.migrate_chat(old_chat_id, new_chat_id)
+
+__mod_name__ = "Disable"
+__help__ = """
+» /cmds: Check the current status of disabled commands
+
+*Admins only:*
+» /enable : Enable that command
+» /disable : Disable that command
+» /listcmds: List all possible toggleable commands
+"""
diff --git a/QueenNoxi/modules/disasters.py b/QueenNoxi/modules/disasters.py
new file mode 100644
index 0000000000000000000000000000000000000000..b69e2b97053bb9a19ef540e9798b085ec7a28f39
--- /dev/null
+++ b/QueenNoxi/modules/disasters.py
@@ -0,0 +1,213 @@
+import html
+from pyrogram import filters, Client, enums
+from pyrogram.types import Message
+
+from QueenNoxi import (
+ DEMONS,
+ DEV_USERS,
+ DRAGONS,
+ OWNER_ID,
+ TIGERS,
+ WOLVES,
+ pbot,
+)
+from QueenNoxi.modules.helper_funcs.chat_status import (
+ dev_plus,
+ sudo_plus,
+ whitelist_plus,
+)
+from QueenNoxi.modules.helper_funcs.extraction import extract_user
+from QueenNoxi.modules.log_channel import gloggable
+
+async def check_user_id(user_id: int, client: Client) -> str:
+ if not user_id:
+ return "That...is a chat! baka ka omae?"
+ if user_id == (await client.get_me()).id:
+ return "This does not work that way."
+ return None
+
+@pbot.on_message(filters.command("addsudo") & filters.user(OWNER_ID))
+@dev_plus
+@gloggable
+async def addsudo(client: Client, message: Message) -> str:
+ user_id = await extract_user(message, message.command[1:])
+ reply = await check_user_id(user_id, client)
+ if reply:
+ await message.reply_text(reply)
+ return ""
+
+ if user_id in DRAGONS:
+ await message.reply_text("This member is already a Dragon Disaster")
+ return ""
+
+ if user_id in DEMONS: DEMONS.remove(user_id)
+ if user_id in WOLVES: WOLVES.remove(user_id)
+ if user_id in TIGERS: TIGERS.remove(user_id)
+
+ DRAGONS.append(user_id)
+ user_member = await client.get_users(user_id)
+ await message.reply_text(f"Successfully set Disaster level of {user_member.first_name} to Dragon!")
+
+ return f"#SUDO\n**Admin:** {message.from_user.mention}\n**User:** {user_member.mention}"
+
+@pbot.on_message(filters.command(["addsupport", "adddemon"]) & filters.user(DRAGONS))
+@sudo_plus
+@gloggable
+async def addsupport(client: Client, message: Message) -> str:
+ user_id = await extract_user(message, message.command[1:])
+ reply = await check_user_id(user_id, client)
+ if reply:
+ await message.reply_text(reply)
+ return ""
+
+ if user_id in DEMONS:
+ await message.reply_text("This user is already a Demon Disaster.")
+ return ""
+
+ if user_id in DRAGONS: DRAGONS.remove(user_id)
+ if user_id in WOLVES: WOLVES.remove(user_id)
+ if user_id in TIGERS: TIGERS.remove(user_id)
+
+ DEMONS.append(user_id)
+ user_member = await client.get_users(user_id)
+ await message.reply_text(f"{user_member.first_name} was added as a Demon Disaster!")
+
+ return f"#SUPPORT\n**Admin:** {message.from_user.mention}\n**User:** {user_member.mention}"
+
+@pbot.on_message(filters.command(["addwhitelist", "addwolf"]) & filters.user(DRAGONS))
+@sudo_plus
+@gloggable
+async def addwhitelist(client: Client, message: Message) -> str:
+ user_id = await extract_user(message, message.command[1:])
+ reply = await check_user_id(user_id, client)
+ if reply:
+ await message.reply_text(reply)
+ return ""
+
+ if user_id in WOLVES:
+ await message.reply_text("This user is already a Wolf Disaster.")
+ return ""
+
+ for l in [DRAGONS, DEMONS, TIGERS]:
+ if user_id in l: l.remove(user_id)
+
+ WOLVES.append(user_id)
+ user_member = await client.get_users(user_id)
+ await message.reply_text(f"Successfully promoted {user_member.first_name} to a Wolf Disaster!")
+
+ return f"#WHITELIST\n**Admin:** {message.from_user.mention}\n**User:** {user_member.mention}"
+
+@pbot.on_message(filters.command("addtiger") & filters.user(DRAGONS))
+@sudo_plus
+@gloggable
+async def addtiger(client: Client, message: Message) -> str:
+ user_id = await extract_user(message, message.command[1:])
+ reply = await check_user_id(user_id, client)
+ if reply:
+ await message.reply_text(reply)
+ return ""
+
+ if user_id in TIGERS:
+ await message.reply_text("This user is already a Tiger.")
+ return ""
+
+ for l in [DRAGONS, DEMONS, WOLVES]:
+ if user_id in l: l.remove(user_id)
+
+ TIGERS.append(user_id)
+ user_member = await client.get_users(user_id)
+ await message.reply_text(f"Successfully promoted {user_member.first_name} to a Tiger Disaster!")
+
+ return f"#TIGER\n**Admin:** {message.from_user.mention}\n**User:** {user_member.mention}"
+
+@pbot.on_message(filters.command(["removesudo", "rmsudo"]) & filters.user(OWNER_ID))
+@dev_plus
+@gloggable
+async def removesudo(client: Client, message: Message) -> str:
+ user_id = await extract_user(message, message.command[1:])
+ if user_id in DRAGONS:
+ DRAGONS.remove(user_id)
+ user_member = await client.get_users(user_id)
+ await message.reply_text("Demoted user to Civilian.")
+ return f"#UNSUDO\n**Admin:** {message.from_user.mention}\n**User:** {user_member.mention}"
+ await message.reply_text("This user is not a Dragon Disaster!")
+ return ""
+
+@pbot.on_message(filters.command(["removesupport", "removedemon"]) & filters.user(DRAGONS))
+@sudo_plus
+@gloggable
+async def removesupport(client: Client, message: Message) -> str:
+ user_id = await extract_user(message, message.command[1:])
+ if user_id in DEMONS:
+ DEMONS.remove(user_id)
+ user_member = await client.get_users(user_id)
+ await message.reply_text("Demoted user to Civilian.")
+ return f"#UNSUPPORT\n**Admin:** {message.from_user.mention}\n**User:** {user_member.mention}"
+ await message.reply_text("This user is not a Demon level Disaster!")
+ return ""
+
+@pbot.on_message(filters.command(["sudolist", "dragons"]))
+@whitelist_plus
+async def sudolist(client: Client, message: Message):
+ true_sudo = list(set(DRAGONS) - set(DEV_USERS))
+ reply = "**Known Dragon Disasters 🐉:**\n"
+ for each_user in true_sudo:
+ try:
+ user = await client.get_users(each_user)
+ reply += f"• {user.mention}\n"
+ except: pass
+ await message.reply_text(reply)
+
+@pbot.on_message(filters.command(["supportlist", "demons"]))
+@whitelist_plus
+async def supportlist(client: Client, message: Message):
+ reply = "**Known Demon Disasters 👹:**\n"
+ for each_user in DEMONS:
+ try:
+ user = await client.get_users(each_user)
+ reply += f"• {user.mention}\n"
+ except: pass
+ await message.reply_text(reply)
+
+@pbot.on_message(filters.command(["wolves", "whitelistlist"]))
+@whitelist_plus
+async def wolveslist(client: Client, message: Message):
+ reply = "**Known Wolf Disasters 🐺:**\n"
+ for each_user in WOLVES:
+ try:
+ user = await client.get_users(each_user)
+ reply += f"• {user.mention}\n"
+ except: pass
+ await message.reply_text(reply)
+
+@pbot.on_message(filters.command("tigers"))
+@whitelist_plus
+async def tigerslist(client: Client, message: Message):
+ reply = "**Known Tiger Disasters 🐯:**\n"
+ for each_user in TIGERS:
+ try:
+ user = await client.get_users(each_user)
+ reply += f"• {user.mention}\n"
+ except: pass
+ await message.reply_text(reply)
+
+@pbot.on_message(filters.command("devlist"))
+@whitelist_plus
+async def devlist(client: Client, message: Message):
+ true_dev = list(set(DEV_USERS) - {OWNER_ID})
+ reply = "✨ **Devs User List:**\n"
+ for each_user in true_dev:
+ try:
+ user = await client.get_users(each_user)
+ reply += f"• {user.mention}\n"
+ except: pass
+ await message.reply_text(reply)
+
+__mod_name__ = "Dᴇᴠꜱ"
+__help__ = """
+★ /sudolist: lists all dragons
+★ /supportlist: lists all demons
+★ /wolves: lists all wolves
+★ /tigers: lists all tigers
+★ /devlist: lists hero association members
+"""
diff --git a/QueenNoxi/modules/echo.py b/QueenNoxi/modules/echo.py
new file mode 100644
index 0000000000000000000000000000000000000000..6870f277022820c2343f6dc954353b0f02f6fbf4
--- /dev/null
+++ b/QueenNoxi/modules/echo.py
@@ -0,0 +1,58 @@
+from pyrogram import filters
+from pyrogram.types import Message
+from QueenNoxi import pbot
+from QueenNoxi.modules.helper_funcs.chat_status import sudo_plus
+
+@pbot.on_message(filters.command("go"))
+async def go_cmd(client, message: Message):
+ if len(message.command) < 2:
+ await message.reply_text("Usage: /go ")
+ return
+
+ text = message.text.split(None, 1)[1]
+ try:
+ await message.delete()
+ except Exception:
+ pass
+ if message.reply_to_message:
+ await message.reply_to_message.reply_text(text)
+ else:
+ await client.send_message(message.chat.id, text)
+
+@pbot.on_message(filters.command("more"))
+@sudo_plus
+async def more_cmd(client, message: Message):
+ if len(message.command) < 3:
+ await message.reply_text("Usage: /more ")
+ return
+
+ args = message.text.split(None, 2)
+ try:
+ count = int(args[1])
+ except ValueError:
+ await message.reply_text("Count must be an integer.")
+ return
+
+ # Cap count to prevent spam/floodwaits
+ count = min(count, 50)
+ text = args[2]
+
+ try:
+ await message.delete()
+ except Exception:
+ pass
+
+ for _ in range(count):
+ if message.reply_to_message:
+ await message.reply_to_message.reply_text(text)
+ else:
+ await client.send_message(message.chat.id, text)
+
+__mod_name__ = "Echo"
+__help__ = """
+**Commands:**
+• `/go `: Delete your message and send text as the bot. (Available to all)
+
+**Sudo/Owner restrict:**
+• `/more `: Repeat text count times as the bot.
+"""
diff --git a/QueenNoxi/modules/encode_decode.py b/QueenNoxi/modules/encode_decode.py
new file mode 100644
index 0000000000000000000000000000000000000000..95f77537896e32aaf3c85526aa7f12698de6f1c2
--- /dev/null
+++ b/QueenNoxi/modules/encode_decode.py
@@ -0,0 +1,90 @@
+import requests
+from .. import pbot as QueenNoxi,BOT_NAME,BOT_USERNAME
+import time
+from pyrogram.enums import ChatAction, ParseMode
+from pyrogram import filters
+@QueenNoxi.on_message(filters.command(["password"]))
+async def passwordgen(bot, message):
+
+ try:
+
+ await bot.send_chat_action(message.chat.id, ChatAction.TYPING)
+ if len(message.command) < 2:
+ return await message.reply_text(
+ "Example:**\n\n`/password `")
+ else:
+ a = message.text.split(' ', 1)[1]
+ response = requests.get(f'https://queennoxi-api.vercel.app/password?num={a}')
+ x=response.json()["results"]
+
+ await message.reply_text(f"Here is your Password:- ` {x}`", parse_mode=ParseMode.MARKDOWN)
+ except Exception as e:
+ await message.reply_text(f"**ᴇʀʀᴏʀ: {e} ")
+@QueenNoxi.on_message(filters.command(["morseencode"]))
+async def morse_en(bot, message):
+
+ try:
+
+ await bot.send_chat_action(message.chat.id, ChatAction.TYPING)
+ if len(message.command) < 2:
+ return await message.reply_text(
+ "Example:**\n\n`/morseencode `")
+ else:
+ a = message.text.split(' ', 1)[1]
+ response = requests.get(f'https://queennoxi-api.vercel.app/morse/encode?query={a}')
+ x=response.json()["results"]
+
+ await message.reply_text(f"`{x}`", parse_mode=ParseMode.MARKDOWN)
+ except Exception as e:
+ await message.reply_text(f"**ᴇʀʀᴏʀ: {e} ")
+@QueenNoxi.on_message(filters.command("morsedecode"))
+async def morse_de(bot, message):
+
+ try:
+
+ await bot.send_chat_action(message.chat.id, ChatAction.TYPING)
+ if len(message.command) < 2:
+ return await message.reply_text(
+ "Example:**\n\n`/morsedecode `")
+ else:
+ a = message.text.split(' ', 1)[1]
+ response = requests.get(f'https://queennoxi-api.vercel.app/morse/decode?query={a}')
+ x=response.json()["results"]
+
+ await message.reply_text(f"`{x}`", parse_mode=ParseMode.MARKDOWN)
+ except Exception as e:
+ await message.reply_text(f"**ᴇʀʀᴏʀ: {e} ")
+@QueenNoxi.on_message(filters.command(["encode"]))
+async def base_en(bot, message):
+
+ try:
+
+ await bot.send_chat_action(message.chat.id, ChatAction.TYPING)
+ if len(message.command) < 2:
+ return await message.reply_text(
+ "Example:**\n\n`/encode `")
+ else:
+ a = message.text.split(' ', 1)[1]
+ response = requests.get(f'https://queennoxi-api.vercel.app/base/encode?query={a}')
+ x=response.json()["results"]
+
+ await message.reply_text(f"` {x}`", parse_mode=ParseMode.MARKDOWN)
+ except Exception as e:
+ await message.reply_text(f"**ᴇʀʀᴏʀ: {e} ")
+@QueenNoxi.on_message(filters.command(["decode"]))
+async def base_de(bot, message):
+
+ try:
+
+ await bot.send_chat_action(message.chat.id, ChatAction.TYPING)
+ if len(message.command) < 2:
+ return await message.reply_text(
+ "Example:**\n\n`/decode `")
+ else:
+ a = message.text.split(' ', 1)[1]
+ response = requests.get(f'https://queennoxi-api.vercel.app/base/decode?query={a}')
+ x=response.json()["results"]
+
+ await message.reply_text(f" `{x}`", parse_mode=ParseMode.MARKDOWN)
+ except Exception as e:
+ await message.reply_text(f"**ᴇʀʀᴏʀ: {e} ")
diff --git a/QueenNoxi/modules/encrypt.py b/QueenNoxi/modules/encrypt.py
new file mode 100644
index 0000000000000000000000000000000000000000..cce560d20e336e3ee2affb35de4c28f544828552
--- /dev/null
+++ b/QueenNoxi/modules/encrypt.py
@@ -0,0 +1,47 @@
+import secureme
+from pyrogram import filters
+from QueenNoxi import pbot as queennoxi
+
+
+@queennoxi.on_message(filters.command("encrypt"))
+async def encyrpt(bot, message):
+ if len(message.command) < 2:
+ return await message.reply_text("**Example:**\n\n`/encyrpt India`")
+ m = message.text.split(' ',1)[1]
+ try:
+ Secure = secureme.encrypt(m)
+
+ await message.reply_text(f"`{Secure}`")
+
+
+ except Exception as e:
+ await message.reply_text(f"Error {e}")
+
+@queennoxi.on_message(filters.command("decrypt"))
+async def decrypt(bot, message):
+ if len(message.command) < 2:
+ return await message.reply_text("**Example:**\n\n`/decrypt Nsinf`")
+ m = message.text.split(' ',1)[1]
+ try:
+ Decrypt = secureme.decrypt(m)
+
+ await message.reply_text(f"`{Decrypt}`")
+
+
+ except Exception as e:
+ await message.reply_text(f"{e}")
+
+
+__mod_name__ = "ᴘᴀssᴡᴏʀᴅ"
+
+__help__ = """
+*ᴄᴏɴᴠᴇʀᴛs*
+ ❍ /encrypt*:* ᴇɴᴄʀʏᴘᴛs ᴛʜᴇ ɢɪᴠᴇɴ ᴛᴇxᴛ
+ ❍ /decrypt*:* ᴅᴇᴄʀʏᴘᴛs ᴘʀᴇᴠɪᴏᴜsʟʏ ᴇᴄʀʏᴘᴛᴇᴅ ᴛᴇxᴛ
+ ❍ /encode*:* ᴇɴᴄᴏᴅᴇ ᴛʜᴇ ɢɪᴠᴇɴ ᴛᴇxᴛ
+ ❍ /decode*:* ᴅᴇᴄᴏᴅᴇ ᴘʀᴇᴠɪᴏᴜsʟʏ ᴇᴄʀʏᴘᴛᴇᴅ ᴛᴇxᴛ
+ ❍ /morseencode*:* Mᴏʀsᴇ Eɴᴄᴏᴅᴇ ᴛʜᴇ ɢɪᴠᴇɴ ᴛᴇxᴛ
+ ❍ /morsedecode*:* ᴅᴇᴄʀʏᴘᴛs ᴘʀᴇᴠɪᴏᴜsʟʏ ᴇᴄʀʏᴘᴛᴇᴅ ᴛᴇxᴛ
+ ❍ /password *:* ɢɪᴠᴇ ʟᴇɴɢᴛʜ ᴏғ ᴘᴀssᴡᴏʀᴅ ᴛᴏ ɢᴇɴᴇʀᴀᴛᴇ
+ ❍ /uselessfact *:* Gᴇɴᴇʀᴀᴛᴇ Rᴀᴍᴅᴏᴍ Usᴇʟᴇss Fᴀᴄᴛ
+"""
diff --git a/QueenNoxi/modules/english.py b/QueenNoxi/modules/english.py
new file mode 100644
index 0000000000000000000000000000000000000000..5ce870cef5eb80a33f7ac6b23dd91586ce75093e
--- /dev/null
+++ b/QueenNoxi/modules/english.py
@@ -0,0 +1,83 @@
+import json
+import requests
+from PyDictionary import PyDictionary
+from pyrogram import filters
+from QueenNoxi import pbot as app
+
+API_KEY = "6ae0c3a0-afdc-4532-a810-82ded0054236"
+URL = "http://services.gingersoftware.com/Ginger/correct/json/GingerTheText"
+
+@app.on_message(filters.command("spell"))
+async def _(_, message):
+ if not message.reply_to_message:
+ return
+ msg = message.reply_to_message.text
+ if not msg:
+ return
+ params = dict(lang="US", clientVersion="2.0", apiKey=API_KEY, text=msg)
+ res = requests.get(URL, params=params)
+ changes = json.loads(res.text).get("LightGingerTheTextResult")
+ curr_string = ""
+ prev_end = 0
+ for change in changes:
+ start = change.get("From")
+ end = change.get("To") + 1
+ suggestions = change.get("Suggestions")
+ if suggestions:
+ sugg_str = suggestions[0].get("Text")
+ curr_string += msg[prev_end:start] + sugg_str
+ prev_end = end
+ curr_string += msg[prev_end:]
+ await message.reply(curr_string)
+
+
+dictionary = PyDictionary()
+
+@app.on_message(filters.command("define"))
+async def _(_, message):
+ if len(message.command) < 2:
+ return
+ word = message.text.split(None, 1)[1]
+ let = dictionary.meaning(word)
+ set_word = str(let)
+ jet = set_word.replace("{", "")
+ net = jet.replace("}", "")
+ got = net.replace("'", "")
+ await message.reply(got)
+
+
+@app.on_message(filters.command("synonyms"))
+async def _(_, message):
+ if len(message.command) < 2:
+ return
+ word = message.text.split(None, 1)[1]
+ let = dictionary.synonym(word)
+ set_word = str(let)
+ jet = set_word.replace("{", "")
+ net = jet.replace("}", "")
+ got = net.replace("'", "")
+ await message.reply(got)
+
+
+@app.on_message(filters.command("antonyms"))
+async def _(_, message):
+ if len(message.command) < 2:
+ return
+ word = message.text.split(None, 1)[1]
+ let = dictionary.antonym(word)
+ set_word = str(let)
+ jet = set_word.replace("{", "")
+ net = jet.replace("}", "")
+ got = net.replace("'", "")
+ await message.reply(got)
+
+
+__help__ = """
+ ❍ /define <ᴛᴇxᴛ>*:* ᴛʏᴘᴇ ᴛʜᴇ ᴡᴏʀᴅ ᴏʀ ᴇxᴘʀᴇssɪᴏɴ ʏᴏᴜ ᴡᴀɴᴛ ᴛᴏ sᴇᴀʀᴄʜ
+ғᴏʀ ᴇxᴀᴍᴘʟᴇ /ᴅᴇғɪɴᴇ ᴋɪʟʟ
+ ❍ /spell *:* ᴡʜɪʟᴇ ʀᴇᴘʟʏɪɴɢ ᴛᴏ ᴀ ᴍᴇssᴀɢᴇ, ᴡɪʟʟ ʀᴇᴘʟʏ ᴡɪᴛʜ ᴀ ɢʀᴀᴍᴍᴀʀ ᴄᴏʀʀᴇᴄᴛᴇᴅ ᴠᴇʀsɪᴏɴ
+ ❍ /synonyms <ᴡᴏʀᴅ>*:* ғɪɴᴅ ᴛʜᴇ sʏɴᴏɴʏᴍs ᴏғ ᴀ ᴡᴏʀᴅ
+ ❍ /antonyms <ᴡᴏʀᴅ>*:* ғɪɴᴅ ᴛʜᴇ ᴀɴᴛᴏɴʏᴍs ᴏғ ᴀ ᴡᴏʀᴅ
+"""
+
+__mod_name__ = "Eɴɢʟɪsʜ"
diff --git a/QueenNoxi/modules/error_handler.py b/QueenNoxi/modules/error_handler.py
new file mode 100644
index 0000000000000000000000000000000000000000..352ecb2008e1c45f1dbdd4d5a9975af3aebaf694
--- /dev/null
+++ b/QueenNoxi/modules/error_handler.py
@@ -0,0 +1,18 @@
+import html
+import traceback
+from pyrogram import filters, Client, enums
+from pyrogram.types import Message
+
+from QueenNoxi import pbot, DEV_USERS
+
+@pbot.on_message(filters.command("errors"))
+async def list_errors(client: Client, message: Message):
+ if message.from_user.id not in DEV_USERS:
+ return
+ await message.reply_text("Error logging is being refactored for Pyrogram. Check logs for details.")
+
+# Global error handling is typically done by wrapping the handler execution loop
+# or using custom middleware in newer Pyrogram versions, but for this migration
+# we'll rely on the standard logging for now.
+
+__mod_name__ = "Errors"
diff --git a/QueenNoxi/modules/eval.py b/QueenNoxi/modules/eval.py
new file mode 100644
index 0000000000000000000000000000000000000000..66f41ff8e906991cced0e2c0e5949aaf093284f5
--- /dev/null
+++ b/QueenNoxi/modules/eval.py
@@ -0,0 +1,112 @@
+import io
+import os
+import textwrap
+import traceback
+from contextlib import redirect_stdout
+
+from pyrogram import filters, Client, enums
+from pyrogram.types import Message
+
+from QueenNoxi import LOGGER, pbot, OWNER_ID
+from QueenNoxi.modules.helper_funcs.chat_status import dev_plus
+
+namespaces = {}
+
+
+def namespace_of(chat, message, client):
+ if chat not in namespaces:
+ namespaces[chat] = {
+ "__builtins__": globals()["__builtins__"],
+ "client": client,
+ "pbot": pbot,
+ "message": message,
+ "user": message.from_user,
+ "chat": message.chat,
+ }
+
+ return namespaces[chat]
+
+
+async def send(msg, message: Message):
+ if len(str(msg)) > 4000:
+ with io.BytesIO(str.encode(msg)) as out_file:
+ out_file.name = "output.txt"
+ await message.reply_document(document=out_file, caption="Output too long, sent as file.")
+ else:
+ await message.reply_text(f"```python\n{msg}```", parse_mode=enums.ParseMode.MARKDOWN)
+
+
+@pbot.on_message(filters.command(["e", "eval"]) & filters.user(OWNER_ID))
+@dev_plus
+async def evaluate(client: Client, message: Message):
+ await send(await do(eval, client, message), message)
+
+
+@pbot.on_message(filters.command(["x", "ex", "exec", "py"]) & filters.user(OWNER_ID))
+@dev_plus
+async def execute(client: Client, message: Message):
+ await send(await do(exec, client, message), message)
+
+
+def cleanup_code(code):
+ if code.startswith("```") and code.endswith("```"):
+ return "\n".join(code.split("\n")[1:-1])
+ return code.strip("` \n")
+
+
+async def do(func, client, message):
+ content = message.text.split(None, 1)[-1]
+ body = cleanup_code(content)
+ env = namespace_of(message.chat.id, message, client)
+
+ stdout = io.StringIO()
+
+ to_compile = f'async def func():\n{textwrap.indent(body, " ")}'
+
+ try:
+ exec(to_compile, env)
+ except Exception as e:
+ return f"{e.__class__.__name__}: {e}"
+
+ func = env["func"]
+
+ try:
+ with redirect_stdout(stdout):
+ func_return = await func()
+ except Exception:
+ value = stdout.getvalue()
+ return f"{value}{traceback.format_exc()}"
+ else:
+ value = stdout.getvalue()
+ result = None
+ if func_return is None:
+ if value:
+ result = f"{value}"
+ else:
+ try:
+ result = f"{repr(eval(body, env))}"
+ except:
+ pass
+ else:
+ result = f"{value}{func_return}"
+ if result:
+ return result
+ return "Clean execution (No output)"
+
+
+@pbot.on_message(filters.command("clearlocals") & filters.user(OWNER_ID))
+@dev_plus
+async def clear_locals(client: Client, message: Message):
+ global namespaces
+ if message.chat.id in namespaces:
+ del namespaces[message.chat.id]
+ await message.reply_text("Locals cleared for this chat.")
+
+
+__mod_name__ = "Eᴠᴀʟ"
+__help__ = """
+★ᴏᴡɴᴇʀ ᴄᴍᴅ ★
+ ❍ /eval: ᴇᴠᴀʟᴜᴀᴛᴇ ᴘʏᴛʜᴏɴ ᴄᴏᴅᴇ
+ ❍ /exec: ᴇxᴇᴄᴜᴛᴇ ᴘʏᴛʜᴏɴ ᴄᴏᴅᴇ
+ ❍ /clearlocals: ᴄʟᴇᴀʀ ʟᴏᴄᴀʟ variables
+"""
diff --git a/QueenNoxi/modules/extra_mod.py b/QueenNoxi/modules/extra_mod.py
new file mode 100644
index 0000000000000000000000000000000000000000..c5490949616e9e4cf1a2636f6c927fd079246c04
--- /dev/null
+++ b/QueenNoxi/modules/extra_mod.py
@@ -0,0 +1,27 @@
+import requests
+from .. import pbot as QueenNoxi,BOT_NAME,BOT_USERNAME
+import time
+from pyrogram.enums import ChatAction, ParseMode
+from pyrogram import filters
+# @QueenNoxi.on_message(filters.command("mahadev"))
+# async def Mahadev(bot, message):
+# try:
+
+# await bot.send_chat_action(message.chat.id, ChatAction.UPLOAD_PHOTO)
+# response = requests.get(f'https://queennoxi-api.vercel.app/mahadev')
+# x=response.json()["results"]
+
+# await message.reply_photo(photo=x,caption=f" \n🎉ᴘᴏᴡᴇʀᴇᴅ ʙʏ @{BOT_USERNAME} ", parse_mode=ParseMode.MARKDOWN)
+# except Exception as e:
+# await message.reply_text(f"**ᴇʀʀᴏʀ: {e} ")
+# @QueenNoxi.on_message(filters.command("uselessfact"))
+# async def uselessa_fact(bot, message):
+# try:
+
+# await bot.send_chat_action(message.chat.id, ChatAction.TYPING)
+# response = requests.get(f'https://queennoxi-api.vercel.app/uselessfact')
+# x=response.json()["results"]
+
+# await message.reply_text(x, parse_mode=ParseMode.MARKDOWN)
+# except Exception as e:
+# await message.reply_text(f"**ᴇʀʀᴏʀ: {e} ")
diff --git a/QueenNoxi/modules/feds.py b/QueenNoxi/modules/feds.py
new file mode 100644
index 0000000000000000000000000000000000000000..b1bf07944886f965b83e8b49dde80789753ae5b0
--- /dev/null
+++ b/QueenNoxi/modules/feds.py
@@ -0,0 +1,772 @@
+import ast
+import csv
+import json
+import os
+import re
+import time
+import uuid
+from io import BytesIO
+
+from pyrogram import filters, Client, enums
+from pyrogram.types import (
+ InlineKeyboardButton,
+ InlineKeyboardMarkup,
+ Message,
+ CallbackQuery,
+ MessageEntity,
+)
+from pyrogram.errors import RPCError, Forbidden, BadRequest
+
+import QueenNoxi.modules.sql.feds_sql as sql
+from QueenNoxi import (
+ DRAGONS,
+ EVENT_LOGS,
+ LOGGER,
+ OWNER_ID,
+ SUPPORT_CHAT,
+ TIGERS,
+ WOLVES,
+ pbot,
+)
+from QueenNoxi.modules.disable import DisableAbleCommandHandler
+from QueenNoxi.modules.helper_funcs.chat_status import user_admin, connection_status
+from QueenNoxi.modules.helper_funcs.extraction import (
+ extract_unt_fedban,
+ extract_user,
+ extract_user_fban,
+)
+from QueenNoxi.modules.helper_funcs.string_handling import markdown_parser
+
+FBAN_ERRORS = {
+ "USER_ADMIN_INVALID",
+ "CHAT_NOT_FOUND",
+ "NOT_ENOUGH_RIGHTS_TO_RESTRICT",
+ "USER_NOT_PARTICIPANT",
+ "PEER_ID_INVALID",
+ "GROUP_DEACTIVATED",
+ "CHAT_ADMIN_REQUIRED",
+ "CHANNEL_PRIVATE",
+ "NOT_IN_CHAT",
+}
+
+UNFBAN_ERRORS = {
+ "USER_ADMIN_INVALID",
+ "CHAT_NOT_FOUND",
+ "NOT_ENOUGH_RIGHTS_TO_RESTRICT",
+ "USER_NOT_PARTICIPANT",
+ "CHANNEL_PRIVATE",
+ "CHAT_ADMIN_REQUIRED",
+}
+
+@pbot.on_message(filters.command("newfed"))
+async def new_fed(client: Client, message: Message):
+ chat = message.chat
+ user = message.from_user
+ if chat.type != enums.ChatType.PRIVATE:
+ await message.reply_text(
+ "Federations can only be created by privately messaging me.",
+ )
+ return
+
+ args = message.command[1:]
+ if not args:
+ await message.reply_text("Please write the name of the federation!")
+ return
+
+ fed_name = " ".join(args)
+ fed_id = str(uuid.uuid4())
+
+ x = sql.new_fed(user.id, fed_name, fed_id)
+ if not x:
+ await message.reply_text(
+ f"Can't federate! Please contact @{SUPPORT_CHAT} if the problem persist.",
+ )
+ return
+
+ await message.reply_text(
+ "**You have succeeded in creating a new federation!**\n"
+ f"Name: `{fed_name}`\n"
+ f"ID: `{fed_id}`\n\n"
+ "Use the command below to join the federation:\n"
+ f"`/joinfed {fed_id}`"
+ )
+ try:
+ await client.send_message(
+ EVENT_LOGS,
+ f"New Federation: {fed_name}\nID:
{fed_id}
",
+ parse_mode=enums.ParseMode.HTML,
+ )
+ except Exception as e:
+ LOGGER.warning(f"Cannot send a message to EVENT_LOGS: {e}")
+
+@pbot.on_message(filters.command("delfed"))
+async def del_fed(client: Client, message: Message):
+ chat = message.chat
+ user = message.from_user
+ if chat.type != enums.ChatType.PRIVATE:
+ await message.reply_text(
+ "Federations can only be deleted in personal chat.",
+ )
+ return
+
+ args = message.command[1:]
+ if not args:
+ await message.reply_text("Please provide your federation ID.")
+ return
+
+ fed_id = args[0]
+ owner_id = sql.get_fed_info(fed_id).get("owner")
+
+ if str(owner_id) != str(user.id):
+ await message.reply_text("Only the federation owner can delete this!")
+ return
+
+ await message.reply_text(
+ "Are you sure you want to delete your federation? This action cannot be undone, you will lose your entire ban list, and all your sub-feds will be disconnected.",
+ reply_markup=InlineKeyboardMarkup(
+ [
+ [
+ InlineKeyboardButton(
+ text="Delete Federation",
+ callback_data=f"rm_fed_{fed_id}",
+ )
+ ],
+ [
+ InlineKeyboardButton(
+ text="Cancel",
+ callback_data="rm_fed_cancel",
+ )
+ ],
+ ]
+ ),
+ )
+
+@pbot.on_message(filters.command("fedname"))
+async def rename_fed(client: Client, message: Message):
+ user = message.from_user
+ args = message.command[1:]
+ if not args:
+ await message.reply_text("Please provide a name for the federation.")
+ return
+
+ fed_id = sql.get_fed_id(message.chat.id)
+ if not fed_id:
+ await message.reply_text("This group is not part of any federation!")
+ return
+
+ info = sql.get_fed_info(fed_id)
+ if str(info["owner"]) != str(user.id):
+ await message.reply_text("Only the federation owner can rename this!")
+ return
+
+ new_name = " ".join(args)
+ sql.rename_fed(fed_id, new_name)
+ await message.reply_text(f"Successfully renamed your federation to {new_name}!")
+
+@pbot.on_message(filters.command("joinfed"))
+async def join_fed(client: Client, message: Message):
+ chat = message.chat
+ user = message.from_user
+
+ if chat.type == enums.ChatType.PRIVATE:
+ await message.reply_text("This command can only be used in groups.")
+ return
+
+ member = await chat.get_member(user.id)
+ if member.status != enums.ChatMemberStatus.OWNER and user.id not in DRAGONS:
+ await message.reply_text("Only group owners can join a federation!")
+ return
+
+ args = message.command[1:]
+ if not args:
+ await message.reply_text("Please provide a federation ID.")
+ return
+
+ fed_id = args[0]
+ getfed = sql.get_fed_info(fed_id)
+ if not getfed:
+ await message.reply_text("Invalid federation ID!")
+ return
+
+ res = sql.chat_join_fed(fed_id, chat.title, chat.id)
+ if res:
+ await message.reply_text(f"Successfully joined the federation {getfed['fname']}!")
+ else:
+ await message.reply_text("This group is already part of a federation!")
+
+@pbot.on_message(filters.command("leavefed"))
+async def leave_fed(client: Client, message: Message):
+ chat = message.chat
+ user = message.from_user
+
+ if chat.type == enums.ChatType.PRIVATE:
+ await message.reply_text("This command can only be used in groups.")
+ return
+
+ member = await chat.get_member(user.id)
+ if member.status != enums.ChatMemberStatus.OWNER and user.id not in DRAGONS:
+ await message.reply_text("Only group owners can leave a federation!")
+ return
+
+ fed_id = sql.get_fed_id(chat.id)
+ if not fed_id:
+ await message.reply_text("This group is not part of any federation!")
+ return
+
+ res = sql.chat_leave_fed(chat.id)
+ if res:
+ await message.reply_text("Successfully left the federation!")
+ else:
+ await message.reply_text("Failed to leave the federation.")
+
+@pbot.on_message(filters.command("fprom"))
+async def user_join_fed(client: Client, message: Message):
+ chat = message.chat
+ user = message.from_user
+ fed_id = sql.get_fed_id(chat.id)
+ if not fed_id:
+ await message.reply_text("This group is not part of any federation!")
+ return
+
+ info = sql.get_fed_info(fed_id)
+ if str(info["owner"]) != str(user.id):
+ await message.reply_text("Only the federation owner can promote people!")
+ return
+
+ user_id = await extract_user(message, message.command[1:])
+ if not user_id:
+ await message.reply_text("You need to specify a user to promote.")
+ return
+
+ res = sql.user_join_fed(fed_id, user_id)
+ if res:
+ await message.reply_text("Successfully promoted user to federation admin!")
+ else:
+ await message.reply_text("User is already a federation admin!")
+
+@pbot.on_message(filters.command("fdem"))
+async def user_demote_fed(client: Client, message: Message):
+ chat = message.chat
+ user = message.from_user
+ fed_id = sql.get_fed_id(chat.id)
+ if not fed_id:
+ await message.reply_text("This group is not part of any federation!")
+ return
+
+ info = sql.get_fed_info(fed_id)
+ if str(info["owner"]) != str(user.id):
+ await message.reply_text("Only the federation owner can demote people!")
+ return
+
+ user_id = await extract_user(message, message.command[1:])
+ if not user_id:
+ await message.reply_text("You need to specify a user to demote.")
+ return
+
+ if str(info["owner"]) == str(user_id):
+ await message.reply_text("You cannot demote the owner!")
+ return
+
+ res = sql.user_demote_fed(fed_id, user_id)
+ if res:
+ await message.reply_text("Successfully demoted user from federation admin!")
+ else:
+ await message.reply_text("User is not a federation admin!")
+
+@pbot.on_message(filters.command("fedinfo"))
+async def fed_info(client: Client, message: Message):
+ args = message.command[1:]
+ if not args:
+ fed_id = sql.get_fed_id(message.chat.id)
+ if not fed_id:
+ await message.reply_text("Please provide a federation ID or use this in a federated group.")
+ return
+ else:
+ fed_id = args[0]
+
+ info = sql.get_fed_info(fed_id)
+ if not info:
+ await message.reply_text("Invalid federation ID!")
+ return
+
+ numban = sql.get_all_fban_users(fed_id)
+ numchat = sql.get_all_fed_chats(fed_id)
+
+ text = (
+ f"**Federation Information:**\n"
+ f"**Name:** {info['fname']}\n"
+ f"**ID:** `{fed_id}`\n"
+ f"**Owner:** {info['owner']}\n"
+ f"**Admins:** {len(sql.all_fed_admins(fed_id))}\n"
+ f"**Banned Users:** {len(numban)}\n"
+ f"**Connected Chats:** {len(numchat)}"
+ )
+ await message.reply_text(text)
+
+@pbot.on_message(filters.command("fedadmins"))
+async def fed_admin(client: Client, message: Message):
+ fed_id = sql.get_fed_id(message.chat.id)
+ if not fed_id:
+ await message.reply_text("This group is not part of any federation!")
+ return
+
+ info = sql.get_fed_info(fed_id)
+ admins = sql.all_fed_admins(fed_id)
+
+ text = f"**Admins in {info['fname']}:**\n"
+ text += f"- [{info['owner']}](tg://user?id={info['owner']}) (Owner)\n"
+ for admin in admins:
+ text += f"- [{admin['user_id']}](tg://user?id={admin['user_id']})\n"
+
+ await message.reply_text(text)
+
+@pbot.on_message(filters.command("fban") & filters.group)
+@DisableAbleCommandHandler("fban")
+async def fed_ban(client: Client, message: Message):
+ chat = message.chat
+ user = message.from_user
+ args = message.command[1:]
+
+ fed_id = sql.get_fed_id(chat.id)
+ if not fed_id:
+ await message.reply_text("This group is not a part of any federation!")
+ return
+
+ info = sql.get_fed_info(fed_id)
+ getfednotif = sql.user_feds_report(info["owner"])
+
+ # Check if user is fed admin
+ if not sql.is_user_fed_admin(fed_id, user.id):
+ await message.reply_text("Only federation admins can do this!")
+ return
+
+ user_id, reason = await extract_unt_fedban(message, args)
+ if not user_id:
+ await message.reply_text("You don't seem to be referring to a user")
+ return
+
+ me = await client.get_me()
+ if user_id == me.id:
+ await message.reply_text("I cannot fban myself!")
+ return
+
+ if sql.is_user_fed_owner(fed_id, user_id):
+ await message.reply_text("He is the federation owner!")
+ return
+
+ if sql.is_user_fed_admin(fed_id, user_id):
+ await message.reply_text("He is a federation admin!")
+ return
+
+ if user_id == OWNER_ID or user_id in DRAGONS or user_id in TIGERS or user_id in WOLVES:
+ await message.reply_text("This user cannot be fed banned!")
+ return
+
+ try:
+ user_chat = await client.get_users(user_id)
+ fban_user_id = user_chat.id
+ fban_user_name = user_chat.first_name
+ fban_user_lname = user_chat.last_name
+ fban_user_uname = user_chat.username
+ except Exception:
+ fban_user_id = int(user_id)
+ fban_user_name = f"user({user_id})"
+ fban_user_lname = None
+ fban_user_uname = None
+
+ user_target = f"[{fban_user_name}](tg://user?id={fban_user_id})"
+
+ fban, fbanreason, fbantime = sql.get_fban_user(fed_id, fban_user_id)
+ if fban:
+ sql.un_fban_user(fed_id, fban_user_id)
+
+ x = sql.fban_user(
+ fed_id,
+ fban_user_id,
+ fban_user_name,
+ fban_user_lname,
+ fban_user_uname,
+ reason or "No reason provided",
+ int(time.time()),
+ )
+ if not x:
+ await message.reply_text("Failed to ban from the federation!")
+ return
+
+ reply_text = (
+ "**New FedBan**\n"
+ f"**Federation:** {info['fname']}\n"
+ f"**Federation Admin:** {user.mention}\n"
+ f"**User:** {user_target}\n"
+ f"**User ID:** `{fban_user_id}`\n"
+ f"**Reason:** {reason or 'No reason provided'}"
+ )
+ await message.reply_text(reply_text)
+
+ get_fedlog = sql.get_fed_log(fed_id)
+ if get_fedlog:
+ try:
+ await client.send_message(get_fedlog, reply_text)
+ except Exception:
+ pass
+
+ fed_chats = sql.all_fed_chats(fed_id)
+ for fchat in fed_chats:
+ try:
+ await client.ban_chat_member(fchat, fban_user_id)
+ except Exception:
+ pass
+
+@pbot.on_message(filters.command("unfban") & filters.group)
+@DisableAbleCommandHandler("unfban")
+async def unfban(client: Client, message: Message):
+ chat = message.chat
+ user = message.from_user
+ args = message.command[1:]
+
+ fed_id = sql.get_fed_id(chat.id)
+ if not fed_id:
+ await message.reply_text("This group is not a part of any federation!")
+ return
+
+ if not sql.is_user_fed_admin(fed_id, user.id):
+ await message.reply_text("Only federation admins can do this!")
+ return
+
+ user_id = await extract_user(message, args)
+ if not user_id:
+ await message.reply_text("You don't seem to be referring to a user")
+ return
+
+ fban, fbanreason, fbantime = sql.get_fban_user(fed_id, user_id)
+ if not fban:
+ await message.reply_text("This user is not fedanned!")
+ return
+
+ sql.un_fban_user(fed_id, user_id)
+ await message.reply_text(f"Successfully un-fedbanned user.")
+
+ fed_chats = sql.all_fed_chats(fed_id)
+ for fchat in fed_chats:
+ try:
+ await client.unban_chat_member(fchat, user_id)
+ except Exception:
+ pass
+
+@pbot.on_message(filters.command("setfrules"))
+async def set_frules(client: Client, message: Message):
+ chat = message.chat
+ user = message.from_user
+ fed_id = sql.get_fed_id(chat.id)
+ if not fed_id:
+ await message.reply_text("This group is not part of any federation!")
+ return
+
+ info = sql.get_fed_info(fed_id)
+ if str(info["owner"]) != str(user.id):
+ await message.reply_text("Only the federation owner can set rules!")
+ return
+
+ rules = message.text.split(None, 1)[1] if len(message.command) > 1 else ""
+ sql.set_frules(fed_id, rules)
+ await message.reply_text("Successfully set federation rules!")
+
+@pbot.on_message(filters.command("frules"))
+async def get_frules(client: Client, message: Message):
+ fed_id = sql.get_fed_id(message.chat.id)
+ if not fed_id:
+ await message.reply_text("This group is not part of any federation!")
+ return
+
+ rules = sql.get_frules(fed_id)
+ if not rules:
+ await message.reply_text("This federation has no rules.")
+ return
+
+ await message.reply_text(f"**Federation Rules:**\n\n{rules}")
+
+@pbot.on_message(filters.command("fbroadcast"))
+async def fed_broadcast(client: Client, message: Message):
+ user = message.from_user
+ fed_id = sql.get_fed_id(message.chat.id)
+ if not fed_id:
+ await message.reply_text("This group is not part of any federation!")
+ return
+
+ info = sql.get_fed_info(fed_id)
+ if str(info["owner"]) != str(user.id):
+ await message.reply_text("Only the federation owner can broadcast!")
+ return
+
+ text = message.text.split(None, 1)[1] if len(message.command) > 1 else ""
+ if not text:
+ await message.reply_text("What do you want to broadcast?")
+ return
+
+ chats = sql.all_fed_chats(fed_id)
+ for chat_id in chats:
+ try:
+ await client.send_message(chat_id, text)
+ except Exception:
+ pass
+ await message.reply_text("Broadcast sent!")
+
+@pbot.on_message(filters.command("fbanlist"))
+async def fed_ban_list(client: Client, message: Message):
+ fed_id = sql.get_fed_id(message.chat.id)
+ if not fed_id:
+ await message.reply_text("This group is not part of any federation!")
+ return
+
+ bans = sql.get_all_fban_users(fed_id)
+ if not bans:
+ await message.reply_text("No users are federation-banned.")
+ return
+
+ text = "**Banned users in this federation:**\n"
+ for ban in bans:
+ text += f"- `{ban['user_id']}`: {ban['reason']}\n"
+
+ # In a real bot, you'd want to handle long list splitting here.
+ await message.reply_text(text[:4000])
+
+@pbot.on_message(filters.command("fednotif"))
+async def fed_notif(client: Client, message: Message):
+ user = message.from_user
+ fed_id = sql.get_fed_id(message.chat.id)
+ if not fed_id:
+ await message.reply_text("This group is not part of any federation!")
+ return
+
+ info = sql.get_fed_info(fed_id)
+ if str(info["owner"]) != str(user.id):
+ await message.reply_text("Only the federation owner can toggle notifications!")
+ return
+
+ args = message.command[1:]
+ if not args:
+ await message.reply_text("Use `on` or `off`.")
+ return
+
+ if args[0].lower() in ("on", "yes", "true"):
+ sql.set_user_feds_report(user.id, True)
+ await message.reply_text("Notifications turned on!")
+ elif args[0].lower() in ("off", "no", "false"):
+ sql.set_user_feds_report(user.id, False)
+ await message.reply_text("Notifications turned off!")
+ else:
+ await message.reply_text("Invalid option!")
+
+@pbot.on_message(filters.command("fedchats"))
+async def fed_chats(client: Client, message: Message):
+ fed_id = sql.get_fed_id(message.chat.id)
+ if not fed_id:
+ await message.reply_text("This group is not part of any federation!")
+ return
+
+ chats = sql.all_fed_chats(fed_id)
+ text = f"**Chats in this federation:**\n"
+ for chat_id in chats:
+ try:
+ c = await client.get_chat(chat_id)
+ text += f"- {c.title} (`{chat_id}`)\n"
+ except:
+ text += f"- `{chat_id}`\n"
+
+ await message.reply_text(text[:4000])
+
+@pbot.on_callback_query(filters.regex(r"^rm_fed_"))
+async def del_fed_button(client: Client, query: CallbackQuery):
+ fed_id = query.data.split("_")[-1]
+ user = query.from_user
+ info = sql.get_fed_info(fed_id)
+
+ if str(info["owner"]) != str(user.id):
+ await query.answer("You are not the owner of this federation!", show_alert=True)
+ return
+
+ sql.del_fed(fed_id)
+ await query.message.edit_text(f"Federation {info['fname']} has been deleted.")
+
+@pbot.on_message(filters.command("fstat"))
+async def fed_stat_user(client: Client, message: Message):
+ user_id = await extract_user(message, message.command[1:])
+ if not user_id:
+ user_id = message.from_user.id
+
+ feds = sql.get_user_fban_list(user_id)
+ if not feds:
+ await message.reply_text("This user has no federation bans.")
+ return
+
+ text = f"**Federation bans for this user:**\n"
+ for fed in feds:
+ info = sql.get_fed_info(fed['fed_id'])
+ text += f"- {info['fname']}: {fed['reason']}\n"
+ await message.reply_text(text)
+
+@pbot.on_message(filters.command("setfedlog"))
+async def set_fed_log(client: Client, message: Message):
+ user = message.from_user
+ fed_id = sql.get_fed_id(message.chat.id)
+ if not fed_id:
+ await message.reply_text("This group is not part of any federation!")
+ return
+
+ info = sql.get_fed_info(fed_id)
+ if str(info["owner"]) != str(user.id):
+ await message.reply_text("Only the federation owner can set log channel!")
+ return
+
+ sql.set_fed_log(fed_id, message.chat.id)
+ await message.reply_text("Successfully set this chat as federation log channel!")
+
+@pbot.on_message(filters.command("unsetfedlog"))
+async def unset_fed_log(client: Client, message: Message):
+ user = message.from_user
+ fed_id = sql.get_fed_id(message.chat.id)
+ if not fed_id:
+ await message.reply_text("This group is not part of any federation!")
+ return
+
+ info = sql.get_fed_info(fed_id)
+ if str(info["owner"]) != str(user.id):
+ await message.reply_text("Only the federation owner can unset log channel!")
+ return
+
+ sql.unset_fed_log(fed_id)
+ await message.reply_text("Successfully unset federation log channel!")
+
+@pbot.on_message(filters.command("subfed"))
+async def subs_feds(client: Client, message: Message):
+ user = message.from_user
+ fed_id = sql.get_fed_id(message.chat.id)
+ if not fed_id:
+ await message.reply_text("This group is not part of any federation!")
+ return
+
+ info = sql.get_fed_info(fed_id)
+ if str(info["owner"]) != str(user.id):
+ await message.reply_text("Only the federation owner can subscribe!")
+ return
+
+ args = message.command[1:]
+ if not args:
+ await message.reply_text("Please provide a federation ID to subscribe to.")
+ return
+
+ target_fed_id = args[0]
+ if target_fed_id == fed_id:
+ await message.reply_text("You cannot subscribe to your own federation!")
+ return
+
+ res = sql.subs_fed(fed_id, target_fed_id)
+ if res:
+ await message.reply_text("Successfully subscribed to federation!")
+ else:
+ await message.reply_text("Already subscribed or invalid ID.")
+
+@pbot.on_message(filters.command("unsubfed"))
+async def unsubs_feds(client: Client, message: Message):
+ user = message.from_user
+ fed_id = sql.get_fed_id(message.chat.id)
+ if not fed_id:
+ await message.reply_text("This group is not part of any federation!")
+ return
+
+ info = sql.get_fed_info(fed_id)
+ if str(info["owner"]) != str(user.id):
+ await message.reply_text("Only the federation owner can unsubscribe!")
+ return
+
+ args = message.command[1:]
+ if not args:
+ await message.reply_text("Please provide a federation ID to unsubscribe from.")
+ return
+
+ target_fed_id = args[0]
+ res = sql.unsubs_fed(fed_id, target_fed_id)
+ if res:
+ await message.reply_text("Successfully unsubscribed from federation!")
+ else:
+ await message.reply_text("Not subscribed to this federation.")
+
+@pbot.on_message(filters.command("fedsubs"))
+async def get_myfedsubs(client: Client, message: Message):
+ fed_id = sql.get_fed_id(message.chat.id)
+ if not fed_id:
+ await message.reply_text("This group is not part of any federation!")
+ return
+
+ subs = sql.get_subscriber(fed_id)
+ if not subs:
+ await message.reply_text("You have no subscribed federations.")
+ return
+
+ text = "**Subscribed federations:**\n"
+ for sub in subs:
+ text += f"- `{sub}`\n"
+ await message.reply_text(text)
+
+@pbot.on_message(filters.command("myfeds"))
+async def get_myfeds_list(client: Client, message: Message):
+ user = message.from_user
+ feds = sql.get_user_owner_fed_full(user.id)
+ if not feds:
+ await message.reply_text("You do not own any federations.")
+ return
+
+ text = "**Your federations:**\n"
+ for fed in feds:
+ text += f"- {fed['fname']} (`{fed['fed_id']}`)\n"
+ await message.reply_text(text)
+
+@pbot.on_message(filters.command("fedownerhelp"))
+async def fed_owner_help(client: Client, message: Message):
+ await message.reply_text(
+ "**Federation Owner Help:**\n"
+ "• `/newfed `: Create a new federation\n"
+ "• `/delfed `: Delete a federation\n"
+ "• `/fedname `: Rename a federation\n"
+ "• `/fprom `: Promote a user to fed admin\n"
+ "• `/fdem `: Demote a user from fed admin\n"
+ "• `/setfrules `: Set federation rules\n"
+ "• `/fbroadcast `: Broadcast a message to all fed chats\n"
+ "• `/setfedlog`: Set current chat as fed log\n"
+ "• `/unsetfedlog`: Unset fed log\n"
+ "• `/subfed `: Subscribe to another federation\n"
+ "• `/unsubfed `: Unsubscribe from another federation\n"
+ "• `/fednotif `: Toggle fed notifications"
+ )
+
+@pbot.on_message(filters.command("fedadminhelp"))
+async def fed_admin_help(client: Client, message: Message):
+ await message.reply_text(
+ "**Federation Admin Help:**\n"
+ "• `/fban [reason]`: Ban a user from the federation\n"
+ "• `/unfban `: Unban a user from the federation\n"
+ "• `/fstat `: Check a user's fed ban status\n"
+ "• `/fedinfo `: Get info about a federation\n"
+ "• `/fedadmins`: List all fed admins\n"
+ "• `/fedchats`: List all chats in the federation\n"
+ "• `/fbanlist`: List all banned users"
+ )
+
+@pbot.on_message(filters.command("feduserhelp"))
+async def fed_user_help(client: Client, message: Message):
+ await message.reply_text(
+ "**Federation User Help:**\n"
+ "• `/fstat`: Check your own fed ban status\n"
+ "• `/frules`: View the rules of the current federation"
+ )
+
+__mod_name__ = "Fed"
+__help__ = """
+Federations allow you to sync bans across multiple groups.
+One ban in the federation affects all chats connected to it.
+
+**Commands:**
+• `/fedownerhelp`: Help for Fed owners
+• `/fedadminhelp`: Help for Fed admins
+• `/feduserhelp`: Help for all users
+"""
diff --git a/QueenNoxi/modules/figlet.py b/QueenNoxi/modules/figlet.py
new file mode 100644
index 0000000000000000000000000000000000000000..db88f0586e0b3a2d31bf98b36014240ec8ac621a
--- /dev/null
+++ b/QueenNoxi/modules/figlet.py
@@ -0,0 +1,35 @@
+from pyrogram import filters
+import asyncio
+import pyfiglet
+from random import choice
+from pyrogram.types import InlineKeyboardButton, InlineKeyboardMarkup, Message, CallbackQuery
+from pyrogram.handlers import MessageHandler
+from .. import pbot as Client
+def figle(text):
+ x = pyfiglet.FigletFont.getFonts()
+ font = choice(x)
+ figled = str(pyfiglet.figlet_format(text,font=font))
+ keyboard = InlineKeyboardMarkup([[InlineKeyboardButton(text="ᴄʜᴀɴɢᴇ", callback_data="figlet"),InlineKeyboardButton(text="ᴄʟᴏsᴇ", callback_data="close_reply")]])
+ return figled, keyboard
+
+@Client.on_message(filters.command("figlet"))
+async def echo(bot, message):
+ global text
+ try:
+ text = message.text.split(' ',1)[1]
+ except IndexError:
+ return await message.reply_text("Example:\n\n`/figlet QueenNoxi`")
+ kul_text, keyboard = figle(text)
+ await message.reply_text(f"ʜᴇʀᴇ ɪs ʏᴏᴜʀ ғɪɢʟᴇᴛ :\n