- Adds support for Ephemeral Chats from coolr.chat

- Adds support for following ephemeral chats
- Adds support for live events at the top of the feed.
- Adds support for NIP-51, kind:10005 public chat lists
- Adds support for Channel feeds
- Moves following of NIP-28 chats from the Contact List to kind: 10005
- Disables following of events at the Contact list
- Improves gallery display to slightly override profile pictures when in list
- Starts the Account refactoring by moving custom Emoji, EphemeralList and PublicChat lists to their own packages
- Refactors NIP-51 lists to use common classes of private tags instead of general list classes.
- Starts to separate all Public chats into their own database.
- Removes old account upgrades from the local storage
- Refactors url NIP-11 loading and unifies icon
- Reduces the dependency of Relay classes in the LocalCache, Notes and User classes
This commit is contained in:
Vitor Pamplona
2025-05-02 21:18:10 -04:00
parent b06e53f59d
commit 63a009f36d
86 changed files with 3694 additions and 1012 deletions
@@ -26,6 +26,8 @@ import com.vitorpamplona.quartz.experimental.audio.header.AudioHeaderEvent
import com.vitorpamplona.quartz.experimental.audio.track.AudioTrackEvent
import com.vitorpamplona.quartz.experimental.edits.PrivateOutboxRelayListEvent
import com.vitorpamplona.quartz.experimental.edits.TextNoteModificationEvent
import com.vitorpamplona.quartz.experimental.ephemChat.chat.EphemeralChatEvent
import com.vitorpamplona.quartz.experimental.ephemChat.list.EphemeralChatListEvent
import com.vitorpamplona.quartz.experimental.interactiveStories.InteractiveStoryPrologueEvent
import com.vitorpamplona.quartz.experimental.interactiveStories.InteractiveStoryReadingStateEvent
import com.vitorpamplona.quartz.experimental.interactiveStories.InteractiveStorySceneEvent
@@ -53,11 +55,11 @@ import com.vitorpamplona.quartz.nip18Reposts.RepostEvent
import com.vitorpamplona.quartz.nip22Comments.CommentEvent
import com.vitorpamplona.quartz.nip23LongContent.LongTextNoteEvent
import com.vitorpamplona.quartz.nip25Reactions.ReactionEvent
import com.vitorpamplona.quartz.nip28PublicChat.ChannelListEvent
import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelCreateEvent
import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelHideMessageEvent
import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelMetadataEvent
import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelMuteUserEvent
import com.vitorpamplona.quartz.nip28PublicChat.list.ChannelListEvent
import com.vitorpamplona.quartz.nip28PublicChat.message.ChannelMessageEvent
import com.vitorpamplona.quartz.nip30CustomEmoji.pack.EmojiPackEvent
import com.vitorpamplona.quartz.nip30CustomEmoji.selection.EmojiPackSelectionEvent
@@ -193,6 +195,8 @@ class EventFactory {
DraftEvent.KIND -> DraftEvent(id, pubKey, createdAt, tags, content, sig)
EmojiPackEvent.KIND -> EmojiPackEvent(id, pubKey, createdAt, tags, content, sig)
EmojiPackSelectionEvent.KIND -> EmojiPackSelectionEvent(id, pubKey, createdAt, tags, content, sig)
EphemeralChatEvent.KIND -> EphemeralChatEvent(id, pubKey, createdAt, tags, content, sig)
EphemeralChatListEvent.KIND -> EphemeralChatListEvent(id, pubKey, createdAt, tags, content, sig)
FileHeaderEvent.KIND -> FileHeaderEvent(id, pubKey, createdAt, tags, content, sig)
ProfileGalleryEntryEvent.KIND -> ProfileGalleryEntryEvent(id, pubKey, createdAt, tags, content, sig)
FileServersEvent.KIND -> FileServersEvent(id, pubKey, createdAt, tags, content, sig)
@@ -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.experimental.ephemChat.chat
import androidx.compose.runtime.Immutable
import com.vitorpamplona.quartz.experimental.ephemChat.chat.tags.RelayTag
import com.vitorpamplona.quartz.experimental.ephemChat.chat.tags.RoomTag
import com.vitorpamplona.quartz.nip01Core.core.Event
import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder
import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate
import com.vitorpamplona.quartz.nip31Alts.alt
import com.vitorpamplona.quartz.utils.TimeUtils
@Immutable
class EphemeralChatEvent(
id: HexKey,
pubKey: HexKey,
createdAt: Long,
tags: Array<Array<String>>,
content: String,
sig: HexKey,
) : Event(id, pubKey, createdAt, KIND, tags, content, sig) {
fun room() = tags.firstNotNullOfOrNull(RoomTag::parse) ?: DEFAULT_ROOM
fun relay() = tags.firstNotNullOfOrNull(RelayTag::parse) ?: ""
fun roomId() = RoomId(room(), relay())
companion object {
const val KIND = 23333
const val ALT_DESCRIPTION = "Ephemeral Chat"
const val DEFAULT_ROOM = "_"
fun build(
message: String,
relay: String,
room: String = DEFAULT_ROOM,
createdAt: Long = TimeUtils.now(),
initializer: TagArrayBuilder<EphemeralChatEvent>.() -> Unit = {},
) = eventTemplate(KIND, message, createdAt) {
room(room)
relay(relay)
alt(ALT_DESCRIPTION)
initializer()
}
}
}
@@ -0,0 +1,32 @@
/**
* 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.experimental.ephemChat.chat
import com.vitorpamplona.quartz.nip65RelayList.RelayUrlFormatter
data class RoomId(
val id: String,
val relayUrl: String,
) {
fun toKey() = "$id@$relayUrl"
fun toDisplayKey() = id + "@" + RelayUrlFormatter.displayUrl(relayUrl)
}
@@ -0,0 +1,29 @@
/**
* 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.experimental.ephemChat.chat
import com.vitorpamplona.quartz.experimental.ephemChat.chat.tags.RelayTag
import com.vitorpamplona.quartz.experimental.ephemChat.chat.tags.RoomTag
import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder
fun TagArrayBuilder<EphemeralChatEvent>.room(room: String) = addUnique(RoomTag.assemble(room))
fun TagArrayBuilder<EphemeralChatEvent>.relay(room: String) = addUnique(RelayTag.assemble(room))
@@ -0,0 +1,41 @@
/**
* 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.experimental.ephemChat.chat.tags
import com.vitorpamplona.quartz.nip01Core.core.has
import com.vitorpamplona.quartz.utils.ensure
class RelayTag {
companion object {
const val TAG_NAME = "relay"
@JvmStatic
fun parse(tag: Array<String>): String? {
ensure(tag.has(1)) { return null }
ensure(tag[0] == TAG_NAME) { return null }
ensure(tag[1].isNotEmpty()) { return null }
return tag[1]
}
@JvmStatic
fun assemble(url: String) = arrayOf(TAG_NAME, url)
}
}
@@ -0,0 +1,41 @@
/**
* 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.experimental.ephemChat.chat.tags
import com.vitorpamplona.quartz.nip01Core.core.has
import com.vitorpamplona.quartz.utils.ensure
class RoomTag {
companion object {
const val TAG_NAME = "d"
@JvmStatic
fun parse(tag: Array<String>): String? {
ensure(tag.has(1)) { return null }
ensure(tag[0] == TAG_NAME) { return null }
ensure(tag[1].isNotEmpty()) { return null }
return tag[1]
}
@JvmStatic
fun assemble(url: String) = arrayOf(TAG_NAME, url)
}
}
@@ -0,0 +1,58 @@
/**
* 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.experimental.ephemChat.db
import androidx.compose.runtime.Stable
import com.vitorpamplona.quartz.experimental.ephemChat.chat.EphemeralChatEvent
import com.vitorpamplona.quartz.experimental.ephemChat.chat.RoomId
import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.utils.LargeCache
import kotlinx.coroutines.flow.MutableStateFlow
@Stable
class Room(
val roomId: RoomId,
) {
constructor(id: String, relayUrl: String) : this(RoomId(id, relayUrl))
val messages = LargeCache<HexKey, EphemeralChatEvent>()
fun addMsg(event: EphemeralChatEvent) {
if (!messages.containsKey(event.id)) {
messages.put(event.id, event)
messageFlow.tryEmit(RoomState(this))
}
}
fun removeMsg(event: EphemeralChatEvent) {
val existingEvent = messages.remove(event.id)
if (existingEvent != null) {
messageFlow.tryEmit(RoomState(this))
}
}
// Observers line up here.
val messageFlow = MutableStateFlow(RoomState(this))
}
class RoomState(
val room: Room,
)
@@ -0,0 +1,45 @@
/**
* 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.experimental.ephemChat.db
import com.vitorpamplona.quartz.experimental.ephemChat.chat.RoomId
import com.vitorpamplona.quartz.utils.LargeCache
class EphemeralRoomCache {
val rooms = LargeCache<RoomId, Room>()
fun getRoomIfExists(
roomId: String,
relayUrl: String,
): Room? = rooms.get(RoomId(roomId, relayUrl))
fun getOrCreateRoom(
roomId: String,
relayUrl: String,
): Room = rooms.getOrCreate(RoomId(roomId, relayUrl)) { Room(roomId, relayUrl) }
fun findRoomsStartingWith(text: String): List<Room> {
if (text.isBlank()) return emptyList()
return rooms.filter { _, room ->
room.roomId.id.startsWith(text)
}
}
}
@@ -0,0 +1,149 @@
/**
* 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.experimental.ephemChat.list
import androidx.compose.runtime.Immutable
import com.vitorpamplona.quartz.experimental.ephemChat.chat.RoomId
import com.vitorpamplona.quartz.experimental.ephemChat.list.tags.RoomIdTag
import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address
import com.vitorpamplona.quartz.nip31Alts.AltTag
import com.vitorpamplona.quartz.nip51Lists.PrivateTagArrayBuilder
import com.vitorpamplona.quartz.nip51Lists.PrivateTagArrayEvent
import com.vitorpamplona.quartz.nip51Lists.encryption.PrivateTagsInContent
import com.vitorpamplona.quartz.utils.TimeUtils
import kotlin.collections.plus
@Immutable
class EphemeralChatListEvent(
id: HexKey,
pubKey: HexKey,
createdAt: Long,
tags: Array<Array<String>>,
content: String,
sig: HexKey,
) : PrivateTagArrayEvent(id, pubKey, createdAt, KIND, tags, content, sig) {
@Transient var publicAndPrivateEventCache: Set<RoomId>? = null
fun publicAndPrivateRoomIds(
signer: NostrSigner,
onReady: (Set<RoomId>) -> Unit,
) {
publicAndPrivateEventCache?.let { eventList ->
onReady(eventList)
return
}
privateTags(signer) {
val set = filterRooms(it)
publicAndPrivateEventCache = set
onReady(set)
}
}
fun filterRooms(privateTags: Array<Array<String>>): Set<RoomId> {
val privateRooms = privateTags.mapNotNull(RoomIdTag::parse)
val publicRooms = tags.mapNotNull(RoomIdTag::parse)
return (privateRooms + publicRooms).toSet()
}
companion object {
const val KIND = 10023
const val ALT = "Ephemeral Chat List"
const val FIXED_D_TAG = ""
fun createAddress(pubKey: HexKey) = Address(KIND, pubKey, FIXED_D_TAG)
fun createRoom(
room: RoomId,
isPrivate: Boolean,
signer: NostrSigner,
createdAt: Long = TimeUtils.now(),
onReady: (EphemeralChatListEvent) -> Unit,
) {
val tags = arrayOf(RoomIdTag.Companion.assemble(room))
if (isPrivate) {
PrivateTagsInContent.Companion.encryptNip04(
privateTags = tags,
signer = signer,
) { encryptedTags ->
create(encryptedTags, emptyArray(), signer, createdAt, onReady)
}
} else {
create("", tags, signer, createdAt, onReady)
}
}
fun removeRoom(
earlierVersion: EphemeralChatListEvent,
room: RoomId,
isPrivate: Boolean,
signer: NostrSigner,
createdAt: Long = TimeUtils.now(),
onReady: (EphemeralChatListEvent) -> Unit,
) {
PrivateTagArrayBuilder.removeAll(
earlierVersion,
RoomIdTag.Companion.assemble(room.id, room.relayUrl),
signer,
) { encryptedContent, newTags ->
create(encryptedContent, newTags, signer, createdAt, onReady)
}
}
fun addRoom(
earlierVersion: EphemeralChatListEvent,
room: RoomId,
isPrivate: Boolean,
signer: NostrSigner,
createdAt: Long = TimeUtils.now(),
onReady: (EphemeralChatListEvent) -> Unit,
) {
PrivateTagArrayBuilder.add(
earlierVersion,
RoomIdTag.Companion.assemble(room.id, room.relayUrl),
isPrivate,
signer,
) { encryptedContent, newTags ->
create(encryptedContent, newTags, signer, createdAt, onReady)
}
}
fun create(
content: String,
tags: Array<Array<String>>,
signer: NostrSigner,
createdAt: Long = TimeUtils.now(),
onReady: (EphemeralChatListEvent) -> Unit,
) {
val newTags =
if (tags.any { it.size > 1 && it[0] == "alt" }) {
tags
} else {
tags + AltTag.Companion.assemble(ALT)
}
signer.sign(createdAt, KIND, newTags, content, onReady)
}
}
}
@@ -0,0 +1,29 @@
/**
* 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.experimental.ephemChat.list
import com.vitorpamplona.quartz.experimental.ephemChat.list.tags.RoomIdTag
import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder
fun TagArrayBuilder<EphemeralChatListEvent>.roomId(
id: String,
relayUrl: String,
) = addUnique(RoomIdTag.assemble(id, relayUrl))
@@ -0,0 +1,49 @@
/**
* 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.experimental.ephemChat.list.tags
import com.vitorpamplona.quartz.experimental.ephemChat.chat.RoomId
import com.vitorpamplona.quartz.nip01Core.core.has
import com.vitorpamplona.quartz.utils.ensure
class RoomIdTag {
companion object {
const val TAG_NAME = "group"
@JvmStatic
fun parse(tag: Array<String>): RoomId? {
ensure(tag.has(1)) { return null }
ensure(tag[0] == TAG_NAME) { return null }
ensure(tag[1].isNotEmpty()) { return null }
ensure(tag[2].isNotEmpty()) { return null }
return RoomId(tag[1], tag[2])
}
@JvmStatic
fun assemble(
id: String,
relayUrl: String,
) = arrayOf(TAG_NAME, id, relayUrl)
@JvmStatic
fun assemble(id: RoomId) = arrayOf(TAG_NAME, id.id, id.relayUrl)
}
}
@@ -29,7 +29,6 @@ import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerSync
import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag
import com.vitorpamplona.quartz.nip01Core.tags.addressables.isTaggedAddressableNote
import com.vitorpamplona.quartz.nip01Core.tags.events.isTaggedEvent
import com.vitorpamplona.quartz.nip01Core.tags.geohash.isTaggedGeoHash
import com.vitorpamplona.quartz.nip01Core.tags.hashtags.countHashtags
import com.vitorpamplona.quartz.nip01Core.tags.hashtags.hashtags
@@ -88,7 +87,6 @@ class ContactListEvent(
followTags: List<String> = emptyList(),
followGeohashes: List<String> = emptyList(),
followCommunities: List<ATag> = emptyList(),
followEvents: List<String> = emptyList(),
relayUse: Map<String, ReadWrite>? = emptyMap(),
signer: NostrSignerSync,
createdAt: Long = TimeUtils.now(),
@@ -99,7 +97,6 @@ class ContactListEvent(
listOf(AltTag.assemble(ALT)) +
followUsers.map { it.toTagArray() } +
followTags.map { arrayOf("t", it) } +
followEvents.map { arrayOf("e", it) } +
followCommunities.map { it.toATagArray() } +
followGeohashes.map { arrayOf("g", it) }
@@ -111,7 +108,6 @@ class ContactListEvent(
followTags: List<String>,
followGeohashes: List<String>,
followCommunities: List<ATag>,
followEvents: List<String>,
relayUse: Map<String, ReadWrite>?,
signer: NostrSigner,
createdAt: Long = TimeUtils.now(),
@@ -122,7 +118,6 @@ class ContactListEvent(
val tags =
followUsers.map { it.toTagArray() } +
followTags.map { arrayOf("t", it) } +
followEvents.map { arrayOf("e", it) } +
followCommunities.map { it.toATagArray() } +
followGeohashes.map { arrayOf("g", it) }
@@ -244,42 +239,6 @@ class ContactListEvent(
)
}
fun followEvent(
earlierVersion: ContactListEvent,
idHex: String,
signer: NostrSigner,
createdAt: Long = TimeUtils.now(),
onReady: (ContactListEvent) -> Unit,
) {
if (earlierVersion.isTaggedEvent(idHex)) return
return create(
content = earlierVersion.content,
tags = earlierVersion.tags.plus(element = arrayOf("e", idHex)),
signer = signer,
createdAt = createdAt,
onReady = onReady,
)
}
fun unfollowEvent(
earlierVersion: ContactListEvent,
idHex: String,
signer: NostrSigner,
createdAt: Long = TimeUtils.now(),
onReady: (ContactListEvent) -> Unit,
) {
if (!earlierVersion.isTaggedEvent(idHex)) return
return create(
content = earlierVersion.content,
tags = earlierVersion.tags.filter { it.size > 1 && it[1] != idHex }.toTypedArray(),
signer = signer,
createdAt = createdAt,
onReady = onReady,
)
}
fun followAddressableEvent(
earlierVersion: ContactListEvent,
aTag: ATag,
@@ -0,0 +1,115 @@
/**
* 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.nip09Deletions
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.utils.LargeCache
class DeletionIndex {
data class DeletionRequest(
val reference: String,
val publicKey: HexKey,
) : Comparable<DeletionRequest> {
override fun compareTo(other: DeletionRequest): Int {
val compared = reference.compareTo(other.reference)
return if (compared == 0) {
publicKey.compareTo(publicKey)
} else {
compared
}
}
}
// stores a set of id OR atags (kind:pubkey:dtag) by pubkey with the created at of the deletion event.
// Anything newer than the date should not be deleted.
private val deletedReferencesBefore = LargeCache<DeletionRequest, DeletionEvent>()
fun add(event: DeletionEvent): Boolean {
var atLeastOne = false
event.tags.forEach {
if (it.size > 1 && (it[0] == "a" || it[0] == "e")) {
if (add(it[1], event.pubKey, event)) {
atLeastOne = true
}
}
}
return atLeastOne
}
private fun add(
ref: String,
byPubKey: HexKey,
deletionEvent: DeletionEvent,
): Boolean {
val key = DeletionRequest(ref, byPubKey)
val previousDeletionEvent = deletedReferencesBefore.get(key)
if (previousDeletionEvent == null || deletionEvent.createdAt > previousDeletionEvent.createdAt) {
deletedReferencesBefore.put(key, deletionEvent)
return true
}
return false
}
fun hasBeenDeletedBy(event: Event): DeletionEvent? {
deletedReferencesBefore.get(DeletionRequest(event.id, event.pubKey))?.let {
return it
}
if (event is AddressableEvent) {
deletedReferencesBefore.get(DeletionRequest(event.addressTag(), event.pubKey))?.let {
if (event.createdAt <= it.createdAt) {
return it
}
}
}
return null
}
fun hasBeenDeleted(event: Event): Boolean {
val key = DeletionRequest(event.id, event.pubKey)
if (hasBeenDeleted(key)) return true
if (event is AddressableEvent) {
if (hasBeenDeleted(DeletionRequest(event.addressTag(), event.pubKey), event.createdAt)) return true
}
return false
}
private fun hasBeenDeleted(key: DeletionRequest) = deletedReferencesBefore.containsKey(key)
private fun hasBeenDeleted(
key: DeletionRequest,
createdAt: Long,
): Boolean {
val deletionEvent = deletedReferencesBefore.get(key)
return deletionEvent != null && createdAt <= deletionEvent.createdAt
}
fun size() = deletedReferencesBefore.size()
}
@@ -1,271 +0,0 @@
/**
* 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.nip28PublicChat
import androidx.compose.runtime.Immutable
import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
import com.vitorpamplona.quartz.nip31Alts.AltTag
import com.vitorpamplona.quartz.nip51Lists.GeneralListEvent
import com.vitorpamplona.quartz.utils.TimeUtils
import com.vitorpamplona.quartz.utils.bytesUsedInMemory
import com.vitorpamplona.quartz.utils.pointerSizeInBytes
import kotlinx.collections.immutable.ImmutableSet
@Immutable
class ChannelListEvent(
id: HexKey,
pubKey: HexKey,
createdAt: Long,
tags: Array<Array<String>>,
content: String,
sig: HexKey,
) : GeneralListEvent(id, pubKey, createdAt, KIND, tags, content, sig) {
@Transient var publicAndPrivateEventCache: ImmutableSet<HexKey>? = null
override fun countMemory(): Long =
super.countMemory() +
32 + (publicAndPrivateEventCache?.sumOf { pointerSizeInBytes + it.bytesUsedInMemory() } ?: 0L) // rough calculation
fun publicAndPrivateEvents(
signer: NostrSigner,
onReady: (ImmutableSet<HexKey>) -> Unit,
) {
publicAndPrivateEventCache?.let { eventList ->
onReady(eventList)
return
}
privateTagsOrEmpty(signer) {
publicAndPrivateEventCache = filterTagList("e", it)
publicAndPrivateEventCache?.let { eventList ->
onReady(eventList)
}
}
}
companion object {
const val KIND = 10005
const val ALT = "Public Chat List"
fun blockListFor(pubKeyHex: HexKey): String = "$KIND:$pubKeyHex:"
fun createListWithTag(
key: String,
tag: String,
isPrivate: Boolean,
signer: NostrSigner,
createdAt: Long = TimeUtils.now(),
onReady: (ChannelListEvent) -> Unit,
) {
if (isPrivate) {
encryptTags(arrayOf(arrayOf(key, tag)), signer) { encryptedTags ->
create(
content = encryptedTags,
tags = emptyArray(),
signer = signer,
createdAt = createdAt,
onReady = onReady,
)
}
} else {
create(
content = "",
tags = arrayOf(arrayOf(key, tag)),
signer = signer,
createdAt = createdAt,
onReady = onReady,
)
}
}
fun createListWithEvent(
eventId: HexKey,
isPrivate: Boolean,
signer: NostrSigner,
createdAt: Long = TimeUtils.now(),
onReady: (ChannelListEvent) -> Unit,
) = createListWithTag("e", eventId, isPrivate, signer, createdAt, onReady)
fun addEvents(
earlierVersion: ChannelListEvent,
listEvents: List<HexKey>,
isPrivate: Boolean,
signer: NostrSigner,
createdAt: Long = TimeUtils.now(),
onReady: (ChannelListEvent) -> Unit,
) {
if (isPrivate) {
earlierVersion.privateTagsOrEmpty(signer) { privateTags ->
encryptTags(
privateTags =
privateTags.plus(
listEvents.map { arrayOf("e", it) },
),
signer = signer,
) { encryptedTags ->
create(
content = encryptedTags,
tags = earlierVersion.tags,
signer = signer,
createdAt = createdAt,
onReady = onReady,
)
}
}
} else {
create(
content = earlierVersion.content,
tags =
earlierVersion.tags.plus(
listEvents.map { arrayOf("e", it) },
),
signer = signer,
createdAt = createdAt,
onReady = onReady,
)
}
}
fun addEvent(
earlierVersion: ChannelListEvent,
event: HexKey,
isPrivate: Boolean,
signer: NostrSigner,
createdAt: Long = TimeUtils.now(),
onReady: (ChannelListEvent) -> Unit,
) = addTag(earlierVersion, "e", event, isPrivate, signer, createdAt, onReady)
fun addTag(
earlierVersion: ChannelListEvent,
key: String,
tag: String,
isPrivate: Boolean,
signer: NostrSigner,
createdAt: Long = TimeUtils.now(),
onReady: (ChannelListEvent) -> Unit,
) {
earlierVersion.isTagged(key, tag, isPrivate, signer) { isTagged ->
if (!isTagged) {
if (isPrivate) {
earlierVersion.privateTagsOrEmpty(signer) { privateTags ->
encryptTags(
privateTags = privateTags.plus(element = arrayOf(key, tag)),
signer = signer,
) { encryptedTags ->
create(
content = encryptedTags,
tags = earlierVersion.tags,
signer = signer,
createdAt = createdAt,
onReady = onReady,
)
}
}
} else {
create(
content = earlierVersion.content,
tags = earlierVersion.tags.plus(element = arrayOf(key, tag)),
signer = signer,
createdAt = createdAt,
onReady = onReady,
)
}
}
}
}
fun removeEvent(
earlierVersion: ChannelListEvent,
event: HexKey,
isPrivate: Boolean,
signer: NostrSigner,
createdAt: Long = TimeUtils.now(),
onReady: (ChannelListEvent) -> Unit,
) = removeTag(earlierVersion, "e", event, isPrivate, signer, createdAt, onReady)
fun removeTag(
earlierVersion: ChannelListEvent,
key: String,
tag: String,
isPrivate: Boolean,
signer: NostrSigner,
createdAt: Long = TimeUtils.now(),
onReady: (ChannelListEvent) -> Unit,
) {
earlierVersion.isTagged(key, tag, isPrivate, signer) { isTagged ->
if (isTagged) {
if (isPrivate) {
earlierVersion.privateTagsOrEmpty(signer) { privateTags ->
encryptTags(
privateTags =
privateTags
.filter { it.size > 1 && !(it[0] == key && it[1] == tag) }
.toTypedArray(),
signer = signer,
) { encryptedTags ->
create(
content = encryptedTags,
tags =
earlierVersion.tags
.filter { it.size > 1 && !(it[0] == key && it[1] == tag) }
.toTypedArray(),
signer = signer,
createdAt = createdAt,
onReady = onReady,
)
}
}
} else {
create(
content = earlierVersion.content,
tags =
earlierVersion.tags
.filter { it.size > 1 && !(it[0] == key && it[1] == tag) }
.toTypedArray(),
signer = signer,
createdAt = createdAt,
onReady = onReady,
)
}
}
}
}
fun create(
content: String,
tags: Array<Array<String>>,
signer: NostrSigner,
createdAt: Long = TimeUtils.now(),
onReady: (ChannelListEvent) -> Unit,
) {
val newTags =
if (tags.any { it.size > 1 && it[0] == "alt" }) {
tags
} else {
tags + AltTag.assemble(ALT)
}
signer.sign(createdAt, KIND, newTags, content, onReady)
}
}
}
@@ -0,0 +1,239 @@
/**
* 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.nip28PublicChat.list
import androidx.compose.runtime.Immutable
import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.nip01Core.hints.EventHintBundle
import com.vitorpamplona.quartz.nip01Core.hints.types.EventIdHint
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerSync
import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address
import com.vitorpamplona.quartz.nip01Core.tags.events.ETag
import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelCreateEvent
import com.vitorpamplona.quartz.nip31Alts.AltTag
import com.vitorpamplona.quartz.nip51Lists.PrivateTagArrayBuilder
import com.vitorpamplona.quartz.nip51Lists.PrivateTagArrayEvent
import com.vitorpamplona.quartz.utils.TimeUtils
import java.lang.reflect.Modifier.isPrivate
import kotlin.collections.plus
@Immutable
class ChannelListEvent(
id: HexKey,
pubKey: HexKey,
createdAt: Long,
tags: Array<Array<String>>,
content: String,
sig: HexKey,
) : PrivateTagArrayEvent(id, pubKey, createdAt, KIND, tags, content, sig) {
@Transient var publicAndPrivateEventCache: Set<EventIdHint>? = null
fun publicAndPrivateChannels(
signer: NostrSigner,
onReady: (Set<EventIdHint>) -> Unit,
) {
publicAndPrivateEventCache?.let { eventList ->
onReady(eventList)
return
}
mergeTagList(signer) {
val set = it.mapNotNull(ETag::parseAsHint).toSet()
publicAndPrivateEventCache = set
onReady(set)
}
}
companion object {
const val KIND = 10005
const val ALT = "Public Chat List"
const val FIXED_D_TAG = ""
fun createAddress(pubKey: HexKey) = Address(KIND, pubKey, FIXED_D_TAG)
private fun createChannelBase(
tags: Array<Array<String>>,
isPrivate: Boolean,
signer: NostrSigner,
createdAt: Long = TimeUtils.now(),
onReady: (ChannelListEvent) -> Unit,
) {
PrivateTagArrayBuilder.create(
tags,
isPrivate,
signer,
) { encryptedContent, newTags ->
create(encryptedContent, newTags, signer, createdAt, onReady)
}
}
fun createChannel(
channel: EventHintBundle<ChannelCreateEvent>,
isPrivate: Boolean,
signer: NostrSigner,
createdAt: Long = TimeUtils.now(),
onReady: (ChannelListEvent) -> Unit,
) = createChannelBase(
tags = arrayOf(ETag.assemble(channel.event.id, channel.relay, channel.event.pubKey)),
isPrivate = isPrivate,
signer = signer,
createdAt = createdAt,
onReady = onReady,
)
fun createChannel(
channel: EventIdHint,
isPrivate: Boolean,
signer: NostrSigner,
createdAt: Long = TimeUtils.now(),
onReady: (ChannelListEvent) -> Unit,
) = createChannelBase(
tags = arrayOf(ETag.assemble(channel.eventId, channel.relay, null)),
isPrivate = isPrivate,
signer = signer,
createdAt = createdAt,
onReady = onReady,
)
fun createChannels(
channels: List<EventIdHint>,
isPrivate: Boolean,
signer: NostrSigner,
createdAt: Long = TimeUtils.now(),
onReady: (ChannelListEvent) -> Unit,
) = createChannelBase(
tags = channels.map { ETag.assemble(it.eventId, it.relay, null) }.toTypedArray(),
isPrivate = isPrivate,
signer = signer,
createdAt = createdAt,
onReady = onReady,
)
fun removeChannel(
earlierVersion: ChannelListEvent,
channel: HexKey,
signer: NostrSigner,
createdAt: Long = TimeUtils.now(),
onReady: (ChannelListEvent) -> Unit,
) {
PrivateTagArrayBuilder.removeAll(
earlierVersion,
ETag.assemble(channel, null, null),
signer,
) { encryptedContent, newTags ->
create(encryptedContent, newTags, signer, createdAt, onReady)
}
}
private fun addChannelBase(
earlierVersion: ChannelListEvent,
newTags: Array<Array<String>>,
isPrivate: Boolean,
signer: NostrSigner,
createdAt: Long = TimeUtils.now(),
onReady: (ChannelListEvent) -> Unit,
) {
PrivateTagArrayBuilder.addAll(
earlierVersion,
newTags,
isPrivate,
signer,
) { encryptedContent, newTags ->
create(encryptedContent, newTags, signer, createdAt, onReady)
}
}
fun addChannel(
earlierVersion: ChannelListEvent,
channel: EventHintBundle<ChannelCreateEvent>,
isPrivate: Boolean,
signer: NostrSigner,
createdAt: Long = TimeUtils.now(),
onReady: (ChannelListEvent) -> Unit,
) = addChannelBase(
earlierVersion,
arrayOf(ETag.assemble(channel.event.id, channel.relay, channel.event.pubKey)),
isPrivate,
signer,
createdAt,
onReady,
)
fun addChannel(
earlierVersion: ChannelListEvent,
channel: EventIdHint,
isPrivate: Boolean,
signer: NostrSigner,
createdAt: Long = TimeUtils.now(),
onReady: (ChannelListEvent) -> Unit,
) = addChannelBase(
earlierVersion,
arrayOf(ETag.assemble(channel.eventId, channel.relay, null)),
isPrivate,
signer,
createdAt,
onReady,
)
fun addChannels(
earlierVersion: ChannelListEvent,
channels: List<EventIdHint>,
isPrivate: Boolean,
signer: NostrSigner,
createdAt: Long = TimeUtils.now(),
onReady: (ChannelListEvent) -> Unit,
) = addChannelBase(
earlierVersion,
channels.map { ETag.assemble(it.eventId, it.relay, null) }.toTypedArray(),
isPrivate,
signer,
createdAt,
onReady,
)
private fun create(
content: String,
tags: Array<Array<String>>,
signer: NostrSigner,
createdAt: Long = TimeUtils.now(),
onReady: (ChannelListEvent) -> Unit,
) {
val newTags =
if (tags.any { it.size > 1 && it[0] == "alt" }) {
tags
} else {
tags + AltTag.Companion.assemble(ALT)
}
signer.sign(createdAt, KIND, newTags, content, onReady)
}
fun create(
list: List<EventIdHint>,
signer: NostrSignerSync,
createdAt: Long = TimeUtils.now(),
): ChannelListEvent? {
val tags = list.map { ETag.assemble(it.eventId, it.relay, null) }.toTypedArray()
return signer.sign(createdAt, KIND, tags, "")
}
}
}
@@ -0,0 +1,30 @@
/**
* 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.nip28PublicChat.list
import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder
import com.vitorpamplona.quartz.nip01Core.tags.events.ETag
fun TagArrayBuilder<ChannelListEvent>.followChat(
eventId: HexKey,
relayUrl: String,
) = addUnique(ETag.assemble(eventId, relayUrl, null))
@@ -0,0 +1,214 @@
/**
* 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.nip51Lists
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
import com.vitorpamplona.quartz.nip51Lists.encryption.PrivateTagsInContent
class PrivateTagArrayBuilder {
companion object {
fun create(
tags: Array<Array<String>>,
toPrivate: Boolean,
signer: NostrSigner,
onReady: (content: String, tags: Array<Array<String>>) -> Unit,
) {
if (toPrivate) {
PrivateTagsInContent.encryptNip04(
privateTags = tags,
signer = signer,
) { encryptedTags ->
onReady(encryptedTags, arrayOf())
}
} else {
onReady("", tags)
}
}
fun add(
current: PrivateTagArrayEvent,
newTag: Array<String>,
toPrivate: Boolean,
signer: NostrSigner,
onReady: (content: String, tags: Array<Array<String>>) -> Unit,
) {
if (toPrivate) {
current.privateTags(signer) { privateTags ->
PrivateTagsInContent.encryptNip04(
privateTags = privateTags.plus(newTag),
signer = signer,
) { encryptedTags ->
onReady(encryptedTags, current.tags)
}
}
} else {
onReady(current.content, current.tags.plus(newTag))
}
}
fun addAll(
current: PrivateTagArrayEvent,
newTag: Array<Array<String>>,
toPrivate: Boolean,
signer: NostrSigner,
onReady: (content: String, tags: Array<Array<String>>) -> Unit,
) {
if (toPrivate) {
current.privateTags(signer) { privateTags ->
PrivateTagsInContent.encryptNip04(
privateTags = privateTags.plus(newTag),
signer = signer,
) { encryptedTags ->
onReady(encryptedTags, current.tags)
}
}
} else {
onReady(current.content, current.tags.plus(newTag))
}
}
fun replaceAllToPrivateNewTag(
dTag: String,
current: PrivateTagArrayEvent?,
oldTagStartsWith: Array<String>,
newTag: Array<String>,
signer: NostrSigner,
onReady: (content: String, tags: Array<Array<String>>) -> Unit,
) {
if (current == null) {
createPrivate(dTag, newTag, signer, onReady)
} else {
replaceAllToPrivateNewTag(current, oldTagStartsWith, newTag, signer, onReady)
}
}
fun replaceAllToPublicNewTag(
dTag: String,
current: PrivateTagArrayEvent?,
oldTagStartsWith: Array<String>,
newTag: Array<String>,
signer: NostrSigner,
onReady: (content: String, tags: Array<Array<String>>) -> Unit,
) {
if (current == null) {
createPublic(dTag, newTag, signer, onReady)
} else {
replaceAllToPublicNewTag(current, oldTagStartsWith, newTag, signer, onReady)
}
}
fun replaceAllToPrivateNewTag(
current: PrivateTagArrayEvent,
oldTagStartsWith: Array<String>,
newTag: Array<String>,
signer: NostrSigner,
onReady: (content: String, tags: Array<Array<String>>) -> Unit,
) {
current.privateTags(signer) { privateTags ->
PrivateTagsInContent.encryptNip04(
privateTags = privateTags.replaceAll(oldTagStartsWith, newTag),
signer = signer,
) { encryptedTags ->
onReady(encryptedTags, current.tags.remove(oldTagStartsWith))
}
}
}
fun replaceAllToPublicNewTag(
current: PrivateTagArrayEvent,
oldTagStartsWith: Array<String>,
newTag: Array<String>,
signer: NostrSigner,
onReady: (content: String, tags: Array<Array<String>>) -> Unit,
) {
current.privateTags(signer) { privateTags ->
PrivateTagsInContent.encryptNip04(
privateTags = privateTags.remove(oldTagStartsWith),
signer = signer,
) { encryptedTags ->
onReady(encryptedTags, current.tags.remove(oldTagStartsWith).plus(newTag))
}
}
}
fun removeAllFromPrivate(
current: PrivateTagArrayEvent,
oldTagStartsWith: Array<String>,
signer: NostrSigner,
onReady: (content: String, tags: Array<Array<String>>) -> Unit,
) {
current.privateTags(signer) { privateTags ->
PrivateTagsInContent.encryptNip04(
privateTags = privateTags.remove(oldTagStartsWith),
signer = signer,
) { encryptedTags ->
onReady(encryptedTags, current.tags)
}
}
}
fun removeAllFromPublic(
current: PrivateTagArrayEvent,
oldTagStartsWith: Array<String>,
signer: NostrSigner,
onReady: (content: String, tags: Array<Array<String>>) -> Unit,
) = onReady(current.content, current.tags.remove(oldTagStartsWith))
fun removeAll(
current: PrivateTagArrayEvent,
oldTagStartsWith: Array<String>,
signer: NostrSigner,
onReady: (content: String, tags: Array<Array<String>>) -> Unit,
) {
current.privateTags(signer) { privateTags ->
PrivateTagsInContent.encryptNip04(
privateTags = privateTags.remove(oldTagStartsWith),
signer = signer,
) { encryptedTags ->
onReady(encryptedTags, current.tags.remove(oldTagStartsWith))
}
}
}
fun createPrivate(
dTag: String,
newTag: Array<String>,
signer: NostrSigner,
onReady: (content: String, tags: Array<Array<String>>) -> Unit,
) {
PrivateTagsInContent.encryptNip04(
privateTags = arrayOf(newTag),
signer = signer,
) { encryptedTags ->
onReady(encryptedTags, arrayOf(arrayOf("d", dTag)))
}
}
fun createPublic(
dTag: String,
newTag: Array<String>,
signer: NostrSigner,
onReady: (content: String, tags: Array<Array<String>>) -> Unit,
) {
onReady("", arrayOf(arrayOf("d", dTag), newTag))
}
}
}
@@ -69,192 +69,27 @@ abstract class PrivateTagArrayEvent(
privateTagsCache?.let { onReady(it) }
}
} catch (e: Throwable) {
onReady(emptyArray())
Log.w("GeneralList", "Error parsing the JSON ${e.message}")
}
}
fun decryptChangeEncrypt(
fun mergeTagList(
signer: NostrSigner,
change: (Array<Array<String>>) -> Array<Array<String>>,
onReady: (content: String) -> Unit,
onReady: (Array<Array<String>>) -> Unit,
) {
privateTags(signer) { privateTags ->
PrivateTagsInContent.encryptNip04(
privateTags = change(privateTags),
signer = signer,
) { encryptedTags ->
onReady(encryptedTags)
}
privateTags(signer) {
onReady(tags + it)
}
}
companion object {
fun add(
current: PrivateTagArrayEvent,
newTag: Array<String>,
toPrivate: Boolean,
signer: NostrSigner,
onReady: (content: String, tags: Array<Array<String>>) -> Unit,
) {
if (toPrivate) {
current.privateTags(signer) { privateTags ->
PrivateTagsInContent.encryptNip04(
privateTags = privateTags.plus(newTag),
signer = signer,
) { encryptedTags ->
onReady(encryptedTags, current.tags)
}
}
} else {
onReady(current.content, current.tags.plus(newTag))
}
}
fun <T> mapAllTags(
privateTags: Array<Array<String>>,
mapper: (Array<String>) -> T,
): Set<T> {
val privateRooms = privateTags.mapNotNull(mapper)
val publicRooms = tags.mapNotNull(mapper)
fun addAll(
current: PrivateTagArrayEvent,
newTag: Array<Array<String>>,
toPrivate: Boolean,
signer: NostrSigner,
onReady: (content: String, tags: Array<Array<String>>) -> Unit,
) {
if (toPrivate) {
current.privateTags(signer) { privateTags ->
PrivateTagsInContent.encryptNip04(
privateTags = privateTags.plus(newTag),
signer = signer,
) { encryptedTags ->
onReady(encryptedTags, current.tags)
}
}
} else {
onReady(current.content, current.tags.plus(newTag))
}
}
fun replaceAllToPrivateNewTag(
dTag: String,
current: PrivateTagArrayEvent?,
oldTagStartsWith: Array<String>,
newTag: Array<String>,
signer: NostrSigner,
onReady: (content: String, tags: Array<Array<String>>) -> Unit,
) {
if (current == null) {
createPrivate(dTag, newTag, signer, onReady)
} else {
replaceAllToPrivateNewTag(current, oldTagStartsWith, newTag, signer, onReady)
}
}
fun replaceAllToPublicNewTag(
dTag: String,
current: PrivateTagArrayEvent?,
oldTagStartsWith: Array<String>,
newTag: Array<String>,
signer: NostrSigner,
onReady: (content: String, tags: Array<Array<String>>) -> Unit,
) {
if (current == null) {
createPublic(dTag, newTag, signer, onReady)
} else {
replaceAllToPublicNewTag(current, oldTagStartsWith, newTag, signer, onReady)
}
}
fun replaceAllToPrivateNewTag(
current: PrivateTagArrayEvent,
oldTagStartsWith: Array<String>,
newTag: Array<String>,
signer: NostrSigner,
onReady: (content: String, tags: Array<Array<String>>) -> Unit,
) {
current.privateTags(signer) { privateTags ->
PrivateTagsInContent.encryptNip04(
privateTags = privateTags.replaceAll(oldTagStartsWith, newTag),
signer = signer,
) { encryptedTags ->
onReady(encryptedTags, current.tags.remove(oldTagStartsWith))
}
}
}
fun replaceAllToPublicNewTag(
current: PrivateTagArrayEvent,
oldTagStartsWith: Array<String>,
newTag: Array<String>,
signer: NostrSigner,
onReady: (content: String, tags: Array<Array<String>>) -> Unit,
) {
current.privateTags(signer) { privateTags ->
PrivateTagsInContent.encryptNip04(
privateTags = privateTags.remove(oldTagStartsWith),
signer = signer,
) { encryptedTags ->
onReady(encryptedTags, current.tags.remove(oldTagStartsWith).plus(newTag))
}
}
}
fun removeAllFromPrivate(
current: PrivateTagArrayEvent,
oldTagStartsWith: Array<String>,
signer: NostrSigner,
onReady: (content: String, tags: Array<Array<String>>) -> Unit,
) {
current.privateTags(signer) { privateTags ->
PrivateTagsInContent.encryptNip04(
privateTags = privateTags.remove(oldTagStartsWith),
signer = signer,
) { encryptedTags ->
onReady(encryptedTags, current.tags)
}
}
}
fun removeAllFromPublic(
current: PrivateTagArrayEvent,
oldTagStartsWith: Array<String>,
signer: NostrSigner,
onReady: (content: String, tags: Array<Array<String>>) -> Unit,
) = onReady(current.content, current.tags.remove(oldTagStartsWith))
fun removeAll(
current: PrivateTagArrayEvent,
oldTagStartsWith: Array<String>,
signer: NostrSigner,
onReady: (content: String, tags: Array<Array<String>>) -> Unit,
) {
current.privateTags(signer) { privateTags ->
PrivateTagsInContent.encryptNip04(
privateTags = privateTags.remove(oldTagStartsWith),
signer = signer,
) { encryptedTags ->
onReady(encryptedTags, current.tags.remove(oldTagStartsWith))
}
}
}
fun createPrivate(
dTag: String,
newTag: Array<String>,
signer: NostrSigner,
onReady: (content: String, tags: Array<Array<String>>) -> Unit,
) {
PrivateTagsInContent.encryptNip04(
privateTags = arrayOf(newTag),
signer = signer,
) { encryptedTags ->
onReady(encryptedTags, arrayOf(arrayOf("d", dTag)))
}
}
fun createPublic(
dTag: String,
newTag: Array<String>,
signer: NostrSigner,
onReady: (content: String, tags: Array<Array<String>>) -> Unit,
) {
onReady("", arrayOf(arrayOf("d", dTag), newTag))
}
return (privateRooms + publicRooms).toSet()
}
}
@@ -0,0 +1,427 @@
/**
* 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.utils
import java.util.concurrent.ConcurrentSkipListMap
import java.util.function.BiConsumer
class LargeCache<K, V> {
private val cache = ConcurrentSkipListMap<K, V>()
fun values() = cache.values
fun get(key: K) = cache.get(key)
fun remove(key: K) = cache.remove(key)
fun size() = cache.size
fun isEmpty() = cache.isEmpty()
fun containsKey(key: K) = cache.containsKey(key)
fun put(
key: K,
value: V,
) {
cache.put(key, value)
}
fun getOrCreate(
key: K,
builder: (key: K) -> V,
): V {
val value = cache.get(key)
return if (value != null) {
value
} else {
val newObject = builder(key)
cache.putIfAbsent(key, newObject) ?: newObject
}
}
fun forEach(consumer: BiConsumer<K, V>) {
innerForEach(consumer)
}
fun filter(consumer: BiFilter<K, V>): List<V> {
val runner = BiFilterCollector(consumer)
innerForEach(runner)
return runner.results
}
fun filterIntoSet(consumer: BiFilter<K, V>): Set<V> {
val runner = BiFilterUniqueCollector(consumer)
innerForEach(runner)
return runner.results
}
fun <R> map(consumer: BiNotNullMapper<K, V, R>): List<R> {
val runner = BiNotNullMapCollector(consumer)
innerForEach(runner)
return runner.results
}
fun <R> mapNotNull(consumer: BiMapper<K, V, R?>): List<R> {
val runner = BiMapCollector(consumer)
innerForEach(runner)
return runner.results
}
fun <R> mapNotNullIntoSet(consumer: BiMapper<K, V, R?>): Set<R> {
val runner = BiMapUniqueCollector(consumer)
innerForEach(runner)
return runner.results
}
fun <R> mapFlatten(consumer: BiMapper<K, V, Collection<R>?>): List<R> {
val runner = BiMapFlattenCollector(consumer)
innerForEach(runner)
return runner.results
}
fun <R> mapFlattenIntoSet(consumer: BiMapper<K, V, Collection<R>?>): Set<R> {
val runner = BiMapFlattenUniqueCollector(consumer)
innerForEach(runner)
return runner.results
}
fun maxOrNullOf(
filter: BiFilter<K, V>,
comparator: Comparator<V>,
): V? {
val runner = BiMaxOfCollector(filter, comparator)
innerForEach(runner)
return runner.maxV
}
fun sumOf(consumer: BiSumOf<K, V>): Int {
val runner = BiSumOfCollector(consumer)
innerForEach(runner)
return runner.sum
}
fun sumOfLong(consumer: BiSumOfLong<K, V>): Long {
val runner = BiSumOfLongCollector(consumer)
innerForEach(runner)
return runner.sum
}
fun <R> groupBy(consumer: BiNotNullMapper<K, V, R>): Map<R, List<V>> {
val runner = BiGroupByCollector(consumer)
innerForEach(runner)
return runner.results
}
fun <R> countByGroup(consumer: BiNotNullMapper<K, V, R>): Map<R, Int> {
val runner = BiCountByGroupCollector(consumer)
innerForEach(runner)
return runner.results
}
fun <R> sumByGroup(
groupMap: BiNotNullMapper<K, V, R>,
sumOf: BiNotNullMapper<K, V, Long>,
): Map<R, Long> {
val runner = BiSumByGroupCollector(groupMap, sumOf)
innerForEach(runner)
return runner.results
}
fun count(consumer: BiFilter<K, V>): Int {
val runner = BiCountIfCollector(consumer)
innerForEach(runner)
return runner.count
}
private fun innerForEach(runner: BiConsumer<K, V>) {
// val (value, elapsed) =
// measureTimedValue {
cache.forEach(runner)
// }
// println("LargeCache full loop $elapsed \t for $runner")
}
}
fun interface BiFilter<K, V> {
fun filter(
k: K,
v: V,
): Boolean
}
class BiFilterCollector<K, V>(
val filter: BiFilter<K, V>,
) : BiConsumer<K, V> {
var results: ArrayList<V> = ArrayList()
override fun accept(
k: K,
v: V,
) {
if (filter.filter(k, v)) {
results.add(v)
}
}
}
class BiFilterUniqueCollector<K, V>(
val filter: BiFilter<K, V>,
) : BiConsumer<K, V> {
var results: HashSet<V> = HashSet()
override fun accept(
k: K,
v: V,
) {
if (filter.filter(k, v)) {
results.add(v)
}
}
}
fun interface BiMapper<K, V, R> {
fun map(
k: K,
v: V,
): R?
}
class BiMapCollector<K, V, R>(
val mapper: BiMapper<K, V, R?>,
) : BiConsumer<K, V> {
var results: ArrayList<R> = ArrayList()
override fun accept(
k: K,
v: V,
) {
val result = mapper.map(k, v)
if (result != null) {
results.add(result)
}
}
}
class BiMapUniqueCollector<K, V, R>(
val mapper: BiMapper<K, V, R?>,
) : BiConsumer<K, V> {
var results: HashSet<R> = HashSet()
override fun accept(
k: K,
v: V,
) {
val result = mapper.map(k, v)
if (result != null) {
results.add(result)
}
}
}
class BiMapFlattenCollector<K, V, R>(
val mapper: BiMapper<K, V, Collection<R>?>,
) : BiConsumer<K, V> {
var results: ArrayList<R> = ArrayList()
override fun accept(
k: K,
v: V,
) {
val result = mapper.map(k, v)
if (result != null) {
results.addAll(result)
}
}
}
class BiMapFlattenUniqueCollector<K, V, R>(
val mapper: BiMapper<K, V, Collection<R>?>,
) : BiConsumer<K, V> {
var results: HashSet<R> = HashSet()
override fun accept(
k: K,
v: V,
) {
val result = mapper.map(k, v)
if (result != null) {
results.addAll(result)
}
}
}
fun interface BiNotNullMapper<K, V, R> {
fun map(
k: K,
v: V,
): R
}
class BiNotNullMapCollector<K, V, R>(
val mapper: BiNotNullMapper<K, V, R>,
) : BiConsumer<K, V> {
var results: ArrayList<R> = ArrayList()
override fun accept(
k: K,
v: V,
) {
results.add(mapper.map(k, v))
}
}
fun interface BiSumOf<K, V> {
fun map(
k: K,
v: V,
): Int
}
class BiMaxOfCollector<K, V>(
val filter: BiFilter<K, V>,
val comparator: Comparator<V>,
) : BiConsumer<K, V> {
var maxK: K? = null
var maxV: V? = null
override fun accept(
k: K,
v: V,
) {
if (filter.filter(k, v)) {
if (maxK == null || comparator.compare(v, maxV) > 0) {
maxK = k
maxV = v
}
}
}
}
class BiSumOfCollector<K, V>(
val mapper: BiSumOf<K, V>,
) : BiConsumer<K, V> {
var sum = 0
override fun accept(
k: K,
v: V,
) {
sum += mapper.map(k, v)
}
}
fun interface BiSumOfLong<K, V> {
fun map(
k: K,
v: V,
): Long
}
class BiSumOfLongCollector<K, V>(
val mapper: BiSumOfLong<K, V>,
) : BiConsumer<K, V> {
var sum = 0L
override fun accept(
k: K,
v: V,
) {
sum += mapper.map(k, v)
}
}
class BiGroupByCollector<K, V, R>(
val mapper: BiNotNullMapper<K, V, R>,
) : BiConsumer<K, V> {
var results = HashMap<R, ArrayList<V>>()
override fun accept(
k: K,
v: V,
) {
val group = mapper.map(k, v)
val list = results[group]
if (list == null) {
val answer = ArrayList<V>()
answer.add(v)
results[group] = answer
} else {
list.add(v)
}
}
}
class BiCountByGroupCollector<K, V, R>(
val mapper: BiNotNullMapper<K, V, R>,
) : BiConsumer<K, V> {
var results = HashMap<R, Int>()
override fun accept(
k: K,
v: V,
) {
val group = mapper.map(k, v)
val count = results[group]
if (count == null) {
results[group] = 1
} else {
results[group] = count + 1
}
}
}
class BiSumByGroupCollector<K, V, R>(
val mapper: BiNotNullMapper<K, V, R>,
val sumOf: BiNotNullMapper<K, V, Long>,
) : BiConsumer<K, V> {
var results = HashMap<R, Long>()
override fun accept(
k: K,
v: V,
) {
val group = mapper.map(k, v)
val sum = results[group]
if (sum == null) {
results[group] = sumOf.map(k, v)
} else {
results[group] = sum + sumOf.map(k, v)
}
}
}
class BiCountIfCollector<K, V>(
val filter: BiFilter<K, V>,
) : BiConsumer<K, V> {
var count = 0
override fun accept(
k: K,
v: V,
) {
if (filter.filter(k, v)) count++
}
}