diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..9c86383 --- /dev/null +++ b/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2019 Sebastian Winkler + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/events/event.go b/events/event.go new file mode 100644 index 0000000..d1fc23f --- /dev/null +++ b/events/event.go @@ -0,0 +1,64 @@ +package events + +import ( + "fmt" + "time" + + "github.com/bwmarrin/discordgo" + "github.com/pkg/errors" +) + +// Type defines the type for a Event +type Type string + +// defines various Event Types +const ( + MessageCreateEventType Type = "message_create" + MessageUpdateEventType Type = "message_update" + MessageDeleteEventType Type = "message_delete" +) + +// Event represents an Event +type Event struct { + Type Type + ReceivedAt time.Time + BotUserID string + + // discordgo event data + MessageCreate *discordgo.MessageCreate + MessageUpdate *discordgo.MessageUpdate + MessageDelete *discordgo.MessageDelete +} + +// GenerateRoutingKey generates an Routing Key for AMQP based on a Event Type +func GenerateRoutingKey(eventType Type) string { + return fmt.Sprintf("cacophony.discord.%s", eventType) +} + +// GenerateEventFromDiscordgoEvent generates an Event from a Discordgo Event +func GenerateEventFromDiscordgoEvent(botUserID string, eventItem interface{}) (*Event, error) { + event := &Event{ + ReceivedAt: time.Now(), + BotUserID: botUserID, + } + + switch t := eventItem.(type) { + case *discordgo.MessageCreate: + event.Type = MessageCreateEventType + event.MessageCreate = t + + return event, nil + case *discordgo.MessageUpdate: + event.Type = MessageUpdateEventType + event.MessageUpdate = t + + return event, nil + case *discordgo.MessageDelete: + event.Type = MessageDeleteEventType + event.MessageDelete = t + + return event, nil + } + + return nil, errors.New("event type is not supported") +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..a3729c3 --- /dev/null +++ b/go.mod @@ -0,0 +1,10 @@ +module gitlab.com/Cacophony/go-kit + +require ( + github.com/bwmarrin/discordgo v0.19.0 + github.com/pkg/errors v0.8.1 + github.com/stretchr/testify v1.3.0 // indirect + go.uber.org/atomic v1.3.2 // indirect + go.uber.org/multierr v1.1.0 // indirect + go.uber.org/zap v1.9.1 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..ef43f63 --- /dev/null +++ b/go.sum @@ -0,0 +1,21 @@ +github.com/bwmarrin/discordgo v0.19.0 h1:kMED/DB0NR1QhRcalb85w0Cu3Ep2OrGAqZH1R5awQiY= +github.com/bwmarrin/discordgo v0.19.0/go.mod h1:O9S4p+ofTFwB02em7jkpkV8M3R0/PUVOwN61zSZ0r4Q= +github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/gorilla/websocket v1.4.0 h1:WDFjx/TMzVgy9VdMMQi2K2Emtwi2QcUQsztZ/zLaH/Q= +github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= +github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +go.uber.org/atomic v1.3.2 h1:2Oa65PReHzfn29GpvgsYwloV9AVFHPDk8tYxt2c2tr4= +go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +go.uber.org/multierr v1.1.0 h1:HoEmRHQPVSqub6w2z2d2EOVs2fjyFRGyofhKuyDq0QI= +go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= +go.uber.org/zap v1.9.1 h1:XCJQEf3W6eZaVwhRBof6ImoYGJSITeKWsyeh3HFu/5o= +go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= +golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16 h1:y6ce7gCWtnH+m3dCjzQ1PCuwl28DDIc3VNnvY29DlIA= +golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= diff --git a/logging/discordgo_logger.go b/logging/discordgo_logger.go new file mode 100644 index 0000000..72c4966 --- /dev/null +++ b/logging/discordgo_logger.go @@ -0,0 +1,63 @@ +package logging + +import ( + "fmt" + "runtime" + "strings" + + "github.com/bwmarrin/discordgo" + "go.uber.org/zap" +) + +// DiscordgoLogger returns a logger to use with github.com/bwmarrin/discordgo +func DiscordgoLogger(logger *zap.Logger) func(msgL, caller int, format string, a ...interface{}) { + + return func(msgL, caller int, format string, a ...interface{}) { + pc, file, line, _ := runtime.Caller(caller) + + files := strings.Split(file, "/") + file = files[len(files)-1] + + name := runtime.FuncForPC(pc).Name() + fns := strings.Split(name, ".") + name = fns[len(fns)-1] + + l := logger.With( + zap.String("file", fmt.Sprintf("%s:%d", file, line)), + zap.String("method", name), + ) + + switch msgL { + case discordgo.LogError: + l.Error( + fmt.Sprintf(format, a...), + ) + + return + case discordgo.LogWarning: + l.Warn( + fmt.Sprintf(format, a...), + ) + + return + case discordgo.LogInformational: + l.Info( + fmt.Sprintf(format, a...), + ) + + return + case discordgo.LogDebug: + l.Debug( + fmt.Sprintf(format, a...), + ) + + return + + } + + l.Info( + fmt.Sprintf(format, a...), + zap.Int("log_level", msgL), + ) + } +} diff --git a/logging/environment.go b/logging/environment.go new file mode 100644 index 0000000..38b3d6c --- /dev/null +++ b/logging/environment.go @@ -0,0 +1,10 @@ +package logging + +// Environment represents a environment in which a service is running +type Environment string + +// defines possibly environments +const ( + DevelopmentEnvironment Environment = "development" + ProductionEnvironment Environment = "production" +) diff --git a/logging/logger.go b/logging/logger.go new file mode 100644 index 0000000..146c29c --- /dev/null +++ b/logging/logger.go @@ -0,0 +1,40 @@ +package logging + +import ( + "net/http" + + "go.uber.org/zap" +) + +// NewLogger creates a zap.DiscordgoLogger based on the environment +func NewLogger(env Environment, service, discordWebhookURL string, client *http.Client) (*zap.Logger, error) { + var logger *zap.Logger + var err error + + switch env { + case ProductionEnvironment: + + logger, err = zap.NewProduction() + if err != nil { + return nil, err + } + default: + + logger, err = zap.NewDevelopment() + if err != nil { + return nil, err + } + } + + logger = logger.With(zap.String("service", service)) + + logger = logger.WithOptions(zap.Hooks( + NewZapHookDiscord( + service, discordWebhookURL, client, + ), + )) + + // TODO: add discord hook + + return logger, nil +} diff --git a/logging/zap_hook_discord.go b/logging/zap_hook_discord.go new file mode 100644 index 0000000..b426275 --- /dev/null +++ b/logging/zap_hook_discord.go @@ -0,0 +1,57 @@ +package logging + +import ( + "bytes" + "encoding/json" + "net/http" + "strings" + "time" + + "github.com/bwmarrin/discordgo" + "go.uber.org/zap/zapcore" +) + +// NewZapHookDiscord sends Zap log messages to a Discord Webhook +// TODO: ratelimit +func NewZapHookDiscord(serviceName, webhookURL string, client *http.Client) func(zapcore.Entry) error { + if webhookURL == "" || client == nil { + return nil + } + + return func(entry zapcore.Entry) error { + if entry.Level == zapcore.DebugLevel || + entry.Level == zapcore.InfoLevel { + return nil + } + + body, err := json.Marshal(discordgo.WebhookParams{ + Username: strings.Title(serviceName), + Embeds: []*discordgo.MessageEmbed{ + { + Title: "Logging message: " + strings.ToUpper(entry.Level.String()), + Description: entry.Message, + Timestamp: entry.Time.Format(time.RFC3339), + Color: 0, // TODO: color per log level + Footer: &discordgo.MessageEmbedFooter{ + Text: "powered by Cacophony", + }, + Fields: []*discordgo.MessageEmbedField{ + { + Name: "caller", + Value: entry.Caller.String(), + }, + }, + }, + }, + }) + if err != nil { + return err + } + + _, err = client.Post( + webhookURL, "application/json", bytes.NewBuffer(body), + ) + return err + } + +}