UNPKG

rsshub

Version:
150 lines (131 loc) 4.97 kB
import ofetch from '@/utils/ofetch'; import path from 'node:path'; import { art } from '@/utils/render'; import { Context } from 'hono'; import { Route } from '@/types'; const CONFIG = { DEFAULT_PAGE_SIZE: 20, MAX_PAGE_SIZE: 100, } as const; const API = { BASE_URL: 'https://hiring.cafe/api/search-jobs', HEADERS: { 'Content-Type': 'application/json', }, } as const; interface GeoLocation { readonly lat: number; readonly lon: number; } interface JobInformation { readonly title: string; readonly description: string; } interface ProcessedJobData { readonly company_name: string; readonly is_compensation_transparent: boolean; readonly yearly_min_compensation?: number; readonly yearly_max_compensation?: number; readonly workplace_type?: string; readonly requirements_summary?: string; readonly job_category: string; readonly role_activities: readonly string[]; readonly formatted_workplace_location?: string; readonly estimated_publish_date_millis: string; } interface JobResult { readonly id: string; readonly apply_url: string; readonly job_information: JobInformation; readonly v5_processed_job_data: ProcessedJobData; readonly _geoloc: readonly GeoLocation[]; } interface ApiResponse { readonly results: readonly JobResult[]; readonly total: number; } interface SearchParams { readonly keywords: string; readonly page?: number; readonly size?: number; readonly sortBy?: 'date' | 'default' | 'compensation_desc' | 'experience_asc'; } const validateSearchParams = ({ keywords, page = 0, size = CONFIG.DEFAULT_PAGE_SIZE }: SearchParams): SearchParams => ({ keywords: keywords.trim(), page: Math.max(0, Math.floor(Number(page))), size: Math.min(Math.max(1, Math.floor(Number(size))), CONFIG.MAX_PAGE_SIZE), }); const fetchJobs = async (searchParams: SearchParams): Promise<ApiResponse> => { const payload = { size: searchParams.size || 20, page: searchParams.page || 0, searchState: { searchQuery: searchParams.keywords, sortBy: searchParams.sortBy || 'date', }, }; return await ofetch<ApiResponse>(API.BASE_URL, { method: 'POST', body: payload, headers: API.HEADERS, }); }; const renderJobDescription = (jobInfo: JobInformation, processedData: ProcessedJobData): string => art(path.join(__dirname, 'templates/jobs.art'), { company_name: processedData.company_name, location: processedData.formatted_workplace_location ?? 'Remote/Unspecified', is_compensation_transparent: Boolean(processedData.is_compensation_transparent && processedData.yearly_min_compensation && processedData.yearly_max_compensation), yearly_min_compensation_formatted: processedData.yearly_min_compensation?.toLocaleString() ?? '', yearly_max_compensation_formatted: processedData.yearly_max_compensation?.toLocaleString() ?? '', workplace_type: processedData.workplace_type ?? 'Not specified', requirements_summary: processedData.requirements_summary ?? 'No requirements specified', job_description: jobInfo.description ?? '', }); const transformJobItem = (item: JobResult) => { const { job_information: jobInfo, v5_processed_job_data: processedData, apply_url, id } = item; return { title: `${jobInfo.title} - ${processedData.company_name}`, description: renderJobDescription(jobInfo, processedData), link: apply_url, pubDate: new Date(processedData.estimated_publish_date_millis).toUTCString(), category: [processedData.job_category, ...processedData.role_activities, processedData.workplace_type].filter((x): x is string => !!x), author: processedData.company_name, guid: id, }; }; async function handler(ctx: Context) { const searchParams = validateSearchParams({ keywords: ctx.req.param('keywords'), }); const response = await fetchJobs(searchParams); const items = response.results.map((item) => transformJobItem(item)); return { title: `HiringCafe Jobs: ${searchParams.keywords}`, description: `Job search results for "${searchParams.keywords}" on HiringCafe`, link: `https://hiring.cafe/jobs?q=${encodeURIComponent(searchParams.keywords)}`, item: items, total: response.total, }; } export const route: Route = { path: '/jobs/:keywords', categories: ['other'], example: '/hiring.cafe/jobs/sustainability', parameters: { keywords: 'Keywords to search for' }, features: { requireConfig: false, requirePuppeteer: false, antiCrawler: false, supportBT: false, supportPodcast: false, supportScihub: false, }, radar: [ { source: ['hiring.cafe'], }, ], name: 'Jobs', maintainers: ['mintyfrankie'], handler, };