mirror of https://github.com/gogits/gogs.git
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
219 lines
4.4 KiB
219 lines
4.4 KiB
7 years ago
|
// 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)
|
||
|
}
|