feat: load tweet conversations
This commit is contained in:
@@ -1,10 +1,23 @@
|
||||
import type { SearchProduct, SearchResult, TweetData } from '@yuta/bird'
|
||||
import { buildSearchQuery, InputError, normalizeUserTarget } from './inputs'
|
||||
import type {
|
||||
GetTweetResult,
|
||||
SearchProduct,
|
||||
SearchResult,
|
||||
TweetData,
|
||||
} from '@yuta/bird'
|
||||
import {
|
||||
buildSearchQuery,
|
||||
InputError,
|
||||
normalizeTweetId,
|
||||
normalizeUserTarget,
|
||||
} from './inputs'
|
||||
import type {
|
||||
LoadError,
|
||||
LoadFailure,
|
||||
LoadResult,
|
||||
Post,
|
||||
SearchPageInput,
|
||||
ThreadLoadResult,
|
||||
ThreadPageInput,
|
||||
UserPageInput,
|
||||
} from './types'
|
||||
|
||||
@@ -15,6 +28,15 @@ type UserLookupResult = {
|
||||
}
|
||||
|
||||
export interface BirdReader {
|
||||
getTweet(tweetId: string): Promise<GetTweetResult>
|
||||
getThreadPaged(
|
||||
tweetId: string,
|
||||
options: {
|
||||
cursor?: string
|
||||
maxPages: number
|
||||
pageDelayMs: number
|
||||
},
|
||||
): Promise<SearchResult>
|
||||
getUserIdByUsername(username: string): Promise<UserLookupResult>
|
||||
getUserTweetsPaged(
|
||||
userId: string,
|
||||
@@ -39,9 +61,12 @@ const failure = (
|
||||
code: LoadError['code'],
|
||||
message: string,
|
||||
retryable: boolean,
|
||||
): LoadResult => ({ ok: false, error: { code, message, retryable } })
|
||||
): LoadFailure => ({ ok: false, error: { code, message, retryable } })
|
||||
|
||||
function upstreamFailure(diagnostic: unknown = ''): LoadResult {
|
||||
function upstreamFailure(
|
||||
diagnostic: unknown = '',
|
||||
subject: 'user' | 'post' = 'user',
|
||||
): LoadFailure {
|
||||
const message =
|
||||
diagnostic instanceof Error
|
||||
? diagnostic.message
|
||||
@@ -53,14 +78,18 @@ function upstreamFailure(diagnostic: unknown = ''): LoadResult {
|
||||
return failure('timeout', '取得がタイムアウトしました。', true)
|
||||
}
|
||||
if (lower.includes('not found')) {
|
||||
return failure('user-not-found', 'ユーザーが見つかりませんでした。', false)
|
||||
return subject === 'post'
|
||||
? failure('post-not-found', '投稿が見つかりませんでした。', false)
|
||||
: failure('user-not-found', 'ユーザーが見つかりませんでした。', false)
|
||||
}
|
||||
if (lower.includes('suspended') || lower.includes('protected')) {
|
||||
return failure(
|
||||
'user-unavailable',
|
||||
'このユーザーの投稿は取得できません。',
|
||||
false,
|
||||
)
|
||||
return subject === 'post'
|
||||
? failure('post-unavailable', 'この投稿は取得できません。', false)
|
||||
: failure(
|
||||
'user-unavailable',
|
||||
'このユーザーの投稿は取得できません。',
|
||||
false,
|
||||
)
|
||||
}
|
||||
console.error('Unexpected Bird failure:', diagnostic)
|
||||
return failure('upstream', 'X から投稿を取得できませんでした。', true)
|
||||
@@ -87,6 +116,54 @@ function resultPage(result: SearchResult): LoadResult {
|
||||
: upstreamFailure(result.error)
|
||||
}
|
||||
|
||||
export async function loadThreadPage(
|
||||
reader: BirdReader,
|
||||
input: ThreadPageInput,
|
||||
): Promise<ThreadLoadResult> {
|
||||
try {
|
||||
const tweetId = normalizeTweetId(input.tweetId)
|
||||
let focalPost: Post | undefined
|
||||
let conversationId: string
|
||||
|
||||
if (input.cursor && input.conversationId) {
|
||||
conversationId = normalizeTweetId(input.conversationId)
|
||||
} else {
|
||||
const focal = await reader.getTweet(tweetId)
|
||||
if (!focal.success || !focal.tweet) {
|
||||
return upstreamFailure(focal.error ?? 'Tweet not found', 'post')
|
||||
}
|
||||
focalPost = publicPost(focal.tweet)
|
||||
conversationId = normalizeTweetId(
|
||||
focal.tweet.conversationId ?? focal.tweet.id,
|
||||
)
|
||||
}
|
||||
|
||||
const result = await reader.getThreadPaged(conversationId, {
|
||||
...(input.cursor ? { cursor: input.cursor } : {}),
|
||||
maxPages: 1,
|
||||
pageDelayMs: 0,
|
||||
})
|
||||
if (!result.success) {
|
||||
return upstreamFailure(result.error, 'post')
|
||||
}
|
||||
|
||||
return {
|
||||
ok: true,
|
||||
page: {
|
||||
tweets: (result.tweets ?? []).map(publicPost),
|
||||
focalPost,
|
||||
conversationId,
|
||||
nextCursor: result.nextCursor,
|
||||
},
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof InputError) {
|
||||
return failure('invalid-input', error.message, false)
|
||||
}
|
||||
return upstreamFailure(error, 'post')
|
||||
}
|
||||
}
|
||||
|
||||
export async function loadUserPage(
|
||||
reader: BirdReader,
|
||||
input: UserPageInput,
|
||||
|
||||
Reference in New Issue
Block a user