К содержанию
FunPay · Разработчикам

Плагины для FunPay

API ctx.funpay, события funpay.*, разрешения, проверка без FunPay и полный пример плагина.

Содержание

Плагин для FunPay работает с аккаунтом продавца через API Lovsel: читает заказы, диалоги и лоты, отвечает покупателям, меняет лоты и реагирует на события площадки. Плагин не получает golden_key — запросы к FunPay выполняет Lovsel, соблюдая ограничения площадки.

Общие правила создания плагина — структура файла, страницы, формы, операции, публикация — в руководстве «Как создать плагин». Здесь — только то, что относится к FunPay.

Манифест

Площадку объявляет поле marketplaces:

python
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.readorders(), order()
chats.readchats(), messages()
chats.writesend_message()
products.readlots(), refresh_lots()
products.writeset_lot_price(), set_lot_active(), raise_lots()
balance.readbalance()

Без нужного разрешения метод выбрасывает 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 занимают до пары секунд — вызывайте их из действий, операций и обработчиков событий, а не при каждой отрисовке страницы.

python
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 приходят исключениями с понятным текстом — ловите их и показывайте продавцу:

python
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).

statusstatus_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.

События

python
@app.on("funpay.new_order")
def on_order(ctx, event):
    order = event["order"]
    ...
СобытиеДанные
funpay.new_orderorder — новый заказ.
funpay.order_statusorder, old_status — покупатель подтвердил заказ, возврат, повторное открытие.
funpay.new_chatchat_id, buyer, buyer_id, message, order — первое сообщение нового диалога.
funpay.new_messagechat_id, buyer, buyer_id, message (id, text, author, author_id, image_url, badge), order (последний заказ покупателя или None).
funpay.new_revieworder, 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 и встанет в ветку этого диалога:

python
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.

python
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 — проверка манифеста и отрисовка страниц.

Пример: благодарность и сводка лотов

python
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.

Главное из кода:

python
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:

python
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».