source: code/trunk/logger.go@ 405

Last change on this file since 405 was 397, checked in by contact, 5 years ago

Escape user/network/entity characters in log file path

ZNC replaces slashes and backslashes with a dashes.

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