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