A social knowledge tool for researchers built on ATProto
at main 154 lines 4.2 kB view raw
1import { IMetadataService } from '../domain/services/IMetadataService'; 2import { UrlMetadata } from '../domain/value-objects/UrlMetadata'; 3import { URL } from '../domain/value-objects/URL'; 4import { Result, ok, err } from '../../../shared/core/Result'; 5 6interface CitoidResponse { 7 key?: string; 8 version?: number; 9 itemType?: string; 10 creators?: Array<{ 11 firstName?: string; 12 lastName?: string; 13 creatorType?: string; 14 }>; 15 tags?: string[]; 16 title?: string; 17 date?: string; 18 url?: string; 19 abstractNote?: string; 20 publicationTitle?: string; 21 language?: string; 22 libraryCatalog?: string; 23 accessDate?: string; 24} 25 26interface CitoidErrorResponse { 27 Error: string; 28} 29 30export class CitoidMetadataService implements IMetadataService { 31 private readonly baseUrl = 32 'https://en.wikipedia.org/api/rest_v1/data/citation/zotero/'; 33 private readonly headers = { 34 accept: 'application/json; charset=utf-8;', 35 }; 36 37 async fetchMetadata(url: URL): Promise<Result<UrlMetadata>> { 38 try { 39 // URL-encode the target URL 40 const encodedUrl = encodeURIComponent(url.value); 41 const fullUrl = this.baseUrl + encodedUrl; 42 43 const response = await fetch(fullUrl, { 44 method: 'GET', 45 headers: this.headers, 46 }); 47 48 if (!response.ok) { 49 return err( 50 new Error( 51 `Citoid API request failed with status ${response.status} and message: ${response.statusText}`, 52 ), 53 ); 54 } 55 56 const data = (await response.json()) as any; 57 58 // Check if the response is an error 59 if (data && typeof data === 'object' && 'Error' in data) { 60 const errorResponse = data as CitoidErrorResponse; 61 return err(new Error(`Citoid service error: ${errorResponse.Error}`)); 62 } 63 64 // Check if it's an array response 65 if (!Array.isArray(data) || data.length === 0) { 66 return err(new Error('No metadata found for the given URL')); 67 } 68 69 // Use the first result 70 const citoidData = data[0] as CitoidResponse; 71 if (!citoidData || !citoidData.itemType) { 72 return err(new Error('Invalid metadata format from Citoid')); 73 } 74 75 // Extract author from creators array 76 const author = 77 citoidData.creators && citoidData.creators.length > 0 78 ? this.formatAuthor(citoidData.creators[0]!) 79 : undefined; 80 81 // Parse published date 82 const publishedDate = citoidData.date 83 ? this.parseDate(citoidData.date) 84 : undefined; 85 86 const metadataResult = UrlMetadata.create({ 87 url: url.value, 88 title: citoidData.title, 89 description: citoidData.abstractNote, 90 author, 91 publishedDate, 92 siteName: citoidData.publicationTitle || citoidData.libraryCatalog, 93 type: citoidData.itemType, 94 }); 95 96 return metadataResult; 97 } catch (error) { 98 return err( 99 new Error( 100 `Failed to fetch metadata from Citoid: ${error instanceof Error ? error.message : 'Unknown error'}`, 101 ), 102 ); 103 } 104 } 105 106 async isAvailable(): Promise<boolean> { 107 try { 108 // Test with a simple URL to check if the service is available 109 const testUrl = this.baseUrl + encodeURIComponent('https://example.com'); 110 const response = await fetch(testUrl, { 111 method: 'HEAD', 112 headers: this.headers, 113 }); 114 115 // Service is available if we get any response (even 4xx errors are fine) 116 return response.status < 500; 117 } catch { 118 return false; 119 } 120 } 121 122 private formatAuthor(creator: { 123 firstName?: string; 124 lastName?: string; 125 }): string { 126 const { firstName, lastName } = creator; 127 128 if (firstName && lastName) { 129 return `${firstName} ${lastName}`; 130 } else if (lastName) { 131 return lastName; 132 } else if (firstName) { 133 return firstName; 134 } 135 136 return ''; 137 } 138 139 private parseDate(dateString: string): Date | undefined { 140 try { 141 // Try to parse the date string 142 const parsed = new Date(dateString); 143 144 // Check if the date is valid 145 if (isNaN(parsed.getTime())) { 146 return undefined; 147 } 148 149 return parsed; 150 } catch { 151 return undefined; 152 } 153 } 154}