Atproto AMA app
1/**
2 * Source integration logic for handling questions and answers from different platforms
3 */
4
5import { SOURCE_TYPES, type SourceType } from './shared-schemas'
6import {
7 createQuestionFromBlueSky,
8 createAnswerFromBlueSky,
9 createQuestionFromStandardSite,
10 detectSourceType
11} from './schema-bridge'
12import { db } from './db'
13import { users, questions } from './schema'
14import { eq } from 'drizzle-orm'
15
16/**
17 * Interface for external post data
18 */
19export interface ExternalPost {
20 uri: string
21 content: string
22 authorDid: string
23 createdAt: string
24 platform: string
25 metadata?: Record<string, unknown>
26}
27
28/**
29 * Interface for Bluesky post data
30 */
31export interface BlueSkyPost extends ExternalPost {
32 platform: 'bluesky'
33 bskyData: {
34 uri: string
35 cid: string
36 record: Record<string, unknown>
37 author: {
38 did: string
39 handle: string
40 displayName?: string
41 }
42 }
43}
44
45/**
46 * Interface for standard.site post data
47 */
48export interface StandardSitePost extends ExternalPost {
49 platform: 'standard.site'
50 standardSiteData: {
51 url: string
52 schema: Record<string, unknown>
53 author: {
54 did: string
55 name?: string
56 }
57 }
58}
59
60/**
61 * Parse a Bluesky post to extract question content
62 */
63export function parseBlueSkyQuestion(post: BlueSkyPost): {
64 content: string
65 targetDid?: string
66 isQuestion: boolean
67} {
68 const text = post.content
69
70 // Look for question patterns
71 const hasQuestionMark = text.includes('?')
72 const hasQuestionWords = /\b(what|how|why|when|where|who|which|can|could|would|should|is|are|do|does|did)\b/i.test(text)
73
74 // Look for mentions that could be the target
75 const mentionRegex = /@([a-zA-Z0-9.-]+)/g
76 const mentions = Array.from(text.matchAll(mentionRegex))
77
78 // Extract target DID from mentions (this would need to be resolved via AT Protocol)
79 let targetDid: string | undefined
80 if (mentions.length > 0) {
81 // In a real implementation, you'd resolve the handle to a DID
82 // For now, we'll use a placeholder
83 targetDid = `did:plc:${mentions[0][1].replace('.', '')}`
84 }
85
86 return {
87 content: text,
88 targetDid,
89 isQuestion: hasQuestionMark || hasQuestionWords
90 }
91}
92
93/**
94 * Parse a standard.site post to extract question content
95 */
96export function parseStandardSiteQuestion(post: StandardSitePost): {
97 content: string
98 targetDid?: string
99 isQuestion: boolean
100} {
101 const schema = post.standardSiteData.schema
102
103 // Check if it's a Question schema.org type
104 const isQuestion = schema['@type'] === 'Question'
105
106 let content = post.content
107 let targetDid: string | undefined
108
109 if (isQuestion && schema.text) {
110 content = schema.text as string
111 }
112
113 // Look for target in acceptedAnswer or other fields
114 if (schema.acceptedAnswer && typeof schema.acceptedAnswer === 'object') {
115 const answer = schema.acceptedAnswer as Record<string, unknown>
116 if (answer.author && typeof answer.author === 'object') {
117 const author = answer.author as Record<string, unknown>
118 if (author.identifier && typeof author.identifier === 'string') {
119 targetDid = author.identifier
120 }
121 }
122 }
123
124 return {
125 content,
126 targetDid,
127 isQuestion
128 }
129}
130
131/**
132 * Import a question from an external source
133 */
134export async function importQuestionFromExternalPost(
135 post: ExternalPost,
136 targetDid: string
137): Promise<{ success: boolean; questionId?: string; error?: string }> {
138 try {
139 const sourceType = detectSourceType(post.uri, { platform: post.platform })
140
141 // Ensure target user exists in our system
142 const targetUser = await db.query.users.findFirst({
143 where: eq(users.did, targetDid)
144 })
145
146 if (!targetUser) {
147 return { success: false, error: 'Target user not found in system' }
148 }
149
150 // Check if we already imported this post
151 const existingQuestion = await db.query.questions.findFirst({
152 where: eq(questions.sourceUri, post.uri)
153 })
154
155 if (existingQuestion) {
156 return { success: false, error: 'Question already imported' }
157 }
158
159 let result
160
161 switch (sourceType) {
162 case SOURCE_TYPES.BLUESKY:
163 const bskyPost = post as BlueSkyPost
164 result = createQuestionFromBlueSky({
165 content: post.content,
166 targetDid,
167 authorDid: post.authorDid,
168 bskyUri: post.uri,
169 bskyPost: bskyPost.bskyData,
170 anonymous: false
171 })
172 break
173
174 case SOURCE_TYPES.STANDARD_SITE:
175 const standardPost = post as StandardSitePost
176 result = createQuestionFromStandardSite({
177 content: post.content,
178 targetDid,
179 authorDid: post.authorDid,
180 standardSiteUri: post.uri,
181 standardSiteData: standardPost.standardSiteData,
182 anonymous: false
183 })
184 break
185
186 default:
187 // Generic import for other sources
188 result = {
189 lexRecord: null,
190 dbData: {
191 content: post.content,
192 targetDid,
193 authorDid: post.authorDid,
194 sourceType: sourceType,
195 anonymous: false,
196 sourceUri: post.uri,
197 sourceData: JSON.stringify(post.metadata || {}),
198 createdAt: new Date(post.createdAt)
199 }
200 }
201 }
202
203 // Insert into database
204 const [inserted] = await db.insert(questions).values({
205 authorDid: result.dbData.authorDid!,
206 targetDid: result.dbData.targetDid!,
207 content: result.dbData.content!,
208 sourceType: result.dbData.sourceType!,
209 anonymous: result.dbData.anonymous!,
210 createdAt: result.dbData.createdAt!,
211 sourceUri: result.dbData.sourceUri,
212 sourceData: result.dbData.sourceData
213 }).returning()
214
215 return { success: true, questionId: inserted.id }
216
217 } catch (error) {
218 console.error('Failed to import question from external post:', error)
219 return {
220 success: false,
221 error: error instanceof Error ? error.message : 'Unknown error'
222 }
223 }
224}
225
226/**
227 * Monitor Bluesky for mentions and questions
228 */
229export async function monitorBlueSkyMentions(userDid: string): Promise<BlueSkyPost[]> {
230 // This would integrate with the Bluesky API to monitor mentions
231 // For now, return empty array as placeholder
232 console.log(`Monitoring Bluesky mentions for ${userDid}`)
233 return []
234}
235
236/**
237 * Monitor standard.site for questions
238 */
239export async function monitorStandardSiteQuestions(userDid: string): Promise<StandardSitePost[]> {
240 // This would integrate with standard.site to monitor questions
241 // For now, return empty array as placeholder
242 console.log(`Monitoring standard.site questions for ${userDid}`)
243 return []
244}
245
246/**
247 * Get source type display information
248 */
249export function getSourceTypeInfo(sourceType: SourceType): {
250 name: string
251 icon: string
252 color: string
253 url?: string
254} {
255 switch (sourceType) {
256 case SOURCE_TYPES.BLUESKY:
257 return {
258 name: 'Bluesky',
259 icon: '🦋',
260 color: '#0085ff',
261 url: 'https://bsky.app'
262 }
263 case SOURCE_TYPES.STANDARD_SITE:
264 return {
265 name: 'Standard.site',
266 icon: '🌐',
267 color: '#6366f1',
268 url: 'https://standard.site'
269 }
270 case SOURCE_TYPES.MASTODON:
271 return {
272 name: 'Mastodon',
273 icon: '🐘',
274 color: '#6364ff'
275 }
276 case SOURCE_TYPES.NOSTR:
277 return {
278 name: 'Nostr',
279 icon: '⚡',
280 color: '#f59e0b'
281 }
282 case SOURCE_TYPES.ASKIMUT:
283 default:
284 return {
285 name: 'Askimut',
286 icon: '❓',
287 color: '#10b981'
288 }
289 }
290}
291
292/**
293 * Format source attribution text
294 */
295export function formatSourceAttribution(sourceType: SourceType, sourceUri?: string): string {
296 const info = getSourceTypeInfo(sourceType)
297
298 if (sourceType === SOURCE_TYPES.ASKIMUT) {
299 return ''
300 }
301
302 if (sourceUri) {
303 return `Originally from ${info.name}`
304 }
305
306 return `Via ${info.name}`
307}
308
309/**
310 * Check if a source type supports real-time monitoring
311 */
312export function supportsRealTimeMonitoring(sourceType: SourceType): boolean {
313 return [SOURCE_TYPES.BLUESKY, SOURCE_TYPES.STANDARD_SITE].includes(sourceType)
314}
315
316/**
317 * Get the original post URL if available
318 */
319export function getOriginalPostUrl(sourceType: SourceType, sourceUri?: string, sourceData?: string): string | null {
320 if (!sourceUri) return null
321
322 switch (sourceType) {
323 case SOURCE_TYPES.BLUESKY:
324 // Convert AT-URI to web URL
325 if (sourceUri.startsWith('at://')) {
326 const parts = sourceUri.replace('at://', '').split('/')
327 if (parts.length >= 3) {
328 const [did, collection, rkey] = parts
329 return `https://bsky.app/profile/${did}/post/${rkey}`
330 }
331 }
332 return sourceUri
333
334 case SOURCE_TYPES.STANDARD_SITE:
335 return sourceUri
336
337 default:
338 return sourceUri
339 }
340}