mirror of
https://github.com/ergochat/ergo.git
synced 2024-11-10 22:19:31 +01:00
Merge remote-tracking branch 'origin/master' into compat.4
This commit is contained in:
commit
5331afbdd4
@ -159,6 +159,12 @@ CAPDEFS = [
|
|||||||
url="https://wiki.znc.in/Query_buffers",
|
url="https://wiki.znc.in/Query_buffers",
|
||||||
standard="ZNC vendor",
|
standard="ZNC vendor",
|
||||||
),
|
),
|
||||||
|
CapDef(
|
||||||
|
identifier="EventPlayback",
|
||||||
|
name="draft/event-playback",
|
||||||
|
url="https://github.com/ircv3/ircv3-specifications/pull/362",
|
||||||
|
standard="Proposed IRCv3",
|
||||||
|
),
|
||||||
]
|
]
|
||||||
|
|
||||||
def validate_defs():
|
def validate_defs():
|
||||||
|
@ -237,7 +237,10 @@ func (am *AccountManager) BouncerAllowed(account string, session *Session) bool
|
|||||||
if !config.Accounts.Bouncer.Enabled {
|
if !config.Accounts.Bouncer.Enabled {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
return config.Accounts.Bouncer.AllowedByDefault || session.capabilities.Has(caps.Bouncer)
|
if config.Accounts.Bouncer.AllowedByDefault {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return session != nil && session.capabilities.Has(caps.Bouncer)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Looks up the enforcement method stored in the database for an account
|
// Looks up the enforcement method stored in the database for an account
|
||||||
|
@ -7,7 +7,7 @@ package caps
|
|||||||
|
|
||||||
const (
|
const (
|
||||||
// number of recognized capabilities:
|
// number of recognized capabilities:
|
||||||
numCapabs = 24
|
numCapabs = 25
|
||||||
// length of the uint64 array that represents the bitset:
|
// length of the uint64 array that represents the bitset:
|
||||||
bitsetLen = 1
|
bitsetLen = 1
|
||||||
)
|
)
|
||||||
@ -108,6 +108,10 @@ const (
|
|||||||
// ZNCSelfMessage is the ZNC vendor capability named "znc.in/self-message":
|
// ZNCSelfMessage is the ZNC vendor capability named "znc.in/self-message":
|
||||||
// https://wiki.znc.in/Query_buffers
|
// https://wiki.znc.in/Query_buffers
|
||||||
ZNCSelfMessage Capability = iota
|
ZNCSelfMessage Capability = iota
|
||||||
|
|
||||||
|
// EventPlayback is the Proposed IRCv3 capability named "draft/event-playback":
|
||||||
|
// https://github.com/ircv3/ircv3-specifications/pull/362
|
||||||
|
EventPlayback Capability = iota
|
||||||
)
|
)
|
||||||
|
|
||||||
// `capabilityNames[capab]` is the string name of the capability `capab`
|
// `capabilityNames[capab]` is the string name of the capability `capab`
|
||||||
@ -137,5 +141,6 @@ var (
|
|||||||
"userhost-in-names",
|
"userhost-in-names",
|
||||||
"oragono.io/bnc",
|
"oragono.io/bnc",
|
||||||
"znc.in/self-message",
|
"znc.in/self-message",
|
||||||
|
"draft/event-playback",
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
161
irc/channel.go
161
irc/channel.go
@ -536,6 +536,8 @@ func (channel *Channel) Join(client *Client, key string, isSajoin bool, rb *Resp
|
|||||||
|
|
||||||
client.server.logger.Debug("join", fmt.Sprintf("%s joined channel %s", details.nick, chname))
|
client.server.logger.Debug("join", fmt.Sprintf("%s joined channel %s", details.nick, chname))
|
||||||
|
|
||||||
|
var message utils.SplitMessage
|
||||||
|
|
||||||
givenMode := func() (givenMode modes.Mode) {
|
givenMode := func() (givenMode modes.Mode) {
|
||||||
channel.joinPartMutex.Lock()
|
channel.joinPartMutex.Lock()
|
||||||
defer channel.joinPartMutex.Unlock()
|
defer channel.joinPartMutex.Unlock()
|
||||||
@ -559,14 +561,15 @@ func (channel *Channel) Join(client *Client, key string, isSajoin bool, rb *Resp
|
|||||||
|
|
||||||
channel.regenerateMembersCache()
|
channel.regenerateMembersCache()
|
||||||
|
|
||||||
message := utils.SplitMessage{}
|
message = utils.MakeSplitMessage("", true)
|
||||||
message.Msgid = details.realname
|
histItem := history.Item{
|
||||||
channel.history.Add(history.Item{
|
|
||||||
Type: history.Join,
|
Type: history.Join,
|
||||||
Nick: details.nickMask,
|
Nick: details.nickMask,
|
||||||
AccountName: details.accountName,
|
AccountName: details.accountName,
|
||||||
Message: message,
|
Message: message,
|
||||||
})
|
}
|
||||||
|
histItem.Params[0] = details.realname
|
||||||
|
channel.history.Add(histItem)
|
||||||
|
|
||||||
return
|
return
|
||||||
}()
|
}()
|
||||||
@ -587,9 +590,9 @@ func (channel *Channel) Join(client *Client, key string, isSajoin bool, rb *Resp
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if session.capabilities.Has(caps.ExtendedJoin) {
|
if session.capabilities.Has(caps.ExtendedJoin) {
|
||||||
session.Send(nil, details.nickMask, "JOIN", chname, details.accountName, details.realname)
|
session.sendFromClientInternal(false, message.Time, message.Msgid, details.nickMask, details.accountName, nil, "JOIN", chname, details.accountName, details.realname)
|
||||||
} else {
|
} else {
|
||||||
session.Send(nil, details.nickMask, "JOIN", chname)
|
session.sendFromClientInternal(false, message.Time, message.Msgid, details.nickMask, details.accountName, nil, "JOIN", chname)
|
||||||
}
|
}
|
||||||
if givenMode != 0 {
|
if givenMode != 0 {
|
||||||
session.Send(nil, client.server.name, "MODE", chname, modestr, details.nick)
|
session.Send(nil, client.server.name, "MODE", chname, modestr, details.nick)
|
||||||
@ -598,9 +601,9 @@ func (channel *Channel) Join(client *Client, key string, isSajoin bool, rb *Resp
|
|||||||
}
|
}
|
||||||
|
|
||||||
if rb.session.capabilities.Has(caps.ExtendedJoin) {
|
if rb.session.capabilities.Has(caps.ExtendedJoin) {
|
||||||
rb.Add(nil, details.nickMask, "JOIN", chname, details.accountName, details.realname)
|
rb.AddFromClient(message.Time, message.Msgid, details.nickMask, details.accountName, nil, "JOIN", chname, details.accountName, details.realname)
|
||||||
} else {
|
} else {
|
||||||
rb.Add(nil, details.nickMask, "JOIN", chname)
|
rb.AddFromClient(message.Time, message.Msgid, details.nickMask, details.accountName, nil, "JOIN", chname)
|
||||||
}
|
}
|
||||||
|
|
||||||
if rb.session.client == client {
|
if rb.session.client == client {
|
||||||
@ -613,10 +616,13 @@ func (channel *Channel) Join(client *Client, key string, isSajoin bool, rb *Resp
|
|||||||
rb.Flush(true)
|
rb.Flush(true)
|
||||||
|
|
||||||
replayLimit := channel.server.Config().History.AutoreplayOnJoin
|
replayLimit := channel.server.Config().History.AutoreplayOnJoin
|
||||||
if replayLimit > 0 {
|
if 0 < replayLimit {
|
||||||
|
// TODO don't replay the client's own JOIN line?
|
||||||
items := channel.history.Latest(replayLimit)
|
items := channel.history.Latest(replayLimit)
|
||||||
channel.replayHistoryItems(rb, items)
|
if 0 < len(items) {
|
||||||
rb.Flush(true)
|
channel.replayHistoryItems(rb, items, true)
|
||||||
|
rb.Flush(true)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -647,14 +653,16 @@ func (channel *Channel) Part(client *Client, message string, rb *ResponseBuffer)
|
|||||||
|
|
||||||
channel.Quit(client)
|
channel.Quit(client)
|
||||||
|
|
||||||
|
splitMessage := utils.MakeSplitMessage(message, true)
|
||||||
|
|
||||||
details := client.Details()
|
details := client.Details()
|
||||||
for _, member := range channel.Members() {
|
for _, member := range channel.Members() {
|
||||||
member.Send(nil, details.nickMask, "PART", chname, message)
|
member.sendFromClientInternal(false, splitMessage.Time, splitMessage.Msgid, details.nickMask, details.accountName, nil, "PART", chname, message)
|
||||||
}
|
}
|
||||||
rb.Add(nil, details.nickMask, "PART", chname, message)
|
rb.AddFromClient(splitMessage.Time, splitMessage.Msgid, details.nickMask, details.accountName, nil, "PART", chname, message)
|
||||||
for _, session := range client.Sessions() {
|
for _, session := range client.Sessions() {
|
||||||
if session != rb.session {
|
if session != rb.session {
|
||||||
session.Send(nil, details.nickMask, "PART", chname, message)
|
session.sendFromClientInternal(false, splitMessage.Time, splitMessage.Msgid, details.nickMask, details.accountName, nil, "PART", chname, message)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -662,7 +670,7 @@ func (channel *Channel) Part(client *Client, message string, rb *ResponseBuffer)
|
|||||||
Type: history.Part,
|
Type: history.Part,
|
||||||
Nick: details.nickMask,
|
Nick: details.nickMask,
|
||||||
AccountName: details.accountName,
|
AccountName: details.accountName,
|
||||||
Message: utils.MakeSplitMessage(message, true),
|
Message: splitMessage,
|
||||||
})
|
})
|
||||||
|
|
||||||
client.server.logger.Debug("part", fmt.Sprintf("%s left channel %s", details.nick, chname))
|
client.server.logger.Debug("part", fmt.Sprintf("%s left channel %s", details.nick, chname))
|
||||||
@ -748,7 +756,7 @@ func (channel *Channel) resumeAndAnnounce(newClient, oldClient *Client) {
|
|||||||
func (channel *Channel) replayHistoryForResume(newClient *Client, after time.Time, before time.Time) {
|
func (channel *Channel) replayHistoryForResume(newClient *Client, after time.Time, before time.Time) {
|
||||||
items, complete := channel.history.Between(after, before, false, 0)
|
items, complete := channel.history.Between(after, before, false, 0)
|
||||||
rb := NewResponseBuffer(newClient.Sessions()[0])
|
rb := NewResponseBuffer(newClient.Sessions()[0])
|
||||||
channel.replayHistoryItems(rb, items)
|
channel.replayHistoryItems(rb, items, false)
|
||||||
if !complete && !newClient.resumeDetails.HistoryIncomplete {
|
if !complete && !newClient.resumeDetails.HistoryIncomplete {
|
||||||
// warn here if we didn't warn already
|
// 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.Add(nil, "HistServ", "NOTICE", channel.Name(), newClient.t("Some additional message history may have been lost"))
|
||||||
@ -759,50 +767,93 @@ func (channel *Channel) replayHistoryForResume(newClient *Client, after time.Tim
|
|||||||
func stripMaskFromNick(nickMask string) (nick string) {
|
func stripMaskFromNick(nickMask string) (nick string) {
|
||||||
index := strings.Index(nickMask, "!")
|
index := strings.Index(nickMask, "!")
|
||||||
if index == -1 {
|
if index == -1 {
|
||||||
return
|
return nickMask
|
||||||
}
|
}
|
||||||
return nickMask[0:index]
|
return nickMask[0:index]
|
||||||
}
|
}
|
||||||
|
|
||||||
func (channel *Channel) replayHistoryItems(rb *ResponseBuffer, items []history.Item) {
|
// munge the msgid corresponding to a replayable event,
|
||||||
|
// yielding a consistent msgid for the fake PRIVMSG from HistServ
|
||||||
|
func mungeMsgidForHistserv(token string) (result string) {
|
||||||
|
return fmt.Sprintf("_%s", token)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (channel *Channel) replayHistoryItems(rb *ResponseBuffer, items []history.Item, autoreplay bool) {
|
||||||
chname := channel.Name()
|
chname := channel.Name()
|
||||||
client := rb.target
|
client := rb.target
|
||||||
serverTime := rb.session.capabilities.Has(caps.ServerTime)
|
eventPlayback := rb.session.capabilities.Has(caps.EventPlayback)
|
||||||
|
extendedJoin := rb.session.capabilities.Has(caps.ExtendedJoin)
|
||||||
|
|
||||||
|
if len(items) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
batchID := rb.StartNestedHistoryBatch(chname)
|
||||||
|
defer rb.EndNestedBatch(batchID)
|
||||||
|
|
||||||
for _, item := range items {
|
for _, item := range items {
|
||||||
var tags map[string]string
|
nick := stripMaskFromNick(item.Nick)
|
||||||
if serverTime {
|
|
||||||
tags = map[string]string{"time": item.Time.Format(IRCv3TimestampFormat)}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO(#437) support history.Tagmsg
|
|
||||||
switch item.Type {
|
switch item.Type {
|
||||||
case history.Privmsg:
|
case history.Privmsg:
|
||||||
rb.AddSplitMessageFromClient(item.Nick, item.AccountName, tags, "PRIVMSG", chname, item.Message)
|
rb.AddSplitMessageFromClient(item.Nick, item.AccountName, item.Tags, "PRIVMSG", chname, item.Message)
|
||||||
case history.Notice:
|
case history.Notice:
|
||||||
rb.AddSplitMessageFromClient(item.Nick, item.AccountName, tags, "NOTICE", chname, item.Message)
|
rb.AddSplitMessageFromClient(item.Nick, item.AccountName, item.Tags, "NOTICE", chname, item.Message)
|
||||||
case history.Join:
|
case history.Tagmsg:
|
||||||
nick := stripMaskFromNick(item.Nick)
|
if rb.session.capabilities.Has(caps.MessageTags) {
|
||||||
var message string
|
rb.AddSplitMessageFromClient(item.Nick, item.AccountName, item.Tags, "TAGMSG", chname, item.Message)
|
||||||
if item.AccountName == "*" {
|
}
|
||||||
message = fmt.Sprintf(client.t("%s joined the channel"), nick)
|
case history.Join:
|
||||||
} else {
|
if eventPlayback {
|
||||||
message = fmt.Sprintf(client.t("%[1]s [account: %[2]s] joined the channel"), nick, item.AccountName)
|
if extendedJoin {
|
||||||
|
rb.AddFromClient(item.Message.Time, item.Message.Msgid, item.Nick, item.AccountName, nil, "HEVENT", "JOIN", chname, item.AccountName, item.Params[0])
|
||||||
|
} else {
|
||||||
|
rb.AddFromClient(item.Message.Time, item.Message.Msgid, item.Nick, item.AccountName, nil, "HEVENT", "JOIN", chname)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if autoreplay {
|
||||||
|
continue // #474
|
||||||
|
}
|
||||||
|
var message string
|
||||||
|
if item.AccountName == "*" {
|
||||||
|
message = fmt.Sprintf(client.t("%s joined the channel"), nick)
|
||||||
|
} else {
|
||||||
|
message = fmt.Sprintf(client.t("%[1]s [account: %[2]s] joined the channel"), nick, item.AccountName)
|
||||||
|
}
|
||||||
|
rb.AddFromClient(item.Message.Time, mungeMsgidForHistserv(item.Message.Msgid), "HistServ", "*", nil, "PRIVMSG", chname, message)
|
||||||
}
|
}
|
||||||
rb.Add(tags, "HistServ", "PRIVMSG", chname, message)
|
|
||||||
case history.Part:
|
case history.Part:
|
||||||
nick := stripMaskFromNick(item.Nick)
|
if eventPlayback {
|
||||||
message := fmt.Sprintf(client.t("%[1]s left the channel (%[2]s)"), nick, item.Message.Message)
|
rb.AddFromClient(item.Message.Time, item.Message.Msgid, item.Nick, item.AccountName, nil, "HEVENT", "PART", chname, item.Message.Message)
|
||||||
rb.Add(tags, "HistServ", "PRIVMSG", chname, message)
|
} else {
|
||||||
case history.Quit:
|
if autoreplay {
|
||||||
nick := stripMaskFromNick(item.Nick)
|
continue // #474
|
||||||
message := fmt.Sprintf(client.t("%[1]s quit (%[2]s)"), nick, item.Message.Message)
|
}
|
||||||
rb.Add(tags, "HistServ", "PRIVMSG", chname, message)
|
message := fmt.Sprintf(client.t("%[1]s left the channel (%[2]s)"), nick, item.Message.Message)
|
||||||
|
rb.AddFromClient(item.Message.Time, mungeMsgidForHistserv(item.Message.Msgid), "HistServ", "*", nil, "PRIVMSG", chname, message)
|
||||||
|
}
|
||||||
case history.Kick:
|
case history.Kick:
|
||||||
nick := stripMaskFromNick(item.Nick)
|
if eventPlayback {
|
||||||
// XXX Msgid is the kick target
|
rb.AddFromClient(item.Message.Time, item.Message.Msgid, item.Nick, item.AccountName, nil, "HEVENT", "KICK", chname, item.Params[0], item.Message.Message)
|
||||||
message := fmt.Sprintf(client.t("%[1]s kicked %[2]s (%[3]s)"), nick, item.Message.Msgid, item.Message.Message)
|
} else {
|
||||||
rb.Add(tags, "HistServ", "PRIVMSG", chname, message)
|
message := fmt.Sprintf(client.t("%[1]s kicked %[2]s (%[3]s)"), nick, item.Params[0], item.Message.Message)
|
||||||
|
rb.AddFromClient(item.Message.Time, mungeMsgidForHistserv(item.Message.Msgid), "HistServ", "*", nil, "PRIVMSG", chname, message)
|
||||||
|
}
|
||||||
|
case history.Quit:
|
||||||
|
if eventPlayback {
|
||||||
|
rb.AddFromClient(item.Message.Time, item.Message.Msgid, item.Nick, item.AccountName, nil, "HEVENT", "QUIT", item.Message.Message)
|
||||||
|
} else {
|
||||||
|
if autoreplay {
|
||||||
|
continue // #474
|
||||||
|
}
|
||||||
|
message := fmt.Sprintf(client.t("%[1]s quit (%[2]s)"), nick, item.Message.Message)
|
||||||
|
rb.AddFromClient(item.Message.Time, mungeMsgidForHistserv(item.Message.Msgid), "HistServ", "*", nil, "PRIVMSG", chname, message)
|
||||||
|
}
|
||||||
|
case history.Nick:
|
||||||
|
if eventPlayback {
|
||||||
|
rb.AddFromClient(item.Message.Time, item.Message.Msgid, item.Nick, item.AccountName, nil, "HEVENT", "NICK", item.Params[0])
|
||||||
|
} else {
|
||||||
|
message := fmt.Sprintf(client.t("%[1]s changed nick to %[2]s"), nick, item.Params[0])
|
||||||
|
rb.AddFromClient(item.Message.Time, mungeMsgidForHistserv(item.Message.Msgid), "HistServ", "*", nil, "PRIVMSG", chname, message)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -934,7 +985,7 @@ func (channel *Channel) SendSplitMessage(command string, minPrefixMode modes.Mod
|
|||||||
tagsToUse = clientOnlyTags
|
tagsToUse = clientOnlyTags
|
||||||
}
|
}
|
||||||
if histType == history.Tagmsg && rb.session.capabilities.Has(caps.MessageTags) {
|
if histType == history.Tagmsg && rb.session.capabilities.Has(caps.MessageTags) {
|
||||||
rb.AddFromClient(message.Msgid, nickmask, account, tagsToUse, command, chname)
|
rb.AddFromClient(message.Time, message.Msgid, nickmask, account, tagsToUse, command, chname)
|
||||||
} else {
|
} else {
|
||||||
rb.AddSplitMessageFromClient(nickmask, account, tagsToUse, command, chname, message)
|
rb.AddSplitMessageFromClient(nickmask, account, tagsToUse, command, chname, message)
|
||||||
}
|
}
|
||||||
@ -986,7 +1037,7 @@ func (channel *Channel) SendSplitMessage(command string, minPrefixMode modes.Mod
|
|||||||
Message: message,
|
Message: message,
|
||||||
Nick: nickmask,
|
Nick: nickmask,
|
||||||
AccountName: account,
|
AccountName: account,
|
||||||
Time: now,
|
Tags: clientOnlyTags,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1082,27 +1133,29 @@ func (channel *Channel) Kick(client *Client, target *Client, comment string, rb
|
|||||||
comment = comment[:kicklimit]
|
comment = comment[:kicklimit]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
message := utils.MakeSplitMessage(comment, true)
|
||||||
clientMask := client.NickMaskString()
|
clientMask := client.NickMaskString()
|
||||||
|
clientAccount := client.AccountName()
|
||||||
|
|
||||||
targetNick := target.Nick()
|
targetNick := target.Nick()
|
||||||
chname := channel.Name()
|
chname := channel.Name()
|
||||||
for _, member := range channel.Members() {
|
for _, member := range channel.Members() {
|
||||||
for _, session := range member.Sessions() {
|
for _, session := range member.Sessions() {
|
||||||
if session != rb.session {
|
if session != rb.session {
|
||||||
session.Send(nil, clientMask, "KICK", chname, targetNick, comment)
|
session.sendFromClientInternal(false, message.Time, message.Msgid, clientMask, clientAccount, nil, "KICK", chname, targetNick, comment)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
rb.Add(nil, clientMask, "KICK", chname, targetNick, comment)
|
rb.Add(nil, clientMask, "KICK", chname, targetNick, comment)
|
||||||
|
|
||||||
message := utils.SplitMessage{}
|
histItem := history.Item{
|
||||||
message.Message = comment
|
|
||||||
message.Msgid = targetNick // XXX abuse this field
|
|
||||||
channel.history.Add(history.Item{
|
|
||||||
Type: history.Kick,
|
Type: history.Kick,
|
||||||
Nick: clientMask,
|
Nick: clientMask,
|
||||||
AccountName: target.AccountName(),
|
AccountName: target.AccountName(),
|
||||||
Message: message,
|
Message: message,
|
||||||
})
|
}
|
||||||
|
histItem.Params[0] = targetNick
|
||||||
|
channel.history.Add(histItem)
|
||||||
|
|
||||||
channel.Quit(target)
|
channel.Quit(target)
|
||||||
}
|
}
|
||||||
|
159
irc/client.go
159
irc/client.go
@ -94,7 +94,14 @@ type Client struct {
|
|||||||
type Session struct {
|
type Session struct {
|
||||||
client *Client
|
client *Client
|
||||||
|
|
||||||
socket *Socket
|
ctime time.Time
|
||||||
|
atime time.Time
|
||||||
|
|
||||||
|
socket *Socket
|
||||||
|
realIP net.IP
|
||||||
|
proxiedIP net.IP
|
||||||
|
rawHostname string
|
||||||
|
|
||||||
idletimer IdleTimer
|
idletimer IdleTimer
|
||||||
fakelag Fakelag
|
fakelag Fakelag
|
||||||
|
|
||||||
@ -104,9 +111,6 @@ type Session struct {
|
|||||||
maxlenRest uint32
|
maxlenRest uint32
|
||||||
capState caps.State
|
capState caps.State
|
||||||
capVersion caps.Version
|
capVersion caps.Version
|
||||||
|
|
||||||
// TODO track per-connection real IP, proxied IP, and hostname here,
|
|
||||||
// so we can list attached sessions and their details
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// sets the session quit message, if there isn't one already
|
// sets the session quit message, if there isn't one already
|
||||||
@ -187,6 +191,8 @@ func RunNewClient(server *Server, conn clientConn) {
|
|||||||
socket: socket,
|
socket: socket,
|
||||||
capVersion: caps.Cap301,
|
capVersion: caps.Cap301,
|
||||||
capState: caps.NoneState,
|
capState: caps.NoneState,
|
||||||
|
ctime: now,
|
||||||
|
atime: now,
|
||||||
}
|
}
|
||||||
session.SetMaxlenRest()
|
session.SetMaxlenRest()
|
||||||
client.sessions = []*Session{session}
|
client.sessions = []*Session{session}
|
||||||
@ -197,20 +203,29 @@ func RunNewClient(server *Server, conn clientConn) {
|
|||||||
client.certfp, _ = socket.CertFP()
|
client.certfp, _ = socket.CertFP()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
remoteAddr := conn.Conn.RemoteAddr()
|
||||||
if conn.IsTor {
|
if conn.IsTor {
|
||||||
client.SetMode(modes.TLS, true)
|
client.SetMode(modes.TLS, true)
|
||||||
client.realIP = utils.IPv4LoopbackAddress
|
session.realIP = utils.AddrToIP(remoteAddr)
|
||||||
client.rawHostname = config.Server.TorListeners.Vhost
|
// cover up details of the tor proxying infrastructure (not a user privacy concern,
|
||||||
|
// but a hardening measure):
|
||||||
|
session.proxiedIP = utils.IPv4LoopbackAddress
|
||||||
|
session.rawHostname = config.Server.TorListeners.Vhost
|
||||||
} else {
|
} else {
|
||||||
remoteAddr := conn.Conn.RemoteAddr()
|
session.realIP = utils.AddrToIP(remoteAddr)
|
||||||
client.realIP = utils.AddrToIP(remoteAddr)
|
// set the hostname for this client (may be overridden later by PROXY or WEBIRC)
|
||||||
// Set the hostname for this client
|
session.rawHostname = utils.LookupHostname(session.realIP.String())
|
||||||
// (may be overridden by a later PROXY command from stunnel)
|
if utils.AddrIsLocal(remoteAddr) {
|
||||||
client.rawHostname = utils.LookupHostname(client.realIP.String())
|
// treat local connections as secure (may be overridden later by WEBIRC)
|
||||||
|
client.SetMode(modes.TLS, true)
|
||||||
|
}
|
||||||
if config.Server.CheckIdent && !utils.AddrIsUnix(remoteAddr) {
|
if config.Server.CheckIdent && !utils.AddrIsUnix(remoteAddr) {
|
||||||
client.doIdentLookup(conn.Conn)
|
client.doIdentLookup(conn.Conn)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
client.realIP = session.realIP
|
||||||
|
client.rawHostname = session.rawHostname
|
||||||
|
client.proxiedIP = session.proxiedIP
|
||||||
|
|
||||||
client.run(session)
|
client.run(session)
|
||||||
}
|
}
|
||||||
@ -308,8 +323,10 @@ func (client *Client) run(session *Session) {
|
|||||||
session.resetFakelag()
|
session.resetFakelag()
|
||||||
|
|
||||||
isReattach := client.Registered()
|
isReattach := client.Registered()
|
||||||
// don't reset the nick timer during a reattach
|
if isReattach {
|
||||||
if !isReattach {
|
client.playReattachMessages(session)
|
||||||
|
} else {
|
||||||
|
// don't reset the nick timer during a reattach
|
||||||
client.nickTimer.Initialize(client)
|
client.nickTimer.Initialize(client)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -371,14 +388,14 @@ func (client *Client) run(session *Session) {
|
|||||||
break
|
break
|
||||||
} else if session.client != client {
|
} else if session.client != client {
|
||||||
// bouncer reattach
|
// bouncer reattach
|
||||||
session.playReattachMessages()
|
|
||||||
go session.client.run(session)
|
go session.client.run(session)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (session *Session) playReattachMessages() {
|
func (client *Client) playReattachMessages(session *Session) {
|
||||||
|
client.server.playRegistrationBurst(session)
|
||||||
for _, channel := range session.client.Channels() {
|
for _, channel := range session.client.Channels() {
|
||||||
channel.playJoinForSession(session)
|
channel.playJoinForSession(session)
|
||||||
}
|
}
|
||||||
@ -389,10 +406,13 @@ func (session *Session) playReattachMessages() {
|
|||||||
//
|
//
|
||||||
|
|
||||||
// Active updates when the client was last 'active' (i.e. the user should be sitting in front of their client).
|
// Active updates when the client was last 'active' (i.e. the user should be sitting in front of their client).
|
||||||
func (client *Client) Active() {
|
func (client *Client) Active(session *Session) {
|
||||||
|
// TODO normalize all times to utc?
|
||||||
|
now := time.Now()
|
||||||
client.stateMutex.Lock()
|
client.stateMutex.Lock()
|
||||||
defer client.stateMutex.Unlock()
|
defer client.stateMutex.Unlock()
|
||||||
client.atime = time.Now()
|
session.atime = now
|
||||||
|
client.atime = now
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ping sends the client a PING message.
|
// Ping sends the client a PING message.
|
||||||
@ -487,7 +507,7 @@ func (client *Client) tryResume() (success bool) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
privmsgMatcher := func(item history.Item) bool {
|
privmsgMatcher := func(item history.Item) bool {
|
||||||
return item.Type == history.Privmsg || item.Type == history.Notice
|
return item.Type == history.Privmsg || item.Type == history.Notice || item.Type == history.Tagmsg
|
||||||
}
|
}
|
||||||
privmsgHistory := oldClient.history.Match(privmsgMatcher, false, 0)
|
privmsgHistory := oldClient.history.Match(privmsgMatcher, false, 0)
|
||||||
lastDiscarded := oldClient.history.LastDiscarded()
|
lastDiscarded := oldClient.history.LastDiscarded()
|
||||||
@ -495,8 +515,7 @@ func (client *Client) tryResume() (success bool) {
|
|||||||
oldestLostMessage = lastDiscarded
|
oldestLostMessage = lastDiscarded
|
||||||
}
|
}
|
||||||
for _, item := range privmsgHistory {
|
for _, item := range privmsgHistory {
|
||||||
// TODO this is the nickmask, fix that
|
sender := server.clients.Get(stripMaskFromNick(item.Nick))
|
||||||
sender := server.clients.Get(item.Nick)
|
|
||||||
if sender != nil {
|
if sender != nil {
|
||||||
friends.Add(sender)
|
friends.Add(sender)
|
||||||
}
|
}
|
||||||
@ -559,8 +578,13 @@ func (client *Client) tryResumeChannels() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (client *Client) replayPrivmsgHistory(rb *ResponseBuffer, items []history.Item, complete bool) {
|
func (client *Client) replayPrivmsgHistory(rb *ResponseBuffer, items []history.Item, complete bool) {
|
||||||
|
var batchID string
|
||||||
nick := client.Nick()
|
nick := client.Nick()
|
||||||
serverTime := rb.session.capabilities.Has(caps.ServerTime)
|
if 0 < len(items) {
|
||||||
|
batchID = rb.StartNestedHistoryBatch(nick)
|
||||||
|
}
|
||||||
|
|
||||||
|
allowTags := rb.session.capabilities.Has(caps.MessageTags)
|
||||||
for _, item := range items {
|
for _, item := range items {
|
||||||
var command string
|
var command string
|
||||||
switch item.Type {
|
switch item.Type {
|
||||||
@ -568,15 +592,23 @@ func (client *Client) replayPrivmsgHistory(rb *ResponseBuffer, items []history.I
|
|||||||
command = "PRIVMSG"
|
command = "PRIVMSG"
|
||||||
case history.Notice:
|
case history.Notice:
|
||||||
command = "NOTICE"
|
command = "NOTICE"
|
||||||
|
case history.Tagmsg:
|
||||||
|
if allowTags {
|
||||||
|
command = "TAGMSG"
|
||||||
|
} else {
|
||||||
|
continue
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
var tags map[string]string
|
var tags map[string]string
|
||||||
if serverTime {
|
if allowTags {
|
||||||
tags = map[string]string{"time": item.Time.Format(IRCv3TimestampFormat)}
|
tags = item.Tags
|
||||||
}
|
}
|
||||||
rb.AddSplitMessageFromClient(item.Nick, item.AccountName, tags, command, nick, item.Message)
|
rb.AddSplitMessageFromClient(item.Nick, item.AccountName, tags, command, nick, item.Message)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
rb.EndNestedBatch(batchID)
|
||||||
if !complete {
|
if !complete {
|
||||||
rb.Add(nil, "HistServ", "NOTICE", nick, client.t("Some additional message history may have been lost"))
|
rb.Add(nil, "HistServ", "NOTICE", nick, client.t("Some additional message history may have been lost"))
|
||||||
}
|
}
|
||||||
@ -891,14 +923,11 @@ func (client *Client) destroy(beingResumed bool, session *Session) {
|
|||||||
|
|
||||||
// allow destroy() to execute at most once
|
// allow destroy() to execute at most once
|
||||||
client.stateMutex.Lock()
|
client.stateMutex.Lock()
|
||||||
nickMaskString := client.nickMaskString
|
details := client.detailsNoMutex()
|
||||||
accountName := client.accountName
|
wasReattach := session != nil && session.client != client
|
||||||
|
|
||||||
alreadyDestroyed := len(client.sessions) == 0
|
|
||||||
sessionRemoved := false
|
sessionRemoved := false
|
||||||
var remainingSessions int
|
var remainingSessions int
|
||||||
if session == nil {
|
if session == nil {
|
||||||
sessionRemoved = !alreadyDestroyed
|
|
||||||
sessionsToDestroy = client.sessions
|
sessionsToDestroy = client.sessions
|
||||||
client.sessions = nil
|
client.sessions = nil
|
||||||
remainingSessions = 0
|
remainingSessions = 0
|
||||||
@ -908,27 +937,42 @@ func (client *Client) destroy(beingResumed bool, session *Session) {
|
|||||||
sessionsToDestroy = []*Session{session}
|
sessionsToDestroy = []*Session{session}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
var quitMessage string
|
|
||||||
if 0 < len(sessionsToDestroy) {
|
|
||||||
quitMessage = sessionsToDestroy[0].quitMessage
|
|
||||||
}
|
|
||||||
client.stateMutex.Unlock()
|
client.stateMutex.Unlock()
|
||||||
|
|
||||||
if alreadyDestroyed || !sessionRemoved {
|
if len(sessionsToDestroy) == 0 {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// destroy all applicable sessions:
|
||||||
|
var quitMessage string
|
||||||
for _, session := range sessionsToDestroy {
|
for _, session := range sessionsToDestroy {
|
||||||
if session.client != client {
|
if session.client != client {
|
||||||
// session has been attached to a new client; do not destroy it
|
// session has been attached to a new client; do not destroy it
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
session.idletimer.Stop()
|
session.idletimer.Stop()
|
||||||
session.socket.Close()
|
|
||||||
// send quit/error message to client if they haven't been sent already
|
// send quit/error message to client if they haven't been sent already
|
||||||
client.Quit("", session)
|
client.Quit("", session)
|
||||||
|
quitMessage = session.quitMessage
|
||||||
|
session.socket.Close()
|
||||||
|
|
||||||
|
// remove from connection limits
|
||||||
|
var source string
|
||||||
|
if client.isTor {
|
||||||
|
client.server.torLimiter.RemoveClient()
|
||||||
|
source = "tor"
|
||||||
|
} else {
|
||||||
|
ip := session.realIP
|
||||||
|
if session.proxiedIP != nil {
|
||||||
|
ip = session.proxiedIP
|
||||||
|
}
|
||||||
|
client.server.connectionLimiter.RemoveClient(ip)
|
||||||
|
source = ip.String()
|
||||||
|
}
|
||||||
|
client.server.logger.Info("localconnect-ip", fmt.Sprintf("disconnecting session of %s from %s", details.nick, source))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ok, now destroy the client, unless it still has sessions:
|
||||||
if remainingSessions != 0 {
|
if remainingSessions != 0 {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -939,39 +983,37 @@ func (client *Client) destroy(beingResumed bool, session *Session) {
|
|||||||
defer client.server.semaphores.ClientDestroy.Release()
|
defer client.server.semaphores.ClientDestroy.Release()
|
||||||
|
|
||||||
if beingResumed {
|
if beingResumed {
|
||||||
client.server.logger.Debug("quit", fmt.Sprintf("%s is being resumed", client.nick))
|
client.server.logger.Debug("quit", fmt.Sprintf("%s is being resumed", details.nick))
|
||||||
} else {
|
} else if !wasReattach {
|
||||||
client.server.logger.Debug("quit", fmt.Sprintf("%s is no longer on the server", client.nick))
|
client.server.logger.Debug("quit", fmt.Sprintf("%s is no longer on the server", details.nick))
|
||||||
}
|
}
|
||||||
|
|
||||||
if !beingResumed {
|
registered := client.Registered()
|
||||||
|
if !beingResumed && registered {
|
||||||
client.server.whoWas.Append(client.WhoWas())
|
client.server.whoWas.Append(client.WhoWas())
|
||||||
}
|
}
|
||||||
|
|
||||||
// remove from connection limits
|
|
||||||
if client.isTor {
|
|
||||||
client.server.torLimiter.RemoveClient()
|
|
||||||
} else {
|
|
||||||
client.server.connectionLimiter.RemoveClient(client.IP())
|
|
||||||
}
|
|
||||||
|
|
||||||
client.server.resumeManager.Delete(client)
|
client.server.resumeManager.Delete(client)
|
||||||
|
|
||||||
// alert monitors
|
// alert monitors
|
||||||
client.server.monitorManager.AlertAbout(client, false)
|
if registered {
|
||||||
|
client.server.monitorManager.AlertAbout(client, false)
|
||||||
|
}
|
||||||
// clean up monitor state
|
// clean up monitor state
|
||||||
client.server.monitorManager.RemoveAll(client)
|
client.server.monitorManager.RemoveAll(client)
|
||||||
|
|
||||||
|
splitQuitMessage := utils.MakeSplitMessage(quitMessage, true)
|
||||||
// clean up channels
|
// clean up channels
|
||||||
|
// (note that if this is a reattach, client has no channels and therefore no friends)
|
||||||
friends := make(ClientSet)
|
friends := make(ClientSet)
|
||||||
for _, channel := range client.Channels() {
|
for _, channel := range client.Channels() {
|
||||||
if !beingResumed {
|
if !beingResumed {
|
||||||
channel.Quit(client)
|
channel.Quit(client)
|
||||||
channel.history.Add(history.Item{
|
channel.history.Add(history.Item{
|
||||||
Type: history.Quit,
|
Type: history.Quit,
|
||||||
Nick: nickMaskString,
|
Nick: details.nickMask,
|
||||||
AccountName: accountName,
|
AccountName: details.accountName,
|
||||||
Message: utils.MakeSplitMessage(quitMessage, true),
|
Message: splitQuitMessage,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
for _, member := range channel.Members() {
|
for _, member := range channel.Members() {
|
||||||
@ -1006,14 +1048,14 @@ func (client *Client) destroy(beingResumed bool, session *Session) {
|
|||||||
if quitMessage == "" {
|
if quitMessage == "" {
|
||||||
quitMessage = "Exited"
|
quitMessage = "Exited"
|
||||||
}
|
}
|
||||||
friend.Send(nil, client.nickMaskString, "QUIT", quitMessage)
|
friend.sendFromClientInternal(false, splitQuitMessage.Time, splitQuitMessage.Msgid, details.nickMask, details.accountName, nil, "QUIT", quitMessage)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if !client.exitedSnomaskSent {
|
if !client.exitedSnomaskSent {
|
||||||
if beingResumed {
|
if beingResumed {
|
||||||
client.server.snomasks.Send(sno.LocalQuits, fmt.Sprintf(ircfmt.Unescape("%s$r is resuming their connection, old client has been destroyed"), client.nick))
|
client.server.snomasks.Send(sno.LocalQuits, fmt.Sprintf(ircfmt.Unescape("%s$r is resuming their connection, old client has been destroyed"), client.nick))
|
||||||
} else {
|
} else {
|
||||||
client.server.snomasks.Send(sno.LocalQuits, fmt.Sprintf(ircfmt.Unescape("%s$r exited the network"), client.nick))
|
client.server.snomasks.Send(sno.LocalQuits, fmt.Sprintf(ircfmt.Unescape("%s$r exited the network"), details.nick))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1030,15 +1072,7 @@ func (session *Session) sendSplitMsgFromClientInternal(blocking bool, serverTime
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// SendFromClient sends an IRC line coming from a specific client.
|
// Sends a line with `nickmask` as the prefix, adding `time` and `account` tags if supported
|
||||||
// Adds account-tag to the line as well.
|
|
||||||
func (client *Client) SendFromClient(msgid string, from *Client, tags map[string]string, command string, params ...string) error {
|
|
||||||
return client.sendFromClientInternal(false, time.Time{}, msgid, from.NickMaskString(), from.AccountName(), tags, command, params...)
|
|
||||||
}
|
|
||||||
|
|
||||||
// this is SendFromClient, but directly exposing nickmask and accountName,
|
|
||||||
// for things like history replay and CHGHOST where they no longer (necessarily)
|
|
||||||
// correspond to the current state of a client
|
|
||||||
func (client *Client) sendFromClientInternal(blocking bool, serverTime time.Time, msgid string, nickmask, accountName string, tags map[string]string, command string, params ...string) (err error) {
|
func (client *Client) sendFromClientInternal(blocking bool, serverTime time.Time, msgid string, nickmask, accountName string, tags map[string]string, command string, params ...string) (err error) {
|
||||||
for _, session := range client.Sessions() {
|
for _, session := range client.Sessions() {
|
||||||
err_ := session.sendFromClientInternal(blocking, serverTime, msgid, nickmask, accountName, tags, command, params...)
|
err_ := session.sendFromClientInternal(blocking, serverTime, msgid, nickmask, accountName, tags, command, params...)
|
||||||
@ -1061,7 +1095,10 @@ func (session *Session) sendFromClientInternal(blocking bool, serverTime time.Ti
|
|||||||
}
|
}
|
||||||
// attach server-time
|
// attach server-time
|
||||||
if session.capabilities.Has(caps.ServerTime) {
|
if session.capabilities.Has(caps.ServerTime) {
|
||||||
msg.SetTag("time", time.Now().UTC().Format(IRCv3TimestampFormat))
|
if serverTime.IsZero() {
|
||||||
|
serverTime = time.Now().UTC()
|
||||||
|
}
|
||||||
|
msg.SetTag("time", serverTime.Format(IRCv3TimestampFormat))
|
||||||
}
|
}
|
||||||
|
|
||||||
return session.SendRawMessage(msg, blocking)
|
return session.SendRawMessage(msg, blocking)
|
||||||
|
@ -152,7 +152,7 @@ func (clients *ClientManager) SetNick(client *Client, session *Session, newNick
|
|||||||
|
|
||||||
currentClient := clients.byNick[newcfnick]
|
currentClient := clients.byNick[newcfnick]
|
||||||
// the client may just be changing case
|
// the client may just be changing case
|
||||||
if currentClient != nil && currentClient != client {
|
if currentClient != nil && currentClient != client && session != nil {
|
||||||
// these conditions forbid reattaching to an existing session:
|
// these conditions forbid reattaching to an existing session:
|
||||||
if client.Registered() || !bouncerAllowed || account == "" || account != currentClient.Account() || client.isTor != currentClient.isTor || client.HasMode(modes.TLS) != currentClient.HasMode(modes.TLS) {
|
if client.Registered() || !bouncerAllowed || account == "" || account != currentClient.Account() || client.isTor != currentClient.isTor || client.HasMode(modes.TLS) != currentClient.HasMode(modes.TLS) {
|
||||||
return errNicknameInUse
|
return errNicknameInUse
|
||||||
@ -160,9 +160,7 @@ func (clients *ClientManager) SetNick(client *Client, session *Session, newNick
|
|||||||
if !currentClient.AddSession(session) {
|
if !currentClient.AddSession(session) {
|
||||||
return errNicknameInUse
|
return errNicknameInUse
|
||||||
}
|
}
|
||||||
// successful reattach. temporarily assign them the nick they'll have going forward
|
// successful reattach!
|
||||||
// (the current `client` will be discarded at the end of command execution)
|
|
||||||
client.updateNick(currentClient.Nick(), newcfnick, newSkeleton)
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
// analogous checks for skeletons
|
// analogous checks for skeletons
|
||||||
|
@ -58,8 +58,8 @@ func (cmd *Command) Run(server *Server, client *Client, session *Session, msg ir
|
|||||||
session.idletimer.Touch()
|
session.idletimer.Touch()
|
||||||
}
|
}
|
||||||
|
|
||||||
if !cmd.leaveClientIdle {
|
if client.registered && !cmd.leaveClientIdle {
|
||||||
client.Active()
|
client.Active(session)
|
||||||
}
|
}
|
||||||
|
|
||||||
return exiting
|
return exiting
|
||||||
|
@ -73,7 +73,9 @@ func (client *Client) ApplyProxiedIP(session *Session, proxiedIP string, tls boo
|
|||||||
|
|
||||||
client.stateMutex.Lock()
|
client.stateMutex.Lock()
|
||||||
defer client.stateMutex.Unlock()
|
defer client.stateMutex.Unlock()
|
||||||
|
session.proxiedIP = parsedProxiedIP
|
||||||
client.proxiedIP = parsedProxiedIP
|
client.proxiedIP = parsedProxiedIP
|
||||||
|
session.rawHostname = rawHostname
|
||||||
client.rawHostname = rawHostname
|
client.rawHostname = rawHostname
|
||||||
// nickmask will be updated when the client completes registration
|
// nickmask will be updated when the client completes registration
|
||||||
// set tls info
|
// set tls info
|
||||||
|
@ -4,6 +4,7 @@
|
|||||||
package irc
|
package irc
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"net"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"time"
|
"time"
|
||||||
"unsafe"
|
"unsafe"
|
||||||
@ -64,6 +65,37 @@ func (client *Client) Sessions() (sessions []*Session) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type SessionData struct {
|
||||||
|
ctime time.Time
|
||||||
|
atime time.Time
|
||||||
|
ip net.IP
|
||||||
|
hostname string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (client *Client) AllSessionData(currentSession *Session) (data []SessionData, currentIndex int) {
|
||||||
|
currentIndex = -1
|
||||||
|
client.stateMutex.RLock()
|
||||||
|
defer client.stateMutex.RUnlock()
|
||||||
|
|
||||||
|
data = make([]SessionData, len(client.sessions))
|
||||||
|
for i, session := range client.sessions {
|
||||||
|
if session == currentSession {
|
||||||
|
currentIndex = i
|
||||||
|
}
|
||||||
|
data[i] = SessionData{
|
||||||
|
atime: session.atime,
|
||||||
|
ctime: session.ctime,
|
||||||
|
hostname: session.rawHostname,
|
||||||
|
}
|
||||||
|
if session.proxiedIP != nil {
|
||||||
|
data[i].ip = session.proxiedIP
|
||||||
|
} else {
|
||||||
|
data[i].ip = session.realIP
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
func (client *Client) AddSession(session *Session) (success bool) {
|
func (client *Client) AddSession(session *Session) (success bool) {
|
||||||
client.stateMutex.Lock()
|
client.stateMutex.Lock()
|
||||||
defer client.stateMutex.Unlock()
|
defer client.stateMutex.Unlock()
|
||||||
@ -291,7 +323,10 @@ func (client *Client) WhoWas() (result WhoWas) {
|
|||||||
func (client *Client) Details() (result ClientDetails) {
|
func (client *Client) Details() (result ClientDetails) {
|
||||||
client.stateMutex.RLock()
|
client.stateMutex.RLock()
|
||||||
defer client.stateMutex.RUnlock()
|
defer client.stateMutex.RUnlock()
|
||||||
|
return client.detailsNoMutex()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (client *Client) detailsNoMutex() (result ClientDetails) {
|
||||||
result.nick = client.nick
|
result.nick = client.nick
|
||||||
result.nickCasefolded = client.nickCasefolded
|
result.nickCasefolded = client.nickCasefolded
|
||||||
result.username = client.username
|
result.username = client.username
|
||||||
|
@ -594,36 +594,36 @@ func capHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Respo
|
|||||||
// e.g., CHATHISTORY #ircv3 BETWEEN timestamp=YYYY-MM-DDThh:mm:ss.sssZ timestamp=YYYY-MM-DDThh:mm:ss.sssZ + 100
|
// e.g., CHATHISTORY #ircv3 BETWEEN timestamp=YYYY-MM-DDThh:mm:ss.sssZ timestamp=YYYY-MM-DDThh:mm:ss.sssZ + 100
|
||||||
func chathistoryHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *ResponseBuffer) (exiting bool) {
|
func chathistoryHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *ResponseBuffer) (exiting bool) {
|
||||||
config := server.Config()
|
config := server.Config()
|
||||||
// batch type is chathistory; send an empty batch if necessary
|
|
||||||
rb.InitializeBatch("chathistory", true)
|
|
||||||
|
|
||||||
var items []history.Item
|
var items []history.Item
|
||||||
success := false
|
success := false
|
||||||
var hist *history.Buffer
|
var hist *history.Buffer
|
||||||
var channel *Channel
|
var channel *Channel
|
||||||
defer func() {
|
defer func() {
|
||||||
if success {
|
// successful responses are sent as a chathistory or history batch
|
||||||
|
if success && 0 < len(items) {
|
||||||
|
batchType := "chathistory"
|
||||||
|
if rb.session.capabilities.Has(caps.EventPlayback) {
|
||||||
|
batchType = "history"
|
||||||
|
}
|
||||||
|
rb.ForceBatchStart(batchType, true)
|
||||||
if channel == nil {
|
if channel == nil {
|
||||||
client.replayPrivmsgHistory(rb, items, true)
|
client.replayPrivmsgHistory(rb, items, true)
|
||||||
} else {
|
} else {
|
||||||
channel.replayHistoryItems(rb, items)
|
channel.replayHistoryItems(rb, items, false)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
rb.Send(true) // terminate the chathistory batch
|
|
||||||
if success && len(items) > 0 {
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
newRb := NewResponseBuffer(rb.session)
|
|
||||||
newRb.Label = rb.Label // same label, new batch
|
// errors are sent either without a batch, or in a draft/labeled-response batch as usual
|
||||||
// TODO: send `WARN CHATHISTORY MAX_MESSAGES_EXCEEDED` when appropriate
|
// TODO: send `WARN CHATHISTORY MAX_MESSAGES_EXCEEDED` when appropriate
|
||||||
if hist == nil {
|
if hist == nil {
|
||||||
newRb.Add(nil, server.name, "ERR", "CHATHISTORY", "NO_SUCH_CHANNEL")
|
rb.Add(nil, server.name, "ERR", "CHATHISTORY", "NO_SUCH_CHANNEL")
|
||||||
} else if len(items) == 0 {
|
} else if len(items) == 0 {
|
||||||
newRb.Add(nil, server.name, "ERR", "CHATHISTORY", "NO_TEXT_TO_SEND")
|
rb.Add(nil, server.name, "ERR", "CHATHISTORY", "NO_TEXT_TO_SEND")
|
||||||
} else if !success {
|
} else if !success {
|
||||||
newRb.Add(nil, server.name, "ERR", "CHATHISTORY", "NEED_MORE_PARAMS")
|
rb.Add(nil, server.name, "ERR", "CHATHISTORY", "NEED_MORE_PARAMS")
|
||||||
}
|
}
|
||||||
newRb.Send(true)
|
|
||||||
}()
|
}()
|
||||||
|
|
||||||
target := msg.Params[0]
|
target := msg.Params[0]
|
||||||
@ -753,7 +753,7 @@ func chathistoryHandler(server *Server, client *Client, msg ircmsg.IrcMessage, r
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
matches = func(item history.Item) bool {
|
matches = func(item history.Item) bool {
|
||||||
return before == item.Time.Before(timestamp)
|
return before == item.Message.Time.Before(timestamp)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
items = hist.Match(matches, !before, limit)
|
items = hist.Match(matches, !before, limit)
|
||||||
@ -776,7 +776,7 @@ func chathistoryHandler(server *Server, client *Client, msg ircmsg.IrcMessage, r
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
matches = func(item history.Item) bool {
|
matches = func(item history.Item) bool {
|
||||||
return item.Time.After(timestamp)
|
return item.Message.Time.After(timestamp)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
items = hist.Match(matches, false, limit)
|
items = hist.Match(matches, false, limit)
|
||||||
@ -799,16 +799,16 @@ func chathistoryHandler(server *Server, client *Client, msg ircmsg.IrcMessage, r
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
initialMatcher = func(item history.Item) (result bool) {
|
initialMatcher = func(item history.Item) (result bool) {
|
||||||
return item.Time.Before(timestamp)
|
return item.Message.Time.Before(timestamp)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
var halfLimit int
|
var halfLimit int
|
||||||
halfLimit = (limit + 1) / 2
|
halfLimit = (limit + 1) / 2
|
||||||
firstPass := hist.Match(initialMatcher, false, halfLimit)
|
firstPass := hist.Match(initialMatcher, false, halfLimit)
|
||||||
if len(firstPass) > 0 {
|
if len(firstPass) > 0 {
|
||||||
timeWindowStart := firstPass[0].Time
|
timeWindowStart := firstPass[0].Message.Time
|
||||||
items = hist.Match(func(item history.Item) bool {
|
items = hist.Match(func(item history.Item) bool {
|
||||||
return item.Time.Equal(timeWindowStart) || item.Time.After(timeWindowStart)
|
return item.Message.Time.Equal(timeWindowStart) || item.Message.Time.After(timeWindowStart)
|
||||||
}, true, limit)
|
}, true, limit)
|
||||||
}
|
}
|
||||||
success = true
|
success = true
|
||||||
@ -1118,7 +1118,7 @@ func historyHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *R
|
|||||||
items := hist.Latest(limit)
|
items := hist.Latest(limit)
|
||||||
|
|
||||||
if channel != nil {
|
if channel != nil {
|
||||||
channel.replayHistoryItems(rb, items)
|
channel.replayHistoryItems(rb, items, false)
|
||||||
} else {
|
} else {
|
||||||
client.replayPrivmsgHistory(rb, items, true)
|
client.replayPrivmsgHistory(rb, items, true)
|
||||||
}
|
}
|
||||||
@ -1963,7 +1963,6 @@ func messageHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *R
|
|||||||
for i, targetString := range targets {
|
for i, targetString := range targets {
|
||||||
// each target gets distinct msgids
|
// each target gets distinct msgids
|
||||||
splitMsg := utils.MakeSplitMessage(message, !rb.session.capabilities.Has(caps.MaxLine))
|
splitMsg := utils.MakeSplitMessage(message, !rb.session.capabilities.Has(caps.MaxLine))
|
||||||
now := time.Now().UTC()
|
|
||||||
|
|
||||||
// max of four targets per privmsg
|
// max of four targets per privmsg
|
||||||
if i > maxTargets-1 {
|
if i > maxTargets-1 {
|
||||||
@ -2012,17 +2011,17 @@ func messageHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *R
|
|||||||
if histType == history.Tagmsg {
|
if histType == history.Tagmsg {
|
||||||
// don't send TAGMSG at all if they don't have the tags cap
|
// don't send TAGMSG at all if they don't have the tags cap
|
||||||
if session.capabilities.Has(caps.MessageTags) {
|
if session.capabilities.Has(caps.MessageTags) {
|
||||||
session.sendFromClientInternal(false, now, splitMsg.Msgid, nickMaskString, accountName, clientOnlyTags, msg.Command, tnick)
|
session.sendFromClientInternal(false, splitMsg.Time, splitMsg.Msgid, nickMaskString, accountName, clientOnlyTags, msg.Command, tnick)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
session.sendSplitMsgFromClientInternal(false, now, nickMaskString, accountName, clientOnlyTags, msg.Command, tnick, splitMsg)
|
session.sendSplitMsgFromClientInternal(false, splitMsg.Time, nickMaskString, accountName, clientOnlyTags, msg.Command, tnick, splitMsg)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// an echo-message may need to be included in the response:
|
// an echo-message may need to be included in the response:
|
||||||
if rb.session.capabilities.Has(caps.EchoMessage) {
|
if rb.session.capabilities.Has(caps.EchoMessage) {
|
||||||
if histType == history.Tagmsg && rb.session.capabilities.Has(caps.MessageTags) {
|
if histType == history.Tagmsg && rb.session.capabilities.Has(caps.MessageTags) {
|
||||||
rb.AddFromClient(splitMsg.Msgid, nickMaskString, accountName, clientOnlyTags, msg.Command, tnick)
|
rb.AddFromClient(splitMsg.Time, splitMsg.Msgid, nickMaskString, accountName, clientOnlyTags, msg.Command, tnick)
|
||||||
} else {
|
} else {
|
||||||
rb.AddSplitMessageFromClient(nickMaskString, accountName, clientOnlyTags, msg.Command, tnick, splitMsg)
|
rb.AddSplitMessageFromClient(nickMaskString, accountName, clientOnlyTags, msg.Command, tnick, splitMsg)
|
||||||
}
|
}
|
||||||
@ -2033,9 +2032,9 @@ func messageHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *R
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if histType == history.Tagmsg && rb.session.capabilities.Has(caps.MessageTags) {
|
if histType == history.Tagmsg && rb.session.capabilities.Has(caps.MessageTags) {
|
||||||
session.sendFromClientInternal(false, now, splitMsg.Msgid, nickMaskString, accountName, clientOnlyTags, msg.Command, tnick)
|
session.sendFromClientInternal(false, splitMsg.Time, splitMsg.Msgid, nickMaskString, accountName, clientOnlyTags, msg.Command, tnick)
|
||||||
} else {
|
} else {
|
||||||
session.sendSplitMsgFromClientInternal(false, now, nickMaskString, accountName, clientOnlyTags, msg.Command, tnick, splitMsg)
|
session.sendSplitMsgFromClientInternal(false, splitMsg.Time, nickMaskString, accountName, clientOnlyTags, msg.Command, tnick, splitMsg)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if histType != history.Notice && user.Away() {
|
if histType != history.Notice && user.Away() {
|
||||||
|
@ -22,25 +22,52 @@ const (
|
|||||||
Quit
|
Quit
|
||||||
Mode
|
Mode
|
||||||
Tagmsg
|
Tagmsg
|
||||||
|
Nick
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// a Tagmsg that consists entirely of transient tags is not stored
|
||||||
|
var transientTags = map[string]bool{
|
||||||
|
"+draft/typing": true,
|
||||||
|
"+typing": true, // future-proofing
|
||||||
|
}
|
||||||
|
|
||||||
// Item represents an event (e.g., a PRIVMSG or a JOIN) and its associated data
|
// Item represents an event (e.g., a PRIVMSG or a JOIN) and its associated data
|
||||||
type Item struct {
|
type Item struct {
|
||||||
Type ItemType
|
Type ItemType
|
||||||
Time time.Time
|
|
||||||
|
|
||||||
Nick string
|
Nick string
|
||||||
// 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
|
|
||||||
// for non-privmsg items, we may stuff some other data in here
|
// for non-privmsg items, we may stuff some other data in here
|
||||||
|
Message utils.SplitMessage
|
||||||
|
Tags map[string]string
|
||||||
|
Params [1]string
|
||||||
}
|
}
|
||||||
|
|
||||||
// HasMsgid tests whether a message has the message id `msgid`.
|
// HasMsgid tests whether a message has the message id `msgid`.
|
||||||
func (item *Item) HasMsgid(msgid string) bool {
|
func (item *Item) HasMsgid(msgid string) bool {
|
||||||
// XXX we stuff other data in the Msgid field sometimes,
|
if item.Message.Msgid == msgid {
|
||||||
// don't match it by accident
|
return true
|
||||||
return (item.Type == Privmsg || item.Type == Notice) && item.Message.Msgid == msgid
|
}
|
||||||
|
for _, pair := range item.Message.Wrapped {
|
||||||
|
if pair.Msgid == msgid {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (item *Item) isStorable() bool {
|
||||||
|
if item.Type == Tagmsg {
|
||||||
|
for name := range item.Tags {
|
||||||
|
if !transientTags[name] {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false // all tags were blacklisted
|
||||||
|
} else {
|
||||||
|
return true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type Predicate func(item Item) (matches bool)
|
type Predicate func(item Item) (matches bool)
|
||||||
@ -94,8 +121,12 @@ func (list *Buffer) Add(item Item) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if item.Time.IsZero() {
|
if !item.isStorable() {
|
||||||
item.Time = time.Now().UTC()
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if item.Message.Time.IsZero() {
|
||||||
|
item.Message.Time = time.Now().UTC()
|
||||||
}
|
}
|
||||||
|
|
||||||
list.Lock()
|
list.Lock()
|
||||||
@ -114,8 +145,8 @@ func (list *Buffer) Add(item Item) {
|
|||||||
list.end = (list.end + 1) % len(list.buffer)
|
list.end = (list.end + 1) % len(list.buffer)
|
||||||
list.start = list.end // advance start as well, overwriting first entry
|
list.start = list.end // advance start as well, overwriting first entry
|
||||||
// record the timestamp of the overwritten item
|
// record the timestamp of the overwritten item
|
||||||
if list.lastDiscarded.Before(list.buffer[pos].Time) {
|
if list.lastDiscarded.Before(list.buffer[pos].Message.Time) {
|
||||||
list.lastDiscarded = list.buffer[pos].Time
|
list.lastDiscarded = list.buffer[pos].Message.Time
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -144,7 +175,7 @@ func (list *Buffer) Between(after, before time.Time, ascending bool, limit int)
|
|||||||
complete = after.Equal(list.lastDiscarded) || after.After(list.lastDiscarded)
|
complete = after.Equal(list.lastDiscarded) || after.After(list.lastDiscarded)
|
||||||
|
|
||||||
satisfies := func(item Item) bool {
|
satisfies := func(item Item) bool {
|
||||||
return (after.IsZero() || item.Time.After(after)) && (before.IsZero() || item.Time.Before(before))
|
return (after.IsZero() || item.Message.Time.After(after)) && (before.IsZero() || item.Message.Time.Before(before))
|
||||||
}
|
}
|
||||||
|
|
||||||
return list.matchInternal(satisfies, ascending, limit), complete
|
return list.matchInternal(satisfies, ascending, limit), complete
|
||||||
@ -264,8 +295,8 @@ func (list *Buffer) Resize(size int) {
|
|||||||
}
|
}
|
||||||
// update lastDiscarded for discarded entries
|
// update lastDiscarded for discarded entries
|
||||||
for i := list.start; i != start; i = (i + 1) % len(list.buffer) {
|
for i := list.start; i != start; i = (i + 1) % len(list.buffer) {
|
||||||
if list.lastDiscarded.Before(list.buffer[i].Time) {
|
if list.lastDiscarded.Before(list.buffer[i].Message.Time) {
|
||||||
list.lastDiscarded = list.buffer[i].Time
|
list.lastDiscarded = list.buffer[i].Message.Time
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -87,6 +87,12 @@ func easyParse(timestamp string) time.Time {
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func easyItem(nick string, timestamp string) (result Item) {
|
||||||
|
result.Message.Time = easyParse(timestamp)
|
||||||
|
result.Nick = nick
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
func assertEqual(supplied, expected interface{}, t *testing.T) {
|
func assertEqual(supplied, expected interface{}, t *testing.T) {
|
||||||
if !reflect.DeepEqual(supplied, expected) {
|
if !reflect.DeepEqual(supplied, expected) {
|
||||||
t.Errorf("expected %v but got %v", expected, supplied)
|
t.Errorf("expected %v but got %v", expected, supplied)
|
||||||
@ -97,30 +103,19 @@ func TestBuffer(t *testing.T) {
|
|||||||
start := easyParse("2006-01-01 00:00:00Z")
|
start := easyParse("2006-01-01 00:00:00Z")
|
||||||
|
|
||||||
buf := NewHistoryBuffer(3)
|
buf := NewHistoryBuffer(3)
|
||||||
buf.Add(Item{
|
buf.Add(easyItem("testnick0", "2006-01-01 15:04:05Z"))
|
||||||
Nick: "testnick0",
|
|
||||||
Time: easyParse("2006-01-01 15:04:05Z"),
|
|
||||||
})
|
|
||||||
|
|
||||||
buf.Add(Item{
|
buf.Add(easyItem("testnick1", "2006-01-02 15:04:05Z"))
|
||||||
Nick: "testnick1",
|
|
||||||
Time: easyParse("2006-01-02 15:04:05Z"),
|
|
||||||
})
|
|
||||||
|
|
||||||
buf.Add(Item{
|
buf.Add(easyItem("testnick2", "2006-01-03 15:04:05Z"))
|
||||||
Nick: "testnick2",
|
|
||||||
Time: easyParse("2006-01-03 15:04:05Z"),
|
|
||||||
})
|
|
||||||
|
|
||||||
since, complete := buf.Between(start, time.Now(), false, 0)
|
since, complete := buf.Between(start, time.Now(), false, 0)
|
||||||
assertEqual(complete, true, t)
|
assertEqual(complete, true, t)
|
||||||
assertEqual(toNicks(since), []string{"testnick0", "testnick1", "testnick2"}, t)
|
assertEqual(toNicks(since), []string{"testnick0", "testnick1", "testnick2"}, t)
|
||||||
|
|
||||||
// add another item, evicting the first
|
// add another item, evicting the first
|
||||||
buf.Add(Item{
|
buf.Add(easyItem("testnick3", "2006-01-04 15:04:05Z"))
|
||||||
Nick: "testnick3",
|
|
||||||
Time: easyParse("2006-01-04 15:04:05Z"),
|
|
||||||
})
|
|
||||||
since, complete = buf.Between(start, time.Now(), false, 0)
|
since, complete = buf.Between(start, time.Now(), false, 0)
|
||||||
assertEqual(complete, false, t)
|
assertEqual(complete, false, t)
|
||||||
assertEqual(toNicks(since), []string{"testnick1", "testnick2", "testnick3"}, t)
|
assertEqual(toNicks(since), []string{"testnick1", "testnick2", "testnick3"}, t)
|
||||||
@ -139,18 +134,9 @@ func TestBuffer(t *testing.T) {
|
|||||||
assertEqual(toNicks(since), []string{"testnick2", "testnick3"}, t)
|
assertEqual(toNicks(since), []string{"testnick2", "testnick3"}, t)
|
||||||
|
|
||||||
buf.Resize(5)
|
buf.Resize(5)
|
||||||
buf.Add(Item{
|
buf.Add(easyItem("testnick4", "2006-01-05 15:04:05Z"))
|
||||||
Nick: "testnick4",
|
buf.Add(easyItem("testnick5", "2006-01-06 15:04:05Z"))
|
||||||
Time: easyParse("2006-01-05 15:04:05Z"),
|
buf.Add(easyItem("testnick6", "2006-01-07 15:04:05Z"))
|
||||||
})
|
|
||||||
buf.Add(Item{
|
|
||||||
Nick: "testnick5",
|
|
||||||
Time: easyParse("2006-01-06 15:04:05Z"),
|
|
||||||
})
|
|
||||||
buf.Add(Item{
|
|
||||||
Nick: "testnick6",
|
|
||||||
Time: easyParse("2006-01-07 15:04:05Z"),
|
|
||||||
})
|
|
||||||
since, complete = buf.Between(easyParse("2006-01-03 00:00:00Z"), time.Now(), false, 0)
|
since, complete = buf.Between(easyParse("2006-01-03 00:00:00Z"), time.Now(), false, 0)
|
||||||
assertEqual(complete, true, t)
|
assertEqual(complete, true, t)
|
||||||
assertEqual(toNicks(since), []string{"testnick2", "testnick3", "testnick4", "testnick5", "testnick6"}, t)
|
assertEqual(toNicks(since), []string{"testnick2", "testnick3", "testnick4", "testnick5", "testnick6"}, t)
|
||||||
|
@ -11,7 +11,9 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/goshuirc/irc-go/ircfmt"
|
"github.com/goshuirc/irc-go/ircfmt"
|
||||||
|
"github.com/oragono/oragono/irc/history"
|
||||||
"github.com/oragono/oragono/irc/sno"
|
"github.com/oragono/oragono/irc/sno"
|
||||||
|
"github.com/oragono/oragono/irc/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@ -44,7 +46,7 @@ func performNickChange(server *Server, client *Client, target *Client, session *
|
|||||||
|
|
||||||
hadNick := target.HasNick()
|
hadNick := target.HasNick()
|
||||||
origNickMask := target.NickMaskString()
|
origNickMask := target.NickMaskString()
|
||||||
whowas := target.WhoWas()
|
details := target.Details()
|
||||||
err = client.server.clients.SetNick(target, session, nickname)
|
err = client.server.clients.SetNick(target, session, nickname)
|
||||||
if err == errNicknameInUse {
|
if err == errNicknameInUse {
|
||||||
rb.Add(nil, server.name, ERR_NICKNAMEINUSE, currentNick, nickname, client.t("Nickname is already in use"))
|
rb.Add(nil, server.name, ERR_NICKNAMEINUSE, currentNick, nickname, client.t("Nickname is already in use"))
|
||||||
@ -57,18 +59,31 @@ func performNickChange(server *Server, client *Client, target *Client, session *
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
message := utils.MakeSplitMessage("", true)
|
||||||
|
histItem := history.Item{
|
||||||
|
Type: history.Nick,
|
||||||
|
Nick: origNickMask,
|
||||||
|
AccountName: details.accountName,
|
||||||
|
Message: message,
|
||||||
|
}
|
||||||
|
histItem.Params[0] = nickname
|
||||||
|
|
||||||
client.server.logger.Debug("nick", fmt.Sprintf("%s changed nickname to %s [%s]", origNickMask, nickname, cfnick))
|
client.server.logger.Debug("nick", fmt.Sprintf("%s changed nickname to %s [%s]", origNickMask, nickname, cfnick))
|
||||||
if hadNick {
|
if hadNick {
|
||||||
target.server.snomasks.Send(sno.LocalNicks, fmt.Sprintf(ircfmt.Unescape("$%s$r changed nickname to %s"), whowas.nick, nickname))
|
target.server.snomasks.Send(sno.LocalNicks, fmt.Sprintf(ircfmt.Unescape("$%s$r changed nickname to %s"), details.nick, nickname))
|
||||||
target.server.whoWas.Append(whowas)
|
target.server.whoWas.Append(details.WhoWas)
|
||||||
rb.Add(nil, origNickMask, "NICK", nickname)
|
rb.AddFromClient(message.Time, message.Msgid, origNickMask, details.accountName, nil, "NICK", nickname)
|
||||||
for session := range target.Friends() {
|
for session := range target.Friends() {
|
||||||
if session != rb.session {
|
if session != rb.session {
|
||||||
session.Send(nil, origNickMask, "NICK", nickname)
|
session.sendFromClientInternal(false, message.Time, message.Msgid, origNickMask, details.accountName, nil, "NICK", nickname)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for _, channel := range client.Channels() {
|
||||||
|
channel.history.Add(histItem)
|
||||||
|
}
|
||||||
|
|
||||||
target.nickTimer.Touch(rb)
|
target.nickTimer.Touch(rb)
|
||||||
|
|
||||||
if target.Registered() {
|
if target.Registered() {
|
||||||
|
@ -7,6 +7,8 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/goshuirc/irc-go/ircfmt"
|
"github.com/goshuirc/irc-go/ircfmt"
|
||||||
|
|
||||||
|
"github.com/oragono/oragono/irc/modes"
|
||||||
)
|
)
|
||||||
|
|
||||||
// "enabled" callbacks for specific nickserv commands
|
// "enabled" callbacks for specific nickserv commands
|
||||||
@ -26,6 +28,10 @@ func nsEnforceEnabled(config *Config) bool {
|
|||||||
return servCmdRequiresNickRes(config) && config.Accounts.NickReservation.AllowCustomEnforcement
|
return servCmdRequiresNickRes(config) && config.Accounts.NickReservation.AllowCustomEnforcement
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func servCmdRequiresBouncerEnabled(config *Config) bool {
|
||||||
|
return config.Accounts.Bouncer.Enabled
|
||||||
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
// ZNC's nickserv module will not detect this unless it is:
|
// ZNC's nickserv module will not detect this unless it is:
|
||||||
// 1. sent with prefix `nickserv`
|
// 1. sent with prefix `nickserv`
|
||||||
@ -142,6 +148,16 @@ an administrator can set use this command to set up user accounts.`,
|
|||||||
capabs: []string{"accreg"},
|
capabs: []string{"accreg"},
|
||||||
minParams: 2,
|
minParams: 2,
|
||||||
},
|
},
|
||||||
|
"sessions": {
|
||||||
|
handler: nsSessionsHandler,
|
||||||
|
help: `Syntax: $bSESSIONS [nickname]$b
|
||||||
|
|
||||||
|
SESSIONS lists information about the sessions currently attached, via
|
||||||
|
the server's bouncer functionality, to your nickname. An administrator
|
||||||
|
can use this command to list another user's sessions.`,
|
||||||
|
helpShort: `$bSESSIONS$b lists the sessions attached to a nickname.`,
|
||||||
|
enabled: servCmdRequiresBouncerEnabled,
|
||||||
|
},
|
||||||
"unregister": {
|
"unregister": {
|
||||||
handler: nsUnregisterHandler,
|
handler: nsUnregisterHandler,
|
||||||
help: `Syntax: $bUNREGISTER <username> [code]$b
|
help: `Syntax: $bUNREGISTER <username> [code]$b
|
||||||
@ -569,3 +585,34 @@ func nsEnforceHandler(server *Server, client *Client, command string, params []s
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func nsSessionsHandler(server *Server, client *Client, command string, params []string, rb *ResponseBuffer) {
|
||||||
|
target := client
|
||||||
|
|
||||||
|
if 0 < len(params) {
|
||||||
|
// same permissions check as RPL_WHOISACTUALLY for now:
|
||||||
|
if !client.HasMode(modes.Operator) {
|
||||||
|
nsNotice(rb, client.t("Command restricted"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
target = server.clients.Get(params[0])
|
||||||
|
if target == nil {
|
||||||
|
nsNotice(rb, client.t("No such nick"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sessionData, currentIndex := target.AllSessionData(rb.session)
|
||||||
|
nsNotice(rb, fmt.Sprintf(client.t("Nickname %s has %d attached session(s)"), target.Nick(), len(sessionData)))
|
||||||
|
for i, session := range sessionData {
|
||||||
|
if currentIndex == i {
|
||||||
|
nsNotice(rb, fmt.Sprintf(client.t("Session %d (currently attached session):"), i+1))
|
||||||
|
} else {
|
||||||
|
nsNotice(rb, fmt.Sprintf(client.t("Session %d:"), i+1))
|
||||||
|
}
|
||||||
|
nsNotice(rb, fmt.Sprintf(client.t("IP address: %s"), session.ip.String()))
|
||||||
|
nsNotice(rb, fmt.Sprintf(client.t("Hostname: %s"), session.hostname))
|
||||||
|
nsNotice(rb, fmt.Sprintf(client.t("Created at: %s"), session.ctime.Format(IRCv3TimestampFormat)))
|
||||||
|
nsNotice(rb, fmt.Sprintf(client.t("Last active: %s"), session.atime.Format(IRCv3TimestampFormat)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -23,8 +23,19 @@ const (
|
|||||||
// 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 // label if this is a labeled response batch
|
||||||
batchID string
|
batchID string // ID of the labeled response batch, if one has been initiated
|
||||||
|
batchType string // type of the labeled response batch (possibly `history` or `chathistory`)
|
||||||
|
|
||||||
|
// stack of batch IDs of nested batches, which are handled separately
|
||||||
|
// from the underlying labeled-response batch. starting a new nested batch
|
||||||
|
// unconditionally enqueues its batch start message; subsequent messages
|
||||||
|
// are tagged with the nested batch ID, until nested batch end.
|
||||||
|
// (the nested batch start itself may have no batch tag, or the batch tag of the
|
||||||
|
// underlying labeled-response batch, or the batch tag of the next outermost
|
||||||
|
// nested batch.)
|
||||||
|
nestedBatches []string
|
||||||
|
|
||||||
messages []ircmsg.IrcMessage
|
messages []ircmsg.IrcMessage
|
||||||
finalized bool
|
finalized bool
|
||||||
target *Client
|
target *Client
|
||||||
@ -40,8 +51,9 @@ func GetLabel(msg ircmsg.IrcMessage) string {
|
|||||||
// NewResponseBuffer returns a new ResponseBuffer.
|
// NewResponseBuffer returns a new ResponseBuffer.
|
||||||
func NewResponseBuffer(session *Session) *ResponseBuffer {
|
func NewResponseBuffer(session *Session) *ResponseBuffer {
|
||||||
return &ResponseBuffer{
|
return &ResponseBuffer{
|
||||||
session: session,
|
session: session,
|
||||||
target: session.client,
|
target: session.client,
|
||||||
|
batchType: defaultBatchType,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -54,6 +66,9 @@ func (rb *ResponseBuffer) AddMessage(msg ircmsg.IrcMessage) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if 0 < len(rb.nestedBatches) {
|
||||||
|
msg.SetTag("batch", rb.nestedBatches[len(rb.nestedBatches)-1])
|
||||||
|
}
|
||||||
rb.messages = append(rb.messages, msg)
|
rb.messages = append(rb.messages, msg)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -63,9 +78,11 @@ func (rb *ResponseBuffer) Add(tags map[string]string, prefix string, command str
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 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, fromNickMask string, fromAccount string, tags map[string]string, command string, params ...string) {
|
func (rb *ResponseBuffer) AddFromClient(time time.Time, msgid string, fromNickMask string, fromAccount string, tags map[string]string, command string, params ...string) {
|
||||||
msg := ircmsg.MakeMessage(nil, fromNickMask, command, params...)
|
msg := ircmsg.MakeMessage(nil, fromNickMask, command, params...)
|
||||||
msg.UpdateTags(tags)
|
if rb.session.capabilities.Has(caps.MessageTags) {
|
||||||
|
msg.UpdateTags(tags)
|
||||||
|
}
|
||||||
|
|
||||||
// attach account-tag
|
// attach account-tag
|
||||||
if rb.session.capabilities.Has(caps.AccountTag) && fromAccount != "*" {
|
if rb.session.capabilities.Has(caps.AccountTag) && fromAccount != "*" {
|
||||||
@ -75,6 +92,10 @@ func (rb *ResponseBuffer) AddFromClient(msgid string, fromNickMask string, fromA
|
|||||||
if len(msgid) > 0 && rb.session.capabilities.Has(caps.MessageTags) {
|
if len(msgid) > 0 && rb.session.capabilities.Has(caps.MessageTags) {
|
||||||
msg.SetTag("draft/msgid", msgid)
|
msg.SetTag("draft/msgid", msgid)
|
||||||
}
|
}
|
||||||
|
// attach server-time
|
||||||
|
if rb.session.capabilities.Has(caps.ServerTime) && !msg.HasTag("time") {
|
||||||
|
msg.SetTag("time", time.UTC().Format(IRCv3TimestampFormat))
|
||||||
|
}
|
||||||
|
|
||||||
rb.AddMessage(msg)
|
rb.AddMessage(msg)
|
||||||
}
|
}
|
||||||
@ -82,33 +103,31 @@ func (rb *ResponseBuffer) AddFromClient(msgid string, fromNickMask string, fromA
|
|||||||
// 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(fromNickMask string, fromAccount string, tags map[string]string, command string, target string, message utils.SplitMessage) {
|
func (rb *ResponseBuffer) AddSplitMessageFromClient(fromNickMask string, fromAccount string, tags map[string]string, command string, target string, message utils.SplitMessage) {
|
||||||
if rb.session.capabilities.Has(caps.MaxLine) || message.Wrapped == nil {
|
if rb.session.capabilities.Has(caps.MaxLine) || message.Wrapped == nil {
|
||||||
rb.AddFromClient(message.Msgid, fromNickMask, fromAccount, tags, command, target, message.Message)
|
rb.AddFromClient(message.Time, message.Msgid, fromNickMask, fromAccount, tags, command, target, message.Message)
|
||||||
} else {
|
} else {
|
||||||
for _, messagePair := range message.Wrapped {
|
for _, messagePair := range message.Wrapped {
|
||||||
rb.AddFromClient(messagePair.Msgid, fromNickMask, fromAccount, tags, command, target, messagePair.Message)
|
rb.AddFromClient(message.Time, messagePair.Msgid, fromNickMask, fromAccount, tags, command, target, messagePair.Message)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// InitializeBatch forcibly starts a batch of batch `batchType`.
|
// ForceBatchStart forcibly starts a batch of batch `batchType`.
|
||||||
// Normally, Send/Flush will decide automatically whether to start a batch
|
// Normally, Send/Flush will decide automatically whether to start a batch
|
||||||
// of type draft/labeled-response. This allows changing the batch type
|
// of type draft/labeled-response. This allows changing the batch type
|
||||||
// and forcing the creation of a possibly empty batch.
|
// and forcing the creation of a possibly empty batch.
|
||||||
func (rb *ResponseBuffer) InitializeBatch(batchType string, blocking bool) {
|
func (rb *ResponseBuffer) ForceBatchStart(batchType string, blocking bool) {
|
||||||
rb.sendBatchStart(batchType, blocking)
|
rb.batchType = batchType
|
||||||
|
rb.sendBatchStart(blocking)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (rb *ResponseBuffer) sendBatchStart(batchType string, blocking bool) {
|
func (rb *ResponseBuffer) sendBatchStart(blocking bool) {
|
||||||
if rb.batchID != "" {
|
if rb.batchID != "" {
|
||||||
// batch already initialized
|
// batch already initialized
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// formerly this combined time.Now.UnixNano() in base 36 with an incrementing counter,
|
|
||||||
// also in base 36. but let's just use a uuidv4-alike (26 base32 characters):
|
|
||||||
rb.batchID = utils.GenerateSecretToken()
|
rb.batchID = utils.GenerateSecretToken()
|
||||||
|
message := ircmsg.MakeMessage(nil, rb.target.server.name, "BATCH", "+"+rb.batchID, rb.batchType)
|
||||||
message := ircmsg.MakeMessage(nil, rb.target.server.name, "BATCH", "+"+rb.batchID, batchType)
|
|
||||||
if rb.Label != "" {
|
if rb.Label != "" {
|
||||||
message.SetTag(caps.LabelTagName, rb.Label)
|
message.SetTag(caps.LabelTagName, rb.Label)
|
||||||
}
|
}
|
||||||
@ -125,6 +144,50 @@ func (rb *ResponseBuffer) sendBatchEnd(blocking bool) {
|
|||||||
rb.session.SendRawMessage(message, blocking)
|
rb.session.SendRawMessage(message, blocking)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Starts a nested batch (see the ResponseBuffer struct definition for a description of
|
||||||
|
// how this works)
|
||||||
|
func (rb *ResponseBuffer) StartNestedBatch(batchType string, params ...string) (batchID string) {
|
||||||
|
batchID = utils.GenerateSecretToken()
|
||||||
|
msgParams := make([]string, len(params)+2)
|
||||||
|
msgParams[0] = "+" + batchID
|
||||||
|
msgParams[1] = batchType
|
||||||
|
copy(msgParams[2:], params)
|
||||||
|
rb.AddMessage(ircmsg.MakeMessage(nil, rb.target.server.name, "BATCH", msgParams...))
|
||||||
|
rb.nestedBatches = append(rb.nestedBatches, batchID)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ends a nested batch
|
||||||
|
func (rb *ResponseBuffer) EndNestedBatch(batchID string) {
|
||||||
|
if batchID == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if 0 == len(rb.nestedBatches) || rb.nestedBatches[len(rb.nestedBatches)-1] != batchID {
|
||||||
|
rb.target.server.logger.Error("internal", "inconsistent batch nesting detected")
|
||||||
|
debug.PrintStack()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
rb.nestedBatches = rb.nestedBatches[0 : len(rb.nestedBatches)-1]
|
||||||
|
rb.AddMessage(ircmsg.MakeMessage(nil, rb.target.server.name, "BATCH", "-"+batchID))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convenience to start a nested batch for history lines, at the highest level
|
||||||
|
// supported by the client (`history`, `chathistory`, or no batch, in descending order).
|
||||||
|
func (rb *ResponseBuffer) StartNestedHistoryBatch(params ...string) (batchID string) {
|
||||||
|
var batchType string
|
||||||
|
if rb.session.capabilities.Has(caps.EventPlayback) {
|
||||||
|
batchType = "history"
|
||||||
|
} else if rb.session.capabilities.Has(caps.Batch) {
|
||||||
|
batchType = "chathistory"
|
||||||
|
}
|
||||||
|
if batchType != "" {
|
||||||
|
batchID = rb.StartNestedBatch(batchType, params...)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// Send sends all messages in the buffer to the client.
|
// Send sends all messages in the buffer to the client.
|
||||||
// Afterwards, the buffer is in an undefined state and MUST NOT be used further.
|
// 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.
|
// If `blocking` is true you MUST be sending to the client from its own goroutine.
|
||||||
@ -158,7 +221,7 @@ func (rb *ResponseBuffer) flushInternal(final bool, blocking bool) error {
|
|||||||
if useLabel && !useBatch && len(rb.messages) == 1 && rb.batchID == "" {
|
if useLabel && !useBatch && len(rb.messages) == 1 && rb.batchID == "" {
|
||||||
rb.messages[0].SetTag(caps.LabelTagName, rb.Label)
|
rb.messages[0].SetTag(caps.LabelTagName, rb.Label)
|
||||||
} else if useBatch {
|
} else if useBatch {
|
||||||
rb.sendBatchStart(defaultBatchType, blocking)
|
rb.sendBatchStart(blocking)
|
||||||
}
|
}
|
||||||
|
|
||||||
// send each message out
|
// send each message out
|
||||||
@ -168,8 +231,9 @@ func (rb *ResponseBuffer) flushInternal(final bool, blocking bool) error {
|
|||||||
message.SetTag("time", time.Now().UTC().Format(IRCv3TimestampFormat))
|
message.SetTag("time", time.Now().UTC().Format(IRCv3TimestampFormat))
|
||||||
}
|
}
|
||||||
|
|
||||||
// attach batch ID
|
// attach batch ID, unless this message was part of a nested batch and is
|
||||||
if rb.batchID != "" {
|
// already tagged
|
||||||
|
if rb.batchID != "" && !message.HasTag("batch") {
|
||||||
message.SetTag("batch", rb.batchID)
|
message.SetTag("batch", rb.batchID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -395,32 +395,43 @@ func (server *Server) tryRegister(c *Client, session *Session) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
reattached := session.client != c
|
if session.client != c {
|
||||||
|
// reattached, bail out.
|
||||||
if !reattached {
|
// we'll play the reg burst later, on the new goroutine associated with
|
||||||
// registration has succeeded:
|
// (thisSession, otherClient). This is to avoid having to transfer state
|
||||||
c.SetRegistered()
|
// like nickname, hostname, etc. to show the correct values in the reg burst.
|
||||||
|
return
|
||||||
// count new user in statistics
|
|
||||||
server.stats.ChangeTotal(1)
|
|
||||||
|
|
||||||
if !resumed {
|
|
||||||
server.monitorManager.AlertAbout(c, true)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// registration has succeeded:
|
||||||
|
c.SetRegistered()
|
||||||
|
// count new user in statistics
|
||||||
|
server.stats.ChangeTotal(1)
|
||||||
|
|
||||||
|
server.playRegistrationBurst(session)
|
||||||
|
|
||||||
|
if resumed {
|
||||||
|
c.tryResumeChannels()
|
||||||
|
} else {
|
||||||
|
server.monitorManager.AlertAbout(c, true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (server *Server) playRegistrationBurst(session *Session) {
|
||||||
|
c := session.client
|
||||||
// continue registration
|
// continue registration
|
||||||
server.logger.Info("localconnect", fmt.Sprintf("Client connected [%s] [u:%s] [r:%s]", c.nick, c.username, c.realname))
|
d := c.Details()
|
||||||
server.snomasks.Send(sno.LocalConnects, fmt.Sprintf("Client connected [%s] [u:%s] [h:%s] [ip:%s] [r:%s]", c.nick, c.username, c.rawHostname, c.IPString(), c.realname))
|
server.logger.Info("localconnect", fmt.Sprintf("Client connected [%s] [u:%s] [r:%s]", d.nick, d.username, d.realname))
|
||||||
|
server.snomasks.Send(sno.LocalConnects, fmt.Sprintf("Client connected [%s] [u:%s] [h:%s] [ip:%s] [r:%s]", d.nick, d.username, c.RawHostname(), c.IPString(), d.realname))
|
||||||
|
|
||||||
// send welcome text
|
// send welcome text
|
||||||
//NOTE(dan): we specifically use the NICK here instead of the nickmask
|
//NOTE(dan): we specifically use the NICK here instead of the nickmask
|
||||||
// see http://modern.ircdocs.horse/#rplwelcome-001 for details on why we avoid using the nickmask
|
// see http://modern.ircdocs.horse/#rplwelcome-001 for details on why we avoid using the nickmask
|
||||||
c.Send(nil, server.name, RPL_WELCOME, c.nick, fmt.Sprintf(c.t("Welcome to the Internet Relay Network %s"), c.nick))
|
session.Send(nil, server.name, RPL_WELCOME, d.nick, fmt.Sprintf(c.t("Welcome to the Internet Relay Network %s"), d.nick))
|
||||||
c.Send(nil, server.name, RPL_YOURHOST, c.nick, fmt.Sprintf(c.t("Your host is %[1]s, running version %[2]s"), server.name, Ver))
|
session.Send(nil, server.name, RPL_YOURHOST, d.nick, fmt.Sprintf(c.t("Your host is %[1]s, running version %[2]s"), server.name, Ver))
|
||||||
c.Send(nil, server.name, RPL_CREATED, c.nick, fmt.Sprintf(c.t("This server was created %s"), server.ctime.Format(time.RFC1123)))
|
session.Send(nil, server.name, RPL_CREATED, d.nick, fmt.Sprintf(c.t("This server was created %s"), server.ctime.Format(time.RFC1123)))
|
||||||
//TODO(dan): Look at adding last optional [<channel modes with a parameter>] parameter
|
//TODO(dan): Look at adding last optional [<channel modes with a parameter>] parameter
|
||||||
c.Send(nil, server.name, RPL_MYINFO, c.nick, server.name, Ver, supportedUserModesString, supportedChannelModesString)
|
session.Send(nil, server.name, RPL_MYINFO, d.nick, server.name, Ver, supportedUserModesString, supportedChannelModesString)
|
||||||
|
|
||||||
rb := NewResponseBuffer(session)
|
rb := NewResponseBuffer(session)
|
||||||
c.RplISupport(rb)
|
c.RplISupport(rb)
|
||||||
@ -429,14 +440,10 @@ func (server *Server) tryRegister(c *Client, session *Session) {
|
|||||||
|
|
||||||
modestring := c.ModeString()
|
modestring := c.ModeString()
|
||||||
if modestring != "+" {
|
if modestring != "+" {
|
||||||
c.Send(nil, c.nickMaskString, RPL_UMODEIS, c.nick, c.ModeString())
|
session.Send(nil, d.nickMask, RPL_UMODEIS, d.nick, modestring)
|
||||||
}
|
}
|
||||||
if server.logger.IsLoggingRawIO() {
|
if server.logger.IsLoggingRawIO() {
|
||||||
c.Notice(c.t("This server is in debug mode and is logging all user I/O. If you do not wish for everything you send to be readable by the server owner(s), please disconnect."))
|
session.Send(nil, c.server.name, "NOTICE", d.nick, c.t("This server is in debug mode and is logging all user I/O. If you do not wish for everything you send to be readable by the server owner(s), please disconnect."))
|
||||||
}
|
|
||||||
|
|
||||||
if resumed {
|
|
||||||
c.tryResumeChannels()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -4,6 +4,7 @@
|
|||||||
package utils
|
package utils
|
||||||
|
|
||||||
import "bytes"
|
import "bytes"
|
||||||
|
import "time"
|
||||||
|
|
||||||
// WordWrap wraps the given text into a series of lines that don't exceed lineWidth characters.
|
// WordWrap wraps the given text into a series of lines that don't exceed lineWidth characters.
|
||||||
func WordWrap(text string, lineWidth int) []string {
|
func WordWrap(text string, lineWidth int) []string {
|
||||||
@ -59,6 +60,7 @@ type MessagePair struct {
|
|||||||
type SplitMessage struct {
|
type SplitMessage struct {
|
||||||
MessagePair
|
MessagePair
|
||||||
Wrapped []MessagePair // if this is nil, `Message` didn't need wrapping and can be sent to anyone
|
Wrapped []MessagePair // if this is nil, `Message` didn't need wrapping and can be sent to anyone
|
||||||
|
Time time.Time
|
||||||
}
|
}
|
||||||
|
|
||||||
const defaultLineWidth = 400
|
const defaultLineWidth = 400
|
||||||
@ -66,6 +68,7 @@ const defaultLineWidth = 400
|
|||||||
func MakeSplitMessage(original string, origIs512 bool) (result SplitMessage) {
|
func MakeSplitMessage(original string, origIs512 bool) (result SplitMessage) {
|
||||||
result.Message = original
|
result.Message = original
|
||||||
result.Msgid = GenerateSecretToken()
|
result.Msgid = GenerateSecretToken()
|
||||||
|
result.Time = time.Now().UTC()
|
||||||
|
|
||||||
if !origIs512 && defaultLineWidth < len(original) {
|
if !origIs512 && defaultLineWidth < len(original) {
|
||||||
wrapped := WordWrap(original, defaultLineWidth)
|
wrapped := WordWrap(original, defaultLineWidth)
|
||||||
|
Loading…
Reference in New Issue
Block a user