Pārlūkot izejas kodu

history replay enhancements

tags/v1.0.0-rc1
Shivaram Lingamneni 5 gadus atpakaļ
vecāks
revīzija
2c7c8fbaf9

+ 1
- 1
README.md Parādīt failu

@@ -129,7 +129,7 @@ Make sure to setup [SASL](https://freenode.net/kb/answer/sasl) in your client to
129 129
 * Niels Freier, added WebSocket support to Ergonomadic, <https://github.com/stumpyfr>
130 130
 * Daniel Oakley, maintainer of Oragono, <https://github.com/DanielOaks>
131 131
 * Euan Kemp, contributor to Oragono and lots of useful fixes, <https://github.com/euank>
132
-* Shivaram Lingamneni, has contributed a ton of fixes, refactoring, and general improvements, <https://github.com/slingamn>
132
+* Shivaram Lingamneni, co-maintainer of Oragono, <https://github.com/slingamn>
133 133
 * James Mills, contributed Docker support, <https://github.com/prologic>
134 134
 * Vegax, implementing some commands and helping when Oragono was just getting started, <https://github.com/vegax87>
135 135
 * Sean Enck, transitioned us from using a custom script to a proper Makefile, <https://github.com/enckse>

+ 0
- 78
irc/batch.go Parādīt failu

@@ -1,78 +0,0 @@
1
-// Copyright (c) 2017 Daniel Oaks <daniel@danieloaks.net>
2
-// released under the MIT license
3
-
4
-package irc
5
-
6
-import (
7
-	"strconv"
8
-	"time"
9
-
10
-	"github.com/goshuirc/irc-go/ircmsg"
11
-	"github.com/oragono/oragono/irc/caps"
12
-)
13
-
14
-const (
15
-	// maxBatchID is the maximum ID the batch counter can get to before it rotates.
16
-	//
17
-	// Batch IDs are made up of the current unix timestamp plus a rolling int ID that's
18
-	// incremented for every new batch. It's an alright solution and will work unless we get
19
-	// more than maxId batches per nanosecond. Later on when we have S2S linking, the batch
20
-	// ID will also contain the server ID to ensure they stay unique.
21
-	maxBatchID uint64 = 60000
22
-)
23
-
24
-// BatchManager helps generate new batches and new batch IDs.
25
-type BatchManager struct {
26
-	idCounter uint64
27
-}
28
-
29
-// NewBatchManager returns a new Manager.
30
-func NewBatchManager() *BatchManager {
31
-	return &BatchManager{}
32
-}
33
-
34
-// NewID returns a new batch ID that should be unique.
35
-func (bm *BatchManager) NewID() string {
36
-	bm.idCounter++
37
-	if maxBatchID < bm.idCounter {
38
-		bm.idCounter = 0
39
-	}
40
-
41
-	return strconv.FormatInt(time.Now().UnixNano(), 36) + strconv.FormatUint(bm.idCounter, 36)
42
-}
43
-
44
-// Batch represents an IRCv3 batch.
45
-type Batch struct {
46
-	ID     string
47
-	Type   string
48
-	Params []string
49
-}
50
-
51
-// New returns a new batch.
52
-func (bm *BatchManager) New(batchType string, params ...string) *Batch {
53
-	newBatch := Batch{
54
-		ID:     bm.NewID(),
55
-		Type:   batchType,
56
-		Params: params,
57
-	}
58
-
59
-	return &newBatch
60
-}
61
-
62
-// Start sends the batch start message to this client
63
-func (b *Batch) Start(client *Client, tags *map[string]ircmsg.TagValue) {
64
-	if client.capabilities.Has(caps.Batch) {
65
-		params := []string{"+" + b.ID, b.Type}
66
-		for _, param := range b.Params {
67
-			params = append(params, param)
68
-		}
69
-		client.Send(tags, client.server.name, "BATCH", params...)
70
-	}
71
-}
72
-
73
-// End sends the batch end message to this client
74
-func (b *Batch) End(client *Client) {
75
-	if client.capabilities.Has(caps.Batch) {
76
-		client.Send(nil, client.server.name, "BATCH", "-"+b.ID)
77
-	}
78
-}

+ 63
- 43
irc/channel.go Parādīt failu

@@ -38,7 +38,7 @@ type Channel struct {
38 38
 	topic             string
39 39
 	topicSetBy        string
40 40
 	topicSetTime      time.Time
41
-	userLimit         uint64
41
+	userLimit         int
42 42
 	accountToUMode    map[string]modes.Mode
43 43
 	history           history.Buffer
44 44
 }
@@ -332,25 +332,12 @@ func (channel *Channel) modeStrings(client *Client) (result []string) {
332 332
 		result = append(result, channel.key)
333 333
 	}
334 334
 	if showUserLimit {
335
-		result = append(result, strconv.FormatUint(channel.userLimit, 10))
335
+		result = append(result, strconv.Itoa(channel.userLimit))
336 336
 	}
337 337
 
338 338
 	return
339 339
 }
340 340
 
341
-// IsFull returns true if this channel is at its' members limit.
342
-func (channel *Channel) IsFull() bool {
343
-	channel.stateMutex.RLock()
344
-	defer channel.stateMutex.RUnlock()
345
-	return (channel.userLimit > 0) && (uint64(len(channel.members)) >= channel.userLimit)
346
-}
347
-
348
-// CheckKey returns true if the key is not set or matches the given key.
349
-func (channel *Channel) CheckKey(key string) bool {
350
-	chkey := channel.Key()
351
-	return chkey == "" || utils.SecretTokensMatch(chkey, key)
352
-}
353
-
354 341
 func (channel *Channel) IsEmpty() bool {
355 342
 	channel.stateMutex.RLock()
356 343
 	defer channel.stateMutex.RUnlock()
@@ -359,26 +346,31 @@ func (channel *Channel) IsEmpty() bool {
359 346
 
360 347
 // Join joins the given client to this channel (if they can be joined).
361 348
 func (channel *Channel) Join(client *Client, key string, isSajoin bool, rb *ResponseBuffer) {
362
-	if channel.hasClient(client) {
363
-		// already joined, no message needs to be sent
364
-		return
365
-	}
366
-
367 349
 	channel.stateMutex.RLock()
368 350
 	chname := channel.name
369 351
 	chcfname := channel.nameCasefolded
370 352
 	founder := channel.registeredFounder
353
+	chkey := channel.key
354
+	limit := channel.userLimit
355
+	chcount := len(channel.members)
356
+	_, alreadyJoined := channel.members[client]
371 357
 	channel.stateMutex.RUnlock()
358
+
359
+	if alreadyJoined {
360
+		// no message needs to be sent
361
+		return
362
+	}
363
+
372 364
 	account := client.Account()
373 365
 	nickMaskCasefolded := client.NickMaskCasefolded()
374 366
 	hasPrivs := isSajoin || (founder != "" && founder == account)
375 367
 
376
-	if !hasPrivs && channel.IsFull() {
368
+	if !hasPrivs && limit != 0 && chcount >= limit {
377 369
 		rb.Add(nil, client.server.name, ERR_CHANNELISFULL, chname, fmt.Sprintf(client.t("Cannot join channel (+%s)"), "l"))
378 370
 		return
379 371
 	}
380 372
 
381
-	if !hasPrivs && !channel.CheckKey(key) {
373
+	if !hasPrivs && chkey != "" && !utils.SecretTokensMatch(chkey, key) {
382 374
 		rb.Add(nil, client.server.name, ERR_BADCHANNELKEY, chname, fmt.Sprintf(client.t("Cannot join channel (+%s)"), "k"))
383 375
 		return
384 376
 	}
@@ -469,7 +461,18 @@ func (channel *Channel) Join(client *Client, key string, isSajoin bool, rb *Resp
469 461
 		Type:        history.Join,
470 462
 		Nick:        nickmask,
471 463
 		AccountName: accountName,
464
+		Msgid:       realname,
472 465
 	})
466
+
467
+	// TODO #259 can be implemented as Flush(false) (i.e., nonblocking) while holding joinPartMutex
468
+	rb.Flush(true)
469
+
470
+	replayLimit := channel.server.Config().History.AutoreplayOnJoin
471
+	if replayLimit > 0 {
472
+		items := channel.history.Latest(replayLimit)
473
+		channel.replayHistoryItems(rb, items)
474
+		rb.Flush(true)
475
+	}
473 476
 }
474 477
 
475 478
 // Part parts the given client from this channel, with the given message.
@@ -506,7 +509,7 @@ func (channel *Channel) Resume(newClient, oldClient *Client, timestamp time.Time
506 509
 	now := time.Now()
507 510
 	channel.resumeAndAnnounce(newClient, oldClient)
508 511
 	if !timestamp.IsZero() {
509
-		channel.replayHistory(newClient, timestamp, now)
512
+		channel.replayHistoryForResume(newClient, timestamp, now)
510 513
 	}
511 514
 }
512 515
 
@@ -560,7 +563,6 @@ func (channel *Channel) resumeAndAnnounce(newClient, oldClient *Client) {
560 563
 
561 564
 	rb := NewResponseBuffer(newClient)
562 565
 	// use blocking i/o to synchronize with the later history replay
563
-	rb.SetBlocking(true)
564 566
 	if newClient.capabilities.Has(caps.ExtendedJoin) {
565 567
 		rb.Add(nil, nickMask, "JOIN", channel.name, accountName, realName)
566 568
 	} else {
@@ -571,41 +573,55 @@ func (channel *Channel) resumeAndAnnounce(newClient, oldClient *Client) {
571 573
 	if 0 < len(oldModes) {
572 574
 		rb.Add(nil, newClient.server.name, "MODE", channel.name, oldModes, nick)
573 575
 	}
574
-	rb.Send()
576
+	rb.Send(true)
577
+}
578
+
579
+func (channel *Channel) replayHistoryForResume(newClient *Client, after time.Time, before time.Time) {
580
+	items, complete := channel.history.Between(after, before)
581
+	rb := NewResponseBuffer(newClient)
582
+	channel.replayHistoryItems(rb, items)
583
+	if !complete && !newClient.resumeDetails.HistoryIncomplete {
584
+		// warn here if we didn't warn already
585
+		rb.Add(nil, "HistServ", "NOTICE", channel.Name(), newClient.t("Some additional message history may have been lost"))
586
+	}
587
+	rb.Send(true)
575 588
 }
576 589
 
577
-func (channel *Channel) replayHistory(newClient *Client, after time.Time, before time.Time) {
590
+func (channel *Channel) replayHistoryItems(rb *ResponseBuffer, items []history.Item) {
578 591
 	chname := channel.Name()
579
-	extendedJoin := newClient.capabilities.Has(caps.ExtendedJoin)
592
+	client := rb.target
593
+	extendedJoin := client.capabilities.Has(caps.ExtendedJoin)
594
+	serverTime := client.capabilities.Has(caps.ServerTime)
580 595
 
581
-	items, complete := channel.history.Between(after, before)
582 596
 	for _, item := range items {
597
+		var tags Tags
598
+		if serverTime {
599
+			tags = ensureTag(tags, "time", item.Time.Format(IRCv3TimestampFormat))
600
+		}
601
+
583 602
 		switch item.Type {
584 603
 		case history.Privmsg:
585
-			newClient.sendSplitMsgFromClientInternal(true, item.Time, item.Msgid, item.Nick, item.AccountName, nil, "PRIVMSG", chname, item.Message)
604
+			rb.AddSplitMessageFromClient(item.Msgid, item.Nick, item.AccountName, tags, "PRIVMSG", chname, item.Message)
586 605
 		case history.Notice:
587
-			newClient.sendSplitMsgFromClientInternal(true, item.Time, item.Msgid, item.Nick, item.AccountName, nil, "NOTICE", chname, item.Message)
606
+			rb.AddSplitMessageFromClient(item.Msgid, item.Nick, item.AccountName, tags, "NOTICE", chname, item.Message)
588 607
 		case history.Join:
589 608
 			if extendedJoin {
590
-				newClient.sendInternal(true, item.Time, nil, item.Nick, "JOIN", chname, item.AccountName, "")
609
+				// XXX Msgid is the realname in this case
610
+				rb.Add(tags, item.Nick, "JOIN", chname, item.AccountName, item.Msgid)
591 611
 			} else {
592
-				newClient.sendInternal(true, item.Time, nil, item.Nick, "JOIN", chname)
612
+				rb.Add(tags, item.Nick, "JOIN", chname)
593 613
 			}
594 614
 		case history.Quit:
595 615
 			// XXX: send QUIT as PART to avoid having to correctly deduplicate and synchronize
596 616
 			// QUIT messages across channels
597 617
 			fallthrough
598 618
 		case history.Part:
599
-			newClient.sendInternal(true, item.Time, nil, item.Nick, "PART", chname, item.Message.Original)
619
+			rb.Add(tags, item.Nick, "PART", chname, item.Message.Original)
600 620
 		case history.Kick:
601
-			newClient.sendInternal(true, item.Time, nil, item.Nick, "KICK", chname, item.Msgid, item.Message.Original)
621
+			// XXX Msgid is the kick target
622
+			rb.Add(tags, item.Nick, "KICK", chname, item.Msgid, item.Message.Original)
602 623
 		}
603 624
 	}
604
-
605
-	if !complete && !newClient.resumeDetails.HistoryIncomplete {
606
-		// warn here if we didn't warn already
607
-		newClient.sendInternal(true, time.Time{}, nil, "HistServ", "NOTICE", chname, newClient.t("Some additional message history may have been lost"))
608
-	}
609 625
 }
610 626
 
611 627
 // SendTopic sends the channel topic to the given client.
@@ -707,10 +723,12 @@ func (channel *Channel) sendMessage(msgid, cmd string, requiredCaps []caps.Capab
707 723
 			messageTagsToUse = clientOnlyTags
708 724
 		}
709 725
 
726
+		nickMaskString := client.NickMaskString()
727
+		accountName := client.AccountName()
710 728
 		if message == nil {
711
-			rb.AddFromClient(msgid, client, messageTagsToUse, cmd, channel.name)
729
+			rb.AddFromClient(msgid, nickMaskString, accountName, messageTagsToUse, cmd, channel.name)
712 730
 		} else {
713
-			rb.AddFromClient(msgid, client, messageTagsToUse, cmd, channel.name, *message)
731
+			rb.AddFromClient(msgid, nickMaskString, accountName, messageTagsToUse, cmd, channel.name, *message)
714 732
 		}
715 733
 	}
716 734
 	for _, member := range channel.Members() {
@@ -773,10 +791,12 @@ func (channel *Channel) sendSplitMessage(msgid, cmd string, histType history.Ite
773 791
 		if client.capabilities.Has(caps.MessageTags) {
774 792
 			tagsToUse = clientOnlyTags
775 793
 		}
794
+		nickMaskString := client.NickMaskString()
795
+		accountName := client.AccountName()
776 796
 		if message == nil {
777
-			rb.AddFromClient(msgid, client, tagsToUse, cmd, channel.name)
797
+			rb.AddFromClient(msgid, nickMaskString, accountName, tagsToUse, cmd, channel.name)
778 798
 		} else {
779
-			rb.AddSplitMessageFromClient(msgid, client, tagsToUse, cmd, channel.name, *message)
799
+			rb.AddSplitMessageFromClient(msgid, nickMaskString, accountName, tagsToUse, cmd, channel.name, *message)
780 800
 		}
781 801
 	}
782 802
 

+ 11
- 9
irc/client.go Parādīt failu

@@ -446,17 +446,19 @@ func (client *Client) TryResume() {
446 446
 			}
447 447
 		}
448 448
 	}
449
-	personalHistory := oldClient.history.All()
449
+	privmsgMatcher := func(item history.Item) bool {
450
+		return item.Type == history.Privmsg || item.Type == history.Notice
451
+	}
452
+	privmsgHistory := oldClient.history.Match(privmsgMatcher, 0)
450 453
 	lastDiscarded := oldClient.history.LastDiscarded()
451 454
 	if lastDiscarded.Before(oldestLostMessage) {
452 455
 		oldestLostMessage = lastDiscarded
453 456
 	}
454
-	for _, item := range personalHistory {
455
-		if item.Type == history.Privmsg || item.Type == history.Notice {
456
-			sender := server.clients.Get(item.Nick)
457
-			if sender != nil {
458
-				friends.Add(sender)
459
-			}
457
+	for _, item := range privmsgHistory {
458
+		// TODO this is the nickmask, fix that
459
+		sender := server.clients.Get(item.Nick)
460
+		if sender != nil {
461
+			friends.Add(sender)
460 462
 		}
461 463
 	}
462 464
 
@@ -482,10 +484,10 @@ func (client *Client) TryResume() {
482 484
 	}
483 485
 
484 486
 	if client.resumeDetails.HistoryIncomplete {
485
-		client.Send(nil, "RESUME", "WARN", fmt.Sprintf(client.t("Resume may have lost up to %d seconds of history"), gapSeconds))
487
+		client.Send(nil, client.server.name, "RESUME", "WARN", fmt.Sprintf(client.t("Resume may have lost up to %d seconds of history"), gapSeconds))
486 488
 	}
487 489
 
488
-	client.Send(nil, "RESUME", "SUCCESS", oldNick)
490
+	client.Send(nil, client.server.name, "RESUME", "SUCCESS", oldNick)
489 491
 
490 492
 	// after we send the rest of the registration burst, we'll try rejoining channels
491 493
 }

+ 1
- 1
irc/commands.go Parādīt failu

@@ -47,7 +47,7 @@ func (cmd *Command) Run(server *Server, client *Client, msg ircmsg.IrcMessage) b
47 47
 	rb := NewResponseBuffer(client)
48 48
 	rb.Label = GetLabel(msg)
49 49
 	exiting := cmd.handler(server, client, msg, rb)
50
-	rb.Send()
50
+	rb.Send(true)
51 51
 
52 52
 	// after each command, see if we can send registration to the client
53 53
 	if !client.registered {

+ 4
- 3
irc/config.go Parādīt failu

@@ -268,9 +268,10 @@ type Config struct {
268 268
 	Fakelag FakelagConfig
269 269
 
270 270
 	History struct {
271
-		Enabled       bool
272
-		ChannelLength int `yaml:"channel-length"`
273
-		ClientLength  int `yaml:"client-length"`
271
+		Enabled          bool
272
+		ChannelLength    int `yaml:"channel-length"`
273
+		ClientLength     int `yaml:"client-length"`
274
+		AutoreplayOnJoin int `yaml:"autoreplay-on-join"`
274 275
 	}
275 276
 
276 277
 	Filename string

+ 1
- 7
irc/getters.go Parādīt failu

@@ -259,13 +259,7 @@ func (channel *Channel) Members() (result []*Client) {
259 259
 	return channel.membersCache
260 260
 }
261 261
 
262
-func (channel *Channel) UserLimit() uint64 {
263
-	channel.stateMutex.RLock()
264
-	defer channel.stateMutex.RUnlock()
265
-	return channel.userLimit
266
-}
267
-
268
-func (channel *Channel) setUserLimit(limit uint64) {
262
+func (channel *Channel) setUserLimit(limit int) {
269 263
 	channel.stateMutex.Lock()
270 264
 	channel.userLimit = limit
271 265
 	channel.stateMutex.Unlock()

+ 12
- 8
irc/handlers.go Parādīt failu

@@ -933,7 +933,7 @@ func sajoinHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Re
933 933
 		server.channels.Join(target, chname, "", true, rb)
934 934
 	}
935 935
 	if client != target {
936
-		rb.Send()
936
+		rb.Send(false)
937 937
 	}
938 938
 	return false
939 939
 }
@@ -1706,16 +1706,18 @@ func noticeHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Re
1706 1706
 			if !user.HasMode(modes.RegisteredOnly) || client.LoggedIntoAccount() {
1707 1707
 				user.SendSplitMsgFromClient(msgid, client, clientOnlyTags, "NOTICE", user.nick, splitMsg)
1708 1708
 			}
1709
+			nickMaskString := client.NickMaskString()
1710
+			accountName := client.AccountName()
1709 1711
 			if client.capabilities.Has(caps.EchoMessage) {
1710
-				rb.AddSplitMessageFromClient(msgid, client, clientOnlyTags, "NOTICE", user.nick, splitMsg)
1712
+				rb.AddSplitMessageFromClient(msgid, nickMaskString, accountName, clientOnlyTags, "NOTICE", user.nick, splitMsg)
1711 1713
 			}
1712 1714
 
1713 1715
 			user.history.Add(history.Item{
1714 1716
 				Type:        history.Notice,
1715 1717
 				Msgid:       msgid,
1716 1718
 				Message:     splitMsg,
1717
-				Nick:        client.NickMaskString(),
1718
-				AccountName: client.AccountName(),
1719
+				Nick:        nickMaskString,
1720
+				AccountName: accountName,
1719 1721
 			})
1720 1722
 		}
1721 1723
 	}
@@ -1916,8 +1918,10 @@ func privmsgHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *R
1916 1918
 			if !user.HasMode(modes.RegisteredOnly) || client.LoggedIntoAccount() {
1917 1919
 				user.SendSplitMsgFromClient(msgid, client, clientOnlyTags, "PRIVMSG", user.nick, splitMsg)
1918 1920
 			}
1921
+			nickMaskString := client.NickMaskString()
1922
+			accountName := client.AccountName()
1919 1923
 			if client.capabilities.Has(caps.EchoMessage) {
1920
-				rb.AddSplitMessageFromClient(msgid, client, clientOnlyTags, "PRIVMSG", user.nick, splitMsg)
1924
+				rb.AddSplitMessageFromClient(msgid, nickMaskString, accountName, clientOnlyTags, "PRIVMSG", user.nick, splitMsg)
1921 1925
 			}
1922 1926
 			if user.HasMode(modes.Away) {
1923 1927
 				//TODO(dan): possibly implement cooldown of away notifications to users
@@ -1928,8 +1932,8 @@ func privmsgHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *R
1928 1932
 				Type:        history.Privmsg,
1929 1933
 				Msgid:       msgid,
1930 1934
 				Message:     splitMsg,
1931
-				Nick:        client.NickMaskString(),
1932
-				AccountName: client.AccountName(),
1935
+				Nick:        nickMaskString,
1936
+				AccountName: accountName,
1933 1937
 			})
1934 1938
 		}
1935 1939
 	}
@@ -2150,7 +2154,7 @@ func tagmsgHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Re
2150 2154
 			}
2151 2155
 			user.SendFromClient(msgid, client, clientOnlyTags, "TAGMSG", user.nick)
2152 2156
 			if client.capabilities.Has(caps.EchoMessage) {
2153
-				rb.AddFromClient(msgid, client, clientOnlyTags, "TAGMSG", user.nick)
2157
+				rb.AddFromClient(msgid, client.NickMaskString(), client.AccountName(), clientOnlyTags, "TAGMSG", user.nick)
2154 2158
 			}
2155 2159
 			if user.HasMode(modes.Away) {
2156 2160
 				//TODO(dan): possibly implement cooldown of away notifications to users

+ 43
- 30
irc/history/history.go Parādīt failu

@@ -32,9 +32,12 @@ type Item struct {
32 32
 	// this is the uncasefolded account name, if there's no account it should be set to "*"
33 33
 	AccountName string
34 34
 	Message     utils.SplitMessage
35
-	Msgid       string
35
+	// for non-privmsg items, we may stuff some other data in here
36
+	Msgid string
36 37
 }
37 38
 
39
+type Predicate func(item Item) (matches bool)
40
+
38 41
 // Buffer is a ring buffer holding message/event history for a channel or user
39 42
 type Buffer struct {
40 43
 	sync.RWMutex
@@ -85,7 +88,7 @@ func (list *Buffer) Add(item Item) {
85 88
 	}
86 89
 
87 90
 	if item.Time.IsZero() {
88
-		item.Time = time.Now()
91
+		item.Time = time.Now().UTC()
89 92
 	}
90 93
 
91 94
 	list.Lock()
@@ -112,6 +115,12 @@ func (list *Buffer) Add(item Item) {
112 115
 	list.buffer[pos] = item
113 116
 }
114 117
 
118
+func reverse(results []Item) {
119
+	for i, j := 0, len(results)-1; i < j; i, j = i+1, j-1 {
120
+		results[i], results[j] = results[j], results[i]
121
+	}
122
+}
123
+
115 124
 // Between returns all history items with a time `after` <= time <= `before`,
116 125
 // with an indication of whether the results are complete or are missing items
117 126
 // because some of that period was discarded. A zero value of `before` is considered
@@ -126,51 +135,55 @@ func (list *Buffer) Between(after, before time.Time) (results []Item, complete b
126 135
 
127 136
 	complete = after.Equal(list.lastDiscarded) || after.After(list.lastDiscarded)
128 137
 
129
-	if list.start == -1 {
130
-		return
138
+	satisfies := func(item Item) bool {
139
+		return (after.IsZero() || item.Time.After(after)) && (before.IsZero() || item.Time.Before(before))
131 140
 	}
132 141
 
133
-	satisfies := func(itime time.Time) bool {
134
-		return (after.IsZero() || itime.After(after)) && (before.IsZero() || itime.Before(before))
142
+	return list.matchInternal(satisfies, 0), complete
143
+}
144
+
145
+// Match returns all history items such that `predicate` returns true for them.
146
+// Items are considered in reverse insertion order, up to a total of `limit` matches.
147
+// `predicate` MAY be a closure that maintains its own state across invocations;
148
+// it MUST NOT acquire any locks or otherwise do anything weird.
149
+func (list *Buffer) Match(predicate Predicate, limit int) (results []Item) {
150
+	if !list.Enabled() {
151
+		return
135 152
 	}
136 153
 
137
-	// TODO: if we can guarantee that the insertion order is also the monotonic clock order,
138
-	// then this can do a single allocation and use binary search and 1-2 copy calls
154
+	list.RLock()
155
+	defer list.RUnlock()
156
+
157
+	return list.matchInternal(predicate, limit)
158
+}
159
+
160
+// you must be holding the read lock to call this
161
+func (list *Buffer) matchInternal(predicate Predicate, limit int) (results []Item) {
162
+	if list.start == -1 {
163
+		return
164
+	}
139 165
 
140 166
 	pos := list.prev(list.end)
141 167
 	for {
142
-		if satisfies(list.buffer[pos].Time) {
168
+		if predicate(list.buffer[pos]) {
143 169
 			results = append(results, list.buffer[pos])
144 170
 		}
145
-		if pos == list.start {
171
+		if pos == list.start || (limit != 0 && len(results) == limit) {
146 172
 			break
147 173
 		}
148 174
 		pos = list.prev(pos)
149 175
 	}
150 176
 
151
-	// reverse the results
152
-	for i, j := 0, len(results)-1; i < j; i, j = i+1, j-1 {
153
-		results[i], results[j] = results[j], results[i]
154
-	}
177
+	// TODO sort by time instead?
178
+	reverse(results)
155 179
 	return
156 180
 }
157 181
 
158
-// All returns all available history items as a slice
159
-func (list *Buffer) All() (results []Item) {
160
-	list.RLock()
161
-	defer list.RUnlock()
162
-
163
-	if list.start == -1 {
164
-		return
165
-	}
166
-	results = make([]Item, list.length())
167
-	if list.start < list.end {
168
-		copy(results, list.buffer[list.start:list.end])
169
-	} else {
170
-		initialSegment := copy(results, list.buffer[list.start:])
171
-		copy(results[initialSegment:], list.buffer[:list.end])
172
-	}
173
-	return
182
+// Latest returns the items most recently added, up to `limit`. If `limit` is 0,
183
+// it returns all items.
184
+func (list *Buffer) Latest(limit int) (results []Item) {
185
+	matchAll := func(item Item) bool { return true }
186
+	return list.Match(matchAll, limit)
174 187
 }
175 188
 
176 189
 // LastDiscarded returns the latest time of any entry that was evicted

+ 2
- 1
irc/history/history_test.go Parādīt failu

@@ -67,7 +67,8 @@ func TestEmptyBuffer(t *testing.T) {
67 67
 	if since[0].Nick != "testnick2" {
68 68
 		t.Error("retrieved junk data")
69 69
 	}
70
-	assertEqual(toNicks(buf.All()), []string{"testnick2"}, t)
70
+	matchAll := func(item Item) bool { return true }
71
+	assertEqual(toNicks(buf.Match(matchAll, 0)), []string{"testnick2"}, t)
71 72
 }
72 73
 
73 74
 func toNicks(items []Item) (result []string) {

+ 1
- 1
irc/modes.go Parādīt failu

@@ -189,7 +189,7 @@ func (channel *Channel) ApplyChannelModeChanges(client *Client, isSamode bool, c
189 189
 		case modes.UserLimit:
190 190
 			switch change.Op {
191 191
 			case modes.Add:
192
-				val, err := strconv.ParseUint(change.Arg, 10, 64)
192
+				val, err := strconv.Atoi(change.Arg)
193 193
 				if err == nil {
194 194
 					channel.setUserLimit(val)
195 195
 					applied = append(applied, change)

+ 1
- 1
irc/nickname.go Parādīt failu

@@ -83,7 +83,7 @@ func (server *Server) RandomlyRename(client *Client) {
83 83
 	nick := fmt.Sprintf("%s%s", prefix, hex.EncodeToString(buf))
84 84
 	rb := NewResponseBuffer(client)
85 85
 	performNickChange(server, client, client, nick, rb)
86
-	rb.Send()
86
+	rb.Send(false)
87 87
 	// technically performNickChange can fail to change the nick,
88 88
 	// but if they're still delinquent, the timer will get them later
89 89
 }

+ 86
- 50
irc/responsebuffer.go Parādīt failu

@@ -4,6 +4,7 @@
4 4
 package irc
5 5
 
6 6
 import (
7
+	"runtime/debug"
7 8
 	"time"
8 9
 
9 10
 	"github.com/goshuirc/irc-go/ircmsg"
@@ -11,16 +12,22 @@ import (
11 12
 	"github.com/oragono/oragono/irc/utils"
12 13
 )
13 14
 
15
+const (
16
+	// https://ircv3.net/specs/extensions/labeled-response.html
17
+	batchType = "draft/labeled-response"
18
+)
19
+
14 20
 // ResponseBuffer - put simply - buffers messages and then outputs them to a given client.
15 21
 //
16 22
 // Using a ResponseBuffer lets you really easily implement labeled-response, since the
17 23
 // buffer will silently create a batch if required and label the outgoing messages as
18 24
 // necessary (or leave it off and simply tag the outgoing message).
19 25
 type ResponseBuffer struct {
20
-	Label    string
21
-	target   *Client
22
-	messages []ircmsg.IrcMessage
23
-	blocking bool
26
+	Label     string
27
+	batchID   string
28
+	target    *Client
29
+	messages  []ircmsg.IrcMessage
30
+	finalized bool
24 31
 }
25 32
 
26 33
 // GetLabel returns the label from the given message.
@@ -35,96 +42,125 @@ func NewResponseBuffer(target *Client) *ResponseBuffer {
35 42
 	}
36 43
 }
37 44
 
38
-func (rb *ResponseBuffer) SetBlocking(blocking bool) {
39
-	rb.blocking = blocking
40
-}
41
-
42 45
 // Add adds a standard new message to our queue.
43 46
 func (rb *ResponseBuffer) Add(tags *map[string]ircmsg.TagValue, prefix string, command string, params ...string) {
44
-	message := ircmsg.MakeMessage(tags, prefix, command, params...)
47
+	if rb.finalized {
48
+		rb.target.server.logger.Error("message added to finalized ResponseBuffer, undefined behavior")
49
+		debug.PrintStack()
50
+		return
51
+	}
45 52
 
53
+	message := ircmsg.MakeMessage(tags, prefix, command, params...)
46 54
 	rb.messages = append(rb.messages, message)
47 55
 }
48 56
 
49 57
 // AddFromClient adds a new message from a specific client to our queue.
50
-func (rb *ResponseBuffer) AddFromClient(msgid string, from *Client, tags *map[string]ircmsg.TagValue, command string, params ...string) {
58
+func (rb *ResponseBuffer) AddFromClient(msgid string, fromNickMask string, fromAccount string, tags *map[string]ircmsg.TagValue, command string, params ...string) {
51 59
 	// attach account-tag
52
-	if rb.target.capabilities.Has(caps.AccountTag) && from.LoggedIntoAccount() {
53
-		if tags == nil {
54
-			tags = ircmsg.MakeTags("account", from.AccountName())
55
-		} else {
56
-			(*tags)["account"] = ircmsg.MakeTagValue(from.AccountName())
60
+	if rb.target.capabilities.Has(caps.AccountTag) {
61
+		if fromAccount != "*" {
62
+			tags = ensureTag(tags, "account", fromAccount)
57 63
 		}
58 64
 	}
59 65
 	// attach message-id
60 66
 	if len(msgid) > 0 && rb.target.capabilities.Has(caps.MessageTags) {
61
-		if tags == nil {
62
-			tags = ircmsg.MakeTags("draft/msgid", msgid)
63
-		} else {
64
-			(*tags)["draft/msgid"] = ircmsg.MakeTagValue(msgid)
65
-		}
67
+		tags = ensureTag(tags, "draft/msgid", msgid)
66 68
 	}
67 69
 
68
-	rb.Add(tags, from.nickMaskString, command, params...)
70
+	rb.Add(tags, fromNickMask, command, params...)
69 71
 }
70 72
 
71 73
 // AddSplitMessageFromClient adds a new split message from a specific client to our queue.
72
-func (rb *ResponseBuffer) AddSplitMessageFromClient(msgid string, from *Client, tags *map[string]ircmsg.TagValue, command string, target string, message utils.SplitMessage) {
74
+func (rb *ResponseBuffer) AddSplitMessageFromClient(msgid string, fromNickMask string, fromAccount string, tags *map[string]ircmsg.TagValue, command string, target string, message utils.SplitMessage) {
73 75
 	if rb.target.capabilities.Has(caps.MaxLine) || message.Wrapped == nil {
74
-		rb.AddFromClient(msgid, from, tags, command, target, message.Original)
76
+		rb.AddFromClient(msgid, fromNickMask, fromAccount, tags, command, target, message.Original)
75 77
 	} else {
76 78
 		for _, str := range message.Wrapped {
77
-			rb.AddFromClient(msgid, from, tags, command, target, str)
79
+			rb.AddFromClient(msgid, fromNickMask, fromAccount, tags, command, target, str)
78 80
 		}
79 81
 	}
80 82
 }
81 83
 
82
-// Send sends the message to our target client.
83
-func (rb *ResponseBuffer) Send() error {
84
-	// fall out if no messages to send
85
-	if len(rb.messages) == 0 {
86
-		return nil
84
+func (rb *ResponseBuffer) sendBatchStart(blocking bool) {
85
+	if rb.batchID != "" {
86
+		// batch already initialized
87
+		return
87 88
 	}
88 89
 
89
-	// make batch and all if required
90
-	var batch *Batch
91
-	useLabel := rb.target.capabilities.Has(caps.LabeledResponse) && rb.Label != ""
92
-	if useLabel && 1 < len(rb.messages) && rb.target.capabilities.Has(caps.Batch) {
93
-		batch = rb.target.server.batches.New("draft/labeled-response")
94
-	}
90
+	// formerly this combined time.Now.UnixNano() in base 36 with an incrementing counter,
91
+	// also in base 36. but let's just use a uuidv4-alike (26 base32 characters):
92
+	rb.batchID = utils.GenerateSecretToken()
95 93
 
96
-	// if label but no batch, add label to first message
97
-	if useLabel && batch == nil {
98
-		message := rb.messages[0]
99
-		message.Tags[caps.LabelTagName] = ircmsg.MakeTagValue(rb.Label)
100
-		rb.messages[0] = message
94
+	message := ircmsg.MakeMessage(nil, rb.target.server.name, "BATCH", "+"+rb.batchID, batchType)
95
+	message.Tags[caps.LabelTagName] = ircmsg.MakeTagValue(rb.Label)
96
+	rb.target.SendRawMessage(message, blocking)
97
+}
98
+
99
+func (rb *ResponseBuffer) sendBatchEnd(blocking bool) {
100
+	if rb.batchID == "" {
101
+		// we are not sending a batch, skip this
102
+		return
101 103
 	}
102 104
 
103
-	// start batch if required
104
-	if batch != nil {
105
-		batch.Start(rb.target, ircmsg.MakeTags(caps.LabelTagName, rb.Label))
105
+	message := ircmsg.MakeMessage(nil, rb.target.server.name, "BATCH", "-"+rb.batchID)
106
+	rb.target.SendRawMessage(message, blocking)
107
+}
108
+
109
+// Send sends all messages in the buffer to the client.
110
+// Afterwards, the buffer is in an undefined state and MUST NOT be used further.
111
+// If `blocking` is true you MUST be sending to the client from its own goroutine.
112
+func (rb *ResponseBuffer) Send(blocking bool) error {
113
+	return rb.flushInternal(true, blocking)
114
+}
115
+
116
+// Flush sends all messages in the buffer to the client.
117
+// Afterwards, the buffer can still be used. Client code MUST subsequently call Send()
118
+// to ensure that the final `BATCH -` message is sent.
119
+// If `blocking` is true you MUST be sending to the client from its own goroutine.
120
+func (rb *ResponseBuffer) Flush(blocking bool) error {
121
+	return rb.flushInternal(false, blocking)
122
+}
123
+
124
+// flushInternal sends the contents of the buffer, either blocking or nonblocking
125
+// It sends the `BATCH +` message if the client supports it and it hasn't been sent already.
126
+// If `final` is true, it also sends `BATCH -` (if necessary).
127
+func (rb *ResponseBuffer) flushInternal(final bool, blocking bool) error {
128
+	useLabel := rb.target.capabilities.Has(caps.LabeledResponse) && rb.Label != ""
129
+	// use a batch if we have a label, and we either currently have multiple messages,
130
+	// or we are doing a Flush() and we have to assume that there will be more messages
131
+	// in the future.
132
+	useBatch := useLabel && (len(rb.messages) > 1 || !final)
133
+
134
+	// if label but no batch, add label to first message
135
+	if useLabel && !useBatch && len(rb.messages) == 1 {
136
+		rb.messages[0].Tags[caps.LabelTagName] = ircmsg.MakeTagValue(rb.Label)
137
+	} else if useBatch {
138
+		rb.sendBatchStart(blocking)
106 139
 	}
107 140
 
108 141
 	// send each message out
109 142
 	for _, message := range rb.messages {
110 143
 		// attach server-time if needed
111 144
 		if rb.target.capabilities.Has(caps.ServerTime) {
112
-			t := time.Now().UTC().Format(IRCv3TimestampFormat)
113
-			message.Tags["time"] = ircmsg.MakeTagValue(t)
145
+			if !message.Tags["time"].HasValue {
146
+				t := time.Now().UTC().Format(IRCv3TimestampFormat)
147
+				message.Tags["time"] = ircmsg.MakeTagValue(t)
148
+			}
114 149
 		}
115 150
 
116 151
 		// attach batch ID
117
-		if batch != nil {
118
-			message.Tags["batch"] = ircmsg.MakeTagValue(batch.ID)
152
+		if rb.batchID != "" {
153
+			message.Tags["batch"] = ircmsg.MakeTagValue(rb.batchID)
119 154
 		}
120 155
 
121 156
 		// send message out
122
-		rb.target.SendRawMessage(message, rb.blocking)
157
+		rb.target.SendRawMessage(message, blocking)
123 158
 	}
124 159
 
125 160
 	// end batch if required
126
-	if batch != nil {
127
-		batch.End(rb.target)
161
+	if final {
162
+		rb.sendBatchEnd(blocking)
163
+		rb.finalized = true
128 164
 	}
129 165
 
130 166
 	// clear out any existing messages

+ 2
- 4
irc/server.go Parādīt failu

@@ -67,7 +67,6 @@ type ListenerWrapper struct {
67 67
 // Server is the main Oragono server.
68 68
 type Server struct {
69 69
 	accounts               *AccountManager
70
-	batches                *BatchManager
71 70
 	channels               *ChannelManager
72 71
 	channelRegistry        *ChannelRegistry
73 72
 	clients                *ClientManager
@@ -116,7 +115,6 @@ type clientConn struct {
116 115
 func NewServer(config *Config, logger *logger.Manager) (*Server, error) {
117 116
 	// initialize data structures
118 117
 	server := &Server{
119
-		batches:             NewBatchManager(),
120 118
 		channels:            NewChannelManager(),
121 119
 		clients:             NewClientManager(),
122 120
 		connectionLimiter:   connection_limits.NewLimiter(),
@@ -406,7 +404,7 @@ func (server *Server) tryRegister(c *Client) {
406 404
 
407 405
 	rb := NewResponseBuffer(c)
408 406
 	nickAssigned := performNickChange(server, c, c, preregNick, rb)
409
-	rb.Send()
407
+	rb.Send(true)
410 408
 	if !nickAssigned {
411 409
 		c.SetPreregNick("")
412 410
 		return
@@ -446,7 +444,7 @@ func (server *Server) tryRegister(c *Client) {
446 444
 	rb = NewResponseBuffer(c)
447 445
 	c.RplISupport(rb)
448 446
 	server.MOTD(c, rb)
449
-	rb.Send()
447
+	rb.Send(true)
450 448
 
451 449
 	modestring := c.ModeString()
452 450
 	if modestring != "+" {

+ 8
- 3
irc/utils/crypto.go Parādīt failu

@@ -6,7 +6,12 @@ package utils
6 6
 import (
7 7
 	"crypto/rand"
8 8
 	"crypto/subtle"
9
-	"encoding/hex"
9
+	"encoding/base32"
10
+)
11
+
12
+var (
13
+	// standard b32 alphabet, but in lowercase for silly aesthetic reasons
14
+	b32encoder = base32.NewEncoding("abcdefghijklmnopqrstuvwxyz234567").WithPadding(base32.NoPadding)
10 15
 )
11 16
 
12 17
 // generate a secret token that cannot be brute-forced via online attacks
@@ -14,8 +19,8 @@ func GenerateSecretToken() string {
14 19
 	// 128 bits of entropy are enough to resist any online attack:
15 20
 	var buf [16]byte
16 21
 	rand.Read(buf[:])
17
-	// 32 ASCII characters, should be fine for most purposes
18
-	return hex.EncodeToString(buf[:])
22
+	// 26 ASCII characters, should be fine for most purposes
23
+	return b32encoder.EncodeToString(buf[:])
19 24
 }
20 25
 
21 26
 // securely check if a supplied token matches a stored token

+ 7
- 1
irc/utils/crypto_test.go Parādīt failu

@@ -16,7 +16,7 @@ const (
16 16
 
17 17
 func TestGenerateSecretToken(t *testing.T) {
18 18
 	token := GenerateSecretToken()
19
-	if len(token) != 32 {
19
+	if len(token) < 22 {
20 20
 		t.Errorf("bad token: %v", token)
21 21
 	}
22 22
 }
@@ -46,3 +46,9 @@ func TestTokenCompare(t *testing.T) {
46 46
 		t.Error("the empty token should not match anything")
47 47
 	}
48 48
 }
49
+
50
+func BenchmarkGenerateSecretToken(b *testing.B) {
51
+	for i := 0; i < b.N; i++ {
52
+		GenerateSecretToken()
53
+	}
54
+}

+ 16
- 12
oragono.go Parādīt failu

@@ -51,17 +51,7 @@ Options:
51 51
 
52 52
 	arguments, _ := docopt.ParseArgs(usage, nil, version)
53 53
 
54
-	configfile := arguments["--conf"].(string)
55
-	config, err := irc.LoadConfig(configfile)
56
-	if err != nil {
57
-		log.Fatal("Config file did not load successfully: ", err.Error())
58
-	}
59
-
60
-	logman, err := logger.NewManager(config.Logging)
61
-	if err != nil {
62
-		log.Fatal("Logger did not load successfully:", err.Error())
63
-	}
64
-
54
+	// don't require a config file for genpasswd
65 55
 	if arguments["genpasswd"].(bool) {
66 56
 		fmt.Print("Enter Password: ")
67 57
 		password := getPassword()
@@ -77,7 +67,21 @@ Options:
77 67
 			log.Fatal("encoding error:", err.Error())
78 68
 		}
79 69
 		fmt.Println(string(hash))
80
-	} else if arguments["initdb"].(bool) {
70
+		return
71
+	}
72
+
73
+	configfile := arguments["--conf"].(string)
74
+	config, err := irc.LoadConfig(configfile)
75
+	if err != nil {
76
+		log.Fatal("Config file did not load successfully: ", err.Error())
77
+	}
78
+
79
+	logman, err := logger.NewManager(config.Logging)
80
+	if err != nil {
81
+		log.Fatal("Logger did not load successfully:", err.Error())
82
+	}
83
+
84
+	if arguments["initdb"].(bool) {
81 85
 		irc.InitDB(config.Datastore.Path)
82 86
 		if !arguments["--quiet"].(bool) {
83 87
 			log.Println("database initialized: ", config.Datastore.Path)

+ 3
- 0
oragono.yaml Parādīt failu

@@ -454,3 +454,6 @@ history:
454 454
 
455 455
     # how many direct messages and notices should be tracked per user?
456 456
     client-length: 64
457
+
458
+    # number of messages to automatically play back on channel join (0 to disable):
459
+    autoreplay-on-join: 0

Notiek ielāde…
Atcelt
Saglabāt