Просмотр исходного кода

implement CHATHISTORY TARGETS

tags/v2.6.0-rc1
Shivaram Lingamneni 3 лет назад
Родитель
Сommit
18b6e2f1cd
9 измененных файлов: 248 добавлений и 78 удалений
  1. 10
    0
      irc/channelmanager.go
  2. 37
    0
      irc/client.go
  3. 22
    24
      irc/handlers.go
  4. 15
    19
      irc/history/history.go
  5. 8
    1
      irc/history/queries.go
  6. 83
    0
      irc/history/targets.go
  7. 61
    5
      irc/mysql/history.go
  8. 7
    0
      irc/server.go
  9. 5
    29
      irc/znc.go

+ 10
- 0
irc/channelmanager.go Просмотреть файл

@@ -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
+}

+ 37
- 0
irc/client.go Просмотреть файл

@@ -1931,6 +1931,43 @@ func (client *Client) addHistoryItem(target *Client, item history.Item, details,
1931 1931
 	return nil
1932 1932
 }
1933 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
+
1934 1971
 func (client *Client) handleRegisterTimeout() {
1935 1972
 	client.Quit(fmt.Sprintf("Registration timeout: %v", RegisterTimeout), nil)
1936 1973
 	client.destroy(nil)

+ 22
- 24
irc/handlers.go Просмотреть файл

@@ -570,25 +570,25 @@ func chathistoryHandler(server *Server, client *Client, msg ircmsg.Message, rb *
570 570
 	var channel *Channel
571 571
 	var sequence history.Sequence
572 572
 	var err error
573
-	var listCorrespondents bool
574
-	var correspondents []history.CorrespondentListing
573
+	var listTargets bool
574
+	var targets []history.TargetListing
575 575
 	defer func() {
576 576
 		// errors are sent either without a batch, or in a draft/labeled-response batch as usual
577 577
 		if err == utils.ErrInvalidParams {
578 578
 			rb.Add(nil, server.name, "FAIL", "CHATHISTORY", "INVALID_PARAMS", msg.Params[0], client.t("Invalid parameters"))
579
-		} else if sequence == nil {
579
+		} else if !listTargets && sequence == nil {
580 580
 			rb.Add(nil, server.name, "FAIL", "CHATHISTORY", "INVALID_TARGET", utils.SafeErrorParam(target), client.t("Messages could not be retrieved"))
581 581
 		} else if err != nil {
582 582
 			rb.Add(nil, server.name, "FAIL", "CHATHISTORY", "MESSAGE_ERROR", msg.Params[0], client.t("Messages could not be retrieved"))
583 583
 		} else {
584 584
 			// successful responses are sent as a chathistory or history batch
585
-			if listCorrespondents {
586
-				batchID := rb.StartNestedBatch("draft/chathistory-listcorrespondents")
585
+			if listTargets {
586
+				batchID := rb.StartNestedBatch("draft/chathistory-targets")
587 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))
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 592
 				}
593 593
 			} else if channel != nil {
594 594
 				channel.replayHistoryItems(rb, items, false)
@@ -605,9 +605,7 @@ func chathistoryHandler(server *Server, client *Client, msg ircmsg.Message, rb *
605 605
 	}
606 606
 	preposition := strings.ToLower(msg.Params[0])
607 607
 	target = msg.Params[1]
608
-	if preposition == "listcorrespondents" {
609
-		target = "*"
610
-	}
608
+	listTargets = (preposition == "targets")
611 609
 
612 610
 	parseQueryParam := func(param string) (msgid string, timestamp time.Time, err error) {
613 611
 		if param == "*" && (preposition == "before" || preposition == "between") {
@@ -642,11 +640,6 @@ func chathistoryHandler(server *Server, client *Client, msg ircmsg.Message, rb *
642 640
 		return
643 641
 	}
644 642
 
645
-	channel, sequence, err = server.GetHistorySequence(nil, client, target)
646
-	if err != nil || sequence == nil {
647
-		return
648
-	}
649
-
650 643
 	roundUp := func(endpoint time.Time) (result time.Time) {
651 644
 		return endpoint.Truncate(time.Millisecond).Add(time.Millisecond)
652 645
 	}
@@ -655,8 +648,7 @@ func chathistoryHandler(server *Server, client *Client, msg ircmsg.Message, rb *
655 648
 	var start, end history.Selector
656 649
 	var limit int
657 650
 	switch preposition {
658
-	case "listcorrespondents":
659
-		listCorrespondents = true
651
+	case "targets":
660 652
 		// use the same selector parsing as BETWEEN,
661 653
 		// except that we have no target so we have one fewer parameter
662 654
 		paramPos = 1
@@ -710,12 +702,18 @@ func chathistoryHandler(server *Server, client *Client, msg ircmsg.Message, rb *
710 702
 		return
711 703
 	}
712 704
 
713
-	if listCorrespondents {
714
-		correspondents, err = sequence.ListCorrespondents(start, end, limit)
715
-	} else if preposition == "around" {
716
-		items, err = sequence.Around(start, limit)
705
+	if listTargets {
706
+		targets, err = client.listTargets(start, end, limit)
717 707
 	} else {
718
-		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
+		}
719 717
 	}
720 718
 	return
721 719
 }

+ 15
- 19
irc/history/history.go Просмотреть файл

@@ -48,11 +48,6 @@ type Item struct {
48 48
 	IsBot           bool   `json:"IsBot,omitempty"`
49 49
 }
50 50
 
51
-type CorrespondentListing struct {
52
-	CfCorrespondent string
53
-	Time            time.Time
54
-}
55
-
56 51
 // HasMsgid tests whether a message has the message id `msgid`.
57 52
 func (item *Item) HasMsgid(msgid string) bool {
58 53
 	return item.Message.Msgid == msgid
@@ -66,13 +61,6 @@ func Reverse(results []Item) {
66 61
 	}
67 62
 }
68 63
 
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
-
76 64
 // Buffer is a ring buffer holding message/event history for a channel or user
77 65
 type Buffer struct {
78 66
 	sync.RWMutex
@@ -214,7 +202,7 @@ func (list *Buffer) betweenHelper(start, end Selector, cutoff time.Time, pred Pr
214 202
 }
215 203
 
216 204
 // returns all correspondents, in reverse time order
217
-func (list *Buffer) allCorrespondents() (results []CorrespondentListing) {
205
+func (list *Buffer) allCorrespondents() (results []TargetListing) {
218 206
 	seen := make(utils.StringSet)
219 207
 
220 208
 	list.RLock()
@@ -231,9 +219,9 @@ func (list *Buffer) allCorrespondents() (results []CorrespondentListing) {
231 219
 	for {
232 220
 		if !seen.Has(list.buffer[pos].CfCorrespondent) {
233 221
 			seen.Add(list.buffer[pos].CfCorrespondent)
234
-			results = append(results, CorrespondentListing{
235
-				CfCorrespondent: list.buffer[pos].CfCorrespondent,
236
-				Time:            list.buffer[pos].Message.Time,
222
+			results = append(results, TargetListing{
223
+				CfName: list.buffer[pos].CfCorrespondent,
224
+				Time:   list.buffer[pos].Message.Time,
237 225
 			})
238 226
 		}
239 227
 
@@ -245,8 +233,8 @@ func (list *Buffer) allCorrespondents() (results []CorrespondentListing) {
245 233
 	return
246 234
 }
247 235
 
248
-// implement LISTCORRESPONDENTS
249
-func (list *Buffer) listCorrespondents(start, end Selector, cutoff time.Time, limit int) (results []CorrespondentListing, err error) {
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) {
250 238
 	after := start.Time
251 239
 	before := end.Time
252 240
 	after, before, ascending := MinMaxAsc(after, before, cutoff)
@@ -316,10 +304,18 @@ func (seq *bufferSequence) Around(start Selector, limit int) (results []Item, er
316 304
 	return GenericAround(seq, start, limit)
317 305
 }
318 306
 
319
-func (seq *bufferSequence) ListCorrespondents(start, end Selector, limit int) (results []CorrespondentListing, err error) {
307
+func (seq *bufferSequence) ListCorrespondents(start, end Selector, limit int) (results []TargetListing, err error) {
320 308
 	return seq.list.listCorrespondents(start, end, seq.cutoff, limit)
321 309
 }
322 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
+
323 319
 // you must be holding the read lock to call this
324 320
 func (list *Buffer) matchInternal(predicate Predicate, ascending bool, limit int) (results []Item) {
325 321
 	if list.start == -1 || len(list.buffer) == 0 {

+ 8
- 1
irc/history/queries.go Просмотреть файл

@@ -20,7 +20,14 @@ type Sequence interface {
20 20
 	Between(start, end Selector, limit int) (results []Item, err error)
21 21
 	Around(start Selector, limit int) (results []Item, err error)
22 22
 
23
-	ListCorrespondents(start, end Selector, limit int) (results []CorrespondentListing, err error)
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
24 31
 }
25 32
 
26 33
 // This is a bad, slow implementation of CHATHISTORY AROUND using the BETWEEN semantics

+ 83
- 0
irc/history/targets.go Просмотреть файл

@@ -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
+}

+ 61
- 5
irc/mysql/history.go Просмотреть файл

@@ -905,7 +905,7 @@ func (mysql *MySQL) betweenTimestamps(ctx context.Context, target, correspondent
905 905
 	return
906 906
 }
907 907
 
908
-func (mysql *MySQL) listCorrespondentsInternal(ctx context.Context, target string, after, before, cutoff time.Time, limit int) (results []history.CorrespondentListing, err error) {
908
+func (mysql *MySQL) listCorrespondentsInternal(ctx context.Context, target string, after, before, cutoff time.Time, limit int) (results []history.TargetListing, err error) {
909 909
 	after, before, ascending := history.MinMaxAsc(after, before, cutoff)
910 910
 	direction := "ASC"
911 911
 	if !ascending {
@@ -941,9 +941,9 @@ func (mysql *MySQL) listCorrespondentsInternal(ctx context.Context, target strin
941 941
 		if err != nil {
942 942
 			return
943 943
 		}
944
-		results = append(results, history.CorrespondentListing{
945
-			CfCorrespondent: correspondent,
946
-			Time:            time.Unix(0, nanotime),
944
+		results = append(results, history.TargetListing{
945
+			CfName: correspondent,
946
+			Time:   time.Unix(0, nanotime),
947 947
 		})
948 948
 	}
949 949
 
@@ -954,6 +954,54 @@ func (mysql *MySQL) listCorrespondentsInternal(ctx context.Context, target strin
954 954
 	return
955 955
 }
956 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
+
957 1005
 func (mysql *MySQL) Close() {
958 1006
 	// closing the database will close our prepared statements as well
959 1007
 	if mysql.db != nil {
@@ -998,7 +1046,7 @@ func (s *mySQLHistorySequence) Around(start history.Selector, limit int) (result
998 1046
 	return history.GenericAround(s, start, limit)
999 1047
 }
1000 1048
 
1001
-func (seq *mySQLHistorySequence) ListCorrespondents(start, end history.Selector, limit int) (results []history.CorrespondentListing, err error) {
1049
+func (seq *mySQLHistorySequence) ListCorrespondents(start, end history.Selector, limit int) (results []history.TargetListing, err error) {
1002 1050
 	ctx, cancel := context.WithTimeout(context.Background(), seq.mysql.getTimeout())
1003 1051
 	defer cancel()
1004 1052
 
@@ -1011,6 +1059,14 @@ func (seq *mySQLHistorySequence) ListCorrespondents(start, end history.Selector,
1011 1059
 	return
1012 1060
 }
1013 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
+
1014 1070
 func (mysql *MySQL) MakeSequence(target, correspondent string, cutoff time.Time) history.Sequence {
1015 1071
 	return &mySQLHistorySequence{
1016 1072
 		target:        target,

+ 7
- 0
irc/server.go Просмотреть файл

@@ -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

+ 5
- 29
irc/znc.go Просмотреть файл

@@ -202,40 +202,16 @@ func zncPlayPrivmsgs(client *Client, rb *ResponseBuffer, target string, after, b
202 202
 
203 203
 // PRIVMSG *playback :list
204 204
 func zncPlaybackListHandler(client *Client, command string, params []string, rb *ResponseBuffer) {
205
-	nick := client.Nick()
206
-	for _, channel := range client.Channels() {
207
-		_, sequence, err := client.server.GetHistorySequence(channel, client, "")
208
-		if sequence == nil {
209
-			continue
210
-		} else if err != nil {
211
-			client.server.logger.Error("internal", "couldn't get history sequence for ZNC list", err.Error())
212
-			continue
213
-		}
214
-		items, err := sequence.Between(history.Selector{}, history.Selector{}, 1) // i.e., LATEST * 1
215
-		if err != nil {
216
-			client.server.logger.Error("internal", "couldn't query history for ZNC list", err.Error())
217
-		} else if len(items) != 0 {
218
-			stamp := timeToZncWireTime(items[0].Message.Time)
219
-			rb.Add(nil, zncPrefix, "PRIVMSG", nick, fmt.Sprintf("%s 0 %s", channel.Name(), stamp))
220
-		}
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 205
 	limit := client.server.Config().History.ChathistoryMax
231
-	correspondents, err := seq.ListCorrespondents(history.Selector{}, history.Selector{}, limit)
206
+	correspondents, err := client.listTargets(history.Selector{}, history.Selector{}, limit)
232 207
 	if err != nil {
233
-		client.server.logger.Error("internal", "couldn't get correspondents for ZNC list", err.Error())
208
+		client.server.logger.Error("internal", "couldn't get history for ZNC list", err.Error())
234 209
 		return
235 210
 	}
211
+	nick := client.Nick()
236 212
 	for _, correspondent := range correspondents {
237 213
 		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))
214
+		unfoldedTarget := client.server.UnfoldName(correspondent.CfName)
215
+		rb.Add(nil, zncPrefix, "PRIVMSG", nick, fmt.Sprintf("%s 0 %s", unfoldedTarget, stamp))
240 216
 	}
241 217
 }

Загрузка…
Отмена
Сохранить