mirror of https://github.com/gogits/gogs.git
Unknwon
7 years ago
7 changed files with 250 additions and 15 deletions
@ -0,0 +1,218 @@
|
||||
// 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) |
||||
} |
Loading…
Reference in new issue