source: code/trunk/logger.go@ 389

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

Extract history loading into functions

These will get re-used for sending history to clients that don't support
the chathistory extension.

File size: 6.9 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
[387]14const messageLoggerMaxTries = 100
15
[215]16type messageLogger struct {
[248]17 network *network
18 entity string
[215]19
[247]20 path string
21 file *os.File
[215]22}
23
[248]24func newMessageLogger(network *network, entity string) *messageLogger {
[215]25 return &messageLogger{
[248]26 network: network,
27 entity: entity,
[215]28 }
29}
30
[247]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
[215]41func (ml *messageLogger) Append(msg *irc.Message) error {
42 s := formatMessage(msg)
43 if s == "" {
44 return nil
45 }
46
[250]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 }
[216]58
[215]59 // TODO: enforce maximum open file handles (LRU cache of file handles)
60 // TODO: handle non-monotonic clock behaviour
[250]61 path := logPath(ml.network, ml.entity, t)
[247]62 if ml.path != path {
[215]63 if ml.file != nil {
64 ml.file.Close()
65 }
66
[247]67 dir := filepath.Dir(path)
[215]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
[247]77 ml.path = path
[215]78 ml.file = f
79 }
80
[250]81 _, err := fmt.Fprintf(ml.file, "[%02d:%02d:%02d] %s\n", t.Hour(), t.Minute(), t.Second(), s)
[215]82 if err != nil {
[247]83 return fmt.Errorf("failed to log message to %q: %v", ml.path, err)
[215]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)
[235]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)
[215]128 case "MODE":
129 return fmt.Sprintf("*** %s sets mode: %s", msg.Prefix.Name, strings.Join(msg.Params[1:], " "))
[234]130 case "NOTICE":
131 return fmt.Sprintf("-%s- %s", msg.Prefix.Name, msg.Params[1])
132 case "PRIVMSG":
[215]133 return fmt.Sprintf("<%s> %s", msg.Prefix.Name, msg.Params[1])
134 default:
135 return ""
136 }
137}
[319]138
[360]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 // TODO: support NOTICE
148 if !strings.HasPrefix(line, "<") {
149 return nil, time.Time{}, nil
150 }
151 i := strings.Index(line, "> ")
152 if i < 0 {
153 return nil, time.Time{}, nil
154 }
155
156 year, month, day := ref.Date()
157 t := time.Date(year, month, day, hour, minute, second, 0, time.Local)
158
159 sender := line[1:i]
160 text := line[i+2:]
161 msg := &irc.Message{
162 Tags: map[string]irc.TagValue{
163 "time": irc.TagValue(t.UTC().Format(serverTimeLayout)),
164 },
[362]165 Prefix: &irc.Prefix{Name: sender},
[360]166 Command: "PRIVMSG",
167 Params: []string{entity, text},
168 }
169 return msg, t, nil
170}
171
172func parseMessagesBefore(network *network, entity string, ref time.Time, limit int) ([]*irc.Message, error) {
173 path := logPath(network, entity, ref)
[319]174 f, err := os.Open(path)
175 if err != nil {
176 if os.IsNotExist(err) {
177 return nil, nil
178 }
179 return nil, err
180 }
181 defer f.Close()
182
183 historyRing := make([]*irc.Message, limit)
184 cur := 0
185
186 sc := bufio.NewScanner(f)
187 for sc.Scan() {
[360]188 msg, t, err := parseMessage(sc.Text(), entity, ref)
[319]189 if err != nil {
190 return nil, err
[360]191 } else if msg == nil {
[319]192 continue
[360]193 } else if !t.Before(ref) {
[319]194 break
195 }
196
[360]197 historyRing[cur%limit] = msg
[319]198 cur++
199 }
200 if sc.Err() != nil {
201 return nil, sc.Err()
202 }
203
204 n := limit
205 if cur < limit {
206 n = cur
207 }
208 start := (cur - n + limit) % limit
209
210 if start+n <= limit { // ring doesnt wrap
211 return historyRing[start : start+n], nil
212 } else { // ring wraps
213 history := make([]*irc.Message, n)
214 r := copy(history, historyRing[start:])
215 copy(history[r:], historyRing[:n-r])
216 return history, nil
217 }
218}
[360]219
220func parseMessagesAfter(network *network, entity string, ref time.Time, limit int) ([]*irc.Message, error) {
221 path := logPath(network, entity, ref)
222 f, err := os.Open(path)
223 if err != nil {
224 if os.IsNotExist(err) {
225 return nil, nil
226 }
227 return nil, err
228 }
229 defer f.Close()
230
231 var history []*irc.Message
232 sc := bufio.NewScanner(f)
233 for sc.Scan() && len(history) < limit {
234 msg, t, err := parseMessage(sc.Text(), entity, ref)
235 if err != nil {
236 return nil, err
237 } else if msg == nil || !t.After(ref) {
238 continue
239 }
240
241 history = append(history, msg)
242 }
243 if sc.Err() != nil {
244 return nil, sc.Err()
245 }
246
247 return history, nil
248}
[387]249
250func loadHistoryBeforeTime(network *network, entity string, t time.Time, limit int) ([]*irc.Message, error) {
251 history := make([]*irc.Message, limit)
252 remaining := limit
253 tries := 0
254 for remaining > 0 && tries < messageLoggerMaxTries {
255 buf, err := parseMessagesBefore(network, entity, t, remaining)
256 if err != nil {
257 return nil, err
258 }
259 if len(buf) == 0 {
260 tries++
261 } else {
262 tries = 0
263 }
264 copy(history[remaining-len(buf):], buf)
265 remaining -= len(buf)
266 year, month, day := t.Date()
267 t = time.Date(year, month, day, 0, 0, 0, 0, t.Location()).Add(-1)
268 }
269
270 return history[remaining:], nil
271}
272
273func loadHistoryAfterTime(network *network, entity string, t time.Time, limit int) ([]*irc.Message, error) {
274 var history []*irc.Message
275 remaining := limit
276 tries := 0
277 now := time.Now()
278 for remaining > 0 && tries < messageLoggerMaxTries && t.Before(now) {
279 buf, err := parseMessagesAfter(network, entity, t, remaining)
280 if err != nil {
281 return nil, err
282 }
283 if len(buf) == 0 {
284 tries++
285 } else {
286 tries = 0
287 }
288 history = append(history, buf...)
289 remaining -= len(buf)
290 year, month, day := t.Date()
291 t = time.Date(year, month, day+1, 0, 0, 0, 0, t.Location())
292 }
293 return history, nil
294}
Note: See TracBrowser for help on using the repository browser.