import { Event, Filter, kinds } from 'nostr-tools' import { getTagValue } from './nostr' import { ModFormState, ModDetails } from '../types' import { RelayController } from '../controllers' import { log, LogType } from './utils' import { toast } from 'react-toastify' import { MOD_FILTER_LIMIT, T_TAG_VALUE } from '../constants' import DOMPurify from 'dompurify' /** * Extracts and normalizes mod data from an event. * * This function extracts specific tag values from an event and maps them to properties * of a `PageData` object. It handles default values and type conversions as needed. * * @param event - The event object from which to extract data. * @returns A `Partial` object containing extracted data. */ export const extractModData = (event: Event): ModDetails => { // Helper function to safely get the first value of a tag or return a default value const getFirstTagValue = (tagIdentifier: string, defaultValue = '') => { const tagValue = getTagValue(event, tagIdentifier) return tagValue ? tagValue[0] : defaultValue } // Helper function to safely parse integer values from tags const getIntTagValue = (tagIdentifier: string, defaultValue: number = -1) => { const tagValue = getTagValue(event, tagIdentifier) return tagValue ? parseInt(tagValue[0], 10) : defaultValue } return { id: event.id, dTag: getFirstTagValue('d'), aTag: getFirstTagValue('a'), rTag: getFirstTagValue('r'), author: event.pubkey, edited_at: event.created_at, body: event.content, published_at: getIntTagValue('published_at'), game: getFirstTagValue('game'), title: getFirstTagValue('title'), featuredImageUrl: getFirstTagValue('featuredImageUrl'), summary: getFirstTagValue('summary'), nsfw: getFirstTagValue('nsfw') === 'true', screenshotsUrls: getTagValue(event, 'screenshotsUrls') || [], tags: getTagValue(event, 'tags') || [], downloadUrls: (getTagValue(event, 'downloadUrls') || []).map((item) => JSON.parse(item) ) } } /** * Constructs a list of `ModDetails` objects from an array of events. * * This function filters out events that do not contain all required data, * extracts the necessary information from the valid events, and constructs * `ModDetails` objects. * * @param events - The array of event objects to be processed. * @returns An array of `ModDetails` objects constructed from valid events. */ export const constructModListFromEvents = (events: Event[]): ModDetails[] => { // Filter and extract mod details from events const modDetailsList: ModDetails[] = events .filter(isModDataComplete) // Filter out incomplete events .map((event) => extractModData(event)) // Extract data and construct ModDetails return modDetailsList } /** * Checks if the provided event contains all the required data for constructing a `ModDetails` object. * * This function verifies that the event has the necessary tags and values to construct a `ModDetails` object. * * @param event - The event object to be checked. * @returns `true` if the event contains all required data; `false` otherwise. */ export const isModDataComplete = (event: Event): boolean => { // Helper function to check if a tag value is present and not empty const hasTagValue = (tagIdentifier: string): boolean => { const value = getTagValue(event, tagIdentifier) return !!value && value.length > 0 && value[0].trim() !== '' } // Check if all required fields are present return ( hasTagValue('d') && hasTagValue('a') && hasTagValue('t') && hasTagValue('published_at') && hasTagValue('game') && hasTagValue('title') && hasTagValue('featuredImageUrl') && hasTagValue('summary') && hasTagValue('nsfw') && getTagValue(event, 'screenshotsUrls') !== null && getTagValue(event, 'tags') !== null && getTagValue(event, 'downloadUrls') !== null ) } /** * Initializes the form state with values from existing mod data or defaults. * * @param existingModData - An optional object containing existing mod details. If provided, its values will be used to populate the form state. * @returns The initial state for the form, with values from existingModData if available, otherwise default values. */ export const initializeFormState = ( existingModData?: ModDetails ): ModFormState => ({ dTag: existingModData?.dTag || '', aTag: existingModData?.aTag || '', rTag: existingModData?.rTag || window.location.host, game: existingModData?.game || '', title: existingModData?.title || '', body: existingModData?.body || '', featuredImageUrl: existingModData?.featuredImageUrl || '', summary: existingModData?.summary || '', nsfw: existingModData?.nsfw || false, screenshotsUrls: existingModData?.screenshotsUrls || [''], tags: existingModData?.tags.join(',') || '', downloadUrls: existingModData?.downloadUrls || [ { url: '', hash: '', signatureKey: '', malwareScanLink: '', modVersion: '', customNote: '' } ] }) interface FetchModsOptions { source?: string until?: number since?: number limit?: number } /** * Fetches a list of mods based on the provided source. * * @param source - The source URL to filter the mods. If it matches the current window location, * it adds a filter condition to the request. * @param until - Optional timestamp to filter events until this time. * @param since - Optional timestamp to filter events from this time. * @returns A promise that resolves to an array of `ModDetails` objects. In case of an error, * it logs the error and shows a notification, then returns an empty array. */ export const fetchMods = async ({ source, until, since, limit }: FetchModsOptions): Promise => { // Define the filter criteria for fetching mods const filter: Filter = { kinds: [kinds.ClassifiedListing], // Specify the kind of events to fetch limit: limit || MOD_FILTER_LIMIT, // Limit the number of events fetched to 20 '#t': [T_TAG_VALUE], until, // Optional filter to fetch events until this timestamp since // Optional filter to fetch events from this timestamp } // If the source matches the current window location, add a filter condition if (source === window.location.host) { filter['#r'] = [window.location.host] // Add a tag filter for the current host } // Fetch events from the relay using the defined filter return RelayController.getInstance() .fetchEvents(filter, []) // Pass the filter and an empty array of options .then((events) => { // Convert the fetched events into a list of mods const modList = constructModListFromEvents(events) return modList // Return the list of mods }) .catch((err) => { // Log the error and show a notification if fetching fails log( true, LogType.Error, 'An error occurred in fetching mods from relays', err ) toast.error('An error occurred in fetching mods from relays') // Show error notification return [] as ModDetails[] // Return an empty array in case of an error }) } /** * Sanitizes the given HTML string and adds target="_blank" to all tags. * * @param htmlString - The HTML string to sanitize and modify. * @returns The modified HTML string with sanitized content and updated links. */ export const sanitizeAndAddTargetBlank = (htmlString: string) => { // Step 1: Sanitize the HTML string using DOMPurify. // This removes any potentially dangerous content and ensures that the HTML is safe to use. const sanitizedHtml = DOMPurify.sanitize(htmlString, { ADD_ATTR: ['target'] }) // Step 2: Create a temporary container (a
element) to parse the sanitized HTML. // This allows us to manipulate the HTML content in a safe and controlled manner. const tempDiv = document.createElement('div') tempDiv.innerHTML = sanitizedHtml // Step 3: Add target="_blank" to all tags within the temporary container. // This ensures that all links open in a new tab when clicked. const links = tempDiv.querySelectorAll('a') links.forEach((link) => { link.setAttribute('target', '_blank') }) // Step 4: Convert the manipulated DOM back to an HTML string. // This string contains the sanitized content with the target="_blank" attribute added to all links. return tempDiv.innerHTML }