1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
package main

import (
	"fmt"
	"io"
	"rss_parrot/dal"
	"rss_parrot/logic"
	"rss_parrot/shared"
	"strings"
	"time"
)

const usage = `usage: rss-kakapo
       rss-kakapo block add|remove URL...
       rss-kakapo block list
       rss-kakapo allow add|remove MONIKER...
       rss-kakapo allow list
`

// listCmd is one of the subcommands that edit a list in the database.
type listCmd struct {
	list      dal.List
	name      string // as in the messages, "blocklist"
	normalise func(string) string
	validate  func(string) error
}

var listCmds = map[string]listCmd{
	"block": {dal.BlockedFeeds, "blocklist", logic.NormaliseFeedUrl, func(e string) error {
		if e == "" || strings.ContainsAny(e, " \t") {
			return fmt.Errorf("not a feed URL: %q", e)
		}
		return nil
	}},
	"allow": {dal.AllowedRequesters, "allowlist", logic.NormaliseMoniker, func(e string) error {
		user, host, ok := strings.Cut(e, "@")
		if !ok || user == "" || host == "" || strings.ContainsAny(user, ":/ \t") || strings.ContainsAny(host, "@/ \t") {
			return fmt.Errorf("not a @user@host: %q", e)
		}
		return nil
	}},
}

// runCli runs the subcommand in args (os.Args without the program name) and
// returns the exit status: 0, 1 if it failed, 2 if it was used wrongly.
func runCli(args []string, stdout, stderr io.Writer) int {
	if args[0] == "help" || args[0] == "-h" || args[0] == "--help" {
		fmt.Fprint(stdout, usage)
		return 0
	}
	cmd, ok := listCmds[args[0]]
	if !ok || len(args) < 2 {
		fmt.Fprint(stderr, usage)
		return 2
	}
	verb, operands := args[1], args[2:]
	if (verb == "list") != (len(operands) == 0) {
		fmt.Fprint(stderr, usage)
		return 2
	}

	var entries []string
	for _, op := range operands {
		e := cmd.normalise(op)
		if err := cmd.validate(e); err != nil {
			fmt.Fprintf(stderr, "rss-kakapo: %v\n", err)
			return 2
		}
		entries = append(entries, e)
	}

	// The database the service uses, migrated if need be. The subcommands
	// work while the service runs: SQLite serialises the writes, and the
	// service looks the lists up on every request.
	cfg := shared.LoadConfig()
	repo := dal.NewRepo(cfg, shared.NewLogger(io.Discard, "Error"))
	repo.InitUpdateDb()

	switch verb {
	case "list":
		list, err := repo.GetList(cmd.list)
		if err != nil {
			fmt.Fprintf(stderr, "rss-kakapo: %v\n", err)
			return 1
		}
		for _, e := range list {
			fmt.Fprintln(stdout, e)
		}
	case "add":
		if err := repo.AddToList(cmd.list, entries, time.Now().UTC()); err != nil {
			fmt.Fprintf(stderr, "rss-kakapo: %v\n", err)
			return 1
		}
	case "remove":
		notFound, err := repo.RemoveFromList(cmd.list, entries)
		if err != nil {
			fmt.Fprintf(stderr, "rss-kakapo: %v\n", err)
			return 1
		}
		for _, e := range notFound {
			fmt.Fprintf(stderr, "rss-kakapo: not in the %s: %s\n", cmd.name, e)
		}
		if len(notFound) != 0 {
			return 1
		}
	default:
		fmt.Fprint(stderr, usage)
		return 2
	}
	return 0
}