source: code/trunk/logger.go@ 391

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

Parse NOTICE messages from logs

File size: 7.0 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 return fmt.Sprintf("<%s> %s", msg.Prefix.Name, msg.Params[1])
134 default:
135 return ""
136 }
137}
138
139func parseMessage(line, entity string, ref time.Time) (*irc.Message, time.Time, error) {
140 var hour, minute, second int
141 _, err := fmt.Sscanf(line, "[%02d:%02d:%02d] ", &hour, &minute, &second)
142 if err != nil {
143 return nil, time.Time{}, err
144 }
145 line = line[11:]
146
147 var cmd, suffix string
148 if strings.HasPrefix(line, "<") {
149 cmd = "PRIVMSG"
150 suffix = "> "
151 } else if strings.HasPrefix(line, "-") {
152 cmd = "NOTICE"
153 suffix = "- "
154 } else {
155 return nil, time.Time{}, nil
156 }
157
158 i := strings.Index(line, suffix)
159 if i < 0 {
160 return nil, time.Time{}, nil
161 }
162 sender := line[1:i]
163 text := line[i+2:]
164
165 year, month, day := ref.Date()
166 t := time.Date(year, month, day, hour, minute, second, 0, time.Local)
167
168 msg := &irc.Message{
169 Tags: map[string]irc.TagValue{
170 "time": irc.TagValue(t.UTC().Format(serverTimeLayout)),
171 },
172 Prefix: &irc.Prefix{Name: sender},
173 Command: cmd,
174 Params: []string{entity, text},
175 }
176 return msg, t, nil
177}
178
179func parseMessagesBefore(network *network, entity string, ref time.Time, limit int) ([]*irc.Message, error) {
180 path := logPath(network, entity, ref)
181 f, err := os.Open(path)
182 if err != nil {
183 if os.IsNotExist(err) {
184 return nil, nil
185 }
186 return nil, err
187 }
188 defer f.Close()
189
190 historyRing := make([]*irc.Message, limit)
191 cur := 0
192
193 sc := bufio.NewScanner(f)
194 for sc.Scan() {
195 msg, t, err := parseMessage(sc.Text(), entity, ref)
196 if err != nil {
197 return nil, err
198 } else if msg == nil {
199 continue
200 } else if !t.Before(ref) {
201 break
202 }
203
204 historyRing[cur%limit] = msg
205 cur++
206 }
207 if sc.Err() != nil {
208 return nil, sc.Err()
209 }
210
211 n := limit
212 if cur < limit {
213 n = cur
214 }
215 start := (cur - n + limit) % limit
216
217 if start+n <= limit { // ring doesnt wrap
218 return historyRing[start : start+n], nil
219 } else { // ring wraps
220 history := make([]*irc.Message, n)
221 r := copy(history, historyRing[start:])
222 copy(history[r:], historyRing[:n-r])
223 return history, nil
224 }
225}
226
227func parseMessagesAfter(network *network, entity string, ref time.Time, limit int) ([]*irc.Message, error) {
228 path := logPath(network, entity, ref)
229 f, err := os.Open(path)
230 if err != nil {
231 if os.IsNotExist(err) {
232 return nil, nil
233 }
234 return nil, err
235 }
236 defer f.Close()
237
238 var history []*irc.Message
239 sc := bufio.NewScanner(f)
240 for sc.Scan() && len(history) < limit {
241 msg, t, err := parseMessage(sc.Text(), entity, ref)
242 if err != nil {
243 return nil, err
244 } else if msg == nil || !t.After(ref) {
245 continue
246 }
247
248 history = append(history, msg)
249 }
250 if sc.Err() != nil {
251 return nil, sc.Err()
252 }
253
254 return history, nil
255}
256
257func loadHistoryBeforeTime(network *network, entity string, t time.Time, limit int) ([]*irc.Message, error) {
258 history := make([]*irc.Message, limit)
259 remaining := limit
260 tries := 0
261 for remaining > 0 && tries < messageLoggerMaxTries {
262 buf, err := parseMessagesBefore(network, entity, t, remaining)
263 if err != nil {
264 return nil, err
265 }
266 if len(buf) == 0 {
267 tries++
268 } else {
269 tries = 0
270 }
271 copy(history[remaining-len(buf):], buf)
272 remaining -= len(buf)
273 year, month, day := t.Date()
274 t = time.Date(year, month, day, 0, 0, 0, 0, t.Location()).Add(-1)
275 }
276
277 return history[remaining:], nil
278}
279
280func loadHistoryAfterTime(network *network, entity string, t time.Time, limit int) ([]*irc.Message, error) {
281 var history []*irc.Message
282 remaining := limit
283 tries := 0
284 now := time.Now()
285 for remaining > 0 && tries < messageLoggerMaxTries && t.Before(now) {
286 buf, err := parseMessagesAfter(network, entity, t, remaining)
287 if err != nil {
288 return nil, err
289 }
290 if len(buf) == 0 {
291 tries++
292 } else {
293 tries = 0
294 }
295 history = append(history, buf...)
296 remaining -= len(buf)
297 year, month, day := t.Date()
298 t = time.Date(year, month, day+1, 0, 0, 0, 0, t.Location())
299 }
300 return history, nil
301}
Note: See TracBrowser for help on using the repository browser.