2024-08-06 15:46:38 +05:00
|
|
|
import { Event, Filter, kinds } from 'nostr-tools'
|
2024-07-25 20:05:28 +05:00
|
|
|
import { getTagValue } from './nostr'
|
2024-08-06 15:46:38 +05:00
|
|
|
import { ModFormState, ModDetails } from '../types'
|
|
|
|
import { RelayController } from '../controllers'
|
|
|
|
import { log, LogType } from './utils'
|
|
|
|
import { toast } from 'react-toastify'
|
2024-08-08 21:56:30 +05:00
|
|
|
import { MOD_FILTER_LIMIT, T_TAG_VALUE } from '../constants'
|
2024-08-13 23:27:49 +05:00
|
|
|
import DOMPurify from 'dompurify'
|
2024-07-25 20:05:28 +05:00
|
|
|
|
|
|
|
/**
|
|
|
|
* 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<PageData>` 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 {
|
2024-08-06 15:46:38 +05:00
|
|
|
id: event.id,
|
|
|
|
dTag: getFirstTagValue('d'),
|
|
|
|
aTag: getFirstTagValue('a'),
|
|
|
|
rTag: getFirstTagValue('r'),
|
2024-07-25 20:05:28 +05:00
|
|
|
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'),
|
2024-08-06 15:46:38 +05:00
|
|
|
nsfw: getFirstTagValue('nsfw') === 'true',
|
2024-07-25 20:05:28 +05:00
|
|
|
screenshotsUrls: getTagValue(event, 'screenshotsUrls') || [],
|
|
|
|
tags: getTagValue(event, 'tags') || [],
|
|
|
|
downloadUrls: (getTagValue(event, 'downloadUrls') || []).map((item) =>
|
|
|
|
JSON.parse(item)
|
|
|
|
)
|
|
|
|
}
|
|
|
|
}
|
2024-07-29 11:26:26 +05:00
|
|
|
|
|
|
|
/**
|
|
|
|
* 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 (
|
2024-08-06 15:46:38 +05:00
|
|
|
hasTagValue('d') &&
|
|
|
|
hasTagValue('a') &&
|
2024-07-29 11:26:26 +05:00
|
|
|
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
|
|
|
|
)
|
|
|
|
}
|
2024-08-06 15:46:38 +05:00
|
|
|
|
|
|
|
/**
|
|
|
|
* 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: ''
|
|
|
|
}
|
|
|
|
]
|
|
|
|
})
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 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: string,
|
|
|
|
until?: number,
|
|
|
|
since?: number
|
|
|
|
): Promise<ModDetails[]> => {
|
|
|
|
// Define the filter criteria for fetching mods
|
|
|
|
const filter: Filter = {
|
|
|
|
kinds: [kinds.ClassifiedListing], // Specify the kind of events to fetch
|
2024-08-08 21:56:30 +05:00
|
|
|
limit: MOD_FILTER_LIMIT, // Limit the number of events fetched to 20
|
2024-08-06 15:46:38 +05:00
|
|
|
'#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
|
|
|
|
})
|
|
|
|
}
|
2024-08-13 23:27:49 +05:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Sanitizes the given HTML string and adds target="_blank" to all <a> 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 <div> 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 <a> 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
|
|
|
|
}
|