Merge pull request #20 from hikariatama/okteto_test

Hikka 1.1.22
pull/1/head
Dan Gazizullin 2022-05-15 21:33:00 +03:00 committed by GitHub
commit 0337c97a1f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 282 additions and 111 deletions

View File

@ -133,6 +133,8 @@ dmypy.json
*.pyc
hikka/api_token.py
hikka/loaded_modules
loaded_modules
hikka/debug_modules
hikka*.session*
database-*.json
*.swp
@ -154,4 +156,11 @@ config.json
*.jpg
*.jpeg
*.webp
*.webm
*.webm
*.tgs
*.mp4
*.mp3
*.ogg
*.m4a
*.mp3
*.avi

5
.gitignore vendored
View File

@ -159,4 +159,7 @@ config.json
*.tgs
*.mp4
*.mp3
*.ogg
*.ogg
*.m4a
*.mp3
*.avi

View File

@ -1,9 +1,37 @@
FROM python:3.8
# Template
FROM python:3.8-slim-buster as main
# Add files
ADD . /
ENV OKTETO=true
RUN pip install -r requirements.txt
RUN pip install -r optional_requirements.txt
RUN apt update && apt install ffmpeg libavcodec-dev libavutil-dev libavformat-dev libswscale-dev libavdevice-dev -y
# Tell Hikka, that it's running docker
# Currently it's used only in .info badge
ENV DOCKER=true
# Suppress weird gitpython error
ENV GIT_PYTHON_REFRESH=quiet
# Do not user pip cache dir
ENV PIP_NO_CACHE_DIR=1
# Install mandatory pip requirements
RUN pip install \
--no-warn-script-location \
--no-cache-dir \
-r requirements.txt
# Install mandatory apt packages
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/*
# Expose IP address
EXPOSE 8080
# Create data dir
RUN mkdir /data
CMD ["python3", "-m", "hikka"]
# Run Hikka
CMD ["python3", "-m", "hikka"]

45
Okteto 100644
View File

@ -0,0 +1,45 @@
# Template
FROM python:3.8-slim-buster as main
# Add files
ADD . /
# Tell Hikka, that it's running on Okteto
ENV OKTETO=true
# Suppress weird gitpython error
ENV GIT_PYTHON_REFRESH=quiet
# Do not user pip cache dir
ENV PIP_NO_CACHE_DIR=1
# Install mandatory pip requirements
RUN pip install \
--no-warn-script-location \
--no-cache-dir \
-r requirements.txt
# Install non-mandatory pip requirements
# As we are running Okteto, we don't care about resources consuming
RUN pip install \
--no-warn-script-location \
--no-cache-dir \
-r optional_requirements.txt
# Install mandatory apt packages
RUN apt update && apt install \
libcairo2 git ffmpeg libavcodec-dev \
libavutil-dev libavformat-dev libswscale-dev \
libavdevice-dev -y --no-install-recommends
# Clean the cache
RUN rm -rf /var/lib/apt/lists /var/cache/apt/archives /tmp/*
# Expose IP address
EXPOSE 8080
# Create data dir
RUN mkdir /data
# Run Hikka
CMD ["python3", "-m", "hikka"]

View File

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

View File

@ -36,6 +36,7 @@ if (
getpass.getuser() == "root"
and "--root" not in " ".join(sys.argv)
and "OKTETO" not in os.environ
and "DOCKER" not in os.environ
):
print("🚫" * 30)
print("NEVER EVER RUN USERBOT FROM ROOT")

View File

@ -22,7 +22,7 @@ from . import utils
DATA_DIR = (
os.path.normpath(os.path.join(utils.get_base_dir(), ".."))
if "OKTETO" not in os.environ
if "OKTETO" not in os.environ and "DOCKER" not in os.environ
else "/data"
)

View File

@ -82,10 +82,11 @@ class StringLoader(SourceLoader):
self.origin = origin
def get_code(self, fullname: str) -> str:
if not (source := self.get_source(fullname)):
return None
return compile(source, self.origin, "exec", dont_inherit=True)
return (
compile(source, self.origin, "exec", dont_inherit=True)
if (source := self.get_source(fullname))
else None
)
def get_filename(self, *args, **kwargs) -> str:
return self.origin
@ -208,7 +209,7 @@ en_keys = "`qwertyuiop[]asdfghjkl;'zxcvbnm,./~@#$%^&QWERTYUIOP{}ASDFGHJKL:\"|ZXC
DATA_DIR = (
os.path.normpath(os.path.join(utils.get_base_dir(), ".."))
if "OKTETO" not in os.environ
if "OKTETO" not in os.environ and "DOCKER" not in os.environ
else "/data"
)

View File

@ -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 (
@ -68,18 +70,18 @@ except ImportError:
else:
web_available = True
is_okteto = "OKTETO" in os.environ
omit_log = False
DATA_DIR = (
os.path.normpath(os.path.join(utils.get_base_dir(), ".."))
if not is_okteto
if "OKTETO" not in os.environ and "DOCKER" not in os.environ
else "/data"
)
CONFIG_PATH = os.path.join(DATA_DIR, "config.json")
uvloop.install()
def run_config(
db: database.Database,
@ -139,9 +141,11 @@ def gen_port() -> int:
"""
Generates random free port in case of VDS, and
8080 in case of Okteto
In case of Docker, also return 8080, as it's already
exposed by default
:returns: Integer value of generated port
"""
if "OKTETO" in os.environ:
if "OKTETO" in os.environ or "DOCKER" in os.environ:
return 8080
# But for own server we generate new free port, and assign to it
@ -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']}",

View File

@ -154,10 +154,11 @@ class HikkaSecurityMod(loader.Module):
is_inline: bool,
):
cmd = (
self.allmodules.commands[command]
if not is_inline
else self.allmodules.inline_handlers[command]
self.allmodules.inline_handlers[command]
if is_inline
else self.allmodules.commands[command]
)
mask = self._db.get(security.__name__, "masks", {}).get(
f"{cmd.__module__}.{cmd.__name__}",
getattr(cmd, "security", security.DEFAULT_PERMISSIONS),
@ -188,7 +189,9 @@ class HikkaSecurityMod(loader.Module):
await call.edit(
self.strings("permissions").format(
self.get_prefix() if not is_inline else f"@{self.inline.bot_username} ",
f"@{self.inline.bot_username} "
if is_inline
else self.get_prefix(),
command,
),
reply_markup=self._build_markup(cmd, is_inline),
@ -231,7 +234,7 @@ class HikkaSecurityMod(loader.Module):
return utils.chunks(
[
{
"text": f"{('🚫' if not level else '')} {self.strings[group]}",
"text": f"{'' if level else '🚫'} {self.strings[group]}",
"callback": self.inline__switch_perm,
"args": (
command.__name__.rsplit("cmd", maxsplit=1)[0],
@ -243,12 +246,20 @@ class HikkaSecurityMod(loader.Module):
for group, level in perms.items()
],
2,
) + [[{"text": self.strings("close_menu"), "callback": self.inline_close}]]
) + [
[
{
"text": self.strings("close_menu"),
"callback": self.inline_close,
}
]
]
return utils.chunks(
[
{
"text": f"{('🚫' if not level else '')} {self.strings[group]}",
"text": f"{'' if level else '🚫'} {self.strings[group]}",
"callback": self.inline__switch_perm,
"args": (
command.__name__.rsplit("_inline_handler", maxsplit=1)[0],
@ -267,7 +278,7 @@ class HikkaSecurityMod(loader.Module):
return utils.chunks(
[
{
"text": f"{('🚫' if not level else '')} {self.strings[group]}",
"text": f"{'' if level else '🚫'} {self.strings[group]}",
"callback": self.inline__switch_perm_bm,
"args": (group, not level, is_inline),
}
@ -286,6 +297,12 @@ class HikkaSecurityMod(loader.Module):
def _perms_map(perms: int, is_inline: bool) -> dict:
return (
{
"sudo": bool(perms & SUDO),
"support": bool(perms & SUPPORT),
"everyone": bool(perms & EVERYONE),
}
if is_inline
else {
"sudo": bool(perms & SUDO),
"support": bool(perms & SUPPORT),
"group_owner": bool(perms & GROUP_OWNER),
@ -300,12 +317,6 @@ class HikkaSecurityMod(loader.Module):
"pm": bool(perms & PM),
"everyone": bool(perms & EVERYONE),
}
if not is_inline
else {
"sudo": bool(perms & SUDO),
"support": bool(perms & SUPPORT),
"everyone": bool(perms & EVERYONE),
}
)
def _get_current_perms(
@ -477,7 +488,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)),
@ -494,9 +505,10 @@ class HikkaSecurityMod(loader.Module):
self._db.set(
security.__name__,
group,
list(set(self._db.get(security.__name__, group, [])) - set([user.id])),
list(set(self._db.get(security.__name__, group, [])) - {user.id}),
)
m = self.strings(f"{group}_removed").format(
user.id,
utils.escape_html(get_display_name(user)),

View File

@ -39,7 +39,7 @@ import sys
import uuid
from collections import ChainMap
from importlib.machinery import ModuleSpec
from typing import List, Optional, Union
from typing import Optional, Union
from urllib.parse import urlparse
import requests
@ -48,7 +48,7 @@ from telethon.tl.types import Message
from .. import loader, main, utils
from ..compat import geek
from ..inline.types import InlineCall, InlineMessage
from ..inline.types import InlineCall
logger = logging.getLogger(__name__)
@ -102,7 +102,7 @@ def get_git_api(url):
branch = m.group(2)
path_ = m.group(3)
api_url = "https://api.github.com/repos{}/contents".format(m.group(1))
api_url = f"https://api.github.com/repos{m.group(1)}/contents"
if path_ is not None and len(path_) > 0:
api_url += path_

View File

@ -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

View File

@ -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,
@ -170,7 +169,7 @@ def get_entity_id(entity: Entity) -> int:
def escape_html(text: str, /) -> str:
"""Pass all untrusted/potentially corrupt input here"""
return str(text).replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;")
return text.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;")
def escape_quotes(text: str, /) -> str:
@ -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,16 +577,23 @@ 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
if is_docker:
return "🐳 Docker"
if is_termux:
return "🕶 Termux"
@ -607,7 +613,7 @@ def uptime() -> int:
def formatted_uptime() -> str:
"""Returnes formmated uptime"""
return "{}".format(str(timedelta(seconds=uptime())))
return f"{str(timedelta(seconds=uptime()))}"
def ascii_face() -> str:
@ -616,18 +622,13 @@ def ascii_face() -> str:
random.choice(
[
"ヽ(๑◠ܫ◠๑)ノ",
"☜(⌒▽⌒)☞",
"/|\\ ^._.^ /|\\",
"(◕ᴥ◕ʋ)",
"ᕙ(`▽´)ᕗ",
"(☞゚∀゚)☞",
"(✿◠‿◠)",
"(▰˘◡˘▰)",
"(˵ ͡° ͜ʖ ͡°˵)",
"ʕっ•ᴥ•ʔっ",
"( ͡° ᴥ ͡°)",
"ʕ♥ᴥ♥ʔ",
"\\m/,(> . <)_\\m/",
"(๑•́ ヮ •̀๑)",
"٩(^‿^)۶",
"(っˆڡˆς)",
@ -635,22 +636,49 @@ def ascii_face() -> str:
"⊙ω⊙",
"٩(^ᴗ^)۶",
"(´・ω・)っ由",
"\\(^o^)/※",
"٩(*❛⊰❛)~❤",
"( ͡~ ͜ʖ ͡°)",
"✧♡(◕‿◕✿)",
"โ๏௰๏ใ ื",
"∩。• ᵕ •。∩ ♡",
"(♡´౪`♡)",
"(◍>◡<◍)⋈。✧♡",
"♥(ˆ⌣ˆԅ)",
"╰(✿´⌣`✿)╯♡",
"ʕ•ᴥ•ʔ",
"ᶘ ◕ᴥ◕ᶅ",
"▼・ᴥ・▼",
"【≽ܫ≼】",
"ฅ^•ﻌ•^ฅ",
"(΄◞ิ౪◟ิ‵)",
"٩(^ᴗ^)۶",
"ᕴーᴥーᕵ",
"ʕ→ᴥ←ʔ",
"ʕᵕᴥᵕʔ",
"ʕᵒᴥᵒʔ",
"ᵔᴥᵔ",
"(✿╹◡╹)",
"(๑→ܫ←)",
"ʕ·ᴥ· ʔ",
"(ノ≧ڡ≦)",
"(≖ᴗ≖✿)",
"(〜^∇^ )〜",
"( ノ・ェ・ )ノ",
"~( ˘▾˘~)",
"(〜^∇^)〜",
"ヽ(^ᴗ^ヽ)",
"(´・ω・`)",
"₍ᐢ•ﻌ•ᐢ₎*・゚。",
"(。・・)_且",
"(=`ω´=)",
"(*•‿•*)",
"(*゚∀゚*)",
"(☉⋆‿⋆☉)",
"ɷ◡ɷ",
"ʘ‿ʘ",
"(。-ω-)ノ",
"( ・ω・)ノ",
"(=゚ω゚)ノ",
"(・ε・`*) …",
"ʕっ•ᴥ•ʔっ",
"(*˘︶˘*)",
]
)
)

View File

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

View File

@ -50,7 +50,7 @@ from telethon.tl.functions.contacts import UnblockRequest
DATA_DIR = (
os.path.normpath(os.path.join(utils.get_base_dir(), ".."))
if "OKTETO" not in os.environ
if "OKTETO" not in os.environ and "DOCKER" not in os.environ
else "/data"
)
@ -67,7 +67,7 @@ def restart(*argv):
class Web:
sign_in_clients = {}
clients = []
_pending_clients = []
_sessions = []
_ratelimit = {}
@ -108,10 +108,11 @@ class Web:
return self.clients_set.wait()
def _check_session(self, request) -> bool:
if not main.hikka.clients:
return True
return request.cookies.get("session", None) in self._sessions
return (
request.cookies.get("session", None) in self._sessions
if main.hikka.clients
else True
)
async def _check_bot(
self,
@ -151,7 +152,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 +288,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()

View File

@ -2,16 +2,18 @@ name: hikka
services:
worker:
public: true
build: .
build:
context: .
dockerfile: Okteto
replicas: 1
ports:
- 8080
resources:
cpu: 900m
memory: 2Gi
cpu: "1"
memory: 3Gi
volumes:
- worker:/data
volumes:
worker:
size: 1Gi
size: 3Gi

View File

@ -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+