// Copyright 2018 Unknwon // // Licensed under the Apache License, Version 2.0 (the "License"): you may // not use this file except in compliance with the License. You may obtain // a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the // License for the specific language governing permissions and limitations // under the License. package clog import ( "bytes" "encoding/json" "errors" "fmt" "io" "io/ioutil" "net/http" "time" ) type ( discordEmbed struct { Title string `json:"title"` Description string `json:"description"` Timestamp string `json:"timestamp"` Color int `json:"color"` } discordPayload struct { Username string `json:"username,omitempty"` Embeds []*discordEmbed `json:"embeds"` } ) var ( discordTitles = []string{ "Tracing", "Information", "Warning", "Error", "Fatal", } discordColors = []int{ 0, // Trace 3843043, // Info 16761600, // Warn 13041721, // Error 9440319, // Fatal } ) type DiscordConfig struct { // Minimum level of messages to be processed. Level LEVEL // Buffer size defines how many messages can be queued before hangs. BufferSize int64 // Discord webhook URL. URL string // Username to be shown for the message. // Leave empty to use default as set in the Discord. Username string } type discord struct { Adapter url string username string } func newDiscord() Logger { return &discord{ Adapter: Adapter{ quitChan: make(chan struct{}), }, } } func (d *discord) Level() LEVEL { return d.level } func (d *discord) Init(v interface{}) error { cfg, ok := v.(DiscordConfig) if !ok { return ErrConfigObject{"DiscordConfig", v} } if !isValidLevel(cfg.Level) { return ErrInvalidLevel{} } d.level = cfg.Level if len(cfg.URL) == 0 { return errors.New("URL cannot be empty") } d.url = cfg.URL d.username = cfg.Username d.msgChan = make(chan *Message, cfg.BufferSize) return nil } func (d *discord) ExchangeChans(errorChan chan<- error) chan *Message { d.errorChan = errorChan return d.msgChan } func buildDiscordPayload(username string, msg *Message) (string, error) { payload := discordPayload{ Username: username, Embeds: []*discordEmbed{ { Title: discordTitles[msg.Level], Description: msg.Body[8:], Timestamp: time.Now().Format(time.RFC3339), Color: discordColors[msg.Level], }, }, } p, err := json.Marshal(&payload) if err != nil { return "", err } return string(p), nil } type rateLimitMsg struct { RetryAfter int64 `json:"retry_after"` } func (d *discord) postMessage(r io.Reader) (int64, error) { resp, err := http.Post(d.url, "application/json", r) if err != nil { return -1, fmt.Errorf("HTTP Post: %v", err) } defer resp.Body.Close() if resp.StatusCode == 429 { rlMsg := &rateLimitMsg{} if err = json.NewDecoder(resp.Body).Decode(&rlMsg); err != nil { return -1, fmt.Errorf("decode rate limit message: %v", err) } return rlMsg.RetryAfter, nil } else if resp.StatusCode/100 != 2 { data, _ := ioutil.ReadAll(resp.Body) return -1, fmt.Errorf("%s", data) } return -1, nil } func (d *discord) write(msg *Message) { payload, err := buildDiscordPayload(d.username, msg) if err != nil { d.errorChan <- fmt.Errorf("discord: builddiscordPayload: %v", err) return } const RETRY_TIMES = 3 // Due to discord limit, try at most x times with respect to "retry_after" parameter. for i := 1; i <= 3; i++ { retryAfter, err := d.postMessage(bytes.NewReader([]byte(payload))) if err != nil { d.errorChan <- fmt.Errorf("discord: postMessage: %v", err) return } if retryAfter > 0 { time.Sleep(time.Duration(retryAfter) * time.Millisecond) continue } return } d.errorChan <- fmt.Errorf("discord: failed to send message after %d retries", RETRY_TIMES) } func (d *discord) Start() { LOOP: for { select { case msg := <-d.msgChan: d.write(msg) case <-d.quitChan: break LOOP } } for { if len(d.msgChan) == 0 { break } d.write(<-d.msgChan) } d.quitChan <- struct{}{} // Notify the cleanup is done. } func (d *discord) Destroy() { d.quitChan <- struct{}{} <-d.quitChan close(d.msgChan) close(d.quitChan) } func init() { Register(DISCORD, newDiscord) }