From b017e03728d3c822259e1072a7f0c934f35ee508 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Wed, 12 Mar 2025 14:51:23 -0400 Subject: [PATCH] Updates NIP-56 to the newest tag structure for Quartz --- .../vitorpamplona/amethyst/model/Account.kt | 14 +-- .../amethyst/model/LocalCache.kt | 8 +- .../com/vitorpamplona/amethyst/model/Note.kt | 11 +++ .../com/vitorpamplona/amethyst/model/User.kt | 5 +- .../amethyst/ui/note/types/Report.kt | 22 +++-- .../ui/screen/loggedIn/AccountViewModel.kt | 6 +- .../ui/screen/loggedIn/ReportNoteDialog.kt | 16 ++-- .../profile/header/UserProfileDropDownMenu.kt | 14 +-- .../quartz/nip56Reports/ReportEvent.kt | 93 ++++++++----------- .../quartz/nip56Reports/ReportType.kt | 61 ++++++++++++ .../quartz/nip56Reports/TagArrayBuilderExt.kt | 52 +++++++++++ .../tags/BaseReportTag.kt} | 10 +- .../nip56Reports/tags/DefaultReportTag.kt | 46 +++++++++ .../quartz/nip56Reports/tags/HashSha256Tag.kt | 55 +++++++++++ .../nip56Reports/tags/ReportedAddressTag.kt | 71 ++++++++++++++ .../nip56Reports/tags/ReportedAuthorTag.kt | 67 +++++++++++++ .../nip56Reports/tags/ReportedEventTag.kt | 67 +++++++++++++ .../quartz/nip56Reports/tags/ServerTag.kt | 43 +++++++++ 18 files changed, 561 insertions(+), 100 deletions(-) create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip56Reports/ReportType.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip56Reports/TagArrayBuilderExt.kt rename quartz/src/main/java/com/vitorpamplona/quartz/{nip70ProtectedEvts/ProtectedTagSerializer.kt => nip56Reports/tags/BaseReportTag.kt} (86%) create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip56Reports/tags/DefaultReportTag.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip56Reports/tags/HashSha256Tag.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip56Reports/tags/ReportedAddressTag.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip56Reports/tags/ReportedAuthorTag.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip56Reports/tags/ReportedEventTag.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip56Reports/tags/ServerTag.kt diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt index d73db1e3c..f6cfb9f61 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt @@ -149,6 +149,7 @@ import com.vitorpamplona.quartz.nip51Lists.GeneralListEvent import com.vitorpamplona.quartz.nip51Lists.MuteListEvent import com.vitorpamplona.quartz.nip51Lists.PeopleListEvent import com.vitorpamplona.quartz.nip56Reports.ReportEvent +import com.vitorpamplona.quartz.nip56Reports.ReportType import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent import com.vitorpamplona.quartz.nip57Zaps.LnZapRequestEvent import com.vitorpamplona.quartz.nip57Zaps.splits.ZapSplitSetup @@ -1616,18 +1617,18 @@ class Account( suspend fun report( note: Note, - type: ReportEvent.ReportType, + type: ReportType, content: String = "", ) { if (!isWriteable()) return - if (note.hasReacted(userProfile(), "⚠️")) { - // has already liked this note + if (note.hasReport(userProfile(), type)) { + // has already reported this note return } note.event?.let { - ReportEvent.create(it, type, signer, content) { + signer.sign(ReportEvent.build(it, type)) { Amethyst.instance.client.send(it) LocalCache.justConsume(it, null) } @@ -1636,7 +1637,7 @@ class Account( suspend fun report( user: User, - type: ReportEvent.ReportType, + type: ReportType, ) { if (!isWriteable()) return @@ -1645,7 +1646,8 @@ class Account( return } - ReportEvent.create(user.pubkeyHex, type, signer) { + val template = ReportEvent.build(user.pubkeyHex, type) + signer.sign(template) { Amethyst.instance.client.send(it) LocalCache.justConsume(it, null) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt index 1c9c97fa0..be5342272 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt @@ -695,8 +695,8 @@ object LocalCache { event.originalPost().mapNotNull { checkGetOrCreateNote(it) } + event.taggedAddresses().map { getOrCreateAddressableNote(it) } is ReportEvent -> - event.reportedPost().mapNotNull { checkGetOrCreateNote(it.key) } + - event.taggedAddresses().map { getOrCreateAddressableNote(it) } + event.reportedPost().mapNotNull { checkGetOrCreateNote(it.eventId) } + + event.reportedAddresses().map { getOrCreateAddressableNote(it.address) } is ChannelMessageEvent -> event .tagsWithoutCitations() @@ -1342,7 +1342,7 @@ object LocalCache { // Already processed this event. 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) note.loadEvent(event, author, repliesTo) @@ -2218,7 +2218,7 @@ object LocalCache { } if (noteEvent is ReportEvent) { noteEvent.reportedAuthor().mapNotNull { - val author = getUserIfExists(it.key) + val author = getUserIfExists(it.pubkey) author?.removeReport(note) author?.clearEOSE() } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Note.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Note.kt index 9868922ca..984781e74 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Note.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Note.kt @@ -68,6 +68,8 @@ import com.vitorpamplona.quartz.nip47WalletConnect.LnZapPaymentResponseEvent import com.vitorpamplona.quartz.nip47WalletConnect.PayInvoiceSuccessResponse import com.vitorpamplona.quartz.nip53LiveActivities.chat.LiveActivitiesChatMessageEvent 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.LnZapRequestEvent 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 = replies .filter { it.event?.hasAdditionalReward() ?: false } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/User.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/User.kt index 712a7f4f9..08c8a7c41 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/User.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/User.kt @@ -46,6 +46,7 @@ import com.vitorpamplona.quartz.nip19Bech32.entities.NProfile import com.vitorpamplona.quartz.nip19Bech32.toNpub import com.vitorpamplona.quartz.nip51Lists.BookmarkListEvent import com.vitorpamplona.quartz.nip56Reports.ReportEvent +import com.vitorpamplona.quartz.nip56Reports.ReportType import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent import com.vitorpamplona.quartz.utils.DualCase @@ -357,11 +358,11 @@ class User( fun hasReport( loggedIn: User, - type: ReportEvent.ReportType, + type: ReportType, ): Boolean = reports[loggedIn]?.firstOrNull { it.event is ReportEvent && - (it.event as ReportEvent).reportedAuthor().any { it.reportType == type } + (it.event as ReportEvent).reportedAuthor().any { it.type == type } } != null fun containsAny(hiddenWordsCase: List): Boolean { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Report.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Report.kt index 4c3aaa019..a2a2a5135 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Report.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Report.kt @@ -36,6 +36,7 @@ import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.theme.replyModifier import com.vitorpamplona.quartz.nip02FollowList.EmptyTagList import com.vitorpamplona.quartz.nip56Reports.ReportEvent +import com.vitorpamplona.quartz.nip56Reports.ReportType @Composable fun RenderReport( @@ -52,16 +53,17 @@ fun RenderReport( val reportType = base .map { - when (it.reportType) { - ReportEvent.ReportType.EXPLICIT -> stringRes(R.string.explicit_content) - ReportEvent.ReportType.NUDITY -> stringRes(R.string.nudity) - ReportEvent.ReportType.PROFANITY -> stringRes(R.string.profanity_hateful_speech) - ReportEvent.ReportType.SPAM -> stringRes(R.string.spam) - ReportEvent.ReportType.IMPERSONATION -> stringRes(R.string.impersonation) - ReportEvent.ReportType.ILLEGAL -> stringRes(R.string.illegal_behavior) - ReportEvent.ReportType.MALWARE -> stringRes(R.string.malware) - ReportEvent.ReportType.MOD -> stringRes(R.string.mod) - ReportEvent.ReportType.OTHER -> stringRes(R.string.other) + when (it.type) { + ReportType.EXPLICIT -> stringRes(R.string.explicit_content) + ReportType.NUDITY -> stringRes(R.string.nudity) + ReportType.PROFANITY -> stringRes(R.string.profanity_hateful_speech) + ReportType.SPAM -> stringRes(R.string.spam) + ReportType.IMPERSONATION -> stringRes(R.string.impersonation) + ReportType.ILLEGAL -> stringRes(R.string.illegal_behavior) + ReportType.MALWARE -> stringRes(R.string.malware) + ReportType.MOD -> stringRes(R.string.mod) + ReportType.OTHER -> stringRes(R.string.other) + null -> stringRes(R.string.other) } }.toSet() .joinToString(", ") diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt index 6b131680e..14d7e73ab 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt @@ -102,7 +102,7 @@ import com.vitorpamplona.quartz.nip37Drafts.DraftEvent import com.vitorpamplona.quartz.nip47WalletConnect.Response import com.vitorpamplona.quartz.nip50Search.SearchRelayListEvent 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.LnZapRequestEvent import com.vitorpamplona.quartz.nip57Zaps.zapraiser.zapraiserAmount @@ -741,7 +741,7 @@ class AccountViewModel( fun report( note: Note, - type: ReportEvent.ReportType, + type: ReportType, content: String = "", ) { viewModelScope.launch(Dispatchers.IO) { account.report(note, type, content) } @@ -749,7 +749,7 @@ class AccountViewModel( fun report( user: User, - type: ReportEvent.ReportType, + type: ReportType, ) { viewModelScope.launch(Dispatchers.IO) { account.report(user, type) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ReportNoteDialog.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ReportNoteDialog.kt index 1fb25ddfa..3ae9a888c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ReportNoteDialog.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ReportNoteDialog.kt @@ -63,7 +63,7 @@ import com.vitorpamplona.amethyst.ui.note.ArrowBackIcon import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.theme.DividerThickness import com.vitorpamplona.amethyst.ui.theme.WarningColor -import com.vitorpamplona.quartz.nip56Reports.ReportEvent +import com.vitorpamplona.quartz.nip56Reports.ReportType import kotlinx.collections.immutable.toImmutableList @OptIn(ExperimentalMaterial3Api::class) @@ -75,13 +75,13 @@ fun ReportNoteDialog( ) { val reportTypes = listOf( - Pair(ReportEvent.ReportType.SPAM, stringRes(R.string.report_dialog_spam)), - Pair(ReportEvent.ReportType.PROFANITY, stringRes(R.string.report_dialog_profanity)), - Pair(ReportEvent.ReportType.IMPERSONATION, stringRes(R.string.report_dialog_impersonation)), - Pair(ReportEvent.ReportType.NUDITY, stringRes(R.string.report_dialog_nudity)), - Pair(ReportEvent.ReportType.ILLEGAL, stringRes(R.string.report_dialog_illegal)), - Pair(ReportEvent.ReportType.MALWARE, stringRes(R.string.report_malware)), - Pair(ReportEvent.ReportType.MOD, stringRes(R.string.report_mod)), + Pair(ReportType.SPAM, stringRes(R.string.report_dialog_spam)), + Pair(ReportType.PROFANITY, stringRes(R.string.report_dialog_profanity)), + Pair(ReportType.IMPERSONATION, stringRes(R.string.report_dialog_impersonation)), + Pair(ReportType.NUDITY, stringRes(R.string.report_dialog_nudity)), + Pair(ReportType.ILLEGAL, stringRes(R.string.report_dialog_illegal)), + Pair(ReportType.MALWARE, stringRes(R.string.report_malware)), + Pair(ReportType.MOD, stringRes(R.string.report_mod)), ) val reasonOptions = remember { reportTypes.map { TitleExplainer(it.second) }.toImmutableList() } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/UserProfileDropDownMenu.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/UserProfileDropDownMenu.kt index 79c05f7ef..2a09eb206 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/UserProfileDropDownMenu.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/UserProfileDropDownMenu.kt @@ -36,7 +36,7 @@ import com.vitorpamplona.amethyst.ui.note.externalLinkForUser import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.theme.DividerThickness -import com.vitorpamplona.quartz.nip56Reports.ReportEvent +import com.vitorpamplona.quartz.nip56Reports.ReportType @Composable fun UserProfileDropDownMenu( @@ -108,42 +108,42 @@ fun UserProfileDropDownMenu( DropdownMenuItem( text = { Text(stringRes(id = R.string.report_spam_scam)) }, onClick = { - accountViewModel.report(user, ReportEvent.ReportType.SPAM) + accountViewModel.report(user, ReportType.SPAM) onDismiss() }, ) DropdownMenuItem( text = { Text(stringRes(R.string.report_hateful_speech)) }, onClick = { - accountViewModel.report(user, ReportEvent.ReportType.PROFANITY) + accountViewModel.report(user, ReportType.PROFANITY) onDismiss() }, ) DropdownMenuItem( text = { Text(stringRes(id = R.string.report_impersonation)) }, onClick = { - accountViewModel.report(user, ReportEvent.ReportType.IMPERSONATION) + accountViewModel.report(user, ReportType.IMPERSONATION) onDismiss() }, ) DropdownMenuItem( text = { Text(stringRes(R.string.report_nudity_porn)) }, onClick = { - accountViewModel.report(user, ReportEvent.ReportType.NUDITY) + accountViewModel.report(user, ReportType.NUDITY) onDismiss() }, ) DropdownMenuItem( text = { Text(stringRes(id = R.string.report_illegal_behaviour)) }, onClick = { - accountViewModel.report(user, ReportEvent.ReportType.ILLEGAL) + accountViewModel.report(user, ReportType.ILLEGAL) onDismiss() }, ) DropdownMenuItem( text = { Text(stringRes(id = R.string.report_malware)) }, onClick = { - accountViewModel.report(user, ReportEvent.ReportType.MALWARE) + accountViewModel.report(user, ReportType.MALWARE) onDismiss() }, ) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip56Reports/ReportEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip56Reports/ReportEvent.kt index 3e222faab..45281e3aa 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip56Reports/ReportEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip56Reports/ReportEvent.kt @@ -25,14 +25,15 @@ import com.vitorpamplona.quartz.nip01Core.core.AddressableEvent import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.core.HexKey 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.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 -@Immutable data class ReportedKey( - val key: String, - val reportType: ReportEvent.ReportType, -) - // NIP 56 event. @Immutable class ReportEvent( @@ -43,48 +44,35 @@ class ReportEvent( content: String, sig: HexKey, ) : 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 { + defaultType?.let { return it } + // Works with old and new structures for report. - var reportType = - tags - .filter { it.firstOrNull() == "report" } - .mapNotNull { it.getOrNull(1) } - .map { ReportType.valueOf(it.uppercase()) } - .firstOrNull() + var reportType = defaultReportTypes().firstOrNull() if (reportType == null) { - reportType = - tags.mapNotNull { it.getOrNull(2) }.map { ReportType.valueOf(it.uppercase()) }.firstOrNull() + reportType = tags.mapNotNull { it.getOrNull(2) }.map { ReportType.parseOrNull(it, emptyArray()) }.firstOrNull() } if (reportType == null) { reportType = ReportType.SPAM } + defaultType = reportType return reportType } - fun reportedPost() = - tags - .filter { it.size > 1 && it[0] == "e" } - .map { - ReportedKey( - it[1], - it.getOrNull(2)?.uppercase()?.let { it1 -> ReportType.valueOf(it1) } - ?: defaultReportType(), - ) - } + fun reportedPost() = tags.mapNotNull { ReportedEventTag.parse(it, defaultReportType()) } - fun reportedAuthor() = - tags - .filter { it.size > 1 && it[0] == "p" } - .map { - ReportedKey( - it[1], - it.getOrNull(2)?.uppercase()?.let { it1 -> ReportType.valueOf(it1) } - ?: defaultReportType(), - ) - } + fun reportedAddresses() = tags.mapNotNull { ReportedAddressTag.parse(it, defaultReportType()) } + + fun reportedAuthor() = tags.mapNotNull { ReportedAuthorTag.parse(it, defaultReportType()) } companion object { const val KIND = 1984 + const val ALT_PREFIX = "Report for " fun create( reportedPost: Event, @@ -108,32 +96,27 @@ class ReportEvent( signer.sign(createdAt, KIND, tags, content, onReady) } - fun create( - reportedUser: String, + fun build( + reportedPost: Event, type: ReportType, - signer: NostrSigner, createdAt: Long = TimeUtils.now(), - onReady: (ReportEvent) -> Unit, - ) { - val content = "" + ) = eventTemplate(KIND, "", createdAt) { + alt(ALT_PREFIX + type.code) + event(reportedPost.id, type) + user(reportedPost.pubKey, type) - val reportAuthorTag = arrayOf("p", reportedUser, type.name.lowercase()) - val alt = AltTag.assemble("Report for ${type.name}") + if (reportedPost is AddressableEvent) { + address(reportedPost.address(), type) + } + } - val tags: Array> = arrayOf(reportAuthorTag, alt) - signer.sign(createdAt, KIND, tags, content, onReady) + fun build( + 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, - } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip56Reports/ReportType.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip56Reports/ReportType.kt new file mode 100644 index 000000000..125926808 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip56Reports/ReportType.kt @@ -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, + ): 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 + } + } + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip56Reports/TagArrayBuilderExt.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip56Reports/TagArrayBuilderExt.kt new file mode 100644 index 000000000..b663a3d13 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip56Reports/TagArrayBuilderExt.kt @@ -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.event( + eventId: HexKey, + reportType: ReportType, +) = addUnique(ReportedEventTag.assemble(eventId, reportType)) + +fun TagArrayBuilder.address( + address: Address, + reportType: ReportType, +) = addUnique(ReportedAddressTag.assemble(address, reportType)) + +fun TagArrayBuilder.user( + pubkey: HexKey, + reportType: ReportType, +) = addUnique(ReportedAuthorTag.assemble(pubkey, reportType)) + +fun TagArrayBuilder.hash( + x: String, + reportType: ReportType, +) = addUnique(HashSha256Tag.assemble(x, reportType)) + +fun TagArrayBuilder.server(url: String) = addUnique(ServerTag.assemble(url)) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip70ProtectedEvts/ProtectedTagSerializer.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip56Reports/tags/BaseReportTag.kt similarity index 86% rename from quartz/src/main/java/com/vitorpamplona/quartz/nip70ProtectedEvts/ProtectedTagSerializer.kt rename to quartz/src/main/java/com/vitorpamplona/quartz/nip56Reports/tags/BaseReportTag.kt index 4245aefd7..0ccff5b2c 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip70ProtectedEvts/ProtectedTagSerializer.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip56Reports/tags/BaseReportTag.kt @@ -18,10 +18,10 @@ * 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.nip70ProtectedEvts +package com.vitorpamplona.quartz.nip56Reports.tags -class ProtectedTagSerializer { - companion object { - fun toTagArray(reason: String = "") = arrayOf("-", reason) - } +import com.vitorpamplona.quartz.nip56Reports.ReportType + +interface BaseReportTag { + val type: ReportType? } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip56Reports/tags/DefaultReportTag.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip56Reports/tags/DefaultReportTag.kt new file mode 100644 index 000000000..24551aa95 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip56Reports/tags/DefaultReportTag.kt @@ -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): 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) + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip56Reports/tags/HashSha256Tag.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip56Reports/tags/HashSha256Tag.kt new file mode 100644 index 000000000..172e94c13 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip56Reports/tags/HashSha256Tag.kt @@ -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, + 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) + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip56Reports/tags/ReportedAddressTag.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip56Reports/tags/ReportedAddressTag.kt new file mode 100644 index 000000000..8910a5fe2 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip56Reports/tags/ReportedAddressTag.kt @@ -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, + 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) + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip56Reports/tags/ReportedAuthorTag.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip56Reports/tags/ReportedAuthorTag.kt new file mode 100644 index 000000000..a0d155dcd --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip56Reports/tags/ReportedAuthorTag.kt @@ -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, + 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) + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip56Reports/tags/ReportedEventTag.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip56Reports/tags/ReportedEventTag.kt new file mode 100644 index 000000000..a939f0c7d --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip56Reports/tags/ReportedEventTag.kt @@ -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, + 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) + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip56Reports/tags/ServerTag.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip56Reports/tags/ServerTag.kt new file mode 100644 index 000000000..9d42dc08d --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip56Reports/tags/ServerTag.kt @@ -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? { + 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) = urls.map { assemble(it) } + } +}