Содержание
Плагин для FunPay работает с аккаунтом продавца через API Lovsel: читает заказы, диалоги и лоты, отвечает покупателям, меняет лоты и реагирует на события площадки. Плагин не получает golden_key — запросы к FunPay выполняет Lovsel, соблюдая ограничения площадки.
Общие правила создания плагина — структура файла, страницы, формы, операции, публикация — в руководстве «Как создать плагин». Здесь — только то, что относится к FunPay.
Манифест
Площадку объявляет поле marketplaces:
MANIFEST = {
"id": "studio.funpay-helper",
"name": "Помощник FunPay",
"version": "1.0.0",
"author": "Studio",
"description": "Благодарит покупателей FunPay и следит за лотами.",
"permissions": ["orders.read", "chats.read", "chats.write", "products.read", "storage"],
"marketplaces": ["funpay"], # ["ggsel", "funpay"] — плагин для обеих площадок
}- Плагин с
["funpay"]не запускается, пока у пользователя не подключён FunPay: в кабинете он отмечен «ждёт подключения» и включается сам сразу после подключения. - Плагин для обеих площадок работает, если подключена любая из них. Проверяйте
ctx.funpay.connectedиctx.ggsel.connectedперед обращением к площадке. - Плагин только для FunPay не может обратиться к
ctx.ggsel(будетPermissionDenied) и не получает события GGsel.
Разрешения
Разрешения общие для всех площадок; для FunPay они открывают такие методы:
| Разрешение | Методы ctx.funpay |
|---|---|
orders.read | orders(), order() |
chats.read | chats(), messages() |
chats.write | send_message() |
products.read | lots(), refresh_lots() |
products.write | set_lot_price(), set_lot_active(), raise_lots() |
balance.read | balance() |
Без нужного разрешения метод выбрасывает PermissionDenied с понятным текстом. Разрешения видны покупателю до покупки и сверяются с кодом при проверке Lovsel — объявляйте только нужные.
API ctx.funpay
| Метод или свойство | Что возвращает | Обращается к FunPay |
|---|---|---|
ctx.funpay.connected | Подключён ли FunPay и объявлена ли площадка в манифесте. | нет |
ctx.funpay.username | Имя продавца на FunPay. | нет |
ctx.funpay.orders(limit=500) | Заказы, новые первыми. | нет |
ctx.funpay.order(order_id) | Заказ с полями покупателя (fields) и отзывом (review). | да |
ctx.funpay.chats(limit=100) | Диалоги с покупателями. | нет |
ctx.funpay.messages(chat_id, limit=50) | Свежая переписка диалога. | да |
ctx.funpay.send_message(chat_id, text) | Отправляет сообщение покупателю (до 2000 символов). | да |
ctx.funpay.lots() | Лоты аккаунта. | нет |
ctx.funpay.refresh_lots() | Перечитывает лоты с FunPay, возвращает их число. | да |
ctx.funpay.set_lot_price(lot_id, price) | Меняет цену лота для продавца. | да |
ctx.funpay.set_lot_active(lot_id, active) | Включает или выключает лот. | да |
ctx.funpay.raise_lots() | Поднимает лоты: по строке на игру (game, ok, wait или error). | да |
ctx.funpay.balance() | {"available": 1250.0, "currency": "RUB"} | нет |
Методы без обращения к FunPay читают данные, которые Lovsel сохраняет опросом площадки каждые несколько секунд: они работают быстро и не нагружают FunPay. Методы с обращением к FunPay занимают до пары секунд — вызывайте их из действий, операций и обработчиков событий, а не при каждой отрисовке страницы.
orders = ctx.funpay.orders()
paid = [o for o in orders if o["status"] == "paid"]
lots = ctx.funpay.lots()
ctx.funpay.send_message(paid[0]["chat_id"], "Здравствуйте! Заказ уже в работе.")Ошибки FunPay приходят исключениями с понятным текстом — ловите их и показывайте продавцу:
try:
ctx.funpay.set_lot_price("1001", 199)
except Exception as exc:
ctx.log("Цена лота не изменена: " + str(exc), "error")Поля данных
Заказ (orders(), order(), события): id, title (название лота), category («Игра, раздел»), amount, currency, quantity, status, status_label, buyer, buyer_id, chat_id, created_at, url. У order() дополнительно fields (поля, заполненные покупателем) и review (stars, text, reply).
status | status_label |
|---|---|
paid | Оплачен |
success | Выполнен |
refunded | Возврат |
partial_refund | Частичный возврат |
pending | Ожидает оплаты |
В интерфейсе показывайте status_label, а не код. Суммы показывайте через ui.money(value, currency) — валюта у каждого заказа своя.
Диалог (chats()): id, buyer, buyer_id, last_text, last_at. Идентификатор диалога FunPay — строка из цифр; сообщения покупателю отправляются в диалог, а не в заказ.
Сообщение (messages()): id, text, from (buyer, seller или system), author, created_at, attachments (изображения).
Лот (lots()): id, title, category, price, currency, amount, active, auto_delivery.
События
@app.on("funpay.new_order")
def on_order(ctx, event):
order = event["order"]
...| Событие | Данные |
|---|---|
funpay.new_order | order — новый заказ. |
funpay.order_status | order, old_status — покупатель подтвердил заказ, возврат, повторное открытие. |
funpay.new_chat | chat_id, buyer, buyer_id, message, order — первое сообщение нового диалога. |
funpay.new_message | chat_id, buyer, buyer_id, message (id, text, author, author_id, image_url, badge), order (последний заказ покупателя или None). |
funpay.new_review | order, chat_id, review (stars, text, replied). |
init | Магазин запущен (получают все плагины). |
В каждом событии есть marketplace: "funpay". События funpay.* приходят только плагинам, объявившим FunPay, и только при подключённом аккаунте. Первое сообщение нового диалога приходит двумя событиями: funpay.new_chat и funpay.new_message.
message.badge заполнен у сообщений поддержки и арбитража FunPay — на них не стоит отвечать автоматически. Сообщения, отправленные Lovsel и плагинами, помечаются как автоматические, поэтому автоответы Lovsel на них не срабатывают.
Обработчик события должен работать быстро. Долгую работу запускайте операцией (ctx.operations.start).
Уведомления
Событие в центр событий, относящееся к заказу или диалогу FunPay, отправляйте с marketplace="funpay" — карточка получит отметку FunPay и встанет в ветку этого диалога:
ctx.notify("Заказ выдан", "Ключ отправлен покупателю", chat_id="700001", order_id="AB12CD34",
marketplace="funpay")needs_action=True или level="error" помещают событие в «Требуют внимания».
Хорошие практики
- Не отправляйте покупателю одно и то же сообщение повторно: храните отметку в
ctx.storage(например,thanked:<id заказа>). - Не опрашивайте FunPay в цикле: данные заказов, диалогов и лотов уже есть в
orders(),chats(),lots(). - FunPay ограничивает частоту сообщений и поднятие лотов (обычно раз в несколько часов на игру) — ошибки приходят исключениями, повторяйте действие позже.
- Не отвечайте автоматически на сообщения с
badge(поддержка и арбитраж FunPay). - Цену и включение лотов игровой валюты FunPay меняет только на сайте — такие лоты пропускайте.
Проверка без FunPay
Тестовый стенд SDK подставляет тестовый аккаунт: сообщения никуда не уходят, а складываются в plugin.funpay_sent.
from lovsel_sdk.testing import load_plugin
plugin = load_plugin("my_plugin.py", funpay={
"orders": [{"id": "AB12CD34", "title": "Ключ Steam", "amount": 199, "currency": "RUB",
"status": "paid", "buyer": "anna", "chat_id": "700001"}],
"chats": [{"id": "700001", "buyer": "anna", "last_text": "Здравствуйте"}],
"lots": [{"id": "1001", "title": "Ключ Steam", "price": 199}],
})
plugin.ctx.funpay.send_message("700001", "Спасибо!")
assert plugin.funpay_sent == [{"chat_id": "700001", "text": "Спасибо!"}]Из командной строки: python -m lovsel_sdk.testing my_plugin.py — проверка манифеста и отрисовка страниц.
Пример: благодарность и сводка лотов
from lovsel_sdk import WebPlugin, ui
MANIFEST = {
"id": "studio.funpay-thanks",
"name": "Благодарность FunPay",
"version": "1.0.0",
"author": "Studio",
"description": "Благодарит за оплату и показывает сводку лотов FunPay.",
"category": "messages",
"permissions": ["orders.read", "chats.write", "products.read", "storage"],
"marketplaces": ["funpay"],
}
app = WebPlugin(MANIFEST)
app.settings([
ui.field.textarea("text", "Текст благодарности", default="Спасибо за покупку! Скоро всё пришлю."),
])
@app.page("overview", "Обзор", icon="layout-dashboard")
def overview(ctx, params):
if not ctx.funpay.connected:
return [ui.empty("FunPay не подключён", "Подключите аккаунт в разделе «Подключение».")]
lots = ctx.funpay.lots()
active = [l for l in lots if l["active"]]
return [
ui.stats([
ui.stat("Лотов", len(lots)),
ui.stat("Активных", len(active)),
ui.stat("Поблагодарили", ctx.storage.get("thanked_count", 0)),
]),
ui.card("Активные лоты", [
ui.table([ui.column("title", "Лот"), ui.column("price", "Цена", align="right", format="money")],
active, empty_text="Активных лотов нет"),
]),
]
@app.on("funpay.new_order")
def thank(ctx, event):
order = event["order"]
key = "thanked:" + str(order["id"])
if order["status"] != "paid" or not order["chat_id"] or ctx.storage.get(key):
return
ctx.funpay.send_message(order["chat_id"], ctx.settings["text"])
ctx.storage.set(key, True)
ctx.storage.incr("thanked_count")Колонка format="money" берёт валюту из поля currency каждой строки — у лотов FunPay оно заполнено.
Полный пример: отзывы и напоминания
Файл docs/examples/funpay_reviews.py (кнопка «Пример плагина» вверху статьи) — готовый плагин для FunPay с интерфейсом, как у встроенных разделов Lovsel, в голубом цвете FunPay.
Что он делает:
- после оплаты заказа (
funpay.new_order, статусpaid) здоровается с покупателем; - когда покупатель подтвердил заказ (
funpay.order_status, статусsuccess), просит оставить отзыв; - на отзыв 4–5★ (
funpay.new_review) отвечает благодарностью, а отзыв 1–3★ отмечает в «Требуют внимания», чтобы продавец ответил лично.
| Страница | Компоненты SDK |
|---|---|
| Обзор | ui.stats со средней оценкой и переходом на страницы, круговая диаграмма ui.chart(kind="donut"), «Требуют внимания» со ссылкой на заказ. |
| Сценарии | ui.tabs(param="tab") — вкладка на сценарий: ui.toggle «Отправлять сообщение» и форма с field.template и предпросмотром в чате FunPay. |
| Заказы | Таблица с вкладками-фильтрами по статусу, поиском, колонкой-аватаром покупателя и скрытой колонкой в меню «Колонки»; нажатие на строку открывает заказ. |
| Заказ | Скрытая страница: ui.steps «Оплачен → Подтверждён → Отзыв», ui.keyvalue, лента действий плагина ui.timeline, переписка в окне сбоку. |
| Отзывы | Два фильтра таблицы: вкладки «Ждут решения / Решены» и кнопка-фильтр по оценке; счётчик нерешённых отзывов в меню. |
| Инструкция | Пункт внизу меню: ui.list_view и ui.message_preview. |
Главное из кода:
app = WebPlugin(MANIFEST, layout="sidebar")
@app.on("funpay.order_status")
def on_status(ctx, event):
order = event.get("order") or {}
if order.get("status") == "success": # покупатель подтвердил заказ
_send(ctx, "confirmed", order)
@app.page("scenarios", "Сценарии", icon="messages", group="Рабочее пространство")
def scenarios_page(ctx, params):
items = _scenarios(ctx)
tabs = [ui.tab(key, base["title"], [
ui.toggle(key, "Отправлять сообщение", items[key]["enabled"], action="toggle_scenario",
description=base["hint"]),
ui.card(None, [ui.form("save_" + key, [
ui.field.hidden("scenario", key),
ui.field.template("text", "Текст сообщения", variables=VARIABLES, required=True),
], values={"text": items[key]["text"]}, action="save_scenario", submit="Сохранить текст")]),
], icon=base["icon"]) for key, base in SCENARIOS.items()]
return [ui.header("Сценарии", "Каждый сценарий можно включить отдельно."), ui.tabs(tabs, param="tab")]
@app.action("load_chat")
def load_chat(ctx, data):
messages = ctx.funpay.messages(data.get("chat_id"), limit=20)
return ui.result(refresh=False, dialog=ui.dialog("Переписка", [ui.chat([
{"text": m["text"], "author": "Вы" if m["from"] == "seller" else "Покупатель",
"direction": "out" if m["from"] == "seller" else "in"} for m in messages if m.get("text")
], marketplace="funpay")], side=True))Проверить без FunPay:
from lovsel_sdk.testing import load_plugin
plugin = load_plugin("docs/examples/funpay_reviews.py", funpay={
"orders": [{"id": "AB12CD34", "title": "Ключ Steam", "amount": 199, "status": "paid", "buyer": "anna",
"chat_id": "700001"}],
})
plugin.emit("funpay.new_order", {"order": {"id": "AB12CD34", "status": "paid", "chat_id": "700001",
"buyer": "anna", "title": "Ключ Steam"}})
assert plugin.funpay_sent[0]["chat_id"] == "700001"
blocks = plugin.page("orders") # таблица заказов проверена на ошибки описанияКак устроены компоненты и что ещё умеют таблицы, формы и мастера — в статье «Компоненты интерфейса SDK».