Spaces:
Runtime error
Runtime error
Create bot.py
Browse files
bot.py
ADDED
|
@@ -0,0 +1,1766 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import sys
|
| 2 |
+
import subprocess
|
| 3 |
+
import os
|
| 4 |
+
import importlib
|
| 5 |
+
import asyncio
|
| 6 |
+
import re
|
| 7 |
+
import time
|
| 8 |
+
import shutil
|
| 9 |
+
import json
|
| 10 |
+
import zipfile
|
| 11 |
+
import urllib.parse
|
| 12 |
+
import random
|
| 13 |
+
import io
|
| 14 |
+
import threading
|
| 15 |
+
from datetime import datetime, timedelta
|
| 16 |
+
|
| 17 |
+
# ================================
|
| 18 |
+
# 🛠️ 1. Auto-Installer & Dependencies
|
| 19 |
+
# ================================
|
| 20 |
+
def install_dependencies():
|
| 21 |
+
packages = [
|
| 22 |
+
("discord.py", "discord"),
|
| 23 |
+
("google-generativeai", "google.generativeai"),
|
| 24 |
+
("aiohttp", "aiohttp"),
|
| 25 |
+
("nest_asyncio", "nest_asyncio"),
|
| 26 |
+
("python-docx", "docx"),
|
| 27 |
+
("patool", "patoolib"),
|
| 28 |
+
("Pillow", "PIL"),
|
| 29 |
+
("bs4", "bs4"),
|
| 30 |
+
("requests", "requests"),
|
| 31 |
+
("cloudscraper", "cloudscraper"),
|
| 32 |
+
("selenium", "selenium"),
|
| 33 |
+
("webdriver-manager", "webdriver_manager"),
|
| 34 |
+
("gdown", "gdown"),
|
| 35 |
+
("flask", "flask")
|
| 36 |
+
]
|
| 37 |
+
try: subprocess.run(["unrar"], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
| 38 |
+
except: pass
|
| 39 |
+
|
| 40 |
+
try:
|
| 41 |
+
subprocess.run("apt-get update && apt-get install -y wget gnupg && wget -q -O - https://dl-ssl.google.com/linux/linux_signing_key.pub | apt-key add - && sh -c 'echo \"deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main\" >> /etc/apt/sources.list.d/google.list' && apt-get update && apt-get install -y google-chrome-stable", shell=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
| 42 |
+
except: pass
|
| 43 |
+
|
| 44 |
+
for pkg, import_name in packages:
|
| 45 |
+
try: importlib.import_module(import_name)
|
| 46 |
+
except ImportError:
|
| 47 |
+
try: subprocess.check_call([sys.executable, "-m", "pip", "install", "-q", "--disable-pip-version-check", "-U", pkg])
|
| 48 |
+
except: pass
|
| 49 |
+
|
| 50 |
+
install_dependencies()
|
| 51 |
+
|
| 52 |
+
import discord
|
| 53 |
+
from discord.ext import commands, tasks
|
| 54 |
+
from discord import app_commands
|
| 55 |
+
from discord.ui import Select, View, Button, Modal, TextInput
|
| 56 |
+
import google.generativeai as genai
|
| 57 |
+
import aiohttp
|
| 58 |
+
import nest_asyncio
|
| 59 |
+
from docx import Document
|
| 60 |
+
import patoolib
|
| 61 |
+
from PIL import Image, ImageDraw, ImageFont, ImageFile
|
| 62 |
+
from bs4 import BeautifulSoup
|
| 63 |
+
import requests
|
| 64 |
+
import cloudscraper
|
| 65 |
+
import gdown
|
| 66 |
+
from flask import Flask
|
| 67 |
+
from threading import Thread
|
| 68 |
+
|
| 69 |
+
from selenium import webdriver
|
| 70 |
+
from selenium.webdriver.chrome.service import Service
|
| 71 |
+
from selenium.webdriver.chrome.options import Options
|
| 72 |
+
from selenium.webdriver.common.by import By
|
| 73 |
+
from selenium.webdriver.support.ui import WebDriverWait
|
| 74 |
+
from selenium.webdriver.support import expected_conditions as EC
|
| 75 |
+
from webdriver_manager.chrome import ChromeDriverManager
|
| 76 |
+
|
| 77 |
+
nest_asyncio.apply()
|
| 78 |
+
Image.MAX_IMAGE_PIXELS = None
|
| 79 |
+
ImageFile.LOAD_TRUNCATED_IMAGES = True
|
| 80 |
+
|
| 81 |
+
# ================================
|
| 82 |
+
# 🌐 2. Keep-Alive Server (Hugging Face)
|
| 83 |
+
# ================================
|
| 84 |
+
app = Flask('')
|
| 85 |
+
@app.route('/')
|
| 86 |
+
def home():
|
| 87 |
+
return "Bot is running perfectly!"
|
| 88 |
+
|
| 89 |
+
def run_server():
|
| 90 |
+
app.run(host='0.0.0.0', port=7860)
|
| 91 |
+
|
| 92 |
+
def keep_alive():
|
| 93 |
+
t = Thread(target=run_server)
|
| 94 |
+
t.start()
|
| 95 |
+
|
| 96 |
+
# ================================
|
| 97 |
+
# ⚙️ 3. Config & Keys
|
| 98 |
+
# ================================
|
| 99 |
+
# سيتم جلب التوكن والمفاتيح من الـ Secrets في Hugging Face للحماية
|
| 100 |
+
DISCORD_TOKEN = os.getenv("DISCORD_TOKEN")
|
| 101 |
+
RAW_KEYS = os.getenv("GEMINI_KEYS", "")
|
| 102 |
+
GEMINI_KEYS = [k.strip() for k in RAW_KEYS.split(",") if k.strip()]
|
| 103 |
+
|
| 104 |
+
WELCOME_CHANNEL_ID = 123456789012345678
|
| 105 |
+
AUTO_ROLE_ID = 123456789012345678
|
| 106 |
+
LOG_CHANNEL_ID = 123456789012345678
|
| 107 |
+
TRACKER_CHANNEL_ID = 123456789012345678
|
| 108 |
+
|
| 109 |
+
BASE_DIR = os.getcwd()
|
| 110 |
+
TEMP_ROOT = os.path.join(BASE_DIR, "bot_temp")
|
| 111 |
+
USERS_DB_FILE = os.path.join(BASE_DIR, "users_db.json")
|
| 112 |
+
TRACKER_DB_FILE = os.path.join(BASE_DIR, "tracker_cache.json")
|
| 113 |
+
active_tasks = {}
|
| 114 |
+
invites_cache = {}
|
| 115 |
+
bot_queue = {"ocr": [], "merge": []}
|
| 116 |
+
|
| 117 |
+
intents = discord.Intents.all()
|
| 118 |
+
bot = commands.Bot(command_prefix="!", intents=intents)
|
| 119 |
+
|
| 120 |
+
def natural_sort_key(s):
|
| 121 |
+
return [int(text) if text.isdigit() else text.lower() for text in re.split(r'(\d+)', s)]
|
| 122 |
+
|
| 123 |
+
# ================================
|
| 124 |
+
# 🧠 4. Gemini Smart Manager (The Core Engine)
|
| 125 |
+
# ================================
|
| 126 |
+
class GeminiManager:
|
| 127 |
+
def __init__(self):
|
| 128 |
+
self.keys = GEMINI_KEYS
|
| 129 |
+
self.cooldowns = {key: 0 for key in self.keys}
|
| 130 |
+
self.lock = threading.Lock()
|
| 131 |
+
|
| 132 |
+
def get_valid_key(self):
|
| 133 |
+
with self.lock:
|
| 134 |
+
current_time = time.time()
|
| 135 |
+
available_keys = [k for k, cooldown in self.cooldowns.items() if current_time > cooldown]
|
| 136 |
+
if not available_keys:
|
| 137 |
+
return None
|
| 138 |
+
return random.choice(available_keys)
|
| 139 |
+
|
| 140 |
+
def report_dead_key(self, key):
|
| 141 |
+
with self.lock:
|
| 142 |
+
self.cooldowns[key] = time.time() + 7200
|
| 143 |
+
print(f"🔴 مفتاح تم حظره لمدة ساعتين: {key[:10]}...")
|
| 144 |
+
|
| 145 |
+
async def analyze_image(self, image_path, advance_mode=False):
|
| 146 |
+
prompt = """TASK: STRICT OCR ONLY (VISION + COMPLETENESS ENFORCED)
|
| 147 |
+
|
| 148 |
+
==================================================
|
| 149 |
+
CORE PRINCIPLES (NON-NEGOTIABLE)
|
| 150 |
+
==================================================
|
| 151 |
+
- VISUAL SHAPE ONLY.
|
| 152 |
+
- NO inference, NO guessing, NO hallucination.
|
| 153 |
+
- Language, meaning, emotion, size, and layout are IRRELEVANT.
|
| 154 |
+
- Extract ONLY what is visibly present on the page.
|
| 155 |
+
- COMPLETE the current page BEFORE moving to the next.
|
| 156 |
+
|
| 157 |
+
==================================================
|
| 158 |
+
ORDER NO. 1: ABSOLUTE EXTRACTION RULES
|
| 159 |
+
==================================================
|
| 160 |
+
1. NO TRANSLATION: Output the EXACT original text only.
|
| 161 |
+
2. NO HALLUCINATION: DO NOT invent, guess, infer, or autocomplete text. If text is NOT clearly visible → DO NOT output it.
|
| 162 |
+
3. NO OMISSION: ALL visible text on the page MUST be extracted. Page is considered COMPLETE only after extracting: Every bubble, Every box, Every small text, Every SFX, Every background text.
|
| 163 |
+
4. NO REPETITION: Do NOT repeat characters endlessly. Use condensed form only.
|
| 164 |
+
|
| 165 |
+
==================================================
|
| 166 |
+
ORDER NO. 2: ONE ELEMENT = ONE LINE (ABSOLUTE LOCK & GROUPING RULES)
|
| 167 |
+
==================================================
|
| 168 |
+
- BUBBLES & BOXES: A bubble/box is ONE continuous visible border. Multiple visual lines inside ONE border MUST be merged into EXACTLY ONE output line. Replace internal line breaks with a single space.
|
| 169 |
+
- BORDERLESS TEXT (OT) & SYSTEM SCREENS (<>): Group contiguous or visually adjacent text lines that belong to the same paragraph or logical block into EXACTLY ONE output line. Do NOT output each physical line as a separate tag.
|
| 170 |
+
- NEVER merge bubbles vertically, horizontally, diagonally. Fragment output is FORBIDDEN.
|
| 171 |
+
|
| 172 |
+
==================================================
|
| 173 |
+
ORDER NO. 3: SENTENCE INTEGRITY
|
| 174 |
+
==================================================
|
| 175 |
+
- A single sentence MUST stay in ONE tag. NEVER continue a sentence using OT: or any other tag.
|
| 176 |
+
|
| 177 |
+
==================================================
|
| 178 |
+
ORDER NO. 4: TAG SELECTION (SHAPE-ONLY)
|
| 179 |
+
==================================================
|
| 180 |
+
Tag selection is STRICTLY based on the VISUAL SHAPE of the bubble's border.
|
| 181 |
+
|
| 182 |
+
1. (): THOUGHT (STRICT IDENTIFICATION)
|
| 183 |
+
- Visual MUST BE EXACTLY ONE OF THESE TWO:
|
| 184 |
+
A) Connected to the character by a DOTTED TAIL (a trail of small distinct circles).
|
| 185 |
+
B) The "LION'S MANE" / "FURRY" border: The border is made of very dense, ultra-thin, fine black lines radiating outward evenly around the entire bubble.
|
| 186 |
+
- Format: `(): Text`
|
| 187 |
+
|
| 188 |
+
2. %: SCREAM / SHOUT / PAIN
|
| 189 |
+
- Visual: LARGE, IRREGULAR jagged zig-zags, explosion-like shapes, or violently shaky borders.
|
| 190 |
+
- CRITICAL NEGATIVE RULE: Do NOT use `%` if the border consists of fine, dense, uniform, hair-like lines (Lion's Mane). That MUST be `()`.
|
| 191 |
+
- Format: `%: Text`
|
| 192 |
+
|
| 193 |
+
3. ST: SMALL TEXT
|
| 194 |
+
- Visibly smaller text, floating freely, annotations, or outside any main bubble.
|
| 195 |
+
- CRITICAL: Do NOT default to `""` for text that is clearly smaller than the main dialogue. It MUST be `ST`.
|
| 196 |
+
- Format: `ST: Text`
|
| 197 |
+
|
| 198 |
+
4. "": SPEECH
|
| 199 |
+
- Smooth oval or rectangular bubble with a normal solid speech tail.
|
| 200 |
+
- Format: `"": Text`
|
| 201 |
+
|
| 202 |
+
5. //: CONNECTED SPEECH
|
| 203 |
+
- Visually connected continuation bubble. SAME speaker only.
|
| 204 |
+
- Format: `//: Text`
|
| 205 |
+
|
| 206 |
+
6. []: BOX
|
| 207 |
+
- Physical rectangular narration box, NOT a screen/UI. Multiple lines inside one box MUST be merged into one single `[]` line.
|
| 208 |
+
- Format: `[]: Text`
|
| 209 |
+
|
| 210 |
+
7. <>: SCREEN SYSTEM
|
| 211 |
+
- Text inside phone screen, game screen, system UI, HUD. Treat the entire screen text block as ONE single `<>` line.
|
| 212 |
+
- Format: `<>: Text`
|
| 213 |
+
|
| 214 |
+
8. OT: ARTWORK / BACKGROUND TEXT
|
| 215 |
+
- Large text written directly on artwork, NO bubble/box. Group adjacent lines together!
|
| 216 |
+
- Format: `OT: Text`
|
| 217 |
+
|
| 218 |
+
9. SFX: SOUND EFFECTS
|
| 219 |
+
- Stylized non-verbal sound lettering.
|
| 220 |
+
- Format: `SFX: Text`
|
| 221 |
+
|
| 222 |
+
==================================================
|
| 223 |
+
ORDER NO. 5: STRICT READING DIRECTION & SEQUENCE (CRITICAL)
|
| 224 |
+
==================================================
|
| 225 |
+
1. PRIMARY RULE (HEIGHT): ALWAYS read from TOP to BOTTOM. The element physically higher on the page comes first.
|
| 226 |
+
2. SECONDARY RULE (TIE-BREAKER): If two separate elements are at the exact same height, read RIGHT to LEFT.
|
| 227 |
+
3. CONNECTED BUBBLES (CHAIN RULE): When bubbles are physically touching or connected by tails, treat them as a flowing chain. You MUST visually trace the chain from the HIGHEST bubble down to the LOWEST. NEVER reverse the order of a connected sequence. The highest bubble is `""`, and the subsequent bubbles attached below it are `//:`.
|
| 228 |
+
|
| 229 |
+
==================================================
|
| 230 |
+
ORDER NO. 6: FINAL ENFORCEMENT (ZERO TOLERANCE)
|
| 231 |
+
==================================================
|
| 232 |
+
- NO translation, NO hallucination, NO missing text, NO merged bubbles, NO split bubbles.
|
| 233 |
+
- ONE tag per line. ONE bubble per line. ALL text must be tagged.
|
| 234 |
+
- STRICT sequence must be followed.
|
| 235 |
+
- IF NO TEXT IS VISIBLE IN THE IMAGE, RETURN EXACTLY: `OT: [No Text]`
|
| 236 |
+
"""
|
| 237 |
+
|
| 238 |
+
safety_settings = [{"category": c, "threshold": "BLOCK_NONE"} for c in [
|
| 239 |
+
"HARM_CATEGORY_HARASSMENT", "HARM_CATEGORY_HATE_SPEECH",
|
| 240 |
+
"HARM_CATEGORY_SEXUALLY_EXPLICIT", "HARM_CATEGORY_DANGEROUS_CONTENT"
|
| 241 |
+
]]
|
| 242 |
+
|
| 243 |
+
gen_config = {"temperature": 0.0, "max_output_tokens": 8192, "top_p": 0.9}
|
| 244 |
+
model_name = db.get_bot_setting("advance_model" if advance_mode else "normal_model")
|
| 245 |
+
|
| 246 |
+
for attempt in range(len(self.keys)):
|
| 247 |
+
current_api_key = self.get_valid_key()
|
| 248 |
+
if not current_api_key:
|
| 249 |
+
await asyncio.sleep(5)
|
| 250 |
+
continue
|
| 251 |
+
|
| 252 |
+
genai.configure(api_key=current_api_key)
|
| 253 |
+
await asyncio.sleep(random.uniform(1.0, 2.5))
|
| 254 |
+
|
| 255 |
+
try:
|
| 256 |
+
model = genai.GenerativeModel(model_name, safety_settings=safety_settings)
|
| 257 |
+
with open(image_path, "rb") as f: img = f.read()
|
| 258 |
+
|
| 259 |
+
response = await asyncio.wait_for(
|
| 260 |
+
asyncio.to_thread(
|
| 261 |
+
model.generate_content,
|
| 262 |
+
[prompt, {"mime_type": "image/jpeg", "data": img}],
|
| 263 |
+
generation_config=gen_config
|
| 264 |
+
), timeout=45.0
|
| 265 |
+
)
|
| 266 |
+
|
| 267 |
+
if response.text: return response.text
|
| 268 |
+
|
| 269 |
+
except asyncio.TimeoutError:
|
| 270 |
+
continue
|
| 271 |
+
except Exception as e:
|
| 272 |
+
error_msg = str(e).lower()
|
| 273 |
+
if "not found" in error_msg or "404" in error_msg or "invalid" in error_msg: continue
|
| 274 |
+
if "429" in error_msg or "quota" in error_msg or "exhausted" in error_msg:
|
| 275 |
+
self.report_dead_key(current_api_key)
|
| 276 |
+
await asyncio.sleep(1.5)
|
| 277 |
+
continue
|
| 278 |
+
if "safety" in error_msg or "finish_reason" in error_msg: break
|
| 279 |
+
continue
|
| 280 |
+
|
| 281 |
+
return ""
|
| 282 |
+
|
| 283 |
+
ai_engine = GeminiManager()
|
| 284 |
+
|
| 285 |
+
# ================================
|
| 286 |
+
# 🧹 5. Clean Text (Anti-Hallucination Filter)
|
| 287 |
+
# ================================
|
| 288 |
+
def clean_text(raw, settings):
|
| 289 |
+
try:
|
| 290 |
+
add_blank = settings.get("blank_line", True)
|
| 291 |
+
sfx_on = settings.get("include_sfx", False)
|
| 292 |
+
c = settings.get("custom_tags", {})
|
| 293 |
+
|
| 294 |
+
tag_map = {k: c.get(k, k) for k in ['""', '//', '()', 'ST', 'OT', 'SFX', '[]', '<>', '%']}
|
| 295 |
+
base_defaults = ['""', '//', '()', 'ST', 'OT', 'SFX', '[]', '<>', '%']
|
| 296 |
+
|
| 297 |
+
all_tags = {d: d for d in base_defaults}
|
| 298 |
+
for k, v in c.items(): all_tags[v] = k
|
| 299 |
+
|
| 300 |
+
hallucination_triggers = [
|
| 301 |
+
"TASK: STRICT OCR", "CORE PRINCIPLES", "ORDER NO. 1",
|
| 302 |
+
"ORDER NO. 2", "ORDER NO. 3", "ORDER NO. 4", "ORDER NO. 5",
|
| 303 |
+
"ORDER NO. 6", "VISUAL SHAPE ONLY", "Format:"
|
| 304 |
+
]
|
| 305 |
+
|
| 306 |
+
out = []
|
| 307 |
+
for line in raw.split('\n'):
|
| 308 |
+
line = line.strip()
|
| 309 |
+
|
| 310 |
+
if not line or re.match(r'^(I|II|III|IV|V|VI|VII|VIII|IX|X)-\s', line, re.IGNORECASE): continue
|
| 311 |
+
if any(trigger.lower() in line.lower() for trigger in hallucination_triggers): continue
|
| 312 |
+
if "panel" in line.lower() or "bubble" in line.lower(): continue
|
| 313 |
+
|
| 314 |
+
line = re.sub(r'^(?:OT:\s*)?\[OT:\s*(.*?)\]?$', r'OT: \1', line)
|
| 315 |
+
line = re.sub(r'^OT:\s*OT:\s*', r'OT: ', line)
|
| 316 |
+
line = line.replace("<:>", "<>:").replace("< : >", "<>:")
|
| 317 |
+
line = re.sub(r'(.)\1{6,}', r'\1\1...', line)
|
| 318 |
+
line = line.replace("Million", "milyun").replace("million", "milyun")
|
| 319 |
+
|
| 320 |
+
detected_tag = None
|
| 321 |
+
content = line
|
| 322 |
+
|
| 323 |
+
for tag_str in sorted(all_tags.keys(), key=len, reverse=True):
|
| 324 |
+
if line.startswith(f"{tag_str}:") or line.startswith(tag_str):
|
| 325 |
+
detected_tag = all_tags[tag_str]
|
| 326 |
+
content = line[len(tag_str)+1:].strip() if line.startswith(f"{tag_str}:") else line[len(tag_str):].strip()
|
| 327 |
+
break
|
| 328 |
+
|
| 329 |
+
if detected_tag:
|
| 330 |
+
if detected_tag == 'SFX' and not sfx_on: continue
|
| 331 |
+
content = content.strip().strip('"').strip("'").strip("`").strip()
|
| 332 |
+
if detected_tag == '()' and content.startswith('(') and content.endswith(')'):
|
| 333 |
+
content = content[1:-1].strip()
|
| 334 |
+
|
| 335 |
+
if len(content) > 600 and " " not in content: content = content[:50] + "..."
|
| 336 |
+
final_tag = tag_map.get(detected_tag, detected_tag)
|
| 337 |
+
out.append(f"{final_tag}: {content}")
|
| 338 |
+
else:
|
| 339 |
+
clean = line.strip().strip('"').strip("'").strip("`").strip()
|
| 340 |
+
if clean:
|
| 341 |
+
if clean.startswith(":") or clean.startswith(":"):
|
| 342 |
+
clean_speech = clean[1:].strip()
|
| 343 |
+
out.append(f"{tag_map.get('\"\"', '\"\"')}: {clean_speech}")
|
| 344 |
+
else:
|
| 345 |
+
if out:
|
| 346 |
+
out[-1] += f" {clean}"
|
| 347 |
+
else:
|
| 348 |
+
final_ot = tag_map.get('OT', 'OT')
|
| 349 |
+
if not any(clean.startswith(v) for v in tag_map.values()): out.append(f"{final_ot}: {clean}")
|
| 350 |
+
else: out.append(clean)
|
| 351 |
+
|
| 352 |
+
final_out = []
|
| 353 |
+
for item in out:
|
| 354 |
+
final_out.append(item)
|
| 355 |
+
if add_blank: final_out.append("")
|
| 356 |
+
|
| 357 |
+
return final_out
|
| 358 |
+
except: return [raw]
|
| 359 |
+
|
| 360 |
+
# ================================
|
| 361 |
+
# 🕹️ 6. UI Components (Modals & Views)
|
| 362 |
+
# ================================
|
| 363 |
+
class CancelView(View):
|
| 364 |
+
def __init__(self, user_id, task_id, total_pages):
|
| 365 |
+
super().__init__(timeout=None)
|
| 366 |
+
self.user_id = user_id
|
| 367 |
+
self.task_id = task_id
|
| 368 |
+
self.total_pages = total_pages
|
| 369 |
+
|
| 370 |
+
@discord.ui.button(label="🛑 Cancel Process", style=discord.ButtonStyle.danger, custom_id="cancel_btn")
|
| 371 |
+
async def stop(self, interaction: discord.Interaction, button: Button):
|
| 372 |
+
if interaction.user.id != self.user_id: return
|
| 373 |
+
|
| 374 |
+
current_progress = active_tasks.get(f"{self.task_id}_progress", 0)
|
| 375 |
+
if self.total_pages > 0 and (current_progress / self.total_pages) >= 0.5:
|
| 376 |
+
await interaction.response.send_message("❌ لا يمكن الإلغاء الآن، تم الانتهاء من أكثر من 50% من العمل.", ephemeral=True)
|
| 377 |
+
button.disabled = True
|
| 378 |
+
await interaction.message.edit(view=self)
|
| 379 |
+
return
|
| 380 |
+
|
| 381 |
+
if self.task_id in active_tasks:
|
| 382 |
+
active_tasks[self.task_id].cancel()
|
| 383 |
+
del active_tasks[self.task_id]
|
| 384 |
+
button.disabled = True
|
| 385 |
+
button.label = "تم الإلغاء ⛔"
|
| 386 |
+
await interaction.response.edit_message(view=self)
|
| 387 |
+
await interaction.followup.send("⛔ تم إلغاء العملية بنجاح. لن يتم خصم المزيد.", ephemeral=True)
|
| 388 |
+
|
| 389 |
+
class FeedbackModal(Modal, title="📝 تقييم جودة الاستخراج (Feedback)"):
|
| 390 |
+
feedback_text = TextInput(label='رأيك في دقة النصوص وملاحظاتك', style=discord.TextStyle.paragraph, placeholder='اكتب مشكلتك أو اقتراحك هنا...', required=True, max_length=1000)
|
| 391 |
+
|
| 392 |
+
async def on_submit(self, interaction: discord.Interaction):
|
| 393 |
+
await interaction.response.send_message("✅ شكراً لتقييمك! تم إرسال ملاحظاتك للإدارة لتطوير البوت.", ephemeral=True)
|
| 394 |
+
embed = discord.Embed(title="📝 تقييم جديد للاستخراج", color=0x3498db, timestamp=datetime.utcnow())
|
| 395 |
+
embed.add_field(name="المستخدم", value=interaction.user.mention, inline=False)
|
| 396 |
+
embed.add_field(name="التقييم/المشكلة", value=self.feedback_text.value, inline=False)
|
| 397 |
+
try: await send_log(interaction.guild, embed)
|
| 398 |
+
except: pass
|
| 399 |
+
|
| 400 |
+
class PostExtractView(View):
|
| 401 |
+
def __init__(self):
|
| 402 |
+
super().__init__(timeout=None)
|
| 403 |
+
|
| 404 |
+
@discord.ui.button(label="📝 إبلاغ عن مشكلة / اقتراح", style=discord.ButtonStyle.secondary, custom_id="feedback_btn")
|
| 405 |
+
async def feedback(self, interaction: discord.Interaction, button: Button):
|
| 406 |
+
await interaction.response.send_modal(FeedbackModal())
|
| 407 |
+
# ================================
|
| 408 |
+
# 🗄️ 7. Database & Auto-Backup System
|
| 409 |
+
# ================================
|
| 410 |
+
class UserDatabase:
|
| 411 |
+
def __init__(self):
|
| 412 |
+
self.lock = threading.Lock()
|
| 413 |
+
self.db = self.load_db()
|
| 414 |
+
self.init_bot_config()
|
| 415 |
+
|
| 416 |
+
def load_db(self):
|
| 417 |
+
if os.path.exists(USERS_DB_FILE):
|
| 418 |
+
try: return json.load(open(USERS_DB_FILE, 'r', encoding='utf-8'))
|
| 419 |
+
except: return {}
|
| 420 |
+
return {}
|
| 421 |
+
|
| 422 |
+
def save_db(self):
|
| 423 |
+
try:
|
| 424 |
+
with open(USERS_DB_FILE, 'w', encoding='utf-8') as f:
|
| 425 |
+
json.dump(self.db, f, indent=4, ensure_ascii=False)
|
| 426 |
+
except: pass
|
| 427 |
+
|
| 428 |
+
def init_bot_config(self):
|
| 429 |
+
with self.lock:
|
| 430 |
+
if "bot_config" not in self.db:
|
| 431 |
+
self.db["bot_config"] = {"advance_model": "gemini-2.5-flash", "normal_model": "gemini-2.5-flash", "bot_admins": []}
|
| 432 |
+
self.save_db()
|
| 433 |
+
|
| 434 |
+
def is_bot_admin(self, user_id):
|
| 435 |
+
return user_id in self.db.get("bot_config", {}).get("bot_admins", [])
|
| 436 |
+
|
| 437 |
+
def add_bot_admin(self, user_id):
|
| 438 |
+
with self.lock:
|
| 439 |
+
if user_id not in self.db["bot_config"]["bot_admins"]:
|
| 440 |
+
self.db["bot_config"]["bot_admins"].append(user_id)
|
| 441 |
+
self.save_db()
|
| 442 |
+
|
| 443 |
+
def get_bot_setting(self, key):
|
| 444 |
+
with self.lock:
|
| 445 |
+
self.db = self.load_db()
|
| 446 |
+
return self.db.get("bot_config", {}).get(key, "gemini-2.5-flash")
|
| 447 |
+
|
| 448 |
+
def update_bot_setting(self, key, value):
|
| 449 |
+
with self.lock:
|
| 450 |
+
self.db = self.load_db()
|
| 451 |
+
if "bot_config" not in self.db: self.db["bot_config"] = {}
|
| 452 |
+
self.db["bot_config"][key] = value
|
| 453 |
+
self.save_db()
|
| 454 |
+
|
| 455 |
+
def get_user(self, user_id, username):
|
| 456 |
+
with self.lock:
|
| 457 |
+
self.db = self.load_db()
|
| 458 |
+
uid = str(user_id)
|
| 459 |
+
if uid not in self.db:
|
| 460 |
+
self.db[uid] = {
|
| 461 |
+
"name": username, "points": 0.0, "registered": False, "rank": "Normal",
|
| 462 |
+
"images_extracted": 0, "points_spent": 0.0, "free_merges": 3,
|
| 463 |
+
"settings": {"format": "docx", "blank_line": True, "include_sfx": False, "language": "ar", "advance_mode": False, "custom_tags": {}}
|
| 464 |
+
}
|
| 465 |
+
self.save_db()
|
| 466 |
+
return self.db[uid]
|
| 467 |
+
|
| 468 |
+
def modify_points(self, user_id, amount, spent=False):
|
| 469 |
+
with self.lock:
|
| 470 |
+
self.db = self.load_db()
|
| 471 |
+
uid = str(user_id)
|
| 472 |
+
if uid in self.db:
|
| 473 |
+
self.db[uid]["points"] = round(self.db[uid]["points"] + amount, 2)
|
| 474 |
+
if spent and amount < 0:
|
| 475 |
+
self.db[uid]["points_spent"] = round(self.db[uid].get("points_spent", 0) + abs(amount), 2)
|
| 476 |
+
self.save_db()
|
| 477 |
+
return self.db[uid]["points"]
|
| 478 |
+
return 0
|
| 479 |
+
|
| 480 |
+
def add_stats(self, user_id, images_count):
|
| 481 |
+
with self.lock:
|
| 482 |
+
uid = str(user_id)
|
| 483 |
+
if uid in self.db:
|
| 484 |
+
self.db[uid]["images_extracted"] = self.db[uid].get("images_extracted", 0) + images_count
|
| 485 |
+
self.save_db()
|
| 486 |
+
|
| 487 |
+
def update_setting(self, user_id, key, value):
|
| 488 |
+
with self.lock:
|
| 489 |
+
self.db = self.load_db()
|
| 490 |
+
uid = str(user_id)
|
| 491 |
+
if uid in self.db:
|
| 492 |
+
self.db[uid]["settings"][key] = value
|
| 493 |
+
self.save_db()
|
| 494 |
+
|
| 495 |
+
db = UserDatabase()
|
| 496 |
+
|
| 497 |
+
# الحفظ الاحتياطي السحابي (كل ساعة) لضمان عدم ضياع البيانات في Hugging Face
|
| 498 |
+
@tasks.loop(hours=1)
|
| 499 |
+
async def backup_task():
|
| 500 |
+
try:
|
| 501 |
+
log_channel = bot.get_channel(LOG_CHANNEL_ID)
|
| 502 |
+
if log_channel and os.path.exists(USERS_DB_FILE):
|
| 503 |
+
async for msg in log_channel.history(limit=10):
|
| 504 |
+
if msg.author == bot.user and "💾 النسخة الاحتياطية" in msg.content:
|
| 505 |
+
try: await msg.delete()
|
| 506 |
+
except: pass
|
| 507 |
+
|
| 508 |
+
# رفع نسخة من قاعدة البيانات وملف التراكر
|
| 509 |
+
files = [discord.File(USERS_DB_FILE)]
|
| 510 |
+
if os.path.exists(TRACKER_DB_FILE): files.append(discord.File(TRACKER_DB_FILE))
|
| 511 |
+
await log_channel.send("💾 النسخة الاحتياطية التلقائية لقاعدة البيانات (لا تقم بمسح هذه الرسالة)", files=files)
|
| 512 |
+
except Exception as e:
|
| 513 |
+
print(f"Backup Error: {e}")
|
| 514 |
+
|
| 515 |
+
async def send_log(guild, embed, files=None):
|
| 516 |
+
if not LOG_CHANNEL_ID: return
|
| 517 |
+
channel = guild.get_channel(LOG_CHANNEL_ID)
|
| 518 |
+
if channel:
|
| 519 |
+
try: await channel.send(embed=embed, files=files or [])
|
| 520 |
+
except: pass
|
| 521 |
+
|
| 522 |
+
# ================================
|
| 523 |
+
# 🖼️ 8. Image Toolbox (Smart Stitching & Splitting)
|
| 524 |
+
# ================================
|
| 525 |
+
class ImageToolbox:
|
| 526 |
+
@staticmethod
|
| 527 |
+
def find_blank_y(img, start_y, end_y):
|
| 528 |
+
# خوارزمية ذكية للبحث عن مساحة بيضاء (فراغ بين الفقاعات) لتفادي القص الخاطئ
|
| 529 |
+
try:
|
| 530 |
+
gray = img.convert('L')
|
| 531 |
+
best_y = start_y
|
| 532 |
+
min_variance = float('inf')
|
| 533 |
+
for y in range(start_y, end_y, 10): # قفزات 10 بيكسل لتسريع العملية
|
| 534 |
+
row = [gray.getpixel((x, y)) for x in range(gray.width)]
|
| 535 |
+
variance = max(row) - min(row)
|
| 536 |
+
if variance < min_variance:
|
| 537 |
+
min_variance = variance
|
| 538 |
+
best_y = y
|
| 539 |
+
if min_variance < 5: return y # خط أبيض مثالي
|
| 540 |
+
return best_y
|
| 541 |
+
except: return (start_y + end_y) // 2
|
| 542 |
+
|
| 543 |
+
@staticmethod
|
| 544 |
+
def check_and_split(image_path, is_advance=False):
|
| 545 |
+
try:
|
| 546 |
+
with Image.open(image_path) as img:
|
| 547 |
+
width, height = img.size
|
| 548 |
+
|
| 549 |
+
# الوضع الاحترافي: يقص بدقة أعلى لمعالجة أوضح
|
| 550 |
+
threshold = 4000 if is_advance else 6000
|
| 551 |
+
if height <= threshold:
|
| 552 |
+
return [image_path], False, height
|
| 553 |
+
|
| 554 |
+
mid = height // 2
|
| 555 |
+
scan_range = int(height * 0.15)
|
| 556 |
+
# البحث عن نقطة القص الذكية في المنتصف
|
| 557 |
+
best_cut_y = ImageToolbox.find_blank_y(img, max(0, mid - scan_range), min(height, mid + scan_range))
|
| 558 |
+
|
| 559 |
+
overlap = 20
|
| 560 |
+
top = img.crop((0, 0, width, min(height, best_cut_y + overlap)))
|
| 561 |
+
btm = img.crop((0, max(0, best_cut_y - overlap), width, height))
|
| 562 |
+
|
| 563 |
+
base, ext = os.path.splitext(image_path)
|
| 564 |
+
p1, p2 = f"{base}_p1{ext}", f"{base}_p2{ext}"
|
| 565 |
+
top.save(p1); btm.save(p2)
|
| 566 |
+
return [p1, p2], True, height
|
| 567 |
+
except: return [image_path], False, 0
|
| 568 |
+
|
| 569 |
+
@staticmethod
|
| 570 |
+
def merge_images_vertically(image_paths, output_path, max_height=50000):
|
| 571 |
+
try:
|
| 572 |
+
imgs = [Image.open(p).convert('RGB') for p in image_paths]
|
| 573 |
+
if not imgs: return False
|
| 574 |
+
base_w = max(im.width for im in imgs)
|
| 575 |
+
parts, curr_imgs, curr_h = [], [], 0
|
| 576 |
+
|
| 577 |
+
for img in imgs:
|
| 578 |
+
# توحيد العرض مع الحفاظ على النسبة والتناسب
|
| 579 |
+
if img.width != base_w:
|
| 580 |
+
img = img.resize((base_w, int(img.height * (base_w / img.width))), Image.Resampling.LANCZOS)
|
| 581 |
+
|
| 582 |
+
# نظام الدمج الذكي لتفادي كسر الـ max_height بشكل غبي
|
| 583 |
+
if curr_h + img.height > max_height and curr_imgs:
|
| 584 |
+
parts.append((curr_imgs, base_w, curr_h))
|
| 585 |
+
curr_imgs, curr_h = [], 0
|
| 586 |
+
|
| 587 |
+
curr_imgs.append(img)
|
| 588 |
+
curr_h += img.height
|
| 589 |
+
|
| 590 |
+
if curr_imgs: parts.append((curr_imgs, base_w, curr_h))
|
| 591 |
+
|
| 592 |
+
out_files = []
|
| 593 |
+
for idx, (p_imgs, w, h) in enumerate(parts):
|
| 594 |
+
canv = Image.new('RGB', (w, h), (255, 255, 255))
|
| 595 |
+
y_off = 0
|
| 596 |
+
for im in p_imgs:
|
| 597 |
+
canv.paste(im, (0, y_off))
|
| 598 |
+
y_off += im.height
|
| 599 |
+
|
| 600 |
+
fname = output_path.replace(".jpg", f"_part{idx+1}.jpg") if len(parts) > 1 else output_path
|
| 601 |
+
canv.save(fname, quality=90)
|
| 602 |
+
out_files.append(fname)
|
| 603 |
+
return out_files
|
| 604 |
+
except Exception as e:
|
| 605 |
+
print(f"Merge Error: {e}")
|
| 606 |
+
return False
|
| 607 |
+
|
| 608 |
+
# ================================
|
| 609 |
+
# 📥 9. Universal Downloader & Advanced Web Scraper
|
| 610 |
+
# ================================
|
| 611 |
+
class DownloadManager:
|
| 612 |
+
@staticmethod
|
| 613 |
+
def extract_archives(folder):
|
| 614 |
+
for root, _, files in os.walk(folder):
|
| 615 |
+
for file in files:
|
| 616 |
+
fp = os.path.join(root, file)
|
| 617 |
+
try:
|
| 618 |
+
if file.lower().endswith(".zip"):
|
| 619 |
+
with zipfile.ZipFile(fp, 'r') as z: z.extractall(root)
|
| 620 |
+
os.remove(fp)
|
| 621 |
+
elif file.lower().endswith((".rar", ".cbr", ".cbz")):
|
| 622 |
+
patoolib.extract_archive(fp, outdir=root, verbosity=-1)
|
| 623 |
+
os.remove(fp)
|
| 624 |
+
except: pass
|
| 625 |
+
for root, dirs, files in os.walk(folder):
|
| 626 |
+
for name in files:
|
| 627 |
+
if name.startswith('.') or name.startswith('__MACOSX'):
|
| 628 |
+
try: os.remove(os.path.join(root, name))
|
| 629 |
+
except: pass
|
| 630 |
+
|
| 631 |
+
@staticmethod
|
| 632 |
+
def scrape_web_selenium(url, output_folder):
|
| 633 |
+
# 🚀 تحديث جذري لسحب مواقع الويب وحل مشكلة صورتين فقط (Lazy Loading Fix)
|
| 634 |
+
driver = None
|
| 635 |
+
try:
|
| 636 |
+
chrome_options = Options()
|
| 637 |
+
chrome_options.add_argument('--headless=new')
|
| 638 |
+
chrome_options.add_argument('--no-sandbox')
|
| 639 |
+
chrome_options.add_argument('--disable-dev-shm-usage')
|
| 640 |
+
chrome_options.add_argument("user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36")
|
| 641 |
+
|
| 642 |
+
service = Service(ChromeDriverManager().install())
|
| 643 |
+
driver = webdriver.Chrome(service=service, options=chrome_options)
|
| 644 |
+
driver.get(url)
|
| 645 |
+
time.sleep(5)
|
| 646 |
+
|
| 647 |
+
# النزول البطيء والمستمر لتحفيز تحميل الصور
|
| 648 |
+
last_h = driver.execute_script("return document.body.scrollHeight")
|
| 649 |
+
for _ in range(60): # محاولة النزول 60 مرة
|
| 650 |
+
driver.execute_script("window.scrollBy(0, 1000);")
|
| 651 |
+
time.sleep(1.2) # انتظار كافٍ لرد السيرفر
|
| 652 |
+
new_h = driver.execute_script("return document.body.scrollHeight")
|
| 653 |
+
if new_h == last_h:
|
| 654 |
+
time.sleep(3) # محاولة أخيرة لو توقف النزول
|
| 655 |
+
new_h = driver.execute_script("return document.body.scrollHeight")
|
| 656 |
+
if new_h == last_h: break
|
| 657 |
+
last_h = new_h
|
| 658 |
+
|
| 659 |
+
time.sleep(3)
|
| 660 |
+
|
| 661 |
+
# استخراج الصور بذكاء (تجاهل الأيقونات والبنرات)
|
| 662 |
+
elems = driver.find_elements(By.TAG_NAME, 'img')
|
| 663 |
+
urls = []
|
| 664 |
+
for e in elems:
|
| 665 |
+
src = e.get_attribute('src') or e.get_attribute('data-src') or e.get_attribute('data-original')
|
| 666 |
+
if src and src.startswith('http') and not any(x in src.lower() for x in ['logo','ad','banner','icon','avatar', 'profile']):
|
| 667 |
+
urls.append(src)
|
| 668 |
+
|
| 669 |
+
# إزالة التكرار مع الحفاظ على الترتيب الأصلي
|
| 670 |
+
urls = list(dict.fromkeys(urls))
|
| 671 |
+
|
| 672 |
+
saved_paths = []
|
| 673 |
+
req_headers = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)'}
|
| 674 |
+
|
| 675 |
+
for idx, u in enumerate(urls):
|
| 676 |
+
try:
|
| 677 |
+
c = requests.get(u, headers=req_headers, timeout=15).content
|
| 678 |
+
im = Image.open(io.BytesIO(c)).convert('RGB')
|
| 679 |
+
# التأكد من أن الصورة هي صفحة مانهوا فعلية (أبعاد كبيرة) وليست أيقونة صغيرة
|
| 680 |
+
if im.width > 300 and im.height > 300:
|
| 681 |
+
path = os.path.join(output_folder, f"page_{idx:03d}.jpg")
|
| 682 |
+
im.save(path)
|
| 683 |
+
saved_paths.append(path)
|
| 684 |
+
except: pass
|
| 685 |
+
|
| 686 |
+
title = driver.title.split('|')[0].strip() if driver.title else "Web_Chapter"
|
| 687 |
+
title = re.sub(r'[\\/*?:"<>|]', "", title)
|
| 688 |
+
driver.quit()
|
| 689 |
+
return saved_paths, title
|
| 690 |
+
except Exception as e:
|
| 691 |
+
if driver: driver.quit()
|
| 692 |
+
print(f"Scraper Error: {e}")
|
| 693 |
+
return [], "Error"
|
| 694 |
+
|
| 695 |
+
@staticmethod
|
| 696 |
+
async def download(url, output_folder):
|
| 697 |
+
success = False
|
| 698 |
+
name = "Downloaded_Content"
|
| 699 |
+
|
| 700 |
+
# التوجه مباشرة لخطة الطوارئ gdown لتحميل الملفات
|
| 701 |
+
if "drive.google.com" in url:
|
| 702 |
+
try:
|
| 703 |
+
if "folder" in url or "drive.google.com/drive/folders" in url:
|
| 704 |
+
res = await asyncio.to_thread(gdown.download_folder, url, output=output_folder, quiet=False, use_cookies=False)
|
| 705 |
+
if res: success = True
|
| 706 |
+
else:
|
| 707 |
+
res = await asyncio.to_thread(gdown.download, url, output=os.path.join(output_folder, "gdown_fallback.zip"), quiet=False, fuzzy=True)
|
| 708 |
+
if res: success = True
|
| 709 |
+
if success: name = "Drive_Content"
|
| 710 |
+
except Exception as e: print(f"Drive Error: {e}")
|
| 711 |
+
else:
|
| 712 |
+
try:
|
| 713 |
+
async with aiohttp.ClientSession() as s:
|
| 714 |
+
async with s.get(url, headers={'User-Agent': 'Mozilla/5.0'}) as r:
|
| 715 |
+
if r.status == 200:
|
| 716 |
+
cd = r.headers.get('Content-Disposition', '')
|
| 717 |
+
if 'filename=' in cd: name = re.findall("filename=(.+)", cd)[0].strip('"')
|
| 718 |
+
else: name = "file.zip"
|
| 719 |
+
with open(os.path.join(output_folder, name), 'wb') as f: f.write(await r.read())
|
| 720 |
+
success = True
|
| 721 |
+
except: pass
|
| 722 |
+
|
| 723 |
+
DownloadManager.extract_archives(output_folder)
|
| 724 |
+
has_images = any(f.lower().endswith(('.png','.jpg','.jpeg','.webp')) for r, d, files in os.walk(output_folder) for f in files)
|
| 725 |
+
if success and has_images: return True, name
|
| 726 |
+
return False, "Error downloading or no images found."
|
| 727 |
+
|
| 728 |
+
@staticmethod
|
| 729 |
+
def get_chapters_dict(folder):
|
| 730 |
+
chapters = {}
|
| 731 |
+
subdirs = [d for d in os.listdir(folder) if os.path.isdir(os.path.join(folder, d))]
|
| 732 |
+
if not subdirs:
|
| 733 |
+
imgs = [os.path.join(folder, f) for f in os.listdir(folder) if f.lower().endswith(('.png','.jpg','.jpeg','.webp'))]
|
| 734 |
+
# هنا نستخدم دالة natural_sort_key التي وضعناها في الجزء الأول لترتيب الصور بشكل بشري دقيق
|
| 735 |
+
if imgs: chapters["Main"] = sorted(imgs, key=natural_sort_key)
|
| 736 |
+
else:
|
| 737 |
+
for d in sorted(subdirs, key=natural_sort_key):
|
| 738 |
+
d_path = os.path.join(folder, d)
|
| 739 |
+
imgs = [os.path.join(d_path, f) for f in os.listdir(d_path) if f.lower().endswith(('.png','.jpg','.jpeg','.webp'))]
|
| 740 |
+
if imgs: chapters[d] = sorted(imgs, key=natural_sort_key)
|
| 741 |
+
return chapters
|
| 742 |
+
|
| 743 |
+
# ================================
|
| 744 |
+
# 🔗 10. Universal Uploader (Gofile + Catbox Fallback)
|
| 745 |
+
# ================================
|
| 746 |
+
class FileUploader:
|
| 747 |
+
@staticmethod
|
| 748 |
+
async def upload(file_path):
|
| 749 |
+
# المحاولة الأولى: Gofile
|
| 750 |
+
try:
|
| 751 |
+
server_res = await asyncio.to_thread(requests.get, "https://api.gofile.io/servers")
|
| 752 |
+
server = server_res.json()['data']['servers'][0]['name']
|
| 753 |
+
with open(file_path, 'rb') as f:
|
| 754 |
+
upload_res = await asyncio.to_thread(requests.post, f"https://{server}.gofile.io/contents/upload", files={'file': f})
|
| 755 |
+
return upload_res.json()['data']['downloadPage']
|
| 756 |
+
except Exception as e:
|
| 757 |
+
print(f"⚠️ فشل الرفع على Gofile، جاري التحويل إلى Catbox... ({e})")
|
| 758 |
+
|
| 759 |
+
# المحاولة الثانية (Fallback): Catbox (سريع وموثوق جداً)
|
| 760 |
+
try:
|
| 761 |
+
with open(file_path, 'rb') as f:
|
| 762 |
+
res = await asyncio.to_thread(requests.post, "https://catbox.moe/user/api.php", data={"reqtype": "fileupload"}, files={"fileToUpload": f})
|
| 763 |
+
if res.status_code == 200: return res.text
|
| 764 |
+
except Exception as e:
|
| 765 |
+
print(f"❌ فشل الرفع على جميع السيرفرات: {e}")
|
| 766 |
+
|
| 767 |
+
return None
|
| 768 |
+
|
| 769 |
+
# ================================
|
| 770 |
+
# 🕹️ 11. UI Components (OCR specific views)
|
| 771 |
+
# ================================
|
| 772 |
+
class CostConfirmView(View):
|
| 773 |
+
def __init__(self, user_id, cost, lang, on_confirm_callback, tmp_dir):
|
| 774 |
+
super().__init__(timeout=86400)
|
| 775 |
+
self.user_id = user_id
|
| 776 |
+
self.callback = on_confirm_callback
|
| 777 |
+
self.tmp_dir = tmp_dir
|
| 778 |
+
|
| 779 |
+
self.btn_yes.label = "✅ استكمال (نعم)" if lang == "ar" else "✅ Proceed (Yes)"
|
| 780 |
+
self.btn_no.label = "❌ إلغاء (لا)" if lang == "ar" else "❌ Cancel (No)"
|
| 781 |
+
|
| 782 |
+
@discord.ui.button(style=discord.ButtonStyle.success, custom_id="btn_yes")
|
| 783 |
+
async def btn_yes(self, interaction: discord.Interaction, button: Button):
|
| 784 |
+
if interaction.user.id != self.user_id: return
|
| 785 |
+
for child in self.children: child.disabled = True
|
| 786 |
+
await interaction.response.edit_message(content=f"{interaction.user.mention} ⏳ يتم التجهيز للاستخراج...", embed=None, view=None)
|
| 787 |
+
await self.callback(interaction)
|
| 788 |
+
self.stop()
|
| 789 |
+
|
| 790 |
+
@discord.ui.button(style=discord.ButtonStyle.danger, custom_id="btn_no")
|
| 791 |
+
async def btn_no(self, interaction: discord.Interaction, button: Button):
|
| 792 |
+
if interaction.user.id != self.user_id: return
|
| 793 |
+
for child in self.children: child.disabled = True
|
| 794 |
+
shutil.rmtree(self.tmp_dir, ignore_errors=True)
|
| 795 |
+
await interaction.response.edit_message(content=f"{interaction.user.mention} ⛔ تم الإلغاء وحذف الملفات المؤقتة.", embed=None, view=None)
|
| 796 |
+
self.stop()
|
| 797 |
+
|
| 798 |
+
async def on_timeout(self):
|
| 799 |
+
shutil.rmtree(self.tmp_dir, ignore_errors=True)
|
| 800 |
+
|
| 801 |
+
class ChapterSelectView(View):
|
| 802 |
+
def __init__(self, user_id, chapters_dict, on_select_callback, tmp_dir):
|
| 803 |
+
super().__init__(timeout=86400)
|
| 804 |
+
self.user_id = user_id
|
| 805 |
+
self.chapters_dict = chapters_dict
|
| 806 |
+
self.callback = on_select_callback
|
| 807 |
+
self.tmp_dir = tmp_dir
|
| 808 |
+
|
| 809 |
+
options = []
|
| 810 |
+
for name, imgs in list(chapters_dict.items())[:24]:
|
| 811 |
+
options.append(discord.SelectOption(label=f"فصل: {name[:50]}", description=f"{len(imgs)} صور", value=name))
|
| 812 |
+
options.append(discord.SelectOption(label="🌟 تحديد الكل", description="سحب كل الفصول", value="ALL"))
|
| 813 |
+
|
| 814 |
+
self.select = Select(placeholder="اختر الفصول المراد معالجتها...", min_values=1, max_values=len(options), options=options)
|
| 815 |
+
self.select.callback = self.select_callback
|
| 816 |
+
self.add_item(self.select)
|
| 817 |
+
|
| 818 |
+
async def select_callback(self, interaction: discord.Interaction):
|
| 819 |
+
if interaction.user.id != self.user_id: return
|
| 820 |
+
self.select.disabled = True
|
| 821 |
+
await interaction.response.edit_message(content="✅ تم الاختيار. جاري الحساب...", view=None)
|
| 822 |
+
|
| 823 |
+
selected_chaps = {}
|
| 824 |
+
if "ALL" in self.select.values: selected_chaps = self.chapters_dict
|
| 825 |
+
else:
|
| 826 |
+
for val in self.select.values: selected_chaps[val] = self.chapters_dict[val]
|
| 827 |
+
|
| 828 |
+
await self.callback(interaction, selected_chaps)
|
| 829 |
+
self.stop()
|
| 830 |
+
|
| 831 |
+
async def on_timeout(self):
|
| 832 |
+
shutil.rmtree(self.tmp_dir, ignore_errors=True)
|
| 833 |
+
|
| 834 |
+
# ================================
|
| 835 |
+
# 🧩 12. Merge Command (/merge)
|
| 836 |
+
# ================================
|
| 837 |
+
@bot.tree.command(name="merge", description="دمج الصور بذكاء ورفعها (أول 3 مرات مجاناً)")
|
| 838 |
+
@app_commands.describe(max_height="أقصى طول للصورة المدمجة (الافتراضي 50000 بيكسل)")
|
| 839 |
+
async def merge_cmd(interaction: discord.Interaction, link: str, max_height: int = 50000):
|
| 840 |
+
user = db.get_user(interaction.user.id, interaction.user.name)
|
| 841 |
+
|
| 842 |
+
is_free = False
|
| 843 |
+
with db.lock:
|
| 844 |
+
if db.db[str(interaction.user.id)].get("free_merges", 0) > 0: is_free = True
|
| 845 |
+
elif db.db[str(interaction.user.id)]["points"] < 0.1:
|
| 846 |
+
return await interaction.response.send_message("❌ رصيدك لا يكفي (مطلوب 0.1 نقطة لعملية الدمج).", ephemeral=True)
|
| 847 |
+
|
| 848 |
+
await interaction.response.send_message(embed=discord.Embed(title="⏳ جاري سحب الصور للدمج...", color=0x9b59b6))
|
| 849 |
+
msg = await interaction.original_response()
|
| 850 |
+
|
| 851 |
+
bot_queue["merge"].append(interaction.user.name)
|
| 852 |
+
tmp = os.path.join(TEMP_ROOT, f"merge_{interaction.user.id}_{int(time.time())}")
|
| 853 |
+
os.makedirs(tmp, exist_ok=True)
|
| 854 |
+
|
| 855 |
+
try:
|
| 856 |
+
success, source_name = await DownloadManager.download(link, tmp)
|
| 857 |
+
if not success:
|
| 858 |
+
if interaction.user.name in bot_queue["merge"]: bot_queue["merge"].remove(interaction.user.name)
|
| 859 |
+
return await msg.edit(content="❌ فشل سحب الصور من الرابط.")
|
| 860 |
+
|
| 861 |
+
chapters_dict = DownloadManager.get_chapters_dict(tmp)
|
| 862 |
+
if not chapters_dict:
|
| 863 |
+
if interaction.user.name in bot_queue["merge"]: bot_queue["merge"].remove(interaction.user.name)
|
| 864 |
+
return await msg.edit(content="❌ لم يتم العثور على صور صالحة للدمج.")
|
| 865 |
+
|
| 866 |
+
imgs = list(chapters_dict.values())[0]
|
| 867 |
+
if len(imgs) < 2:
|
| 868 |
+
if interaction.user.name in bot_queue["merge"]: bot_queue["merge"].remove(interaction.user.name)
|
| 869 |
+
return await msg.edit(content="❌ الرابط يحتوي على أقل من صورتين.")
|
| 870 |
+
|
| 871 |
+
await msg.edit(embed=discord.Embed(title=f"🛠️ جاري دمج الصور بذكاء (الطول الأقصى: {max_height})...", color=0xf1c40f))
|
| 872 |
+
|
| 873 |
+
merged_files = await asyncio.to_thread(ImageToolbox.merge_images_vertically, imgs, os.path.join(tmp, f"{source_name}_merged.jpg"), max_height=max_height)
|
| 874 |
+
|
| 875 |
+
if not merged_files:
|
| 876 |
+
if interaction.user.name in bot_queue["merge"]: bot_queue["merge"].remove(interaction.user.name)
|
| 877 |
+
return await msg.edit(content="❌ فشلت عملية الدمج.")
|
| 878 |
+
|
| 879 |
+
await msg.edit(embed=discord.Embed(title="☁️ جاري الرفع للسحابة...", color=0x3498db))
|
| 880 |
+
|
| 881 |
+
zip_path = os.path.join(tmp, f"{source_name}_Merged.zip")
|
| 882 |
+
with zipfile.ZipFile(zip_path, 'w') as z:
|
| 883 |
+
for f in merged_files: z.write(f, os.path.basename(f))
|
| 884 |
+
|
| 885 |
+
upload_link = await FileUploader.upload(zip_path)
|
| 886 |
+
|
| 887 |
+
if not upload_link:
|
| 888 |
+
if interaction.user.name in bot_queue["merge"]: bot_queue["merge"].remove(interaction.user.name)
|
| 889 |
+
return await msg.edit(content="❌ فشل الرفع على Gofile والسيرفرات البديلة، تم إلغاء العملية ولم يتم خصم نقاط.")
|
| 890 |
+
|
| 891 |
+
cost_msg = ""
|
| 892 |
+
with db.lock:
|
| 893 |
+
if is_free:
|
| 894 |
+
db.db[str(interaction.user.id)]["free_merges"] -= 1
|
| 895 |
+
rem_free = db.db[str(interaction.user.id)]["free_merges"]
|
| 896 |
+
cost_msg = f"🎁 **مجاناً** (تبقى لك {rem_free} مرات مجانية)"
|
| 897 |
+
else:
|
| 898 |
+
db.db[str(interaction.user.id)]["points"] = round(db.db[str(interaction.user.id)]["points"] - 0.1, 2)
|
| 899 |
+
db.db[str(interaction.user.id)]["points_spent"] = round(db.db[str(interaction.user.id)].get("points_spent", 0) + 0.1, 2)
|
| 900 |
+
cost_msg = f"💰 **تم خصم 0.1 نقطة** (رصيدك: {db.db[str(interaction.user.id)]['points']} نقطة)"
|
| 901 |
+
db.save_db()
|
| 902 |
+
|
| 903 |
+
res_embed = discord.Embed(title="✅ تم الدمج والرفع بنجاح!", color=0x2ecc71)
|
| 904 |
+
res_embed.add_field(name="الرابط:", value=f"[اضغط هنا لتحميل الصور المدمجة]({upload_link})", inline=False)
|
| 905 |
+
res_embed.add_field(name="التكلفة:", value=cost_msg, inline=False)
|
| 906 |
+
await msg.edit(content=f"{interaction.user.mention}", embed=res_embed)
|
| 907 |
+
|
| 908 |
+
except Exception as e:
|
| 909 |
+
await msg.edit(content=f"⚠️ حدث خطأ أثناء الدمج: {e}")
|
| 910 |
+
finally:
|
| 911 |
+
if interaction.user.name in bot_queue["merge"]: bot_queue["merge"].remove(interaction.user.name)
|
| 912 |
+
shutil.rmtree(tmp, ignore_errors=True)
|
| 913 |
+
|
| 914 |
+
# ================================
|
| 915 |
+
# 🚀 13. Core Extraction Commands (/ocr)
|
| 916 |
+
# ================================
|
| 917 |
+
def get_rank_multiplier(user):
|
| 918 |
+
rank = user.get("rank", "Normal").lower()
|
| 919 |
+
if rank == "vvip": return 0.50
|
| 920 |
+
if rank == "vip": return 0.75
|
| 921 |
+
return 1.0
|
| 922 |
+
|
| 923 |
+
def calculate_initial_cost(user, original_img_count):
|
| 924 |
+
if original_img_count == 1: cost = 0.1
|
| 925 |
+
elif original_img_count <= 15: cost = 1.0
|
| 926 |
+
else: cost = 1.0 + ((original_img_count - 15) * 0.1)
|
| 927 |
+
return round(max(0.01, cost * get_rank_multiplier(user)), 2)
|
| 928 |
+
|
| 929 |
+
def generate_progress_bar(current, total, length=15):
|
| 930 |
+
if total == 0: return f"[{'░' * length}]"
|
| 931 |
+
percent = current / total
|
| 932 |
+
filled = int(length * percent)
|
| 933 |
+
return f"[{'█' * filled}{'░' * (length - filled)}]"
|
| 934 |
+
|
| 935 |
+
@bot.tree.command(name="ocr", description="استخراج النص من المانجا")
|
| 936 |
+
async def ocr_cmd(interaction: discord.Interaction, link: str = None, file: discord.Attachment = None):
|
| 937 |
+
if not link and not file: return await interaction.response.send_message("❌ يرجى توفير رابط أو إرفاق ملف.", ephemeral=True)
|
| 938 |
+
user = db.get_user(interaction.user.id, interaction.user.name)
|
| 939 |
+
|
| 940 |
+
if user["points"] < 0.1:
|
| 941 |
+
return await interaction.response.send_message("❌ رصيدك غير كافٍ للاستخراج.", ephemeral=True)
|
| 942 |
+
|
| 943 |
+
await interaction.response.send_message(embed=discord.Embed(title="⏳ المرحلة 1: جاري التحميل...", color=0xf1c40f))
|
| 944 |
+
msg = await interaction.original_response()
|
| 945 |
+
|
| 946 |
+
bot_queue["ocr"].append(interaction.user.name)
|
| 947 |
+
start_time = time.time()
|
| 948 |
+
task_id = f"ocr_{interaction.user.id}_{int(start_time)}"
|
| 949 |
+
tmp = os.path.join(TEMP_ROOT, task_id)
|
| 950 |
+
os.makedirs(tmp, exist_ok=True)
|
| 951 |
+
|
| 952 |
+
source_name = "Extracted_Content"
|
| 953 |
+
try:
|
| 954 |
+
dl_start = time.time()
|
| 955 |
+
if file:
|
| 956 |
+
path = os.path.join(tmp, file.filename)
|
| 957 |
+
await file.save(path)
|
| 958 |
+
DownloadManager.extract_archives(tmp)
|
| 959 |
+
source_name = file.filename.replace(".zip", "").replace(".rar", "").replace(".cbz", "")
|
| 960 |
+
else:
|
| 961 |
+
success, error_msg = await DownloadManager.download(link, tmp)
|
| 962 |
+
if not success:
|
| 963 |
+
if interaction.user.name in bot_queue["ocr"]: bot_queue["ocr"].remove(interaction.user.name)
|
| 964 |
+
shutil.rmtree(tmp, ignore_errors=True)
|
| 965 |
+
return await msg.edit(content="❌ فشل السحب. تأكد من صحة الرابط.")
|
| 966 |
+
source_name = error_msg
|
| 967 |
+
dl_time = time.time() - dl_start
|
| 968 |
+
|
| 969 |
+
chapters_dict = DownloadManager.get_chapters_dict(tmp)
|
| 970 |
+
if not chapters_dict:
|
| 971 |
+
if interaction.user.name in bot_queue["ocr"]: bot_queue["ocr"].remove(interaction.user.name)
|
| 972 |
+
shutil.rmtree(tmp, ignore_errors=True)
|
| 973 |
+
return await msg.edit(content="❌ لم يتم العثور على صور صالحة للاستخراج.")
|
| 974 |
+
|
| 975 |
+
if len(chapters_dict) > 1:
|
| 976 |
+
info = f"📦 تم العثور على **{len(chapters_dict)}** فصول. يرجى اختيار الفصول المراد معالجتها:"
|
| 977 |
+
async def on_chapter_selected(inter, selected_chapters):
|
| 978 |
+
await start_parallel_extraction(inter, msg, user, selected_chapters, tmp, task_id, dl_time, source_name)
|
| 979 |
+
view = ChapterSelectView(interaction.user.id, chapters_dict, on_chapter_selected, tmp)
|
| 980 |
+
await msg.edit(content=info, embed=None, view=view)
|
| 981 |
+
else:
|
| 982 |
+
await start_parallel_extraction(interaction, msg, user, chapters_dict, tmp, task_id, dl_time, source_name)
|
| 983 |
+
|
| 984 |
+
except Exception as e:
|
| 985 |
+
if interaction.user.name in bot_queue["ocr"]: bot_queue["ocr"].remove(interaction.user.name)
|
| 986 |
+
await interaction.followup.send(f"⚠️ خطأ غير متوقع: {e}", ephemeral=True)
|
| 987 |
+
shutil.rmtree(tmp, ignore_errors=True)
|
| 988 |
+
|
| 989 |
+
async def start_parallel_extraction(interaction, msg, user, chapters_dict, tmp_dir, task_id, dl_time, source_name):
|
| 990 |
+
task = asyncio.create_task(process_parallel_chapters(interaction, msg, user, chapters_dict, tmp_dir, task_id, dl_time, source_name))
|
| 991 |
+
active_tasks[task_id] = task
|
| 992 |
+
try: await task
|
| 993 |
+
except asyncio.CancelledError: pass
|
| 994 |
+
finally:
|
| 995 |
+
active_tasks.pop(task_id, None); active_tasks.pop(f"{task_id}_progress", None)
|
| 996 |
+
if interaction.user.name in bot_queue["ocr"]: bot_queue["ocr"].remove(interaction.user.name)
|
| 997 |
+
|
| 998 |
+
async def process_parallel_chapters(interaction, msg, user, chapters_dict, tmp_dir, task_id, dl_time, source_name):
|
| 999 |
+
all_imgs = []
|
| 1000 |
+
for imgs in chapters_dict.values(): all_imgs.extend(imgs)
|
| 1001 |
+
is_advance = user["settings"].get("advance_mode", False)
|
| 1002 |
+
original_img_count = len(all_imgs)
|
| 1003 |
+
total_cost = calculate_initial_cost(user, original_img_count)
|
| 1004 |
+
lang = user["settings"].get("language", "ar")
|
| 1005 |
+
|
| 1006 |
+
async def execute_ocr(inter=interaction):
|
| 1007 |
+
total_cost_deducted = 0.0
|
| 1008 |
+
try:
|
| 1009 |
+
if db.get_user(inter.user.id, inter.user.name)["points"] < total_cost:
|
| 1010 |
+
return await msg.edit(content=f"❌ رصيدك لا يكفي. التكلفة `{total_cost:.2f}`.", view=None)
|
| 1011 |
+
|
| 1012 |
+
db.modify_points(inter.user.id, -total_cost, spent=True)
|
| 1013 |
+
total_cost_deducted = total_cost
|
| 1014 |
+
|
| 1015 |
+
ocr_start = time.time()
|
| 1016 |
+
total_pages = original_img_count
|
| 1017 |
+
active_tasks[f"{task_id}_progress"] = 0
|
| 1018 |
+
|
| 1019 |
+
def get_ui_embed(curr, total):
|
| 1020 |
+
e = discord.Embed(title="🚀 جاري استخراج النصوص...", color=0xf1c40f)
|
| 1021 |
+
e.add_field(name="الملف/الفصل", value=f"`{source_name}`", inline=False)
|
| 1022 |
+
percent = int((curr/total)*100) if total > 0 else 0
|
| 1023 |
+
e.add_field(name="تقدم الصور", value=f"`{generate_progress_bar(curr, total)}` **{percent}%** ({curr}/{total})", inline=False)
|
| 1024 |
+
return e
|
| 1025 |
+
|
| 1026 |
+
cancel_view = CancelView(inter.user.id, task_id, total_pages)
|
| 1027 |
+
await msg.edit(content=" ", embed=get_ui_embed(0, total_pages), view=cancel_view)
|
| 1028 |
+
|
| 1029 |
+
total_bubbles = 0
|
| 1030 |
+
failed_pages = 0
|
| 1031 |
+
# تقليل السعة لتفادي ضغط API (Load Balancing)
|
| 1032 |
+
semaphore = asyncio.Semaphore(4)
|
| 1033 |
+
|
| 1034 |
+
async def process_single_image(img_path):
|
| 1035 |
+
async with semaphore:
|
| 1036 |
+
# نقل القص إلى خيط منفصل لمنع خنق المعالج (CPU Choke Fix)
|
| 1037 |
+
parts, _, _ = await asyncio.to_thread(ImageToolbox.check_and_split, img_path, is_advance)
|
| 1038 |
+
page_texts = []
|
| 1039 |
+
for p in parts:
|
| 1040 |
+
raw = await ai_engine.analyze_image(p, advance_mode=is_advance)
|
| 1041 |
+
page_texts.append(raw)
|
| 1042 |
+
|
| 1043 |
+
active_tasks[f"{task_id}_progress"] += 1
|
| 1044 |
+
curr_prog = active_tasks[f"{task_id}_progress"]
|
| 1045 |
+
try: await msg.edit(embed=get_ui_embed(curr_prog, total_pages), view=cancel_view)
|
| 1046 |
+
except: pass
|
| 1047 |
+
|
| 1048 |
+
return page_texts
|
| 1049 |
+
|
| 1050 |
+
for chap_name, imgs in chapters_dict.items():
|
| 1051 |
+
tasks = [process_single_image(img_path) for img_path in imgs]
|
| 1052 |
+
results = await asyncio.gather(*tasks)
|
| 1053 |
+
|
| 1054 |
+
res_text = []
|
| 1055 |
+
for idx, page_parts_raw in enumerate(results):
|
| 1056 |
+
page_txt = []
|
| 1057 |
+
is_failed = True
|
| 1058 |
+
for raw in page_parts_raw:
|
| 1059 |
+
if raw.strip():
|
| 1060 |
+
cleaned = clean_text(raw, user["settings"])
|
| 1061 |
+
if cleaned:
|
| 1062 |
+
is_failed = False
|
| 1063 |
+
valid_lines = [x for x in cleaned if x.strip() and not x.strip() == "OT: [No Text]"]
|
| 1064 |
+
total_bubbles += len(valid_lines)
|
| 1065 |
+
page_txt.append("\n".join(cleaned))
|
| 1066 |
+
|
| 1067 |
+
if is_failed: failed_pages += 1
|
| 1068 |
+
combined_page = "\n".join([pt for pt in page_txt if pt.strip()])
|
| 1069 |
+
res_text.append(f"--- Page {idx+1} ---\n{combined_page}")
|
| 1070 |
+
|
| 1071 |
+
full_text = "\n\n".join(res_text)
|
| 1072 |
+
fmt = user["settings"].get("format", "docx")
|
| 1073 |
+
final_chap_name = source_name if chap_name == "Main" else chap_name
|
| 1074 |
+
safe_chap_name = re.sub(r'[\\/*?:"<>|]', "", final_chap_name)
|
| 1075 |
+
out_p = os.path.join(tmp_dir, f"{safe_chap_name}.{fmt}")
|
| 1076 |
+
|
| 1077 |
+
if fmt == "docx":
|
| 1078 |
+
doc = Document()
|
| 1079 |
+
for line in full_text.split('\n'): doc.add_paragraph(line)
|
| 1080 |
+
doc.save(out_p)
|
| 1081 |
+
else:
|
| 1082 |
+
with open(out_p, 'w', encoding='utf-8') as f: f.write(full_text)
|
| 1083 |
+
|
| 1084 |
+
await inter.channel.send(content=f"📄 تم استخراج فصل: **{final_chap_name}**", file=discord.File(out_p))
|
| 1085 |
+
|
| 1086 |
+
db.add_stats(inter.user.id, total_pages)
|
| 1087 |
+
extract_time = time.time() - ocr_start
|
| 1088 |
+
total_time = dl_time + extract_time
|
| 1089 |
+
|
| 1090 |
+
new_balance = db.get_user(inter.user.id, inter.user.name)["points"]
|
| 1091 |
+
stats_msg = f"""✅ **Extraction Complete for {inter.user.mention}!**
|
| 1092 |
+
|
| 1093 |
+
💰 **{total_cost:.2f} points deducted. New balance: {new_balance:.2f}**
|
| 1094 |
+
|
| 1095 |
+
📊 **Stats for `{source_name}`:**
|
| 1096 |
+
• **Total images:** `{total_pages}`
|
| 1097 |
+
• **Text extracted:** `{total_bubbles}`
|
| 1098 |
+
• **Failed extractions:** `{failed_pages}`
|
| 1099 |
+
|
| 1100 |
+
⏱️ **Processing Time:**
|
| 1101 |
+
• **Download:** `{dl_time:.2f}s`
|
| 1102 |
+
• **Extraction:** `{extract_time:.2f}s`
|
| 1103 |
+
• **Total:** `{total_time:.2f}s`"""
|
| 1104 |
+
|
| 1105 |
+
await msg.delete()
|
| 1106 |
+
await inter.channel.send(content=stats_msg, view=PostExtractView())
|
| 1107 |
+
|
| 1108 |
+
except asyncio.CancelledError:
|
| 1109 |
+
if total_cost_deducted > 0:
|
| 1110 |
+
db.modify_points(inter.user.id, total_cost_deducted, spent=False)
|
| 1111 |
+
await inter.channel.send(f"♻️ تم استرداد **{total_cost_deducted:.2f}** نقطة.")
|
| 1112 |
+
raise
|
| 1113 |
+
except Exception as e:
|
| 1114 |
+
if total_cost_deducted > 0:
|
| 1115 |
+
db.modify_points(inter.user.id, total_cost_deducted, spent=False)
|
| 1116 |
+
await inter.channel.send(f"♻️ خطأ غير متوقع. تم استرداد **{total_cost_deducted:.2f}** نقطة.")
|
| 1117 |
+
await inter.channel.send(f"⚠️ خطأ: {e}")
|
| 1118 |
+
finally:
|
| 1119 |
+
shutil.rmtree(tmp_dir, ignore_errors=True)
|
| 1120 |
+
|
| 1121 |
+
if total_cost > 1.0 and not is_advance:
|
| 1122 |
+
embed = discord.Embed(title="⚠️ تأكيد الفاتورة", description="فصل طويل، راجع التكلفة:", color=0xe67e22)
|
| 1123 |
+
embed.add_field(name="الصور", value=f"`{original_img_count}`", inline=True)
|
| 1124 |
+
embed.add_field(name="التكلفة", value=f"**`{total_cost:.2f}`** نقطة", inline=True)
|
| 1125 |
+
await msg.edit(content=" ", embed=embed, view=CostConfirmView(interaction.user.id, total_cost, lang, execute_ocr, tmp_dir))
|
| 1126 |
+
else:
|
| 1127 |
+
await execute_ocr(interaction)
|
| 1128 |
+
|
| 1129 |
+
# ================================
|
| 1130 |
+
# 🌐 14. Web OCR Command (/ocrweb)
|
| 1131 |
+
# ================================
|
| 1132 |
+
@bot.tree.command(name="ocrweb", description="سحب واستخراج النص من روابط المواقع المحمية")
|
| 1133 |
+
async def ocrweb_cmd(interaction: discord.Interaction, link: str):
|
| 1134 |
+
user = db.get_user(interaction.user.id, interaction.user.name)
|
| 1135 |
+
|
| 1136 |
+
if user["points"] < 0.1:
|
| 1137 |
+
return await interaction.response.send_message("❌ رصيدك لا يكفي (يجب أن يكون 0.1 على الأقل).", ephemeral=True)
|
| 1138 |
+
|
| 1139 |
+
embed = discord.Embed(title="🌐 جاري سحب الصور من الموقع (Web Scraping)...", color=0x9b59b6)
|
| 1140 |
+
await interaction.response.send_message(embed=embed)
|
| 1141 |
+
msg = await interaction.original_response()
|
| 1142 |
+
|
| 1143 |
+
bot_queue["ocr"].append(interaction.user.name)
|
| 1144 |
+
tmp = os.path.join(TEMP_ROOT, f"web_{interaction.user.id}_{int(time.time())}")
|
| 1145 |
+
os.makedirs(tmp, exist_ok=True)
|
| 1146 |
+
|
| 1147 |
+
total_cost_deducted = 0.0
|
| 1148 |
+
try:
|
| 1149 |
+
dl_start = time.time()
|
| 1150 |
+
# استخدام دالة الـ Scraper المحسنة من الجزء الثاني
|
| 1151 |
+
imgs, name = await asyncio.to_thread(DownloadManager.scrape_web_selenium, link, tmp)
|
| 1152 |
+
dl_time = time.time() - dl_start
|
| 1153 |
+
|
| 1154 |
+
if not imgs:
|
| 1155 |
+
if interaction.user.name in bot_queue["ocr"]: bot_queue["ocr"].remove(interaction.user.name)
|
| 1156 |
+
shutil.rmtree(tmp, ignore_errors=True)
|
| 1157 |
+
return await msg.edit(content="❌ فشل السحب. تأكد من صحة الرابط أو أن الموقع مدعوم.")
|
| 1158 |
+
|
| 1159 |
+
merged_path = os.path.join(tmp, f"{name}_merged.jpg")
|
| 1160 |
+
await asyncio.to_thread(ImageToolbox.merge_images_vertically, imgs, merged_path)
|
| 1161 |
+
|
| 1162 |
+
cost = calculate_initial_cost(user, len(imgs)) + 0.5
|
| 1163 |
+
if user["points"] < cost:
|
| 1164 |
+
if interaction.user.name in bot_queue["ocr"]: bot_queue["ocr"].remove(interaction.user.name)
|
| 1165 |
+
shutil.rmtree(tmp, ignore_errors=True)
|
| 1166 |
+
return await msg.edit(content=f"❌ رصيدك لا يكفي. التكلفة: `{cost:.2f}` نقطة.")
|
| 1167 |
+
|
| 1168 |
+
db.modify_points(interaction.user.id, -cost, spent=True)
|
| 1169 |
+
total_cost_deducted = cost
|
| 1170 |
+
|
| 1171 |
+
await msg.edit(embed=discord.Embed(title=f"🚀 جاري استخراج {len(imgs)} صور من الموقع...", color=0x3498db))
|
| 1172 |
+
|
| 1173 |
+
is_advance = user["settings"].get("advance_mode", False)
|
| 1174 |
+
semaphore = asyncio.Semaphore(4)
|
| 1175 |
+
|
| 1176 |
+
extract_start = time.time()
|
| 1177 |
+
failed_pages = 0
|
| 1178 |
+
total_bubbles = 0
|
| 1179 |
+
|
| 1180 |
+
async def process_web_img(idx, img_path):
|
| 1181 |
+
async with semaphore:
|
| 1182 |
+
raw = await ai_engine.analyze_image(img_path, advance_mode=is_advance)
|
| 1183 |
+
return idx, raw
|
| 1184 |
+
|
| 1185 |
+
tasks_list = [process_web_img(i, p) for i, p in enumerate(imgs)]
|
| 1186 |
+
results = await asyncio.gather(*tasks_list)
|
| 1187 |
+
# الترتيب حسب الاندكس لضمان عدم اللخبطة
|
| 1188 |
+
results.sort(key=lambda x: x[0])
|
| 1189 |
+
|
| 1190 |
+
res_text = []
|
| 1191 |
+
for idx, raw in results:
|
| 1192 |
+
if not raw.strip():
|
| 1193 |
+
failed_pages += 1
|
| 1194 |
+
res_text.append(f"--- Page {idx+1} ---\n[Failed to extract text]")
|
| 1195 |
+
continue
|
| 1196 |
+
|
| 1197 |
+
cleaned = clean_text(raw, user["settings"])
|
| 1198 |
+
if not cleaned: failed_pages += 1
|
| 1199 |
+
else:
|
| 1200 |
+
valid_lines = [x for x in cleaned if x.strip() and not x.strip() == "OT: [No Text]"]
|
| 1201 |
+
total_bubbles += len(valid_lines)
|
| 1202 |
+
|
| 1203 |
+
combined_page = "\n".join([pt for pt in cleaned if pt.strip()])
|
| 1204 |
+
res_text.append(f"--- Page {idx+1} ---\n{combined_page}")
|
| 1205 |
+
|
| 1206 |
+
doc_path = os.path.join(tmp, f"{name}.docx")
|
| 1207 |
+
doc = Document()
|
| 1208 |
+
for line in ("\n\n".join(res_text)).split('\n'): doc.add_paragraph(line)
|
| 1209 |
+
doc.save(doc_path)
|
| 1210 |
+
|
| 1211 |
+
db.add_stats(interaction.user.id, len(imgs))
|
| 1212 |
+
|
| 1213 |
+
extract_time = time.time() - extract_start
|
| 1214 |
+
total_time = dl_time + extract_time
|
| 1215 |
+
new_balance = db.get_user(interaction.user.id, interaction.user.name)["points"]
|
| 1216 |
+
|
| 1217 |
+
stats_msg = f"""✅ **Extraction Complete for {interaction.user.mention}!**
|
| 1218 |
+
|
| 1219 |
+
💰 **{cost:.2f} points deducted. New balance: {new_balance:.2f}**
|
| 1220 |
+
|
| 1221 |
+
📊 **Stats for `{name}`:**
|
| 1222 |
+
• **Total images:** `{len(imgs)}`
|
| 1223 |
+
• **Text extracted:** `{total_bubbles}`
|
| 1224 |
+
• **Failed extractions:** `{failed_pages}`
|
| 1225 |
+
|
| 1226 |
+
⏱️ **Processing Time:**
|
| 1227 |
+
• **Download:** `{dl_time:.2f}s`
|
| 1228 |
+
• **Extraction:** `{extract_time:.2f}s`
|
| 1229 |
+
• **Total:** `{total_time:.2f}s`"""
|
| 1230 |
+
|
| 1231 |
+
await msg.delete()
|
| 1232 |
+
await interaction.channel.send(content=stats_msg, files=[discord.File(doc_path), discord.File(merged_path)], view=PostExtractView())
|
| 1233 |
+
except Exception as e:
|
| 1234 |
+
if total_cost_deducted > 0:
|
| 1235 |
+
db.modify_points(interaction.user.id, total_cost_deducted, spent=False)
|
| 1236 |
+
await interaction.channel.send(f"♻️ تم استرداد **{total_cost_deducted:.2f}** نقطة بسبب فشل العملية.")
|
| 1237 |
+
await msg.edit(content=f"⚠️ خطأ: {e}", embed=None)
|
| 1238 |
+
finally:
|
| 1239 |
+
if interaction.user.name in bot_queue["ocr"]: bot_queue["ocr"].remove(interaction.user.name)
|
| 1240 |
+
shutil.rmtree(tmp, ignore_errors=True)
|
| 1241 |
+
# ================================
|
| 1242 |
+
# ⚙️ 15. Settings Command & UI
|
| 1243 |
+
# ================================
|
| 1244 |
+
class LegendsMainModal(Modal, title="تخصيص العلامات الأساسية"):
|
| 1245 |
+
speech = TextInput(label='كلام عادي ("")', placeholder='الافتراضي: ""', required=False, max_length=5)
|
| 1246 |
+
thought = TextInput(label='تفكير (())', placeholder='الافتراضي: ()', required=False, max_length=5)
|
| 1247 |
+
scream = TextInput(label='صراخ (%)', placeholder='الافتراضي: %', required=False, max_length=5)
|
| 1248 |
+
connected = TextInput(label='كلام متصل (//)', placeholder='الافتراضي: //', required=False, max_length=5)
|
| 1249 |
+
|
| 1250 |
+
def __init__(self, user_id):
|
| 1251 |
+
super().__init__()
|
| 1252 |
+
self.user_id = user_id
|
| 1253 |
+
|
| 1254 |
+
async def on_submit(self, interaction: discord.Interaction):
|
| 1255 |
+
user = db.get_user(self.user_id, interaction.user.name)
|
| 1256 |
+
c = user["settings"].get("custom_tags", {})
|
| 1257 |
+
if self.speech.value: c['""'] = self.speech.value
|
| 1258 |
+
if self.thought.value: c['()'] = self.thought.value
|
| 1259 |
+
if self.scream.value: c['%'] = self.scream.value
|
| 1260 |
+
if self.connected.value: c['//'] = self.connected.value
|
| 1261 |
+
db.update_setting(self.user_id, "custom_tags", c)
|
| 1262 |
+
await interaction.response.send_message("✅ تم التحديث بنجاح!", ephemeral=True)
|
| 1263 |
+
|
| 1264 |
+
class SettingsView(View):
|
| 1265 |
+
def __init__(self, user_id, user_data):
|
| 1266 |
+
super().__init__(timeout=120)
|
| 1267 |
+
self.user_id = user_id
|
| 1268 |
+
self.settings = user_data["settings"]
|
| 1269 |
+
self.update_components()
|
| 1270 |
+
|
| 1271 |
+
def update_components(self):
|
| 1272 |
+
for child in self.children:
|
| 1273 |
+
if isinstance(child, discord.ui.Button):
|
| 1274 |
+
if child.custom_id == "blk":
|
| 1275 |
+
child.style = discord.ButtonStyle.success if self.settings.get("blank_line", True) else discord.ButtonStyle.secondary
|
| 1276 |
+
elif child.custom_id == "sfx":
|
| 1277 |
+
child.style = discord.ButtonStyle.success if self.settings.get("include_sfx", False) else discord.ButtonStyle.secondary
|
| 1278 |
+
elif child.custom_id == "adv":
|
| 1279 |
+
child.style = discord.ButtonStyle.success if self.settings.get("advance_mode", False) else discord.ButtonStyle.secondary
|
| 1280 |
+
|
| 1281 |
+
@discord.ui.select(options=[
|
| 1282 |
+
discord.SelectOption(label="تعديل العلامات الأساسية", value="main")
|
| 1283 |
+
], placeholder="تخصيص علاماتك الخاصة (Custom Tags)")
|
| 1284 |
+
async def legend_select(self, interaction: discord.Interaction, select: Select):
|
| 1285 |
+
if interaction.user.id != self.user_id: return
|
| 1286 |
+
await interaction.response.send_modal(LegendsMainModal(self.user_id))
|
| 1287 |
+
|
| 1288 |
+
@discord.ui.button(label="مسافات بين السطور", custom_id="blk", row=2)
|
| 1289 |
+
async def blank_btn(self, interaction: discord.Interaction, button: Button):
|
| 1290 |
+
if interaction.user.id != self.user_id: return
|
| 1291 |
+
val = not self.settings.get("blank_line", True)
|
| 1292 |
+
db.update_setting(self.user_id, "blank_line", val)
|
| 1293 |
+
self.settings["blank_line"] = val
|
| 1294 |
+
self.update_components()
|
| 1295 |
+
await interaction.response.edit_message(view=self)
|
| 1296 |
+
|
| 1297 |
+
@discord.ui.button(label="استخراج الـ SFX", custom_id="sfx", row=2)
|
| 1298 |
+
async def sfx_btn(self, interaction: discord.Interaction, button: Button):
|
| 1299 |
+
if interaction.user.id != self.user_id: return
|
| 1300 |
+
val = not self.settings.get("include_sfx", False)
|
| 1301 |
+
db.update_setting(self.user_id, "include_sfx", val)
|
| 1302 |
+
self.settings["include_sfx"] = val
|
| 1303 |
+
self.update_components()
|
| 1304 |
+
await interaction.response.edit_message(view=self)
|
| 1305 |
+
|
| 1306 |
+
@discord.ui.button(label="وضع Advance ⚡", custom_id="adv", row=2)
|
| 1307 |
+
async def adv_btn(self, interaction: discord.Interaction, button: Button):
|
| 1308 |
+
if interaction.user.id != self.user_id: return
|
| 1309 |
+
val = not self.settings.get("advance_mode", False)
|
| 1310 |
+
db.update_setting(self.user_id, "advance_mode", val)
|
| 1311 |
+
self.settings["advance_mode"] = val
|
| 1312 |
+
self.update_components()
|
| 1313 |
+
await interaction.response.edit_message(view=self)
|
| 1314 |
+
|
| 1315 |
+
@bot.tree.command(name="settings", description="لوحة التحكم: تفعيل وضع Advance وتخصيص العلامات")
|
| 1316 |
+
async def settings_cmd(interaction: discord.Interaction):
|
| 1317 |
+
user = db.get_user(interaction.user.id, interaction.user.name)
|
| 1318 |
+
embed = discord.Embed(
|
| 1319 |
+
title="⚙️ لوحة الإعدادات (Settings)",
|
| 1320 |
+
description="قم بتخصيص طريقة الاستخراج الخاصة بك:\n\n🟢 **أخضر** = مُفعل | ⚪ **رمادي** = مُعطل\n\n⚡ **Advance Mode:** الوضع الاحترافي لاستخراج فائق الدقة.",
|
| 1321 |
+
color=0x3498db
|
| 1322 |
+
)
|
| 1323 |
+
if interaction.user.avatar: embed.set_thumbnail(url=interaction.user.avatar.url)
|
| 1324 |
+
await interaction.response.send_message(embed=embed, view=SettingsView(interaction.user.id, user), ephemeral=True)
|
| 1325 |
+
|
| 1326 |
+
|
| 1327 |
+
# ================================
|
| 1328 |
+
# 📝 16. Translation & Paraphrasing Tools
|
| 1329 |
+
# ================================
|
| 1330 |
+
async def text_ai_helper(prompt_text):
|
| 1331 |
+
current_api_key = ai_engine.get_valid_key()
|
| 1332 |
+
if not current_api_key: return "❌ لا توجد مفاتيح متاحة حالياً، يرجى الانتظار."
|
| 1333 |
+
|
| 1334 |
+
genai.configure(api_key=current_api_key)
|
| 1335 |
+
try:
|
| 1336 |
+
model = genai.GenerativeModel("gemini-2.5-flash")
|
| 1337 |
+
response = await asyncio.wait_for(
|
| 1338 |
+
asyncio.to_thread(model.generate_content, prompt_text),
|
| 1339 |
+
timeout=15.0
|
| 1340 |
+
)
|
| 1341 |
+
return response.text.strip() if response.text else "❌ ف��لت العملية."
|
| 1342 |
+
except Exception as e:
|
| 1343 |
+
ai_engine.report_dead_key(current_api_key)
|
| 1344 |
+
return f"⚠️ حدث خطأ أثناء الاتصال. تم تبديل المفتاح، حاول مجدداً."
|
| 1345 |
+
|
| 1346 |
+
@bot.tree.command(name="translate", description="ترجمة احترافية للنصوص مع دعم تعدد اللغات")
|
| 1347 |
+
@app_commands.describe(text="النص المراد ترجمته", target_lang="اللغة الهدف (الافتراضي: العربية)", source_lang="لغة المصدر (الافتراضي: اكتشاف تلقائي)")
|
| 1348 |
+
async def translate_cmd(interaction: discord.Interaction, text: str, target_lang: str = "العربية", source_lang: str = "اكتشاف تلقائي"):
|
| 1349 |
+
await interaction.response.defer(ephemeral=False)
|
| 1350 |
+
|
| 1351 |
+
prompt = (
|
| 1352 |
+
f"You are a highly skilled professional translator specializing in Manga, Manhwa, and Comics.\n"
|
| 1353 |
+
f"Your task is to translate the following text from [{source_lang}] to [{target_lang}].\n\n"
|
| 1354 |
+
f"STRICT RULES:\n"
|
| 1355 |
+
f"1. Preserve the exact tone, emotion, and context of the scene (e.g., shouting, whispering, thoughts).\n"
|
| 1356 |
+
f"2. Adapt idioms, cultural references, and slang to sound completely natural and eloquent in {target_lang}.\n"
|
| 1357 |
+
f"3. Maintain formatting if there are any specific tags.\n"
|
| 1358 |
+
f"4. OUTPUT ONLY THE TRANSLATED TEXT. DO NOT add any notes, explanations, or introductions.\n\n"
|
| 1359 |
+
f"Original Text:\n{text}"
|
| 1360 |
+
)
|
| 1361 |
+
|
| 1362 |
+
result = await text_ai_helper(prompt)
|
| 1363 |
+
|
| 1364 |
+
embed = discord.Embed(title="🌐 المترجم الاحترافي", color=0x2ecc71)
|
| 1365 |
+
embed.add_field(name=f"النص الأصلي ({source_lang}):", value=f"
|
| 1366 |
+
# ================================
|
| 1367 |
+
# 🎫 19. Ticket & Support System
|
| 1368 |
+
# ================================
|
| 1369 |
+
class TicketControlView(discord.ui.View):
|
| 1370 |
+
def __init__(self):
|
| 1371 |
+
super().__init__(timeout=None)
|
| 1372 |
+
|
| 1373 |
+
@discord.ui.button(label="🔒 إغلاق التذكرة", style=discord.ButtonStyle.danger, custom_id="close_ticket")
|
| 1374 |
+
async def close(self, interaction: discord.Interaction, button: discord.ui.Button):
|
| 1375 |
+
await interaction.response.send_message("⏳ جاري إغلاق التذكرة وحفظ السجل...", ephemeral=True)
|
| 1376 |
+
transcript = f"--- سجل تذكرة {interaction.channel.name} ---\n\n"
|
| 1377 |
+
async for msg in interaction.channel.history(limit=100, oldest_first=True):
|
| 1378 |
+
transcript += f"[{msg.created_at.strftime('%Y-%m-%d %H:%M')}] {msg.author.name}: {msg.content}\n"
|
| 1379 |
+
|
| 1380 |
+
log_file = discord.File(io.StringIO(transcript), filename=f"{interaction.channel.name}_log.txt")
|
| 1381 |
+
log_channel = bot.get_channel(LOG_CHANNEL_ID)
|
| 1382 |
+
if log_channel:
|
| 1383 |
+
await log_channel.send(content=f"📁 سجل تذكرة مغلقة: {interaction.channel.name}", file=log_file)
|
| 1384 |
+
|
| 1385 |
+
await asyncio.sleep(2)
|
| 1386 |
+
await interaction.channel.delete(reason="تم إغلاق التذكرة.")
|
| 1387 |
+
|
| 1388 |
+
class TicketSetupView(discord.ui.View):
|
| 1389 |
+
def __init__(self):
|
| 1390 |
+
super().__init__(timeout=None)
|
| 1391 |
+
|
| 1392 |
+
@discord.ui.button(label="🎫 فتح تذكرة دعم", style=discord.ButtonStyle.primary, custom_id="open_ticket", emoji="🛠️")
|
| 1393 |
+
async def open_ticket(self, interaction: discord.Interaction, button: discord.ui.Button):
|
| 1394 |
+
guild = interaction.guild
|
| 1395 |
+
category = discord.utils.get(guild.categories, name="🎫 • Tickets")
|
| 1396 |
+
if not category:
|
| 1397 |
+
category = await guild.create_category("🎫 • Tickets")
|
| 1398 |
+
|
| 1399 |
+
overwrites = {
|
| 1400 |
+
guild.default_role: discord.PermissionOverwrite(read_messages=False),
|
| 1401 |
+
interaction.user: discord.PermissionOverwrite(read_messages=True, send_messages=True, attach_files=True),
|
| 1402 |
+
guild.me: discord.PermissionOverwrite(read_messages=True, send_messages=True, manage_channels=True)
|
| 1403 |
+
}
|
| 1404 |
+
|
| 1405 |
+
ticket_channel = await guild.create_text_channel(f"تذكرة-{interaction.user.name}", category=category, overwrites=overwrites)
|
| 1406 |
+
embed = discord.Embed(title="🎫 تذكرة دعم فني", description=f"أهلاً بك {interaction.user.mention}!\nيرجى طرح مشكلتك أو طلبك بالتفصيل، وسيقوم أحد المشرفين بالرد عليك قريباً.\n\nلإغلاق التذكرة، اضغط على الزر بالأسفل.", color=0x3498db)
|
| 1407 |
+
await ticket_channel.send(content=f"{interaction.user.mention}", embed=embed, view=TicketControlView())
|
| 1408 |
+
await interaction.response.send_message(f"✅ تم فتح تذكرتك بنجاح: {ticket_channel.mention}", ephemeral=True)
|
| 1409 |
+
|
| 1410 |
+
@bot.tree.command(name="setup_tickets", description="إعداد نظام التذاكر (للمشرفين)")
|
| 1411 |
+
async def setup_tickets_cmd(interaction: discord.Interaction):
|
| 1412 |
+
if not interaction.user.guild_permissions.administrator: return await interaction.response.send_message("❌ للمشرفين فقط.", ephemeral=True)
|
| 1413 |
+
embed = discord.Embed(title="🛠️ مركز الدعم الفني والمساعدة", description="إذا واجهتك أي مشكلة في الاستخراج، أو ترغب في الانضمام للفريق، أو لديك استفسار للإدارة...\n\n**اضغط على الزر بالأسفل لفتح تذكرة خاصة بك! ⬇️**", color=0x2c3e50)
|
| 1414 |
+
await interaction.channel.send(embed=embed, view=TicketSetupView())
|
| 1415 |
+
await interaction.response.send_message("✅ تم إنشاء لوحة التذاكر.", ephemeral=True)
|
| 1416 |
+
|
| 1417 |
+
|
| 1418 |
+
# ================================
|
| 1419 |
+
# 🛡️ 20. Auto-Mod, Trivia & Engagement
|
| 1420 |
+
# ================================
|
| 1421 |
+
active_trivia = {}
|
| 1422 |
+
MANHWA_DB = [
|
| 1423 |
+
{"name": "سونغ جين وو", "hint": "بطل سولو ليفلينج (Solo Leveling)", "img": "https://i.pinimg.com/736x/1a/0a/7b/1a0a7b4f59fb59173f478f2674b967a5.jpg"},
|
| 1424 |
+
{"name": "كيم دوكجا", "hint": "بطل وجهة نظر القارئ الكلي العلم (ORV)", "img": "https://i.pinimg.com/736x/d6/3e/26/d63e261cefc48b26e0bdf605e55b6ef1.jpg"},
|
| 1425 |
+
{"name": "آرثر", "hint": "بطل البداية بعد النهاية (TBATE)", "img": "https://i.pinimg.com/736x/8e/31/76/8e3176d65b169b2d837697ab41ec85c7.jpg"},
|
| 1426 |
+
{"name": "تشونغ ميونغ", "hint": "بطل عودة طائفة جبل هوا", "img": "https://i.pinimg.com/736x/6f/a7/9d/6fa79da2295ed749d214a1e9c8bc8059.jpg"}
|
| 1427 |
+
]
|
| 1428 |
+
|
| 1429 |
+
@bot.event
|
| 1430 |
+
async def on_message(message: discord.Message):
|
| 1431 |
+
if message.author.bot: return
|
| 1432 |
+
|
| 1433 |
+
guild_id = message.guild.id
|
| 1434 |
+
if guild_id in active_trivia:
|
| 1435 |
+
game = active_trivia[guild_id]
|
| 1436 |
+
if message.channel.id == game["channel_id"]:
|
| 1437 |
+
if game["answer"].lower() in message.content.lower():
|
| 1438 |
+
winner = message.author
|
| 1439 |
+
points = game["points"]
|
| 1440 |
+
db.modify_points(winner.id, points)
|
| 1441 |
+
|
| 1442 |
+
embed = discord.Embed(title="🎉 لدينا فائز!", description=f"ألف مبروك لـ {winner.mention} عبقري المانهوا!\nالإجابة الصحيحة هي: **{game['answer']}**\n\n🎁 **كسبت {points:.2f} نقطة استخراج!**", color=0x2ecc71)
|
| 1443 |
+
await message.channel.send(embed=embed)
|
| 1444 |
+
del active_trivia[guild_id]
|
| 1445 |
+
|
| 1446 |
+
if not message.author.guild_permissions.manage_messages:
|
| 1447 |
+
if "discord.gg/" in message.content or "discord.com/invite/" in message.content:
|
| 1448 |
+
await message.delete()
|
| 1449 |
+
try:
|
| 1450 |
+
await message.author.timeout(timedelta(minutes=10), reason="إرسال رابط سيرفر خارجي")
|
| 1451 |
+
warn_msg = await message.channel.send(f"⚠️ {message.author.mention} تم إعطاؤك Mute لمدة 10 دقائق بسبب نشر روابط خارجية!")
|
| 1452 |
+
await asyncio.sleep(5); await warn_msg.delete()
|
| 1453 |
+
except: pass
|
| 1454 |
+
|
| 1455 |
+
@bot.tree.command(name="trivia_auto", description="تشغيل لعبة خمن الشخصية تلقائياً")
|
| 1456 |
+
async def trivia_auto_cmd(interaction: discord.Interaction):
|
| 1457 |
+
if not interaction.user.guild_permissions.manage_messages: return await interaction.response.send_message("❌ للمشرفين فقط.", ephemeral=True)
|
| 1458 |
+
guild_id = interaction.guild.id
|
| 1459 |
+
if guild_id in active_trivia: return await interaction.response.send_message("❌ هناك لعبة نشطة بالفعل!", ephemeral=True)
|
| 1460 |
+
|
| 1461 |
+
character = random.choice(MANHWA_DB)
|
| 1462 |
+
points_reward = round(random.uniform(0.1, 1.5), 2)
|
| 1463 |
+
|
| 1464 |
+
active_trivia[guild_id] = {"answer": character["name"], "points": points_reward, "channel_id": interaction.channel.id}
|
| 1465 |
+
|
| 1466 |
+
embed = discord.Embed(title="🎮 لُعْبَة خَمِّن الْمَانْهْوَا!", description=f"أول شخص يكتب اسم هذه الشخصية بشكل صحيح سيفوز بـ **{points_reward} نقطة استخراج!** 🎁", color=0xe67e22)
|
| 1467 |
+
embed.add_field(name="💡 تلميح:", value=character["hint"], inline=False)
|
| 1468 |
+
embed.set_image(url=character["img"])
|
| 1469 |
+
await interaction.response.send_message(embed=embed)
|
| 1470 |
+
|
| 1471 |
+
@bot.tree.command(name="embed", description="إنشاء رسالة إدارية فخمة (للمشرفين)")
|
| 1472 |
+
async def embed_cmd(interaction: discord.Interaction, title: str, description: str, color: str = "blue", image_url: str = None):
|
| 1473 |
+
if not interaction.user.guild_permissions.manage_messages: return await interaction.response.send_message("❌ للمشرفين فقط.", ephemeral=True)
|
| 1474 |
+
color_mapping = {"red": 0xe74c3c, "blue": 0x3498db, "green": 0x2ecc71, "yellow": 0xf1c40f, "black": 0x000000}
|
| 1475 |
+
embed = discord.Embed(title=title, description=description.replace("\\n", "\n"), color=color_mapping.get(color.lower(), 0x3498db))
|
| 1476 |
+
if image_url: embed.set_image(url=image_url)
|
| 1477 |
+
await interaction.channel.send(embed=embed)
|
| 1478 |
+
await interaction.response.send_message("✅ تم إرسال الإعلان.", ephemeral=True)
|
| 1479 |
+
|
| 1480 |
+
@bot.tree.command(name="giveaway_start", description="بدء مسابقة جديدة (للمشرفين)")
|
| 1481 |
+
async def giveaway_start_cmd(interaction: discord.Interaction, prize: str, hours: int, winners_count: int = 1):
|
| 1482 |
+
if not interaction.user.guild_permissions.manage_messages: return await interaction.response.send_message("❌ للمشرفين فقط.", ephemeral=True)
|
| 1483 |
+
end_time = discord.utils.utcnow() + timedelta(hours=hours)
|
| 1484 |
+
embed = discord.Embed(title="🎉 سحب جديد! (Giveaway)", description=f"**الجائزة:** {prize}\n**عدد الفائزين:** {winners_count}\n\nاضغط على الريأكشن 🎉 للمشاركة!\n\n⏳ **ينتهي في:** <t:{int(end_time.timestamp())}:R>", color=0x9b59b6)
|
| 1485 |
+
await interaction.response.send_message("✅ تم إطلاق السحب!", ephemeral=True)
|
| 1486 |
+
msg = await interaction.channel.send(embed=embed)
|
| 1487 |
+
await msg.add_reaction("🎉")
|
| 1488 |
+
|
| 1489 |
+
async def finish_giveaway():
|
| 1490 |
+
await asyncio.sleep(hours * 3600)
|
| 1491 |
+
new_msg = await interaction.channel.fetch_message(msg.id)
|
| 1492 |
+
reaction = discord.utils.get(new_msg.reactions, emoji="🎉")
|
| 1493 |
+
users = [user async for user in reaction.users() if not user.bot]
|
| 1494 |
+
if len(users) == 0: return await interaction.channel.send(f"❌ انتهى السحب ولم يشارك أحد!")
|
| 1495 |
+
winners = random.sample(users, min(winners_count, len(users)))
|
| 1496 |
+
winners_mentions = ", ".join([w.mention for w in winners])
|
| 1497 |
+
win_embed = discord.Embed(title="🎊 انتهى السحب!", description=f"**الجائزة:** {prize}\n**الفائزون:** {winners_mentions}", color=0x2ecc71)
|
| 1498 |
+
await new_msg.reply(content=f"مبروك لـ {winners_mentions}!", embed=win_embed)
|
| 1499 |
+
|
| 1500 |
+
bot.loop.create_task(finish_giveaway())
|
| 1501 |
+
|
| 1502 |
+
@bot.event
|
| 1503 |
+
async def on_raw_reaction_add(payload: discord.RawReactionActionEvent):
|
| 1504 |
+
if payload.emoji.name == "⭐":
|
| 1505 |
+
channel = bot.get_channel(payload.channel_id)
|
| 1506 |
+
msg = await channel.fetch_message(payload.message_id)
|
| 1507 |
+
reaction = discord.utils.get(msg.reactions, emoji="⭐")
|
| 1508 |
+
if reaction and reaction.count >= 5:
|
| 1509 |
+
starboard_channel = discord.utils.get(msg.guild.channels, name="أفضل-اللقطات")
|
| 1510 |
+
if not starboard_channel: return
|
| 1511 |
+
async for old_msg in starboard_channel.history(limit=50):
|
| 1512 |
+
if str(msg.id) in old_msg.content: return
|
| 1513 |
+
embed = discord.Embed(description=msg.content, color=0xf1c40f, timestamp=msg.created_at)
|
| 1514 |
+
embed.set_author(name=msg.author.display_name, icon_url=msg.author.avatar.url if msg.author.avatar else None)
|
| 1515 |
+
if msg.attachments: embed.set_image(url=msg.attachments[0].url)
|
| 1516 |
+
await starboard_channel.send(content=f"⭐ **{reaction.count}** | {channel.mention} | ID: {msg.id}", embed=embed)
|
| 1517 |
+
|
| 1518 |
+
@bot.event
|
| 1519 |
+
async def on_member_join(member):
|
| 1520 |
+
guild = member.guild
|
| 1521 |
+
try:
|
| 1522 |
+
role = guild.get_role(AUTO_ROLE_ID)
|
| 1523 |
+
if role: await member.add_roles(role)
|
| 1524 |
+
except: pass
|
| 1525 |
+
|
| 1526 |
+
welcome_channel = guild.get_channel(WELCOME_CHANNEL_ID)
|
| 1527 |
+
if welcome_channel:
|
| 1528 |
+
embed = discord.Embed(title=f"مرحباً بك في {guild.name}! 🎭⚡", description=f"أهلاً بك {member.mention}!\n\n📊 **أنت العضو رقم:** `{guild.member_count}`\n🎁 اكتب `/register` للحصول على غرفتك و 3 نقاط مجانية!", color=0x2ecc71)
|
| 1529 |
+
if member.avatar: embed.set_thumbnail(url=member.avatar.url)
|
| 1530 |
+
await welcome_channel.send(content=member.mention, embed=embed)
|
| 1531 |
+
|
| 1532 |
+
@tasks.loop(minutes=15)
|
| 1533 |
+
async def live_stats_updater():
|
| 1534 |
+
for guild in bot.guilds:
|
| 1535 |
+
for channel in guild.voice_channels:
|
| 1536 |
+
if "الأعضاء:" in channel.name or "Members:" in channel.name:
|
| 1537 |
+
try: await channel.edit(name=f"👥 الأعضاء: {guild.member_count}")
|
| 1538 |
+
except: pass
|
| 1539 |
+
|
| 1540 |
+
def get_emoji_for_name(name):
|
| 1541 |
+
name = name.lower()
|
| 1542 |
+
if any(k in name for k in ["ترحيب", "welcome"]): return "👋"
|
| 1543 |
+
if any(k in name for k in ["شات", "سوالف", "chat", "عام"]): return "💬"
|
| 1544 |
+
if any(k in name for k in ["إدارة", "admin", "مشرفين"]): return "🛡️"
|
| 1545 |
+
if any(k in name for k in ["تذاكر", "ticket"]): return "🎫"
|
| 1546 |
+
if any(k in name for k in ["قوانين", "rules"]): return "📜"
|
| 1547 |
+
return "📁"
|
| 1548 |
+
|
| 1549 |
+
@bot.tree.command(name="setup_server", description="تهيئة وتزيين السيرفر بالكامل (إيموجيات وتنسيق)")
|
| 1550 |
+
async def setup_server_cmd(interaction: discord.Interaction):
|
| 1551 |
+
if not interaction.user.guild_permissions.administrator: return await interaction.response.send_message("❌ للمشرفين فقط.", ephemeral=True)
|
| 1552 |
+
await interaction.response.defer(ephemeral=True)
|
| 1553 |
+
guild = interaction.guild
|
| 1554 |
+
|
| 1555 |
+
if not discord.utils.get(guild.channels, name="أفضل-اللقطات"):
|
| 1556 |
+
await guild.create_text_channel("أفضل-اللقطات", topic="تحصل الرسائل المميزة على ⭐ لتظهر هنا!")
|
| 1557 |
+
|
| 1558 |
+
for category in guild.categories:
|
| 1559 |
+
if not re.match(r'^[^\w\s]\s*•', category.name):
|
| 1560 |
+
try: await category.edit(name=f"{get_emoji_for_name(category.name)} • {category.name}")
|
| 1561 |
+
except: pass
|
| 1562 |
+
|
| 1563 |
+
for channel in guild.text_channels:
|
| 1564 |
+
if not re.match(r'^[^\w\s]ـ', channel.name) and "-" not in channel.name[:3]:
|
| 1565 |
+
try: await channel.edit(name=f"{get_emoji_for_name(channel.name)}ـ{channel.name.replace(' ', '-')}")
|
| 1566 |
+
except: pass
|
| 1567 |
+
|
| 1568 |
+
await interaction.followup.send("✅ تم تهيئة وتزيين السيرفر بنجاح!")
|
| 1569 |
+
|
| 1570 |
+
|
| 1571 |
+
# ================================
|
| 1572 |
+
# 💰 21. Admin, Economy & Core Commands
|
| 1573 |
+
# ================================
|
| 1574 |
+
@bot.tree.command(name="add_credits", description="إضافة رصيد لمستخدم (للمشرفين)")
|
| 1575 |
+
async def add_cr(interaction: discord.Interaction, user: discord.User, amount: float):
|
| 1576 |
+
if not interaction.user.guild_permissions.administrator and not db.is_bot_admin(str(interaction.user.id)): return await interaction.response.send_message("❌", ephemeral=True)
|
| 1577 |
+
new_points = db.modify_points(user.id, amount)
|
| 1578 |
+
await interaction.response.send_message(f"✅ تمت إضافة **{amount}** لـ {user.mention}. الرصيد: **{new_points:.2f}**")
|
| 1579 |
+
|
| 1580 |
+
@bot.tree.command(name="remove_credits", description="خصم رصيد من مستخدم (للمشرفين)")
|
| 1581 |
+
async def rem_cr(interaction: discord.Interaction, user: discord.User, amount: float):
|
| 1582 |
+
if not interaction.user.guild_permissions.administrator and not db.is_bot_admin(str(interaction.user.id)): return await interaction.response.send_message("❌", ephemeral=True)
|
| 1583 |
+
new_points = db.modify_points(user.id, -amount)
|
| 1584 |
+
await interaction.response.send_message(f"✅ تم خصم **{amount}** من {user.mention}. الرصيد: **{new_points:.2f}**")
|
| 1585 |
+
|
| 1586 |
+
@bot.tree.command(name="add_rank", description="تغيير رتبة مستخدم (للمشرفين)")
|
| 1587 |
+
@app_commands.choices(rank=[app_commands.Choice(name="Normal", value="Normal"), app_commands.Choice(name="VIP", value="VIP"), app_commands.Choice(name="VVIP", value="VVIP")])
|
| 1588 |
+
async def add_rank(interaction: discord.Interaction, user: discord.User, rank: app_commands.Choice[str]):
|
| 1589 |
+
if not interaction.user.guild_permissions.administrator and not db.is_bot_admin(str(interaction.user.id)): return await interaction.response.send_message("❌", ephemeral=True)
|
| 1590 |
+
with db.lock:
|
| 1591 |
+
uid = str(user.id)
|
| 1592 |
+
if uid in db.db:
|
| 1593 |
+
db.db[uid]["rank"] = rank.value
|
| 1594 |
+
db.save_db()
|
| 1595 |
+
await interaction.response.send_message(f"✅ تم تغيير رتبة {user.mention} إلى **{rank.value}**")
|
| 1596 |
+
|
| 1597 |
+
@bot.tree.command(name="add_admin", description="إضافة أدمن للبوت")
|
| 1598 |
+
async def add_admin_cmd(interaction: discord.Interaction, user: discord.User):
|
| 1599 |
+
if not interaction.user.guild_permissions.administrator: return await interaction.response.send_message("❌", ephemeral=True)
|
| 1600 |
+
db.add_bot_admin(str(user.id))
|
| 1601 |
+
await interaction.response.send_message(f"✅ تم إضافة {user.mention} كأدمن للبوت بنجاح!")
|
| 1602 |
+
|
| 1603 |
+
@bot.tree.command(name="cleanup_rooms", description="مسح الغرف الخاصة الميتة")
|
| 1604 |
+
async def cleanup_rooms_cmd(interaction: discord.Interaction, days_inactive: int = 30):
|
| 1605 |
+
if not interaction.user.guild_permissions.administrator: return await interaction.response.send_message("❌", ephemeral=True)
|
| 1606 |
+
await interaction.response.send_message("⏳ جاري التنظيف...", ephemeral=True)
|
| 1607 |
+
category = discord.utils.get(interaction.guild.categories, name="🎭 • MA SON")
|
| 1608 |
+
if not category: return await interaction.followup.send("❌ القسم غير موجود.")
|
| 1609 |
+
deleted, cutoff = 0, discord.utils.utcnow() - timedelta(days=days_inactive)
|
| 1610 |
+
for channel in category.channels:
|
| 1611 |
+
if isinstance(channel, discord.TextChannel):
|
| 1612 |
+
last_msg = None
|
| 1613 |
+
async for msg in channel.history(limit=1): last_msg = msg
|
| 1614 |
+
if last_msg is None or last_msg.created_at < cutoff:
|
| 1615 |
+
await channel.delete(reason="تنظيف تلقائي")
|
| 1616 |
+
deleted += 1
|
| 1617 |
+
await asyncio.sleep(1)
|
| 1618 |
+
await interaction.followup.send(f"🧹 تم حذف **{deleted}** غرفة ميتة.")
|
| 1619 |
+
|
| 1620 |
+
@bot.tree.command(name="broadcast", description="إرسال إعلان للغرف الخاصة")
|
| 1621 |
+
async def broadcast_cmd(interaction: discord.Interaction, message: str):
|
| 1622 |
+
if not interaction.user.guild_permissions.administrator: return await interaction.response.send_message("❌", ephemeral=True)
|
| 1623 |
+
await interaction.response.send_message("⏳ جاري الإرسال...", ephemeral=True)
|
| 1624 |
+
category = discord.utils.get(interaction.guild.categories, name="🎭 • MA SON")
|
| 1625 |
+
count, embed = 0, discord.Embed(title="📢 إعلان إداري", description=message, color=0xe74c3c)
|
| 1626 |
+
for channel in category.channels:
|
| 1627 |
+
try:
|
| 1628 |
+
await channel.send(embed=embed)
|
| 1629 |
+
count += 1
|
| 1630 |
+
await asyncio.sleep(0.5)
|
| 1631 |
+
except: pass
|
| 1632 |
+
await interaction.followup.send(f"✅ تم الإرسال إلى {count} غرفة.")
|
| 1633 |
+
|
| 1634 |
+
@bot.tree.command(name="lock", description="قفل الروم")
|
| 1635 |
+
async def lock_room(interaction: discord.Interaction):
|
| 1636 |
+
if not interaction.user.guild_permissions.manage_channels: return await interaction.response.send_message("❌", ephemeral=True)
|
| 1637 |
+
ow = interaction.channel.overwrites_for(interaction.guild.default_role)
|
| 1638 |
+
ow.send_messages = False
|
| 1639 |
+
await interaction.channel.set_permissions(interaction.guild.default_role, overwrite=ow)
|
| 1640 |
+
await interaction.response.send_message("🔒 تم قفل الغرفة!")
|
| 1641 |
+
|
| 1642 |
+
@bot.tree.command(name="unlock", description="فتح الروم")
|
| 1643 |
+
async def unlock_room(interaction: discord.Interaction):
|
| 1644 |
+
if not interaction.user.guild_permissions.manage_channels: return await interaction.response.send_message("❌", ephemeral=True)
|
| 1645 |
+
ow = interaction.channel.overwrites_for(interaction.guild.default_role)
|
| 1646 |
+
ow.send_messages = True
|
| 1647 |
+
await interaction.channel.set_permissions(interaction.guild.default_role, overwrite=ow)
|
| 1648 |
+
await interaction.response.send_message("🔓 تم فتح الغرفة!")
|
| 1649 |
+
|
| 1650 |
+
MODELS_LIST = [app_commands.Choice(name=m, value=m) for m in ["gemini-2.5-flash", "gemini-2.5-pro", "gemini-3.0-pro", "gemini-2.0-flash"]]
|
| 1651 |
+
@bot.tree.command(name="set_models", description="تغيير موديل الذكاء الاصطناعي")
|
| 1652 |
+
@app_commands.choices(normal_model=MODELS_LIST, advance_model=MODELS_LIST)
|
| 1653 |
+
async def set_models_cmd(interaction: discord.Interaction, normal_model: app_commands.Choice[str], advance_model: app_commands.Choice[str]):
|
| 1654 |
+
if not interaction.user.guild_permissions.administrator: return await interaction.response.send_message("❌", ephemeral=True)
|
| 1655 |
+
db.update_bot_setting("normal_model", normal_model.value)
|
| 1656 |
+
db.update_bot_setting("advance_model", advance_model.value)
|
| 1657 |
+
await interaction.response.send_message("✅ تم تحديث الموديلات.", ephemeral=True)
|
| 1658 |
+
|
| 1659 |
+
@bot.tree.command(name="add_emoji", description="إضافة إيموجي للسيرفر")
|
| 1660 |
+
async def add_emoji_cmd(interaction: discord.Interaction, name: str, link: str = None, image: discord.Attachment = None):
|
| 1661 |
+
if not interaction.user.guild_permissions.manage_emojis: return await interaction.response.send_message("❌", ephemeral=True)
|
| 1662 |
+
await interaction.response.defer(ephemeral=True)
|
| 1663 |
+
img_data = await image.read() if image else requests.get(link).content if link else None
|
| 1664 |
+
if img_data:
|
| 1665 |
+
emoji = await interaction.guild.create_custom_emoji(name=name, image=img_data)
|
| 1666 |
+
await interaction.followup.send(f"✅ تم: {emoji}")
|
| 1667 |
+
|
| 1668 |
+
@bot.tree.command(name="status", description="مراقبة حالة عمليات الاستخراج والدمج")
|
| 1669 |
+
async def status_cmd(interaction: discord.Interaction):
|
| 1670 |
+
total = len(bot_queue["ocr"]) + len(bot_queue["merge"])
|
| 1671 |
+
embed = discord.Embed(title="🚦 حالة البوت", description=f"يوجد **{total}** عمليات قيد التنفيذ." if total else "🟢 البوت خامل حالياً.", color=0x3498db)
|
| 1672 |
+
await interaction.response.send_message(embed=embed)
|
| 1673 |
+
|
| 1674 |
+
@bot.tree.command(name="profile", description="عرض إحصائياتك ورصيدك")
|
| 1675 |
+
async def profile_cmd(interaction: discord.Interaction, target_user: discord.User = None):
|
| 1676 |
+
t = target_user or interaction.user
|
| 1677 |
+
d = db.get_user(t.id, t.name)
|
| 1678 |
+
embed = discord.Embed(title=f"👤 الملف الشخصي: {t.name}", color=0xf1c40f)
|
| 1679 |
+
embed.add_field(name="💰 الرصيد", value=f"**{d['points']:.2f}**", inline=True)
|
| 1680 |
+
embed.add_field(name="🏆 الرتبة", value=f"**{d.get('rank','Normal')}**", inline=True)
|
| 1681 |
+
embed.add_field(name="🖼️ صور مستخرجة", value=f"**{d.get('images_extracted', 0)}**", inline=True)
|
| 1682 |
+
if t.avatar: embed.set_thumbnail(url=t.avatar.url)
|
| 1683 |
+
await interaction.response.send_message(embed=embed)
|
| 1684 |
+
|
| 1685 |
+
@bot.tree.command(name="top", description="لوحة الشرف لأفضل المترجمين")
|
| 1686 |
+
async def top_cmd(interaction: discord.Interaction):
|
| 1687 |
+
with db.lock: users = sorted([u for u in db.load_db().values() if isinstance(u, dict)], key=lambda x: x.get("images_extracted", 0), reverse=True)[:5]
|
| 1688 |
+
embed = discord.Embed(title="🏆 لوحة الشرف (Top 5)", color=0xf1c40f)
|
| 1689 |
+
for i, u in enumerate(users): embed.add_field(name=f"{['🥇','🥈','🥉','🏅','🏅'][i]} {u.get('name','مجهول')}", value=f"استخرج: {u.get('images_extracted',0)}", inline=False)
|
| 1690 |
+
await interaction.response.send_message(embed=embed)
|
| 1691 |
+
|
| 1692 |
+
@bot.tree.command(name="register", description="إنشاء حسابك واستلام 3 نقاط هدية")
|
| 1693 |
+
async def register_cmd(interaction: discord.Interaction):
|
| 1694 |
+
user = db.get_user(interaction.user.id, interaction.user.name)
|
| 1695 |
+
if user.get("registered"): return await interaction.response.send_message("❌ أنت مسجل بالفعل!", ephemeral=True)
|
| 1696 |
+
db.modify_points(interaction.user.id, 3.0)
|
| 1697 |
+
with db.lock:
|
| 1698 |
+
db.db[str(interaction.user.id)]["registered"] = True
|
| 1699 |
+
db.save_db()
|
| 1700 |
+
|
| 1701 |
+
guild = interaction.guild
|
| 1702 |
+
category = discord.utils.get(guild.categories, name="🎭 • MA SON") or await guild.create_category("🎭 • MA SON")
|
| 1703 |
+
overwrites = {guild.default_role: discord.PermissionOverwrite(read_messages=False), interaction.user: discord.PermissionOverwrite(read_messages=True, send_messages=True)}
|
| 1704 |
+
ch = await guild.create_text_channel(f"مكتب-{interaction.user.name}", category=category, overwrites=overwrites)
|
| 1705 |
+
await ch.send(f"مرحباً {interaction.user.mention} 🎁 لقد حصلت على 3 نقاط مجانية!")
|
| 1706 |
+
await interaction.response.send_message(f"✅ تم إنشاء غرفتك: {ch.mention}", ephemeral=True)
|
| 1707 |
+
|
| 1708 |
+
@bot.tree.command(name="info", description="معلومات البوت والمسؤولين")
|
| 1709 |
+
async def info_cmd(interaction: discord.Interaction):
|
| 1710 |
+
embed = discord.Embed(title="ℹ️ معلومات البوت", color=0x2c3e50)
|
| 1711 |
+
embed.add_field(name="👑 المالك", value=interaction.guild.owner.mention, inline=True)
|
| 1712 |
+
if interaction.user.guild_permissions.administrator:
|
| 1713 |
+
embed.add_field(name="🛠️ أوامر الإدارة", value="`/setup_server` `/setup_tickets` `/giveaway_start` `/embed` `/trivia_auto`\n`/add_credits` `/remove_credits` `/add_admin` `/cleanup_rooms` `/broadcast`", inline=False)
|
| 1714 |
+
await interaction.response.send_message(embed=embed, ephemeral=True)
|
| 1715 |
+
|
| 1716 |
+
@bot.tree.command(name="help", description="دليل أوامر البوت للأعضاء")
|
| 1717 |
+
async def help_cmd(interaction: discord.Interaction):
|
| 1718 |
+
embed = discord.Embed(title="📚 دليل أوامر البوت", color=0x2ecc71)
|
| 1719 |
+
embed.add_field(name="☁️ الاستخراج والدمج", value="**`/ocr`**: استخراج النصوص.\n**`/ocrweb`**: استخراج من الروابط.\n**`/merge`**: دمج صور الفصول.\n**`/settings`**: لوحة تحكم الاستخراج.", inline=False)
|
| 1720 |
+
embed.add_field(name="🌍 الترجمة والمساعدة", value="**`/translate`**: ترجمة المانهوا.\n**`/rewrite`**: إعادة صياغة بأسلوب درامي أو كوميدي.", inline=False)
|
| 1721 |
+
embed.add_field(name="👤 حسابك", value="**`/register`**: إنشاء الغرفة واستلام الهدية.\n**`/profile`**: عرض رصيدك.\n**`/top`**: أفضل المترجمين.", inline=False)
|
| 1722 |
+
await interaction.response.send_message(embed=embed, ephemeral=True)
|
| 1723 |
+
|
| 1724 |
+
# ================================
|
| 1725 |
+
# 🚀 22. Bot Startup & Hugging Face Keep-Alive
|
| 1726 |
+
# ================================
|
| 1727 |
+
@bot.event
|
| 1728 |
+
async def on_ready():
|
| 1729 |
+
print("⏳ جاري البحث عن نسخ احتياطية للبيانات في ديسكورد...")
|
| 1730 |
+
try:
|
| 1731 |
+
log_channel = bot.get_channel(LOG_CHANNEL_ID)
|
| 1732 |
+
if log_channel:
|
| 1733 |
+
async for msg in log_channel.history(limit=50):
|
| 1734 |
+
if msg.author == bot.user and msg.attachments:
|
| 1735 |
+
found_files = 0
|
| 1736 |
+
for att in msg.attachments:
|
| 1737 |
+
if att.filename == "users_db.json":
|
| 1738 |
+
await att.save(USERS_DB_FILE)
|
| 1739 |
+
db.db = db.load_db()
|
| 1740 |
+
found_files += 1
|
| 1741 |
+
elif att.filename == "tracker_cache.json":
|
| 1742 |
+
await att.save(TRACKER_DB_FILE)
|
| 1743 |
+
found_files += 1
|
| 1744 |
+
if found_files > 0:
|
| 1745 |
+
print("✅ تم استرجاع البيانات بنجاح من ديسكورد!")
|
| 1746 |
+
break
|
| 1747 |
+
except Exception as e:
|
| 1748 |
+
print(f"⚠️ خطأ في الاسترجاع: {e}")
|
| 1749 |
+
|
| 1750 |
+
if not backup_task.is_running(): backup_task.start()
|
| 1751 |
+
if not live_stats_updater.is_running(): live_stats_updater.start()
|
| 1752 |
+
if not advanced_tracker_loop.is_running(): advanced_tracker_loop.start()
|
| 1753 |
+
|
| 1754 |
+
try:
|
| 1755 |
+
await bot.tree.sync()
|
| 1756 |
+
print("🔄 Synced Commands Successfully")
|
| 1757 |
+
except Exception as e: print(f"❌ Failed to sync: {e}")
|
| 1758 |
+
|
| 1759 |
+
print(f'🔥 Logged in as {bot.user} (ID: {bot.user.id})')
|
| 1760 |
+
await bot.change_presence(activity=discord.Activity(type=discord.ActivityType.watching, name="Over Webtoons | /help"))
|
| 1761 |
+
|
| 1762 |
+
# تشغيل الخادم الوهمي الخاص بـ Hugging Face لضمان بقاء البوت أونلاين 24/7
|
| 1763 |
+
keep_alive()
|
| 1764 |
+
|
| 1765 |
+
if __name__ == "__main__":
|
| 1766 |
+
bot.run(DISCORD_TOKEN)
|