feat(connections): connect 3rd party apps — browse external feeds with Thingtime comments, reactions & AI filters - #295
Open
lopugit wants to merge 9 commits into
Open
Conversation
…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>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
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>
Contributor
|
🤝 Merged Conflicted files:
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>
…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>
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
Contributor
|
🤝 Merged Conflicted files:
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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):
.rsstwins (public.json403s bots)Many-to-many linking — one external account is linkable from multiple Thingtime accounts: linking converges on one shared deterministic
external-accountthing; each Thingtime account holds its ownexternal-account-linkand earns its own ACL grants.Thingtime features on external posts — synced items are real
external-postthings on the data plane (reservedext-shareId namespace, CI-style source-timestamp-guarded idempotent upserts, per-account sync cooldown), so:targetIdthrough the standard/api/v1/things/comment/reactendpoints — cross-account threads verified;/post/:idpermalinks resolve with aggregated counts and the third-party author (surfaced fromextended.externalinsidetoPublicPosts);tt:user/<name>on each linked user's own read); public feeds publishtt:all.AI feed filters — "warn for sad news, with a button to click 'show'": natural-language rules classified server-side per feed page.⚠️ card with a Show anyway button;
warnveils the post behind ahidedrops it with a summary count. Classification rides the shared Lopu provider waterfall (newgenerateAiCompletionexported frommusing.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
PROTECTED_THINGTIME(a forged link would grant another user's personal feed; a forged account could carry attacker tokens).ext-prefix reserved insanitizeShareId(squat-proof on create AND update).getHomeThingsCollection) so a thin-frontend data-plane override can never capture identities or future OAuth tokens; token storage scaffolds onto the BinDatasecureblob pattern.Verification
pnpm --dir remix run verify:connections— 45/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=1adds a live Hacker News pull (47/47).test:ai-model-routingcontract green.Registration completeness
Route files +
server/routes/api/[...].tsimport map +apiEndpointDocsentries (docs registry = Nitro route table; each entry auto-generates two-docssmoke tests). Indexes increateThingsDataIndexes. FUNDAMENTALS §3, TESTING.md, README (fork-safe placeholders), andremix/CHANGELOG.mdupdated.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