Initial implementation of labeled-responses for WHOIS

This commit is contained in:
Daniel Oaks 2017-10-08 11:05:05 +10:00
parent 095e71b2fe
commit d4a8984e63
5 changed files with 264 additions and 50 deletions

78
irc/batch.go Normal file
View File

@ -0,0 +1,78 @@
// 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(), 10) + strconv.FormatUint(bm.idCounter, 10)
}
// 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)
}
}

View File

@ -14,7 +14,7 @@ import (
var (
// SupportedCapabilities are the caps we advertise.
// MaxLine, SASL and STS are set during server startup.
SupportedCapabilities = caps.NewSet(caps.AccountTag, caps.AccountNotify, caps.AwayNotify, caps.CapNotify, caps.ChgHost, caps.EchoMessage, caps.ExtendedJoin, caps.InviteNotify, caps.MessageTags, caps.MultiPrefix, caps.Rename, caps.ServerTime, caps.UserhostInNames)
SupportedCapabilities = caps.NewSet(caps.AccountTag, caps.AccountNotify, caps.AwayNotify, caps.Batch, caps.CapNotify, caps.ChgHost, caps.EchoMessage, caps.ExtendedJoin, caps.InviteNotify, caps.LabeledResponse, caps.MessageTags, caps.MultiPrefix, caps.Rename, caps.ServerTime, caps.UserhostInNames)
// CapValues are the actual values we advertise to v3.2 clients.
// actual values are set during server startup.

View File

@ -621,7 +621,7 @@ func (client *Client) SendFromClient(msgid string, from *Client, tags *map[strin
var (
// these are all the output commands that MUST have their last param be a trailing.
// this is needed because silly clients like to treat trailing as separate from the
// this is needed because dumb clients like to treat trailing params separately from the
// other params in messages.
commandsThatMustUseTrailing = map[string]bool{
"PRIVMSG": true,
@ -632,6 +632,47 @@ var (
}
)
// SendRawMessage sends a raw message to the client.
func (client *Client) SendRawMessage(message ircmsg.IrcMessage) error {
// use dumb hack to force the last param to be a trailing param if required
var usedTrailingHack bool
if commandsThatMustUseTrailing[strings.ToUpper(message.Command)] && len(message.Params) > 0 {
lastParam := message.Params[len(message.Params)-1]
// to force trailing, we ensure the final param contains a space
if !strings.Contains(lastParam, " ") {
message.Params[len(message.Params)-1] = lastParam + " "
usedTrailingHack = true
}
}
// assemble message
maxlenTags, maxlenRest := client.maxlens()
line, err := message.LineMaxLen(maxlenTags, maxlenRest)
if err != nil {
// try not to fail quietly - especially useful when running tests, as a note to dig deeper
// log.Println("Error assembling message:")
// spew.Dump(message)
// debug.PrintStack()
message = ircmsg.MakeMessage(nil, client.server.name, ERR_UNKNOWNERROR, "*", "Error assembling message for sending")
line, _ := message.Line()
// if we used the trailing hack, we need to strip the final space we appended earlier on
if usedTrailingHack {
line = line[:len(line)-3] + "\r\n"
}
client.socket.Write(line)
return err
}
client.server.logger.Debug("useroutput", client.nick, " ->", strings.TrimRight(line, "\r\n"))
client.socket.Write(line)
return nil
}
// Send sends an IRC line to the client.
func (client *Client) Send(tags *map[string]ircmsg.TagValue, prefix string, command string, params ...string) error {
// attach server-time
@ -644,41 +685,9 @@ func (client *Client) Send(tags *map[string]ircmsg.TagValue, prefix string, comm
}
}
// force trailing, if message requires it
var usedTrailingHack bool
if commandsThatMustUseTrailing[strings.ToUpper(command)] && len(params) > 0 {
lastParam := params[len(params)-1]
// to force trailing, we ensure the final param contains a space
if !strings.Contains(lastParam, " ") {
params[len(params)-1] = lastParam + " "
usedTrailingHack = true
}
}
// send out the message
message := ircmsg.MakeMessage(tags, prefix, command, params...)
maxlenTags, maxlenRest := client.maxlens()
line, err := message.LineMaxLen(maxlenTags, maxlenRest)
if err != nil {
// try not to fail quietly - especially useful when running tests, as a note to dig deeper
// log.Println("Error assembling message:")
// spew.Dump(message)
// debug.PrintStack()
message = ircmsg.MakeMessage(nil, client.server.name, ERR_UNKNOWNERROR, "*", "Error assembling message for sending")
line, _ := message.Line()
client.socket.Write(line)
return err
}
// is we used the trailing hack, we need to strip the final space we appended earlier
if usedTrailingHack {
line = line[:len(line)-3] + "\r\n"
}
client.server.logger.Debug("useroutput", client.nick, " ->", strings.TrimRight(line, "\r\n"))
client.socket.Write(line)
client.SendRawMessage(message)
return nil
}

120
irc/responsebuffer.go Normal file
View File

@ -0,0 +1,120 @@
// Copyright (c) 2016-2017 Daniel Oaks <daniel@danieloaks.net>
// released under the MIT license
package irc
import (
"time"
"github.com/goshuirc/irc-go/ircmsg"
"github.com/oragono/oragono/irc/caps"
)
// 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
// 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).
type ResponseBuffer struct {
Label string
target *Client
messages []ircmsg.IrcMessage
}
// GetLabel returns the label from the given message.
func GetLabel(msg ircmsg.IrcMessage) string {
return msg.Tags["label"].Value
}
// NewResponseBuffer returns a new ResponseBuffer.
func NewResponseBuffer(target *Client) *ResponseBuffer {
return &ResponseBuffer{
target: target,
}
}
// Add adds a standard new message to our queue.
func (rb *ResponseBuffer) Add(tags *map[string]ircmsg.TagValue, prefix string, command string, params ...string) {
message := ircmsg.MakeMessage(tags, prefix, command, params...)
rb.messages = append(rb.messages, message)
}
// 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) {
// attach account-tag
if rb.target.capabilities.Has(caps.AccountTag) && from.account != &NoAccount {
if tags == nil {
tags = ircmsg.MakeTags("account", from.account.Name)
} else {
(*tags)["account"] = ircmsg.MakeTagValue(from.account.Name)
}
}
// attach message-id
if len(msgid) > 0 && rb.target.capabilities.Has(caps.MessageTags) {
if tags == nil {
tags = ircmsg.MakeTags("draft/msgid", msgid)
} else {
(*tags)["draft/msgid"] = ircmsg.MakeTagValue(msgid)
}
}
rb.Add(tags, from.nickMaskString, command, params...)
}
// 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 SplitMessage) {
if rb.target.capabilities.Has(caps.MaxLine) {
rb.AddFromClient(msgid, from, tags, command, target, message.ForMaxLine)
} else {
for _, str := range message.For512 {
rb.AddFromClient(msgid, from, tags, command, target, str)
}
}
}
// Send sends the message to our target client.
func (rb *ResponseBuffer) Send() error {
// make batch and all if required
var batch *Batch
useLabel := rb.target.capabilities.Has(caps.LabeledResponse) && rb.Label != ""
if useLabel && 1 < len(rb.messages) && rb.target.capabilities.Has(caps.Batch) {
batch = rb.target.server.batches.New("draft/labeled-response")
}
// if label but no batch, add label to first message
if useLabel && batch == nil {
message := rb.messages[0]
message.Tags["label"] = ircmsg.MakeTagValue(rb.Label)
rb.messages[0] = message
}
// start batch if required
if batch != nil {
batch.Start(rb.target, ircmsg.MakeTags("label", rb.Label))
}
// send each message out
for _, message := range rb.messages {
// attach server-time if needed
if rb.target.capabilities.Has(caps.ServerTime) {
t := time.Now().UTC().Format("2006-01-02T15:04:05.999Z")
message.Tags["time"] = ircmsg.MakeTagValue(t)
}
// attach batch ID
if batch != nil {
message.Tags["batch"] = ircmsg.MakeTagValue(batch.ID)
}
// send message out
rb.target.SendRawMessage(message)
}
// end batch if required
if batch != nil {
batch.End(rb.target)
}
return nil
}

View File

@ -78,6 +78,7 @@ type Server struct {
accountAuthenticationEnabled bool
accountRegistration *AccountRegistration
accounts map[string]*ClientAccount
batches *BatchManager
channelRegistrationEnabled bool
channels ChannelNameMap
channelJoinPartMutex sync.Mutex // used when joining/parting channels to prevent stomping over each others' access and all
@ -146,6 +147,7 @@ func NewServer(config *Config, logger *logger.Manager) (*Server, error) {
// initialize data structures
server := &Server{
accounts: make(map[string]*ClientAccount),
batches: NewBatchManager(),
channels: *NewChannelNameMap(),
clients: NewClientLookupSet(),
commands: make(chan Command),
@ -988,8 +990,12 @@ func whoisHandler(server *Server, client *Client, msg ircmsg.IrcMessage) bool {
masksString = msg.Params[0]
}
rb := NewResponseBuffer(client)
rb.Label = GetLabel(msg)
if len(strings.TrimSpace(masksString)) < 1 {
client.Send(nil, server.name, ERR_UNKNOWNERROR, client.nick, msg.Command, "No masks given")
rb.Add(nil, server.name, ERR_UNKNOWNERROR, client.nick, msg.Command, "No masks given")
rb.Send()
return false
}
@ -998,16 +1004,16 @@ func whoisHandler(server *Server, client *Client, msg ircmsg.IrcMessage) bool {
for _, mask := range masks {
casefoldedMask, err := Casefold(mask)
if err != nil {
client.Send(nil, client.server.name, ERR_NOSUCHNICK, client.nick, mask, "No such nick")
rb.Add(nil, client.server.name, ERR_NOSUCHNICK, client.nick, mask, "No such nick")
continue
}
matches := server.clients.FindAll(casefoldedMask)
if len(matches) == 0 {
client.Send(nil, client.server.name, ERR_NOSUCHNICK, client.nick, mask, "No such nick")
rb.Add(nil, client.server.name, ERR_NOSUCHNICK, client.nick, mask, "No such nick")
continue
}
for mclient := range matches {
client.getWhoisOf(mclient)
client.getWhoisOf(mclient, rb)
}
}
} else {
@ -1015,36 +1021,37 @@ func whoisHandler(server *Server, client *Client, msg ircmsg.IrcMessage) bool {
casefoldedMask, err := Casefold(strings.Split(masksString, ",")[0])
mclient := server.clients.Get(casefoldedMask)
if err != nil || mclient == nil {
client.Send(nil, client.server.name, ERR_NOSUCHNICK, client.nick, masksString, "No such nick")
rb.Add(nil, client.server.name, ERR_NOSUCHNICK, client.nick, masksString, "No such nick")
// fall through, ENDOFWHOIS is always sent
} else {
client.getWhoisOf(mclient)
client.getWhoisOf(mclient, rb)
}
}
client.Send(nil, server.name, RPL_ENDOFWHOIS, client.nick, masksString, "End of /WHOIS list")
rb.Add(nil, server.name, RPL_ENDOFWHOIS, client.nick, masksString, "End of /WHOIS list")
rb.Send()
return false
}
func (client *Client) getWhoisOf(target *Client) {
client.Send(nil, client.server.name, RPL_WHOISUSER, client.nick, target.nick, target.username, target.hostname, "*", target.realname)
func (client *Client) getWhoisOf(target *Client, rb *ResponseBuffer) {
rb.Add(nil, client.server.name, RPL_WHOISUSER, client.nick, target.nick, target.username, target.hostname, "*", target.realname)
whoischannels := client.WhoisChannelsNames(target)
if whoischannels != nil {
client.Send(nil, client.server.name, RPL_WHOISCHANNELS, client.nick, target.nick, strings.Join(whoischannels, " "))
rb.Add(nil, client.server.name, RPL_WHOISCHANNELS, client.nick, target.nick, strings.Join(whoischannels, " "))
}
if target.class != nil {
client.Send(nil, client.server.name, RPL_WHOISOPERATOR, client.nick, target.nick, target.whoisLine)
rb.Add(nil, client.server.name, RPL_WHOISOPERATOR, client.nick, target.nick, target.whoisLine)
}
if client.flags[Operator] || client == target {
client.Send(nil, client.server.name, RPL_WHOISACTUALLY, client.nick, target.nick, fmt.Sprintf("%s@%s", target.username, utils.LookupHostname(target.IPString())), target.IPString(), "Actual user@host, Actual IP")
rb.Add(nil, client.server.name, RPL_WHOISACTUALLY, client.nick, target.nick, fmt.Sprintf("%s@%s", target.username, utils.LookupHostname(target.IPString())), target.IPString(), "Actual user@host, Actual IP")
}
if target.flags[TLS] {
client.Send(nil, client.server.name, RPL_WHOISSECURE, client.nick, target.nick, "is using a secure connection")
rb.Add(nil, client.server.name, RPL_WHOISSECURE, client.nick, target.nick, "is using a secure connection")
}
if target.certfp != "" && (client.flags[Operator] || client == target) {
client.Send(nil, client.server.name, RPL_WHOISCERTFP, client.nick, target.nick, fmt.Sprintf("has client certificate fingerprint %s", target.certfp))
rb.Add(nil, client.server.name, RPL_WHOISCERTFP, client.nick, target.nick, fmt.Sprintf("has client certificate fingerprint %s", target.certfp))
}
client.Send(nil, client.server.name, RPL_WHOISIDLE, client.nick, target.nick, strconv.FormatUint(target.IdleSeconds(), 10), strconv.FormatInt(target.SignonTime(), 10), "seconds idle, signon time")
rb.Add(nil, client.server.name, RPL_WHOISIDLE, client.nick, target.nick, strconv.FormatUint(target.IdleSeconds(), 10), strconv.FormatInt(target.SignonTime(), 10), "seconds idle, signon time")
}
// RplWhoReplyNoMutex returns the WHO reply between one user and another channel/user.