mirror of
https://github.com/42wim/matterbridge
synced 2024-11-03 15:40:24 +00:00
497 lines
13 KiB
Go
497 lines
13 KiB
Go
// Copyright (c) Liam Stanley <me@liamstanley.io>. All rights reserved. Use
|
|
// of this source code is governed by the MIT license that can be found in
|
|
// the LICENSE file.
|
|
|
|
package girc
|
|
|
|
import (
|
|
"fmt"
|
|
"log"
|
|
"math/rand"
|
|
"runtime"
|
|
"runtime/debug"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
// RunHandlers manually runs handlers for a given event.
|
|
func (c *Client) RunHandlers(event *Event) {
|
|
if event == nil {
|
|
return
|
|
}
|
|
|
|
// Log the event.
|
|
prefix := "< "
|
|
if event.Echo {
|
|
prefix += "[echo-message] "
|
|
}
|
|
c.debug.Print(prefix + StripRaw(event.String()))
|
|
if c.Config.Out != nil {
|
|
if pretty, ok := event.Pretty(); ok {
|
|
fmt.Fprintln(c.Config.Out, StripRaw(pretty))
|
|
}
|
|
}
|
|
|
|
// Background handlers first. If the event is an echo-message, then only
|
|
// send the echo version to ALL_EVENTS.
|
|
c.Handlers.exec(ALL_EVENTS, true, c, event.Copy())
|
|
if !event.Echo {
|
|
c.Handlers.exec(event.Command, true, c, event.Copy())
|
|
}
|
|
|
|
c.Handlers.exec(ALL_EVENTS, false, c, event.Copy())
|
|
if !event.Echo {
|
|
c.Handlers.exec(event.Command, false, c, event.Copy())
|
|
}
|
|
|
|
// Check if it's a CTCP.
|
|
if ctcp := DecodeCTCP(event.Copy()); ctcp != nil {
|
|
// Execute it.
|
|
c.CTCP.call(c, ctcp)
|
|
}
|
|
}
|
|
|
|
// Handler is lower level implementation of a handler. See
|
|
// Caller.AddHandler()
|
|
type Handler interface {
|
|
Execute(*Client, Event)
|
|
}
|
|
|
|
// HandlerFunc is a type that represents the function necessary to
|
|
// implement Handler.
|
|
type HandlerFunc func(client *Client, event Event)
|
|
|
|
// Execute calls the HandlerFunc with the sender and irc message.
|
|
func (f HandlerFunc) Execute(client *Client, event Event) {
|
|
f(client, event)
|
|
}
|
|
|
|
// Caller manages internal and external (user facing) handlers.
|
|
type Caller struct {
|
|
// mu is the mutex that should be used when accessing handlers.
|
|
mu sync.RWMutex
|
|
|
|
// external/internal keys are of structure:
|
|
// map[COMMAND][CUID]Handler
|
|
// Also of note: "COMMAND" should always be uppercase for normalization.
|
|
|
|
// external is a map of user facing handlers.
|
|
external map[string]map[string]Handler
|
|
// internal is a map of internally used handlers for the client.
|
|
internal map[string]map[string]Handler
|
|
// debug is the clients logger used for debugging.
|
|
debug *log.Logger
|
|
}
|
|
|
|
// newCaller creates and initializes a new handler.
|
|
func newCaller(debugOut *log.Logger) *Caller {
|
|
c := &Caller{
|
|
external: map[string]map[string]Handler{},
|
|
internal: map[string]map[string]Handler{},
|
|
debug: debugOut,
|
|
}
|
|
|
|
return c
|
|
}
|
|
|
|
// Len returns the total amount of user-entered registered handlers.
|
|
func (c *Caller) Len() int {
|
|
var total int
|
|
|
|
c.mu.RLock()
|
|
for command := range c.external {
|
|
total += len(c.external[command])
|
|
}
|
|
c.mu.RUnlock()
|
|
|
|
return total
|
|
}
|
|
|
|
// Count is much like Caller.Len(), however it counts the number of
|
|
// registered handlers for a given command.
|
|
func (c *Caller) Count(cmd string) int {
|
|
var total int
|
|
|
|
cmd = strings.ToUpper(cmd)
|
|
|
|
c.mu.RLock()
|
|
for command := range c.external {
|
|
if command == cmd {
|
|
total += len(c.external[command])
|
|
}
|
|
}
|
|
c.mu.RUnlock()
|
|
|
|
return total
|
|
}
|
|
|
|
func (c *Caller) String() string {
|
|
var total int
|
|
|
|
c.mu.RLock()
|
|
for cmd := range c.internal {
|
|
total += len(c.internal[cmd])
|
|
}
|
|
c.mu.RUnlock()
|
|
|
|
return fmt.Sprintf("<Caller external:%d internal:%d>", c.Len(), total)
|
|
}
|
|
|
|
const letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
|
|
|
// cuid generates a unique UID string for each handler for ease of removal.
|
|
func (c *Caller) cuid(cmd string, n int) (cuid, uid string) {
|
|
b := make([]byte, n)
|
|
|
|
for i := range b {
|
|
b[i] = letterBytes[rand.Int63()%int64(len(letterBytes))]
|
|
}
|
|
|
|
return cmd + ":" + string(b), string(b)
|
|
}
|
|
|
|
// cuidToID allows easy mapping between a generated cuid and the caller
|
|
// external/internal handler maps.
|
|
func (c *Caller) cuidToID(input string) (cmd, uid string) {
|
|
i := strings.IndexByte(input, ':')
|
|
if i < 0 {
|
|
return "", ""
|
|
}
|
|
|
|
return input[:i], input[i+1:]
|
|
}
|
|
|
|
type execStack struct {
|
|
Handler
|
|
cuid string
|
|
}
|
|
|
|
// exec executes all handlers pertaining to specified event. Internal first,
|
|
// then external.
|
|
//
|
|
// Please note that there is no specific order/priority for which the handlers
|
|
// are executed.
|
|
func (c *Caller) exec(command string, bg bool, client *Client, event *Event) {
|
|
// Build a stack of handlers which can be executed concurrently.
|
|
var stack []execStack
|
|
|
|
c.mu.RLock()
|
|
// Get internal handlers first.
|
|
if _, ok := c.internal[command]; ok {
|
|
for cuid := range c.internal[command] {
|
|
if (strings.HasSuffix(cuid, ":bg") && !bg) || (!strings.HasSuffix(cuid, ":bg") && bg) {
|
|
continue
|
|
}
|
|
|
|
stack = append(stack, execStack{c.internal[command][cuid], cuid})
|
|
}
|
|
}
|
|
|
|
// Then external handlers.
|
|
if _, ok := c.external[command]; ok {
|
|
for cuid := range c.external[command] {
|
|
if (strings.HasSuffix(cuid, ":bg") && !bg) || (!strings.HasSuffix(cuid, ":bg") && bg) {
|
|
continue
|
|
}
|
|
|
|
stack = append(stack, execStack{c.external[command][cuid], cuid})
|
|
}
|
|
}
|
|
c.mu.RUnlock()
|
|
|
|
// Run all handlers concurrently across the same event. This should
|
|
// still help prevent mis-ordered events, while speeding up the
|
|
// execution speed.
|
|
var wg sync.WaitGroup
|
|
wg.Add(len(stack))
|
|
for i := 0; i < len(stack); i++ {
|
|
go func(index int) {
|
|
defer wg.Done()
|
|
c.debug.Printf("[%d/%d] exec %s => %s", index+1, len(stack), stack[index].cuid, command)
|
|
start := time.Now()
|
|
|
|
if bg {
|
|
go func() {
|
|
if client.Config.RecoverFunc != nil {
|
|
defer recoverHandlerPanic(client, event, stack[index].cuid, 3)
|
|
}
|
|
|
|
stack[index].Execute(client, *event)
|
|
c.debug.Printf("[%d/%d] done %s == %s", index+1, len(stack), stack[index].cuid, time.Since(start))
|
|
}()
|
|
|
|
return
|
|
}
|
|
|
|
if client.Config.RecoverFunc != nil {
|
|
defer recoverHandlerPanic(client, event, stack[index].cuid, 3)
|
|
}
|
|
|
|
stack[index].Execute(client, *event)
|
|
c.debug.Printf("[%d/%d] done %s == %s", index+1, len(stack), stack[index].cuid, time.Since(start))
|
|
}(i)
|
|
}
|
|
|
|
// Wait for all of the handlers to complete. Not doing this may cause
|
|
// new events from becoming ahead of older handlers.
|
|
wg.Wait()
|
|
}
|
|
|
|
// ClearAll clears all external handlers currently setup within the client.
|
|
// This ignores internal handlers.
|
|
func (c *Caller) ClearAll() {
|
|
c.mu.Lock()
|
|
c.external = map[string]map[string]Handler{}
|
|
c.mu.Unlock()
|
|
|
|
c.debug.Print("cleared all external handlers")
|
|
}
|
|
|
|
// clearInternal clears all internal handlers currently setup within the
|
|
// client.
|
|
func (c *Caller) clearInternal() {
|
|
c.mu.Lock()
|
|
c.internal = map[string]map[string]Handler{}
|
|
c.mu.Unlock()
|
|
|
|
c.debug.Print("cleared all internal handlers")
|
|
}
|
|
|
|
// Clear clears all of the handlers for the given event.
|
|
// This ignores internal handlers.
|
|
func (c *Caller) Clear(cmd string) {
|
|
cmd = strings.ToUpper(cmd)
|
|
|
|
c.mu.Lock()
|
|
if _, ok := c.external[cmd]; ok {
|
|
delete(c.external, cmd)
|
|
}
|
|
c.mu.Unlock()
|
|
|
|
c.debug.Printf("cleared external handlers for %s", cmd)
|
|
}
|
|
|
|
// Remove removes the handler with cuid from the handler stack. success
|
|
// indicates that it existed, and has been removed. If not success, it
|
|
// wasn't a registered handler.
|
|
func (c *Caller) Remove(cuid string) (success bool) {
|
|
c.mu.Lock()
|
|
success = c.remove(cuid)
|
|
c.mu.Unlock()
|
|
|
|
return success
|
|
}
|
|
|
|
// remove is much like Remove, however is NOT concurrency safe. Lock Caller.mu
|
|
// on your own.
|
|
func (c *Caller) remove(cuid string) (success bool) {
|
|
cmd, uid := c.cuidToID(cuid)
|
|
if len(cmd) == 0 || len(uid) == 0 {
|
|
return false
|
|
}
|
|
|
|
// Check if the irc command/event has any handlers on it.
|
|
if _, ok := c.external[cmd]; !ok {
|
|
return false
|
|
}
|
|
|
|
// Check to see if it's actually a registered handler.
|
|
if _, ok := c.external[cmd][uid]; !ok {
|
|
return false
|
|
}
|
|
|
|
delete(c.external[cmd], uid)
|
|
c.debug.Printf("removed handler %s", cuid)
|
|
|
|
// Assume success.
|
|
return true
|
|
}
|
|
|
|
// sregister is much like Caller.register(), except that it safely locks
|
|
// the Caller mutex.
|
|
func (c *Caller) sregister(internal, bg bool, cmd string, handler Handler) (cuid string) {
|
|
c.mu.Lock()
|
|
cuid = c.register(internal, bg, cmd, handler)
|
|
c.mu.Unlock()
|
|
|
|
return cuid
|
|
}
|
|
|
|
// register will register a handler in the internal tracker. Unsafe (you
|
|
// must lock c.mu yourself!)
|
|
func (c *Caller) register(internal, bg bool, cmd string, handler Handler) (cuid string) {
|
|
var uid string
|
|
|
|
cmd = strings.ToUpper(cmd)
|
|
|
|
cuid, uid = c.cuid(cmd, 20)
|
|
if bg {
|
|
uid += ":bg"
|
|
cuid += ":bg"
|
|
}
|
|
|
|
if internal {
|
|
if _, ok := c.internal[cmd]; !ok {
|
|
c.internal[cmd] = map[string]Handler{}
|
|
}
|
|
|
|
c.internal[cmd][uid] = handler
|
|
} else {
|
|
if _, ok := c.external[cmd]; !ok {
|
|
c.external[cmd] = map[string]Handler{}
|
|
}
|
|
|
|
c.external[cmd][uid] = handler
|
|
}
|
|
|
|
_, file, line, _ := runtime.Caller(3)
|
|
|
|
c.debug.Printf("reg %q => %s [int:%t bg:%t] %s:%d", uid, cmd, internal, bg, file, line)
|
|
|
|
return cuid
|
|
}
|
|
|
|
// AddHandler registers a handler (matching the handler interface) for the
|
|
// given event. cuid is the handler uid which can be used to remove the
|
|
// handler with Caller.Remove().
|
|
func (c *Caller) AddHandler(cmd string, handler Handler) (cuid string) {
|
|
return c.sregister(false, false, cmd, handler)
|
|
}
|
|
|
|
// Add registers the handler function for the given event. cuid is the
|
|
// handler uid which can be used to remove the handler with Caller.Remove().
|
|
func (c *Caller) Add(cmd string, handler func(client *Client, event Event)) (cuid string) {
|
|
return c.sregister(false, false, cmd, HandlerFunc(handler))
|
|
}
|
|
|
|
// AddBg registers the handler function for the given event and executes it
|
|
// in a go-routine. cuid is the handler uid which can be used to remove the
|
|
// handler with Caller.Remove().
|
|
func (c *Caller) AddBg(cmd string, handler func(client *Client, event Event)) (cuid string) {
|
|
return c.sregister(false, true, cmd, HandlerFunc(handler))
|
|
}
|
|
|
|
// AddTmp adds a "temporary" handler, which is good for one-time or few-time
|
|
// uses. This supports a deadline and/or manual removal, as this differs
|
|
// much from how normal handlers work. An example of a good use for this
|
|
// would be to capture the entire output of a multi-response query to the
|
|
// server. (e.g. LIST, WHOIS, etc)
|
|
//
|
|
// The supplied handler is able to return a boolean, which if true, will
|
|
// remove the handler from the handler stack.
|
|
//
|
|
// Additionally, AddTmp has a useful option, deadline. When set to greater
|
|
// than 0, deadline will be the amount of time that passes before the handler
|
|
// is removed from the stack, regardless of if the handler returns true or not.
|
|
// This is useful in that it ensures that the handler is cleaned up if the
|
|
// server does not respond appropriately, or takes too long to respond.
|
|
//
|
|
// Note that handlers supplied with AddTmp are executed in a goroutine to
|
|
// ensure that they are not blocking other handlers. However, if you are
|
|
// creating a temporary handler from another handler, it should be a
|
|
// background handler.
|
|
//
|
|
// Use cuid with Caller.Remove() to prematurely remove the handler from the
|
|
// stack, bypassing the timeout or waiting for the handler to return that it
|
|
// wants to be removed from the stack.
|
|
func (c *Caller) AddTmp(cmd string, deadline time.Duration, handler func(client *Client, event Event) bool) (cuid string, done chan struct{}) {
|
|
done = make(chan struct{})
|
|
|
|
cuid = c.sregister(false, true, cmd, HandlerFunc(func(client *Client, event Event) {
|
|
remove := handler(client, event)
|
|
if remove {
|
|
if ok := c.Remove(cuid); ok {
|
|
close(done)
|
|
}
|
|
}
|
|
}))
|
|
|
|
if deadline > 0 {
|
|
go func() {
|
|
select {
|
|
case <-time.After(deadline):
|
|
case <-done:
|
|
}
|
|
|
|
if ok := c.Remove(cuid); ok {
|
|
close(done)
|
|
}
|
|
}()
|
|
}
|
|
|
|
return cuid, done
|
|
}
|
|
|
|
// recoverHandlerPanic is used to catch all handler panics, and re-route
|
|
// them if necessary.
|
|
func recoverHandlerPanic(client *Client, event *Event, id string, skip int) {
|
|
perr := recover()
|
|
if perr == nil {
|
|
return
|
|
}
|
|
|
|
var file string
|
|
var line int
|
|
var ok bool
|
|
|
|
_, file, line, ok = runtime.Caller(skip)
|
|
|
|
err := &HandlerError{
|
|
Event: *event,
|
|
ID: id,
|
|
File: file,
|
|
Line: line,
|
|
Panic: perr,
|
|
Stack: debug.Stack(),
|
|
callOk: ok,
|
|
}
|
|
|
|
client.Config.RecoverFunc(client, err)
|
|
return
|
|
}
|
|
|
|
// HandlerError is the error returned when a panic is intentionally recovered
|
|
// from. It contains useful information like the handler identifier (if
|
|
// applicable), filename, line in file where panic occurred, the call
|
|
// trace, and original event.
|
|
type HandlerError struct {
|
|
Event Event // Event is the event that caused the error.
|
|
ID string // ID is the CUID of the handler.
|
|
File string // File is the file from where the panic originated.
|
|
Line int // Line number where panic originated.
|
|
Panic interface{} // Panic is the error that was passed to panic().
|
|
Stack []byte // Stack is the call stack. Note you may have to skip 1 or 2 due to debug functions.
|
|
callOk bool
|
|
}
|
|
|
|
// Error returns a prettified version of HandlerError, containing ID, file,
|
|
// line, and basic error string.
|
|
func (e *HandlerError) Error() string {
|
|
if e.callOk {
|
|
return fmt.Sprintf("panic during handler [%s] execution in %s:%d: %s", e.ID, e.File, e.Line, e.Panic)
|
|
}
|
|
|
|
return fmt.Sprintf("panic during handler [%s] execution in unknown: %s", e.ID, e.Panic)
|
|
}
|
|
|
|
// String returns the error that panic returned, as well as the entire call
|
|
// trace of where it originated.
|
|
func (e *HandlerError) String() string {
|
|
return fmt.Sprintf("panic: %s\n\n%s", e.Panic, string(e.Stack))
|
|
}
|
|
|
|
// DefaultRecoverHandler can be used with Config.RecoverFunc as a default
|
|
// catch-all for panics. This will log the error, and the call trace to the
|
|
// debug log (see Config.Debug), or os.Stdout if Config.Debug is unset.
|
|
func DefaultRecoverHandler(client *Client, err *HandlerError) {
|
|
if client.Config.Debug == nil {
|
|
fmt.Println(err.Error())
|
|
fmt.Println(err.String())
|
|
return
|
|
}
|
|
|
|
client.debug.Println(err.Error())
|
|
client.debug.Println(err.String())
|
|
}
|