Ver código fonte

Merge pull request #1606 from slingamn/listcorrespondents.2

fix #1592
tags/v2.6.0-rc1
Shivaram Lingamneni 3 anos atrás
pai
commit
b83b051632
Nenhuma conta vinculada ao e-mail do autor do commit

+ 3
- 8
irc/channel.go Ver arquivo

@@ -920,7 +920,7 @@ func (channel *Channel) autoReplayHistory(client *Client, rb *ResponseBuffer, sk
920 920
 		_, seq, _ := channel.server.GetHistorySequence(channel, client, "")
921 921
 		if seq != nil {
922 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 925
 	} else if !rb.session.HasHistoryCaps() {
926 926
 		var replayLimit int
@@ -937,7 +937,7 @@ func (channel *Channel) autoReplayHistory(client *Client, rb *ResponseBuffer, sk
937 937
 		if 0 < replayLimit {
938 938
 			_, seq, _ := channel.server.GetHistorySequence(channel, client, "")
939 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,20 +1097,15 @@ func (channel *Channel) resumeAndAnnounce(session *Session) {
1097 1097
 
1098 1098
 func (channel *Channel) replayHistoryForResume(session *Session, after time.Time, before time.Time) {
1099 1099
 	var items []history.Item
1100
-	var complete bool
1101 1100
 	afterS, beforeS := history.Selector{Time: after}, history.Selector{Time: before}
1102 1101
 	_, seq, _ := channel.server.GetHistorySequence(channel, session.client, "")
1103 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 1105
 	rb := NewResponseBuffer(session)
1107 1106
 	if len(items) != 0 {
1108 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 1109
 	rb.Send(true)
1115 1110
 }
1116 1111
 

+ 10
- 0
irc/channelmanager.go Ver arquivo

@@ -458,3 +458,13 @@ func (cm *ChannelManager) ListPurged() (result []string) {
458 458
 	sort.Strings(result)
459 459
 	return
460 460
 }
461
+
462
+func (cm *ChannelManager) UnfoldName(cfname string) (result string) {
463
+	cm.RLock()
464
+	entry := cm.chans[cfname]
465
+	cm.RUnlock()
466
+	if entry != nil && entry.channel.IsLoaded() {
467
+		return entry.channel.Name()
468
+	}
469
+	return cfname
470
+}

+ 41
- 7
irc/client.go Ver arquivo

@@ -990,7 +990,7 @@ func (session *Session) playResume() {
990 990
 	}
991 991
 	_, privmsgSeq, _ := server.GetHistorySequence(nil, client, "*")
992 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 994
 		for _, item := range privmsgs {
995 995
 			sender := server.clients.Get(NUHToNick(item.Nick))
996 996
 			if sender != nil {
@@ -1055,10 +1055,10 @@ func (session *Session) playResume() {
1055 1055
 	// replay direct PRIVSMG history
1056 1056
 	if !timestamp.IsZero() && privmsgSeq != nil {
1057 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 1059
 		if len(items) != 0 {
1060 1060
 			rb := NewResponseBuffer(session)
1061
-			client.replayPrivmsgHistory(rb, items, "", complete)
1061
+			client.replayPrivmsgHistory(rb, items, "")
1062 1062
 			rb.Send(true)
1063 1063
 		}
1064 1064
 	}
@@ -1066,7 +1066,7 @@ func (session *Session) playResume() {
1066 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 1070
 	var batchID string
1071 1071
 	details := client.Details()
1072 1072
 	nick := details.nick
@@ -1126,9 +1126,6 @@ func (client *Client) replayPrivmsgHistory(rb *ResponseBuffer, items []history.I
1126 1126
 	}
1127 1127
 
1128 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 1131
 // IdleTime returns how long this client's been idle.
@@ -1934,6 +1931,43 @@ func (client *Client) addHistoryItem(target *Client, item history.Item, details,
1934 1931
 	return nil
1935 1932
 }
1936 1933
 
1934
+func (client *Client) listTargets(start, end history.Selector, limit int) (results []history.TargetListing, err error) {
1935
+	var base, extras []history.TargetListing
1936
+	var chcfnames []string
1937
+	for _, channel := range client.Channels() {
1938
+		_, seq, err := client.server.GetHistorySequence(channel, client, "")
1939
+		if seq == nil || err != nil {
1940
+			continue
1941
+		}
1942
+		if seq.Ephemeral() {
1943
+			items, err := seq.Between(history.Selector{}, history.Selector{}, 1)
1944
+			if err == nil && len(items) != 0 {
1945
+				extras = append(extras, history.TargetListing{
1946
+					Time:   items[0].Message.Time,
1947
+					CfName: channel.NameCasefolded(),
1948
+				})
1949
+			}
1950
+		} else {
1951
+			chcfnames = append(chcfnames, channel.NameCasefolded())
1952
+		}
1953
+	}
1954
+	persistentExtras, err := client.server.historyDB.ListChannels(chcfnames)
1955
+	if err == nil && len(persistentExtras) != 0 {
1956
+		extras = append(extras, persistentExtras...)
1957
+	}
1958
+
1959
+	_, cSeq, err := client.server.GetHistorySequence(nil, client, "*")
1960
+	if err == nil && cSeq != nil {
1961
+		correspondents, err := cSeq.ListCorrespondents(start, end, limit)
1962
+		if err == nil {
1963
+			base = correspondents
1964
+		}
1965
+	}
1966
+
1967
+	results = history.MergeTargets(base, extras, start.Time, end.Time, limit)
1968
+	return results, nil
1969
+}
1970
+
1937 1971
 func (client *Client) handleRegisterTimeout() {
1938 1972
 	client.Quit(fmt.Sprintf("Registration timeout: %v", RegisterTimeout), nil)
1939 1973
 	client.destroy(nil)

+ 13
- 0
irc/client_lookup_set.go Ver arquivo

@@ -308,3 +308,16 @@ func (clients *ClientManager) FindAll(userhost string) (set ClientSet) {
308 308
 
309 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
+}

+ 37
- 20
irc/handlers.go Ver arquivo

@@ -566,27 +566,34 @@ func capHandler(server *Server, client *Client, msg ircmsg.Message, rb *Response
566 566
 // e.g., CHATHISTORY #ircv3 BETWEEN timestamp=YYYY-MM-DDThh:mm:ss.sssZ timestamp=YYYY-MM-DDThh:mm:ss.sssZ + 100
567 567
 func chathistoryHandler(server *Server, client *Client, msg ircmsg.Message, rb *ResponseBuffer) (exiting bool) {
568 568
 	var items []history.Item
569
-	unknown_command := false
570 569
 	var target string
571 570
 	var channel *Channel
572 571
 	var sequence history.Sequence
573 572
 	var err error
573
+	var listTargets bool
574
+	var targets []history.TargetListing
574 575
 	defer func() {
575 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 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 !listTargets && sequence == nil {
581 580
 			rb.Add(nil, server.name, "FAIL", "CHATHISTORY", "INVALID_TARGET", utils.SafeErrorParam(target), client.t("Messages could not be retrieved"))
582 581
 		} else if err != nil {
583 582
 			rb.Add(nil, server.name, "FAIL", "CHATHISTORY", "MESSAGE_ERROR", msg.Params[0], client.t("Messages could not be retrieved"))
584 583
 		} else {
585 584
 			// successful responses are sent as a chathistory or history batch
586
-			if channel != nil {
585
+			if listTargets {
586
+				batchID := rb.StartNestedBatch("draft/chathistory-targets")
587
+				defer rb.EndNestedBatch(batchID)
588
+				for _, target := range targets {
589
+					name := server.UnfoldName(target.CfName)
590
+					rb.Add(nil, server.name, "CHATHISTORY", "TARGETS", name,
591
+						target.Time.Format(IRCv3TimestampFormat))
592
+				}
593
+			} else if channel != nil {
587 594
 				channel.replayHistoryItems(rb, items, false)
588 595
 			} else {
589
-				client.replayPrivmsgHistory(rb, items, target, true)
596
+				client.replayPrivmsgHistory(rb, items, target)
590 597
 			}
591 598
 		}
592 599
 	}()
@@ -598,6 +605,7 @@ func chathistoryHandler(server *Server, client *Client, msg ircmsg.Message, rb *
598 605
 	}
599 606
 	preposition := strings.ToLower(msg.Params[0])
600 607
 	target = msg.Params[1]
608
+	listTargets = (preposition == "targets")
601 609
 
602 610
 	parseQueryParam := func(param string) (msgid string, timestamp time.Time, err error) {
603 611
 		if param == "*" && (preposition == "before" || preposition == "between") {
@@ -632,24 +640,25 @@ func chathistoryHandler(server *Server, client *Client, msg ircmsg.Message, rb *
632 640
 		return
633 641
 	}
634 642
 
635
-	channel, sequence, err = server.GetHistorySequence(nil, client, target)
636
-	if err != nil || sequence == nil {
637
-		return
638
-	}
639
-
640 643
 	roundUp := func(endpoint time.Time) (result time.Time) {
641 644
 		return endpoint.Truncate(time.Millisecond).Add(time.Millisecond)
642 645
 	}
643 646
 
647
+	paramPos := 2
644 648
 	var start, end history.Selector
645 649
 	var limit int
646 650
 	switch preposition {
651
+	case "targets":
652
+		// use the same selector parsing as BETWEEN,
653
+		// except that we have no target so we have one fewer parameter
654
+		paramPos = 1
655
+		fallthrough
647 656
 	case "between":
648
-		start.Msgid, start.Time, err = parseQueryParam(msg.Params[2])
657
+		start.Msgid, start.Time, err = parseQueryParam(msg.Params[paramPos])
649 658
 		if err != nil {
650 659
 			return
651 660
 		}
652
-		end.Msgid, end.Time, err = parseQueryParam(msg.Params[3])
661
+		end.Msgid, end.Time, err = parseQueryParam(msg.Params[paramPos+1])
653 662
 		if err != nil {
654 663
 			return
655 664
 		}
@@ -662,7 +671,7 @@ func chathistoryHandler(server *Server, client *Client, msg ircmsg.Message, rb *
662 671
 				end.Time = roundUp(end.Time)
663 672
 			}
664 673
 		}
665
-		limit = parseHistoryLimit(4)
674
+		limit = parseHistoryLimit(paramPos + 2)
666 675
 	case "before", "after", "around":
667 676
 		start.Msgid, start.Time, err = parseQueryParam(msg.Params[2])
668 677
 		if err != nil {
@@ -689,14 +698,22 @@ func chathistoryHandler(server *Server, client *Client, msg ircmsg.Message, rb *
689 698
 		}
690 699
 		limit = parseHistoryLimit(3)
691 700
 	default:
692
-		unknown_command = true
701
+		err = utils.ErrInvalidParams
693 702
 		return
694 703
 	}
695 704
 
696
-	if preposition == "around" {
697
-		items, err = sequence.Around(start, limit)
705
+	if listTargets {
706
+		targets, err = client.listTargets(start, end, limit)
698 707
 	} else {
699
-		items, _, err = sequence.Between(start, end, limit)
708
+		channel, sequence, err = server.GetHistorySequence(nil, client, target)
709
+		if err != nil || sequence == nil {
710
+			return
711
+		}
712
+		if preposition == "around" {
713
+			items, err = sequence.Around(start, limit)
714
+		} else {
715
+			items, err = sequence.Between(start, end, limit)
716
+		}
700 717
 	}
701 718
 	return
702 719
 }
@@ -1086,7 +1103,7 @@ func historyHandler(server *Server, client *Client, msg ircmsg.Message, rb *Resp
1086 1103
 		if channel != nil {
1087 1104
 			channel.replayHistoryItems(rb, items, false)
1088 1105
 		} else {
1089
-			client.replayPrivmsgHistory(rb, items, "", true)
1106
+			client.replayPrivmsgHistory(rb, items, "")
1090 1107
 		}
1091 1108
 	}
1092 1109
 	return false

+ 89
- 4
irc/history/history.go Ver arquivo

@@ -44,8 +44,8 @@ type Item struct {
44 44
 	// for a DM, this is the casefolded nickname of the other party (whether this is
45 45
 	// an incoming or outgoing message). this lets us emulate the "query buffer" functionality
46 46
 	// required by CHATHISTORY:
47
-	CfCorrespondent string
48
-	IsBot           bool `json:"IsBot,omitempty"`
47
+	CfCorrespondent string `json:"CfCorrespondent,omitempty"`
48
+	IsBot           bool   `json:"IsBot,omitempty"`
49 49
 }
50 50
 
51 51
 // HasMsgid tests whether a message has the message id `msgid`.
@@ -201,6 +201,78 @@ func (list *Buffer) betweenHelper(start, end Selector, cutoff time.Time, pred Pr
201 201
 	return list.matchInternal(satisfies, ascending, limit), complete, nil
202 202
 }
203 203
 
204
+// returns all correspondents, in reverse time order
205
+func (list *Buffer) allCorrespondents() (results []TargetListing) {
206
+	seen := make(utils.StringSet)
207
+
208
+	list.RLock()
209
+	defer list.RUnlock()
210
+	if list.start == -1 || len(list.buffer) == 0 {
211
+		return
212
+	}
213
+
214
+	// XXX traverse in reverse order, so we get the latest timestamp
215
+	// of any message sent to/from the correspondent
216
+	pos := list.prev(list.end)
217
+	stop := list.start
218
+
219
+	for {
220
+		if !seen.Has(list.buffer[pos].CfCorrespondent) {
221
+			seen.Add(list.buffer[pos].CfCorrespondent)
222
+			results = append(results, TargetListing{
223
+				CfName: list.buffer[pos].CfCorrespondent,
224
+				Time:   list.buffer[pos].Message.Time,
225
+			})
226
+		}
227
+
228
+		if pos == stop {
229
+			break
230
+		}
231
+		pos = list.prev(pos)
232
+	}
233
+	return
234
+}
235
+
236
+// list DM correspondents, as one input to CHATHISTORY TARGETS
237
+func (list *Buffer) listCorrespondents(start, end Selector, cutoff time.Time, limit int) (results []TargetListing, err error) {
238
+	after := start.Time
239
+	before := end.Time
240
+	after, before, ascending := MinMaxAsc(after, before, cutoff)
241
+
242
+	correspondents := list.allCorrespondents()
243
+	if len(correspondents) == 0 {
244
+		return
245
+	}
246
+
247
+	// XXX allCorrespondents returns results in reverse order,
248
+	// so if we're ascending, we actually go backwards
249
+	var i int
250
+	if ascending {
251
+		i = len(correspondents) - 1
252
+	} else {
253
+		i = 0
254
+	}
255
+
256
+	for 0 <= i && i < len(correspondents) && (limit == 0 || len(results) < limit) {
257
+		if (after.IsZero() || correspondents[i].Time.After(after)) &&
258
+			(before.IsZero() || correspondents[i].Time.Before(before)) {
259
+			results = append(results, correspondents[i])
260
+		}
261
+
262
+		if ascending {
263
+			i--
264
+		} else {
265
+			i++
266
+		}
267
+	}
268
+
269
+	if !ascending {
270
+		ReverseCorrespondents(results)
271
+	}
272
+
273
+	return
274
+}
275
+
204 276
 // implements history.Sequence, emulating a single history buffer (for a channel,
205 277
 // a single user's DMs, or a DM conversation)
206 278
 type bufferSequence struct {
@@ -223,14 +295,27 @@ func (list *Buffer) MakeSequence(correspondent string, cutoff time.Time) Sequenc
223 295
 	}
224 296
 }
225 297
 
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)
298
+func (seq *bufferSequence) Between(start, end Selector, limit int) (results []Item, err error) {
299
+	results, _, err = seq.list.betweenHelper(start, end, seq.cutoff, seq.pred, limit)
300
+	return
228 301
 }
229 302
 
230 303
 func (seq *bufferSequence) Around(start Selector, limit int) (results []Item, err error) {
231 304
 	return GenericAround(seq, start, limit)
232 305
 }
233 306
 
307
+func (seq *bufferSequence) ListCorrespondents(start, end Selector, limit int) (results []TargetListing, err error) {
308
+	return seq.list.listCorrespondents(start, end, seq.cutoff, limit)
309
+}
310
+
311
+func (seq *bufferSequence) Cutoff() time.Time {
312
+	return seq.cutoff
313
+}
314
+
315
+func (seq *bufferSequence) Ephemeral() bool {
316
+	return true
317
+}
318
+
234 319
 // you must be holding the read lock to call this
235 320
 func (list *Buffer) matchInternal(predicate Predicate, ascending bool, limit int) (results []Item) {
236 321
 	if list.start == -1 || len(list.buffer) == 0 {

+ 12
- 3
irc/history/queries.go Ver arquivo

@@ -17,15 +17,24 @@ type Selector struct {
17 17
 // it encapsulates restrictions such as registration time cutoffs, or
18 18
 // only looking at a single "query buffer" (DMs with a particular correspondent)
19 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 21
 	Around(start Selector, limit int) (results []Item, err error)
22
+
23
+	ListCorrespondents(start, end Selector, limit int) (results []TargetListing, err error)
24
+
25
+	// this are weird hacks that violate the encapsulation of Sequence to some extent;
26
+	// Cutoff() returns the cutoff time for other code to use (it returns the zero time
27
+	// if none is set), and Ephemeral() returns whether the backing store is in-memory
28
+	// or a persistent database.
29
+	Cutoff() time.Time
30
+	Ephemeral() bool
22 31
 }
23 32
 
24 33
 // This is a bad, slow implementation of CHATHISTORY AROUND using the BETWEEN semantics
25 34
 func GenericAround(seq Sequence, start Selector, limit int) (results []Item, err error) {
26 35
 	var halfLimit int
27 36
 	halfLimit = (limit + 1) / 2
28
-	initialResults, _, err := seq.Between(Selector{}, start, halfLimit)
37
+	initialResults, err := seq.Between(Selector{}, start, halfLimit)
29 38
 	if err != nil {
30 39
 		return
31 40
 	} else if len(initialResults) == 0 {
@@ -34,7 +43,7 @@ func GenericAround(seq Sequence, start Selector, limit int) (results []Item, err
34 43
 		return
35 44
 	}
36 45
 	newStart := Selector{Time: initialResults[0].Message.Time}
37
-	results, _, err = seq.Between(newStart, Selector{}, limit)
46
+	results, err = seq.Between(newStart, Selector{}, limit)
38 47
 	return
39 48
 }
40 49
 

+ 83
- 0
irc/history/targets.go Ver arquivo

@@ -0,0 +1,83 @@
1
+// Copyright (c) 2021 Shivaram Lingamneni <slingamn@cs.stanford.edu>
2
+// released under the MIT license
3
+
4
+package history
5
+
6
+import (
7
+	"sort"
8
+	"time"
9
+)
10
+
11
+type TargetListing struct {
12
+	CfName string
13
+	Time   time.Time
14
+}
15
+
16
+// Merge `base`, a paging window of targets, with `extras` (the target entries
17
+// for all joined channels).
18
+func MergeTargets(base []TargetListing, extra []TargetListing, start, end time.Time, limit int) (results []TargetListing) {
19
+	if len(extra) == 0 {
20
+		return base
21
+	}
22
+	SortCorrespondents(extra)
23
+
24
+	start, end, ascending := MinMaxAsc(start, end, time.Time{})
25
+	predicate := func(t time.Time) bool {
26
+		return (start.IsZero() || start.Before(t)) && (end.IsZero() || end.After(t))
27
+	}
28
+
29
+	prealloc := len(base) + len(extra)
30
+	if limit < prealloc {
31
+		prealloc = limit
32
+	}
33
+	results = make([]TargetListing, 0, prealloc)
34
+
35
+	if !ascending {
36
+		ReverseCorrespondents(base)
37
+		ReverseCorrespondents(extra)
38
+	}
39
+
40
+	for len(results) < limit {
41
+		if len(extra) != 0 {
42
+			if !predicate(extra[0].Time) {
43
+				extra = extra[1:]
44
+				continue
45
+			}
46
+			if len(base) != 0 {
47
+				if base[0].Time.Before(extra[0].Time) == ascending {
48
+					results = append(results, base[0])
49
+					base = base[1:]
50
+				} else {
51
+					results = append(results, extra[0])
52
+					extra = extra[1:]
53
+				}
54
+			} else {
55
+				results = append(results, extra[0])
56
+				extra = extra[1:]
57
+			}
58
+		} else if len(base) != 0 {
59
+			results = append(results, base[0])
60
+			base = base[1:]
61
+		} else {
62
+			break
63
+		}
64
+	}
65
+
66
+	if !ascending {
67
+		ReverseCorrespondents(results)
68
+	}
69
+	return
70
+}
71
+
72
+func ReverseCorrespondents(results []TargetListing) {
73
+	// lol, generics when?
74
+	for i, j := 0, len(results)-1; i < j; i, j = i+1, j-1 {
75
+		results[i], results[j] = results[j], results[i]
76
+	}
77
+}
78
+
79
+func SortCorrespondents(list []TargetListing) {
80
+	sort.Slice(list, func(i, j int) bool {
81
+		return list[i].Time.Before(list[j].Time)
82
+	})
83
+}

+ 2
- 2
irc/histserv.go Ver arquivo

@@ -238,12 +238,12 @@ func easySelectHistory(server *Server, client *Client, params []string) (items [
238 238
 	}
239 239
 
240 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 242
 	} else {
243 243
 		now := time.Now().UTC()
244 244
 		start := history.Selector{Time: now}
245 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 248
 	return
249 249
 }

+ 200
- 12
irc/mysql/history.go Ver arquivo

@@ -4,7 +4,6 @@
4 4
 package mysql
5 5
 
6 6
 import (
7
-	"bytes"
8 7
 	"context"
9 8
 	"database/sql"
10 9
 	"encoding/json"
@@ -12,6 +11,7 @@ import (
12 11
 	"fmt"
13 12
 	"io"
14 13
 	"runtime/debug"
14
+	"strings"
15 15
 	"sync"
16 16
 	"sync/atomic"
17 17
 	"time"
@@ -36,7 +36,7 @@ const (
36 36
 	keySchemaVersion = "db.version"
37 37
 	// minor version indicates rollback-safe upgrades, i.e.,
38 38
 	// you can downgrade oragono and everything will work
39
-	latestDbMinorVersion  = "1"
39
+	latestDbMinorVersion  = "2"
40 40
 	keySchemaMinorVersion = "db.minorversion"
41 41
 	cleanupRowLimit       = 50
42 42
 	cleanupPauseTime      = 10 * time.Minute
@@ -53,6 +53,7 @@ type MySQL struct {
53 53
 	insertHistory        *sql.Stmt
54 54
 	insertSequence       *sql.Stmt
55 55
 	insertConversation   *sql.Stmt
56
+	insertCorrespondent  *sql.Stmt
56 57
 	insertAccountMessage *sql.Stmt
57 58
 
58 59
 	stateMutex sync.Mutex
@@ -155,10 +156,24 @@ func (mysql *MySQL) fixSchemas() (err error) {
155 156
 		if err != nil {
156 157
 			return
157 158
 		}
159
+		err = mysql.createCorrespondentsTable()
160
+		if err != nil {
161
+			return
162
+		}
158 163
 		_, err = mysql.db.Exec(`insert into metadata (key_name, value) values (?, ?);`, keySchemaMinorVersion, latestDbMinorVersion)
159 164
 		if err != nil {
160 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 177
 	} else if err == nil && minorVersion != latestDbMinorVersion {
163 178
 		// TODO: if minorVersion < latestDbMinorVersion, upgrade,
164 179
 		// if latestDbMinorVersion < minorVersion, ignore because backwards compatible
@@ -202,6 +217,11 @@ func (mysql *MySQL) createTables() (err error) {
202 217
 		return err
203 218
 	}
204 219
 
220
+	err = mysql.createCorrespondentsTable()
221
+	if err != nil {
222
+		return err
223
+	}
224
+
205 225
 	err = mysql.createComplianceTables()
206 226
 	if err != nil {
207 227
 		return err
@@ -210,6 +230,19 @@ func (mysql *MySQL) createTables() (err error) {
210 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 246
 func (mysql *MySQL) createComplianceTables() (err error) {
214 247
 	_, err = mysql.db.Exec(fmt.Sprintf(`CREATE TABLE account_messages (
215 248
 		history_id BIGINT UNSIGNED NOT NULL PRIMARY KEY,
@@ -275,12 +308,16 @@ func (mysql *MySQL) doCleanup(age time.Duration) (count int, err error) {
275 308
 
276 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 315
 	return len(ids), mysql.deleteHistoryIDs(ctx, ids)
279 316
 }
280 317
 
281 318
 func (mysql *MySQL) deleteHistoryIDs(ctx context.Context, ids []uint64) (err error) {
282 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 321
 	inBuf.WriteByte('(')
285 322
 	for i, id := range ids {
286 323
 		if i != 0 {
@@ -289,22 +326,23 @@ func (mysql *MySQL) deleteHistoryIDs(ctx context.Context, ids []uint64) (err err
289 326
 		fmt.Fprintf(&inBuf, "%d", id)
290 327
 	}
291 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 332
 	if err != nil {
295 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 336
 	if err != nil {
299 337
 		return
300 338
 	}
301 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 341
 		if err != nil {
304 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 346
 	if err != nil {
309 347
 		return
310 348
 	}
@@ -351,6 +389,18 @@ func (mysql *MySQL) selectCleanupIDs(ctx context.Context, age time.Duration) (id
351 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 404
 // wait for forget queue items and process them one by one
355 405
 func (mysql *MySQL) forgetLoop() {
356 406
 	defer func() {
@@ -470,6 +520,12 @@ func (mysql *MySQL) prepareStatements() (err error) {
470 520
 	if err != nil {
471 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 529
 	mysql.insertAccountMessage, err = mysql.db.Prepare(`INSERT INTO account_messages
474 530
 		(history_id, account) VALUES (?, ?);`)
475 531
 	if err != nil {
@@ -557,6 +613,12 @@ func (mysql *MySQL) insertConversationEntry(ctx context.Context, target, corresp
557 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 622
 func (mysql *MySQL) insertBase(ctx context.Context, item history.Item) (id int64, err error) {
561 623
 	value, err := marshalItem(&item)
562 624
 	if mysql.logError("could not marshal item", err) {
@@ -621,6 +683,10 @@ func (mysql *MySQL) AddDirectMessage(sender, senderAccount, recipient, recipient
621 683
 		if err != nil {
622 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 692
 	if recipientAccount != "" && sender != recipient {
@@ -632,6 +698,10 @@ func (mysql *MySQL) AddDirectMessage(sender, senderAccount, recipient, recipient
632 698
 		if err != nil {
633 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 707
 	err = mysql.insertAccountMessageEntry(ctx, id, senderAccount)
@@ -804,7 +874,7 @@ func (mysql *MySQL) betweenTimestamps(ctx context.Context, target, correspondent
804 874
 		direction = "DESC"
805 875
 	}
806 876
 
807
-	var queryBuf bytes.Buffer
877
+	var queryBuf strings.Builder
808 878
 
809 879
 	args := make([]interface{}, 0, 6)
810 880
 	fmt.Fprintf(&queryBuf,
@@ -835,6 +905,103 @@ func (mysql *MySQL) betweenTimestamps(ctx context.Context, target, correspondent
835 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.TargetListing, 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.TargetListing{
945
+			CfName: correspondent,
946
+			Time:   time.Unix(0, nanotime),
947
+		})
948
+	}
949
+
950
+	if !ascending {
951
+		history.ReverseCorrespondents(results)
952
+	}
953
+
954
+	return
955
+}
956
+
957
+func (mysql *MySQL) ListChannels(cfchannels []string) (results []history.TargetListing, err error) {
958
+	if mysql.db == nil {
959
+		return
960
+	}
961
+
962
+	if len(cfchannels) == 0 {
963
+		return
964
+	}
965
+
966
+	ctx, cancel := context.WithTimeout(context.Background(), mysql.getTimeout())
967
+	defer cancel()
968
+
969
+	var queryBuf strings.Builder
970
+	args := make([]interface{}, 0, len(results))
971
+	// https://dev.mysql.com/doc/refman/8.0/en/group-by-optimization.html
972
+	// this should be a "loose index scan"
973
+	queryBuf.WriteString(`SELECT sequence.target, MAX(sequence.nanotime) FROM sequence
974
+		WHERE sequence.target IN (`)
975
+	for i, chname := range cfchannels {
976
+		if i != 0 {
977
+			queryBuf.WriteString(", ")
978
+		}
979
+		queryBuf.WriteByte('?')
980
+		args = append(args, chname)
981
+	}
982
+	queryBuf.WriteString(") GROUP BY sequence.target;")
983
+
984
+	rows, err := mysql.db.QueryContext(ctx, queryBuf.String(), args...)
985
+	if mysql.logError("could not query channel listings", err) {
986
+		return
987
+	}
988
+	defer rows.Close()
989
+
990
+	var target string
991
+	var nanotime int64
992
+	for rows.Next() {
993
+		err = rows.Scan(&target, &nanotime)
994
+		if mysql.logError("could not scan channel listings", err) {
995
+			return
996
+		}
997
+		results = append(results, history.TargetListing{
998
+			CfName: target,
999
+			Time:   time.Unix(0, nanotime),
1000
+		})
1001
+	}
1002
+	return
1003
+}
1004
+
838 1005
 func (mysql *MySQL) Close() {
839 1006
 	// closing the database will close our prepared statements as well
840 1007
 	if mysql.db != nil {
@@ -852,7 +1019,7 @@ type mySQLHistorySequence struct {
852 1019
 	cutoff        time.Time
853 1020
 }
854 1021
 
855
-func (s *mySQLHistorySequence) Between(start, end history.Selector, limit int) (results []history.Item, complete bool, err error) {
1022
+func (s *mySQLHistorySequence) Between(start, end history.Selector, limit int) (results []history.Item, err error) {
856 1023
 	ctx, cancel := context.WithTimeout(context.Background(), s.mysql.getTimeout())
857 1024
 	defer cancel()
858 1025
 
@@ -860,25 +1027,46 @@ func (s *mySQLHistorySequence) Between(start, end history.Selector, limit int) (
860 1027
 	if start.Msgid != "" {
861 1028
 		startTime, _, _, err = s.mysql.lookupMsgid(ctx, start.Msgid, false)
862 1029
 		if err != nil {
863
-			return nil, false, err
1030
+			return nil, err
864 1031
 		}
865 1032
 	}
866 1033
 	endTime := end.Time
867 1034
 	if end.Msgid != "" {
868 1035
 		endTime, _, _, err = s.mysql.lookupMsgid(ctx, end.Msgid, false)
869 1036
 		if err != nil {
870
-			return nil, false, err
1037
+			return nil, err
871 1038
 		}
872 1039
 	}
873 1040
 
874 1041
 	results, err = s.mysql.betweenTimestamps(ctx, s.target, s.correspondent, startTime, endTime, s.cutoff, limit)
875
-	return results, (err == nil), err
1042
+	return results, err
876 1043
 }
877 1044
 
878 1045
 func (s *mySQLHistorySequence) Around(start history.Selector, limit int) (results []history.Item, err error) {
879 1046
 	return history.GenericAround(s, start, limit)
880 1047
 }
881 1048
 
1049
+func (seq *mySQLHistorySequence) ListCorrespondents(start, end history.Selector, limit int) (results []history.TargetListing, err error) {
1050
+	ctx, cancel := context.WithTimeout(context.Background(), seq.mysql.getTimeout())
1051
+	defer cancel()
1052
+
1053
+	// TODO accept msgids here?
1054
+	startTime := start.Time
1055
+	endTime := end.Time
1056
+
1057
+	results, err = seq.mysql.listCorrespondentsInternal(ctx, seq.target, startTime, endTime, seq.cutoff, limit)
1058
+	seq.mysql.logError("could not read correspondents", err)
1059
+	return
1060
+}
1061
+
1062
+func (seq *mySQLHistorySequence) Cutoff() time.Time {
1063
+	return seq.cutoff
1064
+}
1065
+
1066
+func (seq *mySQLHistorySequence) Ephemeral() bool {
1067
+	return false
1068
+}
1069
+
882 1070
 func (mysql *MySQL) MakeSequence(target, correspondent string, cutoff time.Time) history.Sequence {
883 1071
 	return &mySQLHistorySequence{
884 1072
 		target:        target,

+ 7
- 0
irc/server.go Ver arquivo

@@ -1017,6 +1017,13 @@ func (server *Server) DeleteMessage(target, msgid, accountName string) (err erro
1017 1017
 	return
1018 1018
 }
1019 1019
 
1020
+func (server *Server) UnfoldName(cfname string) (name string) {
1021
+	if strings.HasPrefix(cfname, "#") {
1022
+		return server.channels.UnfoldName(cfname)
1023
+	}
1024
+	return server.clients.UnfoldNick(cfname)
1025
+}
1026
+
1020 1027
 // elistMatcher takes and matches ELIST conditions
1021 1028
 type elistMatcher struct {
1022 1029
 	MinClientsActive bool

+ 14
- 17
irc/znc.go Ver arquivo

@@ -16,6 +16,8 @@ import (
16 16
 const (
17 17
 	// #829, also see "Case 2" in the "three cases" below:
18 18
 	zncPlaybackCommandExpiration = time.Second * 30
19
+
20
+	zncPrefix = "*playback!znc@znc.in"
19 21
 )
20 22
 
21 23
 type zncCommandHandler func(client *Client, command string, params []string, rb *ResponseBuffer)
@@ -192,29 +194,24 @@ func zncPlayPrivmsgs(client *Client, rb *ResponseBuffer, target string, after, b
192 194
 		return
193 195
 	}
194 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 198
 	if err == nil && len(items) != 0 {
197
-		client.replayPrivmsgHistory(rb, items, "", true)
199
+		client.replayPrivmsgHistory(rb, items, "")
198 200
 	}
199 201
 }
200 202
 
201 203
 // PRIVMSG *playback :list
202 204
 func zncPlaybackListHandler(client *Client, command string, params []string, rb *ResponseBuffer) {
205
+	limit := client.server.Config().History.ChathistoryMax
206
+	correspondents, err := client.listTargets(history.Selector{}, history.Selector{}, limit)
207
+	if err != nil {
208
+		client.server.logger.Error("internal", "couldn't get history for ZNC list", err.Error())
209
+		return
210
+	}
203 211
 	nick := client.Nick()
204
-	for _, channel := range client.Channels() {
205
-		_, sequence, err := client.server.GetHistorySequence(channel, client, "")
206
-		if sequence == nil {
207
-			continue
208
-		} else if err != nil {
209
-			client.server.logger.Error("internal", "couldn't get history sequence for ZNC list", err.Error())
210
-			continue
211
-		}
212
-		items, _, err := sequence.Between(history.Selector{}, history.Selector{}, 1) // i.e., LATEST * 1
213
-		if err != nil {
214
-			client.server.logger.Error("internal", "couldn't query history for ZNC list", err.Error())
215
-		} else if len(items) != 0 {
216
-			stamp := timeToZncWireTime(items[0].Message.Time)
217
-			rb.Add(nil, "*playback!znc@znc.in", "PRIVMSG", nick, fmt.Sprintf("%s 0 %s", channel.Name(), stamp))
218
-		}
212
+	for _, correspondent := range correspondents {
213
+		stamp := timeToZncWireTime(correspondent.Time)
214
+		unfoldedTarget := client.server.UnfoldName(correspondent.CfName)
215
+		rb.Add(nil, zncPrefix, "PRIVMSG", nick, fmt.Sprintf("%s 0 %s", unfoldedTarget, stamp))
219 216
 	}
220 217
 }

Carregando…
Cancelar
Salvar