- 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:
@@ -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)
|
||||
|
||||
+67
@@ -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)
|
||||
}
|
||||
+29
@@ -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))
|
||||
+41
@@ -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)
|
||||
}
|
||||
}
|
||||
+41
@@ -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)
|
||||
}
|
||||
}
|
||||
+58
@@ -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,
|
||||
)
|
||||
+45
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
+149
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
+29
@@ -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))
|
||||
+49
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
+239
@@ -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, "")
|
||||
}
|
||||
}
|
||||
}
|
||||
+30
@@ -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++
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user