From 570673889815088ea43b24350dc800d9de3e04ea Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Wed, 5 Jul 2023 11:22:03 -0400 Subject: [PATCH] Initial Support for Communities --- .../amethyst/model/LocalCache.kt | 50 +++++++++++++++++++ .../service/model/CommunityDefinitionEvent.kt | 42 ++++++++++++++++ .../model/CommunityPostApprovalEvent.kt | 47 +++++++++++++++++ .../amethyst/service/model/Event.kt | 2 + 4 files changed, 141 insertions(+) create mode 100644 app/src/main/java/com/vitorpamplona/amethyst/service/model/CommunityDefinitionEvent.kt create mode 100644 app/src/main/java/com/vitorpamplona/amethyst/service/model/CommunityPostApprovalEvent.kt diff --git a/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt b/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt index 562644350..690566c73 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt @@ -317,6 +317,25 @@ object LocalCache { refreshObservers(note) } + private fun consume(event: CommunityDefinitionEvent, relay: Relay?) { + val version = getOrCreateNote(event.id) + val note = getOrCreateAddressableNote(event.address()) + val author = getOrCreateUser(event.pubKey) + + if (version.event == null) { + version.loadEvent(event, author, emptyList()) + version.moveAllReferencesTo(note) + } + + if (note.event?.id() == event.id()) return + + if (event.createdAt > (note.createdAt() ?: 0)) { + note.loadEvent(event, author, emptyList()) + + refreshObservers(note) + } + } + private fun consume(event: LiveActivitiesEvent, relay: Relay?) { val version = getOrCreateNote(event.id) val note = getOrCreateAddressableNote(event.address()) @@ -653,6 +672,30 @@ object LocalCache { refreshObservers(note) } + fun consume(event: CommunityPostApprovalEvent) { + val note = getOrCreateNote(event.id) + + // Already processed this event. + if (note.event != null) return + + // Log.d("TN", "New Boost (${notes.size},${users.size}) ${note.author?.toBestDisplayName()} ${formattedDateTime(event.createdAt)}") + + val author = getOrCreateUser(event.pubKey) + val repliesTo = event.taggedAddresses().map { getOrCreateAddressableNote(it) } + + note.loadEvent(event, author, repliesTo) + + // Prepares user's profile view. + author.addNote(note) + + // Counts the replies + repliesTo.forEach { + it.addReply(note) + } + + refreshObservers(note) + } + fun consume(event: ReactionEvent) { val note = getOrCreateNote(event.id) @@ -1218,6 +1261,13 @@ object LocalCache { is ChannelMessageEvent -> consume(event, relay) is ChannelMetadataEvent -> consume(event) is ChannelMuteUserEvent -> consume(event) + is CommunityDefinitionEvent -> consume(event, relay) + is CommunityPostApprovalEvent -> { + event.containedPost()?.let { + verifyAndConsume(it, relay) + } + consume(event) + } is ContactListEvent -> consume(event) is DeletionEvent -> consume(event) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/CommunityDefinitionEvent.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/CommunityDefinitionEvent.kt new file mode 100644 index 000000000..9c0901c8d --- /dev/null +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/CommunityDefinitionEvent.kt @@ -0,0 +1,42 @@ +package com.vitorpamplona.amethyst.service.model + +import androidx.compose.runtime.Immutable +import com.vitorpamplona.amethyst.model.HexKey +import com.vitorpamplona.amethyst.model.toHexKey +import nostr.postr.Utils +import java.util.Date + +@Immutable +class CommunityDefinitionEvent( + id: HexKey, + pubKey: HexKey, + createdAt: Long, + tags: List>, + content: String, + sig: HexKey +) : Event(id, pubKey, createdAt, kind, tags, content, sig), AddressableEvent { + + override fun dTag() = tags.firstOrNull { it.size > 1 && it[0] == "d" }?.get(1) ?: "" + override fun address() = ATag(kind, pubKey, dTag(), null) + + fun description() = tags.firstOrNull { it.size > 1 && it[0] == "description" }?.get(1) + fun image() = tags.firstOrNull { it.size > 1 && it[0] == "image" }?.get(1) + fun rules() = tags.firstOrNull { it.size > 1 && it[0] == "rules" }?.get(1) + + fun moderators() = tags.filter { it.size > 1 && it[0] == "p" }.map { Participant(it[1], it.getOrNull(3)) } + + companion object { + const val kind = 34550 + + fun create( + privateKey: ByteArray, + createdAt: Long = Date().time / 1000 + ): CommunityDefinitionEvent { + val tags = mutableListOf>() + val pubKey = Utils.pubkeyCreate(privateKey).toHexKey() + val id = generateId(pubKey, createdAt, kind, tags, "") + val sig = Utils.sign(id, privateKey) + return CommunityDefinitionEvent(id.toHexKey(), pubKey, createdAt, tags, "", sig.toHexKey()) + } + } +} diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/CommunityPostApprovalEvent.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/CommunityPostApprovalEvent.kt new file mode 100644 index 000000000..efdc82ba1 --- /dev/null +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/CommunityPostApprovalEvent.kt @@ -0,0 +1,47 @@ +package com.vitorpamplona.amethyst.service.model + +import android.util.Log +import androidx.compose.runtime.Immutable +import com.vitorpamplona.amethyst.model.HexKey +import com.vitorpamplona.amethyst.model.toHexKey +import com.vitorpamplona.amethyst.service.relays.Client +import nostr.postr.Utils +import java.util.Date + +@Immutable +class CommunityPostApprovalEvent( + id: HexKey, + pubKey: HexKey, + createdAt: Long, + tags: List>, + content: String, + sig: HexKey +) : Event(id, pubKey, createdAt, kind, tags, content, sig) { + fun containedPost(): Event? = try { + content.ifBlank { null }?.let { + fromJson(it, Client.lenient) + } + } catch (e: Exception) { + Log.e("LnZapEvent", "Failed to Parse Contained Post $content", e) + null + } + + companion object { + const val kind = 4550 + + fun create(approvedPost: Event, community: CommunityDefinitionEvent, privateKey: ByteArray, createdAt: Long = Date().time / 1000): GenericRepostEvent { + val content = approvedPost.toJson() + + val communities = listOf("a", community.address().toTag()) + val replyToPost = listOf("e", approvedPost.id()) + val replyToAuthor = listOf("p", approvedPost.pubKey()) + val kind = listOf("k", "${approvedPost.kind()}") + + val pubKey = Utils.pubkeyCreate(privateKey).toHexKey() + val tags: List> = listOf(communities, replyToPost, replyToAuthor, kind) + val id = generateId(pubKey, createdAt, GenericRepostEvent.kind, tags, content) + val sig = Utils.sign(id, privateKey) + return GenericRepostEvent(id.toHexKey(), pubKey, createdAt, tags, content, sig.toHexKey()) + } + } +} diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/Event.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/Event.kt index 8a753d7c2..53eb5ac03 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/model/Event.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/Event.kt @@ -243,6 +243,8 @@ open class Event( ChannelMessageEvent.kind -> ChannelMessageEvent(id, pubKey, createdAt, tags, content, sig) ChannelMetadataEvent.kind -> ChannelMetadataEvent(id, pubKey, createdAt, tags, content, sig) ChannelMuteUserEvent.kind -> ChannelMuteUserEvent(id, pubKey, createdAt, tags, content, sig) + CommunityDefinitionEvent.kind -> CommunityDefinitionEvent(id, pubKey, createdAt, tags, content, sig) + CommunityPostApprovalEvent.kind -> CommunityPostApprovalEvent(id, pubKey, createdAt, tags, content, sig) ContactListEvent.kind -> ContactListEvent(id, pubKey, createdAt, tags, content, sig) DeletionEvent.kind -> DeletionEvent(id, pubKey, createdAt, tags, content, sig)