- Fix bugs related to web, more specifically: Session save timing, adding more than 1 account and proper restart
- Rework Dockerfiles so they work properly
- Add `uvloop` so the asyncio runs faster
- Add `Docker` badge to info
- Improve Okteto performance by adjusting settings in `okteto-stack.yml`
- New ascii_faces in utils
- Typehints update
- Fix Okteto pinger messages removal
pull/1/head
hikariatama 2022-05-14 17:46:52 +00:00
parent 5aad62610c
commit a6d01c6cdb
10 changed files with 131 additions and 66 deletions

View File

@ -25,7 +25,7 @@ RUN apt update && apt install \
libcairo2 git -y --no-install-recommends libcairo2 git -y --no-install-recommends
# Clean the cache # 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 IP address
EXPOSE 8080 EXPOSE 8080

2
Okteto
View File

@ -33,7 +33,7 @@ RUN apt update && apt install \
libavdevice-dev -y --no-install-recommends libavdevice-dev -y --no-install-recommends
# Clean the cache # 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 IP address
EXPOSE 8080 EXPOSE 8080

View File

@ -0,0 +1,9 @@
version: "3"
services:
worker:
build: .
volumes:
- worker:/data
volumes:
worker:

View File

@ -41,6 +41,8 @@ import sqlite3
import sys import sys
from math import ceil from math import ceil
from typing import Union from typing import Union
import uvloop
import subprocess
from telethon import TelegramClient, events from telethon import TelegramClient, events
from telethon.errors.rpcerrorlist import ( from telethon.errors.rpcerrorlist import (
@ -78,6 +80,8 @@ DATA_DIR = (
CONFIG_PATH = os.path.join(DATA_DIR, "config.json") CONFIG_PATH = os.path.join(DATA_DIR, "config.json")
uvloop.install()
def run_config( def run_config(
db: database.Database, db: database.Database,
@ -386,35 +390,36 @@ class Hikka:
importlib.invalidate_caches() importlib.invalidate_caches()
self._get_api_token() self._get_api_token()
async def fetch_clients_from_web(self): async def save_client_session(self, client: TelegramClient):
"""Imports clients from web module""" session = SQLiteSession(
for client in self.web.clients: os.path.join(
session = SQLiteSession( self.arguments.data_root or DATA_DIR,
os.path.join( f"hikka-+{'x' * (len(client.phone) - 5)}{client.phone[-4:]}-{(await client.get_me()).id}",
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( session.set_dc(
client.session.dc_id, client.session.dc_id,
client.session.server_address, client.session.server_address,
client.session.port, client.session.port,
) )
session.auth_key = client.session.auth_key session.auth_key = client.session.auth_key
session.save() session.save()
client.session = session client.session = session
# Set db attribute to this client in order to save # Set db attribute to this client in order to save
# custom bot nickname from web # custom bot nickname from web
client.hikka_db = database.Database(client) client.hikka_db = database.Database(client)
await client.hikka_db.init() await client.hikka_db.init()
self.clients = list(set(self.clients + self.web.clients))
def _web_banner(self): def _web_banner(self):
"""Shows web banner""" """Shows web banner"""
print("✅ Web mode ready for configuration") 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): async def wait_for_web_auth(self, token: str):
"""Waits for web auth confirmation in Telegram""" """Waits for web auth confirmation in Telegram"""
@ -449,9 +454,12 @@ class Hikka:
return True return True
def _init_clients(self): def _init_clients(self) -> bool:
"""Reads session from disk and inits them""" """
for phone_id, phone in self.phones.items(): 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( session = os.path.join(
self.arguments.data_root or DATA_DIR, self.arguments.data_root or DATA_DIR,
f'hikka{f"-{phone_id}" if phone_id else ""}', f'hikka{f"-{phone_id}" if phone_id else ""}',
@ -473,7 +481,7 @@ class Hikka:
install_entity_caching(client) install_entity_caching(client)
self.clients.append(client) self.clients += [client]
except sqlite3.OperationalError: except sqlite3.OperationalError:
print( print(
"Check that this is the only instance running. " "Check that this is the only instance running. "
@ -482,20 +490,22 @@ class Hikka:
continue continue
except (TypeError, AuthKeyDuplicatedError): except (TypeError, AuthKeyDuplicatedError):
os.remove(os.path.join(DATA_DIR, f"{session}.session")) os.remove(os.path.join(DATA_DIR, f"{session}.session"))
self.main() del self.phones[phone_id]
except (ValueError, ApiIdInvalidError): except (ValueError, ApiIdInvalidError):
# Bad API hash/ID # Bad API hash/ID
run_config({}, self.arguments.data_root) run_config({}, self.arguments.data_root)
return return False
except PhoneNumberInvalidError: except PhoneNumberInvalidError:
print( print(
"Phone number is incorrect. Use international format (+XX...) " "Phone number is incorrect. Use international format (+XX...) "
"and don't put spaces in it." "and don't put spaces in it."
) )
continue del self.phones[phone_id]
except InteractiveAuthRequired: except InteractiveAuthRequired:
print(f"Session {session} was terminated and re-auth is required") 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): def _init_loop(self):
"""Initializes main event loop and starts handler for each client""" """Initializes main event loop and starts handler for each client"""
@ -654,11 +664,13 @@ class Hikka:
save_config_key("port", self.arguments.port) save_config_key("port", self.arguments.port)
self._get_token() 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 return
self._init_clients()
self.loop.set_exception_handler( self.loop.set_exception_handler(
lambda _, x: logging.error( lambda _, x: logging.error(
f"Exception on event loop! {x['message']}", f"Exception on event loop! {x['message']}",

View File

@ -477,7 +477,7 @@ class HikkaSecurityMod(loader.Module):
list(set(self._db.get(main.__name__, "nonickusers", []) + [user.id])), list(set(self._db.get(main.__name__, "nonickusers", []) + [user.id])),
) )
call.edit( await call.edit(
self.strings("user_nn").format( self.strings("user_nn").format(
user.id, user.id,
utils.escape_html(get_display_name(user)), utils.escape_html(get_display_name(user)),

View File

@ -15,7 +15,10 @@ import time
from telethon.errors.rpcerrorlist import YouBlockedUserError from telethon.errors.rpcerrorlist import YouBlockedUserError
from telethon.tl.functions.contacts import UnblockRequest 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 telethon.tl.types import Message
from .. import loader, main, utils from .. import loader, main, utils
@ -54,8 +57,12 @@ class OktetoMod(loader.Module):
if messages: if messages:
logger.info("Deleting previously scheduled Okteto pinger messages") logger.info("Deleting previously scheduled Okteto pinger messages")
for message in messages: await client(
await message.delete() DeleteScheduledMessagesRequest(
self._bot,
[message.id for message in messages],
)
)
raise loader.SelfUnload raise loader.SelfUnload

View File

@ -45,7 +45,6 @@ import git
import grapheme import grapheme
import requests import requests
import telethon import telethon
from aiogram.types import CallbackQuery
from telethon.hints import Entity from telethon.hints import Entity
from telethon.tl.custom.message import Message from telethon.tl.custom.message import Message
from telethon.tl.functions.account import UpdateNotifySettingsRequest from telethon.tl.functions.account import UpdateNotifySettingsRequest
@ -83,7 +82,7 @@ from telethon.tl.types import (
User, User,
) )
from .inline.types import InlineCall from .inline.types import InlineCall, InlineMessage
FormattingEntity = Union[ FormattingEntity = Union[
MessageEntityUnknown, MessageEntityUnknown,
@ -279,16 +278,16 @@ def relocate_entities(
async def answer( async def answer(
message: Union[Message, CallbackQuery, InlineCall], message: Union[Message, InlineCall, InlineMessage],
response: str, response: str,
**kwargs, **kwargs,
) -> Union[CallbackQuery, Message]: ) -> Union[InlineCall, InlineMessage, Message]:
"""Use this to give the response to a command""" """Use this to give the response to a command"""
# Compatibility with FTG\GeekTG # Compatibility with FTG\GeekTG
if isinstance(message, list) and message: if isinstance(message, list) and message:
message = message[0] message = message[0]
if isinstance(message, (CallbackQuery, InlineCall)): if isinstance(message, (InlineMessage, InlineCall)):
await message.edit(response) await message.edit(response)
return message return message
@ -578,13 +577,16 @@ def chunks(_list: Union[list, tuple, set], n: int, /) -> list:
def get_named_platform() -> str: def get_named_platform() -> str:
"""Returns formatted platform name""" """Returns formatted platform name"""
if os.path.isfile("/proc/device-tree/model"): try:
with open("/proc/device-tree/model") as f: if os.path.isfile("/proc/device-tree/model"):
model = f.read() with open("/proc/device-tree/model") as f:
return f"🍇 {model}" if model.startswith("Raspberry") else f"{model}" 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()) 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_okteto = "OKTETO" in os.environ
is_docker = "DOCKER" in os.environ is_docker = "DOCKER" in os.environ
is_lavhost = "LAVHOST" in os.environ is_lavhost = "LAVHOST" in os.environ
@ -620,18 +622,13 @@ def ascii_face() -> str:
random.choice( random.choice(
[ [
"ヽ(๑◠ܫ◠๑)ノ", "ヽ(๑◠ܫ◠๑)ノ",
"☜(⌒▽⌒)☞",
"/|\\ ^._.^ /|\\",
"(◕ᴥ◕ʋ)", "(◕ᴥ◕ʋ)",
"ᕙ(`▽´)ᕗ", "ᕙ(`▽´)ᕗ",
"(☞゚∀゚)☞",
"(✿◠‿◠)", "(✿◠‿◠)",
"(▰˘◡˘▰)", "(▰˘◡˘▰)",
"(˵ ͡° ͜ʖ ͡°˵)", "(˵ ͡° ͜ʖ ͡°˵)",
"ʕっ•ᴥ•ʔっ", "ʕっ•ᴥ•ʔっ",
"( ͡° ᴥ ͡°)", "( ͡° ᴥ ͡°)",
"ʕ♥ᴥ♥ʔ",
"\\m/,(> . <)_\\m/",
"(๑•́ ヮ •̀๑)", "(๑•́ ヮ •̀๑)",
"٩(^‿^)۶", "٩(^‿^)۶",
"(っˆڡˆς)", "(っˆڡˆς)",
@ -639,22 +636,49 @@ def ascii_face() -> str:
"⊙ω⊙", "⊙ω⊙",
"٩(^ᴗ^)۶", "٩(^ᴗ^)۶",
"(´・ω・)っ由", "(´・ω・)っ由",
"\\(^o^)/※",
"٩(*❛⊰❛)~❤",
"( ͡~ ͜ʖ ͡°)", "( ͡~ ͜ʖ ͡°)",
"✧♡(◕‿◕✿)", "✧♡(◕‿◕✿)",
"โ๏௰๏ใ ื", "โ๏௰๏ใ ื",
"∩。• ᵕ •。∩ ♡", "∩。• ᵕ •。∩ ♡",
"(♡´౪`♡)", "(♡´౪`♡)",
"(◍>◡<◍)⋈。✧♡", "(◍>◡<◍)⋈。✧♡",
"♥(ˆ⌣ˆԅ)",
"╰(✿´⌣`✿)╯♡", "╰(✿´⌣`✿)╯♡",
"ʕ•ᴥ•ʔ", "ʕ•ᴥ•ʔ",
"ᶘ ◕ᴥ◕ᶅ", "ᶘ ◕ᴥ◕ᶅ",
"▼・ᴥ・▼", "▼・ᴥ・▼",
"【≽ܫ≼】",
"ฅ^•ﻌ•^ฅ", "ฅ^•ﻌ•^ฅ",
"(΄◞ิ౪◟ิ‵)", "(΄◞ิ౪◟ิ‵)",
"٩(^ᴗ^)۶",
"ᕴーᴥーᕵ",
"ʕ→ᴥ←ʔ",
"ʕᵕᴥᵕʔ",
"ʕᵒᴥᵒʔ",
"ᵔᴥᵔ",
"(✿╹◡╹)",
"(๑→ܫ←)",
"ʕ·ᴥ· ʔ",
"(ノ≧ڡ≦)",
"(≖ᴗ≖✿)",
"(〜^∇^ )〜",
"( ノ・ェ・ )ノ",
"~( ˘▾˘~)",
"(〜^∇^)〜",
"ヽ(^ᴗ^ヽ)",
"(´・ω・`)",
"₍ᐢ•ﻌ•ᐢ₎*・゚。",
"(。・・)_且",
"(=`ω´=)",
"(*•‿•*)",
"(*゚∀゚*)",
"(☉⋆‿⋆☉)",
"ɷ◡ɷ",
"ʘ‿ʘ",
"(。-ω-)ノ",
"( ・ω・)ノ",
"(=゚ω゚)ノ",
"(・ε・`*) …",
"ʕっ•ᴥ•ʔっ",
"(*˘︶˘*)",
] ]
) )
) )

View File

@ -1,2 +1,2 @@
"""Represents current userbot version""" """Represents current userbot version"""
__version__ = (1, 1, 21) __version__ = (1, 1, 22)

View File

@ -67,7 +67,7 @@ def restart(*argv):
class Web: class Web:
sign_in_clients = {} sign_in_clients = {}
clients = [] _pending_clients = []
_sessions = [] _sessions = []
_ratelimit = {} _ratelimit = {}
@ -151,7 +151,7 @@ class Web:
return web.Response(status=401) return web.Response(status=401)
text = await request.text() text = await request.text()
client = self.clients[0] client = self._pending_clients[0]
db = database.Database(client) db = database.Database(client)
await db.init() await db.init()
@ -287,18 +287,30 @@ class Web:
del self.sign_in_clients[phone] del self.sign_in_clients[phone]
client.phone = f"+{user.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() return web.Response()
async def finish_login(self, request): async def finish_login(self, request):
if not self._check_session(request): if not self._check_session(request):
return web.Response(status=401) return web.Response(status=401)
if not self.clients: if not self._pending_clients:
return web.Response(status=400) return web.Response(status=400)
first_session = not bool(main.hikka.clients) 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() self.clients_set.set()

View File

@ -9,5 +9,6 @@ requests==2.27.1
aiogram==2.19 aiogram==2.19
websockets==10.2 websockets==10.2
grapheme==0.6.0 grapheme==0.6.0
uvloop==0.16.0
# Python 3.8+ # Python 3.8+