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 { appRoutes } from 'routes'
import { store } from 'store'
import { BlogPageLoaderResult, FilterOptions, NSFWFilter } from 'types'
import {
BlogPageLoaderResult,
FilterOptions,
ModeratedFilter,
NSFWFilter
} from 'types'
import {
DEFAULT_FILTER_OPTIONS,
getLocalStorageItem,
@ -59,11 +64,11 @@ export const blogRouteLoader =
getLocalStorageItem('filter-blog', DEFAULT_FILTER_OPTIONS)
) 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 = {
authors: [pubkey],
kinds: [kinds.LongFormArticle],
limit: 4
limit: PROFILE_BLOG_FILTER_LIMIT
}
// Add source filter
if (filterOptions.source === window.location.host) {
@ -75,12 +80,9 @@ export const blogRouteLoader =
// NSFWFilter.Hide_NSFW -> up the limit and filter after fetch
if (filterOptions.nsfw === NSFWFilter.Only_NSFW) {
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([
ndkContext.fetchEvent(filter),
ndkContext.fetchEvents(latestFilter),
@ -121,10 +123,6 @@ export const blogRouteLoader =
result.latest = fetchEventsResult.value
.map(extractBlogCardDetails)
.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') {
log(
true,
@ -134,22 +132,48 @@ export const blogRouteLoader =
)
}
const muteList = settled[2]
if (muteList.status === 'fulfilled' && muteList.value) {
if (muteList && muteList.value) {
const muteLists = settled[2]
if (muteLists.status === 'fulfilled' && muteLists.value) {
if (muteLists && muteLists.value) {
if (result.blog && result.blog.aTag) {
if (
muteList.value.admin.replaceableEvents.includes(
muteLists.value.admin.replaceableEvents.includes(
result.blog.aTag
) ||
muteList.value.user.replaceableEvents.includes(result.blog.aTag)
muteLists.value.user.replaceableEvents.includes(result.blog.aTag)
) {
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') {
log(true, LogType.Error, 'Issue fetching mute list', muteList.reason)
} else if (muteLists.status === 'rejected') {
log(true, LogType.Error, 'Issue fetching mute list', muteLists.reason)
}
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) => {
if (b) {
const isMissingNsfwTag =
!b.nsfw && b.aTag && nsfwList.value.includes(b.aTag)
// Add nsfw tag if it's missing
const isMissingNsfwTag =
!b.nsfw && b.aTag && nsfwList.value.includes(b.aTag)
if (isMissingNsfwTag) {
b.nsfw = true
}
if (isMissingNsfwTag) {
b.nsfw = true
}
return b
})
@ -187,6 +210,17 @@ export const blogRouteLoader =
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
} catch (error) {
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 { NDKContextType } from 'contexts/NDKContext'
import { kinds } from 'nostr-tools'
import { BlogCardDetails } from 'types'
import { log, LogType, npubToHex } from 'utils'
import { extractBlogCardDetails } from 'utils/blog'
@ -15,14 +16,46 @@ export const blogsRouteLoader = (ndkContext: NDKContextType) => async () => {
authors: blogHexkeys,
kinds: [kinds.LongFormArticle]
}
const events = await ndkContext.fetchEvents(filter)
if (!events) {
log(true, LogType.Error, 'Unable to fetch the blog events.')
return null
const settled = await Promise.allSettled([
ndkContext.fetchEvents(filter),
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) {
log(
true,
@ -30,6 +63,6 @@ export const blogsRouteLoader = (ndkContext: NDKContextType) => async () => {
'An error occurred in fetching blog details from relays',
error
)
return null
return []
}
}

View File

@ -3,24 +3,36 @@ import { BlogCardDetails, BlogDetails } from 'types'
import { getFirstTagValue, getFirstTagValueAsInt, getTagValues } from './nostr'
import { kinds, nip19 } from 'nostr-tools'
export const extractBlogDetails = (event: NDKEvent): Partial<BlogDetails> => ({
title: getFirstTagValue(event, 'title'),
content: event.content,
summary: getFirstTagValue(event, 'summary'),
image: getFirstTagValue(event, 'image'),
// 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: getFirstTagValue(event, 'd'),
aTag: getFirstTagValue(event, 'a'),
tTags: getTagValues(event, 't') || []
})
export const extractBlogDetails = (event: NDKEvent): Partial<BlogDetails> => {
const dTag = getFirstTagValue(event, 'd')
// Check if the aTag exists on the blog
let aTag = getFirstTagValue(event, 'a')
// Create aTag from components if aTag is not included
if (typeof aTag === 'undefined' && event.pubkey && dTag) {
aTag = `${kinds.LongFormArticle}:${event.pubkey}:${dTag}`
}
return {
title: getFirstTagValue(event, 'title'),
content: event.content,
summary: getFirstTagValue(event, 'summary'),
image: getFirstTagValue(event, 'image'),
// 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 = (
event: NDKEvent