source: code/trunk/logger.go@ 394

Last change on this file since 394 was 392, checked in by contact, 5 years ago

Format CTCP ACTION messages in logs

File size: 7.5 KB
Line 
1package soju
2
3import (
4 "bufio"
5 "fmt"
6 "os"
7 "path/filepath"
8 "strings"
9 "time"
10
11 "gopkg.in/irc.v3"
12)
13
14const messageLoggerMaxTries = 100
15
16type messageLogger struct {
17 network *network
18 entity string
19
20 path string
21 file *os.File
22}
23
24func newMessageLogger(network *network, entity string) *messageLogger {
25 return &messageLogger{
26 network: network,
27 entity: entity,
28 }
29}
30
31func logPath(network *network, entity string, t time.Time) string {
32 user := network.user
33 srv := user.srv
34
35 // TODO: handle/forbid network/entity names with illegal path characters
36 year, month, day := t.Date()
37 filename := fmt.Sprintf("%04d-%02d-%02d.log", year, month, day)
38 return filepath.Join(srv.LogPath, user.Username, network.GetName(), entity, filename)
39}
40
41func (ml *messageLogger) Append(msg *irc.Message) error {
42 s := formatMessage(msg)
43 if s == "" {
44 return nil
45 }
46
47 var t time.Time
48 if tag, ok := msg.Tags["time"]; ok {
49 var err error
50 t, err = time.Parse(serverTimeLayout, string(tag))
51 if err != nil {
52 return fmt.Errorf("failed to parse message time tag: %v", err)
53 }
54 t = t.In(time.Local)
55 } else {
56 t = time.Now()
57 }
58
59 // TODO: enforce maximum open file handles (LRU cache of file handles)
60 // TODO: handle non-monotonic clock behaviour
61 path := logPath(ml.network, ml.entity, t)
62 if ml.path != path {
63 if ml.file != nil {
64 ml.file.Close()
65 }
66
67 dir := filepath.Dir(path)
68 if err := os.MkdirAll(dir, 0700); err != nil {
69 return fmt.Errorf("failed to create logs directory %q: %v", dir, err)
70 }
71
72 f, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0600)
73 if err != nil {
74 return fmt.Errorf("failed to open log file %q: %v", path, err)
75 }
76
77 ml.path = path
78 ml.file = f
79 }
80
81 _, err := fmt.Fprintf(ml.file, "[%02d:%02d:%02d] %s\n", t.Hour(), t.Minute(), t.Second(), s)
82 if err != nil {
83 return fmt.Errorf("failed to log message to %q: %v", ml.path, err)
84 }
85 return nil
86}
87
88func (ml *messageLogger) Close() error {
89 if ml.file == nil {
90 return nil
91 }
92 return ml.file.Close()
93}
94
95// formatMessage formats a message log line. It assumes a well-formed IRC
96// message.
97func formatMessage(msg *irc.Message) string {
98 switch strings.ToUpper(msg.Command) {
99 case "NICK":
100 return fmt.Sprintf("*** %s is now known as %s", msg.Prefix.Name, msg.Params[0])
101 case "JOIN":
102 return fmt.Sprintf("*** Joins: %s (%s@%s)", msg.Prefix.Name, msg.Prefix.User, msg.Prefix.Host)
103 case "PART":
104 var reason string
105 if len(msg.Params) > 1 {
106 reason = msg.Params[1]
107 }
108 return fmt.Sprintf("*** Parts: %s (%s@%s) (%s)", msg.Prefix.Name, msg.Prefix.User, msg.Prefix.Host, reason)
109 case "KICK":
110 nick := msg.Params[1]
111 var reason string
112 if len(msg.Params) > 2 {
113 reason = msg.Params[2]
114 }
115 return fmt.Sprintf("*** %s was kicked by %s (%s)", nick, msg.Prefix.Name, reason)
116 case "QUIT":
117 var reason string
118 if len(msg.Params) > 0 {
119 reason = msg.Params[0]
120 }
121 return fmt.Sprintf("*** Quits: %s (%s@%s) (%s)", msg.Prefix.Name, msg.Prefix.User, msg.Prefix.Host, reason)
122 case "TOPIC":
123 var topic string
124 if len(msg.Params) > 1 {
125 topic = msg.Params[1]
126 }
127 return fmt.Sprintf("*** %s changes topic to '%s'", msg.Prefix.Name, topic)
128 case "MODE":
129 return fmt.Sprintf("*** %s sets mode: %s", msg.Prefix.Name, strings.Join(msg.Params[1:], " "))
130 case "NOTICE":
131 return fmt.Sprintf("-%s- %s", msg.Prefix.Name, msg.Params[1])
132 case "PRIVMSG":
133 if cmd, params, ok := parseCTCPMessage(msg); ok && cmd == "ACTION" {
134 return fmt.Sprintf("* %s %s", msg.Prefix.Name, params)
135 } else {
136 return fmt.Sprintf("<%s> %s", msg.Prefix.Name, msg.Params[1])
137 }
138 default:
139 return ""
140 }
141}
142
143func parseMessage(line, entity string, ref time.Time) (*irc.Message, time.Time, error) {
144 var hour, minute, second int
145 _, err := fmt.Sscanf(line, "[%02d:%02d:%02d] ", &hour, &minute, &second)
146 if err != nil {
147 return nil, time.Time{}, err
148 }
149 line = line[11:]
150
151 var cmd, sender, text string
152 if strings.HasPrefix(line, "<") {
153 cmd = "PRIVMSG"
154 parts := strings.SplitN(line[1:], "> ", 2)
155 if len(parts) != 2 {
156 return nil, time.Time{}, nil
157 }
158 sender, text = parts[0], parts[1]
159 } else if strings.HasPrefix(line, "-") {
160 cmd = "NOTICE"
161 parts := strings.SplitN(line[1:], "- ", 2)
162 if len(parts) != 2 {
163 return nil, time.Time{}, nil
164 }
165 sender, text = parts[0], parts[1]
166 } else if strings.HasPrefix(line, "* ") {
167 cmd = "PRIVMSG"
168 parts := strings.SplitN(line[2:], " ", 2)
169 if len(parts) != 2 {
170 return nil, time.Time{}, nil
171 }
172 sender, text = parts[0], "\x01ACTION "+parts[1]+"\x01"
173 } else {
174 return nil, time.Time{}, nil
175 }
176
177 year, month, day := ref.Date()
178 t := time.Date(year, month, day, hour, minute, second, 0, time.Local)
179
180 msg := &irc.Message{
181 Tags: map[string]irc.TagValue{
182 "time": irc.TagValue(t.UTC().Format(serverTimeLayout)),
183 },
184 Prefix: &irc.Prefix{Name: sender},
185 Command: cmd,
186 Params: []string{entity, text},
187 }
188 return msg, t, nil
189}
190
191func parseMessagesBefore(network *network, entity string, ref time.Time, limit int) ([]*irc.Message, error) {
192 path := logPath(network, entity, ref)
193 f, err := os.Open(path)
194 if err != nil {
195 if os.IsNotExist(err) {
196 return nil, nil
197 }
198 return nil, err
199 }
200 defer f.Close()
201
202 historyRing := make([]*irc.Message, limit)
203 cur := 0
204
205 sc := bufio.NewScanner(f)
206 for sc.Scan() {
207 msg, t, err := parseMessage(sc.Text(), entity, ref)
208 if err != nil {
209 return nil, err
210 } else if msg == nil {
211 continue
212 } else if !t.Before(ref) {
213 break
214 }
215
216 historyRing[cur%limit] = msg
217 cur++
218 }
219 if sc.Err() != nil {
220 return nil, sc.Err()
221 }
222
223 n := limit
224 if cur < limit {
225 n = cur
226 }
227 start := (cur - n + limit) % limit
228
229 if start+n <= limit { // ring doesnt wrap
230 return historyRing[start : start+n], nil
231 } else { // ring wraps
232 history := make([]*irc.Message, n)
233 r := copy(history, historyRing[start:])
234 copy(history[r:], historyRing[:n-r])
235 return history, nil
236 }
237}
238
239func parseMessagesAfter(network *network, entity string, ref time.Time, limit int) ([]*irc.Message, error) {
240 path := logPath(network, entity, ref)
241 f, err := os.Open(path)
242 if err != nil {
243 if os.IsNotExist(err) {
244 return nil, nil
245 }
246 return nil, err
247 }
248 defer f.Close()
249
250 var history []*irc.Message
251 sc := bufio.NewScanner(f)
252 for sc.Scan() && len(history) < limit {
253 msg, t, err := parseMessage(sc.Text(), entity, ref)
254 if err != nil {
255 return nil, err
256 } else if msg == nil || !t.After(ref) {
257 continue
258 }
259
260 history = append(history, msg)
261 }
262 if sc.Err() != nil {
263 return nil, sc.Err()
264 }
265
266 return history, nil
267}
268
269func loadHistoryBeforeTime(network *network, entity string, t time.Time, limit int) ([]*irc.Message, error) {
270 history := make([]*irc.Message, limit)
271 remaining := limit
272 tries := 0
273 for remaining > 0 && tries < messageLoggerMaxTries {
274 buf, err := parseMessagesBefore(network, entity, t, remaining)
275 if err != nil {
276 return nil, err
277 }
278 if len(buf) == 0 {
279 tries++
280 } else {
281 tries = 0
282 }
283 copy(history[remaining-len(buf):], buf)
284 remaining -= len(buf)
285 year, month, day := t.Date()
286 t = time.Date(year, month, day, 0, 0, 0, 0, t.Location()).Add(-1)
287 }
288
289 return history[remaining:], nil
290}
291
292func loadHistoryAfterTime(network *network, entity string, t time.Time, limit int) ([]*irc.Message, error) {
293 var history []*irc.Message
294 remaining := limit
295 tries := 0
296 now := time.Now()
297 for remaining > 0 && tries < messageLoggerMaxTries && t.Before(now) {
298 buf, err := parseMessagesAfter(network, entity, t, remaining)
299 if err != nil {
300 return nil, err
301 }
302 if len(buf) == 0 {
303 tries++
304 } else {
305 tries = 0
306 }
307 history = append(history, buf...)
308 remaining -= len(buf)
309 year, month, day := t.Date()
310 t = time.Date(year, month, day+1, 0, 0, 0, 0, t.Location())
311 }
312 return history, nil
313}
Note: See TracBrowser for help on using the repository browser.