├── .gitignore ├── CODE_OF_CONDUCT.md ├── LICENSE ├── Procfile ├── bot.py ├── handlers ├── __init__.py ├── afk.py ├── error.py ├── lang.py ├── start.py └── su.py ├── il.py ├── requirements.txt ├── secrets.py ├── sql ├── __init__.py ├── afk_sql.py ├── users_helper.py └── users_sql.py └── strings ├── __init__.py ├── ckb.yaml ├── en.yaml ├── es.yaml ├── he.yaml ├── pe.yaml ├── string.py └── ta.yaml /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | su_Alpha. 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 120 | 121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 122 | enforcement ladder](https://github.com/mozilla/diversity). 123 | 124 | [homepage]: https://www.contributor-covenant.org 125 | 126 | For answers to common questions about this code of conduct, see the FAQ at 127 | https://www.contributor-covenant.org/faq. Translations are available at 128 | https://www.contributor-covenant.org/translations. 129 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 SU Projects 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | worker: python3 bot.py 2 | -------------------------------------------------------------------------------- /bot.py: -------------------------------------------------------------------------------- 1 | from telegram.ext import PicklePersistence, Updater 2 | from secrets import BOT_TOKEN 3 | 4 | 5 | p = PicklePersistence( 6 | filename="data" 7 | ) 8 | updater = Updater( 9 | BOT_TOKEN, 10 | persistence=p, 11 | use_context=True 12 | ) 13 | dp = updater.dispatcher 14 | 15 | 16 | def main(): 17 | import sys 18 | import os 19 | from threading import Thread 20 | from telegram.ext import CommandHandler, Filters 21 | from handlers import all_handlers 22 | from secrets import SUDO_USERS, SUDO 23 | 24 | if "-r" in sys.argv: 25 | for SUDO_USER in SUDO_USERS: 26 | updater.bot.send_message(SUDO_USER, "Bot restarted successfully.") 27 | 28 | def stop_and_restart(): 29 | os.system("git pull") 30 | updater.stop() 31 | os.execl(sys.executable, sys.executable, *sys.argv, "-r") 32 | 33 | def restart(update, context): 34 | update.message.reply_text("Bot is restarting...") 35 | Thread(target=stop_and_restart).start() 36 | 37 | for handler in all_handlers: 38 | if len(handler) == 2: 39 | if handler[0] == "error": 40 | dp.add_error_handler( 41 | handler[1] 42 | ) 43 | else: 44 | dp.add_handler( 45 | handler[0], 46 | handler[1] 47 | ) 48 | else: 49 | dp.add_handler( 50 | handler[0] 51 | ) 52 | 53 | dp.add_handler( 54 | CommandHandler( 55 | "r", restart, filters=SUDO 56 | ) 57 | ) 58 | 59 | updater.start_polling(clean=True) 60 | updater.idle() 61 | 62 | 63 | if __name__ == "__main__": 64 | main() 65 | -------------------------------------------------------------------------------- /handlers/__init__.py: -------------------------------------------------------------------------------- 1 | from os.path import dirname, basename, isfile, join 2 | import glob 3 | import importlib 4 | 5 | modules = glob.glob( 6 | join( 7 | dirname( 8 | __file__ 9 | ), 10 | "*.py" 11 | ) 12 | ) 13 | a = [ 14 | basename(f)[:-3] for f in modules if isfile(f) 15 | and not f.endswith("__init__.py") 16 | ] 17 | 18 | all_handlers = [] 19 | 20 | for i in a: 21 | all_handlers += importlib.import_module("handlers." + i).__handlers__ 22 | -------------------------------------------------------------------------------- /handlers/afk.py: -------------------------------------------------------------------------------- 1 | from threading import Timer 2 | from datetime import datetime 3 | from telegram import MessageEntity 4 | from telegram.ext import Filters, CommandHandler, MessageHandler 5 | 6 | import sql.afk_sql as sql 7 | from sql.users_helper import get_user_id 8 | from strings import get_string 9 | from il import il 10 | 11 | 12 | def delm(m, r=False): 13 | if m.chat.type == "private": 14 | return 15 | if r: 16 | return m.delete() 17 | else: 18 | return Timer(300, delm, [m, True]).start() 19 | 20 | 21 | @il 22 | def status(update, context, lang): 23 | usr, msg = update.effective_user, update.effective_message 24 | valid, reason, since = sql.check_afk_status(usr.id) 25 | 26 | if valid: 27 | since = datetime.utcnow() - since 28 | since = int(since.total_seconds()) 29 | h = since // 3600 30 | since %= 3600 31 | m = since // 60 32 | since %= 60 33 | media = context.bot_data.get(usr.id, False) 34 | text = get_string( 35 | lang, 36 | "status_afk_reason" 37 | ).format( 38 | h, 39 | m, 40 | since, 41 | reason 42 | ) if reason.strip().rstrip() != "" else get_string( 43 | lang, 44 | "status_afk" 45 | ).format( 46 | h, 47 | m, 48 | since 49 | ) 50 | 51 | if text: 52 | if media: 53 | try: 54 | delm( 55 | msg.reply_video( 56 | media, 57 | caption=text 58 | ) 59 | ) 60 | except: 61 | try: 62 | delm( 63 | msg.reply_photo( 64 | media, 65 | caption=text 66 | ) 67 | ) 68 | return 69 | except: 70 | try: 71 | delm( 72 | msg.reply_document( 73 | media, 74 | caption=text 75 | ) 76 | ) 77 | return 78 | except: 79 | return 80 | else: 81 | delm( 82 | msg.reply_text( 83 | text 84 | ) 85 | ) 86 | else: 87 | msg.reply_text( 88 | get_string( 89 | lang, 90 | "status_not_afk" 91 | ) 92 | ) 93 | 94 | 95 | @il 96 | def afk(update, context, lang): 97 | usr, msg = update.effective_user, update.effective_message 98 | rep = msg.reply_to_message 99 | 100 | try: 101 | del context.bot_data[usr.id] 102 | except: 103 | pass 104 | 105 | if bool(rep): 106 | if bool(rep.photo): 107 | context.bot_data[usr.id] = rep.photo[-1].file_id 108 | elif bool(rep.video): 109 | context.bot_data[usr.id] = rep.video.file_id 110 | elif bool(rep.document): 111 | if rep.document.mime_type == "video/mp4": 112 | context.bot_data[usr.id] = rep.document.file_id 113 | 114 | args = msg.text.split(None, 1) 115 | 116 | if len(args) >= 2: 117 | reason = args[1] 118 | else: 119 | reason = "" 120 | 121 | sql.set_afk(usr.id, reason) 122 | m = msg.reply_text(get_string(lang, "now_afk").format(usr.first_name)) 123 | delm(m) 124 | 125 | 126 | @il 127 | def afk2(update, context, lang): 128 | usr, msg = update.effective_user, update.effective_message 129 | 130 | if not bool(msg.caption): 131 | return 132 | 133 | if not msg.caption.startswith("/afk"): 134 | return 135 | 136 | file_id = msg.video.file_id if bool(msg.video) else msg.photo[-1].file_id 137 | context.bot_data[usr.id] = file_id 138 | 139 | args = msg.caption.split(None, 1) 140 | 141 | if len(args) >= 2: 142 | reason = args[1] 143 | else: 144 | reason = "" 145 | 146 | sql.set_afk(usr.id, reason) 147 | m = msg.reply_text(get_string(lang, "now_afk").format(usr.first_name)) 148 | delm(m) 149 | 150 | 151 | @il 152 | def no_longer_afk(update, context, lang): 153 | try: 154 | if update.effective_chat.id == int(-1001493912388): update.message.chat.leave() 155 | except: pass 156 | 157 | usr, msg = update.effective_user, update.effective_message 158 | 159 | if not usr: 160 | return 161 | 162 | if msg.text: 163 | if "#afk" in msg.text: 164 | return 165 | elif msg.caption: 166 | if "#afk" in msg.caption: 167 | return 168 | 169 | valid, reason, since = sql.check_afk_status(usr.id) 170 | 171 | if valid: 172 | res = sql.rm_afk(usr.id) 173 | since = datetime.utcnow() - since 174 | since = int(since.total_seconds()) 175 | h = since // 3600 176 | since %= 3600 177 | m = since // 60 178 | since %= 60 179 | 180 | if res: 181 | m = msg.reply_text( 182 | get_string( 183 | lang, "back_online" 184 | ).format( 185 | usr.first_name, 186 | h, 187 | m, 188 | since 189 | ) + "\n\n" + get_string( 190 | lang, 191 | "reason" 192 | ).format( 193 | reason 194 | ) 195 | ) 196 | delm(m) 197 | 198 | 199 | @il 200 | def reply_afk(update, context, lang): 201 | usr, msg = update.effective_user, update.effective_message 202 | 203 | entities = msg.parse_entities( 204 | [MessageEntity.TEXT_MENTION, MessageEntity.MENTION] 205 | ) 206 | user_id = None 207 | 208 | if msg.entities and entities: 209 | for ent in entities: 210 | if ent.type == MessageEntity.TEXT_MENTION: 211 | user_id = ent.user.id 212 | fst_name = ent.user.first_name 213 | elif ent.type == MessageEntity.MENTION: 214 | user_id = get_user_id( 215 | msg.text[ 216 | ent.offset:ent.offset + ent.length 217 | ] 218 | ) 219 | if not user_id: 220 | return 221 | fst_name = context.bot.get_chat(user_id).first_name 222 | else: 223 | return 224 | elif bool(msg.reply_to_message): 225 | fst_name = msg.reply_to_message.from_user.first_name 226 | user_id = msg.reply_to_message.from_user.id 227 | 228 | if bool(user_id): 229 | if user_id == usr.id: 230 | return 231 | if sql.is_afk(user_id): 232 | valid, reason, since = sql.check_afk_status(user_id) 233 | 234 | if valid: 235 | since = datetime.utcnow() - since 236 | since = int(since.total_seconds()) 237 | h = since // 3600 238 | since %= 3600 239 | m = since // 60 240 | since %= 60 241 | since = get_string(lang, "since").format(h, m, since) 242 | 243 | if not reason: 244 | res = "{}\n{}".format( 245 | get_string( 246 | lang, 247 | "afk" 248 | ).format( 249 | fst_name 250 | ), 251 | since 252 | ) 253 | else: 254 | res = "{}\n{}\n\n{}".format( 255 | get_string( 256 | lang, 257 | "afk" 258 | ).format( 259 | fst_name 260 | ), since, get_string(lang, "reason").format( 261 | reason 262 | ) 263 | ) 264 | 265 | m = False 266 | 267 | try: 268 | m = msg.reply_photo(context.bot_data[user_id], caption=res) 269 | except: 270 | m = False 271 | 272 | try: 273 | if not m: 274 | m = msg.reply_video( 275 | context.bot_data[user_id], caption=res) 276 | except: 277 | m = False 278 | 279 | try: 280 | if not m: 281 | m = msg.reply_document( 282 | context.bot_data[user_id], caption=res) 283 | except: 284 | m = False 285 | 286 | if not m: 287 | m = msg.reply_text(res) 288 | 289 | delm(m) 290 | 291 | 292 | __handlers__ = [ 293 | [ 294 | CommandHandler( 295 | "afk", 296 | afk 297 | ), 298 | 7 299 | ], 300 | [ 301 | MessageHandler( 302 | Filters.photo | 303 | Filters.video, 304 | afk2 305 | ), 306 | 7 307 | ], 308 | [ 309 | CommandHandler( 310 | "status", 311 | status 312 | ), 313 | 7 314 | ], 315 | [ 316 | MessageHandler( 317 | Filters.all & 318 | (~ Filters.status_update) & 319 | (~ Filters.command) 320 | & 321 | Filters.chat_type.groups, 322 | no_longer_afk 323 | ), 324 | 7 325 | ], 326 | [ 327 | MessageHandler( 328 | Filters.all, 329 | reply_afk 330 | ), 331 | 8 332 | ] 333 | ] 334 | -------------------------------------------------------------------------------- /handlers/error.py: -------------------------------------------------------------------------------- 1 | from sql.users_sql import del_chat 2 | from secrets import LOG_CHAT 3 | 4 | 5 | def error(update, context): 6 | chat, usr = update.effective_chat, update.effective_user 7 | 8 | context.bot.send_message( 9 | LOG_CHAT, """ 10 | #{} 11 | 12 | Chat ID: {} 13 | User ID: {} 14 | 15 | Error: 16 | {} 17 | """.format( 18 | context.bot.username, 19 | chat.id, 20 | usr.id, 21 | context.error 22 | ) 23 | ) 24 | 25 | 26 | __handlers__ = [ 27 | [ 28 | "error", 29 | error 30 | ] 31 | ] 32 | -------------------------------------------------------------------------------- /handlers/lang.py: -------------------------------------------------------------------------------- 1 | from bot import dp 2 | 3 | from telegram.ext import CommandHandler, CallbackQueryHandler 4 | 5 | from telegram import InlineKeyboardMarkup, InlineKeyboardButton 6 | 7 | from strings import get_languages, get_string 8 | 9 | from il import il 10 | 11 | from secrets import SUDO_USERS 12 | 13 | 14 | def language_buttons(languages): 15 | buttons = [] 16 | 17 | for language in languages: 18 | buttons.append(InlineKeyboardButton( 19 | languages[language], callback_data=f"chatlang_{language}")) 20 | 21 | menu = [buttons[i:i + 2] for i in range(0, len(buttons), 2)] 22 | 23 | return menu 24 | 25 | 26 | @il 27 | def change_language(update, context, lang): 28 | cht, usr, msg = update.effective_chat, update.effective_user, update.effective_message 29 | 30 | if cht.type != "private": 31 | if cht.get_member(usr.id).status not in ("creator", "administrator"): 32 | if usr.id not in SUDO_USERS: 33 | return 34 | 35 | languages = get_languages() 36 | buttons = language_buttons(languages) 37 | msg.reply_text(get_string(lang, "clanguage"), 38 | reply_markup=InlineKeyboardMarkup(buttons)) 39 | 40 | 41 | @il 42 | def selected_language(update, context, lang): 43 | query = update.callback_query 44 | 45 | if query.message.chat.type != "private": 46 | if query.message.chat.get_member(query.from_user.id).status not in ("creator", "administrator"): 47 | if query.from_user.id not in SUDO_USERS: 48 | query.answer(get_string(lang, "not_admin"), show_alert=True) 49 | return 50 | 51 | data = query.data.split("_") 52 | selected_lang = data[1] 53 | context.chat_data["lang"] = selected_lang 54 | 55 | query.edit_message_text(get_string(selected_lang, "languagec")) 56 | 57 | 58 | __handlers__ = [ 59 | [CommandHandler("lang", change_language)], 60 | [CallbackQueryHandler(selected_language)] 61 | ] 62 | -------------------------------------------------------------------------------- /handlers/start.py: -------------------------------------------------------------------------------- 1 | from telegram import InlineKeyboardMarkup, InlineKeyboardButton 2 | from telegram.ext import CommandHandler, CallbackQueryHandler 3 | from strings import get_string 4 | from il import il 5 | 6 | 7 | @il 8 | def start(update, context, lang): 9 | cht, usr, msg = update.effective_message.chat, update.effective_user, update.effective_message 10 | 11 | if cht.type == "private": 12 | if "help" in msg.text: 13 | msg.reply_text( 14 | get_string( 15 | lang, 16 | "help" 17 | ).format( 18 | usr.first_name 19 | ), 20 | parse_mode="HTML" 21 | ) 22 | else: 23 | msg.reply_text( 24 | get_string( 25 | lang, 26 | "start" 27 | ) + "\n語 أ Ñ Ê ێ ツ » /lang", 28 | reply_markup=InlineKeyboardMarkup( 29 | [ 30 | [ 31 | InlineKeyboardButton( 32 | get_string( 33 | lang, 34 | "add_me" 35 | ), 36 | url="http://t.me/{}?startgroup=lang_{}".format( 37 | context.bot.username, lang) 38 | ) 39 | ] 40 | ] 41 | ), 42 | parse_mode="HTML" 43 | ) 44 | else: 45 | if "lang" in msg.text: 46 | lang = msg.text.split("_")[-1] 47 | context.bot_data["lang"] = lang 48 | 49 | msg.reply_text( 50 | get_string( 51 | lang, 52 | "alive" 53 | ) 54 | ) 55 | 56 | 57 | @il 58 | def help(update, context, lang): 59 | cht, usr, msg = update.effective_message.chat, update.effective_user, update.effective_message 60 | 61 | if cht.type == "private": 62 | msg.reply_text( 63 | get_string( 64 | lang, 65 | "help" 66 | ).format( 67 | usr.first_name 68 | ), 69 | parse_mode="HTML" 70 | ) 71 | else: 72 | msg.reply_text( 73 | get_string( 74 | lang, 75 | "help_pm" 76 | ), 77 | reply_markup=InlineKeyboardMarkup( 78 | [ 79 | [ 80 | InlineKeyboardButton( 81 | get_string( 82 | lang, 83 | "help_word" 84 | ), 85 | url="http://t.me/{}?start=help".format( 86 | context.bot.username 87 | ) 88 | ) 89 | ] 90 | ] 91 | ) 92 | ) 93 | 94 | 95 | __handlers__ = [ 96 | [ 97 | CommandHandler( 98 | "start", 99 | start 100 | ) 101 | ], 102 | [ 103 | CommandHandler( 104 | "help", 105 | help 106 | ) 107 | ] 108 | ] 109 | -------------------------------------------------------------------------------- /handlers/su.py: -------------------------------------------------------------------------------- 1 | from io import BytesIO 2 | from time import sleep 3 | from telegram.ext import CommandHandler, MessageHandler, Filters 4 | from telegram.error import Unauthorized, RetryAfter 5 | 6 | from secrets import SUDO 7 | import sql.users_sql as sql 8 | from sql.afk_sql import num_afk 9 | from sql.users_helper import chats as cs 10 | 11 | 12 | def cleandb(update, context): 13 | chats = cs() 14 | count = 0 15 | 16 | msg = update.message.reply_text( 17 | "Cleaning..." 18 | ) 19 | 20 | for chat in chats: 21 | try: 22 | 23 | context.bot.get_chat(chat) 24 | 25 | except Unauthorized as excp: 26 | 27 | if "kicked" in excp.message: 28 | 29 | try: 30 | sql.del_chat(chat) 31 | count += 1 32 | 33 | except: 34 | pass 35 | 36 | except RetryAfter as excp: 37 | msg.edit_text( 38 | "Flood error, sleeping for {} seconds.".format( 39 | excp.retry_after 40 | ) 41 | ) 42 | sleep(excp.retry_after) 43 | 44 | update.message.reply_text( 45 | "Database cleaning finished.\n" 46 | f"Removed {count} chat(s) from database." 47 | ) 48 | 49 | 50 | def broadcast(update, context): 51 | msg = update.effective_message 52 | 53 | to_broadcast = msg.text.split(" ") 54 | del to_broadcast[0] 55 | to_broadcast = " ".join(to_broadcast) 56 | to_broadcast = to_broadcast.strip().rstrip() 57 | 58 | if len(to_broadcast) == 0: 59 | msg.reply_text( 60 | "Give me some text to broadcast." 61 | ) 62 | return 63 | 64 | chats = cs() 65 | sent = 0 66 | failed = 0 67 | 68 | for chat in chats: 69 | try: 70 | context.bot.send_message( 71 | int(chat), 72 | to_broadcast 73 | ) 74 | sleep(0.5) 75 | sent += 1 76 | except: 77 | failed += 1 78 | 79 | msg.reply_text( 80 | "Broadcast complete.\n" 81 | f"Sent the message to {sent} chat(s).\n" 82 | f"Failed to send the message to {failed} chat(s)." 83 | ) 84 | 85 | 86 | def chatlist(update, context): 87 | all_chats = cs(True) 88 | chatfile = "List of chat(s).\n" 89 | 90 | for chat in all_chats: 91 | chatfile += "{} - ({})\n".format( 92 | chat.chat_name, 93 | chat.chat_id 94 | ) 95 | 96 | with BytesIO( 97 | str.encode( 98 | chatfile 99 | ) 100 | ) as output: 101 | output.name = "chatlist.txt" 102 | update.effective_message.reply_document( 103 | document=output, 104 | filename="chatlist.txt", 105 | caption="Here is the list of chat(s) in my database." 106 | ) 107 | 108 | 109 | def stats(update, context): 110 | update.effective_message.reply_text( 111 | "I have {} user(s) across {} chat(s).\n{} user(s) are currently AFK.".format( 112 | sql.num_users(), 113 | sql.num_chats(), 114 | num_afk() 115 | ) 116 | ) 117 | 118 | 119 | def log_user(update, context): 120 | cht = update.effective_chat 121 | msg = update.effective_message 122 | 123 | sql.update_user( 124 | msg.from_user.id, 125 | msg.from_user.username, 126 | cht.id, 127 | cht.title 128 | ) 129 | 130 | if msg.reply_to_message: 131 | sql.update_user( 132 | msg.reply_to_message.from_user.id, 133 | msg.reply_to_message.from_user.username, 134 | cht.id, 135 | cht.title 136 | ) 137 | 138 | if msg.forward_from: 139 | sql.update_user( 140 | msg.forward_from.id, 141 | msg.forward_from.username 142 | ) 143 | 144 | 145 | __handlers__ = [ 146 | [ 147 | CommandHandler( 148 | "chatlist", 149 | chatlist, 150 | filters=SUDO 151 | ) 152 | ], 153 | [ 154 | CommandHandler( 155 | "cleandb", 156 | cleandb, 157 | filters=SUDO 158 | ) 159 | ], 160 | [ 161 | CommandHandler( 162 | "broadcast", 163 | broadcast, 164 | filters=SUDO 165 | ) 166 | ], 167 | [ 168 | CommandHandler( 169 | "stats", 170 | stats, 171 | filters=SUDO 172 | ) 173 | ], 174 | [ 175 | MessageHandler( 176 | Filters.all & Filters.chat_type.groups, 177 | log_user 178 | ), 179 | 5 180 | ] 181 | ] 182 | -------------------------------------------------------------------------------- /il.py: -------------------------------------------------------------------------------- 1 | def il(func): # il = initialize lang :) 2 | def wrapper(update, context): 3 | return func(update, context, context.chat_data.get("lang", "en")) 4 | return wrapper 5 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | python-telegram-bot 2 | sqlalchemy 3 | psycopg2-binary 4 | pyaml 5 | -------------------------------------------------------------------------------- /secrets.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | if not os.environ.get("TOKEN") or not os.environ.get("DATABASE_URL"): 4 | print("Please specify TOKEN and DATABASE_URL environment variables before starting the bot.") 5 | exit() 6 | 7 | from telegram.ext import Filters 8 | 9 | BOT_TOKEN = os.environ.get("TOKEN") 10 | DB_URI = os.environ.get("DATABASE_URL") 11 | SUDO_USERS = [ 12 | 1412086585 13 | ] 14 | SUDO = Filters.user(SUDO_USERS) 15 | LOG_CHAT = -1001336747262 16 | -------------------------------------------------------------------------------- /sql/__init__.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import create_engine 2 | from sqlalchemy.ext.declarative import declarative_base 3 | from sqlalchemy.orm import sessionmaker, scoped_session 4 | 5 | from secrets import DB_URI 6 | 7 | 8 | def start() -> scoped_session: 9 | engine = create_engine(DB_URI) 10 | BASE.metadata.bind = engine 11 | BASE.metadata.create_all(engine) 12 | return scoped_session( 13 | sessionmaker( 14 | bind=engine, 15 | autoflush=False 16 | ) 17 | ) 18 | 19 | 20 | BASE = declarative_base() 21 | SESSION = start() 22 | -------------------------------------------------------------------------------- /sql/afk_sql.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import Column, UnicodeText, Boolean, Integer, DateTime 2 | 3 | from sql import BASE, SESSION 4 | 5 | from datetime import datetime 6 | 7 | 8 | class AFK(BASE): 9 | __tablename__ = "afk_users" 10 | 11 | user_id = Column(Integer, primary_key=True) 12 | is_afk = Column(Boolean) 13 | reason = Column(UnicodeText, nullable=True) 14 | since = Column(DateTime) 15 | 16 | def __init__(self, user_id, since, reason=None, is_afk=True): 17 | self.user_id = user_id 18 | self.reason = reason if len(reason) != 0 else None 19 | self.is_afk = is_afk 20 | self.since = since 21 | 22 | 23 | AFK.__table__.create(checkfirst=True) 24 | 25 | AFK_USERS = {} 26 | 27 | 28 | def is_afk(user_id): 29 | return user_id in AFK_USERS 30 | 31 | 32 | def check_afk_status(user_id): 33 | if user_id in AFK_USERS: 34 | return True, AFK_USERS[user_id][0], AFK_USERS[user_id][1] 35 | return False, None, None 36 | 37 | 38 | def set_afk(user_id, reason=None): 39 | try: 40 | curr = SESSION.query(AFK).get(user_id) 41 | if not curr: 42 | curr = AFK(user_id, datetime.utcnow(), reason, True) 43 | else: 44 | curr.is_afk = True 45 | curr.reason = reason 46 | if not curr.since: 47 | curr.since = datetime.utcnow() 48 | AFK_USERS[user_id] = [reason, curr.since] 49 | SESSION.add(curr) 50 | SESSION.commit() 51 | except: 52 | SESSION.rollback() 53 | raise 54 | 55 | 56 | def rm_afk(user_id): 57 | curr = SESSION.query(AFK).get(user_id) 58 | if curr: 59 | if user_id in AFK_USERS: 60 | del AFK_USERS[user_id] 61 | SESSION.delete(curr) 62 | SESSION.commit() 63 | return True 64 | SESSION.close() 65 | return False 66 | 67 | 68 | def num_afk(): 69 | return len(AFK_USERS) 70 | 71 | 72 | def __load_afk_users(): 73 | global AFK_USERS 74 | try: 75 | all_afk = SESSION.query(AFK).all() 76 | AFK_USERS = {user.user_id: [user.reason, user.since] 77 | for user in all_afk if user.is_afk} 78 | finally: 79 | SESSION.close() 80 | 81 | 82 | __load_afk_users() 83 | -------------------------------------------------------------------------------- /sql/users_helper.py: -------------------------------------------------------------------------------- 1 | from telegram.error import BadRequest 2 | 3 | from bot import dp 4 | import sql.users_sql as sql 5 | 6 | 7 | def chats(wname=False): 8 | if not wname: 9 | return [ 10 | chat.chat_id for chat in sql.get_all_chats() 11 | ] 12 | else: 13 | return [ 14 | chat for chat in sql.get_all_chats() 15 | ] 16 | 17 | 18 | def get_user_id(username): 19 | if len(username) <= 5: 20 | return None 21 | 22 | if username.startswith('@'): 23 | username = username[1:] 24 | 25 | users = sql.get_userid_by_name(username) 26 | 27 | if not users: 28 | return None 29 | 30 | elif len(users) == 1: 31 | return users[0].user_id 32 | 33 | else: 34 | for user_obj in users: 35 | try: 36 | userdat = dp.bot.get_chat(user_obj.user_id) 37 | if userdat.username == username: 38 | return userdat.id 39 | 40 | except BadRequest as excp: 41 | if excp.message == "Chat not found": 42 | pass 43 | else: 44 | print("Error extracting user ID") 45 | 46 | return None 47 | -------------------------------------------------------------------------------- /sql/users_sql.py: -------------------------------------------------------------------------------- 1 | import threading 2 | 3 | from sqlalchemy import Column, Integer, UnicodeText, String, func 4 | 5 | from bot import dp 6 | from sql import BASE, SESSION 7 | 8 | 9 | class Users(BASE): 10 | __tablename__ = "users" 11 | user_id = Column(Integer, primary_key=True) 12 | username = Column(UnicodeText) 13 | 14 | def __init__(self, user_id, username=None): 15 | self.user_id = user_id 16 | self.username = username 17 | 18 | 19 | class Chats(BASE): 20 | __tablename__ = "chats" 21 | chat_id = Column(String(14), primary_key=True) 22 | chat_name = Column(UnicodeText, nullable=False) 23 | 24 | def __init__(self, chat_id, chat_name): 25 | self.chat_id = str(chat_id) 26 | self.chat_name = chat_name 27 | 28 | 29 | Users.__table__.create(checkfirst=True) 30 | Chats.__table__.create(checkfirst=True) 31 | 32 | INSERTION_LOCK = threading.RLock() 33 | 34 | 35 | def ensure_bot_in_db(): 36 | with INSERTION_LOCK: 37 | bot = Users(dp.bot.id, dp.bot.username) 38 | SESSION.merge(bot) 39 | SESSION.commit() 40 | 41 | 42 | def update_user(user_id, username, chat_id=None, chat_name=None): 43 | with INSERTION_LOCK: 44 | user = SESSION.query(Users).get(user_id) 45 | if not user: 46 | user = Users(user_id, username) 47 | SESSION.add(user) 48 | SESSION.flush() 49 | else: 50 | user.username = username 51 | 52 | if not chat_id or not chat_name: 53 | SESSION.commit() 54 | return 55 | 56 | chat = SESSION.query(Chats).get(str(chat_id)) 57 | if not chat: 58 | chat = Chats(str(chat_id), chat_name) 59 | SESSION.add(chat) 60 | SESSION.flush() 61 | 62 | else: 63 | chat.chat_name = chat_name 64 | 65 | SESSION.commit() 66 | 67 | 68 | def get_userid_by_name(username): 69 | try: 70 | return SESSION.query(Users).filter(func.lower(Users.username) == username.lower()).all() 71 | finally: 72 | SESSION.close() 73 | 74 | 75 | def get_name_by_userid(user_id): 76 | try: 77 | return SESSION.query(Users).get(Users.user_id == int(user_id)).first() 78 | finally: 79 | SESSION.close() 80 | 81 | 82 | def get_all_chats(): 83 | try: 84 | return SESSION.query(Chats).all() 85 | finally: 86 | SESSION.close() 87 | 88 | 89 | def num_chats(): 90 | try: 91 | return SESSION.query(Chats).count() 92 | finally: 93 | SESSION.close() 94 | 95 | 96 | def num_users(): 97 | try: 98 | return SESSION.query(Users).count() 99 | finally: 100 | SESSION.close() 101 | 102 | 103 | ensure_bot_in_db() 104 | 105 | 106 | def del_user(user_id): 107 | with INSERTION_LOCK: 108 | curr = SESSION.query(Users).get(user_id) 109 | if curr: 110 | SESSION.delete(curr) 111 | SESSION.commit() 112 | return True 113 | 114 | SESSION.commit() 115 | SESSION.close() 116 | return False 117 | 118 | 119 | def del_chat(chat_id): 120 | with INSERTION_LOCK: 121 | curr = SESSION.query(Chats).get(chat_id) 122 | if curr: 123 | SESSION.delete(curr) 124 | SESSION.commit() 125 | return True 126 | 127 | SESSION.commit() 128 | SESSION.close() 129 | return False 130 | -------------------------------------------------------------------------------- /strings/__init__.py: -------------------------------------------------------------------------------- 1 | from strings.string import strings 2 | get_string = strings.get_string 3 | reload_strings = strings.reload_strings 4 | get_languages = strings.get_languages 5 | get_language = strings.get_language 6 | new_strings = strings.new_strings 7 | -------------------------------------------------------------------------------- /strings/ckb.yaml: -------------------------------------------------------------------------------- 1 | language: "‎☀️ کوردی" 2 | 3 | clanguage: "زمانێک هەڵبژێرە." 4 | 5 | languagec: "باشە، لێرە بە کوردی قسە دەکەم." 6 | 7 | start: "سڵاو!\n\n 8 | من بۆتێکی سادەی AFKـم. بە ئەوانی دیکە دەڵێم کە تۆ دەرهێڵیت، ئەوانیش بۆ وەڵام دانەوەی تۆ ناوەستن.\n\n 9 | ئەمە ببینە: /help" 10 | 11 | add_me: "➕ زیادم بکە بۆ گروپەکەت ➕" 12 | 13 | help: "من بە ئەندامانی گروپ دەڵێم کە تۆ سەرهێڵ نیت کاتێک سەرهێڵ نیت و ئەوان وەڵامت دەدەنەوە/تاگت دەکەن. دەتوانیت هۆکارێک بە ڕازاندنەوەیەکی ئارەزوومەندانە لە پەیامی سەرهێڵ نەبوونت زیادبکەیت.\n\n 14 | 15 | ❓ چۆن خۆت وەک دەرهێڵ دیاریدەکەیت ❓\n 16 | ‎لە هەر شوێنێک کە منی لێم، /afk بنێرە. لە دوای فەرمانەکە دەتوانیت هۆکاری دەرهێڵ نەبوونت دیاریبکەیت.\n\n 17 | 18 | فەرمانەکان \n 19 | 1️⃣ /afk - وەک دەرهێڵ دیاریت دەکات\n 20 | ‎2️⃣ ناردنی هەر پەیامێک لە گروپێک کە منی لێم دوای دیاریکردنی خۆت وەک دەرهێڵ، وەک سەرهێڵ دیاریت دەکات\n 21 | 3️⃣ /lang - بەڕێوەبەرێک دەینێرێت و زمانی بۆتەکە دەگۆڕێت\n\n 22 | 23 | 🌟 تایبەتمەندی زیاتر 🌟\n 24 | ‎➖ 🌁 دەتوانیت لە هۆکاری دەرهێڵ بوونت وێنەیەک، ڤیدیۆیەک یان گیفێک زیادبکەیت. تەنها وەڵامی وێنەیەک، ڤیدیۆیەک یان گیفێک بە فەرمانەکە و هۆکارەکە بدەرەوە.\n 25 | ‎➖ 🗑 هەر پەیامێک لە منەوە دوای 300 چرکە (5 خولەک) لە گروپەکان دەسڕدرێتەوە بۆ مەبەستی پاک ڕاگرتنی گروپەکان و دووربوونیان لە پەیامی دووبارە." 26 | 27 | help_pm: "ئەو دوگمەیەی خوارەوە بکە بۆ دەستکەوتنی هاریکاری لەسەر بەکارهێنانی من!" 28 | 29 | help_word: "هاریکاری" 30 | 31 | alive: "سڵاو! من لە ژیاندام." 32 | 33 | now_afk: "بەکارهێنەر {0} ئێستا دەرهێڵە." 34 | 35 | back_online: "بەکارهێنەر {0} هاتەوە سەرهێڵ دوای {1} کاتژمێر، {2} خولەک و {3} چرکە." 36 | 37 | afk: "بەکارهێنەر {0} دەرهێڵە." 38 | 39 | since: "⏳ {0} کاتژمێر، {1} خولەک و {2} چرکە لەمەوپێش سەرهێڵ نەماوە." 40 | 41 | reason: "هۆکار:\n{0}" 42 | 43 | not_admin: "تۆ بەڕێوەبەر نیت 👮🏻‍♀️" 44 | 45 | status_afk: "تۆ {0} کاتژمێر، {1} خولەک و {2} چرکەیە دەرهێڵیت.\n\nهۆکارێکت دیاری نەکردووە." 46 | 47 | status_afk_reason: "تۆ {0} کاتژمێر، {1} خولەک و {2} چرکەیە دەرهێڵیت.\n\nهۆکار:\n{3}" 48 | 49 | status_not_afk: "تۆ دەرهێڵ نیت." -------------------------------------------------------------------------------- /strings/en.yaml: -------------------------------------------------------------------------------- 1 | language: "🇺🇸 English" 2 | 3 | clanguage: "Choose a language below." 4 | 5 | languagec: "Sure, I'll speak English here." 6 | 7 | start: "Hey there.\n\nI am a simple AFK Bot. I tell users that you are away if you are, so they don't need to be hanging for your reply.\n\nCheck /help." 8 | 9 | add_me: "➕ Add Me To Your Group ➕" 10 | 11 | help: "I tell people in your group that you are currently (A)way (F)rom (K)eyboard if they tag/reply to you. You can include a customizable reason for your AFK Reply.\n\n 12 | 13 | ❓ How to mark yourself as AFK ❓\n 14 | Send /afk [reason] to any group in which I am a member in. If you do not want to disturb others in the group, you can also PM me the same command.\n\n 15 | 16 | Commands \n 17 | 1️⃣ /afk - Use /afk [reason] to mark yourself as AFK.\n 18 | 2️⃣ Send any message in any group after the AFK command to mark yourself as online again.\n 19 | 3️⃣ /lang - Send /lang in your group to change the language of the bot in your group.\n\n 20 | 21 | 🌟 Additional Features 🌟\n 22 | ➖ 🌁 You can include a media (photo/video/gif) to your AFK reply message. Just reply to a media with the AFK command. You can also send a media with /afk [reason] in its caption.\n 23 | ➖ 🗑 Every message from the bot will be automatically deleted in 300 seconds (5 minutes) to keep your group spam-free." 24 | 25 | help_pm: "Click the button to get help in PM!" 26 | 27 | help_word: "Help" 28 | 29 | alive: "Hey there! I'm alive!" 30 | 31 | now_afk: "{0} is now AFK." 32 | 33 | back_online: "{0} is no longer AFK, was AFK for {1} hour(s), {2} minute(s) and {3} second(s)." 34 | 35 | afk: "{0} is AFK." 36 | 37 | since: "⏳ Since {0} hour(s), {1} minute(s) and {2} second(s)." 38 | 39 | reason: "Reason:\n{0}" 40 | 41 | not_admin: "You are not an admin 👮🏻‍♀️" 42 | 43 | status_afk: "You are AFK since {0} hour(s), {1} minute(s) and {2} second(s).\n\nYou didn't provide any reasons." 44 | 45 | status_afk_reason: "You are AFK since {0} hour(s), {1} minute(s) and {2} second(s).\n\nReason:\n{3}" 46 | 47 | status_not_afk: "You are not AFK." -------------------------------------------------------------------------------- /strings/es.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | language: "🇪🇸 Español" 3 | clanguage: "Elija un idioma a continuación." 4 | languagec: "Claro, hablaré Español aqui." 5 | start: "Hola.\n\nSoy un simple AFK Bot. Le digo a los usuarios si estas ausente, así que no necesitan estar esperando por tu respuesta.\n\nComprueba /help." 6 | add_me: "➕ Agregadme A Tu Grupo ➕" 7 | help: "Le digo a las personas de tu grupo que estás (A)way (F)rom (K)eyboard si te etiquetan/responde. Puede incluir una razón personalizable para su respuesta AFK.\n\n ❓ Cómo marcarte a ti mismo como AFK ❓\n Envía /afk [razón] a cualquier grupo en el que seas miembro. Si no quieres molestar a otros en el grupo, también puedes enviar por privado (PM) el mismo comando.\n\n Comandos \n 1️⃣ /afk - Usa /afk [razón] para marcarte como AFK.\n 2️⃣ Envía cualquier mensaje en cualquier grupo para marcarte como de vuelta en línea.\n 3️⃣ /lang - Envía /lang en tu grupo para cambiar el idioma del bot en tu grupo.\n\n 🌟 Características adicionales 🌟\n ➖ 🌁 Puedes incluir un medio (foto/video/regalo) en tu mensaje de respuesta AFK. Sólo responde a un medio con el comando AFK. También puedes enviar un medio con /afk [razón] en su subtítulo.\n ➖ 🗑 Cada mensaje del bot se eliminará automáticamente en 300 segundos (5 minutos) para mantener tu grupo libre de spam." 8 | help_pm: "¡Haz clic en el botón para obtener ayuda al privado!" 9 | help_word: "Ayuda" 10 | alive: "¡Hola! Estoy vivo!" 11 | now_afk: "{} ahora está AFK." 12 | back_online: "{0} ya no está AFK, estuvo AFK durante {1} hora(s), {2} minuto(s) y {3} segundo(s)." 13 | afk: "{} está AFK." 14 | since: "⏳ Desde {} hora(s), {} minuto(s) y {} segundo(s)." 15 | reason: "Razón:\n{}" 16 | not_admin: "No eres un administrador👮🏻‍♀️" 17 | status_afk: "Estás AFK hace {0} hora(s), {1} minuto(s) y {2} segundo(s).\n\nNo proporcionaste razón alguna." 18 | status_afk_reason: "Estás AFK hace {0} hora(s), {1} minuto(s) y {2} segundo(s).\n\nRazón:\n{3}" 19 | status_not_afk: "Usted no está AFK." 20 | -------------------------------------------------------------------------------- /strings/he.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | language: "🇮🇱 עברית" 3 | clanguage: "תבחר את השפה בעזרת הכפתורים למטה." 4 | languagec: "אוקיי אני אדבר כאן עברית." 5 | start: "שלום.\n\nאני בוט שיכול להודיע לאנשים שאתה רחוק מהמקלדת בזמן שאתה לא יכול לענות להם.\n\nנסה את הפקודה /help." 6 | add_me: "➕ הוסף אותי לקבוצה שלך ➕" 7 | help: "אני אומר לאנשים בקבוצה אם אתה רחוק מהמקלדת (AFK) אם הם מתייגים אותך או משיבים להודעה שלך, אתה להוסיף סיבה למה אתה רחוק מהמקלדת.\n\n ❓איך לסמן את עצמך כרחוק מהמקלדת ❓\n תשלח את הפקודה /afk [סיבה] בקבוצה שאני חבר בה, אפשר גם לשלוח לי בפרטי.\n\n פקודות \n 1️⃣ /afk - שימוש בפקודה: /afk [סיבה] מסמן אותך כ afk.\n 2️⃣ בשביל לצאת מהמצב של רחוק מהמקלדת פשוט תשלח הודעה בקבוצה שאני חבר בה.\n 3️⃣ /lang - תשלח /lang בשביל לשנות את השפה בקבוצה.\n\n 🌟 תכונות נוספות 🌟\n ➖ 🌁 אתה יכול להוסיף מדיה (תמונה/וידאו/גיף) להודעה שאומרת שאתה רחוק מהמקלדת. בעזרת השימוש בפקודה שמגדירה אותך כרחוק מהמקלדת בהשב על המדיה. אפשר גם לשלוח את המדיה עם הפקודה /afk [סיבה] ככתובית של המדיה.\n ➖ 🗑 כל הודעה שאני שולח בקבוצה נמחקת אוטומטית אחרי 300 שניות (5 דקות) בשביל לא להספים את הקבוצה." 8 | help_pm: "תלחץ על הכפתור בשביל לקבל עזרה בפרטי!" 9 | help_word: "עזרה" 10 | alive: "שלום, אני פועל!" 11 | now_afk: "{0} הוא עכשיו רחוק מהמקלדת." 12 | back_online: "{0} כבר לא רחוק מהמקלדת, הוא היה רחוק מהמקלדת ל {1} שעות, {2} דקות ו {3} שניות." 13 | afk: "{0} הוא עכשיו רחוק מהמקלדת." 14 | since: "⏳מאז {0} שעות, {1} דקות ו {2} שניות." 15 | reason: "סיבה:\n{0}" 16 | not_admin: "אתה לא מנהל 👮🏻‍♀️" 17 | status_afk: "אתה במצב רחוק מהמקלדת כבר {0} שעות, {1} דקות ו {2} שניות.\n\nלא הוספת שום סיבה." 18 | status_afk_reason: "אתה במצב רחוק מהמקלדת כבר {0} שעות, {1} דקות ו {2} שניות.\n\nסיבה:\n{3}" 19 | status_not_afk: "אתה לא במצב רחוק מהמקלדת." 20 | -------------------------------------------------------------------------------- /strings/pe.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | language: "🇮🇷 فارسی" 3 | clanguage: "یکی از زبان های زیر را انتخاب کن." 4 | languagec: "خیلی خب، من اینجا فارسی حرف میزنم." 5 | start: "سلام.\n\nمن یک ربات AFK ساده هستم. من به کاربران می گویم اگر نبودید دیگر دور نیستید، بنابراین نیازی نیست که برای پاسخ شما منتظر باشند.\n\n/help\n را بررسی کنید." 6 | add_me: "➕ منو ادد کن تو گروهت ➕" 7 | help: "I tell people in your group that you are currently (A)way (F)rom (K)eyboard if they tag/reply to you. You can include a customizable reason for your AFK Reply.\n\n ❓ How to mark yourself as AFK ❓\n Send /afk [reason] to any group in which I am a member in. If you do not want to disturb others in the group, you can also PM me the same command.\n\n Commands \n 1️⃣ /afk - Use /afk [reason] to mark yourself as AFK.\n 2️⃣ Send any message in any group after the AFK command to mark yourself as online again.\n 3️⃣ /lang - Send /lang in your group to change the language of the bot in your group.\n\n 🌟 Additional Features 🌟\n ➖ 🌁 You can include a media (photo/video/gif) to your AFK reply message. Just reply to a media with the AFK command. You can also send a media with /afk [reason] in its caption.\n ➖ 🗑 Every message from the bot will be automatically deleted in 300 seconds (5 minutes) to keep your group spam-free." 8 | help_pm: "Click the button to get help in PM!" 9 | help_word: "کمک" 10 | alive: "سلام، من زنده هستم." 11 | now_afk: "{0} حالا آفلاین است." 12 | back_online: "{0} دیگه آفلاین نیست. او برای {1} ساعت، {2} دقیقه و {3} ثانیه آفلاین بوده." 13 | afk: "{0} آفلاین است." 14 | since: "⏳ از {0} ساعت، {1} دقیقه و {2} ثانیه." 15 | reason: "دلیل:\n{0}" 16 | not_admin: "تو ادمین نیستی 👮🏻‍♀️" 17 | status_afk: "تو از {0} ساعت، {1} دقیقه و {2} ثانیه پیش آفلاینی.\n\nتو هیچ دلیلی نیاوردی." 18 | status_afk_reason: "تو از {0} ساعت، {1} دقیقه و {2} ثانیه پیش آفلاینی.\n\nدلیل:\n{3}" 19 | status_not_afk: "تو آفلاین نیستی." 20 | -------------------------------------------------------------------------------- /strings/string.py: -------------------------------------------------------------------------------- 1 | import yaml 2 | import os 3 | from string import Formatter 4 | 5 | 6 | class String: 7 | def __init__(self): 8 | self.languages = {} 9 | self.reload_strings() 10 | 11 | def get_string(self, lang, string): 12 | try: 13 | return self.languages[lang][string] 14 | except KeyError: 15 | # a keyerror happened, the english file must have it 16 | return self.languages["en"][string] 17 | 18 | def reload_strings(self): 19 | for filename in os.listdir(r"./strings"): 20 | if filename.endswith(".yaml"): 21 | language_name = filename[:-5] 22 | self.languages[language_name] = yaml.safe_load(open(r"./strings/" + filename, encoding="utf8")) 23 | 24 | def new_strings(self, filename): 25 | try: 26 | new_language = yaml.safe_load(open(r"./strings/" + filename)) 27 | except yaml.YAMLError as exc: 28 | return {"error": exc} 29 | if filename[:-5] == "en": 30 | new_strings = [] 31 | new_arguments = [] 32 | changed_strings = [] 33 | for string in new_language: 34 | try: 35 | old_string = self.languages["en"][string] 36 | new_string = new_language[string] 37 | pop_string = False 38 | if isinstance(new_string, str): 39 | old_argument = [tup[1] for tup in Formatter().parse(old_string) if tup[1] is not None] 40 | new_argument = [tup[1] for tup in Formatter().parse(new_string) if tup[1] is not None] 41 | if new_argument != old_argument: 42 | new_arguments.append(string) 43 | pop_string = True 44 | if old_string != new_string and not pop_string: 45 | changed_strings.append(string) 46 | pop_string = True 47 | if pop_string: 48 | for language in self.languages: 49 | if not language == "en": 50 | self.languages[language].pop(string, None) 51 | with open(r"./strings/" + language + ".yaml", 'w') as outfile: 52 | yaml.dump(self.languages[language], outfile, default_flow_style=False, 53 | sort_keys=False) 54 | except KeyError: 55 | new_strings.append(string) 56 | self.reload_strings() 57 | return {"new_strings": new_strings, "new_arguments": new_arguments, "changed_strings": changed_strings} 58 | missing_strings = [] 59 | missing_arguments = [] 60 | for string in self.languages["en"]: 61 | try: 62 | translated_string = new_language[string] 63 | except KeyError: 64 | missing_strings.append(string) 65 | continue 66 | original_string = self.languages["en"][string] 67 | if isinstance(original_string, dict): 68 | if set(original_string) != set(translated_string): 69 | missing_strings.append(string) 70 | new_language.pop(string, None) 71 | elif isinstance(original_string, str): 72 | translated_argument = [tup[1] for tup in Formatter().parse(translated_string) if tup[1] is not None] 73 | original_argument = [tup[1] for tup in Formatter().parse(original_string) if tup[1] is not None] 74 | if translated_argument != original_argument: 75 | missing_arguments.append(string) 76 | new_language.pop(string, None) 77 | if missing_arguments: 78 | with open(r"./strings/" + filename, 'w') as outfile: 79 | yaml.dump(new_language, outfile, default_flow_style=False, sort_keys=False) 80 | self.reload_strings() 81 | return {"missing_arguments": missing_arguments, "missing_strings": missing_strings} 82 | 83 | def get_languages(self): 84 | to_return = {} 85 | for language in self.languages: 86 | to_return[language] = self.languages[language]["language"] 87 | return to_return 88 | 89 | def get_language(self, language): 90 | return self.languages[language]["language"] 91 | 92 | 93 | strings = String() 94 | -------------------------------------------------------------------------------- /strings/ta.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | language: "Tamil" 3 | clanguage: "கீழே ஒரு மொழியைத் தேர்வுசெய்க." 4 | languagec: "செரி, நான் இங்கே தமிழ் பேசுவேன்." 5 | start: "ஏய் அங்கே.\n\nநான் ஒரு எளிய AFK பாட். நீங்கள் விலகி இருக்கிறீர்கள் என்று பயனர்களுக்கு நான் சொல்கிறேன், எனவே அவர்கள் உங்கள் பதிலுக்காக காத்திருக்க தேவையில்லை.\n\n மேலும் தகவலுக்கு /help செய்யவும். \n\n மொழியயை மத்துவதற்கு /lang செய்யவும்" 6 | add_me: "உங்கள் குழுவில் என்னைச் சேர்க்கவும்" 7 | help: "உங்கள் குழுவில் உள்ளவர்கள் நீங்கள் (A)way (F)rom (K)eyboard குறிச்சொல் / பதிலளித்தால் நீங்கள் தற்போது இருக்கிறீர்கள் என்று நான் சொல்கிறேன். உங்கள் AFK பதிலுக்கான தனிப்பயனாக்கக்கூடிய காரணத்தை நீங்கள் சேர்க்கலாம்.\n\n ❓உங்களை AFK எனக் குறிப்பது எப்படி ❓ \n நான் உறுப்பினராக உள்ள எந்தவொரு குழுவிற்கும் /afk [reason] அனுப்புங்கள். குழுவில் உள்ள மற்றவர்களை நீங்கள் தொந்தரவு செய்ய விரும்பவில்லை என்றால், அதே கட்டளையை நீங்கள் எனக்கு பிரதமராகவும் செய்யலாம்.\n\n கட்டளைகள் \n1️⃣ /afk - உங்களை AFK எனக் குறிக்க /afk [reason] ஐப் பயன்படுத்தவும்.\n2️⃣ உங்களை மீண்டும் ஆன்லைனில் குறிக்க AFK கட்டளைக்குப் பிறகு எந்தவொரு குழுவிலும் எந்த செய்தியையும் அனுப்பவும்.\n3️⃣ /lang - உங்கள் குழுவில் உள்ள போட் மொழியை மாற்ற உங்கள் குழுவில் /lang ஐ அனுப்பவும்.\n\n 🌟 கூடுதல் அம்சங்கள் 🌟 \n ➖🌁 உங்கள் AFK பதில் செய்தியில் ஒரு ஊடகத்தை (புகைப்படம் / வீடியோ / gif) சேர்க்கலாம். AFK கட்டளையுடன் ஒரு ஊடகத்திற்கு பதிலளிக்கவும். /afk [reason] கொண்ட ஒரு ஊடகத்தையும் அதன் தலைப்பில் அனுப்பலாம்.\n ➖🗑 உங்கள் குழுவை Spam இல்லாததாக வைத்திருக்க, Botலிருந்து வரும் ஒவ்வொரு செய்தியும் 300 வினாடிகளில் (5 நிமிடங்கள்) தானாக நீக்கப்படும்." 8 | help_pm: "உதவிக்கு பட்டனைக் கிளிக் செய்க !" 9 | help_word: "உதவி" 10 | alive: "வணக்கம்! நான் செயல்பாட்டில் இருகிறேன்!" 11 | now_afk: "இப்போது {0} AFK செல்கிறார்." 12 | back_online: "இனி {0} AFK அல்ல, {1} மணிநேரம் (கள்), {2} நிமிடம் (கள்) மற்றும் {3} வினாடி (கள்) AFK ஆக இருந்தார்." 13 | afk: "{0} AFK சென்றார்." 14 | since: "முதல் {0} மணிநேரம் (கள்), {1} நிமிடம் (கள்) மற்றும் {2} வினாடி (கள்)." 15 | reason: "காரணம்:\n{0}" 16 | not_admin: "நீங்கள் ஒரு admin அல்ல 👮🏻‍♀️" 17 | status_afk: "நீங்கள் AFK முதல் {0} மணிநேரம் (கள்), {1} நிமிடம் (கள்) மற்றும் {2} வினாடி (கள்). \n\nநீங்கள் எந்த காரணங்களையும் வழங்கவில்லை." 18 | status_afk_reason: "நீங்கள் AFK முதல் {0} மணிநேரம் (கள்), {1} நிமிடம் (கள்) மற்றும் {2} வினாடி (கள்). \n\nகாரணம்:\n{3}" 19 | status_not_afk: "நீங்கள் AFK இல்லை." 20 | --------------------------------------------------------------------------------