diff --git a/pkg/config/config.go b/pkg/config/config.go index 738fc7c..5590afd 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -50,6 +50,8 @@ type Config struct { VIP VIP MessageFilters []*MessageFilter + + Logging Logging } type TurnConfig struct { @@ -99,6 +101,14 @@ type WebhookURL struct { URL string } +// Logging configs to monitor channels or usernames. +type Logging struct { + Enabled bool + Directory string + Channels []string + Usernames []string +} + // Current loaded configuration. var Current = DefaultConfig() @@ -175,6 +185,11 @@ func DefaultConfig() Config { ChatServerResponse: "Watch your language.", }, }, + Logging: Logging{ + Directory: "./logs", + Channels: []string{"lobby", "offtopic"}, + Usernames: []string{}, + }, } c.JWT.Strict = true return c diff --git a/pkg/handlers.go b/pkg/handlers.go index e4bf0d1..29757f0 100644 --- a/pkg/handlers.go +++ b/pkg/handlers.go @@ -220,12 +220,26 @@ func (s *Server) OnMessage(sub *Subscriber, msg messages.Message) { return } + // Log this conversation? + if IsLoggingUsername(sub) { + // The sender of this message is being logged. + LogMessage(sub, rcpt.Username, sub.Username, msg) + } else if IsLoggingUsername(rcpt) { + // The recipient of this message is being logged. + LogMessage(rcpt, sub.Username, sub.Username, msg) + } + if err := s.SendTo(msg.Channel, message); err != nil { sub.ChatServer("Your message could not be delivered: %s", err) } return } + // Are we logging this public channel? + if IsLoggingChannel(msg.Channel) { + LogChannel(s, msg.Channel, sub.Username, msg) + } + // Broadcast a chat message to the room. s.Broadcast(message) } diff --git a/pkg/logging.go b/pkg/logging.go new file mode 100644 index 0000000..fbd1252 --- /dev/null +++ b/pkg/logging.go @@ -0,0 +1,144 @@ +package barertc + +import ( + "fmt" + "io" + "os" + "strings" + "time" + + "git.kirsle.net/apps/barertc/pkg/config" + "git.kirsle.net/apps/barertc/pkg/log" + "git.kirsle.net/apps/barertc/pkg/messages" +) + +// IsLoggingUsername checks whether the app is currently configured to log a user's DMs. +func IsLoggingUsername(sub *Subscriber) bool { + if !config.Current.Logging.Enabled { + return false + } + + // Has a cached setting and writer. + if sub.log { + return true + } + + // Check the server config. + for _, username := range config.Current.Logging.Usernames { + if username == sub.Username { + sub.log = true + } + } + + return sub.log +} + +// IsLoggingChannel checks whether the app is currently logging a public channel. +func IsLoggingChannel(channel string) bool { + if !config.Current.Logging.Enabled { + return false + } + + for _, value := range config.Current.Logging.Channels { + if value == channel { + return true + } + } + return false +} + +// LogMessage appends to a user's conversation log. +func LogMessage(sub *Subscriber, otherUsername, senderUsername string, msg messages.Message) { + if !sub.log { + return + } + + // Create or get the filehandle. + fh, err := initLogFile(sub, "@"+sub.Username, otherUsername) + if err != nil { + log.Error("LogMessage(%s): %s", sub.Username, err) + return + } + + fh.Write( + []byte(fmt.Sprintf( + "%s [%s] %s\n", + time.Now().Format(time.RFC3339), + senderUsername, + msg.Message, + )), + ) +} + +// LogChannel appends to a channel's conversation log. +func LogChannel(s *Server, channel string, username string, msg messages.Message) { + fh, err := initLogFile(s, channel) + if err != nil { + log.Error("LogChannel(%s): %s", channel, err) + } + + fh.Write( + []byte(fmt.Sprintf( + "%s [%s] %s\n", + time.Now().Format(time.RFC3339), + username, + msg.Message, + )), + ) +} + +// Initialize a logging directory. +func initLogFile(sub LogCacheable, components ...string) (io.Writer, error) { + // Initialize the logfh cache? + var logfh = sub.GetLogFilehandleCache() + + var ( + suffix = components[len(components)-1] + middle = components[:len(components)-1] + paths = append([]string{ + config.Current.Logging.Directory, + }, middle..., + ) + filename = strings.Join( + append(paths, suffix+".txt"), + "/", + ) + ) + + // Already have this conversation log open? + if fh, ok := logfh[suffix]; ok { + return fh, nil + } + + log.Warn("Initialize log directory: path=%+v suffix=%s", paths, suffix) + if err := os.MkdirAll(strings.Join(paths, "/"), 0755); err != nil { + return nil, err + } + + fh, err := os.OpenFile(filename, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) + if err != nil { + return nil, err + } + + logfh[suffix] = fh + return logfh[suffix], nil +} + +// Interface for objects that hold log filehandle caches. +type LogCacheable interface { + GetLogFilehandleCache() map[string]io.Writer +} + +// Implementations of LogCacheable. +func (sub *Subscriber) GetLogFilehandleCache() map[string]io.Writer { + if sub.logfh == nil { + sub.logfh = map[string]io.Writer{} + } + return sub.logfh +} +func (s *Server) GetLogFilehandleCache() map[string]io.Writer { + if s.logfh == nil { + s.logfh = map[string]io.Writer{} + } + return s.logfh +} diff --git a/pkg/server.go b/pkg/server.go index 938a7de..0dfaf64 100644 --- a/pkg/server.go +++ b/pkg/server.go @@ -1,6 +1,7 @@ package barertc import ( + "io" "net/http" "sync" ) @@ -16,6 +17,9 @@ type Server struct { // Connected WebSocket subscribers. subscribersMu sync.RWMutex subscribers map[*Subscriber]struct{} + + // Cached filehandles for channel logging. + logfh map[string]io.Writer } // NewServer initializes the Server. diff --git a/pkg/websocket.go b/pkg/websocket.go index 211f235..424a782 100644 --- a/pkg/websocket.go +++ b/pkg/websocket.go @@ -5,6 +5,7 @@ import ( "encoding/json" "errors" "fmt" + "io" "net/http" "sort" "strings" @@ -44,6 +45,10 @@ type Subscriber struct { // Record which message IDs belong to this user. midMu sync.Mutex messageIDs map[int64]struct{} + + // Logging. + log bool + logfh map[string]io.Writer } // ReadLoop spawns a goroutine that reads from the websocket connection.