{"version":3,"file":"mojeek.mjs","names":[],"sources":["../../src/providers/mojeek.ts"],"sourcesContent":["import type { ProviderConfig, SearchRequestOptions, SearchResult } from \"../core/types.ts\";\nimport { Provider, type ProviderSearchPage } from \"../core/provider.ts\";\nimport {\n  AuthError,\n  InvalidSearchContinuationError,\n  DEFAULT_RETRY_AFTER,\n  RateLimitError,\n  WebError,\n  normalizeError,\n} from \"../core/errors.ts\";\n\ninterface MojeekSearchEnvelope {\n  readonly response?: MojeekSearchResponse;\n}\n\ninterface MojeekSearchResponse {\n  readonly status: string;\n  readonly head?: {\n    readonly start: number;\n    readonly return: number;\n    readonly results: number;\n  };\n  readonly results?: readonly MojeekResult[];\n}\n\nconst MOJEEK_MAX_RESULT_START = 1_000;\n\ninterface MojeekResult {\n  readonly url: string;\n  readonly title: string;\n  readonly desc: string;\n  readonly score?: number;\n  readonly pdate?: number;\n  readonly timestamp?: number;\n  readonly cdatetimestamp?: number;\n  readonly size?: string;\n  readonly cfs?: number;\n  readonly mres?: number;\n  readonly image?: {\n    readonly url?: string;\n    readonly width?: number;\n    readonly height?: number;\n  };\n}\n\nexport class MojeekProvider extends Provider {\n  static readonly providerName = \"mojeek\";\n  static readonly defaultBaseURL = \"https://api.mojeek.com\";\n\n  private readonly apiKey: string;\n\n  constructor(config: Readonly<ProviderConfig>) {\n    super(config, MojeekProvider);\n    if (!config.apiKey) {\n      throw new AuthError(\"Missing API key for Mojeek. Set MOJEEK_API_KEY\", \"mojeek\");\n    }\n\n    this.apiKey = config.apiKey;\n  }\n\n  async search(query: string, options?: SearchRequestOptions): Promise<SearchResult[]> {\n    return (await this.searchPage(query, options)).results;\n  }\n\n  async searchPage(\n    query: string,\n    options?: SearchRequestOptions,\n    continuation?: string,\n  ): Promise<ProviderSearchPage> {\n    try {\n      const start = mojeekStart(continuation);\n      const envelope = await this.client.getJSON<MojeekSearchEnvelope>(\n        `${this.baseURL}/search?${searchParams(query, this.apiKey, options, start)}`,\n        undefined,\n        options?.signal,\n      );\n      const response = successfulResponse(envelope);\n      return {\n        results: response.results?.map(mapResult) ?? [],\n        ...mojeekContinuation(response.head),\n      };\n    } catch (error) {\n      throw normalizeError(error, \"mojeek\");\n    }\n  }\n}\n\nfunction searchParams(\n  query: string,\n  apiKey: string,\n  options: SearchRequestOptions | undefined,\n  start: number,\n): string {\n  return new URLSearchParams({\n    q: query,\n    api_key: apiKey,\n    fmt: \"json\",\n    t: String(options?.maxResults ?? 10),\n    date: \"1\",\n    cdate: \"1\",\n    size: \"1\",\n    ...(start === 1 ? {} : { s: String(start) }),\n    ...domainParams(options),\n    ...dateParams(options),\n  }).toString();\n}\n\nfunction mojeekStart(continuation?: string): number {\n  if (continuation === undefined) return 1;\n  if (!/^[1-9]\\d*$/u.test(continuation)) throw new InvalidSearchContinuationError();\n  const start = Number(continuation);\n  if (!Number.isSafeInteger(start) || start > MOJEEK_MAX_RESULT_START) {\n    throw new InvalidSearchContinuationError();\n  }\n  return start;\n}\n\nfunction mojeekContinuation(\n  head: Readonly<{ start: number; return: number; results: number }> | undefined,\n): Record<string, string> {\n  if (!head || !isPositiveSafeInteger(head.start)) return {};\n  if (!isPositiveSafeInteger(head.return) || !isNonnegativeSafeInteger(head.results)) return {};\n  const nextStart = head.start + head.return;\n  return nextStart <= head.results && nextStart <= MOJEEK_MAX_RESULT_START\n    ? { continuation: String(nextStart) }\n    : {};\n}\n\nfunction isPositiveSafeInteger(value: number): boolean {\n  return Number.isSafeInteger(value) && value >= 1;\n}\n\nfunction isNonnegativeSafeInteger(value: number): boolean {\n  return Number.isSafeInteger(value) && value >= 0;\n}\n\nfunction domainParams(options?: SearchRequestOptions): Record<string, string> {\n  return {\n    ...(options?.includeDomains?.length ? { fi: options.includeDomains.join(\",\") } : {}),\n    ...(options?.excludeDomains?.length ? { fe: options.excludeDomains.join(\",\") } : {}),\n  };\n}\n\nfunction dateParams(options?: SearchRequestOptions): Record<string, string> {\n  return {\n    ...(options?.startPublishedDate ? { since: mojeekDate(options.startPublishedDate) } : {}),\n    ...(options?.endPublishedDate ? { before: mojeekDate(options.endPublishedDate) } : {}),\n  };\n}\n\nfunction mojeekDate(value: string): string {\n  return value.slice(0, 10).replaceAll(\"-\", \"\");\n}\n\nfunction successfulResponse(envelope: Readonly<MojeekSearchEnvelope>): MojeekSearchResponse {\n  const response = envelope.response;\n  if (!response) {\n    throw new WebError(\"Mojeek returned an invalid search response\");\n  }\n  if (response.status === \"OK\") {\n    return response;\n  }\n  if (/^Access Denied(?::|$)/i.test(response.status)) {\n    throw new AuthError(`Authentication failed: ${response.status}`, \"mojeek\");\n  }\n  if (/daily limit reached/i.test(response.status)) {\n    throw new RateLimitError(DEFAULT_RETRY_AFTER);\n  }\n  throw new WebError(`Mojeek search failed: ${response.status}`);\n}\n\nfunction mapResult(result: Readonly<MojeekResult>): SearchResult {\n  return {\n    url: result.url,\n    title: result.title,\n    snippet: result.desc,\n    score: result.score,\n    publishedDate: unixSecondsToISOString(result.pdate),\n    image: result.image?.url,\n    metadata: resultMetadata(result),\n  };\n}\n\nfunction resultMetadata(result: Readonly<MojeekResult>): Record<string, unknown> | undefined {\n  const metadata = {\n    ...(result.cfs === undefined ? {} : { confidence: result.cfs }),\n    ...(result.size === undefined ? {} : { documentSize: result.size }),\n    ...optionalDate(\"lastModifiedDate\", result.timestamp),\n    ...optionalDate(\"crawledDate\", result.cdatetimestamp),\n    ...(result.mres === undefined ? {} : { moreResultsFromDomain: result.mres === 1 }),\n    ...(result.image?.width === undefined ? {} : { imageWidth: result.image.width }),\n    ...(result.image?.height === undefined ? {} : { imageHeight: result.image.height }),\n  };\n  return Object.keys(metadata).length > 0 ? metadata : undefined;\n}\n\nfunction optionalDate(name: string, value?: number): Record<string, string> {\n  const date = unixSecondsToISOString(value);\n  return date === undefined ? {} : { [name]: date };\n}\n\nfunction unixSecondsToISOString(value?: number): string | undefined {\n  if (value === undefined || !Number.isFinite(value)) return undefined;\n  const date = new Date(value * 1000);\n  return Number.isNaN(date.getTime()) ? undefined : date.toISOString();\n}\n"],"mappings":";;;AAyBA,MAAM,0BAA0B;AAoBhC,IAAa,iBAAb,MAAa,uBAAuB,SAAS;CAC3C,OAAgB,eAAe;CAC/B,OAAgB,iBAAiB;CAEjC;CAEA,YAAY,QAAkC;EAC5C,MAAM,QAAQ,cAAc;EAC5B,IAAI,CAAC,OAAO,QACV,MAAM,IAAI,UAAU,kDAAkD,QAAQ;EAGhF,KAAK,SAAS,OAAO;CACvB;CAEA,MAAM,OAAO,OAAe,SAAyD;EACnF,QAAQ,MAAM,KAAK,WAAW,OAAO,OAAO,EAAA,CAAG;CACjD;CAEA,MAAM,WACJ,OACA,SACA,cAC6B;EAC7B,IAAI;GACF,MAAM,QAAQ,YAAY,YAAY;GAMtC,MAAM,WAAW,mBAAmB,MALb,KAAK,OAAO,QACjC,GAAG,KAAK,QAAQ,UAAU,aAAa,OAAO,KAAK,QAAQ,SAAS,KAAK,KACzE,KAAA,GACA,SAAS,MACX,CAC4C;GAC5C,OAAO;IACL,SAAS,SAAS,SAAS,IAAI,SAAS,KAAK,CAAC;IAC9C,GAAG,mBAAmB,SAAS,IAAI;GACrC;EACF,SAAS,OAAO;GACd,MAAM,eAAe,OAAO,QAAQ;EACtC;CACF;AACF;AAEA,SAAS,aACP,OACA,QACA,SACA,OACQ;CACR,OAAO,IAAI,gBAAgB;EACzB,GAAG;EACH,SAAS;EACT,KAAK;EACL,GAAG,OAAO,SAAS,cAAc,EAAE;EACnC,MAAM;EACN,OAAO;EACP,MAAM;EACN,GAAI,UAAU,IAAI,CAAC,IAAI,EAAE,GAAG,OAAO,KAAK,EAAE;EAC1C,GAAG,aAAa,OAAO;EACvB,GAAG,WAAW,OAAO;CACvB,CAAC,CAAC,CAAC,SAAS;AACd;AAEA,SAAS,YAAY,cAA+B;CAClD,IAAI,iBAAiB,KAAA,GAAW,OAAO;CACvC,IAAI,CAAC,cAAc,KAAK,YAAY,GAAG,MAAM,IAAI,+BAA+B;CAChF,MAAM,QAAQ,OAAO,YAAY;CACjC,IAAI,CAAC,OAAO,cAAc,KAAK,KAAK,QAAQ,yBAC1C,MAAM,IAAI,+BAA+B;CAE3C,OAAO;AACT;AAEA,SAAS,mBACP,MACwB;CACxB,IAAI,CAAC,QAAQ,CAAC,sBAAsB,KAAK,KAAK,GAAG,OAAO,CAAC;CACzD,IAAI,CAAC,sBAAsB,KAAK,MAAM,KAAK,CAAC,yBAAyB,KAAK,OAAO,GAAG,OAAO,CAAC;CAC5F,MAAM,YAAY,KAAK,QAAQ,KAAK;CACpC,OAAO,aAAa,KAAK,WAAW,aAAa,0BAC7C,EAAE,cAAc,OAAO,SAAS,EAAE,IAClC,CAAC;AACP;AAEA,SAAS,sBAAsB,OAAwB;CACrD,OAAO,OAAO,cAAc,KAAK,KAAK,SAAS;AACjD;AAEA,SAAS,yBAAyB,OAAwB;CACxD,OAAO,OAAO,cAAc,KAAK,KAAK,SAAS;AACjD;AAEA,SAAS,aAAa,SAAwD;CAC5E,OAAO;EACL,GAAI,SAAS,gBAAgB,SAAS,EAAE,IAAI,QAAQ,eAAe,KAAK,GAAG,EAAE,IAAI,CAAC;EAClF,GAAI,SAAS,gBAAgB,SAAS,EAAE,IAAI,QAAQ,eAAe,KAAK,GAAG,EAAE,IAAI,CAAC;CACpF;AACF;AAEA,SAAS,WAAW,SAAwD;CAC1E,OAAO;EACL,GAAI,SAAS,qBAAqB,EAAE,OAAO,WAAW,QAAQ,kBAAkB,EAAE,IAAI,CAAC;EACvF,GAAI,SAAS,mBAAmB,EAAE,QAAQ,WAAW,QAAQ,gBAAgB,EAAE,IAAI,CAAC;CACtF;AACF;AAEA,SAAS,WAAW,OAAuB;CACzC,OAAO,MAAM,MAAM,GAAG,EAAE,CAAC,CAAC,WAAW,KAAK,EAAE;AAC9C;AAEA,SAAS,mBAAmB,UAAgE;CAC1F,MAAM,WAAW,SAAS;CAC1B,IAAI,CAAC,UACH,MAAM,IAAI,SAAS,4CAA4C;CAEjE,IAAI,SAAS,WAAW,MACtB,OAAO;CAET,IAAI,yBAAyB,KAAK,SAAS,MAAM,GAC/C,MAAM,IAAI,UAAU,0BAA0B,SAAS,UAAU,QAAQ;CAE3E,IAAI,uBAAuB,KAAK,SAAS,MAAM,GAC7C,MAAM,IAAI,eAAA,EAAkC;CAE9C,MAAM,IAAI,SAAS,yBAAyB,SAAS,QAAQ;AAC/D;AAEA,SAAS,UAAU,QAA8C;CAC/D,OAAO;EACL,KAAK,OAAO;EACZ,OAAO,OAAO;EACd,SAAS,OAAO;EAChB,OAAO,OAAO;EACd,eAAe,uBAAuB,OAAO,KAAK;EAClD,OAAO,OAAO,OAAO;EACrB,UAAU,eAAe,MAAM;CACjC;AACF;AAEA,SAAS,eAAe,QAAqE;CAC3F,MAAM,WAAW;EACf,GAAI,OAAO,QAAQ,KAAA,IAAY,CAAC,IAAI,EAAE,YAAY,OAAO,IAAI;EAC7D,GAAI,OAAO,SAAS,KAAA,IAAY,CAAC,IAAI,EAAE,cAAc,OAAO,KAAK;EACjE,GAAG,aAAa,oBAAoB,OAAO,SAAS;EACpD,GAAG,aAAa,eAAe,OAAO,cAAc;EACpD,GAAI,OAAO,SAAS,KAAA,IAAY,CAAC,IAAI,EAAE,uBAAuB,OAAO,SAAS,EAAE;EAChF,GAAI,OAAO,OAAO,UAAU,KAAA,IAAY,CAAC,IAAI,EAAE,YAAY,OAAO,MAAM,MAAM;EAC9E,GAAI,OAAO,OAAO,WAAW,KAAA,IAAY,CAAC,IAAI,EAAE,aAAa,OAAO,MAAM,OAAO;CACnF;CACA,OAAO,OAAO,KAAK,QAAQ,CAAC,CAAC,SAAS,IAAI,WAAW,KAAA;AACvD;AAEA,SAAS,aAAa,MAAc,OAAwC;CAC1E,MAAM,OAAO,uBAAuB,KAAK;CACzC,OAAO,SAAS,KAAA,IAAY,CAAC,IAAI,GAAG,OAAO,KAAK;AAClD;AAEA,SAAS,uBAAuB,OAAoC;CAClE,IAAI,UAAU,KAAA,KAAa,CAAC,OAAO,SAAS,KAAK,GAAG,OAAO,KAAA;CAC3D,MAAM,uBAAO,IAAI,KAAK,QAAQ,GAAI;CAClC,OAAO,OAAO,MAAM,KAAK,QAAQ,CAAC,IAAI,KAAA,IAAY,KAAK,YAAY;AACrE"}