Browse Source

Merge pull request #320 from slingamn/replay.1

history replay enhancements
tags/v1.0.0-rc1
Daniel Oaks 5 years ago
parent
commit
cd339281e4
No account linked to committer's email address
18 changed files with 262 additions and 253 deletions
  1. 1
    1
      README.md
  2. 0
    78
      irc/batch.go
  3. 63
    43
      irc/channel.go
  4. 11
    9
      irc/client.go
  5. 1
    1
      irc/commands.go
  6. 4
    3
      irc/config.go
  7. 1
    7
      irc/getters.go
  8. 12
    8
      irc/handlers.go
  9. 43
    30
      irc/history/history.go
  10. 2
    1
      irc/history/history_test.go
  11. 1
    1
      irc/modes.go
  12. 1
    1
      irc/nickname.go
  13. 86
    50
      irc/responsebuffer.go
  14. 2
    4
      irc/server.go
  15. 8
    3
      irc/utils/crypto.go
  16. 7
    1
      irc/utils/crypto_test.go
  17. 16
    12
      oragono.go
  18. 3
    0
      oragono.yaml

+ 1
- 1
README.md View File

129
 * Niels Freier, added WebSocket support to Ergonomadic, <https://github.com/stumpyfr>
129
 * Niels Freier, added WebSocket support to Ergonomadic, <https://github.com/stumpyfr>
130
 * Daniel Oakley, maintainer of Oragono, <https://github.com/DanielOaks>
130
 * Daniel Oakley, maintainer of Oragono, <https://github.com/DanielOaks>
131
 * Euan Kemp, contributor to Oragono and lots of useful fixes, <https://github.com/euank>
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
 * James Mills, contributed Docker support, <https://github.com/prologic>
133
 * James Mills, contributed Docker support, <https://github.com/prologic>
134
 * Vegax, implementing some commands and helping when Oragono was just getting started, <https://github.com/vegax87>
134
 * Vegax, implementing some commands and helping when Oragono was just getting started, <https://github.com/vegax87>
135
 * Sean Enck, transitioned us from using a custom script to a proper Makefile, <https://github.com/enckse>
135
 * Sean Enck, transitioned us from using a custom script to a proper Makefile, <https://github.com/enckse>

+ 0
- 78
irc/batch.go View File

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 View File

38
 	topic             string
38
 	topic             string
39
 	topicSetBy        string
39
 	topicSetBy        string
40
 	topicSetTime      time.Time
40
 	topicSetTime      time.Time
41
-	userLimit         uint64
41
+	userLimit         int
42
 	accountToUMode    map[string]modes.Mode
42
 	accountToUMode    map[string]modes.Mode
43
 	history           history.Buffer
43
 	history           history.Buffer
44
 }
44
 }
332
 		result = append(result, channel.key)
332
 		result = append(result, channel.key)
333
 	}
333
 	}
334
 	if showUserLimit {
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
 	return
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
 func (channel *Channel) IsEmpty() bool {
341
 func (channel *Channel) IsEmpty() bool {
355
 	channel.stateMutex.RLock()
342
 	channel.stateMutex.RLock()
356
 	defer channel.stateMutex.RUnlock()
343
 	defer channel.stateMutex.RUnlock()
359
 
346
 
360
 // Join joins the given client to this channel (if they can be joined).
347
 // Join joins the given client to this channel (if they can be joined).
361
 func (channel *Channel) Join(client *Client, key string, isSajoin bool, rb *ResponseBuffer) {
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
 	channel.stateMutex.RLock()
349
 	channel.stateMutex.RLock()
368
 	chname := channel.name
350
 	chname := channel.name
369
 	chcfname := channel.nameCasefolded
351
 	chcfname := channel.nameCasefolded
370
 	founder := channel.registeredFounder
352
 	founder := channel.registeredFounder
353
+	chkey := channel.key
354
+	limit := channel.userLimit
355
+	chcount := len(channel.members)
356
+	_, alreadyJoined := channel.members[client]
371
 	channel.stateMutex.RUnlock()
357
 	channel.stateMutex.RUnlock()
358
+
359
+	if alreadyJoined {
360
+		// no message needs to be sent
361
+		return
362
+	}
363
+
372
 	account := client.Account()
364
 	account := client.Account()
373
 	nickMaskCasefolded := client.NickMaskCasefolded()
365
 	nickMaskCasefolded := client.NickMaskCasefolded()
374
 	hasPrivs := isSajoin || (founder != "" && founder == account)
366
 	hasPrivs := isSajoin || (founder != "" && founder == account)
375
 
367
 
376
-	if !hasPrivs && channel.IsFull() {
368
+	if !hasPrivs && limit != 0 && chcount >= limit {
377
 		rb.Add(nil, client.server.name, ERR_CHANNELISFULL, chname, fmt.Sprintf(client.t("Cannot join channel (+%s)"), "l"))
369
 		rb.Add(nil, client.server.name, ERR_CHANNELISFULL, chname, fmt.Sprintf(client.t("Cannot join channel (+%s)"), "l"))
378
 		return
370
 		return
379
 	}
371
 	}
380
 
372
 
381
-	if !hasPrivs && !channel.CheckKey(key) {
373
+	if !hasPrivs && chkey != "" && !utils.SecretTokensMatch(chkey, key) {
382
 		rb.Add(nil, client.server.name, ERR_BADCHANNELKEY, chname, fmt.Sprintf(client.t("Cannot join channel (+%s)"), "k"))
374
 		rb.Add(nil, client.server.name, ERR_BADCHANNELKEY, chname, fmt.Sprintf(client.t("Cannot join channel (+%s)"), "k"))
383
 		return
375
 		return
384
 	}
376
 	}
469
 		Type:        history.Join,
461
 		Type:        history.Join,
470
 		Nick:        nickmask,
462
 		Nick:        nickmask,
471
 		AccountName: accountName,
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
 // Part parts the given client from this channel, with the given message.
478
 // Part parts the given client from this channel, with the given message.
506
 	now := time.Now()
509
 	now := time.Now()
507
 	channel.resumeAndAnnounce(newClient, oldClient)
510
 	channel.resumeAndAnnounce(newClient, oldClient)
508
 	if !timestamp.IsZero() {
511
 	if !timestamp.IsZero() {
509
-		channel.replayHistory(newClient, timestamp, now)
512
+		channel.replayHistoryForResume(newClient, timestamp, now)
510
 	}
513
 	}
511
 }
514
 }
512
 
515
 
560
 
563
 
561
 	rb := NewResponseBuffer(newClient)
564
 	rb := NewResponseBuffer(newClient)
562
 	// use blocking i/o to synchronize with the later history replay
565
 	// use blocking i/o to synchronize with the later history replay
563
-	rb.SetBlocking(true)
564
 	if newClient.capabilities.Has(caps.ExtendedJoin) {
566
 	if newClient.capabilities.Has(caps.ExtendedJoin) {
565
 		rb.Add(nil, nickMask, "JOIN", channel.name, accountName, realName)
567
 		rb.Add(nil, nickMask, "JOIN", channel.name, accountName, realName)
566
 	} else {
568
 	} else {
571
 	if 0 < len(oldModes) {
573
 	if 0 < len(oldModes) {
572
 		rb.Add(nil, newClient.server.name, "MODE", channel.name, oldModes, nick)
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
 	chname := channel.Name()
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
 	for _, item := range items {
596
 	for _, item := range items {
597
+		var tags Tags
598
+		if serverTime {
599
+			tags = ensureTag(tags, "time", item.Time.Format(IRCv3TimestampFormat))
600
+		}
601
+
583
 		switch item.Type {
602
 		switch item.Type {
584
 		case history.Privmsg:
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
 		case history.Notice:
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
 		case history.Join:
607
 		case history.Join:
589
 			if extendedJoin {
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
 			} else {
611
 			} else {
592
-				newClient.sendInternal(true, item.Time, nil, item.Nick, "JOIN", chname)
612
+				rb.Add(tags, item.Nick, "JOIN", chname)
593
 			}
613
 			}
594
 		case history.Quit:
614
 		case history.Quit:
595
 			// XXX: send QUIT as PART to avoid having to correctly deduplicate and synchronize
615
 			// XXX: send QUIT as PART to avoid having to correctly deduplicate and synchronize
596
 			// QUIT messages across channels
616
 			// QUIT messages across channels
597
 			fallthrough
617
 			fallthrough
598
 		case history.Part:
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
 		case history.Kick:
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
 // SendTopic sends the channel topic to the given client.
627
 // SendTopic sends the channel topic to the given client.
707
 			messageTagsToUse = clientOnlyTags
723
 			messageTagsToUse = clientOnlyTags
708
 		}
724
 		}
709
 
725
 
726
+		nickMaskString := client.NickMaskString()
727
+		accountName := client.AccountName()
710
 		if message == nil {
728
 		if message == nil {
711
-			rb.AddFromClient(msgid, client, messageTagsToUse, cmd, channel.name)
729
+			rb.AddFromClient(msgid, nickMaskString, accountName, messageTagsToUse, cmd, channel.name)
712
 		} else {
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
 	for _, member := range channel.Members() {
734
 	for _, member := range channel.Members() {
773
 		if client.capabilities.Has(caps.MessageTags) {
791
 		if client.capabilities.Has(caps.MessageTags) {
774
 			tagsToUse = clientOnlyTags
792
 			tagsToUse = clientOnlyTags
775
 		}
793
 		}
794
+		nickMaskString := client.NickMaskString()
795
+		accountName := client.AccountName()
776
 		if message == nil {
796
 		if message == nil {
777
-			rb.AddFromClient(msgid, client, tagsToUse, cmd, channel.name)
797
+			rb.AddFromClient(msgid, nickMaskString, accountName, tagsToUse, cmd, channel.name)
778
 		} else {
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 View File

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
 	lastDiscarded := oldClient.history.LastDiscarded()
453
 	lastDiscarded := oldClient.history.LastDiscarded()
451
 	if lastDiscarded.Before(oldestLostMessage) {
454
 	if lastDiscarded.Before(oldestLostMessage) {
452
 		oldestLostMessage = lastDiscarded
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
 	}
484
 	}
483
 
485
 
484
 	if client.resumeDetails.HistoryIncomplete {
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
 	// after we send the rest of the registration burst, we'll try rejoining channels
492
 	// after we send the rest of the registration burst, we'll try rejoining channels
491
 }
493
 }

+ 1
- 1
irc/commands.go View File

47
 	rb := NewResponseBuffer(client)
47
 	rb := NewResponseBuffer(client)
48
 	rb.Label = GetLabel(msg)
48
 	rb.Label = GetLabel(msg)
49
 	exiting := cmd.handler(server, client, msg, rb)
49
 	exiting := cmd.handler(server, client, msg, rb)
50
-	rb.Send()
50
+	rb.Send(true)
51
 
51
 
52
 	// after each command, see if we can send registration to the client
52
 	// after each command, see if we can send registration to the client
53
 	if !client.registered {
53
 	if !client.registered {

+ 4
- 3
irc/config.go View File

268
 	Fakelag FakelagConfig
268
 	Fakelag FakelagConfig
269
 
269
 
270
 	History struct {
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
 	Filename string
277
 	Filename string

+ 1
- 7
irc/getters.go View File

259
 	return channel.membersCache
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
 	channel.stateMutex.Lock()
263
 	channel.stateMutex.Lock()
270
 	channel.userLimit = limit
264
 	channel.userLimit = limit
271
 	channel.stateMutex.Unlock()
265
 	channel.stateMutex.Unlock()

+ 12
- 8
irc/handlers.go View File

933
 		server.channels.Join(target, chname, "", true, rb)
933
 		server.channels.Join(target, chname, "", true, rb)
934
 	}
934
 	}
935
 	if client != target {
935
 	if client != target {
936
-		rb.Send()
936
+		rb.Send(false)
937
 	}
937
 	}
938
 	return false
938
 	return false
939
 }
939
 }
1706
 			if !user.HasMode(modes.RegisteredOnly) || client.LoggedIntoAccount() {
1706
 			if !user.HasMode(modes.RegisteredOnly) || client.LoggedIntoAccount() {
1707
 				user.SendSplitMsgFromClient(msgid, client, clientOnlyTags, "NOTICE", user.nick, splitMsg)
1707
 				user.SendSplitMsgFromClient(msgid, client, clientOnlyTags, "NOTICE", user.nick, splitMsg)
1708
 			}
1708
 			}
1709
+			nickMaskString := client.NickMaskString()
1710
+			accountName := client.AccountName()
1709
 			if client.capabilities.Has(caps.EchoMessage) {
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
 			user.history.Add(history.Item{
1715
 			user.history.Add(history.Item{
1714
 				Type:        history.Notice,
1716
 				Type:        history.Notice,
1715
 				Msgid:       msgid,
1717
 				Msgid:       msgid,
1716
 				Message:     splitMsg,
1718
 				Message:     splitMsg,
1717
-				Nick:        client.NickMaskString(),
1718
-				AccountName: client.AccountName(),
1719
+				Nick:        nickMaskString,
1720
+				AccountName: accountName,
1719
 			})
1721
 			})
1720
 		}
1722
 		}
1721
 	}
1723
 	}
1916
 			if !user.HasMode(modes.RegisteredOnly) || client.LoggedIntoAccount() {
1918
 			if !user.HasMode(modes.RegisteredOnly) || client.LoggedIntoAccount() {
1917
 				user.SendSplitMsgFromClient(msgid, client, clientOnlyTags, "PRIVMSG", user.nick, splitMsg)
1919
 				user.SendSplitMsgFromClient(msgid, client, clientOnlyTags, "PRIVMSG", user.nick, splitMsg)
1918
 			}
1920
 			}
1921
+			nickMaskString := client.NickMaskString()
1922
+			accountName := client.AccountName()
1919
 			if client.capabilities.Has(caps.EchoMessage) {
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
 			if user.HasMode(modes.Away) {
1926
 			if user.HasMode(modes.Away) {
1923
 				//TODO(dan): possibly implement cooldown of away notifications to users
1927
 				//TODO(dan): possibly implement cooldown of away notifications to users
1928
 				Type:        history.Privmsg,
1932
 				Type:        history.Privmsg,
1929
 				Msgid:       msgid,
1933
 				Msgid:       msgid,
1930
 				Message:     splitMsg,
1934
 				Message:     splitMsg,
1931
-				Nick:        client.NickMaskString(),
1932
-				AccountName: client.AccountName(),
1935
+				Nick:        nickMaskString,
1936
+				AccountName: accountName,
1933
 			})
1937
 			})
1934
 		}
1938
 		}
1935
 	}
1939
 	}
2150
 			}
2154
 			}
2151
 			user.SendFromClient(msgid, client, clientOnlyTags, "TAGMSG", user.nick)
2155
 			user.SendFromClient(msgid, client, clientOnlyTags, "TAGMSG", user.nick)
2152
 			if client.capabilities.Has(caps.EchoMessage) {
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
 			if user.HasMode(modes.Away) {
2159
 			if user.HasMode(modes.Away) {
2156
 				//TODO(dan): possibly implement cooldown of away notifications to users
2160
 				//TODO(dan): possibly implement cooldown of away notifications to users

+ 43
- 30
irc/history/history.go View File

32
 	// this is the uncasefolded account name, if there's no account it should be set to "*"
32
 	// this is the uncasefolded account name, if there's no account it should be set to "*"
33
 	AccountName string
33
 	AccountName string
34
 	Message     utils.SplitMessage
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
 // Buffer is a ring buffer holding message/event history for a channel or user
41
 // Buffer is a ring buffer holding message/event history for a channel or user
39
 type Buffer struct {
42
 type Buffer struct {
40
 	sync.RWMutex
43
 	sync.RWMutex
85
 	}
88
 	}
86
 
89
 
87
 	if item.Time.IsZero() {
90
 	if item.Time.IsZero() {
88
-		item.Time = time.Now()
91
+		item.Time = time.Now().UTC()
89
 	}
92
 	}
90
 
93
 
91
 	list.Lock()
94
 	list.Lock()
112
 	list.buffer[pos] = item
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
 // Between returns all history items with a time `after` <= time <= `before`,
124
 // Between returns all history items with a time `after` <= time <= `before`,
116
 // with an indication of whether the results are complete or are missing items
125
 // with an indication of whether the results are complete or are missing items
117
 // because some of that period was discarded. A zero value of `before` is considered
126
 // because some of that period was discarded. A zero value of `before` is considered
126
 
135
 
127
 	complete = after.Equal(list.lastDiscarded) || after.After(list.lastDiscarded)
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
 	pos := list.prev(list.end)
166
 	pos := list.prev(list.end)
141
 	for {
167
 	for {
142
-		if satisfies(list.buffer[pos].Time) {
168
+		if predicate(list.buffer[pos]) {
143
 			results = append(results, list.buffer[pos])
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
 			break
172
 			break
147
 		}
173
 		}
148
 		pos = list.prev(pos)
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
 	return
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
 // LastDiscarded returns the latest time of any entry that was evicted
189
 // LastDiscarded returns the latest time of any entry that was evicted

+ 2
- 1
irc/history/history_test.go View File

67
 	if since[0].Nick != "testnick2" {
67
 	if since[0].Nick != "testnick2" {
68
 		t.Error("retrieved junk data")
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
 func toNicks(items []Item) (result []string) {
74
 func toNicks(items []Item) (result []string) {

+ 1
- 1
irc/modes.go View File

189
 		case modes.UserLimit:
189
 		case modes.UserLimit:
190
 			switch change.Op {
190
 			switch change.Op {
191
 			case modes.Add:
191
 			case modes.Add:
192
-				val, err := strconv.ParseUint(change.Arg, 10, 64)
192
+				val, err := strconv.Atoi(change.Arg)
193
 				if err == nil {
193
 				if err == nil {
194
 					channel.setUserLimit(val)
194
 					channel.setUserLimit(val)
195
 					applied = append(applied, change)
195
 					applied = append(applied, change)

+ 1
- 1
irc/nickname.go View File

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

+ 86
- 50
irc/responsebuffer.go View File

4
 package irc
4
 package irc
5
 
5
 
6
 import (
6
 import (
7
+	"runtime/debug"
7
 	"time"
8
 	"time"
8
 
9
 
9
 	"github.com/goshuirc/irc-go/ircmsg"
10
 	"github.com/goshuirc/irc-go/ircmsg"
11
 	"github.com/oragono/oragono/irc/utils"
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
 // ResponseBuffer - put simply - buffers messages and then outputs them to a given client.
20
 // ResponseBuffer - put simply - buffers messages and then outputs them to a given client.
15
 //
21
 //
16
 // Using a ResponseBuffer lets you really easily implement labeled-response, since the
22
 // Using a ResponseBuffer lets you really easily implement labeled-response, since the
17
 // buffer will silently create a batch if required and label the outgoing messages as
23
 // buffer will silently create a batch if required and label the outgoing messages as
18
 // necessary (or leave it off and simply tag the outgoing message).
24
 // necessary (or leave it off and simply tag the outgoing message).
19
 type ResponseBuffer struct {
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
 // GetLabel returns the label from the given message.
33
 // GetLabel returns the label from the given message.
35
 	}
42
 	}
36
 }
43
 }
37
 
44
 
38
-func (rb *ResponseBuffer) SetBlocking(blocking bool) {
39
-	rb.blocking = blocking
40
-}
41
-
42
 // Add adds a standard new message to our queue.
45
 // Add adds a standard new message to our queue.
43
 func (rb *ResponseBuffer) Add(tags *map[string]ircmsg.TagValue, prefix string, command string, params ...string) {
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
 	rb.messages = append(rb.messages, message)
54
 	rb.messages = append(rb.messages, message)
47
 }
55
 }
48
 
56
 
49
 // AddFromClient adds a new message from a specific client to our queue.
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
 	// attach account-tag
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
 	// attach message-id
65
 	// attach message-id
60
 	if len(msgid) > 0 && rb.target.capabilities.Has(caps.MessageTags) {
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
 // AddSplitMessageFromClient adds a new split message from a specific client to our queue.
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
 	if rb.target.capabilities.Has(caps.MaxLine) || message.Wrapped == nil {
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
 	} else {
77
 	} else {
76
 		for _, str := range message.Wrapped {
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
 	// send each message out
141
 	// send each message out
109
 	for _, message := range rb.messages {
142
 	for _, message := range rb.messages {
110
 		// attach server-time if needed
143
 		// attach server-time if needed
111
 		if rb.target.capabilities.Has(caps.ServerTime) {
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
 		// attach batch ID
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
 		// send message out
156
 		// send message out
122
-		rb.target.SendRawMessage(message, rb.blocking)
157
+		rb.target.SendRawMessage(message, blocking)
123
 	}
158
 	}
124
 
159
 
125
 	// end batch if required
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
 	// clear out any existing messages
166
 	// clear out any existing messages

+ 2
- 4
irc/server.go View File

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

+ 8
- 3
irc/utils/crypto.go View File

6
 import (
6
 import (
7
 	"crypto/rand"
7
 	"crypto/rand"
8
 	"crypto/subtle"
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
 // generate a secret token that cannot be brute-forced via online attacks
17
 // generate a secret token that cannot be brute-forced via online attacks
14
 	// 128 bits of entropy are enough to resist any online attack:
19
 	// 128 bits of entropy are enough to resist any online attack:
15
 	var buf [16]byte
20
 	var buf [16]byte
16
 	rand.Read(buf[:])
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
 // securely check if a supplied token matches a stored token
26
 // securely check if a supplied token matches a stored token

+ 7
- 1
irc/utils/crypto_test.go View File

16
 
16
 
17
 func TestGenerateSecretToken(t *testing.T) {
17
 func TestGenerateSecretToken(t *testing.T) {
18
 	token := GenerateSecretToken()
18
 	token := GenerateSecretToken()
19
-	if len(token) != 32 {
19
+	if len(token) < 22 {
20
 		t.Errorf("bad token: %v", token)
20
 		t.Errorf("bad token: %v", token)
21
 	}
21
 	}
22
 }
22
 }
46
 		t.Error("the empty token should not match anything")
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 View File

51
 
51
 
52
 	arguments, _ := docopt.ParseArgs(usage, nil, version)
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
 	if arguments["genpasswd"].(bool) {
55
 	if arguments["genpasswd"].(bool) {
66
 		fmt.Print("Enter Password: ")
56
 		fmt.Print("Enter Password: ")
67
 		password := getPassword()
57
 		password := getPassword()
77
 			log.Fatal("encoding error:", err.Error())
67
 			log.Fatal("encoding error:", err.Error())
78
 		}
68
 		}
79
 		fmt.Println(string(hash))
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
 		irc.InitDB(config.Datastore.Path)
85
 		irc.InitDB(config.Datastore.Path)
82
 		if !arguments["--quiet"].(bool) {
86
 		if !arguments["--quiet"].(bool) {
83
 			log.Println("database initialized: ", config.Datastore.Path)
87
 			log.Println("database initialized: ", config.Datastore.Path)

+ 3
- 0
oragono.yaml View File

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

Loading…
Cancel
Save