Merge branch 'main' into claude/implement-nip-85-hBlbp
This commit is contained in:
+3
@@ -37,6 +37,7 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.followPacks.feed.datasource
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.geohash.datasource.GeoHashFilterAssembler
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.hashtag.datasource.HashtagFilterAssembler
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.datasource.HomeFilterAssembler
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.polls.datasource.PollsFilterAssembler
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.datasource.UserProfileFilterAssembler
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relay.datasource.RelayFeedFilterAssembler
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.datasource.RelayInfoNip66FilterAssembler
|
||||
@@ -62,6 +63,7 @@ class RelaySubscriptionsCoordinator(
|
||||
val chatroomList = ChatroomListFilterAssembler(client)
|
||||
val video = VideoFilterAssembler(client)
|
||||
val discovery = DiscoveryFilterAssembler(client)
|
||||
val polls = PollsFilterAssembler(client)
|
||||
|
||||
// loaders of content that is not yet in the device.
|
||||
// they are active when looking at events, users, channels.
|
||||
@@ -95,6 +97,7 @@ class RelaySubscriptionsCoordinator(
|
||||
chatroomList,
|
||||
video,
|
||||
discovery,
|
||||
polls,
|
||||
channelFinder,
|
||||
eventFinder,
|
||||
userFinder,
|
||||
|
||||
+13
-2
@@ -23,6 +23,8 @@ package com.vitorpamplona.amethyst.ui.components
|
||||
import android.Manifest
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.view.WindowManager
|
||||
import android.widget.Toast
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
@@ -301,6 +303,15 @@ private fun DialogContent(
|
||||
}
|
||||
}
|
||||
|
||||
private fun showToastOnMain(
|
||||
context: Context,
|
||||
resId: Int,
|
||||
) {
|
||||
Handler(Looper.getMainLooper()).post {
|
||||
Toast.makeText(context.applicationContext, resId, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun saveMediaToGallery(
|
||||
content: BaseMediaContent,
|
||||
localContext: Context,
|
||||
@@ -324,7 +335,7 @@ private suspend fun saveMediaToGallery(
|
||||
},
|
||||
localContext,
|
||||
onSuccess = {
|
||||
accountViewModel.toastManager.toast(success, success)
|
||||
showToastOnMain(localContext, success)
|
||||
},
|
||||
onError = {
|
||||
accountViewModel.toastManager.toast(failure, null, it)
|
||||
@@ -337,7 +348,7 @@ private suspend fun saveMediaToGallery(
|
||||
content.mimeType,
|
||||
localContext,
|
||||
onSuccess = {
|
||||
accountViewModel.toastManager.toast(success, success)
|
||||
showToastOnMain(localContext, success)
|
||||
},
|
||||
onError = { innerIt ->
|
||||
accountViewModel.toastManager.toast(failure, null, innerIt)
|
||||
|
||||
@@ -55,6 +55,8 @@ object ScrollStateKeys {
|
||||
const val DISCOVER_COMMUNITY = "DiscoverCommunitiesFeed"
|
||||
const val DISCOVER_CHATS = "DiscoverChatsFeed"
|
||||
|
||||
const val POLLS_SCREEN = "PollsFeed"
|
||||
|
||||
const val SEARCH_SCREEN = "SearchFeed"
|
||||
|
||||
const val WEB_BOOKMARKS = "WebBookmarksFeed"
|
||||
|
||||
@@ -101,6 +101,7 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.newUser.ImportFollowListPic
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.newUser.ImportFollowListSelectUserScreen
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.NotificationScreen
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.publicMessages.NewPublicMessageScreen
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.polls.PollsScreen
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.privacy.PrivacyOptionsScreen
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.ProfileScreen
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.qrcode.ShowQRScreen
|
||||
@@ -172,6 +173,7 @@ fun BuildNavigation(
|
||||
composable<Route.Video> { VideoScreen(accountViewModel, nav) }
|
||||
composable<Route.Discover> { DiscoverScreen(accountViewModel, nav) }
|
||||
composable<Route.Notification> { NotificationScreen(accountViewModel, nav) }
|
||||
composableFromEnd<Route.Polls> { PollsScreen(accountViewModel, nav) }
|
||||
composable<Route.Chess> { ChessLobbyScreen(accountViewModel, nav) }
|
||||
|
||||
composableFromEnd<Route.Wallet> { WalletScreen(accountViewModel, nav) }
|
||||
|
||||
+9
@@ -574,6 +574,15 @@ fun ListContent(
|
||||
route = Route.Drafts,
|
||||
)
|
||||
|
||||
NavigationRow(
|
||||
title = R.string.polls,
|
||||
icon = R.drawable.ic_poll,
|
||||
iconReference = 1,
|
||||
tint = MaterialTheme.colorScheme.onBackground,
|
||||
nav = nav,
|
||||
route = Route.Polls,
|
||||
)
|
||||
|
||||
NavigationRow(
|
||||
title = R.string.wallet,
|
||||
icon = Icons.Outlined.AccountBalanceWallet,
|
||||
|
||||
@@ -41,6 +41,8 @@ sealed class Route {
|
||||
|
||||
@Serializable object Notification : Route()
|
||||
|
||||
@Serializable object Polls : Route()
|
||||
|
||||
@Serializable object Chess : Route()
|
||||
|
||||
@Serializable object Wallet : Route()
|
||||
|
||||
+7
@@ -44,6 +44,7 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.CardFeedConte
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.NotificationSummaryState
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.OpenPollsState
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.dal.NotificationFeedFilter
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.polls.dal.PollsFeedFilter
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.video.dal.VideoFeedFilter
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.webBookmarks.dal.WebBookmarkFeedFilter
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
@@ -69,6 +70,8 @@ class AccountFeedContentStates(
|
||||
val discoverCommunities = FeedContentState(DiscoverCommunityFeedFilter(account), scope, LocalCache)
|
||||
val discoverPublicChats = FeedContentState(DiscoverChatFeedFilter(account), scope, LocalCache)
|
||||
|
||||
val pollsFeed = FeedContentState(PollsFeedFilter(account), scope, LocalCache)
|
||||
|
||||
val notifications = CardFeedContentState(NotificationFeedFilter(account), scope)
|
||||
val notificationsOpenPolls = OpenPollsState(account, scope)
|
||||
val notificationSummary = NotificationSummaryState(account)
|
||||
@@ -103,6 +106,8 @@ class AccountFeedContentStates(
|
||||
discoverCommunities.updateFeedWith(newNotes)
|
||||
discoverPublicChats.updateFeedWith(newNotes)
|
||||
|
||||
pollsFeed.updateFeedWith(newNotes)
|
||||
|
||||
notifications.updateFeedWith(newNotes)
|
||||
notificationSummary.invalidateInsertData(newNotes)
|
||||
|
||||
@@ -131,6 +136,8 @@ class AccountFeedContentStates(
|
||||
discoverCommunities.deleteFromFeed(newNotes)
|
||||
discoverPublicChats.deleteFromFeed(newNotes)
|
||||
|
||||
pollsFeed.deleteFromFeed(newNotes)
|
||||
|
||||
notifications.deleteFromFeed(newNotes)
|
||||
notificationSummary.invalidateInsertData(newNotes)
|
||||
|
||||
|
||||
+8
-1
@@ -23,7 +23,10 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.util.LruCache
|
||||
import android.widget.Toast
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.Immutable
|
||||
import androidx.compose.runtime.Stable
|
||||
@@ -1709,7 +1712,11 @@ class AccountViewModel(
|
||||
mimeType = mimeType,
|
||||
localContext = localContext,
|
||||
onSuccess = {
|
||||
toastManager.toast(R.string.video_saved_to_the_gallery, R.string.video_saved_to_the_gallery)
|
||||
Handler(Looper.getMainLooper()).post {
|
||||
Toast
|
||||
.makeText(localContext.applicationContext, R.string.video_saved_to_the_gallery, Toast.LENGTH_SHORT)
|
||||
.show()
|
||||
}
|
||||
},
|
||||
onError = {
|
||||
toastManager.toast(R.string.failed_to_save_the_video, null, it)
|
||||
|
||||
+105
@@ -0,0 +1,105 @@
|
||||
/*
|
||||
* Copyright (c) 2025 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.amethyst.ui.screen.loggedIn.polls
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.vitorpamplona.amethyst.commons.ui.feeds.FeedContentState
|
||||
import com.vitorpamplona.amethyst.ui.feeds.RefresheableBox
|
||||
import com.vitorpamplona.amethyst.ui.feeds.RenderFeedContentState
|
||||
import com.vitorpamplona.amethyst.ui.feeds.SaveableFeedContentState
|
||||
import com.vitorpamplona.amethyst.ui.feeds.ScrollStateKeys
|
||||
import com.vitorpamplona.amethyst.ui.feeds.WatchLifecycleAndUpdateModel
|
||||
import com.vitorpamplona.amethyst.ui.layouts.DisappearingScaffold
|
||||
import com.vitorpamplona.amethyst.ui.navigation.bottombars.AppBottomBar
|
||||
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
|
||||
import com.vitorpamplona.amethyst.ui.navigation.routes.Route
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.polls.datasource.PollsFilterAssemblerSubscription
|
||||
|
||||
@Composable
|
||||
fun PollsScreen(
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: INav,
|
||||
) {
|
||||
PollsScreen(
|
||||
pollsFeedContentState = accountViewModel.feedStates.pollsFeed,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun PollsScreen(
|
||||
pollsFeedContentState: FeedContentState,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: INav,
|
||||
) {
|
||||
WatchLifecycleAndUpdateModel(pollsFeedContentState)
|
||||
WatchAccountForPollsScreen(pollsFeedContentState = pollsFeedContentState, accountViewModel = accountViewModel)
|
||||
PollsFilterAssemblerSubscription(accountViewModel)
|
||||
|
||||
DisappearingScaffold(
|
||||
isInvertedLayout = false,
|
||||
topBar = {
|
||||
PollsTopBar(accountViewModel, nav)
|
||||
},
|
||||
bottomBar = {
|
||||
AppBottomBar(Route.Polls, accountViewModel) { route ->
|
||||
if (route == Route.Polls) {
|
||||
pollsFeedContentState.sendToTop()
|
||||
} else {
|
||||
nav.newStack(route)
|
||||
}
|
||||
}
|
||||
},
|
||||
accountViewModel = accountViewModel,
|
||||
) {
|
||||
RefresheableBox(pollsFeedContentState, true) {
|
||||
SaveableFeedContentState(pollsFeedContentState, scrollStateKey = ScrollStateKeys.POLLS_SCREEN) { listState ->
|
||||
RenderFeedContentState(
|
||||
feedContentState = pollsFeedContentState,
|
||||
accountViewModel = accountViewModel,
|
||||
listState = listState,
|
||||
nav = nav,
|
||||
routeForLastRead = "PollsFeed",
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun WatchAccountForPollsScreen(
|
||||
pollsFeedContentState: FeedContentState,
|
||||
accountViewModel: AccountViewModel,
|
||||
) {
|
||||
val listState by accountViewModel.account.liveDiscoveryFollowLists.collectAsStateWithLifecycle()
|
||||
val hiddenUsers =
|
||||
accountViewModel.account.hiddenUsers.flow
|
||||
.collectAsStateWithLifecycle()
|
||||
|
||||
LaunchedEffect(accountViewModel, listState, hiddenUsers) {
|
||||
pollsFeedContentState.checkKeysInvalidateDataAndSendToTop()
|
||||
}
|
||||
}
|
||||
+70
@@ -0,0 +1,70 @@
|
||||
/*
|
||||
* Copyright (c) 2025 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.amethyst.ui.screen.loggedIn.polls
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.model.TopFilter
|
||||
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
|
||||
import com.vitorpamplona.amethyst.ui.navigation.topbars.FeedFilterSpinner
|
||||
import com.vitorpamplona.amethyst.ui.navigation.topbars.UserDrawerSearchTopBar
|
||||
import com.vitorpamplona.amethyst.ui.screen.FeedDefinition
|
||||
import com.vitorpamplona.amethyst.ui.screen.TopNavFilterState
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
|
||||
@Composable
|
||||
fun PollsTopBar(
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: INav,
|
||||
) {
|
||||
UserDrawerSearchTopBar(accountViewModel, nav) {
|
||||
val list by accountViewModel.account.settings.defaultDiscoveryFollowList
|
||||
.collectAsStateWithLifecycle()
|
||||
|
||||
PollsTopNavFilterBar(
|
||||
followListsModel = accountViewModel.feedStates.feedListOptions,
|
||||
listName = list,
|
||||
accountViewModel = accountViewModel,
|
||||
onChange = accountViewModel.account.settings::changeDefaultDiscoveryFollowList,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun PollsTopNavFilterBar(
|
||||
followListsModel: TopNavFilterState,
|
||||
listName: TopFilter,
|
||||
accountViewModel: AccountViewModel,
|
||||
onChange: (FeedDefinition) -> Unit,
|
||||
) {
|
||||
val allLists by followListsModel.kind3GlobalPeople.collectAsStateWithLifecycle()
|
||||
|
||||
FeedFilterSpinner(
|
||||
placeholderCode = listName,
|
||||
explainer = stringRes(R.string.select_list_to_filter),
|
||||
options = allLists,
|
||||
onSelect = { onChange(allLists.getOrNull(it) ?: followListsModel.allFollows) },
|
||||
accountViewModel = accountViewModel,
|
||||
)
|
||||
}
|
||||
+78
@@ -0,0 +1,78 @@
|
||||
/*
|
||||
* Copyright (c) 2025 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.amethyst.ui.screen.loggedIn.polls.dal
|
||||
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.model.TopFilter
|
||||
import com.vitorpamplona.amethyst.model.filterIntoSet
|
||||
import com.vitorpamplona.amethyst.ui.dal.AdditiveFeedFilter
|
||||
import com.vitorpamplona.amethyst.ui.dal.DefaultFeedOrder
|
||||
import com.vitorpamplona.amethyst.ui.dal.FilterByListParams
|
||||
import com.vitorpamplona.quartz.nip88Polls.poll.PollEvent
|
||||
|
||||
class PollsFeedFilter(
|
||||
val account: Account,
|
||||
) : AdditiveFeedFilter<Note>() {
|
||||
override fun feedKey(): String = account.userProfile().pubkeyHex + "-" + followList().code
|
||||
|
||||
override fun limit() = 200
|
||||
|
||||
fun followList(): TopFilter = account.settings.defaultDiscoveryFollowList.value
|
||||
|
||||
fun TopFilter.isMuteList() = this is TopFilter.MuteList
|
||||
|
||||
fun TopFilter.isBlockList() = this is TopFilter.PeopleList && this.address == account.blockPeopleList.getBlockListAddress()
|
||||
|
||||
fun TopFilter.wantsToSeeNegativeStuff() = isMuteList() || isBlockList()
|
||||
|
||||
override fun showHiddenKey(): Boolean = followList().wantsToSeeNegativeStuff()
|
||||
|
||||
override fun feed(): List<Note> {
|
||||
val params = buildFilterParams(account)
|
||||
val notes =
|
||||
LocalCache.notes.filterIntoSet { _, it ->
|
||||
val noteEvent = it.event
|
||||
noteEvent is PollEvent && params.match(noteEvent, it.relays)
|
||||
}
|
||||
return sort(notes)
|
||||
}
|
||||
|
||||
override fun applyFilter(newItems: Set<Note>): Set<Note> = innerApplyFilter(newItems)
|
||||
|
||||
fun buildFilterParams(account: Account): FilterByListParams =
|
||||
FilterByListParams.create(
|
||||
account.liveDiscoveryFollowLists.value,
|
||||
account.hiddenUsers.flow.value,
|
||||
)
|
||||
|
||||
private fun innerApplyFilter(collection: Collection<Note>): Set<Note> {
|
||||
val params = buildFilterParams(account)
|
||||
|
||||
return collection.filterTo(HashSet()) {
|
||||
val noteEvent = it.event
|
||||
noteEvent is PollEvent && params.match(noteEvent, it.relays)
|
||||
}
|
||||
}
|
||||
|
||||
override fun sort(items: Set<Note>): List<Note> = items.sortedWith(DefaultFeedOrder)
|
||||
}
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
/*
|
||||
* Copyright (c) 2025 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.amethyst.ui.screen.loggedIn.polls.datasource
|
||||
|
||||
import com.vitorpamplona.amethyst.commons.relayClient.composeSubscriptionManagers.ComposeSubscriptionManager
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountFeedContentStates
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
|
||||
class PollsQueryState(
|
||||
val account: Account,
|
||||
val feedStates: AccountFeedContentStates,
|
||||
val scope: CoroutineScope,
|
||||
)
|
||||
|
||||
class PollsFilterAssembler(
|
||||
client: INostrClient,
|
||||
) : ComposeSubscriptionManager<PollsQueryState>() {
|
||||
val group =
|
||||
listOf(
|
||||
PollsSubAssembler(client, ::allKeys),
|
||||
)
|
||||
|
||||
override fun invalidateKeys() = invalidateFilters()
|
||||
|
||||
override fun invalidateFilters() = group.forEach { it.invalidateFilters() }
|
||||
|
||||
override fun destroy() = group.forEach { it.destroy() }
|
||||
}
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
/*
|
||||
* Copyright (c) 2025 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.amethyst.ui.screen.loggedIn.polls.datasource
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.KeyDataSourceSubscription
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
|
||||
@Composable
|
||||
fun PollsFilterAssemblerSubscription(accountViewModel: AccountViewModel) {
|
||||
PollsFilterAssemblerSubscription(
|
||||
accountViewModel.dataSources().polls,
|
||||
accountViewModel,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun PollsFilterAssemblerSubscription(
|
||||
dataSource: PollsFilterAssembler,
|
||||
accountViewModel: AccountViewModel,
|
||||
) {
|
||||
val state =
|
||||
remember(accountViewModel.account) {
|
||||
PollsQueryState(accountViewModel.account, accountViewModel.feedStates, accountViewModel.viewModelScope)
|
||||
}
|
||||
|
||||
KeyDataSourceSubscription(state, dataSource)
|
||||
}
|
||||
+97
@@ -0,0 +1,97 @@
|
||||
/*
|
||||
* Copyright (c) 2025 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.amethyst.ui.screen.loggedIn.polls.datasource
|
||||
|
||||
import com.vitorpamplona.amethyst.model.TopFilter
|
||||
import com.vitorpamplona.amethyst.model.User
|
||||
import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.PerUserAndFollowListEoseManager
|
||||
import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.subscriptions.Subscription
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.FlowPreview
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.flow.sample
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class PollsSubAssembler(
|
||||
client: INostrClient,
|
||||
allKeys: () -> Set<PollsQueryState>,
|
||||
) : PerUserAndFollowListEoseManager<PollsQueryState, TopFilter>(client, allKeys) {
|
||||
override fun updateFilter(
|
||||
key: PollsQueryState,
|
||||
since: SincePerRelayMap?,
|
||||
): List<RelayBasedFilter> {
|
||||
val feedSettings = key.followsPerRelay()
|
||||
|
||||
return makePollsFilter(feedSettings, since, key.feedStates.pollsFeed.lastNoteCreatedAtIfFilled())
|
||||
}
|
||||
|
||||
override fun user(key: PollsQueryState) = key.account.userProfile()
|
||||
|
||||
override fun list(key: PollsQueryState) = key.listName()
|
||||
|
||||
fun PollsQueryState.listNameFlow() = account.settings.defaultDiscoveryFollowList
|
||||
|
||||
fun PollsQueryState.listName() = listNameFlow().value
|
||||
|
||||
fun PollsQueryState.followsPerRelayFlow() = account.liveDiscoveryFollowListsPerRelay
|
||||
|
||||
fun PollsQueryState.followsPerRelay() = followsPerRelayFlow().value
|
||||
|
||||
val userJobMap = mutableMapOf<User, List<Job>>()
|
||||
|
||||
@OptIn(FlowPreview::class)
|
||||
override fun newSub(key: PollsQueryState): Subscription {
|
||||
val user = user(key)
|
||||
userJobMap[user]?.forEach { it.cancel() }
|
||||
userJobMap[user] =
|
||||
listOf(
|
||||
key.scope.launch(Dispatchers.IO) {
|
||||
key.listNameFlow().collectLatest {
|
||||
invalidateFilters()
|
||||
}
|
||||
},
|
||||
key.scope.launch(Dispatchers.IO) {
|
||||
key.followsPerRelayFlow().sample(500).collectLatest {
|
||||
invalidateFilters()
|
||||
}
|
||||
},
|
||||
key.account.scope.launch(Dispatchers.IO) {
|
||||
key.feedStates.pollsFeed.lastNoteCreatedAtWhenFullyLoaded.sample(5000).collectLatest {
|
||||
invalidateFilters()
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
return super.newSub(key)
|
||||
}
|
||||
|
||||
override fun endSub(
|
||||
key: User,
|
||||
subId: String,
|
||||
) {
|
||||
super.endSub(key, subId)
|
||||
userJobMap[key]?.forEach { it.cancel() }
|
||||
}
|
||||
}
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
/*
|
||||
* Copyright (c) 2025 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.amethyst.ui.screen.loggedIn.polls.datasource
|
||||
|
||||
import com.vitorpamplona.amethyst.model.topNavFeeds.IFeedTopNavPerRelayFilterSet
|
||||
import com.vitorpamplona.amethyst.model.topNavFeeds.allFollows.AllFollowsTopNavPerRelayFilterSet
|
||||
import com.vitorpamplona.amethyst.model.topNavFeeds.global.GlobalTopNavPerRelayFilterSet
|
||||
import com.vitorpamplona.amethyst.model.topNavFeeds.hashtag.HashtagTopNavPerRelayFilterSet
|
||||
import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.author.AuthorsTopNavPerRelayFilterSet
|
||||
import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.muted.MutedAuthorsTopNavPerRelayFilterSet
|
||||
import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.polls.datasource.subassemblies.filterPollsByAuthors
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.polls.datasource.subassemblies.filterPollsByFollows
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.polls.datasource.subassemblies.filterPollsByHashtag
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.polls.datasource.subassemblies.filterPollsByMutedAuthors
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.polls.datasource.subassemblies.filterPollsGlobal
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter
|
||||
|
||||
fun makePollsFilter(
|
||||
feedSettings: IFeedTopNavPerRelayFilterSet,
|
||||
since: SincePerRelayMap?,
|
||||
defaultSince: Long? = null,
|
||||
): List<RelayBasedFilter> =
|
||||
when (feedSettings) {
|
||||
is AllFollowsTopNavPerRelayFilterSet -> filterPollsByFollows(feedSettings, since, defaultSince)
|
||||
is AuthorsTopNavPerRelayFilterSet -> filterPollsByAuthors(feedSettings, since, defaultSince)
|
||||
is GlobalTopNavPerRelayFilterSet -> filterPollsGlobal(feedSettings, since, defaultSince)
|
||||
is HashtagTopNavPerRelayFilterSet -> filterPollsByHashtag(feedSettings, since, defaultSince)
|
||||
is MutedAuthorsTopNavPerRelayFilterSet -> filterPollsByMutedAuthors(feedSettings, since, defaultSince)
|
||||
else -> emptyList()
|
||||
}
|
||||
+92
@@ -0,0 +1,92 @@
|
||||
/*
|
||||
* Copyright (c) 2025 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.amethyst.ui.screen.loggedIn.polls.datasource.subassemblies
|
||||
|
||||
import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.author.AuthorsTopNavPerRelayFilterSet
|
||||
import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.muted.MutedAuthorsTopNavPerRelayFilterSet
|
||||
import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
import com.vitorpamplona.quartz.nip88Polls.poll.PollEvent
|
||||
|
||||
fun filterPollsByAuthors(
|
||||
relay: NormalizedRelayUrl,
|
||||
authors: Set<HexKey>,
|
||||
since: Long? = null,
|
||||
): List<RelayBasedFilter> {
|
||||
val authorList = authors.sorted()
|
||||
return listOf(
|
||||
RelayBasedFilter(
|
||||
relay = relay,
|
||||
filter =
|
||||
Filter(
|
||||
authors = authorList,
|
||||
kinds = listOf(PollEvent.KIND),
|
||||
limit = 200,
|
||||
since = since,
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fun filterPollsByAuthors(
|
||||
authorSet: AuthorsTopNavPerRelayFilterSet,
|
||||
since: SincePerRelayMap?,
|
||||
defaultSince: Long? = null,
|
||||
): List<RelayBasedFilter> {
|
||||
if (authorSet.set.isEmpty()) return emptyList()
|
||||
|
||||
return authorSet.set
|
||||
.mapNotNull {
|
||||
if (it.value.authors.isEmpty()) {
|
||||
null
|
||||
} else {
|
||||
filterPollsByAuthors(
|
||||
relay = it.key,
|
||||
authors = it.value.authors,
|
||||
since = since?.get(it.key)?.time ?: defaultSince,
|
||||
)
|
||||
}
|
||||
}.flatten()
|
||||
}
|
||||
|
||||
fun filterPollsByMutedAuthors(
|
||||
authorSet: MutedAuthorsTopNavPerRelayFilterSet,
|
||||
since: SincePerRelayMap?,
|
||||
defaultSince: Long? = null,
|
||||
): List<RelayBasedFilter> {
|
||||
if (authorSet.set.isEmpty()) return emptyList()
|
||||
|
||||
return authorSet.set
|
||||
.mapNotNull {
|
||||
if (it.value.authors.isEmpty()) {
|
||||
null
|
||||
} else {
|
||||
filterPollsByAuthors(
|
||||
relay = it.key,
|
||||
authors = it.value.authors,
|
||||
since = since?.get(it.key)?.time ?: defaultSince,
|
||||
)
|
||||
}
|
||||
}.flatten()
|
||||
}
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
/*
|
||||
* Copyright (c) 2025 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.amethyst.ui.screen.loggedIn.polls.datasource.subassemblies
|
||||
|
||||
import com.vitorpamplona.amethyst.model.topNavFeeds.allFollows.AllFollowsTopNavPerRelayFilterSet
|
||||
import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter
|
||||
|
||||
fun filterPollsByFollows(
|
||||
followsSet: AllFollowsTopNavPerRelayFilterSet,
|
||||
since: SincePerRelayMap?,
|
||||
defaultSince: Long? = null,
|
||||
): List<RelayBasedFilter> {
|
||||
if (followsSet.set.isEmpty()) return emptyList()
|
||||
|
||||
return followsSet.set.flatMap {
|
||||
val since = since?.get(it.key)?.time ?: defaultSince
|
||||
val relay = it.key
|
||||
|
||||
listOfNotNull(
|
||||
it.value.authors?.let {
|
||||
filterPollsByAuthors(relay, it, since)
|
||||
},
|
||||
).flatten()
|
||||
}
|
||||
}
|
||||
+67
@@ -0,0 +1,67 @@
|
||||
/*
|
||||
* Copyright (c) 2025 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.amethyst.ui.screen.loggedIn.polls.datasource.subassemblies
|
||||
|
||||
import com.vitorpamplona.amethyst.model.topNavFeeds.hashtag.HashtagTopNavPerRelayFilterSet
|
||||
import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
import com.vitorpamplona.quartz.nip88Polls.poll.PollEvent
|
||||
|
||||
fun filterPollsByHashtag(
|
||||
relay: NormalizedRelayUrl,
|
||||
hashtags: Set<String>,
|
||||
since: Long? = null,
|
||||
): List<RelayBasedFilter> =
|
||||
listOf(
|
||||
RelayBasedFilter(
|
||||
relay = relay,
|
||||
filter =
|
||||
Filter(
|
||||
kinds = listOf(PollEvent.KIND),
|
||||
tags = mapOf("t" to hashtags.toList()),
|
||||
limit = 200,
|
||||
since = since,
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
fun filterPollsByHashtag(
|
||||
hashtagSet: HashtagTopNavPerRelayFilterSet,
|
||||
since: SincePerRelayMap?,
|
||||
defaultSince: Long? = null,
|
||||
): List<RelayBasedFilter> {
|
||||
if (hashtagSet.set.isEmpty()) return emptyList()
|
||||
|
||||
return hashtagSet.set
|
||||
.mapNotNull { relayHashSet ->
|
||||
if (relayHashSet.value.hashtags.isEmpty()) {
|
||||
null
|
||||
} else {
|
||||
filterPollsByHashtag(
|
||||
relay = relayHashSet.key,
|
||||
hashtags = relayHashSet.value.hashtags,
|
||||
since = since?.get(relayHashSet.key)?.time ?: defaultSince,
|
||||
)
|
||||
}
|
||||
}.flatten()
|
||||
}
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
/*
|
||||
* Copyright (c) 2025 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.amethyst.ui.screen.loggedIn.polls.datasource.subassemblies
|
||||
|
||||
import com.vitorpamplona.amethyst.model.topNavFeeds.global.GlobalTopNavPerRelayFilterSet
|
||||
import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
|
||||
import com.vitorpamplona.quartz.nip88Polls.poll.PollEvent
|
||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||
|
||||
fun filterPollsGlobal(
|
||||
relays: GlobalTopNavPerRelayFilterSet,
|
||||
since: SincePerRelayMap?,
|
||||
defaultSince: Long? = null,
|
||||
): List<RelayBasedFilter> {
|
||||
if (relays.set.isEmpty()) return emptyList()
|
||||
|
||||
return relays.set.map {
|
||||
val since = since?.get(it.key)?.time ?: defaultSince ?: TimeUtils.oneWeekAgo()
|
||||
RelayBasedFilter(
|
||||
relay = it.key,
|
||||
filter =
|
||||
Filter(
|
||||
kinds = listOf(PollEvent.KIND),
|
||||
limit = 200,
|
||||
since = since,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -411,6 +411,7 @@
|
||||
<string name="bookmarks_title">Default Bookmarks</string>
|
||||
<string name="bookmarks_explainer">Your default Bookmarks that many clients support</string>
|
||||
<string name="drafts">Drafts</string>
|
||||
<string name="polls">Polls</string>
|
||||
<string name="private_bookmarks">Private Bookmarks</string>
|
||||
<string name="public_bookmarks">Public Bookmarks</string>
|
||||
<string name="add_to_private_bookmarks">Add to Private Bookmarks</string>
|
||||
|
||||
+57
-11
@@ -75,6 +75,8 @@ kotlin {
|
||||
}
|
||||
}
|
||||
|
||||
linuxX64()
|
||||
|
||||
// This makes sure that the resource file directory is visible for iOS tests.
|
||||
val rootDir = "${rootProject.rootDir.path}/quartz/src/commonTest/resources"
|
||||
|
||||
@@ -222,15 +224,37 @@ kotlin {
|
||||
}
|
||||
}
|
||||
|
||||
iosMain {
|
||||
dependsOn(commonMain.get())
|
||||
dependencies {
|
||||
implementation(libs.charlietap.cachemap)
|
||||
implementation(libs.net.thauvin.erik.urlencoder.lib)
|
||||
implementation(libs.dev.whyoleg.cryptography.provider.apple.optimal)
|
||||
implementation("io.github.andreypfau:kotlinx-crypto-hmac:0.0.4")
|
||||
implementation("io.github.andreypfau:kotlinx-crypto-sha2:0.0.4")
|
||||
// Must be defined before appleMain, linuxMain, etc.
|
||||
val nativeMain =
|
||||
create("nativeMain") {
|
||||
dependsOn(commonMain.get())
|
||||
}
|
||||
|
||||
val nativeTest =
|
||||
create("nativeTest") {
|
||||
dependsOn(commonTest.get())
|
||||
}
|
||||
|
||||
// Must be defined before iosMain and macosMain
|
||||
val appleMain =
|
||||
create("appleMain") {
|
||||
dependsOn(nativeMain)
|
||||
dependencies {
|
||||
implementation(libs.charlietap.cachemap)
|
||||
implementation(libs.net.thauvin.erik.urlencoder.lib)
|
||||
implementation(libs.dev.whyoleg.cryptography.provider.apple.optimal)
|
||||
implementation("io.github.andreypfau:kotlinx-crypto-hmac:0.0.4")
|
||||
implementation("io.github.andreypfau:kotlinx-crypto-sha2:0.0.4")
|
||||
}
|
||||
}
|
||||
|
||||
val appleTest =
|
||||
create("appleTest") {
|
||||
dependsOn(nativeTest)
|
||||
}
|
||||
|
||||
iosMain {
|
||||
dependsOn(appleMain)
|
||||
}
|
||||
|
||||
val iosArm64Main by getting {
|
||||
@@ -242,9 +266,7 @@ kotlin {
|
||||
}
|
||||
|
||||
iosTest {
|
||||
dependsOn(commonTest.get())
|
||||
dependencies {
|
||||
}
|
||||
dependsOn(appleTest)
|
||||
}
|
||||
|
||||
val iosArm64Test by getting {
|
||||
@@ -254,6 +276,30 @@ kotlin {
|
||||
val iosSimulatorArm64Test by getting {
|
||||
dependsOn(iosTest.get())
|
||||
}
|
||||
|
||||
val linuxMain =
|
||||
create("linuxMain") {
|
||||
dependsOn(nativeMain)
|
||||
dependencies {
|
||||
implementation(libs.net.thauvin.erik.urlencoder.lib)
|
||||
implementation(libs.dev.whyoleg.cryptography.provider.apple.optimal)
|
||||
implementation("io.github.andreypfau:kotlinx-crypto-hmac:0.0.4")
|
||||
implementation("io.github.andreypfau:kotlinx-crypto-sha2:0.0.4")
|
||||
}
|
||||
}
|
||||
|
||||
val linuxTest =
|
||||
create("linuxTest") {
|
||||
dependsOn(nativeTest)
|
||||
}
|
||||
|
||||
val linuxX64Main by getting {
|
||||
dependsOn(linuxMain)
|
||||
}
|
||||
|
||||
val linuxX64Test by getting {
|
||||
dependsOn(linuxTest)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -34,6 +34,7 @@ import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerInternal
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.aTag.ATag
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.events.ETag
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.kinds.KindTag
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.people.PTag
|
||||
import com.vitorpamplona.quartz.nip31Alts.AltTag
|
||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||
@@ -101,6 +102,7 @@ class LnZapRequestEvent(
|
||||
arrayOf("e", zappedEvent.id),
|
||||
arrayOf("p", toUserPubHex ?: zappedEvent.pubKey),
|
||||
arrayOf("relays") + relays.map { it.url },
|
||||
KindTag.assemble(zappedEvent.kind),
|
||||
AltTag.assemble(ALT),
|
||||
)
|
||||
if (zappedEvent is AddressableEvent) {
|
||||
|
||||
+3
@@ -83,6 +83,7 @@ class PublicMessageEvent(
|
||||
) = eventTemplate(KIND, "", createdAt) {
|
||||
alt(ALT_DESCRIPTION)
|
||||
initializer()
|
||||
remove("e") // NIP-A4: e tags must not be used
|
||||
}
|
||||
|
||||
fun build(
|
||||
@@ -94,6 +95,7 @@ class PublicMessageEvent(
|
||||
alt(ALT_DESCRIPTION)
|
||||
toUser(to)
|
||||
initializer()
|
||||
remove("e") // NIP-A4: e tags must not be used
|
||||
}
|
||||
|
||||
fun build(
|
||||
@@ -105,6 +107,7 @@ class PublicMessageEvent(
|
||||
alt(ALT_DESCRIPTION)
|
||||
toGroup(to)
|
||||
initializer()
|
||||
remove("e") // NIP-A4: e tags must not be used
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+119
@@ -0,0 +1,119 @@
|
||||
/*
|
||||
* Copyright (c) 2025 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.nip57Zaps
|
||||
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerInternal
|
||||
import com.vitorpamplona.quartz.nipA4PublicMessages.PublicMessageEvent
|
||||
import com.vitorpamplona.quartz.nipA4PublicMessages.tags.ReceiverTag
|
||||
import com.vitorpamplona.quartz.utils.DeterministicSigner
|
||||
import com.vitorpamplona.quartz.utils.nsecToKeyPair
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
class LnZapRequestKindTagTest {
|
||||
private val signer =
|
||||
DeterministicSigner(
|
||||
"nsec10g0wheggqn9dawlc0yuv6adnat6n09anr7eyykevw2dm8xa5fffs0wsdsr".nsecToKeyPair(),
|
||||
)
|
||||
|
||||
private val nostrSigner = NostrSignerInternal(signer.key)
|
||||
|
||||
private val receiverPubKey = "3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d"
|
||||
|
||||
@Test
|
||||
fun `zap request for kind 24 event includes k tag`() =
|
||||
runTest {
|
||||
val receiver = ReceiverTag(receiverPubKey, null)
|
||||
val template = PublicMessageEvent.build(receiver, "Test message")
|
||||
val publicMsg = signer.sign<PublicMessageEvent>(template)
|
||||
|
||||
val relays = setOf(NormalizedRelayUrl("wss://relay.example.com/"))
|
||||
|
||||
val zapRequest =
|
||||
LnZapRequestEvent.create(
|
||||
zappedEvent = publicMsg,
|
||||
relays = relays,
|
||||
signer = nostrSigner,
|
||||
pollOption = null,
|
||||
message = "",
|
||||
zapType = LnZapEvent.ZapType.PUBLIC,
|
||||
toUserPubHex = null,
|
||||
)
|
||||
|
||||
val kTag = zapRequest.tags.firstOrNull { it[0] == "k" }
|
||||
assertTrue(kTag != null, "Zap request must include k tag per NIP-A4")
|
||||
assertEquals("24", kTag[1], "k tag must contain the zapped event's kind")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `zap request for kind 1 event includes k tag`() =
|
||||
runTest {
|
||||
val kind1Event =
|
||||
Event(
|
||||
id = "a".repeat(64),
|
||||
pubKey = receiverPubKey,
|
||||
createdAt = 1000L,
|
||||
kind = 1,
|
||||
tags = emptyArray(),
|
||||
content = "Hello world",
|
||||
sig = "b".repeat(128),
|
||||
)
|
||||
|
||||
val relays = setOf(NormalizedRelayUrl("wss://relay.example.com/"))
|
||||
|
||||
val zapRequest =
|
||||
LnZapRequestEvent.create(
|
||||
zappedEvent = kind1Event,
|
||||
relays = relays,
|
||||
signer = nostrSigner,
|
||||
pollOption = null,
|
||||
message = "",
|
||||
zapType = LnZapEvent.ZapType.PUBLIC,
|
||||
toUserPubHex = null,
|
||||
)
|
||||
|
||||
val kTag = zapRequest.tags.firstOrNull { it[0] == "k" }
|
||||
assertTrue(kTag != null, "Zap request should include k tag")
|
||||
assertEquals("1", kTag[1], "k tag should contain the zapped event's kind")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `zap request for user only has no k tag`() =
|
||||
runTest {
|
||||
val relays = setOf(NormalizedRelayUrl("wss://relay.example.com/"))
|
||||
|
||||
val zapRequest =
|
||||
LnZapRequestEvent.create(
|
||||
userHex = receiverPubKey,
|
||||
relays = relays,
|
||||
signer = nostrSigner,
|
||||
message = "",
|
||||
zapType = LnZapEvent.ZapType.PUBLIC,
|
||||
)
|
||||
|
||||
val kTag = zapRequest.tags.firstOrNull { it[0] == "k" }
|
||||
assertEquals(null, kTag, "User-only zap should not have k tag")
|
||||
}
|
||||
}
|
||||
+184
@@ -0,0 +1,184 @@
|
||||
/*
|
||||
* Copyright (c) 2025 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.nipA4PublicMessages
|
||||
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.events.ETag
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.events.eTag
|
||||
import com.vitorpamplona.quartz.nipA4PublicMessages.tags.ReceiverTag
|
||||
import com.vitorpamplona.quartz.utils.DeterministicSigner
|
||||
import com.vitorpamplona.quartz.utils.nsecToKeyPair
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertFalse
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
class PublicMessageEventTest {
|
||||
private val signer =
|
||||
DeterministicSigner(
|
||||
"nsec10g0wheggqn9dawlc0yuv6adnat6n09anr7eyykevw2dm8xa5fffs0wsdsr".nsecToKeyPair(),
|
||||
)
|
||||
|
||||
private val receiverPubKey = "3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d"
|
||||
private val receiverRelay = "wss://relay.example.com/"
|
||||
|
||||
@Test
|
||||
fun `verify event kind is 24`() {
|
||||
assertEquals(24, PublicMessageEvent.KIND)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `verify alt description`() {
|
||||
assertEquals("Public Message", PublicMessageEvent.ALT_DESCRIPTION)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `build with single receiver`() {
|
||||
val receiver = ReceiverTag(receiverPubKey, null)
|
||||
val template = PublicMessageEvent.build(receiver, "Hello!")
|
||||
val event = signer.sign<PublicMessageEvent>(template)
|
||||
|
||||
assertEquals(24, event.kind)
|
||||
assertEquals("Hello!", event.content)
|
||||
assertTrue(event.tags.any { it[0] == "p" && it[1] == receiverPubKey })
|
||||
assertTrue(event.tags.any { it[0] == "alt" && it[1] == "Public Message" })
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `build with multiple receivers`() {
|
||||
val receiver1 = ReceiverTag(receiverPubKey, null)
|
||||
val receiver2PubKey = "32e1827635450ebb3c5a7d12c1f8e7b2b514439ac10a67eef3d9fd9c5c68e245"
|
||||
val receiver2 = ReceiverTag(receiver2PubKey, null)
|
||||
|
||||
val template = PublicMessageEvent.build(listOf(receiver1, receiver2), "Group message")
|
||||
val event = signer.sign<PublicMessageEvent>(template)
|
||||
|
||||
assertEquals("Group message", event.content)
|
||||
val pTags = event.tags.filter { it[0] == "p" }
|
||||
assertEquals(2, pTags.size)
|
||||
assertTrue(pTags.any { it[1] == receiverPubKey })
|
||||
assertTrue(pTags.any { it[1] == receiver2PubKey })
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `build with relay hint`() {
|
||||
val receiver = ReceiverTag.parse(arrayOf("p", receiverPubKey, "wss://relay.example.com/"))!!
|
||||
val template = PublicMessageEvent.build(receiver, "With relay hint")
|
||||
val event = signer.sign<PublicMessageEvent>(template)
|
||||
|
||||
val pTag = event.tags.first { it[0] == "p" && it[1] == receiverPubKey }
|
||||
assertEquals("wss://relay.example.com/", pTag[2])
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `e tags are stripped from build output`() {
|
||||
val receiver = ReceiverTag(receiverPubKey, null)
|
||||
val fakeEventId = "a".repeat(64)
|
||||
|
||||
val template =
|
||||
PublicMessageEvent.build(receiver, "Test message") {
|
||||
eTag(ETag(fakeEventId, null))
|
||||
}
|
||||
val event = signer.sign<PublicMessageEvent>(template)
|
||||
|
||||
assertFalse(
|
||||
event.tags.any { it[0] == "e" },
|
||||
"NIP-A4 prohibits e tags in kind 24 events",
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `e tags are stripped from empty build`() {
|
||||
val fakeEventId = "b".repeat(64)
|
||||
|
||||
val template =
|
||||
PublicMessageEvent.build {
|
||||
eTag(ETag(fakeEventId, null))
|
||||
}
|
||||
val event = signer.sign<PublicMessageEvent>(template)
|
||||
|
||||
assertFalse(
|
||||
event.tags.any { it[0] == "e" },
|
||||
"NIP-A4 prohibits e tags in kind 24 events",
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `e tags are stripped from group build`() {
|
||||
val receiver = ReceiverTag(receiverPubKey, null)
|
||||
val fakeEventId = "c".repeat(64)
|
||||
|
||||
val template =
|
||||
PublicMessageEvent.build(listOf(receiver), "Group test") {
|
||||
eTag(ETag(fakeEventId, null))
|
||||
}
|
||||
val event = signer.sign<PublicMessageEvent>(template)
|
||||
|
||||
assertFalse(
|
||||
event.tags.any { it[0] == "e" },
|
||||
"NIP-A4 prohibits e tags in kind 24 events",
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `isIncluded checks receivers and author`() {
|
||||
val receiver = ReceiverTag(receiverPubKey, null)
|
||||
val template = PublicMessageEvent.build(receiver, "Test")
|
||||
val event = signer.sign<PublicMessageEvent>(template)
|
||||
|
||||
assertTrue(event.isIncluded(receiverPubKey))
|
||||
assertTrue(event.isIncluded(event.pubKey))
|
||||
assertFalse(event.isIncluded("0".repeat(64)))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `groupKeys includes author and receivers`() {
|
||||
val receiver = ReceiverTag(receiverPubKey, null)
|
||||
val template = PublicMessageEvent.build(receiver, "Test")
|
||||
val event = signer.sign<PublicMessageEvent>(template)
|
||||
|
||||
val keys = event.groupKeys()
|
||||
assertTrue(keys.contains(event.pubKey))
|
||||
assertTrue(keys.contains(receiverPubKey))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `groupKeySet returns unique keys`() {
|
||||
val receiver = ReceiverTag(receiverPubKey, null)
|
||||
val template = PublicMessageEvent.build(receiver, "Test")
|
||||
val event = signer.sign<PublicMessageEvent>(template)
|
||||
|
||||
val keySet = event.groupKeySet()
|
||||
assertEquals(2, keySet.size)
|
||||
assertTrue(keySet.contains(event.pubKey))
|
||||
assertTrue(keySet.contains(receiverPubKey))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `chatroomKey excludes given user`() {
|
||||
val receiver = ReceiverTag(receiverPubKey, null)
|
||||
val template = PublicMessageEvent.build(receiver, "Test")
|
||||
val event = signer.sign<PublicMessageEvent>(template)
|
||||
|
||||
val chatroomKey = event.chatroomKey(event.pubKey)
|
||||
assertFalse(chatroomKey.contains(event.pubKey))
|
||||
assertTrue(chatroomKey.contains(receiverPubKey))
|
||||
}
|
||||
}
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
/*
|
||||
* Copyright (c) 2025 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.nip96FileStorage.info
|
||||
|
||||
actual fun makeAbsoluteIfRelativeUrl(
|
||||
baseUrl: String,
|
||||
potentiallyRelativeUrl: String,
|
||||
): String =
|
||||
try {
|
||||
if (potentiallyRelativeUrl.contains("://")) {
|
||||
potentiallyRelativeUrl
|
||||
} else {
|
||||
val base = if (baseUrl.endsWith("/")) baseUrl else "$baseUrl/"
|
||||
if (potentiallyRelativeUrl.startsWith("/")) {
|
||||
// Absolute path - combine with scheme + host from base
|
||||
val schemeEnd = base.indexOf("://")
|
||||
if (schemeEnd >= 0) {
|
||||
val hostEnd = base.indexOf('/', schemeEnd + 3)
|
||||
if (hostEnd >= 0) {
|
||||
base.substring(0, hostEnd) + potentiallyRelativeUrl
|
||||
} else {
|
||||
base + potentiallyRelativeUrl.removePrefix("/")
|
||||
}
|
||||
} else {
|
||||
potentiallyRelativeUrl
|
||||
}
|
||||
} else {
|
||||
// Relative path
|
||||
base + potentiallyRelativeUrl
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
potentiallyRelativeUrl
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
/*
|
||||
* Copyright (c) 2025 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
|
||||
|
||||
actual class BigDecimal actual constructor(
|
||||
strVal: String,
|
||||
) : Number() {
|
||||
private val value: String = strVal.trim()
|
||||
|
||||
actual constructor(doubleVal: Double) : this(doubleVal.toBigDecimalString())
|
||||
|
||||
actual constructor(intVal: Int) : this(intVal.toString())
|
||||
|
||||
actual constructor(longVal: Long) : this(longVal.toString())
|
||||
|
||||
actual fun add(augend: BigDecimal): BigDecimal = BigDecimal((toDouble() + augend.toDouble()).toBigDecimalString())
|
||||
|
||||
actual fun subtract(subtrahend: BigDecimal): BigDecimal = BigDecimal((toDouble() - subtrahend.toDouble()).toBigDecimalString())
|
||||
|
||||
actual fun multiply(multiplicand: BigDecimal): BigDecimal = BigDecimal((toDouble() * multiplicand.toDouble()).toBigDecimalString())
|
||||
|
||||
actual fun divide(divisor: BigDecimal): BigDecimal = BigDecimal((toDouble() / divisor.toDouble()).toBigDecimalString())
|
||||
|
||||
actual fun negate(): BigDecimal = BigDecimal((-toDouble()).toBigDecimalString())
|
||||
|
||||
actual fun signum(): Int {
|
||||
val d = toDouble()
|
||||
return when {
|
||||
d < 0.0 -> -1
|
||||
d > 0.0 -> 1
|
||||
else -> 0
|
||||
}
|
||||
}
|
||||
|
||||
override fun toInt(): Int = toDouble().toInt()
|
||||
|
||||
override fun toLong(): Long = toDouble().toLong()
|
||||
|
||||
override fun toShort(): Short = toDouble().toInt().toShort()
|
||||
|
||||
override fun toByte(): Byte = toDouble().toInt().toByte()
|
||||
|
||||
override fun toDouble(): Double = value.toDouble()
|
||||
|
||||
override fun toFloat(): Float = value.toFloat()
|
||||
|
||||
override fun equals(other: Any?): Boolean = (this === other) || (other is BigDecimal && value == other.value)
|
||||
|
||||
override fun hashCode(): Int = value.hashCode()
|
||||
|
||||
override fun toString(): String = value
|
||||
}
|
||||
|
||||
private fun Double.toBigDecimalString(): String {
|
||||
if (this == this.toLong().toDouble()) {
|
||||
return this.toLong().toString()
|
||||
}
|
||||
return this.toString()
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
/*
|
||||
* Copyright (c) 2025 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 kotlinx.cinterop.ExperimentalForeignApi
|
||||
import kotlinx.cinterop.addressOf
|
||||
import kotlinx.cinterop.alloc
|
||||
import kotlinx.cinterop.memScoped
|
||||
import kotlinx.cinterop.ptr
|
||||
import kotlinx.cinterop.reinterpret
|
||||
import kotlinx.cinterop.usePinned
|
||||
import platform.zlib.Z_DEFAULT_COMPRESSION
|
||||
import platform.zlib.Z_DEFAULT_STRATEGY
|
||||
import platform.zlib.Z_DEFLATED
|
||||
import platform.zlib.Z_FINISH
|
||||
import platform.zlib.Z_NO_FLUSH
|
||||
import platform.zlib.Z_OK
|
||||
import platform.zlib.Z_STREAM_END
|
||||
import platform.zlib.deflate
|
||||
import platform.zlib.deflateBound
|
||||
import platform.zlib.deflateEnd
|
||||
import platform.zlib.deflateInit2
|
||||
import platform.zlib.inflate
|
||||
import platform.zlib.inflateEnd
|
||||
import platform.zlib.inflateInit2
|
||||
import platform.zlib.z_stream
|
||||
|
||||
@OptIn(ExperimentalForeignApi::class)
|
||||
actual object GZip {
|
||||
actual fun compress(content: String): ByteArray {
|
||||
val input = content.encodeToByteArray()
|
||||
|
||||
return memScoped {
|
||||
val stream = alloc<z_stream>()
|
||||
|
||||
deflateInit2(
|
||||
stream.ptr,
|
||||
Z_DEFAULT_COMPRESSION,
|
||||
Z_DEFLATED,
|
||||
31,
|
||||
8,
|
||||
Z_DEFAULT_STRATEGY,
|
||||
).let { check(it == Z_OK) { "deflateInit2 failed: $it" } }
|
||||
|
||||
val maxSize = deflateBound(stream.ptr, input.size.toULong()).toInt()
|
||||
val output = ByteArray(maxSize)
|
||||
|
||||
val written =
|
||||
input.usePinned { pinIn ->
|
||||
output.usePinned { pinOut ->
|
||||
if (input.isNotEmpty()) {
|
||||
stream.next_in = pinIn.addressOf(0).reinterpret()
|
||||
}
|
||||
stream.avail_in = input.size.toUInt()
|
||||
|
||||
if (output.isNotEmpty()) {
|
||||
stream.next_out = pinOut.addressOf(0).reinterpret()
|
||||
}
|
||||
stream.avail_out = maxSize.toUInt()
|
||||
|
||||
deflate(stream.ptr, Z_FINISH)
|
||||
.let { check(it == Z_STREAM_END) { "deflate failed: $it" } }
|
||||
|
||||
maxSize - stream.avail_out.toInt()
|
||||
}
|
||||
}
|
||||
|
||||
deflateEnd(stream.ptr)
|
||||
output.copyOf(written)
|
||||
}
|
||||
}
|
||||
|
||||
actual fun decompress(content: ByteArray): String {
|
||||
if (content.isEmpty()) return ""
|
||||
|
||||
val chunks = ArrayList<ByteArray>()
|
||||
val chunkSize = maxOf(content.size * 4, 4096)
|
||||
|
||||
memScoped {
|
||||
val stream = alloc<z_stream>()
|
||||
|
||||
inflateInit2(stream.ptr, 47)
|
||||
.let { check(it == Z_OK) { "inflateInit2 failed: $it" } }
|
||||
|
||||
content.usePinned { pinIn ->
|
||||
if (content.isNotEmpty()) {
|
||||
stream.next_in = pinIn.addressOf(0).reinterpret()
|
||||
}
|
||||
stream.avail_in = content.size.toUInt()
|
||||
|
||||
var status: Int = Z_OK
|
||||
do {
|
||||
val chunk = ByteArray(chunkSize)
|
||||
chunk.usePinned { pinOut ->
|
||||
stream.next_out = pinOut.addressOf(0).reinterpret()
|
||||
stream.avail_out = chunkSize.toUInt()
|
||||
status = inflate(stream.ptr, Z_NO_FLUSH)
|
||||
val produced = chunkSize - stream.avail_out.toInt()
|
||||
if (produced > 0) chunks.add(chunk.copyOf(produced))
|
||||
}
|
||||
} while (status == Z_OK)
|
||||
|
||||
check(status == Z_STREAM_END) { "inflate failed: $status" }
|
||||
}
|
||||
|
||||
inflateEnd(stream.ptr)
|
||||
}
|
||||
|
||||
val totalSize = chunks.sumOf { it.size }
|
||||
val result = ByteArray(totalSize)
|
||||
var pos = 0
|
||||
chunks.forEach { chunk ->
|
||||
chunk.copyInto(result, pos)
|
||||
pos += chunk.size
|
||||
}
|
||||
return result.decodeToString()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
/*
|
||||
* Copyright (c) 2025 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
|
||||
|
||||
actual object Log {
|
||||
actual fun w(
|
||||
tag: String,
|
||||
message: String,
|
||||
throwable: Throwable?,
|
||||
) {
|
||||
if (throwable != null) {
|
||||
println("WARN: [$tag] $message. Throwable: $throwable CAUSE ${throwable.cause}")
|
||||
} else {
|
||||
println("WARN: [$tag] $message")
|
||||
}
|
||||
}
|
||||
|
||||
actual fun e(
|
||||
tag: String,
|
||||
message: String,
|
||||
throwable: Throwable?,
|
||||
) {
|
||||
if (throwable != null) {
|
||||
println("ERROR: [$tag] $message. Throwable: $throwable CAUSE ${throwable.cause}")
|
||||
} else {
|
||||
println("ERROR: [$tag] $message")
|
||||
}
|
||||
}
|
||||
|
||||
actual fun d(
|
||||
tag: String,
|
||||
message: String,
|
||||
) {
|
||||
println("DEBUG: [$tag] $message")
|
||||
}
|
||||
|
||||
actual fun i(
|
||||
tag: String,
|
||||
message: String,
|
||||
) {
|
||||
println("INFO: [$tag] $message")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
* Copyright (c) 2025 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 kotlinx.cinterop.ExperimentalForeignApi
|
||||
import kotlinx.cinterop.alloc
|
||||
import kotlinx.cinterop.memScoped
|
||||
import kotlinx.cinterop.ptr
|
||||
import platform.posix.CLOCK_REALTIME
|
||||
import platform.posix.clock_gettime
|
||||
import platform.posix.timespec
|
||||
|
||||
actual fun platform() = "Linux"
|
||||
|
||||
@OptIn(ExperimentalForeignApi::class)
|
||||
actual fun currentTimeSeconds(): Long {
|
||||
memScoped {
|
||||
val ts = alloc<timespec>()
|
||||
clock_gettime(CLOCK_REALTIME, ts.ptr)
|
||||
return ts.tv_sec
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
/*
|
||||
* Copyright (c) 2025 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 kotlinx.cinterop.ExperimentalForeignApi
|
||||
import kotlinx.cinterop.addressOf
|
||||
import kotlinx.cinterop.convert
|
||||
import kotlinx.cinterop.usePinned
|
||||
import platform.posix.O_RDONLY
|
||||
import platform.posix.close
|
||||
import platform.posix.open
|
||||
import platform.posix.read
|
||||
|
||||
actual class SecureRandom {
|
||||
actual fun nextInt(): Int {
|
||||
val bytes = ByteArray(4)
|
||||
nextBytes(bytes)
|
||||
return ((bytes[0].toInt() and 0xFF) shl 24) or
|
||||
((bytes[1].toInt() and 0xFF) shl 16) or
|
||||
((bytes[2].toInt() and 0xFF) shl 8) or
|
||||
(bytes[3].toInt() and 0xFF)
|
||||
}
|
||||
|
||||
actual fun nextInt(bound: Int): Int {
|
||||
require(bound > 0) { throw IllegalArgumentException("Bad Bound $bound") }
|
||||
|
||||
var intValue = nextPositiveInt()
|
||||
|
||||
val m = bound - 1
|
||||
if ((bound and m) == 0) {
|
||||
intValue = ((bound * intValue.toLong()) shr 31).toInt()
|
||||
} else {
|
||||
var u: Int = intValue
|
||||
intValue = u % bound
|
||||
while (u - intValue + m < 0) {
|
||||
u = nextPositiveInt()
|
||||
intValue = u % bound
|
||||
}
|
||||
}
|
||||
|
||||
return intValue
|
||||
}
|
||||
|
||||
actual fun nextLong(): Long {
|
||||
val bytes = ByteArray(8)
|
||||
nextBytes(bytes)
|
||||
return ((bytes[0].toLong() and 0xFFL) shl 56) or
|
||||
((bytes[1].toLong() and 0xFFL) shl 48) or
|
||||
((bytes[2].toLong() and 0xFFL) shl 40) or
|
||||
((bytes[3].toLong() and 0xFFL) shl 32) or
|
||||
((bytes[4].toLong() and 0xFFL) shl 24) or
|
||||
((bytes[5].toLong() and 0xFFL) shl 16) or
|
||||
((bytes[6].toLong() and 0xFFL) shl 8) or
|
||||
(bytes[7].toLong() and 0xFFL)
|
||||
}
|
||||
|
||||
actual fun nextLong(bound: Long): Long {
|
||||
require(bound > 0) { throw IllegalArgumentException("Bad Bound $bound") }
|
||||
|
||||
if (bound < Int.MAX_VALUE) {
|
||||
return nextInt(bound.toInt()).toLong()
|
||||
}
|
||||
|
||||
return nextPositiveLong() % bound
|
||||
}
|
||||
|
||||
fun nextPositiveInt(): Int {
|
||||
val value = nextInt()
|
||||
return if (value > 0) {
|
||||
value
|
||||
} else {
|
||||
-value
|
||||
}
|
||||
}
|
||||
|
||||
fun nextPositiveLong(): Long {
|
||||
val value = nextLong()
|
||||
return if (value > 0) {
|
||||
value
|
||||
} else {
|
||||
-value
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalForeignApi::class)
|
||||
actual fun nextBytes(output: ByteArray) {
|
||||
if (output.isEmpty()) return
|
||||
val fd = open("/dev/urandom", O_RDONLY)
|
||||
if (fd == -1) {
|
||||
throw IllegalStateException("Failed to open /dev/urandom")
|
||||
}
|
||||
try {
|
||||
output.usePinned { pinned ->
|
||||
var bytesRead = 0
|
||||
while (bytesRead < output.size) {
|
||||
val result = read(fd, pinned.addressOf(bytesRead), (output.size - bytesRead).convert())
|
||||
if (result <= 0) {
|
||||
throw IllegalStateException("Failed to read from /dev/urandom")
|
||||
}
|
||||
bytesRead += result.toInt()
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
close(fd)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
/*
|
||||
* Copyright (c) 2025 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
|
||||
|
||||
actual class UnicodeNormalizer {
|
||||
actual fun normalizeNFKC(input: String): String {
|
||||
// NFKC normalization on Linux native without ICU
|
||||
// For most Nostr use cases (NIP-05, etc.) ASCII-safe content works without normalization.
|
||||
// Full NFKC support would require linking against ICU or a pure-Kotlin normalizer.
|
||||
return input
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,161 @@
|
||||
/*
|
||||
* Copyright (c) 2025 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
|
||||
|
||||
actual class UriParser actual constructor(
|
||||
uri: String,
|
||||
) {
|
||||
private val parsedScheme: String?
|
||||
private val parsedHost: String?
|
||||
private val parsedPort: Int?
|
||||
private val parsedPath: String?
|
||||
private val parsedQuery: String?
|
||||
private val parsedFragment: String?
|
||||
|
||||
init {
|
||||
var remaining = uri
|
||||
|
||||
// Parse scheme
|
||||
val schemeEnd = remaining.indexOf("://")
|
||||
if (schemeEnd >= 0) {
|
||||
parsedScheme = remaining.substring(0, schemeEnd)
|
||||
remaining = remaining.substring(schemeEnd + 3)
|
||||
} else {
|
||||
parsedScheme = null
|
||||
}
|
||||
|
||||
// Parse fragment
|
||||
val fragmentStart = remaining.indexOf('#')
|
||||
if (fragmentStart >= 0) {
|
||||
parsedFragment = remaining.substring(fragmentStart + 1)
|
||||
remaining = remaining.substring(0, fragmentStart)
|
||||
} else {
|
||||
parsedFragment = null
|
||||
}
|
||||
|
||||
// Parse query
|
||||
val queryStart = remaining.indexOf('?')
|
||||
if (queryStart >= 0) {
|
||||
parsedQuery = remaining.substring(queryStart + 1)
|
||||
remaining = remaining.substring(0, queryStart)
|
||||
} else {
|
||||
parsedQuery = null
|
||||
}
|
||||
|
||||
// Parse host and port
|
||||
val pathStart = remaining.indexOf('/')
|
||||
val authority =
|
||||
if (pathStart >= 0) {
|
||||
val auth = remaining.substring(0, pathStart)
|
||||
remaining = remaining.substring(pathStart)
|
||||
auth
|
||||
} else {
|
||||
val auth = remaining
|
||||
remaining = ""
|
||||
auth
|
||||
}
|
||||
|
||||
// Remove userinfo
|
||||
val atIndex = authority.lastIndexOf('@')
|
||||
val hostPort = if (atIndex >= 0) authority.substring(atIndex + 1) else authority
|
||||
|
||||
// Handle IPv6
|
||||
if (hostPort.startsWith("[")) {
|
||||
val bracketEnd = hostPort.indexOf(']')
|
||||
if (bracketEnd >= 0) {
|
||||
parsedHost = hostPort.substring(1, bracketEnd)
|
||||
val afterBracket = hostPort.substring(bracketEnd + 1)
|
||||
parsedPort =
|
||||
if (afterBracket.startsWith(":")) {
|
||||
afterBracket.substring(1).toIntOrNull()
|
||||
} else {
|
||||
null
|
||||
}
|
||||
} else {
|
||||
parsedHost = hostPort
|
||||
parsedPort = null
|
||||
}
|
||||
} else {
|
||||
val colonIndex = hostPort.lastIndexOf(':')
|
||||
if (colonIndex >= 0) {
|
||||
val potentialPort = hostPort.substring(colonIndex + 1).toIntOrNull()
|
||||
if (potentialPort != null) {
|
||||
parsedHost = hostPort.substring(0, colonIndex)
|
||||
parsedPort = potentialPort
|
||||
} else {
|
||||
parsedHost = hostPort
|
||||
parsedPort = null
|
||||
}
|
||||
} else {
|
||||
parsedHost = hostPort.ifEmpty { null }
|
||||
parsedPort = null
|
||||
}
|
||||
}
|
||||
|
||||
parsedPath = remaining.ifEmpty { null }
|
||||
}
|
||||
|
||||
actual fun scheme(): String? = parsedScheme
|
||||
|
||||
actual fun host(): String? = parsedHost
|
||||
|
||||
actual fun port(): Int? = parsedPort
|
||||
|
||||
actual fun path(): String? = parsedPath
|
||||
|
||||
actual fun queryParameterNames(): Set<String> {
|
||||
val query = parsedQuery ?: return emptySet()
|
||||
return query
|
||||
.split('&')
|
||||
.mapNotNull { param ->
|
||||
val eqIndex = param.indexOf('=')
|
||||
if (eqIndex >= 0) param.substring(0, eqIndex) else param
|
||||
}.toSet()
|
||||
}
|
||||
|
||||
actual fun getQueryParameter(param: String): String? {
|
||||
val query = parsedQuery ?: return null
|
||||
return query
|
||||
.split('&')
|
||||
.firstOrNull { part ->
|
||||
val eqIndex = part.indexOf('=')
|
||||
if (eqIndex >= 0) part.substring(0, eqIndex) == param else part == param
|
||||
}?.let { part ->
|
||||
val eqIndex = part.indexOf('=')
|
||||
if (eqIndex >= 0) part.substring(eqIndex + 1) else ""
|
||||
}
|
||||
}
|
||||
|
||||
val fragments: Map<String, String> by lazy {
|
||||
parsedFragment?.ifBlank { null }?.let { keyValuePair ->
|
||||
keyValuePair.split('&').associate { paramValue ->
|
||||
val parts = paramValue.split("=", limit = 2)
|
||||
if (parts.size == 2) {
|
||||
parts[0] to parts[1]
|
||||
} else {
|
||||
parts[0] to ""
|
||||
}
|
||||
}
|
||||
} ?: emptyMap()
|
||||
}
|
||||
|
||||
actual fun fragments(): Map<String, String> = fragments
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
/*
|
||||
* Copyright (c) 2025 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 net.thauvin.erik.urlencoder.UrlEncoderUtil
|
||||
|
||||
actual object UrlEncoder {
|
||||
actual fun encode(value: String): String = UrlEncoderUtil.encode(value)
|
||||
|
||||
actual fun decode(value: String): String = UrlEncoderUtil.decode(value)
|
||||
}
|
||||
+319
@@ -0,0 +1,319 @@
|
||||
/*
|
||||
* Copyright (c) 2025 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.cache
|
||||
|
||||
import kotlin.concurrent.AtomicReference
|
||||
|
||||
actual class LargeCache<K, V> : ICacheOperations<K, V> {
|
||||
private val mapRef = AtomicReference(LinkedHashMap<K, V>())
|
||||
|
||||
private inline fun <R> withMap(block: (LinkedHashMap<K, V>) -> R): R = block(mapRef.value)
|
||||
|
||||
private inline fun mutate(block: (LinkedHashMap<K, V>) -> Unit) {
|
||||
val copy = LinkedHashMap(mapRef.value)
|
||||
block(copy)
|
||||
mapRef.value = copy
|
||||
}
|
||||
|
||||
actual fun keys(): Set<K> = withMap { LinkedHashSet(it.keys) }
|
||||
|
||||
actual fun values(): Iterable<V> = withMap { ArrayList(it.values) }
|
||||
|
||||
actual fun get(key: K): V? = withMap { it[key] }
|
||||
|
||||
actual fun remove(key: K): V? {
|
||||
val current = mapRef.value
|
||||
val value = current[key]
|
||||
if (value != null) {
|
||||
mutate { it.remove(key) }
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
actual fun isEmpty(): Boolean = withMap { it.isEmpty() }
|
||||
|
||||
actual fun clear() {
|
||||
mapRef.value = LinkedHashMap()
|
||||
}
|
||||
|
||||
actual fun containsKey(key: K): Boolean = withMap { it.containsKey(key) }
|
||||
|
||||
actual fun put(
|
||||
key: K,
|
||||
value: V,
|
||||
) {
|
||||
mutate { it[key] = value }
|
||||
}
|
||||
|
||||
actual fun getOrCreate(
|
||||
key: K,
|
||||
builder: (K) -> V,
|
||||
): V {
|
||||
val existing = get(key)
|
||||
if (existing != null) return existing
|
||||
val newObject = builder(key)
|
||||
mutate { it[key] = newObject }
|
||||
return get(key) ?: newObject
|
||||
}
|
||||
|
||||
actual fun createIfAbsent(
|
||||
key: K,
|
||||
builder: (K) -> V,
|
||||
): Boolean {
|
||||
val existing = get(key)
|
||||
if (existing != null) return false
|
||||
val newObject = builder(key)
|
||||
mutate { it[key] = newObject }
|
||||
return get(key) != null
|
||||
}
|
||||
|
||||
actual override fun size(): Int = withMap { it.size }
|
||||
|
||||
actual override fun forEach(consumer: ICacheBiConsumer<K, V>) {
|
||||
withMap { map -> map.forEach { consumer.accept(it.key, it.value) } }
|
||||
}
|
||||
|
||||
actual override fun filter(consumer: CacheCollectors.BiFilter<K, V>): List<V> = withMap { map -> map.filter { consumer.filter(it.key, it.value) }.values.toList() }
|
||||
|
||||
actual override fun filterIntoSet(consumer: CacheCollectors.BiFilter<K, V>): Set<V> = withMap { map -> map.filter { consumer.filter(it.key, it.value) }.values.toSet() }
|
||||
|
||||
actual override fun <R> map(consumer: CacheCollectors.BiNotNullMapper<K, V, R>): List<R> = withMap { map -> map.map { consumer.map(it.key, it.value) } }
|
||||
|
||||
actual override fun <R> mapNotNull(consumer: CacheCollectors.BiMapper<K, V, R?>): List<R> = withMap { map -> map.mapNotNull { consumer.map(it.key, it.value) } }
|
||||
|
||||
actual override fun <R> mapNotNullIntoSet(consumer: CacheCollectors.BiMapper<K, V, R?>): Set<R> = mapNotNull(consumer).toSet()
|
||||
|
||||
actual override fun <R> mapFlatten(consumer: CacheCollectors.BiMapper<K, V, Collection<R>?>): List<R> = withMap { map -> map.flatMap { entry -> consumer.map(entry.key, entry.value) ?: emptyList() } }
|
||||
|
||||
actual override fun <R> mapFlattenIntoSet(consumer: CacheCollectors.BiMapper<K, V, Collection<R>?>): Set<R> = mapFlatten(consumer).toSet()
|
||||
|
||||
actual override fun maxOrNullOf(
|
||||
filter: CacheCollectors.BiFilter<K, V>,
|
||||
comparator: Comparator<V>,
|
||||
): V? =
|
||||
withMap { map ->
|
||||
var maxV: V? = null
|
||||
map.forEach {
|
||||
if (filter.filter(it.key, it.value)) {
|
||||
if (maxV == null || comparator.compare(it.value, maxV) > 0) {
|
||||
maxV = it.value
|
||||
}
|
||||
}
|
||||
}
|
||||
maxV
|
||||
}
|
||||
|
||||
actual override fun sumOf(consumer: CacheCollectors.BiSumOf<K, V>): Int =
|
||||
withMap { map ->
|
||||
var sum = 0
|
||||
map.forEach { sum += consumer.map(it.key, it.value) }
|
||||
sum
|
||||
}
|
||||
|
||||
actual override fun sumOfLong(consumer: CacheCollectors.BiSumOfLong<K, V>): Long =
|
||||
withMap { map ->
|
||||
var sum = 0L
|
||||
map.forEach { sum += consumer.map(it.key, it.value) }
|
||||
sum
|
||||
}
|
||||
|
||||
actual override fun <R> groupBy(consumer: CacheCollectors.BiNotNullMapper<K, V, R>): Map<R, List<V>> =
|
||||
withMap { map ->
|
||||
val results = HashMap<R, ArrayList<V>>()
|
||||
map.forEach {
|
||||
val group = consumer.map(it.key, it.value)
|
||||
results.getOrPut(group) { ArrayList() }.add(it.value)
|
||||
}
|
||||
results
|
||||
}
|
||||
|
||||
actual override fun <R> countByGroup(consumer: CacheCollectors.BiNotNullMapper<K, V, R>): Map<R, Int> =
|
||||
withMap { map ->
|
||||
val results = HashMap<R, Int>()
|
||||
map.forEach {
|
||||
val group = consumer.map(it.key, it.value)
|
||||
results[group] = (results[group] ?: 0) + 1
|
||||
}
|
||||
results
|
||||
}
|
||||
|
||||
actual override fun <R> sumByGroup(
|
||||
groupMap: CacheCollectors.BiNotNullMapper<K, V, R>,
|
||||
sumOf: CacheCollectors.BiNotNullMapper<K, V, Long>,
|
||||
): Map<R, Long> =
|
||||
withMap { map ->
|
||||
val results = HashMap<R, Long>()
|
||||
map.forEach {
|
||||
val group = groupMap.map(it.key, it.value)
|
||||
results[group] = (results[group] ?: 0L) + sumOf.map(it.key, it.value)
|
||||
}
|
||||
results
|
||||
}
|
||||
|
||||
actual override fun count(consumer: CacheCollectors.BiFilter<K, V>): Int = withMap { map -> map.count { consumer.filter(it.key, it.value) } }
|
||||
|
||||
actual override fun <T, U> associate(transform: (K, V) -> Pair<T, U>): Map<T, U> =
|
||||
withMap { map ->
|
||||
val results = LinkedHashMap<T, U>(map.size)
|
||||
map.forEach {
|
||||
val pair = transform(it.key, it.value)
|
||||
results[pair.first] = pair.second
|
||||
}
|
||||
results
|
||||
}
|
||||
|
||||
actual override fun <U> associateWith(transform: (K, V) -> U?): Map<K, U?> =
|
||||
withMap { map ->
|
||||
val results = LinkedHashMap<K, U?>(map.size)
|
||||
map.forEach {
|
||||
results[it.key] = transform(it.key, it.value)
|
||||
}
|
||||
results
|
||||
}
|
||||
|
||||
actual override fun filter(
|
||||
from: K,
|
||||
to: K,
|
||||
consumer: CacheCollectors.BiFilter<K, V>,
|
||||
): List<V> = filter(consumer)
|
||||
|
||||
actual override fun filterIntoSet(
|
||||
from: K,
|
||||
to: K,
|
||||
consumer: CacheCollectors.BiFilter<K, V>,
|
||||
): Set<V> = filterIntoSet(consumer)
|
||||
|
||||
actual override fun <R> map(
|
||||
from: K,
|
||||
to: K,
|
||||
consumer: CacheCollectors.BiNotNullMapper<K, V, R>,
|
||||
): List<R> = map(consumer)
|
||||
|
||||
actual override fun <R> mapNotNull(
|
||||
from: K,
|
||||
to: K,
|
||||
consumer: CacheCollectors.BiMapper<K, V, R?>,
|
||||
): List<R> = mapNotNull(consumer)
|
||||
|
||||
actual override fun <R> mapNotNullIntoSet(
|
||||
from: K,
|
||||
to: K,
|
||||
consumer: CacheCollectors.BiMapper<K, V, R?>,
|
||||
): Set<R> = mapNotNullIntoSet(consumer)
|
||||
|
||||
actual override fun <R> mapFlatten(
|
||||
from: K,
|
||||
to: K,
|
||||
consumer: CacheCollectors.BiMapper<K, V, Collection<R>?>,
|
||||
): List<R> = mapFlatten(consumer)
|
||||
|
||||
actual override fun <R> mapFlattenIntoSet(
|
||||
from: K,
|
||||
to: K,
|
||||
consumer: CacheCollectors.BiMapper<K, V, Collection<R>?>,
|
||||
): Set<R> = mapFlattenIntoSet(consumer)
|
||||
|
||||
actual override fun maxOrNullOf(
|
||||
from: K,
|
||||
to: K,
|
||||
filter: CacheCollectors.BiFilter<K, V>,
|
||||
comparator: Comparator<V>,
|
||||
): V? = maxOrNullOf(filter, comparator)
|
||||
|
||||
actual override fun sumOf(
|
||||
from: K,
|
||||
to: K,
|
||||
consumer: CacheCollectors.BiSumOf<K, V>,
|
||||
): Int = sumOf(consumer)
|
||||
|
||||
actual override fun sumOfLong(
|
||||
from: K,
|
||||
to: K,
|
||||
consumer: CacheCollectors.BiSumOfLong<K, V>,
|
||||
): Long = sumOfLong(consumer)
|
||||
|
||||
actual override fun <R> groupBy(
|
||||
from: K,
|
||||
to: K,
|
||||
consumer: CacheCollectors.BiNotNullMapper<K, V, R>,
|
||||
): Map<R, List<V>> = groupBy(consumer)
|
||||
|
||||
actual override fun <R> countByGroup(
|
||||
from: K,
|
||||
to: K,
|
||||
consumer: CacheCollectors.BiNotNullMapper<K, V, R>,
|
||||
): Map<R, Int> = countByGroup(consumer)
|
||||
|
||||
actual override fun <R> sumByGroup(
|
||||
from: K,
|
||||
to: K,
|
||||
groupMap: CacheCollectors.BiNotNullMapper<K, V, R>,
|
||||
sumOf: CacheCollectors.BiNotNullMapper<K, V, Long>,
|
||||
): Map<R, Long> = sumByGroup(groupMap, sumOf)
|
||||
|
||||
actual override fun count(
|
||||
from: K,
|
||||
to: K,
|
||||
consumer: CacheCollectors.BiFilter<K, V>,
|
||||
): Int = count(consumer)
|
||||
|
||||
actual override fun <T, U> associate(
|
||||
from: K,
|
||||
to: K,
|
||||
transform: (K, V) -> Pair<T, U>,
|
||||
): Map<T, U> = associate(transform)
|
||||
|
||||
actual override fun <U> associateWith(
|
||||
from: K,
|
||||
to: K,
|
||||
transform: (K, V) -> U?,
|
||||
): Map<K, U?> = associateWith(transform)
|
||||
|
||||
actual override fun joinToString(
|
||||
separator: CharSequence,
|
||||
prefix: CharSequence,
|
||||
postfix: CharSequence,
|
||||
limit: Int,
|
||||
truncated: CharSequence,
|
||||
transform: ((K, V) -> CharSequence)?,
|
||||
): String {
|
||||
val buffer = StringBuilder()
|
||||
buffer.append(prefix)
|
||||
var count = 0
|
||||
forEach { key, value ->
|
||||
val str = if (transform != null) transform(key, value) else ""
|
||||
if (str.isNotEmpty()) {
|
||||
if (++count > 1) buffer.append(separator)
|
||||
if (limit < 0 || count <= limit) {
|
||||
when {
|
||||
transform != null -> buffer.append(str)
|
||||
else -> buffer.append("$key $value")
|
||||
}
|
||||
} else {
|
||||
return@forEach
|
||||
}
|
||||
}
|
||||
}
|
||||
if (limit >= 0 && count > limit) buffer.append(truncated)
|
||||
buffer.append(postfix)
|
||||
return buffer.toString()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
/*
|
||||
* Copyright (c) 2025 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.ciphers
|
||||
|
||||
import com.vitorpamplona.quartz.utils.Log
|
||||
import dev.whyoleg.cryptography.CryptographyProvider
|
||||
import dev.whyoleg.cryptography.DelicateCryptographyApi
|
||||
import dev.whyoleg.cryptography.algorithms.AES
|
||||
|
||||
actual class AESCBC actual constructor(
|
||||
actual val keyBytes: ByteArray,
|
||||
actual val iv: ByteArray,
|
||||
) : NostrCipher {
|
||||
private val cryptoProvider = CryptographyProvider.Default
|
||||
private val aesCbc = cryptoProvider.get(AES.CBC)
|
||||
|
||||
private val keyDecoder =
|
||||
aesCbc
|
||||
.keyDecoder()
|
||||
.decodeFromByteArrayBlocking(format = AES.Key.Format.RAW, keyBytes)
|
||||
|
||||
private fun cipher() = keyDecoder.cipher()
|
||||
|
||||
actual override fun name(): String = "aes-cbc"
|
||||
|
||||
@OptIn(DelicateCryptographyApi::class)
|
||||
actual override fun encrypt(bytesToEncrypt: ByteArray): ByteArray =
|
||||
with(cipher()) {
|
||||
encryptWithIvBlocking(iv, bytesToEncrypt)
|
||||
}
|
||||
|
||||
@OptIn(DelicateCryptographyApi::class)
|
||||
actual override fun decrypt(bytesToDecrypt: ByteArray): ByteArray =
|
||||
with(cipher()) {
|
||||
decryptWithIvBlocking(iv, bytesToDecrypt)
|
||||
}
|
||||
|
||||
@OptIn(DelicateCryptographyApi::class)
|
||||
actual override fun decryptOrNull(bytesToDecrypt: ByteArray): ByteArray? =
|
||||
try {
|
||||
with(cipher()) {
|
||||
decryptWithIvBlocking(iv, bytesToDecrypt)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.w("AESCBC", "Failed to decrypt", e)
|
||||
null
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
/*
|
||||
* Copyright (c) 2025 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.ciphers
|
||||
|
||||
import com.vitorpamplona.quartz.utils.Log
|
||||
import dev.whyoleg.cryptography.CryptographyProvider
|
||||
import dev.whyoleg.cryptography.DelicateCryptographyApi
|
||||
import dev.whyoleg.cryptography.algorithms.AES
|
||||
|
||||
actual class AESGCM actual constructor(
|
||||
actual val keyBytes: ByteArray,
|
||||
actual val nonce: ByteArray,
|
||||
) : NostrCipher {
|
||||
private val provider = CryptographyProvider.Default
|
||||
private val aesGcm = provider.get(AES.GCM)
|
||||
|
||||
private val keyDecoder =
|
||||
aesGcm
|
||||
.keyDecoder()
|
||||
.decodeFromByteArrayBlocking(AES.Key.Format.RAW, keyBytes)
|
||||
|
||||
private fun cipher() = keyDecoder.cipher()
|
||||
|
||||
actual override fun name(): String = "aes-gcm"
|
||||
|
||||
@OptIn(DelicateCryptographyApi::class)
|
||||
actual override fun encrypt(bytesToEncrypt: ByteArray): ByteArray =
|
||||
with(cipher()) {
|
||||
encryptWithIvBlocking(nonce, bytesToEncrypt)
|
||||
}
|
||||
|
||||
@OptIn(DelicateCryptographyApi::class)
|
||||
actual override fun decrypt(bytesToDecrypt: ByteArray): ByteArray =
|
||||
with(cipher()) {
|
||||
decryptWithIvBlocking(nonce, bytesToDecrypt)
|
||||
}
|
||||
|
||||
@OptIn(DelicateCryptographyApi::class)
|
||||
actual override fun decryptOrNull(bytesToDecrypt: ByteArray): ByteArray? =
|
||||
try {
|
||||
with(cipher()) {
|
||||
decryptWithIvBlocking(nonce, bytesToDecrypt)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.w("AESGCM", "Failed to decrypt", e)
|
||||
null
|
||||
}
|
||||
}
|
||||
+58
@@ -0,0 +1,58 @@
|
||||
/*
|
||||
* Copyright (c) 2025 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.diggest
|
||||
|
||||
import dev.whyoleg.cryptography.CryptographyProvider
|
||||
import dev.whyoleg.cryptography.DelicateCryptographyApi
|
||||
import dev.whyoleg.cryptography.algorithms.RIPEMD160
|
||||
import dev.whyoleg.cryptography.algorithms.SHA1
|
||||
import dev.whyoleg.cryptography.algorithms.SHA256
|
||||
|
||||
actual class DigestInstance actual constructor(
|
||||
algorithm: String,
|
||||
) {
|
||||
private val cryptoProvider = CryptographyProvider.Default
|
||||
|
||||
private val hasher = cryptoProvider.get(digestForAlgorithm(algorithm)).hasher()
|
||||
private val hashFunction = hasher.createHashFunction()
|
||||
|
||||
actual fun update(array: ByteArray) {
|
||||
hashFunction.update(array)
|
||||
}
|
||||
|
||||
actual fun update(byte: Byte) {
|
||||
hashFunction.update(byteArrayOf(byte))
|
||||
}
|
||||
|
||||
actual fun digest(): ByteArray = hashFunction.hashToByteArray()
|
||||
|
||||
actual fun digest(input: ByteArray): ByteArray = hasher.hashBlocking(input)
|
||||
|
||||
@OptIn(DelicateCryptographyApi::class)
|
||||
private fun digestForAlgorithm(algorithmName: String) =
|
||||
when (algorithmName) {
|
||||
"SHA-256" -> SHA256
|
||||
"SHA-1" -> SHA1
|
||||
"ripemd160" -> RIPEMD160
|
||||
"keccak256" -> error("KECCAK-256 is not yet supported.")
|
||||
else -> error("This message digest is not supported.")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
/*
|
||||
* Copyright (c) 2025 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.mac
|
||||
|
||||
import io.github.andreypfau.kotlinx.crypto.HMac
|
||||
import io.github.andreypfau.kotlinx.crypto.Sha256
|
||||
import io.github.andreypfau.kotlinx.crypto.Sha512
|
||||
|
||||
actual class MacInstance actual constructor(
|
||||
algorithm: String,
|
||||
key: ByteArray,
|
||||
) {
|
||||
private var nativeHmac = HMac(digestForAlgorithm(algorithm), key)
|
||||
|
||||
actual fun init(
|
||||
key: ByteArray,
|
||||
algorithm: String,
|
||||
) {
|
||||
nativeHmac = HMac(digestForAlgorithm(algorithm), key)
|
||||
}
|
||||
|
||||
actual fun getMacLength(): Int = nativeHmac.macSize
|
||||
|
||||
actual fun update(array: ByteArray) {
|
||||
nativeHmac.update(array)
|
||||
}
|
||||
|
||||
actual fun update(byte: Byte) {
|
||||
nativeHmac.update(byte)
|
||||
}
|
||||
|
||||
actual fun doFinal(): ByteArray = nativeHmac.digest()
|
||||
|
||||
actual fun doFinal(
|
||||
output: ByteArray,
|
||||
offset: Int,
|
||||
) {
|
||||
nativeHmac.digest(output, offset)
|
||||
}
|
||||
|
||||
private fun digestForAlgorithm(algorithm: String) =
|
||||
when (algorithm) {
|
||||
"HmacSHA256" -> Sha256()
|
||||
"HmacSHA512" -> Sha512()
|
||||
else -> error("Algorithm is not yet supported.")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
/*
|
||||
* Copyright (c) 2025 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.sha256
|
||||
|
||||
import dev.whyoleg.cryptography.CryptographyProvider
|
||||
import dev.whyoleg.cryptography.algorithms.SHA256
|
||||
|
||||
actual fun sha256(data: ByteArray): ByteArray {
|
||||
val provider = CryptographyProvider.Default
|
||||
val hasher = provider.get(SHA256).hasher()
|
||||
return hasher.hashBlocking(data)
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
/*
|
||||
* Copyright (c) 2025 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 platform.Foundation.NSDate
|
||||
import platform.Foundation.timeIntervalSince1970
|
||||
|
||||
actual fun platform() = "macOS"
|
||||
|
||||
actual fun currentTimeSeconds(): Long = (NSDate().timeIntervalSince1970).toLong()
|
||||
+1
-6
@@ -45,7 +45,6 @@ actual object EventHasherSerializer {
|
||||
add(kind)
|
||||
addJsonArray {
|
||||
tags.fastForEach { tag ->
|
||||
// add(JsonArray(content = tag.map { JsonPrimitive(it) }))
|
||||
addJsonArray { tag.fastForEach { add(it) } }
|
||||
}
|
||||
}
|
||||
@@ -68,10 +67,7 @@ actual object EventHasherSerializer {
|
||||
kind: Int,
|
||||
tags: Array<Array<String>>,
|
||||
content: String,
|
||||
): ByteArray {
|
||||
// Also use makeJsonObjectForId(...) above. May change this if needed(for performance).
|
||||
return makeJsonObjectForId(pubKey, createdAt, kind, tags, content).toString().encodeToByteArray()
|
||||
}
|
||||
): ByteArray = makeJsonObjectForId(pubKey, createdAt, kind, tags, content).toString().encodeToByteArray()
|
||||
|
||||
actual fun makeJsonForIdHashAndCheck(
|
||||
id: HexKey,
|
||||
@@ -81,7 +77,6 @@ actual object EventHasherSerializer {
|
||||
tags: Array<Array<String>>,
|
||||
content: String,
|
||||
): Boolean {
|
||||
// Also use makeJsonObjectForId(...) above. May change byteArray encode method if needed.
|
||||
val eventIdHash = sha256(makeJsonObjectForId(pubKey, createdAt, kind, tags, content).toString().encodeToByteArray())
|
||||
return Hex.isEqual(id, eventIdHash)
|
||||
}
|
||||
Reference in New Issue
Block a user