Advertisement
Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- main.go
- ```go
- package main
- import (
- "flag"
- "fmt"
- "os"
- "strings"
- "github.com/DataDog/datadog-go/statsd"
- "github.com/nbd-wtf/go-nostr/nip19"
- log "github.com/sirupsen/logrus"
- )
- const patience = 4
- type Bot struct {
- PublicKey string
- Npub string
- ImpersonationPublicKey string
- ImpersonationNpub string
- SecretKey string
- WaitSeconds int
- Nip05Domain string
- }
- func main() {
- npubFile := flag.String("npub-file", "", "File with line-delimited npubs to impersonate, these will also receive spam")
- relayFile := flag.String("relay-file", "", "File with line-delimited relay URLs")
- logFormat := flag.String("log-format", "json", "Log format ('json' or 'text')")
- logLevel := flag.String("log-level", "info", "Log level ('debug', 'info', 'warn', 'error')")
- nip05Domain := flag.String("nip-05-domain", "", "Domain used for NIP-05 identification (e.g., spamstr.lol)")
- flag.Usage = func() {
- fmt.Fprintf(os.Stderr, "Usage: %s -npub-file <file> -relay-file <file> [options]\n\n", os.Args[0])
- fmt.Fprintf(os.Stderr, "Required:\n")
- fmt.Fprintf(os.Stderr, " -npub-file <file> File with line-delimited npubs to impersonate and spam\n")
- fmt.Fprintf(os.Stderr, " -relay-file <file> File with line-delimited relay URLs\n\n")
- fmt.Fprintf(os.Stderr, "Options:\n")
- fmt.Fprintf(os.Stderr, " -log-format <format> Log format ('json' or 'text') (default: json)\n")
- fmt.Fprintf(os.Stderr, " -log-level <level> Log level ('debug', 'info', 'warn', 'error') (default: info)\n")
- fmt.Fprintf(os.Stderr, " -nip-05-domain <domain> Domain used for NIP-05 identification\n\n")
- fmt.Fprintf(os.Stderr, "Example:\n")
- fmt.Fprintf(os.Stderr, " %s -npub-file ~/.config/nostr-spam-bot-target-npubs.txt -relay-file ~/.config/nostr-spam-bot-relays.txt -log-format json -log-level info\n", os.Args[0])
- }
- flag.Parse()
- switch strings.ToLower(*logLevel) {
- case "debug":
- log.SetLevel(log.DebugLevel)
- case "info":
- log.SetLevel(log.InfoLevel)
- case "warn":
- log.SetLevel(log.WarnLevel)
- case "error":
- log.SetLevel(log.ErrorLevel)
- default:
- log.SetLevel(log.InfoLevel)
- }
- if len(*npubFile) == 0 || len(*relayFile) == 0 {
- flag.Usage()
- os.Exit(1)
- }
- if *logFormat == "json" {
- log.SetFormatter(&log.JSONFormatter{
- TimestampFormat: "2006-01-02 15:04:05",
- })
- } else if *logFormat == "text" {
- log.SetFormatter(&log.TextFormatter{
- TimestampFormat: "2006-01-02 15:04:05",
- FullTimestamp: true,
- })
- } else {
- fmt.Printf("Invalid log format (expected 'json' or 'text'): %s", *logFormat)
- os.Exit(1)
- }
- *npubFile = expandTilde(*npubFile)
- *relayFile = expandTilde(*relayFile)
- targetPubkeys, err := readAndDecodeNpubsFromFile(*npubFile)
- if err != nil {
- log.Fatal(err)
- }
- relays, err := readRelaysFromFile(*relayFile)
- if err != nil {
- log.Fatal(err)
- }
- datadog, err := statsd.New("localhost:8125")
- if err != nil {
- log.Errorf("Error initializing Datadog client: %s", err)
- }
- defer datadog.Close()
- log.Printf("Spamming %d pubkey(s)", len(targetPubkeys))
- // Initialize bots asynchronously
- botChannel := make(chan Bot, len(targetPubkeys))
- for i, pubkey := range targetPubkeys {
- go func(i int, pubkey string) {
- sk, pk := generateKeys()
- npub, _ := nip19.EncodePublicKey(pk)
- impersonationNpub, _ := nip19.EncodePublicKey(pubkey)
- bot := Bot{Npub: npub, PublicKey: pk, SecretKey: sk, ImpersonationNpub: impersonationNpub, ImpersonationPublicKey: pubkey, WaitSeconds: patience * len(targetPubkeys), Nip05Domain: *nip05Domain}
- log.Printf("Generated bot. npub: %s, pubkey: %s", bot.Npub, bot.PublicKey)
- impersonateProfile(bot, relays, datadog)
- botChannel <- bot
- }(i, pubkey)
- }
- // Collect generated bots and run them
- for i := 0; i < len(targetPubkeys); i++ {
- bot := <-botChannel
- go runBot(&bot, targetPubkeys, relays, datadog)
- }
- select {}
- }
- ```
- nostr.go
- ```go
- package main
- import (
- "bytes"
- "context"
- "encoding/json"
- "fmt"
- "net/http"
- "regexp"
- "strings"
- "time"
- "github.com/DataDog/datadog-go/statsd"
- "github.com/nbd-wtf/go-nostr"
- log "github.com/sirupsen/logrus"
- "golang.org/x/exp/rand"
- )
- type ProfileMetadata struct {
- Name string `json:"name"`
- About string `json:"about"`
- Picture string `json:"picture"`
- Nip05 string `json:"nip05,omitempty"`
- }
- func runBot(bot *Bot, targetPubkeys []string, relays []string, datadog *statsd.Client) {
- spamEventChannel := make(chan *nostr.Event, 500)
- targetEventChannel := make(chan *nostr.Event, 500)
- for _, relay := range relays {
- go listenForSpamEvents(bot, relay, spamEventChannel, datadog)
- go listenForTargetEvents(bot, relay, targetPubkeys, targetEventChannel, datadog)
- }
- // This runs forever but sleeps after every published note
- replayNotes(bot, relays, spamEventChannel, targetEventChannel, datadog)
- }
- func listenForSpamEvents(bot *Bot, relay string, spamEventChannel chan<- *nostr.Event, datadog *statsd.Client) {
- botLogger := log.WithFields(log.Fields{
- "bot_npub": bot.Npub,
- "impersonation_npub": bot.ImpersonationNpub,
- "relay": relay,
- })
- tags := []string{
- fmt.Sprintf("relay:%s", relay),
- fmt.Sprintf("bot_npub:%s", bot.Npub),
- }
- botLogger.Debug("Listening for spam notes")
- ctx := context.Background()
- conn, err := nostr.RelayConnect(ctx, relay)
- if err != nil {
- botLogger.Error(err)
- datadog.Incr("nostr_spam_bot.relay.connect.failure", tags, 1)
- return
- }
- datadog.Incr("nostr_spam_bot.relay.connect.success", tags, 1)
- defer conn.Close()
- filters := []nostr.Filter{{
- Kinds: []int{nostr.KindTextNote},
- }}
- sub, err := conn.Subscribe(ctx, filters)
- if err != nil {
- datadog.Incr("nostr_spam_bot.subscription.failure", tags, 1)
- botLogger.Error(err)
- return
- }
- datadog.Incr("nostr_spam_bot.subscription.success", tags, 1)
- for event := range sub.Events {
- select {
- case spamEventChannel <- event:
- default:
- datadog.Incr("nostr_spam_bot.event.skipped", tags, 1)
- }
- }
- }
- func listenForTargetEvents(bot *Bot, relay string, targetPubkeys []string, targetEventChannel chan<- *nostr.Event, datadog *statsd.Client) {
- botLogger := log.WithFields(log.Fields{
- "bot_npub": bot.Npub,
- "impersonation_npub": bot.ImpersonationNpub,
- "relay": relay,
- })
- botLogger.Debug("Listening for target notes")
- ctx := context.Background()
- conn, err := nostr.RelayConnect(ctx, relay)
- if err != nil {
- botLogger.Error(err)
- return
- }
- defer conn.Close()
- filters := []nostr.Filter{{
- Kinds: []int{nostr.KindTextNote},
- Authors: targetPubkeys,
- }}
- sub, err := conn.Subscribe(ctx, filters)
- if err != nil {
- botLogger.Error(err)
- return
- }
- for event := range sub.Events {
- select {
- case targetEventChannel <- event:
- default:
- tags := []string{
- fmt.Sprintf("relay:%s", relay),
- fmt.Sprintf("bot_npub:%s", bot.Npub),
- }
- datadog.Incr("nostr_spam_bot.event.skipped", tags, 1)
- }
- }
- }
- func replayNotes(bot *Bot, relays []string, spamEventChannel <-chan *nostr.Event, targetEventChannel <-chan *nostr.Event, datadog *statsd.Client) {
- for {
- select {
- case spamEvent := <-spamEventChannel:
- select {
- case targetEvent := <-targetEventChannel:
- for _, relay := range relays {
- go func(bot *Bot, relay string, spamEvent *nostr.Event, targetEvent *nostr.Event) {
- botLogger := log.WithFields(log.Fields{
- "bot_npub": bot.Npub,
- "impersonation_npub": bot.ImpersonationNpub,
- "relay": relay,
- })
- newEvent := nostr.Event{
- PubKey: bot.PublicKey,
- CreatedAt: nostr.Timestamp(time.Now().Unix()),
- Kind: nostr.KindTextNote,
- Tags: nostr.Tags{
- nostr.Tag{"e", targetEvent.ID},
- nostr.Tag{"p", targetEvent.PubKey},
- },
- Content: addRandomEmojis(spamEvent.Content),
- }
- newEvent.Sign(bot.SecretKey)
- ctx := context.Background()
- conn, err := nostr.RelayConnect(context.Background(), relay)
- if err != nil {
- log.Error(err)
- return
- }
- defer conn.Close()
- err = conn.Publish(ctx, newEvent)
- if err != nil {
- botLogger.Error(err)
- tags := []string{
- fmt.Sprintf("relay:%s", relay),
- fmt.Sprintf("bot_npub:%s", bot.Npub),
- }
- datadog.Incr("nostr_spam_bot.publish.failure", tags, 1)
- return
- }
- tags := []string{
- fmt.Sprintf("relay:%s", relay),
- fmt.Sprintf("bot_npub:%s", bot.Npub),
- }
- datadog.Incr("nostr_spam_bot.publish.success", tags, 1)
- botLogger.WithFields(log.Fields{
- "target_note": targetEvent,
- "spam_note": newEvent,
- }).Debugf("Spammed note %s", newEvent.ID)
- }(bot, relay, spamEvent, targetEvent)
- }
- }
- }
- log.Debugf("Sleeping for %d seconds", bot.WaitSeconds)
- time.Sleep(time.Duration(bot.WaitSeconds) * time.Second)
- }
- }
- func impersonateProfile(bot Bot, relays []string, datadog *statsd.Client) {
- for _, relay := range relays {
- botLogger := log.WithFields(log.Fields{
- "bot_npub": bot.Npub,
- "impersonation_npub": bot.ImpersonationNpub,
- "relay": relay,
- })
- botLogger.Printf("Connecting to relay")
- conn, err := nostr.RelayConnect(context.Background(), relay)
- if err != nil {
- log.Error(err)
- continue
- }
- defer conn.Close()
- filters := []nostr.Filter{{
- Kinds: []int{nostr.KindProfileMetadata},
- Authors: []string{bot.ImpersonationPublicKey},
- Limit: 20,
- }}
- ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
- defer cancel()
- botLogger.Printf("Subscribing using filters %+v", filters)
- sub, err := conn.Subscribe(ctx, filters)
- if err != nil {
- log.Error(err)
- continue
- }
- for event := range sub.Events {
- if event.Kind != nostr.KindProfileMetadata {
- botLogger.Warnf("Event is not profile metadata despite filter: %+v", event)
- continue
- }
- // Modifying metadata, this removes easy to detect false NIP-05
- botLogger.Debug("Modifying metadata content")
- var profileMetadata ProfileMetadata
- var newContent string
- err := json.Unmarshal([]byte(event.Content), &profileMetadata)
- if err != nil {
- botLogger.Errorf("Failed to unmarshal profile metadata: %v", err)
- newContent = event.Content
- } else {
- profileMetadata.Nip05 = ""
- marshalledProfile, err := json.Marshal(profileMetadata)
- if err != nil {
- botLogger.Errorf("Failed to marshal profile metadata: %v", err)
- newContent = event.Content
- } else {
- newContent = string(marshalledProfile)
- botLogger.Debugf("Successfully altered metadata: %s", newContent)
- }
- }
- botLogger.Debug("Appending NIP-05 identifier")
- if bot.Nip05Domain != "" {
- contentWithNip05, err := setNip05FromMetadata(bot, profileMetadata)
- if err != nil {
- botLogger.Errorf("failed to set NIP-05 identifier: %s", err)
- } else {
- botLogger.Debugf("updated metadata with NIP-05: %s", contentWithNip05)
- newContent = contentWithNip05
- }
- } else {
- botLogger.Info("No NIP-05 domain specified")
- }
- newEvent := nostr.Event{
- PubKey: bot.PublicKey,
- CreatedAt: nostr.Timestamp(time.Now().Unix()),
- Kind: event.Kind,
- Tags: event.Tags,
- Content: newContent,
- }
- err = newEvent.Sign(bot.SecretKey)
- if err != nil {
- botLogger.Error(err)
- continue
- }
- botLogger.Debugf("Publishing metadata event %s", newEvent.ID)
- err = conn.Publish(ctx, newEvent)
- tags := []string{
- fmt.Sprintf("relay:%s", relay),
- fmt.Sprintf("bot_npub:%s", bot.Npub),
- }
- if err != nil {
- botLogger.Error(err)
- datadog.Incr("nostr_spam_bot.publish.failure", tags, 1)
- continue
- }
- datadog.Incr("nostr_spam_bot.publish.success", tags, 1)
- break
- }
- }
- }
- func generateKeys() (string, string) {
- sk := nostr.GeneratePrivateKey()
- pk, _ := nostr.GetPublicKey(sk)
- return sk, pk
- }
- func setNip05FromMetadata(bot Bot, profileMetadata ProfileMetadata) (string, error) {
- // Remove special characters and convert to lowercase
- reg, err := regexp.Compile("[^a-zA-Z0-9]+")
- if err != nil {
- return "", fmt.Errorf("failed to compile regex: %v", err)
- }
- cleanName := reg.ReplaceAllString(profileMetadata.Name, "")
- cleanName = strings.ToLower(cleanName)
- // Add random 3 digit suffix
- suffix := fmt.Sprintf("%03d", rand.Intn(1000))
- modifiedName := cleanName + suffix
- // Prepare the request body
- requestBody := struct {
- Name string `json:"name"`
- Pubkey string `json:"pubkey"`
- }{
- Name: modifiedName,
- Pubkey: bot.PublicKey,
- }
- jsonBody, err := json.Marshal(requestBody)
- if err != nil {
- return "", fmt.Errorf("failed to marshal request body: %v", err)
- }
- // Make the PUT request
- url := fmt.Sprintf("https://%s/.well-known/nostr.json", bot.Nip05Domain)
- req, err := http.NewRequest(http.MethodPut, url, bytes.NewBuffer(jsonBody))
- if err != nil {
- return "", fmt.Errorf("failed to create request: %v", err)
- }
- req.Header.Set("Content-Type", "application/json")
- client := &http.Client{}
- resp, err := client.Do(req)
- if err != nil {
- return "", fmt.Errorf("failed to send request: %v", err)
- }
- defer resp.Body.Close()
- if resp.StatusCode != http.StatusCreated {
- return "", fmt.Errorf("expected 201 CREATED but got: %d", resp.StatusCode)
- }
- // Update the profile metadata with the new NIP-05
- profileMetadata.Nip05 = fmt.Sprintf("%s@%s", modifiedName, bot.Nip05Domain)
- // Marshal the updated profile metadata
- updatedContent, err := json.Marshal(profileMetadata)
- if err != nil {
- return "", fmt.Errorf("failed to marshal updated profile metadata: %v", err)
- }
- return string(updatedContent), nil
- }
- ```
- generic.go
- ```go
- package main
- import (
- "bufio"
- "os"
- "path/filepath"
- "strings"
- "github.com/nbd-wtf/go-nostr/nip19"
- log "github.com/sirupsen/logrus"
- "golang.org/x/exp/rand"
- )
- func expandTilde(path string) string {
- if strings.HasPrefix(path, "~") {
- home, err := os.UserHomeDir()
- if err != nil {
- return path
- }
- return filepath.Join(home, path[1:])
- }
- return path
- }
- func readAndDecodeNpubsFromFile(filename string) ([]string, error) {
- file, err := os.Open(filename)
- if err != nil {
- return nil, err
- }
- defer file.Close()
- var lines []string
- scanner := bufio.NewScanner(file)
- for scanner.Scan() {
- lines = append(lines, scanner.Text())
- }
- if err := scanner.Err(); err != nil {
- return nil, err
- }
- var npubs []string
- for _, npub := range lines {
- _, pubkey, err := nip19.Decode(npub)
- if err != nil {
- log.Error(err)
- continue
- }
- npubs = append(npubs, pubkey.(string))
- }
- return npubs, nil
- }
- func readRelaysFromFile(filename string) ([]string, error) {
- file, err := os.Open(filename)
- if err != nil {
- return nil, err
- }
- defer file.Close()
- var relays []string
- scanner := bufio.NewScanner(file)
- for scanner.Scan() {
- relay := strings.TrimSpace(scanner.Text())
- if relay != "" {
- relays = append(relays, relay)
- }
- }
- if err := scanner.Err(); err != nil {
- return nil, err
- }
- return relays, nil
- }
- func addRandomEmojis(content string) string {
- emojis := []string{"😀", "😂", "🤔", "👍", "🎉", "🌈", "🔥", "💯"}
- words := strings.Fields(content)
- for i := range words {
- if rand.Float32() < 0.2 {
- words[i] += " " + emojis[rand.Intn(len(emojis))]
- }
- }
- return strings.Join(words, " ")
- }
- ```
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement