Получите API-ключ
Получите секретный API-ключ в панели GrassFlow. Храните его только в переменных окружения вашего сервера.
GrassFlow
Лёгкий API для получения спонсорских заданий, проверки их выполнения пользователем и открытия доступа в Telegram-боте.
https://grassflow.ru/
Получите секретный API-ключ в панели GrassFlow. Храните его только в переменных окружения вашего сервера.
Получите список заданий для пользователя запросом POST /sponsors и покажите ссылки через кнопки.
После выполнения заданий вызовите POST /sponsors/check и откройте доступ, если все условия выполнены.
Пользователь отправляет вашему боту команду /start.
API возвращает ссылки и данные спонсорских заданий.
Каждая ссылка открывается отдельной кнопкой Telegram.
Если API подтвердил все статусы, бот открывает пользователю доступ.
Используйте Bearer-токен во всех запросах к API.
GRASSFLOW_API_KEY только на сервере.Добавляйте эти заголовки в каждый запрос:
Authorization: Bearer YOUR_GRASSFLOW_API_KEY
Content-Type: application/jsonGRASSFLOW_API_KEY=your_secret_keyВозвращает доступные сейчас спонсорские задания для пользователя.
/sponsorsОграничьте максимальное число заданий параметром max_sponsors.
{
"user_id": 123456789,
"chat_id": 123456789,
"max_sponsors": 5
}user_idID пользователя Telegram.
chat_idID чата с ботом.
max_sponsorsМаксимальное число заданий в ответе.
{
"sponsors": [
{
"id": "sp_01",
"title": "Example channel",
"link": "https://t.me/example"
}
]
}Подтверждает выполнение пользователем открытых спонсорских заданий.
/sponsors/checkПередайте ранее полученные ссылки спонсоров в массиве links.
{
"user_id": 123456789,
"links": [
"https://t.me/example_one",
"https://t.me/example_two"
]
}{
"sponsors": [
{ "link": "https://t.me/example_one", "status": "subscribed" },
{ "link": "https://t.me/example_two", "status": "pending" }
]
}Исходный пример адаптирован под GrassFlow: ключ берётся из переменной окружения, а для всех запросов используется одна сессия ClientSession.
import asyncio
import os
import aiohttp
from aiogram import Bot, Dispatcher, F, Router
from aiogram.filters import CommandStart
from aiogram.types import CallbackQuery, InlineKeyboardButton, InlineKeyboardMarkup, Message
BOT_TOKEN = os.getenv("BOT_TOKEN")
GRASSFLOW_API_KEY = os.getenv("GRASSFLOW_API_KEY")
GRASSFLOW_API_URL = "https://grassflow.ru/"
bot = Bot(token=BOT_TOKEN)
dp = Dispatcher()
router = Router()
pending_links: dict[int, list[str]] = {}
async def grassflow_request(session, method, path, payload=None):
async with session.request(
method,
f"{GRASSFLOW_API_URL}{path}",
json=payload,
headers={
"Authorization": f"Bearer {GRASSFLOW_API_KEY}",
"Content-Type": "application/json",
},
) as response:
data = await response.json(content_type=None)
if response.status >= 400:
raise RuntimeError(data.get("message") or f"GrassFlow error {response.status}")
return data
def sponsors_keyboard(sponsors):
buttons = [[InlineKeyboardButton(text="Выполнить задание", url=item["link"])] for item in sponsors]
buttons.append([InlineKeyboardButton(text="Проверить", callback_data="grassflow:check")])
return InlineKeyboardMarkup(inline_keyboard=buttons)
@router.message(CommandStart())
async def start_handler(message: Message, session: aiohttp.ClientSession):
tasks = await grassflow_request(session, "POST", "/sponsors", {
"user_id": message.from_user.id,
"chat_id": message.chat.id,
"max_sponsors": 5,
})
sponsors = tasks.get("sponsors") or []
if not sponsors:
await message.answer("Сейчас нет доступных заданий.")
return
pending_links[message.from_user.id] = [item["link"] for item in sponsors]
await message.answer("Подпишитесь на спонсоров, затем нажмите Проверить.", reply_markup=sponsors_keyboard(sponsors))
@router.callback_query(F.data == "grassflow:check")
async def check_handler(callback: CallbackQuery, session: aiohttp.ClientSession):
links = pending_links.get(callback.from_user.id, [])
result = await grassflow_request(session, "POST", "/sponsors/check", {"user_id": callback.from_user.id, "links": links})
done = result.get("sponsors") or []
if done and all(item.get("status") == "subscribed" for item in done):
pending_links.pop(callback.from_user.id, None)
await callback.message.answer("Все задания выполнены. Доступ открыт.")
else:
await callback.message.answer("Не все задания выполнены. Попробуйте ещё раз.")
await callback.answer()
async def main():
async with aiohttp.ClientSession(timeout=aiohttp.ClientTimeout(total=15)) as session:
dp.workflow_data["session"] = session
dp.include_router(router)
await dp.start_polling(bot)
if __name__ == "__main__":
asyncio.run(main())Текущий баланс аккаунта и краткая сводка по всем подключённым ботам. Тело запроса не требуется.
Авторизация передаётся в заголовке Auth. Не размещайте ваш API-токен в клиентском коде.
/get-balancehttps://grassflow.ruВозвращает баланс аккаунта и сведения о ботах, подключённых к аккаунту.
curl -X POST 'https://grassflow.ru/get-balance' \\
-H 'Auth: ВАШ_API_TOKEN' \\
-H 'Content-Type: application/json'Отправьте пустой POST-запрос с заголовками Auth и Content-Type.
{
"status": "ok",
"code": 200,
"message": "Баланс и информация о ботах успешно получена",
"balance": 219.21,
"bots_info": [
{
"bot_id": 12345678,
"bot_username": "my_super_bot",
"total_followers": 60667,
"revenue": 120542.45
}
]
}statusИтог обработки запроса: ok — успех, error — ошибка.
codeЧисловой код, дублирующий HTTP-статус ответа.
messageЧеловекочитаемое описание результата или ошибки.
balanceТекущий баланс аккаунта.
bots_infoКраткая сводка по каждому подключённому боту.
bot_idИдентификатор бота.
bot_usernameЮзернейм бота.
total_followersОбщее число подписчиков, приведённых ботом.
revenueЗаработано ботом за всё время.
Баланс и сводка по ботам находятся в объекте ответа.
Описание причины возвращается в поле message.
Обрабатывайте ответы API с использованием стандартных HTTP-кодов состояния.
200Запрос выполнен успешно.
400Отсутствует обязательный параметр или указан неверный формат.
401Bearer-токен отсутствует, неверный или истёк.
429За короткое время отправлено слишком много запросов.
500Временная неполадка; повторите попытку позже.
{ "message": "Unauthorized" }