И так в марте 2022 Steam отключила в российском сегменте Steam все основные способы оплаты для пользователей из России.
У игроков осталось 3 пути:
Продавать вещи со своего инвентаря.
Покупать ключи и аккаунты в сомнительных магазинах.
Возиться с QIWI для перевода через Тенге.
Я на тот момент активно изучал новый для себя язык Python, и решил потренироваться создав бота позволяющего быстро и просто пополнять пользователям пополнять свой steam аккаунт.
Сразу приложу ссылку на репозиторий, в котором храниться этот проект и оговорюсь что его поддержку я закончил несколько месяце назад в связи с участившимся отказом QIWI в переводах на Steam через API, якобы из-за недостатка средств на балансе, при том что баланс всегда был с запасом, и вручную оплата спокойно проводилась.
Пример
Основные проблемы пользователя которые должен устранять бот.
Необходимость иметь собственный QIWI кошелёк - все операции должны выполняются на уже созданном кошельке. В силу того что я не ожидал большого потока пользователей для начала я решил использовать свой личный аккаунт.
Ручной перевод средств - после того как бот получал подтверждения оплаты, он должен автоматически переводить рубли в тенге и отправлять их на аккаунт пользователя используя средства предоставляемые API кошелька.
Структура проекта
Проект планировалось создать из 3х основных частей
Телеграмм бот.
Фронт: осуществляет интерфейс взаимодействия с пользователем
Бек: отправляет API запросы и анализирует ответы, обновляет данные в базе
MS SQL Server.
Ответственен за хранение всей информации в проекте, ниже расписаны содержащиеся в ней таблицы и назначение полей в них.
customers — Аккаунты которыми оперировал пользователь.
No - Номер строки (ПК)
TgID - Идентификатор пользователя в Телеграмм
NickName - Логин пользователя в Steam
KZ - Баланс тенге
Logined - Флаг указывающий на выбраны в данный момент аккаунт Steam
orders - Информация о заказах
No - номер заказа (ПК)
NickName - целевой аккаунт (внешний ключ к customers)
RU - Количество рублей прошедших через заказ
KZ - Количество тенге прошедших через заказ
Status - Статус заказа
Url - ссылка на форму оплаты
CreateDateTime - Дата и Время создания
PiadDateTime - дата и Время исполнения
config - Настройки работы проекта
commission - комиссия за выполнение (в основном использовалась как поправочный коэффициент покрывающий комиссию QIWI за переводы и конвертацию валют)
wallet - Состояние и баланс кошелька
Name - имя QIWI кошелька (ПК)
Is_default - Выбранный кошелек для операций
Интеграция с QIWI API
Эта часть по сути является набором готовых методов которые можно использовать в боте для взаимодействия с кошельком.
Схема проекта
Общий принцип работы
При первом заходе пользователь вводил свой логин от аккаунта, после чего ему становится доступен основное меню, так у него на выбор несколько действий.
Создать ссылку на пополнение Steam
Подтвердить статус оплаты
Менеджер аккаунтов
При нажатии на первую опцию, пользователю предлагается выбрать сумму на которую он хочет пополнить кошелёк. После ввода желаемой суммы бот должен получить текущий кур валют, рассчитать сколько понадобиться рублей для покрытия комиссий, оправить запрос через API на создание формы оплаты QIWI, дождаться ссылки и вернуть пользователю.
После произведения оплаты, пользователь может выбрать вторую опцию и проверить статус его перевода, я заложил следующие
Ожидание (WAITING) - заказ был инициализирован через API и ожидает оплаты.
Оплачен (PAID) - пользователь внес средства в рублях.
Конвертирован (CROSSED) - рубли были конвертированы в тенге.
Исполнен (COMPLETED) - тенге были доставлены на аккаунт.
И тут немного магии, именно эта кнопка и запускала сам процесс перевода, поскольку я тогда был не в силах разобраться как реализовать калбек от QIWI по факту оплаты, было решено проверять статус оплаты именно в этот момент.
Если заказ застревал где-то в цепочке исполнения, эта информация передавалась пользователю, и он мог или попробовать снова протолкнуть оплату сам или воспользоваться поддержкой в лице меня :-)
Третья опция позволяла сменить аккаунт Steam на который создавались заказы, так пользователь мог пополнять кошельки не только себе но и знакомым, которые бы опасались сами использовать нового бота, и тем самым продвигая его.
Реализация
И так общий план приложения намечен, можно было приступать к разработке, весь процесс я поделил на четыре стадии.
Разработка интерфейса - создание основных элементов меню, навигация, предстояло продумать пользовательские сценарии.
Создание обертки над API QIWI - на тот момент я не имел опыта работы с сторонними сервисами и мне предстояло создать для точки взаимодействия с API QIWI, благо на мой взгляд там достаточно хорошая документация.
Создание прослойки для работы с сервером MS SQL, ох знал бы я тогда что такое ORM с экономил бы кучу времени )
Интеграция функционала в интерфейс, собственно последним этом предстояло привязать готовые наборы действий кнопкам на интерфейсе.
Разработка интерфейса
Первым делом разметим наборы кнопок которые будут отображаться пользователю в соответствующих меню. Реализованы они с помощью ReplyKeyboardMarkup, это шаблоны сообщений, в них не заложить логики, но они помогают наглядно указать пользователю какие действия ему сейчас доступны.
Код
# Пустой набор
Delete_markup = types.ReplyKeyboardRemove()
# Регистрация
Regestration_markup = types.ReplyKeyboardMarkup(resize_keyboard = True)
Regestration_markup.add(types.KeyboardButton("Вход"))
# Главное меню
Main_menu_markup = types.ReplyKeyboardMarkup(resize_keyboard = True)
Main_menu_markup.add(types.KeyboardButton("Создать ссылку на пополнение Steam"))
Main_menu_markup.add(types.KeyboardButton("Подтвердить статус оплаты"))
Main_menu_markup.add(types.KeyboardButton("Менеджер акаунтов"))
# Менеждер акаунтов
Nick_Name_menu_markup = types.ReplyKeyboardMarkup(resize_keyboard = True)
Nick_Name_menu_markup.add(types.KeyboardButton("Добавить новый акаунт Steam"))
Nick_Name_menu_markup.add(types.KeyboardButton("Сменить Steam акаунт"))
Nick_Name_menu_markup.add(types.KeyboardButton("Назад"))
# Заказы, предпологал возможность расширения опций
Order_menu_markup = types.ReplyKeyboardMarkup(resize_keyboard = True)
Order_menu_markup.add(types.KeyboardButton("Назад"))
Затем нужно сделать функций для декоратора обработчика сообщений.
По сути все сводится к тому, что надо определить какую команду ввел пользователь, ответить ему и заменить набор опций при необходимости
reply_markup = *набор команд*
после чего указать какой из обработчиков будет взаимодействовать с этим пользователем.
Bot.register_next_step_handler(*сообщение от пользователя*, *обработчик*)
Код
@Bot.message_handler(content_types=['text'])
def main(message):
if "Создать ссылку на пополнение Steam" == message.text:
Bot.send_message(message.chat.id, 'Введите сумму на котору пополнить акаунт\n'+nick_name+'\nМинимум 85 (требование QIWI)',reply_markup = Order_menu_markup)
Bot.register_next_step_handler(message,createpayment)
if "Подтвердить статус оплаты" == message.text:
Bot.send_message(message.chat.id, 'Подтверждено пополнений '+0+'\nЗаказов отправлено на Steam '+0+'\nБудем рады если вы оставите отзыв от том какая сумма пришла на Steam\nЭто поможет нам улучшить сервис\nhttps://t.me/ander_kot_1',reply_markup= Main_menu_markup)
Bot.register_next_step_handler(message,main)
else:
Bot.send_message(message.chat.id, 'Оплат по ссылкам не найдено !\nЕсли вы производили оплату свяжитесь с подержкой!\nhttps://t.me/ander_kot_1',reply_markup= Main_menu_markup)
Bot.register_next_step_handler(message,main)
if "Менеджер акаунтов" == message.text:
Bot.send_message(message.chat.id, 'Ваш текущий ник: '+ 'Ander_kot',reply_markup= Nick_Name_menu_markup)
Bot.register_next_step_handler(message,NickNameMenu)
Обертка над API QIWI
Как уже говорил у QIWI есть хорошая документация по API с примерами использования на разных языках и ожидаемыми ответами.
https://developer.qiwi.com/ru/p2p-payments/#p2p-
Самое интересное тут это отправка средств на счет клиента и получение ссылки на оплату.
Код
# Создание заказа в QIWI API
url = "https://api.qiwi.com/partner/bill/v1/bills/"+str(order_ID)
end_datetime = datetime.date.today() + datetime.timedelta(1) # Дата и время когда QIWI поститает заказ просроченым
# Заголовок
headers_API = CaseInsensitiveDict()
headers_API["content-type"] = "application/json"
headers_API["accept"] = "application/json"
headers_API["Authorization"] = "Bearer " + api_secret_token # Ваш P2P кльч https://qiwi.com/p2p-admin/api
# Данные
post_json = {"amount": {"currency": "RUB","value": ""},"comment": "","expirationDateTime": "","customer": {"phone": "","email": "","account": ""},"customFields" : {"paySourcesFilter":"","themeCode": "","yourParam1": "","yourParam2": ""}}
post_json["amount"]["value"] = amount_str
post_json["comment"] = comment+': '+str(nick_name)
post_json["expirationDateTime"] = str(end_datetime.isoformat())+'T12:00:00+03:00'
post_json["customer"]["account"] = str(nick_name)
# Запрос
respons = requests.put(url, headers=headers_API, json=post_json)
if respons.ok:
# Получение ссылки на оплату
respons_Json = respons.json()
url = str(respons_Json['payUrl'])
print(url)
return {'successfully':True, 'data':url}
else:
return {'successfully':False, 'data':''}
# Перевод на стим
def Send_To_Steam(api_access_token, nickName, amount_KZT, order_ID):
amount_KZT_str = str(amount_KZT)
url = "https://edge.qiwi.com/sinap/api/v2/terms/31212/payments"
# Заголовок
headers_API = CaseInsensitiveDict()
headers_API["content-type"] = "application/json"
headers_API["accept"] = "application/json"
headers_API["Authorization"] = "Bearer " + api_access_token
# Данные
json_API = {"id":"","sum": {"amount":"","currency":"398"},"paymentMethod": {"type":"Account","accountId":"398"},"fields": {"account":""}}
json_API['id'] = str(order_ID)
json_API['sum']['amount'] = amount_KZT_str
json_API['fields']['account'] = nickName
# Запрос
respons = requests.post(url, headers=headers_API, json=json_API)
if respons.ok:
return {'successfully':True, 'data':''}
else:
return {'successfully':False, 'data':respons.text} # тест ошибки
Тут упомяну "прикол" который может сэкономить кому-то время при разработке собственного приложения, дело в том что для обращения к некоторым методам API QIWI нужно присылать уникальный ID операции, так вот у меня этот параметр был привязан к номеру заказа, который изначально был обычным авто инкрементом в БД, что привело к падению приложения на каждом 2м заказе, дело полагаю в том что перевод на Steam из тенге по сути представляет собой 2 операции, конвертацию в доллары и уже затем перевод, из-за забиваются сразу 2 ID операции вместо одного и поэтому когда я пытался отправить тенге для второго заказа у которого ID+1 API возвращало мне ошибку "такой ID уже бы использован."
Работа с сервером MS SQL
Все свое общение с SQL я реализовал через 1 процедуру, которую приведу ниже, алгоритм простой, но позволял в ран тайме понимать что где и когда сработало не так.
Всего в нем пара траев.
Первый был подключением к БД.
Второй на получение информации.
Как я уже говорил об ORM я узнал значительно позже, вследствие чего все SQL запросы я составлял ручками и не о чем не жалею, практика есть практика )
Под катом также расположены методы которые я использовал для работы с заказами в БД.
Код
# Подключение к серверу SQL --
def Create_SQL_connection(host_name, user_name, user_password, db_name):
connection = None
try:
connection = mysql.connector.connect(
host=host_name,
user=user_name,
passwd=user_password,
database=db_name
)
except Error as e:
print(f"Ошибка подключения к MySQL '{e}'")
return connection
# Отправка запроса SQL
def execute_query(query, tip='не определено'):
connection = Create_SQL_connection(SQLHostName,SQLUserName,SQLRassword,SQLBaseName)
cursor = connection.cursor()
try:
cursor.execute(query)
result = cursor.fetchall()
connection.commit()
print('Запрос на '+tip+' отправлен')
return {'successfully':True, 'data':result}
except Error as e:
print(f"Ошибка в запросе '{e}'")
return {'successfully':False, 'data':''}
# Создание заказа
def Create_order(api_secret_token, amount, comment, nick_name):
datetime_str = str(datetime.datetime.today().replace(microsecond=0).isoformat())
print(datetime_str)
print(api_secret_token)
# Запрос коммиссии
respons_SQL = Get_Commission()
if respons_SQL['successfully'] and respons_SQL['data']:
# Расчет стоимости заказа
commission = respons_SQL['data']
amount_decimal = Decimal(amount)
commission_decimal = Decimal(commission)/Decimal(100)+Decimal(1)
amount_str = str(round(amount_decimal*commission_decimal,2))
# Создание заказа в QSL
query = "SELECT MAX(No) FROM orders;"
respons_SQL = execute_query(query,'Сбор ID заказа')
order_ID = respons_SQL['data'][0][0]+5 # отстум в 5 ID из-за того самого "прикола"
query = "INSERT INTO orders(No,NickName,RU,CreateDateTime) VALUES ("+str(order_ID)+",'"+nick_name+"',"+amount_str+",'"+datetime_str+"');"
respons_SQL = execute_query(query,'Создание pаказа для '+nick_name)
return respons_SQL
else:
return {'successfully':False, 'data':''}
# Добавить Url к заказу
def Add_URL(order_URL,order_ID):
order_ID_str = str(order_ID)
query = "UPDATE orders SET Url = '"+order_URL+"' WHERE No = "+order_ID_str+";"
respons_SQL = execute_query(query,'Установка URL заказу '+str(order_ID)+': '+str(order_URL))
if respons_SQL['successfully']:
return {'successfully':True, 'data':''}
else:
return {'successfully':False, 'data':''}
Естественно часто приходилось делать "гибридные" функции, которые одновременно работают как с БД так и с API QIWI, самым простым из примеров будет обновление статуса заказа.
Код
# Обновление статуса заказа
def Check_Oreder(api_secret_token, order_ID):
# API ---
url = "https://api.qiwi.com/partner/bill/v1/bills/"+str(order_ID)
headers_API = CaseInsensitiveDict()
headers_API["content-type"] = "application/json"
headers_API["accept"] = "application/json"
headers_API["Authorization"] = "Bearer " + api_secret_token
respons = requests.get(url, headers=headers_API)
if respons.ok:
respons_Json = respons.json()
status = str(respons_Json['status']['value'])
# SQL ---
query = "UPDATE orders SET Status = '"+status+"' WHERE No = '"+str(order_ID)+"';"
if execute_query(query,'Обновление pаказа '+status+'|'+str(order_ID)):
return {'successfully':True, 'data':status}
else:
return {'successfully':False, 'data':''}
return {'successfully':False, 'data':''}
Интеграция функционала в интерфейс
Когда все базовые взаимодействия с окружением были готовы настало время добавить их в интерфейс, это уже не составляло особого труда.
Смотришь что нажал пользователь, пытаешься произвести действие, получилось ?
Отлично можно двигать его дальше по интерфейсу.
Произошла ошибка?
Выводим оповещение с описанием ошибки, просим повторить, если ситуация хуже откатываем на предыдущую позицию в интерфейсе, если совсем все плохо, просим обратиться в поддержку.
Ниже описан обработчик сообщений в меню заказа.
Код
# Пользователь нажал на "Создать ссылку на пополнение Steam"
# Предыдущий обработчик попросил пользователя ввечсти желаемую сумму и перевел управление сюда
def createpayment(message):
# Возврат в главное меню
if("Назад" == message.text):
Bot.send_message(message.chat.id, 'Выберите действие',reply_markup= Main_menu_markup)
Bot.register_next_step_handler(message,main)
else:
# Проверка на "число"
if message.text.isdigit():
amount_Dec = round(Decimal(message.text),2)
# Для перевода на стим есть минимальный лимит
# Если пользователь попытается сделать заказ меньше он просто не пройдет
# Поэтму заранее отсекаем такие заказы
if amount_Dec >= round(Decimal('85'),2):
# Если введенная сумма верна создаем заказ, это может занять время
# Оповещаем пользователя что процесс пошел
Bot.send_message(message.chat.id, 'Создание ссылки для оплаты')
# Получаем целевой ник Steam
respons_SQL = QIWI_API.Check_Customer(message.chat.id)
if respons_SQL['successfully'] and respons_SQL['data']:
nick_name = respons_SQL['data'][0][0]
# Создаем заказ
respons_SQL = QIWI_API.Create_order( QIWI_API.SecretKey,message.text,'Account replenishment',nick_name)
if respons_SQL['successfully'] and respons_SQL['data']:
# Если все Ок отдаем ссылку пользователю и возвращаем к меню, где он может проверить статус заказа
order_URL = respons_SQL['data']
Bot.send_message(message.chat.id, 'После оплаты нажмите на "Подтвердить статус оплаты"\nВаша ссылка для оплаты:\n'+order_URL,reply_markup= Main_menu_markup)
Bot.register_next_step_handler(message,main)
else:
print('У клиента ошибка ! '+str(message.chat.id)+'\nСсылка на заказ не создана')
Bot.send_message(message.chat.id, 'Ошибка!\nСсылка не создана\nПовторите попытку или свяжитесь с подержкой!\nhttps://t.me/ander_kot_1',reply_markup= Main_menu_markup)
Bot.register_next_step_handler(message,main)
else:
print('У клиента ошибка ! '+str(message.chat.id)+'\nНе найден ник при создании заказа')
Bot.send_message(message.chat.id, 'Ошибка!\nВаш ник не найден\nПовторите попытку или свяжитесь с подержкой!\nhttps://t.me/ander_kot_1',reply_markup = Main_menu_markup)
Bot.register_next_step_handler(message,main)
else:
Bot.send_message(message.chat.id, 'Платеж должен составлять минимум 85 (требование QIWI)',reply_markup = Order_menu_markup)
Bot.register_next_step_handler(message,createpayment)
else:
Bot.send_message(message.chat.id, 'Используйте только цифры',reply_markup = Order_menu_markup)
Bot.register_next_step_handler(message,createpayment)
Итоги
Бот проработал с 16 марта 2022 по 15 октября того же года (8 месяцев).
За это время он помог перевести 41051р на кошельки Steam.
Самая большая разовая сумма 2100р.
87% пользователей первым взносом выбирали самую низкую сумму из возможных 105р.
Заработано было 0р 0к, комиссию за собственный сервис я не брал.
В общем это был интересный опыт по проработке собственного сервиса, который позволил мне погрузиться в мир API и SQL в купе с практикой по Python.
Комментарии (6)
WhiteApfel
06.04.2023 11:43Зачем ещё одна обёртка над апи qiwi, если есть glqiwiapi?
Ander_kot Автор
06.04.2023 11:43Не предполагал даже что есть что-то готовое, спасибо, воспользуюсь при необходимости.
SergeyDeryabin
06.04.2023 11:43-1Сейчас проще перечислить на что нет или официального SKD или десятка реализаций API
aborouhin
Вам очень повезло, что Вы завершили проект, не успев познакомиться поближе с Росфинмониторингом :) Суммы, конечно, у Вас смешные, чтобы вызвать повышенное внимание - но если бы к размеру оборотов прибавилось пару нулей на конце, много узнали бы интересного.
chuikoffru
А в чем проблема собственно? Можете вкратце рассказать что такого сделал человек, чтобы быть интересным этому органу?
aborouhin
Росфинмониторинг возбуждается не на нарушение, а на аномальную активность (большой объём поступлений от разных физ. лиц, которые тут же уходят транзитом). Нарушение тут будет скорее по линии ЦБ. Безналичные переводы по поручению третьих лиц без открытия счёта - это или банковская лицензия, или агентский договор с банком, которые в принципе физику получить невозможно (а у ИП/юрлица к этому добавятся обязанности по 115-ФЗ). Что по поводу всего этого подумает налоговая - тоже вопрос интересный (то, что нет прибыли - не значит, что нет доходов).
В общем, довольно непростая ситуация, которую надо было бы проанализировать с учётом всего нашего финансового и налогового регулирования ДО того, как начинать... впрочем, сделать такой анализ - гораздо дороже, чем те деньги, которые автор прогнал через своего бота.