123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296 |
- // Copyright (c) 2016-2017 Daniel Oaks <daniel@danieloaks.net>
- // released under the MIT license
-
- package irc
-
- import (
- "runtime/debug"
- "time"
-
- "github.com/goshuirc/irc-go/ircmsg"
- "github.com/oragono/oragono/irc/caps"
- "github.com/oragono/oragono/irc/utils"
- )
-
- const (
- // https://ircv3.net/specs/extensions/labeled-response.html
- defaultBatchType = "labeled-response"
- )
-
- // 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 // label if this is a labeled response batch
- 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
- finalized bool
- target *Client
- session *Session
- }
-
- // GetLabel returns the label from the given message.
- func GetLabel(msg ircmsg.IrcMessage) string {
- _, value := msg.GetTag(caps.LabelTagName)
- return value
- }
-
- // NewResponseBuffer returns a new ResponseBuffer.
- func NewResponseBuffer(session *Session) *ResponseBuffer {
- return &ResponseBuffer{
- session: session,
- target: session.client,
- batchType: defaultBatchType,
- }
- }
-
- func (rb *ResponseBuffer) AddMessage(msg ircmsg.IrcMessage) {
- if rb.finalized {
- rb.target.server.logger.Error("internal", "message added to finalized ResponseBuffer, undefined behavior")
- debug.PrintStack()
- // TODO(dan): send a NOTICE to the end user with a string representation of the message,
- // for debugging purposes
- return
- }
-
- rb.session.setTimeTag(&msg, time.Time{})
- rb.setNestedBatchTag(&msg)
-
- rb.messages = append(rb.messages, msg)
- }
-
- func (rb *ResponseBuffer) setNestedBatchTag(msg *ircmsg.IrcMessage) {
- if 0 < len(rb.nestedBatches) {
- msg.SetTag("batch", rb.nestedBatches[len(rb.nestedBatches)-1])
- }
- }
-
- // Add adds a standard new message to our queue.
- func (rb *ResponseBuffer) Add(tags map[string]string, prefix string, command string, params ...string) {
- rb.AddMessage(ircmsg.MakeMessage(tags, prefix, command, params...))
- }
-
- // Broadcast adds a standard new message to our queue, then sends an unlabeled copy
- // to all other sessions.
- func (rb *ResponseBuffer) Broadcast(tags map[string]string, prefix string, command string, params ...string) {
- // can't reuse the IrcMessage object because of tag pollution :-\
- rb.Add(tags, prefix, command, params...)
- for _, session := range rb.session.client.Sessions() {
- if session != rb.session {
- session.Send(tags, prefix, command, params...)
- }
- }
- }
-
- // AddFromClient adds a new message from a specific client to our queue.
- 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...)
- if rb.session.capabilities.Has(caps.MessageTags) {
- msg.UpdateTags(tags)
- }
-
- // attach account-tag
- if rb.session.capabilities.Has(caps.AccountTag) && fromAccount != "*" {
- msg.SetTag("account", fromAccount)
- }
- // attach message-id
- if len(msgid) > 0 && rb.session.capabilities.Has(caps.MessageTags) {
- msg.SetTag("msgid", msgid)
- }
- // attach server-time
- rb.session.setTimeTag(&msg, time)
-
- rb.AddMessage(msg)
- }
-
- // 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) {
- if message.Is512() {
- rb.AddFromClient(message.Time, message.Msgid, fromNickMask, fromAccount, tags, command, target, message.Message)
- } else {
- if rb.session.capabilities.Has(caps.Multiline) {
- batch := rb.session.composeMultilineBatch(fromNickMask, fromAccount, tags, command, target, message)
- rb.setNestedBatchTag(&batch[0])
- rb.setNestedBatchTag(&batch[len(batch)-1])
- rb.messages = append(rb.messages, batch...)
- } else {
- for i, messagePair := range message.Split {
- var msgid string
- if i == 0 {
- msgid = message.Msgid
- }
- rb.AddFromClient(message.Time, msgid, fromNickMask, fromAccount, tags, command, target, messagePair.Message)
- }
- }
- }
- }
-
- func (rb *ResponseBuffer) sendBatchStart(blocking bool) {
- if rb.batchID != "" {
- // batch already initialized
- return
- }
-
- rb.batchID = rb.session.generateBatchID()
- message := ircmsg.MakeMessage(nil, rb.target.server.name, "BATCH", "+"+rb.batchID, rb.batchType)
- if rb.Label != "" {
- message.SetTag(caps.LabelTagName, rb.Label)
- }
- rb.session.SendRawMessage(message, blocking)
- }
-
- func (rb *ResponseBuffer) sendBatchEnd(blocking bool) {
- if rb.batchID == "" {
- // we are not sending a batch, skip this
- return
- }
-
- message := ircmsg.MakeMessage(nil, rb.target.server.name, "BATCH", "-"+rb.batchID)
- rb.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 = rb.session.generateBatchID()
- 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.
- // Afterwards, the buffer is in an undefined state and MUST NOT be used further.
- // If `blocking` is true you MUST be sending to the client from its own goroutine.
- func (rb *ResponseBuffer) Send(blocking bool) error {
- return rb.flushInternal(true, blocking)
- }
-
- // Flush sends all messages in the buffer to the client.
- // Afterwards, the buffer can still be used. Client code MUST subsequently call Send()
- // to ensure that the final `BATCH -` message is sent.
- // If `blocking` is true you MUST be sending to the client from its own goroutine.
- func (rb *ResponseBuffer) Flush(blocking bool) error {
- return rb.flushInternal(false, blocking)
- }
-
- // detects whether the response buffer consists of a single, unflushed nested batch,
- // in which case it can be collapsed down to that batch
- func (rb *ResponseBuffer) isCollapsible() (result bool) {
- // rb.batchID indicates that we already flushed some lines
- if rb.batchID != "" || len(rb.messages) < 2 {
- return false
- }
- first, last := rb.messages[0], rb.messages[len(rb.messages)-1]
- if first.Command != "BATCH" || last.Command != "BATCH" {
- return false
- }
- if len(first.Params) == 0 || len(first.Params[0]) == 0 || len(last.Params) == 0 || len(last.Params[0]) == 0 {
- return false
- }
- return first.Params[0][1:] == last.Params[0][1:]
- }
-
- // flushInternal sends the contents of the buffer, either blocking or nonblocking
- // It sends the `BATCH +` message if the client supports it and it hasn't been sent already.
- // If `final` is true, it also sends `BATCH -` (if necessary).
- func (rb *ResponseBuffer) flushInternal(final bool, blocking bool) error {
- if rb.finalized {
- return nil
- }
-
- if rb.session.capabilities.Has(caps.LabeledResponse) && rb.Label != "" {
- if final && rb.isCollapsible() {
- // collapse to the outermost nested batch
- rb.messages[0].SetTag(caps.LabelTagName, rb.Label)
- } else if !final || 2 <= len(rb.messages) {
- // we either have 2+ messages, or we are doing a Flush() and have to assume
- // there will be more messages in the future
- rb.sendBatchStart(blocking)
- } else if len(rb.messages) == 1 && rb.batchID == "" {
- // single labeled message
- rb.messages[0].SetTag(caps.LabelTagName, rb.Label)
- } else if len(rb.messages) == 0 && rb.batchID == "" {
- // ACK message
- message := ircmsg.MakeMessage(nil, rb.session.client.server.name, "ACK")
- message.SetTag(caps.LabelTagName, rb.Label)
- rb.session.setTimeTag(&message, time.Time{})
- rb.session.SendRawMessage(message, blocking)
- }
- }
-
- // send each message out
- for _, message := range rb.messages {
- // attach batch ID, unless this message was part of a nested batch and is
- // already tagged
- if rb.batchID != "" && !message.HasTag("batch") {
- message.SetTag("batch", rb.batchID)
- }
-
- // send message out
- rb.session.SendRawMessage(message, blocking)
- }
-
- // end batch if required
- if final {
- rb.sendBatchEnd(blocking)
- rb.finalized = true
- }
-
- // clear out any existing messages
- rb.messages = rb.messages[:0]
-
- return nil
- }
-
- // Notice sends the client the given notice from the server.
- func (rb *ResponseBuffer) Notice(text string) {
- rb.Add(nil, rb.target.server.name, "NOTICE", rb.target.Nick(), text)
- }
|