mirror of
https://github.com/fiatjaf/nak.git
synced 2026-08-08 23:34:41 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0735ded0fc | ||
|
|
5233a77510 | ||
|
|
fd34cc7c5e | ||
|
|
2d151c2ac8 | ||
|
|
037e8efcc6 | ||
|
|
1b380dea9a | ||
|
|
f126b3f7ee | ||
|
|
dc5ffe5129 | ||
|
|
7637b5018f | ||
|
|
d5ab34bb2f | ||
|
|
49345333c4 | ||
|
|
b5de7b78bc | ||
|
|
ba9a5badc6 |
+3
-3
@@ -1,8 +1,8 @@
|
||||
# build stage
|
||||
FROM golang:1.24-alpine AS builder
|
||||
FROM golang:1.25-alpine AS builder
|
||||
|
||||
# install git and ca-certificates (needed for fetching dependencies)
|
||||
RUN apk add --no-cache git ca-certificates
|
||||
RUN apk add --no-cache git ca-certificates gcc musl-dev
|
||||
|
||||
# set working directory
|
||||
WORKDIR /app
|
||||
@@ -19,7 +19,7 @@ COPY . .
|
||||
# build the application
|
||||
# use cgo_enabled=0 to create a static binary
|
||||
# use -ldflags to strip debug info and reduce binary size
|
||||
RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-w -s" -o nak .
|
||||
RUN CGO_ENABLED=1 GOOS=linux go build -ldflags="-w -s" -o nak .
|
||||
|
||||
# runtime stage
|
||||
FROM alpine:latest
|
||||
|
||||
@@ -6,14 +6,14 @@ install with this one-liner:
|
||||
curl -sSL https://raw.githubusercontent.com/fiatjaf/nak/master/install.sh | sh
|
||||
```
|
||||
|
||||
- or install with `go install github.com/fiatjaf/nak@latest` if you have [Go](https://pkg.go.dev) set up.
|
||||
- or install with `go install github.com/fiatjaf/nak@latest` if you have **Go** set up.
|
||||
- or [download a binary](https://github.com/fiatjaf/nak/releases) manually.
|
||||
- or get the source with `git clone https://github.com/fiatjaf/nak` then
|
||||
- install with `go install`;
|
||||
- or run with docker using `docker build -t nak . && docker run nak event`.
|
||||
- or install with `brew install nak` if you use **macOS Homebrew**.
|
||||
- or install with `paru -S nak-bin` or `yay -S nak-bin` if you are on **Arch Linux**.
|
||||
- or install with `nix-env --install ripgrep` if you use **Nix**.
|
||||
- or install with `nix-env --install nak` if you use **Nix**.
|
||||
|
||||
## what can you do with it?
|
||||
|
||||
|
||||
@@ -418,6 +418,13 @@ var bunker = &cli.Command{
|
||||
return true
|
||||
}
|
||||
if slices.Contains(authorizedSecrets, secret) {
|
||||
// add client to authorized list for subsequent requests
|
||||
if !slices.ContainsFunc(config.Clients, func(c BunkerConfigClient) bool { return c.PubKey == from }) {
|
||||
config.Clients = append(config.Clients, BunkerConfigClient{PubKey: from})
|
||||
if persist != nil {
|
||||
persist()
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
|
||||
@@ -9,63 +9,17 @@ import (
|
||||
"fiatjaf.com/nostr"
|
||||
"fiatjaf.com/nostr/nip45"
|
||||
"fiatjaf.com/nostr/nip45/hyperloglog"
|
||||
"github.com/mailru/easyjson"
|
||||
"github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
var count = &cli.Command{
|
||||
Name: "count",
|
||||
Usage: "generates encoded COUNT messages and optionally use them to talk to relays",
|
||||
Description: `outputs a nip45 request (the flags are mostly the same as 'nak req').`,
|
||||
Description: `like 'nak req', but does a "COUNT" call instead. Will attempt to perform HyperLogLog aggregation if more than one relay is specified.`,
|
||||
DisableSliceFlagSeparator: true,
|
||||
Flags: []cli.Flag{
|
||||
&PubKeySliceFlag{
|
||||
Name: "author",
|
||||
Aliases: []string{"a"},
|
||||
Usage: "only accept events from these authors",
|
||||
Category: CATEGORY_FILTER_ATTRIBUTES,
|
||||
},
|
||||
&cli.IntSliceFlag{
|
||||
Name: "kind",
|
||||
Aliases: []string{"k"},
|
||||
Usage: "only accept events with these kind numbers",
|
||||
Category: CATEGORY_FILTER_ATTRIBUTES,
|
||||
},
|
||||
&cli.StringSliceFlag{
|
||||
Name: "tag",
|
||||
Aliases: []string{"t"},
|
||||
Usage: "takes a tag like -t e=<id>, only accept events with these tags",
|
||||
Category: CATEGORY_FILTER_ATTRIBUTES,
|
||||
},
|
||||
&cli.StringSliceFlag{
|
||||
Name: "e",
|
||||
Usage: "shortcut for --tag e=<value>",
|
||||
Category: CATEGORY_FILTER_ATTRIBUTES,
|
||||
},
|
||||
&cli.StringSliceFlag{
|
||||
Name: "p",
|
||||
Usage: "shortcut for --tag p=<value>",
|
||||
Category: CATEGORY_FILTER_ATTRIBUTES,
|
||||
},
|
||||
&NaturalTimeFlag{
|
||||
Name: "since",
|
||||
Aliases: []string{"s"},
|
||||
Usage: "only accept events newer than this (unix timestamp)",
|
||||
Category: CATEGORY_FILTER_ATTRIBUTES,
|
||||
},
|
||||
&NaturalTimeFlag{
|
||||
Name: "until",
|
||||
Aliases: []string{"u"},
|
||||
Usage: "only accept events older than this (unix timestamp)",
|
||||
Category: CATEGORY_FILTER_ATTRIBUTES,
|
||||
},
|
||||
&cli.IntFlag{
|
||||
Name: "limit",
|
||||
Aliases: []string{"l"},
|
||||
Usage: "only accept up to this number of events",
|
||||
Category: CATEGORY_FILTER_ATTRIBUTES,
|
||||
},
|
||||
},
|
||||
ArgsUsage: "[relay...]",
|
||||
Flags: reqFilterFlags,
|
||||
ArgsUsage: "[relay...]",
|
||||
Action: func(ctx context.Context, c *cli.Command) error {
|
||||
biggerUrlSize := 0
|
||||
relayUrls := c.Args().Slice()
|
||||
@@ -84,95 +38,62 @@ var count = &cli.Command{
|
||||
}
|
||||
}
|
||||
|
||||
filter := nostr.Filter{}
|
||||
|
||||
if authors := getPubKeySlice(c, "author"); len(authors) > 0 {
|
||||
filter.Authors = authors
|
||||
}
|
||||
if kinds64 := c.IntSlice("kind"); len(kinds64) > 0 {
|
||||
kinds := make([]nostr.Kind, len(kinds64))
|
||||
for i, v := range kinds64 {
|
||||
kinds[i] = nostr.Kind(v)
|
||||
}
|
||||
filter.Kinds = kinds
|
||||
}
|
||||
|
||||
tags := make([][]string, 0, 5)
|
||||
for _, tagFlag := range c.StringSlice("tag") {
|
||||
spl := strings.SplitN(tagFlag, "=", 2)
|
||||
if len(spl) == 2 {
|
||||
tags = append(tags, []string{spl[0], decodeTagValue(spl[1])})
|
||||
} else {
|
||||
return fmt.Errorf("invalid --tag '%s'", tagFlag)
|
||||
}
|
||||
}
|
||||
for _, etag := range c.StringSlice("e") {
|
||||
tags = append(tags, []string{"e", decodeTagValue(etag)})
|
||||
}
|
||||
for _, ptag := range c.StringSlice("p") {
|
||||
tags = append(tags, []string{"p", decodeTagValue(ptag)})
|
||||
}
|
||||
if len(tags) > 0 {
|
||||
filter.Tags = make(nostr.TagMap)
|
||||
for _, tag := range tags {
|
||||
if _, ok := filter.Tags[tag[0]]; !ok {
|
||||
filter.Tags[tag[0]] = make([]string, 0, 3)
|
||||
}
|
||||
filter.Tags[tag[0]] = append(filter.Tags[tag[0]], tag[1])
|
||||
}
|
||||
}
|
||||
|
||||
if c.IsSet("since") {
|
||||
filter.Since = getNaturalDate(c, "since")
|
||||
}
|
||||
if c.IsSet("until") {
|
||||
filter.Until = getNaturalDate(c, "until")
|
||||
}
|
||||
|
||||
if limit := c.Int("limit"); limit != 0 {
|
||||
filter.Limit = int(limit)
|
||||
}
|
||||
|
||||
successes := 0
|
||||
if len(relayUrls) > 0 {
|
||||
var hll *hyperloglog.HyperLogLog
|
||||
if offset := nip45.HyperLogLogEventPubkeyOffsetForFilter(filter); offset != -1 && len(relayUrls) > 1 {
|
||||
hll = hyperloglog.New(offset)
|
||||
}
|
||||
for _, relayUrl := range relayUrls {
|
||||
relay, _ := sys.Pool.EnsureRelay(relayUrl)
|
||||
count, hllRegisters, err := relay.Count(ctx, filter, nostr.SubscriptionOptions{
|
||||
Label: "nak-count",
|
||||
})
|
||||
fmt.Fprintf(os.Stderr, "%s%s: ", strings.Repeat(" ", biggerUrlSize-len(relayUrl)), relayUrl)
|
||||
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "❌ %s\n", err)
|
||||
// go line by line from stdin or run once with input from flags
|
||||
for stdinFilter := range getJsonsOrBlank() {
|
||||
filter := nostr.Filter{}
|
||||
if stdinFilter != "" {
|
||||
if err := easyjson.Unmarshal([]byte(stdinFilter), &filter); err != nil {
|
||||
ctx = lineProcessingError(ctx, "invalid filter '%s' received from stdin: %s", stdinFilter, err)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
var hasHLLStr string
|
||||
if hll != nil && len(hllRegisters) == 256 {
|
||||
hll.MergeRegisters(hllRegisters)
|
||||
hasHLLStr = " 📋"
|
||||
if err := applyFlagsToFilter(c, &filter); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
successes := 0
|
||||
if len(relayUrls) > 0 {
|
||||
var hll *hyperloglog.HyperLogLog
|
||||
if offset := nip45.HyperLogLogEventPubkeyOffsetForFilter(filter); offset != -1 && len(relayUrls) > 1 {
|
||||
hll = hyperloglog.New(offset)
|
||||
}
|
||||
for _, relayUrl := range relayUrls {
|
||||
relay, _ := sys.Pool.EnsureRelay(relayUrl)
|
||||
count, hllRegisters, err := relay.Count(ctx, filter, nostr.SubscriptionOptions{
|
||||
Label: "nak-count",
|
||||
})
|
||||
fmt.Fprintf(os.Stderr, "%s%s: ", strings.Repeat(" ", biggerUrlSize-len(relayUrl)), relayUrl)
|
||||
|
||||
fmt.Fprintf(os.Stderr, "%d%s\n", count, hasHLLStr)
|
||||
successes++
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "error: %s\n", err)
|
||||
continue
|
||||
}
|
||||
|
||||
var hasHLLStr string
|
||||
if hll != nil && len(hllRegisters) == 256 {
|
||||
hll.MergeRegisters(hllRegisters)
|
||||
hasHLLStr = " (hll)"
|
||||
}
|
||||
|
||||
fmt.Fprintf(os.Stderr, "%d%s\n", count, hasHLLStr)
|
||||
successes++
|
||||
}
|
||||
if successes == 0 {
|
||||
return fmt.Errorf("all relays have failed")
|
||||
} else if hll != nil {
|
||||
fmt.Fprintf(os.Stderr, "HyperLogLog sum: %d\n", hll.Count())
|
||||
}
|
||||
} else {
|
||||
// no relays given, will just print the filter
|
||||
var result string
|
||||
j, _ := json.Marshal([]any{"COUNT", "nak", filter})
|
||||
result = string(j)
|
||||
stdout(result)
|
||||
}
|
||||
if successes == 0 {
|
||||
return fmt.Errorf("all relays have failed")
|
||||
} else if hll != nil {
|
||||
fmt.Fprintf(os.Stderr, "📋 HyperLogLog sum: %d\n", hll.Count())
|
||||
}
|
||||
} else {
|
||||
// no relays given, will just print the filter
|
||||
var result string
|
||||
j, _ := json.Marshal([]any{"COUNT", "nak", filter})
|
||||
result = string(j)
|
||||
stdout(result)
|
||||
}
|
||||
|
||||
exitIfLineProcessingError(ctx)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
@@ -105,10 +105,10 @@ func (t *naturalTimeValue) Set(value string) error {
|
||||
DefaultTimezone: time.Local,
|
||||
CurrentTime: time.Now(),
|
||||
}, value)
|
||||
ts = date.Time
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ts = date.Time
|
||||
}
|
||||
|
||||
if t.timestamp != nil {
|
||||
|
||||
@@ -1465,23 +1465,20 @@ func parseRepositoryAddress(
|
||||
return ptr.PublicKey, ptr.Identifier, ptr.Relays, nil
|
||||
}
|
||||
|
||||
// format 2: nostr://<npub>/<relay_hostname>/<identifier> (ngit-style)
|
||||
// format 2: nostr://<npub_or_nip05>/<relay_hostname>/<identifier> (ngit-style)
|
||||
if strings.HasPrefix(address, "nostr://") {
|
||||
parts := strings.Split(address, "/")
|
||||
if len(parts) != 5 {
|
||||
return nostr.PubKey{}, "", nil, fmt.Errorf(
|
||||
"invalid nostr URL format, expected nostr://<npub>/<relay_hostname>/<identifier>, got: %s", address,
|
||||
"invalid nostr URL format, expected nostr://<npub|nip05>/<relay_hostname>/<identifier>, got: %s", address,
|
||||
)
|
||||
}
|
||||
|
||||
prefix, data, err := nip19.Decode(parts[2])
|
||||
owner, err = parsePubKey(parts[2])
|
||||
if err != nil {
|
||||
return nostr.PubKey{}, "", nil, fmt.Errorf("invalid owner public key: %w", err)
|
||||
return nostr.PubKey{}, "", nil, fmt.Errorf("invalid owner in URL: %w", err)
|
||||
}
|
||||
if prefix != "npub" {
|
||||
return nostr.PubKey{}, "", nil, fmt.Errorf("expected npub in URL")
|
||||
}
|
||||
owner = data.(nostr.PubKey)
|
||||
|
||||
relayHost := parts[3]
|
||||
identifier = parts[4]
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ module github.com/fiatjaf/nak
|
||||
go 1.25
|
||||
|
||||
require (
|
||||
fiatjaf.com/nostr v0.0.0-20260126202222-ca3730e50817
|
||||
fiatjaf.com/nostr v0.0.0-20260304030104-1d14e6bebe83
|
||||
github.com/AlecAivazis/survey/v2 v2.3.7
|
||||
github.com/bep/debounce v1.2.1
|
||||
github.com/btcsuite/btcd/btcec/v2 v2.3.6
|
||||
@@ -29,7 +29,7 @@ require (
|
||||
)
|
||||
|
||||
require (
|
||||
fiatjaf.com/lib v0.3.2
|
||||
fiatjaf.com/lib v0.3.6
|
||||
github.com/hanwen/go-fuse/v2 v2.9.0
|
||||
)
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
fiatjaf.com/lib v0.3.2 h1:RBS41z70d8Rp8e2nemQsbPY1NLLnEGShiY2c+Bom3+Q=
|
||||
fiatjaf.com/lib v0.3.2/go.mod h1:UlHaZvPHj25PtKLh9GjZkUHRmQ2xZ8Jkoa4VRaLeeQ8=
|
||||
fiatjaf.com/nostr v0.0.0-20260126202222-ca3730e50817 h1:Zp6rPetvwYFOLD+36RtmWmns2C0CLbtphD3DLu3cxCo=
|
||||
fiatjaf.com/nostr v0.0.0-20260126202222-ca3730e50817/go.mod h1:ue7yw0zHfZj23Ml2kVSdBx0ENEaZiuvGxs/8VEN93FU=
|
||||
fiatjaf.com/lib v0.3.6 h1:GRZNSxHI2EWdjSKVuzaT+c0aifLDtS16SzkeJaHyJfY=
|
||||
fiatjaf.com/lib v0.3.6/go.mod h1:UlHaZvPHj25PtKLh9GjZkUHRmQ2xZ8Jkoa4VRaLeeQ8=
|
||||
fiatjaf.com/nostr v0.0.0-20260304030104-1d14e6bebe83 h1:6pX4gFT6N7AoxeLcV3DzJAJCvAR1iPR2tcfInPV+7Ak=
|
||||
fiatjaf.com/nostr v0.0.0-20260304030104-1d14e6bebe83/go.mod h1:iRKV8eYKzePA30MdbaYBpAv8pYQ6to8rDr3W+R2hJzM=
|
||||
github.com/AlecAivazis/survey/v2 v2.3.7 h1:6I/u8FvytdGsgonrYsVn2t8t4QiRnh6QSTqkkhIiSjQ=
|
||||
github.com/AlecAivazis/survey/v2 v2.3.7/go.mod h1:xUTIdE4KCOIjsBAE1JYsUPoCqYdZ1reCfTwbto0Fduo=
|
||||
github.com/FastFilter/xorfilter v0.2.1 h1:lbdeLG9BdpquK64ZsleBS8B4xO/QW1IM0gMzF7KaBKc=
|
||||
|
||||
@@ -234,7 +234,6 @@ var group = &cli.Command{
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer sub.Close()
|
||||
|
||||
eosed := false
|
||||
messages := make([]struct {
|
||||
|
||||
+1
-1
@@ -261,7 +261,7 @@ func connectToSingleRelay(
|
||||
for range 5 {
|
||||
if err := relay.Auth(ctx, func(ctx context.Context, authEvent *nostr.Event) error {
|
||||
challengeTag := authEvent.Tags.Find("challenge")
|
||||
if challengeTag[1] == "" {
|
||||
if challengeTag == nil || len(challengeTag) < 2 || challengeTag[1] == "" {
|
||||
return fmt.Errorf("auth not received yet *****") // what a giant hack
|
||||
}
|
||||
return preAuthSigner(ctx, c, logthis, authEvent)
|
||||
|
||||
+18
-18
@@ -1,7 +1,7 @@
|
||||
#!/usr/bin/env sh
|
||||
set -e
|
||||
|
||||
# Detect OS
|
||||
# detect OS
|
||||
detect_os() {
|
||||
case "$(uname -s)" in
|
||||
Linux*) echo "linux";;
|
||||
@@ -9,65 +9,65 @@ detect_os() {
|
||||
FreeBSD*) echo "freebsd";;
|
||||
MINGW*|MSYS*|CYGWIN*) echo "windows";;
|
||||
*)
|
||||
echo "Error: Unsupported OS $(uname -s)" >&2
|
||||
echo "error: unsupported OS $(uname -s)" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
# Detect architecture
|
||||
# detect architecture
|
||||
detect_arch() {
|
||||
case "$(uname -m)" in
|
||||
x86_64|amd64) echo "amd64";;
|
||||
aarch64|arm64) echo "arm64";;
|
||||
riscv64) echo "riscv64";;
|
||||
*)
|
||||
echo "Error: Unsupported architecture $(uname -m)" >&2
|
||||
echo "error: unsupported architecture $(uname -m)" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
# Set install directory
|
||||
# set install directory
|
||||
INSTALL_DIR="${INSTALL_DIR:-$HOME/.local/bin}"
|
||||
|
||||
# Detect platform
|
||||
# detect platform
|
||||
OS=$(detect_os)
|
||||
ARCH=$(detect_arch)
|
||||
|
||||
echo "Installing nak ($OS-$ARCH) to $INSTALL_DIR..."
|
||||
echo "installing nak ($OS-$ARCH) to $INSTALL_DIR..."
|
||||
|
||||
# Check if curl is available
|
||||
command -v curl >/dev/null 2>&1 || { echo "Error: curl is required" >&2; exit 1; }
|
||||
# check if curl is available
|
||||
command -v curl >/dev/null 2>&1 || { echo "error: curl is required" >&2; exit 1; }
|
||||
|
||||
# Get latest release tag
|
||||
# get latest release tag
|
||||
RELEASE_INFO=$(curl -s https://api.github.com/repos/fiatjaf/nak/releases/latest)
|
||||
TAG="${RELEASE_INFO#*\"tag_name\"}"
|
||||
TAG="${TAG#*\"}"
|
||||
TAG="${TAG%%\"*}"
|
||||
|
||||
[ -z "$TAG" ] && { echo "Error: Failed to fetch release info" >&2; exit 1; }
|
||||
[ -z "$TAG" ] && { echo "error: failed to fetch release info" >&2; exit 1; }
|
||||
|
||||
# Construct download URL
|
||||
# construct download URL
|
||||
BINARY_NAME="nak-${TAG}-${OS}-${ARCH}"
|
||||
[ "$OS" = "windows" ] && BINARY_NAME="${BINARY_NAME}.exe"
|
||||
DOWNLOAD_URL="https://github.com/fiatjaf/nak/releases/download/${TAG}/${BINARY_NAME}"
|
||||
|
||||
# Create install directory and download
|
||||
# create install directory and download
|
||||
mkdir -p "$INSTALL_DIR"
|
||||
TARGET_PATH="$INSTALL_DIR/nak"
|
||||
[ "$OS" = "windows" ] && TARGET_PATH="${TARGET_PATH}.exe"
|
||||
|
||||
if curl -sS -L -f -o "$TARGET_PATH" "$DOWNLOAD_URL"; then
|
||||
chmod +x "$TARGET_PATH"
|
||||
echo "Installed nak $TAG to $TARGET_PATH"
|
||||
|
||||
# Check if install dir is in PATH
|
||||
echo "installed nak $TAG to $TARGET_PATH"
|
||||
|
||||
# check if install dir is in PATH
|
||||
case ":$PATH:" in
|
||||
*":$INSTALL_DIR:"*) ;;
|
||||
*) echo "Note: Add $INSTALL_DIR to your PATH" ;;
|
||||
*) echo "note: add $INSTALL_DIR to your PATH" ;;
|
||||
esac
|
||||
else
|
||||
echo "Error: Download failed from $DOWNLOAD_URL" >&2
|
||||
echo "error: download failed from $DOWNLOAD_URL" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
@@ -54,6 +54,7 @@ var app = &cli.Command{
|
||||
nip,
|
||||
syncCmd,
|
||||
spell,
|
||||
profile,
|
||||
},
|
||||
Version: version,
|
||||
Flags: []cli.Flag{
|
||||
@@ -127,9 +128,7 @@ func main() {
|
||||
// a megahack to enable this curl command proxy
|
||||
if len(os.Args) > 2 && os.Args[1] == "curl" {
|
||||
if err := realCurl(); err != nil {
|
||||
if err != nil {
|
||||
log(color.YellowString(err.Error()) + "\n")
|
||||
}
|
||||
log(color.YellowString(err.Error()) + "\n")
|
||||
colors.reset()
|
||||
os.Exit(1)
|
||||
}
|
||||
@@ -137,9 +136,7 @@ func main() {
|
||||
}
|
||||
|
||||
if err := app.Run(context.Background(), os.Args); err != nil {
|
||||
if err != nil {
|
||||
log("%s\n", color.RedString(err.Error()))
|
||||
}
|
||||
log("%s\n", color.RedString(err.Error()))
|
||||
colors.reset()
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
@@ -158,29 +158,40 @@ var mcpServer = &cli.Command{
|
||||
name := required[string](r, "name")
|
||||
limit, _ := optional[float64](r, "limit")
|
||||
|
||||
filter := nostr.Filter{Search: name, Kinds: []nostr.Kind{0}}
|
||||
if limit > 0 {
|
||||
filter.Limit = int(limit)
|
||||
}
|
||||
|
||||
res := strings.Builder{}
|
||||
res.Grow(500)
|
||||
res.WriteString("search results: ")
|
||||
l := 0
|
||||
for result := range sys.Pool.FetchMany(ctx, []string{"relay.nostr.band", "nostr.wine"}, filter, nostr.SubscriptionOptions{
|
||||
Label: "nak-mcp-search",
|
||||
}) {
|
||||
l++
|
||||
pm, _ := sdk.ParseMetadata(result.Event)
|
||||
res.WriteString(fmt.Sprintf("\n\nResult %d\nUser name: \"%s\"\nPublic key: \"%s\"\nDescription: \"%s\"\n",
|
||||
l, pm.ShortName(), pm.PubKey.Hex(), pm.About))
|
||||
|
||||
if l >= int(limit) {
|
||||
break
|
||||
// check if input is already a valid pubkey
|
||||
if pubkey, err := nostr.PubKeyFromHex(name); err == nil {
|
||||
pm := sys.FetchProfileMetadata(ctx, pubkey)
|
||||
res.WriteString(fmt.Sprintf("\n\nResult 1\nUser name: \"%s\"\nPublic key: \"%s\"\nDescription: \"%s\"\n",
|
||||
pm.ShortName(), pm.PubKey.Hex(), pm.About))
|
||||
} else {
|
||||
// otherwise try to search
|
||||
filter := nostr.Filter{Search: name, Kinds: []nostr.Kind{0}}
|
||||
if limit > 0 {
|
||||
filter.Limit = int(limit)
|
||||
}
|
||||
|
||||
l := 0
|
||||
for result := range sys.Pool.FetchMany(ctx, []string{"relay.nostr.band", "nostr.wine", "search.nos.social"}, filter, nostr.SubscriptionOptions{
|
||||
Label: "nak-mcp-search",
|
||||
}) {
|
||||
l++
|
||||
pm, _ := sdk.ParseMetadata(result.Event)
|
||||
res.WriteString(fmt.Sprintf("\n\nResult %d\nUser name: \"%s\"\nPublic key: \"%s\"\nDescription: \"%s\"\n",
|
||||
l, pm.ShortName(), pm.PubKey.Hex(), pm.About))
|
||||
|
||||
if l >= int(limit) {
|
||||
break
|
||||
}
|
||||
}
|
||||
if l == 0 {
|
||||
return mcp.NewToolResultError("couldn't find anyone with that name."), nil
|
||||
}
|
||||
}
|
||||
if l == 0 {
|
||||
return mcp.NewToolResultError("couldn't find anyone with that name."), nil
|
||||
}
|
||||
|
||||
return mcp.NewToolResultText(res.String()), nil
|
||||
})
|
||||
|
||||
|
||||
+102
@@ -0,0 +1,102 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"fiatjaf.com/nostr/nip19"
|
||||
"github.com/fatih/color"
|
||||
"github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
var profile = &cli.Command{
|
||||
Name: "profile",
|
||||
Usage: "displays profile information for a given pubkey",
|
||||
Description: `fetches and displays profile metadata, relays, and contact count for a given pubkey.
|
||||
|
||||
example usage:
|
||||
nak profile npub1h8spmtw9m2huyv6v2j2qd5zv956z2zdugl6mgx02f2upffwpm3nqv0j4ps
|
||||
nak profile user@example.com`,
|
||||
ArgsUsage: "[pubkey]",
|
||||
Action: func(ctx context.Context, c *cli.Command) error {
|
||||
for pubkeyInput := range getStdinLinesOrArguments(c.Args()) {
|
||||
pk, err := parsePubKey(pubkeyInput)
|
||||
if err != nil {
|
||||
ctx = lineProcessingError(ctx, "invalid pubkey '%s': %s", pubkeyInput, err)
|
||||
continue
|
||||
}
|
||||
|
||||
pm := sys.FetchProfileMetadata(ctx, pk)
|
||||
|
||||
npub := nip19.EncodeNpub(pk)
|
||||
stdout(colors.bold("pubkey (hex):"), pk.Hex())
|
||||
stdout(colors.bold("npub:"), color.HiCyanString(npub))
|
||||
|
||||
relayList := sys.FetchRelayList(ctx, pk)
|
||||
writeRelays := make([]string, 0, 3)
|
||||
for _, rl := range relayList.Items {
|
||||
if rl.Outbox {
|
||||
writeRelays = append(writeRelays, rl.URL)
|
||||
if len(writeRelays) == 3 {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(writeRelays) > 0 {
|
||||
nprofile := nip19.EncodeNprofile(pk, writeRelays)
|
||||
stdout(colors.bold("profile uri:"), color.HiCyanString("nostr:"+nprofile))
|
||||
}
|
||||
|
||||
if pm.Name != "" {
|
||||
stdout(colors.bold("name:"), color.HiBlueString(pm.Name))
|
||||
}
|
||||
if pm.DisplayName != "" {
|
||||
stdout(colors.bold("display_name:"), color.HiBlueString(pm.DisplayName))
|
||||
}
|
||||
if pm.About != "" {
|
||||
stdout(colors.bold("about:"), color.HiBlueString(pm.About))
|
||||
}
|
||||
if pm.Picture != "" {
|
||||
stdout(colors.bold("picture:"), color.HiBlueString(pm.Picture))
|
||||
}
|
||||
if pm.Banner != "" {
|
||||
stdout(colors.bold("banner:"), color.HiBlueString(pm.Banner))
|
||||
}
|
||||
if pm.Website != "" {
|
||||
stdout(colors.bold("website:"), color.HiBlueString(pm.Website))
|
||||
}
|
||||
if pm.NIP05 != "" {
|
||||
isValid := pm.NIP05Valid(ctx)
|
||||
if isValid {
|
||||
stdout(colors.bold("nip05:"), color.HiGreenString(pm.NIP05), color.HiGreenString("(verified)"))
|
||||
} else {
|
||||
stdout(colors.bold("nip05:"), color.HiRedString(pm.NIP05), color.HiRedString("(not verified)"))
|
||||
}
|
||||
}
|
||||
if pm.LUD16 != "" {
|
||||
stdout(colors.bold("lud16:"), color.HiBlueString(pm.LUD16))
|
||||
}
|
||||
|
||||
if len(relayList.Items) > 0 {
|
||||
stdout(colors.bold("relays:"))
|
||||
for _, relay := range relayList.Items {
|
||||
access := ""
|
||||
if relay.Inbox && relay.Outbox {
|
||||
access = "read/write"
|
||||
} else if relay.Inbox {
|
||||
access = "read"
|
||||
} else if relay.Outbox {
|
||||
access = "write"
|
||||
}
|
||||
stdout(" ", color.HiBlueString(relay.URL), color.HiCyanString("(%s)", access))
|
||||
}
|
||||
}
|
||||
|
||||
followList := sys.FetchFollowList(ctx, pk)
|
||||
contactCount := len(followList.Items)
|
||||
stdout(colors.bold("follows:"), color.HiCyanString("%d", contactCount))
|
||||
}
|
||||
|
||||
exitIfLineProcessingError(ctx)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
@@ -139,7 +139,11 @@ var wallet = &cli.Command{
|
||||
}
|
||||
|
||||
for _, url := range w.Mints {
|
||||
stdout(strings.Split(url, "://")[1])
|
||||
if _, host, ok := strings.Cut(url, "://"); ok {
|
||||
stdout(host)
|
||||
} else {
|
||||
stdout(url)
|
||||
}
|
||||
}
|
||||
|
||||
closew()
|
||||
@@ -195,7 +199,11 @@ var wallet = &cli.Command{
|
||||
}
|
||||
|
||||
for _, token := range w.Tokens {
|
||||
stdout(token.ID(), token.Proofs.Amount(), strings.Split(token.Mint, "://")[1])
|
||||
_, mintHost, _ := strings.Cut(token.Mint, "://")
|
||||
if mintHost == "" {
|
||||
mintHost = token.Mint
|
||||
}
|
||||
stdout(token.ID(), token.Proofs.Amount(), mintHost)
|
||||
}
|
||||
|
||||
closew()
|
||||
@@ -221,7 +229,11 @@ var wallet = &cli.Command{
|
||||
for _, token := range w.Tokens {
|
||||
if slices.Contains(ids, token.ID()) {
|
||||
w.DropToken(ctx, token.ID())
|
||||
log("dropped %s %d %s\n", token.ID(), token.Proofs.Amount(), strings.Split(token.Mint, "://")[1])
|
||||
_, mintHost, _ := strings.Cut(token.Mint, "://")
|
||||
if mintHost == "" {
|
||||
mintHost = token.Mint
|
||||
}
|
||||
log("dropped %s %d %s\n", token.ID(), token.Proofs.Amount(), mintHost)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user