mirror of
https://github.com/fiatjaf/nak.git
synced 2024-10-30 00:59:07 -04:00
support running nak with multiple lines of stdin sequentially.
This commit is contained in:
parent
31b42c3499
commit
78932833df
191
event.go
191
event.go
|
@ -90,115 +90,118 @@ example:
|
||||||
},
|
},
|
||||||
ArgsUsage: "[relay...]",
|
ArgsUsage: "[relay...]",
|
||||||
Action: func(c *cli.Context) error {
|
Action: func(c *cli.Context) error {
|
||||||
evt := nostr.Event{
|
for stdinEvent := range getStdinLinesOrBlank() {
|
||||||
Tags: make(nostr.Tags, 0, 3),
|
evt := nostr.Event{
|
||||||
}
|
Tags: make(nostr.Tags, 0, 3),
|
||||||
|
|
||||||
mustRehashAndResign := false
|
|
||||||
|
|
||||||
if stdinEvent := getStdin(); stdinEvent != "" {
|
|
||||||
if err := json.Unmarshal([]byte(stdinEvent), &evt); err != nil {
|
|
||||||
return fmt.Errorf("invalid event received from stdin: %w", err)
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if kind := c.Int("kind"); kind != 0 {
|
mustRehashAndResign := false
|
||||||
evt.Kind = kind
|
if stdinEvent != "" {
|
||||||
mustRehashAndResign = true
|
if err := json.Unmarshal([]byte(stdinEvent), &evt); err != nil {
|
||||||
} else if evt.Kind == 0 {
|
lineProcessingError(c, "invalid event received from stdin: %s", err)
|
||||||
evt.Kind = 1
|
continue
|
||||||
mustRehashAndResign = true
|
|
||||||
}
|
|
||||||
|
|
||||||
if content := c.String("content"); content != "" {
|
|
||||||
evt.Content = content
|
|
||||||
mustRehashAndResign = true
|
|
||||||
} else if evt.Content == "" && evt.Kind == 1 {
|
|
||||||
evt.Content = "hello from the nostr army knife"
|
|
||||||
mustRehashAndResign = true
|
|
||||||
}
|
|
||||||
|
|
||||||
tags := make(nostr.Tags, 0, 5)
|
|
||||||
for _, tagFlag := range c.StringSlice("tag") {
|
|
||||||
// tags are in the format key=value
|
|
||||||
spl := strings.Split(tagFlag, "=")
|
|
||||||
if len(spl) == 2 && len(spl[0]) > 0 {
|
|
||||||
tag := nostr.Tag{spl[0]}
|
|
||||||
// tags may also contain extra elements separated with a ";"
|
|
||||||
spl2 := strings.Split(spl[1], ";")
|
|
||||||
tag = append(tag, spl2...)
|
|
||||||
// ~
|
|
||||||
tags = append(tags, tag)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for _, etag := range c.StringSlice("e") {
|
|
||||||
tags = append(tags, []string{"e", etag})
|
|
||||||
mustRehashAndResign = true
|
|
||||||
}
|
|
||||||
for _, ptag := range c.StringSlice("p") {
|
|
||||||
tags = append(tags, []string{"p", ptag})
|
|
||||||
mustRehashAndResign = true
|
|
||||||
}
|
|
||||||
if len(tags) > 0 {
|
|
||||||
for _, tag := range tags {
|
|
||||||
evt.Tags = append(evt.Tags, tag)
|
|
||||||
}
|
|
||||||
mustRehashAndResign = true
|
|
||||||
}
|
|
||||||
|
|
||||||
if createdAt := c.String("created-at"); createdAt != "" {
|
|
||||||
ts := time.Now()
|
|
||||||
if createdAt != "now" {
|
|
||||||
if v, err := strconv.ParseInt(createdAt, 10, 64); err != nil {
|
|
||||||
return fmt.Errorf("failed to parse timestamp '%s': %w", createdAt, err)
|
|
||||||
} else {
|
|
||||||
ts = time.Unix(v, 0)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
evt.CreatedAt = nostr.Timestamp(ts.Unix())
|
|
||||||
mustRehashAndResign = true
|
|
||||||
} else if evt.CreatedAt == 0 {
|
|
||||||
evt.CreatedAt = nostr.Now()
|
|
||||||
mustRehashAndResign = true
|
|
||||||
}
|
|
||||||
|
|
||||||
if evt.Sig == "" || mustRehashAndResign {
|
if kind := c.Int("kind"); kind != 0 {
|
||||||
if err := evt.Sign(c.String("sec")); err != nil {
|
evt.Kind = kind
|
||||||
return fmt.Errorf("error signing with provided key: %w", err)
|
mustRehashAndResign = true
|
||||||
|
} else if evt.Kind == 0 {
|
||||||
|
evt.Kind = 1
|
||||||
|
mustRehashAndResign = true
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
relays := c.Args().Slice()
|
if content := c.String("content"); content != "" {
|
||||||
if len(relays) > 0 {
|
evt.Content = content
|
||||||
fmt.Println(evt.String())
|
mustRehashAndResign = true
|
||||||
for _, url := range relays {
|
} else if evt.Content == "" && evt.Kind == 1 {
|
||||||
fmt.Fprintf(os.Stderr, "publishing to %s... ", url)
|
evt.Content = "hello from the nostr army knife"
|
||||||
if relay, err := nostr.RelayConnect(c.Context, url); err != nil {
|
mustRehashAndResign = true
|
||||||
fmt.Fprintf(os.Stderr, "failed to connect: %s\n", err)
|
}
|
||||||
} else {
|
|
||||||
ctx, cancel := context.WithTimeout(c.Context, 10*time.Second)
|
tags := make(nostr.Tags, 0, 5)
|
||||||
defer cancel()
|
for _, tagFlag := range c.StringSlice("tag") {
|
||||||
if status, err := relay.Publish(ctx, evt); err != nil {
|
// tags are in the format key=value
|
||||||
fmt.Fprintf(os.Stderr, "failed: %s\n", err)
|
spl := strings.Split(tagFlag, "=")
|
||||||
|
if len(spl) == 2 && len(spl[0]) > 0 {
|
||||||
|
tag := nostr.Tag{spl[0]}
|
||||||
|
// tags may also contain extra elements separated with a ";"
|
||||||
|
spl2 := strings.Split(spl[1], ";")
|
||||||
|
tag = append(tag, spl2...)
|
||||||
|
// ~
|
||||||
|
tags = append(tags, tag)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, etag := range c.StringSlice("e") {
|
||||||
|
tags = append(tags, []string{"e", etag})
|
||||||
|
mustRehashAndResign = true
|
||||||
|
}
|
||||||
|
for _, ptag := range c.StringSlice("p") {
|
||||||
|
tags = append(tags, []string{"p", ptag})
|
||||||
|
mustRehashAndResign = true
|
||||||
|
}
|
||||||
|
if len(tags) > 0 {
|
||||||
|
for _, tag := range tags {
|
||||||
|
evt.Tags = append(evt.Tags, tag)
|
||||||
|
}
|
||||||
|
mustRehashAndResign = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if createdAt := c.String("created-at"); createdAt != "" {
|
||||||
|
ts := time.Now()
|
||||||
|
if createdAt != "now" {
|
||||||
|
if v, err := strconv.ParseInt(createdAt, 10, 64); err != nil {
|
||||||
|
return fmt.Errorf("failed to parse timestamp '%s': %w", createdAt, err)
|
||||||
} else {
|
} else {
|
||||||
fmt.Fprintf(os.Stderr, "%s.\n", status)
|
ts = time.Unix(v, 0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
evt.CreatedAt = nostr.Timestamp(ts.Unix())
|
||||||
|
mustRehashAndResign = true
|
||||||
|
} else if evt.CreatedAt == 0 {
|
||||||
|
evt.CreatedAt = nostr.Now()
|
||||||
|
mustRehashAndResign = true
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
var result string
|
if evt.Sig == "" || mustRehashAndResign {
|
||||||
if c.Bool("envelope") {
|
if err := evt.Sign(c.String("sec")); err != nil {
|
||||||
j, _ := json.Marshal([]any{"EVENT", evt})
|
return fmt.Errorf("error signing with provided key: %w", err)
|
||||||
result = string(j)
|
}
|
||||||
} else if c.Bool("nson") {
|
}
|
||||||
result, _ = nson.Marshal(&evt)
|
|
||||||
|
relays := c.Args().Slice()
|
||||||
|
if len(relays) > 0 {
|
||||||
|
fmt.Println(evt.String())
|
||||||
|
for _, url := range relays {
|
||||||
|
fmt.Fprintf(os.Stderr, "publishing to %s... ", url)
|
||||||
|
if relay, err := nostr.RelayConnect(c.Context, url); err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "failed to connect: %s\n", err)
|
||||||
|
} else {
|
||||||
|
ctx, cancel := context.WithTimeout(c.Context, 10*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
if status, err := relay.Publish(ctx, evt); err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "failed: %s\n", err)
|
||||||
|
} else {
|
||||||
|
fmt.Fprintf(os.Stderr, "%s.\n", status)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
j, _ := easyjson.Marshal(&evt)
|
var result string
|
||||||
result = string(j)
|
if c.Bool("envelope") {
|
||||||
|
j, _ := json.Marshal([]any{"EVENT", evt})
|
||||||
|
result = string(j)
|
||||||
|
} else if c.Bool("nson") {
|
||||||
|
result, _ = nson.Marshal(&evt)
|
||||||
|
} else {
|
||||||
|
j, _ := easyjson.Marshal(&evt)
|
||||||
|
result = string(j)
|
||||||
|
}
|
||||||
|
fmt.Println(result)
|
||||||
}
|
}
|
||||||
fmt.Println(result)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
exitIfLineProcessingError(c)
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
52
helpers.go
52
helpers.go
|
@ -1,7 +1,9 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bufio"
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
@ -11,7 +13,36 @@ import (
|
||||||
"github.com/urfave/cli/v2"
|
"github.com/urfave/cli/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
func getStdin() string {
|
const (
|
||||||
|
LINE_PROCESSING_ERROR = iota
|
||||||
|
)
|
||||||
|
|
||||||
|
func getStdinLinesOrBlank() chan string {
|
||||||
|
ch := make(chan string)
|
||||||
|
go func() {
|
||||||
|
r := bufio.NewReader(os.Stdin)
|
||||||
|
if _, err := r.Peek(1); err != nil {
|
||||||
|
ch <- ""
|
||||||
|
close(ch)
|
||||||
|
} else {
|
||||||
|
scanner := bufio.NewScanner(r)
|
||||||
|
for scanner.Scan() {
|
||||||
|
ch <- scanner.Text()
|
||||||
|
}
|
||||||
|
close(ch)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
return ch
|
||||||
|
}
|
||||||
|
|
||||||
|
func getStdinOrFirstArgument(c *cli.Context) string {
|
||||||
|
// try the first argument
|
||||||
|
target := c.Args().First()
|
||||||
|
if target != "" {
|
||||||
|
return target
|
||||||
|
}
|
||||||
|
|
||||||
|
// try the stdin
|
||||||
stat, _ := os.Stdin.Stat()
|
stat, _ := os.Stdin.Stat()
|
||||||
if (stat.Mode() & os.ModeCharDevice) == 0 {
|
if (stat.Mode() & os.ModeCharDevice) == 0 {
|
||||||
read := bytes.NewBuffer(make([]byte, 0, 1000))
|
read := bytes.NewBuffer(make([]byte, 0, 1000))
|
||||||
|
@ -23,14 +54,6 @@ func getStdin() string {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
func getStdinOrFirstArgument(c *cli.Context) string {
|
|
||||||
target := c.Args().First()
|
|
||||||
if target != "" {
|
|
||||||
return target
|
|
||||||
}
|
|
||||||
return getStdin()
|
|
||||||
}
|
|
||||||
|
|
||||||
func validateRelayURLs(wsurls []string) error {
|
func validateRelayURLs(wsurls []string) error {
|
||||||
for _, wsurl := range wsurls {
|
for _, wsurl := range wsurls {
|
||||||
u, err := url.Parse(wsurl)
|
u, err := url.Parse(wsurl)
|
||||||
|
@ -49,3 +72,14 @@ func validateRelayURLs(wsurls []string) error {
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func lineProcessingError(c *cli.Context, msg string, args ...any) {
|
||||||
|
c.Context = context.WithValue(c.Context, LINE_PROCESSING_ERROR, true)
|
||||||
|
fmt.Fprintf(os.Stderr, msg+"\n", args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func exitIfLineProcessingError(c *cli.Context) {
|
||||||
|
if val := c.Context.Value(LINE_PROCESSING_ERROR); val != nil && val.(bool) {
|
||||||
|
os.Exit(123)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
154
req.go
154
req.go
|
@ -95,87 +95,91 @@ example:
|
||||||
},
|
},
|
||||||
ArgsUsage: "[relay...]",
|
ArgsUsage: "[relay...]",
|
||||||
Action: func(c *cli.Context) error {
|
Action: func(c *cli.Context) error {
|
||||||
filter := nostr.Filter{}
|
for stdinFilter := range getStdinLinesOrBlank() {
|
||||||
if stdinFilter := getStdin(); stdinFilter != "" {
|
filter := nostr.Filter{}
|
||||||
if err := json.Unmarshal([]byte(stdinFilter), &filter); err != nil {
|
if stdinFilter != "" {
|
||||||
return fmt.Errorf("invalid filter received from stdin: %w", err)
|
if err := json.Unmarshal([]byte(stdinFilter), &filter); err != nil {
|
||||||
|
lineProcessingError(c, "invalid filter received from stdin: %s", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if authors := c.StringSlice("author"); len(authors) > 0 {
|
if authors := c.StringSlice("author"); len(authors) > 0 {
|
||||||
filter.Authors = append(filter.Authors, authors...)
|
filter.Authors = append(filter.Authors, authors...)
|
||||||
}
|
}
|
||||||
if ids := c.StringSlice("id"); len(ids) > 0 {
|
if ids := c.StringSlice("id"); len(ids) > 0 {
|
||||||
filter.IDs = append(filter.IDs, ids...)
|
filter.IDs = append(filter.IDs, ids...)
|
||||||
}
|
}
|
||||||
if kinds := c.IntSlice("kind"); len(kinds) > 0 {
|
if kinds := c.IntSlice("kind"); len(kinds) > 0 {
|
||||||
filter.Kinds = append(filter.Kinds, kinds...)
|
filter.Kinds = append(filter.Kinds, kinds...)
|
||||||
}
|
}
|
||||||
if search := c.String("search"); search != "" {
|
if search := c.String("search"); search != "" {
|
||||||
filter.Search = search
|
filter.Search = search
|
||||||
}
|
}
|
||||||
tags := make([][]string, 0, 5)
|
tags := make([][]string, 0, 5)
|
||||||
for _, tagFlag := range c.StringSlice("tag") {
|
for _, tagFlag := range c.StringSlice("tag") {
|
||||||
spl := strings.Split(tagFlag, "=")
|
spl := strings.Split(tagFlag, "=")
|
||||||
if len(spl) == 2 && len(spl[0]) == 1 {
|
if len(spl) == 2 && len(spl[0]) == 1 {
|
||||||
tags = append(tags, spl)
|
tags = append(tags, spl)
|
||||||
|
} else {
|
||||||
|
return fmt.Errorf("invalid --tag '%s'", tagFlag)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, etag := range c.StringSlice("e") {
|
||||||
|
tags = append(tags, []string{"e", etag})
|
||||||
|
}
|
||||||
|
for _, ptag := range c.StringSlice("p") {
|
||||||
|
tags = append(tags, []string{"p", ptag})
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(tags) > 0 && filter.Tags == nil {
|
||||||
|
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 since := c.Int("since"); since != 0 {
|
||||||
|
ts := nostr.Timestamp(since)
|
||||||
|
filter.Since = &ts
|
||||||
|
}
|
||||||
|
if until := c.Int("until"); until != 0 {
|
||||||
|
ts := nostr.Timestamp(until)
|
||||||
|
filter.Until = &ts
|
||||||
|
}
|
||||||
|
if limit := c.Int("limit"); limit != 0 {
|
||||||
|
filter.Limit = limit
|
||||||
|
}
|
||||||
|
|
||||||
|
relays := c.Args().Slice()
|
||||||
|
if len(relays) > 0 {
|
||||||
|
pool := nostr.NewSimplePool(c.Context)
|
||||||
|
fn := pool.SubManyEose
|
||||||
|
if c.Bool("stream") {
|
||||||
|
fn = pool.SubMany
|
||||||
|
}
|
||||||
|
for ie := range fn(c.Context, relays, nostr.Filters{filter}) {
|
||||||
|
fmt.Println(ie.Event)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
return fmt.Errorf("invalid --tag '%s'", tagFlag)
|
// no relays given, will just print the filter
|
||||||
|
var result string
|
||||||
|
if c.Bool("bare") {
|
||||||
|
result = filter.String()
|
||||||
|
} else {
|
||||||
|
j, _ := json.Marshal([]any{"REQ", "nak", filter})
|
||||||
|
result = string(j)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println(result)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for _, etag := range c.StringSlice("e") {
|
|
||||||
tags = append(tags, []string{"e", etag})
|
|
||||||
}
|
|
||||||
for _, ptag := range c.StringSlice("p") {
|
|
||||||
tags = append(tags, []string{"p", ptag})
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(tags) > 0 && filter.Tags == nil {
|
|
||||||
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 since := c.Int("since"); since != 0 {
|
|
||||||
ts := nostr.Timestamp(since)
|
|
||||||
filter.Since = &ts
|
|
||||||
}
|
|
||||||
if until := c.Int("until"); until != 0 {
|
|
||||||
ts := nostr.Timestamp(until)
|
|
||||||
filter.Until = &ts
|
|
||||||
}
|
|
||||||
if limit := c.Int("limit"); limit != 0 {
|
|
||||||
filter.Limit = limit
|
|
||||||
}
|
|
||||||
|
|
||||||
relays := c.Args().Slice()
|
|
||||||
if len(relays) > 0 {
|
|
||||||
pool := nostr.NewSimplePool(c.Context)
|
|
||||||
fn := pool.SubManyEose
|
|
||||||
if c.Bool("stream") {
|
|
||||||
fn = pool.SubMany
|
|
||||||
}
|
|
||||||
for ie := range fn(c.Context, relays, nostr.Filters{filter}) {
|
|
||||||
fmt.Println(ie.Event)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// no relays given, will just print the filter
|
|
||||||
var result string
|
|
||||||
if c.Bool("bare") {
|
|
||||||
result = filter.String()
|
|
||||||
} else {
|
|
||||||
j, _ := json.Marshal([]any{"REQ", "nak", filter})
|
|
||||||
result = string(j)
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Println(result)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
exitIfLineProcessingError(c)
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
31
verify.go
31
verify.go
|
@ -2,7 +2,6 @@ package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"github.com/nbd-wtf/go-nostr"
|
"github.com/nbd-wtf/go-nostr"
|
||||||
"github.com/urfave/cli/v2"
|
"github.com/urfave/cli/v2"
|
||||||
|
@ -17,21 +16,27 @@ var verify = &cli.Command{
|
||||||
it outputs nothing if the verification is successful.
|
it outputs nothing if the verification is successful.
|
||||||
`,
|
`,
|
||||||
Action: func(c *cli.Context) error {
|
Action: func(c *cli.Context) error {
|
||||||
evt := nostr.Event{}
|
for stdinEvent := range getStdinLinesOrBlank() {
|
||||||
if stdinEvent := getStdin(); stdinEvent != "" {
|
evt := nostr.Event{}
|
||||||
if err := json.Unmarshal([]byte(stdinEvent), &evt); err != nil {
|
if stdinEvent != "" {
|
||||||
return fmt.Errorf("invalid JSON: %w", err)
|
if err := json.Unmarshal([]byte(stdinEvent), &evt); err != nil {
|
||||||
|
lineProcessingError(c, "invalid event: %s", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if evt.GetID() != evt.ID {
|
||||||
|
lineProcessingError(c, "invalid .id, expected %s, got %s", evt.GetID(), evt.ID)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if ok, err := evt.CheckSignature(); !ok {
|
||||||
|
lineProcessingError(c, "invalid signature: %s", err)
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if evt.GetID() != evt.ID {
|
exitIfLineProcessingError(c)
|
||||||
return fmt.Errorf("invalid .id, expected %s, got %s", evt.GetID(), evt.ID)
|
|
||||||
}
|
|
||||||
|
|
||||||
if ok, err := evt.CheckSignature(); !ok {
|
|
||||||
return fmt.Errorf("invalid signature: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue
Block a user