Adds support for Interactive Stories

This commit is contained in:
Vitor Pamplona
2024-11-26 19:07:12 -05:00
parent 034ee46543
commit bdf012f641
24 changed files with 994 additions and 92 deletions
@@ -132,6 +132,17 @@ class DraftEvent(
create(dTag, originalNote, tagsWithMarkers, signer, createdAt, onReady)
}
fun create(
dTag: String,
originalNote: InteractiveStoryBaseEvent,
signer: NostrSigner,
createdAt: Long = TimeUtils.now(),
onReady: (DraftEvent) -> Unit,
) {
val tags = mutableListOf<Array<String>>()
create(dTag, originalNote, tags, signer, createdAt, onReady)
}
fun create(
dTag: String,
originalNote: LiveActivitiesChatMessageEvent,
@@ -556,7 +556,7 @@ class HostStub(
interface AddressableEvent {
fun dTag(): String
fun address(): ATag
fun address(relayHint: String? = null): ATag
fun addressTag(): String
}
@@ -574,7 +574,7 @@ open class BaseAddressableEvent(
AddressableEvent {
override fun dTag() = tags.firstOrNull { it.size > 1 && it[0] == "d" }?.get(1) ?: ""
override fun address() = ATag(kind, pubKey, dTag(), null)
override fun address(relayHint: String?) = ATag(kind, pubKey, dTag(), relayHint)
/**
* Creates the tag in a memory effecient way (without creating the ATag class
@@ -26,7 +26,7 @@ import com.vitorpamplona.quartz.events.nip46.NostrConnectEvent
class EventFactory {
companion object {
val additionalFactories: MutableMap<Int, (HexKey, HexKey, Long, Array<Array<String>>, String, HexKey) -> Event> = mutableMapOf()
val factories: MutableMap<Int, (HexKey, HexKey, Long, Array<Array<String>>, String, HexKey) -> Event> = mutableMapOf()
fun create(
id: String,
@@ -99,6 +99,9 @@ class EventFactory {
GoalEvent.KIND -> GoalEvent(id, pubKey, createdAt, tags, content, sig)
HighlightEvent.KIND -> HighlightEvent(id, pubKey, createdAt, tags, content, sig)
HTTPAuthorizationEvent.KIND -> HTTPAuthorizationEvent(id, pubKey, createdAt, tags, content, sig)
InteractiveStoryPrologueEvent.KIND -> InteractiveStoryPrologueEvent(id, pubKey, createdAt, tags, content, sig)
InteractiveStorySceneEvent.KIND -> InteractiveStorySceneEvent(id, pubKey, createdAt, tags, content, sig)
InteractiveStoryReadingStateEvent.KIND -> InteractiveStoryReadingStateEvent(id, pubKey, createdAt, tags, content, sig)
LiveActivitiesChatMessageEvent.KIND -> LiveActivitiesChatMessageEvent(id, pubKey, createdAt, tags, content, sig)
LiveActivitiesEvent.KIND -> LiveActivitiesEvent(id, pubKey, createdAt, tags, content, sig)
LnZapEvent.KIND -> LnZapEvent(id, pubKey, createdAt, tags, content, sig)
@@ -141,7 +144,7 @@ class EventFactory {
VideoViewEvent.KIND -> VideoViewEvent(id, pubKey, createdAt, tags, content, sig)
WikiNoteEvent.KIND -> WikiNoteEvent(id, pubKey, createdAt, tags, content, sig)
else -> {
additionalFactories[kind]?.let {
factories[kind]?.let {
return it(id, pubKey, createdAt, tags, content, sig)
}
@@ -0,0 +1,115 @@
/**
* Copyright (c) 2024 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.quartz.events
import com.vitorpamplona.quartz.encoders.ATag
import com.vitorpamplona.quartz.encoders.HexKey
import com.vitorpamplona.quartz.encoders.Nip92MediaAttachments
open class InteractiveStoryBaseEvent(
id: HexKey,
pubKey: HexKey,
createdAt: Long,
kind: Int,
tags: Array<Array<String>>,
content: String,
sig: HexKey,
) : BaseAddressableEvent(id, pubKey, createdAt, kind, tags, content, sig) {
fun title() = firstTag("title")
fun summary() = firstTag("summary")
fun image() = firstTag("image")
fun options() =
tags
.filter { it.size > 2 && it[0] == "option" }
.mapNotNull { ATag.parse(it[2], it.getOrNull(3))?.let { aTag -> StoryOption(it[1], aTag) } }
companion object {
fun generalTags(
content: String,
zapReceiver: List<ZapSplitSetup>? = null,
markAsSensitive: Boolean = false,
zapRaiserAmount: Long? = null,
geohash: String? = null,
nip94attachments: List<FileHeaderEvent>? = null,
): Array<Array<String>> {
val tags = mutableListOf<Array<String>>()
findHashtags(content).forEach {
val lowercaseTag = it.lowercase()
tags.add(arrayOf("t", it))
if (it != lowercaseTag) {
tags.add(arrayOf("t", it.lowercase()))
}
}
findURLs(content).forEach { tags.add(arrayOf("r", it)) }
zapReceiver?.forEach {
tags.add(arrayOf("zap", it.lnAddressOrPubKeyHex, it.relay ?: "", it.weight.toString()))
}
if (markAsSensitive) {
tags.add(arrayOf("content-warning", ""))
}
zapRaiserAmount?.let { tags.add(arrayOf("zapraiser", "$it")) }
geohash?.let { tags.addAll(geohashMipMap(it)) }
nip94attachments?.let {
it.forEach {
Nip92MediaAttachments().convertFromFileHeader(it)?.let {
tags.add(it)
}
}
}
return tags.toTypedArray()
}
fun makeTags(
baseId: String,
alt: String,
title: String,
summary: String? = null,
image: String? = null,
options: List<StoryOption> = emptyList(),
): Array<Array<String>> =
(
listOfNotNull(
arrayOf("d", baseId),
arrayOf("title", title),
summary?.let { arrayOf("summary", it) },
image?.let { arrayOf("image", it) },
arrayOf("alt", alt),
) +
options.map {
val relayUrl = it.address.relay
if (relayUrl != null) {
arrayOf("option", it.option, it.address.toTag(), relayUrl)
} else {
arrayOf("option", it.option, it.address.toTag())
}
}
).toTypedArray()
}
}
class StoryOption(
val option: String,
val address: ATag,
)
@@ -0,0 +1,78 @@
/**
* Copyright (c) 2024 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.quartz.events
import com.vitorpamplona.quartz.encoders.ATag
import com.vitorpamplona.quartz.encoders.HexKey
import com.vitorpamplona.quartz.signers.NostrSigner
import com.vitorpamplona.quartz.utils.TimeUtils
class InteractiveStoryPrologueEvent(
id: HexKey,
pubKey: HexKey,
createdAt: Long,
tags: Array<Array<String>>,
content: String,
sig: HexKey,
) : InteractiveStoryBaseEvent(id, pubKey, createdAt, KIND, tags, content, sig) {
companion object {
const val KIND = 30296
const val ALT = "The prologue of an interative story called "
fun createAddressATag(
pubKey: HexKey,
dtag: String,
): ATag = ATag(KIND, pubKey, dtag, null)
fun createAddressTag(
pubKey: HexKey,
dtag: String,
): String = ATag.assembleATag(KIND, pubKey, dtag)
fun create(
baseId: String,
title: String,
content: String,
options: List<StoryOption>,
summary: String? = null,
image: String? = null,
zapReceiver: List<ZapSplitSetup>? = null,
markAsSensitive: Boolean = false,
zapRaiserAmount: Long? = null,
geohash: String? = null,
nip94attachments: List<FileHeaderEvent>? = null,
signer: NostrSigner,
createdAt: Long = TimeUtils.now(),
isDraft: Boolean,
onReady: (InteractiveStoryPrologueEvent) -> Unit,
) {
val tags =
makeTags(baseId, ALT + title, title, summary, image, options) +
generalTags(content, zapReceiver, markAsSensitive, zapRaiserAmount, geohash, nip94attachments)
if (isDraft) {
signer.assembleRumor(createdAt, KIND, tags, content, onReady)
} else {
signer.sign(createdAt, KIND, tags, content, onReady)
}
}
}
}
@@ -0,0 +1,143 @@
/**
* Copyright (c) 2024 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.quartz.events
import androidx.compose.runtime.Immutable
import com.vitorpamplona.quartz.encoders.ATag
import com.vitorpamplona.quartz.encoders.HexKey
import com.vitorpamplona.quartz.signers.NostrSigner
import com.vitorpamplona.quartz.utils.TimeUtils
import com.vitorpamplona.quartz.utils.removeTrailingNullsAndEmptyOthers
@Immutable
class InteractiveStoryReadingStateEvent(
id: HexKey,
pubKey: HexKey,
createdAt: Long,
tags: Array<Array<String>>,
content: String,
sig: HexKey,
) : BaseAddressableEvent(id, pubKey, createdAt, KIND, tags, content, sig) {
fun title() = firstTag("title")
fun summary() = firstTag("summary")
fun image() = firstTag("image")
fun status() = firstTag("status")
fun root() =
tags.firstOrNull { it.size > 1 && it[0] == "A" }?.let {
ATag.parse(it[1], it.getOrNull(2))
}
fun currentScene() =
tags.firstOrNull { it.size > 1 && it[0] == "a" }?.let {
ATag.parse(it[1], it.getOrNull(2))
}
companion object {
const val KIND = 30298
const val ALT1 = "Interactive Story Reading state"
const val ALT2 = "The reading state of "
fun createAddressATag(
pubKey: HexKey,
dtag: String,
): ATag = ATag(KIND, pubKey, dtag, null)
fun createAddressTag(
pubKey: HexKey,
dtag: String,
): String = ATag.assembleATag(KIND, pubKey, dtag)
fun update(
base: InteractiveStoryReadingStateEvent,
currentScene: InteractiveStoryBaseEvent,
currentSceneRelay: String?,
signer: NostrSigner,
createdAt: Long = TimeUtils.now(),
onReady: (InteractiveStoryReadingStateEvent) -> Unit,
) {
val rootTag = base.dTag()
val sceneTag = currentScene.addressTag()
val status =
if (rootTag == sceneTag) {
"new"
} else if (currentScene.options().isEmpty()) {
"done"
} else {
"reading"
}
val tags =
base.tags.filter { it[0] != "a" && it[0] != "status" } +
listOf(
removeTrailingNullsAndEmptyOthers("a", sceneTag, currentSceneRelay),
arrayOf("status", status),
)
signer.sign(createdAt, KIND, tags.toTypedArray(), "", onReady)
}
fun create(
root: InteractiveStoryBaseEvent,
rootRelay: String?,
currentScene: InteractiveStoryBaseEvent,
currentSceneRelay: String?,
signer: NostrSigner,
createdAt: Long = TimeUtils.now(),
onReady: (InteractiveStoryReadingStateEvent) -> Unit,
) {
val rootTag = root.addressTag()
val sceneTag = currentScene.addressTag()
val status =
if (rootTag == sceneTag) {
"new"
} else if (currentScene.options().isEmpty()) {
"done"
} else {
"reading"
}
val tags =
listOfNotNull(
arrayOf("d", rootTag),
arrayOf("alt", root.title()?.let { ALT2 + it } ?: ALT1),
root.title()?.let { arrayOf("title", it) },
root.summary()?.let { arrayOf("summary", it) },
root.image()?.let { arrayOf("image", it) },
removeTrailingNullsAndEmptyOthers("A", rootTag, rootRelay),
removeTrailingNullsAndEmptyOthers("a", sceneTag, currentSceneRelay),
arrayOf("status", status),
).toTypedArray()
signer.sign(createdAt, KIND, tags, "", onReady)
}
}
enum class ReadingStatus {
NEW,
READING,
DONE,
}
}
@@ -0,0 +1,76 @@
/**
* Copyright (c) 2024 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.quartz.events
import com.vitorpamplona.quartz.encoders.ATag
import com.vitorpamplona.quartz.encoders.HexKey
import com.vitorpamplona.quartz.signers.NostrSigner
import com.vitorpamplona.quartz.utils.TimeUtils
class InteractiveStorySceneEvent(
id: HexKey,
pubKey: HexKey,
createdAt: Long,
tags: Array<Array<String>>,
content: String,
sig: HexKey,
) : InteractiveStoryBaseEvent(id, pubKey, createdAt, KIND, tags, content, sig) {
companion object {
const val KIND = 30297
const val ALT = "A scene of an interative story called "
fun createAddressATag(
pubKey: HexKey,
dtag: String,
): ATag = ATag(KIND, pubKey, dtag, null)
fun createAddressTag(
pubKey: HexKey,
dtag: String,
): String = ATag.assembleATag(KIND, pubKey, dtag)
fun create(
baseId: String,
title: String,
content: String,
options: List<StoryOption>,
zapReceiver: List<ZapSplitSetup>? = null,
markAsSensitive: Boolean = false,
zapRaiserAmount: Long? = null,
geohash: String? = null,
nip94attachments: List<FileHeaderEvent>? = null,
signer: NostrSigner,
createdAt: Long = TimeUtils.now(),
isDraft: Boolean,
onReady: (InteractiveStorySceneEvent) -> Unit,
) {
val tags =
makeTags(baseId, ALT + title, title, options = options) +
generalTags(content, zapReceiver, markAsSensitive, zapRaiserAmount, geohash, nip94attachments)
if (isDraft) {
signer.assembleRumor(createdAt, KIND, tags, content, onReady)
} else {
signer.sign(createdAt, KIND, tags, content, onReady)
}
}
}
}
@@ -38,7 +38,7 @@ class LongTextNoteEvent(
AddressableEvent {
override fun dTag() = tags.firstOrNull { it.size > 1 && it[0] == "d" }?.get(1) ?: ""
override fun address() = ATag(kind, pubKey, dTag(), null)
override fun address(relayHint: String?) = ATag(kind, pubKey, dTag(), relayHint)
override fun addressTag() = ATag.assembleATag(kind, pubKey, dTag())
@@ -38,7 +38,7 @@ class WikiNoteEvent(
AddressableEvent {
override fun dTag() = tags.firstOrNull { it.size > 1 && it[0] == "d" }?.get(1) ?: ""
override fun address() = ATag(kind, pubKey, dTag(), null)
override fun address(relayHint: String?) = ATag(kind, pubKey, dTag(), relayHint)
override fun addressTag() = ATag.assembleATag(kind, pubKey, dTag())