internal/control/react.go

144 lines · 4380 bytes

  1package control
  2
  3import (
  4	"fmt"
  5	"io"
  6	"strconv"
  7
  8	"gitbay.org/gitbay/internal/policy"
  9	"gitbay.org/gitbay/internal/protocol"
 10	"gitbay.org/gitbay/internal/store"
 11)
 12
 13// Reactions on issues, merge requests and their comments (#291). A
 14// reaction is not an event: it files no notification, no activity row
 15// and no webhook.
 16
 17// ReactionOut is one reaction on one item as show emits it.
 18type ReactionOut struct {
 19	Reaction string `json:"reaction"`
 20	Count    int    `json:"count"`
 21	Me       bool   `json:"me"`
 22}
 23
 24// Reacted is what issue react and mr react emit.
 25type Reacted struct {
 26	Reaction string `json:"reaction"`
 27	Comment  int64  `json:"comment,omitempty"`
 28	Removed  bool   `json:"removed,omitempty"`
 29}
 30
 31func reactionsOut(cs []store.ReactionCount) []ReactionOut {
 32	var out []ReactionOut
 33	for _, rc := range cs {
 34		out = append(out, ReactionOut{rc.Reaction, rc.Count, rc.Me})
 35	}
 36	return out
 37}
 38
 39func init() {
 40	register(Command{Path: []string{"issue", "react"},
 41		Summary: "react to an issue or one of its comments",
 42		Usage:   "issue react <owner/name> <n> [--comment <id>] <reaction> [--remove]",
 43		Flags: []Flag{
 44			{"--comment", "<id>", "react to this comment instead of the issue", ""},
 45			{"--remove", "", "take the reaction back", ""},
 46		},
 47		Examples: []string{
 48			"issue react krz/gitbay 42 +1",
 49			"issue react krz/gitbay 42 --comment 7 hooray",
 50			"issue react krz/gitbay 42 --remove +1",
 51		},
 52		Run: func(c *Ctx, args []string) int { return runReact(c, args, "issue") }})
 53	register(Command{Path: []string{"mr", "react"},
 54		Summary: "react to a merge request or one of its comments",
 55		Usage:   "mr react <owner/name> <n> [--comment <id>] <reaction> [--remove]",
 56		Flags: []Flag{
 57			{"--comment", "<id>", "react to this comment instead of the merge request", ""},
 58			{"--remove", "", "take the reaction back", ""},
 59		},
 60		Examples: []string{
 61			"mr react krz/gitbay 431 rocket",
 62			"mr react krz/gitbay 431 --comment 7 eyes",
 63		},
 64		Run: func(c *Ctx, args []string) int { return runReact(c, args, "mr") }})
 65}
 66
 67// runReact is issue react and mr react. Whoever may comment may react,
 68// so it asks what comment asks: read access, and a repository that is
 69// not archived.
 70func runReact(c *Ctx, args []string, noun string) int {
 71	f, err := c.parseArgs(args, flagSpec{Values: []string{"--comment"}, Bools: []string{"--remove"}, MaxPos: 3,
 72		Usage: noun + " react <owner/name> <n> [--comment <id>] <reaction> [--remove]"})
 73	if err != nil {
 74		return c.fail(protocol.ExitUsage, "%v", err)
 75	}
 76	if len(f.Pos) != 3 {
 77		return c.usage()
 78	}
 79	var (
 80		repo     store.Repo
 81		threadID int64
 82		number   int64
 83		code     int
 84		sym      string
 85	)
 86	if noun == "issue" {
 87		var iss store.Issue
 88		repo, iss, code = issueRef(c, f.Pos[:2], policy.CanRead)
 89		threadID, number, sym = iss.ID, iss.Number, "#"
 90	} else {
 91		var mr store.MR
 92		repo, mr, code = mrRef(c, f.Pos[:2], policy.CanRead)
 93		threadID, number, sym = mr.ID, mr.Number, "!"
 94	}
 95	if code >= 0 {
 96		return code
 97	}
 98	reaction, ok := store.ParseReaction(f.Pos[2])
 99	if !ok {
100		return c.fail(protocol.ExitUsage, "unknown reaction %q; one of %s", f.Pos[2], reactionNames())
101	}
102	var commentID int64
103	if f.Has("--comment") {
104		commentID, err = strconv.ParseInt(f.Value("--comment"), 10, 64)
105		if err != nil || commentID <= 0 {
106			return c.fail(protocol.ExitUsage, "bad comment id %q", f.Value("--comment"))
107		}
108	}
109	if code := refuseArchived(c, repo); code >= 0 {
110		return code
111	}
112	if err := c.Store.ReactionTarget(noun, threadID, commentID); err != nil {
113		if err == store.ErrNotFound {
114			return c.fail(protocol.ExitNotFound, "comment %d not found on %s%d", commentID, sym, number)
115		}
116		return c.fail(protocol.ExitFailure, "%v", err)
117	}
118	remove := f.Has("--remove")
119	if err := c.Store.SetReaction(noun, threadID, commentID, c.User.ID, reaction, !remove); err != nil {
120		return c.fail(protocol.ExitFailure, "%v", err)
121	}
122	return c.emit(Reacted{Reaction: reaction, Comment: commentID, Removed: remove}, func(w io.Writer) {
123		what := fmt.Sprintf("%s%s%d", repo.Path(), sym, number)
124		if commentID != 0 {
125			what += fmt.Sprintf(" comment %d", commentID)
126		}
127		verb := "reacted"
128		if remove {
129			verb = "removed reaction"
130		}
131		fmt.Fprintf(w, "%s %s on %s\n", verb, store.ReactionEmoji(reaction), what)
132	})
133}
134
135func reactionNames() string {
136	s := ""
137	for i, r := range store.Reactions {
138		if i > 0 {
139			s += " "
140		}
141		s += r.Name
142	}
143	return s
144}