mirror of
https://github.com/ergochat/ergo.git
synced 2024-11-13 23:49:30 +01:00
history replay enhancements
This commit is contained in:
parent
f3d138d909
commit
2c7c8fbaf9
@ -129,7 +129,7 @@ Make sure to setup [SASL](https://freenode.net/kb/answer/sasl) in your client to
|
|||||||
* Niels Freier, added WebSocket support to Ergonomadic, <https://github.com/stumpyfr>
|
* Niels Freier, added WebSocket support to Ergonomadic, <https://github.com/stumpyfr>
|
||||||
* Daniel Oakley, maintainer of Oragono, <https://github.com/DanielOaks>
|
* Daniel Oakley, maintainer of Oragono, <https://github.com/DanielOaks>
|
||||||
* Euan Kemp, contributor to Oragono and lots of useful fixes, <https://github.com/euank>
|
* Euan Kemp, contributor to Oragono and lots of useful fixes, <https://github.com/euank>
|
||||||
* Shivaram Lingamneni, has contributed a ton of fixes, refactoring, and general improvements, <https://github.com/slingamn>
|
* Shivaram Lingamneni, co-maintainer of Oragono, <https://github.com/slingamn>
|
||||||
* James Mills, contributed Docker support, <https://github.com/prologic>
|
* James Mills, contributed Docker support, <https://github.com/prologic>
|
||||||
* Vegax, implementing some commands and helping when Oragono was just getting started, <https://github.com/vegax87>
|
* Vegax, implementing some commands and helping when Oragono was just getting started, <https://github.com/vegax87>
|
||||||
* Sean Enck, transitioned us from using a custom script to a proper Makefile, <https://github.com/enckse>
|
* Sean Enck, transitioned us from using a custom script to a proper Makefile, <https://github.com/enckse>
|
||||||
|
78
irc/batch.go
78
irc/batch.go
@ -1,78 +0,0 @@
|
|||||||
// Copyright (c) 2017 Daniel Oaks <daniel@danieloaks.net>
|
|
||||||
// released under the MIT license
|
|
||||||
|
|
||||||
package irc
|
|
||||||
|
|
||||||
import (
|
|
||||||
"strconv"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/goshuirc/irc-go/ircmsg"
|
|
||||||
"github.com/oragono/oragono/irc/caps"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
// maxBatchID is the maximum ID the batch counter can get to before it rotates.
|
|
||||||
//
|
|
||||||
// Batch IDs are made up of the current unix timestamp plus a rolling int ID that's
|
|
||||||
// incremented for every new batch. It's an alright solution and will work unless we get
|
|
||||||
// more than maxId batches per nanosecond. Later on when we have S2S linking, the batch
|
|
||||||
// ID will also contain the server ID to ensure they stay unique.
|
|
||||||
maxBatchID uint64 = 60000
|
|
||||||
)
|
|
||||||
|
|
||||||
// BatchManager helps generate new batches and new batch IDs.
|
|
||||||
type BatchManager struct {
|
|
||||||
idCounter uint64
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewBatchManager returns a new Manager.
|
|
||||||
func NewBatchManager() *BatchManager {
|
|
||||||
return &BatchManager{}
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewID returns a new batch ID that should be unique.
|
|
||||||
func (bm *BatchManager) NewID() string {
|
|
||||||
bm.idCounter++
|
|
||||||
if maxBatchID < bm.idCounter {
|
|
||||||
bm.idCounter = 0
|
|
||||||
}
|
|
||||||
|
|
||||||
return strconv.FormatInt(time.Now().UnixNano(), 36) + strconv.FormatUint(bm.idCounter, 36)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Batch represents an IRCv3 batch.
|
|
||||||
type Batch struct {
|
|
||||||
ID string
|
|
||||||
Type string
|
|
||||||
Params []string
|
|
||||||
}
|
|
||||||
|
|
||||||
// New returns a new batch.
|
|
||||||
func (bm *BatchManager) New(batchType string, params ...string) *Batch {
|
|
||||||
newBatch := Batch{
|
|
||||||
ID: bm.NewID(),
|
|
||||||
Type: batchType,
|
|
||||||
Params: params,
|
|
||||||
}
|
|
||||||
|
|
||||||
return &newBatch
|
|
||||||
}
|
|
||||||
|
|
||||||
// Start sends the batch start message to this client
|
|
||||||
func (b *Batch) Start(client *Client, tags *map[string]ircmsg.TagValue) {
|
|
||||||
if client.capabilities.Has(caps.Batch) {
|
|
||||||
params := []string{"+" + b.ID, b.Type}
|
|
||||||
for _, param := range b.Params {
|
|
||||||
params = append(params, param)
|
|
||||||
}
|
|
||||||
client.Send(tags, client.server.name, "BATCH", params...)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// End sends the batch end message to this client
|
|
||||||
func (b *Batch) End(client *Client) {
|
|
||||||
if client.capabilities.Has(caps.Batch) {
|
|
||||||
client.Send(nil, client.server.name, "BATCH", "-"+b.ID)
|
|
||||||
}
|
|
||||||
}
|
|
108
irc/channel.go
108
irc/channel.go
@ -38,7 +38,7 @@ type Channel struct {
|
|||||||
topic string
|
topic string
|
||||||
topicSetBy string
|
topicSetBy string
|
||||||
topicSetTime time.Time
|
topicSetTime time.Time
|
||||||
userLimit uint64
|
userLimit int
|
||||||
accountToUMode map[string]modes.Mode
|
accountToUMode map[string]modes.Mode
|
||||||
history history.Buffer
|
history history.Buffer
|
||||||
}
|
}
|
||||||
@ -332,25 +332,12 @@ func (channel *Channel) modeStrings(client *Client) (result []string) {
|
|||||||
result = append(result, channel.key)
|
result = append(result, channel.key)
|
||||||
}
|
}
|
||||||
if showUserLimit {
|
if showUserLimit {
|
||||||
result = append(result, strconv.FormatUint(channel.userLimit, 10))
|
result = append(result, strconv.Itoa(channel.userLimit))
|
||||||
}
|
}
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsFull returns true if this channel is at its' members limit.
|
|
||||||
func (channel *Channel) IsFull() bool {
|
|
||||||
channel.stateMutex.RLock()
|
|
||||||
defer channel.stateMutex.RUnlock()
|
|
||||||
return (channel.userLimit > 0) && (uint64(len(channel.members)) >= channel.userLimit)
|
|
||||||
}
|
|
||||||
|
|
||||||
// CheckKey returns true if the key is not set or matches the given key.
|
|
||||||
func (channel *Channel) CheckKey(key string) bool {
|
|
||||||
chkey := channel.Key()
|
|
||||||
return chkey == "" || utils.SecretTokensMatch(chkey, key)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (channel *Channel) IsEmpty() bool {
|
func (channel *Channel) IsEmpty() bool {
|
||||||
channel.stateMutex.RLock()
|
channel.stateMutex.RLock()
|
||||||
defer channel.stateMutex.RUnlock()
|
defer channel.stateMutex.RUnlock()
|
||||||
@ -359,26 +346,31 @@ func (channel *Channel) IsEmpty() bool {
|
|||||||
|
|
||||||
// Join joins the given client to this channel (if they can be joined).
|
// Join joins the given client to this channel (if they can be joined).
|
||||||
func (channel *Channel) Join(client *Client, key string, isSajoin bool, rb *ResponseBuffer) {
|
func (channel *Channel) Join(client *Client, key string, isSajoin bool, rb *ResponseBuffer) {
|
||||||
if channel.hasClient(client) {
|
|
||||||
// already joined, no message needs to be sent
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
channel.stateMutex.RLock()
|
channel.stateMutex.RLock()
|
||||||
chname := channel.name
|
chname := channel.name
|
||||||
chcfname := channel.nameCasefolded
|
chcfname := channel.nameCasefolded
|
||||||
founder := channel.registeredFounder
|
founder := channel.registeredFounder
|
||||||
|
chkey := channel.key
|
||||||
|
limit := channel.userLimit
|
||||||
|
chcount := len(channel.members)
|
||||||
|
_, alreadyJoined := channel.members[client]
|
||||||
channel.stateMutex.RUnlock()
|
channel.stateMutex.RUnlock()
|
||||||
|
|
||||||
|
if alreadyJoined {
|
||||||
|
// no message needs to be sent
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
account := client.Account()
|
account := client.Account()
|
||||||
nickMaskCasefolded := client.NickMaskCasefolded()
|
nickMaskCasefolded := client.NickMaskCasefolded()
|
||||||
hasPrivs := isSajoin || (founder != "" && founder == account)
|
hasPrivs := isSajoin || (founder != "" && founder == account)
|
||||||
|
|
||||||
if !hasPrivs && channel.IsFull() {
|
if !hasPrivs && limit != 0 && chcount >= limit {
|
||||||
rb.Add(nil, client.server.name, ERR_CHANNELISFULL, chname, fmt.Sprintf(client.t("Cannot join channel (+%s)"), "l"))
|
rb.Add(nil, client.server.name, ERR_CHANNELISFULL, chname, fmt.Sprintf(client.t("Cannot join channel (+%s)"), "l"))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if !hasPrivs && !channel.CheckKey(key) {
|
if !hasPrivs && chkey != "" && !utils.SecretTokensMatch(chkey, key) {
|
||||||
rb.Add(nil, client.server.name, ERR_BADCHANNELKEY, chname, fmt.Sprintf(client.t("Cannot join channel (+%s)"), "k"))
|
rb.Add(nil, client.server.name, ERR_BADCHANNELKEY, chname, fmt.Sprintf(client.t("Cannot join channel (+%s)"), "k"))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -469,7 +461,18 @@ func (channel *Channel) Join(client *Client, key string, isSajoin bool, rb *Resp
|
|||||||
Type: history.Join,
|
Type: history.Join,
|
||||||
Nick: nickmask,
|
Nick: nickmask,
|
||||||
AccountName: accountName,
|
AccountName: accountName,
|
||||||
|
Msgid: realname,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// TODO #259 can be implemented as Flush(false) (i.e., nonblocking) while holding joinPartMutex
|
||||||
|
rb.Flush(true)
|
||||||
|
|
||||||
|
replayLimit := channel.server.Config().History.AutoreplayOnJoin
|
||||||
|
if replayLimit > 0 {
|
||||||
|
items := channel.history.Latest(replayLimit)
|
||||||
|
channel.replayHistoryItems(rb, items)
|
||||||
|
rb.Flush(true)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Part parts the given client from this channel, with the given message.
|
// 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
|
|||||||
now := time.Now()
|
now := time.Now()
|
||||||
channel.resumeAndAnnounce(newClient, oldClient)
|
channel.resumeAndAnnounce(newClient, oldClient)
|
||||||
if !timestamp.IsZero() {
|
if !timestamp.IsZero() {
|
||||||
channel.replayHistory(newClient, timestamp, now)
|
channel.replayHistoryForResume(newClient, timestamp, now)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -560,7 +563,6 @@ func (channel *Channel) resumeAndAnnounce(newClient, oldClient *Client) {
|
|||||||
|
|
||||||
rb := NewResponseBuffer(newClient)
|
rb := NewResponseBuffer(newClient)
|
||||||
// use blocking i/o to synchronize with the later history replay
|
// use blocking i/o to synchronize with the later history replay
|
||||||
rb.SetBlocking(true)
|
|
||||||
if newClient.capabilities.Has(caps.ExtendedJoin) {
|
if newClient.capabilities.Has(caps.ExtendedJoin) {
|
||||||
rb.Add(nil, nickMask, "JOIN", channel.name, accountName, realName)
|
rb.Add(nil, nickMask, "JOIN", channel.name, accountName, realName)
|
||||||
} else {
|
} else {
|
||||||
@ -571,41 +573,55 @@ func (channel *Channel) resumeAndAnnounce(newClient, oldClient *Client) {
|
|||||||
if 0 < len(oldModes) {
|
if 0 < len(oldModes) {
|
||||||
rb.Add(nil, newClient.server.name, "MODE", channel.name, oldModes, nick)
|
rb.Add(nil, newClient.server.name, "MODE", channel.name, oldModes, nick)
|
||||||
}
|
}
|
||||||
rb.Send()
|
rb.Send(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (channel *Channel) replayHistory(newClient *Client, after time.Time, before time.Time) {
|
func (channel *Channel) replayHistoryForResume(newClient *Client, after time.Time, before time.Time) {
|
||||||
chname := channel.Name()
|
|
||||||
extendedJoin := newClient.capabilities.Has(caps.ExtendedJoin)
|
|
||||||
|
|
||||||
items, complete := channel.history.Between(after, before)
|
items, complete := channel.history.Between(after, before)
|
||||||
|
rb := NewResponseBuffer(newClient)
|
||||||
|
channel.replayHistoryItems(rb, items)
|
||||||
|
if !complete && !newClient.resumeDetails.HistoryIncomplete {
|
||||||
|
// warn here if we didn't warn already
|
||||||
|
rb.Add(nil, "HistServ", "NOTICE", channel.Name(), newClient.t("Some additional message history may have been lost"))
|
||||||
|
}
|
||||||
|
rb.Send(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (channel *Channel) replayHistoryItems(rb *ResponseBuffer, items []history.Item) {
|
||||||
|
chname := channel.Name()
|
||||||
|
client := rb.target
|
||||||
|
extendedJoin := client.capabilities.Has(caps.ExtendedJoin)
|
||||||
|
serverTime := client.capabilities.Has(caps.ServerTime)
|
||||||
|
|
||||||
for _, item := range items {
|
for _, item := range items {
|
||||||
|
var tags Tags
|
||||||
|
if serverTime {
|
||||||
|
tags = ensureTag(tags, "time", item.Time.Format(IRCv3TimestampFormat))
|
||||||
|
}
|
||||||
|
|
||||||
switch item.Type {
|
switch item.Type {
|
||||||
case history.Privmsg:
|
case history.Privmsg:
|
||||||
newClient.sendSplitMsgFromClientInternal(true, item.Time, item.Msgid, item.Nick, item.AccountName, nil, "PRIVMSG", chname, item.Message)
|
rb.AddSplitMessageFromClient(item.Msgid, item.Nick, item.AccountName, tags, "PRIVMSG", chname, item.Message)
|
||||||
case history.Notice:
|
case history.Notice:
|
||||||
newClient.sendSplitMsgFromClientInternal(true, item.Time, item.Msgid, item.Nick, item.AccountName, nil, "NOTICE", chname, item.Message)
|
rb.AddSplitMessageFromClient(item.Msgid, item.Nick, item.AccountName, tags, "NOTICE", chname, item.Message)
|
||||||
case history.Join:
|
case history.Join:
|
||||||
if extendedJoin {
|
if extendedJoin {
|
||||||
newClient.sendInternal(true, item.Time, nil, item.Nick, "JOIN", chname, item.AccountName, "")
|
// XXX Msgid is the realname in this case
|
||||||
|
rb.Add(tags, item.Nick, "JOIN", chname, item.AccountName, item.Msgid)
|
||||||
} else {
|
} else {
|
||||||
newClient.sendInternal(true, item.Time, nil, item.Nick, "JOIN", chname)
|
rb.Add(tags, item.Nick, "JOIN", chname)
|
||||||
}
|
}
|
||||||
case history.Quit:
|
case history.Quit:
|
||||||
// XXX: send QUIT as PART to avoid having to correctly deduplicate and synchronize
|
// XXX: send QUIT as PART to avoid having to correctly deduplicate and synchronize
|
||||||
// QUIT messages across channels
|
// QUIT messages across channels
|
||||||
fallthrough
|
fallthrough
|
||||||
case history.Part:
|
case history.Part:
|
||||||
newClient.sendInternal(true, item.Time, nil, item.Nick, "PART", chname, item.Message.Original)
|
rb.Add(tags, item.Nick, "PART", chname, item.Message.Original)
|
||||||
case history.Kick:
|
case history.Kick:
|
||||||
newClient.sendInternal(true, item.Time, nil, item.Nick, "KICK", chname, item.Msgid, item.Message.Original)
|
// XXX Msgid is the kick target
|
||||||
|
rb.Add(tags, item.Nick, "KICK", chname, item.Msgid, item.Message.Original)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if !complete && !newClient.resumeDetails.HistoryIncomplete {
|
|
||||||
// warn here if we didn't warn already
|
|
||||||
newClient.sendInternal(true, time.Time{}, nil, "HistServ", "NOTICE", chname, newClient.t("Some additional message history may have been lost"))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// SendTopic sends the channel topic to the given client.
|
// SendTopic sends the channel topic to the given client.
|
||||||
@ -707,10 +723,12 @@ func (channel *Channel) sendMessage(msgid, cmd string, requiredCaps []caps.Capab
|
|||||||
messageTagsToUse = clientOnlyTags
|
messageTagsToUse = clientOnlyTags
|
||||||
}
|
}
|
||||||
|
|
||||||
|
nickMaskString := client.NickMaskString()
|
||||||
|
accountName := client.AccountName()
|
||||||
if message == nil {
|
if message == nil {
|
||||||
rb.AddFromClient(msgid, client, messageTagsToUse, cmd, channel.name)
|
rb.AddFromClient(msgid, nickMaskString, accountName, messageTagsToUse, cmd, channel.name)
|
||||||
} else {
|
} else {
|
||||||
rb.AddFromClient(msgid, client, messageTagsToUse, cmd, channel.name, *message)
|
rb.AddFromClient(msgid, nickMaskString, accountName, messageTagsToUse, cmd, channel.name, *message)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for _, member := range channel.Members() {
|
for _, member := range channel.Members() {
|
||||||
@ -773,10 +791,12 @@ func (channel *Channel) sendSplitMessage(msgid, cmd string, histType history.Ite
|
|||||||
if client.capabilities.Has(caps.MessageTags) {
|
if client.capabilities.Has(caps.MessageTags) {
|
||||||
tagsToUse = clientOnlyTags
|
tagsToUse = clientOnlyTags
|
||||||
}
|
}
|
||||||
|
nickMaskString := client.NickMaskString()
|
||||||
|
accountName := client.AccountName()
|
||||||
if message == nil {
|
if message == nil {
|
||||||
rb.AddFromClient(msgid, client, tagsToUse, cmd, channel.name)
|
rb.AddFromClient(msgid, nickMaskString, accountName, tagsToUse, cmd, channel.name)
|
||||||
} else {
|
} else {
|
||||||
rb.AddSplitMessageFromClient(msgid, client, tagsToUse, cmd, channel.name, *message)
|
rb.AddSplitMessageFromClient(msgid, nickMaskString, accountName, tagsToUse, cmd, channel.name, *message)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -446,17 +446,19 @@ func (client *Client) TryResume() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
personalHistory := oldClient.history.All()
|
privmsgMatcher := func(item history.Item) bool {
|
||||||
|
return item.Type == history.Privmsg || item.Type == history.Notice
|
||||||
|
}
|
||||||
|
privmsgHistory := oldClient.history.Match(privmsgMatcher, 0)
|
||||||
lastDiscarded := oldClient.history.LastDiscarded()
|
lastDiscarded := oldClient.history.LastDiscarded()
|
||||||
if lastDiscarded.Before(oldestLostMessage) {
|
if lastDiscarded.Before(oldestLostMessage) {
|
||||||
oldestLostMessage = lastDiscarded
|
oldestLostMessage = lastDiscarded
|
||||||
}
|
}
|
||||||
for _, item := range personalHistory {
|
for _, item := range privmsgHistory {
|
||||||
if item.Type == history.Privmsg || item.Type == history.Notice {
|
// TODO this is the nickmask, fix that
|
||||||
sender := server.clients.Get(item.Nick)
|
sender := server.clients.Get(item.Nick)
|
||||||
if sender != nil {
|
if sender != nil {
|
||||||
friends.Add(sender)
|
friends.Add(sender)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -482,10 +484,10 @@ func (client *Client) TryResume() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if client.resumeDetails.HistoryIncomplete {
|
if client.resumeDetails.HistoryIncomplete {
|
||||||
client.Send(nil, "RESUME", "WARN", fmt.Sprintf(client.t("Resume may have lost up to %d seconds of history"), gapSeconds))
|
client.Send(nil, client.server.name, "RESUME", "WARN", fmt.Sprintf(client.t("Resume may have lost up to %d seconds of history"), gapSeconds))
|
||||||
}
|
}
|
||||||
|
|
||||||
client.Send(nil, "RESUME", "SUCCESS", oldNick)
|
client.Send(nil, client.server.name, "RESUME", "SUCCESS", oldNick)
|
||||||
|
|
||||||
// after we send the rest of the registration burst, we'll try rejoining channels
|
// after we send the rest of the registration burst, we'll try rejoining channels
|
||||||
}
|
}
|
||||||
|
@ -47,7 +47,7 @@ func (cmd *Command) Run(server *Server, client *Client, msg ircmsg.IrcMessage) b
|
|||||||
rb := NewResponseBuffer(client)
|
rb := NewResponseBuffer(client)
|
||||||
rb.Label = GetLabel(msg)
|
rb.Label = GetLabel(msg)
|
||||||
exiting := cmd.handler(server, client, msg, rb)
|
exiting := cmd.handler(server, client, msg, rb)
|
||||||
rb.Send()
|
rb.Send(true)
|
||||||
|
|
||||||
// after each command, see if we can send registration to the client
|
// after each command, see if we can send registration to the client
|
||||||
if !client.registered {
|
if !client.registered {
|
||||||
|
@ -268,9 +268,10 @@ type Config struct {
|
|||||||
Fakelag FakelagConfig
|
Fakelag FakelagConfig
|
||||||
|
|
||||||
History struct {
|
History struct {
|
||||||
Enabled bool
|
Enabled bool
|
||||||
ChannelLength int `yaml:"channel-length"`
|
ChannelLength int `yaml:"channel-length"`
|
||||||
ClientLength int `yaml:"client-length"`
|
ClientLength int `yaml:"client-length"`
|
||||||
|
AutoreplayOnJoin int `yaml:"autoreplay-on-join"`
|
||||||
}
|
}
|
||||||
|
|
||||||
Filename string
|
Filename string
|
||||||
|
@ -259,13 +259,7 @@ func (channel *Channel) Members() (result []*Client) {
|
|||||||
return channel.membersCache
|
return channel.membersCache
|
||||||
}
|
}
|
||||||
|
|
||||||
func (channel *Channel) UserLimit() uint64 {
|
func (channel *Channel) setUserLimit(limit int) {
|
||||||
channel.stateMutex.RLock()
|
|
||||||
defer channel.stateMutex.RUnlock()
|
|
||||||
return channel.userLimit
|
|
||||||
}
|
|
||||||
|
|
||||||
func (channel *Channel) setUserLimit(limit uint64) {
|
|
||||||
channel.stateMutex.Lock()
|
channel.stateMutex.Lock()
|
||||||
channel.userLimit = limit
|
channel.userLimit = limit
|
||||||
channel.stateMutex.Unlock()
|
channel.stateMutex.Unlock()
|
||||||
|
@ -933,7 +933,7 @@ func sajoinHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Re
|
|||||||
server.channels.Join(target, chname, "", true, rb)
|
server.channels.Join(target, chname, "", true, rb)
|
||||||
}
|
}
|
||||||
if client != target {
|
if client != target {
|
||||||
rb.Send()
|
rb.Send(false)
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@ -1706,16 +1706,18 @@ func noticeHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Re
|
|||||||
if !user.HasMode(modes.RegisteredOnly) || client.LoggedIntoAccount() {
|
if !user.HasMode(modes.RegisteredOnly) || client.LoggedIntoAccount() {
|
||||||
user.SendSplitMsgFromClient(msgid, client, clientOnlyTags, "NOTICE", user.nick, splitMsg)
|
user.SendSplitMsgFromClient(msgid, client, clientOnlyTags, "NOTICE", user.nick, splitMsg)
|
||||||
}
|
}
|
||||||
|
nickMaskString := client.NickMaskString()
|
||||||
|
accountName := client.AccountName()
|
||||||
if client.capabilities.Has(caps.EchoMessage) {
|
if client.capabilities.Has(caps.EchoMessage) {
|
||||||
rb.AddSplitMessageFromClient(msgid, client, clientOnlyTags, "NOTICE", user.nick, splitMsg)
|
rb.AddSplitMessageFromClient(msgid, nickMaskString, accountName, clientOnlyTags, "NOTICE", user.nick, splitMsg)
|
||||||
}
|
}
|
||||||
|
|
||||||
user.history.Add(history.Item{
|
user.history.Add(history.Item{
|
||||||
Type: history.Notice,
|
Type: history.Notice,
|
||||||
Msgid: msgid,
|
Msgid: msgid,
|
||||||
Message: splitMsg,
|
Message: splitMsg,
|
||||||
Nick: client.NickMaskString(),
|
Nick: nickMaskString,
|
||||||
AccountName: client.AccountName(),
|
AccountName: accountName,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1916,8 +1918,10 @@ func privmsgHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *R
|
|||||||
if !user.HasMode(modes.RegisteredOnly) || client.LoggedIntoAccount() {
|
if !user.HasMode(modes.RegisteredOnly) || client.LoggedIntoAccount() {
|
||||||
user.SendSplitMsgFromClient(msgid, client, clientOnlyTags, "PRIVMSG", user.nick, splitMsg)
|
user.SendSplitMsgFromClient(msgid, client, clientOnlyTags, "PRIVMSG", user.nick, splitMsg)
|
||||||
}
|
}
|
||||||
|
nickMaskString := client.NickMaskString()
|
||||||
|
accountName := client.AccountName()
|
||||||
if client.capabilities.Has(caps.EchoMessage) {
|
if client.capabilities.Has(caps.EchoMessage) {
|
||||||
rb.AddSplitMessageFromClient(msgid, client, clientOnlyTags, "PRIVMSG", user.nick, splitMsg)
|
rb.AddSplitMessageFromClient(msgid, nickMaskString, accountName, clientOnlyTags, "PRIVMSG", user.nick, splitMsg)
|
||||||
}
|
}
|
||||||
if user.HasMode(modes.Away) {
|
if user.HasMode(modes.Away) {
|
||||||
//TODO(dan): possibly implement cooldown of away notifications to users
|
//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
|
|||||||
Type: history.Privmsg,
|
Type: history.Privmsg,
|
||||||
Msgid: msgid,
|
Msgid: msgid,
|
||||||
Message: splitMsg,
|
Message: splitMsg,
|
||||||
Nick: client.NickMaskString(),
|
Nick: nickMaskString,
|
||||||
AccountName: client.AccountName(),
|
AccountName: accountName,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -2150,7 +2154,7 @@ func tagmsgHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Re
|
|||||||
}
|
}
|
||||||
user.SendFromClient(msgid, client, clientOnlyTags, "TAGMSG", user.nick)
|
user.SendFromClient(msgid, client, clientOnlyTags, "TAGMSG", user.nick)
|
||||||
if client.capabilities.Has(caps.EchoMessage) {
|
if client.capabilities.Has(caps.EchoMessage) {
|
||||||
rb.AddFromClient(msgid, client, clientOnlyTags, "TAGMSG", user.nick)
|
rb.AddFromClient(msgid, client.NickMaskString(), client.AccountName(), clientOnlyTags, "TAGMSG", user.nick)
|
||||||
}
|
}
|
||||||
if user.HasMode(modes.Away) {
|
if user.HasMode(modes.Away) {
|
||||||
//TODO(dan): possibly implement cooldown of away notifications to users
|
//TODO(dan): possibly implement cooldown of away notifications to users
|
||||||
|
@ -32,9 +32,12 @@ type Item struct {
|
|||||||
// this is the uncasefolded account name, if there's no account it should be set to "*"
|
// this is the uncasefolded account name, if there's no account it should be set to "*"
|
||||||
AccountName string
|
AccountName string
|
||||||
Message utils.SplitMessage
|
Message utils.SplitMessage
|
||||||
Msgid string
|
// for non-privmsg items, we may stuff some other data in here
|
||||||
|
Msgid string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Predicate func(item Item) (matches bool)
|
||||||
|
|
||||||
// Buffer is a ring buffer holding message/event history for a channel or user
|
// Buffer is a ring buffer holding message/event history for a channel or user
|
||||||
type Buffer struct {
|
type Buffer struct {
|
||||||
sync.RWMutex
|
sync.RWMutex
|
||||||
@ -85,7 +88,7 @@ func (list *Buffer) Add(item Item) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if item.Time.IsZero() {
|
if item.Time.IsZero() {
|
||||||
item.Time = time.Now()
|
item.Time = time.Now().UTC()
|
||||||
}
|
}
|
||||||
|
|
||||||
list.Lock()
|
list.Lock()
|
||||||
@ -112,6 +115,12 @@ func (list *Buffer) Add(item Item) {
|
|||||||
list.buffer[pos] = item
|
list.buffer[pos] = item
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func reverse(results []Item) {
|
||||||
|
for i, j := 0, len(results)-1; i < j; i, j = i+1, j-1 {
|
||||||
|
results[i], results[j] = results[j], results[i]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Between returns all history items with a time `after` <= time <= `before`,
|
// Between returns all history items with a time `after` <= time <= `before`,
|
||||||
// with an indication of whether the results are complete or are missing items
|
// with an indication of whether the results are complete or are missing items
|
||||||
// because some of that period was discarded. A zero value of `before` is considered
|
// 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
|
|||||||
|
|
||||||
complete = after.Equal(list.lastDiscarded) || after.After(list.lastDiscarded)
|
complete = after.Equal(list.lastDiscarded) || after.After(list.lastDiscarded)
|
||||||
|
|
||||||
|
satisfies := func(item Item) bool {
|
||||||
|
return (after.IsZero() || item.Time.After(after)) && (before.IsZero() || item.Time.Before(before))
|
||||||
|
}
|
||||||
|
|
||||||
|
return list.matchInternal(satisfies, 0), complete
|
||||||
|
}
|
||||||
|
|
||||||
|
// Match returns all history items such that `predicate` returns true for them.
|
||||||
|
// Items are considered in reverse insertion order, up to a total of `limit` matches.
|
||||||
|
// `predicate` MAY be a closure that maintains its own state across invocations;
|
||||||
|
// it MUST NOT acquire any locks or otherwise do anything weird.
|
||||||
|
func (list *Buffer) Match(predicate Predicate, limit int) (results []Item) {
|
||||||
|
if !list.Enabled() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
list.RLock()
|
||||||
|
defer list.RUnlock()
|
||||||
|
|
||||||
|
return list.matchInternal(predicate, limit)
|
||||||
|
}
|
||||||
|
|
||||||
|
// you must be holding the read lock to call this
|
||||||
|
func (list *Buffer) matchInternal(predicate Predicate, limit int) (results []Item) {
|
||||||
if list.start == -1 {
|
if list.start == -1 {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
satisfies := func(itime time.Time) bool {
|
|
||||||
return (after.IsZero() || itime.After(after)) && (before.IsZero() || itime.Before(before))
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: if we can guarantee that the insertion order is also the monotonic clock order,
|
|
||||||
// then this can do a single allocation and use binary search and 1-2 copy calls
|
|
||||||
|
|
||||||
pos := list.prev(list.end)
|
pos := list.prev(list.end)
|
||||||
for {
|
for {
|
||||||
if satisfies(list.buffer[pos].Time) {
|
if predicate(list.buffer[pos]) {
|
||||||
results = append(results, list.buffer[pos])
|
results = append(results, list.buffer[pos])
|
||||||
}
|
}
|
||||||
if pos == list.start {
|
if pos == list.start || (limit != 0 && len(results) == limit) {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
pos = list.prev(pos)
|
pos = list.prev(pos)
|
||||||
}
|
}
|
||||||
|
|
||||||
// reverse the results
|
// TODO sort by time instead?
|
||||||
for i, j := 0, len(results)-1; i < j; i, j = i+1, j-1 {
|
reverse(results)
|
||||||
results[i], results[j] = results[j], results[i]
|
|
||||||
}
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// All returns all available history items as a slice
|
// Latest returns the items most recently added, up to `limit`. If `limit` is 0,
|
||||||
func (list *Buffer) All() (results []Item) {
|
// it returns all items.
|
||||||
list.RLock()
|
func (list *Buffer) Latest(limit int) (results []Item) {
|
||||||
defer list.RUnlock()
|
matchAll := func(item Item) bool { return true }
|
||||||
|
return list.Match(matchAll, limit)
|
||||||
if list.start == -1 {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
results = make([]Item, list.length())
|
|
||||||
if list.start < list.end {
|
|
||||||
copy(results, list.buffer[list.start:list.end])
|
|
||||||
} else {
|
|
||||||
initialSegment := copy(results, list.buffer[list.start:])
|
|
||||||
copy(results[initialSegment:], list.buffer[:list.end])
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// LastDiscarded returns the latest time of any entry that was evicted
|
// LastDiscarded returns the latest time of any entry that was evicted
|
||||||
|
@ -67,7 +67,8 @@ func TestEmptyBuffer(t *testing.T) {
|
|||||||
if since[0].Nick != "testnick2" {
|
if since[0].Nick != "testnick2" {
|
||||||
t.Error("retrieved junk data")
|
t.Error("retrieved junk data")
|
||||||
}
|
}
|
||||||
assertEqual(toNicks(buf.All()), []string{"testnick2"}, t)
|
matchAll := func(item Item) bool { return true }
|
||||||
|
assertEqual(toNicks(buf.Match(matchAll, 0)), []string{"testnick2"}, t)
|
||||||
}
|
}
|
||||||
|
|
||||||
func toNicks(items []Item) (result []string) {
|
func toNicks(items []Item) (result []string) {
|
||||||
|
@ -189,7 +189,7 @@ func (channel *Channel) ApplyChannelModeChanges(client *Client, isSamode bool, c
|
|||||||
case modes.UserLimit:
|
case modes.UserLimit:
|
||||||
switch change.Op {
|
switch change.Op {
|
||||||
case modes.Add:
|
case modes.Add:
|
||||||
val, err := strconv.ParseUint(change.Arg, 10, 64)
|
val, err := strconv.Atoi(change.Arg)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
channel.setUserLimit(val)
|
channel.setUserLimit(val)
|
||||||
applied = append(applied, change)
|
applied = append(applied, change)
|
||||||
|
@ -83,7 +83,7 @@ func (server *Server) RandomlyRename(client *Client) {
|
|||||||
nick := fmt.Sprintf("%s%s", prefix, hex.EncodeToString(buf))
|
nick := fmt.Sprintf("%s%s", prefix, hex.EncodeToString(buf))
|
||||||
rb := NewResponseBuffer(client)
|
rb := NewResponseBuffer(client)
|
||||||
performNickChange(server, client, client, nick, rb)
|
performNickChange(server, client, client, nick, rb)
|
||||||
rb.Send()
|
rb.Send(false)
|
||||||
// technically performNickChange can fail to change the nick,
|
// technically performNickChange can fail to change the nick,
|
||||||
// but if they're still delinquent, the timer will get them later
|
// but if they're still delinquent, the timer will get them later
|
||||||
}
|
}
|
||||||
|
@ -4,6 +4,7 @@
|
|||||||
package irc
|
package irc
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"runtime/debug"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/goshuirc/irc-go/ircmsg"
|
"github.com/goshuirc/irc-go/ircmsg"
|
||||||
@ -11,16 +12,22 @@ import (
|
|||||||
"github.com/oragono/oragono/irc/utils"
|
"github.com/oragono/oragono/irc/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// https://ircv3.net/specs/extensions/labeled-response.html
|
||||||
|
batchType = "draft/labeled-response"
|
||||||
|
)
|
||||||
|
|
||||||
// ResponseBuffer - put simply - buffers messages and then outputs them to a given client.
|
// ResponseBuffer - put simply - buffers messages and then outputs them to a given client.
|
||||||
//
|
//
|
||||||
// Using a ResponseBuffer lets you really easily implement labeled-response, since the
|
// Using a ResponseBuffer lets you really easily implement labeled-response, since the
|
||||||
// buffer will silently create a batch if required and label the outgoing messages as
|
// buffer will silently create a batch if required and label the outgoing messages as
|
||||||
// necessary (or leave it off and simply tag the outgoing message).
|
// necessary (or leave it off and simply tag the outgoing message).
|
||||||
type ResponseBuffer struct {
|
type ResponseBuffer struct {
|
||||||
Label string
|
Label string
|
||||||
target *Client
|
batchID string
|
||||||
messages []ircmsg.IrcMessage
|
target *Client
|
||||||
blocking bool
|
messages []ircmsg.IrcMessage
|
||||||
|
finalized bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetLabel returns the label from the given message.
|
// GetLabel returns the label from the given message.
|
||||||
@ -35,96 +42,125 @@ func NewResponseBuffer(target *Client) *ResponseBuffer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (rb *ResponseBuffer) SetBlocking(blocking bool) {
|
|
||||||
rb.blocking = blocking
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add adds a standard new message to our queue.
|
// Add adds a standard new message to our queue.
|
||||||
func (rb *ResponseBuffer) Add(tags *map[string]ircmsg.TagValue, prefix string, command string, params ...string) {
|
func (rb *ResponseBuffer) Add(tags *map[string]ircmsg.TagValue, prefix string, command string, params ...string) {
|
||||||
message := ircmsg.MakeMessage(tags, prefix, command, params...)
|
if rb.finalized {
|
||||||
|
rb.target.server.logger.Error("message added to finalized ResponseBuffer, undefined behavior")
|
||||||
|
debug.PrintStack()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
message := ircmsg.MakeMessage(tags, prefix, command, params...)
|
||||||
rb.messages = append(rb.messages, message)
|
rb.messages = append(rb.messages, message)
|
||||||
}
|
}
|
||||||
|
|
||||||
// AddFromClient adds a new message from a specific client to our queue.
|
// AddFromClient adds a new message from a specific client to our queue.
|
||||||
func (rb *ResponseBuffer) AddFromClient(msgid string, from *Client, tags *map[string]ircmsg.TagValue, command string, params ...string) {
|
func (rb *ResponseBuffer) AddFromClient(msgid string, fromNickMask string, fromAccount string, tags *map[string]ircmsg.TagValue, command string, params ...string) {
|
||||||
// attach account-tag
|
// attach account-tag
|
||||||
if rb.target.capabilities.Has(caps.AccountTag) && from.LoggedIntoAccount() {
|
if rb.target.capabilities.Has(caps.AccountTag) {
|
||||||
if tags == nil {
|
if fromAccount != "*" {
|
||||||
tags = ircmsg.MakeTags("account", from.AccountName())
|
tags = ensureTag(tags, "account", fromAccount)
|
||||||
} else {
|
|
||||||
(*tags)["account"] = ircmsg.MakeTagValue(from.AccountName())
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// attach message-id
|
// attach message-id
|
||||||
if len(msgid) > 0 && rb.target.capabilities.Has(caps.MessageTags) {
|
if len(msgid) > 0 && rb.target.capabilities.Has(caps.MessageTags) {
|
||||||
if tags == nil {
|
tags = ensureTag(tags, "draft/msgid", msgid)
|
||||||
tags = ircmsg.MakeTags("draft/msgid", msgid)
|
|
||||||
} else {
|
|
||||||
(*tags)["draft/msgid"] = ircmsg.MakeTagValue(msgid)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
rb.Add(tags, from.nickMaskString, command, params...)
|
rb.Add(tags, fromNickMask, command, params...)
|
||||||
}
|
}
|
||||||
|
|
||||||
// AddSplitMessageFromClient adds a new split message from a specific client to our queue.
|
// AddSplitMessageFromClient adds a new split message from a specific client to our queue.
|
||||||
func (rb *ResponseBuffer) AddSplitMessageFromClient(msgid string, from *Client, tags *map[string]ircmsg.TagValue, command string, target string, message utils.SplitMessage) {
|
func (rb *ResponseBuffer) AddSplitMessageFromClient(msgid string, fromNickMask string, fromAccount string, tags *map[string]ircmsg.TagValue, command string, target string, message utils.SplitMessage) {
|
||||||
if rb.target.capabilities.Has(caps.MaxLine) || message.Wrapped == nil {
|
if rb.target.capabilities.Has(caps.MaxLine) || message.Wrapped == nil {
|
||||||
rb.AddFromClient(msgid, from, tags, command, target, message.Original)
|
rb.AddFromClient(msgid, fromNickMask, fromAccount, tags, command, target, message.Original)
|
||||||
} else {
|
} else {
|
||||||
for _, str := range message.Wrapped {
|
for _, str := range message.Wrapped {
|
||||||
rb.AddFromClient(msgid, from, tags, command, target, str)
|
rb.AddFromClient(msgid, fromNickMask, fromAccount, tags, command, target, str)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Send sends the message to our target client.
|
func (rb *ResponseBuffer) sendBatchStart(blocking bool) {
|
||||||
func (rb *ResponseBuffer) Send() error {
|
if rb.batchID != "" {
|
||||||
// fall out if no messages to send
|
// batch already initialized
|
||||||
if len(rb.messages) == 0 {
|
return
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// make batch and all if required
|
// formerly this combined time.Now.UnixNano() in base 36 with an incrementing counter,
|
||||||
var batch *Batch
|
// also in base 36. but let's just use a uuidv4-alike (26 base32 characters):
|
||||||
useLabel := rb.target.capabilities.Has(caps.LabeledResponse) && rb.Label != ""
|
rb.batchID = utils.GenerateSecretToken()
|
||||||
if useLabel && 1 < len(rb.messages) && rb.target.capabilities.Has(caps.Batch) {
|
|
||||||
batch = rb.target.server.batches.New("draft/labeled-response")
|
message := ircmsg.MakeMessage(nil, rb.target.server.name, "BATCH", "+"+rb.batchID, batchType)
|
||||||
|
message.Tags[caps.LabelTagName] = ircmsg.MakeTagValue(rb.Label)
|
||||||
|
rb.target.SendRawMessage(message, blocking)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rb *ResponseBuffer) sendBatchEnd(blocking bool) {
|
||||||
|
if rb.batchID == "" {
|
||||||
|
// we are not sending a batch, skip this
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
message := ircmsg.MakeMessage(nil, rb.target.server.name, "BATCH", "-"+rb.batchID)
|
||||||
|
rb.target.SendRawMessage(message, blocking)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send sends all messages in the buffer to the client.
|
||||||
|
// Afterwards, the buffer is in an undefined state and MUST NOT be used further.
|
||||||
|
// If `blocking` is true you MUST be sending to the client from its own goroutine.
|
||||||
|
func (rb *ResponseBuffer) Send(blocking bool) error {
|
||||||
|
return rb.flushInternal(true, blocking)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Flush sends all messages in the buffer to the client.
|
||||||
|
// Afterwards, the buffer can still be used. Client code MUST subsequently call Send()
|
||||||
|
// to ensure that the final `BATCH -` message is sent.
|
||||||
|
// If `blocking` is true you MUST be sending to the client from its own goroutine.
|
||||||
|
func (rb *ResponseBuffer) Flush(blocking bool) error {
|
||||||
|
return rb.flushInternal(false, blocking)
|
||||||
|
}
|
||||||
|
|
||||||
|
// flushInternal sends the contents of the buffer, either blocking or nonblocking
|
||||||
|
// It sends the `BATCH +` message if the client supports it and it hasn't been sent already.
|
||||||
|
// If `final` is true, it also sends `BATCH -` (if necessary).
|
||||||
|
func (rb *ResponseBuffer) flushInternal(final bool, blocking bool) error {
|
||||||
|
useLabel := rb.target.capabilities.Has(caps.LabeledResponse) && rb.Label != ""
|
||||||
|
// use a batch if we have a label, and we either currently have multiple messages,
|
||||||
|
// or we are doing a Flush() and we have to assume that there will be more messages
|
||||||
|
// in the future.
|
||||||
|
useBatch := useLabel && (len(rb.messages) > 1 || !final)
|
||||||
|
|
||||||
// if label but no batch, add label to first message
|
// if label but no batch, add label to first message
|
||||||
if useLabel && batch == nil {
|
if useLabel && !useBatch && len(rb.messages) == 1 {
|
||||||
message := rb.messages[0]
|
rb.messages[0].Tags[caps.LabelTagName] = ircmsg.MakeTagValue(rb.Label)
|
||||||
message.Tags[caps.LabelTagName] = ircmsg.MakeTagValue(rb.Label)
|
} else if useBatch {
|
||||||
rb.messages[0] = message
|
rb.sendBatchStart(blocking)
|
||||||
}
|
|
||||||
|
|
||||||
// start batch if required
|
|
||||||
if batch != nil {
|
|
||||||
batch.Start(rb.target, ircmsg.MakeTags(caps.LabelTagName, rb.Label))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// send each message out
|
// send each message out
|
||||||
for _, message := range rb.messages {
|
for _, message := range rb.messages {
|
||||||
// attach server-time if needed
|
// attach server-time if needed
|
||||||
if rb.target.capabilities.Has(caps.ServerTime) {
|
if rb.target.capabilities.Has(caps.ServerTime) {
|
||||||
t := time.Now().UTC().Format(IRCv3TimestampFormat)
|
if !message.Tags["time"].HasValue {
|
||||||
message.Tags["time"] = ircmsg.MakeTagValue(t)
|
t := time.Now().UTC().Format(IRCv3TimestampFormat)
|
||||||
|
message.Tags["time"] = ircmsg.MakeTagValue(t)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// attach batch ID
|
// attach batch ID
|
||||||
if batch != nil {
|
if rb.batchID != "" {
|
||||||
message.Tags["batch"] = ircmsg.MakeTagValue(batch.ID)
|
message.Tags["batch"] = ircmsg.MakeTagValue(rb.batchID)
|
||||||
}
|
}
|
||||||
|
|
||||||
// send message out
|
// send message out
|
||||||
rb.target.SendRawMessage(message, rb.blocking)
|
rb.target.SendRawMessage(message, blocking)
|
||||||
}
|
}
|
||||||
|
|
||||||
// end batch if required
|
// end batch if required
|
||||||
if batch != nil {
|
if final {
|
||||||
batch.End(rb.target)
|
rb.sendBatchEnd(blocking)
|
||||||
|
rb.finalized = true
|
||||||
}
|
}
|
||||||
|
|
||||||
// clear out any existing messages
|
// clear out any existing messages
|
||||||
|
@ -67,7 +67,6 @@ type ListenerWrapper struct {
|
|||||||
// Server is the main Oragono server.
|
// Server is the main Oragono server.
|
||||||
type Server struct {
|
type Server struct {
|
||||||
accounts *AccountManager
|
accounts *AccountManager
|
||||||
batches *BatchManager
|
|
||||||
channels *ChannelManager
|
channels *ChannelManager
|
||||||
channelRegistry *ChannelRegistry
|
channelRegistry *ChannelRegistry
|
||||||
clients *ClientManager
|
clients *ClientManager
|
||||||
@ -116,7 +115,6 @@ type clientConn struct {
|
|||||||
func NewServer(config *Config, logger *logger.Manager) (*Server, error) {
|
func NewServer(config *Config, logger *logger.Manager) (*Server, error) {
|
||||||
// initialize data structures
|
// initialize data structures
|
||||||
server := &Server{
|
server := &Server{
|
||||||
batches: NewBatchManager(),
|
|
||||||
channels: NewChannelManager(),
|
channels: NewChannelManager(),
|
||||||
clients: NewClientManager(),
|
clients: NewClientManager(),
|
||||||
connectionLimiter: connection_limits.NewLimiter(),
|
connectionLimiter: connection_limits.NewLimiter(),
|
||||||
@ -406,7 +404,7 @@ func (server *Server) tryRegister(c *Client) {
|
|||||||
|
|
||||||
rb := NewResponseBuffer(c)
|
rb := NewResponseBuffer(c)
|
||||||
nickAssigned := performNickChange(server, c, c, preregNick, rb)
|
nickAssigned := performNickChange(server, c, c, preregNick, rb)
|
||||||
rb.Send()
|
rb.Send(true)
|
||||||
if !nickAssigned {
|
if !nickAssigned {
|
||||||
c.SetPreregNick("")
|
c.SetPreregNick("")
|
||||||
return
|
return
|
||||||
@ -446,7 +444,7 @@ func (server *Server) tryRegister(c *Client) {
|
|||||||
rb = NewResponseBuffer(c)
|
rb = NewResponseBuffer(c)
|
||||||
c.RplISupport(rb)
|
c.RplISupport(rb)
|
||||||
server.MOTD(c, rb)
|
server.MOTD(c, rb)
|
||||||
rb.Send()
|
rb.Send(true)
|
||||||
|
|
||||||
modestring := c.ModeString()
|
modestring := c.ModeString()
|
||||||
if modestring != "+" {
|
if modestring != "+" {
|
||||||
|
@ -6,7 +6,12 @@ package utils
|
|||||||
import (
|
import (
|
||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
"crypto/subtle"
|
"crypto/subtle"
|
||||||
"encoding/hex"
|
"encoding/base32"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// standard b32 alphabet, but in lowercase for silly aesthetic reasons
|
||||||
|
b32encoder = base32.NewEncoding("abcdefghijklmnopqrstuvwxyz234567").WithPadding(base32.NoPadding)
|
||||||
)
|
)
|
||||||
|
|
||||||
// generate a secret token that cannot be brute-forced via online attacks
|
// generate a secret token that cannot be brute-forced via online attacks
|
||||||
@ -14,8 +19,8 @@ func GenerateSecretToken() string {
|
|||||||
// 128 bits of entropy are enough to resist any online attack:
|
// 128 bits of entropy are enough to resist any online attack:
|
||||||
var buf [16]byte
|
var buf [16]byte
|
||||||
rand.Read(buf[:])
|
rand.Read(buf[:])
|
||||||
// 32 ASCII characters, should be fine for most purposes
|
// 26 ASCII characters, should be fine for most purposes
|
||||||
return hex.EncodeToString(buf[:])
|
return b32encoder.EncodeToString(buf[:])
|
||||||
}
|
}
|
||||||
|
|
||||||
// securely check if a supplied token matches a stored token
|
// securely check if a supplied token matches a stored token
|
||||||
|
@ -16,7 +16,7 @@ const (
|
|||||||
|
|
||||||
func TestGenerateSecretToken(t *testing.T) {
|
func TestGenerateSecretToken(t *testing.T) {
|
||||||
token := GenerateSecretToken()
|
token := GenerateSecretToken()
|
||||||
if len(token) != 32 {
|
if len(token) < 22 {
|
||||||
t.Errorf("bad token: %v", token)
|
t.Errorf("bad token: %v", token)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -46,3 +46,9 @@ func TestTokenCompare(t *testing.T) {
|
|||||||
t.Error("the empty token should not match anything")
|
t.Error("the empty token should not match anything")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func BenchmarkGenerateSecretToken(b *testing.B) {
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
GenerateSecretToken()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
28
oragono.go
28
oragono.go
@ -51,17 +51,7 @@ Options:
|
|||||||
|
|
||||||
arguments, _ := docopt.ParseArgs(usage, nil, version)
|
arguments, _ := docopt.ParseArgs(usage, nil, version)
|
||||||
|
|
||||||
configfile := arguments["--conf"].(string)
|
// don't require a config file for genpasswd
|
||||||
config, err := irc.LoadConfig(configfile)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal("Config file did not load successfully: ", err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
logman, err := logger.NewManager(config.Logging)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal("Logger did not load successfully:", err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
if arguments["genpasswd"].(bool) {
|
if arguments["genpasswd"].(bool) {
|
||||||
fmt.Print("Enter Password: ")
|
fmt.Print("Enter Password: ")
|
||||||
password := getPassword()
|
password := getPassword()
|
||||||
@ -77,7 +67,21 @@ Options:
|
|||||||
log.Fatal("encoding error:", err.Error())
|
log.Fatal("encoding error:", err.Error())
|
||||||
}
|
}
|
||||||
fmt.Println(string(hash))
|
fmt.Println(string(hash))
|
||||||
} else if arguments["initdb"].(bool) {
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
configfile := arguments["--conf"].(string)
|
||||||
|
config, err := irc.LoadConfig(configfile)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal("Config file did not load successfully: ", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
logman, err := logger.NewManager(config.Logging)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal("Logger did not load successfully:", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
if arguments["initdb"].(bool) {
|
||||||
irc.InitDB(config.Datastore.Path)
|
irc.InitDB(config.Datastore.Path)
|
||||||
if !arguments["--quiet"].(bool) {
|
if !arguments["--quiet"].(bool) {
|
||||||
log.Println("database initialized: ", config.Datastore.Path)
|
log.Println("database initialized: ", config.Datastore.Path)
|
||||||
|
@ -454,3 +454,6 @@ history:
|
|||||||
|
|
||||||
# how many direct messages and notices should be tracked per user?
|
# how many direct messages and notices should be tracked per user?
|
||||||
client-length: 64
|
client-length: 64
|
||||||
|
|
||||||
|
# number of messages to automatically play back on channel join (0 to disable):
|
||||||
|
autoreplay-on-join: 0
|
||||||
|
Loading…
Reference in New Issue
Block a user