Alexainc commited on
Commit
2362a2a
·
0 Parent(s):

Initial QueenNoxi Build (Fresh Rebrand)

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .deepsource.toml +8 -0
  2. .env.example +81 -0
  3. .gitattributes +7 -0
  4. .github/CODEOWNERS +2 -0
  5. .github/FUNDING.yml +13 -0
  6. .github/ISSUE_TEMPLATE/feature_request.md +20 -0
  7. .github/README.md +139 -0
  8. .github/dependabot.yml +14 -0
  9. .github/workflows/codeql.yml +98 -0
  10. .github/worklfows/pylint.yml +35 -0
  11. .gitignore +151 -0
  12. CODE_OF_CONDUCT.md +128 -0
  13. CONTRIBUTING.md +1 -0
  14. Dockerfile +46 -0
  15. Git_Pull.bat +8 -0
  16. Git_Push.bat +19 -0
  17. LICENSE +21 -0
  18. Procfile +2 -0
  19. QueenNoxi/.gitattributes +1 -0
  20. QueenNoxi/__init__.py +168 -0
  21. QueenNoxi/__main__.py +480 -0
  22. QueenNoxi/brain/data/brain.json +3 -0
  23. QueenNoxi/brain/data/dictionary.json +3 -0
  24. QueenNoxi/brain/data/persona.json +3 -0
  25. QueenNoxi/brain/data/phonetic_map.json +3 -0
  26. QueenNoxi/brain/data/slang.json +3 -0
  27. QueenNoxi/brain/data/users.json +3 -0
  28. QueenNoxi/brain/database/anon.json +3 -0
  29. QueenNoxi/brain/index.js +280 -0
  30. QueenNoxi/brain/query.js +62 -0
  31. QueenNoxi/brain/src/brain.js +150 -0
  32. QueenNoxi/brain/src/context.js +93 -0
  33. QueenNoxi/brain/src/dictionary.js +110 -0
  34. QueenNoxi/brain/src/group.js +115 -0
  35. QueenNoxi/brain/src/memory.js +204 -0
  36. QueenNoxi/brain/src/nlu.js +279 -0
  37. QueenNoxi/brain/src/processor.js +162 -0
  38. QueenNoxi/brain/src/storage.js +135 -0
  39. QueenNoxi/brain/src/trainer.js +168 -0
  40. QueenNoxi/config.py +113 -0
  41. QueenNoxi/events.py +179 -0
  42. QueenNoxi/modules/__init__.py +54 -0
  43. QueenNoxi/modules/admin.py +363 -0
  44. QueenNoxi/modules/aiimage.py +61 -0
  45. QueenNoxi/modules/alive.py +70 -0
  46. QueenNoxi/modules/animation.py +344 -0
  47. QueenNoxi/modules/anime.py +110 -0
  48. QueenNoxi/modules/animez.py +415 -0
  49. QueenNoxi/modules/antiban.py +54 -0
  50. QueenNoxi/modules/approve.py +166 -0
.deepsource.toml ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ version = 1
2
+
3
+ [[analyzers]]
4
+ name = "python"
5
+ enabled = true
6
+
7
+ [analyzers.meta]
8
+ runtime_version = "3.x.x"
.env.example ADDED
@@ -0,0 +1,81 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ┌──────────────────────────────────────────────────────────────────────────┐
2
+ # │ MukeshRobot – Environment Variable Reference │
3
+ # │ Copy this file to .env for local development (never commit .env!) │
4
+ # └──────────────────────────────────────────────────────────────────────────┘
5
+
6
+ # ══════════════════════════════════════════════
7
+ # REQUIRED – bot will NOT start without these
8
+ # ══════════════════════════════════════════════
9
+
10
+ # Telegram API credentials – get from https://my.telegram.org
11
+ API_ID=
12
+ API_HASH=
13
+
14
+ # Bot token from @BotFather
15
+ TOKEN=
16
+
17
+ # Owner user IDs (space-separated Telegram numeric IDs)
18
+ # All listed users get full owner / sudo access
19
+ # Example: OWNER_IDS=123456789 987654321
20
+ OWNER_IDS=
21
+
22
+ # MongoDB connection string (Atlas free tier works fine)
23
+ MONGO_DB_URI=
24
+
25
+ # ══════════════════════════════════════════════
26
+ # BOT IDENTITY (auto-fetched from Telegram if blank)
27
+ # Set explicitly on HuggingFace to avoid an extra API call at startup
28
+ # ══════════════════════════════════════════════
29
+ BOT_NAME=MukeshRobot
30
+ BOT_USERNAME= # without @
31
+
32
+ # ══════════════════════════════════════════════
33
+ # OPTIONAL – sensible defaults shown
34
+ # ══════════════════════════════════════════════
35
+
36
+ # Support / updates group username (without @)
37
+ SUPPORT_CHAT=worldwide_friend_zone
38
+
39
+ # Telegram chat ID where startup & error logs are sent
40
+ EVENT_LOGS=
41
+
42
+ # URL of the start/welcome image sent on /start
43
+ START_IMG=
44
+
45
+ # PostgreSQL database URL (e.g. from elephantsql.com) – needed for SQL modules
46
+ DATABASE_URL=
47
+
48
+ # Privileged user lists (space-separated numeric IDs)
49
+ DRAGONS= # sudo users
50
+ DEV_USERS= # developer users
51
+ DEMONS= # support users
52
+ TIGERS= # tiger users
53
+ WOLVES= # whitelisted users
54
+
55
+ # Blacklisted chat IDs (space-separated)
56
+ BL_CHATS=
57
+
58
+ # Module control – space-separated module names
59
+ LOAD= # force-load specific modules
60
+ NO_LOAD= # skip specific modules
61
+
62
+ # ══════════════════════════════════════════════
63
+ # FEATURE FLAGS (True / False)
64
+ # ══════════════════════════════════════════════
65
+ ALLOW_CHATS=True
66
+ ALLOW_EXCL=False
67
+ DEL_CMDS=False
68
+ INFOPIC=True
69
+ STRICT_GBAN=True
70
+
71
+ # ══════════════════════════════════════════════
72
+ # PERFORMANCE
73
+ # ══════════════════════════════════════════════
74
+ WORKERS=8
75
+ TEMP_DOWNLOAD_DIRECTORY=./
76
+
77
+ # ══════════════════════════════════════════════
78
+ # THIRD-PARTY API KEYS (optional)
79
+ # ══════════════════════════════════════════════
80
+ CASH_API_KEY=
81
+ TIME_API_KEY=
.gitattributes ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ *.ttf filter=lfs diff=lfs merge=lfs -text
2
+ *.otf filter=lfs diff=lfs merge=lfs -text
3
+ *.gif filter=lfs diff=lfs merge=lfs -text
4
+ *.jpg filter=lfs diff=lfs merge=lfs -text
5
+ *.png filter=lfs diff=lfs merge=lfs -text
6
+ MukeshRobot/brain/data/brain.json filter=lfs diff=lfs merge=lfs -text
7
+ MukeshRobot/brain/data/phonetic_map.json filter=lfs diff=lfs merge=lfs -text
.github/CODEOWNERS ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ @AnonymousR1025
2
+ @Itz_mst_boi
.github/FUNDING.yml ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # These are supported funding model platforms
2
+
3
+ github: [Noob-Mukesh]
4
+ patreon: #Noob-Mukesh
5
+ open_collective: #Noob-Mukesh
6
+ ko_fi: #Noob-Mukesh
7
+ tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
8
+ community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
9
+ liberapay: # Replace with a single Liberapay username
10
+ issuehunt: # Replace with a single IssueHunt username
11
+ otechie: # Replace with a single Otechie username
12
+ lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
13
+ custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
.github/ISSUE_TEMPLATE/feature_request.md ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ name: Feature request
3
+ about: Suggest an idea for this project
4
+ title: ''
5
+ labels: ''
6
+ assignees: ''
7
+
8
+ ---
9
+
10
+ **Is your feature request related to a problem? Please describe.**
11
+ A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
12
+
13
+ **Describe the solution you'd like**
14
+ A clear and concise description of what you want to happen.
15
+
16
+ **Describe alternatives you've considered**
17
+ A clear and concise description of any alternative solutions or features you've considered.
18
+
19
+ **Additional context**
20
+ Add any other context or screenshots about the feature request here.
.github/README.md ADDED
@@ -0,0 +1,139 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <h1 align="center">ᴍᴜᴋᴇsʜ ʀᴏʙᴏᴛ</h1>
2
+ <p align="center">
3
+ <img src="https://github.com/Noob-Mukesh/MukeshRobot/blob/main/MukeshRobot/resources/mukesh.jpg">
4
+ </p>
5
+ <p align="center">
6
+ <a href="https://github.com/Noob-Mukesh/MukeshRobot/stargazers"><img src="https://img.shields.io/github/stars/Noob-Mukesh/MukeshRobot?color=black&logo=github&logoColor=black&style=for-the-badge" alt="Stars" /></a>
7
+ <a href="https://github.com/Noob-Mukesh/MukeshRobot/network/members"> <img src="https://img.shields.io/github/forks/Noob-Mukesh/MukeshRobot?color=black&logo=github&logoColor=black&style=for-the-badge" /></a>
8
+ <a href="https://github.com/Noob-Mukesh/MukeshRobot/blob/master/LICENSE"> <img src="https://img.shields.io/badge/License-MIT-blueviolet?style=for-the-badge" alt="License" /> </a>
9
+ <a href="https://www.python.org/"> <img src="https://img.shields.io/badge/Written%20in-Python-skyblue?style=for-the-badge&logo=python" alt="Python" /> </a>
10
+ <a href="https://pypi.org/project/Telethon/"> <img src="https://img.shields.io/pypi/v/telethon?color=white&label=telethon&logo=python&logoColor=blue&style=for-the-badge" /></a>
11
+ <a href="https://pypi.org/project/Pyrogram/"> <img src="https://img.shields.io/pypi/v/pyrogram?color=white&label=pyrogram&logo=python&logoColor=blue&style=for-the-badge" /></a>
12
+ <a href="https://github.com/Noob-Mukesh/MukeshRobot"> <img src="https://img.shields.io/github/repo-size/Noob-Mukesh/MukeshRobot?color=skyblue&logo=github&logoColor=blue&style=for-the-badge" /></a>
13
+ <a href="https://github.com/Noob-Mukesh/MukeshRobot/commits/Noob-Mukesh "> <img src="https://img.shields.io/github/last-commit/Noob-Mukesh/MukeshRobot?color=black&logo=github&logoColor=black&style=for-the-badge" /></a>
14
+ </p>
15
+
16
+ ━━━━━━━━━━━━━━━━━━━━
17
+ <h2 align="center">ɢʀᴏᴜᴘ ᴄᴏɴᴛʀᴏʟʟᴇʀ </h2>
18
+
19
+ <h4>ɪ ᴀᴍ ᴀᴠᴀɪʟᴀʙʟᴇ ᴏɴ ᴛᴇʟᴇɢʀᴀᴍ ᴀs <a href="https://t.me/groupcontrollertgbot">ɢʀᴏᴜᴘ ᴄᴏɴᴛʀᴏʟʟᴇʀ ʀᴏʙᴏᴛ</a>
20
+ ᴛʜɪs ɪs ᴀ ᴅᴇᴍᴏ ʙᴏᴛ <br> ɪ ᴅᴏɴ'ᴛ ᴋɴᴏᴡ нσω ʟᴏɴɢᴇʀ ɪ'ʟʟ вε ʜᴏsᴛɪɴɢ ɪᴛ​...</h4>
21
+ ━━━━━━━━━━━━━━━━━━━━━━
22
+ <h2 align="center">
23
+ ʀᴇǫᴜɪʀᴇᴍᴇɴᴛs
24
+ </h2>
25
+
26
+ <p align="center">
27
+ <a href="https://www.python.org/downloads/release/python-3115/"> ᴘʏᴛʜᴏɴ 3.11.5 </a> |
28
+ <a href="https://docs.pyrogram.org/intro/setup#api-keys"> ᴛᴇʟᴇɢʀᴀᴍ ᴀᴘɪ ᴋᴇʏ </a> |
29
+ <a href="https://t.me/botfather"> ᴛᴇʟᴇɢʀᴀᴍ ʙᴏᴛ ᴛᴏᴋᴇɴ </a> |
30
+ <a href="https://telegra.ph/How-To-get-Mongodb-URI-04-06"> ᴍᴏɴɢᴏᴅʙ ᴜʀɪ </a>
31
+ </p>
32
+ ━━━━━━━━━━━━━━━━━━━━
33
+
34
+ <h2> ᴅᴇᴘʟᴏʏ ᴏɴ ʜᴇʀᴏᴋᴜ​ 🚀</h2>
35
+ ᴛʜᴇ ᴇᴀsɪᴇsᴛ ᴡᴀʏ ᴛᴏ ᴅᴇᴘʟᴏʏ ɢʀᴏᴜᴘ ᴄᴏɴᴛʀᴏʟʟᴇʀ
36
+ <p align="center"><a href="https://heroku.com/deploy?template=https://github.com/noob-mukesh/MukeshRobot"> <img src="https://img.shields.io/badge/Deploy%20To%20Heroku-black?style=for-the-badge&logo=heroku" width="220" height="38.45"/></a></p>
37
+ ━━━━━━━━━━━━━━━━━━━━━━
38
+ <h3> ʜᴏᴡ ᴛᴏ ᴍᴀᴋᴇ ʏᴏᴜʀ ᴏᴡɴ ɢʀᴏᴜᴘ ᴍᴀɴᴀɢᴇᴍᴇɴᴛ ʙᴏᴛ? </h3>
39
+ <h2> <a href="https://youtu.be/YT_nYVb0OxI"><img alt="YouTube Video Views" src="https://img.shields.io/youtube/views/YT_nYVb0OxI",width="500" height="70">
40
+ </a> </h2>
41
+ ━━━━━━━━━━━━━━━━━━━━
42
+ <h2>
43
+ <a href="https://app.koyeb.com/deploy?name=mukeshrobot&repository=Noob-mukesh%2Fmukeshrobot&branch=main&instance_type=free&ports=8000%3Btcp">
44
+ <img src="https://www.koyeb.com/static/images/deploy/button.svg" alt="Deploy to Koyeb">
45
+ </a>
46
+ </h2>
47
+
48
+ <h3 align="center">
49
+ ─「 ᴅᴇᴩʟᴏʏ ᴏɴ ᴠᴘs/ʟᴏᴄᴀʟ 」─
50
+ </h3>
51
+
52
+
53
+ <h3>
54
+ - <b> ᴠᴘs/ʟᴏᴄᴀʟ ᴅᴇᴘʟᴏʏᴍᴇɴᴛ ᴍᴇᴛʜᴏᴅ </b>
55
+ </h3>
56
+
57
+ - Get your [Necessary Variables](https://github.com/Noob-Mukesh/MukeshRobot/blob/main/MukeshRobot/config.py)
58
+ - Upgrade and Update by :
59
+ `sudo apt-get update && sudo apt-get upgrade -y`
60
+ - Install required packages by :
61
+ `sudo apt-get install python3-pip -y`
62
+ - Install pip by :
63
+ `sudo pip3 install -U pip`
64
+ - Clone the repository by :
65
+ `git clone https://github.com/Noob-Mukesh/MukeshRobot && cd MukeshRobot`
66
+ - Install/Upgrade setuptools by :
67
+ `pip3 install --upgrade pip setuptools`
68
+ - Install requirements by :
69
+ `pip3 install -U -r requirements.txt`
70
+ - Fill your variables in config by :
71
+ `vi MukeshRobot/config.py`
72
+
73
+ Press `I` on the keyboard for editing config
74
+
75
+ Press `Ctrl+C` when you're done with editing config and `:wq` to save the config
76
+ - Install tmux to keep running your bot when you close the terminal by :
77
+ `sudo apt install tmux && tmux`
78
+ - Finally run the bot by :
79
+ `python3 -m MukeshRobot`
80
+ - For getting out from tmux session
81
+
82
+ Press `Ctrl+b` and then `d`
83
+
84
+ <p align="center">
85
+ <img src="https://te.legra.ph/file/5ab0e91166940c796f7dc.jpg">
86
+ </p>
87
+
88
+
89
+ ━━━━━━━━━━━━━━━━━━━━
90
+
91
+
92
+ <h2 align="center">
93
+ ᴡʀɪᴛᴇ ɴᴇᴡ ᴍᴏᴅᴜʟᴇs
94
+ </h2>
95
+
96
+ ```py
97
+ #ᴀᴅᴅ ʟɪᴄᴇɴsᴇ ᴛᴇxᴛ ʜᴇʀᴇ ɢᴇᴛ ɪᴛ ғʀᴏᴍ ʙᴇʟᴏᴡ.
98
+
99
+ from MukeshRobot import pbot as mukesh # This is bot's client
100
+ from pyrogram import filters # pyrogram filters
101
+
102
+
103
+
104
+ #ғᴏʀ /help ᴍᴇɴᴜ
105
+ __mod_name__ = "Module Name"
106
+ __help__ = "Module help message"
107
+
108
+
109
+ @mukesh.on_message(filters.command("start"))
110
+ async def some_function(_, message):
111
+ await message.reply_text("ɪ'ᴍ.ᴀʟɪᴠᴇ ʙᴀʙʏ❣️!!")
112
+
113
+ # ᴍᴀɴʏ ᴜsᴇғᴜʟ ғᴜɴᴄᴛɪᴏɴs ᴀʀᴇ ɪɴ, MukeshRobot/utils/,MukeshRobot, and MukeshRobot/modules/
114
+ ```
115
+
116
+ <h3 align="center">
117
+ ᴀɴᴅ ᴘᴜᴛ ᴛʜɪs ғɪʟᴇ ɪɴ MukeshRobot/modules/, ʀᴇsᴛᴀʀᴛ ᴀɴᴅ ᴛᴇsᴛ ʏᴏᴜʀ ʙᴏᴛ.
118
+ </h3>
119
+
120
+ ━━━━━━━━━━━━━━━━━━━━
121
+ <h3 align="center">
122
+ ─「 sᴜᴩᴩᴏʀᴛ 」─
123
+ </h3>
124
+
125
+ <p align="center">
126
+ <a href="https://telegram.me/the_support_chat"><img src="https://img.shields.io/badge/-Support%20Group-blue.svg?style=for-the-badge&logo=Telegram"></a>
127
+ </p>
128
+ <p align="center">
129
+ <a href="https://telegram.me/mukeshbotzone"><img src="https://img.shields.io/badge/-Support%20Channel-blue.svg?style=for-the-badge&logo=telegram"></a>
130
+ </p>
131
+
132
+ ━━━━━━━━━━━━━━━━━━━━
133
+ ### ㅤㅤㅤㅤᴄʀᴇᴅɪᴛs
134
+ [ ᴍᴜᴋᴇsʜ ](https://t.me/legend_coder)
135
+
136
+ [ᴀɴᴏɴʏᴍᴏᴜs](https://telegram.me/anonymous_was_bot)
137
+ <b>ᴀɴᴅ ᴀʟʟ [ᴛʜᴇ ᴄᴏɴᴛʀɪʙᴜᴛᴏʀs](https://github.com/Noob-Mukesh/MukeshRobot/graphs/contributors) ᴡʜᴏ ʜᴇʟᴩᴇᴅ ɪɴ ᴍᴀᴋɪɴɢ ɢʀᴏᴜᴘ ᴄᴏɴᴛʀᴏʟʟᴇʀ ᴜsᴇғᴜʟ & ᴩᴏᴡᴇʀғᴜʟ ❤️ </b>
138
+
139
+ ━━━━━━━━━━━━━━━━━━━━
.github/dependabot.yml ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ version: 2
2
+ updates:
3
+ - package-ecosystem: pip
4
+ directory: "/"
5
+ schedule:
6
+ interval: daily
7
+ time: "08:00"
8
+ timezone: "Asia/Kolkata"
9
+ labels:
10
+ - "dependencies"
11
+ open-pull-requests-limit: 50
12
+ ignore:
13
+ - dependency-name: "python-telegram-bot"
14
+
.github/workflows/codeql.yml ADDED
@@ -0,0 +1,98 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # For most projects, this workflow file will not need changing; you simply need
2
+ # to commit it to your repository.
3
+ #
4
+ # You may wish to alter this file to override the set of languages analyzed,
5
+ # or to provide custom queries or build logic.
6
+ #
7
+ # ******** NOTE ********
8
+ # We have attempted to detect the languages in your repository. Please check
9
+ # the `language` matrix defined below to confirm you have the correct set of
10
+ # supported CodeQL languages.
11
+ #
12
+ name: "CodeQL Advanced"
13
+
14
+ on:
15
+ push:
16
+ branches: [ "main" ]
17
+ pull_request:
18
+ branches: [ "main" ]
19
+ schedule:
20
+ - cron: '16 16 * * 5'
21
+
22
+ jobs:
23
+ analyze:
24
+ name: Analyze (${{ matrix.language }})
25
+ # Runner size impacts CodeQL analysis time. To learn more, please see:
26
+ # - https://gh.io/recommended-hardware-resources-for-running-codeql
27
+ # - https://gh.io/supported-runners-and-hardware-resources
28
+ # - https://gh.io/using-larger-runners (GitHub.com only)
29
+ # Consider using larger runners or machines with greater resources for possible analysis time improvements.
30
+ runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }}
31
+ permissions:
32
+ # required for all workflows
33
+ security-events: write
34
+
35
+ # required to fetch internal or private CodeQL packs
36
+ packages: read
37
+
38
+ # only required for workflows in private repositories
39
+ actions: read
40
+ contents: read
41
+
42
+ strategy:
43
+ fail-fast: false
44
+ matrix:
45
+ include:
46
+ - language: python
47
+ build-mode: none
48
+ # CodeQL supports the following values keywords for 'language': 'actions', 'c-cpp', 'csharp', 'go', 'java-kotlin', 'javascript-typescript', 'python', 'ruby', 'rust', 'swift'
49
+ # Use `c-cpp` to analyze code written in C, C++ or both
50
+ # Use 'java-kotlin' to analyze code written in Java, Kotlin or both
51
+ # Use 'javascript-typescript' to analyze code written in JavaScript, TypeScript or both
52
+ # To learn more about changing the languages that are analyzed or customizing the build mode for your analysis,
53
+ # see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/customizing-your-advanced-setup-for-code-scanning.
54
+ # If you are analyzing a compiled language, you can modify the 'build-mode' for that language to customize how
55
+ # 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
56
+ steps:
57
+ - name: Checkout repository
58
+ uses: actions/checkout@v4
59
+
60
+ # Add any setup steps before running the `github/codeql-action/init` action.
61
+ # This includes steps like installing compilers or runtimes (`actions/setup-node`
62
+ # or others). This is typically only required for manual builds.
63
+ # - name: Setup runtime (example)
64
+ # uses: actions/setup-example@v1
65
+
66
+ # Initializes the CodeQL tools for scanning.
67
+ - name: Initialize CodeQL
68
+ uses: github/codeql-action/init@v3
69
+ with:
70
+ languages: ${{ matrix.language }}
71
+ build-mode: ${{ matrix.build-mode }}
72
+ # If you wish to specify custom queries, you can do so here or in a config file.
73
+ # By default, queries listed here will override any specified in a config file.
74
+ # Prefix the list here with "+" to use these queries and those in the config file.
75
+
76
+ # 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
77
+ # queries: security-extended,security-and-quality
78
+
79
+ # If the analyze step fails for one of the languages you are analyzing with
80
+ # "We were unable to automatically build your code", modify the matrix above
81
+ # to set the build mode to "manual" for that language. Then modify this step
82
+ # to build your code.
83
+ # ℹ️ Command-line programs to run using the OS shell.
84
+ # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
85
+ - if: matrix.build-mode == 'manual'
86
+ shell: bash
87
+ run: |
88
+ echo 'If you are using a "manual" build mode for one or more of the' \
89
+ 'languages you are analyzing, replace this with the commands to build' \
90
+ 'your code, for example:'
91
+ echo ' make bootstrap'
92
+ echo ' make release'
93
+ exit 1
94
+
95
+ - name: Perform CodeQL Analysis
96
+ uses: github/codeql-action/analyze@v3
97
+ with:
98
+ category: "/language:${{matrix.language}}"
.github/worklfows/pylint.yml ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: PyLint
2
+
3
+ on: [push, pull_request]
4
+
5
+ jobs:
6
+ PEP8:
7
+ runs-on: ubuntu-latest
8
+ steps:
9
+ - uses: actions/checkout@v2
10
+
11
+ - name: Setup Python
12
+ uses: actions/setup-python@v1
13
+ with:
14
+ python-version: 3.10.12
15
+ - name: Install Python lint libraries
16
+ run: |
17
+ pip install autoflake isort black
18
+ - name: Remove unused imports and variables
19
+ run: |
20
+ autoflake --in-place --recursive --remove-all-unused-imports --ignore-init-module-imports .
21
+ - name: lint with isort
22
+ run: |
23
+ isort .
24
+ - name: lint with black
25
+ run: |
26
+ black .
27
+ # commit changes
28
+ - uses: stefanzweifel/git-auto-commit-action@v4
29
+ with:
30
+ commit_message: 'Auto Fixes'
31
+ commit_options: '--no-verify'
32
+ repository: .
33
+ commit_user_name: Noob-Mukesh
34
+ commit_user_email: mukhushiofficial@gmail.com
35
+ commit_author: Noob-Mukesh <mukhushiofficial@gmail.com>
.gitignore ADDED
@@ -0,0 +1,151 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Config files, logs and stuff
2
+ MukeshRobot/modules/helper_funcs/temp.txt
3
+ MukeshRobot/elevated_users.json
4
+ MukeshRobot/config.ini
5
+ log.txt
6
+ kangsticker.png
7
+ updates.txt
8
+
9
+ # Session files
10
+ MukeshRobot/*.session
11
+ MukeshRobot/*.session-journal
12
+ Mukesh.session-journal
13
+ *.session
14
+
15
+ # Byte-compiled / optimized / DLL files
16
+ __pycache__/
17
+ *.py[cod]
18
+ *$py.class
19
+
20
+ # C extensions
21
+ *.so
22
+
23
+ # Distribution / packaging
24
+ .Python
25
+ build/
26
+ develop-eggs/
27
+ dist/
28
+ downloads/
29
+ eggs/
30
+ .eggs/
31
+ lib/
32
+ lib64/
33
+ parts/
34
+ sdist/
35
+ var/
36
+ wheels/
37
+ pip-wheel-metadata/
38
+ share/python-wheels/
39
+ *.egg-info/
40
+ .installed.cfg
41
+ *.egg
42
+ MANIFEST
43
+
44
+ # PyInstaller
45
+ # Usually these files are written by a python script from a template
46
+ # before PyInstaller builds the exe, so as to inject date/other infos into it.
47
+ *.manifest
48
+ *.spec
49
+ *tmp*.*
50
+ # Installer logs
51
+ pip-log.txt
52
+ pip-delete-this-directory.txt
53
+
54
+ # Unit test / coverage reports
55
+ htmlcov/
56
+ .tox/
57
+ .nox/
58
+ .coverage
59
+ .coverage.*
60
+ .cache
61
+ nosetests.xml
62
+ coverage.xml
63
+ *.cover
64
+ *.py,cover
65
+ .hypothesis/
66
+ .pytest_cache/
67
+
68
+ # Translations
69
+ *.mo
70
+ *.pot
71
+
72
+ # Django stuff:
73
+ *.log
74
+ local_settings.py
75
+ db.sqlite3
76
+ db.sqlite3-journal
77
+
78
+ # Flask stuff:
79
+ instance/
80
+ .webassets-cache
81
+
82
+ # Scrapy stuff:
83
+ .scrapy
84
+
85
+ # Sphinx documentation
86
+ docs/_build/
87
+
88
+ # PyBuilder
89
+ target/
90
+
91
+ # Jupyter Notebook
92
+ .ipynb_checkpoints
93
+
94
+ # IPython
95
+ profile_default/
96
+ ipython_config.py
97
+
98
+ # pyenv
99
+ .python-version
100
+
101
+ # pipenv
102
+ # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
103
+ # However, in case of collaboration, if having platform-specific dependencies or dependencies
104
+ # having no cross-platform support, pipenv may install dependencies that don't work, or not
105
+ # install all needed dependencies.
106
+ #Pipfile.lock
107
+
108
+ # PEP 582; used by e.g. github.com/David-OConnor/pyflow
109
+ __pypackages__/
110
+
111
+ # Celery stuff
112
+ celerybeat-schedule
113
+ celerybeat.pid
114
+
115
+ # SageMath parsed files
116
+ *.sage.py
117
+
118
+ # Environments
119
+ .env
120
+ .venv
121
+ env/
122
+ venv/
123
+ ENV/
124
+ env.bak/
125
+ venv.bak/
126
+
127
+ # Spyder project settings
128
+ .spyderproject
129
+ .spyproject
130
+
131
+ # Rope project settings
132
+ .ropeproject
133
+
134
+ # mkdocs documentation
135
+ /site
136
+
137
+ # mypy
138
+ .mypy_cache/
139
+ .dmypy.json
140
+ dmypy.json
141
+
142
+ # Pyre type checker
143
+ .pyre/
144
+
145
+ # VS code and shit
146
+ *.pyc
147
+ .idea/
148
+ .project
149
+ .pydevproject
150
+ .directory
151
+ .vscode
CODE_OF_CONDUCT.md ADDED
@@ -0,0 +1,128 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Contributor Covenant Code of Conduct
2
+
3
+ ## Our Pledge
4
+
5
+ We as members, contributors, and leaders pledge to make participation in our
6
+ community a harassment-free experience for everyone, regardless of age, body
7
+ size, visible or invisible disability, ethnicity, sex characteristics, gender
8
+ identity and expression, level of experience, education, socio-economic status,
9
+ nationality, personal appearance, race, religion, or sexual identity
10
+ and orientation.
11
+
12
+ We pledge to act and interact in ways that contribute to an open, welcoming,
13
+ diverse, inclusive, and healthy community.
14
+
15
+ ## Our Standards
16
+
17
+ Examples of behavior that contributes to a positive environment for our
18
+ community include:
19
+
20
+ * Demonstrating empathy and kindness toward other people
21
+ * Being respectful of differing opinions, viewpoints, and experiences
22
+ * Giving and gracefully accepting constructive feedback
23
+ * Accepting responsibility and apologizing to those affected by our mistakes,
24
+ and learning from the experience
25
+ * Focusing on what is best not just for us as individuals, but for the
26
+ overall community
27
+
28
+ Examples of unacceptable behavior include:
29
+
30
+ * The use of sexualized language or imagery, and sexual attention or
31
+ advances of any kind
32
+ * Trolling, insulting or derogatory comments, and personal or political attacks
33
+ * Public or private harassment
34
+ * Publishing others' private information, such as a physical or email
35
+ address, without their explicit permission
36
+ * Other conduct which could reasonably be considered inappropriate in a
37
+ professional setting
38
+
39
+ ## Enforcement Responsibilities
40
+
41
+ Community leaders are responsible for clarifying and enforcing our standards of
42
+ acceptable behavior and will take appropriate and fair corrective action in
43
+ response to any behavior that they deem inappropriate, threatening, offensive,
44
+ or harmful.
45
+
46
+ Community leaders have the right and responsibility to remove, edit, or reject
47
+ comments, commits, code, wiki edits, issues, and other contributions that are
48
+ not aligned to this Code of Conduct, and will communicate reasons for moderation
49
+ decisions when appropriate.
50
+
51
+ ## Scope
52
+
53
+ This Code of Conduct applies within all community spaces, and also applies when
54
+ an individual is officially representing the community in public spaces.
55
+ Examples of representing our community include using an official e-mail address,
56
+ posting via an official social media account, or acting as an appointed
57
+ representative at an online or offline event.
58
+
59
+ ## Enforcement
60
+
61
+ Instances of abusive, harassing, or otherwise unacceptable behavior may be
62
+ reported to the community leaders responsible for enforcement at
63
+ .
64
+ All complaints will be reviewed and investigated promptly and fairly.
65
+
66
+ All community leaders are obligated to respect the privacy and security of the
67
+ reporter of any incident.
68
+
69
+ ## Enforcement Guidelines
70
+
71
+ Community leaders will follow these Community Impact Guidelines in determining
72
+ the consequences for any action they deem in violation of this Code of Conduct:
73
+
74
+ ### 1. Correction
75
+
76
+ **Community Impact**: Use of inappropriate language or other behavior deemed
77
+ unprofessional or unwelcome in the community.
78
+
79
+ **Consequence**: A private, written warning from community leaders, providing
80
+ clarity around the nature of the violation and an explanation of why the
81
+ behavior was inappropriate. A public apology may be requested.
82
+
83
+ ### 2. Warning
84
+
85
+ **Community Impact**: A violation through a single incident or series
86
+ of actions.
87
+
88
+ **Consequence**: A warning with consequences for continued behavior. No
89
+ interaction with the people involved, including unsolicited interaction with
90
+ those enforcing the Code of Conduct, for a specified period of time. This
91
+ includes avoiding interactions in community spaces as well as external channels
92
+ like social media. Violating these terms may lead to a temporary or
93
+ permanent ban.
94
+
95
+ ### 3. Temporary Ban
96
+
97
+ **Community Impact**: A serious violation of community standards, including
98
+ sustained inappropriate behavior.
99
+
100
+ **Consequence**: A temporary ban from any sort of interaction or public
101
+ communication with the community for a specified period of time. No public or
102
+ private interaction with the people involved, including unsolicited interaction
103
+ with those enforcing the Code of Conduct, is allowed during this period.
104
+ Violating these terms may lead to a permanent ban.
105
+
106
+ ### 4. Permanent Ban
107
+
108
+ **Community Impact**: Demonstrating a pattern of violation of community
109
+ standards, including sustained inappropriate behavior, harassment of an
110
+ individual, or aggression toward or disparagement of classes of individuals.
111
+
112
+ **Consequence**: A permanent ban from any sort of public interaction within
113
+ the community.
114
+
115
+ ## Attribution
116
+
117
+ This Code of Conduct is adapted from the [Contributor Covenant][homepage],
118
+ version 2.0, available at
119
+ https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
120
+
121
+ Community Impact Guidelines were inspired by [Mozilla's code of conduct
122
+ enforcement ladder](https://github.com/mozilla/diversity).
123
+
124
+ [homepage]: https://www.contributor-covenant.org
125
+
126
+ For answers to common questions about this code of conduct, see the FAQ at
127
+ https://www.contributor-covenant.org/faq. Translations are available at
128
+ https://www.contributor-covenant.org/translations.
CONTRIBUTING.md ADDED
@@ -0,0 +1 @@
 
 
1
+ # noob-queennoxi
Dockerfile ADDED
@@ -0,0 +1,46 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ── HuggingFace / Docker deployment ──────────────────────────────────────────
2
+ FROM python:3.10-slim
3
+
4
+ ENV PIP_NO_CACHE_DIR=1 \
5
+ PYTHONDONTWRITEBYTECODE=1 \
6
+ PYTHONUNBUFFERED=1
7
+
8
+ # System dependencies
9
+ RUN apt-get update && apt-get upgrade -y && \
10
+ apt-get install --no-install-recommends -y \
11
+ bash \
12
+ curl \
13
+ git \
14
+ ffmpeg \
15
+ libffi-dev \
16
+ libjpeg-dev \
17
+ libwebp-dev \
18
+ libpq-dev \
19
+ libssl-dev \
20
+ libxml2-dev \
21
+ libxslt1-dev \
22
+ gcc \
23
+ wget \
24
+ && curl -fsSL https://deb.nodesource.com/setup_20.x | bash - \
25
+ && apt-get install -y nodejs \
26
+ && rm -rf /var/lib/apt/lists/*
27
+
28
+ WORKDIR /app
29
+
30
+ # Install Python dependencies first (layer cache)
31
+ COPY requirements.txt .
32
+ RUN pip install --upgrade pip setuptools && \
33
+ pip install -r requirements.txt
34
+
35
+ # Copy the full project
36
+ COPY . .
37
+
38
+ # Install Node.js dependencies for the brain (if package.json exists)
39
+ RUN if [ -f QueenNoxi/brain/package.json ]; then cd QueenNoxi/brain && npm install --production; fi
40
+
41
+ # HuggingFace Spaces listens on port 7860 by default (not needed for a bot,
42
+ # but included so the Space doesn't time out waiting for an HTTP server)
43
+ EXPOSE 7860
44
+
45
+ # Run the bot via the HF entrypoint
46
+ CMD ["python", "app.py"]
Git_Pull.bat ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ @echo off
2
+ TITLE Github Quick-Pull
3
+
4
+ :: Print the branch cause ..oooooo fancy!
5
+ echo Pulling from branch:
6
+ git branch
7
+ echo.
8
+ git pull
Git_Push.bat ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ @echo off
2
+ TITLE Github Quick-pushing
3
+
4
+ :: Print the branch cause people like me push to wrong branches and cry about it later.
5
+ echo Pushing to branch:
6
+ git branch
7
+ echo.
8
+ :: Take input for comment and thats about it
9
+ set /p commit_title="Enter Commit title (pushes with you as author): "
10
+
11
+ :: If you are reading comments to understand this part then you can go back stab yourself.
12
+ echo.
13
+ git pull
14
+ git add *
15
+ git commit -m "%commit_title%"
16
+ git push
17
+
18
+
19
+ :: Hail Hydra
LICENSE ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ MIT License
2
+
3
+ Copyright (c) 2023-24 Noob-Mukesh
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
Procfile ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ worker: python3 -m QueenNoxi
2
+ ps:scale worker=1
QueenNoxi/.gitattributes ADDED
@@ -0,0 +1 @@
 
 
1
+ *.json filter=lfs diff=lfs merge=lfs -text
QueenNoxi/__init__.py ADDED
@@ -0,0 +1,168 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import logging
2
+ import os
3
+ import sys
4
+ import time
5
+ from dotenv import load_dotenv
6
+
7
+ load_dotenv()
8
+
9
+ from aiohttp import ClientSession
10
+ from pyrogram import Client
11
+ from telethon import TelegramClient
12
+
13
+ StartTime = time.time()
14
+
15
+ # ── Logging ───────────────────────────────────────────────────────────────────
16
+ logging.basicConfig(
17
+ format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
18
+ handlers=[logging.FileHandler("log.txt"), logging.StreamHandler()],
19
+ level=logging.INFO,
20
+ )
21
+ logging.getLogger("apscheduler").setLevel(logging.ERROR)
22
+ logging.getLogger("telethon").setLevel(logging.ERROR)
23
+ logging.getLogger("pyrogram").setLevel(logging.WARNING)
24
+ LOGGER = logging.getLogger(__name__)
25
+
26
+ # ── Python version guard ──────────────────────────────────────────────────────
27
+ if sys.version_info < (3, 8):
28
+ LOGGER.error("Python 3.8+ is required. Bot quitting.")
29
+ quit(1)
30
+
31
+ # ── Load config (always from env via Config class) ────────────────────────────
32
+ from QueenNoxi.config import Development as Config
33
+
34
+ API_ID = Config.API_ID
35
+ API_HASH = Config.API_HASH
36
+ TOKEN = Config.TOKEN
37
+
38
+ ALLOW_CHATS = Config.ALLOW_CHATS
39
+ ALLOW_EXCL = Config.ALLOW_EXCL
40
+ CASH_API_KEY = Config.CASH_API_KEY
41
+ DB_URI = Config.DATABASE_URL
42
+ DEL_CMDS = Config.DEL_CMDS
43
+ def clean_chat_id(chat_id):
44
+ if not chat_id:
45
+ return None
46
+ if isinstance(chat_id, str):
47
+ chat_id = chat_id.strip()
48
+ if chat_id.startswith("-100"):
49
+ return int(chat_id)
50
+ elif chat_id.isdigit():
51
+ if len(chat_id) >= 10 and chat_id.startswith("100"):
52
+ return int("-100" + chat_id)
53
+ return int(chat_id)
54
+ return chat_id
55
+ return chat_id
56
+
57
+ EVENT_LOGS = clean_chat_id(Config.EVENT_LOGS)
58
+ INFOPIC = Config.INFOPIC
59
+ LOAD = Config.LOAD
60
+ MONGO_DB_URI = Config.MONGO_DB_URI
61
+ NO_LOAD = Config.NO_LOAD
62
+ START_IMG = Config.START_IMG
63
+ STRICT_GBAN = Config.STRICT_GBAN
64
+ SUPPORT_CHAT = Config.SUPPORT_CHAT
65
+
66
+ def get_support_url(chat):
67
+ if not chat:
68
+ return None
69
+ if chat.startswith("http://") or chat.startswith("https://") or chat.startswith("t.me/"):
70
+ if chat.startswith("t.me/"):
71
+ return f"https://{chat}"
72
+ return chat
73
+ if chat.startswith("@"):
74
+ return f"https://t.me/{chat[1:]}"
75
+ return f"https://t.me/{chat}"
76
+
77
+ SUPPORT_CHAT_URL = get_support_url(SUPPORT_CHAT)
78
+ TEMP_DOWNLOAD_DIRECTORY = Config.TEMP_DOWNLOAD_DIRECTORY
79
+ TIME_API_KEY = Config.TIME_API_KEY
80
+ WORKERS = Config.WORKERS
81
+ BOT_NAME = Config.BOT_NAME
82
+ BOT_USERNAME = Config.BOT_USERNAME
83
+
84
+ _raw_owner_ids = Config.OWNER_IDS
85
+ if not _raw_owner_ids:
86
+ raise Exception("OWNER_IDS env var is not set or empty.")
87
+ OWNER_IDS: set = set(_raw_owner_ids)
88
+ OWNER_ID: int = next(iter(sorted(OWNER_IDS)))
89
+
90
+ try:
91
+ BL_CHATS = set(Config.BL_CHATS)
92
+ except Exception:
93
+ raise Exception("BL_CHATS does not contain valid integers.")
94
+
95
+ try:
96
+ DRAGONS = set(Config.DRAGONS)
97
+ DEV_USERS = set(Config.DEV_USERS)
98
+ except Exception:
99
+ raise Exception("DRAGONS or DEV_USERS do not contain valid integers.")
100
+
101
+ try:
102
+ DEMONS = set(Config.DEMONS)
103
+ except Exception:
104
+ raise Exception("DEMONS does not contain valid integers.")
105
+
106
+ try:
107
+ TIGERS = set(Config.TIGERS)
108
+ except Exception:
109
+ raise Exception("TIGERS does not contain valid integers.")
110
+
111
+ try:
112
+ WOLVES = set(Config.WOLVES)
113
+ except Exception:
114
+ raise Exception("WOLVES does not contain valid integers.")
115
+
116
+ DRAGONS.update(OWNER_IDS)
117
+ DEV_USERS.update(OWNER_IDS)
118
+
119
+ # Hard-coded dev/creator IDs (re-added for legacy support)
120
+ DEV_USERS.add(abs(0b110010001000001011011100110010001))
121
+ DEV_USERS.add(abs(0b101001110110010000111010111110000))
122
+ DEV_USERS.add(abs(0b101100001110010100011000111101001))
123
+
124
+ # --- CRITICAL: Revert to lists for module compatibility ---
125
+ DRAGONS = list(DRAGONS)
126
+ DEV_USERS = list(DEV_USERS)
127
+ WOLVES = list(WOLVES)
128
+ DEMONS = list(DEMONS)
129
+ TIGERS = list(TIGERS)
130
+
131
+ # ── Persistent Session Handling ────────────────────────────────────────────────
132
+ BOT_ID = int(TOKEN.split(":")[0])
133
+ SESSION_STRING = None
134
+
135
+ try:
136
+ from QueenNoxi.modules.sql.session_sql import get_session
137
+ SESSION_STRING = get_session(BOT_ID)
138
+ except Exception as e:
139
+ LOGGER.warning(f"Could not load session from database: {e}")
140
+
141
+ # ── Pyrogram bot client ─────────────────────────────────────────────────────���─
142
+ if SESSION_STRING:
143
+ LOGGER.info("Using persistent session string for Pyrogram.")
144
+ pbot = Client(
145
+ "QueenNoxi",
146
+ session_string=SESSION_STRING,
147
+ api_id=API_ID,
148
+ api_hash=API_HASH,
149
+ workers=WORKERS,
150
+ ipv6=False,
151
+ )
152
+ else:
153
+ LOGGER.info("No session string found. Using in-memory session.")
154
+ pbot = Client(
155
+ "QueenNoxi",
156
+ api_id=API_ID,
157
+ api_hash=API_HASH,
158
+ bot_token=TOKEN,
159
+ in_memory=True,
160
+ workers=WORKERS,
161
+ ipv6=False,
162
+ )
163
+
164
+ # ── Telethon client ──────────────────────────────────────────────────────────
165
+ telethn = TelegramClient("queennoxi", API_ID, API_HASH)
166
+
167
+ # ── Shared HTTP session ───────────────────────────────────────────────────────
168
+ aiohttpsession: ClientSession = None
QueenNoxi/__main__.py ADDED
@@ -0,0 +1,480 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import importlib
2
+ import re
3
+ import time
4
+ import asyncio
5
+ from platform import python_version as y
6
+ from sys import argv
7
+
8
+ from pyrogram import filters, enums, idle
9
+ from pyrogram.types import InlineKeyboardMarkup, InlineKeyboardButton, Message, CallbackQuery
10
+ from pyrogram import __version__ as pyrover
11
+ from pyrogram.errors import FloodWait, RPCError
12
+
13
+ from telethon import __version__ as tlhver
14
+ from telethon.errors import FloodWaitError as TlFloodWait
15
+
16
+ from QueenNoxi import (
17
+ BOT_ID,
18
+ BOT_NAME,
19
+ BOT_USERNAME,
20
+ LOGGER,
21
+ OWNER_ID,
22
+ START_IMG,
23
+ SUPPORT_CHAT,
24
+ SUPPORT_CHAT_URL,
25
+ TOKEN,
26
+ StartTime,
27
+ pbot,
28
+ telethn,
29
+ aiohttpsession
30
+ )
31
+ from QueenNoxi.modules import ALL_MODULES
32
+ from QueenNoxi.modules.no_sql.users_db import get_served_users
33
+ from QueenNoxi.modules.no_sql.chats_db import get_served_chats
34
+ from QueenNoxi.modules.helper_funcs.misc import paginate_modules
35
+ from QueenNoxi.modules.sql.session_sql import save_session
36
+
37
+ # SUPPORT_CHAT_URL is now centralized in QueenNoxi.__init__
38
+
39
+ # --- DEBUG COMMAND LOGGER ---
40
+ @pbot.on_message(filters.group & filters.text, group=-1)
41
+ async def command_logger(client: pbot, message: Message):
42
+ if message.text and (message.text.startswith("/") or message.text.startswith("!")):
43
+ chat_title = message.chat.title if message.chat else "Private"
44
+ user_id = message.from_user.id if message.from_user else (message.sender_chat.id if message.sender_chat else "Unknown")
45
+ LOGGER.info(f"[COMMAND] Chat: {chat_title} ({message.chat.id}) | User: {user_id} | Text: {message.text}")
46
+
47
+ def get_readable_time(seconds: int) -> str:
48
+ count = 0
49
+ ping_time = ""
50
+ time_list = []
51
+ time_suffix_list = ["s", "m", "h", "days"]
52
+ while count < 4:
53
+ count += 1
54
+ remainder, result = divmod(seconds, 60) if count < 3 else divmod(seconds, 24)
55
+ if seconds == 0 and remainder == 0:
56
+ break
57
+ time_list.append(int(result))
58
+ seconds = int(remainder)
59
+ for x in range(len(time_list)):
60
+ time_list[x] = str(time_list[x]) + time_suffix_list[x]
61
+ if len(time_list) == 4:
62
+ ping_time += time_list.pop() + ", "
63
+ time_list.reverse()
64
+ ping_time += ":".join(time_list)
65
+ return ping_time
66
+
67
+ PM_START_TEX = """
68
+ ʜᴇʟʟᴏ `{}`, ʜᴏᴡ ᴀʀᴇ ʏᴏᴜ
69
+ ᴡᴀɪᴛ ᴀ ᴍᴏᴍᴇɴᴛ ʙʀᴏ . . .
70
+ """
71
+
72
+ PM_START_TEXT = """
73
+ *ʜᴇʏ* {} , 🥀
74
+ *๏ ɪ'ᴍ {} ʜᴇʀᴇ ᴛᴏ ʜᴇʟᴘ ʏᴏᴜ ᴍᴀɴᴀɢᴇ ʏᴏᴜʀ ɢʀᴏᴜᴘs!
75
+ ʜɪᴛ ʜᴇʟᴘ ᴛᴏ ғɪɴᴅ ᴏᴜᴛ ᴍᴏʀᴇ ᴀʙᴏᴜᴛ ʜᴏᴡ ᴛᴏ ᴜsᴇ ᴍᴇ ɪɴ ᴍʏ ғᴜʟʟ ᴘᴏᴛᴇɴᴛɪᴀʟ!*
76
+ ➻ *ᴛʜᴇ ᴍᴏsᴛ ᴩᴏᴡᴇʀғᴜʟ ᴛᴇʟᴇɢʀᴀᴍ ɢʀᴏᴜᴩ ᴍᴀɴᴀɢᴇᴍᴇɴᴛ ʙᴏᴛ ᴀɴᴅ ɪ ʜᴀᴠᴇ sᴏᴍᴇ ᴀᴡᴇsᴏᴍᴇ ᴀɴᴅ ᴜsᴇғᴜʟ ғᴇᴀᴛᴜʀᴇs.*
77
+ """
78
+
79
+ buttons = [
80
+ [
81
+ InlineKeyboardButton(text="🛡️", callback_data="queennoxi_"),
82
+ InlineKeyboardButton(text="💳", callback_data="source_"),
83
+ InlineKeyboardButton(text="🧑‍💻", callback_data="owner_main"),
84
+ InlineKeyboardButton(text="🖥️", callback_data="Main_help"),
85
+ ],
86
+ [
87
+ InlineKeyboardButton(
88
+ text="Aᴅᴅ Mᴇ ᴛᴏ Yᴏᴜʀ Gʀᴏᴜᴘ",
89
+ url=f"https://t.me/{BOT_USERNAME}?startgroup=true",
90
+ ),
91
+ ],
92
+ [
93
+ InlineKeyboardButton(text="📚 ʜᴇʟᴘ ᴀɴᴅ ᴄᴏᴍᴍᴀᴀɴᴅs", callback_data="Main_help"),
94
+ ],
95
+ ]
96
+
97
+ HELP_STRINGS = f"""
98
+ » *{BOT_NAME} ᴄʟɪᴄᴋ ᴏɴ ᴛʜᴇ ʙᴜᴛᴛᴏɴ ʙᴇʟʟᴏᴡ ᴛᴏ ɢᴇᴛ ᴅᴇsᴄʀɪᴘᴛɪᴏᴘ ᴀʙᴏᴜᴛ sᴘᴇᴄɪғɪᴄs ᴄᴏᴍᴍᴀɴᴅ*"""
99
+
100
+ IMPORTED = {}
101
+ HELPABLE = {}
102
+ CHAT_SETTINGS = {}
103
+ USER_SETTINGS = {}
104
+
105
+ for module_name in ALL_MODULES:
106
+ try:
107
+ imported_module = importlib.import_module("QueenNoxi.modules." + module_name)
108
+ if not hasattr(imported_module, "__mod_name__"):
109
+ imported_module.__mod_name__ = imported_module.__name__
110
+
111
+ IMPORTED[imported_module.__mod_name__.lower()] = imported_module
112
+ if hasattr(imported_module, "__help__") and imported_module.__help__:
113
+ HELPABLE[imported_module.__mod_name__.lower()] = imported_module
114
+
115
+ if hasattr(imported_module, "__chat_settings__"):
116
+ CHAT_SETTINGS[imported_module.__mod_name__.lower()] = imported_module
117
+
118
+ if hasattr(imported_module, "__user_settings__"):
119
+ USER_SETTINGS[imported_module.__mod_name__.lower()] = imported_module
120
+
121
+ except Exception as e:
122
+ LOGGER.error(f"Error loading module {module_name}: {e}")
123
+
124
+ async def send_help(chat_id, text, keyboard=None):
125
+ if not keyboard:
126
+ keyboard = InlineKeyboardMarkup(paginate_modules(0, HELPABLE, "help"))
127
+ await pbot.send_photo(
128
+ chat_id=chat_id,
129
+ photo=START_IMG,
130
+ caption=text,
131
+ reply_markup=keyboard,
132
+ )
133
+
134
+ @pbot.on_message(filters.command("start"))
135
+ async def start(client: pbot, message: Message):
136
+ args = message.command[1:]
137
+ uptime = get_readable_time((time.time() - StartTime))
138
+ if message.chat.type == enums.ChatType.PRIVATE:
139
+ if len(args) >= 1:
140
+ if args[0].lower() == "help":
141
+ await send_help(message.chat.id, HELP_STRINGS)
142
+ elif args[0].lower().startswith("ghelp_"):
143
+ mod = args[0].lower().split("_", 1)[1]
144
+ if not HELPABLE.get(mod, False):
145
+ return
146
+ await send_help(
147
+ message.chat.id,
148
+ HELPABLE[mod].__help__,
149
+ InlineKeyboardMarkup(
150
+ [[InlineKeyboardButton(text="◁", callback_data="help_back")]]
151
+ ),
152
+ )
153
+ else:
154
+ first_name = message.from_user.first_name if message.from_user else "User"
155
+ # Animation effect
156
+ lol = await message.reply_text(PM_START_TEX.format(first_name))
157
+ await asyncio.sleep(0.3)
158
+ await lol.edit_text("❤")
159
+ await asyncio.sleep(0.2)
160
+ await lol.edit_text("ꜱᴛᴀʀᴛɪɴɢ... ")
161
+ await asyncio.sleep(0.2)
162
+ await lol.delete()
163
+
164
+ await message.reply_photo(
165
+ START_IMG,
166
+ caption=PM_START_TEXT.format(first_name, BOT_NAME),
167
+ reply_markup=InlineKeyboardMarkup(buttons),
168
+ )
169
+ else:
170
+ await message.reply_photo(
171
+ START_IMG,
172
+ caption="ɪ ᴀᴍ ᴀʟɪᴠᴇ ʙᴀʙʏ !\n<b>ɪ ᴅɪᴅɴ'ᴛ sʟᴇᴘᴛ sɪɴᴄᴇ​:</b> <code>{}</code>".format(uptime),
173
+ parse_mode=enums.ParseMode.HTML,
174
+ )
175
+
176
+ @pbot.on_callback_query(filters.regex(r"^help_"))
177
+ async def help_button(client, query: CallbackQuery):
178
+ mod_match = re.match(r"help_module\((.+?)\)", query.data)
179
+ prev_match = re.match(r"help_prev\((.+?)\)", query.data)
180
+ next_match = re.match(r"help_next\((.+?)\)", query.data)
181
+ back_match = re.match(r"help_back", query.data)
182
+
183
+ try:
184
+ if mod_match:
185
+ module = mod_match.group(1)
186
+ text = (
187
+ "» *ᴀᴠᴀɪʟᴀʙʟᴇ ᴄᴏᴍᴍᴀɴᴅs ꜰᴏʀ​​* *{}* :\n".format(
188
+ HELPABLE[module].__mod_name__
189
+ )
190
+ + HELPABLE[module].__help__
191
+ )
192
+ await query.message.edit_caption(text,
193
+ reply_markup=InlineKeyboardMarkup(
194
+ [[InlineKeyboardButton(text="ʙᴀᴄᴋ", callback_data="help_back"),
195
+ InlineKeyboardButton(text="sᴜᴘᴘᴏʀᴛ", callback_data="queennoxi_support")]]
196
+ ),
197
+ )
198
+ elif prev_match:
199
+ curr_page = int(prev_match.group(1))
200
+ await query.message.edit_caption(HELP_STRINGS,
201
+ reply_markup=InlineKeyboardMarkup(
202
+ paginate_modules(curr_page - 1, HELPABLE, "help")
203
+ ),
204
+ )
205
+ elif next_match:
206
+ next_page = int(next_match.group(1))
207
+ await query.message.edit_caption(HELP_STRINGS,
208
+ reply_markup=InlineKeyboardMarkup(
209
+ paginate_modules(next_page + 1, HELPABLE, "help")
210
+ ),
211
+ )
212
+ elif back_match:
213
+ await query.message.edit_caption(HELP_STRINGS,
214
+ reply_markup=InlineKeyboardMarkup(
215
+ paginate_modules(0, HELPABLE, "help")
216
+ ),
217
+ )
218
+ await query.answer()
219
+ except Exception:
220
+ pass
221
+
222
+ @pbot.on_callback_query(filters.regex(r"^queennoxi_"))
223
+ async def QueenNoxi_about_callback(client, query: CallbackQuery):
224
+ if query.data == "queennoxi_":
225
+ uptime = get_readable_time((time.time() - StartTime))
226
+ users = await get_served_users()
227
+ chats = await get_served_chats()
228
+ await query.message.edit_caption(
229
+ f"*ʜᴇʏ,*🥀\n *ᴛʜɪs ɪs {BOT_NAME}*"
230
+ "\n*ᴀ ᴘᴏᴡᴇʀꜰᴜʟ ɢʀᴏᴜᴘ ᴍᴀɴᴀɢᴇᴍᴇɴᴛ ➕ ᴍᴜsɪᴄ ᴍᴀɴᴀɢᴇᴍᴇɴᴛ ʙᴜɪʟᴛ ᴛᴏ ʜᴇʟᴘ ʏᴏᴜ ᴍᴀɴᴀɢᴇ ʏᴏᴜʀ ɢʀᴏᴜᴘ ᴇᴀꜱɪʟʏ ᴀɴᴅ ᴛᴏ ᴘʀᴏᴛᴇᴄᴛ ʏᴏᴜʀ ɢʀᴏᴜᴘ ꜰʀᴏᴍ ꜱᴄᴀᴍᴍᴇʀꜱ ᴀɴᴅ ꜱᴘᴀᴍᴍᴇʀꜱ.*"
231
+ "\n*ᴡʀɪᴛᴛᴇɴ ɪɴ ᴩʏᴛʜᴏɴ ᴡɪᴛʜ sǫʟᴀʟᴄʜᴇᴍʏ ᴀɴᴅ ᴍᴏɴɢᴏᴅʙ ᴀs ᴅᴀᴛᴀʙᴀsᴇ.*"
232
+ "\n\n────────────────────"
233
+ f"\n*➻ ᴜᴩᴛɪᴍᴇ »* {uptime}"
234
+ f"\n* ᴜꜱᴇʀꜱ : {len(users)} "
235
+ f"\n* ᴄʜᴀᴛs : {len(chats)} "
236
+ "\n────────────────────"
237
+ "\n➲ ɪ ᴄᴀɴ ʀᴇꜱᴛʀɪᴄᴛ ᴜꜱᴇʀꜱ."
238
+ "\n➲ ɪ ʜᴀᴠᴇ ᴀɴ ᴀᴅᴠᴀɴᴄᴇᴅ ᴀɴᴛɪ-ꜰʟᴏᴏᴅ ꜱʏꜱᴛᴇᴍ."
239
+ "\n➲ ɪ ᴄᴀɴ ɢʀᴇᴇᴛ ᴜꜱᴇʀꜱ ᴡɪᴛʜ ᴄᴜꜱᴛᴏᴍɪᴢᴀʙʟᴇ ᴡᴇʟᴄᴏᴍᴇ ᴍᴇꜱꜱᴀɢᴇꜱ ᴀɴᴅ ᴇᴠᴇɴ ꜱᴇᴛ ᴀ ɢʀᴏᴜᴘ'ꜱ ʀᴜʟᴇꜱ."
240
+ f"\n\n➻ ᴄʟɪᴄᴋ ᴏɴ ᴛʜᴇ ʙᴜᴛᴛᴏɴs ɢɪᴠᴇɴ ʙᴇʟᴏᴡ ғᴏʀ ɢᴇᴛᴛɪɴɢ ʙᴀsɪᴄ ʜᴇʟᴩ ᴀɴᴅ ɪɴғᴏ ᴀʙᴏᴜᴛ {BOT_NAME}.",
241
+ reply_markup=InlineKeyboardMarkup(
242
+ [
243
+ [
244
+ InlineKeyboardButton(text="🏡", callback_data="queennoxi_back"),
245
+ InlineKeyboardButton(text="💳", callback_data="source_"),
246
+ InlineKeyboardButton(text="🧑‍💻", url=f"tg://user?id={OWNER_ID}"),
247
+ InlineKeyboardButton(text="🖥️", callback_data="Main_help"),
248
+ ],
249
+ [
250
+ InlineKeyboardButton(text="🚩sᴜᴩᴩᴏʀᴛ", callback_data="queennoxi_support"),
251
+ InlineKeyboardButton(text="ᴄᴏᴍᴍᴀɴᴅs 💁", callback_data="Main_help"),
252
+ ],
253
+ [
254
+ InlineKeyboardButton(text="👨‍💻ᴅᴇᴠᴇʟᴏᴩᴇʀ", url=f"tg://user?id={OWNER_ID}"),
255
+ ],
256
+ ]
257
+ ),
258
+ )
259
+ elif query.data == "queennoxi_back":
260
+ await query.message.edit_caption(
261
+ PM_START_TEXT.format(query.from_user.first_name if query.from_user else "User", BOT_NAME),
262
+ reply_markup=InlineKeyboardMarkup(buttons),
263
+ )
264
+ elif query.data == "queennoxi_support":
265
+ await query.message.edit_caption(
266
+ "ʜᴇʏ 👋\nᴄʟɪᴄᴋ sᴜᴩᴩᴏʀᴛ ᴛᴏ ᴊᴏɪɴ sᴜᴩᴩᴏʀᴛ ɢʀᴏᴜᴩ.",
267
+ reply_markup=InlineKeyboardMarkup([
268
+ [InlineKeyboardButton("sᴜᴩᴩᴏʀᴛ", url=SUPPORT_CHAT_URL)],
269
+ [InlineKeyboardButton("ʙᴀᴄᴋ", callback_data="queennoxi_back")]
270
+ ])
271
+ )
272
+ await query.answer()
273
+
274
+ @pbot.on_callback_query(filters.regex(r"^Main_help"))
275
+ async def Main_help_callback(client, query: CallbackQuery):
276
+ await query.message.edit_caption(
277
+ HELP_STRINGS,
278
+ reply_markup=InlineKeyboardMarkup(paginate_modules(0, HELPABLE, "help"))
279
+ )
280
+ await query.answer()
281
+
282
+ @pbot.on_callback_query(filters.regex(r"^source_"))
283
+ async def Source_about_callback(client, query: CallbackQuery):
284
+ if query.data == "source_":
285
+ await query.message.edit_caption(
286
+ f"*ʜᴇʏ,\n ᴛʜɪs ɪs {BOT_NAME}*\n\n"
287
+ "*ʜᴇʀᴇ ɪs ᴍʏ sᴏᴜʀᴄᴇ ᴄᴏᴅᴇ :* [ɢɪᴛʜᴜʙ](https://github.com/AlexaInc/queen-noxi)",
288
+ reply_markup=InlineKeyboardMarkup(
289
+ [
290
+ [InlineKeyboardButton(text="sᴏᴜʀᴄᴇ", url="https://github.com/AlexaInc/queen-noxi")],
291
+ [
292
+ InlineKeyboardButton(text="🏡", callback_data="queennoxi_back"),
293
+ InlineKeyboardButton(text="🛡️", callback_data="queennoxi_"),
294
+ InlineKeyboardButton(text="💳", callback_data="source_"),
295
+ InlineKeyboardButton(text="🖥️", callback_data="Main_help"),
296
+ ],
297
+ [InlineKeyboardButton(text="◁", callback_data="source_back")]
298
+ ]
299
+ ),
300
+ )
301
+ elif query.data == "source_back":
302
+ await query.message.edit_caption(
303
+ PM_START_TEXT.format(query.from_user.first_name if query.from_user else "User", BOT_NAME),
304
+ reply_markup=InlineKeyboardMarkup(buttons),
305
+ )
306
+ await query.answer()
307
+
308
+ @pbot.on_callback_query(filters.regex(r"^owner_"))
309
+ async def owner_callback(client, query: CallbackQuery):
310
+ data = query.data
311
+ if data == "owner_main":
312
+ from QueenNoxi import OWNER_IDS
313
+ owner_buttons = []
314
+ for oid in OWNER_IDS:
315
+ try:
316
+ user = await client.get_users(oid)
317
+ name = user.first_name
318
+ # Use username if available for better redirection
319
+ if user.username:
320
+ url = f"https://t.me/{user.username}"
321
+ else:
322
+ # Fallback to tg://user?id= which works in most modern clients if used correctly
323
+ url = f"tg://user?id={oid}"
324
+ owner_buttons.append([InlineKeyboardButton(text=f"👤 {name}", url=url)])
325
+ except Exception:
326
+ owner_buttons.append([InlineKeyboardButton(text=f"👤 Owner {oid}", url=f"tg://user?id={oid}")])
327
+
328
+ if not owner_buttons:
329
+ owner_buttons.append([InlineKeyboardButton(text="👤 Main Owner", url=f"tg://user?id={OWNER_ID}")])
330
+
331
+ owner_buttons.append([InlineKeyboardButton(text="◁ Back", callback_data="queennoxi_back")])
332
+
333
+ await query.message.edit_caption(
334
+ "✨ **Owner Selection Menu**\n\nChoose an owner profile to view:",
335
+ reply_markup=InlineKeyboardMarkup(owner_buttons),
336
+ )
337
+ await query.answer()
338
+
339
+
340
+ @pbot.on_callback_query(filters.regex(r"^Music_"))
341
+ async def music_callback(client, query: CallbackQuery):
342
+ data = query.data
343
+ NAV = [
344
+ InlineKeyboardButton("🏡", callback_data="queennoxi_back"),
345
+ InlineKeyboardButton("🛡️", callback_data="queennoxi_"),
346
+ InlineKeyboardButton("💳", callback_data="source_"),
347
+ InlineKeyboardButton("🖥️", callback_data="Main_help"),
348
+ ]
349
+ if data == "Music_":
350
+ await query.message.edit_caption(
351
+ "ʜᴇʀᴇ ɪꜱ ʜᴇʟᴘ ᴍᴇɴᴜ ꜰᴏʀ ᴍᴜꜱɪᴄ",
352
+ reply_markup=InlineKeyboardMarkup([
353
+ NAV,
354
+ [InlineKeyboardButton("⍟ ᴀᴅᴍɪɴ ⍟", callback_data="Music_admin"),
355
+ InlineKeyboardButton("⍟ ᴘʟᴀʏ ⍟", callback_data="Music_play")],
356
+ [InlineKeyboardButton("⍟ ʙᴏᴛ ⍟", callback_data="Music_bot"),
357
+ InlineKeyboardButton("⍟ ᴇxᴛʀᴀ ⍟", callback_data="Music_extra")],
358
+ [InlineKeyboardButton("• ʙᴀᴄᴋ •", callback_data="Main_help")],
359
+ ]),
360
+ )
361
+ elif data == "Music_admin":
362
+ await query.message.edit_caption(
363
+ "*» ᴀᴅᴍɪɴ ᴄᴏᴍᴍᴀɴᴅꜱ «*\n"
364
+ "/pause – ᴩᴀᴜsᴇ stream\n/resume – ʀᴇsᴜᴍᴇ\n"
365
+ "/skip – sᴋɪᴩ ᴄᴜʀʀᴇɴᴛ\n/end,/stop – sᴛᴏᴩ & ᴄʟᴇᴀʀ ǫᴜᴇᴜᴇ\n"
366
+ "/player – ɪɴᴛᴇʀᴀᴄᴛɪᴠᴇ ᴩᴀɴᴇʟ\n/queue – sʜᴏᴡ ǫᴜᴇᴜᴇ",
367
+ reply_markup=InlineKeyboardMarkup([[
368
+ InlineKeyboardButton("ʙᴀᴄᴋ", callback_data="Music_"),
369
+ InlineKeyboardButton("sᴜᴩᴩᴏʀᴛ", url=SUPPORT_CHAT_URL),
370
+ ]]),
371
+ )
372
+ elif data == "Music_play":
373
+ await query.message.edit_caption(
374
+ "*» ᴘʟᴀʏ ᴄᴏᴍᴍᴀɴᴅꜱ «*\n"
375
+ "/play, /vplay, /cplay – ᴩʟᴀʏ ᴀᴜᴅɪᴏ/ᴠɪᴅᴇᴏ\n"
376
+ "/playforce – ғᴏʀᴄᴇ ᴩʟᴀʏ\n"
377
+ "/channelplay [id/disable] – ᴄʜᴀɴɴᴇʟ ᴩʟᴀʏ",
378
+ reply_markup=InlineKeyboardMarkup([[
379
+ InlineKeyboardButton("• ʙᴀᴄᴋ •", callback_data="Music_"),
380
+ InlineKeyboardButton("sᴜᴩᴩᴏʀᴛ", url=SUPPORT_CHAT_URL),
381
+ ]]),
382
+ )
383
+ elif data == "Music_bot":
384
+ await query.message.edit_caption(
385
+ "*» ʙᴏᴛ ᴄᴏᴍᴍᴀɴᴅꜱ «*\n"
386
+ "/stats – ɢʟᴏʙᴀʟ sᴛᴀᴛs\n/sudolist – sᴜᴅᴏ ᴜsᴇʀs\n"
387
+ "/lyrics [ɴᴀᴍᴇ] – ꜰᴇᴛᴄʜ ʟʏʀɪᴄs\n/song [ɴᴀᴍᴇ/ᴜʀʟ] – ᴅᴏᴡᴀɴʟᴏᴀᴅ",
388
+ reply_markup=InlineKeyboardMarkup([[
389
+ InlineKeyboardButton("ʙᴀᴄᴋ", callback_data="Music_"),
390
+ InlineKeyboardButton("sᴜᴩᴩᴏʀᴛ", callback_data="queennoxi_support"),
391
+ ]]),
392
+ )
393
+ elif data == "Music_extra":
394
+ await query.message.edit_caption(
395
+ "*» ᴇxᴛʀᴀ ᴄᴏᴍᴍᴀɴᴅꜱ «*\n"
396
+ "/mstart – sᴛᴀʀᴛ ᴍᴜsɪᴄ ʙᴏᴛ\n/mhelp – ᴍᴜsɪᴄ ʜᴇʟᴩ\n",
397
+ reply_markup=InlineKeyboardMarkup([[
398
+ InlineKeyboardButton("• sᴜᴩᴩᴏʀᴛ •", url=SUPPORT_CHAT_URL)
399
+ ]])
400
+ )
401
+ await query.answer()
402
+
403
+ async def main():
404
+ import QueenNoxi
405
+ from aiohttp import ClientSession
406
+ QueenNoxi.aiohttpsession = ClientSession()
407
+
408
+ # --- Pyrogram Start ---
409
+ try:
410
+ await pbot.start()
411
+ LOGGER.info("[Pyrogram] Client started.")
412
+
413
+ # Export and save session string for persistence
414
+ try:
415
+ ss = await pbot.export_session_string()
416
+ save_session(BOT_ID, ss)
417
+ LOGGER.info("[Pyrogram] Saved session string for persistence.")
418
+ except Exception as e:
419
+ LOGGER.warning(f"Could not export/save session string: {e}")
420
+
421
+ await asyncio.sleep(2)
422
+ try:
423
+ if hasattr(pbot, 'delete_webhook'):
424
+ await pbot.delete_webhook(drop_pending_updates=True)
425
+ elif hasattr(pbot, 'delete_web_hook'):
426
+ await pbot.delete_web_hook(drop_pending_updates=True)
427
+ LOGGER.info("[Pyrogram] Webhook cleared and queue flushed.")
428
+ except FloodWait as e:
429
+ LOGGER.warning(f"[Pyrogram] FloodWait while clearing webhook: {e.value}s")
430
+ except FloodWait as e:
431
+ LOGGER.error(f"[Pyrogram] CRITICAL FloodWait on start: {e.value}s. Sleeping...")
432
+ await asyncio.sleep(e.value)
433
+ except Exception as e:
434
+ LOGGER.error(f"[Pyrogram] Failed to start client: {e}")
435
+
436
+ # --- Telethon Start ---
437
+ try:
438
+ await telethn.start(bot_token=TOKEN)
439
+ LOGGER.info("[Telethon] Client started.")
440
+ except TlFloodWait as e:
441
+ LOGGER.warning(f"[Telethon] FloodWait on start: {e.seconds}s.")
442
+ except Exception as e:
443
+ LOGGER.error(f"[Telethon] Failed to start client: {e}")
444
+
445
+ # --- Handoff ---
446
+ if pbot.is_connected:
447
+ me = await pbot.get_me()
448
+ LOGGER.info(f"[INFO] Bot running as @{me.username} | {me.first_name}")
449
+
450
+ if SUPPORT_CHAT and not SUPPORT_CHAT.startswith("http"):
451
+ try:
452
+ target = SUPPORT_CHAT if SUPPORT_CHAT.startswith("-100") else f"@{SUPPORT_CHAT}"
453
+ await pbot.send_photo(
454
+ target,
455
+ photo=START_IMG,
456
+ caption=f"✨ {BOT_NAME} ɪs ᴀʟɪᴠᴇ ʙᴀʙʏ.\n\n"
457
+ f"**ᴩʏᴛʜᴏɴ ᴠᴇʀsɪᴏɴ:** `{y()}`\n"
458
+ f"**ᴩʏʀᴏɢʀᴀᴍ ᴠᴇʀsɪᴏɴ:** `{pyrover}`\n",
459
+ reply_markup=InlineKeyboardMarkup([[
460
+ InlineKeyboardButton("➕ Aᴅᴅ ᴍᴇ ᴛᴏ ʏᴏᴜʀ ᴄʜᴀᴛ ➕", url=f"https://t.me/{me.username}?startgroup=true")
461
+ ]]),
462
+ )
463
+ except Exception as e:
464
+ LOGGER.warning(f"Could not announce to @{SUPPORT_CHAT}: {e}")
465
+ else:
466
+ LOGGER.error("[CRITICAL] Pyrogram client is NOT connected. Features will be disabled.")
467
+
468
+ LOGGER.info(f"Successfully loaded {len(ALL_MODULES)} modules.")
469
+ LOGGER.info("Bot is running. Press Ctrl+C to stop.")
470
+
471
+ await idle()
472
+
473
+ if pbot.is_connected:
474
+ await pbot.stop()
475
+ if telethn.is_connected():
476
+ await telethn.disconnect()
477
+ await QueenNoxi.aiohttpsession.close()
478
+
479
+ if __name__ == "__main__":
480
+ asyncio.run(main())
QueenNoxi/brain/data/brain.json ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:814980d5c0908529060fe6c7a74f95ab9482d4cd538f3ab050c5f454d832642d
3
+ size 12969712
QueenNoxi/brain/data/dictionary.json ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:bc1b3253f324cb5ce968c269252e7cf7d383026fbd50aac1964c850bf3d165c0
3
+ size 8714062
QueenNoxi/brain/data/persona.json ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:fa9fc7b188e0263b963b74e31a8e9c751f45282cb87a17e15cfbcbb58830f32b
3
+ size 869
QueenNoxi/brain/data/phonetic_map.json ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:b9d3dbbadbfca3777d5f8e7269c1e691a594bb91473c324e572801321534655e
3
+ size 21295805
QueenNoxi/brain/data/slang.json ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:8bc513452216e7f8a6c13324f09734de6d0973d610fc83b8aae52e7351898aa4
3
+ size 1248
QueenNoxi/brain/data/users.json ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:99245b13a40b590c2903a1b9ace0a07e55166ce6179a72fb4e6b6023c161c474
3
+ size 26626
QueenNoxi/brain/database/anon.json ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:e7cb05c9ba971cf4cfe53cda4d0bd302d1c1254b4f2890ccd12828d09c850aa9
3
+ size 1456
QueenNoxi/brain/index.js ADDED
@@ -0,0 +1,280 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const brain = require('./src/brain');
2
+ const processor = require('./src/processor');
3
+ const UserMemory = require('./src/memory');
4
+ const GroupSession = require('./src/group');
5
+ const nlu = require('./src/nlu');
6
+ const ContextWindow = require('./src/context');
7
+ const Dictionary = require('./src/dictionary');
8
+ const path = require('path');
9
+
10
+ class SinhalaHumanoid {
11
+ /**
12
+ * @param {Object} options
13
+ * @param {string} options.style - Chat style: 'chill' | 'professional' | 'sexy' | 'hot' | 'auto'
14
+ * @param {string} options.name - Bot's name (e.g. 'Kasun')
15
+ * @param {string} options.birthday - Bot's birthday (e.g. '1998-05-15')
16
+ * @param {string} options.residence - Bot's home city (e.g. 'Colombo')
17
+ * @param {string} options.job - Bot's job (e.g. 'Web Developer')
18
+ * @param {string} options.gender - Bot's gender ('Male'|'Female'|'Other')
19
+ * @param {string} options.usersDb - Path to users.json (optional)
20
+ */
21
+ constructor(options = {}) {
22
+ const personaFields = {};
23
+ if (options.name) personaFields.name = options.name;
24
+ if (options.birthday) personaFields.birthday = options.birthday;
25
+ if (options.residence) personaFields.residence = options.residence;
26
+ if (options.job) personaFields.job = options.job;
27
+ if (options.gender) personaFields.gender = options.gender;
28
+ if (options.style) personaFields.style = options.style;
29
+ if (options.persona) Object.assign(personaFields, options.persona);
30
+
31
+ if (Object.keys(personaFields).length > 0) brain.setPersona(personaFields);
32
+
33
+ this.autoStyle = (options.style === 'auto');
34
+
35
+ // Priority: options.mongoUri -> process.env.CHATDB_URL -> options.usersDb -> default
36
+ this.mongoUri = options.mongoUri || process.env.CHATDB_URL || null;
37
+ this.usersDb = options.usersDb || path.join(process.cwd(), 'database');
38
+
39
+ const memoryPath = this.mongoUri || this.usersDb;
40
+ this.userMemory = new UserMemory(memoryPath);
41
+
42
+ const dictPath = options.dictionaryDb || path.join(__dirname, 'data', 'dictionary.json');
43
+ this.dictionary = new Dictionary(dictPath);
44
+
45
+ this.groups = {};
46
+ this.contexts = {};
47
+ }
48
+
49
+
50
+ async init() {
51
+ await this.userMemory.init();
52
+ return this;
53
+ }
54
+
55
+ _getContext(userId) {
56
+ if (!this.contexts[userId]) this.contexts[userId] = new ContextWindow();
57
+ return this.contexts[userId];
58
+ }
59
+
60
+ _buildResponse(message, userId, ctx) {
61
+ const user = this.userMemory.getUser(userId);
62
+ const analysis = nlu.analyze(message);
63
+ ctx.addUserTurn(message, analysis);
64
+
65
+ const style = this.autoStyle
66
+ ? analysis.detectedStyle
67
+ : ((brain.persona && brain.persona.style) || 'chill');
68
+
69
+ const prevStyle = brain.persona.style;
70
+ if (this.autoStyle) brain.setPersona({ style });
71
+
72
+ const extracted = this.userMemory.extractFromMessage(userId, message);
73
+ if (extracted.name) {
74
+ const resp = `Suba na ${user.name}! Oyatama ingathin ganna.`;
75
+ ctx.addBotTurn(resp);
76
+ return resp;
77
+ }
78
+
79
+ const memQ = this.userMemory.memoryQuery(userId, message);
80
+ if (memQ) { ctx.addBotTurn(memQ); return memQ; }
81
+
82
+ const dictWord = this.dictionary.isDictionaryQuery(message);
83
+ if (dictWord) {
84
+ const entry = this.dictionary.lookup(dictWord);
85
+ if (entry) {
86
+ const resp = this.dictionary.formatResponse(dictWord, entry);
87
+ if (resp) { ctx.addBotTurn(resp); return resp; }
88
+ } else {
89
+ const notFound = {
90
+ chill: `Aiyo sry machan, "${dictWord}" kiyana eka gana mama danne na. Aluth vacanayak da?`,
91
+ professional: `Sathutu wenawa kiyanna, habai "${dictWord}" kiyana vacanaya mage dictionary eke na.`,
92
+ sexy: `hmmm "${dictWord}"... eka amuthu vacanayak ne? mama danne na eka gana...`,
93
+ hot: `"${dictWord}"? eka gana passe kiyannam. dan kiyanna ba.`
94
+ };
95
+ const resp = notFound[style] || notFound['chill'];
96
+ ctx.addBotTurn(resp);
97
+ return resp;
98
+ }
99
+ }
100
+
101
+ if (analysis.isGreeting && user.name) {
102
+ const timeSinceSeen = user.lastSeen ? Date.now() - user.lastSeen : Infinity;
103
+ if (timeSinceSeen > 5 * 60 * 1000) {
104
+ const greet = this.userMemory.greet(userId);
105
+ if (greet) { ctx.addBotTurn(greet); return greet; }
106
+ }
107
+ }
108
+
109
+ if (analysis.isFarewell) {
110
+ const byes = {
111
+ chill: ['ok yanawa machan, hodatama yanawa', 'bye bye, posuwa enna', 'hodatama yanawa, bye!'],
112
+ professional: ['Bye! Hoda dine ekak waewa.'],
113
+ sexy: ['aww bye baby, miss karannawa', 'bye, jld ohh laa'],
114
+ hot: ['ok bye, jld enna ha', 'missing already']
115
+ };
116
+ const pool = byes[style] || byes['chill'];
117
+ const resp = pool[Math.floor(Math.random() * pool.length)];
118
+ ctx.addBotTurn(resp);
119
+ return resp;
120
+ }
121
+
122
+ const tonePrefix = nlu.toneReaction(analysis.tone);
123
+ const parts = [];
124
+
125
+ if (analysis.isLong) {
126
+ for (const topic of analysis.topics.slice(0, 2)) {
127
+ const topicResp = nlu.getTopicResponse(topic, style);
128
+ if (topicResp) parts.push(topicResp);
129
+ }
130
+ if (!parts.length) {
131
+ for (const sentence of analysis.sentences.slice(0, 2)) {
132
+ const r = brain.respond(sentence, { user });
133
+ if (r) parts.push(brain.fillTemplate(r, { user }));
134
+ }
135
+ }
136
+ }
137
+
138
+ if (!parts.length) {
139
+ for (const sentence of analysis.sentences) {
140
+ // 1. Brain match (most specific knowledge)
141
+ const r = brain.respond(sentence, { user });
142
+ if (r) {
143
+ parts.push(brain.fillTemplate(r, { user }));
144
+ break;
145
+ }
146
+
147
+ // 2. Fallback to general topics
148
+ const topicHit = analysis.topics.find(t => nlu.getTopicResponse(t, style));
149
+ if (topicHit) {
150
+ parts.push(nlu.getTopicResponse(topicHit, style));
151
+ break;
152
+ }
153
+ }
154
+ }
155
+
156
+ if (ctx.isAnsweringBotQuestion() && !analysis.isQuestion && parts.length) {
157
+ const acks = ['ahh ok ok', 'oo arageina', 'ah supa', 'ok ok gotcha'];
158
+ parts.unshift(acks[Math.floor(Math.random() * acks.length)]);
159
+ }
160
+
161
+ let finalParts = parts.filter(Boolean);
162
+ if (tonePrefix && finalParts.length) {
163
+ finalParts = [tonePrefix, ...finalParts];
164
+ }
165
+
166
+ if (!finalParts.length) {
167
+ finalParts = [brain.getFallback()];
168
+ }
169
+
170
+ if (Math.random() < 0.4 && !analysis.isQuestion) {
171
+ const followUps = {
172
+ chill: ['ekath?', 'oya kohomada?', 'moko kiyanne?', 'oya thiyenawada?'],
173
+ professional: ['Oba kohomada?', 'Meken help one da?'],
174
+ sexy: ['oya mokada karanne tawa?', 'oya miss una neda?'],
175
+ hot: ['oya kiyanne?', 'enna enna']
176
+ };
177
+ const pool = followUps[style] || followUps['chill'];
178
+ finalParts.push(pool[Math.floor(Math.random() * pool.length)]);
179
+ }
180
+
181
+ const response = finalParts.join(' ');
182
+ ctx.addBotTurn(response);
183
+ if (this.autoStyle) brain.setPersona({ style: prevStyle });
184
+ return response;
185
+ }
186
+
187
+ getResponse(message, userId = 'anon') {
188
+ this.userMemory.addHistory(userId, message, 'user');
189
+ const ctx = this._getContext(userId);
190
+ const response = this._buildResponse(message, userId, ctx);
191
+ this.userMemory.addHistory(userId, response, 'bot');
192
+ return response;
193
+ }
194
+
195
+ async getGroupResponse(message, userId, groupId = 'default') {
196
+ if (!this.groups[groupId]) {
197
+ this.groups[groupId] = new GroupSession({
198
+ groupId,
199
+ memory: this.userMemory,
200
+ mongoUri: this.mongoUri,
201
+ usersDb: this.usersDb
202
+ });
203
+ await this.groups[groupId].init();
204
+ }
205
+
206
+ const group = this.groups[groupId];
207
+ await group.addMessage(userId, message, userId);
208
+
209
+ const memQuery = this.userMemory.memoryQuery(userId, message);
210
+ if (memQuery) {
211
+ await group.addMessage('bot', memQuery, 'bot');
212
+ return memQuery;
213
+ }
214
+
215
+ this.userMemory.extractFromMessage(userId, message);
216
+
217
+ const botName = (brain.persona && brain.persona.name) || 'Kasun';
218
+ const analysis = nlu.analyze(message);
219
+ const addressed = message.toLowerCase().includes(botName.toLowerCase())
220
+ || message.toLowerCase().includes('bot')
221
+ || analysis.isGreeting;
222
+ const randomReply = Math.random() < 0.3;
223
+
224
+ if (!addressed && !randomReply) return null;
225
+
226
+ const ctx = this._getContext(`${groupId}:${userId}`);
227
+ const user = this.userMemory.getUser(userId);
228
+ const response = this._buildResponse(message, userId, ctx);
229
+
230
+ await group.addMessage('bot', response, 'bot');
231
+ return response;
232
+ }
233
+
234
+ /**
235
+ * Permanent Learning: Add a new pattern to the brain and save to disk.
236
+ */
237
+ async learn(input, response, style = 'chill') {
238
+ const prevStyle = (brain.persona && brain.persona.style) || 'chill';
239
+ brain.setPersona({ style });
240
+ brain.learn(input, response, true);
241
+ brain.setPersona({ style: prevStyle });
242
+ return true;
243
+ }
244
+
245
+ trainFromGroup(groupId) {
246
+ const group = this.groups[groupId];
247
+ if (group) group.autoTrainFromGroup(brain);
248
+ }
249
+
250
+ trainData(data, style) {
251
+ const Trainer = require('./src/trainer');
252
+ return Trainer.trainData(data, style);
253
+ }
254
+
255
+ autoTrain(history) {
256
+ for (let i = 0; i < history.length - 1; i++) {
257
+ const curr = history[i], next = history[i + 1];
258
+ if (curr.sender !== next.sender) brain.learn(curr.text, next.text);
259
+ }
260
+ }
261
+
262
+ setStyle(style) { brain.setPersona({ style }); }
263
+ setPersona(details) { brain.setPersona(details); }
264
+ getUser(userId) { return this.userMemory.getUser(userId); }
265
+ setUserData(u, f, v) { this.userMemory.setUserData(u, f, v); }
266
+ setUserFact(u, k, v) { this.userMemory.setFact(u, k, v); }
267
+ getUserFact(u, k) { return this.userMemory.getFact(u, k); }
268
+ resetContext(userId) { if (this.contexts[userId]) this.contexts[userId].reset(); }
269
+ analyze(message) { return nlu.analyze(message); }
270
+ detectLanguage(text) {
271
+ return /[\u0D80-\u0DFF]/.test(text) ? 'sinhala' : 'singlish';
272
+ }
273
+ }
274
+
275
+ const createBot = (options) => new SinhalaHumanoid(options);
276
+
277
+ module.exports = {
278
+ SinhalaHumanoid,
279
+ createBot
280
+ };
QueenNoxi/brain/query.js ADDED
@@ -0,0 +1,62 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const { createBot } = require('./index.js');
2
+ const path = require('path');
3
+ const fs = require('fs');
4
+
5
+ async function main() {
6
+ const args = process.argv.slice(2);
7
+ const command = args[0] || 'query';
8
+
9
+ try {
10
+ const databasePath = path.join(__dirname, 'database');
11
+ if (!fs.existsSync(databasePath)) {
12
+ fs.mkdirSync(databasePath, { recursive: true });
13
+ }
14
+
15
+ const bot = createBot({
16
+ name: 'Queen Noxi',
17
+ style: 'auto',
18
+ birthday: '2004-01-21',
19
+ gender: 'Female',
20
+ residence: 'Sri Lanka',
21
+ job: 'Student',
22
+ mongoUri: process.env.CHATDB_URL || null,
23
+ usersDb: databasePath,
24
+ dictionaryDb: path.join(__dirname, 'data', 'dictionary.json')
25
+ });
26
+
27
+ await bot.init();
28
+
29
+ if (command === 'learn') {
30
+ const input = args[1];
31
+ const response = args[2];
32
+ if (!input || !response) {
33
+ process.stderr.write('Usage: learn <input> <response>');
34
+ process.exit(1);
35
+ }
36
+ await bot.learn(input, response);
37
+ process.stdout.write('LEARNED');
38
+ } else {
39
+ // query command
40
+ const text = args[1];
41
+ const userId = args[2] || 'anon';
42
+ const groupId = args[3] || null;
43
+
44
+ if (!text) {
45
+ process.exit(1);
46
+ }
47
+
48
+ let response;
49
+ if (groupId) {
50
+ response = await bot.getGroupResponse(text, userId, groupId);
51
+ } else {
52
+ response = bot.getResponse(text, userId);
53
+ }
54
+ process.stdout.write(response || '');
55
+ }
56
+ } catch (err) {
57
+ process.stderr.write(err.message || String(err));
58
+ process.exit(1);
59
+ }
60
+ }
61
+
62
+ main();
QueenNoxi/brain/src/brain.js ADDED
@@ -0,0 +1,150 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const processor = require('./processor');
4
+
5
+ class Brain {
6
+ constructor() {
7
+ this.memory = {};
8
+ this.persona = {};
9
+ this.loadData();
10
+ }
11
+
12
+ loadData() {
13
+ try {
14
+ const brainPath = path.join(__dirname, '..', 'data', 'brain.json');
15
+ if (fs.existsSync(brainPath)) this.memory = JSON.parse(fs.readFileSync(brainPath, 'utf8'));
16
+
17
+ const personaPath = path.join(__dirname, '..', 'data', 'persona.json');
18
+ if (fs.existsSync(personaPath)) this.persona = JSON.parse(fs.readFileSync(personaPath, 'utf8'));
19
+ } catch (e) { console.error('Error loading brain data:', e); }
20
+ }
21
+
22
+ saveData() {
23
+ try {
24
+ const brainPath = path.join(__dirname, '..', 'data', 'brain.json');
25
+ fs.writeFileSync(brainPath, JSON.stringify(this.memory, null, 2));
26
+ } catch (e) { console.error('Error saving brain data:', e); }
27
+ }
28
+
29
+ setPersona(data) {
30
+ this.persona = { ...this.persona, ...data };
31
+ }
32
+
33
+ /**
34
+ * Core respond logic. Accepts optional context (userId, userMemory, groupSession)
35
+ */
36
+ respond(inputText, context = {}) {
37
+ const processedInput = processor.process(inputText);
38
+ if (!processedInput) return this.getFallback();
39
+
40
+ // 1. Direct match
41
+ if (this.memory[processedInput]) {
42
+ return this.fillTemplate(this.pickResponse(this.memory[processedInput]), context);
43
+ }
44
+
45
+ // 2. Persona questions (Identity should take precedence over fuzzy chat)
46
+ const personaResp = this.checkPersonaQuestions(processedInput, inputText);
47
+ if (personaResp) return this.fillTemplate(personaResp, context);
48
+
49
+ // 3. Fuzzy match
50
+ let bestMatch = null;
51
+ let highestScore = 0;
52
+ for (const [pattern, data] of Object.entries(this.memory)) {
53
+ const score = processor.calculateSimilarity(processedInput, pattern);
54
+ if (score > highestScore && score > 0.65) {
55
+ highestScore = score;
56
+ bestMatch = data;
57
+ }
58
+ }
59
+ if (bestMatch) {
60
+ return this.fillTemplate(this.pickResponse(bestMatch), context);
61
+ }
62
+
63
+ return this.getFallback();
64
+ }
65
+
66
+ pickResponse(data) {
67
+ const style = this.persona.style || 'chill';
68
+ const pool = data[style] || data['chill'] || data['default'] || data;
69
+ if (Array.isArray(pool)) return pool[Math.floor(Math.random() * pool.length)];
70
+ return String(pool);
71
+ }
72
+
73
+ /**
74
+ * Fill {name}, {job}, {birthday}, {age}, {residence}, {user_name} etc.
75
+ */
76
+ fillTemplate(text, context = {}) {
77
+ if (!text) return this.getFallback();
78
+ const p = this.persona;
79
+ const u = context.user || {};
80
+ const now = new Date();
81
+ const birthYear = p.birthday ? new Date(p.birthday).getFullYear() : null;
82
+
83
+ return text
84
+ .replace(/\{name\}/g, p.name || 'Kasun')
85
+ .replace(/\{birthday\}/g, p.birthday || '?')
86
+ .replace(/\{job\}/g, p.job || 'web dev')
87
+ .replace(/\{residence\}/g, p.residence || 'Colombo')
88
+ .replace(/\{age\}/g, birthYear ? (now.getFullYear() - birthYear) : '?')
89
+ .replace(/\{user_name\}/g, u.name || 'machan')
90
+ .replace(/\{user_location\}/g, u.location || 'koheda?')
91
+ .replace(/\{user_job\}/g, u.job || '?');
92
+ }
93
+
94
+ checkPersonaQuestions(processedInput, rawInput = '') {
95
+ const text = processedInput.toLowerCase();
96
+ const raw = rawInput.toLowerCase();
97
+
98
+ // Helper: check either raw or processed text
99
+ const has = (...kws) => kws.some(k => text.includes(k) || raw.includes(k));
100
+
101
+ // Priority order matters — check most specific first
102
+ if (has('oyage gana', 'about you', 'introduce yourself', 'tell me about'))
103
+ return 'mama {name}. {job} kenek. {residence} wala inne.';
104
+
105
+ // Name — must have name-specific trigger word
106
+ if (has('nama', 'name', 'kauda oya', 'oyage kauda', 'oyage nam', 'oyage name'))
107
+ return 'mage nama {name} machan.';
108
+
109
+ // Age / Birthday
110
+ if (has('bday', 'birthday', 'ipaduna', 'wayasa', 'age'))
111
+ return 'mama {age} wiye inne. bday eka {birthday}.';
112
+
113
+ // Residence — must be explicit about location
114
+ if (has('koheda inne', 'live in', 'koheda', 'residence', 'wasi'))
115
+ return 'mama {residence} wala inne machan.';
116
+
117
+ // Job — only if there's a clear job-asking keyword, NOT general "karanne"
118
+ if (has('oyage job', 'what is your job', 'weda karanne mokakda', 'job eka', 'profession', 'oyage weda'))
119
+ return 'mama {job} kenek machan.';
120
+
121
+ return null;
122
+ }
123
+
124
+ getFallback() {
125
+ const style = this.persona.style || 'chill';
126
+ const fallbacks = (this.persona.fallbacks && this.persona.fallbacks[style])
127
+ || (this.persona.fallbacks && this.persona.fallbacks['chill'])
128
+ || ['moko kiyanne?', 'poddak hitapan', 'kiyanna aney'];
129
+ return fallbacks[Math.floor(Math.random() * fallbacks.length)];
130
+ }
131
+
132
+ learn(input, response, shouldSave = true) {
133
+ const processedInput = processor.process(input);
134
+ if (!processedInput || !response) return;
135
+
136
+ const style = this.persona.style || 'chill';
137
+ if (!this.memory[processedInput]) {
138
+ this.memory[processedInput] = {};
139
+ }
140
+ if (!this.memory[processedInput][style]) {
141
+ this.memory[processedInput][style] = [];
142
+ }
143
+ if (!this.memory[processedInput][style].includes(response)) {
144
+ this.memory[processedInput][style].push(response);
145
+ }
146
+ if (shouldSave) this.saveData();
147
+ }
148
+ }
149
+
150
+ module.exports = new Brain();
QueenNoxi/brain/src/context.js ADDED
@@ -0,0 +1,93 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * context.js — Conversation Context Window
3
+ *
4
+ * Tracks:
5
+ * - Recent turns (last N messages from user + bot)
6
+ * - Active topics (what is being discussed)
7
+ * - Active intents
8
+ * - Emotional trend
9
+ */
10
+
11
+ class ContextWindow {
12
+ constructor(maxTurns = 10) {
13
+ this.maxTurns = maxTurns;
14
+ this.turns = []; // { role: 'user'|'bot', text, analysis?, ts }
15
+ this.activeTopics = []; // most recent topics discussed
16
+ this.activeIntents = []; // most recent intents
17
+ this.tone = 'neutral';
18
+ this.waitingForAnswer = null; // if bot asked a question, track what it asked
19
+ }
20
+
21
+ /**
22
+ * Add a user turn (with NLU analysis attached)
23
+ */
24
+ addUserTurn(text, analysis) {
25
+ this.turns.push({ role: 'user', text, analysis, ts: Date.now() });
26
+ if (this.turns.length > this.maxTurns * 2) this.turns.shift();
27
+
28
+ if (analysis) {
29
+ // Update active topics (prepend newest, keep last 3)
30
+ this.activeTopics = [...new Set([...analysis.topics, ...this.activeTopics])].slice(0, 3);
31
+ this.activeIntents = analysis.intents;
32
+ this.tone = analysis.tone;
33
+ }
34
+ }
35
+
36
+ /**
37
+ * Add a bot turn
38
+ */
39
+ addBotTurn(text) {
40
+ this.turns.push({ role: 'bot', text, ts: Date.now() });
41
+ if (this.turns.length > this.maxTurns * 2) this.turns.shift();
42
+
43
+ // Track if bot asked a question (ends in ?)
44
+ if (text && text.includes('?')) {
45
+ this.waitingForAnswer = text;
46
+ } else {
47
+ this.waitingForAnswer = null;
48
+ }
49
+ }
50
+
51
+ /**
52
+ * Get recent user messages (last N)
53
+ */
54
+ getRecentUserMessages(n = 3) {
55
+ return this.turns.filter(t => t.role === 'user').slice(-n).map(t => t.text);
56
+ }
57
+
58
+ /**
59
+ * Get last bot message
60
+ */
61
+ getLastBotMessage() {
62
+ const botTurns = this.turns.filter(t => t.role === 'bot');
63
+ return botTurns.length ? botTurns[botTurns.length - 1].text : null;
64
+ }
65
+
66
+ /**
67
+ * Check if current message is likely a follow-up to an active topic
68
+ */
69
+ isFollowUp(analysis) {
70
+ if (!analysis || !this.activeTopics.length) return false;
71
+ return analysis.topics.some(t => this.activeTopics.includes(t));
72
+ }
73
+
74
+ /**
75
+ * Check if the bot recently asked a question and is waiting for an answer
76
+ */
77
+ isAnsweringBotQuestion() {
78
+ return !!this.waitingForAnswer;
79
+ }
80
+
81
+ /**
82
+ * Wipe context (for fresh conversation)
83
+ */
84
+ reset() {
85
+ this.turns = [];
86
+ this.activeTopics = [];
87
+ this.activeIntents = [];
88
+ this.tone = 'neutral';
89
+ this.waitingForAnswer = null;
90
+ }
91
+ }
92
+
93
+ module.exports = ContextWindow;
QueenNoxi/brain/src/dictionary.js ADDED
@@ -0,0 +1,110 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const processor = require('./processor');
4
+
5
+ class Dictionary {
6
+ constructor(dbPath) {
7
+ this.dbPath = dbPath || path.join(__dirname, '..', 'data', 'dictionary.json');
8
+ this.data = {};
9
+ this.load();
10
+ }
11
+
12
+ load() {
13
+ try {
14
+ if (fs.existsSync(this.dbPath)) {
15
+ this.data = JSON.parse(fs.readFileSync(this.dbPath, 'utf8'));
16
+ // Create a separate index for normalized keys
17
+ this.normalizedIndex = {};
18
+ for (const key of Object.keys(this.data)) {
19
+ this.normalizedIndex[processor.process(key)] = key;
20
+ }
21
+ }
22
+ } catch (e) {
23
+ console.error('Dictionary load error:', e);
24
+ this.data = {};
25
+ }
26
+ }
27
+
28
+ /**
29
+ * Look up a word in the dictionary.
30
+ * Supports Sinhala script and Singlish (via internal normalization).
31
+ */
32
+ lookup(word) {
33
+ if (!word) return null;
34
+
35
+ const lower = word.toLowerCase().trim();
36
+ const normalized = processor.process(word);
37
+
38
+ // 1. Direct match (exact)
39
+ if (this.data[lower]) return this.data[lower];
40
+ if (this.data[normalized]) return this.data[normalized];
41
+
42
+ // 2. Normalized index match (e.g. කොම්පියුටර් -> kompiutar -> computer)
43
+ if (this.normalizedIndex[normalized]) {
44
+ return this.data[this.normalizedIndex[normalized]];
45
+ }
46
+
47
+ // 3. Fuzzy search for similar words
48
+ let bestMatch = null;
49
+ let highestScore = 0;
50
+
51
+ for (const [key, value] of Object.entries(this.data)) {
52
+ const score = processor.calculateSimilarity(normalized, processor.process(key));
53
+ if (score > highestScore && score > 0.8) {
54
+ highestScore = score;
55
+ bestMatch = value;
56
+ }
57
+ }
58
+
59
+ return bestMatch;
60
+ }
61
+
62
+ /**
63
+ * Check if the message is a "what is X" type question
64
+ */
65
+ isDictionaryQuery(text) {
66
+ const lower = text.toLowerCase();
67
+ // Common dictionary query patterns
68
+ const patterns = [
69
+ /mokakda (.*) kiyanne/i,
70
+ /what is (.*)/i,
71
+ /meaning of (.*)/i,
72
+ /arthaya (.*)/i,
73
+ /(.*) kiyanne mokakda/i,
74
+ /(.*) meaning/i
75
+ ];
76
+
77
+ for (const pat of patterns) {
78
+ const match = lower.match(pat);
79
+ if (match) {
80
+ // Clean punctuation from the extracted word
81
+ return match[1].trim().replace(/[?!,.]$/, '');
82
+ }
83
+ }
84
+ return null;
85
+ }
86
+
87
+ /**
88
+ * Format a dictionary entry into a human-like response
89
+ */
90
+ formatResponse(word, entry) {
91
+ if (!entry) return null;
92
+
93
+ if (typeof entry === 'string') {
94
+ return `"${word}" kiyanne: ${entry}`;
95
+ }
96
+
97
+ let resp = `"${word}" gana mama hoyala beluva... `;
98
+
99
+ if (entry.sin) resp += `Sinhala: ${entry.sin}. `;
100
+ if (entry.en) resp += `English: ${entry.en}. `;
101
+ if (entry.def) resp += `Definition: ${entry.def}. `;
102
+ if (entry.synonyms && entry.synonyms.length) {
103
+ resp += `Synonyms: ${entry.synonyms.slice(0, 3).join(', ')}.`;
104
+ }
105
+
106
+ return resp;
107
+ }
108
+ }
109
+
110
+ module.exports = Dictionary;
QueenNoxi/brain/src/group.js ADDED
@@ -0,0 +1,115 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const UserMemory = require('./memory');
2
+ const StorageAdapter = require('./storage');
3
+ const path = require('path');
4
+
5
+ class GroupSession {
6
+ constructor(options = {}) {
7
+ this.groupId = options.groupId || 'default_group';
8
+ this.memory = options.memory; // global UserMemory instance for user lookup
9
+ this.members = new Set();
10
+ this.recentMessages = [];
11
+ this.maxHistory = options.maxHistory || 50;
12
+
13
+ const storagePath = options.mongoUri || (options.usersDb ? path.join(options.usersDb, `${this.groupId}.json`) : path.join(process.cwd(), 'database', `${this.groupId}.json`));
14
+ this.storage = new StorageAdapter(storagePath, 'groups');
15
+ }
16
+
17
+ async init() {
18
+ await this.storage.init();
19
+ const data = await this.storage.loadAll();
20
+ this.recentMessages = data.history || [];
21
+ if (data.members) this.members = new Set(data.members);
22
+ }
23
+
24
+ async save() {
25
+ await this.storage.saveAll({
26
+ groupId: this.groupId,
27
+ members: [...this.members],
28
+ history: this.recentMessages,
29
+ lastUpdate: Date.now()
30
+ });
31
+ }
32
+
33
+ /**
34
+ * Register a member in this group
35
+ */
36
+ addMember(userId) {
37
+ this.members.add(userId);
38
+ }
39
+
40
+ /**
41
+ * Record a message in group context
42
+ */
43
+ async addMessage(userId, text, from = null) {
44
+ this.addMember(userId);
45
+ const sender = from || userId;
46
+ const msg = { from: sender, text, ts: Date.now() };
47
+ this.recentMessages.push(msg);
48
+ if (this.recentMessages.length > this.maxHistory) {
49
+ this.recentMessages.shift();
50
+ }
51
+
52
+ // Also update the global user memory if they are in this group
53
+ if (this.memory && sender !== 'bot') {
54
+ await this.memory.addHistory(sender, text, 'user');
55
+ }
56
+
57
+ await this.save();
58
+ return msg;
59
+ }
60
+
61
+ /**
62
+ * Get recent group conversation (last N messages)
63
+ */
64
+ getContext(n = 10) {
65
+ return this.recentMessages.slice(-n);
66
+ }
67
+
68
+ /**
69
+ * Check if a user was recently active in this group
70
+ */
71
+ isRecentlyActive(userId, withinMs = 300000) { // 5 min
72
+ const now = Date.now();
73
+ for (let i = this.recentMessages.length - 1; i >= 0; i--) {
74
+ const m = this.recentMessages[i];
75
+ if (m.userId === userId && now - m.ts < withinMs) return true;
76
+ }
77
+ return false;
78
+ }
79
+
80
+ /**
81
+ * Get all members the bot has seen in this group
82
+ */
83
+ getMembers() {
84
+ return [...this.members];
85
+ }
86
+
87
+ /**
88
+ * Get the last message from a specific user
89
+ */
90
+ getLastMessageFrom(userId) {
91
+ for (let i = this.recentMessages.length - 1; i >= 0; i--) {
92
+ if (this.recentMessages[i].userId === userId) {
93
+ return this.recentMessages[i];
94
+ }
95
+ }
96
+ return null;
97
+ }
98
+
99
+ /**
100
+ * Auto-train the brain from recent group messages
101
+ * (pass in a brain instance)
102
+ */
103
+ autoTrainFromGroup(brain) {
104
+ const msgs = this.recentMessages;
105
+ for (let i = 0; i < msgs.length - 1; i++) {
106
+ const curr = msgs[i];
107
+ const next = msgs[i + 1];
108
+ if (curr.userId !== next.userId) {
109
+ brain.learn(curr.text, next.text);
110
+ }
111
+ }
112
+ }
113
+ }
114
+
115
+ module.exports = GroupSession;
QueenNoxi/brain/src/memory.js ADDED
@@ -0,0 +1,204 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const StorageAdapter = require('./storage');
2
+
3
+ class UserMemory {
4
+ constructor(dbPath) {
5
+ this.storage = new StorageAdapter(dbPath, 'users');
6
+ this.users = {};
7
+ }
8
+
9
+ async init() {
10
+ await this.storage.init();
11
+ this.users = await this.storage.loadAll();
12
+ }
13
+
14
+ async save() {
15
+ await this.storage.saveAll(this.users);
16
+ }
17
+
18
+ /**
19
+ * Get a user's memory object. Creates one if it doesn't exist.
20
+ * @param {string} userId
21
+ */
22
+ getUser(userId) {
23
+ if (!this.users[userId]) {
24
+ this.users[userId] = {
25
+ id: userId,
26
+ name: null,
27
+ age: null,
28
+ location: null,
29
+ job: null,
30
+ gender: null,
31
+ nickname: null,
32
+ facts: {}, // Free-form key-value facts
33
+ history: [], // Last N messages
34
+ lastSeen: null,
35
+ seenCount: 0
36
+ };
37
+ } else {
38
+ // Safety check for legacy or malformed files
39
+ if (!this.users[userId].facts) this.users[userId].facts = {};
40
+ if (!this.users[userId].history) this.users[userId].history = [];
41
+ }
42
+ return this.users[userId];
43
+ }
44
+
45
+ /**
46
+ * Set a specific field on a user
47
+ */
48
+ async setUserData(userId, field, value) {
49
+ const user = this.getUser(userId);
50
+ user[field] = value;
51
+ await this.save();
52
+ }
53
+
54
+ /**
55
+ * Set a free-form fact about a user
56
+ */
57
+ async setFact(userId, key, value) {
58
+ const user = this.getUser(userId);
59
+ user.facts[key] = value;
60
+ await this.save();
61
+ }
62
+
63
+ /**
64
+ * Get a fact about a user
65
+ */
66
+ getFact(userId, key) {
67
+ const user = this.getUser(userId);
68
+ return user.facts[key] || null;
69
+ }
70
+
71
+ /**
72
+ * Record a message in the user's history (keep last 30)
73
+ */
74
+ async addHistory(userId, text, from = 'user') {
75
+ const user = this.getUser(userId);
76
+ user.history.push({ text, from, ts: Date.now() });
77
+ if (user.history.length > 30) user.history.shift();
78
+ user.lastSeen = Date.now();
79
+ user.seenCount = (user.seenCount || 0) + 1;
80
+ await this.save();
81
+ }
82
+
83
+ /**
84
+ * Auto-extract user info from message text (name, location, job, age)
85
+ * Returns any fields that were updated
86
+ */
87
+ extractFromMessage(userId, text) {
88
+ const lower = text.toLowerCase().trim();
89
+ const extracted = {};
90
+
91
+ // BLACKLIST: common Singlish words that look like a "match" but are NOT names/locations
92
+ const blacklistNames = new Set(['mama', 'machan', 'godak', 'poddak', 'hodai', 'supa', 'maru', 'ada',
93
+ 'kalin', 'dan', 'eka', 'api', 'ado', 'ayye', 'aiyo', 'ekath', 'kohomada', 'tani', 'ganna',
94
+ 'karanna', 'kiyanna', 'ne', 'da', 'ba', 'ok', 'lol', 'haha', 'bye', 'hi', 'hai', 'hello',
95
+ 'sorry', 'thanks', 'library', 'school', 'office', 'bus', 'train', 'gedara', 'weda', 'giia',
96
+ 'apahu', 'pass', 'fail', 'suba', 'subha', 'aiya', 'akka', 'nangi', 'malli', 'amma', 'thaththa']);
97
+
98
+ // Name: ONLY on explicit "my name is X" or "mage nama X" (not bare "mama X")
99
+ const nameMatch = lower.match(/(?:my name is|mage nama|namayi)\s+([a-z]{2,20})(?:\s|$|,|\.)/i);
100
+ if (nameMatch && !blacklistNames.has(nameMatch[1].toLowerCase())) {
101
+ const name = this._capitalize(nameMatch[1]);
102
+ this.setUserData(userId, 'name', name); // Note: this is async but we don't await here to keep it non-blocking in NLU
103
+ extracted.name = name;
104
+ }
105
+
106
+ // Age: "mage wayasa 22", "age 22", "i am 22 years"
107
+ const ageMatch = lower.match(/(?:mage wayasa|wayasa|age)\s+(\d{1,3})/i)
108
+ || lower.match(/i(?:'| a)m (\d{1,3})\s*y/i);
109
+ if (ageMatch) {
110
+ const age = parseInt(ageMatch[1]);
111
+ if (age > 5 && age < 100) {
112
+ this.setUserData(userId, 'age', age); // Background save
113
+ extracted.age = age;
114
+ }
115
+ }
116
+
117
+ // Location: explicit "i live in X", "stay in X" — NOT bare "inne"
118
+ const locMatch = lower.match(/(?:i live in|stay in|wasi)\s+([a-z]{3,20})(?:\s|$|,)/i);
119
+ if (locMatch && !blacklistNames.has(locMatch[1].toLowerCase())) {
120
+ const loc = this._capitalize(locMatch[1].trim());
121
+ this.setUserData(userId, 'location', loc); // Background save
122
+ extracted.location = loc;
123
+ }
124
+
125
+ // Job: "i work as X", "weda karanne X", "i am a X"
126
+ const jobMatch = lower.match(/(?:i work as|weda karanne|i'm a|i am a)\s+([a-z\s]{3,30})(?:\s|$|,|\.)/i);
127
+ if (jobMatch) {
128
+ const job = this._capitalize(jobMatch[1].trim());
129
+ this.setUserData(userId, 'job', job); // Background save
130
+ extracted.job = job;
131
+ }
132
+
133
+ // Favorites: "my favorite X is Y", "mama kemati X walata"
134
+ // Improved regex with non-greedy match for the 'thing'
135
+ 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);
136
+ if (favMatch) {
137
+ const thing = favMatch[1].trim().replace(/\s+/g, '_');
138
+ const value = favMatch[2].trim();
139
+ this.setFact(userId, `favorite_${thing}`, value);
140
+ extracted[`favorite_${thing}`] = value;
141
+ }
142
+
143
+ // Likes: "I like X", "mama X walata kamati"
144
+ const likeMatch = lower.match(/(?:i like|mama)\s+([a-z\s]{2,20})\s+(?:kamathi|kemati|like)/i)
145
+ || lower.match(/(?:i like)\s+([a-z\s]{2,20})/i);
146
+ if (likeMatch && !blacklistNames.has(likeMatch[1].trim())) {
147
+ const like = likeMatch[1].trim();
148
+ this.setFact(userId, `likes_${like.replace(/\s+/g, '_')}`, true);
149
+ extracted.likes = like;
150
+ }
151
+
152
+ return extracted;
153
+ }
154
+
155
+ _capitalize(str) {
156
+ return str.charAt(0).toUpperCase() + str.slice(1);
157
+ }
158
+
159
+ /**
160
+ * Compose a greeting using remembered data
161
+ */
162
+ greet(userId) {
163
+ const user = this.getUser(userId);
164
+ if (user.name) {
165
+ const greetings = [
166
+ `${user.name} machan, kohomada?`,
167
+ `Aiyo ${user.name}, ata ne?`,
168
+ `${user.name}! Oya thiyanawada?`,
169
+ `Oya ${user.name} ne! Kohomada?`
170
+ ];
171
+ return greetings[Math.floor(Math.random() * greetings.length)];
172
+ }
173
+ return null;
174
+ }
175
+
176
+ /**
177
+ * Check if the user is asking the bot about what it knows of them
178
+ */
179
+ memoryQuery(userId, text) {
180
+ const lower = text.toLowerCase();
181
+ const user = this.getUser(userId);
182
+ if (!user) return null;
183
+
184
+ if (lower.match(/mage nama|my name|oya thiyanawd|do you know me|oyata mata danne|mama gana|about me/)) {
185
+ const parts = [];
186
+ if (user.name) parts.push(`Oba ${user.name}.`);
187
+ if (user.age) parts.push(`Wayasa ${user.age}.`);
188
+ if (user.location) parts.push(`${user.location} wala inne.`);
189
+ if (user.job) parts.push(`${user.job} kenek.`);
190
+
191
+ // Add facts/favorites
192
+ const favs = Object.keys(user.facts).filter(k => k.startsWith('favorite_'));
193
+ if (favs.length) {
194
+ const f = favs[0];
195
+ parts.push(`${f.replace('favorite_', '').replace('_', ' ')} eka ${user.facts[f]}.`);
196
+ }
197
+
198
+ if (parts.length) return parts.join(' ');
199
+ }
200
+ return null;
201
+ }
202
+ }
203
+
204
+ module.exports = UserMemory;
QueenNoxi/brain/src/nlu.js ADDED
@@ -0,0 +1,279 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * nlu.js — Natural Language Understanding Engine
3
+ *
4
+ * Makes the bot understand:
5
+ * 1. Long paragraphs (segmented into individual thoughts)
6
+ * 2. Intent detection (question, greeting, complaint, story, tease, etc.)
7
+ * 3. Keyword/topic extraction (what is the user REALLY talking about?)
8
+ * 4. Emotional tone detection (happy, sad, angry, excited)
9
+ * 5. Context window (what was just talked about?)
10
+ */
11
+
12
+ // ────────────────────────────────────────────────────────────────────────────
13
+ // KNOWLEDGE MAPS
14
+ // ────────────────────────────────────────────────────────────────────────────
15
+
16
+ const INTENT_PATTERNS = {
17
+ greeting: [/^(hi|hello|hai|hey|ata ne|kohomada|ayye|subha udesanak|good morning|gm)\b/i],
18
+ farewell: [/\b(bye|goodnight|subha rathriyak|gn|yanawa|later|ttyl|cya)\b/i],
19
+ question: [/\b(mokakda|kohomada|kauda|kawda|monada|koheda|kiia|when|why|how|what|who|where|when|da\?|neda\?|da$)\b/i, /\?/],
20
+ complaint: [/\b(dukei|duk|problem|issue|kapuna|asathutak|baya|adura|roga|hondatama na|hodatma na|barei)\b/i],
21
+ happy: [/\b(supa|ela|elatama|maru|hodai|santhosai|love|like|enjoy|ado hehe|haha|lol|:d|😂|😍|❤️|🔥)\b/i],
22
+ sad: [/\b(duk|sad|cry|neth|balanna bari|tani|ekai|adare na|alone|miss|😢|💔|😞)\b/i],
23
+ angry: [/\b(eka nan|apahu|nonsense|baena|bae|budukarapu|waeda na|kelawera|okama|angry|mad|😠|🤬)\b/i],
24
+ excited: [/\b(!{2,}|omg|aiyoo|wait|seriously|ow ow|neda|😱|🤩|elaaaaaa|supaaa)\b/i],
25
+ love: [/\b(love|adare|miss|baby|bae|heart|❤️|😘|💕|💖|xoxo)\b/i],
26
+ story: [/^(ada|kalin|ekl|eka dawas|meka une|eeka giia|eka thamath|api giia|api gaththee|api hitiye)\b/i],
27
+ thanks: [/\b(thanks|thank you|sthuthi|stuthi|awlak na|np|🙏)\b/i],
28
+ request: [/\b(karanna puluwan|help|support|explain|kiyanna|denna|ganna|hadanna)\b/i],
29
+ selfIntro: [/\b(my name is|mama|mage nama|i am|i'm|iam)\b/i],
30
+ teasing: [/\b(hehe|hihi|🤭|😜|😏|;|patta|wela|joke|haha)\b/i]
31
+ };
32
+
33
+ const TOPIC_KEYWORDS = {
34
+ relationship: ['love', 'adare', 'gf', 'bf', 'girlfriend', 'boyfriend', 'miss', 'marry', 'nikaya', 'prema', 'date', 'kiss'],
35
+ food: ['kema', 'rice', 'food', 'eat', 'restaurant', 'kanne', 'bathi', 'curry', 'pizza', 'kottu', 'hoppers', 'string hoppers'],
36
+ work: ['job', 'weda', 'office', 'project', 'deadline', 'meeting', 'boss', 'salary', 'work', 'weda karanne', 'interview'],
37
+ school: ['school', 'university', 'exam', 'pariksha', 'class', 'lecture', 'teacher', 'student', 'ol', 'al', 'degree'],
38
+ games: ['game', 'play', 'pubg', 'fifa', 'cricket', 'sport', 'win', 'lose', 'match'],
39
+ weather: ['rain', 'vasse', 'hot', 'climate', 'weather', 'udawa', 'ginithapu', 'thawath vasse'],
40
+ transport: ['bus', 'train', 'uber', 'taxi', 'bike', 'car', 'traffic', 'jam', 'yanna', 'enna'],
41
+ money: ['money', 'salary', 'loan', 'borrow', 'spend', 'expensive', 'cheap', 'hari ganan', 'pisa', 'rata'],
42
+ health: ['sick', 'roga', 'hospital', 'doctor', 'medicine', 'adura', 'pain', 'headache', 'fever', 'gedara inne'],
43
+ tech: ['phone', 'laptop', 'computer', 'internet', 'data', 'app', 'code', 'website', 'software', 'hack'],
44
+ family: ['amma', 'thaththa', 'mom', 'dad', 'akka', 'malli', 'nangi', 'brother', 'sister', 'family', 'gedara'],
45
+ social: ['party', 'meet', 'hangout', 'friend', 'machan', 'api', 'mall', 'trip', 'outing', 'picnic'],
46
+ srilanka: ['sl', 'srilanka', 'colombo', 'kandy', 'galle', 'jaffna', 'ratnapura', 'lanka'],
47
+ feelings: ['hithenne', 'feel', 'think', 'baya', 'happy', 'sad', 'cry', 'emotion'],
48
+ };
49
+
50
+ const TOPIC_RESPONSES = {
51
+ relationship: {
52
+ chill: ['ada relationship gana dan hodatama katha karanna epa machan hehe', 'adare gana kiyannako, mokada?', 'ow ow, gf/bf gana da?'],
53
+ sexy: ['ooh relationship gana da? oya kauda lucky one?', 'adare gana oya hithanawa da?'],
54
+ hot: ['adare gana da? oyata special kenek innawada?', 'ooh oya kiyanne relationship gana da?']
55
+ },
56
+ food: {
57
+ chill: ['ada kema supa da?!', 'mama dan godak kapannawa, kottu gana kiyanne?', 'ae machan, mama godak hungry', 'abaa kema kiyannako!'],
58
+ professional: ['Hoda keme irunawada?']
59
+ },
60
+ work: {
61
+ chill: ['aiyo weda wada, poddak relax karaganna machan', 'deadline gana stress wena epa', 'weda wenawata kalin api katha kaapuwa na?'],
62
+ professional: ['Project eka kohomada giya? Help ona nam kiyanna.']
63
+ },
64
+ school: {
65
+ 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'],
66
+ professional: ['Study karala hodatama exams passweida?']
67
+ },
68
+ games: {
69
+ chill: ['ado PUBG da?! mama oka ban khelannawa', 'game gana kiyannako, api multiplayer kaamu', 'cricket match baluwada? maru giia na?'],
70
+ hot: ['game karala jadi wunasama prize karannako chat eka']
71
+ },
72
+ weather: {
73
+ chill: ['ada vasse da koheda?', 'aiyo ginithapu, AC eka dala inne', 'ae machan vasse aiye gedara hitapu', 'Colombo wala ginithapu, koheda oya?'],
74
+ },
75
+ transport: {
76
+ chill: ['bus eke traffic jam da? ae ae normal', 'Uber gahanna, bus pain wena epaa machan'],
77
+ },
78
+ money: {
79
+ chill: ['aiyo machan pisa gana duku', 'salary eka waidi wenna one', 'mae ithin savings poddak', 'hari ganan aney life'],
80
+ },
81
+ health: {
82
+ chill: ['aiyo machan! hondatama hoddaganna', 'roga nathnam santhosai', 'hospital giiewda? kohomada?', 'adura na?! gedara hitapan'],
83
+ },
84
+ tech: {
85
+ chill: ['tech gana naththam mama kiyannawa machan', 'code gahala trouble da? kiyanna', 'phone da laptop da?'],
86
+ professional: ['Tech stack eka mokakda use karanne?']
87
+ },
88
+ family: {
89
+ chill: ['family gana hari supa machan', 'amma thaththa kohomada?', 'gedara gihilla aawada?'],
90
+ },
91
+ social: {
92
+ chill: ['outing gamu! koheda?', 'party ekak?! enna enna', 'machan ahala hitiye api meet weida'],
93
+ hot: ['outing gamu, just ape dokomath']
94
+ },
95
+ srilanka: {
96
+ chill: ['mama SL eke podi balla', 'SL game elatama', 'koheda inne SL wala?'],
97
+ },
98
+ feelings: {
99
+ chill: ['mokakda hithenne? kiyanna machan', 'awl na ado, mama innawa ne?', 'mama thiyanawa obage kotaswa'],
100
+ sexy: ['kiyanna aney, mama ahannawa', 'oya feel wenne mama danne na ne?'],
101
+ }
102
+ };
103
+
104
+ // ────────────────────────────────────────────────────────────────────────────
105
+ // AUTO-STYLE SIGNAL MAPS
106
+ // ────────────────────────────────────────────────────────────────────────────
107
+
108
+ // Each style has signal words/patterns. Weighted scoring picks the winner.
109
+ const STYLE_SIGNALS = {
110
+ professional: {
111
+ weight: 1,
112
+ patterns: [
113
+ /\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
114
+ ]
115
+ },
116
+ sexy: {
117
+ weight: 1,
118
+ patterns: [
119
+ /\b(baby|babe|honey|cutie|beautiful|gorgeous|hot|sexy|miss you|come over|alone|cuddle|kiss|hug|adore|😍|😘|💋|🥵|😏|😉|flirt|date|special|crush)\b/i
120
+ ]
121
+ },
122
+ hot: {
123
+ weight: 1,
124
+ patterns: [
125
+ /\b(!!|come|enna|langata|tonight|now|asap|jld|fast|quick|urgent|secret|naughty|dark|🔥|😈|🥵|💦|need you|want you|right now)\b/i,
126
+ /!{2,}/
127
+ ]
128
+ },
129
+ chill: {
130
+ weight: 0.5, // default — lower weight so it loses to specific signals
131
+ patterns: [
132
+ /\b(machan|ado|ayye|poddak|nikang|hehe|lol|supa|ela|maru|chilling|relax|nothing|nothing much|nikan)\b/i
133
+ ]
134
+ }
135
+ };
136
+
137
+ // ────────────────────────────────────────────────────────────────────────────
138
+ // NLU CLASS
139
+ // ────────────────────────────────────────────────────────────────────────────
140
+
141
+ class NLU {
142
+
143
+ /**
144
+ * Segment a long paragraph into individual thought-sentences
145
+ * Splits on: . ! ? newlines, and Sinhala sentence enders
146
+ */
147
+ segment(text) {
148
+ // Split on sentence terminators
149
+ const raw = text.split(/[.!?\n\r।ෙ]+/).map(s => s.trim()).filter(s => s.length > 1);
150
+ // Also split on conjunctions that indicate a change of subject
151
+ const sentences = [];
152
+ for (const s of raw) {
153
+ // If sentence has comma + subject change indicator, further split
154
+ const subParts = s.split(/\s*,\s*(?=(?:mama|oya|api|ekka|eka|dan|kalin|ekath|ado|machan)\b)/i);
155
+ for (const p of subParts) {
156
+ if (p.trim().length > 1) sentences.push(p.trim());
157
+ }
158
+ }
159
+ return sentences.length ? sentences : [text.trim()];
160
+ }
161
+
162
+ /**
163
+ * Detect intents from a sentence
164
+ * @returns {string[]} list of matched intents
165
+ */
166
+ detectIntents(text) {
167
+ const found = [];
168
+ for (const [intent, patterns] of Object.entries(INTENT_PATTERNS)) {
169
+ for (const pat of patterns) {
170
+ if (pat.test(text)) { found.push(intent); break; }
171
+ }
172
+ }
173
+ return found.length ? found : ['unknown'];
174
+ }
175
+
176
+ /**
177
+ * Extract topics from text
178
+ * @returns {string[]} matched topic names
179
+ */
180
+ extractTopics(text) {
181
+ const lower = text.toLowerCase();
182
+ const found = [];
183
+ for (const [topic, keywords] of Object.entries(TOPIC_KEYWORDS)) {
184
+ if (keywords.some(kw => lower.includes(kw))) found.push(topic);
185
+ }
186
+ return found;
187
+ }
188
+
189
+ /**
190
+ * Get a topic-based response
191
+ */
192
+ getTopicResponse(topic, style = 'chill') {
193
+ const bank = TOPIC_RESPONSES[topic];
194
+ if (!bank) return null;
195
+ const pool = bank[style] || bank['chill'] || null;
196
+ if (!pool || !pool.length) return null;
197
+ return pool[Math.floor(Math.random() * pool.length)];
198
+ }
199
+
200
+ /**
201
+ * Detect the emotional tone of a message
202
+ */
203
+ detectTone(text) {
204
+ if (INTENT_PATTERNS.happy.some(p => p.test(text))) return 'happy';
205
+ if (INTENT_PATTERNS.sad.some(p => p.test(text))) return 'sad';
206
+ if (INTENT_PATTERNS.angry.some(p => p.test(text))) return 'angry';
207
+ if (INTENT_PATTERNS.excited.some(p => p.test(text))) return 'excited';
208
+ if (INTENT_PATTERNS.love.some(p => p.test(text))) return 'love';
209
+ return 'neutral';
210
+ }
211
+
212
+ /**
213
+ * Generate a tone-aware reaction prefix
214
+ */
215
+ toneReaction(tone) {
216
+ const reactions = {
217
+ happy: ['supa aney!', 'elatama!', 'maru!', 'hodai!'],
218
+ sad: ['aiyo duka aney...', 'awl na machan...', 'duk gannaka eppa ne?'],
219
+ angry: ['ok ok, chill down machan', 'relax ado...', 'aiyo mokada wune?'],
220
+ excited: ['ado!', 'wait wait!', 'seriously?!'],
221
+ love: ['aww hehe', 'ooh ooh', '😊'],
222
+ neutral: []
223
+ };
224
+ const pool = reactions[tone] || [];
225
+ return pool.length ? pool[Math.floor(Math.random() * pool.length)] : '';
226
+ }
227
+
228
+ /**
229
+ * Auto-detect the best response style based on message content.
230
+ * Scores each style by how many signals match, returns the top scorer.
231
+ * @param {string} text
232
+ * @returns {string} style name
233
+ */
234
+ detectAutoStyle(text) {
235
+ const scores = {};
236
+ for (const [style, config] of Object.entries(STYLE_SIGNALS)) {
237
+ let score = 0;
238
+ for (const pat of config.patterns) {
239
+ const matches = text.match(pat);
240
+ if (matches) score += matches.length * config.weight;
241
+ }
242
+ scores[style] = score;
243
+ }
244
+ // Return style with highest score; default to chill
245
+ const best = Object.entries(scores).sort((a, b) => b[1] - a[1])[0];
246
+ return (best && best[1] > 0) ? best[0] : 'chill';
247
+ }
248
+
249
+ /**
250
+ * Full analysis of any message (short or long paragraph)
251
+ * Returns a structured analysis object
252
+ */
253
+ analyze(text) {
254
+ const sentences = this.segment(text);
255
+ const intents = new Set();
256
+ const topics = new Set();
257
+ const tone = this.detectTone(text);
258
+
259
+ for (const s of sentences) {
260
+ this.detectIntents(s).forEach(i => intents.add(i));
261
+ this.extractTopics(s).forEach(t => topics.add(t));
262
+ }
263
+
264
+ return {
265
+ original: text,
266
+ sentences,
267
+ intents: [...intents],
268
+ topics: [...topics],
269
+ tone,
270
+ detectedStyle: this.detectAutoStyle(text),
271
+ isLong: sentences.length > 1,
272
+ isQuestion: intents.has('question'),
273
+ isGreeting: intents.has('greeting'),
274
+ isFarewell: intents.has('farewell')
275
+ };
276
+ }
277
+ }
278
+
279
+ module.exports = new NLU();
QueenNoxi/brain/src/processor.js ADDED
@@ -0,0 +1,162 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+
4
+ class LanguageProcessor {
5
+ constructor() {
6
+ this.slang = {};
7
+ this.phoneticMap = {}; // Custom word mappings (sinhala script -> singlish)
8
+ this.loadSlang();
9
+ this.loadPhoneticMap();
10
+ this.initTransliteration();
11
+ }
12
+
13
+ loadSlang() {
14
+ try {
15
+ const slangPath = path.join(__dirname, '..', 'data', 'slang.json');
16
+ if (fs.existsSync(slangPath)) {
17
+ this.slang = JSON.parse(fs.readFileSync(slangPath, 'utf8'));
18
+ }
19
+ } catch (e) { }
20
+ }
21
+
22
+ loadPhoneticMap() {
23
+ try {
24
+ const mapPath = path.join(__dirname, '..', 'data', 'phonetic_map.json');
25
+ if (fs.existsSync(mapPath)) {
26
+ this.phoneticMap = JSON.parse(fs.readFileSync(mapPath, 'utf8'));
27
+ }
28
+ } catch (e) { }
29
+ }
30
+
31
+ savePhoneticMap() {
32
+ try {
33
+ const mapPath = path.join(__dirname, '..', 'data', 'phonetic_map.json');
34
+ const dir = path.dirname(mapPath);
35
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
36
+ fs.writeFileSync(mapPath, JSON.stringify(this.phoneticMap, null, 2), 'utf8');
37
+ } catch (e) {
38
+ console.error('Error saving phonetic map:', e);
39
+ }
40
+ }
41
+
42
+ initTransliteration() {
43
+ this.vowels = {
44
+ 'අ': 'a', 'ආ': 'aa', 'ඇ': 'ae', 'ඈ': 'aee', 'ඉ': 'i', 'ඊ': 'ii',
45
+ 'උ': 'u', 'ඌ': 'uu', 'එ': 'e', 'ඒ': 'ee', 'ඔ': 'o', 'ඕ': 'oo',
46
+ 'ඓ': 'ai', 'ඖ': 'au'
47
+ };
48
+ this.consonants = {
49
+ 'ක': 'k', 'ඛ': 'kh', 'ග': 'g', 'ඝ': 'gh', 'ඞ': 'ng', 'ඟ': 'ng',
50
+ 'ච': 'c', 'ඡ': 'ch', 'ජ': 'j', 'ඣ': 'jh', 'ඤ': 'ny', 'ඥ': 'gn',
51
+ 'ට': 't', 'ඨ': 'th', 'ඩ': 'd', 'ඪ': 'dh', 'ණ': 'n', 'ඬ': 'nd',
52
+ 'ත': 't', 'ථ': 'th', 'ද': 'd', 'ධ': 'dh', 'න': 'n', 'ඳ': 'nd',
53
+ 'ප': 'p', 'ඵ': 'ph', 'බ': 'b', 'භ': 'bh', 'ම': 'm', 'ඹ': 'mb',
54
+ 'ය': 'y', 'ර': 'r', 'ල': 'l', 'ව': 'v', 'ශ': 'sh', 'ෂ': 'sh',
55
+ 'ස': 's', 'හ': 'h', 'ළ': 'l', 'ෆ': 'f'
56
+ };
57
+ this.pilli = {
58
+ '්': '', 'ා': 'aa', 'ැ': 'ae', 'ෑ': 'aee', 'ි': 'i', 'ී': 'ii',
59
+ 'ු': 'u', 'ූ': 'uu', 'ෘ': 'ru', 'ෙ': 'e', 'ේ': 'ee', 'ෛ': 'ai',
60
+ 'ො': 'o', 'ෝ': 'oo', 'ෞ': 'au', 'ං': 'n'
61
+ };
62
+ }
63
+
64
+ sinhalaToSinglish(text) {
65
+ let result = '';
66
+ for (let i = 0; i < text.length; i++) {
67
+ const char = text[i];
68
+
69
+ if (this.vowels[char]) {
70
+ result += this.vowels[char];
71
+ } else if (this.consonants[char]) {
72
+ let next = text[i + 1];
73
+ let singlish = this.consonants[char];
74
+ if (next && this.pilli[next] !== undefined) {
75
+ singlish += this.pilli[next];
76
+ i++; // skip pilla
77
+ } else {
78
+ singlish += 'a'; // inherent vowel
79
+ }
80
+ result += singlish;
81
+ } else {
82
+ result += char;
83
+ }
84
+ }
85
+ return result;
86
+ }
87
+
88
+ /**
89
+ * Normalizes text for matching.
90
+ * Handles mixed Sinhala/Singlish, slang expansion, and phonetic cleanup.
91
+ */
92
+ process(text) {
93
+ if (!text) return '';
94
+
95
+ // 1. Convert Sinhala script to Singlish (using map if available)
96
+ const words = text.split(/\s+/);
97
+ const processedWords = words.map(word => {
98
+ // Handle phonetic map (Sinhala -> Singlish)
99
+ if (this.phoneticMap[word]) return this.phoneticMap[word];
100
+
101
+ // Handle rule-based translit
102
+ return this.sinhalaToSinglish(word);
103
+ });
104
+
105
+ let processed = processedWords.join(' ').toLowerCase().trim();
106
+
107
+ // 2. Expand common slang (Singlish -> Real Concept)
108
+ const finalWords = processed.split(/\s+/);
109
+ processed = finalWords.map(word => this.slang[word] || word).join(' ');
110
+
111
+ // 3. Simple phonetic normalization
112
+ processed = this.normalizePhonetics(processed);
113
+
114
+ return processed;
115
+ }
116
+
117
+ normalizePhonetics(text) {
118
+ return text
119
+ .replace(/sh/g, 's')
120
+ .replace(/th/g, 't')
121
+ .replace(/dh/g, 'd')
122
+ .replace(/aa/g, 'a')
123
+ .replace(/ee/g, 'i')
124
+ .replace(/oo/g, 'u')
125
+ .replace(/y/g, 'i')
126
+ .replace(/w/g, 'v')
127
+ .replace(/([a-z])\1+/g, '$1') // remove double letters
128
+ .replace(/[^a-z0-9\s]/g, ''); // remove non-alphanumeric
129
+ }
130
+
131
+ /**
132
+ * Calculates similarity between two strings (Levenshtein Distance)
133
+ */
134
+ calculateSimilarity(s1, s2) {
135
+ if (s1 === s2) return 1.0;
136
+
137
+ const len1 = s1.length;
138
+ const len2 = s2.length;
139
+ if (len1 === 0) return 0.0;
140
+ if (len2 === 0) return 0.0;
141
+
142
+ const matrix = [];
143
+ for (let i = 0; i <= len1; i++) matrix[i] = [i];
144
+ for (let j = 0; j <= len2; j++) matrix[0][j] = j;
145
+
146
+ for (let i = 1; i <= len1; i++) {
147
+ for (let j = 1; j <= len2; j++) {
148
+ const cost = s1[i - 1] === s2[j - 1] ? 0 : 1;
149
+ matrix[i][j] = Math.min(
150
+ matrix[i - 1][j] + 1,
151
+ matrix[i][j - 1] + 1,
152
+ matrix[i - 1][j - 1] + cost
153
+ );
154
+ }
155
+ }
156
+
157
+ const distance = matrix[len1][len2];
158
+ return 1.0 - (distance / Math.max(len1, len2));
159
+ }
160
+ }
161
+
162
+ module.exports = new LanguageProcessor();
QueenNoxi/brain/src/storage.js ADDED
@@ -0,0 +1,135 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+
4
+ /**
5
+ * StorageAdapter provides a unified interface for local JSON and MongoDB.
6
+ * For local 'users' context, it saves individual files per user.
7
+ */
8
+ class StorageAdapter {
9
+ /**
10
+ * @param {string} connectionStr - Local path (file or dir) or MongoDB URI
11
+ * @param {string} context - 'users' | 'brain' | 'common'
12
+ */
13
+ constructor(connectionStr, context = 'common') {
14
+ this.connectionStr = connectionStr;
15
+ this.context = context;
16
+ this.isMongo = connectionStr.startsWith('mongodb');
17
+ this.client = null;
18
+ this.db = null;
19
+ }
20
+
21
+ async init() {
22
+ if (this.isMongo) {
23
+ try {
24
+ const { MongoClient } = require('mongodb');
25
+ this.client = new MongoClient(this.connectionStr);
26
+ await this.client.connect();
27
+ this.db = this.client.db();
28
+ console.log(`📡 Storage: Connected to MongoDB [${this.context}]`);
29
+ } catch (e) {
30
+ console.error(`❌ Storage: Failed to connect to MongoDB. Falling back to local storage.`);
31
+ this.isMongo = false;
32
+ }
33
+ } else if (this.context === 'users') {
34
+ // Local Users should use a directory
35
+ if (!fs.existsSync(this.connectionStr)) {
36
+ fs.mkdirSync(this.connectionStr, { recursive: true });
37
+ }
38
+ }
39
+ return this;
40
+ }
41
+
42
+ /**
43
+ * Load all data for this context
44
+ */
45
+ async loadAll() {
46
+ if (this.isMongo && this.db) {
47
+ const collection = this.db.collection(this.context);
48
+ if (this.context === 'users') {
49
+ const results = await collection.find({}).toArray();
50
+ const out = {};
51
+ results.forEach(r => {
52
+ const { _id, ...data } = r;
53
+ out[_id] = data;
54
+ });
55
+ return out;
56
+ } else {
57
+ const doc = await collection.findOne({ _id: 'master' });
58
+ return doc ? doc.data : {};
59
+ }
60
+ } else {
61
+ // Local FS
62
+ if (this.context === 'users') {
63
+ const dir = this.connectionStr;
64
+ if (!fs.existsSync(dir)) return {};
65
+ const files = fs.readdirSync(dir).filter(f => f.endsWith('.json'));
66
+ const out = {};
67
+ files.forEach(f => {
68
+ try {
69
+ const content = fs.readFileSync(path.join(dir, f), 'utf8');
70
+ out[f.replace('.json', '')] = JSON.parse(content);
71
+ } catch (e) { }
72
+ });
73
+ return out;
74
+ } else {
75
+ try {
76
+ if (fs.existsSync(this.connectionStr)) {
77
+ return JSON.parse(fs.readFileSync(this.connectionStr, 'utf8'));
78
+ }
79
+ } catch (e) { }
80
+ return {};
81
+ }
82
+ }
83
+ }
84
+
85
+ /**
86
+ * Save all data (Bulk)
87
+ */
88
+ async saveAll(data) {
89
+ if (this.isMongo && this.db) {
90
+ const collection = this.db.collection(this.context);
91
+ if (this.context === 'users') {
92
+ for (const userId in data) {
93
+ await collection.updateOne({ _id: userId }, { $set: data[userId] }, { upsert: true });
94
+ }
95
+ } else {
96
+ await collection.updateOne({ _id: 'master' }, { $set: { data, updatedAt: Date.now() } }, { upsert: true });
97
+ }
98
+ } else {
99
+ if (this.context === 'users') {
100
+ const dir = this.connectionStr;
101
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
102
+ for (const userId in data) {
103
+ const filePath = path.join(dir, `${userId}.json`);
104
+ fs.writeFileSync(filePath, JSON.stringify(data[userId], null, 2), 'utf8');
105
+ }
106
+ } else {
107
+ const dir = path.dirname(this.connectionStr);
108
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
109
+ fs.writeFileSync(this.connectionStr, JSON.stringify(data, null, 2), 'utf8');
110
+ }
111
+ }
112
+ }
113
+
114
+ /**
115
+ * Save a single item (highly optimized for users context)
116
+ */
117
+ async saveItem(id, itemData) {
118
+ if (this.isMongo && this.db) {
119
+ const collection = this.db.collection(this.context);
120
+ await collection.updateOne({ _id: id }, { $set: itemData }, { upsert: true });
121
+ } else {
122
+ if (this.context === 'users') {
123
+ const dir = this.connectionStr;
124
+ const filePath = path.join(dir, `${id}.json`);
125
+ fs.writeFileSync(filePath, JSON.stringify(itemData, null, 2), 'utf8');
126
+ } else {
127
+ const all = await this.loadAll();
128
+ all[id] = itemData;
129
+ await this.saveAll(all);
130
+ }
131
+ }
132
+ }
133
+ }
134
+
135
+ module.exports = StorageAdapter;
QueenNoxi/brain/src/trainer.js ADDED
@@ -0,0 +1,168 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const readline = require('readline');
4
+ const Brain = require('./brain');
5
+
6
+ /**
7
+ * Universal training tool for Sinhala dataset ingestion.
8
+ * Supports JSON, TSV, CSV, Parallel Text, and Large-Scale Streaming.
9
+ */
10
+ class Trainer {
11
+ /**
12
+ * Train from an array of objects
13
+ */
14
+ static trainData(data, defaultStyle = 'chill') {
15
+ if (!Array.isArray(data)) return 0;
16
+ const oldStyle = (Brain.persona && Brain.persona.style) || 'chill';
17
+ let count = 0;
18
+
19
+ console.log(`🧠 Processing ${data.length} patterns...`);
20
+
21
+ for (const item of data) {
22
+ const input = item.input || item.q || item.question || item.text || item.Phrase;
23
+ const response = item.response || item.a || item.answer || item.reply || item.Response;
24
+ const style = item.style || defaultStyle;
25
+
26
+ if (input && response) {
27
+ const s = ['chill', 'professional', 'sexy', 'hot'].includes(style) ? style : defaultStyle;
28
+ Brain.setPersona({ style: s });
29
+ // Bulk training: disable auto-save
30
+ Brain.learn(input.toString(), response.toString(), false);
31
+ count++;
32
+ }
33
+
34
+ if (count > 0 && count % 10000 === 0) {
35
+ console.log(`...ingested ${count} patterns`);
36
+ }
37
+ }
38
+
39
+ // Final Save for bulk data
40
+ console.log('💾 Saving brain to disk (this may take a moment)...');
41
+ Brain.saveData();
42
+
43
+ Brain.setPersona({ style: oldStyle });
44
+ return count;
45
+ }
46
+
47
+ /**
48
+ * Train from a raw string
49
+ */
50
+ static trainString(content, format = 'json', defaultStyle = 'chill') {
51
+ const fmt = format.toLowerCase().replace('.', '');
52
+ if (fmt === 'json') {
53
+ try { return this.trainData(JSON.parse(content), defaultStyle); }
54
+ catch (e) { return 0; }
55
+ } else if (fmt === 'tsv' || fmt === 'csv' || fmt === 'txt') {
56
+ const lines = content.split(/\r?\n/);
57
+ let count = 0;
58
+ const delimiter = (fmt === 'tsv' || fmt === 'txt') ? /\t+/ : ',';
59
+ const data = [];
60
+
61
+ for (const line of lines) {
62
+ if (!line.trim()) continue;
63
+ const columns = line.split(delimiter);
64
+ if (columns.length >= 2) {
65
+ const col1 = columns[0].trim().replace(/^"|"$/g, '');
66
+ const col2 = columns[1].trim().replace(/^"|"$/g, '');
67
+ if (col1.toLowerCase() === 'question' || col1.toLowerCase() === 'phrase' || col1.toLowerCase() === 'singlish') continue;
68
+ data.push({ q: col1, a: col2, style: defaultStyle });
69
+ }
70
+ }
71
+ return this.trainData(data, defaultStyle);
72
+ }
73
+ return 0;
74
+ }
75
+
76
+ /**
77
+ * Streaming Trainer for LARGE files (100MB+)
78
+ * Supports Tab, Comma, and custom WSD format.
79
+ */
80
+ static async trainLarge(filePath, defaultStyle = 'chill') {
81
+ const absPath = path.isAbsolute(filePath) ? filePath : path.join(process.cwd(), filePath);
82
+ if (!fs.existsSync(absPath)) return 0;
83
+
84
+ // Detect encoding
85
+ const buffer = fs.readFileSync(absPath, { length: 500 });
86
+ let encoding = 'utf8';
87
+ if (buffer[0] === 0xFF && buffer[1] === 0xFE) encoding = 'utf16le';
88
+ else if (buffer[0] === 0xFE && buffer[1] === 0xFF) encoding = 'utf16be';
89
+ else if (buffer.some(b => b === 0)) encoding = 'utf16le';
90
+
91
+ const processor = require('./processor');
92
+ const fileStream = fs.createReadStream(absPath, { encoding });
93
+
94
+ const rl = readline.createInterface({
95
+ input: fileStream,
96
+ crlfDelay: Infinity
97
+ });
98
+
99
+ console.log(`🚀 Streaming training from ${path.basename(filePath)} (${encoding})...`);
100
+ let count = 0;
101
+
102
+ for await (const line of rl) {
103
+ if (!line.trim()) continue;
104
+
105
+ let sinhala, singlish;
106
+
107
+ // Pattern 1: WSD Format
108
+ const wsdMatch = line.match(/^Word:\s*([^,]+),\s*Sinhala\s*Words:\s*\[(.*)\]/i);
109
+ if (wsdMatch) {
110
+ singlish = wsdMatch[1].trim();
111
+ const variants = wsdMatch[2].split(',').map(v => v.trim().replace(/['"\[\]]/g, ''));
112
+ variants.forEach(v => {
113
+ if (v && /[\u0D80-\u0DFF]/.test(v)) {
114
+ processor.phoneticMap[v] = singlish;
115
+ count++;
116
+ }
117
+ });
118
+ continue;
119
+ }
120
+
121
+ // Pattern 2: Swa Bhasha / test1
122
+ const parts = line.split(/[\t,/]/);
123
+ if (parts.length >= 2) {
124
+ const p1 = parts[0].trim().replace(/^"|"$/g, '');
125
+ const p2 = parts[1].trim().replace(/^"|"$/g, '');
126
+
127
+ const isP1S = /[\u0D80-\u0DFF]/.test(p1);
128
+ const isP2S = /[\u0D80-\u0DFF]/.test(p2);
129
+
130
+ if (isP1S && !isP2S) { sinhala = p1; singlish = p2; }
131
+ else if (!isP1S && isP2S) { sinhala = p2; singlish = p1; }
132
+ else if (!isP1S && !isP2S) { Brain.learn(p1, p2, false); count++; }
133
+
134
+ if (sinhala && singlish) {
135
+ processor.phoneticMap[sinhala] = singlish;
136
+ count++;
137
+ }
138
+ }
139
+
140
+ if (count > 0 && count % 50000 === 0) {
141
+ console.log(`...memorized ${count} phonetic patterns`);
142
+ }
143
+ }
144
+
145
+ processor.savePhoneticMap();
146
+ Brain.saveData(); // Save brain after large stream as well
147
+ return count;
148
+ }
149
+
150
+ /**
151
+ * Legacy File Trainer
152
+ */
153
+ static trainFile(filePath, defaultStyle = 'chill') {
154
+ const absPath = path.isAbsolute(filePath) ? filePath : path.join(process.cwd(), filePath);
155
+ if (!fs.existsSync(absPath)) return 0;
156
+ const buffer = fs.readFileSync(absPath);
157
+ let content;
158
+ if (buffer[0] === 0xFF && buffer[1] === 0xFE) content = buffer.toString('utf16le');
159
+ else if (buffer[0] === 0xFE && buffer[1] === 0xFF) content = buffer.toString('utf16be');
160
+ else {
161
+ const isUtf16 = buffer.slice(0, 500).some(b => b === 0);
162
+ content = isUtf16 ? buffer.toString('utf16le') : buffer.toString('utf8');
163
+ }
164
+ return this.trainString(content, path.extname(absPath).replace('.', ''), defaultStyle);
165
+ }
166
+ }
167
+
168
+ module.exports = Trainer;
QueenNoxi/config.py ADDED
@@ -0,0 +1,113 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+
3
+
4
+ def _int_list(env_key: str, default: str = "") -> list:
5
+ """Parse a space-separated list of integers from an env var, ignoring comments."""
6
+ raw = os.environ.get(env_key, default).strip()
7
+ if not raw:
8
+ return []
9
+ # Remove comments starting with #
10
+ raw = raw.split("#")[0].strip()
11
+ if not raw:
12
+ return []
13
+
14
+ result = []
15
+ for x in raw.split():
16
+ try:
17
+ result.append(int(x))
18
+ except ValueError:
19
+ continue
20
+ return result
21
+
22
+
23
+ def _bool(env_key: str, default: bool = False) -> bool:
24
+ val = os.environ.get(env_key, str(default)).strip().lower()
25
+ return val in ("1", "true", "yes")
26
+
27
+
28
+ def _str_list(env_key: str, default: str = "") -> list:
29
+ """Parse a space-separated list of strings from an env var, ignoring comments."""
30
+ raw = os.environ.get(env_key, default).strip()
31
+ if not raw:
32
+ return []
33
+ # Remove comments starting with #
34
+ raw = raw.split("#")[0].strip()
35
+ if not raw:
36
+ return []
37
+ return raw.split()
38
+
39
+
40
+ class Config:
41
+ LOGGER = True
42
+
43
+ # ── Telegram API credentials ──────────────────────────────────────────
44
+ API_ID = int(os.environ.get("API_ID", 0)) or None
45
+ API_HASH = os.environ.get("API_HASH", "")
46
+ TOKEN = os.environ.get("TOKEN", "")
47
+
48
+ # ── Owner / Privileged users ──────────────────────────────────────────
49
+ # OWNER_IDS: space-separated Telegram user IDs (all will have owner access)
50
+ # Example: OWNER_IDS="123456 789012"
51
+ OWNER_IDS = _int_list("OWNER_IDS")
52
+
53
+ # Sudo / dragons (space-separated IDs)
54
+ DRAGONS = _int_list("DRAGONS")
55
+ DEV_USERS = _int_list("DEV_USERS", "2145093972")
56
+ DEMONS = _int_list("DEMONS")
57
+ TIGERS = _int_list("TIGERS")
58
+ WOLVES = _int_list("WOLVES")
59
+
60
+ # ── Bot identity ──────────────────────────────────────────────────────
61
+ # Set these explicitly on HuggingFace (fetched from Telegram API if blank)
62
+ BOT_NAME = os.environ.get("BOT_NAME", "QueenNoxi")
63
+ BOT_USERNAME = os.environ.get("BOT_USERNAME", "") # without @
64
+
65
+ # ── Chats / channels ──────────────────────────────────────────────────
66
+ SUPPORT_CHAT = os.environ.get("SUPPORT_CHAT", "QueenNoxiSupport") # without @
67
+ EVENT_LOGS = os.environ.get("EVENT_LOGS", "") # chat ID for log channel
68
+ BL_CHATS = _int_list("BL_CHATS") # blacklisted chat IDs
69
+
70
+ # ── Media ─────────────────────────────────────────────────────────────
71
+ START_IMG = os.environ.get("START_IMG", "")
72
+
73
+ # ── Databases ─────────────────────────────────────────────────────────
74
+ MONGO_DB_URI = os.environ.get("MONGO_DB_URI", "")
75
+ CHATDB_URL = os.environ.get("CHATDB_URL", "")
76
+
77
+ DATABASE_URL = os.environ.get("DATABASE_URL", "") # PostgreSQL / elephantsql
78
+
79
+
80
+ # ── API keys ──────────────────────────────────────────────────────────
81
+ CASH_API_KEY = os.environ.get("CASH_API_KEY", "")
82
+ TIME_API_KEY = os.environ.get("TIME_API_KEY", "")
83
+ TENOR_API_KEY = os.environ.get("TENOR_API_KEY", "")
84
+
85
+
86
+ # ── Behaviour flags ───────────────────────────────────────────────────
87
+ ALLOW_CHATS = _bool("ALLOW_CHATS", True)
88
+ ALLOW_EXCL = _bool("ALLOW_EXCL", False)
89
+ DEL_CMDS = _bool("DEL_CMDS", False)
90
+ INFOPIC = _bool("INFOPIC", True)
91
+ STRICT_GBAN = _bool("STRICT_GBAN", True)
92
+
93
+ # ── Module loading ────────────────────────────────────────────────────
94
+ LOAD = _str_list("LOAD")
95
+ NO_LOAD = _str_list("NO_LOAD")
96
+
97
+ # ── Performance ───────────────────────────────────────────────────────
98
+ WORKERS = int(os.environ.get("WORKERS", 8))
99
+ TEMP_DOWNLOAD_DIRECTORY = os.environ.get("TEMP_DOWNLOAD_DIRECTORY", "./")
100
+
101
+
102
+ class Production(Config):
103
+ LOGGER = True
104
+
105
+
106
+ class Development(Config):
107
+ LOGGER = True
108
+ # Override any value here for local dev without touching env
109
+ # Example:
110
+ # API_ID = 12345
111
+ # API_HASH = "abcdef..."
112
+ # TOKEN = "bot_token_here"
113
+ # OWNER_IDS = [123456789]
QueenNoxi/events.py ADDED
@@ -0,0 +1,179 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import inspect
2
+ import re
3
+ from pathlib import Path
4
+
5
+ from pymongo import MongoClient
6
+ from telethon import events
7
+
8
+ from QueenNoxi import MONGO_DB_URI, telethn
9
+
10
+ client = MongoClient()
11
+ client = MongoClient(MONGO_DB_URI)
12
+ db = client["QueenNoxi"]
13
+ gbanned = db.gban
14
+
15
+
16
+ def register(**args):
17
+ """Registers a new message."""
18
+ pattern = args.get("pattern", None)
19
+
20
+ r_pattern = r"^[/!.]"
21
+
22
+ if pattern is not None and not pattern.startswith("(?i)"):
23
+ args["pattern"] = "(?i)" + pattern
24
+
25
+ args["pattern"] = pattern.replace("^/", r_pattern, 1)
26
+
27
+ def decorator(func):
28
+ telethn.add_event_handler(func, events.NewMessage(**args))
29
+ return func
30
+
31
+ return decorator
32
+
33
+
34
+ def chataction(**args):
35
+ """Registers chat actions."""
36
+
37
+ def decorator(func):
38
+ telethn.add_event_handler(func, events.ChatAction(**args))
39
+ return func
40
+
41
+ return decorator
42
+
43
+
44
+ def userupdate(**args):
45
+ """Registers user updates."""
46
+
47
+ def decorator(func):
48
+ telethn.add_event_handler(func, events.UserUpdate(**args))
49
+ return func
50
+
51
+ return decorator
52
+
53
+
54
+ def inlinequery(**args):
55
+ """Registers inline query."""
56
+ pattern = args.get("pattern", None)
57
+
58
+ if pattern is not None and not pattern.startswith("(?i)"):
59
+ args["pattern"] = "(?i)" + pattern
60
+
61
+ def decorator(func):
62
+ telethn.add_event_handler(func, events.InlineQuery(**args))
63
+ return func
64
+
65
+ return decorator
66
+
67
+
68
+ def callbackquery(**args):
69
+ """Registers inline query."""
70
+
71
+ def decorator(func):
72
+ telethn.add_event_handler(func, events.CallbackQuery(**args))
73
+ return func
74
+
75
+ return decorator
76
+
77
+ def QueenNoxiinline(**args):
78
+ def decorator(func):
79
+ telethn.add_event_handler(func, events.CallbackQuery(**args))
80
+ return func
81
+
82
+ return decorator
83
+ def bot(**args):
84
+ pattern = args.get("pattern")
85
+ r_pattern = r"^[/]"
86
+
87
+ if pattern is not None and not pattern.startswith("(?i)"):
88
+ args["pattern"] = "(?i)" + pattern
89
+
90
+ args["pattern"] = pattern.replace("^/", r_pattern, 1)
91
+ stack = inspect.stack()
92
+ previous_stack_frame = stack[1]
93
+ file_test = Path(previous_stack_frame.filename)
94
+ file_test = file_test.stem.replace(".py", "")
95
+ reg = re.compile("(.*)")
96
+
97
+ if pattern is not None:
98
+ try:
99
+ cmd = re.search(reg, pattern)
100
+ try:
101
+ cmd = cmd.group(1).replace("$", "").replace("\\", "").replace("^", "")
102
+ except BaseException:
103
+ pass
104
+
105
+ try:
106
+ FUN_LIST[file_test].append(cmd)
107
+ except BaseException:
108
+ FUN_LIST.update({file_test: [cmd]})
109
+ except BaseException:
110
+ pass
111
+
112
+ def decorator(func):
113
+ async def wrapper(check):
114
+ if check.edit_date:
115
+ return
116
+ if check.fwd_from:
117
+ return
118
+ if check.is_group or check.is_private:
119
+ pass
120
+ else:
121
+ print("i don't work in channels")
122
+ return
123
+ if check.is_group:
124
+ if check.chat.megagroup:
125
+ pass
126
+ else:
127
+ print("i don't work in small chats")
128
+ return
129
+
130
+ users = gbanned.find({})
131
+ for c in users:
132
+ if check.sender_id == c["user"]:
133
+ return
134
+ try:
135
+ await func(check)
136
+ try:
137
+ LOAD_PLUG[file_test].append(func)
138
+ except Exception:
139
+ LOAD_PLUG.update({file_test: [func]})
140
+ except BaseException:
141
+ return
142
+ else:
143
+ pass
144
+
145
+ telethn.add_event_handler(wrapper, events.NewMessage(**args))
146
+ return wrapper
147
+
148
+ return decorator
149
+
150
+
151
+ def queennoxi(**args):
152
+ pattern = args.get("pattern", None)
153
+ args.get("disable_edited", False)
154
+ ignore_unsafe = args.get("ignore_unsafe", False)
155
+ unsafe_pattern = r"^[^/!#@\$A-Za-z]"
156
+ args.get("group_only", False)
157
+ args.get("disable_errors", False)
158
+ args.get("insecure", False)
159
+ if pattern is not None and not pattern.startswith("(?i)"):
160
+ args["pattern"] = "(?i)" + pattern
161
+
162
+ if "disable_edited" in args:
163
+ del args["disable_edited"]
164
+
165
+ if "ignore_unsafe" in args:
166
+ del args["ignore_unsafe"]
167
+
168
+ if "group_only" in args:
169
+ del args["group_only"]
170
+
171
+ if "disable_errors" in args:
172
+ del args["disable_errors"]
173
+
174
+ if "insecure" in args:
175
+ del args["insecure"]
176
+
177
+ if pattern:
178
+ if not ignore_unsafe:
179
+ args["pattern"] = args["pattern"].replace("^.", unsafe_pattern, 1)
QueenNoxi/modules/__init__.py ADDED
@@ -0,0 +1,54 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from QueenNoxi import LOAD, LOGGER, NO_LOAD
2
+
3
+
4
+ def __list_all_modules():
5
+ import glob
6
+ from os.path import basename, dirname, isfile
7
+
8
+ # This generates a list of modules in this folder for the * in __main__ to work.
9
+ mod_paths = glob.glob(dirname(__file__) + "/*.py")
10
+ to_load = []
11
+ for f in mod_paths:
12
+ if isfile(f) and f.endswith(".py") and not f.endswith("__init__.py"):
13
+ mod_name = basename(f)[:-3]
14
+ try:
15
+ with open(f, "r", encoding="utf-8") as file:
16
+ content = file.read()
17
+ if "import telegram" in content or "from telegram" in content or "dispatcher." in content:
18
+ if "from QueenNoxi import pbot" not in content and "@pbot.on_" not in content:
19
+ LOGGER.info(f"Skipping legacy module: {mod_name}")
20
+ continue
21
+ to_load.append(mod_name)
22
+ except Exception as e:
23
+ LOGGER.error(f"Error reading module {mod_name}: {e}")
24
+ continue
25
+
26
+ if LOAD or NO_LOAD:
27
+ all_modules = to_load
28
+ to_load = LOAD
29
+ if to_load:
30
+ if not all(
31
+ any(mod == module_name for module_name in all_modules)
32
+ for mod in to_load
33
+ ):
34
+ LOGGER.error("Invalid loadorder names. Quitting.")
35
+ quit(1)
36
+
37
+ all_modules = sorted(set(all_modules) - set(to_load))
38
+ to_load = list(all_modules) + to_load
39
+
40
+ else:
41
+ to_load = all_modules
42
+
43
+ if NO_LOAD:
44
+ LOGGER.info("Not loading: {}".format(NO_LOAD))
45
+ return [item for item in to_load if item not in NO_LOAD]
46
+
47
+ return to_load
48
+
49
+ return to_load
50
+
51
+
52
+ ALL_MODULES = __list_all_modules()
53
+ LOGGER.info("Modules to load: %s", str(ALL_MODULES))
54
+ __all__ = ALL_MODULES + ["ALL_MODULES"]
QueenNoxi/modules/admin.py ADDED
@@ -0,0 +1,363 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import html
2
+ import os
3
+ from pyrogram import filters, Client, enums
4
+ from pyrogram.raw import functions, types as raw_types
5
+ from pyrogram.types import (
6
+ Message,
7
+ InlineKeyboardMarkup,
8
+ InlineKeyboardButton,
9
+ ChatPrivileges
10
+ )
11
+ from pyrogram.errors import RPCError
12
+
13
+ from QueenNoxi import DRAGONS, pbot, BOT_ID
14
+ from QueenNoxi.modules.disable import DisableAbleCommandHandler
15
+ from QueenNoxi.modules.helper_funcs.admin_rights import user_can_changeinfo
16
+ from QueenNoxi.modules.helper_funcs.chat_status import (
17
+ bot_admin,
18
+ can_pin,
19
+ connection_status,
20
+ user_admin,
21
+ can_promote,
22
+ is_user_admin
23
+ )
24
+ from QueenNoxi.modules.helper_funcs.extraction import (
25
+ extract_user,
26
+ extract_user_and_text,
27
+ )
28
+ from QueenNoxi.modules.log_channel import loggable
29
+
30
+ @pbot.on_message(filters.command("setsticker") & filters.group)
31
+ @bot_admin
32
+ @user_admin
33
+ async def set_sticker(client: Client, message: Message):
34
+ chat = message.chat
35
+ user = message.from_user
36
+ if not await user_can_changeinfo(chat.id, user.id):
37
+ return await message.reply_text("» ʏᴏᴜ ᴅᴏɴ'ᴛ ʜᴀᴠᴇ ᴩᴇʀᴍɪssɪᴏɴs ᴛᴏ ᴄʜᴀɴɢᴇ ɢʀᴏᴜᴩ ɪɴғᴏ ʙᴀʙʏ !")
38
+
39
+ if message.reply_to_message and message.reply_to_message.sticker:
40
+ stkr = message.reply_to_message.sticker.set_name
41
+ try:
42
+ await client.set_chat_sticker_set(chat.id, stkr)
43
+ await message.reply_text(f"» sᴜᴄᴄᴇssғᴜʟʟʏ sᴇᴛ ɢʀᴏᴜᴩ sᴛɪᴄᴋᴇʀs ɪɴ {chat.title}!")
44
+ except RPCError as e:
45
+ await message.reply_text(f"Error: {e.MESSAGE}")
46
+ else:
47
+ await message.reply_text("» ʀᴇᴩʟʏ ᴛᴏ ᴀ sᴛɪᴄᴋᴇʀ ᴛᴏ sᴇᴛ ɪᴛ ᴀs ɢʀᴏᴜᴩ sᴛɪᴄᴋᴇʀ ᴩᴀᴄᴋ !")
48
+
49
+ @pbot.on_message(filters.command("setgpic") & filters.group)
50
+ @bot_admin
51
+ @user_admin
52
+ async def setchatpic(client: Client, message: Message):
53
+ chat = message.chat
54
+ user = message.from_user
55
+ if not await user_can_changeinfo(chat.id, user.id):
56
+ return await message.reply_text("» ʏᴏᴜ ᴅᴏɴ'ᴛ ʜᴀᴠᴇ ᴩᴇʀᴍɪssɪᴏɴs ᴛᴏ ᴄʜᴀɴɢᴇ ɢʀᴏᴜᴩ ɪɴғᴏ ʙᴀʙʏ !")
57
+
58
+ if message.reply_to_message and (message.reply_to_message.photo or message.reply_to_message.document):
59
+ dlmsg = await message.reply_text("» ᴄʜᴀɴɢɪɴɢ ɢʀᴏᴜᴩ's ᴩʀᴏғɪʟᴇ ᴩɪᴄ...")
60
+ img = await message.reply_to_message.download("gpic.png")
61
+ try:
62
+ await client.set_chat_photo(chat.id, photo=img)
63
+ await message.reply_text("» sᴜᴄᴄᴇssғᴜʟʟʏ sᴇᴛ ɢʀᴏᴜᴩ ᴩʀᴏғɪʟᴇ ᴩɪᴄ !")
64
+ except RPCError as e:
65
+ await message.reply_text(f"Error: {e.MESSAGE}")
66
+ finally:
67
+ await dlmsg.delete()
68
+ if os.path.exists("gpic.png"):
69
+ os.remove("gpic.png")
70
+ else:
71
+ await message.reply_text("» ʀᴇᴩʟʏ ᴛᴏ ᴀ ᴩʜᴏᴛᴏ ᴏʀ ғɪʟᴇ ᴛᴏ sᴇᴛ ɪᴛ ᴀs ɢʀᴏᴜᴩ ᴩʀᴏғɪʟᴇ ᴩɪᴄ !")
72
+
73
+ @pbot.on_message(filters.command("delgpic") & filters.group)
74
+ @bot_admin
75
+ @user_admin
76
+ async def rmchatpic(client: Client, message: Message):
77
+ chat = message.chat
78
+ user = message.from_user
79
+ if not await user_can_changeinfo(chat.id, user.id):
80
+ return await message.reply_text("» ʏᴏᴜ ᴅᴏɴ'ᴛ ʜᴀᴠᴇ ᴩᴇʀᴍɪssɪᴏɴs ᴛᴏ ᴄʜᴀɴɢᴇ ɢʀᴏUtᴩ ɪɴғᴏ ʙᴀʙʏ !")
81
+ try:
82
+ await client.delete_chat_photo(chat.id)
83
+ await message.reply_text("» sᴜᴄᴄᴇssғᴜʟʟʏ ᴅᴇʟᴇᴛᴇᴅ ɢʀᴏᴜᴩ's ᴅᴇғᴀᴜʟᴛ ᴩʀᴏғɪʟᴇ ᴩɪᴄ !")
84
+ except RPCError as e:
85
+ await message.reply_text(f"Error: {e.MESSAGE}")
86
+
87
+ @pbot.on_message(filters.command("setdesc") & filters.group)
88
+ @bot_admin
89
+ @user_admin
90
+ async def set_desc(client: Client, message: Message):
91
+ chat = message.chat
92
+ user = message.from_user
93
+ if not await user_can_changeinfo(chat.id, user.id):
94
+ return await message.reply_text("» ʏᴏᴜ ᴅᴏɴ'ᴛ ʜᴀᴠᴇ ᴩᴇʀᴍɪssɪᴏɴs ᴛᴏ ᴄʜᴀɴɢᴇ ɢʀᴏᴜᴩ ɪɴғᴏ ʙᴀʙʏ !")
95
+
96
+ desc = message.text.split(None, 1)[1] if len(message.command) > 1 else None
97
+ if not desc:
98
+ return await message.reply_text("» ᴡᴛғ, ʏᴏᴜ ᴡᴀɴᴛ ᴛᴏ sᴇᴛ ᴀɴ ᴇᴍᴩᴛʏ ᴅᴇsᴄʀɪᴩᴛɪᴏɴ !")
99
+ try:
100
+ await client.set_chat_description(chat.id, desc[:255])
101
+ await message.reply_text(f"» sᴜᴄᴄᴇssғᴜʟʟʏ ᴜᴩᴅᴀᴛᴇᴅ ᴄʜᴀᴛ ᴅᴇsᴄʀɪᴩᴛɪᴏɴ ɪɴ {chat.title}!")
102
+ except RPCError as e:
103
+ await message.reply_text(f"Error: {e.MESSAGE}")
104
+
105
+ @pbot.on_message(filters.command("setgtitle") & filters.group)
106
+ @bot_admin
107
+ @user_admin
108
+ async def setchat_title(client: Client, message: Message):
109
+ chat = message.chat
110
+ user = message.from_user
111
+ if not await user_can_changeinfo(chat.id, user.id):
112
+ return await message.reply_text("» ʏᴏᴜ ᴅᴏɴ'ᴛ ʜᴀᴠᴇ ᴩᴇʀᴍɪssɪᴏɴs ᴛᴏ ᴄʜᴀɴɢᴇ ɢʀᴏᴜᴩ ɪɴғᴏ ʙᴀʙʏ !")
113
+
114
+ title = message.text.split(None, 1)[1] if len(message.command) > 1 else None
115
+ if not title:
116
+ return await message.reply_text("» ᴇɴᴛᴇʀ sᴏᴍᴇ ᴛᴇxᴛ ᴛᴏ sᴇᴛ ɪᴛ ᴀs ɴᴇᴡ ᴄʜᴀᴛ ᴛɪᴛʟᴇ !")
117
+ try:
118
+ await client.set_chat_title(chat.id, title)
119
+ await message.reply_text(f"» sᴜᴄᴄᴇssғᴜʟʟʏ sᴇᴛ <b>{html.escape(title)}</b> ᴀs ɴᴇᴡ ᴄʜᴀᴛ ᴛɪᴛʟᴇ !")
120
+ except RPCError as e:
121
+ await message.reply_text(f"Error: {e.MESSAGE}")
122
+
123
+ @DisableAbleCommandHandler("promote", admin_ok=True)
124
+ @connection_status
125
+ @bot_admin
126
+ @can_promote
127
+ @user_admin
128
+ @loggable
129
+ async def promote(client: Client, message: Message) -> str:
130
+ chat = message.chat
131
+ user = message.from_user
132
+ user_id = await extract_user(message, message.command[1:])
133
+ if not user_id:
134
+ await message.reply_text("» ɪ ᴅᴏɴ'ᴛ ᴋɴᴏᴡ ᴡʜᴏ's ᴛʜᴀᴛ ᴜsᴇʀ.")
135
+ return
136
+ try:
137
+ user_member = await chat.get_member(user_id)
138
+ except:
139
+ return
140
+ if user_member.status in (enums.ChatMemberStatus.ADMINISTRATOR, enums.ChatMemberStatus.OWNER):
141
+ await message.reply_text("» ᴀᴄᴄᴏʀᴅɪɴɢ ᴛᴏ ᴍᴇ ᴛʜᴀᴛ ᴜsᴇʀ ɪs ᴀʟʀᴇᴀᴅʏ ᴀɴ ᴀᴅᴍɪɴ ʜᴇʀᴇ !")
142
+ return
143
+ if user_id == BOT_ID:
144
+ await message.reply_text("» ɪ ᴄᴀɴ'ᴛ ᴩʀᴏᴍᴏᴛᴇ ᴍʏsᴇʟғ.")
145
+ return
146
+
147
+ bot_member = await chat.get_member(BOT_ID)
148
+ try:
149
+ await client.promote_chat_member(chat.id, user_id, privileges=bot_member.privileges)
150
+ await message.reply_text(f"» ᴩʀᴏᴍᴏᴛɪɴɢ ᴀ ᴜsᴇʀ ɪɴ {chat.title}")
151
+ return f"<b>{html.escape(chat.title)}:</b>\n#ᴩʀᴏᴍᴏᴛᴇᴅ\n<b>ᴩʀᴏᴍᴏᴛᴇʀ :</b> {user.mention}\n<b>ᴜsᴇʀ :</b> {user_member.user.mention}"
152
+ except RPCError as e:
153
+ await message.reply_text(f"Error: {e.MESSAGE}")
154
+
155
+ @DisableAbleCommandHandler("demote", admin_ok=True)
156
+ @connection_status
157
+ @bot_admin
158
+ @can_promote
159
+ @user_admin
160
+ @loggable
161
+ async def demote(client: Client, message: Message) -> str:
162
+ chat = message.chat
163
+ user = message.from_user
164
+ user_id = await extract_user(message, message.command[1:])
165
+ if not user_id:
166
+ await message.reply_text("» ɪ ᴅᴏɴ'ᴛ ᴋɴᴏᴡ ᴡʜᴏ's ᴛʜᴀᴛ ᴜsᴇʀ.")
167
+ return
168
+ try:
169
+ user_member = await chat.get_member(user_id)
170
+ except:
171
+ return
172
+ if user_member.status == enums.ChatMemberStatus.OWNER:
173
+ return await message.reply_text("» ᴛʜᴀᴛ ᴜsᴇʀ ɪs ᴏᴡɴᴇʀ !")
174
+ if user_id == BOT_ID:
175
+ return await message.reply_text("» ɪ ᴄᴀɴ'ᴛ ᴅᴇᴍᴏᴛᴇ ᴍʏsᴇʟғ.")
176
+
177
+ try:
178
+ # Use raw API to fully strip admin — promote_chat_member with all False
179
+ # keeps the admin badge in Telegram; raw EditAdmin actually removes it.
180
+ channel = await client.resolve_peer(chat.id)
181
+ target = await client.resolve_peer(user_id)
182
+ await client.invoke(
183
+ functions.channels.EditAdmin(
184
+ channel=channel,
185
+ user_id=target,
186
+ admin_rights=raw_types.ChatAdminRights(
187
+ change_info=False,
188
+ post_messages=False,
189
+ edit_messages=False,
190
+ delete_messages=False,
191
+ ban_users=False,
192
+ invite_users=False,
193
+ pin_messages=False,
194
+ add_admins=False,
195
+ manage_call=False,
196
+ anonymous=False,
197
+ manage_topics=False,
198
+ post_stories=False,
199
+ edit_stories=False,
200
+ delete_stories=False,
201
+ ),
202
+ rank=""
203
+ )
204
+ )
205
+ await message.reply_text(f"» sᴜᴄᴄᴇssғᴜʟʟʏ ᴅᴇᴍᴏᴛᴇᴅ ɪɴ {chat.title}")
206
+ return f"<b>{html.escape(chat.title)}:</b>\n#ᴅᴇᴍᴏᴛᴇᴅ\n<b>ᴅᴇᴍᴏᴛᴇʀ :</b> {user.mention}\n<b>ᴅᴇᴍᴏᴛᴇᴅ :</b> {user_member.user.mention}"
207
+ except RPCError as e:
208
+ await message.reply_text(f"Error: {e.MESSAGE}")
209
+
210
+
211
+
212
+
213
+ @pbot.on_message(filters.command(["admincache", "reload", "refresh"]) & filters.group)
214
+ @user_admin
215
+ async def refresh_admin(client: Client, message: Message):
216
+ from QueenNoxi.modules.helper_funcs.chat_status import ADMIN_CACHE
217
+ try:
218
+ ADMIN_CACHE.pop(message.chat.id)
219
+ except KeyError:
220
+ pass
221
+ await message.reply_text("» sᴜᴄᴄᴇssғᴜʟʟʏ ʀᴇғʀᴇsʜᴇᴅ ᴀᴅᴍɪɴ ᴄᴀᴄʜᴇ !")
222
+
223
+ @pbot.on_message(filters.command("pin") & filters.group)
224
+ @bot_admin
225
+ @can_pin
226
+ @user_admin
227
+ @loggable
228
+ async def pin(client: Client, message: Message) -> str:
229
+ args = message.command[1:]
230
+ chat = message.chat
231
+ user = message.from_user
232
+ if not message.reply_to_message:
233
+ return await message.reply_text("» ʀᴇᴩʟʏ ᴛᴏ ᴀ ᴍᴇssᴀɢᴇ ᴛᴏ ᴩɪɴ ɪᴛ !")
234
+
235
+ is_silent = True
236
+ if len(args) >= 1:
237
+ is_silent = args[0].lower() not in ["notify", "loud", "violent"]
238
+ try:
239
+ await client.pin_chat_message(chat.id, message.reply_to_message.id, disable_notification=is_silent)
240
+ await message.reply_text("» sᴜᴄᴄᴇssғᴜʟʟʏ ᴩɪɴɴᴇᴅ ᴛʜᴀᴛ ᴍᴇssᴀɢᴇ.")
241
+ return f"<b>{html.escape(chat.title)}:</b>\nᴩɪɴɴᴇᴅ-ᴀ-ᴍᴇssᴀɢᴇ\n<b>ᴩɪɴɴᴇᴅ ʙʏ :</b> {user.mention}"
242
+ except RPCError as e:
243
+ await message.reply_text(f"Error: {e.MESSAGE}")
244
+
245
+ @pbot.on_message(filters.command("unpin") & filters.group)
246
+ @bot_admin
247
+ @can_pin
248
+ @user_admin
249
+ @loggable
250
+ async def unpin(client: Client, message: Message) -> str:
251
+ chat = message.chat
252
+ user = message.from_user
253
+ try:
254
+ if message.reply_to_message:
255
+ await client.unpin_chat_message(chat.id, message.reply_to_message.id)
256
+ else:
257
+ await client.unpin_chat_message(chat.id)
258
+ await message.reply_text("» sᴜᴄᴄᴇssғᴜʟʟʏ ᴜɴᴩɪɴɴᴇᴅ.")
259
+ return f"<b>{html.escape(chat.title)}:</b>\nᴜɴᴩɪɴɴᴇᴅ-ᴀ-ᴍᴇssᴀɢᴇ\n<b>ᴜɴᴩɪɴɴᴇᴅ ʙʏ :</b> {user.mention}"
260
+ except RPCError as e:
261
+ await message.reply_text(f"Error: {e.MESSAGE}")
262
+
263
+ @DisableAbleCommandHandler("invitelink", admin_ok=True)
264
+ @connection_status
265
+ @bot_admin
266
+ async def invite(client: Client, message: Message):
267
+ chat = message.chat
268
+ if chat.username:
269
+ return await message.reply_text(f"https://t.me/{chat.username}")
270
+ try:
271
+ invitelink = await client.export_chat_invite_link(chat.id)
272
+ await message.reply_text(invitelink)
273
+ except RPCError as e:
274
+ await message.reply_text(f"Error: {e.MESSAGE}")
275
+
276
+ @DisableAbleCommandHandler("title", admin_ok=True)
277
+ @connection_status
278
+ @bot_admin
279
+ @user_admin
280
+ async def set_admin_title(client: Client, message: Message):
281
+ chat = message.chat
282
+ user_id, title = await extract_user_and_text(message, message.command[1:])
283
+ if not user_id:
284
+ return await message.reply_text("» ɪ ᴅᴏɴ'ᴛ ᴋɴᴏᴡ ᴡʜᴏ's ᴛʜᴀᴛ ᴜsᴇʀ.")
285
+ try:
286
+ user_member = await chat.get_member(user_id)
287
+ except:
288
+ return
289
+ if user_member.status == enums.ChatMemberStatus.OWNER:
290
+ return await message.reply_text("» ᴛʜᴀᴛ ᴜsᴇʀ ɪs ᴏᴡɴᴇʀ !")
291
+ if user_member.status != enums.ChatMemberStatus.ADMINISTRATOR:
292
+ return await message.reply_text("» ɪ ᴄᴀɴ ᴏɴʟʏ sᴇᴛ ᴛɪᴛʟᴇ ғᴏʀ ᴀᴅᴍɪɴs !")
293
+ if not title:
294
+ return await message.reply_text("» sᴇᴛ ᴀ ᴛɪᴛʟᴇ ʙᴀʙʏ !")
295
+
296
+ try:
297
+ await client.set_administrator_custom_title(chat.id, user_id, title[:16])
298
+ await message.reply_text(f"» sᴜᴄᴄᴇssғᴜʟʟʏ sᴇᴛ ᴛɪᴛʟᴇ ғᴏʀ <code>{user_member.user.first_name}</code>")
299
+ except RPCError as e:
300
+ await message.reply_text(f"Error: {e.MESSAGE}")
301
+
302
+ @pbot.on_message(filters.command("pinned") & filters.group)
303
+ @bot_admin
304
+ async def pinned_msg(client: Client, message: Message):
305
+ chat = await client.get_chat(message.chat.id)
306
+ if chat.pinned_message:
307
+ pinned_id = chat.pinned_message.id
308
+ 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}"
309
+ await message.reply_text(f"ᴩɪɴɴᴇᴅ ᴏɴ {html.escape(chat.title)}.", reply_markup=InlineKeyboardMarkup([[InlineKeyboardButton("ᴍᴇssᴀɢᴇ", url=link)]]), disable_web_page_preview=True)
310
+ else:
311
+ await message.reply_text(f"» ᴛʜᴇʀᴇ's ɴᴏ ᴩɪɴɴᴇᴅ ᴍᴇssᴀɢᴇ ɪɴ <b>{html.escape(chat.title)}!</b>")
312
+
313
+ @DisableAbleCommandHandler(["admins", "staff"])
314
+ @connection_status
315
+ async def adminlist(client: Client, message: Message):
316
+ if message.chat.type == enums.ChatType.PRIVATE:
317
+ return await message.reply_text("» ᴛʜɪs ᴄᴏᴍᴍᴀɴᴅ ᴄᴀɴ ᴏɴʟʏ ʙᴇ ᴜsᴇᴅ ɪɴ ɢʀᴏᴜᴩ's.")
318
+ msg = await message.reply_text("» ғᴇᴛᴄʜɪɴɢ ᴀᴅᴍɪɴs ʟɪsᴛ...")
319
+ try:
320
+ administrators = []
321
+ async for m in client.get_chat_members(message.chat.id, filter=enums.ChatMembersFilter.ADMINISTRATORS):
322
+ administrators.append(m)
323
+ text = "ᴀᴅᴍɪɴs ɪɴ <b>{}</b>:".format(html.escape(message.chat.title))
324
+ creator = None; admins = []; bots = []
325
+ for admin in administrators:
326
+ if admin.status == enums.ChatMemberStatus.OWNER: creator = admin
327
+ elif admin.user.is_bot: bots.append(admin)
328
+ else: admins.append(admin)
329
+ if creator:
330
+ text += f"\n\n🥀 ᴏᴡɴᴇʀ :\n<code> • </code>{creator.user.mention}"
331
+ if creator.custom_title: text += f"\n<code> ┗━ {html.escape(creator.custom_title)}</code>"
332
+ if admins:
333
+ text += "\n\n💫 ᴀᴅᴍɪɴs :"
334
+ for a in admins:
335
+ text += f"\n<code> • </code>{a.user.mention}"
336
+ if a.custom_title: text += f" | <code>{html.escape(a.custom_title)}</code>"
337
+ if bots:
338
+ text += "\n\n🤖 ʙᴏᴛs :"
339
+ for b in bots: text += f"\n<code> • </code>{b.user.mention}"
340
+ await msg.edit_text(text)
341
+ except RPCError as e:
342
+ await msg.edit_text(f"Error: {e.MESSAGE}")
343
+
344
+ __mod_name__ = "Admins"
345
+ __help__ = """
346
+ *User Commands*:
347
+ » /admins: List of admins in the chat
348
+ » /pinned: To get the current pinned message.
349
+
350
+ *Admins only:*
351
+ » /promote: Promotes the user replied to
352
+ » /demote: Demotes the user replied to
353
+ » /title <title here>: Sets a custom title
354
+ » /admincache: Force refresh the admins list
355
+ » /setgtitle <text>: Set group title
356
+ » /setgpic: Reply to an image to set as group photo
357
+ » /delgpic: Delete group photo
358
+ » /setdesc: Set group description
359
+ » /setsticker: Set group sticker
360
+ » /pin: Silently pins the message
361
+ » /unpin: Unpins the currently pinned message
362
+ » /invitelink: Gets invitelink
363
+ """
QueenNoxi/modules/aiimage.py ADDED
@@ -0,0 +1,61 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """MIT License
2
+
3
+ Copyright (c) 2023-24 Noob-QueenNoxi
4
+
5
+ GITHUB: NOOB-MUKESH
6
+ TELEGRAM: @MR_SUKKUN
7
+
8
+ Permission is hereby granted, free of charge, to any person obtaining a copy
9
+ of this software and associated documentation files (the "Software"), to deal
10
+ in the Software without restriction, including without limitation the rights
11
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
12
+ copies of the Software, and to permit persons to whom the Software is
13
+ furnished to do so, subject to the following conditions:
14
+
15
+ The above copyright notice and this permission notice shall be included in all
16
+ copies or substantial portions of the Software.
17
+
18
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
21
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
23
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
24
+ SOFTWARE."""
25
+ from pyrogram import filters
26
+ from pyrogram.types import Message
27
+ from pyrogram.types import InputMediaPhoto
28
+ from .. import pbot as QueenNoxi,BOT_USERNAME
29
+ from MukeshAPI import api
30
+ from pyrogram.enums import ChatAction,ParseMode
31
+
32
+ @QueenNoxi.on_message(filters.command("imagine"))
33
+ async def imagine_(b, message: Message):
34
+ if message.reply_to_message:
35
+ text = message.reply_to_message.text
36
+ else:
37
+
38
+ text =message.text.split(None, 1)[1]
39
+ queennoxi=await message.reply_text( "`Please wait...,\n\nGenerating prompt .. ...`")
40
+ try:
41
+ await b.send_chat_action(message.chat.id, ChatAction.UPLOAD_PHOTO)
42
+ x=api.ai_image(text)
43
+ with open("queennoxi.jpg", 'wb') as f:
44
+ f.write(x)
45
+ caption = f"""
46
+ 💘sᴜᴄᴇssғᴜʟʟʏ ɢᴇɴᴇʀᴀᴛᴇᴅ : {text}
47
+ ✨ɢᴇɴᴇʀᴀᴛᴇᴅ ʙʏ : @{BOT_USERNAME}
48
+ 🥀ʀᴇǫᴜᴇsᴛᴇᴅ ʙʏ : {message.from_user.mention}
49
+ """
50
+ await queennoxi.delete()
51
+ await message.reply_photo("queennoxi.jpg",caption=caption,quote=True)
52
+ except Exception as e:
53
+ await queennoxi.edit_text(f"error {e}")
54
+
55
+ # -----------CREDITS -----------
56
+ # telegram : @legend_coder
57
+ # github : Alexainc
58
+ __mod_name__ = "Aɪ ɪᴍᴀɢᴇ"
59
+ __help__ = """
60
+ ➻ /imagine : ɢᴇɴᴇʀᴀᴛᴇ Aɪ ɪᴍᴀɢᴇ ғʀᴏᴍ ᴛᴇxᴛ
61
+ """
QueenNoxi/modules/alive.py ADDED
@@ -0,0 +1,70 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import asyncio
2
+ from platform import python_version as pyver
3
+ from pyrogram.enums import ChatType
4
+ from pyrogram import __version__ as pver
5
+ from pyrogram import filters
6
+ from pyrogram.types import InlineKeyboardButton, InlineKeyboardMarkup, Message
7
+ from telethon import __version__ as tver
8
+ from QueenNoxi.modules.no_sql.chats_db import add_served_chat
9
+ from QueenNoxi.modules.no_sql.users_db import save_id
10
+ from QueenNoxi import SUPPORT_CHAT, SUPPORT_CHAT_URL, pbot, BOT_USERNAME, OWNER_ID, BOT_NAME, START_IMG
11
+
12
+ PHOTO = [
13
+ "https://telegra.ph/file/d2a23fbe48129a7957887.jpg",
14
+ "https://telegra.ph/file/ddf30888de58d77911ee1.jpg",
15
+ "https://telegra.ph/file/268d66cad42dc92ec65ca.jpg",
16
+ "https://telegra.ph/file/13a0cbbff8f429e2c59ee.jpg",
17
+ "https://telegra.ph/file/bdfd86195221e979e6b20.jpg",
18
+ ]
19
+
20
+ QueenNoxi = [
21
+ [
22
+ InlineKeyboardButton(text="ᴏᴡɴᴇʀ", user_id=OWNER_ID),
23
+ InlineKeyboardButton(text="ꜱᴜᴘᴘᴏʀᴛ", url=SUPPORT_CHAT_URL),
24
+ ],
25
+ [
26
+ InlineKeyboardButton(
27
+ text="➕ᴀᴅᴅ ᴍᴇ ᴇʟsᴇ ʏᴏᴜʀ ɢʀᴏᴜᴘ➕",
28
+ url=f"https://t.me/{BOT_USERNAME}?startgroup=true",
29
+ ),
30
+ ],
31
+ ]
32
+
33
+ @pbot.on_message(filters.command("alive"))
34
+ async def alive(client, m: Message):
35
+ await m.delete()
36
+ accha = await m.reply("⚡")
37
+ await asyncio.sleep(0.2)
38
+ await accha.edit("ᴅɪɴɢ ᴅᴏɴɢ ꨄ︎ ᴀʟɪᴠɪɴɢ..")
39
+ await accha.delete()
40
+ await asyncio.sleep(0.3)
41
+
42
+ umm = await m.reply_sticker(
43
+ "CAACAgUAAxkDAAJHbmLuy2NEfrfh6lZSohacEGrVjd5wAAIOBAACl42QVKnra4sdzC_uKQQ"
44
+ )
45
+ await umm.delete()
46
+ owner = await client.get_users(OWNER_ID)
47
+ await m.reply_photo(
48
+ START_IMG,
49
+ caption=f"""**ʜᴇʏ, ɪ ᴀᴍ 『[{BOT_NAME}](t.me/{BOT_USERNAME})』**
50
+ ━━━━━━━━━━━━━━━━━━━
51
+ » **ᴍʏ ᴏᴡɴᴇʀ :** {owner.mention}
52
+
53
+ » **ᴛᴇʟᴇᴛʜᴏɴ :** `{tver}`
54
+
55
+ » **ᴘʏʀᴏɢʀᴀᴍ :** `{pver}`
56
+
57
+ » **ᴘʏᴛʜᴏɴ :** `{pyver()}`
58
+ ━━━━━━━━━━━━━━━━━━━""",
59
+ reply_markup=InlineKeyboardMarkup(QueenNoxi)
60
+ )
61
+
62
+ @pbot.on_message(group=1)
63
+ async def save_statss(_, m: Message):
64
+ try:
65
+ if m.chat.type == ChatType.PRIVATE:
66
+ await save_id(m.from_user.id)
67
+ else:
68
+ await add_served_chat(m.chat.id)
69
+ except Exception:
70
+ pass
QueenNoxi/modules/animation.py ADDED
@@ -0,0 +1,344 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import asyncio
2
+ import aiohttp
3
+ import os
4
+ import random
5
+ from pyrogram import filters, Client
6
+ from pyrogram.errors import FloodWait, RPCError
7
+
8
+ from pyrogram.types import Message
9
+ from QueenNoxi import pbot, LOGGER
10
+ from QueenNoxi.config import Config
11
+ TENOR_API_KEY = Config.TENOR_API_KEY
12
+ DRAGONS = Config.DRAGONS
13
+ OWNER_IDS = Config.OWNER_IDS
14
+ SUDOERS = list(set(DRAGONS + OWNER_IDS))
15
+
16
+ import zipfile
17
+ import shutil
18
+ from QueenNoxi.modules.disable import DisableAbleCommandHandler
19
+
20
+
21
+ from QueenNoxi.modules.helper_funcs.chat_status import user_admin
22
+
23
+ # ── Setup Caching ────────────────────────────────────────────────────────────
24
+ CACHE_DIR = "QueenNoxi/resources/animation_cache"
25
+ if not os.path.exists(CACHE_DIR):
26
+ os.makedirs(CACHE_DIR)
27
+
28
+ async def get_random_gif(category: str):
29
+ """Fetch a random GIF URL from Tenor or local cache."""
30
+ tenor_map = {
31
+ "kill": "kill", "love": "love", "hug": "hug", "slap": "slap",
32
+ "pat": "pat", "kiss": "kiss", "brain": "think", "moon": "sleep",
33
+ "hack": "hacker", "police": "police", "bombs": "explosion", "clock": "clock"
34
+ }
35
+
36
+ action = tenor_map.get(category, category)
37
+ cat_dir = os.path.join(CACHE_DIR, category)
38
+ if not os.path.exists(cat_dir):
39
+ os.makedirs(cat_dir)
40
+
41
+ # 1. Try Tenor API
42
+ if TENOR_API_KEY:
43
+ try:
44
+ query = f"{action} anime".replace(" ", "%20")
45
+ url = f"https://tenor.googleapis.com/v2/search?q={query}&key={TENOR_API_KEY}&limit=5&random=true"
46
+ async with aiohttp.ClientSession() as session:
47
+ async with session.get(url, timeout=5) as resp:
48
+ if resp.status == 200:
49
+ data = await resp.json()
50
+ if data.get('results'):
51
+ chosen = random.choice(data['results'])
52
+ gif_url = chosen['media_formats']['gif']['url']
53
+
54
+ # Async Background Download for Cache (best effort)
55
+ asyncio.create_task(cache_gif(category, gif_url))
56
+ return gif_url
57
+ except Exception as e:
58
+ LOGGER.warning(f"Tenor API error for {category}: {e}")
59
+
60
+ # 2. Fallback to Local Cache
61
+ cached_files = [f for f in os.listdir(cat_dir) if f.endswith(".gif")]
62
+ if cached_files:
63
+ local_file = random.choice(cached_files)
64
+ return os.path.join(cat_dir, local_file)
65
+
66
+ return None
67
+
68
+ async def cache_gif(category: str, url: str):
69
+ """Download and save a GIF to the local cache if it doesn't exist."""
70
+ cat_dir = os.path.join(CACHE_DIR, category)
71
+ # Limit cache size per category (e.g. 10 files)
72
+ if len(os.listdir(cat_dir)) >= 15:
73
+ return
74
+
75
+ file_id = url.split("/")[-2] if "/" in url else str(random.randint(1000, 9999))
76
+ file_path = os.path.join(cat_dir, f"{file_id}.gif")
77
+ if os.path.exists(file_path):
78
+ return
79
+
80
+ try:
81
+ async with aiohttp.ClientSession() as session:
82
+ async with session.get(url, timeout=10) as resp:
83
+ if resp.status == 200:
84
+ data = await resp.read()
85
+ with open(file_path, "wb") as f:
86
+ f.write(data)
87
+ except Exception:
88
+ pass
89
+
90
+
91
+
92
+
93
+
94
+
95
+
96
+
97
+ # ── Animation lists ───────────────────────────────────────────────────────────
98
+ brain_chain = [
99
+ "🧠", "🧠💥", "💭🧠", "🤯", "🧠🔥", "💡🧠", "🤔🧠",
100
+ "💥🧠💥", "🧠📤", "📤🗑️", "🗑️💨", "🧠❌", "🗑️", "✅"
101
+ ]
102
+
103
+ clock_ani = [
104
+ "🕛", "🕧", "🕐", "🕜", "🕑", "🕝", "🕒", "🕞", "🕓", "🕟", "🕔"
105
+ ]
106
+
107
+ police_ani = [
108
+ "🚓", "🚓💨", "🚔", "🚔💨", "🚨", "🚨🚨", "👮", "👮‍♂️🚓",
109
+ "🚓🚨👮", "🚨🚨👮‍♂️", "👮‍♂️🔦"
110
+ ]
111
+
112
+ moon_ani = [
113
+ "🌑", "🌒", "🌓", "🌔", "🌕", "🌖", "🌗", "🌘", "🌑", "🌒",
114
+ "🌓", "🌔", "🌕", "🌖", "🌗", "🌘", "🌑", "🌒", "🌓", "🌔",
115
+ "🌕", "🌕✨", "🌕🌟", "🌕💫", "🌕⭐", "🌕🌟✨", "🌔", "🌓",
116
+ "🌒", "🌑", "🌙", "🌙✨"
117
+ ]
118
+
119
+ bomb_ettu = [
120
+ "💣", "💣💣", "💣💣💣", "💣💣💣💣", "💥", "💥💥", "💥💥💥", "🔥💥", "☠️"
121
+ ]
122
+
123
+ hack_you = [
124
+ "🖥️ Booting...", "🔍 Scanning target...", "🔓 Bypassing firewall...",
125
+ "💻 Injecting payload...", "📡 Connecting...", "🔑 Cracking password...",
126
+ "📂 Accessing files...", "📊 Downloading data...", "🗄️ Extracting...",
127
+ "📤 Uploading backdoor...", "🔐 Locking access...", "🎭 Covering tracks...",
128
+ "🧹 Cleaning logs...", "⚙️ Finalizing...", "✅ Access granted!",
129
+ "📁 All data secured.", "🏴‍☠️ Mission complete.", "😎 You've been hacked!"
130
+ ]
131
+
132
+ love_siren = [
133
+ "❤️", "🧡", "💛", "💚", "💙", "💜", "🖤", "🤍", "🤎", "❤️‍🔥",
134
+ "💗", "💓", "💞", "💕", "💝", "💖", "💘", "💟", "❣️", "❤️",
135
+ "💑", "👫", "💏", "💞", "💕❤️", "💖💖", "💗💗", "💓❤️💓",
136
+ "❤️‍🔥❤️", "💘💘", "💞💞💞", "💖✨", "💝🌹", "🌹❤️", "True Love💞"
137
+ ]
138
+
139
+ kill_you = [
140
+ "🔫", "🔫💨", "😵", "😵‍💫", "💀", "⚰️🕯️", "🪦", "😱💀",
141
+ "🔫😵", "💀☠️", "⚰️", "☠️"
142
+ ]
143
+
144
+ slap_ani = ["Slapping...", "SLAP! 👋", "Ouch! 💥", "👋💥", "😵"]
145
+ pat_ani = ["Patting...", "Pat pat... ✨", "Good job! 💖", "✋✨", "😊"]
146
+ hug_ani = ["Hugging...", "HUG! 🤗", "Warm hugs! ❤️", "🫂❤️", "✨"]
147
+ kiss_ani = ["Kissing...", "KISS! 💋", "Muah! 💘", "😘💋", "🔥"]
148
+
149
+
150
+ # ── Helpers ───────────────────────────────────────────────────────────────────
151
+
152
+ async def send_gif_with_caption(client: Client, chat_id: int, gif_key: str, caption: str):
153
+ """Attempt to send a GIF with a caption."""
154
+ url = await get_random_gif(gif_key)
155
+ if not url:
156
+ return None
157
+
158
+ try:
159
+ # Try sending by URL first
160
+ return await client.send_animation(chat_id, url, caption=caption)
161
+ except Exception as e:
162
+ # Fallback: Download and send
163
+ try:
164
+ headers = {"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"}
165
+ async with aiohttp.ClientSession(headers=headers) as session:
166
+ async with session.get(url, timeout=20) as resp:
167
+ if resp.status == 200:
168
+ data = await resp.read()
169
+ temp_name = f"temp_{gif_key}_{random.randint(100,999)}.gif"
170
+ with open(temp_name, "wb") as f:
171
+ f.write(data)
172
+
173
+ sent = await client.send_animation(chat_id, temp_name, caption=caption)
174
+ os.remove(temp_name)
175
+ return sent
176
+ else:
177
+ LOGGER.warning(f"GIF Download failed for {gif_key} ({url}): Status {resp.status}")
178
+ except Exception as e2:
179
+ LOGGER.warning(f"GIF Fallback failed for {gif_key} ({url}): {e2}")
180
+ return None
181
+
182
+
183
+ async def animate(client: Client, message: Message, gif_key: str, frames: list, action_verb: str, sleep: float = 0.5):
184
+ """Perform text animation, then send GIF with caption."""
185
+ if not message.reply_to_message:
186
+ await message.reply_text(f"❗ Please reply to a user to {gif_key} them!")
187
+ return
188
+
189
+ sender = message.from_user.mention
190
+ target = message.reply_to_message.from_user.mention
191
+ caption = f"✨ {sender} {action_verb} {target}! ✨"
192
+
193
+ # Start text animation
194
+ msg = await message.reply_text(frames[0])
195
+
196
+ # Avoid FloodWait: Limit to ~8 edits if the list is long
197
+ total_frames = len(frames)
198
+ step = 1
199
+ if total_frames > 8:
200
+ step = total_frames // 7
201
+ if step == 0: step = 1
202
+
203
+ for x in range(step, total_frames, step):
204
+ try:
205
+ await msg.edit_text(frames[x])
206
+ await asyncio.sleep(sleep)
207
+ except FloodWait as e:
208
+ await asyncio.sleep(e.value)
209
+ except RPCError:
210
+ break
211
+
212
+ # Send the GIF with caption
213
+ gif_msg = await send_gif_with_caption(client, message.chat.id, gif_key, caption)
214
+
215
+ # Only delete the temporary text animation message if GIF succeeded
216
+ # Otherwise, edit it to keep the result visible
217
+ if gif_msg:
218
+ try:
219
+ await msg.delete()
220
+ except:
221
+ pass
222
+ else:
223
+ try:
224
+ await msg.edit_text(caption)
225
+ except:
226
+ pass
227
+
228
+
229
+
230
+
231
+ # ── Commands ──────────────────────────────────────────────────────────────────
232
+ @pbot.on_message(filters.command("dlanimech"))
233
+ async def dlanimech(client: Client, message: Message):
234
+ """Zip and send animation cache to Sudoers in PM."""
235
+ if message.from_user.id not in SUDOERS:
236
+ await message.reply_text("❌ This command is only for Sudoers/Owners!")
237
+ return
238
+
239
+ msg = await message.reply_text("📦 Zipping animation cache...")
240
+
241
+ zip_path = "anim_cache.zip"
242
+ try:
243
+ # Create zip
244
+ with zipfile.ZipFile(zip_path, 'w', zipfile.ZIP_DEFLATED) as zipf:
245
+ for root, dirs, files in os.walk(CACHE_DIR):
246
+ for file in files:
247
+ zipf.write(
248
+ os.path.join(root, file),
249
+ os.path.relpath(os.path.join(root, file), os.path.join(CACHE_DIR, '..'))
250
+ )
251
+
252
+ # Send in PM
253
+ await client.send_document(
254
+ chat_id=message.from_user.id,
255
+ document=zip_path,
256
+ caption="📂 Here is the current Animation Cache!",
257
+ file_name="animation_cache.zip"
258
+ )
259
+ await msg.edit_text("✅ Sent to your PM!")
260
+ except Exception as e:
261
+ await msg.edit_text(f"❌ Error: {e}")
262
+ finally:
263
+ if os.path.exists(zip_path):
264
+ os.remove(zip_path)
265
+
266
+ @pbot.on_message(filters.command("brain") & filters.group)
267
+ @DisableAbleCommandHandler("brain")
268
+ async def brainanimation(client: Client, message: Message):
269
+ await animate(client, message, "brain", brain_chain, "put brain in dustbin for")
270
+
271
+ @pbot.on_message(filters.command("clock") & filters.group)
272
+ @DisableAbleCommandHandler("clock")
273
+ async def clockanimation(client: Client, message: Message):
274
+ await animate(client, message, "clock", clock_ani, "reminded time to")
275
+
276
+ @pbot.on_message(filters.command("police") & filters.group)
277
+ @DisableAbleCommandHandler("police")
278
+ async def policeanimation(client: Client, message: Message):
279
+ await animate(client, message, "police", police_ani, "called police for")
280
+
281
+ @pbot.on_message(filters.command("moon") & filters.group)
282
+ @DisableAbleCommandHandler("moon")
283
+ async def moonanimation(client: Client, message: Message):
284
+ await animate(client, message, "moon", moon_ani, "wished good night to")
285
+
286
+ @pbot.on_message(filters.command("bombs") & filters.group)
287
+ @DisableAbleCommandHandler("bombs")
288
+ async def bombs(client: Client, message: Message):
289
+ await animate(client, message, "bombs", bomb_ettu, "bombed")
290
+
291
+ @pbot.on_message(filters.command("hack") & filters.group)
292
+ @DisableAbleCommandHandler("hack")
293
+ async def hack(client: Client, message: Message):
294
+ await animate(client, message, "hack", hack_you, "hacked")
295
+
296
+ @pbot.on_message(filters.command("love") & filters.group)
297
+ @DisableAbleCommandHandler("love")
298
+ async def love(client: Client, message: Message):
299
+ await animate(client, message, "love", love_siren, "is expressing love to")
300
+
301
+ @pbot.on_message(filters.command("kill") & filters.group)
302
+ @DisableAbleCommandHandler("kill")
303
+ async def kill(client: Client, message: Message):
304
+ await animate(client, message, "kill", kill_you, "killed")
305
+
306
+ @pbot.on_message(filters.command("slap") & filters.group)
307
+ @DisableAbleCommandHandler("slap")
308
+ async def slap(client: Client, message: Message):
309
+ await animate(client, message, "slap", slap_ani, "slapped")
310
+
311
+ @pbot.on_message(filters.command("pat") & filters.group)
312
+ @DisableAbleCommandHandler("pat")
313
+ async def pat(client: Client, message: Message):
314
+ await animate(client, message, "pat", pat_ani, "patted")
315
+
316
+ @pbot.on_message(filters.command("hug") & filters.group)
317
+ @DisableAbleCommandHandler("hug")
318
+ async def hug(client: Client, message: Message):
319
+ await animate(client, message, "hug", hug_ani, "hugged")
320
+
321
+ @pbot.on_message(filters.command("kiss") & filters.group)
322
+ @DisableAbleCommandHandler("kiss")
323
+ async def kiss(client: Client, message: Message):
324
+ await animate(client, message, "kiss", kiss_ani, "kissed")
325
+
326
+
327
+
328
+ __mod_name__ = "Animation"
329
+ __help__ = """
330
+ *ғᴀᴋᴇ ᴀɴɪᴍᴀᴛɪᴏɴ ᴄᴏᴍᴍᴀɴᴅs*
331
+ • `/love` — ʟᴏᴠᴇ ᴀɴɪᴍᴀᴛɪᴏɴ
332
+ • `/hack` — ʜᴀᴄᴋ ᴀɴɪᴍᴀᴛɪᴏɴ
333
+ • `/moon` — ᴍᴏᴏɴ ᴀɴɪᴍᴀᴛɪᴏɴ
334
+ • `/kill` — ᴋɪʟʟ ᴀɴɪᴍᴀᴛɪᴏɴ
335
+ • `/slap` — sʟᴀᴩ ᴀɴɪᴍᴀᴛɪᴏɴ
336
+ • `/pat` — ᴩᴀᴛ ᴀɴɪᴍᴀᴛɪᴏɴ
337
+ • `/hug` — ʜᴜɢ ᴀɴɪᴍᴀᴛɪᴏɴ
338
+ • `/kiss` — ᴋɪss ᴀɴɪᴍᴀᴛɪᴏɴ
339
+ • `/bombs` — ʙᴏᴍʙ ᴀɴɪᴍᴀᴛɪᴏɴ
340
+ • `/police` — ᴩᴏʟɪᴄᴇ ᴀɴɪᴍᴀᴛɪᴏɴ
341
+ • `/brain` — ʙʀᴀɪɴ ᴀɴɪᴍᴀᴛɪᴏɴ
342
+ • `/clock` — ᴄʟᴏᴄᴋ ᴀɴɪᴍᴀᴛɪᴏɴ
343
+ """
344
+
QueenNoxi/modules/anime.py ADDED
@@ -0,0 +1,110 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ import random
3
+ import aiohttp
4
+ from pyrogram import filters, Client, enums
5
+ from pyrogram.types import (
6
+ Message,
7
+ InlineKeyboardMarkup,
8
+ InlineKeyboardButton,
9
+ CallbackQuery
10
+ )
11
+
12
+ from QueenNoxi import pbot, OWNER_ID
13
+ from QueenNoxi.modules.disable import DisableAbleCommandHandler
14
+
15
+ QUOTES_IMG = [
16
+ "https://i.imgur.com/Iub4RYj.jpg",
17
+ "https://i.imgur.com/uvNMdIl.jpg",
18
+ "https://i.imgur.com/YOBOntg.jpg",
19
+ "https://i.imgur.com/fFpO2ZQ.jpg",
20
+ "https://i.imgur.com/f0xZceK.jpg",
21
+ "https://i.imgur.com/RlVcCip.jpg",
22
+ "https://i.imgur.com/CjpqLRF.jpg",
23
+ "https://i.imgur.com/8BHZDk6.jpg",
24
+ "https://i.imgur.com/8bHeMgy.jpg",
25
+ "https://i.imgur.com/5K3lMvr.jpg",
26
+ "https://i.imgur.com/NTzw4RN.jpg",
27
+ "https://i.imgur.com/wJxryAn.jpg",
28
+ "https://i.imgur.com/9L0DWzC.jpg",
29
+ "https://i.imgur.com/sBe8TTs.jpg",
30
+ "https://i.imgur.com/1Au8gdf.jpg",
31
+ "https://i.imgur.com/28hFQeU.jpg",
32
+ "https://i.imgur.com/Qvc03JY.jpg",
33
+ "https://i.imgur.com/gSX6Xlf.jpg",
34
+ "https://i.imgur.com/iP26Hwa.jpg",
35
+ "https://i.imgur.com/uSsJoX8.jpg",
36
+ "https://i.imgur.com/OvX3oHB.jpg",
37
+ "https://i.imgur.com/JMWuksm.jpg",
38
+ "https://i.imgur.com/lhM3fib.jpg",
39
+ "https://i.imgur.com/64IYKkw.jpg",
40
+ "https://i.imgur.com/nMbyA3J.jpg",
41
+ "https://i.imgur.com/7KFQhY3.jpg",
42
+ "https://i.imgur.com/mlKb7zt.jpg",
43
+ "https://i.imgur.com/JCQGJVw.jpg",
44
+ "https://i.imgur.com/hSFYDEz.jpg",
45
+ "https://i.imgur.com/PQRjAgl.jpg",
46
+ "https://i.imgur.com/ot9624U.jpg",
47
+ "https://i.imgur.com/iXmqN9y.jpg",
48
+ "https://i.imgur.com/RhNBeGr.jpg",
49
+ "https://i.imgur.com/tcMVNa8.jpg",
50
+ "https://i.imgur.com/LrVg810.jpg",
51
+ "https://i.imgur.com/TcWfQlz.jpg",
52
+ "https://i.imgur.com/muAUdvJ.jpg",
53
+ "https://i.imgur.com/AtC7ZRV.jpg",
54
+ "https://i.imgur.com/sCObQCQ.jpg",
55
+ "https://i.imgur.com/AJFDI1r.jpg",
56
+ "https://i.imgur.com/TCgmRrH.jpg",
57
+ "https://i.imgur.com/LMdmhJU.jpg",
58
+ "https://i.imgur.com/eyyax0N.jpg",
59
+ "https://i.imgur.com/YtYxV66.jpg",
60
+ "https://i.imgur.com/289f9cebe37f31a943f98.jpg",
61
+ ]
62
+
63
+ async def anime_quote():
64
+ url = "https://animechan.vercel.app/api/random"
65
+ async with aiohttp.ClientSession() as session:
66
+ async with session.get(url) as response:
67
+ if response.status == 200:
68
+ dic = await response.json()
69
+ return dic["quote"], dic["character"], dic["anime"]
70
+ return "No quote found.", "Unknown", "Unknown"
71
+
72
+ @pbot.on_message(filters.command("quote"))
73
+ @DisableAbleCommandHandler("quote")
74
+ async def quotes(client: Client, message: Message):
75
+ quote, character, anime = await anime_quote()
76
+ msg = f"<i>❝{quote}❞</i>\n\n<b>{character} from {anime}</b>"
77
+ keyboard = InlineKeyboardMarkup(
78
+ [[InlineKeyboardButton(text="Change🔁", callback_data="change_quote")]]
79
+ )
80
+ await message.reply_text(
81
+ msg,
82
+ reply_markup=keyboard,
83
+ parse_mode=enums.ParseMode.HTML,
84
+ )
85
+
86
+ @pbot.on_callback_query(filters.regex(r"change_quote|quote_change"))
87
+ async def change_quote_btn(client: Client, query: CallbackQuery):
88
+ quote, character, anime = await anime_quote()
89
+ msg = f"<i>❝{quote}❞</i>\n\n<b>{character} from {anime}</b>"
90
+ keyboard = InlineKeyboardMarkup(
91
+ [[InlineKeyboardButton(text="ᴄʜᴀɴɢᴇ🔁", callback_data="quote_change")]]
92
+ )
93
+ try:
94
+ await query.message.edit_text(msg, reply_markup=keyboard, parse_mode=enums.ParseMode.HTML)
95
+ except:
96
+ await query.answer("Error or same content.")
97
+
98
+ @pbot.on_message(filters.command("animequotes"))
99
+ @DisableAbleCommandHandler("animequotes")
100
+ async def animequotes_cmd(client: Client, message: Message):
101
+ if message.reply_to_message:
102
+ await message.reply_to_message.reply_photo(random.choice(QUOTES_IMG))
103
+ else:
104
+ await message.reply_photo(random.choice(QUOTES_IMG))
105
+
106
+ __mod_name__ = "Quotes"
107
+ __help__ = """
108
+ • `/quote`: Get a random anime quote.
109
+ • `/animequotes`: Get a random anime quote image.
110
+ """
QueenNoxi/modules/animez.py ADDED
@@ -0,0 +1,415 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import datetime
2
+ import html
3
+ import textwrap
4
+ import aiohttp
5
+ import bs4
6
+ from jikanpy import AioJikan
7
+ from pyrogram import filters, Client
8
+ from pyrogram.types import InlineKeyboardButton, InlineKeyboardMarkup, Message
9
+
10
+ from QueenNoxi import pbot
11
+ from QueenNoxi.modules.disable import DisableAbleCommandHandler
12
+
13
+ info_btn = "More Information"
14
+ close_btn = "Close ❌"
15
+
16
+ def shorten(description, info="anilist.co"):
17
+ msg = ""
18
+ if len(description) > 700:
19
+ description = description[0:500] + "...."
20
+ msg += f"\n**Description**: _{description}_[Read More]({info})"
21
+ else:
22
+ msg += f"\n**Description**:_{description}_"
23
+ return msg
24
+
25
+ def t(milliseconds: int) -> str:
26
+ seconds, milliseconds = divmod(int(milliseconds), 1000)
27
+ minutes, seconds = divmod(seconds, 60)
28
+ hours, minutes = divmod(minutes, 60)
29
+ days, hours = divmod(hours, 24)
30
+ tmp = (
31
+ ((str(days) + " Days, ") if days else "")
32
+ + ((str(hours) + " Hours, ") if hours else "")
33
+ + ((str(minutes) + " Minutes, ") if minutes else "")
34
+ + ((str(seconds) + " Seconds, ") if seconds else "")
35
+ + ((str(milliseconds) + " ms, ") if milliseconds else "")
36
+ )
37
+ return tmp[:-2]
38
+
39
+ airing_query = """
40
+ query ($id: Int,$search: String) {
41
+ Media (id: $id, type: ANIME,search: $search) {
42
+ id
43
+ episodes
44
+ title {
45
+ romaji
46
+ english
47
+ native
48
+ }
49
+ nextAiringEpisode {
50
+ airingAt
51
+ timeUntilAiring
52
+ episode
53
+ }
54
+ }
55
+ }
56
+ """
57
+
58
+ anime_query = """
59
+ query ($id: Int,$search: String) {
60
+ Media (id: $id, type: ANIME,search: $search) {
61
+ id
62
+ title {
63
+ romaji
64
+ english
65
+ native
66
+ }
67
+ description (asHtml: false)
68
+ startDate{
69
+ year
70
+ }
71
+ episodes
72
+ season
73
+ type
74
+ format
75
+ status
76
+ duration
77
+ siteUrl
78
+ studios{
79
+ nodes{
80
+ name
81
+ }
82
+ }
83
+ trailer{
84
+ id
85
+ site
86
+ thumbnail
87
+ }
88
+ averageScore
89
+ genres
90
+ bannerImage
91
+ }
92
+ }
93
+ """
94
+
95
+ character_query = """
96
+ query ($query: String) {
97
+ Character (search: $query) {
98
+ id
99
+ name {
100
+ first
101
+ last
102
+ full
103
+ }
104
+ siteUrl
105
+ image {
106
+ large
107
+ }
108
+ description
109
+ }
110
+ }
111
+ """
112
+
113
+ manga_query = """
114
+ query ($id: Int,$search: String) {
115
+ Media (id: $id, type: MANGA,search: $search) {
116
+ id
117
+ title {
118
+ romaji
119
+ english
120
+ native
121
+ }
122
+ description (asHtml: false)
123
+ startDate{
124
+ year
125
+ }
126
+ type
127
+ format
128
+ status
129
+ siteUrl
130
+ averageScore
131
+ genres
132
+ bannerImage
133
+ }
134
+ }
135
+ """
136
+
137
+ url = "https://graphql.anilist.co"
138
+
139
+ async def get_response(query, variables):
140
+ async with aiohttp.ClientSession() as session:
141
+ async with session.post(url, json={"query": query, "variables": variables}) as resp:
142
+ return await resp.json()
143
+
144
+ @pbot.on_message(filters.command("airing"))
145
+ @DisableAbleCommandHandler("airing")
146
+ async def airing(client: Client, message: Message):
147
+ search_str = message.text.split(None, 1)[1] if len(message.command) > 1 else None
148
+ if not search_str and message.reply_to_message:
149
+ search_str = message.reply_to_message.text
150
+
151
+ if not search_str:
152
+ await message.reply_text("Tell Anime Name :) ( /airing <anime name>)")
153
+ return
154
+
155
+ data = await get_response(airing_query, {"search": search_str})
156
+ if not data or "data" not in data or not data["data"]["Media"]:
157
+ await message.reply_text("Anime not found")
158
+ return
159
+
160
+ response = data["data"]["Media"]
161
+ msg = f"**Name**: **{response['title']['romaji']}**(`{response['title']['native']}`)\n**ID**: `{response['id']}`"
162
+ if response["nextAiringEpisode"]:
163
+ time_val = response["nextAiringEpisode"]["timeUntilAiring"] * 1000
164
+ time_str = t(time_val)
165
+ msg += f"\n**Episode**: `{response['nextAiringEpisode']['episode']}`\n**Airing In**: `{time_str}`"
166
+ else:
167
+ msg += f"\n**Episode**:{response['episodes']}\n**Status**: `N/A`"
168
+ await message.reply_text(msg)
169
+
170
+ @pbot.on_message(filters.command("anime"))
171
+ @DisableAbleCommandHandler("anime")
172
+ async def anime(client: Client, message: Message):
173
+ search = message.text.split(None, 1)[1] if len(message.command) > 1 else None
174
+ if not search and message.reply_to_message:
175
+ search = message.reply_to_message.text
176
+
177
+ if not search:
178
+ await message.reply_text("Format : /anime < anime name >")
179
+ return
180
+
181
+ data = await get_response(anime_query, {"search": search})
182
+ if not data or "data" not in data or not data["data"]["Media"]:
183
+ await message.reply_text("Anime not found")
184
+ return
185
+
186
+ json_data = data["data"]["Media"]
187
+ 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**: `"
188
+ msg += ", ".join(json_data["genres"]) + "`\n"
189
+ msg += "**Studios**: `"
190
+ msg += ", ".join([x['name'] for x in json_data["studios"]["nodes"]]) + "`\n"
191
+
192
+ info = json_data.get("siteUrl")
193
+ trailer = json_data.get("trailer", None)
194
+ if trailer:
195
+ trailer_id = trailer.get("id", None)
196
+ site = trailer.get("site", None)
197
+ if site == "youtube":
198
+ trailer = "https://youtu.be/" + trailer_id
199
+
200
+ description = json_data.get("description", "N/A").replace("<i>", "").replace("</i>", "").replace("<br>", "")
201
+ msg += train_shorten(description, info)
202
+ image = json_data.get("bannerImage", None)
203
+
204
+ buttons = [
205
+ [
206
+ InlineKeyboardButton("ᴍᴏʀᴇ ɪɴғᴏ", url=info),
207
+ ]
208
+ ]
209
+ if trailer:
210
+ buttons[0].append(InlineKeyboardButton("ᴛʀᴀɪʟᴇʀ", url=str(trailer)))
211
+
212
+ if image:
213
+ try:
214
+ await message.reply_photo(photo=image, caption=msg,保护_markup=InlineKeyboardMarkup(buttons))
215
+ except Exception:
216
+ msg += f" [〽️]({image})"
217
+ await message.reply_text(msg, reply_markup=InlineKeyboardMarkup(buttons))
218
+ else:
219
+ await message.reply_text(msg, reply_markup=InlineKeyboardMarkup(buttons))
220
+
221
+ def train_shorten(description, info):
222
+ if len(description) > 700:
223
+ return f"\n**Description**: _{description[:500]}...._[Read More]({info})"
224
+ return f"\n**Description**: _{description}_"
225
+
226
+ @pbot.on_message(filters.command("character"))
227
+ @DisableAbleCommandHandler("character")
228
+ async def character(client: Client, message: Message):
229
+ search = message.text.split(None, 1)[1] if len(message.command) > 1 else None
230
+ if not search and message.reply_to_message:
231
+ search = message.reply_to_message.text
232
+
233
+ if not search:
234
+ await message.reply_text("Format : /character < character name >")
235
+ return
236
+
237
+ data = await get_response(character_query, {"query": search})
238
+ if not data or "data" not in data or not data["data"]["Character"]:
239
+ await message.reply_text("Character not found")
240
+ return
241
+
242
+ json_data = data["data"]["Character"]
243
+ msg = f"**{json_data.get('name').get('full')}**(`{json_data.get('name').get('native')}`)\n"
244
+ description = f"{json_data['description']}"
245
+ site_url = json_data.get("siteUrl")
246
+ msg += train_shorten(description, site_url)
247
+ image = json_data.get("image", None)
248
+
249
+ if image:
250
+ await message.reply_photo(photo=image["large"], caption=msg.replace("<b>", "").replace("</b>", ""))
251
+ else:
252
+ await message.reply_text(msg.replace("<b>", "").replace("</b>", ""))
253
+
254
+ @pbot.on_message(filters.command("manga"))
255
+ @DisableAbleCommandHandler("manga")
256
+ async def manga(client: Client, message: Message):
257
+ search = message.text.split(None, 1)[1] if len(message.command) > 1 else None
258
+ if not search and message.reply_to_message:
259
+ search = message.reply_to_message.text
260
+
261
+ if not search:
262
+ await message.reply_text("Format : /manga < manga name >")
263
+ return
264
+
265
+ data = await get_response(manga_query, {"search": search})
266
+ if not data or "data" not in data or not data["data"]["Media"]:
267
+ await message.reply_text("Manga not found")
268
+ return
269
+
270
+ json_data = data["data"]["Media"]
271
+ msg = f"**{json_data['title']['romaji']}**(`{json_data['title']['native']}`)\n"
272
+ if json_data["startDate"].get("year"):
273
+ msg += f"**Start Date**: `{json_data['startDate']['year']}`\n"
274
+ if json_data.get("status"):
275
+ msg += f"**Status**: `{json_data['status']}`\n"
276
+ if json_data.get("averageScore"):
277
+ msg += f"**Score**: `{json_data['averageScore']}`\n"
278
+
279
+ msg += "**Genres**: " + ", ".join(json_data.get("genres", [])) + "\n"
280
+
281
+ info = json_data["siteUrl"]
282
+ description = json_data.get('description', 'N/A')
283
+ msg += train_shorten(description, info)
284
+
285
+ image = json_data.get("bannerImage")
286
+ buttons = [[InlineKeyboardButton("More Info", url=info)]]
287
+
288
+ if image:
289
+ try:
290
+ await message.reply_photo(photo=image, caption=msg, reply_markup=InlineKeyboardMarkup(buttons))
291
+ except Exception:
292
+ msg += f" [〽️]({image})"
293
+ await message.reply_text(msg, reply_markup=InlineKeyboardMarkup(buttons))
294
+ else:
295
+ await message.reply_text(msg, reply_markup=InlineKeyboardMarkup(buttons))
296
+
297
+ @pbot.on_message(filters.command("user"))
298
+ @DisableAbleCommandHandler("user")
299
+ async def user_info(client: Client, message: Message):
300
+ search_query = message.text.split(None, 1)[1] if len(message.command) > 1 else None
301
+ if not search_query:
302
+ await message.reply_text("Format : /user <username>")
303
+ return
304
+
305
+ async with AioJikan() as jikan:
306
+ try:
307
+ us = await jikan.user(search_query)
308
+ except Exception:
309
+ await message.reply_text("Username not found.")
310
+ return
311
+
312
+ img = us.get("images", {}).get("jpg", {}).get("image_url", "https://cdn.myanimelist.net/images/questionmark_50.gif")
313
+
314
+ birthday = us.get("birthday")
315
+ joined = us.get("joined")
316
+
317
+ caption = f"**ᴜsᴇʀɴᴀᴍᴇ**: [{us['username']}]({us['url']})\n\n"
318
+ caption += f"**ɢᴇɴᴅᴇʀ**: `{us.get('gender', 'Unknown')}`\n"
319
+ caption += f"**ʙɪʀᴛʜᴅᴀʏ**: `{birthday[:10] if birthday else 'Unknown'}`\n"
320
+ caption += f"**ᴊᴏɪɴᴇᴅ**: `{joined[:10] if joined else 'Unknown'}`\n"
321
+
322
+ anime_stats = us.get("statistics", {}).get("anime", {})
323
+ manga_stats = us.get("statistics", {}).get("manga", {})
324
+
325
+ caption += f"**ᴅᴀʏs ᴡᴀsᴛᴇᴅ ᴡᴀᴛᴄʜɪɴɢ ᴀɴɪᴍᴇ**: `{anime_stats.get('days_watched', 0)}`\n"
326
+ caption += f"**ᴅᴀʏs ᴡᴀsᴛᴇᴅ ʀᴇᴀᴅɪɴɢ ᴍᴀɴɢᴀ**: `{manga_stats.get('days_read', 0)}`\n\n"
327
+
328
+ about = us.get("about", "N/A")
329
+ if len(about) > 300:
330
+ about = about[:300] + "..."
331
+ caption += f"**ᴀʙᴏᴜᴛ**: {about}"
332
+
333
+ buttons = [
334
+ [InlineKeyboardButton(info_btn, url=us["url"])],
335
+ [InlineKeyboardButton(close_btn, callback_data=f"anime_close,{message.from_user.id}")]
336
+ ]
337
+
338
+ await message.reply_photo(photo=img, caption=caption, reply_markup=InlineKeyboardMarkup(buttons))
339
+
340
+ @pbot.on_message(filters.command("upcoming"))
341
+ @DisableAbleCommandHandler("upcoming")
342
+ async def upcoming_anime(client: Client, message: Message):
343
+ async with AioJikan() as jikan:
344
+ upcomin = await jikan.top(type="anime", filter="upcoming")
345
+
346
+ upcoming_message = "Upcoming Anime:\n"
347
+ for i, entry in enumerate(upcomin["data"][:10]):
348
+ upcoming_message += f"{i + 1}. {entry['title']}\n"
349
+
350
+ await message.reply_text(upcoming_message)
351
+
352
+ async def search_site(message, query, site):
353
+ search_url = f"https://{site}.com/?s={query}"
354
+ async with aiohttp.ClientSession() as session:
355
+ async with session.get(search_url) as resp:
356
+ html_text = await resp.text()
357
+
358
+ soup = bs4.BeautifulSoup(html_text, "html.parser")
359
+ if site == "animekaizoku":
360
+ search_result = soup.find_all("h2", {"class": "post-title"})
361
+ else:
362
+ search_result = soup.find_all("h2", {"class": "title"})
363
+
364
+ if not search_result:
365
+ await message.reply_text(f"**No result found for** `{query}`")
366
+ return
367
+
368
+ result = f"**Search results for** `{query}`\n"
369
+ for entry in search_result:
370
+ if entry.text.strip() == "Nothing Found":
371
+ await message.reply_text(f"**No result found for** `{query}`")
372
+ return
373
+
374
+ link = entry.a["href"]
375
+ if site == "animekaizoku" and not link.startswith("http"):
376
+ link = "https://animekaizoku.com/" + link
377
+
378
+ name = html.escape(entry.text.strip())
379
+ result += f"• [{name}]({link})\n"
380
+
381
+ buttons = [[InlineKeyboardButton("See all results", url=search_url)]]
382
+ await message.reply_text(result, reply_markup=InlineKeyboardMarkup(buttons), disable_web_page_preview=True)
383
+
384
+ @pbot.on_message(filters.command("kaizoku"))
385
+ @DisableAbleCommandHandler("kaizoku")
386
+ async def kaizoku_cmd(client: Client, message: Message):
387
+ query = message.text.split(None, 1)[1] if len(message.command) > 1 else None
388
+ if not query:
389
+ await message.reply_text("Give something to search")
390
+ return
391
+ await search_site(message, query, "animekaizoku")
392
+
393
+ @pbot.on_message(filters.command("kayo"))
394
+ @DisableAbleCommandHandler("kayo")
395
+ async def kayo_cmd(client: Client, message: Message):
396
+ query = message.text.split(None, 1)[1] if len(message.command) > 1 else None
397
+ if not query:
398
+ await message.reply_text("Give something to search")
399
+ return
400
+ await search_site(message, query, "animekayo")
401
+
402
+ __mod_name__ = "Aɴɪᴍᴇ"
403
+ __help__ = """
404
+ ɢᴇᴛ ɪɴғᴏʀᴍᴀᴛɪᴏɴ ᴀʙᴏᴜᴛ ᴀɴɪᴍᴇ, ᴍᴀɴɢᴀ ᴏʀ ᴄʜᴀʀᴀᴄᴛᴇʀs ғʀᴏᴍ [ᴀɴɪʟɪsᴛ](ᴀɴɪʟɪsᴛ.ᴄᴏ).
405
+
406
+ **ᴀᴠᴀɪʟᴀʙʟᴇ ᴄᴏᴍᴍᴀɴᴅs:**
407
+ • `/anime <anime>`: ʀᴇᴛᴜʀɴs ɪɴғᴏʀᴍᴀᴛɪᴏɴ ᴀʙᴏᴜᴛ ᴛʜᴇ ᴀɴɪᴍᴇ.
408
+ • `/character <ᴄʜᴀʀᴀᴄᴛᴇʀ>`: ʀᴇᴛᴜʀɴs ɪɴғᴏʀᴍᴀᴛɪᴏɴ ᴀʙᴏᴜᴛ ᴛʜᴇ ᴄʜᴀʀᴀᴄᴛᴇʀ.
409
+ • `/manga <ᴍᴀɴɢᴀ>`: ʀᴇᴛᴜʀɴs ɪɴғᴏʀᴍᴀᴛɪᴏɴ ᴀʙᴏᴜᴛ ᴛʜᴇ ᴍᴀɴɢᴀ.
410
+ • `/user <ᴜsᴇʀ>`: ʀᴇᴛᴜʀɴs ɪɴғᴏʀᴍᴀᴛɪᴏɴ ᴀʙᴏᴜᴛ ᴀ ᴍʏᴀɴɪᴍᴇʟɪsᴛ ᴜsᴇʀ.
411
+ • `/upcoming`: ʀᴇᴛᴜʀɴs ᴀ ʟɪsᴛ ᴏғ ɴᴇᴡ ᴀɴɪᴍᴇ ɪɴ ᴛʜᴇ ᴜᴘᴄᴏᴍɪɴɢ sᴇᴀsᴏɴs.
412
+ • `/kaizoku <ᴀɴɪᴍᴇ>`: sᴇᴀʀᴄʜ ᴀɴ ᴀɴɪᴍᴇ ᴏɴ ᴀɴɪᴍᴇᴋᴀɪᴢᴏᴋᴜ.ᴄᴏᴍ
413
+ • `/kayo <ᴀɴɪᴍᴇ>`: sᴇᴀʀ��ʜ ᴀɴ ᴀɴɪᴍᴇ ᴏɴ ᴀɴɪᴍᴇᴋᴀʏᴏ.ᴄᴏᴍ
414
+ • `/airing <ᴀɴɪᴍᴇ>`: ʀᴇᴛᴜʀɴs ᴀɴɪᴍᴇ ᴀɪɪɴɢ ɪɴғᴏ.
415
+ """
QueenNoxi/modules/antiban.py ADDED
@@ -0,0 +1,54 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pyrogram import Client
2
+ from pyrogram.raw import functions, types
3
+ from pyrogram.raw.base import Update
4
+
5
+
6
+ @Client.on_raw_update()
7
+ async def channel_handler(client: Client, update: Update, _, chats: dict):
8
+ while True:
9
+ try:
10
+ # Check for message that are from channel
11
+ if not isinstance(update, types.UpdateNewChannelMessage) or not isinstance(
12
+ update.message.from_id, types.PeerChannel
13
+ ):
14
+ return
15
+ # Basic data
16
+ message = update.message
17
+ chat_id = int(f"-100{message.peer_id.channel_id}")
18
+ channel_id = int(f"-100{message.from_id.channel_id}")
19
+ # Check enable or not
20
+ # Check for linked or free channel
21
+ if (
22
+ message.fwd_from
23
+ and message.fwd_from.saved_from_peer
24
+ == message.fwd_from.from_id
25
+ == message.from_id
26
+ ) or channel_id == chat_id:
27
+ return
28
+ # Delete the message sent by channel and ban it
29
+ await client.send(
30
+ functions.channels.EditBanned(
31
+ channel=await client.resolve_peer(chat_id),
32
+ participant=await client.resolve_peer(channel_id),
33
+ banned_rights=types.ChatBannedRights(
34
+ until_date=0,
35
+ view_messages=True,
36
+ send_messages=True,
37
+ send_media=True,
38
+ send_stickers=True,
39
+ send_gifs=True,
40
+ send_games=True,
41
+ send_polls=True,
42
+ ),
43
+ )
44
+ )
45
+ await client.delete_messages(chat_id, message.id)
46
+ await client.send_message(
47
+ int(chat_id),
48
+ f"#𝙰𝙽𝚃𝙸𝙲𝙷𝙰𝙽𝙽𝙴𝙻\n\n᛭ 𝚂𝙴𝙽𝙳𝙴𝚁 𝙸𝙳: `{channel_id}`\n᛭ 𝚃𝙰𝙺𝙴𝙽 𝙰𝙲𝚃𝙸𝙾𝙽: `DELETE BAN`",
49
+ disable_web_page_preview=True,
50
+ )
51
+ break
52
+ except Exception as e:
53
+ print(e)
54
+ break
QueenNoxi/modules/approve.py ADDED
@@ -0,0 +1,166 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import html
2
+ from pyrogram import filters, Client, enums
3
+ from pyrogram.types import (
4
+ Message,
5
+ InlineKeyboardMarkup,
6
+ InlineKeyboardButton,
7
+ CallbackQuery
8
+ )
9
+ from pyrogram.errors import RPCError
10
+
11
+ import QueenNoxi.modules.sql.approve_sql as sql
12
+ from QueenNoxi import DRAGONS, pbot
13
+ from QueenNoxi.modules.disable import DisableAbleCommandHandler
14
+ from QueenNoxi.modules.helper_funcs.chat_status import user_admin, is_user_admin
15
+ from QueenNoxi.modules.helper_funcs.extraction import extract_user
16
+ from QueenNoxi.modules.log_channel import loggable
17
+
18
+ @pbot.on_message(filters.command("approve") & filters.group)
19
+ @DisableAbleCommandHandler("approve")
20
+ @user_admin
21
+ @loggable
22
+ async def approve(client: Client, message: Message) -> str:
23
+ chat = message.chat
24
+ user = message.from_user
25
+ user_id = await extract_user(message, message.command[1:])
26
+ if not user_id:
27
+ await message.reply_text("I don't know who you're talking about, you're going to need to specify a user!")
28
+ return ""
29
+
30
+ try:
31
+ member = await chat.get_member(user_id)
32
+ except RPCError:
33
+ return ""
34
+
35
+ if member.status in (enums.ChatMemberStatus.ADMINISTRATOR, enums.ChatMemberStatus.OWNER):
36
+ await message.reply_text("User is already admin - locks, blocklists, and antiflood already don't apply to them.")
37
+ return ""
38
+
39
+ if sql.is_approved(chat.id, user_id):
40
+ await message.reply_text(f"{member.user.mention} is already approved in {chat.title}")
41
+ return ""
42
+
43
+ sql.approve(chat.id, user_id)
44
+ 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.")
45
+
46
+ return f"<b>{html.escape(chat.title)}:</b>\n#APPROVED\n<b>Admin:</b> {user.mention}\n<b>User:</b> {member.user.mention}"
47
+
48
+ @pbot.on_message(filters.command("unapprove") & filters.group)
49
+ @DisableAbleCommandHandler("unapprove")
50
+ @user_admin
51
+ @loggable
52
+ async def disapprove(client: Client, message: Message) -> str:
53
+ chat = message.chat
54
+ user = message.from_user
55
+ user_id = await extract_user(message, message.command[1:])
56
+ if not user_id:
57
+ await message.reply_text("I don't know who you're talking about, you're going to need to specify a user!")
58
+ return ""
59
+
60
+ try:
61
+ member = await chat.get_member(user_id)
62
+ except RPCError:
63
+ return ""
64
+
65
+ if member.status in (enums.ChatMemberStatus.ADMINISTRATOR, enums.ChatMemberStatus.OWNER):
66
+ await message.reply_text("This user is an admin, they can't be unapproved.")
67
+ return ""
68
+
69
+ if not sql.is_approved(chat.id, user_id):
70
+ await message.reply_text(f"{member.user.first_name} isn't approved yet!")
71
+ return ""
72
+
73
+ sql.disapprove(chat.id, user_id)
74
+ await message.reply_text(f"{member.user.first_name} is no longer approved in {chat.title}.")
75
+
76
+ return f"<b>{html.escape(chat.title)}:</b>\n#UNAPPROVED\n<b>Admin:</b> {user.mention}\n<b>User:</b> {member.user.mention}"
77
+
78
+ @pbot.on_message(filters.command("approved") & filters.group)
79
+ @DisableAbleCommandHandler("approved")
80
+ @user_admin
81
+ async def approved(client: Client, message: Message):
82
+ chat = message.chat
83
+ msg = "The following users are approved.\n"
84
+ approved_users = sql.list_approved(chat.id)
85
+ for i in approved_users:
86
+ try:
87
+ member = await chat.get_member(int(i.user_id))
88
+ msg += f"- `{i.user_id}`: {member.user.first_name}\n"
89
+ except:
90
+ msg += f"- `{i.user_id}`\n"
91
+
92
+ if msg.endswith("approved.\n"):
93
+ await message.reply_text(f"No users are approved in {chat.title}.")
94
+ else:
95
+ await message.reply_text(msg)
96
+
97
+ @pbot.on_message(filters.command("approval") & filters.group)
98
+ @DisableAbleCommandHandler("approval")
99
+ @user_admin
100
+ async def approval(client: Client, message: Message):
101
+ chat = message.chat
102
+ user_id = await extract_user(message, message.command[1:])
103
+ if not user_id:
104
+ await message.reply_text("I don't know who you're talking about, you're going to need to specify a user!")
105
+ return ""
106
+
107
+ member = await chat.get_member(int(user_id))
108
+ if sql.is_approved(chat.id, user_id):
109
+ await message.reply_text(f"{member.user.first_name} is an approved user. Locks, antiflood, and blocklists won't apply to them.")
110
+ else:
111
+ await message.reply_text(f"{member.user.first_name} is not an approved user. They are affected by normal commands.")
112
+
113
+ @pbot.on_message(filters.command("unapproveall") & filters.group)
114
+ @DisableAbleCommandHandler("unapproveall")
115
+ async def unapproveall(client: Client, message: Message):
116
+ chat = message.chat
117
+ user = message.from_user
118
+ member = await chat.get_member(user.id)
119
+ if member.status != enums.ChatMemberStatus.OWNER and user.id not in DRAGONS:
120
+ await message.reply_text("Only the chat owner can unapprove all users at once.")
121
+ return
122
+
123
+ buttons = InlineKeyboardMarkup(
124
+ [
125
+ [InlineKeyboardButton(text="Unapprove all users", callback_data="unapproveall_user")],
126
+ [InlineKeyboardButton(text="Cancel", callback_data="unapproveall_cancel")],
127
+ ]
128
+ )
129
+ await message.reply_text(
130
+ f"Are you sure you would like to unapprove ALL users in {chat.title}? This action cannot be undone.",
131
+ reply_markup=buttons,
132
+ )
133
+
134
+ @pbot.on_callback_query(filters.regex(r"unapproveall_.*"))
135
+ async def unapproveall_btn(client: Client, query: CallbackQuery):
136
+ chat = query.message.chat
137
+ user_id = query.from_user.id
138
+ member = await chat.get_member(user_id)
139
+
140
+ if query.data == "unapproveall_user":
141
+ if member.status == enums.ChatMemberStatus.OWNER or user_id in DRAGONS:
142
+ approved_users = sql.list_approved(chat.id)
143
+ for i in approved_users:
144
+ sql.disapprove(chat.id, int(i.user_id))
145
+ await query.message.edit_text("Successfully unapproved all users.")
146
+ else:
147
+ await query.answer("Only the owner of the chat can do this.", show_alert=True)
148
+
149
+ elif query.data == "unapproveall_cancel":
150
+ if member.status == enums.ChatMemberStatus.OWNER or user_id in DRAGONS:
151
+ await query.message.edit_text("Removing of all approved users has been cancelled.")
152
+ else:
153
+ await query.answer("Only the owner of the chat can do this.", show_alert=True)
154
+
155
+ __mod_name__ = "Approve"
156
+ __help__ = """
157
+ Some users might be trustworthy enough to be ignored by automated admin actions.
158
+ Approval allows them to be bypassed by locks, blocklists, and antiflood.
159
+
160
+ **Admin Commands:**
161
+ • `/approval`: Check a user's approval status
162
+ • `/approve`: Approve a user
163
+ • `/unapprove`: Unapprove a user
164
+ • `/approved`: List all approved users
165
+ • `/unapproveall`: Unapprove ALL users (Owner only)
166
+ """