package com.vitorpamplona.amethyst.service import com.vitorpamplona.amethyst.model.AddressableNote import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.service.model.BadgeAwardEvent import com.vitorpamplona.amethyst.service.model.BadgeDefinitionEvent import com.vitorpamplona.amethyst.service.model.BadgeProfilesEvent 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.LnZapEvent import com.vitorpamplona.amethyst.service.model.LnZapRequestEvent import com.vitorpamplona.amethyst.service.model.LongTextNoteEvent import com.vitorpamplona.amethyst.service.model.ReactionEvent import com.vitorpamplona.amethyst.service.model.ReportEvent import com.vitorpamplona.amethyst.service.model.RepostEvent import com.vitorpamplona.amethyst.service.model.TextNoteEvent import com.vitorpamplona.amethyst.service.relays.FeedType import com.vitorpamplona.amethyst.service.relays.JsonFilter import com.vitorpamplona.amethyst.service.relays.TypedFilter import java.util.Date object NostrSingleEventDataSource : NostrDataSource("SingleEventFeed") { private var eventsToWatch = setOf() private var addressesToWatch = setOf() private fun createTagToAddressFilter(): List? { val addressesToWatch = eventsToWatch.filter { it.address() != null } + addressesToWatch if (addressesToWatch.isEmpty()) { return null } val now = Date().time / 1000 return addressesToWatch.filter { val lastTime = it.lastReactionsDownloadTime lastTime == null || lastTime < (now - 10) }.mapNotNull { it.address()?.let { aTag -> TypedFilter( types = FeedType.values().toSet(), filter = JsonFilter( kinds = listOf( TextNoteEvent.kind, LongTextNoteEvent.kind, ReactionEvent.kind, RepostEvent.kind, ReportEvent.kind, LnZapEvent.kind, LnZapRequestEvent.kind, BadgeAwardEvent.kind, BadgeDefinitionEvent.kind, BadgeProfilesEvent.kind ), tags = mapOf("a" to listOf(aTag.toTag())), since = it.lastReactionsDownloadTime ) ) } } } private fun createAddressFilter(): List? { val addressesToWatch = addressesToWatch.filter { it.event == null } if (addressesToWatch.isEmpty()) { return null } val now = Date().time / 1000 return addressesToWatch.filter { val lastTime = it.lastReactionsDownloadTime lastTime == null || lastTime < (now - 10) }.mapNotNull { it.address()?.let { aTag -> TypedFilter( types = FeedType.values().toSet(), filter = JsonFilter( kinds = listOf(aTag.kind), tags = mapOf("d" to listOf(aTag.dTag)), authors = listOf(aTag.pubKeyHex.substring(0, 8)) ) ) } } } private fun createRepliesAndReactionsFilter(): List? { val reactionsToWatch = eventsToWatch if (reactionsToWatch.isEmpty()) { return null } val now = Date().time / 1000 return reactionsToWatch.filter { val lastTime = it.lastReactionsDownloadTime lastTime == null || lastTime < (now - 10) }.map { TypedFilter( types = FeedType.values().toSet(), filter = JsonFilter( kinds = listOf( TextNoteEvent.kind, LongTextNoteEvent.kind, ReactionEvent.kind, RepostEvent.kind, ReportEvent.kind, LnZapEvent.kind, LnZapRequestEvent.kind ), tags = mapOf("e" to listOf(it.idHex)), since = it.lastReactionsDownloadTime ) ) } } fun createLoadEventsIfNotLoadedFilter(): List? { val directEventsToLoad = eventsToWatch .filter { it.event == null } val threadingEventsToLoad = eventsToWatch .mapNotNull { it.replyTo } .flatten() .filter { it !is AddressableNote && it.event == null } val interestedEvents = (directEventsToLoad + threadingEventsToLoad) .map { it.idHex }.toSet() if (interestedEvents.isEmpty()) { return null } // downloads linked events to this event. return listOf( TypedFilter( types = FeedType.values().toSet(), filter = JsonFilter( kinds = listOf( TextNoteEvent.kind, LongTextNoteEvent.kind, ReactionEvent.kind, RepostEvent.kind, LnZapEvent.kind, LnZapRequestEvent.kind, ChannelMessageEvent.kind, ChannelCreateEvent.kind, ChannelMetadataEvent.kind, BadgeDefinitionEvent.kind, BadgeAwardEvent.kind, BadgeProfilesEvent.kind ), ids = interestedEvents.toList() ) ) ) } val singleEventChannel = requestNewChannel { time -> eventsToWatch.forEach { it.lastReactionsDownloadTime = time } // Many relays operate with limits in the amount of filters. // As information comes, the filters will be rotated to get more data. invalidateFilters() } override fun updateChannelFilters() { val reactions = createRepliesAndReactionsFilter() val missing = createLoadEventsIfNotLoadedFilter() val addresses = createAddressFilter() val addressReactions = createTagToAddressFilter() singleEventChannel.typedFilters = listOfNotNull(reactions, missing, addresses, addressReactions).flatten().ifEmpty { null } } fun add(eventId: Note) { eventsToWatch = eventsToWatch.plus(eventId) invalidateFilters() } fun remove(eventId: Note) { eventsToWatch = eventsToWatch.minus(eventId) invalidateFilters() } fun addAddress(aTag: Note) { addressesToWatch = addressesToWatch.plus(aTag) invalidateFilters() } fun removeAddress(aTag: Note) { addressesToWatch = addressesToWatch.minus(aTag) invalidateFilters() } }