Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 884f1d7b66 | |||
| 9bec5e0164 |
+1
-1
@@ -5,7 +5,7 @@ var DefaultTimeFormat = "2006-01-02 15:04:05"
|
||||
var Default = &Log{
|
||||
component: "ALL",
|
||||
fmtTime: DefaultTimeFormat,
|
||||
level: LogAll,
|
||||
level: LogNormal,
|
||||
printer: ColorPrinterClassic,
|
||||
}
|
||||
|
||||
|
||||
+3
-2
@@ -24,7 +24,8 @@ const (
|
||||
LogPrint
|
||||
LogDebug
|
||||
|
||||
LogAll = LogError | LogWarn | LogEvent | LogInfo | LogHighlight | LogPrint | LogDebug
|
||||
LogNormal = LogError | LogWarn | LogEvent | LogInfo | LogHighlight | LogPrint
|
||||
LogAll = LogError | LogWarn | LogEvent | LogInfo | LogHighlight | LogPrint | LogDebug
|
||||
)
|
||||
|
||||
type Log struct {
|
||||
@@ -110,7 +111,7 @@ func (l *Log) WithName(name string) *Log {
|
||||
func New(name string) *Log {
|
||||
return &Log{
|
||||
component: name,
|
||||
level: LogAll,
|
||||
level: LogNormal,
|
||||
fmtTime: Default.fmtTime,
|
||||
printer: Default.printer,
|
||||
}
|
||||
|
||||
+22
-14
@@ -9,8 +9,11 @@ import (
|
||||
"github.com/gotify/go-api-client/v2/client/message"
|
||||
"github.com/gotify/go-api-client/v2/gotify"
|
||||
"github.com/gotify/go-api-client/v2/models"
|
||||
)
|
||||
|
||||
"git.twelvetwelve.org/library/core/notifiers"
|
||||
const (
|
||||
EventMetadata = "notification::event::metadata"
|
||||
EventType = "notification::event::type"
|
||||
)
|
||||
|
||||
type Notifier struct {
|
||||
@@ -18,6 +21,12 @@ type Notifier struct {
|
||||
token string
|
||||
}
|
||||
|
||||
type NotificationExtras struct {
|
||||
Click struct {
|
||||
URL string `json:"url"`
|
||||
} `json:"click"`
|
||||
}
|
||||
|
||||
func (n *Notifier) Notify(event string, msg string, kvs map[string]string) error {
|
||||
params := message.NewCreateMessageParams()
|
||||
params.Body = &models.MessageExternal{
|
||||
@@ -27,22 +36,17 @@ func (n *Notifier) Notify(event string, msg string, kvs map[string]string) error
|
||||
Extras: make(map[string]interface{}),
|
||||
}
|
||||
|
||||
params.Body.Extras[EventType] = event
|
||||
params.Body.Extras[EventMetadata] = kvs
|
||||
|
||||
for k, v := range kvs {
|
||||
switch k {
|
||||
case notifiers.MetadataURL:
|
||||
params.Body.Extras["client::notification"] = struct {
|
||||
Click struct {
|
||||
URL string `json:"url"`
|
||||
}
|
||||
}{
|
||||
Click: struct {
|
||||
URL string `json:"url"`
|
||||
}{
|
||||
URL: v,
|
||||
},
|
||||
}
|
||||
case "url", "metadata.url":
|
||||
extras := NotificationExtras{}
|
||||
extras.Click.URL = v
|
||||
params.Body.Extras["client::notification"] = &extras
|
||||
default:
|
||||
params.Body.Extras[k] = v
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,6 +54,10 @@ func (n *Notifier) Notify(event string, msg string, kvs map[string]string) error
|
||||
return err
|
||||
}
|
||||
|
||||
func (n *Notifier) Client() *client.GotifyREST {
|
||||
return n.client
|
||||
}
|
||||
|
||||
func NewNotifier(serverUrl, token string) *Notifier {
|
||||
uri, _ := url.Parse(serverUrl)
|
||||
client := gotify.NewClient(uri, &http.Client{})
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
package notifiers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"git.twelvetwelve.org/library/core/notifiers/colornotifier"
|
||||
"git.twelvetwelve.org/library/core/notifiers/gotify"
|
||||
)
|
||||
|
||||
const (
|
||||
MetadataURL = "metadata.url"
|
||||
)
|
||||
@@ -11,5 +19,25 @@ type Notification struct {
|
||||
}
|
||||
|
||||
type Notifier interface {
|
||||
// Notify sends a notificatioin event containg a message and the set of kv pairs
|
||||
// Event must contain a usable event id string we recommend using https://en.wikipedia.org/wiki/Uniform_Type_Identifier
|
||||
Notify(event string, message string, kvs map[string]string) error
|
||||
}
|
||||
|
||||
func NewNotifier(kind string, options map[string]string) (Notifier, error) {
|
||||
switch strings.ToLower(kind) {
|
||||
case "slack":
|
||||
return NewSlackNotifier(options["Hook"]).WithIcon(options["Icon"]).WithUsername(options["User"]).WithChannel(options["Channel"]), nil
|
||||
|
||||
case "speaker":
|
||||
//sc.notifier = speaker.NewSpeakerNotifier(sc.Options["Token"], sc.Options["Host"])
|
||||
//nm.notifiers[sc.ID] = sc
|
||||
case "gotify":
|
||||
return gotify.NewNotifier(options["Host"], options["Token"]), nil
|
||||
|
||||
case "log":
|
||||
return colornotifier.New(), nil
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("unknown notifier %s", kind)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
package notifiers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
"time"
|
||||
)
|
||||
|
||||
type LogNotifier struct{}
|
||||
|
||||
func (s *LogNotifier) Notify(event string, message string, kvs map[string]string) error {
|
||||
fmt.Printf("%s:%s: %s\n", time.Now().Format(time.Stamp), event, message)
|
||||
|
||||
keys := make([]string, 0, len(kvs))
|
||||
for k := range kvs {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
for _, k := range keys {
|
||||
v, _ := kvs[k]
|
||||
if v == "" {
|
||||
continue
|
||||
}
|
||||
fmt.Printf(" %s: %s\n", k, v)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func NewLogNotifier() *LogNotifier {
|
||||
return &LogNotifier{}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
package notifiers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
type NotifierState struct {
|
||||
Muter
|
||||
notifier Notifier
|
||||
}
|
||||
|
||||
type Multiplexer struct {
|
||||
notifiers map[string]*NotifierState
|
||||
}
|
||||
|
||||
func (m *Multiplexer) Notify(event string, message string, kvs map[string]string) error {
|
||||
var lastErr error
|
||||
|
||||
for _, node := range m.notifiers {
|
||||
if node.IsMuted() {
|
||||
continue
|
||||
}
|
||||
|
||||
err := node.notifier.Notify(event, message, kvs)
|
||||
if err != nil {
|
||||
lastErr = err
|
||||
}
|
||||
}
|
||||
return lastErr
|
||||
}
|
||||
|
||||
func (m *Multiplexer) WithNotifier(n Notifier) *Multiplexer {
|
||||
m.AddNotifier(fmt.Sprintf("%v", n), n)
|
||||
return m
|
||||
}
|
||||
|
||||
func (m *Multiplexer) AddNotifier(id string, n Notifier) {
|
||||
m.notifiers[id] = &NotifierState{
|
||||
Muter: Muter{
|
||||
Muted: false,
|
||||
},
|
||||
notifier: n,
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Multiplexer) Mute(id string, duration time.Duration) {
|
||||
if node, ok := m.notifiers[id]; ok {
|
||||
node.Mute(duration)
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Multiplexer) UnMute(id string) {
|
||||
if node, ok := m.notifiers[id]; ok {
|
||||
node.Unmute()
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
func (m *Multiplexer) List() []NotificationConfig {
|
||||
list := make([]NotificationConfig, 0, len(m.notifiers))
|
||||
for k := range m.notifiers {
|
||||
nc := m.notifiers[k]
|
||||
list = append(list, *nc)
|
||||
}
|
||||
|
||||
sort.Slice(list, func(i, j int) bool {
|
||||
return list[i].ID < list[j].ID
|
||||
})
|
||||
|
||||
return list
|
||||
}*/
|
||||
|
||||
func NewMultiplexer() *Multiplexer {
|
||||
m := &Multiplexer{
|
||||
notifiers: make(map[string]*NotifierState),
|
||||
}
|
||||
return m
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package notifiers
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
type Muter struct {
|
||||
Muted bool
|
||||
MutedUntil time.Time
|
||||
}
|
||||
|
||||
func (m *Muter) Mute(duration time.Duration) {
|
||||
m.Muted = true
|
||||
if duration != 0 {
|
||||
m.MutedUntil = time.Now().Add(duration)
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Muter) Unmute() {
|
||||
m.Muted = false
|
||||
m.MutedUntil = time.Time{}
|
||||
}
|
||||
|
||||
func (m *Muter) IsMuted() bool {
|
||||
if m.MutedUntil.IsZero() {
|
||||
return m.Muted
|
||||
}
|
||||
|
||||
if time.Now().After(m.MutedUntil) {
|
||||
m.Muted = false
|
||||
m.MutedUntil = time.Time{}
|
||||
}
|
||||
|
||||
return m.Muted
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
package notifiers
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"git.twelvetwelve.org/library/core/log"
|
||||
)
|
||||
|
||||
type NotificationRecord struct {
|
||||
Timestamp time.Time
|
||||
Event string
|
||||
Message string
|
||||
Link string
|
||||
KVs map[string]string
|
||||
}
|
||||
|
||||
type Recorder struct {
|
||||
max int
|
||||
notifications []NotificationRecord
|
||||
lock sync.Mutex
|
||||
}
|
||||
|
||||
func (s *Recorder) List() []NotificationRecord {
|
||||
s.lock.Lock()
|
||||
defer s.lock.Unlock()
|
||||
|
||||
log.Debugf("Recording list: %d\n", len(s.notifications))
|
||||
|
||||
response := make([]NotificationRecord, len(s.notifications))
|
||||
copy(response, s.notifications)
|
||||
return response
|
||||
}
|
||||
|
||||
func (s *Recorder) Notify(event string, message string, kvs map[string]string) error {
|
||||
s.lock.Lock()
|
||||
defer s.lock.Unlock()
|
||||
log.Debugf("Recording notification: %s\n", message)
|
||||
|
||||
if kvs == nil {
|
||||
kvs = make(map[string]string)
|
||||
}
|
||||
|
||||
link, _ := kvs[MetadataURL]
|
||||
|
||||
s.notifications = append(s.notifications, NotificationRecord{
|
||||
Timestamp: time.Now(),
|
||||
Event: event,
|
||||
Message: message,
|
||||
Link: link,
|
||||
KVs: kvs,
|
||||
})
|
||||
|
||||
if len(s.notifications) > s.max {
|
||||
s.notifications = s.notifications[1:]
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func NewRecorder(max int) *Recorder {
|
||||
return &Recorder{
|
||||
notifications: make([]NotificationRecord, 0),
|
||||
max: max,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
package notifiers
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"sort"
|
||||
)
|
||||
|
||||
var (
|
||||
FormatPlainText = "plain_text"
|
||||
FormatMarkdown = "mrkdwn"
|
||||
)
|
||||
|
||||
type Field struct {
|
||||
Title string `json:"title"`
|
||||
Value string `json:"value"`
|
||||
Short bool `json:"short"`
|
||||
}
|
||||
|
||||
type Attachment struct {
|
||||
Fallback string `json:"fallback"`
|
||||
Color string `json:"color"`
|
||||
PreText string `json:"pretext"`
|
||||
AuthorName string `json:"author_name"`
|
||||
AuthorLink string `json:"author_link"`
|
||||
AuthorIcon string `json:"author_icon"`
|
||||
Title string `json:"title"`
|
||||
TitleLink string `json:"title_link"`
|
||||
Text string `json:"text"`
|
||||
ImageUrl string `json:"image_url"`
|
||||
Fields []Field `json:"fields"`
|
||||
Footer string `json:"footer"`
|
||||
FooterIcon string `json:"footer_icon"`
|
||||
}
|
||||
|
||||
func (attachment *Attachment) AddField(field Field) {
|
||||
attachment.Fields = append(attachment.Fields, field)
|
||||
}
|
||||
|
||||
type Payload struct {
|
||||
Channel string `json:"channel,omitempty"`
|
||||
Username string `json:"username,omitempty"`
|
||||
Text string `json:"text,omitempty"`
|
||||
IconEmoji string `json:"icon_emoji,omitempty"`
|
||||
IconURL string `json:"icon_url,omitempty"`
|
||||
Attachments []Attachment `json:"attachments,omitempty"`
|
||||
}
|
||||
|
||||
type SlackNotifier struct {
|
||||
webhook string
|
||||
icon string
|
||||
user string
|
||||
channel string
|
||||
}
|
||||
|
||||
func (payload *Payload) Attach(attachment Attachment) {
|
||||
payload.Attachments = append(payload.Attachments, attachment)
|
||||
}
|
||||
|
||||
func PostWebhook(url string, p *Payload) error {
|
||||
|
||||
json, err := json.Marshal(p)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
resp, err := http.Post(url, "application/json", bytes.NewReader(json))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
body, _ := ioutil.ReadAll(resp.Body)
|
||||
defer resp.Body.Close()
|
||||
|
||||
return fmt.Errorf("status code: %d, response body: %s", resp.StatusCode, body)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *SlackNotifier) Notify(event string, message string, kvs map[string]string) error {
|
||||
p := Payload{
|
||||
IconEmoji: s.icon,
|
||||
Username: s.user,
|
||||
Channel: s.channel,
|
||||
}
|
||||
|
||||
a := Attachment{
|
||||
Fallback: message,
|
||||
Title: message,
|
||||
}
|
||||
|
||||
keys := make([]string, 0, len(kvs))
|
||||
for k := range kvs {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
for _, k := range keys {
|
||||
v, _ := kvs[k]
|
||||
if v == "" {
|
||||
continue
|
||||
}
|
||||
a.AddField(Field{
|
||||
Title: k,
|
||||
Value: v,
|
||||
Short: true,
|
||||
})
|
||||
}
|
||||
p.Attach(a)
|
||||
return PostWebhook(s.webhook, &p)
|
||||
}
|
||||
|
||||
func (s *SlackNotifier) WithIcon(icon string) *SlackNotifier {
|
||||
s.icon = icon
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *SlackNotifier) WithUsername(user string) *SlackNotifier {
|
||||
s.user = user
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *SlackNotifier) WithChannel(channel string) *SlackNotifier {
|
||||
s.channel = channel
|
||||
return s
|
||||
}
|
||||
|
||||
func NewSlackNotifier(uri string) *SlackNotifier {
|
||||
return &SlackNotifier{
|
||||
webhook: uri,
|
||||
icon: ":large_yellow_circle:",
|
||||
user: "Border Patrol",
|
||||
channel: "#alerts",
|
||||
}
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
package user
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path"
|
||||
|
||||
"github.com/mitchellh/go-homedir"
|
||||
)
|
||||
|
||||
func ReadUserConfig(appName, configName string, config interface{}) error {
|
||||
dir, err := homedir.Dir()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
filePath := path.Join(dir, ".config", appName, configName)
|
||||
|
||||
data, err := os.ReadFile(filePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return json.Unmarshal(data, config)
|
||||
}
|
||||
|
||||
func WriteUserConfig(appName, configName string, config interface{}) error {
|
||||
dir, err := homedir.Dir()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
data, err := json.Marshal(config)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
filePath := path.Join(dir, ".config", appName)
|
||||
|
||||
err = os.MkdirAll(filePath, 0700)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
filePath = path.Join(filePath, configName)
|
||||
|
||||
return os.WriteFile(filePath, data, 0700)
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
package userconfig
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path"
|
||||
)
|
||||
|
||||
var defaultSearchBase = []string{".ssh"}
|
||||
|
||||
// UserConfig represents configuration settings for managing user-specific configuration files.
|
||||
type UserConfig struct {
|
||||
homeDir string
|
||||
appName string
|
||||
perm os.FileMode
|
||||
searchBase []string
|
||||
}
|
||||
|
||||
// makePath constructs a file path by combining the home directory, search path, application name (if set), and config filename.
|
||||
func (c *UserConfig) makePath(search, config string) string {
|
||||
if c.appName != "" {
|
||||
return path.Join(c.homeDir, search, c.appName, config)
|
||||
}
|
||||
|
||||
return path.Join(c.homeDir, search, config)
|
||||
}
|
||||
|
||||
// WithAppName sets the application name for the configuration paths and returns the updated UserConfig.
|
||||
func (c *UserConfig) WithAppName(appName string) *UserConfig {
|
||||
c.appName = appName
|
||||
return c
|
||||
}
|
||||
|
||||
// WithSearchPaths sets the search paths for configuration files and returns the updated UserConfig.
|
||||
func (c *UserConfig) WithSearchPaths(base []string) *UserConfig {
|
||||
c.searchBase = base
|
||||
return c
|
||||
}
|
||||
|
||||
// WithPermission sets the file permission mode for created configuration files and returns the updated UserConfig.
|
||||
func (c *UserConfig) WithPermission(perm os.FileMode) *UserConfig {
|
||||
c.perm = perm
|
||||
return c
|
||||
}
|
||||
|
||||
// Read searches for a configuration file by name in specified search paths and unmarshals its content into the provided object.
|
||||
func (c *UserConfig) Read(configName string, config interface{}) error {
|
||||
if c.homeDir == "" {
|
||||
return os.ErrNotExist
|
||||
}
|
||||
|
||||
for _, search := range c.searchBase {
|
||||
filePath := c.makePath(search, configName)
|
||||
if _, err := os.Stat(filePath); err == nil {
|
||||
data, err := os.ReadFile(filePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return json.Unmarshal(data, config)
|
||||
}
|
||||
}
|
||||
return os.ErrNotExist
|
||||
}
|
||||
|
||||
// Write saves the provided configuration object to a file with the given name in the first available search path.
|
||||
func (c *UserConfig) Write(configName string, config interface{}) error {
|
||||
if c.homeDir == "" {
|
||||
return os.ErrNotExist
|
||||
}
|
||||
|
||||
if len(c.searchBase) == 0 {
|
||||
return os.ErrNotExist
|
||||
}
|
||||
|
||||
search := c.searchBase[0]
|
||||
filePath := c.makePath(search, configName)
|
||||
|
||||
data, err := json.Marshal(config)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return os.WriteFile(filePath, data, c.perm)
|
||||
}
|
||||
|
||||
// New initializes and returns a new instance of UserConfig with default settings including home directory and permissions.
|
||||
func New() *UserConfig {
|
||||
dir, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return &UserConfig{}
|
||||
}
|
||||
|
||||
return &UserConfig{
|
||||
homeDir: dir,
|
||||
perm: 0600,
|
||||
searchBase: defaultSearchBase,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user