MINARA

QQ Bot

QQ Official Bot v2 messaging with Ed25519 webhook events. Active messages are capped to 4 per month per bot, design for passive replies.

⚠️ Use passive replies, not push, QQ caps active messages at 4 per month per bot. Daily caps: 200 active DMs, 20 active sub-channel messages per channel. Most production interactions must reply to an inbound user message (the bot has 5 seconds to respond, free quota), pushing actively only for critical alerts.

What you get

  • Four send surfaces, routed by channel prefix:
    • c2c:<openid>/v2/users/{openid}/messages (C2C DMs)
    • group:<openid>/v2/groups/{openid}/messages (group)
    • channel:<id> (default) → /channels/{channel_id}/messages (public guild channel)
    • dm:<guild_id>/dms/{guild_id}/messages (private guild DM)
  • Inbound webhook at /webhooks/qq with Ed25519 signatures.
  • access_token cached for 7200 seconds with auto-refresh.
  • Text only. Rich-media messages (images, files, ARK cards) are a future enhancement.
  • 4000-char text limit.

Setup

1. Register a bot

  1. Sign in to QQ Open Platform Bot Console
  2. Create a Bot. Note the AppID and AppSecret on the bot detail page
  3. Under "Develop Settings" → "Webhook", set the URL to https://<your-host>/webhooks/qq and pick "Webhook Mode"
  4. The platform sends a one-time op=13 validation handshake. Minara responds with the signed plain_token automatically.

2. Identify a default target

Pick the most-common destination and encode it with the prefix:

ScenarioQQ_BOT_DEFAULT_CHANNEL_ID
Reply to DMs in a public guildchannel:<channel id>
Push to a groupgroup:<group openid>
Push to a single userc2c:<user openid>
Reply to private guild DMdm:<guild id>

You can omit the prefix for the public-guild channel case (the most common):

QQ_BOT_DEFAULT_CHANNEL_ID=1234567890

Equivalent to channel:1234567890.

3. Configure Minara

minara auth messaging add
# pick `qq`; paste AppID + AppSecret + default target.

Or set env vars:

QQ_BOT_APP_ID=12345678
QQ_BOT_APP_SECRET=<opaque secret>
QQ_BOT_TOKEN=<bot token>
QQ_BOT_DEFAULT_CHANNEL_ID=channel:1234567890

4. Test

minara auth messaging test qq

Counts against your monthly active-message quota. Plan around it.

Inbound webhook

QQ signs inbound bodies with Ed25519 over <timestamp><raw_body>. The public key is not published by the platform; it is derived from your bot secret:

  1. take the UTF-8 bytes of QQ_BOT_APP_SECRET
  2. repeat them until the buffer reaches 32 bytes
  3. that is the Ed25519 seed; derive keypair

Minara handles this seed-derived keypair internally. A 5-minute replay window is enforced (matches the convention used by Discord and Slack).

Validation handshake: when the platform sends {op: 13, d: {plain_token, event_ts}}, Minara signs event_ts + plain_token with the derived private key and replies with the signed hex. This is how the platform proves your endpoint owns the secret.

Inbound event types accepted:

  • AT_MESSAGE_CREATE, bot was @-mentioned in a public guild channel
  • GROUP_AT_MESSAGE_CREATE, bot was @-mentioned in a group
  • C2C_MESSAGE_CREATE, DM from a user

Other op=0 dispatch events (channel updates, reactions, etc.) are ignored.

Limits & caveats

  • Active-message quotas. 4 / month, 200 / day DMs, 20 / day per sub-channel. Errors with code 130_001 once exhausted; treat them as expected if you push at any volume.
  • Passive reply pattern is what's intended. The user @-mentions the bot; within 5 seconds the bot replies. Replies do not count against the active quota.
  • Webhook is mandatory for new bots. The WebSocket sandbox mode is deprecated for v2.

Troubleshooting

"Test message returns errcode 130001"

  • Monthly active-message quota exhausted. Use passive replies.

"Inbound webhook returns 401"

  • Most common cause: clock skew on your host. The 5-minute replay window is strict.
  • Less common: you pasted only part of the App Secret. The seed derivation repeats short secrets, but a wrong secret produces a wrong public key.

"Validation handshake (op=13) fails"

  • QQ_BOT_APP_SECRET is not configured at boot. Without it the seed derivation fails and the platform never finishes binding.

Reference

On this page