quote posts

This commit is contained in:
Moon.eth
2026-07-30 19:45:40 +09:00
parent 49c9c2ba57
commit 657d517503
22 changed files with 869 additions and 14 deletions
+53
View File
@@ -2,6 +2,7 @@ import { afterEach, describe, expect, it, vi } from 'vitest'
import { ApiClient } from './client'
import {
fetchNotifications,
fetchQuotes,
postStatus,
setEmojiReaction,
updateProfileFields,
@@ -181,6 +182,58 @@ describe('emoji reaction endpoints', () => {
})
})
describe('quote endpoints', () => {
it('sends both current and legacy quote parameters when composing', async () => {
const fetchMock = vi.fn(async (_url: string, init?: RequestInit) => {
expect(JSON.parse(String(init?.body))).toMatchObject({
status: 'Commentary',
quoted_status_id: 'quoted/one',
quote_id: 'quoted/one',
})
return new Response(JSON.stringify({ id: 'quote-1' }), {
status: 200,
headers: { 'Content-Type': 'application/json' },
})
})
vi.stubGlobal('fetch', fetchMock)
await postStatus(new ApiClient('example.test', 'token'), {
status: 'Commentary',
quoted_status_id: 'quoted/one',
})
})
it('falls back to the legacy Pleroma quote-list endpoint', async () => {
const fetchMock = vi
.fn()
.mockResolvedValueOnce(
new Response(JSON.stringify({ error: 'Not found' }), {
status: 404,
headers: { 'Content-Type': 'application/json' },
}),
)
.mockResolvedValueOnce(
new Response(JSON.stringify([{ id: 'quote-1' }]), {
status: 200,
headers: { 'Content-Type': 'application/json' },
}),
)
vi.stubGlobal('fetch', fetchMock)
const page = await fetchQuotes(new ApiClient('old-pleroma.example', 'token'), 'status/one', {
limit: 20,
})
expect(page.items).toEqual([{ id: 'quote-1' }])
expect(fetchMock.mock.calls[0][0]).toBe(
'https://old-pleroma.example/api/v1/statuses/status%2Fone/quotes?limit=20',
)
expect(fetchMock.mock.calls[1][0]).toBe(
'https://old-pleroma.example/api/v1/pleroma/statuses/status%2Fone/quotes?limit=20',
)
})
})
describe('fetchNotifications', () => {
it('defensively applies requested types when a server ignores the filter', async () => {
vi.stubGlobal(
+21
View File
@@ -283,6 +283,7 @@ export interface ComposeOptions {
expires_in: number
multiple: boolean
}
quoted_status_id?: string
}
export function postStatus(api: ApiClient, options: ComposeOptions): Promise<Status> {
@@ -297,9 +298,29 @@ export function postStatus(api: ApiClient, options: ComposeOptions): Promise<Sta
if (options.media_ids?.length) body.media_ids = options.media_ids
if (options.language) body.language = options.language
if (options.poll) body.poll = options.poll
if (options.quoted_status_id) {
body.quoted_status_id = options.quoted_status_id
// Older Pleroma/Akkoma releases predate the standardized Mastodon name.
// Mastodon ignores unknown JSON keys, while those servers require this.
body.quote_id = options.quoted_status_id
}
return api.post<Status>('/api/v1/statuses', body)
}
export async function fetchQuotes(
api: ApiClient,
id: string,
cursor: Cursor = {},
): Promise<Page<Status>> {
const encoded = encodeURIComponent(id)
try {
return await api.page<Status>(`/api/v1/statuses/${encoded}/quotes`, { ...cursor })
} catch (cause) {
if (!(cause instanceof ApiError) || (cause.status !== 404 && cause.status !== 405)) throw cause
return api.page<Status>(`/api/v1/pleroma/statuses/${encoded}/quotes`, { ...cursor })
}
}
export function deleteStatus(api: ApiClient, id: string): Promise<Status> {
return api.delete<Status>(`/api/v1/statuses/${encodeURIComponent(id)}`)
}
+40
View File
@@ -178,6 +178,32 @@ export interface EmojiReaction {
accounts?: Account[]
}
export type QuoteState =
| 'pending'
| 'accepted'
| 'rejected'
| 'revoked'
| 'deleted'
| 'unauthorized'
| 'blocked_account'
| 'blocked_domain'
| 'muted_account'
| string
/** Mastodon 4.4+ quote envelope. Pleroma places the Status under `pleroma.quote`. */
export interface StatusQuote {
state: QuoteState
quoted_status?: Status | null
/** Mastodon ShallowQuote form. */
quoted_status_id?: string | null
}
export interface QuoteApproval {
automatic: string[]
manual: string[]
current_user: 'automatic' | 'manual' | 'denied' | 'unknown' | string
}
export interface Status {
id: string
uri: string
@@ -198,6 +224,7 @@ export interface Status {
replies_count: number
reblogs_count: number
favourites_count: number
quotes_count?: number
media_attachments: MediaAttachment[]
mentions: StatusMention[]
@@ -205,6 +232,12 @@ export interface Status {
emojis: CustomEmoji[]
card?: PreviewCard | null
poll?: Poll | null
/** Mastodon quote envelope, or a raw quoted Status on a few compatible forks. */
quote?: StatusQuote | Status | null
quote_approval?: QuoteApproval | null
/** Compatibility fields returned at top-level by some Pleroma-family forks. */
quote_id?: string | null
quote_url?: string | null
application?: { name: string; website?: string | null } | null
reblog: Status | null
@@ -222,6 +255,11 @@ export interface Status {
spoiler_text?: Record<string, string>
/** Pleroma/Akkoma emoji reactions, including custom emoji URLs. */
emoji_reactions?: EmojiReaction[]
quote?: Status | null
quote_id?: string | null
quote_url?: string | null
quote_visible?: boolean
quotes_count?: number
}
}
@@ -250,6 +288,8 @@ export type NotificationType =
| 'favourite'
| 'poll'
| 'update'
| 'quote'
| 'quoted_update'
| 'admin.sign_up'
| 'admin.report'
| 'pleroma:emoji_reaction'