Какой способ залива на финансы самый лучший? Правильно — залив через витрины. Конечно, ничего стопроцентного в арбитраже нет, но витрина — как минимум один из наиболее конвертящих подходов для этой ниши.
А что будет, если сократить воронку и сделать витрину не на ленде, а сразу на трафик-сорсе? Что ж, в случае с Telegram такое возможно. О том, как сделать бот-витрину под залив на финансы, читайте ниже.
* Бот-витрина может быть использован не только для залива на финансы, но сегодня рассмотрим именно на примере finance-вертикали
Принцип работы бота-витрины для Telegram
Сразу предупреждаем: кода много. Но за это благодарите разработчиков API Телеги. С точки зрения алгоритма же бот получился очень простым. Авторизация также простая — через классический bot-api. Как говорится: «Без регистрации и СМС».
Однако есть нюанс: так как наш бот состоит из пользовательского и админского функционалов, то для удобства понимания алгоритма мы разобьем его описание на две части.
Алгоритм юзер-функционала
- Авторизация по bot-api.
- Считывание БД с актуальным содержимым меню.
- Вывод меню, состоящего из «Категорий», «Страниц», а также кнопок «Главное меню» и «Назад».
- Прослушивание эфира и реакция на нажатие клавиш в меню.
- Переход на более глубокую категорию при нажатии на кнопку типа «Категория».
- Переход на уровень вверх при нажатии на кнопку «Назад».
- Возврат в главное меню — уровень вверх при нажатии на кнопку «Главное меню».
- Вывод содержимого страницы при нажатии на кнопку типа «Страница».
Проведем аналогию: категории — это папки, страницы — это посты*. Технически это не так, но для понимания принципа работы опустим детали.
[*Посты, которые могут состоять из текста, одной картинки, одного видео, текста + одной картинки или текста + одного видео. Такое решение обусловлено спецификой витрин — маловероятно, что пост с МФО-оффером будет содержать аудио, геопозицию, контакты телефонной книги или иные типы вложений. Также маловероятно, что вам понадобится несколько вложений или вложения разных типов. А вот реализация таких возможностей увеличила бы и без того сложный для понимания новичком код еще раза в два-три. Но при желании этот функционал можно расширить. Если не получится самостоятельно — стучите в Телегу.]
Алгоритм юзер-функционала
- Весь юзер-функционал.
- Считывание БД с постоянным содержимым админ-меню.
- Проверка ID пользователя на принадлежность к администрации (если пишет «не админ» — админ-меню он не увидит).
- Вывод админ-меню, состоящего из «Редактировать содержимое страницы», «Переименовать», «Удалить», «Создать страницу», «Создать категорию».
- Прослушивание эфира и реакция на нажатие клавиш в админ-меню.
- Вывод запроса на название категории и ее создание при нажатии «Создать категорию».
- Вывод запроса на название страницы, ввод ее содержимого и ее создание при нажатии «Создать страницу».
- Вывод запроса на подтверждение удаления страницы/категории (вместе с содержимым) при нажатии «Удалить». В случае ввода «да» — удаление. В случае ввода иного текста — отмена удаления.
- Смена названия страницы/категории при нажатии «Переименовать».
- Изменения содержимого страницы при нажатии «Редактировать содержимое страницы».
Навигация для аминов происходит через обычное меню. Так, например, если вы переместились по пути «Категория 1» → «Категория 1.1» → «Категория 1.1.5» и нажали «Переименовать», то доступными элементами для изменения будут лишь те категории и страницы, которые находятся внутри «Категория 1.1.5».
* Это проще поклацать — алгоритм нелинейный, а потому объяснить в формате А → Б невозможно.
Пошаговая инструкция, как создать бот-витрину в Teleram для залива на финансы
Начинаем всегда по классике: с поиска сервера. Рекомендовать какой-либо сервис мы не будем — пусть это будет ваш личный выбор. А вот материал, как создать свой Python-сервер, посоветуем. Как минимум для текстов — самое оно. Как только сервер будет запущен, потребуется его настроить под взаимодействие с Telegram. Делается это довольно просто — достаточно ввести в консоль:
pip install telebot
Как только все импорты завершены, а модули установлены, можно переходить к созданию бота:
- Для начала пишем BotFather и получаем токен, следуя указаниям бота (BotFather — это официальный «бот для ботов» от самой Телеги).
- Затем нужно создать на сервере 3 текстовых файла и переименовать их в bot.py, menu.json и admin.json.
- В bot.py вставляем вот это:
import json
import os
from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup, InputMediaPhoto, InputMediaVideo
from telegram.ext import ApplicationBuilder, CommandHandler, CallbackQueryHandler, MessageHandler, filters, ContextTypes
# Токен ботаTOKEN = "ВАШ ТОКЕН"
# ID пользователя с админскими правами (без кавычек)ADMIN_ID = ВАШ ID
if not os.path.exists('images'): os.makedirs('images')
if not os.path.exists('menu.json'): with open('menu.json', 'w', encoding='utf-8') as file:
json.dump({"menu": []}, file)
if not os.path.exists('admin.json'): with open('admin.json', 'w', encoding='utf-8') as file:
json.dump([], file)
with open('menu.json', 'r', encoding='utf-8') as file: menu = json.load(file)["menu"]
with open('admin.json', 'r', encoding='utf-8') as file: admin_menu = json.load(file)
def create_buttons(items, include_navigation=True): buttons = []
for item in items:
buttons.append([InlineKeyboardButton(item['title'], callback_data=str(item['id']))])
if include_navigation:
buttons.append([InlineKeyboardButton("Главное меню", callback_data='main_menu')])
buttons.append([InlineKeyboardButton("Назад", callback_data='back')])
return buttons
def find_item_by_id(items, id): for item in items:
if item['id'].startswith(id):
return item
if 'children' in item:
found = find_item_by_id(item['children'], id)
if found:
return found
return None
def get_current_path(path): current_menu = menu
for item_id in path:
item = find_item_by_id(current_menu, item_id)
if item:
current_menu = item.get('children', [])
return current_menu
async def start(update: Update, context: ContextTypes.DEFAULT_TYPE): keyboard = create_buttons(menu, include_navigation=False)
if update.effective_user.id == ADMIN_ID:
for admin_button in admin_menu:
keyboard.insert(0, [InlineKeyboardButton(admin_button['title'], callback_data=f"admin_{admin_button['id']}")])
reply_markup = InlineKeyboardMarkup(keyboard)
await update.message.reply_text('Выберите категорию:', reply_markup=reply_markup)
context.user_data['path'] = []
async def button(update: Update, context: ContextTypes.DEFAULT_TYPE): query = update.callback_query
await query.answer()
data = query.data
if 'admin_action' in context.user_data: if context.user_data['admin_action'] == 'create_category':
await create_category(update, context)
elif context.user_data['admin_action'] == 'create_page':
await create_page(update, context)
elif context.user_data['admin_action'] == 'delete':
if 'delete_mode' in context.user_data and context.user_data['delete_mode']:
context.user_data['selected_item'] = data
item = find_item_by_id(menu, data)
await query.edit_message_text(f"Вы действительно хотите удалить элемент '{item['title']}' и все его содержимое? Введите 'да' для подтверждения или любой другой текст для отмены.")
return
elif context.user_data['admin_action'] == 'rename':
if 'rename_mode' in context.user_data and context.user_data['rename_mode']:
context.user_data['selected_item'] = data
await query.message.reply_text("Введите новый заголовок для выбранного элемента:")
return
elif context.user_data['admin_action'] == 'edit_page':
if 'edit_mode' in context.user_data and context.user_data['edit_mode']:
context.user_data['selected_item'] = data
await query.message.reply_text("Задайте содержание страницы:")
return
def add_admin_buttons(keyboard): if query.from_user.id == ADMIN_ID:
for admin_button in admin_menu:
keyboard.insert(0, [InlineKeyboardButton(admin_button['title'], callback_data=f"admin_{admin_button['id']}")])
return keyboard
if data == 'main_menu': context.user_data['path'] = []
keyboard = create_buttons(menu, include_navigation=False)
keyboard = add_admin_buttons(keyboard)
reply_markup = InlineKeyboardMarkup(keyboard)
await query.edit_message_text('Выберите категорию:', reply_markup=reply_markup)
return
if data == 'back': if context.user_data['path']:
context.user_data['path'].pop()
if not context.user_data['path']:
keyboard = create_buttons(menu, include_navigation=False)
keyboard = add_admin_buttons(keyboard)
reply_markup = InlineKeyboardMarkup(keyboard)
await query.edit_message_text('Выберите категорию:', reply_markup=reply_markup)
else:
item_id = context.user_data['path'][-1]
item = find_item_by_id(menu, item_id)
keyboard = create_buttons(item['children'])
keyboard = add_admin_buttons(keyboard)
reply_markup = InlineKeyboardMarkup(keyboard)
await query.edit_message_text(f"Выберите категорию в {item['title']}:", reply_markup=reply_markup)
return
if data.startswith('admin_'): admin_action = data[len('admin_'):]
for admin_button in admin_menu:
if admin_button['id'] == admin_action:
await handle_admin_action(query, admin_button['action'], context)
return
item = find_item_by_id(menu, data) if item['type'] == 'category':
context.user_data['path'].append(data)
keyboard = create_buttons(item['children'])
keyboard = add_admin_buttons(keyboard)
reply_markup = InlineKeyboardMarkup(keyboard)
await query.edit_message_text(f"Выберите категорию в {item['title']}:", reply_markup=reply_markup)
elif item['type'] == 'page': text_exists = 'text' in item and item['text'] is not None
photo_exists = 'photo_id' in item and item['photo_id'] is not None
video_exists = 'video_id' in item and item['video_id'] is not None
if not text_exists and not photo_exists and not video_exists: await query.edit_message_text("Страница пустая")
elif text_exists and photo_exists and not video_exists:
await query.message.reply_photo(photo=item['photo_id'], caption=item['text'])
elif text_exists and video_exists and not photo_exists:
await query.message.reply_video(video=item['video_id'], caption=item['text'])
elif photo_exists and not text_exists and not video_exists:
await query.message.reply_photo(photo=item['photo_id'])
elif video_exists and not text_exists and not photo_exists:
await query.message.reply_video(video=item['video_id'])
elif text_exists and not photo_exists and not video_exists:
await query.message.reply_text(item['text'])
async def handle_admin_action(query, action: str, context: ContextTypes.DEFAULT_TYPE): path = context.user_data['path']
current_menu = get_current_path(path)
current_location = " > ".join([find_item_by_id(menu, p)['title'] for p in path]) if path else "главное меню"
context.user_data['admin_action'] = action context.user_data['current_location'] = current_location
if action == "create_category": await query.edit_message_text(f"Введите название новой категории в {current_location}:")
elif action == "create_page":
await query.edit_message_text(f"Введите название новой страницы в {current_location}:")
context.user_data['create_page_title'] = True
elif action == "delete":
context.user_data['delete_mode'] = True
keyboard = create_buttons(current_menu)
reply_markup = InlineKeyboardMarkup(keyboard)
await query.edit_message_text(f"Выберите элемент для удаления в {current_location}:", reply_markup=reply_markup)
elif action == "rename":
context.user_data['rename_mode'] = True
keyboard = create_buttons(current_menu)
reply_markup = InlineKeyboardMarkup(keyboard)
await query.edit_message_text(f"Выберите элемент для переименования в {current_location}:", reply_markup=reply_markup)
elif action == "edit_page":
context.user_data['edit_mode'] = True
keyboard = create_buttons(current_menu)
reply_markup = InlineKeyboardMarkup(keyboard)
await query.edit_message_text(f"Выберите страницу для изменения в {current_location}:", reply_markup=reply_markup)
async def text_message_handler(update: Update, context: ContextTypes.DEFAULT_TYPE): if 'admin_action' not in context.user_data:
return
if context.user_data['admin_action'] == 'create_category': await create_category(update, context)
elif context.user_data['admin_action'] == 'create_page':
if context.user_data.get('create_page_title', False):
context.user_data['create_page_title'] = False
context.user_data['create_page_title_value'] = update.message.text
await update.message.reply_text(f"Теперь введите текст новой страницы в {context.user_data['current_location']}:")
context.user_data['waiting_for_page_text'] = True
elif context.user_data.get('waiting_for_page_text', False):
context.user_data['waiting_for_page_text'] = False
await create_page(update, context)
elif context.user_data['admin_action'] == 'delete':
if update.message.text.lower() == 'да':
await confirm_delete(update, context)
else:
await cancel_delete(update, context)
elif context.user_data['admin_action'] == 'rename':
new_title = update.message.text
await rename(update, context, new_title)
elif context.user_data['admin_action'] == 'edit_page':
await edit_page(update, context)
async def rename(update: Update, context: ContextTypes.DEFAULT_TYPE, new_title: str):
item_id = context.user_data['selected_item']
item = find_item_by_id(menu, item_id)
if item:
item['title'] = new_title
save_menu()
del context.user_data['admin_action']
del context.user_data['rename_mode']
await update.message.reply_text(f"Элемент успешно переименован в '{new_title}'!")
path = context.user_data.get('path', [])
keyboard = create_buttons(get_current_path(path))
reply_markup = InlineKeyboardMarkup(keyboard)
await update.message.reply_text('Выберите категорию:', reply_markup=reply_markup)
else:
await update.message.reply_text("Ошибка: Элемент не найден.")
async def confirm_delete(update: Update, context: ContextTypes.DEFAULT_TYPE): item_id = context.user_data['selected_item']
path = context.user_data.get('path', [])
delete_item_from_menu(path, item_id)
save_menu()
del context.user_data['admin_action']
del context.user_data['delete_mode']
del context.user_data['selected_item']
await update.message.reply_text("Элемент успешно удален!")
keyboard = create_buttons(get_current_path(path))
reply_markup = InlineKeyboardMarkup(keyboard)
await update.message.reply_text('Выберите категорию:', reply_markup=reply_markup)
async def cancel_delete(update: Update, context: ContextTypes.DEFAULT_TYPE): path = context.user_data.get('path', [])
del context.user_data['admin_action']
del context.user_data['delete_mode']
del context.user_data['selected_item']
await update.message.reply_text("Удаление отменено.")
keyboard = create_buttons(get_current_path(path))
reply_markup = InlineKeyboardMarkup(keyboard)
await update.message.reply_text('Выберите категорию:', reply_markup=reply_markup)
def delete_item_from_menu(items, item_id): current_menu = menu
for item in items:
current_menu = find_item_by_id(current_menu, item)['children']
for index, item in enumerate(current_menu):
if item['id'] == item_id:
del current_menu[index]
break
async def create_category(update: Update, context: ContextTypes.DEFAULT_TYPE): if update.message is None:
return
title = update.message.text
path = context.user_data.get('path', [])
new_id = generate_new_id(get_current_path(path), 'category', path)
new_category = {
"id": new_id,
"title": title,
"type": "category",
"children": []
}
add_item_to_menu(path, new_category)
save_menu()
del context.user_data['admin_action']
await update.message.reply_text("Категория успешно создана!")
keyboard = create_buttons(get_current_path(path))
reply_markup = InlineKeyboardMarkup(keyboard)
await update.message.reply_text('Выберите категорию:', reply_markup=reply_markup)
async def process_message(message): text = None
caption = None
photo_id = None
video_id = None
if message.text: text = message.text
if message.caption: caption = message.caption
text = caption
if message.photo: photo = message.photo[-1]
photo_id = photo.file_id
if message.video:
video = message.video
video_id = video.file_id
return {'text': text, 'photo_id': photo_id, 'video_id': video_id}
async def edit_page(update: Update, context: ContextTypes.DEFAULT_TYPE):
path = context.user_data.get('path', [])
item_id = context.user_data['selected_item']
item = find_item_by_id(menu, item_id)
processed_message = await process_message(update.message)
text = processed_message['text']
photo_id = processed_message['photo_id']
video_id = processed_message['video_id']
if item:
item['text'] = text
item['photo_id'] = photo_id
item['video_id'] = video_id
save_menu()
del context.user_data['admin_action']
del context.user_data['edit_mode']
await update.message.reply_text(f"ЗЫБС!!")
path = context.user_data.get('path', [])
keyboard = create_buttons(get_current_path(path))
reply_markup = InlineKeyboardMarkup(keyboard)
await update.message.reply_text('Выберите категорию:', reply_markup=reply_markup)
else:
await update.message.reply_text("Ошибка: Элемент не найден.")
async def create_page(update: Update, context: ContextTypes.DEFAULT_TYPE): title = context.user_data['create_page_title_value']
processed_message = await process_message(update.message)
text = processed_message['text']
photo_id = processed_message['photo_id']
video_id = processed_message['video_id']
path = context.user_data.get('path', [])
new_id = generate_new_id(get_current_path(path), 'page', path)
new_page = {
"id": new_id,
"title": title,
"type": "page",
"text": text,
"photo_id": photo_id,
"video_id": video_id
}
add_item_to_menu(path, new_page)
save_menu()
del context.user_data['admin_action']
await update.message.reply_text("Элемент успешно создан!")
keyboard = create_buttons(get_current_path(path))
reply_markup = InlineKeyboardMarkup(keyboard)
await update.message.reply_text('Выберите категорию:', reply_markup=reply_markup)
def generate_new_id(items, item_type, path): path_str = ''.join(path)
item_prefix = 'c' if item_type == 'category' else 'p'
max_id = 0
for item in items:
if item['id'].startswith(path_str) and item['id'][len(path_str):].startswith(item_prefix):
item_id = int(item['id'][len(path_str) + len(item_prefix):])
if item_id > max_id:
max_id = item_id
return f"{path_str}{item_prefix}{max_id + 1}"
def add_item_to_menu(path, item): current_menu = menu
for item_id in path:
current_menu = find_item_by_id(current_menu, item_id)['children']
current_menu.append(item)
def save_menu(): with open('menu.json', 'w', encoding='utf-8') as file:
json.dump({"menu": menu}, file, ensure_ascii=False, indent=2)
def main(): app = ApplicationBuilder().token(TOKEN).build()
app.add_handler(CommandHandler("start", start)) app.add_handler(CallbackQueryHandler(button))
app.add_handler(MessageHandler(None, text_message_handler))
app.run_polling()
if __name__ == '__main__': main() - Заменяем «ВАШ ТОКЕН» и «ВАШ ID» на токен, полученный в из шаге 1 и ID вашего профиля.
- Сохраняем файл.
- В admin.json вставляем вот это:
- Сохраняем файл.
[
{ "id": "01",
"title": "Создать категорию",
"action": "create_category"
},
{
"id": "02",
"title": "Создать страницу",
"action": "create_page"
},
{
"id": "03",
"title": "Удалить",
"action": "delete"
},
{
"id": "04",
"title": "Переименовать",
"action": "rename"
},
{
"id": "05",
"title": "Редактировать содержимое страницы",
"action": "edit_page"
}
] - В menu.json вставляет вот это:
{
"menu": [ {
"id": "c1",
"title": "Категория 1",
"type": "category",
"children": [
{
"id": "c1c1",
"title": "Категория 1.1",
"type": "category",
"children": [
{
"id": "c1c1c1c1",
"title": "Категория 1.1.1",
"type": "category",
"children": [
{
"id": "c1c1c1c1c1c1c1p1",
"title": "Страница 1.1.1.1",
"type": "page",
"text": "Тексты страницы 1.1.1.1",
"photo_id": null,
"video_id": null
}
]
}
]
}
]
},
{
"id": "c2",
"title": "Категория 2",
"type": "category",
"children": [
{
"id": "c2c1",
"title": "Категория 2.1",
"type": "category",
"children": [
{
"id": "c2c2c1c1",
"title": "Категория 2.1.1",
"type": "category",
"children": []
},
{
"id": "c2c2c1c2",
"title": "Категория 2.1.2",
"type": "category",
"children": []
}
]
},
{
"id": "c2c2",
"title": "Категория 2.2",
"type": "category",
"children": []
},
{
"id": "c2p1",
"title": "Страница 2.1",
"type": "page",
"text": "Текст страницы 2.1",
"photo_id": null,
"video_id": null
}
]
},
{
"id": "c3",
"title": "Категория 3",
"type": "category",
"children": [
{
"id": "c3c1",
"title": "Категория 3.1",
"type": "category",
"children": []
}
]
},
{
"id": "p1",
"title": "Страница 1",
"type": "page",
"text": "Текст страницы 1",
"photo_id": null,
"video_id": null
}
]
} - Сохраняем файл.
- Запускаем бот командой
python bot.py - Клацаем кнопочки с аккаунта админа — тестируем админ-функционал.
- Клацаем кнопочки с юзера — тестируем юзер-функционал
- Удаляем результаты тестов и оформляем все под финансы/другую тематику.
Оформили? Супер! Ваш бот-витрина готов к заливу трафика. Советуем использовать офферы от разных реклов либо смартлинку.
Демонстрация работы
Чтобы вы сразу могли понимать, как работает бот, приведем несколько примеров. Сразу начнем со схематической демонстрации, а после перейдем к прикладным вариантам использования.
Юзер/Админ
Красным подсвечиваются действия. Синим — результат. Читать сверху вниз (если два выделения одного цвета, то первым делался тот, что выше).
Мы захотели создать категорию 4, но потом нам не понравилось, что страница затесалась между категориями, и мы решили ее удалить
После косметического удаления страницы мы решили создать ее заново, но уже с картинкой
Так как страница теперь с картинкой, мы решили ее переименовать. Последние два скрина — проверка результата
Здесь приведена работа всех пяти админских кнопок. С их помощью создается все то, что будет продемонстрировано ниже. Но там уже идет демонстрация глазами юзера, а не админа.
Финансы — вариант 1
Одним из вариантов использования бота является классическая финансовая витрина — агрегатор банковских услуг. Ниже приведен вариант того, как это может выглядеть.
* Все названия выдуманы — любые совпадения случайны.
Финансы — вариант 2
А вот другой вариант использования бота при заливе на финансы. Вы можете усложнить путь по меню для создания иллюзии анкетирования и повышения траста.
Нутровый формат
Вариант нутровой витрины. Несмотря на шуточную подачу, вы можете использовать трастовый текст и заливаться не только на финансы, но и на нутру.
Гемблинг-/беттинг-формат
Еще один вариант применения данной витрины — гембла/беттинг. Благодаря использованию в качества БД меню json, остается возможность автоматического подтягивания информации об актуальных матчах и коэффициентах в бот. Но это отдельная тема — просто обращаем внимание, что можно и так.
Подводя итоги
Как видите, создать бот-витрину не так уж и сложно. Пусть его код и выглядит объемным, но на деле его базовый алгоритм достаточно прост. Все остальное — это уже опциональные настройки. А на этом у нас все — следите за нашим Telegram-каналом, чтобы узнавать о новых ботах для арбитража первыми!