> ## Documentation Index
> Fetch the complete documentation index at: https://docs.mavera.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Community Sentiment Analysis

### Scenario

Your Discord community's mood shifts week to week — a buggy release tanks sentiment, a popular feature launch creates excitement. This job pulls messages from key channels (general, support, feedback), analyzes sentiment via Mave, and produces a weekly sentiment report with trend comparisons, topic breakdown, and recommended community actions.

**Flow:** Discord `GET /channels/{id}/messages` (multiple channels) → Aggregate → Mavera `POST /mave/chat` → Weekly sentiment report

### Code

<CodeGroup>
  ```python Python theme={"dark"}
  import os, requests, time
  from datetime import datetime, timedelta

  DC_TOKEN = os.environ["DISCORD_BOT_TOKEN"]
  DC_BASE = "https://discord.com/api/v10"
  DC_H = {"Authorization": f"Bot {DC_TOKEN}"}
  MV = os.environ["MAVERA_API_KEY"]
  MV_BASE = "https://app.mavera.io/api/v1"
  MV_H = {"Authorization": f"Bearer {MV}", "Content-Type": "application/json"}

  CHANNELS = {
      "general": "CHANNEL_ID_1",
      "support": "CHANNEL_ID_2",
      "feedback": "CHANNEL_ID_3",
  }
  DAYS_BACK = 7
  cutoff = datetime.utcnow() - timedelta(days=DAYS_BACK)
  snowflake_cutoff = str(int((cutoff.timestamp() - 1420070400) * 1000) << 22)

  # 1. Fetch messages from each channel
  all_messages = {}
  for name, channel_id in CHANNELS.items():
      msgs = []
      after = snowflake_cutoff
      while True:
          r = requests.get(f"{DC_BASE}/channels/{channel_id}/messages", headers=DC_H,
              params={"limit": 100, "after": after})
          if r.status_code == 429:
              retry = r.json().get("retry_after", 1)
              time.sleep(retry)
              continue
          r.raise_for_status()
          batch = r.json()
          if not batch:
              break
          msgs.extend(batch)
          after = batch[0]["id"]
          time.sleep(0.5)
      human = [m for m in msgs if not m.get("author",{}).get("bot",False)
               and len(m.get("content","")) > 10]
      all_messages[name] = human
      print(f"#{name}: {len(human)} messages")

  # 2. Build per-channel corpus
  corpus_parts = []
  for name, msgs in all_messages.items():
      corpus_parts.append(f"\n## #{name} ({len(msgs)} messages)")
      for m in sorted(msgs, key=lambda x: x["id"])[-40:]:
          corpus_parts.append(
              f"- [{m.get('timestamp','')[:10]}] {m.get('content','')[:300]}"
          )

  corpus = "\n".join(corpus_parts)

  # 3. Mave sentiment analysis
  report = requests.post(f"{MV_BASE}/mave/chat", headers=MV_H, json={
      "message": f"Community sentiment analyst. Analyze {sum(len(v) for v in all_messages.values())} Discord messages from the past {DAYS_BACK} days.\n\n"
          f"{corpus[:8000]}\n\n"
          "Produce a WEEKLY COMMUNITY SENTIMENT REPORT:\n\n"
          "1. **Overall Sentiment** — Score (1-100) with label (thriving/healthy/neutral/concerning/critical)\n"
          "2. **Channel Breakdown** — Sentiment per channel with top topic\n"
          "3. **Hot Topics** — Top 5 discussion themes ranked by volume + sentiment\n"
          "4. **Praise Highlights** — What the community loves right now (with quotes)\n"
          "5. **Pain Points** — Top 3 frustrations (with quotes)\n"
          "6. **Emerging Trends** — New topics that weren't discussed last week\n"
          "7. **Recommended Actions** — 3 specific things the team should do this week\n\n"
          "Include representative quotes for each section."
  }).json()

  print(f"\n{'='*60}\nWEEKLY COMMUNITY SENTIMENT REPORT\n{cutoff.strftime('%b %d')} – {datetime.utcnow().strftime('%b %d, %Y')}\n{'='*60}")
  print(report.get("content", "")[:3000])
  ```

  ```javascript JavaScript theme={"dark"}
  const DC_TOKEN = process.env.DISCORD_BOT_TOKEN;
  const DC_BASE = "https://discord.com/api/v10";
  const DC_H = { Authorization: `Bot ${DC_TOKEN}` };
  const MV = process.env.MAVERA_API_KEY;
  const MV_BASE = "https://app.mavera.io/api/v1";
  const MV_H = { Authorization: `Bearer ${MV}`, "Content-Type": "application/json" };

  const CHANNELS = { general: "CHANNEL_ID_1", support: "CHANNEL_ID_2", feedback: "CHANNEL_ID_3" };
  const DAYS_BACK = 7;
  const cutoff = new Date(Date.now() - DAYS_BACK * 86400000);
  const snowflakeCutoff = String(BigInt(Math.floor((cutoff.getTime() - 1420070400000))) << 22n);

  // 1. Fetch
  const allMessages = {};
  for (const [name, channelId] of Object.entries(CHANNELS)) {
    const msgs = [];
    let after = snowflakeCutoff;
    while (true) {
      const r = await fetch(`${DC_BASE}/channels/${channelId}/messages?limit=100&after=${after}`, { headers: DC_H });
      if (r.status === 429) { await new Promise(res => setTimeout(res, (await r.json()).retry_after * 1000 || 1000)); continue; }
      const batch = await r.json();
      if (!batch.length) break;
      msgs.push(...batch);
      after = batch[0].id;
      await new Promise(r => setTimeout(r, 500));
    }
    allMessages[name] = msgs.filter(m => !m.author?.bot && (m.content||"").length > 10);
    console.log(`#${name}: ${allMessages[name].length} messages`);
  }

  // 2. Corpus
  let corpus = "";
  for (const [name, msgs] of Object.entries(allMessages)) {
    corpus += `\n## #${name} (${msgs.length} messages)\n`;
    for (const m of msgs.sort((a,b) => a.id.localeCompare(b.id)).slice(-40))
      corpus += `- [${(m.timestamp||"").slice(0,10)}] ${(m.content||"").slice(0,300)}\n`;
  }

  // 3. Sentiment report
  const report = await fetch(`${MV_BASE}/mave/chat`, { method: "POST", headers: MV_H,
    body: JSON.stringify({ message: `Community sentiment analyst. ${Object.values(allMessages).reduce((s,v)=>s+v.length,0)} Discord messages, ${DAYS_BACK} days.\n\n${corpus.slice(0,8000)}\n\nWEEKLY REPORT: Overall Sentiment (1-100), Channel Breakdown, Hot Topics (5), Praise Highlights, Pain Points (3), Emerging Trends, Recommended Actions (3). Include quotes.` }),
  }).then(r => r.json());

  console.log(`\n${"=".repeat(60)}\nWEEKLY COMMUNITY SENTIMENT REPORT`);
  console.log((report.content || "").slice(0, 3000));
  ```
</CodeGroup>

### Example Output

```text theme={"dark"}
#general: 312 messages
#support: 187 messages
#feedback: 94 messages

WEEKLY COMMUNITY SENTIMENT REPORT
Mar 10 – Mar 17, 2026
============================================================

1. OVERALL SENTIMENT: 72/100 (Healthy)
   Up from ~65 last week — new feature launch drove positive discussion.

2. CHANNEL BREAKDOWN:
   - #general: 78/100 — Excited about v2.5 launch
   - #support: 58/100 — Mobile app crashes dominating
   - #feedback: 74/100 — Feature requests constructive

3. HOT TOPICS:
   1. v2.5 Launch (142 mentions, positive)
   2. Mobile App Crashes (34 mentions, negative)
   3. API Rate Limits (22 mentions, mixed)
   4. Dark Mode Request (18 mentions, positive)
   5. Pricing Discussion (12 mentions, neutral)

4. PRAISE: "The new dashboard is actually insane. Switched from
   [Competitor] last month and don't regret it at all."

5. PAIN POINTS:
   - "App crashes every time I open notifications on Android 14"
   - "Why is the API limited to 100 req/min? That's not usable."
```

### Error Handling

<AccordionGroup>
  <Accordion title="Message Content Intent">Without `MESSAGE_CONTENT` intent enabled, bot receives empty `content` for messages in guilds with 100+ members. Enable it in Developer Portal → Bot settings.</Accordion>
  <Accordion title="Rate limits">Discord returns `429` with `retry_after` in seconds. The code handles this automatically. Global limit is 50 req/sec but per-route limits are lower.</Accordion>
  <Accordion title="Snowflake pagination">Discord uses snowflake IDs for pagination. The `after` parameter fetches messages newer than a given ID. Convert timestamps to snowflakes for date-based filtering.</Accordion>
</AccordionGroup>
