mirror of
https://github.com/fiatjaf/nak.git
synced 2026-02-01 06:48:51 +00:00
Compare commits
4 Commits
e05b455a05
...
v0.17.9
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
186ead1408 | ||
|
|
b2d5aa9bc2 | ||
|
|
0e6a6d7506 | ||
|
|
cff60b2f9f |
@@ -427,7 +427,6 @@ gitnostr.com... ok.
|
|||||||
```shell
|
```shell
|
||||||
~> nak git clone
|
~> nak git clone
|
||||||
~> nak git init
|
~> nak git init
|
||||||
~> nak git status
|
|
||||||
~> nak git sync
|
~> nak git sync
|
||||||
~> nak git fetch
|
~> nak git fetch
|
||||||
~> nak git pull
|
~> nak git pull
|
||||||
|
|||||||
157
blossom.go
157
blossom.go
@@ -3,10 +3,15 @@ package main
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
|
"crypto/sha256"
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/hex"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
|
"fiatjaf.com/nostr"
|
||||||
"fiatjaf.com/nostr/keyer"
|
"fiatjaf.com/nostr/keyer"
|
||||||
"fiatjaf.com/nostr/nipb0/blossom"
|
"fiatjaf.com/nostr/nipb0/blossom"
|
||||||
"github.com/urfave/cli/v3"
|
"github.com/urfave/cli/v3"
|
||||||
@@ -229,56 +234,47 @@ if any of the files are not found the command will fail, otherwise it will succe
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: "mirror",
|
Name: "mirror",
|
||||||
Usage: "mirrors a from a server to another",
|
Usage: "mirrors blobs from source server to target server",
|
||||||
Description: `examples:
|
Description: `lists all blobs from the source server and mirrors them to the target server using BUD-04. requires --sec to sign the authorization event.`,
|
||||||
mirroring a single blob:
|
|
||||||
nak blossom mirror https://nostr.download/5672be22e6da91c12b929a0f46b9e74de8b5366b9b19a645ff949c24052f9ad4 -s blossom.band
|
|
||||||
|
|
||||||
mirroring all blobs from a certain pubkey from one server to the other:
|
|
||||||
nak blossom list 78ce6faa72264387284e647ba6938995735ec8c7d5c5a65737e55130f026307d -s nostr.download | nak blossom mirror -s blossom.band`,
|
|
||||||
DisableSliceFlagSeparator: true,
|
DisableSliceFlagSeparator: true,
|
||||||
Action: func(ctx context.Context, c *cli.Command) error {
|
Action: func(ctx context.Context, c *cli.Command) error {
|
||||||
client, err := getBlossomClient(ctx, c)
|
targetClient, err := getBlossomClient(ctx, c)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
var bd blossom.BlobDescriptor
|
// Create client for source server
|
||||||
if input := c.Args().First(); input != "" {
|
sourceServer := c.Args().First()
|
||||||
blobURL := input
|
keyer, _, err := gatherKeyerFromArguments(ctx, c)
|
||||||
if err := json.Unmarshal([]byte(input), &bd); err == nil {
|
if err != nil {
|
||||||
blobURL = bd.URL
|
return err
|
||||||
}
|
}
|
||||||
bd, err := client.MirrorBlob(ctx, blobURL)
|
sourceClient := blossom.NewClient(sourceServer, keyer)
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
out, _ := json.Marshal(bd)
|
|
||||||
stdout(out)
|
|
||||||
return nil
|
|
||||||
} else {
|
|
||||||
for input := range getJsonsOrBlank() {
|
|
||||||
if input == "{}" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
blobURL := input
|
// Get list of blobs from source server
|
||||||
if err := json.Unmarshal([]byte(input), &bd); err == nil {
|
bds, err := sourceClient.List(ctx)
|
||||||
blobURL = bd.URL
|
if err != nil {
|
||||||
}
|
return fmt.Errorf("failed to list blobs from source server: %w", err)
|
||||||
bd, err := client.MirrorBlob(ctx, blobURL)
|
|
||||||
if err != nil {
|
|
||||||
ctx = lineProcessingError(ctx, "failed to mirror '%s': %w", blobURL, err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
out, _ := json.Marshal(bd)
|
|
||||||
stdout(out)
|
|
||||||
}
|
|
||||||
|
|
||||||
exitIfLineProcessingError(ctx)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Mirror each blob to target server
|
||||||
|
hasError := false
|
||||||
|
for _, bd := range bds {
|
||||||
|
mirrored, err := mirrorBlob(ctx, targetClient, bd.URL)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "failed to mirror %s: %s\n", bd.SHA256, err)
|
||||||
|
hasError = true
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
j, _ := json.Marshal(mirrored)
|
||||||
|
stdout(string(j))
|
||||||
|
}
|
||||||
|
|
||||||
|
if hasError {
|
||||||
|
os.Exit(3)
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -292,3 +288,82 @@ func getBlossomClient(ctx context.Context, c *cli.Command) (*blossom.Client, err
|
|||||||
}
|
}
|
||||||
return blossom.NewClient(c.String("server"), keyer), nil
|
return blossom.NewClient(c.String("server"), keyer), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// mirrorBlob mirrors a blob from a URL to the mediaserver using BUD-04
|
||||||
|
func mirrorBlob(ctx context.Context, client *blossom.Client, url string) (*blossom.BlobDescriptor, error) {
|
||||||
|
resp, err := http.Get(url)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to download blob from URL: %w", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
return nil, fmt.Errorf("failed to download blob: HTTP %d", resp.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to read blob content: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
hash := sha256.Sum256(data)
|
||||||
|
hashHex := hex.EncodeToString(hash[:])
|
||||||
|
|
||||||
|
signer := client.GetSigner()
|
||||||
|
pubkey, _ := signer.GetPublicKey(ctx)
|
||||||
|
|
||||||
|
evt := nostr.Event{
|
||||||
|
Kind: 24242,
|
||||||
|
CreatedAt: nostr.Now(),
|
||||||
|
Tags: nostr.Tags{
|
||||||
|
{"t", "upload"},
|
||||||
|
{"x", hashHex},
|
||||||
|
{"expiration", fmt.Sprintf("%d", nostr.Now()+60)},
|
||||||
|
},
|
||||||
|
Content: "blossom stuff",
|
||||||
|
PubKey: pubkey,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := signer.SignEvent(ctx, &evt); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to sign authorization event: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
evtj, err := json.Marshal(evt)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to marshal authorization event: %w", err)
|
||||||
|
}
|
||||||
|
auth := base64.StdEncoding.EncodeToString(evtj)
|
||||||
|
|
||||||
|
mediaserver := client.GetMediaServer()
|
||||||
|
mirrorURL := mediaserver + "mirror"
|
||||||
|
|
||||||
|
requestBody := map[string]string{"url": url}
|
||||||
|
requestJSON, _ := json.Marshal(requestBody)
|
||||||
|
|
||||||
|
req, err := http.NewRequestWithContext(ctx, "PUT", mirrorURL, bytes.NewReader(requestJSON))
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to create mirror request: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
req.Header.Set("Authorization", "Nostr "+auth)
|
||||||
|
|
||||||
|
httpClient := &http.Client{}
|
||||||
|
mirrorResp, err := httpClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to send mirror request: %w", err)
|
||||||
|
}
|
||||||
|
defer mirrorResp.Body.Close()
|
||||||
|
|
||||||
|
if mirrorResp.StatusCode < 200 || mirrorResp.StatusCode >= 300 {
|
||||||
|
body, _ := io.ReadAll(mirrorResp.Body)
|
||||||
|
return nil, fmt.Errorf("mirror request failed with HTTP %d: %s", mirrorResp.StatusCode, string(body))
|
||||||
|
}
|
||||||
|
|
||||||
|
var bd blossom.BlobDescriptor
|
||||||
|
if err := json.NewDecoder(mirrorResp.Body).Decode(&bd); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to decode blob descriptor: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &bd, nil
|
||||||
|
}
|
||||||
|
|||||||
2
event.go
2
event.go
@@ -145,7 +145,7 @@ example:
|
|||||||
if relayUrls := c.Args().Slice(); len(relayUrls) > 0 {
|
if relayUrls := c.Args().Slice(); len(relayUrls) > 0 {
|
||||||
relays = connectToAllRelays(ctx, c, relayUrls, nil,
|
relays = connectToAllRelays(ctx, c, relayUrls, nil,
|
||||||
nostr.PoolOptions{
|
nostr.PoolOptions{
|
||||||
AuthRequiredHandler: func(ctx context.Context, authEvent *nostr.Event) error {
|
AuthHandler: func(ctx context.Context, authEvent *nostr.Event) error {
|
||||||
return authSigner(ctx, c, func(s string, args ...any) {}, authEvent)
|
return authSigner(ctx, c, func(s string, args ...any) {}, authEvent)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
2
fs.go
2
fs.go
@@ -1,4 +1,4 @@
|
|||||||
//go:build !windows && !openbsd && !cgofuse
|
//go:build !windows && !openbsd
|
||||||
|
|
||||||
package main
|
package main
|
||||||
|
|
||||||
|
|||||||
118
fs_cgo.go
118
fs_cgo.go
@@ -1,118 +0,0 @@
|
|||||||
//go:build cgofuse && !windows && !openbsd
|
|
||||||
|
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"os/signal"
|
|
||||||
"syscall"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"fiatjaf.com/nostr"
|
|
||||||
"fiatjaf.com/nostr/keyer"
|
|
||||||
"github.com/fatih/color"
|
|
||||||
nostrfs "github.com/fiatjaf/nak/nostrfs_cgo"
|
|
||||||
"github.com/urfave/cli/v3"
|
|
||||||
"github.com/winfsp/cgofuse/fuse"
|
|
||||||
)
|
|
||||||
|
|
||||||
var fsCmd = &cli.Command{
|
|
||||||
Name: "fs",
|
|
||||||
Usage: "mount a FUSE filesystem that exposes Nostr events as files.",
|
|
||||||
Description: `(experimental)`,
|
|
||||||
ArgsUsage: "<mountpoint>",
|
|
||||||
Flags: append(defaultKeyFlags,
|
|
||||||
&PubKeyFlag{
|
|
||||||
Name: "pubkey",
|
|
||||||
Usage: "public key from where to to prepopulate directories",
|
|
||||||
},
|
|
||||||
&cli.DurationFlag{
|
|
||||||
Name: "auto-publish-notes",
|
|
||||||
Usage: "delay after which new notes will be auto-published, set to -1 to not publish.",
|
|
||||||
Value: time.Second * 30,
|
|
||||||
},
|
|
||||||
&cli.DurationFlag{
|
|
||||||
Name: "auto-publish-articles",
|
|
||||||
Usage: "delay after which edited articles will be auto-published.",
|
|
||||||
Value: time.Hour * 24 * 365 * 2,
|
|
||||||
DefaultText: "basically infinite",
|
|
||||||
},
|
|
||||||
),
|
|
||||||
DisableSliceFlagSeparator: true,
|
|
||||||
Action: func(ctx context.Context, c *cli.Command) error {
|
|
||||||
mountpoint := c.Args().First()
|
|
||||||
if mountpoint == "" {
|
|
||||||
return fmt.Errorf("must be called with a directory path to serve as the mountpoint as an argument")
|
|
||||||
}
|
|
||||||
|
|
||||||
var kr nostr.User
|
|
||||||
if signer, _, err := gatherKeyerFromArguments(ctx, c); err == nil {
|
|
||||||
kr = signer
|
|
||||||
} else {
|
|
||||||
kr = keyer.NewReadOnlyUser(getPubKey(c, "pubkey"))
|
|
||||||
}
|
|
||||||
|
|
||||||
apnt := c.Duration("auto-publish-notes")
|
|
||||||
if apnt < 0 {
|
|
||||||
apnt = time.Hour * 24 * 365 * 3
|
|
||||||
}
|
|
||||||
apat := c.Duration("auto-publish-articles")
|
|
||||||
if apat < 0 {
|
|
||||||
apat = time.Hour * 24 * 365 * 3
|
|
||||||
}
|
|
||||||
|
|
||||||
root := nostrfs.NewNostrRoot(
|
|
||||||
context.WithValue(
|
|
||||||
context.WithValue(
|
|
||||||
ctx,
|
|
||||||
"log", log,
|
|
||||||
),
|
|
||||||
"logverbose", logverbose,
|
|
||||||
),
|
|
||||||
sys,
|
|
||||||
kr,
|
|
||||||
mountpoint,
|
|
||||||
nostrfs.Options{
|
|
||||||
AutoPublishNotesTimeout: apnt,
|
|
||||||
AutoPublishArticlesTimeout: apat,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
// create the server
|
|
||||||
log("- mounting at %s... ", color.HiCyanString(mountpoint))
|
|
||||||
|
|
||||||
// create cgofuse host
|
|
||||||
host := fuse.NewFileSystemHost(root)
|
|
||||||
host.SetCapReaddirPlus(true)
|
|
||||||
host.SetUseIno(true)
|
|
||||||
|
|
||||||
// mount the filesystem
|
|
||||||
mountArgs := []string{"-s", mountpoint}
|
|
||||||
if isVerbose {
|
|
||||||
mountArgs = append([]string{"-d"}, mountArgs...)
|
|
||||||
}
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
host.Mount("", mountArgs)
|
|
||||||
}()
|
|
||||||
|
|
||||||
log("ok.\n")
|
|
||||||
|
|
||||||
// setup signal handling for clean unmount
|
|
||||||
ch := make(chan os.Signal, 1)
|
|
||||||
chErr := make(chan error)
|
|
||||||
signal.Notify(ch, syscall.SIGINT, syscall.SIGTERM)
|
|
||||||
go func() {
|
|
||||||
<-ch
|
|
||||||
log("- unmounting... ")
|
|
||||||
// cgofuse doesn't have explicit unmount, it unmounts on process exit
|
|
||||||
log("ok\n")
|
|
||||||
chErr <- nil
|
|
||||||
}()
|
|
||||||
|
|
||||||
// wait for signals
|
|
||||||
return <-chErr
|
|
||||||
},
|
|
||||||
}
|
|
||||||
@@ -1,4 +1,6 @@
|
|||||||
package nostrfs
|
//go:build windows
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
@@ -17,18 +19,18 @@ import (
|
|||||||
"github.com/winfsp/cgofuse/fuse"
|
"github.com/winfsp/cgofuse/fuse"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Options struct {
|
type FSOptions struct {
|
||||||
AutoPublishNotesTimeout time.Duration
|
AutoPublishNotesTimeout time.Duration
|
||||||
AutoPublishArticlesTimeout time.Duration
|
AutoPublishArticlesTimeout time.Duration
|
||||||
}
|
}
|
||||||
|
|
||||||
type NostrRoot struct {
|
type FSRoot struct {
|
||||||
fuse.FileSystemBase
|
fuse.FileSystemBase
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
sys *sdk.System
|
sys *sdk.System
|
||||||
rootPubKey nostr.PubKey
|
rootPubKey nostr.PubKey
|
||||||
signer nostr.Signer
|
signer nostr.Signer
|
||||||
opts Options
|
opts FSOptions
|
||||||
mountpoint string
|
mountpoint string
|
||||||
|
|
||||||
mu sync.RWMutex
|
mu sync.RWMutex
|
||||||
@@ -51,9 +53,9 @@ type FSNode struct {
|
|||||||
loaded bool
|
loaded bool
|
||||||
}
|
}
|
||||||
|
|
||||||
var _ fuse.FileSystemInterface = (*NostrRoot)(nil)
|
var _ fuse.FileSystemInterface = (*FSRoot)(nil)
|
||||||
|
|
||||||
func NewNostrRoot(ctx context.Context, sys interface{}, user interface{}, mountpoint string, o Options) *NostrRoot {
|
func NewFSRoot(ctx context.Context, sys interface{}, user interface{}, mountpoint string, o FSOptions) *FSRoot {
|
||||||
var system *sdk.System
|
var system *sdk.System
|
||||||
if sys != nil {
|
if sys != nil {
|
||||||
system = sys.(*sdk.System)
|
system = sys.(*sdk.System)
|
||||||
@@ -71,7 +73,7 @@ func NewNostrRoot(ctx context.Context, sys interface{}, user interface{}, mountp
|
|||||||
|
|
||||||
abs, _ := filepath.Abs(mountpoint)
|
abs, _ := filepath.Abs(mountpoint)
|
||||||
|
|
||||||
root := &NostrRoot{
|
root := &FSRoot{
|
||||||
ctx: ctx,
|
ctx: ctx,
|
||||||
sys: system,
|
sys: system,
|
||||||
rootPubKey: pubkey,
|
rootPubKey: pubkey,
|
||||||
@@ -101,7 +103,7 @@ func NewNostrRoot(ctx context.Context, sys interface{}, user interface{}, mountp
|
|||||||
return root
|
return root
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *NostrRoot) initialize() {
|
func (r *FSRoot) initialize() {
|
||||||
if r.rootPubKey == nostr.ZeroPK {
|
if r.rootPubKey == nostr.ZeroPK {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -146,7 +148,7 @@ func (r *NostrRoot) initialize() {
|
|||||||
r.nodes["/"].children["@me"] = meNode
|
r.nodes["/"].children["@me"] = meNode
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *NostrRoot) fetchMetadata(dirPath string, pubkey nostr.PubKey) {
|
func (r *FSRoot) fetchMetadata(dirPath string, pubkey nostr.PubKey) {
|
||||||
pm := r.sys.FetchProfileMetadata(r.ctx, pubkey)
|
pm := r.sys.FetchProfileMetadata(r.ctx, pubkey)
|
||||||
if pm.Event == nil {
|
if pm.Event == nil {
|
||||||
return
|
return
|
||||||
@@ -175,7 +177,7 @@ func (r *NostrRoot) fetchMetadata(dirPath string, pubkey nostr.PubKey) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *NostrRoot) fetchProfilePicture(dirPath string, pubkey nostr.PubKey) {
|
func (r *FSRoot) fetchProfilePicture(dirPath string, pubkey nostr.PubKey) {
|
||||||
pm := r.sys.FetchProfileMetadata(r.ctx, pubkey)
|
pm := r.sys.FetchProfileMetadata(r.ctx, pubkey)
|
||||||
if pm.Event == nil || pm.Picture == "" {
|
if pm.Event == nil || pm.Picture == "" {
|
||||||
return
|
return
|
||||||
@@ -256,7 +258,7 @@ func (r *NostrRoot) fetchProfilePicture(dirPath string, pubkey nostr.PubKey) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *NostrRoot) fetchEvents(dirPath string, filter nostr.Filter) {
|
func (r *FSRoot) fetchEvents(dirPath string, filter nostr.Filter) {
|
||||||
ctx, cancel := context.WithTimeout(r.ctx, time.Second*10)
|
ctx, cancel := context.WithTimeout(r.ctx, time.Second*10)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
@@ -355,7 +357,7 @@ func (r *NostrRoot) fetchEvents(dirPath string, filter nostr.Filter) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *NostrRoot) eventToFilename(evt *nostr.Event) string {
|
func (r *FSRoot) eventToFilename(evt *nostr.Event) string {
|
||||||
// use event ID first 8 chars + extension based on kind
|
// use event ID first 8 chars + extension based on kind
|
||||||
ext := kindToExtension(evt.Kind)
|
ext := kindToExtension(evt.Kind)
|
||||||
|
|
||||||
@@ -391,14 +393,14 @@ func (r *NostrRoot) eventToFilename(evt *nostr.Event) string {
|
|||||||
return fmt.Sprintf("%s.%s", idHex, ext)
|
return fmt.Sprintf("%s.%s", idHex, ext)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *NostrRoot) getLog() func(string, ...interface{}) {
|
func (r *FSRoot) getLog() func(string, ...interface{}) {
|
||||||
if log := r.ctx.Value("log"); log != nil {
|
if log := r.ctx.Value("log"); log != nil {
|
||||||
return log.(func(string, ...interface{}))
|
return log.(func(string, ...interface{}))
|
||||||
}
|
}
|
||||||
return func(string, ...interface{}) {}
|
return func(string, ...interface{}) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *NostrRoot) getNode(path string) *FSNode {
|
func (r *FSRoot) getNode(path string) *FSNode {
|
||||||
originalPath := path
|
originalPath := path
|
||||||
|
|
||||||
// normalize path
|
// normalize path
|
||||||
@@ -451,7 +453,7 @@ func (r *NostrRoot) getNode(path string) *FSNode {
|
|||||||
return node
|
return node
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *NostrRoot) Getattr(path string, stat *fuse.Stat_t, fh uint64) int {
|
func (r *FSRoot) Getattr(path string, stat *fuse.Stat_t, fh uint64) int {
|
||||||
node := r.getNode(path)
|
node := r.getNode(path)
|
||||||
|
|
||||||
// if node doesn't exist, try dynamic lookup
|
// if node doesn't exist, try dynamic lookup
|
||||||
@@ -480,7 +482,7 @@ func (r *NostrRoot) Getattr(path string, stat *fuse.Stat_t, fh uint64) int {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// dynamicLookup tries to create nodes on-demand for npub/note/nevent paths
|
// dynamicLookup tries to create nodes on-demand for npub/note/nevent paths
|
||||||
func (r *NostrRoot) dynamicLookup(path string) bool {
|
func (r *FSRoot) dynamicLookup(path string) bool {
|
||||||
// normalize path
|
// normalize path
|
||||||
path = strings.ReplaceAll(path, "\\", "/")
|
path = strings.ReplaceAll(path, "\\", "/")
|
||||||
if !strings.HasPrefix(path, "/") {
|
if !strings.HasPrefix(path, "/") {
|
||||||
@@ -535,7 +537,7 @@ func (r *NostrRoot) dynamicLookup(path string) bool {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *NostrRoot) createNpubDirLocked(npub string, pubkey nostr.PubKey, signer nostr.Signer) {
|
func (r *FSRoot) createNpubDirLocked(npub string, pubkey nostr.PubKey, signer nostr.Signer) {
|
||||||
dirPath := "/" + npub
|
dirPath := "/" + npub
|
||||||
|
|
||||||
// check if already exists
|
// check if already exists
|
||||||
@@ -628,7 +630,7 @@ func (r *NostrRoot) createNpubDirLocked(npub string, pubkey nostr.PubKey, signer
|
|||||||
go r.fetchProfilePicture(dirPath, pubkey)
|
go r.fetchProfilePicture(dirPath, pubkey)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *NostrRoot) createViewDirLocked(parentPath, name string, filter nostr.Filter) {
|
func (r *FSRoot) createViewDirLocked(parentPath, name string, filter nostr.Filter) {
|
||||||
dirPath := parentPath + "/" + name
|
dirPath := parentPath + "/" + name
|
||||||
|
|
||||||
// check if already exists
|
// check if already exists
|
||||||
@@ -656,7 +658,7 @@ func (r *NostrRoot) createViewDirLocked(parentPath, name string, filter nostr.Fi
|
|||||||
go r.fetchEvents(dirPath, filter)
|
go r.fetchEvents(dirPath, filter)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *NostrRoot) createEventDirLocked(name string, pointer nostr.EventPointer) bool {
|
func (r *FSRoot) createEventDirLocked(name string, pointer nostr.EventPointer) bool {
|
||||||
dirPath := "/" + name
|
dirPath := "/" + name
|
||||||
|
|
||||||
// fetch the event
|
// fetch the event
|
||||||
@@ -737,7 +739,7 @@ func (r *NostrRoot) createEventDirLocked(name string, pointer nostr.EventPointer
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *NostrRoot) Readdir(path string,
|
func (r *FSRoot) Readdir(path string,
|
||||||
fill func(name string, stat *fuse.Stat_t, ofst int64) bool,
|
fill func(name string, stat *fuse.Stat_t, ofst int64) bool,
|
||||||
ofst int64,
|
ofst int64,
|
||||||
fh uint64,
|
fh uint64,
|
||||||
@@ -768,7 +770,7 @@ func (r *NostrRoot) Readdir(path string,
|
|||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *NostrRoot) Open(path string, flags int) (int, uint64) {
|
func (r *FSRoot) Open(path string, flags int) (int, uint64) {
|
||||||
// log the open attempt
|
// log the open attempt
|
||||||
if r.ctx.Value("logverbose") != nil {
|
if r.ctx.Value("logverbose") != nil {
|
||||||
logv := r.ctx.Value("logverbose").(func(string, ...interface{}))
|
logv := r.ctx.Value("logverbose").(func(string, ...interface{}))
|
||||||
@@ -799,7 +801,7 @@ func (r *NostrRoot) Open(path string, flags int) (int, uint64) {
|
|||||||
return 0, node.ino
|
return 0, node.ino
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *NostrRoot) Read(path string, buff []byte, ofst int64, fh uint64) int {
|
func (r *FSRoot) Read(path string, buff []byte, ofst int64, fh uint64) int {
|
||||||
node := r.getNode(path)
|
node := r.getNode(path)
|
||||||
if node == nil || node.isDir {
|
if node == nil || node.isDir {
|
||||||
return -fuse.ENOENT
|
return -fuse.ENOENT
|
||||||
@@ -818,7 +820,7 @@ func (r *NostrRoot) Read(path string, buff []byte, ofst int64, fh uint64) int {
|
|||||||
return n
|
return n
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *NostrRoot) Opendir(path string) (int, uint64) {
|
func (r *FSRoot) Opendir(path string) (int, uint64) {
|
||||||
node := r.getNode(path)
|
node := r.getNode(path)
|
||||||
if node == nil {
|
if node == nil {
|
||||||
return -fuse.ENOENT, ^uint64(0)
|
return -fuse.ENOENT, ^uint64(0)
|
||||||
@@ -829,16 +831,16 @@ func (r *NostrRoot) Opendir(path string) (int, uint64) {
|
|||||||
return 0, node.ino
|
return 0, node.ino
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *NostrRoot) Release(path string, fh uint64) int {
|
func (r *FSRoot) Release(path string, fh uint64) int {
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *NostrRoot) Releasedir(path string, fh uint64) int {
|
func (r *FSRoot) Releasedir(path string, fh uint64) int {
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create creates a new file
|
// Create creates a new file
|
||||||
func (r *NostrRoot) Create(path string, flags int, mode uint32) (int, uint64) {
|
func (r *FSRoot) Create(path string, flags int, mode uint32) (int, uint64) {
|
||||||
// parse path
|
// parse path
|
||||||
path = strings.ReplaceAll(path, "\\", "/")
|
path = strings.ReplaceAll(path, "\\", "/")
|
||||||
if !strings.HasPrefix(path, "/") {
|
if !strings.HasPrefix(path, "/") {
|
||||||
@@ -882,7 +884,7 @@ func (r *NostrRoot) Create(path string, flags int, mode uint32) (int, uint64) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Truncate truncates a file
|
// Truncate truncates a file
|
||||||
func (r *NostrRoot) Truncate(path string, size int64, fh uint64) int {
|
func (r *FSRoot) Truncate(path string, size int64, fh uint64) int {
|
||||||
node := r.getNode(path)
|
node := r.getNode(path)
|
||||||
if node == nil {
|
if node == nil {
|
||||||
return -fuse.ENOENT
|
return -fuse.ENOENT
|
||||||
@@ -911,7 +913,7 @@ func (r *NostrRoot) Truncate(path string, size int64, fh uint64) int {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Write writes data to a file
|
// Write writes data to a file
|
||||||
func (r *NostrRoot) Write(path string, buff []byte, ofst int64, fh uint64) int {
|
func (r *FSRoot) Write(path string, buff []byte, ofst int64, fh uint64) int {
|
||||||
node := r.getNode(path)
|
node := r.getNode(path)
|
||||||
if node == nil {
|
if node == nil {
|
||||||
return -fuse.ENOENT
|
return -fuse.ENOENT
|
||||||
@@ -955,7 +957,7 @@ func (r *NostrRoot) Write(path string, buff []byte, ofst int64, fh uint64) int {
|
|||||||
return n
|
return n
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *NostrRoot) publishNote(path string) {
|
func (r *FSRoot) publishNote(path string) {
|
||||||
r.mu.Lock()
|
r.mu.Lock()
|
||||||
node, ok := r.nodes[path]
|
node, ok := r.nodes[path]
|
||||||
if !ok {
|
if !ok {
|
||||||
@@ -1032,7 +1034,7 @@ func (r *NostrRoot) publishNote(path string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Unlink deletes a file
|
// Unlink deletes a file
|
||||||
func (r *NostrRoot) Unlink(path string) int {
|
func (r *FSRoot) Unlink(path string) int {
|
||||||
path = strings.ReplaceAll(path, "\\", "/")
|
path = strings.ReplaceAll(path, "\\", "/")
|
||||||
if !strings.HasPrefix(path, "/") {
|
if !strings.HasPrefix(path, "/") {
|
||||||
path = "/" + path
|
path = "/" + path
|
||||||
@@ -1065,7 +1067,7 @@ func (r *NostrRoot) Unlink(path string) int {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Mkdir creates a new directory
|
// Mkdir creates a new directory
|
||||||
func (r *NostrRoot) Mkdir(path string, mode uint32) int {
|
func (r *FSRoot) Mkdir(path string, mode uint32) int {
|
||||||
path = strings.ReplaceAll(path, "\\", "/")
|
path = strings.ReplaceAll(path, "\\", "/")
|
||||||
if !strings.HasPrefix(path, "/") {
|
if !strings.HasPrefix(path, "/") {
|
||||||
path = "/" + path
|
path = "/" + path
|
||||||
@@ -1107,7 +1109,7 @@ func (r *NostrRoot) Mkdir(path string, mode uint32) int {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Rmdir removes a directory
|
// Rmdir removes a directory
|
||||||
func (r *NostrRoot) Rmdir(path string) int {
|
func (r *FSRoot) Rmdir(path string) int {
|
||||||
path = strings.ReplaceAll(path, "\\", "/")
|
path = strings.ReplaceAll(path, "\\", "/")
|
||||||
if !strings.HasPrefix(path, "/") {
|
if !strings.HasPrefix(path, "/") {
|
||||||
path = "/" + path
|
path = "/" + path
|
||||||
@@ -1149,7 +1151,7 @@ func (r *NostrRoot) Rmdir(path string) int {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Utimens updates file timestamps
|
// Utimens updates file timestamps
|
||||||
func (r *NostrRoot) Utimens(path string, tmsp []fuse.Timespec) int {
|
func (r *FSRoot) Utimens(path string, tmsp []fuse.Timespec) int {
|
||||||
node := r.getNode(path)
|
node := r.getNode(path)
|
||||||
if node == nil {
|
if node == nil {
|
||||||
return -fuse.ENOENT
|
return -fuse.ENOENT
|
||||||
@@ -12,7 +12,6 @@ import (
|
|||||||
"fiatjaf.com/nostr"
|
"fiatjaf.com/nostr"
|
||||||
"fiatjaf.com/nostr/keyer"
|
"fiatjaf.com/nostr/keyer"
|
||||||
"github.com/fatih/color"
|
"github.com/fatih/color"
|
||||||
nostrfs "github.com/fiatjaf/nak/nostrfs_cgo"
|
|
||||||
"github.com/urfave/cli/v3"
|
"github.com/urfave/cli/v3"
|
||||||
"github.com/winfsp/cgofuse/fuse"
|
"github.com/winfsp/cgofuse/fuse"
|
||||||
)
|
)
|
||||||
@@ -62,7 +61,7 @@ var fsCmd = &cli.Command{
|
|||||||
apat = time.Hour * 24 * 365 * 3
|
apat = time.Hour * 24 * 365 * 3
|
||||||
}
|
}
|
||||||
|
|
||||||
root := nostrfs.NewNostrRoot(
|
root := NewFSRoot(
|
||||||
context.WithValue(
|
context.WithValue(
|
||||||
context.WithValue(
|
context.WithValue(
|
||||||
ctx,
|
ctx,
|
||||||
@@ -73,7 +72,7 @@ var fsCmd = &cli.Command{
|
|||||||
sys,
|
sys,
|
||||||
kr,
|
kr,
|
||||||
mountpoint,
|
mountpoint,
|
||||||
nostrfs.Options{
|
FSOptions{
|
||||||
AutoPublishNotesTimeout: apnt,
|
AutoPublishNotesTimeout: apnt,
|
||||||
AutoPublishArticlesTimeout: apat,
|
AutoPublishArticlesTimeout: apat,
|
||||||
},
|
},
|
||||||
|
|||||||
162
git.go
162
git.go
@@ -181,7 +181,7 @@ aside from those, there is also:
|
|||||||
var fetchedRepo *nip34.Repository
|
var fetchedRepo *nip34.Repository
|
||||||
if existingConfig.Identifier == "" {
|
if existingConfig.Identifier == "" {
|
||||||
log(" searching for existing events... ")
|
log(" searching for existing events... ")
|
||||||
repo, _, _, _, err := fetchRepositoryAndState(ctx, owner, identifier, nil)
|
repo, _, _, err := fetchRepositoryAndState(ctx, owner, identifier, nil)
|
||||||
if err == nil && repo.Event.ID != nostr.ZeroID {
|
if err == nil && repo.Event.ID != nostr.ZeroID {
|
||||||
fetchedRepo = &repo
|
fetchedRepo = &repo
|
||||||
log("found one from %s.\n", repo.Event.CreatedAt.Time().Format(time.DateOnly))
|
log("found one from %s.\n", repo.Event.CreatedAt.Time().Format(time.DateOnly))
|
||||||
@@ -371,7 +371,7 @@ aside from those, there is also:
|
|||||||
}
|
}
|
||||||
|
|
||||||
// fetch repository metadata and state
|
// fetch repository metadata and state
|
||||||
repo, _, _, state, err := fetchRepositoryAndState(ctx, owner, identifier, relayHints)
|
repo, _, state, err := fetchRepositoryAndState(ctx, owner, identifier, relayHints)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -782,98 +782,6 @@ aside from those, there is also:
|
|||||||
return err
|
return err
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
|
||||||
Name: "status",
|
|
||||||
Usage: "show repository status and synchronization information",
|
|
||||||
Action: func(ctx context.Context, c *cli.Command) error {
|
|
||||||
// read local config
|
|
||||||
localConfig, err := readNip34ConfigFile("")
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to read nip34.json: %w (run 'nak git init' first)", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// parse owner
|
|
||||||
owner, err := parsePubKey(localConfig.Owner)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("invalid owner public key: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
repo := localConfig.ToRepository()
|
|
||||||
stdout("\n" + color.CyanString("metadata:"))
|
|
||||||
stdout(" identifier:", color.CyanString(repo.ID))
|
|
||||||
stdout(" name:", color.CyanString(repo.Name))
|
|
||||||
stdout(" owner:", color.CyanString(nip19.EncodeNpub(repo.Event.PubKey)))
|
|
||||||
stdout(" description:", color.CyanString(repo.Description))
|
|
||||||
stdout(" web urls:")
|
|
||||||
for _, url := range repo.Web {
|
|
||||||
stdout(" ", url)
|
|
||||||
}
|
|
||||||
stdout(" earliest unique commit:", color.CyanString(repo.EarliestUniqueCommitID))
|
|
||||||
|
|
||||||
// fetch repository announcement and state from relays
|
|
||||||
_, _, upToDateRelays, state, err := fetchRepositoryAndState(ctx, owner, localConfig.Identifier, localConfig.GraspServers)
|
|
||||||
if err != nil {
|
|
||||||
// create a local repo object for display purposes
|
|
||||||
log("failed to fetch repository announcement from relays: %s\n", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if state == nil {
|
|
||||||
stdout(color.YellowString("\n repository state not published."))
|
|
||||||
}
|
|
||||||
|
|
||||||
stateHEAD, _ := state.Branches[state.HEAD]
|
|
||||||
|
|
||||||
stdout("\n" + color.CyanString("grasp status:"))
|
|
||||||
rows := make([][3]string, len(localConfig.GraspServers))
|
|
||||||
for s, server := range localConfig.GraspServers {
|
|
||||||
row := [3]string{}
|
|
||||||
|
|
||||||
url := graspServerHost(server)
|
|
||||||
row[0] = url
|
|
||||||
|
|
||||||
upToDate := upToDateRelays != nil && slices.ContainsFunc(upToDateRelays, func(s string) bool { return graspServerHost(s) == url })
|
|
||||||
if upToDate {
|
|
||||||
row[1] = color.GreenString("announcement up-to-date")
|
|
||||||
} else {
|
|
||||||
row[1] = color.YellowString("announcement outdated")
|
|
||||||
}
|
|
||||||
|
|
||||||
if state != nil {
|
|
||||||
remoteName := gitRemoteName(url)
|
|
||||||
refSpec := fmt.Sprintf("refs/remotes/%s/HEAD", remoteName)
|
|
||||||
lsRemoteCmd := exec.Command("git", "rev-parse", "--verify", refSpec)
|
|
||||||
commitOutput, err := lsRemoteCmd.Output()
|
|
||||||
if err != nil {
|
|
||||||
row[2] = color.YellowString("repository not pushed")
|
|
||||||
} else {
|
|
||||||
commit := strings.TrimSpace(string(commitOutput))
|
|
||||||
if commit == stateHEAD {
|
|
||||||
row[2] = color.GreenString("repository synced with state")
|
|
||||||
} else {
|
|
||||||
row[2] = color.YellowString("mismatched HEAD state=%s, pushed=%s", state.HEAD, commit)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
rows[s] = row
|
|
||||||
}
|
|
||||||
|
|
||||||
maxCol := [3]int{}
|
|
||||||
for i := range maxCol {
|
|
||||||
for _, row := range rows {
|
|
||||||
if len(row[i]) > maxCol[i] {
|
|
||||||
maxCol[i] = len(row[i])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for _, row := range rows {
|
|
||||||
line := " " + row[0] + strings.Repeat(" ", maxCol[0]-len(row[0])) + " " + strings.Repeat(" ", maxCol[1]-len(row[1])) + row[1] + " " + strings.Repeat(" ", maxCol[2]-len(row[2])) + row[2]
|
|
||||||
stdout(line)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -961,7 +869,7 @@ func gitSync(ctx context.Context, signer nostr.Keyer) (nip34.Repository, *nip34.
|
|||||||
}
|
}
|
||||||
|
|
||||||
// fetch repository announcement and state from relays
|
// fetch repository announcement and state from relays
|
||||||
repo, upToDateAnnouncementEvent, upToDateRelays, state, err := fetchRepositoryAndState(ctx, owner, localConfig.Identifier, localConfig.GraspServers)
|
repo, upToDateRelays, state, err := fetchRepositoryAndState(ctx, owner, localConfig.Identifier, localConfig.GraspServers)
|
||||||
notUpToDate := func(graspServer string) bool {
|
notUpToDate := func(graspServer string) bool {
|
||||||
return !slices.Contains(upToDateRelays, nostr.NormalizeURL(graspServer))
|
return !slices.Contains(upToDateRelays, nostr.NormalizeURL(graspServer))
|
||||||
}
|
}
|
||||||
@@ -981,40 +889,33 @@ func gitSync(ctx context.Context, signer nostr.Keyer) (nip34.Repository, *nip34.
|
|||||||
}
|
}
|
||||||
log("some grasp servers (%v) are not up-to-date, will publish to them\n", relays)
|
log("some grasp servers (%v) are not up-to-date, will publish to them\n", relays)
|
||||||
}
|
}
|
||||||
var event nostr.Event
|
// create a local repository object from config and publish it
|
||||||
if upToDateAnnouncementEvent != nil {
|
localRepo := localConfig.ToRepository()
|
||||||
// publish the latest event to the other relays
|
|
||||||
event = *upToDateAnnouncementEvent
|
if signer != nil {
|
||||||
repo = nip34.ParseRepository(event)
|
signerPk, err := signer.GetPublicKey(ctx)
|
||||||
} else {
|
if err != nil {
|
||||||
// create a local repository object from config and publish it
|
return repo, nil, fmt.Errorf("failed to get signer pubkey: %w", err)
|
||||||
localRepo := localConfig.ToRepository()
|
}
|
||||||
if signer != nil {
|
if signerPk != owner {
|
||||||
signerPk, err := signer.GetPublicKey(ctx)
|
return repo, nil, fmt.Errorf("provided signer pubkey does not match owner, can't publish repository")
|
||||||
if err != nil {
|
} else {
|
||||||
return repo, nil, fmt.Errorf("failed to get signer pubkey: %w", err)
|
event := localRepo.ToEvent()
|
||||||
|
if err := signer.SignEvent(ctx, &event); err != nil {
|
||||||
|
return repo, state, fmt.Errorf("failed to sign announcement: %w", err)
|
||||||
}
|
}
|
||||||
if signerPk != owner {
|
|
||||||
return repo, nil, fmt.Errorf("provided signer pubkey does not match owner, can't publish repository")
|
for res := range sys.Pool.PublishMany(ctx, relays, event) {
|
||||||
} else {
|
if res.Error != nil {
|
||||||
event = localRepo.ToEvent()
|
log("! error publishing to %s: %v\n", color.YellowString(res.RelayURL), res.Error)
|
||||||
if err := signer.SignEvent(ctx, &event); err != nil {
|
} else {
|
||||||
return repo, state, fmt.Errorf("failed to sign announcement: %w", err)
|
log("> published to %s\n", color.GreenString(res.RelayURL))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
repo = localRepo
|
||||||
return repo, nil, fmt.Errorf("no signer provided to publish repository (run 'nak git sync' with the '--sec' flag)")
|
|
||||||
}
|
|
||||||
|
|
||||||
repo = localRepo
|
|
||||||
}
|
|
||||||
|
|
||||||
for res := range sys.Pool.PublishMany(ctx, relays, *upToDateAnnouncementEvent) {
|
|
||||||
if res.Error != nil {
|
|
||||||
log("! error publishing to %s: %v\n", color.YellowString(res.RelayURL), res.Error)
|
|
||||||
} else {
|
|
||||||
log("> published to %s\n", color.GreenString(res.RelayURL))
|
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
return repo, nil, fmt.Errorf("no signer provided to publish repository (run 'nak git sync' with the '--sec' flag)")
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -1050,7 +951,6 @@ func gitSync(ctx context.Context, signer nostr.Keyer) (nip34.Repository, *nip34.
|
|||||||
} else {
|
} else {
|
||||||
log("local configuration is newer, publishing updated repository announcement...\n")
|
log("local configuration is newer, publishing updated repository announcement...\n")
|
||||||
announcementEvent := localRepo.ToEvent()
|
announcementEvent := localRepo.ToEvent()
|
||||||
announcementEvent.CreatedAt = nostr.Timestamp(configModTime.Unix())
|
|
||||||
if err := signer.SignEvent(ctx, &announcementEvent); err != nil {
|
if err := signer.SignEvent(ctx, &announcementEvent); err != nil {
|
||||||
return repo, state, fmt.Errorf("failed to sign announcement: %w", err)
|
return repo, state, fmt.Errorf("failed to sign announcement: %w", err)
|
||||||
}
|
}
|
||||||
@@ -1255,7 +1155,7 @@ func fetchRepositoryAndState(
|
|||||||
pubkey nostr.PubKey,
|
pubkey nostr.PubKey,
|
||||||
identifier string,
|
identifier string,
|
||||||
relayHints []string,
|
relayHints []string,
|
||||||
) (repo nip34.Repository, upToDateAnnouncementEvent *nostr.Event, upToDateRelays []string, state *nip34.RepositoryState, err error) {
|
) (repo nip34.Repository, upToDateRelays []string, state *nip34.RepositoryState, err error) {
|
||||||
// fetch repository announcement (30617)
|
// fetch repository announcement (30617)
|
||||||
relays := appendUnique(relayHints, sys.FetchOutboxRelays(ctx, pubkey, 3)...)
|
relays := appendUnique(relayHints, sys.FetchOutboxRelays(ctx, pubkey, 3)...)
|
||||||
for ie := range sys.Pool.FetchMany(ctx, relays, nostr.Filter{
|
for ie := range sys.Pool.FetchMany(ctx, relays, nostr.Filter{
|
||||||
@@ -1276,15 +1176,13 @@ func fetchRepositoryAndState(
|
|||||||
|
|
||||||
// reset this list as the previous was for relays with the older version
|
// reset this list as the previous was for relays with the older version
|
||||||
upToDateRelays = []string{ie.Relay.URL}
|
upToDateRelays = []string{ie.Relay.URL}
|
||||||
|
|
||||||
upToDateAnnouncementEvent = &ie.Event
|
|
||||||
} else if ie.Event.CreatedAt == repo.CreatedAt {
|
} else if ie.Event.CreatedAt == repo.CreatedAt {
|
||||||
// we discard this because it's the same, but this relay is up-to-date
|
// we discard this because it's the same, but this relay is up-to-date
|
||||||
upToDateRelays = append(upToDateRelays, ie.Relay.URL)
|
upToDateRelays = append(upToDateRelays, ie.Relay.URL)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if repo.Event.ID == nostr.ZeroID {
|
if repo.Event.ID == nostr.ZeroID {
|
||||||
return repo, nil, upToDateRelays, state, fmt.Errorf("no repository announcement (kind 30617) found for %s", identifier)
|
return repo, upToDateRelays, state, fmt.Errorf("no repository announcement (kind 30617) found for %s", identifier)
|
||||||
}
|
}
|
||||||
|
|
||||||
// fetch repository state (30618)
|
// fetch repository state (30618)
|
||||||
@@ -1314,10 +1212,10 @@ func fetchRepositoryAndState(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if stateErr != nil {
|
if stateErr != nil {
|
||||||
return repo, upToDateAnnouncementEvent, upToDateRelays, state, stateErr
|
return repo, upToDateRelays, state, stateErr
|
||||||
}
|
}
|
||||||
|
|
||||||
return repo, upToDateAnnouncementEvent, upToDateRelays, state, nil
|
return repo, upToDateRelays, state, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type StateErr struct{ string }
|
type StateErr struct{ string }
|
||||||
|
|||||||
8
go.mod
8
go.mod
@@ -3,7 +3,7 @@ module github.com/fiatjaf/nak
|
|||||||
go 1.25
|
go 1.25
|
||||||
|
|
||||||
require (
|
require (
|
||||||
fiatjaf.com/nostr v0.0.0-20260119010708-31af06f4c7c4
|
fiatjaf.com/nostr v0.0.0-20251230181913-e52ffa631bd6
|
||||||
github.com/AlecAivazis/survey/v2 v2.3.7
|
github.com/AlecAivazis/survey/v2 v2.3.7
|
||||||
github.com/bep/debounce v1.2.1
|
github.com/bep/debounce v1.2.1
|
||||||
github.com/btcsuite/btcd/btcec/v2 v2.3.6
|
github.com/btcsuite/btcd/btcec/v2 v2.3.6
|
||||||
@@ -11,6 +11,7 @@ require (
|
|||||||
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e
|
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e
|
||||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0
|
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0
|
||||||
github.com/fatih/color v1.16.0
|
github.com/fatih/color v1.16.0
|
||||||
|
github.com/hanwen/go-fuse/v2 v2.7.2
|
||||||
github.com/json-iterator/go v1.1.12
|
github.com/json-iterator/go v1.1.12
|
||||||
github.com/liamg/magic v0.0.1
|
github.com/liamg/magic v0.0.1
|
||||||
github.com/mailru/easyjson v0.9.1
|
github.com/mailru/easyjson v0.9.1
|
||||||
@@ -28,10 +29,7 @@ require (
|
|||||||
golang.org/x/term v0.32.0
|
golang.org/x/term v0.32.0
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require fiatjaf.com/lib v0.3.2
|
||||||
fiatjaf.com/lib v0.3.2
|
|
||||||
github.com/hanwen/go-fuse/v2 v2.9.0
|
|
||||||
)
|
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/FastFilter/xorfilter v0.2.1 // indirect
|
github.com/FastFilter/xorfilter v0.2.1 // indirect
|
||||||
|
|||||||
16
go.sum
16
go.sum
@@ -1,7 +1,7 @@
|
|||||||
fiatjaf.com/lib v0.3.2 h1:RBS41z70d8Rp8e2nemQsbPY1NLLnEGShiY2c+Bom3+Q=
|
fiatjaf.com/lib v0.3.2 h1:RBS41z70d8Rp8e2nemQsbPY1NLLnEGShiY2c+Bom3+Q=
|
||||||
fiatjaf.com/lib v0.3.2/go.mod h1:UlHaZvPHj25PtKLh9GjZkUHRmQ2xZ8Jkoa4VRaLeeQ8=
|
fiatjaf.com/lib v0.3.2/go.mod h1:UlHaZvPHj25PtKLh9GjZkUHRmQ2xZ8Jkoa4VRaLeeQ8=
|
||||||
fiatjaf.com/nostr v0.0.0-20260119010708-31af06f4c7c4 h1:/6AVjHIbbgyuiilcUuoFPMXGNXqialKGQM7uskF0b/0=
|
fiatjaf.com/nostr v0.0.0-20251230181913-e52ffa631bd6 h1:yH+cU9ZNgUdMCRa5eS3pmqTPP/QdZtSmQAIrN/U5nEc=
|
||||||
fiatjaf.com/nostr v0.0.0-20260119010708-31af06f4c7c4/go.mod h1:ue7yw0zHfZj23Ml2kVSdBx0ENEaZiuvGxs/8VEN93FU=
|
fiatjaf.com/nostr v0.0.0-20251230181913-e52ffa631bd6/go.mod h1:ue7yw0zHfZj23Ml2kVSdBx0ENEaZiuvGxs/8VEN93FU=
|
||||||
github.com/AlecAivazis/survey/v2 v2.3.7 h1:6I/u8FvytdGsgonrYsVn2t8t4QiRnh6QSTqkkhIiSjQ=
|
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/AlecAivazis/survey/v2 v2.3.7/go.mod h1:xUTIdE4KCOIjsBAE1JYsUPoCqYdZ1reCfTwbto0Fduo=
|
||||||
github.com/FastFilter/xorfilter v0.2.1 h1:lbdeLG9BdpquK64ZsleBS8B4xO/QW1IM0gMzF7KaBKc=
|
github.com/FastFilter/xorfilter v0.2.1 h1:lbdeLG9BdpquK64ZsleBS8B4xO/QW1IM0gMzF7KaBKc=
|
||||||
@@ -144,8 +144,8 @@ github.com/hablullah/go-hijri v1.0.2 h1:drT/MZpSZJQXo7jftf5fthArShcaMtsal0Zf/dnm
|
|||||||
github.com/hablullah/go-hijri v1.0.2/go.mod h1:OS5qyYLDjORXzK4O1adFw9Q5WfhOcMdAKglDkcTxgWQ=
|
github.com/hablullah/go-hijri v1.0.2/go.mod h1:OS5qyYLDjORXzK4O1adFw9Q5WfhOcMdAKglDkcTxgWQ=
|
||||||
github.com/hablullah/go-juliandays v1.0.0 h1:A8YM7wIj16SzlKT0SRJc9CD29iiaUzpBLzh5hr0/5p0=
|
github.com/hablullah/go-juliandays v1.0.0 h1:A8YM7wIj16SzlKT0SRJc9CD29iiaUzpBLzh5hr0/5p0=
|
||||||
github.com/hablullah/go-juliandays v1.0.0/go.mod h1:0JOYq4oFOuDja+oospuc61YoX+uNEn7Z6uHYTbBzdGc=
|
github.com/hablullah/go-juliandays v1.0.0/go.mod h1:0JOYq4oFOuDja+oospuc61YoX+uNEn7Z6uHYTbBzdGc=
|
||||||
github.com/hanwen/go-fuse/v2 v2.9.0 h1:0AOGUkHtbOVeyGLr0tXupiid1Vg7QB7M6YUcdmVdC58=
|
github.com/hanwen/go-fuse/v2 v2.7.2 h1:SbJP1sUP+n1UF8NXBA14BuojmTez+mDgOk0bC057HQw=
|
||||||
github.com/hanwen/go-fuse/v2 v2.9.0/go.mod h1:yE6D2PqWwm3CbYRxFXV9xUd8Md5d6NG0WBs5spCswmI=
|
github.com/hanwen/go-fuse/v2 v2.7.2/go.mod h1:ugNaD/iv5JYyS1Rcvi57Wz7/vrLQJo10mmketmoef48=
|
||||||
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
|
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
|
||||||
github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
|
github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
|
||||||
github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec h1:qv2VnGeEQHchGaZ/u7lxST/RaJw+cv273q79D81Xbog=
|
github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec h1:qv2VnGeEQHchGaZ/u7lxST/RaJw+cv273q79D81Xbog=
|
||||||
@@ -169,8 +169,8 @@ github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
|||||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||||
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
|
github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348 h1:MtvEpTB6LX3vkb4ax0b5D2DHbNAUsen0Gx5wZoq3lV4=
|
||||||
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
|
github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348/go.mod h1:B69LEHPfb2qLo0BaaOLcbitczOKLWTsrBG9LczfCD4k=
|
||||||
github.com/liamg/magic v0.0.1 h1:Ru22ElY+sCh6RvRTWjQzKKCxsEco8hE0co8n1qe7TBM=
|
github.com/liamg/magic v0.0.1 h1:Ru22ElY+sCh6RvRTWjQzKKCxsEco8hE0co8n1qe7TBM=
|
||||||
github.com/liamg/magic v0.0.1/go.mod h1:yQkOmZZI52EA+SQ2xyHpVw8fNvTBruF873Y+Vt6S+fk=
|
github.com/liamg/magic v0.0.1/go.mod h1:yQkOmZZI52EA+SQ2xyHpVw8fNvTBruF873Y+Vt6S+fk=
|
||||||
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
|
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
|
||||||
@@ -200,8 +200,8 @@ github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b h1:j7+1HpAFS1zy5+Q4qx1f
|
|||||||
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
|
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
|
||||||
github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk=
|
github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk=
|
||||||
github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA=
|
github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA=
|
||||||
github.com/moby/sys/mountinfo v0.7.2 h1:1shs6aH5s4o5H2zQLn796ADW1wMrIwHsyJ2v9KouLrg=
|
github.com/moby/sys/mountinfo v0.6.2 h1:BzJjoreD5BMFNmD9Rus6gdd1pLuecOFPt8wC+Vygl78=
|
||||||
github.com/moby/sys/mountinfo v0.7.2/go.mod h1:1YOa8w8Ih7uW0wALDUgT1dTTSBrZ+HiBLGws92L2RU4=
|
github.com/moby/sys/mountinfo v0.6.2/go.mod h1:IJb6JQeOklcdMU9F5xQ8ZALD+CUr5VlGpwtX+VE0rpI=
|
||||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||||
|
|||||||
383
group.go
383
group.go
@@ -1,383 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"strings"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"fiatjaf.com/nostr"
|
|
||||||
"fiatjaf.com/nostr/nip29"
|
|
||||||
"github.com/fatih/color"
|
|
||||||
"github.com/urfave/cli/v3"
|
|
||||||
)
|
|
||||||
|
|
||||||
var group = &cli.Command{
|
|
||||||
Name: "group",
|
|
||||||
Aliases: []string{"nip29"},
|
|
||||||
Usage: "group-related operations: info, chat, forum, members, admins, roles",
|
|
||||||
Description: `manage and interact with Nostr communities (NIP-29). Use "nak group <subcommand> <relay>'<identifier>" where host.tld is the relay and identifier is the group identifier.`,
|
|
||||||
DisableSliceFlagSeparator: true,
|
|
||||||
ArgsUsage: "<subcommand> <relay>'<identifier> [flags]",
|
|
||||||
Flags: defaultKeyFlags,
|
|
||||||
Commands: []*cli.Command{
|
|
||||||
{
|
|
||||||
Name: "info",
|
|
||||||
Usage: "show group information",
|
|
||||||
Description: "displays basic group metadata.",
|
|
||||||
ArgsUsage: "<relay>'<identifier>",
|
|
||||||
Action: func(ctx context.Context, c *cli.Command) error {
|
|
||||||
relay, identifier, err := parseGroupIdentifier(c)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
group := nip29.Group{}
|
|
||||||
for ie := range sys.Pool.FetchMany(ctx, []string{relay}, nostr.Filter{
|
|
||||||
Kinds: []nostr.Kind{nostr.KindSimpleGroupMetadata},
|
|
||||||
Tags: nostr.TagMap{"d": []string{identifier}},
|
|
||||||
}, nostr.SubscriptionOptions{Label: "nak-nip29"}) {
|
|
||||||
if err := group.MergeInMetadataEvent(&ie.Event); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
stdout("address:", color.HiBlueString(strings.SplitN(nostr.NormalizeURL(relay), "/", 3)[2]+"'"+identifier))
|
|
||||||
stdout("name:", color.HiBlueString(group.Name))
|
|
||||||
stdout("picture:", color.HiBlueString(group.Picture))
|
|
||||||
stdout("about:", color.HiBlueString(group.About))
|
|
||||||
stdout("restricted:",
|
|
||||||
color.HiBlueString("%s", cond(group.Restricted, "yes", "no"))+
|
|
||||||
", "+
|
|
||||||
cond(group.Restricted, "only explicit members can publish", "non-members can publish (restricted by relay policy)"),
|
|
||||||
)
|
|
||||||
stdout("closed:",
|
|
||||||
color.HiBlueString("%s", cond(group.Closed, "yes", "no"))+
|
|
||||||
", "+
|
|
||||||
cond(group.Closed, "joining requires an invite", "anyone can join (restricted by relay policy)"),
|
|
||||||
)
|
|
||||||
stdout("hidden:",
|
|
||||||
color.HiBlueString("%s", cond(group.Hidden, "yes", "no"))+
|
|
||||||
", "+
|
|
||||||
cond(group.Hidden, "group doesn't show up when listing relay groups", "group is visible to users browsing the relay"),
|
|
||||||
)
|
|
||||||
stdout("private:",
|
|
||||||
color.HiBlueString("%s", cond(group.Private, "yes", "no"))+
|
|
||||||
", "+
|
|
||||||
cond(group.Private, "group content is not accessible to non-members", "group content is public"),
|
|
||||||
)
|
|
||||||
return nil
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "members",
|
|
||||||
Usage: "list and manage group members",
|
|
||||||
Description: "view group membership information.",
|
|
||||||
ArgsUsage: "<relay>'<identifier>",
|
|
||||||
Action: func(ctx context.Context, c *cli.Command) error {
|
|
||||||
relay, identifier, err := parseGroupIdentifier(c)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
group := nip29.Group{
|
|
||||||
Members: make(map[nostr.PubKey][]*nip29.Role),
|
|
||||||
}
|
|
||||||
for ie := range sys.Pool.FetchMany(ctx, []string{relay}, nostr.Filter{
|
|
||||||
Kinds: []nostr.Kind{nostr.KindSimpleGroupMembers},
|
|
||||||
Tags: nostr.TagMap{"d": []string{identifier}},
|
|
||||||
}, nostr.SubscriptionOptions{Label: "nak-nip29"}) {
|
|
||||||
if err := group.MergeInMembersEvent(&ie.Event); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
lines := make(chan string)
|
|
||||||
wg := sync.WaitGroup{}
|
|
||||||
|
|
||||||
for member, roles := range group.Members {
|
|
||||||
wg.Go(func() {
|
|
||||||
line := member.Hex()
|
|
||||||
|
|
||||||
meta := sys.FetchProfileMetadata(ctx, member)
|
|
||||||
line += " (" + color.HiBlueString(meta.ShortName()) + ")"
|
|
||||||
|
|
||||||
for _, role := range roles {
|
|
||||||
line += ", " + role.Name
|
|
||||||
}
|
|
||||||
|
|
||||||
lines <- line
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
wg.Wait()
|
|
||||||
close(lines)
|
|
||||||
}()
|
|
||||||
|
|
||||||
for line := range lines {
|
|
||||||
stdout(line)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "admins",
|
|
||||||
Usage: "manage group administrators",
|
|
||||||
Description: "view and manage group admin permissions.",
|
|
||||||
ArgsUsage: "<relay>'<identifier>",
|
|
||||||
Action: func(ctx context.Context, c *cli.Command) error {
|
|
||||||
relay, identifier, err := parseGroupIdentifier(c)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
group := nip29.Group{
|
|
||||||
Members: make(map[nostr.PubKey][]*nip29.Role),
|
|
||||||
}
|
|
||||||
for ie := range sys.Pool.FetchMany(ctx, []string{relay}, nostr.Filter{
|
|
||||||
Kinds: []nostr.Kind{nostr.KindSimpleGroupAdmins},
|
|
||||||
Tags: nostr.TagMap{"d": []string{identifier}},
|
|
||||||
}, nostr.SubscriptionOptions{Label: "nak-nip29"}) {
|
|
||||||
if err := group.MergeInAdminsEvent(&ie.Event); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
lines := make(chan string)
|
|
||||||
wg := sync.WaitGroup{}
|
|
||||||
|
|
||||||
for member, roles := range group.Members {
|
|
||||||
wg.Go(func() {
|
|
||||||
line := member.Hex()
|
|
||||||
|
|
||||||
meta := sys.FetchProfileMetadata(ctx, member)
|
|
||||||
line += " (" + color.HiBlueString(meta.ShortName()) + ")"
|
|
||||||
|
|
||||||
for _, role := range roles {
|
|
||||||
line += ", " + role.Name
|
|
||||||
}
|
|
||||||
|
|
||||||
lines <- line
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
wg.Wait()
|
|
||||||
close(lines)
|
|
||||||
}()
|
|
||||||
|
|
||||||
for line := range lines {
|
|
||||||
stdout(line)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "roles",
|
|
||||||
Usage: "manage group roles and permissions",
|
|
||||||
Description: "configure custom roles and permissions within the group.",
|
|
||||||
ArgsUsage: "<relay>'<identifier>",
|
|
||||||
Action: func(ctx context.Context, c *cli.Command) error {
|
|
||||||
relay, identifier, err := parseGroupIdentifier(c)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
group := nip29.Group{
|
|
||||||
Roles: make([]*nip29.Role, 0),
|
|
||||||
}
|
|
||||||
for ie := range sys.Pool.FetchMany(ctx, []string{relay}, nostr.Filter{
|
|
||||||
Kinds: []nostr.Kind{nostr.KindSimpleGroupRoles},
|
|
||||||
Tags: nostr.TagMap{"d": []string{identifier}},
|
|
||||||
}, nostr.SubscriptionOptions{Label: "nak-nip29"}) {
|
|
||||||
if err := group.MergeInRolesEvent(&ie.Event); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, role := range group.Roles {
|
|
||||||
stdout(color.HiBlueString(role.Name) + " " + role.Description)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "chat",
|
|
||||||
Usage: "send and read group chat messages",
|
|
||||||
Description: "interact with group chat functionality.",
|
|
||||||
ArgsUsage: "<relay>'<identifier>",
|
|
||||||
Action: func(ctx context.Context, c *cli.Command) error {
|
|
||||||
relay, identifier, err := parseGroupIdentifier(c)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
r, err := sys.Pool.EnsureRelay(relay)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
sub, err := r.Subscribe(ctx, nostr.Filter{
|
|
||||||
Kinds: []nostr.Kind{9},
|
|
||||||
Tags: nostr.TagMap{"h": []string{identifier}},
|
|
||||||
Limit: 200,
|
|
||||||
}, nostr.SubscriptionOptions{Label: "nak-nip29"})
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer sub.Close()
|
|
||||||
|
|
||||||
eosed := false
|
|
||||||
messages := make([]struct {
|
|
||||||
message string
|
|
||||||
rendered bool
|
|
||||||
}, 200)
|
|
||||||
base := len(messages)
|
|
||||||
|
|
||||||
tryRender := func(i int) {
|
|
||||||
// if all messages before these are loaded we can render this,
|
|
||||||
// otherwise we render whatever we can and stop
|
|
||||||
for m, msg := range messages[base:] {
|
|
||||||
if msg.rendered {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if msg.message == "" {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
messages[base+m].rendered = true
|
|
||||||
stdout(msg.message)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case evt := <-sub.Events:
|
|
||||||
var i int
|
|
||||||
if eosed {
|
|
||||||
i = len(messages)
|
|
||||||
messages = append(messages, struct {
|
|
||||||
message string
|
|
||||||
rendered bool
|
|
||||||
}{})
|
|
||||||
} else {
|
|
||||||
base--
|
|
||||||
i = base
|
|
||||||
}
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
meta := sys.FetchProfileMetadata(ctx, evt.PubKey)
|
|
||||||
messages[i].message = color.HiBlueString(meta.ShortName()) + " " + color.HiCyanString(evt.CreatedAt.Time().Format(time.DateTime)) + ": " + evt.Content
|
|
||||||
|
|
||||||
if eosed {
|
|
||||||
tryRender(i)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
case reason := <-sub.ClosedReason:
|
|
||||||
stdout("closed:" + color.YellowString(reason))
|
|
||||||
case <-sub.EndOfStoredEvents:
|
|
||||||
eosed = true
|
|
||||||
tryRender(len(messages) - 1)
|
|
||||||
case <-sub.Context.Done():
|
|
||||||
return fmt.Errorf("subscription ended: %w", context.Cause(sub.Context))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
Commands: []*cli.Command{
|
|
||||||
{
|
|
||||||
Name: "send",
|
|
||||||
Usage: "sends a message to the chat",
|
|
||||||
ArgsUsage: "<message>",
|
|
||||||
Action: func(ctx context.Context, c *cli.Command) error {
|
|
||||||
relay, identifier, err := parseGroupIdentifier(c)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
kr, _, err := gatherKeyerFromArguments(ctx, c)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
msg := nostr.Event{
|
|
||||||
Kind: 9,
|
|
||||||
CreatedAt: nostr.Now(),
|
|
||||||
Content: strings.Join(c.Args().Tail(), " "),
|
|
||||||
Tags: nostr.Tags{
|
|
||||||
{"h", identifier},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
if err := kr.SignEvent(ctx, &msg); err != nil {
|
|
||||||
return fmt.Errorf("failed to sign message: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if r, err := sys.Pool.EnsureRelay(relay); err != nil {
|
|
||||||
return err
|
|
||||||
} else {
|
|
||||||
return r.Publish(ctx, msg)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "forum",
|
|
||||||
Usage: "read group forum posts",
|
|
||||||
Description: "access group forum functionality.",
|
|
||||||
ArgsUsage: "<relay>'<identifier>",
|
|
||||||
Action: func(ctx context.Context, c *cli.Command) error {
|
|
||||||
relay, identifier, err := parseGroupIdentifier(c)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
for evt := range sys.Pool.FetchMany(ctx, []string{relay}, nostr.Filter{
|
|
||||||
Kinds: []nostr.Kind{11},
|
|
||||||
Tags: nostr.TagMap{"#h": []string{identifier}},
|
|
||||||
}, nostr.SubscriptionOptions{Label: "nak-nip29"}) {
|
|
||||||
title := evt.Tags.Find("title")
|
|
||||||
if title != nil {
|
|
||||||
stdout(colors.bold(title[1]))
|
|
||||||
} else {
|
|
||||||
stdout(colors.bold("<untitled>"))
|
|
||||||
}
|
|
||||||
meta := sys.FetchProfileMetadata(ctx, evt.PubKey)
|
|
||||||
stdout("by " + evt.PubKey.Hex() + " (" + color.HiBlueString(meta.ShortName()) + ") at " + evt.CreatedAt.Time().Format(time.DateTime))
|
|
||||||
stdout(evt.Content)
|
|
||||||
}
|
|
||||||
// TODO: see what to do about this
|
|
||||||
|
|
||||||
return nil
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
func cond(b bool, ifYes string, ifNo string) string {
|
|
||||||
if b {
|
|
||||||
return ifYes
|
|
||||||
}
|
|
||||||
return ifNo
|
|
||||||
}
|
|
||||||
|
|
||||||
func parseGroupIdentifier(c *cli.Command) (relay string, identifier string, err error) {
|
|
||||||
groupArg := c.Args().First()
|
|
||||||
if !strings.Contains(groupArg, "'") {
|
|
||||||
return "", "", fmt.Errorf("invalid group identifier format, expected <relay>'<identifier>")
|
|
||||||
}
|
|
||||||
|
|
||||||
parts := strings.SplitN(groupArg, "'", 2)
|
|
||||||
if len(parts) != 2 || parts[0] == "" || parts[1] == "" {
|
|
||||||
return "", "", fmt.Errorf("invalid group identifier format, expected <relay>'<identifier>")
|
|
||||||
}
|
|
||||||
|
|
||||||
return strings.TrimSuffix(parts[0], "/"), parts[1], nil
|
|
||||||
}
|
|
||||||
1
main.go
1
main.go
@@ -50,7 +50,6 @@ var app = &cli.Command{
|
|||||||
fsCmd,
|
fsCmd,
|
||||||
publish,
|
publish,
|
||||||
git,
|
git,
|
||||||
group,
|
|
||||||
nip,
|
nip,
|
||||||
syncCmd,
|
syncCmd,
|
||||||
spell,
|
spell,
|
||||||
|
|||||||
@@ -153,7 +153,7 @@ example:
|
|||||||
relayUrls = nostr.AppendUnique(relayUrls, c.Args().Slice()...)
|
relayUrls = nostr.AppendUnique(relayUrls, c.Args().Slice()...)
|
||||||
relays := connectToAllRelays(ctx, c, relayUrls, nil,
|
relays := connectToAllRelays(ctx, c, relayUrls, nil,
|
||||||
nostr.PoolOptions{
|
nostr.PoolOptions{
|
||||||
AuthRequiredHandler: func(ctx context.Context, authEvent *nostr.Event) error {
|
AuthHandler: func(ctx context.Context, authEvent *nostr.Event) error {
|
||||||
return authSigner(ctx, c, func(s string, args ...any) {}, authEvent)
|
return authSigner(ctx, c, func(s string, args ...any) {}, authEvent)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
2
req.go
2
req.go
@@ -138,7 +138,7 @@ example:
|
|||||||
relayUrls,
|
relayUrls,
|
||||||
forcePreAuthSigner,
|
forcePreAuthSigner,
|
||||||
nostr.PoolOptions{
|
nostr.PoolOptions{
|
||||||
AuthRequiredHandler: func(ctx context.Context, authEvent *nostr.Event) error {
|
AuthHandler: func(ctx context.Context, authEvent *nostr.Event) error {
|
||||||
return authSigner(ctx, c, func(s string, args ...any) {
|
return authSigner(ctx, c, func(s string, args ...any) {
|
||||||
if strings.HasPrefix(s, "authenticating as") {
|
if strings.HasPrefix(s, "authenticating as") {
|
||||||
cleanUrl, _ := strings.CutPrefix(
|
cleanUrl, _ := strings.CutPrefix(
|
||||||
|
|||||||
Reference in New Issue
Block a user