Updates NIP-56 to the newest tag structure for Quartz

This commit is contained in:
Vitor Pamplona 2025-03-12 14:51:23 -04:00
parent 5766bb1887
commit b017e03728
18 changed files with 561 additions and 100 deletions

View File

@ -149,6 +149,7 @@ import com.vitorpamplona.quartz.nip51Lists.GeneralListEvent
import com.vitorpamplona.quartz.nip51Lists.MuteListEvent import com.vitorpamplona.quartz.nip51Lists.MuteListEvent
import com.vitorpamplona.quartz.nip51Lists.PeopleListEvent import com.vitorpamplona.quartz.nip51Lists.PeopleListEvent
import com.vitorpamplona.quartz.nip56Reports.ReportEvent import com.vitorpamplona.quartz.nip56Reports.ReportEvent
import com.vitorpamplona.quartz.nip56Reports.ReportType
import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent
import com.vitorpamplona.quartz.nip57Zaps.LnZapRequestEvent import com.vitorpamplona.quartz.nip57Zaps.LnZapRequestEvent
import com.vitorpamplona.quartz.nip57Zaps.splits.ZapSplitSetup import com.vitorpamplona.quartz.nip57Zaps.splits.ZapSplitSetup
@ -1616,18 +1617,18 @@ class Account(
suspend fun report( suspend fun report(
note: Note, note: Note,
type: ReportEvent.ReportType, type: ReportType,
content: String = "", content: String = "",
) { ) {
if (!isWriteable()) return if (!isWriteable()) return
if (note.hasReacted(userProfile(), "⚠️")) { if (note.hasReport(userProfile(), type)) {
// has already liked this note // has already reported this note
return return
} }
note.event?.let { note.event?.let {
ReportEvent.create(it, type, signer, content) { signer.sign(ReportEvent.build(it, type)) {
Amethyst.instance.client.send(it) Amethyst.instance.client.send(it)
LocalCache.justConsume(it, null) LocalCache.justConsume(it, null)
} }
@ -1636,7 +1637,7 @@ class Account(
suspend fun report( suspend fun report(
user: User, user: User,
type: ReportEvent.ReportType, type: ReportType,
) { ) {
if (!isWriteable()) return if (!isWriteable()) return
@ -1645,7 +1646,8 @@ class Account(
return return
} }
ReportEvent.create(user.pubkeyHex, type, signer) { val template = ReportEvent.build(user.pubkeyHex, type)
signer.sign(template) {
Amethyst.instance.client.send(it) Amethyst.instance.client.send(it)
LocalCache.justConsume(it, null) LocalCache.justConsume(it, null)
} }

View File

@ -695,8 +695,8 @@ object LocalCache {
event.originalPost().mapNotNull { checkGetOrCreateNote(it) } + event.originalPost().mapNotNull { checkGetOrCreateNote(it) } +
event.taggedAddresses().map { getOrCreateAddressableNote(it) } event.taggedAddresses().map { getOrCreateAddressableNote(it) }
is ReportEvent -> is ReportEvent ->
event.reportedPost().mapNotNull { checkGetOrCreateNote(it.key) } + event.reportedPost().mapNotNull { checkGetOrCreateNote(it.eventId) } +
event.taggedAddresses().map { getOrCreateAddressableNote(it) } event.reportedAddresses().map { getOrCreateAddressableNote(it.address) }
is ChannelMessageEvent -> is ChannelMessageEvent ->
event event
.tagsWithoutCitations() .tagsWithoutCitations()
@ -1342,7 +1342,7 @@ object LocalCache {
// Already processed this event. // Already processed this event.
if (note.event != null) return if (note.event != null) return
val mentions = event.reportedAuthor().mapNotNull { checkGetOrCreateUser(it.key) } val mentions = event.reportedAuthor().mapNotNull { checkGetOrCreateUser(it.pubkey) }
val repliesTo = computeReplyTo(event) val repliesTo = computeReplyTo(event)
note.loadEvent(event, author, repliesTo) note.loadEvent(event, author, repliesTo)
@ -2218,7 +2218,7 @@ object LocalCache {
} }
if (noteEvent is ReportEvent) { if (noteEvent is ReportEvent) {
noteEvent.reportedAuthor().mapNotNull { noteEvent.reportedAuthor().mapNotNull {
val author = getUserIfExists(it.key) val author = getUserIfExists(it.pubkey)
author?.removeReport(note) author?.removeReport(note)
author?.clearEOSE() author?.clearEOSE()
} }

View File

@ -68,6 +68,8 @@ import com.vitorpamplona.quartz.nip47WalletConnect.LnZapPaymentResponseEvent
import com.vitorpamplona.quartz.nip47WalletConnect.PayInvoiceSuccessResponse import com.vitorpamplona.quartz.nip47WalletConnect.PayInvoiceSuccessResponse
import com.vitorpamplona.quartz.nip53LiveActivities.chat.LiveActivitiesChatMessageEvent import com.vitorpamplona.quartz.nip53LiveActivities.chat.LiveActivitiesChatMessageEvent
import com.vitorpamplona.quartz.nip53LiveActivities.streaming.LiveActivitiesEvent import com.vitorpamplona.quartz.nip53LiveActivities.streaming.LiveActivitiesEvent
import com.vitorpamplona.quartz.nip56Reports.ReportEvent
import com.vitorpamplona.quartz.nip56Reports.ReportType
import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent
import com.vitorpamplona.quartz.nip57Zaps.LnZapRequestEvent import com.vitorpamplona.quartz.nip57Zaps.LnZapRequestEvent
import com.vitorpamplona.quartz.nip59Giftwrap.WrappedEvent import com.vitorpamplona.quartz.nip59Giftwrap.WrappedEvent
@ -717,6 +719,15 @@ open class Note(
) )
} }
fun hasReport(
loggedIn: User,
type: ReportType,
): Boolean =
reports[loggedIn]?.firstOrNull {
it.event is ReportEvent &&
(it.event as ReportEvent).reportedAuthor().any { it.type == type }
} != null
fun hasPledgeBy(user: User): Boolean = fun hasPledgeBy(user: User): Boolean =
replies replies
.filter { it.event?.hasAdditionalReward() ?: false } .filter { it.event?.hasAdditionalReward() ?: false }

View File

@ -46,6 +46,7 @@ import com.vitorpamplona.quartz.nip19Bech32.entities.NProfile
import com.vitorpamplona.quartz.nip19Bech32.toNpub import com.vitorpamplona.quartz.nip19Bech32.toNpub
import com.vitorpamplona.quartz.nip51Lists.BookmarkListEvent import com.vitorpamplona.quartz.nip51Lists.BookmarkListEvent
import com.vitorpamplona.quartz.nip56Reports.ReportEvent import com.vitorpamplona.quartz.nip56Reports.ReportEvent
import com.vitorpamplona.quartz.nip56Reports.ReportType
import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent
import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent
import com.vitorpamplona.quartz.utils.DualCase import com.vitorpamplona.quartz.utils.DualCase
@ -357,11 +358,11 @@ class User(
fun hasReport( fun hasReport(
loggedIn: User, loggedIn: User,
type: ReportEvent.ReportType, type: ReportType,
): Boolean = ): Boolean =
reports[loggedIn]?.firstOrNull { reports[loggedIn]?.firstOrNull {
it.event is ReportEvent && it.event is ReportEvent &&
(it.event as ReportEvent).reportedAuthor().any { it.reportType == type } (it.event as ReportEvent).reportedAuthor().any { it.type == type }
} != null } != null
fun containsAny(hiddenWordsCase: List<DualCase>): Boolean { fun containsAny(hiddenWordsCase: List<DualCase>): Boolean {

View File

@ -36,6 +36,7 @@ import com.vitorpamplona.amethyst.ui.stringRes
import com.vitorpamplona.amethyst.ui.theme.replyModifier import com.vitorpamplona.amethyst.ui.theme.replyModifier
import com.vitorpamplona.quartz.nip02FollowList.EmptyTagList import com.vitorpamplona.quartz.nip02FollowList.EmptyTagList
import com.vitorpamplona.quartz.nip56Reports.ReportEvent import com.vitorpamplona.quartz.nip56Reports.ReportEvent
import com.vitorpamplona.quartz.nip56Reports.ReportType
@Composable @Composable
fun RenderReport( fun RenderReport(
@ -52,16 +53,17 @@ fun RenderReport(
val reportType = val reportType =
base base
.map { .map {
when (it.reportType) { when (it.type) {
ReportEvent.ReportType.EXPLICIT -> stringRes(R.string.explicit_content) ReportType.EXPLICIT -> stringRes(R.string.explicit_content)
ReportEvent.ReportType.NUDITY -> stringRes(R.string.nudity) ReportType.NUDITY -> stringRes(R.string.nudity)
ReportEvent.ReportType.PROFANITY -> stringRes(R.string.profanity_hateful_speech) ReportType.PROFANITY -> stringRes(R.string.profanity_hateful_speech)
ReportEvent.ReportType.SPAM -> stringRes(R.string.spam) ReportType.SPAM -> stringRes(R.string.spam)
ReportEvent.ReportType.IMPERSONATION -> stringRes(R.string.impersonation) ReportType.IMPERSONATION -> stringRes(R.string.impersonation)
ReportEvent.ReportType.ILLEGAL -> stringRes(R.string.illegal_behavior) ReportType.ILLEGAL -> stringRes(R.string.illegal_behavior)
ReportEvent.ReportType.MALWARE -> stringRes(R.string.malware) ReportType.MALWARE -> stringRes(R.string.malware)
ReportEvent.ReportType.MOD -> stringRes(R.string.mod) ReportType.MOD -> stringRes(R.string.mod)
ReportEvent.ReportType.OTHER -> stringRes(R.string.other) ReportType.OTHER -> stringRes(R.string.other)
null -> stringRes(R.string.other)
} }
}.toSet() }.toSet()
.joinToString(", ") .joinToString(", ")

View File

@ -102,7 +102,7 @@ import com.vitorpamplona.quartz.nip37Drafts.DraftEvent
import com.vitorpamplona.quartz.nip47WalletConnect.Response import com.vitorpamplona.quartz.nip47WalletConnect.Response
import com.vitorpamplona.quartz.nip50Search.SearchRelayListEvent import com.vitorpamplona.quartz.nip50Search.SearchRelayListEvent
import com.vitorpamplona.quartz.nip51Lists.GeneralListEvent import com.vitorpamplona.quartz.nip51Lists.GeneralListEvent
import com.vitorpamplona.quartz.nip56Reports.ReportEvent import com.vitorpamplona.quartz.nip56Reports.ReportType
import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent
import com.vitorpamplona.quartz.nip57Zaps.LnZapRequestEvent import com.vitorpamplona.quartz.nip57Zaps.LnZapRequestEvent
import com.vitorpamplona.quartz.nip57Zaps.zapraiser.zapraiserAmount import com.vitorpamplona.quartz.nip57Zaps.zapraiser.zapraiserAmount
@ -741,7 +741,7 @@ class AccountViewModel(
fun report( fun report(
note: Note, note: Note,
type: ReportEvent.ReportType, type: ReportType,
content: String = "", content: String = "",
) { ) {
viewModelScope.launch(Dispatchers.IO) { account.report(note, type, content) } viewModelScope.launch(Dispatchers.IO) { account.report(note, type, content) }
@ -749,7 +749,7 @@ class AccountViewModel(
fun report( fun report(
user: User, user: User,
type: ReportEvent.ReportType, type: ReportType,
) { ) {
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
account.report(user, type) account.report(user, type)

View File

@ -63,7 +63,7 @@ import com.vitorpamplona.amethyst.ui.note.ArrowBackIcon
import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.stringRes
import com.vitorpamplona.amethyst.ui.theme.DividerThickness import com.vitorpamplona.amethyst.ui.theme.DividerThickness
import com.vitorpamplona.amethyst.ui.theme.WarningColor import com.vitorpamplona.amethyst.ui.theme.WarningColor
import com.vitorpamplona.quartz.nip56Reports.ReportEvent import com.vitorpamplona.quartz.nip56Reports.ReportType
import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toImmutableList
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@ -75,13 +75,13 @@ fun ReportNoteDialog(
) { ) {
val reportTypes = val reportTypes =
listOf( listOf(
Pair(ReportEvent.ReportType.SPAM, stringRes(R.string.report_dialog_spam)), Pair(ReportType.SPAM, stringRes(R.string.report_dialog_spam)),
Pair(ReportEvent.ReportType.PROFANITY, stringRes(R.string.report_dialog_profanity)), Pair(ReportType.PROFANITY, stringRes(R.string.report_dialog_profanity)),
Pair(ReportEvent.ReportType.IMPERSONATION, stringRes(R.string.report_dialog_impersonation)), Pair(ReportType.IMPERSONATION, stringRes(R.string.report_dialog_impersonation)),
Pair(ReportEvent.ReportType.NUDITY, stringRes(R.string.report_dialog_nudity)), Pair(ReportType.NUDITY, stringRes(R.string.report_dialog_nudity)),
Pair(ReportEvent.ReportType.ILLEGAL, stringRes(R.string.report_dialog_illegal)), Pair(ReportType.ILLEGAL, stringRes(R.string.report_dialog_illegal)),
Pair(ReportEvent.ReportType.MALWARE, stringRes(R.string.report_malware)), Pair(ReportType.MALWARE, stringRes(R.string.report_malware)),
Pair(ReportEvent.ReportType.MOD, stringRes(R.string.report_mod)), Pair(ReportType.MOD, stringRes(R.string.report_mod)),
) )
val reasonOptions = remember { reportTypes.map { TitleExplainer(it.second) }.toImmutableList() } val reasonOptions = remember { reportTypes.map { TitleExplainer(it.second) }.toImmutableList() }

View File

@ -36,7 +36,7 @@ import com.vitorpamplona.amethyst.ui.note.externalLinkForUser
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.stringRes
import com.vitorpamplona.amethyst.ui.theme.DividerThickness import com.vitorpamplona.amethyst.ui.theme.DividerThickness
import com.vitorpamplona.quartz.nip56Reports.ReportEvent import com.vitorpamplona.quartz.nip56Reports.ReportType
@Composable @Composable
fun UserProfileDropDownMenu( fun UserProfileDropDownMenu(
@ -108,42 +108,42 @@ fun UserProfileDropDownMenu(
DropdownMenuItem( DropdownMenuItem(
text = { Text(stringRes(id = R.string.report_spam_scam)) }, text = { Text(stringRes(id = R.string.report_spam_scam)) },
onClick = { onClick = {
accountViewModel.report(user, ReportEvent.ReportType.SPAM) accountViewModel.report(user, ReportType.SPAM)
onDismiss() onDismiss()
}, },
) )
DropdownMenuItem( DropdownMenuItem(
text = { Text(stringRes(R.string.report_hateful_speech)) }, text = { Text(stringRes(R.string.report_hateful_speech)) },
onClick = { onClick = {
accountViewModel.report(user, ReportEvent.ReportType.PROFANITY) accountViewModel.report(user, ReportType.PROFANITY)
onDismiss() onDismiss()
}, },
) )
DropdownMenuItem( DropdownMenuItem(
text = { Text(stringRes(id = R.string.report_impersonation)) }, text = { Text(stringRes(id = R.string.report_impersonation)) },
onClick = { onClick = {
accountViewModel.report(user, ReportEvent.ReportType.IMPERSONATION) accountViewModel.report(user, ReportType.IMPERSONATION)
onDismiss() onDismiss()
}, },
) )
DropdownMenuItem( DropdownMenuItem(
text = { Text(stringRes(R.string.report_nudity_porn)) }, text = { Text(stringRes(R.string.report_nudity_porn)) },
onClick = { onClick = {
accountViewModel.report(user, ReportEvent.ReportType.NUDITY) accountViewModel.report(user, ReportType.NUDITY)
onDismiss() onDismiss()
}, },
) )
DropdownMenuItem( DropdownMenuItem(
text = { Text(stringRes(id = R.string.report_illegal_behaviour)) }, text = { Text(stringRes(id = R.string.report_illegal_behaviour)) },
onClick = { onClick = {
accountViewModel.report(user, ReportEvent.ReportType.ILLEGAL) accountViewModel.report(user, ReportType.ILLEGAL)
onDismiss() onDismiss()
}, },
) )
DropdownMenuItem( DropdownMenuItem(
text = { Text(stringRes(id = R.string.report_malware)) }, text = { Text(stringRes(id = R.string.report_malware)) },
onClick = { onClick = {
accountViewModel.report(user, ReportEvent.ReportType.MALWARE) accountViewModel.report(user, ReportType.MALWARE)
onDismiss() onDismiss()
}, },
) )

View File

@ -25,14 +25,15 @@ import com.vitorpamplona.quartz.nip01Core.core.AddressableEvent
import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.core.Event
import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate
import com.vitorpamplona.quartz.nip31Alts.AltTag import com.vitorpamplona.quartz.nip31Alts.AltTag
import com.vitorpamplona.quartz.nip31Alts.alt
import com.vitorpamplona.quartz.nip56Reports.tags.DefaultReportTag
import com.vitorpamplona.quartz.nip56Reports.tags.ReportedAddressTag
import com.vitorpamplona.quartz.nip56Reports.tags.ReportedAuthorTag
import com.vitorpamplona.quartz.nip56Reports.tags.ReportedEventTag
import com.vitorpamplona.quartz.utils.TimeUtils import com.vitorpamplona.quartz.utils.TimeUtils
@Immutable data class ReportedKey(
val key: String,
val reportType: ReportEvent.ReportType,
)
// NIP 56 event. // NIP 56 event.
@Immutable @Immutable
class ReportEvent( class ReportEvent(
@ -43,48 +44,35 @@ class ReportEvent(
content: String, content: String,
sig: HexKey, sig: HexKey,
) : Event(id, pubKey, createdAt, KIND, tags, content, sig) { ) : Event(id, pubKey, createdAt, KIND, tags, content, sig) {
@Transient
private var defaultType: ReportType? = null
private fun defaultReportTypes() = tags.mapNotNull(DefaultReportTag::parse)
private fun defaultReportType(): ReportType { private fun defaultReportType(): ReportType {
defaultType?.let { return it }
// Works with old and new structures for report. // Works with old and new structures for report.
var reportType = var reportType = defaultReportTypes().firstOrNull()
tags
.filter { it.firstOrNull() == "report" }
.mapNotNull { it.getOrNull(1) }
.map { ReportType.valueOf(it.uppercase()) }
.firstOrNull()
if (reportType == null) { if (reportType == null) {
reportType = reportType = tags.mapNotNull { it.getOrNull(2) }.map { ReportType.parseOrNull(it, emptyArray()) }.firstOrNull()
tags.mapNotNull { it.getOrNull(2) }.map { ReportType.valueOf(it.uppercase()) }.firstOrNull()
} }
if (reportType == null) { if (reportType == null) {
reportType = ReportType.SPAM reportType = ReportType.SPAM
} }
defaultType = reportType
return reportType return reportType
} }
fun reportedPost() = fun reportedPost() = tags.mapNotNull { ReportedEventTag.parse(it, defaultReportType()) }
tags
.filter { it.size > 1 && it[0] == "e" }
.map {
ReportedKey(
it[1],
it.getOrNull(2)?.uppercase()?.let { it1 -> ReportType.valueOf(it1) }
?: defaultReportType(),
)
}
fun reportedAuthor() = fun reportedAddresses() = tags.mapNotNull { ReportedAddressTag.parse(it, defaultReportType()) }
tags
.filter { it.size > 1 && it[0] == "p" } fun reportedAuthor() = tags.mapNotNull { ReportedAuthorTag.parse(it, defaultReportType()) }
.map {
ReportedKey(
it[1],
it.getOrNull(2)?.uppercase()?.let { it1 -> ReportType.valueOf(it1) }
?: defaultReportType(),
)
}
companion object { companion object {
const val KIND = 1984 const val KIND = 1984
const val ALT_PREFIX = "Report for "
fun create( fun create(
reportedPost: Event, reportedPost: Event,
@ -108,32 +96,27 @@ class ReportEvent(
signer.sign(createdAt, KIND, tags, content, onReady) signer.sign(createdAt, KIND, tags, content, onReady)
} }
fun create( fun build(
reportedUser: String, reportedPost: Event,
type: ReportType, type: ReportType,
signer: NostrSigner,
createdAt: Long = TimeUtils.now(), createdAt: Long = TimeUtils.now(),
onReady: (ReportEvent) -> Unit, ) = eventTemplate(KIND, "", createdAt) {
) { alt(ALT_PREFIX + type.code)
val content = "" event(reportedPost.id, type)
user(reportedPost.pubKey, type)
val reportAuthorTag = arrayOf("p", reportedUser, type.name.lowercase()) if (reportedPost is AddressableEvent) {
val alt = AltTag.assemble("Report for ${type.name}") address(reportedPost.address(), type)
}
}
val tags: Array<Array<String>> = arrayOf(reportAuthorTag, alt) fun build(
signer.sign(createdAt, KIND, tags, content, onReady) reportedUser: HexKey,
type: ReportType,
createdAt: Long = TimeUtils.now(),
) = eventTemplate(KIND, "", createdAt) {
alt(ALT_PREFIX + type.code)
user(reportedUser, type)
} }
} }
enum class ReportType {
EXPLICIT, // Not used anymore.
ILLEGAL,
SPAM,
IMPERSONATION,
NUDITY,
PROFANITY,
MALWARE,
MOD,
OTHER,
}
} }

View File

@ -0,0 +1,61 @@
/**
* Copyright (c) 2024 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.quartz.nip56Reports
import android.util.Log
enum class ReportType(
val code: String,
) {
EXPLICIT("explicit"), // Not used anymore.
ILLEGAL("illegal"),
SPAM("spam"),
IMPERSONATION("impersonation"),
NUDITY("nudity"),
PROFANITY("profanity"),
MALWARE("malware"),
MOD("mod"),
OTHER("other"),
;
companion object {
fun parseOrNull(
code: String,
tag: Array<String>,
): ReportType? =
when (code) {
EXPLICIT.code -> EXPLICIT
ILLEGAL.code -> ILLEGAL
SPAM.code -> SPAM
IMPERSONATION.code -> IMPERSONATION
NUDITY.code -> NUDITY
PROFANITY.code -> PROFANITY
MALWARE.code -> MALWARE
MOD.code -> MOD
"MOD" -> MOD
OTHER.code -> OTHER
else -> {
Log.w("ReportedEventTag", "Report type not supported: $code ${tag.joinToString(", ")}")
null
}
}
}
}

View File

@ -0,0 +1,52 @@
/**
* Copyright (c) 2024 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.quartz.nip56Reports
import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder
import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address
import com.vitorpamplona.quartz.nip56Reports.tags.HashSha256Tag
import com.vitorpamplona.quartz.nip56Reports.tags.ReportedAddressTag
import com.vitorpamplona.quartz.nip56Reports.tags.ReportedAuthorTag
import com.vitorpamplona.quartz.nip56Reports.tags.ReportedEventTag
import com.vitorpamplona.quartz.nip56Reports.tags.ServerTag
fun TagArrayBuilder<ReportEvent>.event(
eventId: HexKey,
reportType: ReportType,
) = addUnique(ReportedEventTag.assemble(eventId, reportType))
fun TagArrayBuilder<ReportEvent>.address(
address: Address,
reportType: ReportType,
) = addUnique(ReportedAddressTag.assemble(address, reportType))
fun TagArrayBuilder<ReportEvent>.user(
pubkey: HexKey,
reportType: ReportType,
) = addUnique(ReportedAuthorTag.assemble(pubkey, reportType))
fun TagArrayBuilder<ReportEvent>.hash(
x: String,
reportType: ReportType,
) = addUnique(HashSha256Tag.assemble(x, reportType))
fun TagArrayBuilder<ReportEvent>.server(url: String) = addUnique(ServerTag.assemble(url))

View File

@ -18,10 +18,10 @@
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/ */
package com.vitorpamplona.quartz.nip70ProtectedEvts package com.vitorpamplona.quartz.nip56Reports.tags
class ProtectedTagSerializer { import com.vitorpamplona.quartz.nip56Reports.ReportType
companion object {
fun toTagArray(reason: String = "") = arrayOf("-", reason) interface BaseReportTag {
} val type: ReportType?
} }

View File

@ -0,0 +1,46 @@
/**
* Copyright (c) 2024 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.quartz.nip56Reports.tags
import androidx.compose.runtime.Immutable
import com.vitorpamplona.quartz.nip01Core.core.has
import com.vitorpamplona.quartz.nip56Reports.ReportType
import com.vitorpamplona.quartz.utils.arrayOfNotNull
import com.vitorpamplona.quartz.utils.ensure
@Immutable
class DefaultReportTag {
companion object {
const val TAG_NAME = "report"
@JvmStatic
fun parse(tag: Array<String>): ReportType? {
ensure(tag.has(1)) { return null }
ensure(tag[0] == TAG_NAME) { return null }
ensure(tag[1].isNotEmpty()) { return null }
return ReportType.parseOrNull(tag[1], tag)
}
@JvmStatic
fun assemble(type: ReportType? = null) = arrayOfNotNull(TAG_NAME, type?.code)
}
}

View File

@ -0,0 +1,55 @@
/**
* Copyright (c) 2024 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.quartz.nip56Reports.tags
import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.nip01Core.core.has
import com.vitorpamplona.quartz.nip56Reports.ReportType
import com.vitorpamplona.quartz.utils.arrayOfNotNull
import com.vitorpamplona.quartz.utils.ensure
class HashSha256Tag(
val hash: HexKey,
override val type: ReportType? = null,
) : BaseReportTag {
companion object {
const val TAG_NAME = "x"
@JvmStatic
fun parse(
tag: Array<String>,
defaultReportType: ReportType? = null,
): HashSha256Tag? {
ensure(tag.has(1)) { return null }
ensure(tag[0] == TAG_NAME) { return null }
ensure(tag[1].length == 64) { return null }
val type = tag.getOrNull(2)?.let { ReportType.parseOrNull(it, tag) } ?: defaultReportType
return HashSha256Tag(tag[1], type)
}
@JvmStatic
fun assemble(
hash: String,
type: ReportType? = null,
) = arrayOfNotNull(TAG_NAME, hash, type?.code)
}
}

View File

@ -0,0 +1,71 @@
/**
* Copyright (c) 2024 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.quartz.nip56Reports.tags
import androidx.compose.runtime.Immutable
import com.vitorpamplona.quartz.nip01Core.core.has
import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address
import com.vitorpamplona.quartz.nip56Reports.ReportType
import com.vitorpamplona.quartz.utils.arrayOfNotNull
import com.vitorpamplona.quartz.utils.ensure
@Immutable
class ReportedAddressTag(
val address: Address,
override val type: ReportType? = null,
) : BaseReportTag {
fun toTagArray() = assemble(address, type)
companion object {
const val TAG_NAME = "a"
@JvmStatic
fun parse(
tag: Array<String>,
defaultReportType: ReportType? = null,
): ReportedAddressTag? {
ensure(tag.has(1)) { return null }
ensure(tag[0] == TAG_NAME) { return null }
ensure(tag[1].isNotEmpty()) { return null }
val address = Address.parse(tag[1])
ensure(address != null) { return null }
val type =
if (tag.size == 2) {
defaultReportType
} else if (tag.size == 3) {
ReportType.parseOrNull(tag[2], tag) ?: defaultReportType
} else {
ReportType.parseOrNull(tag[3], tag) ?: defaultReportType
}
return ReportedAddressTag(address, type)
}
@JvmStatic
fun assemble(
address: Address,
type: ReportType? = null,
) = arrayOfNotNull(TAG_NAME, address.toValue(), type?.code)
}
}

View File

@ -0,0 +1,67 @@
/**
* Copyright (c) 2024 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.quartz.nip56Reports.tags
import androidx.compose.runtime.Immutable
import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.nip01Core.core.has
import com.vitorpamplona.quartz.nip56Reports.ReportType
import com.vitorpamplona.quartz.utils.arrayOfNotNull
import com.vitorpamplona.quartz.utils.ensure
@Immutable
class ReportedAuthorTag(
val pubkey: HexKey,
override val type: ReportType? = null,
) : BaseReportTag {
fun toTagArray() = assemble(pubkey, type)
companion object {
const val TAG_NAME = "p"
@JvmStatic
fun parse(
tag: Array<String>,
defaultReportType: ReportType? = null,
): ReportedAuthorTag? {
ensure(tag.has(1)) { return null }
ensure(tag[0] == TAG_NAME) { return null }
ensure(tag[1].length == 64) { return null }
val type =
if (tag.size == 2) {
defaultReportType
} else if (tag.size == 3) {
ReportType.parseOrNull(tag[2], tag) ?: defaultReportType
} else {
ReportType.parseOrNull(tag[3], tag) ?: defaultReportType
}
return ReportedAuthorTag(tag[1], type)
}
@JvmStatic
fun assemble(
pubkey: HexKey,
type: ReportType? = null,
) = arrayOfNotNull(TAG_NAME, pubkey, type?.code)
}
}

View File

@ -0,0 +1,67 @@
/**
* Copyright (c) 2024 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.quartz.nip56Reports.tags
import androidx.compose.runtime.Immutable
import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.nip01Core.core.has
import com.vitorpamplona.quartz.nip56Reports.ReportType
import com.vitorpamplona.quartz.utils.arrayOfNotNull
import com.vitorpamplona.quartz.utils.ensure
@Immutable
class ReportedEventTag(
val eventId: HexKey,
override val type: ReportType? = null,
) : BaseReportTag {
fun toTagArray() = assemble(eventId, type)
companion object {
const val TAG_NAME = "e"
@JvmStatic
fun parse(
tag: Array<String>,
defaultReportType: ReportType? = null,
): ReportedEventTag? {
ensure(tag.has(1)) { return null }
ensure(tag[0] == TAG_NAME) { return null }
ensure(tag[1].length == 64) { return null }
val type =
if (tag.size == 2) {
defaultReportType
} else if (tag.size == 3) {
ReportType.parseOrNull(tag[2], tag) ?: defaultReportType
} else {
ReportType.parseOrNull(tag[3], tag) ?: defaultReportType
}
return ReportedEventTag(tag[1], type)
}
@JvmStatic
fun assemble(
eventId: HexKey,
type: ReportType? = null,
) = arrayOfNotNull(TAG_NAME, eventId, type?.code)
}
}

View File

@ -0,0 +1,43 @@
/**
* Copyright (c) 2024 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.quartz.nip56Reports.tags
import com.vitorpamplona.quartz.nip01Core.core.has
import com.vitorpamplona.quartz.utils.ensure
class ServerTag {
companion object {
const val TAG_NAME = "server"
@JvmStatic
fun parse(tag: Array<String>): String? {
ensure(tag.has(1)) { return null }
ensure(tag[0] == TAG_NAME) { return null }
return tag[1]
}
@JvmStatic
fun assemble(url: String) = arrayOf(TAG_NAME, url)
@JvmStatic
fun assemble(urls: List<String>) = urls.map { assemble(it) }
}
}