Переглянути джерело

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

+ 4
- 7
irc/client.go Переглянути файл

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

+ 13
- 0
irc/client_lookup_set.go Переглянути файл

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

+ 32
- 13
irc/handlers.go Переглянути файл

@@ -566,16 +566,15 @@ 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 listCorrespondents bool
574
+	var correspondents []history.CorrespondentListing
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 579
 		} else if sequence == nil {
581 580
 			rb.Add(nil, server.name, "FAIL", "CHATHISTORY", "INVALID_TARGET", utils.SafeErrorParam(target), client.t("Messages could not be retrieved"))
@@ -583,10 +582,18 @@ func chathistoryHandler(server *Server, client *Client, msg ircmsg.Message, rb *
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 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 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,9 @@ 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
+	if preposition == "listcorrespondents" {
609
+		target = "*"
610
+	}
601 611
 
602 612
 	parseQueryParam := func(param string) (msgid string, timestamp time.Time, err error) {
603 613
 		if param == "*" && (preposition == "before" || preposition == "between") {
@@ -641,15 +651,22 @@ func chathistoryHandler(server *Server, client *Client, msg ircmsg.Message, rb *
641 651
 		return endpoint.Truncate(time.Millisecond).Add(time.Millisecond)
642 652
 	}
643 653
 
654
+	paramPos := 2
644 655
 	var start, end history.Selector
645 656
 	var limit int
646 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 664
 	case "between":
648
-		start.Msgid, start.Time, err = parseQueryParam(msg.Params[2])
665
+		start.Msgid, start.Time, err = parseQueryParam(msg.Params[paramPos])
649 666
 		if err != nil {
650 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 670
 		if err != nil {
654 671
 			return
655 672
 		}
@@ -662,7 +679,7 @@ func chathistoryHandler(server *Server, client *Client, msg ircmsg.Message, rb *
662 679
 				end.Time = roundUp(end.Time)
663 680
 			}
664 681
 		}
665
-		limit = parseHistoryLimit(4)
682
+		limit = parseHistoryLimit(paramPos + 2)
666 683
 	case "before", "after", "around":
667 684
 		start.Msgid, start.Time, err = parseQueryParam(msg.Params[2])
668 685
 		if err != nil {
@@ -689,14 +706,16 @@ func chathistoryHandler(server *Server, client *Client, msg ircmsg.Message, rb *
689 706
 		}
690 707
 		limit = parseHistoryLimit(3)
691 708
 	default:
692
-		unknown_command = true
709
+		err = utils.ErrInvalidParams
693 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 716
 		items, err = sequence.Around(start, limit)
698 717
 	} else {
699
-		items, _, err = sequence.Between(start, end, limit)
718
+		items, err = sequence.Between(start, end, limit)
700 719
 	}
701 720
 	return
702 721
 }
@@ -1086,7 +1105,7 @@ func historyHandler(server *Server, client *Client, msg ircmsg.Message, rb *Resp
1086 1105
 		if channel != nil {
1087 1106
 			channel.replayHistoryItems(rb, items, false)
1088 1107
 		} else {
1089
-			client.replayPrivmsgHistory(rb, items, "", true)
1108
+			client.replayPrivmsgHistory(rb, items, "")
1090 1109
 		}
1091 1110
 	}
1092 1111
 	return false

+ 92
- 3
irc/history/history.go Переглянути файл

@@ -44,8 +44,13 @@ 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 `json:"CfCorrespondent,omitempty"`
48
+	IsBot           bool   `json:"IsBot,omitempty"`
49
+}
50
+
51
+type CorrespondentListing struct {
47 52
 	CfCorrespondent string
48
-	IsBot           bool `json:"IsBot,omitempty"`
53
+	Time            time.Time
49 54
 }
50 55
 
51 56
 // HasMsgid tests whether a message has the message id `msgid`.
@@ -61,6 +66,13 @@ func Reverse(results []Item) {
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 76
 // Buffer is a ring buffer holding message/event history for a channel or user
65 77
 type Buffer struct {
66 78
 	sync.RWMutex
@@ -201,6 +213,78 @@ func (list *Buffer) betweenHelper(start, end Selector, cutoff time.Time, pred Pr
201 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 288
 // implements history.Sequence, emulating a single history buffer (for a channel,
205 289
 // a single user's DMs, or a DM conversation)
206 290
 type bufferSequence struct {
@@ -223,14 +307,19 @@ func (list *Buffer) MakeSequence(correspondent string, cutoff time.Time) Sequenc
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 315
 func (seq *bufferSequence) Around(start Selector, limit int) (results []Item, err error) {
231 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 323
 // you must be holding the read lock to call this
235 324
 func (list *Buffer) matchInternal(predicate Predicate, ascending bool, limit int) (results []Item) {
236 325
 	if list.start == -1 || len(list.buffer) == 0 {

+ 5
- 3
irc/history/queries.go Переглянути файл

@@ -17,15 +17,17 @@ 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 []CorrespondentListing, err error)
22 24
 }
23 25
 
24 26
 // This is a bad, slow implementation of CHATHISTORY AROUND using the BETWEEN semantics
25 27
 func GenericAround(seq Sequence, start Selector, limit int) (results []Item, err error) {
26 28
 	var halfLimit int
27 29
 	halfLimit = (limit + 1) / 2
28
-	initialResults, _, err := seq.Between(Selector{}, start, halfLimit)
30
+	initialResults, err := seq.Between(Selector{}, start, halfLimit)
29 31
 	if err != nil {
30 32
 		return
31 33
 	} else if len(initialResults) == 0 {
@@ -34,7 +36,7 @@ func GenericAround(seq Sequence, start Selector, limit int) (results []Item, err
34 36
 		return
35 37
 	}
36 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 40
 	return
39 41
 }
40 42
 

+ 2
- 2
irc/histserv.go Переглянути файл

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

+ 144
- 12
irc/mysql/history.go Переглянути файл

@@ -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,55 @@ 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.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 957
 func (mysql *MySQL) Close() {
839 958
 	// closing the database will close our prepared statements as well
840 959
 	if mysql.db != nil {
@@ -852,7 +971,7 @@ type mySQLHistorySequence struct {
852 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 975
 	ctx, cancel := context.WithTimeout(context.Background(), s.mysql.getTimeout())
857 976
 	defer cancel()
858 977
 
@@ -860,25 +979,38 @@ func (s *mySQLHistorySequence) Between(start, end history.Selector, limit int) (
860 979
 	if start.Msgid != "" {
861 980
 		startTime, _, _, err = s.mysql.lookupMsgid(ctx, start.Msgid, false)
862 981
 		if err != nil {
863
-			return nil, false, err
982
+			return nil, err
864 983
 		}
865 984
 	}
866 985
 	endTime := end.Time
867 986
 	if end.Msgid != "" {
868 987
 		endTime, _, _, err = s.mysql.lookupMsgid(ctx, end.Msgid, false)
869 988
 		if err != nil {
870
-			return nil, false, err
989
+			return nil, err
871 990
 		}
872 991
 	}
873 992
 
874 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 997
 func (s *mySQLHistorySequence) Around(start history.Selector, limit int) (results []history.Item, err error) {
879 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 1014
 func (mysql *MySQL) MakeSequence(target, correspondent string, cutoff time.Time) history.Sequence {
883 1015
 	return &mySQLHistorySequence{
884 1016
 		target:        target,

+ 25
- 4
irc/znc.go Переглянути файл

@@ -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,9 +194,9 @@ 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
 
@@ -209,12 +211,31 @@ func zncPlaybackListHandler(client *Client, command string, params []string, rb
209 211
 			client.server.logger.Error("internal", "couldn't get history sequence for ZNC list", err.Error())
210 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 215
 		if err != nil {
214 216
 			client.server.logger.Error("internal", "couldn't query history for ZNC list", err.Error())
215 217
 		} else if len(items) != 0 {
216 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
 }

Завантаження…
Відмінити
Зберегти