diff --git a/Dockerfile b/Dockerfile index 980f0c8..c6fac9f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -25,7 +25,7 @@ RUN apt update && apt install \ libcairo2 git -y --no-install-recommends # Clean the cache -RUN rm -rf /var/lib/apt/lists /var/cache/apt/archives /tmp +RUN rm -rf /var/lib/apt/lists /var/cache/apt/archives /tmp/* # Expose IP address EXPOSE 8080 diff --git a/Okteto b/Okteto index db9ffd9..fca00ee 100644 --- a/Okteto +++ b/Okteto @@ -33,7 +33,7 @@ RUN apt update && apt install \ libavdevice-dev -y --no-install-recommends # Clean the cache -RUN rm -rf /var/lib/apt/lists /var/cache/apt/archives /tmp +RUN rm -rf /var/lib/apt/lists /var/cache/apt/archives /tmp/* # Expose IP address EXPOSE 8080 diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..3f08c06 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,9 @@ +version: "3" +services: + worker: + build: . + volumes: + - worker:/data + +volumes: + worker: diff --git a/hikka/main.py b/hikka/main.py index 4fee296..e0a1285 100755 --- a/hikka/main.py +++ b/hikka/main.py @@ -41,6 +41,8 @@ import sqlite3 import sys from math import ceil from typing import Union +import uvloop +import subprocess from telethon import TelegramClient, events from telethon.errors.rpcerrorlist import ( @@ -78,6 +80,8 @@ DATA_DIR = ( CONFIG_PATH = os.path.join(DATA_DIR, "config.json") +uvloop.install() + def run_config( db: database.Database, @@ -386,35 +390,36 @@ class Hikka: importlib.invalidate_caches() self._get_api_token() - async def fetch_clients_from_web(self): - """Imports clients from web module""" - for client in self.web.clients: - session = SQLiteSession( - os.path.join( - self.arguments.data_root or DATA_DIR, - f"hikka-+{'x' * (len(client.phone) - 5)}{client.phone[-4:]}-{(await client.get_me()).id}", - ) + async def save_client_session(self, client: TelegramClient): + session = SQLiteSession( + os.path.join( + self.arguments.data_root or DATA_DIR, + f"hikka-+{'x' * (len(client.phone) - 5)}{client.phone[-4:]}-{(await client.get_me()).id}", ) + ) - session.set_dc( - client.session.dc_id, - client.session.server_address, - client.session.port, - ) - session.auth_key = client.session.auth_key - session.save() - client.session = session - # Set db attribute to this client in order to save - # custom bot nickname from web - client.hikka_db = database.Database(client) - await client.hikka_db.init() - - self.clients = list(set(self.clients + self.web.clients)) + session.set_dc( + client.session.dc_id, + client.session.server_address, + client.session.port, + ) + session.auth_key = client.session.auth_key + session.save() + client.session = session + # Set db attribute to this client in order to save + # custom bot nickname from web + client.hikka_db = database.Database(client) + await client.hikka_db.init() def _web_banner(self): """Shows web banner""" print("✅ Web mode ready for configuration") - print(f"🌐 Please visit http://127.0.0.1:{self.web.port}") + ip = ( + "127.0.0.1" + if "DOCKER" not in os.environ + else subprocess.run(["hostname", "-i"], stdout=subprocess.PIPE).stdout + ) + print(f"🌐 Please visit http://{ip}:{self.web.port}") async def wait_for_web_auth(self, token: str): """Waits for web auth confirmation in Telegram""" @@ -449,9 +454,12 @@ class Hikka: return True - def _init_clients(self): - """Reads session from disk and inits them""" - for phone_id, phone in self.phones.items(): + def _init_clients(self) -> bool: + """ + Reads session from disk and inits them + :returns: `True` if at least one client started successfully + """ + for phone_id, phone in self.phones.copy().items(): session = os.path.join( self.arguments.data_root or DATA_DIR, f'hikka{f"-{phone_id}" if phone_id else ""}', @@ -473,7 +481,7 @@ class Hikka: install_entity_caching(client) - self.clients.append(client) + self.clients += [client] except sqlite3.OperationalError: print( "Check that this is the only instance running. " @@ -482,20 +490,22 @@ class Hikka: continue except (TypeError, AuthKeyDuplicatedError): os.remove(os.path.join(DATA_DIR, f"{session}.session")) - self.main() + del self.phones[phone_id] except (ValueError, ApiIdInvalidError): # Bad API hash/ID run_config({}, self.arguments.data_root) - return + return False except PhoneNumberInvalidError: print( "Phone number is incorrect. Use international format (+XX...) " "and don't put spaces in it." ) - continue + del self.phones[phone_id] except InteractiveAuthRequired: print(f"Session {session} was terminated and re-auth is required") - continue + del self.phones[phone_id] + + return bool(self.phones) def _init_loop(self): """Initializes main event loop and starts handler for each client""" @@ -654,11 +664,13 @@ class Hikka: save_config_key("port", self.arguments.port) self._get_token() - if not self.clients and not self.phones and not self._initial_setup(): + if ( + not self.clients # Search for already inited clients + and not self.phones # Search for already added phones / sessions + or not self._init_clients() # Attempt to read sessions from env + ) and not self._initial_setup(): # Otherwise attempt to run setup return - self._init_clients() - self.loop.set_exception_handler( lambda _, x: logging.error( f"Exception on event loop! {x['message']}", diff --git a/hikka/modules/hikka_security.py b/hikka/modules/hikka_security.py index c8d0ba6..b62e574 100755 --- a/hikka/modules/hikka_security.py +++ b/hikka/modules/hikka_security.py @@ -477,7 +477,7 @@ class HikkaSecurityMod(loader.Module): list(set(self._db.get(main.__name__, "nonickusers", []) + [user.id])), ) - call.edit( + await call.edit( self.strings("user_nn").format( user.id, utils.escape_html(get_display_name(user)), diff --git a/hikka/modules/okteto.py b/hikka/modules/okteto.py index 26a4db1..6e8a4ad 100644 --- a/hikka/modules/okteto.py +++ b/hikka/modules/okteto.py @@ -15,7 +15,10 @@ import time from telethon.errors.rpcerrorlist import YouBlockedUserError from telethon.tl.functions.contacts import UnblockRequest -from telethon.tl.functions.messages import GetScheduledHistoryRequest +from telethon.tl.functions.messages import ( + GetScheduledHistoryRequest, + DeleteScheduledMessagesRequest, +) from telethon.tl.types import Message from .. import loader, main, utils @@ -54,8 +57,12 @@ class OktetoMod(loader.Module): if messages: logger.info("Deleting previously scheduled Okteto pinger messages") - for message in messages: - await message.delete() + await client( + DeleteScheduledMessagesRequest( + self._bot, + [message.id for message in messages], + ) + ) raise loader.SelfUnload diff --git a/hikka/utils.py b/hikka/utils.py index 3217bfe..30b5257 100755 --- a/hikka/utils.py +++ b/hikka/utils.py @@ -45,7 +45,6 @@ import git import grapheme import requests import telethon -from aiogram.types import CallbackQuery from telethon.hints import Entity from telethon.tl.custom.message import Message from telethon.tl.functions.account import UpdateNotifySettingsRequest @@ -83,7 +82,7 @@ from telethon.tl.types import ( User, ) -from .inline.types import InlineCall +from .inline.types import InlineCall, InlineMessage FormattingEntity = Union[ MessageEntityUnknown, @@ -279,16 +278,16 @@ def relocate_entities( async def answer( - message: Union[Message, CallbackQuery, InlineCall], + message: Union[Message, InlineCall, InlineMessage], response: str, **kwargs, -) -> Union[CallbackQuery, Message]: +) -> Union[InlineCall, InlineMessage, Message]: """Use this to give the response to a command""" # Compatibility with FTG\GeekTG if isinstance(message, list) and message: message = message[0] - if isinstance(message, (CallbackQuery, InlineCall)): + if isinstance(message, (InlineMessage, InlineCall)): await message.edit(response) return message @@ -578,13 +577,16 @@ def chunks(_list: Union[list, tuple, set], n: int, /) -> list: def get_named_platform() -> str: """Returns formatted platform name""" - if os.path.isfile("/proc/device-tree/model"): - with open("/proc/device-tree/model") as f: - model = f.read() - return f"🍇 {model}" if model.startswith("Raspberry") else f"❓ {model}" - - is_termux = bool(os.popen('echo $PREFIX | grep -o "com.termux"').read()) + try: + if os.path.isfile("/proc/device-tree/model"): + with open("/proc/device-tree/model") as f: + model = f.read() + return f"🍇 {model}" if model.startswith("Raspberry") else f"❓ {model}" + except Exception: + # In case of weird fs, aka Termux + pass + is_termux = "com.termux" in os.environ.get("PREFIX", "") is_okteto = "OKTETO" in os.environ is_docker = "DOCKER" in os.environ is_lavhost = "LAVHOST" in os.environ @@ -620,18 +622,13 @@ def ascii_face() -> str: random.choice( [ "ヽ(๑◠ܫ◠๑)ノ", - "☜(⌒▽⌒)☞", - "/|\\ ^._.^ /|\\", "(◕ᴥ◕ʋ)", "ᕙ(`▽´)ᕗ", - "(☞゚∀゚)☞", "(✿◠‿◠)", "(▰˘◡˘▰)", "(˵ ͡° ͜ʖ ͡°˵)", "ʕっ•ᴥ•ʔっ", "( ͡° ᴥ ͡°)", - "ʕ♥ᴥ♥ʔ", - "\\m/,(> . <)_\\m/", "(๑•́ ヮ •̀๑)", "٩(^‿^)۶", "(っˆڡˆς)", @@ -639,22 +636,49 @@ def ascii_face() -> str: "⊙ω⊙", "٩(^ᴗ^)۶", "(´・ω・)っ由", - "※\\(^o^)/※", - "٩(*❛⊰❛)~❤", "( ͡~ ͜ʖ ͡°)", "✧♡(◕‿◕✿)", "โ๏௰๏ใ ื", "∩。• ᵕ •。∩ ♡", "(♡´౪`♡)", "(◍>◡<◍)⋈。✧♡", - "♥(ˆ⌣ˆԅ)", "╰(✿´⌣`✿)╯♡", "ʕ•ᴥ•ʔ", "ᶘ ◕ᴥ◕ᶅ", "▼・ᴥ・▼", - "【≽ܫ≼】", "ฅ^•ﻌ•^ฅ", "(΄◞ิ౪◟ิ‵)", + "٩(^ᴗ^)۶", + "ᕴーᴥーᕵ", + "ʕ→ᴥ←ʔ", + "ʕᵕᴥᵕʔ", + "ʕᵒᴥᵒʔ", + "ᵔᴥᵔ", + "(✿╹◡╹)", + "(๑→ܫ←)", + "ʕ·ᴥ· ʔ", + "(ノ≧ڡ≦)", + "(≖ᴗ≖✿)", + "(〜^∇^ )〜", + "( ノ・ェ・ )ノ", + "~( ˘▾˘~)", + "(〜^∇^)〜", + "ヽ(^ᴗ^ヽ)", + "(´・ω・`)", + "₍ᐢ•ﻌ•ᐢ₎*・゚。", + "(。・・)_且", + "(=`ω´=)", + "(*•‿•*)", + "(*゚∀゚*)", + "(☉⋆‿⋆☉)", + "ɷ◡ɷ", + "ʘ‿ʘ", + "(。-ω-)ノ", + "( ・ω・)ノ", + "(=゚ω゚)ノ", + "(・ε・`*) …", + "ʕっ•ᴥ•ʔっ", + "(*˘︶˘*)", ] ) ) diff --git a/hikka/version.py b/hikka/version.py index 48121d2..2e6891b 100644 --- a/hikka/version.py +++ b/hikka/version.py @@ -1,2 +1,2 @@ """Represents current userbot version""" -__version__ = (1, 1, 21) +__version__ = (1, 1, 22) diff --git a/hikka/web/root.py b/hikka/web/root.py index dc32a3a..1b48bba 100644 --- a/hikka/web/root.py +++ b/hikka/web/root.py @@ -67,7 +67,7 @@ def restart(*argv): class Web: sign_in_clients = {} - clients = [] + _pending_clients = [] _sessions = [] _ratelimit = {} @@ -151,7 +151,7 @@ class Web: return web.Response(status=401) text = await request.text() - client = self.clients[0] + client = self._pending_clients[0] db = database.Database(client) await db.init() @@ -287,18 +287,30 @@ class Web: del self.sign_in_clients[phone] client.phone = f"+{user.phone}" - self.clients.append(client) + + # At this step we don't want `main.hikka` to "know" about our client + # so it doesn't create bot immediately. That's why we only save its session + # in case user closes web early. It will be handled on restart + # If user finishes login further, client will be passed to main + await main.hikka.save_client_session(client) + + # But now it's pending + self._pending_clients += [client] + return web.Response() async def finish_login(self, request): if not self._check_session(request): return web.Response(status=401) - if not self.clients: + if not self._pending_clients: return web.Response(status=400) first_session = not bool(main.hikka.clients) - await main.hikka.fetch_clients_from_web() + + # Client is ready to pass in to dispatcher + main.hikka.clients = list(set(main.hikka.clients + self._pending_clients)) + self._pending_clients = [] self.clients_set.set() diff --git a/requirements.txt b/requirements.txt index ba9ef41..d255a46 100755 --- a/requirements.txt +++ b/requirements.txt @@ -9,5 +9,6 @@ requests==2.27.1 aiogram==2.19 websockets==10.2 grapheme==0.6.0 +uvloop==0.16.0 # Python 3.8+