Compare commits

..
26 Commits
Author SHA1 Message Date
brinza 3e97c318c7 Remove keyboards.py and some refactor 2024-08-08 23:29:42 +03:00
brinza 08d50a3391 add redis in requirements.txt 2024-08-08 03:42:55 +03:00
brinza de24812cdc some refactor with redis 2024-08-08 03:19:39 +03:00
brinza 3e64441936 fix pymysql in requirements.txt 2024-08-08 03:08:10 +03:00
brinza b2eac62bd2 change to use load_config() in migrations/env.py 2024-08-08 02:59:51 +03:00
brinza 6e8238243a Add ability to use self-signed certificate, refactor config.py 2024-08-08 02:55:51 +03:00
brinza 3b12868201 Add more security in webhooks 2024-08-07 02:38:33 +03:00
brinza a8f39d4630 Fix i18n persistence 2024-07-30 18:06:05 +03:00
brinza b3a1ea1309 Refactored and fix docker deploy 2024-07-30 18:02:46 +03:00
brinza c309d4975e Make run deleteWebhook even if polling assumed 2024-07-30 18:01:08 +03:00
brinza 4ac6be06cf Fix Dockerfile 2024-07-30 16:11:06 +03:00
brinza a1bd24d3fc Fix webhook 2024-07-30 16:10:38 +03:00
brinza 238d144b6d Add pymysql in requirements.txt 2024-07-30 13:58:13 +03:00
brinza db3e02356d Try to add webhook using flask 2024-07-30 03:56:00 +03:00
brinza e230d6f3dc Add webhook config 2024-07-30 02:59:58 +03:00
brinza f8c09d963e Fix circular imports 2024-07-30 02:45:20 +03:00
brinza cd5845c180 move pool_recycle and pool_pre_ping in config.py 2024-07-30 02:39:47 +03:00
brinza f62e05fd06 Add DatabaseMiddleware 2024-07-30 02:37:36 +03:00
brinza 3ec83e408c rename class in arguments.py 2024-07-30 02:24:00 +03:00
brinza d2c6b972e5 Removed bot module 2024-07-26 03:33:13 +03:00
brinza e09347a03f Add default for SS_TYPE 2024-07-26 03:04:33 +03:00
brinza fbe174e4fb Fix last line 2024-07-26 03:04:18 +03:00
brinza 273c0bf2da Refactored config 2024-07-26 02:58:19 +03:00
brinza 7b0aac673f Add sqlalchemy and alembic 2024-07-26 01:45:29 +03:00
brinza 028c1ac91d Add docker things 2024-07-21 04:46:50 +03:00
brinza 028d51af18 Fix i18n system 2024-07-21 02:40:49 +03:00
24 changed files with 656 additions and 89 deletions
+2
View File
@@ -3,4 +3,6 @@ __pycache__/
venv/
.venv/
logs/
data/
*.env
*.db
+39
View File
@@ -0,0 +1,39 @@
ARG PYTHON_VERSION=3.10
FROM python:${PYTHON_VERSION}-slim
# avoid .pyc and buffering stdout/stderr
ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1
# add non-root user
ARG UID=1000
RUN adduser \
--disabled-password \
--no-create-home \
--uid "${UID}" \
--home "/app" \
bot
# install requirements
RUN --mount=type=cache,target=/root/.cache/pip \
--mount=type=bind,source=requirements.txt,target=requirements.txt \
pip install -r requirements.txt
# copy sources
COPY --chown=bot --chmod=774 docker-entrypoint.sh /app/
COPY --chown=bot migrations /app/migrations
COPY --chown=bot mybot /app/mybot
COPY --chown=bot i18n.yaml /app/
# prepare environment
ENV I18N_PATH=/data/i18n.yaml
ENV DB_URL=sqlite:////data/bot.db
RUN mkdir -p /data
RUN chown bot:bot /data
VOLUME /data
USER bot
WORKDIR /app
ENTRYPOINT ["./docker-entrypoint.sh"]
+28
View File
@@ -0,0 +1,28 @@
version: "3.0"
volumes:
i18n: {}
redis-config: {}
redis-data: {}
services:
bot:
build:
dockerfile: Dockerfile
context: .
depends_on: [redis]
restart: unless-stopped
volumes:
- i18n:/i18n
env_file: .env
environment:
- SS_TYPE=redis
- SS_REDIS_HOST=redis
redis:
image: redis
restart: unless-stopped
volumes:
- redis-config:/etc/redis
- redis-data:/data
command: redis-server --save 20 1
+17
View File
@@ -0,0 +1,17 @@
#!/bin/sh
if [ ! -f ${I18N_PATH} ]; then
cp i18n.yaml ${I18N_PATH}
fi
alembic -c migrations/alembic.ini upgrade head
if [ $# -eq 0 ]; then
if [ -z "${USE_WEBHOOK}" ]; then
exec python3 -m mybot
else
exec gunicorn -b 0.0.0.0:8080 "mybot:main()"
fi
else
exec $1
fi
+4
View File
@@ -1,3 +1,7 @@
en:
start: "Hello, {message.from_user.full_name}"
help: "Just simple bot, which greets the user. Use /start"
ru:
start: "Привет, {message.from_user.full_name}"
help: "Это простой бот, который приветствует пользователя. Используй /start"
+117
View File
@@ -0,0 +1,117 @@
# A generic, single database configuration.
[alembic]
# path to migration scripts
# Use forward slashes (/) also on windows to provide an os agnostic path
script_location = migrations
# template used to generate migration file names; The default value is %%(rev)s_%%(slug)s
# Uncomment the line below if you want the files to be prepended with date and time
# see https://alembic.sqlalchemy.org/en/latest/tutorial.html#editing-the-ini-file
# for all available tokens
# file_template = %%(year)d_%%(month).2d_%%(day).2d_%%(hour).2d%%(minute).2d-%%(rev)s_%%(slug)s
# sys.path path, will be prepended to sys.path if present.
# defaults to the current working directory.
prepend_sys_path = .
# timezone to use when rendering the date within the migration file
# as well as the filename.
# If specified, requires the python>=3.9 or backports.zoneinfo library.
# Any required deps can installed by adding `alembic[tz]` to the pip requirements
# string value is passed to ZoneInfo()
# leave blank for localtime
# timezone =
# max length of characters to apply to the "slug" field
# truncate_slug_length = 40
# set to 'true' to run the environment during
# the 'revision' command, regardless of autogenerate
# revision_environment = false
# set to 'true' to allow .pyc and .pyo files without
# a source .py file to be detected as revisions in the
# versions/ directory
# sourceless = false
# version location specification; This defaults
# to migrations/versions. When using multiple version
# directories, initial revisions must be specified with --version-path.
# The path separator used here should be the separator specified by "version_path_separator" below.
# version_locations = %(here)s/bar:%(here)s/bat:migrations/versions
# version path separator; As mentioned above, this is the character used to split
# version_locations. The default within new alembic.ini files is "os", which uses os.pathsep.
# If this key is omitted entirely, it falls back to the legacy behavior of splitting on spaces and/or commas.
# Valid values for version_path_separator are:
#
# version_path_separator = :
# version_path_separator = ;
# version_path_separator = space
version_path_separator = os # Use os.pathsep. Default configuration used for new projects.
# set to 'true' to search source files recursively
# in each "version_locations" directory
# new in Alembic version 1.10
# recursive_version_locations = false
# the output encoding used when revision files
# are written from script.py.mako
# output_encoding = utf-8
# ignore this since it will be loaded in migrations/env.py
sqlalchemy.url = driver://user:pass@localhost/dbname
[post_write_hooks]
# post_write_hooks defines scripts or Python functions that are run
# on newly generated revision scripts. See the documentation for further
# detail and examples
# format using "black" - use the console_scripts runner, against the "black" entrypoint
# hooks = black
# black.type = console_scripts
# black.entrypoint = black
# black.options = -l 79 REVISION_SCRIPT_FILENAME
# lint with attempts to fix using "ruff" - use the exec runner, execute a binary
# hooks = ruff
# ruff.type = exec
# ruff.executable = %(here)s/.venv/bin/ruff
# ruff.options = --fix REVISION_SCRIPT_FILENAME
# Logging configuration
[loggers]
keys = root,sqlalchemy,alembic
[handlers]
keys = console
[formatters]
keys = generic
[logger_root]
level = WARN
handlers = console
qualname =
[logger_sqlalchemy]
level = WARN
handlers =
qualname = sqlalchemy.engine
[logger_alembic]
level = INFO
handlers =
qualname = alembic
[handler_console]
class = StreamHandler
args = (sys.stderr,)
level = NOTSET
formatter = generic
[formatter_generic]
format = %(levelname)-5.5s [%(name)s] %(message)s
datefmt = %H:%M:%S
+75
View File
@@ -0,0 +1,75 @@
from logging.config import fileConfig
from sqlalchemy import engine_from_config
from sqlalchemy import pool
from alembic import context
from mybot.config import load_config
from mybot.database import Base
import mybot.database.models # do not delete this
config = context.config
if config.config_file_name is not None:
fileConfig(config.config_file_name)
# add your model's MetaData object here
target_metadata = Base.metadata
# set sqlalchemy.url since it can not be set in alembic.ini file
app_config = load_config()
config.set_main_option("sqlalchemy.url", app_config.database.url)
def run_migrations_offline() -> None:
"""Run migrations in 'offline' mode.
This configures the context with just a URL
and not an Engine, though an Engine is acceptable
here as well. By skipping the Engine creation
we don't even need a DBAPI to be available.
Calls to context.execute() here emit the given string to the
script output.
"""
url = config.get_main_option("sqlalchemy.url")
context.configure(
url=url,
target_metadata=target_metadata,
literal_binds=True,
dialect_opts={"paramstyle": "named"},
)
with context.begin_transaction():
context.run_migrations()
def run_migrations_online() -> None:
"""Run migrations in 'online' mode.
In this scenario we need to create an Engine
and associate a connection with the context.
"""
connectable = engine_from_config(
config.get_section(config.config_ini_section, {}),
prefix="sqlalchemy.",
poolclass=pool.NullPool,
)
with connectable.connect() as connection:
context.configure(
connection=connection, target_metadata=target_metadata
)
with context.begin_transaction():
context.run_migrations()
if context.is_offline_mode():
run_migrations_offline()
else:
run_migrations_online()
+26
View File
@@ -0,0 +1,26 @@
"""${message}
Revision ID: ${up_revision}
Revises: ${down_revision | comma,n}
Create Date: ${create_date}
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
${imports if imports else ""}
# revision identifiers, used by Alembic.
revision: str = ${repr(up_revision)}
down_revision: Union[str, None] = ${repr(down_revision)}
branch_labels: Union[str, Sequence[str], None] = ${repr(branch_labels)}
depends_on: Union[str, Sequence[str], None] = ${repr(depends_on)}
def upgrade() -> None:
${upgrades if upgrades else "pass"}
def downgrade() -> None:
${downgrades if downgrades else "pass"}
@@ -0,0 +1,36 @@
"""Add User model
Revision ID: ef447fba99de
Revises:
Create Date: 2024-07-26 01:43:16.748576
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision: str = 'ef447fba99de'
down_revision: Union[str, None] = None
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('user',
sa.Column('id', sa.BIGINT(), autoincrement=False, nullable=False),
sa.Column('username', sa.String(length=32), nullable=True),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('id'),
sa.UniqueConstraint('username')
)
# ### end Alembic commands ###
def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table('user')
# ### end Alembic commands ###
+46 -7
View File
@@ -1,12 +1,51 @@
from .config import Config
from telebot import TeleBot
from .config import Config, load_config
from .logger import create_logger
from .bot import create_bot
from .i18n import I18N
from .database import get_engine
from .states import get_state_storage
from .handlers import register_handlers
from .middlewares import setup_middlewares
from .filters import add_custom_filters
from .webhook import create_app
def create_bot(config: Config, i18n: I18N, engine):
state_storage = get_state_storage(config.states)
bot = TeleBot(config.bot.token,
parse_mode=config.bot.parse_mode,
skip_pending=config.bot.skip_pending,
num_threads=config.bot.num_threads,
use_class_middlewares=True,
state_storage=state_storage,
threaded=False if config.use_webhook else True)
register_handlers(bot)
setup_middlewares(bot, i18n, engine)
add_custom_filters(bot, config)
bot.delete_webhook()
if config.use_webhook:
bot.set_webhook(config.webhook.url,
drop_pending_updates=config.webhook.drop_pending_updates,
max_connections=config.webhook.max_connections,
secret_token=config.webhook.secret_token,
certificate=config.webhook.cert_path)
return bot
def main():
config = Config()
logger = create_logger("mybot", config.LOG_LEVEL)
i18n = I18N(logger, config.I18N_PATH)
bot = create_bot(config, logger, i18n)
bot.infinity_polling(config.TIMEOUT, config.DROP_PENDING, config.POLLING_TIMEOUT)
config = load_config()
# logger = create_logger("mybot", config.log_level)
i18n = I18N(config.i18n)
engine = get_engine(config.database)
bot = create_bot(config, i18n, engine)
if config.use_webhook:
app = create_app(bot, config)
return app
bot.infinity_polling(
timeout=config.bot.timeout,
long_polling_timeout=config.bot.polling_timeout,
skip_pending=config.bot.skip_pending
)
-36
View File
@@ -1,36 +0,0 @@
from telebot import TeleBot
from telebot.storage import StateMemoryStorage, StateRedisStorage
from .handlers import register_handlers
from .middlewares import setup_middlewares
from .filters import add_custom_filters
def create_bot(config, logger, i18n):
if config.SS_TYPE == "memory":
state_storage = StateMemoryStorage()
elif config.SS_TYPE == "redis":
state_storage = StateRedisStorage(config.SS_REDIS_HOST,
config.SS_REDIS_PORT,
config.SS_REDIS_DB,
config.SS_REDIS_PASS)
else:
raise RuntimeWarning(f"Unknown state storage type: '{config.SS_TYPE}'")
bot = TeleBot(config.BOT_TOKEN,
parse_mode=config.PARSE_MODE,
skip_pending=config.DROP_PENDING,
num_threads=config.NUM_THREADS,
use_class_middlewares=True,
state_storage=state_storage)
logger.debug("Setting up middlewares")
setup_middlewares(bot, logger, i18n)
logger.debug("Registering handlers")
register_handlers(bot)
logger.debug("Adding custom filters")
add_custom_filters(bot, config)
return bot
+121 -22
View File
@@ -1,30 +1,129 @@
import os
import secrets
from dataclasses import dataclass
from typing import Optional
@dataclass
class BotConfig:
token: str
skip_pending: bool
timeout: int
polling_timeout: int
num_threads: int
parse_mode: str
@classmethod
def from_env(cls):
return cls(os.getenv("BOT_TOKEN"),
bool(int(os.getenv("BOT_SKIP_PENDING", True))),
int(os.getenv("BOT_TIMEOUT", 20)),
int(os.getenv("BOT_POLLING_TIMEOUT", 20)),
int(os.getenv("BOT_NUM_THREADS", 2)),
os.getenv("BOT_PARSE_MODE", "html"))
@dataclass
class WebhookConfig:
domain: Optional[str]
url_path: str
max_connections: int
drop_pending_updates: bool
# secret token
use_secret_token: bool
secret_token: Optional[str]
# self-signed certificate
cert_path: Optional[str]
def __post_init__(self):
if self.use_secret_token and not self.secret_token:
self.secret_token = secrets.token_hex()
@property
def url(self):
return f"https://{self.domain}{self.url_path}"
@classmethod
def from_env(cls):
return cls(os.getenv("WEBHOOK_DOMAIN"),
os.getenv("WEBHOOK_URL_PATH", "/"),
int(os.getenv("WEBHOOK_MAX_CONNECTIONS", 40)),
bool(int(os.getenv("WEBHOOK_DROP_PENDING", True))),
bool(int(os.getenv("WEBHOOK_USE_SECRET_TOKEN", True))),
os.getenv("WEBHOOK_SECRET_TOKEN"),
os.getenv("WEBHOOK_CERT_PATH"))
@dataclass
class I18NConfig:
path: str
lang: str
fallback_lang: str
@classmethod
def from_env(cls):
return cls(os.getenv("I18N_PATH", "i18n.yaml"),
os.getenv("I18N_LANG", "en"),
os.getenv("I18N_FALLBACK_LANG", "en"))
@dataclass
class StateStorageConfig:
type: str
redis_host: Optional[str]
redis_port: int
redis_db: int
redis_pass: Optional[str]
@classmethod
def from_env(cls):
return cls(os.getenv("SS_TYPE", "memory"),
os.getenv("SS_REDIS_HOST"),
int(os.getenv("SS_REDIS_PORT", 6379)),
int(os.getenv("SS_REDIS_DB", 0)),
os.getenv("SS_REDIS_PASS"))
@dataclass
class DatabaseConfig:
url: str
pool_recycle: int
pool_pre_ping: bool
@classmethod
def from_env(cls):
return cls(os.getenv("DB_URL", "sqlite:///bot.db"),
int(os.getenv("DB_POOL_RECYCLE", 3600)),
bool(int(os.getenv("DB_POOL_PRE_PING", True))))
@dataclass
class Config:
# bot setup
BOT_TOKEN: str = os.getenv("BOT_TOKEN")
OWNER_ID: int = int(os.getenv("OWNER_ID", 1))
LOG_LEVEL: str = os.getenv("LOG_LEVEL", "INFO")
bot: BotConfig
i18n: I18NConfig
states: StateStorageConfig
database: DatabaseConfig
webhook: WebhookConfig
# bot behaviour
DROP_PENDING: bool = bool(int(os.getenv("DROP_PENDING", True)))
TIMEOUT: int = int(os.getenv("TIMEOUT", 20))
POLLING_TIMEOUT: int = int(os.getenv("POLLING_TIMEOUT", 20))
NUM_THREADS: int = int(os.getenv("NUM_THREADS", 2))
PARSE_MODE: str = os.getenv("PARSE_MODE", "html")
use_webhook: bool
log_level: str
owner_id: int
# i18n
I18N_PATH: str = os.getenv("I18N_PATH", "i18n.yaml")
I18N_LANG: str = os.getenv("I18N_LANG", "en")
@classmethod
def from_env(cls):
return cls(
bot=BotConfig.from_env(),
i18n=I18NConfig.from_env(),
states=StateStorageConfig.from_env(),
database=DatabaseConfig.from_env(),
webhook=WebhookConfig.from_env(),
use_webhook=bool(int(os.getenv("USE_WEBHOOK", False))),
log_level=os.getenv("LOG_LEVEL", "INFO"),
owner_id=int(os.getenv("OWNER_ID", 1)),
)
# state storage
SS_TYPE: str = os.getenv("SS_TYPE", "memory").lower()
SS_REDIS_HOST: str = os.getenv("SS_REDIS_HOST")
SS_REDIS_PORT: int = int(os.getenv("SS_REDIS_PORT", 6379))
SS_REDIS_DB: int = int(os.getenv("SS_REDIS_DB", 0))
SS_REDIS_PASS: str = os.getenv("SS_REDIS_PASS")
def __init__(self):
if not self.BOT_TOKEN:
raise RuntimeError("Missing BOT_TOKEN")
def load_config() -> Config:
return Config.from_env()
+15
View File
@@ -0,0 +1,15 @@
from sqlalchemy import create_engine
from sqlalchemy.orm import DeclarativeBase
from ..config import DatabaseConfig
def get_engine(config: DatabaseConfig):
engine = create_engine(config.url,
pool_recycle=config.pool_recycle,
pool_pre_ping=config.pool_pre_ping)
return engine
class Base (DeclarativeBase):
pass
+15
View File
@@ -0,0 +1,15 @@
from sqlalchemy import BIGINT, String
from sqlalchemy.orm import Mapped, mapped_column
from . import Base
class User (Base):
__tablename__ = "user"
id: Mapped[int] = mapped_column(BIGINT, primary_key=True, unique=True, autoincrement=False)
username: Mapped[str] = mapped_column(String(32), unique=True, nullable=True)
# additional fields go here
def __init__(self, id: int, username: str):
super().__init__(id=id, username=username)
+3 -2
View File
@@ -2,8 +2,9 @@ from telebot import TeleBot
from telebot.custom_filters import StateFilter
from .isowner import IsOwnerFilter
from ..config import Config
def add_custom_filters(bot: TeleBot, config):
def add_custom_filters(bot: TeleBot, config: Config):
bot.add_custom_filter(StateFilter(bot))
bot.add_custom_filter(IsOwnerFilter(config.OWNER_ID))
bot.add_custom_filter(IsOwnerFilter(config.owner_id))
+19 -8
View File
@@ -1,14 +1,15 @@
import logging
from typing import Optional
from yaml import safe_load
from .config import I18NConfig
class I18N:
def __init__(self, logger: logging.Logger, path="i18n.yaml", fallback_lang="en"):
self.logger = logger
self._path = path
self._fallback_lang = fallback_lang
def __init__(self, config: I18NConfig):
self._path = config.path
self._fallback_lang = config.fallback_lang
self._lang = config.lang or self.fallback_lang
self._dict = dict()
self.load()
@@ -20,6 +21,16 @@ class I18N:
def fallback_lang(self):
return self._fallback_lang
def set_lang(self, lang: Optional[str] = None):
if lang in self._dict:
self._lang = lang
else:
self._lang = self.fallback_lang
@property
def lang(self):
return self._lang
def load(self):
self._dict.clear()
with open(self._path) as f:
@@ -28,14 +39,14 @@ class I18N:
raise RuntimeError("I18N file doesn't contain fallback language section")
def get(self, phrase: str, lang: Optional[str] = None):
lang = lang or self.fallback_lang
lang = lang or self.lang
if lang not in self._dict:
self.logger.warning(f"Language '{lang}' not found in i18n, using fallback")
# self.logger.warning(f"Language '{lang}' not found in i18n, using fallback")
lang = self.fallback_lang
lang_dict = self._dict.get(lang)
result = lang_dict.get(phrase)
if result is None:
self.logger.error(f"Phrase '{phrase}' not found in language '{lang}'")
# self.logger.error(f"Phrase '{phrase}' not found in language '{lang}'")
result = f"<Phrase '{phrase}' not found in language '{lang}'>"
return result
-1
View File
@@ -1 +0,0 @@
# keyboards will be defined here
-1
View File
@@ -27,4 +27,3 @@ def create_logger(name: str,
logger.addHandler(file_handler)
return logger
+6 -5
View File
@@ -1,9 +1,10 @@
import logging
from telebot import TeleBot
from .arguments import ExtraArguments
from ..i18n import I18N
from .arguments import ArgumentsMiddleware
from .database import DatabaseMiddleware
def setup_middlewares(bot: TeleBot, logger: logging.Logger, i18n):
bot.setup_middleware(ExtraArguments(logger, i18n))
def setup_middlewares(bot: TeleBot, i18n: I18N, engine):
bot.setup_middleware(ArgumentsMiddleware(i18n))
bot.setup_middleware(DatabaseMiddleware(engine))
+2 -6
View File
@@ -1,18 +1,14 @@
import logging
from telebot.handler_backends import BaseMiddleware
from telebot.types import Message, CallbackQuery
class ExtraArguments(BaseMiddleware):
def __init__(self, logger: logging.Logger, i18n):
class ArgumentsMiddleware (BaseMiddleware):
def __init__(self, i18n):
super().__init__()
self.logger = logger
self.i18n = i18n
self.update_types = ["message", "callback_query"]
def pre_process(self, obj, data: dict):
data["logger"] = self.logger
if isinstance(obj, Message):
data["t"] = self.i18n.customized_call(message=obj)
elif isinstance(obj, CallbackQuery):
+29
View File
@@ -0,0 +1,29 @@
from telebot.handler_backends import BaseMiddleware
from telebot.types import Message, CallbackQuery
from sqlalchemy.orm import Session
from ..database.models import User
class DatabaseMiddleware (BaseMiddleware):
def __init__(self, engine):
super().__init__()
self.engine = engine
self.update_types = ["message", "callback_query"]
def pre_process(self, obj: [Message, CallbackQuery], data: dict):
session = Session(self.engine)
user = session.get(User, obj.from_user.id)
if user is None:
user = User(id=obj.from_user.id, username=obj.from_user.username)
session.add(user)
session.commit()
data["db"] = session
data["user"] = user
def post_process(self, message, data: dict, exception: BaseException):
if "db" in data:
session: Session = data["db"]
session.rollback()
session.close()
+14 -1
View File
@@ -1 +1,14 @@
# states will be defined here
from telebot.storage import StateMemoryStorage, StateRedisStorage
from .config import StateStorageConfig
def get_state_storage(config: StateStorageConfig):
if config.type == "memory":
state_storage = StateMemoryStorage()
elif config.type == "redis":
state_storage = StateRedisStorage(config.redis_host, config.redis_port,
config.redis_db, config.redis_pass)
else:
raise RuntimeWarning(f"Unknown state storage type: '{config.type}'")
return state_storage
+35
View File
@@ -0,0 +1,35 @@
from flask import Flask, request, abort, g
from telebot import TeleBot
from telebot.types import Update
from ..config import Config
def handle_updates():
if request.method == "GET":
abort(404) # safer to 404
if g.config.webhook.use_secret_token:
if request.headers.get("X-Telegram-Bot-Api-Secret-Token") != g.config.webhook.secret_token:
abort(404)
if request.headers.get("content-type") == "application/json":
update = Update.de_json(request.get_json())
g.bot.process_new_updates([update])
return ""
else:
abort(404) # safer to 404
def inject_g(**kwargs):
def inner():
for k, v in kwargs.items():
setattr(g, k, v)
return inner
def create_app(bot: TeleBot, config: Config):
app = Flask(__name__)
app.add_url_rule(config.webhook.url_path,
view_func=handle_updates,
methods=["GET", "POST"])
app.before_request(inject_g(bot=bot, config=config))
return app
+7
View File
@@ -1,2 +1,9 @@
pytelegrambotapi
pyyaml
sqlalchemy
alembic
psycopg
pymysql[rsa]
flask
gunicorn
redis