Skip to content

Updated activitypub Bluesky sharing enablement flow#25189

Merged
mike182uk merged 5 commits intomainfrom
ap-bluesky-handle-confirmation
Nov 20, 2025
Merged

Updated activitypub Bluesky sharing enablement flow#25189
mike182uk merged 5 commits intomainfrom
ap-bluesky-handle-confirmation

Conversation

@mike182uk
Copy link
Member

@mike182uk mike182uk commented Oct 16, 2025

ref TryGhost/ActivityPub#1377

We updated the backend implementation of the Bluesky sharing enablement process to include a confirmation step in which the client would prompt the backend to check what handle has been assigned to the Bluesky account generated by Bridgy Fed - This circumvents the need to for us to compute the handle manually and potentially getting it wrong if we compute it in a different way to Bridgy Fed


Note

Switch Bluesky sharing to v2 enable/disable endpoints, add handle confirmation flow with polling, update UI/state, and bump package to 2.0.0.

  • API:
    • Migrate Bluesky endpoints to v2 (enable, disable) and add confirm-handle endpoint.
    • Change enableBluesky() to no longer return a handle; introduce confirmBlueskyHandle() returning a confirmed handle or empty string.
    • Extend Account with blueskyHandleConfirmed and make blueskyHandle nullable.
  • Hooks (apps/activitypub/src/hooks/use-activity-pub-queries.ts):
    • Add useConfirmBlueskyHandleMutationForUser and cache updater for Bluesky details.
    • Update enable/disable mutations to set {blueskyEnabled, blueskyHandleConfirmed, blueskyHandle} appropriately and invalidate follows.
  • UI (views/Preferences/components/BlueskySharing.tsx):
    • Implement enablement confirmation flow with polling, loading states, success/error toasts, and conditional rendering once handle is confirmed.
    • Update disable flow and button states; show link to Bluesky profile when confirmed.
  • Tests (src/api/activitypub.test.ts):
    • Update Bluesky tests for v2 endpoints; add tests for handle confirmation and null/invalid responses.
  • Package:
    • Bump @tryghost/activitypub version to 2.0.0.

Written by Cursor Bugbot for commit c743df5. This will update automatically on new commits. Configure here.

@github-actions github-actions bot added the community [triage] Community features and bugs label Oct 16, 2025
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Oct 16, 2025

Note

Other AI code review bot(s) detected

CodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review.

Walkthrough

This PR bumps apps/activitypub to 2.0.0 and migrates Bluesky endpoints from v1 to v2. ActivityPub API: enableBluesky/disableBluesky now POST to v2 and return void; new confirmBlueskyHandle() POSTs to v2 confirm-handle and returns a handle string (with null/invalid responses handled). Account gains blueskyHandleConfirmed?: boolean and changes blueskyHandle to string | null. React Query hooks introduce a BlueskyDetails shape, consolidated cache updates, and a useConfirmBlueskyHandleMutationForUser hook. BlueskySharing UI adds polling, retry logic, and updated enabled/confirmed UI states.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

  • Verify API signature and routing changes and tests: apps/activitypub/src/api/activitypub.ts, apps/activitypub/src/api/activitypub.test.ts
  • Check Account type propagation and nullability: apps/activitypub/src/api/activitypub.ts and any consumers
  • Review React Query cache changes and new BlueskyDetails merge logic: apps/activitypub/src/hooks/use-activity-pub-queries.ts
  • Inspect polling, retry, timers, cleanup, and UX flows in BlueskySharing: apps/activitypub/src/views/Preferences/components/BlueskySharing.tsx
  • Confirm tests cover void returns, confirm-handle success/null/invalid shapes, and ensure no stale-cache scenarios are introduced

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The PR title accurately describes the main change: updating the Bluesky sharing enablement flow with new v2 endpoints, confirmation steps, and related API/UI changes.
Description check ✅ Passed The PR description is comprehensive and directly related to the changeset, detailing API updates, hook changes, UI modifications, test updates, and package version bump for the Bluesky confirmation flow.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch ap-bluesky-handle-confirmation

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@mike182uk mike182uk force-pushed the ap-bluesky-handle-confirmation branch 2 times, most recently from 6684dcb to 65a2079 Compare October 16, 2025 15:42
@mike182uk mike182uk marked this pull request as ready for review October 16, 2025 15:57
@mike182uk mike182uk removed the community [triage] Community features and bugs label Oct 16, 2025
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (4)
apps/activitypub/src/api/activitypub.ts (1)

661-666: Add explicit return types for consistency.

Most methods specify Promise. Make enableBluesky/disableBluesky explicit.

-    async enableBluesky() {
+    async enableBluesky(): Promise<void> {
       const url = new URL('.ghost/activitypub/v1/actions/bluesky/enable', this.apiUrl);
       await this.fetchJSON(url, 'POST');
     }
-    async disableBluesky() {
+    async disableBluesky(): Promise<void> {
       const url = new URL('.ghost/activitypub/v1/actions/bluesky/disable', this.apiUrl);
       await this.fetchJSON(url, 'POST');
     }

Also applies to: 667-672

apps/activitypub/src/api/activitypub.test.ts (2)

1639-1642: Optionally assert auth and accept headers for disable.

Now disable uses fetchJSON (adds Authorization and Accept). Add header assertions for parity with other tests.

-                [`https://activitypub.api/.ghost/activitypub/v1/actions/bluesky/disable`]: {
-                    async assert(_resource, init) {
-                        expect(init?.method).toEqual('POST');
-                    },
+                [`https://activitypub.api/.ghost/activitypub/v1/actions/bluesky/disable`]: {
+                    async assert(_resource, init) {
+                        expect(init?.method).toEqual('POST');
+                        const headers = new Headers(init?.headers);
+                        expect(headers.get('Authorization')).toContain('fake-token');
+                        expect(headers.get('Accept')).toEqual('application/activity+json');
+                    },
                     response: JSONResponse(null)
                 }

Based on learnings

Also applies to: 1654-1654


1668-1767: Add request assertions in confirmBlueskyHandle tests.

Assert method and headers on the confirm-handle endpoint at least in the “confirms the bluesky handle” test.

-                [`https://activitypub.api/.ghost/activitypub/v1/actions/bluesky/confirm-handle`]: {
-                    response: JSONResponse({
-                        handle: 'foo@bar.baz'
-                    })
-                }
+                [`https://activitypub.api/.ghost/activitypub/v1/actions/bluesky/confirm-handle`]: {
+                    async assert(_resource, init) {
+                        expect(init?.method).toEqual('POST');
+                        const headers = new Headers(init?.headers);
+                        expect(headers.get('Authorization')).toContain('fake-token');
+                        expect(headers.get('Accept')).toEqual('application/activity+json');
+                    },
+                    response: JSONResponse({
+                        handle: 'foo@bar.baz'
+                    })
+                }
apps/activitypub/src/views/Preferences/components/BlueskySharing.tsx (1)

82-116: Kick off confirmation immediately and avoid overlapping requests.

  • Fire an immediate confirm before starting the interval to reduce perceived latency.
  • Optionally guard against overlapping requests with a simple in-flight ref.
-        const confirmHandleInterval = setInterval(() => {
+        // Trigger immediately
+        confirmHandle();
+
+        const confirmHandleInterval = setInterval(() => {
             if (retryCountRef.current >= MAX_CONFIRMATION_RETRIES) {
                 clearInterval(confirmHandleInterval);
                 return;
             }
             retryCountRef.current += 1;
             confirmHandle();
         }, CONFIRMATION_INTERVAL);
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between baff818 and 65a2079.

📒 Files selected for processing (5)
  • apps/activitypub/package.json (1 hunks)
  • apps/activitypub/src/api/activitypub.test.ts (10 hunks)
  • apps/activitypub/src/api/activitypub.ts (2 hunks)
  • apps/activitypub/src/hooks/use-activity-pub-queries.ts (4 hunks)
  • apps/activitypub/src/views/Preferences/components/BlueskySharing.tsx (4 hunks)
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-08-22T08:55:11.602Z
Learnt from: mike182uk
PR: TryGhost/Ghost#24733
File: apps/admin-x-activitypub/src/api/activitypub.test.ts:1601-1627
Timestamp: 2025-08-22T08:55:11.602Z
Learning: In the Ghost ActivityPub module, `disableBluesky()` uses direct fetch where auth headers are manually passed, while `enableBluesky()` uses `fetchJSON` which abstracts auth header handling away. This is why the disable test needs to assert Authorization headers but the enable test doesn't.

Applied to files:

  • apps/activitypub/src/api/activitypub.test.ts
  • apps/activitypub/src/api/activitypub.ts
🧬 Code graph analysis (2)
apps/activitypub/src/api/activitypub.test.ts (1)
apps/activitypub/src/api/activitypub.ts (1)
  • ActivityPubAPI (215-684)
apps/activitypub/src/views/Preferences/components/BlueskySharing.tsx (1)
apps/activitypub/src/hooks/use-activity-pub-queries.ts (4)
  • useAccountForUser (1609-1621)
  • useEnableBlueskyMutationForUser (2642-2672)
  • useDisableBlueskyMutationForUser (2674-2704)
  • useConfirmBlueskyHandleMutationForUser (2706-2735)
🔇 Additional comments (8)
apps/activitypub/package.json (1)

3-3: Version bump to 1.0.16 looks good.

apps/activitypub/src/api/activitypub.ts (2)

29-31: Account model changes look correct.

Nullable blueskyHandle and added blueskyHandleConfirmed align with the new confirm flow.


673-683: confirmBlueskyHandle implementation LGTM.

Defensive empty-string fallback is fine for the polling UX.

apps/activitypub/src/api/activitypub.test.ts (1)

1611-1614: enableBluesky test updates LGTM.

POST assertion and void return check align with new API.

Also applies to: 1625-1625

apps/activitypub/src/views/Preferences/components/BlueskySharing.tsx (1)

181-216: Confirmed state UI looks good.

Copy, avatar, and external profile link behave as expected.

apps/activitypub/src/hooks/use-activity-pub-queries.ts (3)

2652-2657: Enable onSuccess cache update LGTM.

Sets enabled, resets confirmation and handle as expected.


2685-2689: Disable onSuccess cache update LGTM.

Clears enabled/confirmed/handle and invalidates follows.


2706-2735: confirmBlueskyHandle mutation LGTM.

Only updates cache on non-empty handle; error handling matches rate-limit UX.

Comment on lines 74 to 81
const confirmHandle = useCallback(() => {
confirmBlueskyHandleMutation.mutateAsync().then((handle) => {
if (handle) {
setHandleConfirmed(true);
}
});
}, [confirmBlueskyHandleMutation]);

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Catch promise to avoid unhandled rejections during polling.

mutateAsync rejects on error; without catch, interval-triggered calls can cause unhandled rejections.

-    const confirmHandle = useCallback(() => {
-        confirmBlueskyHandleMutation.mutateAsync().then((handle) => {
-            if (handle) {
-                setHandleConfirmed(true);
-            }
-        });
-    }, [confirmBlueskyHandleMutation]);
+    const confirmHandle = useCallback(async () => {
+        try {
+            const handle = await confirmBlueskyHandleMutation.mutateAsync();
+            if (handle) {
+                setHandleConfirmed(true);
+            }
+        } catch {
+            // no-op; onError in the mutation already surfaces rate-limit errors
+        }
+    }, [confirmBlueskyHandleMutation]);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const confirmHandle = useCallback(() => {
confirmBlueskyHandleMutation.mutateAsync().then((handle) => {
if (handle) {
setHandleConfirmed(true);
}
});
}, [confirmBlueskyHandleMutation]);
const confirmHandle = useCallback(async () => {
try {
const handle = await confirmBlueskyHandleMutation.mutateAsync();
if (handle) {
setHandleConfirmed(true);
}
} catch {
// no-op; onError in the mutation already surfaces rate-limit errors
}
}, [confirmBlueskyHandleMutation]);
🤖 Prompt for AI Agents
In apps/activitypub/src/views/Preferences/components/BlueskySharing.tsx around
lines 74 to 81, the confirmHandle callback calls
confirmBlueskyHandleMutation.mutateAsync() without handling rejections, which
can produce unhandled promise rejections when run from an interval; wrap the
call in error handling by either making confirmHandle async and awaiting
mutateAsync inside a try/catch that logs or ignores the error, or append a
.catch(...) to the promise chain to handle failures and still set state on
success; ensure any caught errors are logged via the app logger or console so
failures during polling are not swallowed.

@mike182uk mike182uk force-pushed the ap-bluesky-handle-confirmation branch 2 times, most recently from b66c802 to 2121679 Compare November 3, 2025 10:55
Copy link

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This PR is being reviewed by Cursor Bugbot

Details

Your team is on the Bugbot Free tier. On this plan, Bugbot will review limited PRs each billing cycle for each member of your team.

To receive Bugbot reviews on all of your PRs, visit the Cursor dashboard to activate Pro and start your 14-day free trial.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (1)
apps/activitypub/src/views/Preferences/components/BlueskySharing.tsx (1)

73-81: Catch promise rejection to prevent unhandled errors during polling.

The mutateAsync() call can reject on errors (e.g., rate limits, network failures), and without a .catch() handler, the interval-triggered calls will cause unhandled promise rejections.

Apply this diff to handle rejections:

-    const confirmHandle = useCallback(() => {
-        confirmBlueskyHandleMutation.mutateAsync().then((handle) => {
-            if (handle) {
-                setHandleConfirmed(true);
-            }
-        });
-        // eslint-disable-next-line react-hooks/exhaustive-deps
-    }, []); // Empty deps - mutations are stable in practice
+    const confirmHandle = useCallback(async () => {
+        try {
+            const handle = await confirmBlueskyHandleMutation.mutateAsync();
+            if (handle) {
+                setHandleConfirmed(true);
+            }
+        } catch {
+            // no-op; onError in the mutation already surfaces rate-limit errors
+        }
+    }, [confirmBlueskyHandleMutation]);
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2121679 and e281602.

📒 Files selected for processing (1)
  • apps/activitypub/src/views/Preferences/components/BlueskySharing.tsx (6 hunks)
🧰 Additional context used
🧠 Learnings (7)
📓 Common learnings
Learnt from: mike182uk
Repo: TryGhost/Ghost PR: 24733
File: apps/admin-x-activitypub/src/api/activitypub.test.ts:1601-1627
Timestamp: 2025-08-22T08:55:11.602Z
Learning: In the Ghost ActivityPub module, `disableBluesky()` uses direct fetch where auth headers are manually passed, while `enableBluesky()` uses `fetchJSON` which abstracts auth header handling away. This is why the disable test needs to assert Authorization headers but the enable test doesn't.
Learnt from: mike182uk
Repo: TryGhost/Ghost PR: 22471
File: apps/admin-x-activitypub/src/utils/pending-activity.ts:13-71
Timestamp: 2025-03-13T09:00:20.205Z
Learning: The pending activity utilities in the Ghost ActivityPub module are covered by tests in the file `apps/admin-x-activitypub/test/unit/utils/pending-activity.ts`.
📚 Learning: 2025-10-15T07:53:49.814Z
Learnt from: CR
Repo: TryGhost/Ghost PR: 0
File: apps/shade/AGENTS.md:0-0
Timestamp: 2025-10-15T07:53:49.814Z
Learning: Applies to apps/shade/src/components/**/*.{tsx,ts} : Prefer compound subcomponents (e.g., Header.Title/Meta/Actions) with small focused parts attached as static properties and exported as named exports

Applied to files:

  • apps/activitypub/src/views/Preferences/components/BlueskySharing.tsx
📚 Learning: 2025-07-14T12:20:35.268Z
Learnt from: sagzy
Repo: TryGhost/Ghost PR: 24346
File: apps/admin-x-settings/src/components/settings/growth/Network.tsx:8-12
Timestamp: 2025-07-14T12:20:35.268Z
Learning: The Network component toggle in `apps/admin-x-settings/src/components/settings/growth/Network.tsx` is intentionally implemented as static UI with a no-op onChange handler, as part of a UI-first development approach before connecting actual ActivityPub functionality.

Applied to files:

  • apps/activitypub/src/views/Preferences/components/BlueskySharing.tsx
📚 Learning: 2025-03-13T09:02:50.102Z
Learnt from: mike182uk
Repo: TryGhost/Ghost PR: 22471
File: apps/admin-x-activitypub/src/views/Feed/components/NewPostModal.tsx:29-34
Timestamp: 2025-03-13T09:02:50.102Z
Learning: In the ActivityPub module, error handling for mutations is handled at the hook level (in use-activity-pub-queries.ts) rather than in individual components. This allows for centralized error handling across the application.

Applied to files:

  • apps/activitypub/src/views/Preferences/components/BlueskySharing.tsx
📚 Learning: 2025-03-13T09:02:50.102Z
Learnt from: mike182uk
Repo: TryGhost/Ghost PR: 22471
File: apps/admin-x-activitypub/src/views/Feed/components/NewPostModal.tsx:29-34
Timestamp: 2025-03-13T09:02:50.102Z
Learning: In the Ghost ActivityPub module, error handling for mutations is handled at the hook level (in use-activity-pub-queries.ts) rather than in individual components. This allows for centralized error handling across the application.

Applied to files:

  • apps/activitypub/src/views/Preferences/components/BlueskySharing.tsx
📚 Learning: 2025-08-11T19:39:00.428Z
Learnt from: kevinansfield
Repo: TryGhost/Ghost PR: 24651
File: ghost/core/test/utils/urlUtils.js:53-57
Timestamp: 2025-08-11T19:39:00.428Z
Learning: In Ghost's test utilities, when fixing specific issues like async behavior, it's preferred to maintain existing error handling patterns (even if suboptimal) to keep PRs focused on their primary objective. Error handling improvements can be addressed in separate, dedicated PRs.

Applied to files:

  • apps/activitypub/src/views/Preferences/components/BlueskySharing.tsx
📚 Learning: 2025-10-15T07:53:49.814Z
Learnt from: CR
Repo: TryGhost/Ghost PR: 0
File: apps/shade/AGENTS.md:0-0
Timestamp: 2025-10-15T07:53:49.814Z
Learning: Applies to apps/shade/**/*.{ts,tsx} : Use React + TypeScript and prefer composable components over heavy prop configurations

Applied to files:

  • apps/activitypub/src/views/Preferences/components/BlueskySharing.tsx
🧬 Code graph analysis (1)
apps/activitypub/src/views/Preferences/components/BlueskySharing.tsx (1)
apps/activitypub/src/hooks/use-activity-pub-queries.ts (4)
  • useAccountForUser (1615-1627)
  • useEnableBlueskyMutationForUser (2685-2715)
  • useDisableBlueskyMutationForUser (2717-2747)
  • useConfirmBlueskyHandleMutationForUser (2749-2778)
🔇 Additional comments (4)
apps/activitypub/src/views/Preferences/components/BlueskySharing.tsx (4)

4-4: LGTM!

The new imports and configuration constants appropriately support the polling-based confirmation workflow.

Also applies to: 25-28


32-32: LGTM!

State initialization and refs are appropriate for the confirmation workflow. Using useRef for retry count avoids unnecessary re-renders.

Also applies to: 36-37, 40-40


173-187: LGTM!

The loading state UI clearly communicates the handle creation process, and the helper text about leaving the page provides good UX during the potentially lengthy confirmation wait.


140-140: LGTM!

The confirmed state UI is well-designed with clear visual indicators (avatar badge, handle display, copy functionality) and the profile link correctly strips the @ prefix for the Bluesky URL.

Also applies to: 192-230

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

♻️ Duplicate comments (1)
apps/activitypub/src/hooks/use-activity-pub-queries.ts (1)

2664-2683: Type safety issue flagged in previous review still present.

The currentProfile parameter in updateAccountBlueskyCache is typed as BlueskyDetails, but the account cache at QUERY_KEYS.account('index') stores Account objects. This weakens type safety as previously noted.

🧹 Nitpick comments (2)
apps/activitypub/src/api/activitypub.ts (2)

666-670: Consider explicit return type annotation.

While the method correctly returns Promise<void> implicitly, adding an explicit annotation would improve clarity:

-async enableBluesky() {
+async enableBluesky(): Promise<void> {

This is consistent with methods like confirmBlueskyHandle (line 678) that do include explicit return types.


672-676: Consider explicit return type annotation.

Same suggestion as enableBluesky - add explicit Promise<void> return type for clarity.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e281602 and 69dd0d8.

📒 Files selected for processing (3)
  • apps/activitypub/package.json (1 hunks)
  • apps/activitypub/src/api/activitypub.ts (2 hunks)
  • apps/activitypub/src/hooks/use-activity-pub-queries.ts (4 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/activitypub/package.json
🧰 Additional context used
🧠 Learnings (5)
📓 Common learnings
Learnt from: mike182uk
Repo: TryGhost/Ghost PR: 24733
File: apps/admin-x-activitypub/src/api/activitypub.test.ts:1601-1627
Timestamp: 2025-08-22T08:55:11.602Z
Learning: In the Ghost ActivityPub module, `disableBluesky()` uses direct fetch where auth headers are manually passed, while `enableBluesky()` uses `fetchJSON` which abstracts auth header handling away. This is why the disable test needs to assert Authorization headers but the enable test doesn't.
Learnt from: mike182uk
Repo: TryGhost/Ghost PR: 22471
File: apps/admin-x-activitypub/src/utils/pending-activity.ts:13-71
Timestamp: 2025-03-13T09:00:20.205Z
Learning: The pending activity utilities in the Ghost ActivityPub module are covered by tests in the file `apps/admin-x-activitypub/test/unit/utils/pending-activity.ts`.
📚 Learning: 2025-08-22T08:55:11.602Z
Learnt from: mike182uk
Repo: TryGhost/Ghost PR: 24733
File: apps/admin-x-activitypub/src/api/activitypub.test.ts:1601-1627
Timestamp: 2025-08-22T08:55:11.602Z
Learning: In the Ghost ActivityPub module, `disableBluesky()` uses direct fetch where auth headers are manually passed, while `enableBluesky()` uses `fetchJSON` which abstracts auth header handling away. This is why the disable test needs to assert Authorization headers but the enable test doesn't.

Applied to files:

  • apps/activitypub/src/api/activitypub.ts
  • apps/activitypub/src/hooks/use-activity-pub-queries.ts
📚 Learning: 2025-03-13T09:02:50.102Z
Learnt from: mike182uk
Repo: TryGhost/Ghost PR: 22471
File: apps/admin-x-activitypub/src/views/Feed/components/NewPostModal.tsx:29-34
Timestamp: 2025-03-13T09:02:50.102Z
Learning: In the ActivityPub module, error handling for mutations is handled at the hook level (in use-activity-pub-queries.ts) rather than in individual components. This allows for centralized error handling across the application.

Applied to files:

  • apps/activitypub/src/hooks/use-activity-pub-queries.ts
📚 Learning: 2025-03-13T09:02:50.102Z
Learnt from: mike182uk
Repo: TryGhost/Ghost PR: 22471
File: apps/admin-x-activitypub/src/views/Feed/components/NewPostModal.tsx:29-34
Timestamp: 2025-03-13T09:02:50.102Z
Learning: In the Ghost ActivityPub module, error handling for mutations is handled at the hook level (in use-activity-pub-queries.ts) rather than in individual components. This allows for centralized error handling across the application.

Applied to files:

  • apps/activitypub/src/hooks/use-activity-pub-queries.ts
📚 Learning: 2025-11-05T16:49:21.083Z
Learnt from: rob-ghost
Repo: TryGhost/Ghost PR: 25356
File: apps/admin/src/hooks/user-preferences.ts:20-20
Timestamp: 2025-11-05T16:49:21.083Z
Learning: In apps/admin/src/hooks/user-preferences.ts, the query key for user preferences intentionally includes `user?.accessibility` (not just `user?.id`) so the cache automatically reacts to changes from any source (useEditUserPreferences, other editUser calls, external updates). Combined with `cacheTime: 0`, this design garbage-collects orphaned entries while keeping the active entry cached. This is a documented, intentional architectural decision.

Applied to files:

  • apps/activitypub/src/hooks/use-activity-pub-queries.ts
🧬 Code graph analysis (1)
apps/activitypub/src/hooks/use-activity-pub-queries.ts (1)
apps/admin-x-framework/src/index.ts (1)
  • useQueryClient (44-44)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: Setup
  • GitHub Check: Build & Push
  • GitHub Check: Setup
🔇 Additional comments (4)
apps/activitypub/src/hooks/use-activity-pub-queries.ts (2)

2685-2747: LGTM: Enable/disable mutations correctly manage Bluesky state.

The consolidated cache updates using BlueskyDetails properly reflect the Bluesky lifecycle:

  • Enable sets blueskyEnabled: true with unconfirmed state
  • Disable resets all fields
  • Both appropriately invalidate the following query

2749-2778: LGTM: Confirm mutation correctly handles polling workflow.

The early return when blueskyHandle === '' (line 2762) is appropriate for the polling scenario, keeping the existing cache state until confirmation succeeds. Error handling for rate limiting is properly implemented.

apps/activitypub/src/api/activitypub.ts (2)

29-30: LGTM: Account interface correctly models Bluesky confirmation state.

The addition of blueskyHandleConfirmed and making blueskyHandle nullable properly represents the three-state workflow (disabled, enabled-unconfirmed, enabled-confirmed).


678-688: LGTM: Confirm handle method properly handles response variations.

The null/invalid response handling (returning empty string) correctly supports the polling workflow where the handle may not be immediately available.

Minor note: The String() conversion on line 687 is redundant given the typeof json.handle !== 'string' check, but it's harmless defensive coding.

@github-actions
Copy link
Contributor

E2E Tests Failed

To view the Playwright test report locally, run:

REPORT_DIR=$(mktemp -d) && gh run download 19220515993 -n playwright-report -D "$REPORT_DIR" && npx playwright show-report "$REPORT_DIR"

@mike182uk mike182uk force-pushed the ap-bluesky-handle-confirmation branch from 79eddba to b8dd7a6 Compare November 20, 2025 11:15
toast.error('Something went wrong, please try again.');

await disableBlueskyMutation.mutateAsync();
setLoading(false);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: Unhandled error leaves loading state stuck

When confirmation retries are exhausted, disableBlueskyMutation.mutateAsync() is called without error handling. If this call fails, the subsequent setLoading(false) won't execute, leaving the component stuck in a loading state indefinitely with no way for the user to recover.

Fix in Cursor Fix in Web

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

♻️ Duplicate comments (2)
apps/activitypub/src/hooks/use-activity-pub-queries.ts (1)

2725-2839: Unify Bluesky cache typing with Account to keep type-safety.

The Bluesky enable/disable/confirm flows and cache merges look good, but updateAccountBlueskyCache currently treats the account('index') cache as BlueskyDetails rather than Account, which weakens typings compared to the rest of this file where that key holds Account. You can keep the BlueskyDetails payload while preserving the Account-shaped cache:

-type BlueskyDetails = {
-    blueskyEnabled: boolean;
-    blueskyHandleConfirmed: boolean;
-    blueskyHandle: string | null;
-}
-
-function updateAccountBlueskyCache(queryClient: QueryClient, blueskyDetails: BlueskyDetails) {
+type BlueskyDetails = Pick<Account, 'blueskyEnabled' | 'blueskyHandleConfirmed' | 'blueskyHandle'>;
+
+function updateAccountBlueskyCache(
+    queryClient: QueryClient,
+    blueskyDetails: BlueskyDetails
+) {
     const profileQueryKey = QUERY_KEYS.account('index');
 
-    queryClient.setQueryData(profileQueryKey, (currentProfile?: BlueskyDetails) => {
+    queryClient.setQueryData(profileQueryKey, (currentProfile?: Account) => {
         if (!currentProfile) {
             return currentProfile;
         }
 
         return {
             ...currentProfile,
             ...blueskyDetails
         };
     });
 }

This keeps the shared cache consistent while still centralizing Bluesky updates.

apps/activitypub/src/views/Preferences/components/BlueskySharing.tsx (1)

48-71: Guard mutateAsync calls to avoid unhandled rejections in polling/disable flows.

mutateAsync rejects on failure; the current uses can surface unhandled promise rejections:

  • confirmHandle uses .then(...) with no .catch(...).
  • The confirmation interval await disableBlueskyMutation.mutateAsync(); has no try/catch.
  • handleDisable uses await disableBlueskyMutation.mutateAsync(); inside a try without a catch.

Because neither the interval nor the click handler ever await these calls from outside, any rejection escapes as an unhandled rejection. You can keep error surfacing in the hook’s onError while making these callers safe:

-const handleDisable = async () => {
-    setLoading(true);
-    try {
-        await disableBlueskyMutation.mutateAsync();
-        setShowConfirm(false);
-        toast.success('Bluesky sharing disabled');
-    } finally {
-        setLoading(false);
-    }
-};
+const handleDisable = async () => {
+    setLoading(true);
+    try {
+        await disableBlueskyMutation.mutateAsync();
+        setShowConfirm(false);
+        toast.success('Bluesky sharing disabled');
+    } catch {
+        // onError from the mutation already handles surfacing errors (e.g. rate limits)
+    } finally {
+        setLoading(false);
+    }
+};
 
-const confirmHandle = useCallback(() => {
-    confirmBlueskyHandleMutation.mutateAsync().then((handle) => {
-        if (handle) {
-            setHandleConfirmed(true);
-        }
-    });
-    // eslint-disable-next-line react-hooks/exhaustive-deps
-}, []); // Empty deps - mutations are stable in practice
+const confirmHandle = useCallback(async () => {
+    try {
+        const handle = await confirmBlueskyHandleMutation.mutateAsync();
+        if (handle) {
+            setHandleConfirmed(true);
+        }
+    } catch {
+        // handled by mutation onError; polling should just continue
+    }
+}, [confirmBlueskyHandleMutation]);

-        const confirmHandleInterval = setInterval(async () => {
-            retryCountRef.current += 1;
-
-            if (retryCountRef.current > MAX_CONFIRMATION_RETRIES) {
-                clearInterval(confirmHandleInterval);
-
-                toast.error('Something went wrong, please try again.');
-
-                await disableBlueskyMutation.mutateAsync();
-                setLoading(false);
-
-                return;
-            }
-
-            confirmHandle();
-        }, CONFIRMATION_INTERVAL);
+        const confirmHandleInterval = setInterval(async () => {
+            retryCountRef.current += 1;
+
+            if (retryCountRef.current > MAX_CONFIRMATION_RETRIES) {
+                clearInterval(confirmHandleInterval);
+
+                toast.error('Something went wrong, please try again.');
+
+                try {
+                    await disableBlueskyMutation.mutateAsync();
+                } catch {
+                    // mutation onError already reported any issues
+                } finally {
+                    setLoading(false);
+                }
+
+                return;
+            }
+
+            confirmHandle();
+        }, CONFIRMATION_INTERVAL);

This keeps the polling behavior the same while avoiding noisy unhandled rejections.

Also applies to: 73-80, 108-120

🧹 Nitpick comments (2)
apps/activitypub/src/api/activitypub.test.ts (1)

1668-1767: confirmBlueskyHandle tests cover main response shapes well.

The success, null, missing-handle, and invalid-handle cases mirror the adapter’s ''-fallback logic nicely. If you want a tiny extra safeguard, you could also assert init?.method === 'POST' in one of these specs, but it’s not strictly necessary given the implementation.

apps/activitypub/src/views/Preferences/components/BlueskySharing.tsx (1)

32-37: Bluesky enabled/confirmed state handling is clear; one minor state nit.

The showAsEnabled check (blueskyEnabled && blueskyHandleConfirmed) and the separate “enabling” vs “confirmed” UI, including the profile card, copy-handle, and open-profile actions, are all nicely wired to the account cache. The only small nit is that the loading state’s initializer derives from account but is effectively overridden by the effect once account data loads; you could simplify to useState(false) and rely solely on the effect, but the current behavior is functionally fine.

Also applies to: 144-196

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 79eddba and b8dd7a6.

📒 Files selected for processing (5)
  • apps/activitypub/package.json (1 hunks)
  • apps/activitypub/src/api/activitypub.test.ts (10 hunks)
  • apps/activitypub/src/api/activitypub.ts (2 hunks)
  • apps/activitypub/src/hooks/use-activity-pub-queries.ts (4 hunks)
  • apps/activitypub/src/views/Preferences/components/BlueskySharing.tsx (6 hunks)
🧰 Additional context used
🧠 Learnings (11)
📓 Common learnings
Learnt from: mike182uk
Repo: TryGhost/Ghost PR: 24733
File: apps/admin-x-activitypub/src/api/activitypub.test.ts:1601-1627
Timestamp: 2025-08-22T08:55:11.602Z
Learning: In the Ghost ActivityPub module, `disableBluesky()` uses direct fetch where auth headers are manually passed, while `enableBluesky()` uses `fetchJSON` which abstracts auth header handling away. This is why the disable test needs to assert Authorization headers but the enable test doesn't.
Learnt from: mike182uk
Repo: TryGhost/Ghost PR: 22471
File: apps/admin-x-activitypub/src/utils/pending-activity.ts:13-71
Timestamp: 2025-03-13T09:00:20.205Z
Learning: The pending activity utilities in the Ghost ActivityPub module are covered by tests in the file `apps/admin-x-activitypub/test/unit/utils/pending-activity.ts`.
Learnt from: mike182uk
Repo: TryGhost/Ghost PR: 22471
File: apps/admin-x-activitypub/src/views/Feed/components/NewPostModal.tsx:29-34
Timestamp: 2025-03-13T09:02:50.102Z
Learning: In the ActivityPub module, error handling for mutations is handled at the hook level (in use-activity-pub-queries.ts) rather than in individual components. This allows for centralized error handling across the application.
Learnt from: mike182uk
Repo: TryGhost/Ghost PR: 22471
File: apps/admin-x-activitypub/src/views/Feed/components/NewPostModal.tsx:29-34
Timestamp: 2025-03-13T09:02:50.102Z
Learning: In the Ghost ActivityPub module, error handling for mutations is handled at the hook level (in use-activity-pub-queries.ts) rather than in individual components. This allows for centralized error handling across the application.
📚 Learning: 2025-08-22T08:55:11.602Z
Learnt from: mike182uk
Repo: TryGhost/Ghost PR: 24733
File: apps/admin-x-activitypub/src/api/activitypub.test.ts:1601-1627
Timestamp: 2025-08-22T08:55:11.602Z
Learning: In the Ghost ActivityPub module, `disableBluesky()` uses direct fetch where auth headers are manually passed, while `enableBluesky()` uses `fetchJSON` which abstracts auth header handling away. This is why the disable test needs to assert Authorization headers but the enable test doesn't.

Applied to files:

  • apps/activitypub/src/api/activitypub.test.ts
  • apps/activitypub/src/api/activitypub.ts
  • apps/activitypub/src/hooks/use-activity-pub-queries.ts
📚 Learning: 2025-03-13T09:00:20.205Z
Learnt from: mike182uk
Repo: TryGhost/Ghost PR: 22471
File: apps/admin-x-activitypub/src/utils/pending-activity.ts:13-71
Timestamp: 2025-03-13T09:00:20.205Z
Learning: The pending activity utilities in the Ghost ActivityPub module are covered by tests in the file `apps/admin-x-activitypub/test/unit/utils/pending-activity.ts`.

Applied to files:

  • apps/activitypub/src/api/activitypub.test.ts
  • apps/activitypub/package.json
📚 Learning: 2025-03-13T09:00:20.205Z
Learnt from: mike182uk
Repo: TryGhost/Ghost PR: 22471
File: apps/admin-x-activitypub/src/utils/pending-activity.ts:13-71
Timestamp: 2025-03-13T09:00:20.205Z
Learning: The pending activity utilities in Ghost's ActivityPub module are thoroughly tested in `apps/admin-x-activitypub/test/unit/utils/pending-activity.ts`, which covers `generatePendingActivityId`, `isPendingActivity`, and `generatePendingActivity` functions.

Applied to files:

  • apps/activitypub/src/api/activitypub.test.ts
  • apps/activitypub/package.json
📚 Learning: 2025-03-13T09:02:50.102Z
Learnt from: mike182uk
Repo: TryGhost/Ghost PR: 22471
File: apps/admin-x-activitypub/src/views/Feed/components/NewPostModal.tsx:29-34
Timestamp: 2025-03-13T09:02:50.102Z
Learning: In the Ghost ActivityPub module, error handling for mutations is handled at the hook level (in use-activity-pub-queries.ts) rather than in individual components. This allows for centralized error handling across the application.

Applied to files:

  • apps/activitypub/src/api/activitypub.test.ts
  • apps/activitypub/src/views/Preferences/components/BlueskySharing.tsx
  • apps/activitypub/src/hooks/use-activity-pub-queries.ts
📚 Learning: 2025-05-29T10:37:26.369Z
Learnt from: ErisDS
Repo: TryGhost/Ghost PR: 23588
File: ghost/core/test/e2e-api/admin/backup.test.js:136-148
Timestamp: 2025-05-29T10:37:26.369Z
Learning: In the Ghost test framework, when testing CSV exports via the admin API, the response `body` field is empty while the actual CSV data is provided through the `text` field. Tests should use `expectEmptyBody()` and then validate the CSV content via `.expect(({text}) => ...)` - this is not contradictory behavior.

Applied to files:

  • apps/activitypub/src/api/activitypub.test.ts
📚 Learning: 2025-07-14T12:20:35.268Z
Learnt from: sagzy
Repo: TryGhost/Ghost PR: 24346
File: apps/admin-x-settings/src/components/settings/growth/Network.tsx:8-12
Timestamp: 2025-07-14T12:20:35.268Z
Learning: The Network component toggle in `apps/admin-x-settings/src/components/settings/growth/Network.tsx` is intentionally implemented as static UI with a no-op onChange handler, as part of a UI-first development approach before connecting actual ActivityPub functionality.

Applied to files:

  • apps/activitypub/src/views/Preferences/components/BlueskySharing.tsx
📚 Learning: 2025-03-13T09:02:50.102Z
Learnt from: mike182uk
Repo: TryGhost/Ghost PR: 22471
File: apps/admin-x-activitypub/src/views/Feed/components/NewPostModal.tsx:29-34
Timestamp: 2025-03-13T09:02:50.102Z
Learning: In the ActivityPub module, error handling for mutations is handled at the hook level (in use-activity-pub-queries.ts) rather than in individual components. This allows for centralized error handling across the application.

Applied to files:

  • apps/activitypub/src/views/Preferences/components/BlueskySharing.tsx
  • apps/activitypub/src/hooks/use-activity-pub-queries.ts
📚 Learning: 2025-08-11T19:39:00.428Z
Learnt from: kevinansfield
Repo: TryGhost/Ghost PR: 24651
File: ghost/core/test/utils/urlUtils.js:53-57
Timestamp: 2025-08-11T19:39:00.428Z
Learning: In Ghost's test utilities, when fixing specific issues like async behavior, it's preferred to maintain existing error handling patterns (even if suboptimal) to keep PRs focused on their primary objective. Error handling improvements can be addressed in separate, dedicated PRs.

Applied to files:

  • apps/activitypub/src/views/Preferences/components/BlueskySharing.tsx
📚 Learning: 2025-11-05T16:49:21.124Z
Learnt from: rob-ghost
Repo: TryGhost/Ghost PR: 25356
File: apps/admin/src/hooks/user-preferences.ts:20-20
Timestamp: 2025-11-05T16:49:21.124Z
Learning: In apps/admin/src/hooks/user-preferences.ts, the query key for user preferences intentionally includes `user?.accessibility` (not just `user?.id`) so the cache automatically reacts to changes from any source (useEditUserPreferences, other editUser calls, external updates). Combined with `cacheTime: 0`, this design garbage-collects orphaned entries while keeping the active entry cached. This is a documented, intentional architectural decision.

Applied to files:

  • apps/activitypub/src/hooks/use-activity-pub-queries.ts
📚 Learning: 2025-11-10T17:07:54.169Z
Learnt from: kevinansfield
Repo: TryGhost/Ghost PR: 25377
File: apps/admin/src/ember-bridge/EmberBridge.test.tsx:146-174
Timestamp: 2025-11-10T17:07:54.169Z
Learning: In apps/admin/src/ember-bridge/EmberBridge.tsx, the useEmberAuthSync hook invalidates all React Query caches whenever an emberAuthChange event is emitted, regardless of the isAuthenticated field value in the event payload. The isAuthenticated field exists in the event type but is not currently used by the handler.

Applied to files:

  • apps/activitypub/src/hooks/use-activity-pub-queries.ts
🧬 Code graph analysis (2)
apps/activitypub/src/api/activitypub.test.ts (1)
apps/activitypub/src/api/activitypub.ts (1)
  • ActivityPubAPI (230-734)
apps/activitypub/src/views/Preferences/components/BlueskySharing.tsx (1)
apps/activitypub/src/hooks/use-activity-pub-queries.ts (4)
  • useAccountForUser (1616-1628)
  • useEnableBlueskyMutationForUser (2746-2776)
  • useDisableBlueskyMutationForUser (2778-2808)
  • useConfirmBlueskyHandleMutationForUser (2810-2839)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Cursor Bugbot
🔇 Additional comments (3)
apps/activitypub/package.json (1)

3-3: Version bump to 2.0.0 looks consistent with API changes.

The package version aligns with the new Bluesky v2 API and confirm-handle additions; no issues from this file alone.

apps/activitypub/src/api/activitypub.ts (1)

29-30: Bluesky v2 API methods and Account fields look coherent.

Account’s optional blueskyHandleConfirmed and nullable blueskyHandle match how the hooks/UI treat “enabled but unconfirmed” vs confirmed. The v2 enableBluesky/disableBluesky POSTs reusing fetchJSON and confirmBlueskyHandle’s empty-string fallback on null/invalid payloads all look correct and are well-covered by tests.

Also applies to: 711-733

apps/activitypub/src/api/activitypub.test.ts (1)

1610-1655: enable/disable Bluesky v2 tests align with new API shape.

The tests now assert v2 endpoints, POST method, and that both enableBluesky and disableBluesky resolve without a return value, matching the new implementation that delegates auth to fetchJSON.

mike182uk and others added 5 commits November 20, 2025 12:07
ref TryGhost/ActivityPub#1377

We updated the backend implementation of the Bluesky sharing enablement process
to include a confirmation step in which the client would prompt the backend
to check what handle has been assigned to the Bluesky account generated by
Bridgy Fed - This circumvents the need to for us to compute the handle manually
and potentially getting it wrong if we compute it in a different way to Bridgy
Fed
@mike182uk mike182uk force-pushed the ap-bluesky-handle-confirmation branch from b8dd7a6 to c743df5 Compare November 20, 2025 12:07
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

♻️ Duplicate comments (2)
apps/activitypub/src/hooks/use-activity-pub-queries.ts (1)

2725-2839: Tighten Bluesky cache typing to match Account and avoid narrowing.

updateAccountBlueskyCache operates on the Account('index') cache entry but types both BlueskyDetails and currentProfile as the three-field subset. That works at runtime but weakens safety and makes it easier to drift from the real Account shape.

Consider aligning the types with Account directly:

-type BlueskyDetails = {
-    blueskyEnabled: boolean;
-    blueskyHandleConfirmed: boolean;
-    blueskyHandle: string | null;
-}
+type BlueskyDetails = Pick<Account, 'blueskyEnabled' | 'blueskyHandleConfirmed' | 'blueskyHandle'>;

-function updateAccountBlueskyCache(queryClient: QueryClient, blueskyDetails: BlueskyDetails) {
+function updateAccountBlueskyCache(
+    queryClient: QueryClient,
+    blueskyDetails: BlueskyDetails
+) {
     const profileQueryKey = QUERY_KEYS.account('index');

-    queryClient.setQueryData(profileQueryKey, (currentProfile?: BlueskyDetails) => {
+    queryClient.setQueryData(profileQueryKey, (currentProfile?: Account) => {
         if (!currentProfile) {
             return currentProfile;
         }

         return {
             ...currentProfile,
             ...blueskyDetails
         };
     });
 }

This keeps the helper focused on Bluesky-specific fields while preserving the full Account type in the cache.

apps/activitypub/src/views/Preferences/components/BlueskySharing.tsx (1)

73-80: Handle mutateAsync rejections in polling to avoid unhandled promises and stuck loading.

Two places in the polling flow can produce unhandled rejections and inconsistent UI state:

  • confirmHandle calls confirmBlueskyHandleMutation.mutateAsync().then(...) with no .catch, so any API error during polling bubbles as an unhandled rejection (especially problematic inside setInterval).
  • In the retry timeout path, await disableBlueskyMutation.mutateAsync(); is not wrapped, so if disable fails, setLoading(false) never runs and the user can remain stuck in a loading state even though polling has stopped.

You can make both paths safer by converting to async+try/catch/finally and letting the hook-level onError own user-facing error messaging:

-    const confirmHandle = useCallback(() => {
-        confirmBlueskyHandleMutation.mutateAsync().then((handle) => {
-            if (handle) {
-                setHandleConfirmed(true);
-            }
-        });
-        // eslint-disable-next-line react-hooks/exhaustive-deps
-    }, []); // Empty deps - mutations are stable in practice
+    const confirmHandle = useCallback(async () => {
+        try {
+            const handle = await confirmBlueskyHandleMutation.mutateAsync();
+            if (handle) {
+                setHandleConfirmed(true);
+            }
+        } catch {
+            // onError in the mutation already surfaces rate-limit errors; swallow here
+        }
+    }, [confirmBlueskyHandleMutation]);

@@
-        const confirmHandleInterval = setInterval(async () => {
-            retryCountRef.current += 1;
-
-            if (retryCountRef.current > MAX_CONFIRMATION_RETRIES) {
-                clearInterval(confirmHandleInterval);
-
-                toast.error('Something went wrong, please try again.');
-
-                await disableBlueskyMutation.mutateAsync();
-                setLoading(false);
-
-                return;
-            }
-
-            confirmHandle();
-        }, CONFIRMATION_INTERVAL);
+        const confirmHandleInterval = setInterval(async () => {
+            retryCountRef.current += 1;
+
+            if (retryCountRef.current > MAX_CONFIRMATION_RETRIES) {
+                clearInterval(confirmHandleInterval);
+
+                toast.error('Something went wrong, please try again.');
+
+                try {
+                    await disableBlueskyMutation.mutateAsync();
+                } catch {
+                    // disable hook handles its own error toasts
+                } finally {
+                    setLoading(false);
+                }
+
+                return;
+            }
+
+            await confirmHandle();
+        }, CONFIRMATION_INTERVAL);

This keeps the polling logic robust without changing the external behavior or toast contract.

Also applies to: 108-127

🧹 Nitpick comments (1)
apps/activitypub/src/views/Preferences/components/BlueskySharing.tsx (1)

48-58: Avoid double toasts by delegating enable failures to the mutation hook.

handleEnable catches all errors from enableBlueskyMutation.mutateAsync() and shows a generic “Something went wrong” toast, but the hook’s onError already renders a rate‑limit toast for 429 responses. For 429s this results in two error toasts for a single failure.

You could either remove the component‑level toast or only show it for non‑429 errors:

-        try {
-            await enableBlueskyMutation.mutateAsync();
-        } catch (error) {
-            setLoading(false);
-            toast.error('Something went wrong, please try again.');
-        }
+        try {
+            await enableBlueskyMutation.mutateAsync();
+        } catch (error: any) {
+            setLoading(false);
+            if (error?.statusCode !== 429) {
+                toast.error('Something went wrong, please try again.');
+            }
+        }

Keeps the “centralized error handling in hooks” pattern while still giving a fallback message for unexpected failures.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b8dd7a6 and c743df5.

📒 Files selected for processing (5)
  • apps/activitypub/package.json (1 hunks)
  • apps/activitypub/src/api/activitypub.test.ts (10 hunks)
  • apps/activitypub/src/api/activitypub.ts (2 hunks)
  • apps/activitypub/src/hooks/use-activity-pub-queries.ts (4 hunks)
  • apps/activitypub/src/views/Preferences/components/BlueskySharing.tsx (6 hunks)
✅ Files skipped from review due to trivial changes (1)
  • apps/activitypub/package.json
🧰 Additional context used
🧠 Learnings (9)
📓 Common learnings
Learnt from: mike182uk
Repo: TryGhost/Ghost PR: 24733
File: apps/admin-x-activitypub/src/api/activitypub.test.ts:1601-1627
Timestamp: 2025-08-22T08:55:11.602Z
Learning: In the Ghost ActivityPub module, `disableBluesky()` uses direct fetch where auth headers are manually passed, while `enableBluesky()` uses `fetchJSON` which abstracts auth header handling away. This is why the disable test needs to assert Authorization headers but the enable test doesn't.
Learnt from: mike182uk
Repo: TryGhost/Ghost PR: 22471
File: apps/admin-x-activitypub/src/utils/pending-activity.ts:13-71
Timestamp: 2025-03-13T09:00:20.205Z
Learning: The pending activity utilities in the Ghost ActivityPub module are covered by tests in the file `apps/admin-x-activitypub/test/unit/utils/pending-activity.ts`.
Learnt from: mike182uk
Repo: TryGhost/Ghost PR: 22471
File: apps/admin-x-activitypub/src/views/Feed/components/NewPostModal.tsx:29-34
Timestamp: 2025-03-13T09:02:50.102Z
Learning: In the ActivityPub module, error handling for mutations is handled at the hook level (in use-activity-pub-queries.ts) rather than in individual components. This allows for centralized error handling across the application.
Learnt from: mike182uk
Repo: TryGhost/Ghost PR: 22471
File: apps/admin-x-activitypub/src/views/Feed/components/NewPostModal.tsx:29-34
Timestamp: 2025-03-13T09:02:50.102Z
Learning: In the Ghost ActivityPub module, error handling for mutations is handled at the hook level (in use-activity-pub-queries.ts) rather than in individual components. This allows for centralized error handling across the application.
📚 Learning: 2025-08-22T08:55:11.602Z
Learnt from: mike182uk
Repo: TryGhost/Ghost PR: 24733
File: apps/admin-x-activitypub/src/api/activitypub.test.ts:1601-1627
Timestamp: 2025-08-22T08:55:11.602Z
Learning: In the Ghost ActivityPub module, `disableBluesky()` uses direct fetch where auth headers are manually passed, while `enableBluesky()` uses `fetchJSON` which abstracts auth header handling away. This is why the disable test needs to assert Authorization headers but the enable test doesn't.

Applied to files:

  • apps/activitypub/src/api/activitypub.ts
  • apps/activitypub/src/api/activitypub.test.ts
  • apps/activitypub/src/hooks/use-activity-pub-queries.ts
📚 Learning: 2025-03-13T09:00:20.205Z
Learnt from: mike182uk
Repo: TryGhost/Ghost PR: 22471
File: apps/admin-x-activitypub/src/utils/pending-activity.ts:13-71
Timestamp: 2025-03-13T09:00:20.205Z
Learning: The pending activity utilities in the Ghost ActivityPub module are covered by tests in the file `apps/admin-x-activitypub/test/unit/utils/pending-activity.ts`.

Applied to files:

  • apps/activitypub/src/api/activitypub.test.ts
📚 Learning: 2025-03-13T09:00:20.205Z
Learnt from: mike182uk
Repo: TryGhost/Ghost PR: 22471
File: apps/admin-x-activitypub/src/utils/pending-activity.ts:13-71
Timestamp: 2025-03-13T09:00:20.205Z
Learning: The pending activity utilities in Ghost's ActivityPub module are thoroughly tested in `apps/admin-x-activitypub/test/unit/utils/pending-activity.ts`, which covers `generatePendingActivityId`, `isPendingActivity`, and `generatePendingActivity` functions.

Applied to files:

  • apps/activitypub/src/api/activitypub.test.ts
📚 Learning: 2025-03-13T09:02:50.102Z
Learnt from: mike182uk
Repo: TryGhost/Ghost PR: 22471
File: apps/admin-x-activitypub/src/views/Feed/components/NewPostModal.tsx:29-34
Timestamp: 2025-03-13T09:02:50.102Z
Learning: In the Ghost ActivityPub module, error handling for mutations is handled at the hook level (in use-activity-pub-queries.ts) rather than in individual components. This allows for centralized error handling across the application.

Applied to files:

  • apps/activitypub/src/api/activitypub.test.ts
  • apps/activitypub/src/hooks/use-activity-pub-queries.ts
  • apps/activitypub/src/views/Preferences/components/BlueskySharing.tsx
📚 Learning: 2025-05-29T10:37:26.369Z
Learnt from: ErisDS
Repo: TryGhost/Ghost PR: 23588
File: ghost/core/test/e2e-api/admin/backup.test.js:136-148
Timestamp: 2025-05-29T10:37:26.369Z
Learning: In the Ghost test framework, when testing CSV exports via the admin API, the response `body` field is empty while the actual CSV data is provided through the `text` field. Tests should use `expectEmptyBody()` and then validate the CSV content via `.expect(({text}) => ...)` - this is not contradictory behavior.

Applied to files:

  • apps/activitypub/src/api/activitypub.test.ts
📚 Learning: 2025-03-13T09:02:50.102Z
Learnt from: mike182uk
Repo: TryGhost/Ghost PR: 22471
File: apps/admin-x-activitypub/src/views/Feed/components/NewPostModal.tsx:29-34
Timestamp: 2025-03-13T09:02:50.102Z
Learning: In the ActivityPub module, error handling for mutations is handled at the hook level (in use-activity-pub-queries.ts) rather than in individual components. This allows for centralized error handling across the application.

Applied to files:

  • apps/activitypub/src/hooks/use-activity-pub-queries.ts
  • apps/activitypub/src/views/Preferences/components/BlueskySharing.tsx
📚 Learning: 2025-11-05T16:49:21.124Z
Learnt from: rob-ghost
Repo: TryGhost/Ghost PR: 25356
File: apps/admin/src/hooks/user-preferences.ts:20-20
Timestamp: 2025-11-05T16:49:21.124Z
Learning: In apps/admin/src/hooks/user-preferences.ts, the query key for user preferences intentionally includes `user?.accessibility` (not just `user?.id`) so the cache automatically reacts to changes from any source (useEditUserPreferences, other editUser calls, external updates). Combined with `cacheTime: 0`, this design garbage-collects orphaned entries while keeping the active entry cached. This is a documented, intentional architectural decision.

Applied to files:

  • apps/activitypub/src/hooks/use-activity-pub-queries.ts
📚 Learning: 2025-08-11T19:39:00.428Z
Learnt from: kevinansfield
Repo: TryGhost/Ghost PR: 24651
File: ghost/core/test/utils/urlUtils.js:53-57
Timestamp: 2025-08-11T19:39:00.428Z
Learning: In Ghost's test utilities, when fixing specific issues like async behavior, it's preferred to maintain existing error handling patterns (even if suboptimal) to keep PRs focused on their primary objective. Error handling improvements can be addressed in separate, dedicated PRs.

Applied to files:

  • apps/activitypub/src/views/Preferences/components/BlueskySharing.tsx
🧬 Code graph analysis (2)
apps/activitypub/src/api/activitypub.test.ts (1)
apps/activitypub/src/api/activitypub.ts (1)
  • ActivityPubAPI (230-734)
apps/activitypub/src/views/Preferences/components/BlueskySharing.tsx (1)
apps/activitypub/src/hooks/use-activity-pub-queries.ts (4)
  • useAccountForUser (1616-1628)
  • useEnableBlueskyMutationForUser (2746-2776)
  • useDisableBlueskyMutationForUser (2778-2808)
  • useConfirmBlueskyHandleMutationForUser (2810-2839)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
  • GitHub Check: Cursor Bugbot
  • GitHub Check: Setup
  • GitHub Check: Build & Push
  • GitHub Check: Setup
🔇 Additional comments (2)
apps/activitypub/src/api/activitypub.ts (1)

29-30: Bluesky account fields and v2 actions look consistent and defensive.

The Account bluesky fields and the v2 enableBluesky/disableBluesky/confirmBlueskyHandle implementations align with the described API: endpoints are correct, enable/disable are POST + void, and confirmBlueskyHandle safely normalizes unexpected/empty responses to '' so callers don’t need to null-check.

Also applies to: 711-733

apps/activitypub/src/api/activitypub.test.ts (1)

1600-1627: Tests correctly cover v2 Bluesky flows including confirm-handle edge cases.

The updated tests target the v2 endpoints, assert POST usage for enable/disable, and exercise confirmBlueskyHandle across valid, null, and malformed responses, matching the new API semantics and guarding against unexpected payloads.

Also applies to: 1629-1656, 1658-1768

@mike182uk mike182uk merged commit fea64ef into main Nov 20, 2025
45 of 48 checks passed
@mike182uk mike182uk deleted the ap-bluesky-handle-confirmation branch November 20, 2025 13:26
cmraible pushed a commit that referenced this pull request Dec 1, 2025
ref TryGhost/ActivityPub#1377

We updated the backend implementation of the Bluesky sharing enablement
process to include a confirmation step in which the client would prompt
the backend to check what handle has been assigned to the Bluesky
account generated by Bridgy Fed - This circumvents the need to for us to
compute the handle manually and potentially getting it wrong if we
compute it in a different way to Bridgy Fed

---------

Co-authored-by: Sodbileg Gansukh <sodbileg.gansukh@gmail.com>
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.

2 participants

Comments