mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-08-08 23:54:39 +00:00
feat(cli): add nak-style git (NIP-34) + podcast (NIP-F4) commands
git (repository metadata + collaboration events; clone/push packfile
transport is out of scope):
- git announce publish a kind:30617 repository announcement
- git list list a user's repo announcements
- git show print one announcement (naddr or kind:pubkey:id), cache-first
- git issue publish a kind:1621 issue against a repo (fetches the repo
announcement to build the EventHintBundle)
podcast (NIP-F4):
- podcast metadata publish kind:10154 show metadata (replaceable)
- podcast publish publish a kind:54 episode (--audio URL[,URL])
- podcast list list a user's metadata + episodes
Thin assembly over quartz GitRepositoryEvent / GitIssueEvent /
PodcastMetadataEvent / PodcastEpisodeEvent. Verified offline: announce->
show cache round-trip (name/clone/hashtags), issue kind:1621 against the
repo, podcast metadata kind:10154 + episode kind:54.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011SapGdtAc1j7woifoCZ9fY
This commit is contained in:
@@ -245,6 +245,25 @@ Filter flags are shared by `fetch` and `subscribe`: `--kind K[,K]`, `--author U[
|
||||
| `amy gift wrap --to USER [EVENT-JSON] [--relay …]` | NIP-59: seal a signed inner event for USER and wrap it in a kind:1059 gift wrap. Prints the wrap; `--relay` also broadcasts it. |
|
||||
| `amy gift unwrap [GIFTWRAP-JSON]` | Decrypt + unseal a kind:1059 wrap addressed to the active account; prints the inner event. |
|
||||
|
||||
### Git (NIP-34)
|
||||
|
||||
nak's `clone`/`push`/`pull` (git-packfile transport over relays/GRASP) are out of scope — these are the metadata + collaboration events.
|
||||
|
||||
| Command | What it does |
|
||||
|---|---|
|
||||
| `amy git announce --name N [--description D] [--clone URL[,URL]] [--web URL[,URL]] [--relay URL[,URL]] [--maintainer HEX[,HEX]] [--hashtag T[,T]] [--earliest-commit C] [--d ID]` | Publish a kind:30617 repository announcement. |
|
||||
| `amy git list [USER]` | List a user's repo announcements (defaults to self). |
|
||||
| `amy git show NADDR\|kind:pubkey:id` | Print one repo announcement (cache-first). |
|
||||
| `amy git issue NADDR\|coords --subject S [BODY] [--hashtag T[,T]]` | Publish a kind:1621 issue against a repo. BODY from arg or stdin. |
|
||||
|
||||
### Podcasts (NIP-F4)
|
||||
|
||||
| Command | What it does |
|
||||
|---|---|
|
||||
| `amy podcast metadata --title T --image URL --description D [--website URL[,URL]]` | Publish kind:10154 show metadata (replaceable). |
|
||||
| `amy podcast publish --title T --description D --audio URL[,URL] [--audio-type MIME] [--image URL] [--content MD]` | Publish a kind:54 episode. |
|
||||
| `amy podcast list [USER] [--limit N]` | List a user's show metadata + episodes. |
|
||||
|
||||
### Blossom blobs (NIP-B7)
|
||||
|
||||
| Command | What it does |
|
||||
|
||||
@@ -35,6 +35,7 @@ import com.vitorpamplona.amethyst.cli.commands.FetchCommand
|
||||
import com.vitorpamplona.amethyst.cli.commands.FilterCommand
|
||||
import com.vitorpamplona.amethyst.cli.commands.FollowCommand
|
||||
import com.vitorpamplona.amethyst.cli.commands.GiftCommands
|
||||
import com.vitorpamplona.amethyst.cli.commands.GitCommands
|
||||
import com.vitorpamplona.amethyst.cli.commands.GroupCommands
|
||||
import com.vitorpamplona.amethyst.cli.commands.InitCommands
|
||||
import com.vitorpamplona.amethyst.cli.commands.KeyCommands
|
||||
@@ -47,6 +48,7 @@ import com.vitorpamplona.amethyst.cli.commands.NotesCommands
|
||||
import com.vitorpamplona.amethyst.cli.commands.NsiteCommands
|
||||
import com.vitorpamplona.amethyst.cli.commands.OfferCommands
|
||||
import com.vitorpamplona.amethyst.cli.commands.OutboxCommand
|
||||
import com.vitorpamplona.amethyst.cli.commands.PodcastCommands
|
||||
import com.vitorpamplona.amethyst.cli.commands.ProfileCommands
|
||||
import com.vitorpamplona.amethyst.cli.commands.PublishCommand
|
||||
import com.vitorpamplona.amethyst.cli.commands.RelayCommands
|
||||
@@ -209,6 +211,8 @@ private suspend fun dispatch(argv: Array<String>): Int {
|
||||
"outbox" -> OutboxCommand.run(dataDir, tail)
|
||||
"blossom" -> BlossomCommands.dispatch(dataDir, tail)
|
||||
"sync" -> SyncCommand.run(dataDir, tail)
|
||||
"git" -> GitCommands.dispatch(dataDir, tail)
|
||||
"podcast" -> PodcastCommands.dispatch(dataDir, tail)
|
||||
else -> {
|
||||
System.err.println("unknown subcommand: $head")
|
||||
printUsage()
|
||||
@@ -414,6 +418,24 @@ private fun printUsage() {
|
||||
| blossom list --server URL [USER] list a user's blobs (defaults to self)
|
||||
| blossom delete HASH --server URL delete a blob you own
|
||||
|
|
||||
|Git (NIP-34):
|
||||
| git announce --name N [--description D] publish a kind:30617 repo announcement
|
||||
| [--clone URL[,URL]] [--web URL[,URL]] (--d sets the identifier; defaults to name)
|
||||
| [--relay URL[,URL]] [--maintainer HEX[,]]
|
||||
| [--hashtag T[,T]] [--earliest-commit C] [--d ID]
|
||||
| git list [USER] list a user's repo announcements (default self)
|
||||
| git show NADDR|kind:pubkey:id print one repo announcement
|
||||
| git issue NADDR|coords --subject S [BODY] publish a kind:1621 issue against a repo
|
||||
| [--hashtag T[,T]] [--relay URL[,URL]] (BODY from arg or stdin)
|
||||
|
|
||||
|Podcasts (NIP-F4):
|
||||
| podcast metadata --title T --image URL publish kind:10154 show metadata
|
||||
| --description D [--website URL[,URL]]
|
||||
| podcast publish --title T --description D publish a kind:54 episode
|
||||
| --audio URL[,URL] [--audio-type MIME]
|
||||
| [--image URL] [--content MARKDOWN]
|
||||
| podcast list [USER] [--limit N] list a user's metadata + episodes
|
||||
|
|
||||
|Static websites (NIP-5A kind:15128/35128):
|
||||
| nsite fetch AUTHOR [--d ID] [--path P] resolve one path over Nostr + Blossom and
|
||||
| [--server URL[,URL]] [--relay URL[,URL]] VERIFY it against the manifest's sha256 pin
|
||||
|
||||
@@ -0,0 +1,248 @@
|
||||
/*
|
||||
* 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.cli.commands
|
||||
|
||||
import com.vitorpamplona.amethyst.cli.Args
|
||||
import com.vitorpamplona.amethyst.cli.Context
|
||||
import com.vitorpamplona.amethyst.cli.DataDir
|
||||
import com.vitorpamplona.amethyst.cli.Output
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Address
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||
import com.vitorpamplona.quartz.nip01Core.hints.EventHintBundle
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer
|
||||
import com.vitorpamplona.quartz.nip19Bech32.entities.NAddress
|
||||
import com.vitorpamplona.quartz.nip34Git.issue.GitIssueEvent
|
||||
import com.vitorpamplona.quartz.nip34Git.repository.GitRepositoryEvent
|
||||
|
||||
/**
|
||||
* `amy git <announce|list|show|issue>` — NIP-34 Nostr-native git
|
||||
* repositories (nak's `git`, adapted to amy's event-publish model).
|
||||
*
|
||||
* announce publish a kind:30617 repository announcement
|
||||
* list list a user's repository announcements
|
||||
* show print one repository announcement (naddr or coordinates)
|
||||
* issue publish a kind:1621 issue against a repository
|
||||
*
|
||||
* nak's clone/push/pull (git-packfile transport over relays/GRASP) are out
|
||||
* of scope — they need a real git plumbing layer. These are the metadata /
|
||||
* collaboration events. Thin assembly only: every event lives in quartz
|
||||
* (`GitRepositoryEvent`, `GitIssueEvent`).
|
||||
*/
|
||||
object GitCommands {
|
||||
suspend fun dispatch(
|
||||
dataDir: DataDir,
|
||||
tail: Array<String>,
|
||||
): Int =
|
||||
route(
|
||||
"git",
|
||||
tail,
|
||||
"git <announce|list|show|issue>",
|
||||
mapOf(
|
||||
"announce" to { rest -> announce(dataDir, rest) },
|
||||
"list" to { rest -> list(dataDir, rest) },
|
||||
"show" to { rest -> show(dataDir, rest) },
|
||||
"issue" to { rest -> issue(dataDir, rest) },
|
||||
),
|
||||
)
|
||||
|
||||
private suspend fun announce(
|
||||
dataDir: DataDir,
|
||||
rest: Array<String>,
|
||||
): Int {
|
||||
val args = Args(rest)
|
||||
val name = args.flag("name") ?: return Output.error("bad_args", "git announce requires --name")
|
||||
val csv = { key: String ->
|
||||
args
|
||||
.flag(key)
|
||||
?.split(',')
|
||||
?.map { it.trim() }
|
||||
?.filter { it.isNotEmpty() }
|
||||
.orEmpty()
|
||||
}
|
||||
|
||||
Context.open(dataDir).use { ctx ->
|
||||
ctx.prepare()
|
||||
val template =
|
||||
GitRepositoryEvent.build(
|
||||
name = name,
|
||||
description = args.flag("description"),
|
||||
webUrls = csv("web"),
|
||||
cloneUrls = csv("clone"),
|
||||
relays = csv("relay"),
|
||||
maintainers = csv("maintainer"),
|
||||
hashtags = csv("hashtag"),
|
||||
earliestUniqueCommit = args.flag("earliest-commit"),
|
||||
personalFork = args.bool("personal-fork"),
|
||||
dTag = args.flag("d") ?: name,
|
||||
)
|
||||
val signed = ctx.signer.sign(template)
|
||||
val targets = RawEventSupport.publishTargets(ctx, args)
|
||||
val ack = ctx.publish(signed, targets)
|
||||
Output.emit(
|
||||
mapOf(
|
||||
"event_id" to signed.id,
|
||||
"address" to Address.assemble(signed.kind, signed.pubKey, args.flag("d") ?: name),
|
||||
"published_to" to ack.filterValues { it }.keys.map { it.url },
|
||||
),
|
||||
)
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun list(
|
||||
dataDir: DataDir,
|
||||
rest: Array<String>,
|
||||
): Int {
|
||||
val args = Args(rest)
|
||||
Context.open(dataDir).use { ctx ->
|
||||
ctx.prepare()
|
||||
val author = args.positionalOrNull(0)?.let { ctx.requireUserHex(it) } ?: ctx.identity.pubKeyHex
|
||||
val relays = RawEventSupport.queryTargets(ctx, args)
|
||||
val received = ctx.drain(relays.associateWith { listOf(Filter(kinds = listOf(GitRepositoryEvent.KIND), authors = listOf(author))) })
|
||||
val repos =
|
||||
received
|
||||
.asSequence()
|
||||
.map { it.second }
|
||||
.filterIsInstance<GitRepositoryEvent>()
|
||||
.distinctBy { it.dTag() }
|
||||
.sortedByDescending { it.createdAt }
|
||||
.map { repoSummary(it) }
|
||||
.toList()
|
||||
Output.emit(mapOf("pubkey" to author, "count" to repos.size, "repositories" to repos))
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun show(
|
||||
dataDir: DataDir,
|
||||
rest: Array<String>,
|
||||
): Int {
|
||||
val args = Args(rest)
|
||||
val coord = args.positional(0, "naddr-or-coordinates")
|
||||
val addr = resolveAddress(coord) ?: return Output.error("bad_args", "expected an naddr or kind:pubkey:identifier (or pubkey:identifier)")
|
||||
if (addr.kind != GitRepositoryEvent.KIND) {
|
||||
return Output.error("bad_args", "not a git repository address (expected kind ${GitRepositoryEvent.KIND}, got ${addr.kind})")
|
||||
}
|
||||
|
||||
Context.open(dataDir).use { ctx ->
|
||||
ctx.prepare()
|
||||
val repo = fetchRepo(ctx, addr, args) ?: return Output.error("not_found", "no repository announcement found for $coord")
|
||||
Output.emit(repoSummary(repo) + mapOf("event_id" to repo.id, "content" to repo.content))
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun issue(
|
||||
dataDir: DataDir,
|
||||
rest: Array<String>,
|
||||
): Int {
|
||||
val args = Args(rest)
|
||||
val coord = args.positional(0, "repo-naddr-or-coordinates")
|
||||
val subject = args.flag("subject") ?: return Output.error("bad_args", "git issue requires --subject")
|
||||
val addr = resolveAddress(coord) ?: return Output.error("bad_args", "expected an naddr or kind:pubkey:identifier")
|
||||
val body = args.positionalOrNull(1) ?: ""
|
||||
val topics =
|
||||
args
|
||||
.flag("hashtag")
|
||||
?.split(',')
|
||||
?.map { it.trim() }
|
||||
?.filter { it.isNotEmpty() }
|
||||
.orEmpty()
|
||||
|
||||
Context.open(dataDir).use { ctx ->
|
||||
ctx.prepare()
|
||||
val repo = fetchRepo(ctx, addr, args) ?: return Output.error("not_found", "no repository announcement found for $coord")
|
||||
val template =
|
||||
GitIssueEvent.build(
|
||||
subject = subject,
|
||||
content = body,
|
||||
repository = EventHintBundle(repo),
|
||||
notify = emptyList(),
|
||||
topics = topics,
|
||||
)
|
||||
val signed = ctx.signer.sign(template)
|
||||
// Deliver to the repo's advertised relays when present, else our targets.
|
||||
val repoRelays = repo.relays().mapNotNull { RelayUrlNormalizer.normalizeOrNull(it) }.toSet()
|
||||
val targets = RawEventSupport.relayFlag(args).ifEmpty { repoRelays }.ifEmpty { ctx.outboxRelays() }
|
||||
val ack = ctx.publish(signed, targets)
|
||||
Output.emit(
|
||||
mapOf(
|
||||
"event_id" to signed.id,
|
||||
"kind" to signed.kind,
|
||||
"repository" to Address.assemble(addr.kind, addr.pubKeyHex, addr.dTag),
|
||||
"subject" to subject,
|
||||
"published_to" to ack.filterValues { it }.keys.map { it.url },
|
||||
),
|
||||
)
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
private suspend fun fetchRepo(
|
||||
ctx: Context,
|
||||
addr: Address,
|
||||
args: Args,
|
||||
): GitRepositoryEvent? {
|
||||
// Cache-first, then drain the query relays.
|
||||
val filter =
|
||||
Filter(
|
||||
kinds = listOf(GitRepositoryEvent.KIND),
|
||||
authors = listOf(addr.pubKeyHex),
|
||||
tags = mapOf("d" to listOf(addr.dTag)),
|
||||
limit = 1,
|
||||
)
|
||||
ctx.store
|
||||
.query<Event>(filter)
|
||||
.firstOrNull()
|
||||
?.let { return it as? GitRepositoryEvent }
|
||||
val relays = RawEventSupport.queryTargets(ctx, args)
|
||||
ctx.drain(relays.associateWith { listOf(filter) })
|
||||
return ctx.store.query<Event>(filter).firstOrNull() as? GitRepositoryEvent
|
||||
}
|
||||
|
||||
/** Accept `naddr1…`, `kind:pubkey:dtag`, or `pubkey:dtag` (kind defaults to 30617). */
|
||||
private fun resolveAddress(input: String): Address? {
|
||||
val trimmed = input.trim().removePrefix("nostr:")
|
||||
if (trimmed.startsWith("naddr")) {
|
||||
val n = NAddress.parse(trimmed) ?: return null
|
||||
return Address(n.kind, n.author, n.dTag)
|
||||
}
|
||||
Address.parse(trimmed)?.let { return it }
|
||||
val parts = trimmed.split(":")
|
||||
return if (parts.size == 2 && parts[0].length == 64) Address(GitRepositoryEvent.KIND, parts[0], parts[1]) else null
|
||||
}
|
||||
|
||||
private fun repoSummary(repo: GitRepositoryEvent): Map<String, Any?> =
|
||||
mapOf(
|
||||
"identifier" to repo.dTag(),
|
||||
"name" to repo.name(),
|
||||
"description" to repo.description(),
|
||||
"clone" to repo.clones(),
|
||||
"web" to repo.webs(),
|
||||
"relays" to repo.relays(),
|
||||
"maintainers" to repo.maintainers(),
|
||||
"hashtags" to repo.hashtags(),
|
||||
"address" to Address.assemble(repo.kind, repo.pubKey, repo.dTag()),
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,178 @@
|
||||
/*
|
||||
* 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.cli.commands
|
||||
|
||||
import com.vitorpamplona.amethyst.cli.Args
|
||||
import com.vitorpamplona.amethyst.cli.Context
|
||||
import com.vitorpamplona.amethyst.cli.DataDir
|
||||
import com.vitorpamplona.amethyst.cli.Output
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
|
||||
import com.vitorpamplona.quartz.nipF4Podcasts.episode.PodcastEpisodeEvent
|
||||
import com.vitorpamplona.quartz.nipF4Podcasts.episode.tags.AudioTag
|
||||
import com.vitorpamplona.quartz.nipF4Podcasts.metadata.PodcastMetadataEvent
|
||||
|
||||
/**
|
||||
* `amy podcast <metadata|publish|list>` — NIP-F4 podcasts (nak's `podcast`).
|
||||
*
|
||||
* metadata publish a kind:10154 show metadata (replaceable)
|
||||
* publish publish a kind:54 episode
|
||||
* list list a user's podcast metadata + episodes
|
||||
*
|
||||
* Thin assembly only: events live in quartz (`PodcastMetadataEvent`,
|
||||
* `PodcastEpisodeEvent`).
|
||||
*/
|
||||
object PodcastCommands {
|
||||
suspend fun dispatch(
|
||||
dataDir: DataDir,
|
||||
tail: Array<String>,
|
||||
): Int =
|
||||
route(
|
||||
"podcast",
|
||||
tail,
|
||||
"podcast <metadata|publish|list>",
|
||||
mapOf(
|
||||
"metadata" to { rest -> metadata(dataDir, rest) },
|
||||
"publish" to { rest -> publish(dataDir, rest) },
|
||||
"list" to { rest -> list(dataDir, rest) },
|
||||
),
|
||||
)
|
||||
|
||||
private suspend fun metadata(
|
||||
dataDir: DataDir,
|
||||
rest: Array<String>,
|
||||
): Int {
|
||||
val args = Args(rest)
|
||||
val title = args.flag("title") ?: return Output.error("bad_args", "podcast metadata requires --title")
|
||||
val image = args.flag("image") ?: return Output.error("bad_args", "podcast metadata requires --image")
|
||||
val description = args.flag("description") ?: return Output.error("bad_args", "podcast metadata requires --description")
|
||||
val websites =
|
||||
args
|
||||
.flag("website")
|
||||
?.split(',')
|
||||
?.map { it.trim() }
|
||||
?.filter { it.isNotEmpty() }
|
||||
.orEmpty()
|
||||
|
||||
Context.open(dataDir).use { ctx ->
|
||||
ctx.prepare()
|
||||
val signed = ctx.signer.sign(PodcastMetadataEvent.build(title, image, description, websites))
|
||||
val ack = ctx.publish(signed, RawEventSupport.publishTargets(ctx, args))
|
||||
Output.emit(
|
||||
mapOf(
|
||||
"event_id" to signed.id,
|
||||
"kind" to signed.kind,
|
||||
"title" to title,
|
||||
"published_to" to ack.filterValues { it }.keys.map { it.url },
|
||||
),
|
||||
)
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun publish(
|
||||
dataDir: DataDir,
|
||||
rest: Array<String>,
|
||||
): Int {
|
||||
val args = Args(rest)
|
||||
val title = args.flag("title") ?: return Output.error("bad_args", "podcast publish requires --title")
|
||||
val description = args.flag("description") ?: return Output.error("bad_args", "podcast publish requires --description")
|
||||
val audioType = args.flag("audio-type")
|
||||
val audios =
|
||||
args
|
||||
.flag("audio")
|
||||
?.split(',')
|
||||
?.map { it.trim() }
|
||||
?.filter { it.isNotEmpty() }
|
||||
?.map { AudioTag(it, audioType) }
|
||||
.orEmpty()
|
||||
if (audios.isEmpty()) return Output.error("bad_args", "podcast publish requires --audio URL[,URL…]")
|
||||
|
||||
Context.open(dataDir).use { ctx ->
|
||||
ctx.prepare()
|
||||
val template =
|
||||
PodcastEpisodeEvent.build(
|
||||
title = title,
|
||||
description = description,
|
||||
audios = audios,
|
||||
markdownContent = args.flag("content", "") ?: "",
|
||||
image = args.flag("image"),
|
||||
)
|
||||
val signed = ctx.signer.sign(template)
|
||||
val ack = ctx.publish(signed, RawEventSupport.publishTargets(ctx, args))
|
||||
Output.emit(
|
||||
mapOf(
|
||||
"event_id" to signed.id,
|
||||
"kind" to signed.kind,
|
||||
"title" to title,
|
||||
"audios" to audios.map { it.url },
|
||||
"published_to" to ack.filterValues { it }.keys.map { it.url },
|
||||
),
|
||||
)
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun list(
|
||||
dataDir: DataDir,
|
||||
rest: Array<String>,
|
||||
): Int {
|
||||
val args = Args(rest)
|
||||
val limit = args.intFlag("limit", 50)
|
||||
Context.open(dataDir).use { ctx ->
|
||||
ctx.prepare()
|
||||
val author = args.positionalOrNull(0)?.let { ctx.requireUserHex(it) } ?: ctx.identity.pubKeyHex
|
||||
val relays = RawEventSupport.queryTargets(ctx, args)
|
||||
val received =
|
||||
ctx.drain(
|
||||
relays.associateWith {
|
||||
listOf(Filter(kinds = listOf(PodcastMetadataEvent.KIND, PodcastEpisodeEvent.KIND), authors = listOf(author), limit = limit))
|
||||
},
|
||||
)
|
||||
val events = received.map { it.second }.distinctBy { it.id }
|
||||
val show = events.filterIsInstance<PodcastMetadataEvent>().maxByOrNull { it.createdAt }
|
||||
val episodes =
|
||||
events
|
||||
.filterIsInstance<PodcastEpisodeEvent>()
|
||||
.sortedByDescending { it.createdAt }
|
||||
.map {
|
||||
mapOf(
|
||||
"event_id" to it.id,
|
||||
"title" to it.title(),
|
||||
"description" to it.description(),
|
||||
"audios" to it.audios().map { a -> a.url },
|
||||
"created_at" to it.createdAt,
|
||||
)
|
||||
}
|
||||
Output.emit(
|
||||
mapOf(
|
||||
"pubkey" to author,
|
||||
"metadata" to
|
||||
show?.let {
|
||||
mapOf("title" to it.title(), "description" to it.description(), "image" to it.image(), "websites" to it.websites())
|
||||
},
|
||||
"episode_count" to episodes.size,
|
||||
"episodes" to episodes,
|
||||
),
|
||||
)
|
||||
return 0
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user