fix(blogs): moderation and missing aTag
All checks were successful
Release to Staging / build_and_release (push) Successful in 45s

This commit is contained in:
enes 2024-11-13 14:09:13 +01:00
parent 3ee9e313de
commit 718350d2bc
3 changed files with 128 additions and 49 deletions

View File

@ -5,7 +5,12 @@ import { kinds, nip19 } from 'nostr-tools'
import { LoaderFunctionArgs, redirect } from 'react-router-dom' import { LoaderFunctionArgs, redirect } from 'react-router-dom'
import { appRoutes } from 'routes' import { appRoutes } from 'routes'
import { store } from 'store' import { store } from 'store'
import { BlogPageLoaderResult, FilterOptions, NSFWFilter } from 'types' import {
BlogPageLoaderResult,
FilterOptions,
ModeratedFilter,
NSFWFilter
} from 'types'
import { import {
DEFAULT_FILTER_OPTIONS, DEFAULT_FILTER_OPTIONS,
getLocalStorageItem, getLocalStorageItem,
@ -59,11 +64,11 @@ export const blogRouteLoader =
getLocalStorageItem('filter-blog', DEFAULT_FILTER_OPTIONS) getLocalStorageItem('filter-blog', DEFAULT_FILTER_OPTIONS)
) as FilterOptions ) as FilterOptions
// Fetch 4 in case the current blog is included in the latest // Fetch more in case the current blog is included in the latest and filters remove some
const latestFilter: NDKFilter = { const latestFilter: NDKFilter = {
authors: [pubkey], authors: [pubkey],
kinds: [kinds.LongFormArticle], kinds: [kinds.LongFormArticle],
limit: 4 limit: PROFILE_BLOG_FILTER_LIMIT
} }
// Add source filter // Add source filter
if (filterOptions.source === window.location.host) { if (filterOptions.source === window.location.host) {
@ -75,12 +80,9 @@ export const blogRouteLoader =
// NSFWFilter.Hide_NSFW -> up the limit and filter after fetch // NSFWFilter.Hide_NSFW -> up the limit and filter after fetch
if (filterOptions.nsfw === NSFWFilter.Only_NSFW) { if (filterOptions.nsfw === NSFWFilter.Only_NSFW) {
latestFilter['#L'] = ['content-warning'] latestFilter['#L'] = ['content-warning']
} else if (filterOptions.nsfw === NSFWFilter.Hide_NSFW) {
// Up the limit in case we fetch multiple NSFW blogs
latestFilter.limit = PROFILE_BLOG_FILTER_LIMIT
} }
// Parallel fetch blog event, latest events, mute, and nsfw lists in parallel // Parallel fetch blog event, latest events, mute, and nsfw lists
const settled = await Promise.allSettled([ const settled = await Promise.allSettled([
ndkContext.fetchEvent(filter), ndkContext.fetchEvent(filter),
ndkContext.fetchEvents(latestFilter), ndkContext.fetchEvents(latestFilter),
@ -121,10 +123,6 @@ export const blogRouteLoader =
result.latest = fetchEventsResult.value result.latest = fetchEventsResult.value
.map(extractBlogCardDetails) .map(extractBlogCardDetails)
.filter((b) => b.id !== result.blog?.id) // Filter out current blog if present .filter((b) => b.id !== result.blog?.id) // Filter out current blog if present
.filter(
(b) => !(b.nsfw && filterOptions.nsfw === NSFWFilter.Hide_NSFW)
) // Filter out the NSFW if selected
.slice(0, 3) // Take only three
} else if (fetchEventsResult.status === 'rejected') { } else if (fetchEventsResult.status === 'rejected') {
log( log(
true, true,
@ -134,22 +132,48 @@ export const blogRouteLoader =
) )
} }
const muteList = settled[2] const muteLists = settled[2]
if (muteList.status === 'fulfilled' && muteList.value) { if (muteLists.status === 'fulfilled' && muteLists.value) {
if (muteList && muteList.value) { if (muteLists && muteLists.value) {
if (result.blog && result.blog.aTag) { if (result.blog && result.blog.aTag) {
if ( if (
muteList.value.admin.replaceableEvents.includes( muteLists.value.admin.replaceableEvents.includes(
result.blog.aTag result.blog.aTag
) || ) ||
muteList.value.user.replaceableEvents.includes(result.blog.aTag) muteLists.value.user.replaceableEvents.includes(result.blog.aTag)
) { ) {
result.isBlocked = true result.isBlocked = true
} }
} }
// Moderate the latest
const isAdmin =
userState.user?.npub === import.meta.env.VITE_REPORTING_NPUB
const isOwner =
userState.user?.pubkey && userState.user.pubkey === pubkey
const isUnmoderatedFully =
filterOptions.moderated === ModeratedFilter.Unmoderated_Fully
// Only apply filtering if the user is not an admin or the admin has not selected "Unmoderated Fully"
// Allow "Unmoderated Fully" when author visits own profile
if (!((isAdmin || isOwner) && isUnmoderatedFully)) {
result.latest = result.latest.filter(
(b) =>
!muteLists.value.admin.authors.includes(b.author!) &&
!muteLists.value.admin.replaceableEvents.includes(b.aTag!)
)
}
if (filterOptions.moderated === ModeratedFilter.Moderated) {
result.latest = result.latest.filter(
(b) =>
!muteLists.value.user.authors.includes(b.author!) &&
!muteLists.value.user.replaceableEvents.includes(b.aTag!)
)
}
} }
} else if (muteList.status === 'rejected') { } else if (muteLists.status === 'rejected') {
log(true, LogType.Error, 'Issue fetching mute list', muteList.reason) log(true, LogType.Error, 'Issue fetching mute list', muteLists.reason)
} }
const nsfwList = settled[3] const nsfwList = settled[3]
@ -171,15 +195,14 @@ export const blogRouteLoader =
} }
} }
// Check if the the latest blogs too // Check the latest blogs too
result.latest = result.latest.map((b) => { result.latest = result.latest.map((b) => {
if (b) { // Add nsfw tag if it's missing
const isMissingNsfwTag = const isMissingNsfwTag =
!b.nsfw && b.aTag && nsfwList.value.includes(b.aTag) !b.nsfw && b.aTag && nsfwList.value.includes(b.aTag)
if (isMissingNsfwTag) { if (isMissingNsfwTag) {
b.nsfw = true b.nsfw = true
}
} }
return b return b
}) })
@ -187,6 +210,17 @@ export const blogRouteLoader =
log(true, LogType.Error, 'Issue fetching nsfw list', nsfwList.reason) log(true, LogType.Error, 'Issue fetching nsfw list', nsfwList.reason)
} }
// Filter latest, sort and take only three
result.latest = result.latest
.filter(
// Filter out the NSFW if selected
(b) => !(b.nsfw && filterOptions.nsfw === NSFWFilter.Hide_NSFW)
)
.sort((a, b) =>
a.published_at && b.published_at ? b.published_at - a.published_at : 0
)
.slice(0, 3)
return result return result
} catch (error) { } catch (error) {
let message = 'An error occurred in fetching blog details from relays' let message = 'An error occurred in fetching blog details from relays'

View File

@ -1,6 +1,7 @@
import { NDKFilter } from '@nostr-dev-kit/ndk' import { NDKFilter } from '@nostr-dev-kit/ndk'
import { NDKContextType } from 'contexts/NDKContext' import { NDKContextType } from 'contexts/NDKContext'
import { kinds } from 'nostr-tools' import { kinds } from 'nostr-tools'
import { BlogCardDetails } from 'types'
import { log, LogType, npubToHex } from 'utils' import { log, LogType, npubToHex } from 'utils'
import { extractBlogCardDetails } from 'utils/blog' import { extractBlogCardDetails } from 'utils/blog'
@ -15,14 +16,46 @@ export const blogsRouteLoader = (ndkContext: NDKContextType) => async () => {
authors: blogHexkeys, authors: blogHexkeys,
kinds: [kinds.LongFormArticle] kinds: [kinds.LongFormArticle]
} }
const events = await ndkContext.fetchEvents(filter)
if (!events) { const settled = await Promise.allSettled([
log(true, LogType.Error, 'Unable to fetch the blog events.') ndkContext.fetchEvents(filter),
return null ndkContext.getMuteLists()
])
let blogs: Partial<BlogCardDetails>[] = []
const fetchEventsResult = settled[0]
if (fetchEventsResult.status === 'fulfilled' && fetchEventsResult.value) {
// Extract the blog card details from the events
blogs = fetchEventsResult.value
.map(extractBlogCardDetails)
.filter((b) => b.naddr)
} else if (fetchEventsResult.status === 'rejected') {
log(
true,
LogType.Error,
'Unable to fetch the blog events.',
fetchEventsResult.reason
)
return []
} }
return events.map(extractBlogCardDetails).filter((e) => e.naddr) const muteListResult = settled[1]
if (muteListResult.status === 'fulfilled' && muteListResult.value) {
// Filter out the blocked events
blogs = blogs.filter(
(b) =>
b.aTag &&
!muteListResult.value.admin.replaceableEvents.includes(b.aTag)
)
} else if (muteListResult.status === 'rejected') {
log(
true,
LogType.Error,
'Failed to fetch mutelists.',
muteListResult.reason
)
}
return blogs
} catch (error) { } catch (error) {
log( log(
true, true,
@ -30,6 +63,6 @@ export const blogsRouteLoader = (ndkContext: NDKContextType) => async () => {
'An error occurred in fetching blog details from relays', 'An error occurred in fetching blog details from relays',
error error
) )
return null return []
} }
} }

View File

@ -3,24 +3,36 @@ import { BlogCardDetails, BlogDetails } from 'types'
import { getFirstTagValue, getFirstTagValueAsInt, getTagValues } from './nostr' import { getFirstTagValue, getFirstTagValueAsInt, getTagValues } from './nostr'
import { kinds, nip19 } from 'nostr-tools' import { kinds, nip19 } from 'nostr-tools'
export const extractBlogDetails = (event: NDKEvent): Partial<BlogDetails> => ({ export const extractBlogDetails = (event: NDKEvent): Partial<BlogDetails> => {
title: getFirstTagValue(event, 'title'), const dTag = getFirstTagValue(event, 'd')
content: event.content,
summary: getFirstTagValue(event, 'summary'), // Check if the aTag exists on the blog
image: getFirstTagValue(event, 'image'), let aTag = getFirstTagValue(event, 'a')
// Check L label namespace for content warning or nsfw (backwards compatibility)
nsfw: // Create aTag from components if aTag is not included
getFirstTagValue(event, 'L') === 'content-warning' || if (typeof aTag === 'undefined' && event.pubkey && dTag) {
getFirstTagValue(event, 'nsfw') === 'true', aTag = `${kinds.LongFormArticle}:${event.pubkey}:${dTag}`
id: event.id, }
author: event.pubkey,
published_at: getFirstTagValueAsInt(event, 'published_at'), return {
edited_at: event.created_at, title: getFirstTagValue(event, 'title'),
rTag: getFirstTagValue(event, 'r') || 'N/A', content: event.content,
dTag: getFirstTagValue(event, 'd'), summary: getFirstTagValue(event, 'summary'),
aTag: getFirstTagValue(event, 'a'), image: getFirstTagValue(event, 'image'),
tTags: getTagValues(event, 't') || [] // Check L label namespace for content warning or nsfw (backwards compatibility)
}) nsfw:
getFirstTagValue(event, 'L') === 'content-warning' ||
getFirstTagValue(event, 'nsfw') === 'true',
id: event.id,
author: event.pubkey,
published_at: getFirstTagValueAsInt(event, 'published_at'),
edited_at: event.created_at,
rTag: getFirstTagValue(event, 'r') || 'N/A',
dTag: dTag,
aTag: aTag,
tTags: getTagValues(event, 't') || []
}
}
export const extractBlogCardDetails = ( export const extractBlogCardDetails = (
event: NDKEvent event: NDKEvent