Mastodon-compatible API at truthsocial.com/api/v2/search. Opt-in via TRUTHSOCIAL_TOKEN env var (bearer token from browser). Silent when unconfigured. Full pipeline: search, parse, normalize, score, dedupe, render across all 10 pipeline files. 27 new tests, 440 total passing. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
5.9 KiB
title, type, status, date
| title | type | status | date |
|---|---|---|---|
| feat: Add Truth Social as opt-in source | feat | completed | 2026-03-09 |
feat: Add Truth Social as opt-in source
Add Truth Social (Mastodon fork) as an opt-in social source. When TRUTHSOCIAL_TOKEN is set, posts from Truth Social appear alongside other sources in research results. When not configured, completely silent.
Problem / Motivation
Issue #63 requested Truth Social support. Truth Social is a Mastodon fork with ~7M monthly active users. For users who care about that community's perspective on a topic, it's a valuable signal source. Follows the same opt-in pattern as Bluesky.
Approach
Use Truth Social's Mastodon-compatible API directly with urllib3 (no external dependencies). One env var: TRUTHSOCIAL_TOKEN (bearer token). Follow the Bluesky source pattern exactly across all 10 pipeline files.
Why bearer token (not username/password): Truth Social's OAuth uses a non-standard /oauth/v2/token endpoint with hardcoded client_id/client_secret extracted from their JS bundle. These values break when Truth Social updates their frontend. A bearer token is more stable - user extracts it once from browser dev tools (Application > Local Storage > truthsocial.com > access_token) or via truthbrush CLI.
API endpoint:
GET https://truthsocial.com/api/v2/search
Authorization: Bearer {token}
Params: q={topic}&type=statuses&limit=40
Response format: Standard Mastodon status objects with content (HTML), created_at, url, account, favourites_count, reblogs_count, replies_count.
Files to Change
| # | File | Change |
|---|---|---|
| 1 | scripts/lib/truthsocial.py |
New file. API client: search_truthsocial(topic, from_date, to_date, depth, config) + parse_truthsocial_response(response). Strip HTML tags from content. Handle 401/403/429 gracefully. |
| 2 | scripts/lib/env.py |
Add ('TRUTHSOCIAL_TOKEN', None) to get_config(). Add is_truthsocial_available(config). |
| 3 | scripts/lib/schema.py |
Add TruthSocialItem dataclass (id prefix "TS"). Add truthsocial/truthsocial_error fields to Report. Update to_dict()/from_dict(). |
| 4 | scripts/lib/normalize.py |
Add normalize_truthsocial_items(). Map Mastodon fields: favourites_count -> likes, reblogs_count -> reposts, replies_count -> replies. |
| 5 | scripts/lib/score.py |
Add compute_truthsocial_engagement_raw() + score_truthsocial_items(). Same weighted log formula as Bluesky. |
| 6 | scripts/lib/dedupe.py |
Add dedupe_truthsocial() (one-liner wrapping dedupe_items). |
| 7 | scripts/lib/render.py |
Add Truth Social sections to _xref_tag(), _assess_data_freshness(), render_compact(), render_source_status(), render_context_snippet(), render_full_report(). |
| 8 | scripts/last30days.py |
~20 touchpoints: TIMEOUT_PROFILES, VALID_SEARCH_SOURCES, import, _search_truthsocial(), run_research() param + dispatch + collect, main() availability + diag + flag + normalize + score + sort + dedupe + report. |
| 9 | SKILL.md |
Add Truth Social to source lists, optionalEnv (TRUTHSOCIAL_TOKEN), stats template, security/privacy section. |
| 10 | tests/test_truthsocial.py |
New file. Tests: HTML stripping, date parsing, response parsing, empty response, missing fields, depth config, auth error handling, successful search with mocked HTTP. |
Key Implementation Details
HTML stripping (truthsocial.py)
Truth Social returns HTML content (<p>Post text</p>). Strip tags to plain text:
import re
def _strip_html(html: str) -> str:
text = re.sub(r'<br\s*/?>', '\n', html)
text = re.sub(r'<[^>]+>', '', text)
return text.strip()
Date filtering
Mastodon created_at is ISO 8601 (2026-03-09T12:00:00.000Z). Use [:10] slice for YYYY-MM-DD comparison against from_date/to_date.
Engagement mapping
| Mastodon field | Internal field | Display |
|---|---|---|
favourites_count |
likes |
{N}lk |
reblogs_count |
reposts |
{N}rp |
replies_count |
replies |
{N}re |
Error handling
| HTTP Status | Behavior |
|---|---|
| 200 | Parse and return results |
| 401 | Return {"statuses": [], "error": "Truth Social token expired"} |
| 403 | Return {"statuses": [], "error": "Truth Social access denied (Cloudflare)"} |
| 429 | Return {"statuses": [], "error": "Truth Social rate limited"} |
| Other | Return {"statuses": [], "error": "Truth Social search failed: {status}"} |
All errors return empty results gracefully - never crash the research run.
DEPTH_CONFIG
| Depth | Limit |
|---|---|
| quick | 15 |
| default | 30 |
| deep | 60 |
What NOT to Change
lib/__init__.py- must stay bare (no eager imports)cross_source_link.py- works generically on items withcross_refsfieldfilter.py- date filtering is generic- No new pip dependencies
Acceptance Criteria
is_truthsocial_available()returns False whenTRUTHSOCIAL_TOKENnot set- No Truth Social stats line, no error, no mention when unconfigured
- With valid
TRUTHSOCIAL_TOKEN, posts are returned and rendered - HTML tags stripped from post content
- Token expiry (401) returns empty results gracefully
- Cloudflare block (403) returns empty results gracefully
--diagnoseshows Truth Social availability status- SKILL.md documents
TRUTHSOCIAL_TOKENenv var - All existing tests still pass
- New tests cover: HTML stripping, parsing, auth errors, successful search
bash scripts/sync.shdeploys successfully
Sources
- Issue #63: https://github.com/mvanhorn/last30days-skill/issues/63
- Truth Social API: Mastodon-compatible at
truthsocial.com/api/v2/search - Auth: Bearer token via browser dev tools or
truthbrushCLI - Pattern reference:
scripts/lib/bluesky.py(most recent source addition) - Bluesky auth plan:
docs/plans/2026-03-09-fix-bluesky-auth-opt-in-plan.md