├── bot ├── __init__.py ├── keyboards.py ├── structure.py ├── states.py └── handlers.py ├── database ├── __init__.py ├── ydb_settings.py ├── utils.py ├── queries.py └── model.py ├── requirements.txt ├── screenshots ├── 02-name-folder.png ├── 20-bot_start.png ├── 01-create-folder.png ├── 13-function-logs.png ├── 14-ydb-settings.png ├── 21-bot_register.png ├── 22-function-logs.png ├── 05-1_bot_commands.png ├── 07-name-api-gateway.png ├── 08-create-function.png ├── 10-copy-function-id.png ├── 12-api-gateway-logs.png ├── 15-set-bot-commands.png ├── 06-create-api-gateway.png ├── 18-save-ydb-settings.png ├── 19-create-ydb-tables.png ├── 23-ydb-after-register.png ├── 03-newly-created-folder.png ├── 05-create-telegram-bot.png ├── 08-1-select-environment.png ├── 08-make-function-public.png ├── 14-create-ydb-database.png ├── 17-create-ydb-database.png ├── 04-create-service-account.png ├── 16-create-function-version-gui.png ├── 09-create-default-function-version.png └── 11-connect-api-gateway-to-fucntion.png ├── tests ├── fixtures.py └── utils.py ├── index.py ├── create_function_version.sh ├── LICENSE ├── user_interaction └── texts.py ├── logs.py ├── .gitignore └── README.md /bot/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /database/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | pyTelegramBotAPI==4.6.0 2 | ydb==3.3.1 3 | python-json-logger==2.0.7 4 | protobuf==3.20.0 5 | -------------------------------------------------------------------------------- /screenshots/02-name-folder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mskozlova/ydb_serverless_telegram_bot/HEAD/screenshots/02-name-folder.png -------------------------------------------------------------------------------- /screenshots/20-bot_start.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mskozlova/ydb_serverless_telegram_bot/HEAD/screenshots/20-bot_start.png -------------------------------------------------------------------------------- /screenshots/01-create-folder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mskozlova/ydb_serverless_telegram_bot/HEAD/screenshots/01-create-folder.png -------------------------------------------------------------------------------- /screenshots/13-function-logs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mskozlova/ydb_serverless_telegram_bot/HEAD/screenshots/13-function-logs.png -------------------------------------------------------------------------------- /screenshots/14-ydb-settings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mskozlova/ydb_serverless_telegram_bot/HEAD/screenshots/14-ydb-settings.png -------------------------------------------------------------------------------- /screenshots/21-bot_register.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mskozlova/ydb_serverless_telegram_bot/HEAD/screenshots/21-bot_register.png -------------------------------------------------------------------------------- /screenshots/22-function-logs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mskozlova/ydb_serverless_telegram_bot/HEAD/screenshots/22-function-logs.png -------------------------------------------------------------------------------- /screenshots/05-1_bot_commands.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mskozlova/ydb_serverless_telegram_bot/HEAD/screenshots/05-1_bot_commands.png -------------------------------------------------------------------------------- /screenshots/07-name-api-gateway.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mskozlova/ydb_serverless_telegram_bot/HEAD/screenshots/07-name-api-gateway.png -------------------------------------------------------------------------------- /screenshots/08-create-function.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mskozlova/ydb_serverless_telegram_bot/HEAD/screenshots/08-create-function.png -------------------------------------------------------------------------------- /screenshots/10-copy-function-id.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mskozlova/ydb_serverless_telegram_bot/HEAD/screenshots/10-copy-function-id.png -------------------------------------------------------------------------------- /screenshots/12-api-gateway-logs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mskozlova/ydb_serverless_telegram_bot/HEAD/screenshots/12-api-gateway-logs.png -------------------------------------------------------------------------------- /screenshots/15-set-bot-commands.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mskozlova/ydb_serverless_telegram_bot/HEAD/screenshots/15-set-bot-commands.png -------------------------------------------------------------------------------- /screenshots/06-create-api-gateway.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mskozlova/ydb_serverless_telegram_bot/HEAD/screenshots/06-create-api-gateway.png -------------------------------------------------------------------------------- /screenshots/18-save-ydb-settings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mskozlova/ydb_serverless_telegram_bot/HEAD/screenshots/18-save-ydb-settings.png -------------------------------------------------------------------------------- /screenshots/19-create-ydb-tables.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mskozlova/ydb_serverless_telegram_bot/HEAD/screenshots/19-create-ydb-tables.png -------------------------------------------------------------------------------- /screenshots/23-ydb-after-register.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mskozlova/ydb_serverless_telegram_bot/HEAD/screenshots/23-ydb-after-register.png -------------------------------------------------------------------------------- /screenshots/03-newly-created-folder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mskozlova/ydb_serverless_telegram_bot/HEAD/screenshots/03-newly-created-folder.png -------------------------------------------------------------------------------- /screenshots/05-create-telegram-bot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mskozlova/ydb_serverless_telegram_bot/HEAD/screenshots/05-create-telegram-bot.png -------------------------------------------------------------------------------- /screenshots/08-1-select-environment.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mskozlova/ydb_serverless_telegram_bot/HEAD/screenshots/08-1-select-environment.png -------------------------------------------------------------------------------- /screenshots/08-make-function-public.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mskozlova/ydb_serverless_telegram_bot/HEAD/screenshots/08-make-function-public.png -------------------------------------------------------------------------------- /screenshots/14-create-ydb-database.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mskozlova/ydb_serverless_telegram_bot/HEAD/screenshots/14-create-ydb-database.png -------------------------------------------------------------------------------- /screenshots/17-create-ydb-database.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mskozlova/ydb_serverless_telegram_bot/HEAD/screenshots/17-create-ydb-database.png -------------------------------------------------------------------------------- /screenshots/04-create-service-account.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mskozlova/ydb_serverless_telegram_bot/HEAD/screenshots/04-create-service-account.png -------------------------------------------------------------------------------- /screenshots/16-create-function-version-gui.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mskozlova/ydb_serverless_telegram_bot/HEAD/screenshots/16-create-function-version-gui.png -------------------------------------------------------------------------------- /screenshots/09-create-default-function-version.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mskozlova/ydb_serverless_telegram_bot/HEAD/screenshots/09-create-default-function-version.png -------------------------------------------------------------------------------- /screenshots/11-connect-api-gateway-to-fucntion.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mskozlova/ydb_serverless_telegram_bot/HEAD/screenshots/11-connect-api-gateway-to-fucntion.png -------------------------------------------------------------------------------- /database/ydb_settings.py: -------------------------------------------------------------------------------- 1 | import ydb 2 | 3 | 4 | def get_ydb_pool(ydb_endpoint, ydb_database, timeout=30): 5 | ydb_driver_config = ydb.DriverConfig( 6 | ydb_endpoint, 7 | ydb_database, 8 | credentials=ydb.credentials_from_env_variables(), 9 | root_certificates=ydb.load_ydb_root_certificate(), 10 | ) 11 | 12 | ydb_driver = ydb.Driver(ydb_driver_config) 13 | ydb_driver.wait(fail_fast=True, timeout=timeout) 14 | return ydb.SessionPool(ydb_driver) 15 | -------------------------------------------------------------------------------- /bot/keyboards.py: -------------------------------------------------------------------------------- 1 | from telebot import types 2 | 3 | EMPTY = types.ReplyKeyboardRemove() 4 | 5 | 6 | def get_reply_keyboard(options, additional=None, **kwargs): 7 | row_width = kwargs.get("row_width", len(options)) 8 | 9 | markup = types.ReplyKeyboardMarkup( 10 | row_width=row_width, 11 | resize_keyboard=True, 12 | one_time_keyboard=True, 13 | ) 14 | markup.add(*options, row_width=row_width) 15 | if additional: 16 | markup.add(*additional, row_width=len(additional)) 17 | 18 | return markup 19 | -------------------------------------------------------------------------------- /tests/fixtures.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import pytest 4 | from pyrogram import Client 5 | 6 | api_id = os.environ.get("TELEGRAM_API_ID") 7 | api_hash = os.environ.get("TELEGRAM_API_HASH") 8 | client_name = "languagecardsbottester" 9 | workdir = "/Users/mariakozlova/ml_and_staff/language_cards_bot/tests" 10 | 11 | 12 | @pytest.fixture 13 | def test_client(): 14 | client = Client(client_name, api_id, api_hash, workdir=workdir) 15 | client.start() 16 | yield client 17 | client.stop() 18 | 19 | 20 | @pytest.fixture 21 | def chat_id(): 22 | return "@language_cards_tester_bot" 23 | -------------------------------------------------------------------------------- /index.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import telebot 4 | 5 | from bot.structure import create_bot 6 | from database.ydb_settings import get_ydb_pool 7 | from logs import logger 8 | 9 | YDB_ENDPOINT = os.getenv("YDB_ENDPOINT") 10 | YDB_DATABASE = os.getenv("YDB_DATABASE") 11 | BOT_TOKEN = os.getenv("BOT_TOKEN") 12 | 13 | 14 | def handler(event, _): 15 | logger.debug(f"New event: {event}") 16 | 17 | pool = get_ydb_pool(YDB_ENDPOINT, YDB_DATABASE) 18 | bot = create_bot(BOT_TOKEN, pool) 19 | 20 | message = telebot.types.Update.de_json(event["body"]) 21 | bot.process_new_updates([message]) 22 | return { 23 | "statusCode": 200, 24 | "body": "!", 25 | } 26 | -------------------------------------------------------------------------------- /create_function_version.sh: -------------------------------------------------------------------------------- 1 | export function_id="" 2 | export service_account_id="" 3 | export ydb_database="" 4 | export ydb_endpoint="" 5 | export bot_token="" 6 | 7 | zip code *.py *.md *.txt database/* user_interaction/* bot/* tests/*.py && 8 | yc serverless function version create \ 9 | --function-id="$function_id" \ 10 | --runtime python311 \ 11 | --entrypoint index.handler \ 12 | --memory 128m \ 13 | --execution-timeout 40s \ 14 | --source-path code.zip \ 15 | --service-account-id="$service_account_id" \ 16 | --environment YDB_DATABASE="$ydb_database" \ 17 | --environment YDB_ENDPOINT="$ydb_endpoint" \ 18 | --environment BOT_TOKEN="$bot_token" 19 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 mskozlova 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /database/utils.py: -------------------------------------------------------------------------------- 1 | import ydb 2 | 3 | 4 | def _format_kwargs(kwargs): 5 | return {"${}".format(key): value for key, value in kwargs.items()} 6 | 7 | 8 | # using prepared statements 9 | # https://ydb.tech/en/docs/reference/ydb-sdk/example/python/#param-prepared-queries 10 | def execute_update_query(pool, query, **kwargs): 11 | def callee(session): 12 | prepared_query = session.prepare(query) 13 | session.transaction(ydb.SerializableReadWrite()).execute( 14 | prepared_query, _format_kwargs(kwargs), commit_tx=True 15 | ) 16 | 17 | return pool.retry_operation_sync(callee) 18 | 19 | 20 | # using prepared statements 21 | # https://ydb.tech/en/docs/reference/ydb-sdk/example/python/#param-prepared-queries 22 | def execute_select_query(pool, query, **kwargs): 23 | def callee(session): 24 | prepared_query = session.prepare(query) 25 | result_sets = session.transaction(ydb.SerializableReadWrite()).execute( 26 | prepared_query, _format_kwargs(kwargs), commit_tx=True 27 | ) 28 | return result_sets[0].rows 29 | 30 | return pool.retry_operation_sync(callee) 31 | -------------------------------------------------------------------------------- /user_interaction/texts.py: -------------------------------------------------------------------------------- 1 | START = ( 2 | "Hello! This is a simple bot that can store your name and age, " 3 | "show them back to you and delete them if requested.\n\n" 4 | "List of commands:\n" 5 | "/start\n" 6 | "/register\n" 7 | "/show_data\n" 8 | "/delete_account" 9 | ) 10 | 11 | FIRST_NAME = "Enter your first name." 12 | LAST_NAME = "Enter your last name." 13 | AGE = "Enter your age." 14 | AGE_IS_NOT_NUMBER = "Age should be a positive number, try again." 15 | 16 | SHOW_DATA = "First name: {}\nLast name: {}\nAge: {}" 17 | 18 | DATA_IS_SAVED = "Your data is saved!\n" + SHOW_DATA 19 | ALREADY_REGISTERED = "You are already registered!\n" + SHOW_DATA 20 | SHOW_DATA_WITH_PREFIX = "Your data:\n" + SHOW_DATA 21 | 22 | NOT_REGISTERED = "You are not registered yet, try /register." 23 | 24 | CANCEL_REGISTER = "Cancelled! Your data is not saved." 25 | 26 | DELETE_ACCOUNT = "Are you sure you want to delete your account?" 27 | DELETE_ACCOUNT_OPTIONS = {"Yes!": True, "No..": False} 28 | DELETE_ACCOUNT_UNKNOWN = "I don't understand this command." 29 | DELETE_ACCOUNT_DONE = "Done! You can /register again." 30 | DELETE_ACCOUNT_CANCEL = "Ok, stay for longer!" 31 | -------------------------------------------------------------------------------- /database/queries.py: -------------------------------------------------------------------------------- 1 | USERS_INFO_TABLE_PATH = "user_personal_info" 2 | STATES_TABLE_PATH = "states" 3 | 4 | 5 | get_user_state = f""" 6 | DECLARE $user_id AS Uint64; 7 | 8 | SELECT state 9 | FROM `{STATES_TABLE_PATH}` 10 | WHERE user_id == $user_id; 11 | """ 12 | 13 | set_user_state = f""" 14 | DECLARE $user_id AS Uint64; 15 | DECLARE $state AS Utf8?; 16 | 17 | UPSERT INTO `{STATES_TABLE_PATH}` (`user_id`, `state`) 18 | VALUES ($user_id, $state); 19 | """ 20 | 21 | get_user_info = f""" 22 | DECLARE $user_id AS Int64; 23 | 24 | SELECT 25 | user_id, 26 | age, 27 | first_name, 28 | last_name 29 | FROM `{USERS_INFO_TABLE_PATH}` 30 | WHERE user_id == $user_id; 31 | """ 32 | 33 | add_user_info = f""" 34 | DECLARE $user_id AS Uint64; 35 | DECLARE $first_name AS Utf8; 36 | DECLARE $last_name AS Utf8; 37 | DECLARE $age AS Uint64; 38 | 39 | INSERT INTO `{USERS_INFO_TABLE_PATH}` (user_id, first_name, last_name, age) 40 | VALUES ($user_id, $first_name, $last_name, $age); 41 | """ 42 | 43 | delete_user_info = f""" 44 | DECLARE $user_id AS Uint64; 45 | 46 | DELETE FROM `{USERS_INFO_TABLE_PATH}` 47 | WHERE user_id == $user_id; 48 | 49 | DELETE FROM `{STATES_TABLE_PATH}` 50 | WHERE user_id == $user_id; 51 | """ 52 | -------------------------------------------------------------------------------- /database/model.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | from database import queries 4 | from database.utils import execute_select_query, execute_update_query 5 | 6 | 7 | def get_state(pool, user_id): 8 | results = execute_select_query(pool, queries.get_user_state, user_id=user_id) 9 | if len(results) == 0: 10 | return None 11 | if results[0]["state"] is None: 12 | return None 13 | return json.loads(results[0]["state"]) 14 | 15 | 16 | def set_state(pool, user_id, state): 17 | execute_update_query( 18 | pool, queries.set_user_state, user_id=user_id, state=json.dumps(state) 19 | ) 20 | 21 | 22 | def clear_state(pool, user_id): 23 | execute_update_query(pool, queries.set_user_state, user_id=user_id, state=None) 24 | 25 | 26 | def add_user_info(pool, user_id, first_name, last_name, age): 27 | execute_update_query( 28 | pool, 29 | queries.add_user_info, 30 | user_id=user_id, 31 | first_name=first_name, 32 | last_name=last_name, 33 | age=age, 34 | ) 35 | 36 | 37 | def get_user_info(pool, user_id): 38 | result = execute_select_query(pool, queries.get_user_info, user_id=user_id) 39 | 40 | if len(result) != 1: 41 | return None 42 | return result[0] 43 | 44 | 45 | def delete_user_info(pool, user_id): 46 | execute_update_query(pool, queries.delete_user_info, user_id=user_id) 47 | -------------------------------------------------------------------------------- /bot/structure.py: -------------------------------------------------------------------------------- 1 | from functools import partial 2 | 3 | from telebot import TeleBot, custom_filters 4 | 5 | from bot import handlers as handlers 6 | from bot import states as bot_states 7 | 8 | # import tests.handlers as test_handlers 9 | 10 | 11 | class Handler: 12 | def __init__(self, callback, **kwargs): 13 | self.callback = callback 14 | self.kwargs = kwargs 15 | 16 | 17 | def get_start_handlers(): 18 | return [ 19 | Handler(callback=handlers.handle_start, commands=["start"]), 20 | ] 21 | 22 | 23 | def get_registration_handlers(): 24 | return [ 25 | Handler(callback=handlers.handle_register, commands=["register"]), 26 | Handler( 27 | callback=handlers.handle_cancel_registration, 28 | commands=["cancel"], 29 | state=[ 30 | bot_states.RegisterState.first_name, 31 | bot_states.RegisterState.last_name, 32 | bot_states.RegisterState.age, 33 | ], 34 | ), 35 | Handler( 36 | callback=handlers.handle_get_first_name, 37 | state=bot_states.RegisterState.first_name, 38 | ), 39 | Handler( 40 | callback=handlers.handle_get_last_name, 41 | state=bot_states.RegisterState.last_name, 42 | ), 43 | Handler(callback=handlers.handle_get_age, state=bot_states.RegisterState.age), 44 | ] 45 | 46 | 47 | def get_show_data_handlers(): 48 | return [ 49 | Handler(callback=handlers.handle_show_data, commands=["show_data"]), 50 | ] 51 | 52 | 53 | def get_delete_account_handlers(): 54 | return [ 55 | Handler(callback=handlers.handle_delete_account, commands=["delete_account"]), 56 | Handler( 57 | callback=handlers.handle_finish_delete_account, 58 | state=bot_states.DeleteAccountState.are_you_sure, 59 | ), 60 | ] 61 | 62 | 63 | def create_bot(bot_token, pool): 64 | state_storage = bot_states.StateYDBStorage(pool) 65 | bot = TeleBot(bot_token, state_storage=state_storage) 66 | 67 | handlers = [] 68 | handlers.extend(get_start_handlers()) 69 | handlers.extend(get_registration_handlers()) 70 | handlers.extend(get_show_data_handlers()) 71 | handlers.extend(get_delete_account_handlers()) 72 | 73 | for handler in handlers: 74 | bot.register_message_handler( 75 | partial(handler.callback, pool=pool), **handler.kwargs, pass_bot=True 76 | ) 77 | 78 | bot.add_custom_filter(custom_filters.StateFilter(bot)) 79 | return bot 80 | -------------------------------------------------------------------------------- /logs.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import traceback 3 | 4 | from pythonjsonlogger import jsonlogger 5 | from telebot.types import Message 6 | 7 | 8 | # https://cloud.yandex.com/en/docs/functions/operations/function/logs-write#function-examples 9 | class YcLoggingFormatter(jsonlogger.JsonFormatter): 10 | def add_fields(self, log_record, record, message_dict): 11 | super(YcLoggingFormatter, self).add_fields(log_record, record, message_dict) 12 | log_record["logger"] = record.name 13 | log_record["level"] = str.replace( 14 | str.replace(record.levelname, "WARNING", "WARN"), "CRITICAL", "FATAL" 15 | ) 16 | 17 | 18 | logHandler = logging.StreamHandler() 19 | logHandler.setFormatter(YcLoggingFormatter("%(message)s %(level)s %(logger)s")) 20 | 21 | logger = logging.getLogger("logger") 22 | logger.addHandler(logHandler) 23 | logger.setLevel(logging.DEBUG) 24 | 25 | 26 | def find_in_args(args, target_type): 27 | for arg in args: 28 | if isinstance(arg, target_type): 29 | return arg 30 | 31 | 32 | def find_in_kwargs(kwargs, target_type): 33 | return find_in_args(kwargs.values(), target_type) 34 | 35 | 36 | def get_message_info(*args, **kwargs): 37 | message_args = find_in_args(args, Message) 38 | if message_args is not None: 39 | return message_args.chat.id, message_args.text 40 | 41 | message_kwargs = find_in_kwargs(kwargs, Message) 42 | if message_kwargs is not None: 43 | return message_kwargs.chat.id, message_kwargs.text 44 | 45 | return "UNKNOWN", "UNKNOWN" 46 | 47 | 48 | def logged_execution(func): 49 | def wrapper(*args, **kwargs): 50 | chat_id, text = get_message_info(*args, **kwargs) 51 | 52 | logger.info( 53 | f"[LOG] Starting {func.__name__} - chat_id {chat_id}", 54 | extra={ 55 | "text": text, 56 | "arg": str(args), 57 | "kwarg": str(kwargs), 58 | }, 59 | ) 60 | try: 61 | func(*args, **kwargs) 62 | logger.info( 63 | f"[LOG] Finished {func.__name__} - chat_id {chat_id}", 64 | extra={ 65 | "text": text, 66 | "arg": str(args), 67 | "kwarg": str(kwargs), 68 | }, 69 | ) 70 | except Exception as e: 71 | logger.error( 72 | f"[LOG] Failed {func.__name__} - chat_id {chat_id} - exception {e}", 73 | extra={ 74 | "text": text, 75 | "arg": str(args), 76 | "kwarg": str(kwargs), 77 | "error": e, 78 | "traceback": traceback.format_exc(), 79 | }, 80 | ) 81 | 82 | return wrapper 83 | -------------------------------------------------------------------------------- /bot/states.py: -------------------------------------------------------------------------------- 1 | from telebot.handler_backends import State, StatesGroup 2 | from telebot.storage.base_storage import StateContext, StateStorageBase 3 | 4 | from database import model as db_model 5 | 6 | 7 | # based on Telebot example 8 | # https://github.com/eternnoir/pyTelegramBotAPI/blob/0f52ca688ffb7af6176d2f73fca92335dc3560eb/telebot/storage/redis_storage.py 9 | class StateYDBStorage(StateStorageBase): 10 | """ 11 | This class is for YDB storage to be used by the bot to track user states. 12 | """ 13 | 14 | def __init__(self, ydb_pool): 15 | super().__init__() 16 | self.pool = ydb_pool 17 | 18 | def set_data(self, chat_id, user_id, key, value): 19 | """ 20 | Set data for a user in a particular chat. 21 | """ 22 | if db_model.get_state(self.pool, user_id) is None: 23 | return False 24 | 25 | full_state = db_model.get_state(self.pool, user_id) 26 | full_state["data"][key] = value 27 | 28 | db_model.set_state(self.pool, user_id, full_state) 29 | return True 30 | 31 | def get_data(self, chat_id, user_id): 32 | """ 33 | Get data for a user in a particular chat. 34 | """ 35 | full_state = db_model.get_state(self.pool, user_id) 36 | if full_state: 37 | return full_state.get("data", {}) 38 | 39 | return {} 40 | 41 | def set_state(self, chat_id, user_id, state): 42 | if hasattr(state, "name"): 43 | state = state.name 44 | 45 | data = self.get_data(chat_id, user_id) 46 | full_state = {"state": state, "data": data} 47 | db_model.set_state(self.pool, user_id, full_state) 48 | return True 49 | 50 | def delete_state(self, chat_id, user_id): 51 | """ 52 | Delete state for a particular user. 53 | """ 54 | if db_model.get_state(self.pool, user_id) is None: 55 | return False 56 | 57 | db_model.clear_state(self.pool, user_id) 58 | return True 59 | 60 | def reset_data(self, chat_id, user_id): 61 | """ 62 | Reset data for a particular user in a chat. 63 | """ 64 | full_state = db_model.get_state(self.pool, user_id) 65 | if full_state: 66 | full_state["data"] = {} 67 | db_model.set_state(self.pool, user_id, full_state) 68 | return True 69 | return False 70 | 71 | def get_state(self, chat_id, user_id): 72 | states = db_model.get_state(self.pool, user_id) 73 | if states is None: 74 | return None 75 | return states.get("state") 76 | 77 | def get_interactive_data(self, chat_id, user_id): 78 | return StateContext(self, chat_id, user_id) 79 | 80 | def save(self, chat_id, user_id, data): 81 | full_state = db_model.get_state(self.pool, user_id) 82 | if full_state: 83 | full_state["data"] = data 84 | db_model.set_state(self.pool, user_id, full_state) 85 | return True 86 | 87 | 88 | class RegisterState(StatesGroup): 89 | first_name = State() 90 | last_name = State() 91 | age = State() 92 | 93 | 94 | class DeleteAccountState(StatesGroup): 95 | are_you_sure = State() 96 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # poetry 98 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 99 | # This is especially recommended for binary packages to ensure reproducibility, and is more 100 | # commonly ignored for libraries. 101 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 102 | #poetry.lock 103 | 104 | # pdm 105 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 106 | #pdm.lock 107 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 108 | # in version control. 109 | # https://pdm.fming.dev/#use-with-ide 110 | .pdm.toml 111 | 112 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 113 | __pypackages__/ 114 | 115 | # Celery stuff 116 | celerybeat-schedule 117 | celerybeat.pid 118 | 119 | # SageMath parsed files 120 | *.sage.py 121 | 122 | # Environments 123 | .env 124 | .venv 125 | env/ 126 | venv/ 127 | ENV/ 128 | env.bak/ 129 | venv.bak/ 130 | 131 | # Spyder project settings 132 | .spyderproject 133 | .spyproject 134 | 135 | # Rope project settings 136 | .ropeproject 137 | 138 | # mkdocs documentation 139 | /site 140 | 141 | # mypy 142 | .mypy_cache/ 143 | .dmypy.json 144 | dmypy.json 145 | 146 | # Pyre type checker 147 | .pyre/ 148 | 149 | # pytype static type analyzer 150 | .pytype/ 151 | 152 | # Cython debug symbols 153 | cython_debug/ 154 | 155 | # PyCharm 156 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 157 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 158 | # and can be added to the global gitignore or merged into this file. For a more nuclear 159 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 160 | #.idea/ 161 | 162 | # custom 163 | code.zip 164 | .DS_Store 165 | create_function_version_local.sh 166 | -------------------------------------------------------------------------------- /tests/utils.py: -------------------------------------------------------------------------------- 1 | from time import sleep 2 | 3 | 4 | class CommandContext: 5 | def __init__(self, client, chat_id, command): 6 | self.client = client 7 | self.chat_id = chat_id 8 | self.command = command 9 | self.step = 1 10 | 11 | def __enter__(self): 12 | self.message = self.client.send_message(chat_id=self.chat_id, text=self.command) 13 | return self 14 | 15 | def __exit__(self, exc_type, exc_val, exc_tb): 16 | return 17 | 18 | def expect_next(self, correct_response, sleep_s=0.2, timeout_s=60): 19 | assert correct_response is not None, "correct_response should be specified" 20 | 21 | timer = 0 22 | while timer <= timeout_s: 23 | response = self.client.get_messages( 24 | self.chat_id, self.message.id + self.step 25 | ).text 26 | if response is not None: # found target message 27 | self.step += 1 28 | assert response == correct_response, ( 29 | f"'{self.command}' failed due to wrong reaction on step {self.step - 1}" 30 | f"\nreaction: {response}\nexpected: {correct_response}" 31 | ) 32 | return 33 | 34 | timer += sleep_s 35 | sleep(sleep_s) 36 | 37 | def expect_next_prefix(self, correct_response_prefix, sleep_s=0.2, timeout_s=60): 38 | assert ( 39 | correct_response_prefix is not None 40 | ), "correct_response should be specified" 41 | 42 | timer = 0 43 | while timer <= timeout_s: 44 | response = self.client.get_messages( 45 | self.chat_id, self.message.id + self.step 46 | ).text 47 | if response is not None: # found target message 48 | self.step += 1 49 | assert response.startswith(correct_response_prefix), ( 50 | f"'{self.command}' failed due to wrong reaction prefix on step {self.step - 1}" 51 | f"\nreaction: {response}\nexpected: {correct_response_prefix}" 52 | ) 53 | return 54 | 55 | timer += sleep_s 56 | sleep(sleep_s) 57 | 58 | def expect_none(self, sleep_s=0.5, timeout_s=2): 59 | timer = 0 60 | while timer <= timeout_s: 61 | response = self.client.get_messages( 62 | self.chat_id, self.message.id + self.step 63 | ).text 64 | assert response is None, ( 65 | f"'{self.command}' failed due to presence of reaction on step {self.step}" 66 | f"\nreaction: {response}" 67 | ) 68 | 69 | timer += sleep_s 70 | sleep(sleep_s) 71 | 72 | def expect_length(self, num_rows, sleep_s=0.5, timeout_s=60): 73 | raise NotImplementedError 74 | 75 | def expect_any(self, sleep_s=0.2, timeout_s=60): 76 | timer = 0 77 | while timer <= timeout_s: 78 | response = self.client.get_messages( 79 | self.chat_id, self.message.id + self.step 80 | ).text 81 | if response is not None: # found target message 82 | self.step += 1 83 | break 84 | 85 | timer += sleep_s 86 | sleep(sleep_s) 87 | 88 | assert ( 89 | response is not None 90 | ), f"'{self.command}' failed due to absence of reaction on step {self.step - 1}" 91 | 92 | def expect_any_multiple(self, number_of_responses, sleep_s=0.2, timeout_s=60): 93 | for i in range(number_of_responses): 94 | self.expect_any(sleep_s=sleep_s, timeout_s=timeout_s) 95 | 96 | def expect_next_number_of_rows(self, n_rows, sleep_s=0.2, timeout_s=60): 97 | timer = 0 98 | while timer <= timeout_s: 99 | response = self.client.get_messages( 100 | self.chat_id, self.message.id + self.step 101 | ).text 102 | if response is not None: # found target message 103 | self.step += 1 104 | assert len(response.split("\n")) == n_rows, ( 105 | f"'{self.command}' failed due to wrong number of rows on step {self.step - 1}" 106 | f"\nreaction: {response}\nexpected number of rows: {n_rows}" 107 | ) 108 | return 109 | 110 | timer += sleep_s 111 | sleep(sleep_s) 112 | -------------------------------------------------------------------------------- /bot/handlers.py: -------------------------------------------------------------------------------- 1 | from bot import keyboards, states 2 | from database import model as db_model 3 | from logs import logged_execution 4 | from user_interaction import texts 5 | 6 | 7 | @logged_execution 8 | def handle_start(message, bot, pool): 9 | bot.send_message(message.chat.id, texts.START, reply_markup=keyboards.EMPTY) 10 | 11 | 12 | @logged_execution 13 | def handle_register(message, bot, pool): 14 | current_data = db_model.get_user_info(pool, message.from_user.id) 15 | 16 | if current_data: 17 | bot.send_message( 18 | message.chat.id, 19 | texts.ALREADY_REGISTERED.format( 20 | current_data["first_name"], 21 | current_data["last_name"], 22 | current_data["age"], 23 | ), 24 | reply_markup=keyboards.EMPTY, 25 | ) 26 | return 27 | 28 | bot.send_message( 29 | message.chat.id, 30 | texts.FIRST_NAME, 31 | reply_markup=keyboards.get_reply_keyboard(["/cancel"]), 32 | ) 33 | bot.set_state( 34 | message.from_user.id, states.RegisterState.first_name, message.chat.id 35 | ) 36 | 37 | 38 | @logged_execution 39 | def handle_cancel_registration(message, bot, pool): 40 | bot.delete_state(message.from_user.id, message.chat.id) 41 | bot.send_message( 42 | message.chat.id, 43 | texts.CANCEL_REGISTER, 44 | reply_markup=keyboards.EMPTY, 45 | ) 46 | 47 | 48 | @logged_execution 49 | def handle_get_first_name(message, bot, pool): 50 | with bot.retrieve_data(message.from_user.id, message.chat.id) as data: 51 | data["first_name"] = message.text 52 | bot.set_state(message.from_user.id, states.RegisterState.last_name, message.chat.id) 53 | bot.send_message( 54 | message.chat.id, 55 | texts.LAST_NAME, 56 | reply_markup=keyboards.get_reply_keyboard(["/cancel"]), 57 | ) 58 | 59 | 60 | @logged_execution 61 | def handle_get_last_name(message, bot, pool): 62 | with bot.retrieve_data(message.from_user.id, message.chat.id) as data: 63 | data["last_name"] = message.text 64 | bot.set_state(message.from_user.id, states.RegisterState.age, message.chat.id) 65 | bot.send_message( 66 | message.chat.id, 67 | texts.AGE, 68 | reply_markup=keyboards.get_reply_keyboard(["/cancel"]), 69 | ) 70 | 71 | 72 | @logged_execution 73 | def handle_get_age(message, bot, pool): 74 | if not message.text.isdigit(): 75 | bot.send_message( 76 | message.chat.id, 77 | texts.AGE_IS_NOT_NUMBER, 78 | reply_markup=keyboards.EMPTY, 79 | ) 80 | return 81 | 82 | with bot.retrieve_data(message.from_user.id, message.chat.id) as data: 83 | first_name = data["first_name"] 84 | last_name = data["last_name"] 85 | age = int(message.text) 86 | 87 | bot.delete_state(message.from_user.id, message.chat.id) 88 | db_model.add_user_info(pool, message.from_user.id, first_name, last_name, age) 89 | 90 | bot.send_message( 91 | message.chat.id, 92 | texts.DATA_IS_SAVED.format(first_name, last_name, age), 93 | reply_markup=keyboards.EMPTY, 94 | ) 95 | 96 | 97 | @logged_execution 98 | def handle_show_data(message, bot, pool): 99 | current_data = db_model.get_user_info(pool, message.from_user.id) 100 | 101 | if not current_data: 102 | bot.send_message( 103 | message.chat.id, texts.NOT_REGISTERED, reply_markup=keyboards.EMPTY 104 | ) 105 | return 106 | 107 | bot.send_message( 108 | message.chat.id, 109 | texts.SHOW_DATA_WITH_PREFIX.format( 110 | current_data["first_name"], current_data["last_name"], current_data["age"] 111 | ), 112 | reply_markup=keyboards.EMPTY, 113 | ) 114 | 115 | 116 | @logged_execution 117 | def handle_delete_account(message, bot, pool): 118 | current_data = db_model.get_user_info(pool, message.from_user.id) 119 | if not current_data: 120 | bot.send_message( 121 | message.chat.id, texts.NOT_REGISTERED, reply_markup=keyboards.EMPTY 122 | ) 123 | return 124 | 125 | bot.send_message( 126 | message.chat.id, 127 | texts.DELETE_ACCOUNT, 128 | reply_markup=keyboards.get_reply_keyboard(texts.DELETE_ACCOUNT_OPTIONS), 129 | ) 130 | bot.set_state( 131 | message.from_user.id, states.DeleteAccountState.are_you_sure, message.chat.id 132 | ) 133 | 134 | 135 | @logged_execution 136 | def handle_finish_delete_account(message, bot, pool): 137 | bot.delete_state(message.from_user.id, message.chat.id) 138 | 139 | if message.text not in texts.DELETE_ACCOUNT_OPTIONS: 140 | bot.send_message( 141 | message.chat.id, 142 | texts.DELETE_ACCOUNT_UNKNOWN, 143 | reply_markup=keyboards.EMPTY, 144 | ) 145 | return 146 | 147 | if texts.DELETE_ACCOUNT_OPTIONS[message.text]: 148 | db_model.delete_user_info(pool, message.from_user.id) 149 | bot.send_message( 150 | message.chat.id, 151 | texts.DELETE_ACCOUNT_DONE, 152 | reply_markup=keyboards.EMPTY, 153 | ) 154 | else: 155 | bot.send_message( 156 | message.chat.id, 157 | texts.DELETE_ACCOUNT_CANCEL, 158 | reply_markup=keyboards.EMPTY, 159 | ) 160 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # About 2 | This is a simple example of a telegram bot implementation. This code is designed to be run on [Yandex Cloud Serverless Function](https://cloud.yandex.com/en/docs/functions/quickstart/?from=int-console-help-center-or-nav) connected to [YDB database](https://cloud.yandex.com/en/docs/ydb/quickstart?from=int-console-help-center-or-nav) using [TeleBot (pyTelegramBotAPI)](https://pytba.readthedocs.io/en/latest/index.html) python3 package. 3 | 4 | ## Advantages 5 | 6 | This repository can be used as a template for creating more complicated bots. 7 | 8 | This implementation supports: 9 | - full logging adapted to Yandex Cloud Functions 10 | - handling user's states, which allows to conveniently process each text input in appropriate context and make complicated logics manageable 11 | - handling a variety of Reply Keyboards and simple text inputs 12 | - testing the bot (TBD) 13 | 14 | ## What does the bot do 15 | List of the bot's functions: 16 | - asks for the user's first name, last name and age step-by-step 17 | - checks correctness of the input data (age) 18 | - saves the info into the database (i.e. 'registers' the user) 19 | - shows the info back when required 20 | - supports deleting the database entry (i.e. 'deletes the account') 21 | 22 | You can check out the instance of this bot [here](https://t.me/ydb_serverless_example_bot). 23 | 24 | # How to set up an instance of the bot 25 | 26 | ## Creating Yandex Cloud function 27 | 28 | 1) Visit [Yandex Cloud page](https://cloud.yandex.com/) and click `Console` in upper right corner. Login into Yandex ID, or create an account. 29 | 2) In Yandex Cloud console set up Yandex Cloud billing account, if you don't have one. **No payments will be needed to complete this instruction.** 30 | 3) In Yandex Console create a folder for your resources. Choose any name.
Screenshot 31 | ![Yandex Console Screenshot](screenshots/01-create-folder.png?raw=true "Title")
32 | 4) Create a service account with any name and assign it the `editor` and the `serverless.functions.invoker` roles for your folder.
Screenshot 33 | ![Yandex Console Screenshot](screenshots/04-create-service-account.png?raw=true "Title")
34 | 5) Create an API gateway with any name and the default specification.
Screenshot 35 | ![Yandex Console Screenshot](screenshots/06-create-api-gateway.png?raw=true "Title")
36 | 6) Create a Serverless Function with Python3.11 environment. Choose any name. In the Editor tab create a first default version, in the Overview tab make it public.
ScreenshotsCreate a function 37 | ![Yandex Console Screenshot](screenshots/08-create-function.png?raw=true "Title") Select the environment ![Yandex Console Screenshot](screenshots/08-1-select-environment.png?raw=true "Title") Create a default version ![Yandex Console Screenshot](screenshots/09-create-default-function-version.png?raw=true "Title") Make the function public ![Yandex Console Screenshot](screenshots/08-make-function-public.png?raw=true "Title")
38 | 7) Copy your function ID and save for the next step.
Screenshot 39 | ![Yandex Console Screenshot](screenshots/10-copy-function-id.png?raw=true "Title")
40 | 8) Create a link between the API gateway and the Function - edit the API gateway specification and add the following code in the end, replacing `` with value copied during the last step. Pay attention to the indentation - it should be exactly as in this snippet: 41 | ``` 42 | /fshtb-function: 43 | post: 44 | x-yc-apigateway-integration: 45 | type: cloud_functions 46 | function_id: 47 | operationId: fshtb-function 48 | ``` 49 | 50 | ## Creating a bot and linking it with the function 51 | 1) Create a telegram bot by sending `/newbot` command for BotFather in Telegram. Give it a name and a login, then receive a token for your bot.
Screenshot 52 |
53 | 2) (optional) Set up bot commands to create a menu. Send `/setcommands` to `BotFather`, choose your bot from the list and sent the following list of commands. This list will appear when clicking on the button in the bottom left corner of the bot chat.
Commands
 54 |   start - show welcome message and bot description
 55 |   register - store your name and age in the database
 56 |   cancel - stop registering process
 57 |   show_data - show your name and age stored in the database
 58 |   delete_account - delete your info from the database
 59 |   
60 |
61 | 62 | 3) Create a link between the telegram bot and the function. Run the following request from terminal, replacing `` with the token from BotFather and `` with `Default domain` value from Overview tab of your API gateway. All went well if you received response `{"ok":true,"result":true,"description":"Webhook was set"}`. 63 | -
Request 64 | 65 | ``` 66 | curl \ 67 | --request POST \ 68 | --url https://api.telegram.org/bot/setWebhook \ 69 | --header 'content-type: application/json' \ 70 | --data '{"url": "/fshtb-function"}' 71 | ``` 72 | 73 |
74 | 75 | -
Request for Windows 76 | 77 | ``` 78 | curl --request POST --url https://api.telegram.org/bot/setWebhook --header "content-type:application/json" --data "{\"url\": \"/fshtb-function\"}" 79 | ``` 80 | 81 |
82 |
83 | 84 | At this stage sending `/start` to your bot should lead to successful POST requests from API gateway and successful Function invocations, which you can track on their respective Logs tabs. 85 |
Successful API gateway logs 86 | 87 | ![Yandex Console Screenshot](screenshots/12-api-gateway-logs.png?raw=true "Title") 88 |
89 |
Successful function logs 90 | 91 | ![Yandex Console Screenshot](screenshots/13-function-logs.png?raw=true "Title") 92 |
93 |
94 | Note: the function does not do anything yet, except for waking up and going back to sleep. 95 | 96 | ## Creating a YDB database 97 | 1) Create a new serverless YDB database resource with any name in your folder.
ScreenshotsCreate YDB database resource ![Yandex Console Screenshot](screenshots/17-create-ydb-database.png?raw=true "Title") Give it any name ![Yandex Console Screenshot](screenshots/18-save-ydb-settings.png?raw=true "Title") 98 |
99 | 100 | 2) Go to Navigation tab of the new YDB database, click `New SQL query` and run the following request to create 2 necessary tables.
Screenshot 101 | ![Yandex Console Screenshot](screenshots/19-create-ydb-tables.png?raw=true "Title") 102 |
103 | 104 | -
SQL script 105 | 106 | ``` 107 | CREATE TABLE `user_personal_info` 108 | ( 109 | `user_id` Uint64, 110 | `last_name` Utf8, 111 | `first_name` Utf8, 112 | `age` Uint64, 113 | PRIMARY KEY (`user_id`) 114 | ); 115 | 116 | COMMIT; 117 | 118 | CREATE TABLE `states` 119 | ( 120 | `user_id` Uint64, 121 | `state` Utf8, 122 | PRIMARY KEY (`user_id`) 123 | ); 124 | ``` 125 | 126 |
127 | 128 |
129 | 130 | 131 | ## Make your bot do something 132 | 1) Download the code from this repository and in terminal go to the directory, which contains `index.py`. Create a ZIP archive with the directory contents `zip -r ../code.zip *`. The command will create an archive in the parent folder. 133 | 2) In Editor tab of function: 134 | - Choose the upload method `ZIP archive`. 135 | - Click `Attach file` and select the code archive. 136 | - Fill `Entrypoint` field with `index.handler`. 137 | - Select your service account. 138 | - Create 3 environment variables: `YDB_DATABASE`, `YDB_ENDPOINT`, `BOT_TOKEN`.
How to choose their values 139 | - `YDB_DATABASE` is a value from YDB database Overview tab: `Connection > Database`. 140 | - `YDB_ENDPOINT` is a value from YDB database Overview tab: `Connection > Endpoint`. 141 | - `BOT_TOKEN` is the token you received from BotFather after creating the new bot.
How it should look like in GUI - screenshot. 142 | ![Yandex Console Screenshot](screenshots/16-create-function-version-gui.png?raw=true "Title") 143 |
144 | 3) Click `Create version` and wait for it to be created. 145 | 146 |
147 | Alternatively, you can use command line interface to do that. 148 |
Create Function version using CLI 149 | 150 | 1) Download code from this repository. 151 | 2) Edit `create_function_version.sh` - fill the placeholders with your IDs and tokens to set up all the necessary version parameters. 152 | 3) Prepare Yandex Cloud command line interface - [instruction](https://cloud.yandex.com/en/docs/cli/quickstart). 153 | 4) Execute `create_function_version.sh` to create a ZIP archive with the code and create a new version of your function using Yandex Cloud CLI. 154 |
155 | 156 |
157 | Awesome! Now try your bot! 158 | 159 | ## What next? 160 | 1) Play around with the bot.
Bot command examples - screenshots`/start`

`/register`
161 |
162 | 163 | 2) Visit function's Logs tab to see logs for each input message an debug errors if something went wrong. Click the `eye` icon (`JSON` column) on each log to see additional details.
Function logs - screenshot 164 |
165 | 166 | 3. Check out the database tables' contents in YDB database Navigation tab.
YDB table - screenshot 167 |
168 | 169 | # Testing 170 | TBD.. 171 | --------------------------------------------------------------------------------