diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/Channel.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/Channel.kt deleted file mode 100644 index 748e28608..000000000 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/Channel.kt +++ /dev/null @@ -1,16 +0,0 @@ -package com.vitorpamplona.amethyst.service - -import com.vitorpamplona.amethyst.service.relays.TypedFilter -import java.util.UUID -import nostr.postr.JsonFilter - -data class Channel ( - val id: String = UUID.randomUUID().toString().substring(0,4), - val onEOSE: ((Long) -> Unit)? = null -) { - var filter: List? = null // Inactive when null - - fun updateEOSE(l: Long) { - onEOSE?.let { it(l) } - } -} \ No newline at end of file diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrAccountDataSource.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrAccountDataSource.kt index 1d39be607..ef317d600 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrAccountDataSource.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrAccountDataSource.kt @@ -3,7 +3,6 @@ package com.vitorpamplona.amethyst.service import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.UserState import com.vitorpamplona.amethyst.service.model.ReportEvent import com.vitorpamplona.amethyst.service.model.RepostEvent import com.vitorpamplona.amethyst.service.relays.FeedType @@ -69,7 +68,7 @@ object NostrAccountDataSource: NostrDataSource("AccountData") { override fun updateChannelFilters() { // gets everthing about the user logged in - accountChannel.filter = listOf( + accountChannel.typedFilters = listOf( createAccountMetadataFilter(), createAccountContactListFilter(), createNotificationFilter(), diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrChannelDataSource.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrChannelDataSource.kt index ff483434e..37d1a65be 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrChannelDataSource.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrChannelDataSource.kt @@ -39,6 +39,6 @@ object NostrChannelDataSource: NostrDataSource("ChatroomFeed") { } override fun updateChannelFilters() { - messagesChannel.filter = listOfNotNull(createMessagesToChannelFilter()).ifEmpty { null } + messagesChannel.typedFilters = listOfNotNull(createMessagesToChannelFilter()).ifEmpty { null } } } \ No newline at end of file diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrChatRoomDataSource.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrChatRoomDataSource.kt index 8d8d30083..f9b404ff7 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrChatRoomDataSource.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrChatRoomDataSource.kt @@ -62,6 +62,6 @@ object NostrChatRoomDataSource: NostrDataSource("ChatroomFeed") { } override fun updateChannelFilters() { - inandoutChannel.filter = listOfNotNull(createMessagesToMeFilter(), createMessagesFromMeFilter()).ifEmpty { null } + inandoutChannel.typedFilters = listOfNotNull(createMessagesToMeFilter(), createMessagesFromMeFilter()).ifEmpty { null } } } \ No newline at end of file diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrChatroomListDataSource.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrChatroomListDataSource.kt index 6cc180b48..0d3b9d5cd 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrChatroomListDataSource.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrChatroomListDataSource.kt @@ -1,7 +1,6 @@ package com.vitorpamplona.amethyst.service import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.service.model.ChannelCreateEvent import com.vitorpamplona.amethyst.service.model.ChannelMessageEvent @@ -97,7 +96,7 @@ object NostrChatroomListDataSource: NostrDataSource("MailBoxFeed") { createMyChannelsFilter() ) - chatroomListChannel.filter = listOfNotNull( + chatroomListChannel.typedFilters = listOfNotNull( list, createLastChannelInfoFilter(), createLastMessageOfEachChannelFilter() diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrDataSource.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrDataSource.kt index b2ef9ed72..54706c5ee 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrDataSource.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrDataSource.kt @@ -13,6 +13,7 @@ import com.vitorpamplona.amethyst.service.model.ReportEvent import com.vitorpamplona.amethyst.service.model.RepostEvent import com.vitorpamplona.amethyst.service.relays.Client import com.vitorpamplona.amethyst.service.relays.Relay +import com.vitorpamplona.amethyst.service.relays.Subscription import java.util.Date import java.util.UUID import kotlinx.coroutines.CoroutineScope @@ -29,7 +30,7 @@ import nostr.postr.events.RecommendRelayEvent import nostr.postr.events.TextNoteEvent abstract class NostrDataSource(val debugName: String) { - private var channels = mapOf() + private var subscriptions = mapOf() private var eventCounter = mapOf() fun printCounter() { @@ -40,7 +41,7 @@ abstract class NostrDataSource(val debugName: String) { private val clientListener = object : Client.Listener() { override fun onEvent(event: Event, subscriptionId: String, relay: Relay) { - if (subscriptionId in channels.keys) { + if (subscriptionId in subscriptions.keys) { val key = "${debugName} ${subscriptionId} ${event.kind}" val keyValue = eventCounter.get(key) if (keyValue != null) { @@ -95,7 +96,7 @@ abstract class NostrDataSource(val debugName: String) { if (type == Relay.Type.EOSE && channel != null) { // updates a per subscripton since date - channels[channel]?.updateEOSE(Date().time / 1000) + subscriptions[channel]?.updateEOSE(Date().time / 1000) } } @@ -113,8 +114,8 @@ abstract class NostrDataSource(val debugName: String) { } open fun stop() { - channels.values.forEach { channel -> - if (channel.filter != null) // if it is active, close + subscriptions.values.forEach { channel -> + if (channel.typedFilters != null) // if it is active, close Client.close(channel.id) } } @@ -139,15 +140,15 @@ abstract class NostrDataSource(val debugName: String) { } } - fun requestNewChannel(onEOSE: ((Long) -> Unit)? = null): Channel { - val newChannel = Channel(UUID.randomUUID().toString().substring(0,4), onEOSE) - channels = channels + Pair(newChannel.id, newChannel) - return newChannel + fun requestNewChannel(onEOSE: ((Long) -> Unit)? = null): Subscription { + val newSubscription = Subscription(UUID.randomUUID().toString().substring(0,4), onEOSE) + subscriptions = subscriptions + Pair(newSubscription.id, newSubscription) + return newSubscription } - fun dismissChannel(channel: Channel) { - Client.close(channel.id) - channels = channels.minus(channel.id) + fun dismissChannel(subscription: Subscription) { + Client.close(subscription.id) + subscriptions = subscriptions.minus(subscription.id) } var handlerWaiting = false @@ -173,37 +174,37 @@ abstract class NostrDataSource(val debugName: String) { fun resetFiltersSuspend() { // saves the channels that are currently active - val activeChannels = channels.values.filter { it.filter != null } + val activeSubscriptions = subscriptions.values.filter { it.typedFilters != null } // saves the current content to only update if it changes - val currentFilter = activeChannels.associate { it.id to it.filter!!.joinToString("|") { it.filter.toJson() } } + val currentFilters = activeSubscriptions.associate { it.id to it.toJson() } updateChannelFilters() // Makes sure to only send an updated filter when it actually changes. - channels.values.forEach { channel -> - val channelsNewFilter = channel.filter + subscriptions.values.forEach { updatedSubscription -> + val updatedSubscriotionNewFilters = updatedSubscription.typedFilters - if (channel in activeChannels) { - if (channelsNewFilter == null) { + if (updatedSubscription.id in currentFilters.keys) { + if (updatedSubscriotionNewFilters == null) { // was active and is not active anymore, just close. - Client.close(channel.id) + Client.close(updatedSubscription.id) } else { // was active and is still active, check if it has changed. - if (channelsNewFilter.joinToString("|") { it.filter.toJson() } != currentFilter[channel.id]) { - Client.close(channel.id) - Client.sendFilter(channel.id, channelsNewFilter) + if (updatedSubscription.toJson() != currentFilters[updatedSubscription.id]) { + Client.close(updatedSubscription.id) + Client.sendFilter(updatedSubscription.id, updatedSubscriotionNewFilters) } else { // hasn't changed, does nothing. - Client.sendFilterOnlyIfDisconnected(channel.id, channelsNewFilter) + Client.sendFilterOnlyIfDisconnected(updatedSubscription.id, updatedSubscriotionNewFilters) } } } else { - if (channelsNewFilter == null) { + if (updatedSubscriotionNewFilters == null) { // was not active and is still not active, does nothing } else { // was not active and becomes active, sends the filter. - if (channelsNewFilter.joinToString("|") { it.filter.toJson() } != currentFilter[channel.id]) { - Client.sendFilter(channel.id, channelsNewFilter) + if (updatedSubscription.toJson() != currentFilters[updatedSubscription.id]) { + Client.sendFilter(updatedSubscription.id, updatedSubscriotionNewFilters) } } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrGlobalDataSource.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrGlobalDataSource.kt index 588d96148..e56ad00c3 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrGlobalDataSource.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrGlobalDataSource.kt @@ -31,6 +31,6 @@ object NostrGlobalDataSource: NostrDataSource("GlobalFeed") { .reversed() override fun updateChannelFilters() { - globalFeedChannel.filter = listOf(createGlobalFilter()).ifEmpty { null } + globalFeedChannel.typedFilters = listOf(createGlobalFilter()).ifEmpty { null } } } \ No newline at end of file diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrHomeDataSource.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrHomeDataSource.kt index cc70cfe09..c8ac2ba82 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrHomeDataSource.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrHomeDataSource.kt @@ -64,6 +64,6 @@ object NostrHomeDataSource: NostrDataSource("HomeFeed") { } override fun updateChannelFilters() { - followAccountChannel.filter = listOf(createFollowAccountsFilter()).ifEmpty { null } + followAccountChannel.typedFilters = listOf(createFollowAccountsFilter()).ifEmpty { null } } } \ No newline at end of file diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrSearchEventOrUserDataSource.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrSearchEventOrUserDataSource.kt index cd1f1bdb2..476549aaa 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrSearchEventOrUserDataSource.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrSearchEventOrUserDataSource.kt @@ -1,16 +1,11 @@ package com.vitorpamplona.amethyst.service -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.decodePublicKey -import com.vitorpamplona.amethyst.service.model.ReactionEvent -import com.vitorpamplona.amethyst.service.model.RepostEvent import com.vitorpamplona.amethyst.service.relays.FeedType import com.vitorpamplona.amethyst.service.relays.TypedFilter -import java.util.Collections import nostr.postr.JsonFilter import nostr.postr.bechToBytes -import nostr.postr.events.TextNoteEvent import nostr.postr.toHex object NostrSearchEventOrUserDataSource: NostrDataSource("SingleEventFeed") { @@ -43,7 +38,7 @@ object NostrSearchEventOrUserDataSource: NostrDataSource("SingleEventFeed" } override fun updateChannelFilters() { - searchChannel.filter = createAnythingWithIDFilter() + searchChannel.typedFilters = createAnythingWithIDFilter() } fun search(eventId: String) { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrSingleChannelDataSource.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrSingleChannelDataSource.kt index 243d3e7a7..e96c8eb4d 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrSingleChannelDataSource.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrSingleChannelDataSource.kt @@ -3,15 +3,10 @@ package com.vitorpamplona.amethyst.service import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.service.model.ChannelCreateEvent -import com.vitorpamplona.amethyst.service.model.ChannelMessageEvent import com.vitorpamplona.amethyst.service.model.ChannelMetadataEvent -import com.vitorpamplona.amethyst.service.model.ReactionEvent -import com.vitorpamplona.amethyst.service.model.RepostEvent import com.vitorpamplona.amethyst.service.relays.FeedType import com.vitorpamplona.amethyst.service.relays.TypedFilter -import java.util.Collections import nostr.postr.JsonFilter -import nostr.postr.events.TextNoteEvent object NostrSingleChannelDataSource: NostrDataSource("SingleChannelFeed") { private var channelsToWatch = setOf() @@ -64,7 +59,7 @@ object NostrSingleChannelDataSource: NostrDataSource("SingleChannelFeed") val reactions = createRepliesAndReactionsFilter() val missing = createLoadEventsIfNotLoadedFilter() - singleChannelChannel.filter = listOfNotNull(reactions, missing).ifEmpty { null } + singleChannelChannel.typedFilters = listOfNotNull(reactions, missing).ifEmpty { null } } fun add(eventId: String) { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrSingleEventDataSource.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrSingleEventDataSource.kt index 6dc2d401d..84bf97c69 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrSingleEventDataSource.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrSingleEventDataSource.kt @@ -10,10 +10,8 @@ import com.vitorpamplona.amethyst.service.model.ReportEvent import com.vitorpamplona.amethyst.service.model.RepostEvent import com.vitorpamplona.amethyst.service.relays.FeedType import com.vitorpamplona.amethyst.service.relays.TypedFilter -import java.util.Collections import java.util.Date import nostr.postr.JsonFilter -import nostr.postr.events.MetadataEvent import nostr.postr.events.TextNoteEvent object NostrSingleEventDataSource: NostrDataSource("SingleEventFeed") { @@ -100,7 +98,7 @@ object NostrSingleEventDataSource: NostrDataSource("SingleEventFeed") { val reactions = createRepliesAndReactionsFilter() val missing = createLoadEventsIfNotLoadedFilter() - singleEventChannel.filter = listOfNotNull(reactions, missing).flatten().ifEmpty { null } + singleEventChannel.typedFilters = listOfNotNull(reactions, missing).flatten().ifEmpty { null } } fun add(eventId: String) { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrSingleUserDataSource.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrSingleUserDataSource.kt index f588bd8e2..3a8b74d95 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrSingleUserDataSource.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrSingleUserDataSource.kt @@ -1,12 +1,10 @@ package com.vitorpamplona.amethyst.service import com.vitorpamplona.amethyst.model.LocalCache -import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.service.model.ReportEvent import com.vitorpamplona.amethyst.service.relays.FeedType import com.vitorpamplona.amethyst.service.relays.TypedFilter -import java.util.Collections import nostr.postr.JsonFilter import nostr.postr.events.MetadataEvent @@ -57,7 +55,7 @@ object NostrSingleUserDataSource: NostrDataSource("SingleUserFeed") { } override fun updateChannelFilters() { - userChannel.filter = listOfNotNull(createUserFilter(), createUserReportFilter()).flatten() + userChannel.typedFilters = listOfNotNull(createUserFilter(), createUserReportFilter()).flatten() } fun add(userId: String) { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrThreadDataSource.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrThreadDataSource.kt index ca54e20fd..f5b8e50c8 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrThreadDataSource.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrThreadDataSource.kt @@ -6,7 +6,6 @@ import com.vitorpamplona.amethyst.service.model.ReactionEvent import com.vitorpamplona.amethyst.service.model.RepostEvent import com.vitorpamplona.amethyst.service.relays.FeedType import com.vitorpamplona.amethyst.service.relays.TypedFilter -import java.util.Collections import nostr.postr.JsonFilter import nostr.postr.events.TextNoteEvent @@ -58,7 +57,7 @@ object NostrThreadDataSource: NostrDataSource("SingleThreadFeed") { } override fun updateChannelFilters() { - loadEventsChannel.filter = listOfNotNull(createLoadEventsIfNotLoadedFilter(), createRepliesAndReactionsFilter()).ifEmpty { null } + loadEventsChannel.typedFilters = listOfNotNull(createLoadEventsIfNotLoadedFilter(), createRepliesAndReactionsFilter()).ifEmpty { null } } fun searchRoot(note: Note, testedNotes: MutableSet = mutableSetOf()): Note? { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrUserProfileDataSource.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrUserProfileDataSource.kt index 6971a5c21..451adf915 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrUserProfileDataSource.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrUserProfileDataSource.kt @@ -4,7 +4,6 @@ import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.User -import com.vitorpamplona.amethyst.model.toByteArray import com.vitorpamplona.amethyst.service.relays.FeedType import com.vitorpamplona.amethyst.service.relays.TypedFilter import nostr.postr.JsonFilter @@ -73,7 +72,7 @@ object NostrUserProfileDataSource: NostrDataSource("UserProfileFeed") { } override fun updateChannelFilters() { - userInfoChannel.filter = listOf( + userInfoChannel.typedFilters = listOf( createUserInfoFilter(), createUserPostsFilter(), createFollowFilter(), diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/relays/Relay.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/relays/Relay.kt index 8f81260b6..ae41168cc 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/relays/Relay.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/relays/Relay.kt @@ -170,7 +170,7 @@ class Relay( if (filters.isNotEmpty()) { val request = """["REQ","$requestId",${filters.take(10).joinToString(",") { it.filter.toJson() }}]""" - //println("FILTERSSENT ${url} " + """["REQ","$requestId",${filters.joinToString(",") { it.toJson() }}]""") + //println("FILTERSSENT ${url} ${request}") socket?.send(request) } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/relays/Subscription.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/relays/Subscription.kt new file mode 100644 index 000000000..6465fa9e6 --- /dev/null +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/relays/Subscription.kt @@ -0,0 +1,30 @@ +package com.vitorpamplona.amethyst.service.relays + +import com.google.gson.GsonBuilder +import com.google.gson.JsonArray +import com.google.gson.JsonObject +import java.util.UUID + +data class Subscription ( + val id: String = UUID.randomUUID().toString().substring(0,4), + val onEOSE: ((Long) -> Unit)? = null +) { + var typedFilters: List? = null // Inactive when null + + fun updateEOSE(l: Long) { + onEOSE?.let { it(l) } + } + + fun toJson(): String { + return GsonBuilder().create().toJson(toJsonObject()) + } + + fun toJsonObject(): JsonObject { + val jsonObject = JsonObject() + jsonObject.addProperty("id", id) + typedFilters?.run { + jsonObject.add("typedFilters", JsonArray().apply { typedFilters?.forEach { add(it.toJsonObject()) } }) + } + return jsonObject + } +} \ No newline at end of file diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/relays/TypedFilter.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/relays/TypedFilter.kt index 2612a13fa..382e73793 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/relays/TypedFilter.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/relays/TypedFilter.kt @@ -1,8 +1,56 @@ package com.vitorpamplona.amethyst.service.relays +import com.google.gson.Gson +import com.google.gson.GsonBuilder +import com.google.gson.JsonArray +import com.google.gson.JsonObject import nostr.postr.JsonFilter class TypedFilter( val types: Set, val filter: JsonFilter -) \ No newline at end of file +) { + + fun toJson(): String { + return GsonBuilder().create().toJson(toJsonObject()) + } + + fun toJsonObject(): JsonObject { + val jsonObject = JsonObject() + jsonObject.add("types", typesToJson(types)) + jsonObject.add("filter", filterToJson(filter)) + return jsonObject + } + + fun typesToJson(types: Set): JsonArray { + return JsonArray().apply { types.forEach { add(it.name.toLowerCase()) } } + } + + fun filterToJson(filter: JsonFilter): JsonObject { + val jsonObject = JsonObject() + filter.ids?.run { + jsonObject.add("ids", JsonArray().apply { filter.ids?.forEach { add(it) } }) + } + filter.authors?.run { + jsonObject.add("authors", JsonArray().apply { filter.authors?.forEach { add(it) } }) + } + filter.kinds?.run { + jsonObject.add("kinds", JsonArray().apply { filter.kinds?.forEach { add(it) } }) + } + filter.tags?.run { + entries.forEach { kv -> + jsonObject.add("#${kv.key}", JsonArray().apply { kv.value.forEach { add(it) } }) + } + } + filter.since?.run { + jsonObject.addProperty("since", filter.since) + } + filter.until?.run { + jsonObject.addProperty("until", filter.until) + } + filter.limit?.run { + jsonObject.addProperty("limit", filter.limit) + } + return jsonObject + } +} \ No newline at end of file