Merge branch 'main' into claude/implement-nip5a-rendering-5ioVq
This commit is contained in:
@@ -114,6 +114,7 @@ import com.vitorpamplona.amethyst.ui.note.types.RenderAudioTrack
|
|||||||
import com.vitorpamplona.amethyst.ui.note.types.RenderBadgeAward
|
import com.vitorpamplona.amethyst.ui.note.types.RenderBadgeAward
|
||||||
import com.vitorpamplona.amethyst.ui.note.types.RenderCalendarDateSlotEvent
|
import com.vitorpamplona.amethyst.ui.note.types.RenderCalendarDateSlotEvent
|
||||||
import com.vitorpamplona.amethyst.ui.note.types.RenderCalendarTimeSlotEvent
|
import com.vitorpamplona.amethyst.ui.note.types.RenderCalendarTimeSlotEvent
|
||||||
|
import com.vitorpamplona.amethyst.ui.note.types.RenderCashuMint
|
||||||
import com.vitorpamplona.amethyst.ui.note.types.RenderChannelMessage
|
import com.vitorpamplona.amethyst.ui.note.types.RenderChannelMessage
|
||||||
import com.vitorpamplona.amethyst.ui.note.types.RenderChatMessage
|
import com.vitorpamplona.amethyst.ui.note.types.RenderChatMessage
|
||||||
import com.vitorpamplona.amethyst.ui.note.types.RenderChatMessageEncryptedFile
|
import com.vitorpamplona.amethyst.ui.note.types.RenderChatMessageEncryptedFile
|
||||||
@@ -122,6 +123,7 @@ import com.vitorpamplona.amethyst.ui.note.types.RenderClassifieds
|
|||||||
import com.vitorpamplona.amethyst.ui.note.types.RenderCodeSnippetEvent
|
import com.vitorpamplona.amethyst.ui.note.types.RenderCodeSnippetEvent
|
||||||
import com.vitorpamplona.amethyst.ui.note.types.RenderCommunity
|
import com.vitorpamplona.amethyst.ui.note.types.RenderCommunity
|
||||||
import com.vitorpamplona.amethyst.ui.note.types.RenderEmojiPack
|
import com.vitorpamplona.amethyst.ui.note.types.RenderEmojiPack
|
||||||
|
import com.vitorpamplona.amethyst.ui.note.types.RenderFedimint
|
||||||
import com.vitorpamplona.amethyst.ui.note.types.RenderFhirResource
|
import com.vitorpamplona.amethyst.ui.note.types.RenderFhirResource
|
||||||
import com.vitorpamplona.amethyst.ui.note.types.RenderGitIssueEvent
|
import com.vitorpamplona.amethyst.ui.note.types.RenderGitIssueEvent
|
||||||
import com.vitorpamplona.amethyst.ui.note.types.RenderGitPatchEvent
|
import com.vitorpamplona.amethyst.ui.note.types.RenderGitPatchEvent
|
||||||
@@ -135,6 +137,7 @@ import com.vitorpamplona.amethyst.ui.note.types.RenderLiveChessChallenge
|
|||||||
import com.vitorpamplona.amethyst.ui.note.types.RenderLiveChessGameEnd
|
import com.vitorpamplona.amethyst.ui.note.types.RenderLiveChessGameEnd
|
||||||
import com.vitorpamplona.amethyst.ui.note.types.RenderLnZap
|
import com.vitorpamplona.amethyst.ui.note.types.RenderLnZap
|
||||||
import com.vitorpamplona.amethyst.ui.note.types.RenderLongFormContent
|
import com.vitorpamplona.amethyst.ui.note.types.RenderLongFormContent
|
||||||
|
import com.vitorpamplona.amethyst.ui.note.types.RenderMintRecommendation
|
||||||
import com.vitorpamplona.amethyst.ui.note.types.RenderNIP90ContentDiscoveryResponse
|
import com.vitorpamplona.amethyst.ui.note.types.RenderNIP90ContentDiscoveryResponse
|
||||||
import com.vitorpamplona.amethyst.ui.note.types.RenderNIP90Status
|
import com.vitorpamplona.amethyst.ui.note.types.RenderNIP90Status
|
||||||
import com.vitorpamplona.amethyst.ui.note.types.RenderNamedSiteEvent
|
import com.vitorpamplona.amethyst.ui.note.types.RenderNamedSiteEvent
|
||||||
@@ -155,6 +158,7 @@ import com.vitorpamplona.amethyst.ui.note.types.RenderReport
|
|||||||
import com.vitorpamplona.amethyst.ui.note.types.RenderRootSiteEvent
|
import com.vitorpamplona.amethyst.ui.note.types.RenderRootSiteEvent
|
||||||
import com.vitorpamplona.amethyst.ui.note.types.RenderTextEvent
|
import com.vitorpamplona.amethyst.ui.note.types.RenderTextEvent
|
||||||
import com.vitorpamplona.amethyst.ui.note.types.RenderTextModificationEvent
|
import com.vitorpamplona.amethyst.ui.note.types.RenderTextModificationEvent
|
||||||
|
import com.vitorpamplona.amethyst.ui.note.types.RenderThread
|
||||||
import com.vitorpamplona.amethyst.ui.note.types.RenderTorrent
|
import com.vitorpamplona.amethyst.ui.note.types.RenderTorrent
|
||||||
import com.vitorpamplona.amethyst.ui.note.types.RenderTorrentComment
|
import com.vitorpamplona.amethyst.ui.note.types.RenderTorrentComment
|
||||||
import com.vitorpamplona.amethyst.ui.note.types.RenderVoiceTrack
|
import com.vitorpamplona.amethyst.ui.note.types.RenderVoiceTrack
|
||||||
@@ -270,7 +274,11 @@ import com.vitorpamplona.quartz.nip72ModCommunities.communityAddress
|
|||||||
import com.vitorpamplona.quartz.nip72ModCommunities.definition.CommunityDefinitionEvent
|
import com.vitorpamplona.quartz.nip72ModCommunities.definition.CommunityDefinitionEvent
|
||||||
import com.vitorpamplona.quartz.nip72ModCommunities.isACommunityPost
|
import com.vitorpamplona.quartz.nip72ModCommunities.isACommunityPost
|
||||||
import com.vitorpamplona.quartz.nip75ZapGoals.GoalEvent
|
import com.vitorpamplona.quartz.nip75ZapGoals.GoalEvent
|
||||||
|
import com.vitorpamplona.quartz.nip7DThreads.ThreadEvent
|
||||||
import com.vitorpamplona.quartz.nip84Highlights.HighlightEvent
|
import com.vitorpamplona.quartz.nip84Highlights.HighlightEvent
|
||||||
|
import com.vitorpamplona.quartz.nip87Ecash.cashu.CashuMintEvent
|
||||||
|
import com.vitorpamplona.quartz.nip87Ecash.fedimint.FedimintEvent
|
||||||
|
import com.vitorpamplona.quartz.nip87Ecash.recommendation.MintRecommendationEvent
|
||||||
import com.vitorpamplona.quartz.nip88Polls.poll.PollEvent
|
import com.vitorpamplona.quartz.nip88Polls.poll.PollEvent
|
||||||
import com.vitorpamplona.quartz.nip89AppHandlers.definition.AppDefinitionEvent
|
import com.vitorpamplona.quartz.nip89AppHandlers.definition.AppDefinitionEvent
|
||||||
import com.vitorpamplona.quartz.nip90Dvms.NIP90ContentDiscoveryResponseEvent
|
import com.vitorpamplona.quartz.nip90Dvms.NIP90ContentDiscoveryResponseEvent
|
||||||
@@ -855,6 +863,20 @@ private fun RenderNoteRow(
|
|||||||
RenderLongFormContent(baseNote, accountViewModel, nav)
|
RenderLongFormContent(baseNote, accountViewModel, nav)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
is ThreadEvent -> {
|
||||||
|
RenderThread(
|
||||||
|
baseNote,
|
||||||
|
makeItShort,
|
||||||
|
canPreview,
|
||||||
|
quotesLeft,
|
||||||
|
unPackReply,
|
||||||
|
backgroundColor,
|
||||||
|
editState,
|
||||||
|
accountViewModel,
|
||||||
|
nav,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
is CodeSnippetEvent -> {
|
is CodeSnippetEvent -> {
|
||||||
RenderCodeSnippetEvent(baseNote)
|
RenderCodeSnippetEvent(baseNote)
|
||||||
}
|
}
|
||||||
@@ -1064,6 +1086,18 @@ private fun RenderNoteRow(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
is CashuMintEvent -> {
|
||||||
|
RenderCashuMint(noteEvent)
|
||||||
|
}
|
||||||
|
|
||||||
|
is FedimintEvent -> {
|
||||||
|
RenderFedimint(noteEvent)
|
||||||
|
}
|
||||||
|
|
||||||
|
is MintRecommendationEvent -> {
|
||||||
|
RenderMintRecommendation(noteEvent)
|
||||||
|
}
|
||||||
|
|
||||||
is ClassifiedsEvent -> {
|
is ClassifiedsEvent -> {
|
||||||
RenderClassifieds(
|
RenderClassifieds(
|
||||||
noteEvent,
|
noteEvent,
|
||||||
|
|||||||
@@ -0,0 +1,205 @@
|
|||||||
|
/*
|
||||||
|
* 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.note.types
|
||||||
|
|
||||||
|
import androidx.compose.foundation.border
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
||||||
|
import androidx.compose.foundation.layout.FlowRow
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import com.vitorpamplona.amethyst.ui.theme.QuoteBorder
|
||||||
|
import com.vitorpamplona.amethyst.ui.theme.SmallBorder
|
||||||
|
import com.vitorpamplona.amethyst.ui.theme.subtleBorder
|
||||||
|
import com.vitorpamplona.quartz.nip87Ecash.cashu.CashuMintEvent
|
||||||
|
import com.vitorpamplona.quartz.nip87Ecash.fedimint.FedimintEvent
|
||||||
|
import com.vitorpamplona.quartz.nip87Ecash.recommendation.MintRecommendationEvent
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun RenderCashuMint(noteEvent: CashuMintEvent) {
|
||||||
|
val mintUrl = remember(noteEvent) { noteEvent.mintUrl() }
|
||||||
|
val nuts = remember(noteEvent) { noteEvent.nuts() }
|
||||||
|
val network = remember(noteEvent) { noteEvent.network() }
|
||||||
|
|
||||||
|
MintAnnouncementCard(
|
||||||
|
title = "Cashu Mint",
|
||||||
|
url = mintUrl,
|
||||||
|
network = network.code,
|
||||||
|
capabilities = nuts,
|
||||||
|
capabilitiesLabel = "NUTs",
|
||||||
|
metadata = noteEvent.content.ifBlank { null },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun RenderFedimint(noteEvent: FedimintEvent) {
|
||||||
|
val inviteCodes = remember(noteEvent) { noteEvent.inviteCodes() }
|
||||||
|
val modules = remember(noteEvent) { noteEvent.modules() }
|
||||||
|
val network = remember(noteEvent) { noteEvent.network() }
|
||||||
|
|
||||||
|
MintAnnouncementCard(
|
||||||
|
title = "Fedimint",
|
||||||
|
url = inviteCodes.firstOrNull(),
|
||||||
|
network = network.code,
|
||||||
|
capabilities = modules,
|
||||||
|
capabilitiesLabel = "Modules",
|
||||||
|
metadata = noteEvent.content.ifBlank { null },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun RenderMintRecommendation(noteEvent: MintRecommendationEvent) {
|
||||||
|
val mintUrls = remember(noteEvent) { noteEvent.mintUrls() }
|
||||||
|
val mintType =
|
||||||
|
remember(noteEvent) {
|
||||||
|
when {
|
||||||
|
noteEvent.isCashuRecommendation() -> "Cashu Mint"
|
||||||
|
noteEvent.isFedimintRecommendation() -> "Fedimint"
|
||||||
|
else -> "Ecash Mint"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MintAnnouncementCard(
|
||||||
|
title = "$mintType Recommendation",
|
||||||
|
url = mintUrls.firstOrNull(),
|
||||||
|
network = null,
|
||||||
|
capabilities = emptyList(),
|
||||||
|
capabilitiesLabel = null,
|
||||||
|
metadata = noteEvent.content.ifBlank { null },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalLayoutApi::class)
|
||||||
|
@Composable
|
||||||
|
private fun MintAnnouncementCard(
|
||||||
|
title: String,
|
||||||
|
url: String?,
|
||||||
|
network: String?,
|
||||||
|
capabilities: List<String>,
|
||||||
|
capabilitiesLabel: String?,
|
||||||
|
metadata: String?,
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.clip(shape = QuoteBorder)
|
||||||
|
.border(
|
||||||
|
1.dp,
|
||||||
|
MaterialTheme.colorScheme.subtleBorder,
|
||||||
|
QuoteBorder,
|
||||||
|
).fillMaxWidth(),
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.padding(10.dp),
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = title,
|
||||||
|
style = MaterialTheme.typography.bodyLarge,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
)
|
||||||
|
|
||||||
|
if (network != null) {
|
||||||
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
|
Text(
|
||||||
|
text = network,
|
||||||
|
style = MaterialTheme.typography.labelSmall,
|
||||||
|
color = MaterialTheme.colorScheme.primary,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.clip(SmallBorder)
|
||||||
|
.border(1.dp, MaterialTheme.colorScheme.primary, SmallBorder)
|
||||||
|
.padding(horizontal = 6.dp, vertical = 2.dp),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
url?.let {
|
||||||
|
Text(
|
||||||
|
text = it,
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = MaterialTheme.colorScheme.primary,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
modifier = Modifier.padding(top = 4.dp),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (capabilities.isNotEmpty() && capabilitiesLabel != null) {
|
||||||
|
FlowRow(
|
||||||
|
modifier = Modifier.padding(top = 6.dp),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(4.dp),
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "$capabilitiesLabel: ",
|
||||||
|
style = MaterialTheme.typography.labelSmall,
|
||||||
|
color = Color.Gray,
|
||||||
|
)
|
||||||
|
capabilities.forEach { capability ->
|
||||||
|
Text(
|
||||||
|
text = capability,
|
||||||
|
style = MaterialTheme.typography.labelSmall,
|
||||||
|
color = Color.Gray,
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.clip(SmallBorder)
|
||||||
|
.border(1.dp, Color.Gray.copy(alpha = 0.3f), SmallBorder)
|
||||||
|
.padding(horizontal = 4.dp, vertical = 1.dp),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
metadata?.let {
|
||||||
|
Text(
|
||||||
|
text = it,
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = Color.Gray,
|
||||||
|
maxLines = 3,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
modifier = Modifier.padding(top = 6.dp),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
/*
|
||||||
|
* 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.note.types
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.MutableState
|
||||||
|
import androidx.compose.runtime.State
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import com.vitorpamplona.amethyst.model.Note
|
||||||
|
import com.vitorpamplona.amethyst.ui.components.GenericLoadable
|
||||||
|
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||||
|
import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer
|
||||||
|
import com.vitorpamplona.amethyst.ui.theme.replyModifier
|
||||||
|
import com.vitorpamplona.quartz.nip7DThreads.ThreadEvent
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun RenderThread(
|
||||||
|
note: Note,
|
||||||
|
makeItShort: Boolean,
|
||||||
|
canPreview: Boolean,
|
||||||
|
quotesLeft: Int,
|
||||||
|
unPackReply: ReplyRenderType,
|
||||||
|
backgroundColor: MutableState<Color>,
|
||||||
|
editState: State<GenericLoadable<EditState>>,
|
||||||
|
accountViewModel: AccountViewModel,
|
||||||
|
nav: INav,
|
||||||
|
) {
|
||||||
|
val noteEvent = note.event as? ThreadEvent ?: return
|
||||||
|
val title = noteEvent.title()
|
||||||
|
|
||||||
|
if (title != null) {
|
||||||
|
Column(MaterialTheme.colorScheme.replyModifier) {
|
||||||
|
Text(
|
||||||
|
text = title,
|
||||||
|
style = MaterialTheme.typography.bodyLarge,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(start = 10.dp, end = 10.dp, top = 10.dp, bottom = 10.dp),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Spacer(modifier = StdVertSpacer)
|
||||||
|
}
|
||||||
|
|
||||||
|
RenderTextEvent(
|
||||||
|
note,
|
||||||
|
makeItShort,
|
||||||
|
canPreview,
|
||||||
|
quotesLeft,
|
||||||
|
unPackReply,
|
||||||
|
backgroundColor,
|
||||||
|
editState,
|
||||||
|
accountViewModel,
|
||||||
|
nav,
|
||||||
|
)
|
||||||
|
}
|
||||||
+28
@@ -147,10 +147,12 @@ import com.vitorpamplona.amethyst.ui.note.types.RenderAttestorProficiency
|
|||||||
import com.vitorpamplona.amethyst.ui.note.types.RenderAttestorRecommendation
|
import com.vitorpamplona.amethyst.ui.note.types.RenderAttestorRecommendation
|
||||||
import com.vitorpamplona.amethyst.ui.note.types.RenderCalendarDateSlotEvent
|
import com.vitorpamplona.amethyst.ui.note.types.RenderCalendarDateSlotEvent
|
||||||
import com.vitorpamplona.amethyst.ui.note.types.RenderCalendarTimeSlotEvent
|
import com.vitorpamplona.amethyst.ui.note.types.RenderCalendarTimeSlotEvent
|
||||||
|
import com.vitorpamplona.amethyst.ui.note.types.RenderCashuMint
|
||||||
import com.vitorpamplona.amethyst.ui.note.types.RenderChannelMessage
|
import com.vitorpamplona.amethyst.ui.note.types.RenderChannelMessage
|
||||||
import com.vitorpamplona.amethyst.ui.note.types.RenderChatMessageEncryptedFile
|
import com.vitorpamplona.amethyst.ui.note.types.RenderChatMessageEncryptedFile
|
||||||
import com.vitorpamplona.amethyst.ui.note.types.RenderCodeSnippetHeaderForThread
|
import com.vitorpamplona.amethyst.ui.note.types.RenderCodeSnippetHeaderForThread
|
||||||
import com.vitorpamplona.amethyst.ui.note.types.RenderEmojiPack
|
import com.vitorpamplona.amethyst.ui.note.types.RenderEmojiPack
|
||||||
|
import com.vitorpamplona.amethyst.ui.note.types.RenderFedimint
|
||||||
import com.vitorpamplona.amethyst.ui.note.types.RenderFhirResource
|
import com.vitorpamplona.amethyst.ui.note.types.RenderFhirResource
|
||||||
import com.vitorpamplona.amethyst.ui.note.types.RenderGitIssueEvent
|
import com.vitorpamplona.amethyst.ui.note.types.RenderGitIssueEvent
|
||||||
import com.vitorpamplona.amethyst.ui.note.types.RenderGitPatchEvent
|
import com.vitorpamplona.amethyst.ui.note.types.RenderGitPatchEvent
|
||||||
@@ -161,6 +163,7 @@ import com.vitorpamplona.amethyst.ui.note.types.RenderInteractiveStory
|
|||||||
import com.vitorpamplona.amethyst.ui.note.types.RenderLiveActivityChatMessage
|
import com.vitorpamplona.amethyst.ui.note.types.RenderLiveActivityChatMessage
|
||||||
import com.vitorpamplona.amethyst.ui.note.types.RenderLnZap
|
import com.vitorpamplona.amethyst.ui.note.types.RenderLnZap
|
||||||
import com.vitorpamplona.amethyst.ui.note.types.RenderNamedSiteEvent
|
import com.vitorpamplona.amethyst.ui.note.types.RenderNamedSiteEvent
|
||||||
|
import com.vitorpamplona.amethyst.ui.note.types.RenderMintRecommendation
|
||||||
import com.vitorpamplona.amethyst.ui.note.types.RenderPinListEvent
|
import com.vitorpamplona.amethyst.ui.note.types.RenderPinListEvent
|
||||||
import com.vitorpamplona.amethyst.ui.note.types.RenderPoll
|
import com.vitorpamplona.amethyst.ui.note.types.RenderPoll
|
||||||
import com.vitorpamplona.amethyst.ui.note.types.RenderPostApproval
|
import com.vitorpamplona.amethyst.ui.note.types.RenderPostApproval
|
||||||
@@ -271,7 +274,11 @@ import com.vitorpamplona.quartz.nip72ModCommunities.approval.CommunityPostApprov
|
|||||||
import com.vitorpamplona.quartz.nip72ModCommunities.communityAddress
|
import com.vitorpamplona.quartz.nip72ModCommunities.communityAddress
|
||||||
import com.vitorpamplona.quartz.nip72ModCommunities.isACommunityPost
|
import com.vitorpamplona.quartz.nip72ModCommunities.isACommunityPost
|
||||||
import com.vitorpamplona.quartz.nip75ZapGoals.GoalEvent
|
import com.vitorpamplona.quartz.nip75ZapGoals.GoalEvent
|
||||||
|
import com.vitorpamplona.quartz.nip7DThreads.ThreadEvent
|
||||||
import com.vitorpamplona.quartz.nip84Highlights.HighlightEvent
|
import com.vitorpamplona.quartz.nip84Highlights.HighlightEvent
|
||||||
|
import com.vitorpamplona.quartz.nip87Ecash.cashu.CashuMintEvent
|
||||||
|
import com.vitorpamplona.quartz.nip87Ecash.fedimint.FedimintEvent
|
||||||
|
import com.vitorpamplona.quartz.nip87Ecash.recommendation.MintRecommendationEvent
|
||||||
import com.vitorpamplona.quartz.nip88Polls.poll.PollEvent
|
import com.vitorpamplona.quartz.nip88Polls.poll.PollEvent
|
||||||
import com.vitorpamplona.quartz.nip89AppHandlers.definition.AppDefinitionEvent
|
import com.vitorpamplona.quartz.nip89AppHandlers.definition.AppDefinitionEvent
|
||||||
import com.vitorpamplona.quartz.nip94FileMetadata.FileHeaderEvent
|
import com.vitorpamplona.quartz.nip94FileMetadata.FileHeaderEvent
|
||||||
@@ -567,6 +574,7 @@ private fun FullBleedNoteCompose(
|
|||||||
when (noteEvent) {
|
when (noteEvent) {
|
||||||
is BadgeDefinitionEvent -> BadgeDisplay(baseNote = baseNote, accountViewModel)
|
is BadgeDefinitionEvent -> BadgeDisplay(baseNote = baseNote, accountViewModel)
|
||||||
is LongTextNoteEvent -> RenderLongFormHeaderForThread(noteEvent, baseNote, accountViewModel)
|
is LongTextNoteEvent -> RenderLongFormHeaderForThread(noteEvent, baseNote, accountViewModel)
|
||||||
|
is ThreadEvent -> RenderThreadHeaderForThread(noteEvent)
|
||||||
is WikiNoteEvent -> RenderWikiHeaderForThread(noteEvent, accountViewModel, nav)
|
is WikiNoteEvent -> RenderWikiHeaderForThread(noteEvent, accountViewModel, nav)
|
||||||
is ClassifiedsEvent -> RenderClassifiedsReaderForThread(noteEvent, baseNote, accountViewModel, nav)
|
is ClassifiedsEvent -> RenderClassifiedsReaderForThread(noteEvent, baseNote, accountViewModel, nav)
|
||||||
is CodeSnippetEvent -> RenderCodeSnippetHeaderForThread(noteEvent)
|
is CodeSnippetEvent -> RenderCodeSnippetHeaderForThread(noteEvent)
|
||||||
@@ -761,6 +769,12 @@ private fun FullBleedNoteCompose(
|
|||||||
accountViewModel,
|
accountViewModel,
|
||||||
nav,
|
nav,
|
||||||
)
|
)
|
||||||
|
} else if (noteEvent is CashuMintEvent) {
|
||||||
|
RenderCashuMint(noteEvent)
|
||||||
|
} else if (noteEvent is FedimintEvent) {
|
||||||
|
RenderFedimint(noteEvent)
|
||||||
|
} else if (noteEvent is MintRecommendationEvent) {
|
||||||
|
RenderMintRecommendation(noteEvent)
|
||||||
} else if (noteEvent is PollEvent) {
|
} else if (noteEvent is PollEvent) {
|
||||||
RenderPoll(
|
RenderPoll(
|
||||||
note = baseNote,
|
note = baseNote,
|
||||||
@@ -1132,6 +1146,20 @@ private fun RenderLongFormHeaderForThread(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun RenderThreadHeaderForThread(noteEvent: ThreadEvent) {
|
||||||
|
noteEvent.title()?.let {
|
||||||
|
Column(modifier = Modifier.padding(start = 12.dp, end = 12.dp, bottom = 12.dp)) {
|
||||||
|
Text(
|
||||||
|
text = it,
|
||||||
|
fontSize = 28.sp,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Preview
|
@Preview
|
||||||
@Composable
|
@Composable
|
||||||
private fun RenderWikiHeaderForThreadPreview() {
|
private fun RenderWikiHeaderForThreadPreview() {
|
||||||
|
|||||||
@@ -77,7 +77,7 @@ Za podpisovanje se je potrebno prijaviti s privatnim ključem</string>
|
|||||||
<string name="zaps">Zapi</string>
|
<string name="zaps">Zapi</string>
|
||||||
<string name="view_count">Števec vpogledov</string>
|
<string name="view_count">Števec vpogledov</string>
|
||||||
<string name="boost">Pošlji naprej</string>
|
<string name="boost">Pošlji naprej</string>
|
||||||
<string name="boosted">Poslano naprej</string>
|
<string name="boosted">poslano naprej</string>
|
||||||
<string name="edited">posodobljeno</string>
|
<string name="edited">posodobljeno</string>
|
||||||
<string name="edited_number">uredi #%1$s</string>
|
<string name="edited_number">uredi #%1$s</string>
|
||||||
<string name="original">original</string>
|
<string name="original">original</string>
|
||||||
|
|||||||
+26
@@ -0,0 +1,26 @@
|
|||||||
|
/*
|
||||||
|
* 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.nip7DThreads
|
||||||
|
|
||||||
|
import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder
|
||||||
|
import com.vitorpamplona.quartz.nip23LongContent.tags.TitleTag
|
||||||
|
|
||||||
|
fun TagArrayBuilder<ThreadEvent>.title(title: String) = addUnique(TitleTag.assemble(title))
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
/*
|
||||||
|
* 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.nip7DThreads
|
||||||
|
|
||||||
|
import com.vitorpamplona.quartz.nip01Core.core.TagArray
|
||||||
|
import com.vitorpamplona.quartz.nip23LongContent.tags.TitleTag
|
||||||
|
|
||||||
|
fun TagArray.title() = firstNotNullOfOrNull(TitleTag::parse)
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
/*
|
||||||
|
* 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.nip7DThreads
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Immutable
|
||||||
|
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||||
|
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||||
|
import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder
|
||||||
|
import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate
|
||||||
|
import com.vitorpamplona.quartz.nip22Comments.RootScope
|
||||||
|
import com.vitorpamplona.quartz.nip31Alts.alt
|
||||||
|
import com.vitorpamplona.quartz.nip50Search.SearchableEvent
|
||||||
|
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||||
|
|
||||||
|
@Immutable
|
||||||
|
class ThreadEvent(
|
||||||
|
id: HexKey,
|
||||||
|
pubKey: HexKey,
|
||||||
|
createdAt: Long,
|
||||||
|
tags: Array<Array<String>>,
|
||||||
|
content: String,
|
||||||
|
sig: HexKey,
|
||||||
|
) : Event(id, pubKey, createdAt, KIND, tags, content, sig),
|
||||||
|
RootScope,
|
||||||
|
SearchableEvent {
|
||||||
|
override fun indexableContent() = "title: " + title() + "\n" + content
|
||||||
|
|
||||||
|
fun title() = tags.title()
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val KIND = 11
|
||||||
|
const val ALT_DESCRIPTION = "Thread"
|
||||||
|
|
||||||
|
fun build(
|
||||||
|
content: String,
|
||||||
|
title: String,
|
||||||
|
createdAt: Long = TimeUtils.now(),
|
||||||
|
initializer: TagArrayBuilder<ThreadEvent>.() -> Unit = {},
|
||||||
|
) = eventTemplate(KIND, content, createdAt) {
|
||||||
|
alt(ALT_DESCRIPTION)
|
||||||
|
title(title)
|
||||||
|
initializer()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
/*
|
||||||
|
* 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.nip87Ecash
|
||||||
|
|
||||||
|
import com.vitorpamplona.quartz.nip01Core.core.has
|
||||||
|
import com.vitorpamplona.quartz.utils.ensure
|
||||||
|
|
||||||
|
class MintUrlTag {
|
||||||
|
companion object {
|
||||||
|
const val TAG_NAME = "u"
|
||||||
|
|
||||||
|
fun parse(tag: Array<String>): String? {
|
||||||
|
ensure(tag.has(1)) { return null }
|
||||||
|
ensure(tag[0] == TAG_NAME) { return null }
|
||||||
|
ensure(tag[1].isNotEmpty()) { return null }
|
||||||
|
return tag[1]
|
||||||
|
}
|
||||||
|
|
||||||
|
fun assemble(url: String) = arrayOf(TAG_NAME, url)
|
||||||
|
|
||||||
|
fun assemble(urls: List<String>) = urls.map { assemble(it) }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
/*
|
||||||
|
* 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.nip87Ecash
|
||||||
|
|
||||||
|
import com.vitorpamplona.quartz.nip01Core.core.has
|
||||||
|
import com.vitorpamplona.quartz.utils.ensure
|
||||||
|
|
||||||
|
enum class NetworkType(
|
||||||
|
val code: String,
|
||||||
|
) {
|
||||||
|
MAINNET("mainnet"),
|
||||||
|
TESTNET("testnet"),
|
||||||
|
SIGNET("signet"),
|
||||||
|
REGTEST("regtest"),
|
||||||
|
;
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun fromCode(code: String) = entries.firstOrNull { it.code == code }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class NetworkTag {
|
||||||
|
companion object {
|
||||||
|
const val TAG_NAME = "n"
|
||||||
|
|
||||||
|
fun parse(tag: Array<String>): NetworkType? {
|
||||||
|
ensure(tag.has(1)) { return null }
|
||||||
|
ensure(tag[0] == TAG_NAME) { return null }
|
||||||
|
ensure(tag[1].isNotEmpty()) { return null }
|
||||||
|
return NetworkType.fromCode(tag[1])
|
||||||
|
}
|
||||||
|
|
||||||
|
fun assemble(network: NetworkType) = arrayOf(TAG_NAME, network.code)
|
||||||
|
}
|
||||||
|
}
|
||||||
+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.quartz.nip87Ecash.cashu
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Immutable
|
||||||
|
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||||
|
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||||
|
import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder
|
||||||
|
import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate
|
||||||
|
import com.vitorpamplona.quartz.nip31Alts.alt
|
||||||
|
import com.vitorpamplona.quartz.nip87Ecash.NetworkType
|
||||||
|
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||||
|
|
||||||
|
@Immutable
|
||||||
|
class CashuMintEvent(
|
||||||
|
id: HexKey,
|
||||||
|
pubKey: HexKey,
|
||||||
|
createdAt: Long,
|
||||||
|
tags: Array<Array<String>>,
|
||||||
|
content: String,
|
||||||
|
sig: HexKey,
|
||||||
|
) : Event(id, pubKey, createdAt, KIND, tags, content, sig) {
|
||||||
|
fun mintUrl() = tags.mintUrl()
|
||||||
|
|
||||||
|
fun nuts() = tags.nuts()
|
||||||
|
|
||||||
|
fun network() = tags.network()
|
||||||
|
|
||||||
|
fun dTag() = tags.dTag()
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val KIND = 38172
|
||||||
|
const val ALT_DESCRIPTION = "Cashu Mint Announcement"
|
||||||
|
|
||||||
|
fun build(
|
||||||
|
mintPubKey: String,
|
||||||
|
mintUrl: String,
|
||||||
|
nuts: List<String>,
|
||||||
|
network: NetworkType = NetworkType.MAINNET,
|
||||||
|
metadata: String = "",
|
||||||
|
createdAt: Long = TimeUtils.now(),
|
||||||
|
initializer: TagArrayBuilder<CashuMintEvent>.() -> Unit = {},
|
||||||
|
) = eventTemplate(KIND, metadata, createdAt) {
|
||||||
|
alt(ALT_DESCRIPTION)
|
||||||
|
dTag(mintPubKey)
|
||||||
|
mintUrl(mintUrl)
|
||||||
|
nuts(nuts)
|
||||||
|
network(network)
|
||||||
|
initializer()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+35
@@ -0,0 +1,35 @@
|
|||||||
|
/*
|
||||||
|
* 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.nip87Ecash.cashu
|
||||||
|
|
||||||
|
import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder
|
||||||
|
import com.vitorpamplona.quartz.nip87Ecash.MintUrlTag
|
||||||
|
import com.vitorpamplona.quartz.nip87Ecash.NetworkTag
|
||||||
|
import com.vitorpamplona.quartz.nip87Ecash.NetworkType
|
||||||
|
import com.vitorpamplona.quartz.nip87Ecash.cashu.tags.NutsTag
|
||||||
|
|
||||||
|
fun TagArrayBuilder<CashuMintEvent>.dTag(mintPubKey: String) = addUnique(arrayOf("d", mintPubKey))
|
||||||
|
|
||||||
|
fun TagArrayBuilder<CashuMintEvent>.mintUrl(url: String) = addUnique(MintUrlTag.assemble(url))
|
||||||
|
|
||||||
|
fun TagArrayBuilder<CashuMintEvent>.nuts(nuts: List<String>) = add(NutsTag.assemble(nuts))
|
||||||
|
|
||||||
|
fun TagArrayBuilder<CashuMintEvent>.network(network: NetworkType) = addUnique(NetworkTag.assemble(network))
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
/*
|
||||||
|
* 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.nip87Ecash.cashu
|
||||||
|
|
||||||
|
import com.vitorpamplona.quartz.nip01Core.core.TagArray
|
||||||
|
import com.vitorpamplona.quartz.nip87Ecash.MintUrlTag
|
||||||
|
import com.vitorpamplona.quartz.nip87Ecash.NetworkTag
|
||||||
|
import com.vitorpamplona.quartz.nip87Ecash.NetworkType
|
||||||
|
import com.vitorpamplona.quartz.nip87Ecash.cashu.tags.NutsTag
|
||||||
|
|
||||||
|
fun TagArray.mintUrl(): String? = firstNotNullOfOrNull(MintUrlTag::parse)
|
||||||
|
|
||||||
|
fun TagArray.nuts(): List<String> = firstNotNullOfOrNull(NutsTag::parse) ?: emptyList()
|
||||||
|
|
||||||
|
fun TagArray.network(): NetworkType = firstNotNullOfOrNull(NetworkTag::parse) ?: NetworkType.MAINNET
|
||||||
|
|
||||||
|
fun TagArray.dTag(): String? = firstOrNull { it.size >= 2 && it[0] == "d" }?.get(1)
|
||||||
+39
@@ -0,0 +1,39 @@
|
|||||||
|
/*
|
||||||
|
* 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.nip87Ecash.cashu.tags
|
||||||
|
|
||||||
|
import com.vitorpamplona.quartz.nip01Core.core.has
|
||||||
|
import com.vitorpamplona.quartz.utils.ensure
|
||||||
|
|
||||||
|
class NutsTag {
|
||||||
|
companion object {
|
||||||
|
const val TAG_NAME = "nuts"
|
||||||
|
|
||||||
|
fun parse(tag: Array<String>): List<String>? {
|
||||||
|
ensure(tag.has(1)) { return null }
|
||||||
|
ensure(tag[0] == TAG_NAME) { return null }
|
||||||
|
ensure(tag[1].isNotEmpty()) { return null }
|
||||||
|
return tag.drop(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun assemble(nuts: List<String>) = arrayOf(TAG_NAME, *nuts.toTypedArray())
|
||||||
|
}
|
||||||
|
}
|
||||||
+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.quartz.nip87Ecash.fedimint
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Immutable
|
||||||
|
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||||
|
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||||
|
import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder
|
||||||
|
import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate
|
||||||
|
import com.vitorpamplona.quartz.nip31Alts.alt
|
||||||
|
import com.vitorpamplona.quartz.nip87Ecash.NetworkType
|
||||||
|
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||||
|
|
||||||
|
@Immutable
|
||||||
|
class FedimintEvent(
|
||||||
|
id: HexKey,
|
||||||
|
pubKey: HexKey,
|
||||||
|
createdAt: Long,
|
||||||
|
tags: Array<Array<String>>,
|
||||||
|
content: String,
|
||||||
|
sig: HexKey,
|
||||||
|
) : Event(id, pubKey, createdAt, KIND, tags, content, sig) {
|
||||||
|
fun inviteCodes() = tags.inviteCodes()
|
||||||
|
|
||||||
|
fun modules() = tags.modules()
|
||||||
|
|
||||||
|
fun network() = tags.network()
|
||||||
|
|
||||||
|
fun dTag() = tags.dTag()
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val KIND = 38173
|
||||||
|
const val ALT_DESCRIPTION = "Fedimint Announcement"
|
||||||
|
|
||||||
|
fun build(
|
||||||
|
federationId: String,
|
||||||
|
inviteCodes: List<String>,
|
||||||
|
modules: List<String>,
|
||||||
|
network: NetworkType = NetworkType.MAINNET,
|
||||||
|
metadata: String = "",
|
||||||
|
createdAt: Long = TimeUtils.now(),
|
||||||
|
initializer: TagArrayBuilder<FedimintEvent>.() -> Unit = {},
|
||||||
|
) = eventTemplate(KIND, metadata, createdAt) {
|
||||||
|
alt(ALT_DESCRIPTION)
|
||||||
|
dTag(federationId)
|
||||||
|
inviteCodes(inviteCodes)
|
||||||
|
modules(modules)
|
||||||
|
network(network)
|
||||||
|
initializer()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+35
@@ -0,0 +1,35 @@
|
|||||||
|
/*
|
||||||
|
* 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.nip87Ecash.fedimint
|
||||||
|
|
||||||
|
import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder
|
||||||
|
import com.vitorpamplona.quartz.nip87Ecash.MintUrlTag
|
||||||
|
import com.vitorpamplona.quartz.nip87Ecash.NetworkTag
|
||||||
|
import com.vitorpamplona.quartz.nip87Ecash.NetworkType
|
||||||
|
import com.vitorpamplona.quartz.nip87Ecash.fedimint.tags.ModulesTag
|
||||||
|
|
||||||
|
fun TagArrayBuilder<FedimintEvent>.dTag(federationId: String) = addUnique(arrayOf("d", federationId))
|
||||||
|
|
||||||
|
fun TagArrayBuilder<FedimintEvent>.inviteCodes(codes: List<String>) = addAll(MintUrlTag.assemble(codes))
|
||||||
|
|
||||||
|
fun TagArrayBuilder<FedimintEvent>.modules(modules: List<String>) = add(ModulesTag.assemble(modules))
|
||||||
|
|
||||||
|
fun TagArrayBuilder<FedimintEvent>.network(network: NetworkType) = addUnique(NetworkTag.assemble(network))
|
||||||
+35
@@ -0,0 +1,35 @@
|
|||||||
|
/*
|
||||||
|
* 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.nip87Ecash.fedimint
|
||||||
|
|
||||||
|
import com.vitorpamplona.quartz.nip01Core.core.TagArray
|
||||||
|
import com.vitorpamplona.quartz.nip87Ecash.MintUrlTag
|
||||||
|
import com.vitorpamplona.quartz.nip87Ecash.NetworkTag
|
||||||
|
import com.vitorpamplona.quartz.nip87Ecash.NetworkType
|
||||||
|
import com.vitorpamplona.quartz.nip87Ecash.fedimint.tags.ModulesTag
|
||||||
|
|
||||||
|
fun TagArray.inviteCodes(): List<String> = mapNotNull(MintUrlTag::parse)
|
||||||
|
|
||||||
|
fun TagArray.modules(): List<String> = firstNotNullOfOrNull(ModulesTag::parse) ?: emptyList()
|
||||||
|
|
||||||
|
fun TagArray.network(): NetworkType = firstNotNullOfOrNull(NetworkTag::parse) ?: NetworkType.MAINNET
|
||||||
|
|
||||||
|
fun TagArray.dTag(): String? = firstOrNull { it.size >= 2 && it[0] == "d" }?.get(1)
|
||||||
+39
@@ -0,0 +1,39 @@
|
|||||||
|
/*
|
||||||
|
* 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.nip87Ecash.fedimint.tags
|
||||||
|
|
||||||
|
import com.vitorpamplona.quartz.nip01Core.core.has
|
||||||
|
import com.vitorpamplona.quartz.utils.ensure
|
||||||
|
|
||||||
|
class ModulesTag {
|
||||||
|
companion object {
|
||||||
|
const val TAG_NAME = "modules"
|
||||||
|
|
||||||
|
fun parse(tag: Array<String>): List<String>? {
|
||||||
|
ensure(tag.has(1)) { return null }
|
||||||
|
ensure(tag[0] == TAG_NAME) { return null }
|
||||||
|
ensure(tag[1].isNotEmpty()) { return null }
|
||||||
|
return tag.drop(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun assemble(modules: List<String>) = arrayOf(TAG_NAME, *modules.toTypedArray())
|
||||||
|
}
|
||||||
|
}
|
||||||
+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.quartz.nip87Ecash.recommendation
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Immutable
|
||||||
|
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||||
|
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||||
|
import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder
|
||||||
|
import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate
|
||||||
|
import com.vitorpamplona.quartz.nip31Alts.alt
|
||||||
|
import com.vitorpamplona.quartz.nip87Ecash.cashu.CashuMintEvent
|
||||||
|
import com.vitorpamplona.quartz.nip87Ecash.fedimint.FedimintEvent
|
||||||
|
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||||
|
|
||||||
|
@Immutable
|
||||||
|
class MintRecommendationEvent(
|
||||||
|
id: HexKey,
|
||||||
|
pubKey: HexKey,
|
||||||
|
createdAt: Long,
|
||||||
|
tags: Array<Array<String>>,
|
||||||
|
content: String,
|
||||||
|
sig: HexKey,
|
||||||
|
) : Event(id, pubKey, createdAt, KIND, tags, content, sig) {
|
||||||
|
fun mintUrls() = tags.mintUrls()
|
||||||
|
|
||||||
|
fun mintEventKind() = tags.mintEventKind()
|
||||||
|
|
||||||
|
fun dTag() = tags.dTag()
|
||||||
|
|
||||||
|
fun mintEventAddresses() = tags.mintEventAddresses()
|
||||||
|
|
||||||
|
fun isCashuRecommendation() = mintEventKind() == CashuMintEvent.KIND
|
||||||
|
|
||||||
|
fun isFedimintRecommendation() = mintEventKind() == FedimintEvent.KIND
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val KIND = 38000
|
||||||
|
const val ALT_DESCRIPTION = "Mint Recommendation"
|
||||||
|
|
||||||
|
fun build(
|
||||||
|
mintIdentifier: String,
|
||||||
|
mintKind: Int,
|
||||||
|
review: String = "",
|
||||||
|
mintUrls: List<String> = emptyList(),
|
||||||
|
mintEventAddress: String? = null,
|
||||||
|
mintEventRelay: String? = null,
|
||||||
|
createdAt: Long = TimeUtils.now(),
|
||||||
|
initializer: TagArrayBuilder<MintRecommendationEvent>.() -> Unit = {},
|
||||||
|
) = eventTemplate(KIND, review, createdAt) {
|
||||||
|
alt(ALT_DESCRIPTION)
|
||||||
|
dTag(mintIdentifier)
|
||||||
|
kTag(mintKind)
|
||||||
|
mintUrls.forEach { mintUrl(it) }
|
||||||
|
if (mintEventAddress != null) {
|
||||||
|
aTag(mintEventAddress, mintEventRelay)
|
||||||
|
}
|
||||||
|
initializer()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+39
@@ -0,0 +1,39 @@
|
|||||||
|
/*
|
||||||
|
* 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.nip87Ecash.recommendation
|
||||||
|
|
||||||
|
import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder
|
||||||
|
import com.vitorpamplona.quartz.nip87Ecash.MintUrlTag
|
||||||
|
|
||||||
|
fun TagArrayBuilder<MintRecommendationEvent>.dTag(mintIdentifier: String) = addUnique(arrayOf("d", mintIdentifier))
|
||||||
|
|
||||||
|
fun TagArrayBuilder<MintRecommendationEvent>.kTag(kind: Int) = addUnique(arrayOf("k", kind.toString()))
|
||||||
|
|
||||||
|
fun TagArrayBuilder<MintRecommendationEvent>.mintUrl(url: String) = add(MintUrlTag.assemble(url))
|
||||||
|
|
||||||
|
fun TagArrayBuilder<MintRecommendationEvent>.aTag(
|
||||||
|
address: String,
|
||||||
|
relay: String? = null,
|
||||||
|
) = if (relay != null) {
|
||||||
|
add(arrayOf("a", address, relay))
|
||||||
|
} else {
|
||||||
|
add(arrayOf("a", address))
|
||||||
|
}
|
||||||
+32
@@ -0,0 +1,32 @@
|
|||||||
|
/*
|
||||||
|
* 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.nip87Ecash.recommendation
|
||||||
|
|
||||||
|
import com.vitorpamplona.quartz.nip01Core.core.TagArray
|
||||||
|
import com.vitorpamplona.quartz.nip87Ecash.MintUrlTag
|
||||||
|
|
||||||
|
fun TagArray.mintUrls(): List<String> = mapNotNull(MintUrlTag::parse)
|
||||||
|
|
||||||
|
fun TagArray.mintEventKind(): Int? = firstOrNull { it.size >= 2 && it[0] == "k" }?.get(1)?.toIntOrNull()
|
||||||
|
|
||||||
|
fun TagArray.dTag(): String? = firstOrNull { it.size >= 2 && it[0] == "d" }?.get(1)
|
||||||
|
|
||||||
|
fun TagArray.mintEventAddresses(): List<String> = filter { it.size >= 2 && it[0] == "a" }.map { it[1] }
|
||||||
@@ -156,12 +156,16 @@ import com.vitorpamplona.quartz.nip72ModCommunities.definition.CommunityDefiniti
|
|||||||
import com.vitorpamplona.quartz.nip72ModCommunities.follow.CommunityListEvent
|
import com.vitorpamplona.quartz.nip72ModCommunities.follow.CommunityListEvent
|
||||||
import com.vitorpamplona.quartz.nip75ZapGoals.GoalEvent
|
import com.vitorpamplona.quartz.nip75ZapGoals.GoalEvent
|
||||||
import com.vitorpamplona.quartz.nip78AppData.AppSpecificDataEvent
|
import com.vitorpamplona.quartz.nip78AppData.AppSpecificDataEvent
|
||||||
|
import com.vitorpamplona.quartz.nip7DThreads.ThreadEvent
|
||||||
import com.vitorpamplona.quartz.nip84Highlights.HighlightEvent
|
import com.vitorpamplona.quartz.nip84Highlights.HighlightEvent
|
||||||
import com.vitorpamplona.quartz.nip85TrustedAssertions.addressables.AddressableAssertionEvent
|
import com.vitorpamplona.quartz.nip85TrustedAssertions.addressables.AddressableAssertionEvent
|
||||||
import com.vitorpamplona.quartz.nip85TrustedAssertions.events.EventAssertionEvent
|
import com.vitorpamplona.quartz.nip85TrustedAssertions.events.EventAssertionEvent
|
||||||
import com.vitorpamplona.quartz.nip85TrustedAssertions.externalIds.ExternalIdAssertionEvent
|
import com.vitorpamplona.quartz.nip85TrustedAssertions.externalIds.ExternalIdAssertionEvent
|
||||||
import com.vitorpamplona.quartz.nip85TrustedAssertions.list.TrustProviderListEvent
|
import com.vitorpamplona.quartz.nip85TrustedAssertions.list.TrustProviderListEvent
|
||||||
import com.vitorpamplona.quartz.nip85TrustedAssertions.users.ContactCardEvent
|
import com.vitorpamplona.quartz.nip85TrustedAssertions.users.ContactCardEvent
|
||||||
|
import com.vitorpamplona.quartz.nip87Ecash.cashu.CashuMintEvent
|
||||||
|
import com.vitorpamplona.quartz.nip87Ecash.fedimint.FedimintEvent
|
||||||
|
import com.vitorpamplona.quartz.nip87Ecash.recommendation.MintRecommendationEvent
|
||||||
import com.vitorpamplona.quartz.nip88Polls.poll.PollEvent
|
import com.vitorpamplona.quartz.nip88Polls.poll.PollEvent
|
||||||
import com.vitorpamplona.quartz.nip88Polls.response.PollResponseEvent
|
import com.vitorpamplona.quartz.nip88Polls.response.PollResponseEvent
|
||||||
import com.vitorpamplona.quartz.nip89AppHandlers.definition.AppDefinitionEvent
|
import com.vitorpamplona.quartz.nip89AppHandlers.definition.AppDefinitionEvent
|
||||||
@@ -230,6 +234,7 @@ class EventFactory {
|
|||||||
CalendarEvent.KIND -> CalendarEvent(id, pubKey, createdAt, tags, content, sig)
|
CalendarEvent.KIND -> CalendarEvent(id, pubKey, createdAt, tags, content, sig)
|
||||||
CalendarTimeSlotEvent.KIND -> CalendarTimeSlotEvent(id, pubKey, createdAt, tags, content, sig)
|
CalendarTimeSlotEvent.KIND -> CalendarTimeSlotEvent(id, pubKey, createdAt, tags, content, sig)
|
||||||
CalendarRSVPEvent.KIND -> CalendarRSVPEvent(id, pubKey, createdAt, tags, content, sig)
|
CalendarRSVPEvent.KIND -> CalendarRSVPEvent(id, pubKey, createdAt, tags, content, sig)
|
||||||
|
CashuMintEvent.KIND -> CashuMintEvent(id, pubKey, createdAt, tags, content, sig)
|
||||||
CashuMintQuoteEvent.KIND -> CashuMintQuoteEvent(id, pubKey, createdAt, tags, content, sig)
|
CashuMintQuoteEvent.KIND -> CashuMintQuoteEvent(id, pubKey, createdAt, tags, content, sig)
|
||||||
CashuTokenEvent.KIND -> CashuTokenEvent(id, pubKey, createdAt, tags, content, sig)
|
CashuTokenEvent.KIND -> CashuTokenEvent(id, pubKey, createdAt, tags, content, sig)
|
||||||
CashuSpendingHistoryEvent.KIND -> CashuSpendingHistoryEvent(id, pubKey, createdAt, tags, content, sig)
|
CashuSpendingHistoryEvent.KIND -> CashuSpendingHistoryEvent(id, pubKey, createdAt, tags, content, sig)
|
||||||
@@ -265,6 +270,7 @@ class EventFactory {
|
|||||||
EphemeralChatEvent.KIND -> EphemeralChatEvent(id, pubKey, createdAt, tags, content, sig)
|
EphemeralChatEvent.KIND -> EphemeralChatEvent(id, pubKey, createdAt, tags, content, sig)
|
||||||
EphemeralChatListEvent.KIND -> EphemeralChatListEvent(id, pubKey, createdAt, tags, content, sig)
|
EphemeralChatListEvent.KIND -> EphemeralChatListEvent(id, pubKey, createdAt, tags, content, sig)
|
||||||
ExternalIdentitiesEvent.KIND -> ExternalIdentitiesEvent(id, pubKey, createdAt, tags, content, sig)
|
ExternalIdentitiesEvent.KIND -> ExternalIdentitiesEvent(id, pubKey, createdAt, tags, content, sig)
|
||||||
|
FedimintEvent.KIND -> FedimintEvent(id, pubKey, createdAt, tags, content, sig)
|
||||||
FileHeaderEvent.KIND -> FileHeaderEvent(id, pubKey, createdAt, tags, content, sig)
|
FileHeaderEvent.KIND -> FileHeaderEvent(id, pubKey, createdAt, tags, content, sig)
|
||||||
ProfileGalleryEntryEvent.KIND -> ProfileGalleryEntryEvent(id, pubKey, createdAt, tags, content, sig)
|
ProfileGalleryEntryEvent.KIND -> ProfileGalleryEntryEvent(id, pubKey, createdAt, tags, content, sig)
|
||||||
FileServersEvent.KIND -> FileServersEvent(id, pubKey, createdAt, tags, content, sig)
|
FileServersEvent.KIND -> FileServersEvent(id, pubKey, createdAt, tags, content, sig)
|
||||||
@@ -303,6 +309,7 @@ class EventFactory {
|
|||||||
MeetingRoomEvent.KIND -> MeetingRoomEvent(id, pubKey, createdAt, tags, content, sig)
|
MeetingRoomEvent.KIND -> MeetingRoomEvent(id, pubKey, createdAt, tags, content, sig)
|
||||||
MeetingRoomPresenceEvent.KIND -> MeetingRoomPresenceEvent(id, pubKey, createdAt, tags, content, sig)
|
MeetingRoomPresenceEvent.KIND -> MeetingRoomPresenceEvent(id, pubKey, createdAt, tags, content, sig)
|
||||||
MeetingSpaceEvent.KIND -> MeetingSpaceEvent(id, pubKey, createdAt, tags, content, sig)
|
MeetingSpaceEvent.KIND -> MeetingSpaceEvent(id, pubKey, createdAt, tags, content, sig)
|
||||||
|
MintRecommendationEvent.KIND -> MintRecommendationEvent(id, pubKey, createdAt, tags, content, sig)
|
||||||
MetadataEvent.KIND -> MetadataEvent(id, pubKey, createdAt, tags, content, sig)
|
MetadataEvent.KIND -> MetadataEvent(id, pubKey, createdAt, tags, content, sig)
|
||||||
MuteListEvent.KIND -> MuteListEvent(id, pubKey, createdAt, tags, content, sig)
|
MuteListEvent.KIND -> MuteListEvent(id, pubKey, createdAt, tags, content, sig)
|
||||||
NamedSiteEvent.KIND -> NamedSiteEvent(id, pubKey, createdAt, tags, content, sig)
|
NamedSiteEvent.KIND -> NamedSiteEvent(id, pubKey, createdAt, tags, content, sig)
|
||||||
@@ -352,6 +359,7 @@ class EventFactory {
|
|||||||
SearchRelayListEvent.KIND -> SearchRelayListEvent(id, pubKey, createdAt, tags, content, sig)
|
SearchRelayListEvent.KIND -> SearchRelayListEvent(id, pubKey, createdAt, tags, content, sig)
|
||||||
StatusEvent.KIND -> StatusEvent(id, pubKey, createdAt, tags, content, sig)
|
StatusEvent.KIND -> StatusEvent(id, pubKey, createdAt, tags, content, sig)
|
||||||
TextNoteEvent.KIND -> TextNoteEvent(id, pubKey, createdAt, tags, content, sig)
|
TextNoteEvent.KIND -> TextNoteEvent(id, pubKey, createdAt, tags, content, sig)
|
||||||
|
ThreadEvent.KIND -> ThreadEvent(id, pubKey, createdAt, tags, content, sig)
|
||||||
TextNoteModificationEvent.KIND -> TextNoteModificationEvent(id, pubKey, createdAt, tags, content, sig)
|
TextNoteModificationEvent.KIND -> TextNoteModificationEvent(id, pubKey, createdAt, tags, content, sig)
|
||||||
TokenEvent.KIND -> TokenEvent(id, pubKey, createdAt, tags, content, sig)
|
TokenEvent.KIND -> TokenEvent(id, pubKey, createdAt, tags, content, sig)
|
||||||
TorrentEvent.KIND -> TorrentEvent(id, pubKey, createdAt, tags, content, sig)
|
TorrentEvent.KIND -> TorrentEvent(id, pubKey, createdAt, tags, content, sig)
|
||||||
|
|||||||
@@ -245,95 +245,74 @@ class Url(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Removes dot segments from the given path as stated in
|
* Removes dot segments from the given path per
|
||||||
* ["RFC 3986, 5.2.4. Remove Dot Segments"](https://www.rfc-editor.org/rfc/rfc3986#section-5.2.4).
|
* [RFC 3986 §5.2.4](https://www.rfc-editor.org/rfc/rfc3986#section-5.2.4).
|
||||||
*
|
|
||||||
* @param path
|
|
||||||
* The path from which dot segments are to be removed.
|
|
||||||
*
|
|
||||||
* @return
|
|
||||||
* The path from which dot segments are removed.
|
|
||||||
*/
|
*/
|
||||||
fun removeDotSegments(path: String): String {
|
fun removeDotSegments(path: String): String {
|
||||||
// Initialize the input with the no-appended path components and the output
|
|
||||||
// with the empty string.
|
|
||||||
var input = path
|
var input = path
|
||||||
var output = ""
|
val output = StringBuilder()
|
||||||
|
|
||||||
// While the input is not empty, loop the following steps.
|
|
||||||
while (input.isNotEmpty()) {
|
while (input.isNotEmpty()) {
|
||||||
// If the input begins with a prefix of "../" or "./", then
|
when {
|
||||||
// remove that prefix from the input;
|
// A: Remove leading "../" or "./"
|
||||||
if (DOT_DOT_SLASH.find(input) != null) {
|
input.startsWith("../") -> {
|
||||||
input = DOT_DOT_SLASH.replaceFirst(input, "")
|
input = input.substring(3)
|
||||||
continue
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// If the input begins with a prefix of "/./" or "/.", where
|
input.startsWith("./") -> {
|
||||||
// "." is a complete path segment, then replace that prefix
|
input = input.substring(2)
|
||||||
// with "/" in the input.
|
}
|
||||||
if (SLASH_DOT_SLASH.find(input) != null) {
|
|
||||||
input = SLASH_DOT_SLASH.replaceFirst(input, "/")
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// If the input begins with a prefix of "/../" or "/..",
|
// B: Replace leading "/./" or "/." (end) with "/"
|
||||||
// where ".." is a complete path segment, then replace that
|
input.startsWith("/./") -> {
|
||||||
// prefix with "/" in the input and remove the last segment
|
input = "/" + input.substring(3)
|
||||||
// and its preceding "/" (if any) from the output.
|
}
|
||||||
if (SLASH_DOT_DOT_SLASH.find(input) != null) {
|
|
||||||
input = SLASH_DOT_DOT_SLASH.replaceFirst(input, "/")
|
|
||||||
output = dropLastSegment(output, true)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// If the input consists only of "." or "..", then remove
|
input == "/." -> {
|
||||||
// that from the input.
|
input = "/"
|
||||||
if (DOT_OR_DOT_DOT.find(input) != null) {
|
}
|
||||||
input = DOT_OR_DOT_DOT.replaceFirst(input, "")
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// Move the first path segment in the input buffer to the
|
// C: Replace leading "/../" or "/.." (end) with "/" and drop last output segment
|
||||||
// end of the output, including the initial "/" character
|
input.startsWith("/../") -> {
|
||||||
// (if any) and any subsequent characters up to, but not
|
input = "/" + input.substring(4)
|
||||||
// including, the next "/" character or the end of the input.
|
dropLastSegment(output)
|
||||||
val matchResult = MOVE_REGEX.find(input)
|
}
|
||||||
if (matchResult != null) {
|
|
||||||
input = matchResult.groups["remaining"]!!.value
|
input == "/.." -> {
|
||||||
output += matchResult.groups["firstsegment"]!!.value
|
input = "/"
|
||||||
continue
|
dropLastSegment(output)
|
||||||
|
}
|
||||||
|
|
||||||
|
// D: Input is just "." or ".."
|
||||||
|
input == "." || input == ".." -> {
|
||||||
|
input = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// E: Move the first path segment to output
|
||||||
|
else -> {
|
||||||
|
val startIdx = if (input.startsWith("/")) 1 else 0
|
||||||
|
val idx = input.indexOf('/', startIdx)
|
||||||
|
val segEnd = if (idx == -1) input.length else idx
|
||||||
|
output.append(input, 0, segEnd)
|
||||||
|
input = input.substring(segEnd)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return output
|
return output.toString()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Drops the last segment (= characters after the last slash) of a path and
|
* Removes the last segment and its preceding "/" from the output buffer.
|
||||||
* optionally the last slash. If the path doesn't contain slash, an empty string
|
* For example, "/a/b" becomes "/a" and "/a" becomes "".
|
||||||
* is returned.
|
|
||||||
*
|
|
||||||
* @param path
|
|
||||||
* The path.
|
|
||||||
*
|
|
||||||
* @param dropLastSlash
|
|
||||||
* Whether or not to drop the last slash if present.
|
|
||||||
*
|
|
||||||
* @return The path from which the last segment is removed.
|
|
||||||
*/
|
*/
|
||||||
fun dropLastSegment(
|
private fun dropLastSegment(output: StringBuilder) {
|
||||||
path: String,
|
val lastSlash = output.lastIndexOf('/')
|
||||||
dropLastSlash: Boolean,
|
if (lastSlash >= 0) {
|
||||||
): String {
|
output.delete(lastSlash, output.length)
|
||||||
// The regular expression for the target.
|
} else {
|
||||||
val m = if (dropLastSlash) DROP_LAST_SLASH_REGEX else DROP_LAST_SEGMENT_REGEX
|
output.clear()
|
||||||
|
}
|
||||||
// Find the target. (Any inputs matches the pattern.)
|
|
||||||
m.find(path)
|
|
||||||
|
|
||||||
// Drop the target.
|
|
||||||
return m.replaceFirst(path, "")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -416,34 +395,6 @@ class Url(
|
|||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
val DROP_LAST_SLASH_REGEX = Regex("\\/?[^/]*$")
|
|
||||||
val DROP_LAST_SEGMENT_REGEX = Regex("[^/]*$")
|
|
||||||
|
|
||||||
// If the input begins with a prefix of "../" or "./", then
|
|
||||||
// remove that prefix from the input;
|
|
||||||
val DOT_DOT_SLASH = Regex("^\\.?\\./")
|
|
||||||
|
|
||||||
// If the input begins with a prefix of "/./" or "/.", where
|
|
||||||
// "." is a complete path segment, then replace that prefix
|
|
||||||
// with "/" in the input.
|
|
||||||
val SLASH_DOT_SLASH = Regex("^\\/\\.(\\/|$)")
|
|
||||||
|
|
||||||
// If the input begins with a prefix of "/../" or "/..",
|
|
||||||
// where ".." is a complete path segment, then replace that
|
|
||||||
// prefix with "/" in the input and remove the last segment
|
|
||||||
// and its preceding "/" (if any) from the output.
|
|
||||||
val SLASH_DOT_DOT_SLASH = Regex("^\\/\\.\\.(\\/|$)")
|
|
||||||
|
|
||||||
// If the input consists only of "." or "..", then remove
|
|
||||||
// that from the input.
|
|
||||||
val DOT_OR_DOT_DOT = Regex("^\\.?\\.$")
|
|
||||||
|
|
||||||
// Move the first path segment in the input buffer to the
|
|
||||||
// end of the output, including the initial "/" character
|
|
||||||
// (if any) and any subsequent characters up to, but not
|
|
||||||
// including, the next "/" character or the end of the input.
|
|
||||||
val MOVE_REGEX = Regex("^(?<firstsegment>\\/?[^/]*)(?<remaining>.*)$")
|
|
||||||
|
|
||||||
private const val DEFAULT_SCHEME = "https"
|
private const val DEFAULT_SCHEME = "https"
|
||||||
private val SCHEME_PORT_MAP: Map<String, Int> =
|
private val SCHEME_PORT_MAP: Map<String, Int> =
|
||||||
mapOf(
|
mapOf(
|
||||||
|
|||||||
+154
-195
@@ -146,128 +146,132 @@ class DomainNameReader(
|
|||||||
private set
|
private set
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Reads and parses the current string to make sure the domain name started where it was supposed to,
|
* Validates the buffered domain-name prefix ([current]) that was accumulated before
|
||||||
* and the current domain name is correct.
|
* the caller started reading from the stream. Updates label/dot counters, detects
|
||||||
* @return The next state to use after reading the current.
|
* hex-numeric IPs, brackets for IPv6, and ASCII/international char boundaries.
|
||||||
|
*
|
||||||
|
* If an invalid character is found mid-string the domain is restarted from that
|
||||||
|
* position (e.g. `asdf%asdf.google.com` → restart at `asdf.google.com`).
|
||||||
|
*
|
||||||
|
* @return [ReaderNextState.ValidDomainName] when the prefix is acceptable,
|
||||||
|
* [ReaderNextState.InvalidDomainName] otherwise.
|
||||||
*/
|
*/
|
||||||
private fun readCurrent(): ReaderNextState {
|
private fun readCurrent(): ReaderNextState {
|
||||||
if (current != null) {
|
if (current == null) {
|
||||||
// Handles the case where the string is ".hello"
|
startDomainName = buffer.length
|
||||||
if (current.length == 1 && isDot(current[0])) {
|
return ReaderNextState.ValidDomainName
|
||||||
return ReaderNextState.InvalidDomainName
|
}
|
||||||
} else if (current.length == 3 && current.isDotPercent()) {
|
|
||||||
|
// A lone dot or percent-encoded dot is never a valid domain start.
|
||||||
|
if ((current.length == 1 && isDot(current[0])) ||
|
||||||
|
(current.length == 3 && current.isDotPercent())
|
||||||
|
) {
|
||||||
|
return ReaderNextState.InvalidDomainName
|
||||||
|
}
|
||||||
|
|
||||||
|
startDomainName = buffer.length - current.length
|
||||||
|
numeric = true
|
||||||
|
|
||||||
|
// Index into `current` where we'd restart the domain if we hit an invalid char.
|
||||||
|
var newStart = 0
|
||||||
|
|
||||||
|
val chars = current.toCharArray()
|
||||||
|
val length = chars.size
|
||||||
|
|
||||||
|
// Detect hex literal prefix (0x...)
|
||||||
|
var isAllHexSoFar = length > 2 && chars[0] == '0' && (chars[1] == 'x' || chars[1] == 'X')
|
||||||
|
var lastWasAscii = length > 0 && chars[0].code < INTERNATIONAL_CHAR_START
|
||||||
|
|
||||||
|
var index = if (isAllHexSoFar) 2 else 0
|
||||||
|
|
||||||
|
while (index < length) {
|
||||||
|
val ch = chars[index]
|
||||||
|
val isAscii = ch.code < INTERNATIONAL_CHAR_START
|
||||||
|
|
||||||
|
currentLabelLength++
|
||||||
|
topLevelLength = currentLabelLength
|
||||||
|
|
||||||
|
if (currentLabelLength > MAX_LABEL_LENGTH) {
|
||||||
return ReaderNextState.InvalidDomainName
|
return ReaderNextState.InvalidDomainName
|
||||||
}
|
}
|
||||||
|
|
||||||
// The location where the domain name started.
|
when {
|
||||||
startDomainName = buffer.length - current.length
|
isDot(ch) -> {
|
||||||
|
|
||||||
// flag that the domain is currently all numbers and/or dots.
|
|
||||||
numeric = true
|
|
||||||
|
|
||||||
// If an invalid char is found, we can just restart the domain from there.
|
|
||||||
var newStart = 0
|
|
||||||
|
|
||||||
val currArray = current.toCharArray()
|
|
||||||
val length = currArray.size
|
|
||||||
|
|
||||||
// hex special case
|
|
||||||
var isAllHexSoFar =
|
|
||||||
length > 2 && (currArray[0] == '0' && (currArray[1] == 'x' || currArray[1] == 'X'))
|
|
||||||
|
|
||||||
var lastWasAscii = length > 0 && currArray[0].code < INTERNATIONAL_CHAR_START
|
|
||||||
|
|
||||||
var index = if (isAllHexSoFar) 2 else 0
|
|
||||||
var done = false
|
|
||||||
var isAscii = false
|
|
||||||
|
|
||||||
while (index < length && !done) {
|
|
||||||
// get the current character and update length counts.
|
|
||||||
val curr = currArray[index]
|
|
||||||
isAscii = curr.code < INTERNATIONAL_CHAR_START
|
|
||||||
|
|
||||||
currentLabelLength++
|
|
||||||
topLevelLength = currentLabelLength
|
|
||||||
|
|
||||||
// Is the length of the last part > 64 (plus one since we just incremented)
|
|
||||||
if (currentLabelLength > MAX_LABEL_LENGTH) {
|
|
||||||
return ReaderNextState.InvalidDomainName
|
|
||||||
} else if (isDot(curr)) {
|
|
||||||
// found a dot. Increment dot count, and reset last length
|
|
||||||
dots++
|
dots++
|
||||||
currentLabelLength = 0
|
currentLabelLength = 0
|
||||||
} else if (curr == '[') {
|
}
|
||||||
|
|
||||||
|
ch == '[' -> {
|
||||||
seenBracket = true
|
seenBracket = true
|
||||||
numeric = false
|
numeric = false
|
||||||
} else if (curr == '%' && index + 2 < length && isHex(currArray[index + 1]) && isHex(currArray[index + 2])) {
|
}
|
||||||
// handle url encoded dot
|
|
||||||
if (currArray[index + 1] == '2' && currArray[index + 2] == 'e') {
|
ch == '%' && index + 2 < length && isHex(chars[index + 1]) && isHex(chars[index + 2]) -> {
|
||||||
|
// Percent-encoded byte; check for encoded dot (%2e)
|
||||||
|
if (chars[index + 1] == '2' && chars[index + 2] == 'e') {
|
||||||
dots++
|
dots++
|
||||||
currentLabelLength = 0
|
currentLabelLength = 0
|
||||||
} else {
|
} else {
|
||||||
numeric = false
|
numeric = false
|
||||||
}
|
}
|
||||||
index += 2
|
index += 2
|
||||||
} else if (isAllHexSoFar) {
|
}
|
||||||
// if it's a valid character in the domain that is not numeric
|
|
||||||
if (!isHex(curr)) {
|
isAllHexSoFar && !isHex(ch) -> {
|
||||||
numeric = false
|
// Thought it was hex but this char isn't — reprocess as non-hex
|
||||||
isAllHexSoFar = false
|
numeric = false
|
||||||
index-- // backtrack to rerun last character knowing it isn't hex.
|
isAllHexSoFar = false
|
||||||
}
|
index-- // backtrack to re-evaluate this char
|
||||||
} else if (isAscii == lastWasAscii && (isAlpha(curr) || curr == '-' || !isAscii)) {
|
}
|
||||||
// we don't allow mixed domains: doesn't come here if it changed form ascii to not ascii.
|
|
||||||
|
isAscii == lastWasAscii && (isAlpha(ch) || ch == '-' || !isAscii) -> {
|
||||||
|
// Valid domain character (same script as previous)
|
||||||
numeric = false
|
numeric = false
|
||||||
lastWasAscii = isAscii
|
lastWasAscii = isAscii
|
||||||
} else if (isAscii != lastWasAscii) {
|
}
|
||||||
// if its not _numeric and not alphabetical, then restart searching for a domain from this point.
|
|
||||||
|
isAscii != lastWasAscii -> {
|
||||||
|
// Script boundary (ASCII ↔ international) — restart domain from here
|
||||||
newStart = index
|
newStart = index
|
||||||
currentLabelLength = 0
|
resetDomainCounters()
|
||||||
topLevelLength = 0
|
|
||||||
numeric = true
|
|
||||||
dots = 0
|
|
||||||
// done = true
|
|
||||||
|
|
||||||
lastWasAscii = isAscii
|
lastWasAscii = isAscii
|
||||||
} else if (index == 0) {
|
|
||||||
if (curr in UrlDetector.CANNOT_BEGIN_URLS_WITH) {
|
|
||||||
newStart = index + 1
|
|
||||||
currentLabelLength = 0
|
|
||||||
topLevelLength = 0
|
|
||||||
numeric = true
|
|
||||||
dots = 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
index++
|
|
||||||
}
|
|
||||||
|
|
||||||
// An invalid character for the domain was found somewhere in the current buffer.
|
|
||||||
// cut the first part of the domain out. For example:
|
|
||||||
// http://asdf%asdf.google.com <- asdf.google.com is still valid, so restart from the %
|
|
||||||
if (newStart > 0) {
|
|
||||||
// make sure the location is not at the end. Otherwise the thing is just invalid.
|
|
||||||
|
|
||||||
if (newStart < current.length) {
|
|
||||||
buffer.clear()
|
|
||||||
buffer.append(current.substring(newStart))
|
|
||||||
|
|
||||||
// cut out the previous part, so now the domain name has to be from here.
|
|
||||||
startDomainName = 0
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// now after cutting if the buffer is just "." newStart > current (last character in current is invalid)
|
index == 0 && ch in UrlDetector.CANNOT_BEGIN_URLS_WITH -> {
|
||||||
if (newStart >= current.length || buffer.toString() == ".") {
|
// Invalid leading char — restart after it
|
||||||
return ReaderNextState.InvalidDomainName
|
newStart = index + 1
|
||||||
|
resetDomainCounters()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
index++
|
||||||
startDomainName = buffer.length
|
}
|
||||||
|
|
||||||
|
// If we found an invalid region, trim the buffer to start after it.
|
||||||
|
if (newStart > 0) {
|
||||||
|
if (newStart < current.length) {
|
||||||
|
buffer.clear()
|
||||||
|
buffer.append(current.substring(newStart))
|
||||||
|
startDomainName = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newStart >= current.length || buffer.toString() == ".") {
|
||||||
|
return ReaderNextState.InvalidDomainName
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// all else is good, return OK
|
|
||||||
return ReaderNextState.ValidDomainName
|
return ReaderNextState.ValidDomainName
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resets domain tracking counters when the domain start is being moved forward.
|
||||||
|
*/
|
||||||
|
private fun resetDomainCounters() {
|
||||||
|
currentLabelLength = 0
|
||||||
|
topLevelLength = 0
|
||||||
|
numeric = true
|
||||||
|
dots = 0
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Reads the Dns and returns the next state the state machine should take in throwing this out, or continue processing
|
* Reads the Dns and returns the next state the state machine should take in throwing this out, or continue processing
|
||||||
* if this is a valid domain name.
|
* if this is a valid domain name.
|
||||||
@@ -527,111 +531,66 @@ class DomainNameReader(
|
|||||||
* @return Returns true if it's a valid ipv4 address
|
* @return Returns true if it's a valid ipv4 address
|
||||||
*/
|
*/
|
||||||
private fun isValidIpv4(testDomain: String): Boolean {
|
private fun isValidIpv4(testDomain: String): Boolean {
|
||||||
var valid = false
|
if (testDomain.isEmpty()) return false
|
||||||
val length: Int = testDomain.length
|
|
||||||
if (length > 0) {
|
|
||||||
// handling format without dots. Ex: http://2123123123123/path/a, http://0x8242343/aksdjf
|
|
||||||
if (dots == 0) {
|
|
||||||
try {
|
|
||||||
val value: Long
|
|
||||||
if (length > 2 && testDomain[0] == '0' && testDomain[1] == 'x') { // hex
|
|
||||||
// digit must be within ['0', '9'] or ['A', 'F'] or ['a', 'f']
|
|
||||||
for (c in 2..<length) {
|
|
||||||
val d: Char = testDomain[c]
|
|
||||||
if ((d < '0' || (d in ':'..<'A') || (d in 'G'..<'a') || d > 'f')) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
value = testDomain.substring(2).toLong(16)
|
|
||||||
} else if (testDomain[0] == '0') { // octal
|
|
||||||
// digit must be within ['0', '7']
|
|
||||||
for (c in 1..<length) {
|
|
||||||
val d: Char = testDomain[c]
|
|
||||||
if (d !in '0'..'7') {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
value = testDomain.substring(1).toLong(8)
|
|
||||||
} else { // decimal
|
|
||||||
// digit must be within ['0', '9']
|
|
||||||
for (c in 0..<length) {
|
|
||||||
val d: Char = testDomain[c]
|
|
||||||
if (d !in '0'..'9') {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
value = testDomain.toLong()
|
|
||||||
}
|
|
||||||
valid = value in MIN_NUMERIC_DOMAIN_VALUE..MAX_NUMERIC_DOMAIN_VALUE
|
|
||||||
} catch (_: NumberFormatException) {
|
|
||||||
valid = false
|
|
||||||
}
|
|
||||||
} else if (dots == 3) {
|
|
||||||
// Dotted decimal/hex/octal format
|
|
||||||
val parts: List<String> = splitByDot(testDomain)
|
|
||||||
valid = true
|
|
||||||
|
|
||||||
// check each part of the ip and make sure its valid.
|
// Dotless format: http://2123123123123/path, http://0x8242343/aksdjf
|
||||||
var i = 0
|
if (dots == 0) {
|
||||||
while (i < parts.size && valid) {
|
val value = parseNumericLiteral(testDomain) ?: return false
|
||||||
val part = parts[i]
|
return value in MIN_NUMERIC_DOMAIN_VALUE..MAX_NUMERIC_DOMAIN_VALUE
|
||||||
val partLen: Int = part.length
|
}
|
||||||
if (partLen > 0) {
|
|
||||||
val parsedNum: String
|
|
||||||
val base: Int
|
|
||||||
if (partLen > 2 && part[0] == '0' && part[1] == 'x') { // dotted hex
|
|
||||||
// digit must be within ['0', '9'] or ['A', 'F'] or ['a', 'f']
|
|
||||||
for (c in 2..<partLen) {
|
|
||||||
val d: Char = part[c]
|
|
||||||
if ((d < '0' || (d in ':'..<'A') || (d in 'G'..<'a') || d > 'f')) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
parsedNum = part.substring(2)
|
|
||||||
base = 16
|
|
||||||
} else if (part[0] == '0') { // dotted octal
|
|
||||||
// digit must be within ['0', '7']
|
|
||||||
for (c in 1..<partLen) {
|
|
||||||
val d: Char = part[c]
|
|
||||||
if (d !in '0'..'7') {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
parsedNum = part.substring(1)
|
|
||||||
base = 8
|
|
||||||
} else { // dotted decimal
|
|
||||||
// digit must be within ['0', '9']
|
|
||||||
for (c in 0..<partLen) {
|
|
||||||
val d: Char = part[c]
|
|
||||||
if (d !in '0'..'9') {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
parsedNum = part
|
|
||||||
base = 10
|
|
||||||
}
|
|
||||||
|
|
||||||
val section =
|
// Dotted format: must have exactly 4 parts (3 dots)
|
||||||
if (parsedNum.isEmpty()) {
|
if (dots != 3) return false
|
||||||
0
|
|
||||||
} else {
|
val parts = splitByDot(testDomain)
|
||||||
try {
|
for (part in parts) {
|
||||||
parsedNum.toInt(base)
|
if (part.isEmpty()) return false
|
||||||
} catch (_: NumberFormatException) {
|
val section = parseNumericLiteral(part) ?: return false
|
||||||
return false
|
if (section !in MIN_IP_PART..MAX_IP_PART) return false
|
||||||
}
|
}
|
||||||
}
|
return true
|
||||||
if (section !in MIN_IP_PART..MAX_IP_PART) {
|
}
|
||||||
valid = false
|
|
||||||
}
|
/**
|
||||||
} else {
|
* Parses a numeric literal that may be decimal, hexadecimal (0x prefix), or octal (0 prefix).
|
||||||
valid = false
|
* Validates digit ranges before parsing to avoid exceptions.
|
||||||
}
|
* @return The parsed value as a Long, or null if the string is not a valid numeric literal.
|
||||||
i++
|
*/
|
||||||
}
|
private fun parseNumericLiteral(s: String): Long? {
|
||||||
|
if (s.isEmpty()) return 0L
|
||||||
|
|
||||||
|
val digits: String
|
||||||
|
val base: Int
|
||||||
|
|
||||||
|
if (s.length > 2 && s[0] == '0' && (s[1] == 'x' || s[1] == 'X')) {
|
||||||
|
// Hexadecimal
|
||||||
|
digits = s.substring(2)
|
||||||
|
base = 16
|
||||||
|
for (c in digits) {
|
||||||
|
if (!isHex(c)) return null
|
||||||
|
}
|
||||||
|
} else if (s[0] == '0') {
|
||||||
|
// Octal
|
||||||
|
digits = s.substring(1)
|
||||||
|
base = 8
|
||||||
|
for (c in digits) {
|
||||||
|
if (c !in '0'..'7') return null
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Decimal
|
||||||
|
digits = s
|
||||||
|
base = 10
|
||||||
|
for (c in digits) {
|
||||||
|
if (c !in '0'..'9') return null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return valid
|
|
||||||
|
if (digits.isEmpty()) return 0L
|
||||||
|
return try {
|
||||||
|
digits.toLong(base)
|
||||||
|
} catch (_: NumberFormatException) {
|
||||||
|
null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
+59
-89
@@ -315,81 +315,75 @@ class UrlDetector(
|
|||||||
while (!reader.eof()) {
|
while (!reader.eof()) {
|
||||||
val curr = reader.read()
|
val curr = reader.read()
|
||||||
|
|
||||||
// if we match a slash, look for a second one.
|
|
||||||
if (curr == '/') {
|
if (curr == '/') {
|
||||||
buffer.append(curr)
|
buffer.append(curr)
|
||||||
if (numSlashes == 1) {
|
if (numSlashes == 1) {
|
||||||
// return only if its an approved protocol. This can be expanded to allow others
|
// Two slashes found — check for a valid scheme like "http://"
|
||||||
val schemeStartIndex: Int = findValidSchemeStartIndex(buffer.toString())
|
val schemeStartIndex = findValidSchemeStartIndex(buffer.toString())
|
||||||
if (schemeStartIndex >= 0) {
|
if (schemeStartIndex >= 0) {
|
||||||
buffer.deleteRange(0, schemeStartIndex)
|
buffer.deleteRange(0, schemeStartIndex)
|
||||||
currentUrlMarker.setIndex(UrlPart.SCHEME, 0)
|
currentUrlMarker.setIndex(UrlPart.SCHEME, 0)
|
||||||
return true
|
return true
|
||||||
} else {
|
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
numSlashes++
|
numSlashes++
|
||||||
} else if (curr == ' ') {
|
} else if (curr == ' ') {
|
||||||
// if we find a space or end of input, then nothing found.
|
|
||||||
buffer.append(curr)
|
buffer.append(curr)
|
||||||
return false
|
return false
|
||||||
} else if (curr == '[') { // if we're starting to see an ipv6 address
|
} else if (curr == '[') {
|
||||||
reader.goBack() // unread the '[', so that we can start looking for ipv6
|
// Start of IPv6 — unread and let the caller handle it
|
||||||
return false
|
|
||||||
} else if (originalLength > 0 && numSlashes == 0 && CharUtils.isAlpha(curr)) {
|
|
||||||
// If we had already read something before the : and we are matching regardless of slashes, assume it's a scheme
|
|
||||||
|
|
||||||
// Add the slashes to the end of the scheme so it matches what's in the scheme list
|
|
||||||
val schemeStartIndex = findValidSchemeNoSlashesStartIndex(buffer.toString())
|
|
||||||
if (schemeStartIndex >= 0) {
|
|
||||||
if (schemeStartIndex > 0) {
|
|
||||||
buffer.deleteRange(0, schemeStartIndex)
|
|
||||||
}
|
|
||||||
currentUrlMarker.setIndex(UrlPart.SCHEME, 0)
|
|
||||||
reader.goBack()
|
|
||||||
return true
|
|
||||||
} else {
|
|
||||||
reader.goBack()
|
|
||||||
return readUserPass(0)
|
|
||||||
}
|
|
||||||
// If this didn't match a defined scheme, continue processing as usual
|
|
||||||
} else if (originalLength > 0 || numSlashes > 0 || !CharUtils.isAlpha(curr)) {
|
|
||||||
// if it's not a character a-z or A-Z then assume we aren't matching scheme, but instead
|
|
||||||
// matching username and password.
|
|
||||||
// Add the slashes to the end of the scheme so it matches what's in the scheme list
|
|
||||||
val schemeStartIndex = findValidSchemeNoSlashesStartIndex(buffer.toString())
|
|
||||||
if (schemeStartIndex >= 0) {
|
|
||||||
if (schemeStartIndex > 0) {
|
|
||||||
buffer.deleteRange(0, schemeStartIndex)
|
|
||||||
}
|
|
||||||
currentUrlMarker.setIndex(UrlPart.SCHEME, 0)
|
|
||||||
reader.goBack()
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
reader.goBack()
|
reader.goBack()
|
||||||
return readUserPass(0)
|
return false
|
||||||
|
} else if (originalLength > 0 || numSlashes > 0 || !CharUtils.isAlpha(curr)) {
|
||||||
|
// Not a plain alpha char continuing a potential scheme name, or we already
|
||||||
|
// had content before the colon / had slashes. Try matching a scheme without
|
||||||
|
// slashes (e.g. "nostr:npub1...") then fall back to username:password.
|
||||||
|
return trySchemeNoSlashesOrUserPass()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun findValidSchemeStartIndex(optionalScheme: String): Int {
|
/**
|
||||||
val optionalSchemeLowercase = optionalScheme.lowercase()
|
* Attempts to match the buffer as a scheme without slashes (e.g. "nostr:").
|
||||||
return VALID_SCHEMES
|
* If that fails, treats the content as a potential username:password.
|
||||||
.filter(optionalSchemeLowercase::endsWith)
|
*/
|
||||||
.map(optionalSchemeLowercase::lastIndexOf)
|
private fun trySchemeNoSlashesOrUserPass(): Boolean {
|
||||||
.firstOrNull() ?: -1
|
val schemeStartIndex = findValidSchemeNoSlashesStartIndex(buffer.toString())
|
||||||
|
if (schemeStartIndex >= 0) {
|
||||||
|
if (schemeStartIndex > 0) {
|
||||||
|
buffer.deleteRange(0, schemeStartIndex)
|
||||||
|
}
|
||||||
|
currentUrlMarker.setIndex(UrlPart.SCHEME, 0)
|
||||||
|
reader.goBack()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
reader.goBack()
|
||||||
|
return readUserPass(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun findValidSchemeNoSlashesStartIndex(optionalScheme: String): Int {
|
private fun findValidSchemeStartIndex(optionalScheme: String): Int = findSchemeSuffix(optionalScheme, VALID_SCHEMES)
|
||||||
val optionalSchemeLowercase = optionalScheme.lowercase()
|
|
||||||
return VALID_SCHEMES_NO_SLASHES
|
private fun findValidSchemeNoSlashesStartIndex(optionalScheme: String): Int = findSchemeSuffix(optionalScheme, VALID_SCHEMES_NO_SLASHES)
|
||||||
.filter(optionalSchemeLowercase::endsWith)
|
|
||||||
.map(optionalSchemeLowercase::lastIndexOf)
|
/**
|
||||||
.firstOrNull() ?: -1
|
* Checks if [buffer] ends with any of the [schemes] (case-insensitive)
|
||||||
|
* and returns the start index of the match, or -1 if none match.
|
||||||
|
*/
|
||||||
|
private fun findSchemeSuffix(
|
||||||
|
buffer: String,
|
||||||
|
schemes: List<String>,
|
||||||
|
): Int {
|
||||||
|
val len = buffer.length
|
||||||
|
for (scheme in schemes) {
|
||||||
|
val schemeLen = scheme.length
|
||||||
|
if (len >= schemeLen && buffer.regionMatches(len - schemeLen, scheme, 0, schemeLen, ignoreCase = true)) {
|
||||||
|
return len - schemeLen
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -1
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -618,57 +612,33 @@ class UrlDetector(
|
|||||||
var endsOnASlash = true
|
var endsOnASlash = true
|
||||||
|
|
||||||
while (!reader.eof()) {
|
while (!reader.eof()) {
|
||||||
// read the next char
|
|
||||||
val curr = reader.read()
|
val curr = reader.read()
|
||||||
|
|
||||||
if (curr == ' ') {
|
if (curr == ' ') {
|
||||||
// if end of state and we got here, then the url is valid
|
return readEnd(if (isPathValid(endsOnASlash)) ReadEndState.ValidUrl else ReadEndState.InvalidUrl)
|
||||||
// if it is not just a word/word
|
|
||||||
if (
|
|
||||||
currentUrlMarker.hasScheme() ||
|
|
||||||
currentUrlMarker.hasPort() ||
|
|
||||||
currentUrlMarker.hasUsernamePassword() ||
|
|
||||||
!isSingleLevelLabel ||
|
|
||||||
endsOnASlash
|
|
||||||
) {
|
|
||||||
return readEnd(ReadEndState.ValidUrl)
|
|
||||||
} else {
|
|
||||||
return readEnd(ReadEndState.InvalidUrl)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// append the char
|
|
||||||
buffer.append(curr)
|
buffer.append(curr)
|
||||||
|
|
||||||
// now see if we move to another state.
|
if (curr == '?') return readQueryString()
|
||||||
if (curr == '?') {
|
if (curr == '#') return readFragment()
|
||||||
// if ? read query string
|
|
||||||
return readQueryString()
|
|
||||||
} else if (curr == '#') {
|
|
||||||
// if # read the fragment
|
|
||||||
return readFragment()
|
|
||||||
}
|
|
||||||
|
|
||||||
endsOnASlash = curr == '/'
|
endsOnASlash = curr == '/'
|
||||||
}
|
}
|
||||||
|
|
||||||
// end of input then this url is good.
|
return readEnd(if (isPathValid(endsOnASlash)) ReadEndState.ValidUrl else ReadEndState.InvalidUrl)
|
||||||
// if end of state and we got here, then the url is valid
|
}
|
||||||
// if it is not just a word/word
|
|
||||||
// no need to check for query and fragments
|
/**
|
||||||
// here we accept urls that end in /
|
* A path is valid if the URL has additional context (scheme, port, credentials)
|
||||||
if (
|
* or if it's not an ambiguous single-level label like "word/word".
|
||||||
currentUrlMarker.hasScheme() ||
|
*/
|
||||||
|
private fun isPathValid(endsOnASlash: Boolean): Boolean =
|
||||||
|
currentUrlMarker.hasScheme() ||
|
||||||
currentUrlMarker.hasPort() ||
|
currentUrlMarker.hasPort() ||
|
||||||
currentUrlMarker.hasUsernamePassword() ||
|
currentUrlMarker.hasUsernamePassword() ||
|
||||||
!isSingleLevelLabel ||
|
!isSingleLevelLabel ||
|
||||||
endsOnASlash
|
endsOnASlash
|
||||||
) {
|
|
||||||
return readEnd(ReadEndState.ValidUrl)
|
|
||||||
} else {
|
|
||||||
return readEnd(ReadEndState.InvalidUrl)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The url has been read to here. Remember the url if its valid, and reset state.
|
* The url has been read to here. Remember the url if its valid, and reset state.
|
||||||
|
|||||||
Reference in New Issue
Block a user