소스 검색

fix #1592

Implements the new `CHATHISTORY LISTCORRESPONDENTS` API.
tags/v2.6.0-rc1
Shivaram Lingamneni 3 년 전
부모
커밋
4052cd12fe
9개의 변경된 파일320개의 추가작업 그리고 52개의 파일을 삭제
  1. 3
    8
      irc/channel.go
  2. 4
    7
      irc/client.go
  3. 13
    0
      irc/client_lookup_set.go
  4. 32
    13
      irc/handlers.go
  5. 92
    3
      irc/history/history.go
  6. 5
    3
      irc/history/queries.go
  7. 2
    2
      irc/histserv.go
  8. 144
    12
      irc/mysql/history.go
  9. 25
    4
      irc/znc.go

+ 3
- 8
irc/channel.go 파일 보기

920
 		_, seq, _ := channel.server.GetHistorySequence(channel, client, "")
920
 		_, seq, _ := channel.server.GetHistorySequence(channel, client, "")
921
 		if seq != nil {
921
 		if seq != nil {
922
 			zncMax := channel.server.Config().History.ZNCMax
922
 			zncMax := channel.server.Config().History.ZNCMax
923
-			items, _, _ = seq.Between(history.Selector{Time: start}, history.Selector{Time: end}, zncMax)
923
+			items, _ = seq.Between(history.Selector{Time: start}, history.Selector{Time: end}, zncMax)
924
 		}
924
 		}
925
 	} else if !rb.session.HasHistoryCaps() {
925
 	} else if !rb.session.HasHistoryCaps() {
926
 		var replayLimit int
926
 		var replayLimit int
937
 		if 0 < replayLimit {
937
 		if 0 < replayLimit {
938
 			_, seq, _ := channel.server.GetHistorySequence(channel, client, "")
938
 			_, seq, _ := channel.server.GetHistorySequence(channel, client, "")
939
 			if seq != nil {
939
 			if seq != nil {
940
-				items, _, _ = seq.Between(history.Selector{}, history.Selector{}, replayLimit)
940
+				items, _ = seq.Between(history.Selector{}, history.Selector{}, replayLimit)
941
 			}
941
 			}
942
 		}
942
 		}
943
 	}
943
 	}
1097
 
1097
 
1098
 func (channel *Channel) replayHistoryForResume(session *Session, after time.Time, before time.Time) {
1098
 func (channel *Channel) replayHistoryForResume(session *Session, after time.Time, before time.Time) {
1099
 	var items []history.Item
1099
 	var items []history.Item
1100
-	var complete bool
1101
 	afterS, beforeS := history.Selector{Time: after}, history.Selector{Time: before}
1100
 	afterS, beforeS := history.Selector{Time: after}, history.Selector{Time: before}
1102
 	_, seq, _ := channel.server.GetHistorySequence(channel, session.client, "")
1101
 	_, seq, _ := channel.server.GetHistorySequence(channel, session.client, "")
1103
 	if seq != nil {
1102
 	if seq != nil {
1104
-		items, complete, _ = seq.Between(afterS, beforeS, channel.server.Config().History.ZNCMax)
1103
+		items, _ = seq.Between(afterS, beforeS, channel.server.Config().History.ZNCMax)
1105
 	}
1104
 	}
1106
 	rb := NewResponseBuffer(session)
1105
 	rb := NewResponseBuffer(session)
1107
 	if len(items) != 0 {
1106
 	if len(items) != 0 {
1108
 		channel.replayHistoryItems(rb, items, false)
1107
 		channel.replayHistoryItems(rb, items, false)
1109
 	}
1108
 	}
1110
-	if !complete && !session.resumeDetails.HistoryIncomplete {
1111
-		// warn here if we didn't warn already
1112
-		rb.Add(nil, histservService.prefix, "NOTICE", channel.Name(), session.client.t("Some additional message history may have been lost"))
1113
-	}
1114
 	rb.Send(true)
1109
 	rb.Send(true)
1115
 }
1110
 }
1116
 
1111
 

+ 4
- 7
irc/client.go 파일 보기

990
 	}
990
 	}
991
 	_, privmsgSeq, _ := server.GetHistorySequence(nil, client, "*")
991
 	_, privmsgSeq, _ := server.GetHistorySequence(nil, client, "*")
992
 	if privmsgSeq != nil {
992
 	if privmsgSeq != nil {
993
-		privmsgs, _, _ := privmsgSeq.Between(history.Selector{}, history.Selector{}, config.History.ClientLength)
993
+		privmsgs, _ := privmsgSeq.Between(history.Selector{}, history.Selector{}, config.History.ClientLength)
994
 		for _, item := range privmsgs {
994
 		for _, item := range privmsgs {
995
 			sender := server.clients.Get(NUHToNick(item.Nick))
995
 			sender := server.clients.Get(NUHToNick(item.Nick))
996
 			if sender != nil {
996
 			if sender != nil {
1055
 	// replay direct PRIVSMG history
1055
 	// replay direct PRIVSMG history
1056
 	if !timestamp.IsZero() && privmsgSeq != nil {
1056
 	if !timestamp.IsZero() && privmsgSeq != nil {
1057
 		after := history.Selector{Time: timestamp}
1057
 		after := history.Selector{Time: timestamp}
1058
-		items, complete, _ := privmsgSeq.Between(after, history.Selector{}, config.History.ZNCMax)
1058
+		items, _ := privmsgSeq.Between(after, history.Selector{}, config.History.ZNCMax)
1059
 		if len(items) != 0 {
1059
 		if len(items) != 0 {
1060
 			rb := NewResponseBuffer(session)
1060
 			rb := NewResponseBuffer(session)
1061
-			client.replayPrivmsgHistory(rb, items, "", complete)
1061
+			client.replayPrivmsgHistory(rb, items, "")
1062
 			rb.Send(true)
1062
 			rb.Send(true)
1063
 		}
1063
 		}
1064
 	}
1064
 	}
1066
 	session.resumeDetails = nil
1066
 	session.resumeDetails = nil
1067
 }
1067
 }
1068
 
1068
 
1069
-func (client *Client) replayPrivmsgHistory(rb *ResponseBuffer, items []history.Item, target string, complete bool) {
1069
+func (client *Client) replayPrivmsgHistory(rb *ResponseBuffer, items []history.Item, target string) {
1070
 	var batchID string
1070
 	var batchID string
1071
 	details := client.Details()
1071
 	details := client.Details()
1072
 	nick := details.nick
1072
 	nick := details.nick
1126
 	}
1126
 	}
1127
 
1127
 
1128
 	rb.EndNestedBatch(batchID)
1128
 	rb.EndNestedBatch(batchID)
1129
-	if !complete {
1130
-		rb.Add(nil, histservService.prefix, "NOTICE", nick, client.t("Some additional message history may have been lost"))
1131
-	}
1132
 }
1129
 }
1133
 
1130
 
1134
 // IdleTime returns how long this client's been idle.
1131
 // IdleTime returns how long this client's been idle.

+ 13
- 0
irc/client_lookup_set.go 파일 보기

308
 
308
 
309
 	return set
309
 	return set
310
 }
310
 }
311
+
312
+// Determine the canonical / unfolded form of a nick, if a client matching it
313
+// is present (or always-on).
314
+func (clients *ClientManager) UnfoldNick(cfnick string) (nick string) {
315
+	clients.RLock()
316
+	c := clients.byNick[cfnick]
317
+	clients.RUnlock()
318
+	if c != nil {
319
+		return c.Nick()
320
+	} else {
321
+		return cfnick
322
+	}
323
+}

+ 32
- 13
irc/handlers.go 파일 보기

566
 // e.g., CHATHISTORY #ircv3 BETWEEN timestamp=YYYY-MM-DDThh:mm:ss.sssZ timestamp=YYYY-MM-DDThh:mm:ss.sssZ + 100
566
 // e.g., CHATHISTORY #ircv3 BETWEEN timestamp=YYYY-MM-DDThh:mm:ss.sssZ timestamp=YYYY-MM-DDThh:mm:ss.sssZ + 100
567
 func chathistoryHandler(server *Server, client *Client, msg ircmsg.Message, rb *ResponseBuffer) (exiting bool) {
567
 func chathistoryHandler(server *Server, client *Client, msg ircmsg.Message, rb *ResponseBuffer) (exiting bool) {
568
 	var items []history.Item
568
 	var items []history.Item
569
-	unknown_command := false
570
 	var target string
569
 	var target string
571
 	var channel *Channel
570
 	var channel *Channel
572
 	var sequence history.Sequence
571
 	var sequence history.Sequence
573
 	var err error
572
 	var err error
573
+	var listCorrespondents bool
574
+	var correspondents []history.CorrespondentListing
574
 	defer func() {
575
 	defer func() {
575
 		// errors are sent either without a batch, or in a draft/labeled-response batch as usual
576
 		// errors are sent either without a batch, or in a draft/labeled-response batch as usual
576
-		if unknown_command {
577
-			rb.Add(nil, server.name, "FAIL", "CHATHISTORY", "UNKNOWN_COMMAND", utils.SafeErrorParam(msg.Params[0]), client.t("Unknown command"))
578
-		} else if err == utils.ErrInvalidParams {
577
+		if err == utils.ErrInvalidParams {
579
 			rb.Add(nil, server.name, "FAIL", "CHATHISTORY", "INVALID_PARAMS", msg.Params[0], client.t("Invalid parameters"))
578
 			rb.Add(nil, server.name, "FAIL", "CHATHISTORY", "INVALID_PARAMS", msg.Params[0], client.t("Invalid parameters"))
580
 		} else if sequence == nil {
579
 		} else if sequence == nil {
581
 			rb.Add(nil, server.name, "FAIL", "CHATHISTORY", "INVALID_TARGET", utils.SafeErrorParam(target), client.t("Messages could not be retrieved"))
580
 			rb.Add(nil, server.name, "FAIL", "CHATHISTORY", "INVALID_TARGET", utils.SafeErrorParam(target), client.t("Messages could not be retrieved"))
583
 			rb.Add(nil, server.name, "FAIL", "CHATHISTORY", "MESSAGE_ERROR", msg.Params[0], client.t("Messages could not be retrieved"))
582
 			rb.Add(nil, server.name, "FAIL", "CHATHISTORY", "MESSAGE_ERROR", msg.Params[0], client.t("Messages could not be retrieved"))
584
 		} else {
583
 		} else {
585
 			// successful responses are sent as a chathistory or history batch
584
 			// successful responses are sent as a chathistory or history batch
586
-			if channel != nil {
585
+			if listCorrespondents {
586
+				batchID := rb.StartNestedBatch("draft/chathistory-listcorrespondents")
587
+				defer rb.EndNestedBatch(batchID)
588
+				for _, correspondent := range correspondents {
589
+					nick := server.clients.UnfoldNick(correspondent.CfCorrespondent)
590
+					rb.Add(nil, server.name, "CHATHISTORY", "CORRESPONDENT", nick,
591
+						correspondent.Time.Format(IRCv3TimestampFormat))
592
+				}
593
+			} else if channel != nil {
587
 				channel.replayHistoryItems(rb, items, false)
594
 				channel.replayHistoryItems(rb, items, false)
588
 			} else {
595
 			} else {
589
-				client.replayPrivmsgHistory(rb, items, target, true)
596
+				client.replayPrivmsgHistory(rb, items, target)
590
 			}
597
 			}
591
 		}
598
 		}
592
 	}()
599
 	}()
598
 	}
605
 	}
599
 	preposition := strings.ToLower(msg.Params[0])
606
 	preposition := strings.ToLower(msg.Params[0])
600
 	target = msg.Params[1]
607
 	target = msg.Params[1]
608
+	if preposition == "listcorrespondents" {
609
+		target = "*"
610
+	}
601
 
611
 
602
 	parseQueryParam := func(param string) (msgid string, timestamp time.Time, err error) {
612
 	parseQueryParam := func(param string) (msgid string, timestamp time.Time, err error) {
603
 		if param == "*" && (preposition == "before" || preposition == "between") {
613
 		if param == "*" && (preposition == "before" || preposition == "between") {
641
 		return endpoint.Truncate(time.Millisecond).Add(time.Millisecond)
651
 		return endpoint.Truncate(time.Millisecond).Add(time.Millisecond)
642
 	}
652
 	}
643
 
653
 
654
+	paramPos := 2
644
 	var start, end history.Selector
655
 	var start, end history.Selector
645
 	var limit int
656
 	var limit int
646
 	switch preposition {
657
 	switch preposition {
658
+	case "listcorrespondents":
659
+		listCorrespondents = true
660
+		// use the same selector parsing as BETWEEN,
661
+		// except that we have no target so we have one fewer parameter
662
+		paramPos = 1
663
+		fallthrough
647
 	case "between":
664
 	case "between":
648
-		start.Msgid, start.Time, err = parseQueryParam(msg.Params[2])
665
+		start.Msgid, start.Time, err = parseQueryParam(msg.Params[paramPos])
649
 		if err != nil {
666
 		if err != nil {
650
 			return
667
 			return
651
 		}
668
 		}
652
-		end.Msgid, end.Time, err = parseQueryParam(msg.Params[3])
669
+		end.Msgid, end.Time, err = parseQueryParam(msg.Params[paramPos+1])
653
 		if err != nil {
670
 		if err != nil {
654
 			return
671
 			return
655
 		}
672
 		}
662
 				end.Time = roundUp(end.Time)
679
 				end.Time = roundUp(end.Time)
663
 			}
680
 			}
664
 		}
681
 		}
665
-		limit = parseHistoryLimit(4)
682
+		limit = parseHistoryLimit(paramPos + 2)
666
 	case "before", "after", "around":
683
 	case "before", "after", "around":
667
 		start.Msgid, start.Time, err = parseQueryParam(msg.Params[2])
684
 		start.Msgid, start.Time, err = parseQueryParam(msg.Params[2])
668
 		if err != nil {
685
 		if err != nil {
689
 		}
706
 		}
690
 		limit = parseHistoryLimit(3)
707
 		limit = parseHistoryLimit(3)
691
 	default:
708
 	default:
692
-		unknown_command = true
709
+		err = utils.ErrInvalidParams
693
 		return
710
 		return
694
 	}
711
 	}
695
 
712
 
696
-	if preposition == "around" {
713
+	if listCorrespondents {
714
+		correspondents, err = sequence.ListCorrespondents(start, end, limit)
715
+	} else if preposition == "around" {
697
 		items, err = sequence.Around(start, limit)
716
 		items, err = sequence.Around(start, limit)
698
 	} else {
717
 	} else {
699
-		items, _, err = sequence.Between(start, end, limit)
718
+		items, err = sequence.Between(start, end, limit)
700
 	}
719
 	}
701
 	return
720
 	return
702
 }
721
 }
1086
 		if channel != nil {
1105
 		if channel != nil {
1087
 			channel.replayHistoryItems(rb, items, false)
1106
 			channel.replayHistoryItems(rb, items, false)
1088
 		} else {
1107
 		} else {
1089
-			client.replayPrivmsgHistory(rb, items, "", true)
1108
+			client.replayPrivmsgHistory(rb, items, "")
1090
 		}
1109
 		}
1091
 	}
1110
 	}
1092
 	return false
1111
 	return false

+ 92
- 3
irc/history/history.go 파일 보기

44
 	// for a DM, this is the casefolded nickname of the other party (whether this is
44
 	// for a DM, this is the casefolded nickname of the other party (whether this is
45
 	// an incoming or outgoing message). this lets us emulate the "query buffer" functionality
45
 	// an incoming or outgoing message). this lets us emulate the "query buffer" functionality
46
 	// required by CHATHISTORY:
46
 	// required by CHATHISTORY:
47
+	CfCorrespondent string `json:"CfCorrespondent,omitempty"`
48
+	IsBot           bool   `json:"IsBot,omitempty"`
49
+}
50
+
51
+type CorrespondentListing struct {
47
 	CfCorrespondent string
52
 	CfCorrespondent string
48
-	IsBot           bool `json:"IsBot,omitempty"`
53
+	Time            time.Time
49
 }
54
 }
50
 
55
 
51
 // HasMsgid tests whether a message has the message id `msgid`.
56
 // HasMsgid tests whether a message has the message id `msgid`.
61
 	}
66
 	}
62
 }
67
 }
63
 
68
 
69
+func ReverseCorrespondents(results []CorrespondentListing) {
70
+	// lol, generics when?
71
+	for i, j := 0, len(results)-1; i < j; i, j = i+1, j-1 {
72
+		results[i], results[j] = results[j], results[i]
73
+	}
74
+}
75
+
64
 // Buffer is a ring buffer holding message/event history for a channel or user
76
 // Buffer is a ring buffer holding message/event history for a channel or user
65
 type Buffer struct {
77
 type Buffer struct {
66
 	sync.RWMutex
78
 	sync.RWMutex
201
 	return list.matchInternal(satisfies, ascending, limit), complete, nil
213
 	return list.matchInternal(satisfies, ascending, limit), complete, nil
202
 }
214
 }
203
 
215
 
216
+// returns all correspondents, in reverse time order
217
+func (list *Buffer) allCorrespondents() (results []CorrespondentListing) {
218
+	seen := make(utils.StringSet)
219
+
220
+	list.RLock()
221
+	defer list.RUnlock()
222
+	if list.start == -1 || len(list.buffer) == 0 {
223
+		return
224
+	}
225
+
226
+	// XXX traverse in reverse order, so we get the latest timestamp
227
+	// of any message sent to/from the correspondent
228
+	pos := list.prev(list.end)
229
+	stop := list.start
230
+
231
+	for {
232
+		if !seen.Has(list.buffer[pos].CfCorrespondent) {
233
+			seen.Add(list.buffer[pos].CfCorrespondent)
234
+			results = append(results, CorrespondentListing{
235
+				CfCorrespondent: list.buffer[pos].CfCorrespondent,
236
+				Time:            list.buffer[pos].Message.Time,
237
+			})
238
+		}
239
+
240
+		if pos == stop {
241
+			break
242
+		}
243
+		pos = list.prev(pos)
244
+	}
245
+	return
246
+}
247
+
248
+// implement LISTCORRESPONDENTS
249
+func (list *Buffer) listCorrespondents(start, end Selector, cutoff time.Time, limit int) (results []CorrespondentListing, err error) {
250
+	after := start.Time
251
+	before := end.Time
252
+	after, before, ascending := MinMaxAsc(after, before, cutoff)
253
+
254
+	correspondents := list.allCorrespondents()
255
+	if len(correspondents) == 0 {
256
+		return
257
+	}
258
+
259
+	// XXX allCorrespondents returns results in reverse order,
260
+	// so if we're ascending, we actually go backwards
261
+	var i int
262
+	if ascending {
263
+		i = len(correspondents) - 1
264
+	} else {
265
+		i = 0
266
+	}
267
+
268
+	for 0 <= i && i < len(correspondents) && (limit == 0 || len(results) < limit) {
269
+		if (after.IsZero() || correspondents[i].Time.After(after)) &&
270
+			(before.IsZero() || correspondents[i].Time.Before(before)) {
271
+			results = append(results, correspondents[i])
272
+		}
273
+
274
+		if ascending {
275
+			i--
276
+		} else {
277
+			i++
278
+		}
279
+	}
280
+
281
+	if !ascending {
282
+		ReverseCorrespondents(results)
283
+	}
284
+
285
+	return
286
+}
287
+
204
 // implements history.Sequence, emulating a single history buffer (for a channel,
288
 // implements history.Sequence, emulating a single history buffer (for a channel,
205
 // a single user's DMs, or a DM conversation)
289
 // a single user's DMs, or a DM conversation)
206
 type bufferSequence struct {
290
 type bufferSequence struct {
223
 	}
307
 	}
224
 }
308
 }
225
 
309
 
226
-func (seq *bufferSequence) Between(start, end Selector, limit int) (results []Item, complete bool, err error) {
227
-	return seq.list.betweenHelper(start, end, seq.cutoff, seq.pred, limit)
310
+func (seq *bufferSequence) Between(start, end Selector, limit int) (results []Item, err error) {
311
+	results, _, err = seq.list.betweenHelper(start, end, seq.cutoff, seq.pred, limit)
312
+	return
228
 }
313
 }
229
 
314
 
230
 func (seq *bufferSequence) Around(start Selector, limit int) (results []Item, err error) {
315
 func (seq *bufferSequence) Around(start Selector, limit int) (results []Item, err error) {
231
 	return GenericAround(seq, start, limit)
316
 	return GenericAround(seq, start, limit)
232
 }
317
 }
233
 
318
 
319
+func (seq *bufferSequence) ListCorrespondents(start, end Selector, limit int) (results []CorrespondentListing, err error) {
320
+	return seq.list.listCorrespondents(start, end, seq.cutoff, limit)
321
+}
322
+
234
 // you must be holding the read lock to call this
323
 // you must be holding the read lock to call this
235
 func (list *Buffer) matchInternal(predicate Predicate, ascending bool, limit int) (results []Item) {
324
 func (list *Buffer) matchInternal(predicate Predicate, ascending bool, limit int) (results []Item) {
236
 	if list.start == -1 || len(list.buffer) == 0 {
325
 	if list.start == -1 || len(list.buffer) == 0 {

+ 5
- 3
irc/history/queries.go 파일 보기

17
 // it encapsulates restrictions such as registration time cutoffs, or
17
 // it encapsulates restrictions such as registration time cutoffs, or
18
 // only looking at a single "query buffer" (DMs with a particular correspondent)
18
 // only looking at a single "query buffer" (DMs with a particular correspondent)
19
 type Sequence interface {
19
 type Sequence interface {
20
-	Between(start, end Selector, limit int) (results []Item, complete bool, err error)
20
+	Between(start, end Selector, limit int) (results []Item, err error)
21
 	Around(start Selector, limit int) (results []Item, err error)
21
 	Around(start Selector, limit int) (results []Item, err error)
22
+
23
+	ListCorrespondents(start, end Selector, limit int) (results []CorrespondentListing, err error)
22
 }
24
 }
23
 
25
 
24
 // This is a bad, slow implementation of CHATHISTORY AROUND using the BETWEEN semantics
26
 // This is a bad, slow implementation of CHATHISTORY AROUND using the BETWEEN semantics
25
 func GenericAround(seq Sequence, start Selector, limit int) (results []Item, err error) {
27
 func GenericAround(seq Sequence, start Selector, limit int) (results []Item, err error) {
26
 	var halfLimit int
28
 	var halfLimit int
27
 	halfLimit = (limit + 1) / 2
29
 	halfLimit = (limit + 1) / 2
28
-	initialResults, _, err := seq.Between(Selector{}, start, halfLimit)
30
+	initialResults, err := seq.Between(Selector{}, start, halfLimit)
29
 	if err != nil {
31
 	if err != nil {
30
 		return
32
 		return
31
 	} else if len(initialResults) == 0 {
33
 	} else if len(initialResults) == 0 {
34
 		return
36
 		return
35
 	}
37
 	}
36
 	newStart := Selector{Time: initialResults[0].Message.Time}
38
 	newStart := Selector{Time: initialResults[0].Message.Time}
37
-	results, _, err = seq.Between(newStart, Selector{}, limit)
39
+	results, err = seq.Between(newStart, Selector{}, limit)
38
 	return
40
 	return
39
 }
41
 }
40
 
42
 

+ 2
- 2
irc/histserv.go 파일 보기

238
 	}
238
 	}
239
 
239
 
240
 	if duration == 0 {
240
 	if duration == 0 {
241
-		items, _, err = sequence.Between(history.Selector{}, history.Selector{}, limit)
241
+		items, err = sequence.Between(history.Selector{}, history.Selector{}, limit)
242
 	} else {
242
 	} else {
243
 		now := time.Now().UTC()
243
 		now := time.Now().UTC()
244
 		start := history.Selector{Time: now}
244
 		start := history.Selector{Time: now}
245
 		end := history.Selector{Time: now.Add(-duration)}
245
 		end := history.Selector{Time: now.Add(-duration)}
246
-		items, _, err = sequence.Between(start, end, limit)
246
+		items, err = sequence.Between(start, end, limit)
247
 	}
247
 	}
248
 	return
248
 	return
249
 }
249
 }

+ 144
- 12
irc/mysql/history.go 파일 보기

4
 package mysql
4
 package mysql
5
 
5
 
6
 import (
6
 import (
7
-	"bytes"
8
 	"context"
7
 	"context"
9
 	"database/sql"
8
 	"database/sql"
10
 	"encoding/json"
9
 	"encoding/json"
12
 	"fmt"
11
 	"fmt"
13
 	"io"
12
 	"io"
14
 	"runtime/debug"
13
 	"runtime/debug"
14
+	"strings"
15
 	"sync"
15
 	"sync"
16
 	"sync/atomic"
16
 	"sync/atomic"
17
 	"time"
17
 	"time"
36
 	keySchemaVersion = "db.version"
36
 	keySchemaVersion = "db.version"
37
 	// minor version indicates rollback-safe upgrades, i.e.,
37
 	// minor version indicates rollback-safe upgrades, i.e.,
38
 	// you can downgrade oragono and everything will work
38
 	// you can downgrade oragono and everything will work
39
-	latestDbMinorVersion  = "1"
39
+	latestDbMinorVersion  = "2"
40
 	keySchemaMinorVersion = "db.minorversion"
40
 	keySchemaMinorVersion = "db.minorversion"
41
 	cleanupRowLimit       = 50
41
 	cleanupRowLimit       = 50
42
 	cleanupPauseTime      = 10 * time.Minute
42
 	cleanupPauseTime      = 10 * time.Minute
53
 	insertHistory        *sql.Stmt
53
 	insertHistory        *sql.Stmt
54
 	insertSequence       *sql.Stmt
54
 	insertSequence       *sql.Stmt
55
 	insertConversation   *sql.Stmt
55
 	insertConversation   *sql.Stmt
56
+	insertCorrespondent  *sql.Stmt
56
 	insertAccountMessage *sql.Stmt
57
 	insertAccountMessage *sql.Stmt
57
 
58
 
58
 	stateMutex sync.Mutex
59
 	stateMutex sync.Mutex
155
 		if err != nil {
156
 		if err != nil {
156
 			return
157
 			return
157
 		}
158
 		}
159
+		err = mysql.createCorrespondentsTable()
160
+		if err != nil {
161
+			return
162
+		}
158
 		_, err = mysql.db.Exec(`insert into metadata (key_name, value) values (?, ?);`, keySchemaMinorVersion, latestDbMinorVersion)
163
 		_, err = mysql.db.Exec(`insert into metadata (key_name, value) values (?, ?);`, keySchemaMinorVersion, latestDbMinorVersion)
159
 		if err != nil {
164
 		if err != nil {
160
 			return
165
 			return
161
 		}
166
 		}
167
+	} else if err == nil && minorVersion == "1" {
168
+		// upgrade from 2.1 to 2.2: create the correspondents table
169
+		err = mysql.createCorrespondentsTable()
170
+		if err != nil {
171
+			return
172
+		}
173
+		_, err = mysql.db.Exec(`update metadata set value = ? where key_name = ?;`, latestDbMinorVersion, keySchemaMinorVersion)
174
+		if err != nil {
175
+			return
176
+		}
162
 	} else if err == nil && minorVersion != latestDbMinorVersion {
177
 	} else if err == nil && minorVersion != latestDbMinorVersion {
163
 		// TODO: if minorVersion < latestDbMinorVersion, upgrade,
178
 		// TODO: if minorVersion < latestDbMinorVersion, upgrade,
164
 		// if latestDbMinorVersion < minorVersion, ignore because backwards compatible
179
 		// if latestDbMinorVersion < minorVersion, ignore because backwards compatible
202
 		return err
217
 		return err
203
 	}
218
 	}
204
 
219
 
220
+	err = mysql.createCorrespondentsTable()
221
+	if err != nil {
222
+		return err
223
+	}
224
+
205
 	err = mysql.createComplianceTables()
225
 	err = mysql.createComplianceTables()
206
 	if err != nil {
226
 	if err != nil {
207
 		return err
227
 		return err
210
 	return nil
230
 	return nil
211
 }
231
 }
212
 
232
 
233
+func (mysql *MySQL) createCorrespondentsTable() (err error) {
234
+	_, err = mysql.db.Exec(fmt.Sprintf(`CREATE TABLE correspondents (
235
+		id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
236
+		target VARBINARY(%[1]d) NOT NULL,
237
+		correspondent VARBINARY(%[1]d) NOT NULL,
238
+		nanotime BIGINT UNSIGNED NOT NULL,
239
+		UNIQUE KEY (target, correspondent),
240
+		KEY (target, nanotime),
241
+		KEY (nanotime)
242
+	) CHARSET=ascii COLLATE=ascii_bin;`, MaxTargetLength))
243
+	return
244
+}
245
+
213
 func (mysql *MySQL) createComplianceTables() (err error) {
246
 func (mysql *MySQL) createComplianceTables() (err error) {
214
 	_, err = mysql.db.Exec(fmt.Sprintf(`CREATE TABLE account_messages (
247
 	_, err = mysql.db.Exec(fmt.Sprintf(`CREATE TABLE account_messages (
215
 		history_id BIGINT UNSIGNED NOT NULL PRIMARY KEY,
248
 		history_id BIGINT UNSIGNED NOT NULL PRIMARY KEY,
275
 
308
 
276
 	mysql.logger.Debug("mysql", fmt.Sprintf("deleting %d history rows, max age %s", len(ids), utils.NanoToTimestamp(maxNanotime)))
309
 	mysql.logger.Debug("mysql", fmt.Sprintf("deleting %d history rows, max age %s", len(ids), utils.NanoToTimestamp(maxNanotime)))
277
 
310
 
311
+	if maxNanotime != 0 {
312
+		mysql.deleteCorrespondents(ctx, maxNanotime)
313
+	}
314
+
278
 	return len(ids), mysql.deleteHistoryIDs(ctx, ids)
315
 	return len(ids), mysql.deleteHistoryIDs(ctx, ids)
279
 }
316
 }
280
 
317
 
281
 func (mysql *MySQL) deleteHistoryIDs(ctx context.Context, ids []uint64) (err error) {
318
 func (mysql *MySQL) deleteHistoryIDs(ctx context.Context, ids []uint64) (err error) {
282
 	// can't use ? binding for a variable number of arguments, build the IN clause manually
319
 	// can't use ? binding for a variable number of arguments, build the IN clause manually
283
-	var inBuf bytes.Buffer
320
+	var inBuf strings.Builder
284
 	inBuf.WriteByte('(')
321
 	inBuf.WriteByte('(')
285
 	for i, id := range ids {
322
 	for i, id := range ids {
286
 		if i != 0 {
323
 		if i != 0 {
289
 		fmt.Fprintf(&inBuf, "%d", id)
326
 		fmt.Fprintf(&inBuf, "%d", id)
290
 	}
327
 	}
291
 	inBuf.WriteRune(')')
328
 	inBuf.WriteRune(')')
329
+	inClause := inBuf.String()
292
 
330
 
293
-	_, err = mysql.db.ExecContext(ctx, fmt.Sprintf(`DELETE FROM conversations WHERE history_id in %s;`, inBuf.Bytes()))
331
+	_, err = mysql.db.ExecContext(ctx, fmt.Sprintf(`DELETE FROM conversations WHERE history_id in %s;`, inClause))
294
 	if err != nil {
332
 	if err != nil {
295
 		return
333
 		return
296
 	}
334
 	}
297
-	_, err = mysql.db.ExecContext(ctx, fmt.Sprintf(`DELETE FROM sequence WHERE history_id in %s;`, inBuf.Bytes()))
335
+	_, err = mysql.db.ExecContext(ctx, fmt.Sprintf(`DELETE FROM sequence WHERE history_id in %s;`, inClause))
298
 	if err != nil {
336
 	if err != nil {
299
 		return
337
 		return
300
 	}
338
 	}
301
 	if mysql.isTrackingAccountMessages() {
339
 	if mysql.isTrackingAccountMessages() {
302
-		_, err = mysql.db.ExecContext(ctx, fmt.Sprintf(`DELETE FROM account_messages WHERE history_id in %s;`, inBuf.Bytes()))
340
+		_, err = mysql.db.ExecContext(ctx, fmt.Sprintf(`DELETE FROM account_messages WHERE history_id in %s;`, inClause))
303
 		if err != nil {
341
 		if err != nil {
304
 			return
342
 			return
305
 		}
343
 		}
306
 	}
344
 	}
307
-	_, err = mysql.db.ExecContext(ctx, fmt.Sprintf(`DELETE FROM history WHERE id in %s;`, inBuf.Bytes()))
345
+	_, err = mysql.db.ExecContext(ctx, fmt.Sprintf(`DELETE FROM history WHERE id in %s;`, inClause))
308
 	if err != nil {
346
 	if err != nil {
309
 		return
347
 		return
310
 	}
348
 	}
351
 	return
389
 	return
352
 }
390
 }
353
 
391
 
392
+func (mysql *MySQL) deleteCorrespondents(ctx context.Context, threshold int64) {
393
+	result, err := mysql.db.ExecContext(ctx, `DELETE FROM correspondents WHERE nanotime <= (?);`, threshold)
394
+	if err != nil {
395
+		mysql.logError("error deleting correspondents", err)
396
+	} else {
397
+		count, err := result.RowsAffected()
398
+		if err != nil {
399
+			mysql.logger.Debug(fmt.Sprintf("deleted %d correspondents entries", count))
400
+		}
401
+	}
402
+}
403
+
354
 // wait for forget queue items and process them one by one
404
 // wait for forget queue items and process them one by one
355
 func (mysql *MySQL) forgetLoop() {
405
 func (mysql *MySQL) forgetLoop() {
356
 	defer func() {
406
 	defer func() {
470
 	if err != nil {
520
 	if err != nil {
471
 		return
521
 		return
472
 	}
522
 	}
523
+	mysql.insertCorrespondent, err = mysql.db.Prepare(`INSERT INTO correspondents
524
+		(target, correspondent, nanotime) VALUES (?, ?, ?)
525
+		ON DUPLICATE KEY UPDATE nanotime = GREATEST(nanotime, ?);`)
526
+	if err != nil {
527
+		return
528
+	}
473
 	mysql.insertAccountMessage, err = mysql.db.Prepare(`INSERT INTO account_messages
529
 	mysql.insertAccountMessage, err = mysql.db.Prepare(`INSERT INTO account_messages
474
 		(history_id, account) VALUES (?, ?);`)
530
 		(history_id, account) VALUES (?, ?);`)
475
 	if err != nil {
531
 	if err != nil {
557
 	return
613
 	return
558
 }
614
 }
559
 
615
 
616
+func (mysql *MySQL) insertCorrespondentsEntry(ctx context.Context, target, correspondent string, messageTime int64, historyId int64) (err error) {
617
+	_, err = mysql.insertCorrespondent.ExecContext(ctx, target, correspondent, messageTime, messageTime)
618
+	mysql.logError("could not insert conversations entry", err)
619
+	return
620
+}
621
+
560
 func (mysql *MySQL) insertBase(ctx context.Context, item history.Item) (id int64, err error) {
622
 func (mysql *MySQL) insertBase(ctx context.Context, item history.Item) (id int64, err error) {
561
 	value, err := marshalItem(&item)
623
 	value, err := marshalItem(&item)
562
 	if mysql.logError("could not marshal item", err) {
624
 	if mysql.logError("could not marshal item", err) {
621
 		if err != nil {
683
 		if err != nil {
622
 			return
684
 			return
623
 		}
685
 		}
686
+		err = mysql.insertCorrespondentsEntry(ctx, senderAccount, recipient, nanotime, id)
687
+		if err != nil {
688
+			return
689
+		}
624
 	}
690
 	}
625
 
691
 
626
 	if recipientAccount != "" && sender != recipient {
692
 	if recipientAccount != "" && sender != recipient {
632
 		if err != nil {
698
 		if err != nil {
633
 			return
699
 			return
634
 		}
700
 		}
701
+		err = mysql.insertCorrespondentsEntry(ctx, recipientAccount, sender, nanotime, id)
702
+		if err != nil {
703
+			return
704
+		}
635
 	}
705
 	}
636
 
706
 
637
 	err = mysql.insertAccountMessageEntry(ctx, id, senderAccount)
707
 	err = mysql.insertAccountMessageEntry(ctx, id, senderAccount)
804
 		direction = "DESC"
874
 		direction = "DESC"
805
 	}
875
 	}
806
 
876
 
807
-	var queryBuf bytes.Buffer
877
+	var queryBuf strings.Builder
808
 
878
 
809
 	args := make([]interface{}, 0, 6)
879
 	args := make([]interface{}, 0, 6)
810
 	fmt.Fprintf(&queryBuf,
880
 	fmt.Fprintf(&queryBuf,
835
 	return
905
 	return
836
 }
906
 }
837
 
907
 
908
+func (mysql *MySQL) listCorrespondentsInternal(ctx context.Context, target string, after, before, cutoff time.Time, limit int) (results []history.CorrespondentListing, err error) {
909
+	after, before, ascending := history.MinMaxAsc(after, before, cutoff)
910
+	direction := "ASC"
911
+	if !ascending {
912
+		direction = "DESC"
913
+	}
914
+
915
+	var queryBuf strings.Builder
916
+	args := make([]interface{}, 0, 4)
917
+	queryBuf.WriteString(`SELECT correspondents.correspondent, correspondents.nanotime from correspondents
918
+		WHERE target = ?`)
919
+	args = append(args, target)
920
+	if !after.IsZero() {
921
+		queryBuf.WriteString(" AND correspondents.nanotime > ?")
922
+		args = append(args, after.UnixNano())
923
+	}
924
+	if !before.IsZero() {
925
+		queryBuf.WriteString(" AND correspondents.nanotime < ?")
926
+		args = append(args, before.UnixNano())
927
+	}
928
+	fmt.Fprintf(&queryBuf, " ORDER BY correspondents.nanotime %s LIMIT ?;", direction)
929
+	args = append(args, limit)
930
+	query := queryBuf.String()
931
+
932
+	rows, err := mysql.db.QueryContext(ctx, query, args...)
933
+	if err != nil {
934
+		return
935
+	}
936
+	defer rows.Close()
937
+	var correspondent string
938
+	var nanotime int64
939
+	for rows.Next() {
940
+		err = rows.Scan(&correspondent, &nanotime)
941
+		if err != nil {
942
+			return
943
+		}
944
+		results = append(results, history.CorrespondentListing{
945
+			CfCorrespondent: correspondent,
946
+			Time:            time.Unix(0, nanotime),
947
+		})
948
+	}
949
+
950
+	if !ascending {
951
+		history.ReverseCorrespondents(results)
952
+	}
953
+
954
+	return
955
+}
956
+
838
 func (mysql *MySQL) Close() {
957
 func (mysql *MySQL) Close() {
839
 	// closing the database will close our prepared statements as well
958
 	// closing the database will close our prepared statements as well
840
 	if mysql.db != nil {
959
 	if mysql.db != nil {
852
 	cutoff        time.Time
971
 	cutoff        time.Time
853
 }
972
 }
854
 
973
 
855
-func (s *mySQLHistorySequence) Between(start, end history.Selector, limit int) (results []history.Item, complete bool, err error) {
974
+func (s *mySQLHistorySequence) Between(start, end history.Selector, limit int) (results []history.Item, err error) {
856
 	ctx, cancel := context.WithTimeout(context.Background(), s.mysql.getTimeout())
975
 	ctx, cancel := context.WithTimeout(context.Background(), s.mysql.getTimeout())
857
 	defer cancel()
976
 	defer cancel()
858
 
977
 
860
 	if start.Msgid != "" {
979
 	if start.Msgid != "" {
861
 		startTime, _, _, err = s.mysql.lookupMsgid(ctx, start.Msgid, false)
980
 		startTime, _, _, err = s.mysql.lookupMsgid(ctx, start.Msgid, false)
862
 		if err != nil {
981
 		if err != nil {
863
-			return nil, false, err
982
+			return nil, err
864
 		}
983
 		}
865
 	}
984
 	}
866
 	endTime := end.Time
985
 	endTime := end.Time
867
 	if end.Msgid != "" {
986
 	if end.Msgid != "" {
868
 		endTime, _, _, err = s.mysql.lookupMsgid(ctx, end.Msgid, false)
987
 		endTime, _, _, err = s.mysql.lookupMsgid(ctx, end.Msgid, false)
869
 		if err != nil {
988
 		if err != nil {
870
-			return nil, false, err
989
+			return nil, err
871
 		}
990
 		}
872
 	}
991
 	}
873
 
992
 
874
 	results, err = s.mysql.betweenTimestamps(ctx, s.target, s.correspondent, startTime, endTime, s.cutoff, limit)
993
 	results, err = s.mysql.betweenTimestamps(ctx, s.target, s.correspondent, startTime, endTime, s.cutoff, limit)
875
-	return results, (err == nil), err
994
+	return results, err
876
 }
995
 }
877
 
996
 
878
 func (s *mySQLHistorySequence) Around(start history.Selector, limit int) (results []history.Item, err error) {
997
 func (s *mySQLHistorySequence) Around(start history.Selector, limit int) (results []history.Item, err error) {
879
 	return history.GenericAround(s, start, limit)
998
 	return history.GenericAround(s, start, limit)
880
 }
999
 }
881
 
1000
 
1001
+func (seq *mySQLHistorySequence) ListCorrespondents(start, end history.Selector, limit int) (results []history.CorrespondentListing, err error) {
1002
+	ctx, cancel := context.WithTimeout(context.Background(), seq.mysql.getTimeout())
1003
+	defer cancel()
1004
+
1005
+	// TODO accept msgids here?
1006
+	startTime := start.Time
1007
+	endTime := end.Time
1008
+
1009
+	results, err = seq.mysql.listCorrespondentsInternal(ctx, seq.target, startTime, endTime, seq.cutoff, limit)
1010
+	seq.mysql.logError("could not read correspondents", err)
1011
+	return
1012
+}
1013
+
882
 func (mysql *MySQL) MakeSequence(target, correspondent string, cutoff time.Time) history.Sequence {
1014
 func (mysql *MySQL) MakeSequence(target, correspondent string, cutoff time.Time) history.Sequence {
883
 	return &mySQLHistorySequence{
1015
 	return &mySQLHistorySequence{
884
 		target:        target,
1016
 		target:        target,

+ 25
- 4
irc/znc.go 파일 보기

16
 const (
16
 const (
17
 	// #829, also see "Case 2" in the "three cases" below:
17
 	// #829, also see "Case 2" in the "three cases" below:
18
 	zncPlaybackCommandExpiration = time.Second * 30
18
 	zncPlaybackCommandExpiration = time.Second * 30
19
+
20
+	zncPrefix = "*playback!znc@znc.in"
19
 )
21
 )
20
 
22
 
21
 type zncCommandHandler func(client *Client, command string, params []string, rb *ResponseBuffer)
23
 type zncCommandHandler func(client *Client, command string, params []string, rb *ResponseBuffer)
192
 		return
194
 		return
193
 	}
195
 	}
194
 	zncMax := client.server.Config().History.ZNCMax
196
 	zncMax := client.server.Config().History.ZNCMax
195
-	items, _, err := sequence.Between(history.Selector{Time: after}, history.Selector{Time: before}, zncMax)
197
+	items, err := sequence.Between(history.Selector{Time: after}, history.Selector{Time: before}, zncMax)
196
 	if err == nil && len(items) != 0 {
198
 	if err == nil && len(items) != 0 {
197
-		client.replayPrivmsgHistory(rb, items, "", true)
199
+		client.replayPrivmsgHistory(rb, items, "")
198
 	}
200
 	}
199
 }
201
 }
200
 
202
 
209
 			client.server.logger.Error("internal", "couldn't get history sequence for ZNC list", err.Error())
211
 			client.server.logger.Error("internal", "couldn't get history sequence for ZNC list", err.Error())
210
 			continue
212
 			continue
211
 		}
213
 		}
212
-		items, _, err := sequence.Between(history.Selector{}, history.Selector{}, 1) // i.e., LATEST * 1
214
+		items, err := sequence.Between(history.Selector{}, history.Selector{}, 1) // i.e., LATEST * 1
213
 		if err != nil {
215
 		if err != nil {
214
 			client.server.logger.Error("internal", "couldn't query history for ZNC list", err.Error())
216
 			client.server.logger.Error("internal", "couldn't query history for ZNC list", err.Error())
215
 		} else if len(items) != 0 {
217
 		} else if len(items) != 0 {
216
 			stamp := timeToZncWireTime(items[0].Message.Time)
218
 			stamp := timeToZncWireTime(items[0].Message.Time)
217
-			rb.Add(nil, "*playback!znc@znc.in", "PRIVMSG", nick, fmt.Sprintf("%s 0 %s", channel.Name(), stamp))
219
+			rb.Add(nil, zncPrefix, "PRIVMSG", nick, fmt.Sprintf("%s 0 %s", channel.Name(), stamp))
218
 		}
220
 		}
219
 	}
221
 	}
222
+
223
+	_, seq, err := client.server.GetHistorySequence(nil, client, "*")
224
+	if seq == nil {
225
+		return
226
+	} else if err != nil {
227
+		client.server.logger.Error("internal", "couldn't get client history sequence for ZNC list", err.Error())
228
+		return
229
+	}
230
+	limit := client.server.Config().History.ChathistoryMax
231
+	correspondents, err := seq.ListCorrespondents(history.Selector{}, history.Selector{}, limit)
232
+	if err != nil {
233
+		client.server.logger.Error("internal", "couldn't get correspondents for ZNC list", err.Error())
234
+		return
235
+	}
236
+	for _, correspondent := range correspondents {
237
+		stamp := timeToZncWireTime(correspondent.Time)
238
+		correspondentNick := client.server.clients.UnfoldNick(correspondent.CfCorrespondent)
239
+		rb.Add(nil, zncPrefix, "PRIVMSG", nick, fmt.Sprintf("%s 0 %s", correspondentNick, stamp))
240
+	}
220
 }
241
 }

Loading…
취소
저장