async def on_edit(c: Client, cq: CallbackQuery): lang = cq._lang key = cq.matches[0]["key"] value = (await Config.get_or_none(key=key)).value text = lang.edit_env_text(key=key, value=value) keyboard = ikb([[(lang.back, "setting_env")]]) last_msg = await cq.edit(text, keyboard) env_requires_restart = ["PREFIXES", "DATABASE_URL", "BOT_TOKEN"] try: while True: msg = await cq.from_user.listen(filters.text, None) await last_msg.remove_keyboard() await Config.get(key=key).update(value=msg.text) if key in env_requires_restart: text = lang.edit_env_text_restart(key=key, value=msg.text) keyboard = ikb([[(lang.restart_now, "restart_now")], [(lang.back, "setting_env")]]) else: text = lang.edit_env_text(key=key, value=msg.text) keyboard = ikb([[(lang.back, "setting_env")]]) last_msg = await msg.reply_text(text, reply_markup=keyboard) except errors.ListenerCanceled: pass
async def on_about_userlixo(c: Client, cq: CallbackQuery): lang = cq._lang subject = cq.matches[0]["subject"] keyboard = ikb([[(lang.back, "help")]]) text = { "userlixo": lang.about_userlixo_text, "plugins": lang.about_plugins_text, "commands": lang.about_commands_text, } text = text[subject] if subject == "commands": commands = [*cmds.keys()] total = len(commands) prefixes = os.getenv("PREFIXES") examples = [] for n, p in enumerate([*prefixes]): if n > total - 1: # if passed the end break examples.append("<code>" + p + commands[n] + "</code>") examples = ", ".join(examples) commands_list = [*map(lambda x: f"<code>{x}</code>", commands)] text.escape_html = False text = text( total=total, commands=", ".join(commands_list[:-1]), last_command=commands_list[-1], prefixes=prefixes, examples=examples, ) await cq.edit(text, keyboard, disable_web_page_preview=True)
async def on_index(c: Client, iq: InlineQuery): index = int(iq.matches[0]["index"]) message = await Message.get_or_none(key=index) if not message: results = [ InlineQueryResultArticle( title="undefined index", input_message_content=InputTextMessageContent( f"Undefined index {index}" ), ) ] return await iq.answer(results, cache_time=0) keyboard = ikb(message.keyboard) text = message.text results = [ InlineQueryResultArticle( title="index", input_message_content=InputTextMessageContent( text, disable_web_page_preview=True ), reply_markup=keyboard, ) ] await iq.answer(results, cache_time=0) await (await Message.get(key=message.key)).delete()
async def on_info_plugin(c: Client, cq: CallbackQuery): lang = cq._lang basename = cq.matches[0]["basename"] plugin_type = cq.matches[0]["plugin_type"] pg = int(cq.matches[0]["pg"]) if basename not in plugins[plugin_type]: return await cq.answer("UNKNOWN") plugin = plugins[plugin_type][basename] status = lang.active first_btn = (lang.deactivate, f"deactivate_plugin {basename} {plugin_type} {pg}") inactive = await get_inactive_plugins(plugins) if plugin["notation"] in inactive: status = lang.inactive first_btn = (lang.activate, f"activate_plugin {basename} {plugin_type} {pg}") status_line = "\n" + status lines = [[ first_btn, (lang.remove, f"remove_plugin {basename} {plugin_type} {pg}") ]] if "settings" in plugin and plugin["settings"]: lines.append([(lang.settings, f"plugin_settings {basename} {plugin_type} {pg}")]) lines.append([(lang.back, f"{plugin_type}_plugins {pg}")]) keyb = ikb(lines) text = write_plugin_info(plugins, lang, plugin, status_line=status_line) await cq.edit(text, keyb, disable_web_page_preview=True)
async def edit_text(self, text: str, reply_markup=None, *args, **kwargs): if type(reply_markup) == list: reply_markup = ikb(reply_markup) return await self._client.edit_message_text(self.chat.id, self.id, text, reply_markup=reply_markup, **kwargs)
async def on_help_u(c: Client, u: Union[Message, CallbackQuery]): is_query = hasattr(u, "data") lang = u._lang keyb = [ [(lang.about_userlixo, "about_userlixo")], [(lang.commands, "about_commands"), (lang.plugins, "about_plugins")], [ (lang.chat, "https://t.me/UserLixoChat", "url"), (lang.channel, "https://t.me/UserLixo", "url"), ], ] keyb = ikb(keyb) await (u.edit if is_query else u.reply)(lang.help_text, keyb)
async def on_start_u(c: Client, u: Union[Message, CallbackQuery]): is_query = hasattr(u, "data") lang = u._lang keyb = ikb([ [(lang.upgrade, "upgrade"), [lang.restart, "restart"]], [(lang.commands, "list_commands 0"), (lang.plugins, "list_plugins")], [(lang.help, "help"), (lang.settings, "settings")], ]) text = lang.start_text kwargs = {} if not is_query: kwargs["quote"] = True await (u.edit if is_query else u.reply)(text, keyb, **kwargs)
async def on_setting_language(c: Client, cq: CallbackQuery): lang = cq._lang buttons = [] for code, obj in lang.strings.items(): text, data = ( (f"✅ {obj['NAME']}", "noop") if obj["LANGUAGE_CODE"] == lang.code else (obj["NAME"], f"set_language {obj['LANGUAGE_CODE']}") ) buttons.append((text, data)) lines = array_chunk(buttons, 2) lines.append([(lang.back, "settings")]) keyboard = ikb(lines) await cq.edit(lang.choose_language, keyboard)
async def on_setting_env(c: Client, cq: CallbackQuery): if cq.message: cq.message.chat.cancel_listener() lang = cq._lang buttons = [] async for row in Config.all(): btn = (f"👁🗨 {row.key}", f"view_env {row.key}") if cq.message and cq.message.from_user.id == bot.me.id: btn = (f"📝 {row.key}", f"edit_env {row.key}") buttons.append(btn) lines = array_chunk(buttons, 2) lines.append([(lang.back, "settings")]) keyboard = ikb(lines) await cq.edit(lang.settings_env_text, keyboard)
async def on_settings_u(c: Client, u: Union[Message, CallbackQuery]): lang = u._lang is_query = hasattr(u, "data") lines = [ [(lang.language, "setting_language")], [(lang.sudoers, "setting_sudoers")], [(lang.env_vars, "setting_env")], ] if is_query: lines.append([(lang.back, "start")]) keyb = ikb(lines) await (u.edit if is_query else u.reply)(lang.settings_text, keyb)
async def on_info_command(c: Client, cq: CallbackQuery): lang = cq._lang cmd = cq.matches[0]["cmd"] pg = int(cq.matches[0]["pg"]) if cmd not in cmds: return await cq.answer(lang.unknown_command) info = lang.strings[lang.code].get(f"cmd_info_{cmd}", cmds[cmd]) if len(info) < 100: return await cq.answer("ℹ️ " + info, show_alert=True) kb = ikb([[(lang.back, f"list_commands {pg}")]]) text = lang.command_info text.escape_html = False await cq.edit(text(command=cmd, info=info), kb)
async def on_set_language(c: Client, cq: CallbackQuery): lang = cq._lang match = cq.matches[0] lang = lang.get_language(match["code"]) await Config.get(key="LANGUAGE").update(value=lang.code) os.environ["LANGUAGE"] = lang.code buttons = [] for code, obj in lang.strings.items(): text, data = ( (f"✅ {obj['NAME']}", "noop") if obj["LANGUAGE_CODE"] == lang.code else (obj["NAME"], f"set_language {obj['LANGUAGE_CODE']}") ) buttons.append((text, data)) lines = array_chunk(buttons, 2) lines.append([(lang.back, "settings")]) keyboard = ikb(lines) await cq.edit(lang.choose_language, keyboard, {"text": lang.language_chosen})
async def on_add_plugin_u(c: Client, u: Union[Message, CallbackQuery]): is_query = hasattr(u, "data") if is_query: await u.message.delete() lang = u._lang loop_count = 0 while True: loop_count += 1 if not is_query and u.document: msg = u if loop_count > 1: break # avoid infinite loop elif not is_query and u.reply_to_message and u.reply_to_message.document: msg = u.reply_to_message if loop_count > 1: break # avoid infinite loop else: msg = await u.from_user.ask(lang.plugin_file_ask) if await filters.regex("/cancel")(c, msg): return await msg.reply(lang.command_cancelled) if not msg.document: await msg.reply(lang.plugin_waiting_file, quote=True) continue if not re.search("(py)$", msg.document.file_name): await msg.reply(lang.plugin_format_not_accepted, quote=True) continue if msg.document.file_size > (5 * 1024 * 1024): await msg.reply(lang.plugin_too_big, quote=True) continue break filename = await msg.download("cache/") filename = os.path.relpath(filename) plugin = read_plugin_info(filename) # Showing info text = write_plugin_info(plugins, lang, plugin, status_line="") lines = [[ (lang.add, f"confirm_add_plugin {plugin['type']} {filename}"), (lang.cancel, "cancel_plugin"), ]] keyb = ikb(lines) await msg.reply(text, keyb, disable_web_page_preview=True, quote=True)
async def on_list_commands_u(c: Client, u: Union[Message, CallbackQuery]): lang = u._lang is_query = hasattr(u, "data") page = int(u.matches[0]["page"]) item_format = "info_command {} {}" page_format = "list_commands {}" layout = Pagination( [*cmds.items()], item_data=lambda i, pg: item_format.format(i[0], pg), item_title=lambda i, pg: i[0], page_data=lambda pg: page_format.format(pg), ) lines = layout.create(page, columns=2, lines=3) if is_query: lines.append([(lang.back, "start")]) keyb = ikb(lines) await (u.edit if is_query else u.reply)(lang.commands_text, keyb)
async def sudoers_interface(cq: CallbackQuery): lang = cq._lang c = cq._client text = lang.setting_sudoers_text + "\n" buttons = [] added = [] for user_id in sudoers: try: user_obj = await c.get_users(user_id) except BaseException: import traceback traceback.print_exc() user_obj = None id = user_obj.id if user_obj else user_id if id in added: continue added.append(id) mention = user_id if user_obj: mention = (f"@{user_obj.username}" if user_obj.username else user_obj.first_name) text += f"\n👤 {mention}" if id not in ["me", user.me.id, cq.from_user.id]: buttons.append((f"🗑 {mention}", f"remove_sudoer {user_id}")) lines = array_chunk(buttons, 2) if bot.me.username: lines.append([( lang.add_sudoer, f"https://t.me/{bot.me.username}?start=add_sudoer", "url", )]) lines.append([(lang.back, "settings")]) keyboard = ikb(lines) return text, keyboard
async def main(): await connect_database() await load_env() os.system("clear") @aiocron.crontab("*/1 * * * *") async def clean_cache(): for file in glob.glob("cache/*"): if not os.path.isfile(file): continue creation_time = datetime.fromtimestamp(os.path.getctime(file)) now_time = datetime.now() diff = now_time - creation_time minutes_passed = diff.total_seconds() / 60 if minutes_passed >= 10: os.remove(file) if not os.path.exists("user.session"): from userlixo.login import main as login await login() os.system("clear") await user.start() await bot.start() await unload_inactive_plugins() user.me = await user.get_me() bot.me = await bot.get_me() user.assistant = bot if user.me.id not in sudoers: sudoers.append(user.me.id) # Editing restaring alert restarting_alert = await Config.filter(key="restarting_alert") if len(restarting_alert) > 1: await Config.filter(key="restarting_alert").delete() restarting_alert = [] if restarting_alert: restarting_alert = restarting_alert[0] parts = restarting_alert.value.split("|") message_id, chat_id, cmd_timestamp, from_cmd = parts cmd_timestamp = float(cmd_timestamp) now_timestamp = datetime.now().timestamp() diff = round(now_timestamp - cmd_timestamp, 2) title, p = await shell_exec('git log --format="%B" -1') rev, p = await shell_exec("git rev-parse --short HEAD") date, p = await shell_exec( 'git log -1 --format=%cd --date=format:"%d/%m %H:%M"') timezone, p = await shell_exec( 'git log -1 --format=%cd --date=format:"%z"') local_version = int((await shell_exec("git rev-list --count HEAD"))[0]) timezone = timezone_shortener(timezone) date += f" ({timezone})" kwargs = {} text = (langs.upgraded_alert if from_cmd.startswith("upgrade") else langs.restarted_alert) text = text(rev=rev, date=date, seconds=diff, local_version=local_version) try: editor = bot if from_cmd.endswith("_bot") else user if editor == bot: keyb = ikb([[(langs.back, "start")]]) kwargs.update(reply_markup=keyb) if chat_id == "inline": await bot.edit_inline_text(message_id, text, **kwargs) else: await editor.edit_message_text(tryint(chat_id), tryint(message_id), text, **kwargs) except BaseException as e: print( f"[yellow]Failed to edit the restarting alert. Maybe the message has been deleted or somehow it became inacessible.\n>> {e}[/yellow]" ) try: await (await Config.get(id=restarting_alert.id)).delete() except OperationalError: pass # Showing alert in cli date, p = await shell_exec( 'git log -1 --format=%cd --date=format:"%d/%m %H:%M"') timezone, p = await shell_exec('git log -1 --format=%cd --date=format:"%z"' ) local_version = int((await shell_exec("git rev-list --count HEAD"))[0]) timezone = timezone_shortener(timezone) date += f" ({timezone})" mention = "@" + user.me.username if user.me.username else user.me.id text = ":ok: [bold green]UserLixo is running[/bold green] :ok:" userlixo_info = { "Version": local_version, "Account": mention, "Bot": "@" + bot.me.username, "Prefixes": os.getenv("PREFIXES"), "Logs_chat": os.getenv("LOGS_CHAT"), "Sudoers": ", ".join([*set(map(str, sudoers)) ]), # using set() to make the values unique "Commit_date": date, } for k, v in userlixo_info.items(): text += f"\n[dim cyan]{k}:[/dim cyan] [dim]{v}[/dim]" print(Panel.fit(text, border_style="green", box=box.ASCII)) # Sending alert via Telegram try: await alert_startup() except Exception as e: print( f"[bold yellow]Error while sending startup alert to LOGS_CHAT: {e}" ) # Alert about unused requirements if unused_requirements: unused = ", ".join(unused_requirements) print( f"[yellow]The following packages are not required by plugins anymore: [/][cyan]{unused}" ) try: await user.send_message( os.getenv("LOGS_CHAT"), f"The following packages are not required by plugins anymore: {unused}", ) except Exception as e: print("Error while sending alert about unused_requirements:\n > ", e) await idle() await user.stop() await bot.stop()
async def on_confirm_plugin(c: Client, cq: CallbackQuery): lang = cq._lang module = None plugin_type = cq.matches[0]["plugin_type"] client = (user, bot)[plugin_type == "bot"] cache_filename = cq.matches[0]["filename"] basename = os.path.basename(cache_filename) new_filename = "userlixo/handlers/" + plugin_type + "/plugins/" + basename plugin = read_plugin_info(cache_filename) new_notation = re.sub("\.py$", "", os.path.relpath(new_filename)).replace("/", ".") requirements = plugin.get("requirements") if requirements: DGRAY = 'echo -e "\033[1;30m"' RESET = 'echo -e "\033[0m"' req_list = requirements.split() req_text = "".join(f" ├ <code>{r}</code>\n" for r in req_list[:-1]) req_text += f" └ <code>{req_list[-1]}</code>" text = lang.installing_plugin_requirements text.escape_html = False await cq.edit(text(requirements=req_text)) os.system( f"{DGRAY}; {sys.executable} -m pip install {requirements}; {RESET}" ) # Safely unload the plugin if existent if os.path.exists(new_filename): try: module = importlib.import_module(new_notation) except Exception as e: return await cq.edit( lang.plugin_could_not_load_existent(name=basename, e=e)) functions = [*filter(callable, module.__dict__.values())] functions = [*filter(lambda f: hasattr(f, "handlers"), functions)] for f in functions: for handler in f.handlers: client.remove_handler(*handler) os.renames(cache_filename, new_filename) plugin = read_plugin_info(new_filename) try: if module: importlib.reload(module) module = importlib.import_module(plugin["notation"]) except Exception as e: os.remove(new_filename) await cq.edit(lang.plugin_could_not_load(e=e)) raise e functions = [*filter(callable, module.__dict__.values())] functions = [*filter(lambda f: hasattr(f, "handlers"), functions)] # if not len(functions): # os.remove(new_filename) # return await cq.edit(lang.plugin_has_no_handlers) for f in functions: for handler in f.handlers: client.add_handler(*handler) if module: r = None functions = [*filter(callable, module.__dict__.values())] for f in functions: if hasattr(f, "__name__") and f.__name__ == "post_install_script": await cq.edit(lang.running_post_install_script) if inspect.iscoroutinefunction(f): r = await f() else: r = await asyncio.get_event_loop().run_in_executor(None, f) break if r is not None: unload = False if isinstance(r, (tuple, list)): if len(r) == 2: if r[0] != 1: await cq.edit(lang.plugin_could_not_load(e=r[1])) unload = True else: await cq.edit( lang.plugin_could_not_load( e="The return of post_install_script should be like this: (0, 'nodejs not found')" )) unload = True else: await cq.edit( lang.plugin_could_not_load( e="The return of post_install_script should be a list or tuple" )) unload = True if unload: functions = [ *filter(lambda f: hasattr(f, "handlers"), functions) ] for f in functions: for handler in f.handlers: client.remove_handler(*handler) os.remove(new_filename) return plugins[plugin_type][basename] = plugin reload_plugins_requirements(plugins) inactive = await get_inactive_plugins(plugins) if plugin["notation"] in inactive: inactive = [x for x in inactive if x != plugin["notation"]] await Config.get(key="INACTIVE_PLUGINS" ).update(value=json.dumps(inactive)) # Discover which page is this plugin listed in quant_per_page = 4 * 2 # lines times columns page = math.ceil(len(plugins) / quant_per_page) keyb = ikb([[(lang.see_plugin_info, f"info_plugin {basename} {plugin_type} {page}")]]) text = lang.plugin_added(name=basename) await cq.edit(text, keyb)
async def on_upgrade_u(c: Client, u: Union[Message, CallbackQuery]): lang = u._lang is_query = hasattr(u, "data") is_inline = is_query and not u.message from_where = "_bot" if is_query else "" act = u.edit if is_query else u.reply keyb = ikb([[(lang.back, "start")]]) try: with open(".git/HEAD") as f: branch = f.read().split("/")[-1].rstrip() except FileNotFoundError: return await act(lang.upgrade_error_not_git, keyb) stdout, process = await shell_exec("git fetch && git status -uno") if process.returncode != 0: await act( lang.upgrade_failed(branch=branch, code=process.returncode, output=stdout), keyb, ) return await shell_exec("git merge --abort") if "Your branch is up to date" in stdout: # title, p = await shell_exec('git log --format="%B" -1') rev, p = await shell_exec("git rev-parse --short HEAD") date, p = await shell_exec( 'git log -1 --format=%cd --date=format:"%d/%m %H:%M"') timezone, p = await shell_exec( 'git log -1 --format=%cd --date=format:"%z"') local_version = int((await shell_exec("git rev-list --count HEAD"))[0]) timezone = timezone_shortener(timezone) date += f" ({timezone})" args = [] if is_query: args.append(keyb) return await act( lang.upgrade_alert_already_uptodate(rev=rev, date=date, local_version=local_version), *args, ) msg = await act(lang.upgrading_now_alert) stdout, process = await shell_exec(f"git pull --no-edit origin {branch}") if process.returncode != 0: await msg.edit( lang.upgrade_failed(branch=branch, code=process.returncode, output=stdout), keyb, ) return await shell_exec("git merge --abort") await Config.filter(key="restarting_alert").delete() message_id = u.inline_message_id if is_inline else msg.message_id chat_id = "inline" if is_inline else msg.chat.username or msg.chat.id await Config.create( **{ "key": "restarting_alert", "value": f"{message_id}|{chat_id}|{datetime.now().timestamp()}|upgrade{from_where}", }) args = [sys.executable, "-m", "userlixo"] if "--no-update" in sys.argv: args.append("--no-update") os.execv(sys.executable, args)
async def ytdlcmd(c: Client, m: Message): user = m.from_user.id if m.reply_to_message and m.reply_to_message.text: url = m.reply_to_message.text elif len(m.command) > 1: url = m.text.split(None, 1)[1] else: await m.reply_text(await tld(m.chat.id, "ytdl_missing_argument")) return ydl = yt_dlp.YoutubeDL({ "outtmpl": "dls/%(title)s-%(id)s.%(ext)s", "format": "mp4", "noplaylist": True }) rege = re.match( r"http(?:s?):\/\/(?:www\.)?youtu(?:be\.com\/watch\?v=|\.be\/)([\w\-\_]*)(&(amp;)?[\w\?=]*)?", url, re.M, ) if "t=" in url: temp = url.split("t=")[1].split("&")[0] else: temp = 0 if not rege: yt = await extract_info(ydl, "ytsearch:" + url, download=False) yt = yt["entries"][0] else: yt = await extract_info(ydl, rege.group(), download=False) for f in yt["formats"]: if f["format_id"] == "140": afsize = f["filesize"] or 0 if f["ext"] == "mp4" and f["filesize"] is not None: vfsize = f["filesize"] or 0 vformat = f["format_id"] keyboard = [[ ( await tld(m.chat.id, "ytdl_audio_button"), f'_aud.{yt["id"]}|{afsize}|{temp}|{vformat}|{m.chat.id}|{user}|{m.message_id}', ), ( await tld(m.chat.id, "ytdl_video_button"), f'_vid.{yt["id"]}|{vfsize}|{temp}|{vformat}|{m.chat.id}|{user}|{m.message_id}', ), ]] if " - " in yt["title"]: performer, title = yt["title"].rsplit(" - ", 1) else: performer = yt.get("creator") or yt.get("uploader") title = yt["title"] text = f"🎧 <b>{performer}</b> - <i>{title}</i>\n" text += f"💾 <code>{pretty_size(afsize)}</code> (audio) / <code>{pretty_size(int(vfsize))}</code> (video)\n" text += f"⏳ <code>{datetime.timedelta(seconds=yt.get('duration'))}</code>" await m.reply_text(text, reply_markup=ikb(keyboard))