source: code/trunk/logger.go@ 363

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

go fmt

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