package gozulipbot import ( "encoding/json" "fmt" "io/ioutil" "net/http" "net/url" ) // A Message is all of the necessary metadata to post on Zulip. // It can be either a public message, where Topic is set, or a private message, // where there is at least one element in Emails. // // If the length of Emails is not 0, functions will always assume it is a private message. type Message struct { Stream string Topic string Emails []string Content string } type EventMessage struct { AvatarURL string `json:"avatar_url"` Client string `json:"client"` Content string `json:"content"` ContentType string `json:"content_type"` DisplayRecipient DisplayRecipient `json:"display_recipient"` GravatarHash string `json:"gravatar_hash"` ID int `json:"id"` RecipientID int `json:"recipient_id"` SenderDomain string `json:"sender_domain"` SenderEmail string `json:"sender_email"` SenderFullName string `json:"sender_full_name"` SenderID int `json:"sender_id"` SenderShortName string `json:"sender_short_name"` Subject string `json:"subject"` SubjectLinks []interface{} `json:"subject_links"` StreamID int `json:"stream_id"` Timestamp int `json:"timestamp"` Type string `json:"type"` Queue *Queue `json:"-"` } type DisplayRecipient struct { Users []User `json:"users,omitempty"` Topic string `json:"topic,omitempty"` } type User struct { Domain string `json:"domain"` Email string `json:"email"` FullName string `json:"full_name"` ID int `json:"id"` IsMirrorDummy bool `json:"is_mirror_dummy"` ShortName string `json:"short_name"` } func (d *DisplayRecipient) UnmarshalJSON(b []byte) (err error) { topic, users := "", make([]User, 1) if err = json.Unmarshal(b, &topic); err == nil { d.Topic = topic return } if err = json.Unmarshal(b, &users); err == nil { d.Users = users return } return } // Message posts a message to Zulip. If any emails have been set on the message, // the message will be re-routed to the PrivateMessage function. func (b *Bot) Message(m Message) (*http.Response, error) { if m.Content == "" { return nil, fmt.Errorf("content cannot be empty") } // if any emails are set, this is a private message if len(m.Emails) != 0 { return b.PrivateMessage(m) } // otherwise it's a stream message if m.Stream == "" { return nil, fmt.Errorf("stream cannot be empty") } if m.Topic == "" { return nil, fmt.Errorf("topic cannot be empty") } req, err := b.constructMessageRequest(m) if err != nil { return nil, err } return b.Client.Do(req) } // PrivateMessage sends a message to the users in the message email slice. func (b *Bot) PrivateMessage(m Message) (*http.Response, error) { if len(m.Emails) == 0 { return nil, fmt.Errorf("there must be at least one recipient") } req, err := b.constructMessageRequest(m) if err != nil { return nil, err } return b.Client.Do(req) } // Respond sends a given message as a response to whatever context from which // an EventMessage was received. func (b *Bot) Respond(e EventMessage, response string) (*http.Response, error) { if response == "" { return nil, fmt.Errorf("Message response cannot be blank") } m := Message{ Stream: e.DisplayRecipient.Topic, Topic: e.Subject, Content: response, } if m.Topic != "" { return b.Message(m) } // private message if m.Stream == "" { emails, err := b.privateResponseList(e) if err != nil { return nil, err } m.Emails = emails return b.Message(m) } return nil, fmt.Errorf("EventMessage is not understood: %v\n", e) } // privateResponseList gets the list of other users in a private multiple // message conversation. func (b *Bot) privateResponseList(e EventMessage) ([]string, error) { var out []string for _, u := range e.DisplayRecipient.Users { if u.Email != b.Email { out = append(out, u.Email) } } if len(out) == 0 { return nil, fmt.Errorf("EventMessage had no Users within the DisplayRecipient") } return out, nil } // constructMessageRequest is a helper for simplifying sending a message. func (b *Bot) constructMessageRequest(m Message) (*http.Request, error) { to := m.Stream mtype := "stream" le := len(m.Emails) if le != 0 { mtype = "private" } if le == 1 { to = m.Emails[0] } if le > 1 { to = "" for i, e := range m.Emails { to += e if i != le-1 { to += "," } } } values := url.Values{} values.Set("type", mtype) values.Set("to", to) values.Set("content", m.Content) if mtype == "stream" { values.Set("subject", m.Topic) } return b.constructRequest("POST", "messages", values.Encode()) } func (b *Bot) UpdateMessage(id string, content string) (*http.Response, error) { //mid, _ := strconv.Atoi(id) values := url.Values{} values.Set("content", content) req, err := b.constructRequest("PATCH", "messages/"+id, values.Encode()) if err != nil { return nil, err } return b.Client.Do(req) } // React adds an emoji reaction to an EventMessage. func (b *Bot) React(e EventMessage, emoji string) (*http.Response, error) { url := fmt.Sprintf("messages/%d/emoji_reactions/%s", e.ID, emoji) req, err := b.constructRequest("PUT", url, "") if err != nil { return nil, err } return b.Client.Do(req) } // Unreact removes an emoji reaction from an EventMessage. func (b *Bot) Unreact(e EventMessage, emoji string) (*http.Response, error) { url := fmt.Sprintf("messages/%d/emoji_reactions/%s", e.ID, emoji) req, err := b.constructRequest("DELETE", url, "") if err != nil { return nil, err } return b.Client.Do(req) } type Emoji struct { Author string `json:"author"` DisplayURL string `json:"display_url"` SourceURL string `json:"source_url"` } type EmojiResponse struct { Emoji map[string]*Emoji `json:"emoji"` Msg string `json:"msg"` Result string `json:"result"` } // RealmEmoji gets the custom emoji information for the Zulip instance. func (b *Bot) RealmEmoji() (map[string]*Emoji, error) { req, err := b.constructRequest("GET", "realm/emoji", "") if err != nil { return nil, err } resp, err := b.Client.Do(req) if err != nil { return nil, err } defer resp.Body.Close() body, err := ioutil.ReadAll(resp.Body) if err != nil { return nil, err } var emjResp EmojiResponse err = json.Unmarshal(body, &emjResp) if err != nil { return nil, err } return emjResp.Emoji, nil } // RealmEmojiSet makes a set of the names of the custom emoji in the Zulip instance. func (b *Bot) RealmEmojiSet() (map[string]struct{}, error) { emj, err := b.RealmEmoji() if err != nil { return nil, nil } out := map[string]struct{}{} for k, _ := range emj { out[k] = struct{}{} } return out, nil }