Skip to content

feat(connections): connect 3rd party apps — browse external feeds with Thingtime comments, reactions & AI filters - #295

Open
lopugit wants to merge 9 commits into
developfrom
claude/third-party-app-integration-c96057
Open

feat(connections): connect 3rd party apps — browse external feeds with Thingtime comments, reactions & AI filters#295
lopugit wants to merge 9 commits into
developfrom
claude/third-party-app-integration-c96057

Conversation

@lopugit

@lopugit lopugit commented Aug 17, 2026

Copy link
Copy Markdown
Owner

Connect 3rd party apps 🔗

Adds the third-party app integration feature: link external accounts to your Thingtime account, browse their feeds inside Thingtime, and layer Thingtime features (comments, reactions, AI filters) on top of the external posts.

What's in

9 linkable platforms, all feed-pull tested live (keyless public-content APIs — zero configuration needed):

Platform Feed Tested
Demo 🧪 deterministic personal algorithm (E2E harness) ✅ 20 posts, acl-gated
Reddit 👽 subreddit multi via .rss twins (public .json 403s bots) ✅ 30 live
Hacker News 🟠 top/new/best ✅ 25 live
YouTube 📺 channel uploads feed ✅ 15 live
Mastodon 🐘 instance local timeline or account statuses ✅ 30 live
Bluesky 🦋 account posts via public AppView ✅ 30 live
Lemmy 🐭 instance front page or community ✅ 30 live
GitHub 🐙 user public activity ✅ 30 live
RSS/Atom 📰 any feed URL ✅ 20 live

Many-to-many linking — one external account is linkable from multiple Thingtime accounts: linking converges on one shared deterministic external-account thing; each Thingtime account holds its own external-account-link and earns its own ACL grants.

Thingtime features on external posts — synced items are real external-post things on the data plane (reserved ext- shareId namespace, CI-style source-timestamp-guarded idempotent upserts, per-account sync cooldown), so:

  • native comments + reactions attach by targetId through the standard /api/v1/things/comment / react endpoints — cross-account threads verified;
  • /post/:id permalinks resolve with aggregated counts and the third-party author (surfaced from extended.external inside toPublicPosts);
  • personal-algorithm feeds are per-user ACL-granted (tt:user/<name> on each linked user's own read); public feeds publish tt:all.

AI feed filters — "warn for sad news, with a button to click 'show'": natural-language rules classified server-side per feed page. warn veils the post behind a ⚠️ card with a Show anyway button; hide drops it with a summary count. Classification rides the shared Lopu provider waterfall (new generateAiCompletion exported from musing.ts — the AI client contract test stays green) with verdicts cached per (filter revision, post), and a deterministic keyword heuristic fallback when no AI key is configured.

UI: /connections (provider catalog, connect forms, linked accounts, filter manager) and /connections/feed (merged + per-connection tabs, native PostCard rendering, veil overlay), drawer nav entries, optimistic local-cache first paint, Lopu toasts.

Security posture

  • All five new kinds are PROTECTED_THINGTIME (a forged link would grant another user's personal feed; a forged account could carry attacker tokens).
  • ext- prefix reserved in sanitizeShareId (squat-proof on create AND update).
  • Accounts/links/filters are home-pinned (getHomeThingsCollection) so a thin-frontend data-plane override can never capture identities or future OAuth tokens; token storage scaffolds onto the BinData secure blob pattern.
  • Personal external posts 404 for non-linked users; connection kinds never leak through generic things listings/updates/deletes.

Verification

  • pnpm --dir remix run verify:connections45/45 real-API checks (FUNDAMENTALS §2: everything through the live API, no direct Mongo), covering walls, idempotent connect, many-to-many, cooldown, acl gating, cross-account comment threads, reactions, permalink aggregation, filter lifecycle (warn/hide/pause/delete/re-classify), squat/forge protections, unlink lifecycle. TT_VERIFY_LIVE=1 adds a live Hacker News pull (47/47).
  • Browser-verified desktop + mobile: connect flow, filter creation, veil → Show anyway, permalink, no overflow/clipping, nav clearance.
  • Targeted ESLint clean; test:ai-model-routing contract green.

Registration completeness

Route files + server/routes/api/[...].ts import map + apiEndpointDocs entries (docs registry = Nitro route table; each entry auto-generates two -docs smoke tests). Indexes in createThingsDataIndexes. FUNDAMENTALS §3, TESTING.md, README (fork-safe placeholders), and remix/CHANGELOG.md updated.

Next (loop continues)

OAuth2 scaffolding (config-gated Facebook/Instagram/X/Twitch/Tumblr/Pinterest/LinkedIn/TikTok), per-connection sync settings, PR note in PRs/.

🤖 Generated with Claude Code

…ts, browse feeds with native Thingtime layers + AI filters

Adds the /api/v1/connections/* family and /connections UI: link third-party
accounts (Reddit, YouTube, Mastodon, Bluesky, Lemmy, Hacker News, GitHub,
generic RSS/Atom, and a deterministic demo personal-algorithm provider) to a
Thingtime account. One external account is linkable from multiple Thingtime
accounts (shared deterministic external-account things, many-to-many links).
Feeds sync idempotently into external-post things (reserved ext- shareId
namespace, source-timestamp-guarded upserts, per-account cooldown) on the
data plane, so native comments/reactions attach by targetId and /post/:id
permalinks resolve with the third-party author. AI feed filters (warn = veil
behind a Show button, hide = drop) classify feeds via the shared Lopu
provider waterfall (new generateAiCompletion in musing.ts) with cached
per-revision verdicts and a deterministic keyword fallback without AI keys.

All five kinds are PROTECTED_THINGTIME; ext- prefixes are reserved in
sanitizeShareId; accounts/links/filters are home-pinned. Verified end to end
by remix/scripts/verify-connections.mjs (45 checks, +2 live) plus desktop and
mobile browser passes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@vercel

vercel Bot commented Aug 17, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
thingtime Ready Ready Preview Aug 18, 2026 3:27pm
thingtime (develop) Ready Ready Preview Aug 18, 2026 3:27pm

Request Review

@github-actions
github-actions Bot temporarily deployed to develop-pr-295 August 17, 2026 12:05 Destroyed
@github-actions

github-actions Bot commented Aug 17, 2026

Copy link
Copy Markdown
Contributor

✅ Develop S3 preview ready

The alias passed the develop bucket CORS preflight and a final live PR/SHA fence.

Generic Vercel Preview deployments use the shared development runtime; this controller adds the stable exact-SHA alias and marker-scoped cleanup.

…YouTube subscriptions

OAuth core: POST /api/v1/connections/oauth/begin mints the provider authorize
URL with a state JWT bound to the starting session (signJwt/verifyJwt); the
GET callback exchanges the code server-side, seals the token response into
the external-account root secure BinData blob (never projected, refreshed
near expiry via provider refresh grants), links the account, and lands back
on /connections. SSO providers, each config-gated by env credentials with
honest official-API copy: facebook (/me/posts timeline — Meta removed the
friends News Feed API), instagram (own media, ig_refresh), tiktok (own
videos, refresh_token grant), youtube-account (real subscriptions feed:
subscriptions.list → one batched channels call → merged playlistItems).

Virtual YouTube subscription system (the owner's beloved ytsubber pattern):
the youtube provider is now a per-user Thingtime-managed channel LIST —
add/remove via POST /api/v1/connections/youtube/channels (first add
auto-creates the connection), channel name search via GET .../youtube/search
(YouTube Data API when YOUTUBE_API_KEY/GOOGLE_API_KEY is set; ids, URLs and
@Handles resolve keylessly via RSS), merged uploads feed, legacy single-
channel configs folded in.

External posts gain a root sourceIds array (one video reached through many
sources stays ONE post with unified comments; partial index; legacy
crystal.accountId reads keep working) and the feed paginates into providers:
deepen=1 raises a capped per-account sync depth (FB/IG paging.next, TikTok
cursor, YT per-channel depth) with auto-deepen on scroll + a manual
fetch-older button. Reddit double-escaped Atom bodies now decode cleanly.

verify-connections.mjs: 64/64 (new SSO-guardrail + virtual-YouTube
sections); UI verified in-browser (search → subscribe → connection listed).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

🤝 Merged develop into claude/third-party-app-integration-c96057 — conflicts auto-resolved by the resolve-pr-conflicts workflow.

Conflicted files:

  • remix/CHANGELOG.md

Please review the merge commit before relying on it.

…lved conflicts)

Conflicted paths: remix/CHANGELOG.md

Resolved by the resolve-pr-conflicts workflow: https://github.com/lopugit/thingtime/actions/runs/32104782886

Co-Authored-By: Claude <noreply@anthropic.com>
lopugit and others added 2 commits August 18, 2026 18:23
…ng and dedup cleanups

Ten-angle code review over the branch (48 candidates, 30 unique, every
finding verified against the live suite). Correctness fixes:

- YouTube reconnect can no longer wipe the managed virtual channel list
  (provider mergeConfig union; display fields derive from merged state)
- Filter partial updates only touch sent fields — the enabled-only toggle
  no longer rewrites 'hide' to 'warn' or re-enables paused filters
- Unlink revokes the ex-member's acl grants on personal posts (membership
  IS the authorization); remaining members keep access
- The two YouTube providers now share one post namespace + Atom externalId
  grammar, so the same video is ONE post with unified comments
- isPostLikeThing replaces ad-hoc or-branches: Share on external posts
  works (was a guaranteed 404), permalinks unchanged
- Idempotent reconnects (incl. SSO token reseals) bypass the link caps;
  new MAX_LINKS_PER_ACCOUNT bounds per-post acl growth (FUNDAMENTALS §3)
- ensureViewerGrant keys on sourceIds (not last-writer crystal.accountId)
- Feed tab bar survives narrowed reads + bootstraps on deep links
- Provider dates clamped + cursors guarded finite (pre-1970 pubDates could
  loop pagination with duplicate pages); shared parseChronoCursor reused
- TikTok HTTP-200 error envelopes fail the link instead of minting
  placeholder accounts; IG short-token fallback stamps expiry and the
  refresh path can upgrade it long-lived; token refresh persists via
  compare-and-set so rotated refresh tokens never get clobbered
- Degraded heuristic verdicts are never cached while AI is configured;
  classification batches shrink (12 @ 3000 tokens), filters classify
  concurrently, verdict cache prunes on prompt edit + filter delete
- Fetch-helper catches can't rethrow on malformed URLs; Graph paging.next
  followed only when absolute https; stripHtml is linear-time and
  input-capped (ReDoS hardening); enclosure images parse in any attr order
- Virtual YouTube total sync failure surfaces lastSyncError instead of
  silently reporting success; deepen respects the cooldown at max depth
- External authors carry externalUrl — avatars/names link to the real
  third-party profile instead of a dead native /profile route

Cleanups: shared connections/shared.ts (sha48 + error union), shared UI
types/cardStyle, pagedGraphFeed for the Meta family, oauthCredsFor single
cred resolution, MAX_FEED_PAGES page-clamp helpers, one  verdict-cache
read across all filters, dead deepen param removed.

verify-connections.mjs: 68/68 (new regression checks for toggle/action,
reconnect-preserves-channels, unlink-revocation).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…m:lopugit/thingtime into claude/third-party-app-integration-c96057
…Mastodon home, Bluesky following

The platforms whose official APIs genuinely expose the user's algorithmic
feed, delivering the full 'your algorithm inside Thingtime' experience:

- reddit-account (OAuth2): Basic-auth token exchange + rotating refresh,
  identity via oauth.reddit.com/api/v1/me, feed = /best — the REAL
  personalized front page, paged by fullname cursor. REDDIT_CLIENT_ID/SECRET.
- mastodon-account (OAuth2): one registered app per deployment instance
  (MASTODON_INSTANCE + MASTODON_CLIENT_ID/SECRET), non-expiring tokens,
  feed = /api/v1/timelines/home paged by max_id.
- bluesky-account (NEW credential auth mode): connects with an app password
  — no developer registration needed, works on every deployment. The typed
  password is exchanged server-side for an AT-proto session (createSession)
  and NEVER stored; the session pair seals into the secure blob and rotates
  via refreshSession (new provider-level refreshTokens hook for
  non-OAuth providers). Feed = app.bsky.feed.getTimeline (following
  timeline), cursor-paged. Secret connect fields render masked in the UI.

Both YouTube-style dedupe namespaces apply: reddit-account/mastodon-account/
bluesky-account share post namespaces with their public twins, so a post
reached through your home feed AND a public follow stays ONE post with
unified comments. Status/post mappers extracted and shared between each
public/account provider pair. verify-connections.mjs: 73/73.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…eed first-paint

Five remaining SSO scaffolds, all config-gated with honest about-copy on
official API limits: X (OAuth2 with PKCE — the S256 verifier rides the
signed state JWT; reverse-chronological home timeline, paid-tier gated,
rotation-safe refresh), Twitch (helix Client-Id header support in
authedJson; followed channels that are LIVE), Tumblr (the REAL dashboard
feed, offset-paged), Pinterest (own pins, bookmark-paged; no home-feed
API), LinkedIn (identity-only linking — member feeds are partner-gated, so
an empty sync is the honest steady state).

Instant feed first-paint (stale-while-revalidate, serverless-safe):
GET /api/v1/connections/feed?sync=defer serves the stored page with ZERO
provider fan-out; the client paints it immediately and re-requests without
defer to sync fresh — revalidation is a second request, never post-response
background work, so it works on Vercel. Verified in-browser: defer request
then full request, both 200.

PR note added at PRs/295-third-party-app-integration-connect-3rd-party-apps.md
(architecture, verification, review rounds, known boundaries). README env
placeholders for the five new apps. verify-connections.mjs: 80/80 (catalog +
defer checks; the SSO guardrail loop covers the new providers automatically).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…potify

Relational grants at the right altitude: external posts now carry ONE
tt:extacct/<accountShareId> audience entry per SOURCE, evaluated live
against the viewer's connection links (registry aclEntryMatches + lazy
ensureExtAccountIds enrichment inside canViewInherited, memoised per
request — the exact tt:userFriends/withFriendIds pattern). Per-member acl
grants, ensureViewerGrant's per-read updateMany sweep, and the unlink
revocation sweep are all DELETED: links are the live authorization truth,
so unlinking revokes instantly and new links grant instantly with nothing
materialized per member. The FUNDAMENTALS §3 unbounded-acl concern
dissolves — audience entries are bounded by sources, not members.

formOAuthGrant factory: one rotation-aware implementation of the
form-encoded exchange/refresh (body vs HTTP-Basic client auth, custom
client-id param names, PKCE extra params). Google, TikTok, Twitch, Tumblr,
X, Pinterest and Reddit refitted — ~200 lines of drift-prone duplication
gone, and Pinterest's missing refresh-token rotation fixed by construction.

Spotify provider (22 platforms total): recently-played listening feed plus
new releases from followed artists (bounded fan-out), config-gated by
SPOTIFY_CLIENT_ID/SECRET on the factory.

verify-connections.mjs: 81/81 — the unlink-revocation checks now pass via
live evaluation rather than grant sweeps, proving the semantic held.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@github-actions
github-actions Bot temporarily deployed to develop-pr-295 August 18, 2026 13:16 Destroyed
lopugit added a commit that referenced this pull request Aug 18, 2026
…se external feeds with Thingtime comments, reactions & AI filters
lopugit added a commit that referenced this pull request Aug 18, 2026
…se external feeds with Thingtime comments, reactions & AI filters
@github-actions

github-actions Bot commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

🤝 Merged develop into claude/third-party-app-integration-c96057 — conflicts auto-resolved by the resolve-pr-conflicts workflow.

Conflicted files:

  • remix/CHANGELOG.md

Please review the merge commit before relying on it.

…lved conflicts)

Conflicted paths: remix/CHANGELOG.md

Resolved by the resolve-pr-conflicts workflow: https://github.com/lopugit/thingtime/actions/runs/32153894547

Co-Authored-By: Claude <noreply@anthropic.com>
github-actions Bot added a commit that referenced this pull request Aug 18, 2026
…se external feeds with Thingtime comments, reactions & AI filters
github-actions Bot added a commit that referenced this pull request Aug 19, 2026
…se external feeds with Thingtime comments, reactions & AI filters
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant