Как стать автором
Обновить

EasySteamPaybot или как я помогал людям пополнять Steam

Уровень сложностиПростой
Время на прочтение11 мин
Количество просмотров9.3K
КДПВ
КДПВ

И так в марте 2022 Steam отключила в российском сегменте Steam все основные способы оплаты для пользователей из России.
У игроков осталось 3 пути:

  1. Продавать вещи со своего инвентаря.

  2. Покупать ключи и аккаунты в сомнительных магазинах.

  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

Эта часть по сути является набором готовых методов которые можно использовать в боте для взаимодействия с кошельком.

Схема проекта

Общий принцип работы

При первом заходе пользователь вводил свой логин от аккаунта, после чего ему становится доступен основное меню, так у него на выбор несколько действий.

  1. Создать ссылку на пополнение Steam

  2. Подтвердить статус оплаты

  3. Менеджер аккаунтов

При нажатии на первую опцию, пользователю предлагается выбрать сумму на которую он хочет пополнить кошелёк. После ввода желаемой суммы бот должен получить текущий кур валют, рассчитать сколько понадобиться рублей для покрытия комиссий, оправить запрос через API на создание формы оплаты QIWI, дождаться ссылки и вернуть пользователю.
После произведения оплаты, пользователь может выбрать вторую опцию и проверить статус его перевода, я заложил следующие

  • Ожидание (WAITING) - заказ был инициализирован через API и ожидает оплаты.

  • Оплачен (PAID) - пользователь внес средства в рублях.

  • Конвертирован (CROSSED) - рубли были конвертированы в тенге.

  • Исполнен (COMPLETED) - тенге были доставлены на аккаунт.

И тут немного магии, именно эта кнопка и запускала сам процесс перевода, поскольку я тогда был не в силах разобраться как реализовать калбек от QIWI по факту оплаты, было решено проверять статус оплаты именно в этот момент.

Если заказ застревал где-то в цепочке исполнения, эта информация передавалась пользователю, и он мог или попробовать снова протолкнуть оплату сам или воспользоваться поддержкой в лице меня :-)

Третья опция позволяла сменить аккаунт Steam на который создавались заказы, так пользователь мог пополнять кошельки не только себе но и знакомым, которые бы опасались сами использовать нового бота, и тем самым продвигая его.

Реализация

Процесс реализации в цвете
Процесс реализации в цвете

И так общий план приложения намечен, можно было приступать к разработке, весь процесс я поделил на четыре стадии.

  1. Разработка интерфейса - создание основных элементов меню, навигация, предстояло продумать пользовательские сценарии.

  2. Создание обертки над API QIWI - на тот момент я не имел опыта работы с сторонними сервисами и мне предстояло создать для точки взаимодействия с API QIWI, благо на мой взгляд там достаточно хорошая документация.

  3. Создание прослойки для работы с сервером MS SQL, ох знал бы я тогда что такое ORM с экономил бы кучу времени )

  4. Интеграция функционала в интерфейс, собственно последним этом предстояло привязать готовые наборы действий кнопкам на интерфейсе.

Разработка интерфейса

Первым делом разметим наборы кнопок которые будут отображаться пользователю в соответствующих меню. Реализованы они с помощью 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.

Теги:
Хабы:
Всего голосов 7: ↑7 и ↓0+7
Комментарии8

Публикации

Истории

Работа

Data Scientist
61 вакансия
Python разработчик
138 вакансий

Ближайшие события