internal/control/react.go
144 lines · 4380 bytes
6 symbols in this file
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}