Browse Source
			
			
			Added input handling and m_roleplay client-side handlers (...)
			
				
		Added input handling and m_roleplay client-side handlers (...)
	
		
	
			
				- Added Client.EmitInput() - Added ircutil.ParseArgAndText for common /msg <target> <message...> type inputs - Added target getters for Event - Added default handling for input events that haven't been killed - Removed handler_debug - Changed timeout for interaction to 2s - Fixed comment typo in event_packet.gomaster
				 11 changed files with 383 additions and 57 deletions
			
			
		- 
					42client.go
 - 
					46client_test.go
 - 
					56event.go
 - 
					27event_input.go
 - 
					2event_packet.go
 - 
					46handler_debug.go
 - 
					111handlers/input.go
 - 
					87handlers/mroleplay.go
 - 
					4handlers/package.go
 - 
					2internal/irctest/interaction.go
 - 
					17ircutil/parse-arg-and-text.go
 
@ -0,0 +1,27 @@ | 
				
			|||
package irc | 
				
			|||
 | 
				
			|||
import ( | 
				
			|||
	"strings" | 
				
			|||
	"time" | 
				
			|||
) | 
				
			|||
 | 
				
			|||
// ParseInput parses an input command into an event.
 | 
				
			|||
func ParseInput(line string) Event { | 
				
			|||
	event := NewEvent("input", "") | 
				
			|||
	event.Time = time.Now() | 
				
			|||
 | 
				
			|||
	if strings.HasPrefix(line, "/") { | 
				
			|||
		split := strings.SplitN(line[1:], " ", 2) | 
				
			|||
		event.verb = split[0] | 
				
			|||
		if len(split) == 2 { | 
				
			|||
			event.Text = split[1] | 
				
			|||
		} | 
				
			|||
	} else { | 
				
			|||
		event.Text = line | 
				
			|||
		event.verb = "text" | 
				
			|||
	} | 
				
			|||
 | 
				
			|||
	event.name = event.kind + "." + event.verb | 
				
			|||
 | 
				
			|||
	return event | 
				
			|||
} | 
				
			|||
@ -1,46 +0,0 @@ | 
				
			|||
package irc | 
				
			|||
 | 
				
			|||
import ( | 
				
			|||
	"encoding/json" | 
				
			|||
	"log" | 
				
			|||
) | 
				
			|||
 | 
				
			|||
// DebugLogger is for
 | 
				
			|||
type DebugLogger interface { | 
				
			|||
	Println(v ...interface{}) | 
				
			|||
} | 
				
			|||
 | 
				
			|||
type defaultDebugLogger struct{} | 
				
			|||
 | 
				
			|||
func (logger *defaultDebugLogger) Println(v ...interface{}) { | 
				
			|||
	log.Println(v...) | 
				
			|||
} | 
				
			|||
 | 
				
			|||
// EnableDebug logs all events that passes through it, ignoring killed
 | 
				
			|||
// events. It will always include the standard handlers, but any custom
 | 
				
			|||
// handlers defined after EnableDebug will not have their effects shown.
 | 
				
			|||
// You may pass `nil` as a logger to use the standard log package's Println.
 | 
				
			|||
func EnableDebug(logger DebugLogger, indented bool) { | 
				
			|||
	if logger != nil { | 
				
			|||
		logger = &defaultDebugLogger{} | 
				
			|||
	} | 
				
			|||
 | 
				
			|||
	Handle(func(event *Event, client *Client) { | 
				
			|||
		var data []byte | 
				
			|||
		var err error | 
				
			|||
 | 
				
			|||
		if indented { | 
				
			|||
			data, err = json.MarshalIndent(event, "", "  ") | 
				
			|||
			if err != nil { | 
				
			|||
				return | 
				
			|||
			} | 
				
			|||
		} else { | 
				
			|||
			data, err = json.Marshal(event) | 
				
			|||
			if err != nil { | 
				
			|||
				return | 
				
			|||
			} | 
				
			|||
		} | 
				
			|||
 | 
				
			|||
		logger.Println(string(data)) | 
				
			|||
	}) | 
				
			|||
} | 
				
			|||
@ -0,0 +1,111 @@ | 
				
			|||
package handlers | 
				
			|||
 | 
				
			|||
import ( | 
				
			|||
	"git.aiterp.net/gisle/irc" | 
				
			|||
	"git.aiterp.net/gisle/irc/ircutil" | 
				
			|||
) | 
				
			|||
 | 
				
			|||
// Input handles the default input.
 | 
				
			|||
func Input(event *irc.Event, client *irc.Client) { | 
				
			|||
	switch event.Name() { | 
				
			|||
 | 
				
			|||
	// /msg sends an action to a target specified before the message.
 | 
				
			|||
	case "input.msg": | 
				
			|||
		{ | 
				
			|||
			targetName, text := ircutil.ParseArgAndText(event.Text) | 
				
			|||
			if targetName == "" || text == "" { | 
				
			|||
				client.EmitNonBlocking(irc.NewErrorEvent("input", "Usage: /msg <target> <text...>")) | 
				
			|||
				break | 
				
			|||
			} | 
				
			|||
 | 
				
			|||
			overhead := client.PrivmsgOverhead(targetName, true) | 
				
			|||
			cuts := ircutil.CutMessage(text, overhead) | 
				
			|||
			for _, cut := range cuts { | 
				
			|||
				client.Sendf("PRIVMSG %s :%s", targetName, cut) | 
				
			|||
			} | 
				
			|||
 | 
				
			|||
			event.Kill() | 
				
			|||
		} | 
				
			|||
 | 
				
			|||
	// /text (or text without a command) sends a message to the target.
 | 
				
			|||
	case "input.text": | 
				
			|||
		{ | 
				
			|||
			if event.Text == "" { | 
				
			|||
				client.EmitNonBlocking(irc.NewErrorEvent("input", "Usage: /text <text...>")) | 
				
			|||
				break | 
				
			|||
			} | 
				
			|||
 | 
				
			|||
			target := event.Target("query", "channel") | 
				
			|||
			if target == nil { | 
				
			|||
				client.EmitNonBlocking(irc.NewErrorEvent("input", "Target is not a channel or query")) | 
				
			|||
				break | 
				
			|||
			} | 
				
			|||
 | 
				
			|||
			overhead := client.PrivmsgOverhead(target.Name(), false) | 
				
			|||
			cuts := ircutil.CutMessage(event.Text, overhead) | 
				
			|||
			for _, cut := range cuts { | 
				
			|||
				client.SendQueuedf("PRIVMSG %s :%s", target.Name(), cut) | 
				
			|||
			} | 
				
			|||
 | 
				
			|||
			event.Kill() | 
				
			|||
		} | 
				
			|||
 | 
				
			|||
	// /me and /action sends a CTCP ACTION.
 | 
				
			|||
	case "input.me", "input.action": | 
				
			|||
		{ | 
				
			|||
			if event.Text == "" { | 
				
			|||
				client.EmitNonBlocking(irc.NewErrorEvent("input", "Usage: /me <text...>")) | 
				
			|||
				break | 
				
			|||
			} | 
				
			|||
 | 
				
			|||
			target := event.Target("query", "channel") | 
				
			|||
			if target == nil { | 
				
			|||
				client.EmitNonBlocking(irc.NewErrorEvent("input", "Target is not a channel or query")) | 
				
			|||
				break | 
				
			|||
			} | 
				
			|||
 | 
				
			|||
			overhead := client.PrivmsgOverhead(target.Name(), true) | 
				
			|||
			cuts := ircutil.CutMessage(event.Text, overhead) | 
				
			|||
			for _, cut := range cuts { | 
				
			|||
				client.SendCTCP("ACTION", target.Name(), false, cut) | 
				
			|||
			} | 
				
			|||
 | 
				
			|||
			event.Kill() | 
				
			|||
		} | 
				
			|||
 | 
				
			|||
	// /describe sends an action to a target specified before the message, like /msg.
 | 
				
			|||
	case "input.describe": | 
				
			|||
		{ | 
				
			|||
			targetName, text := ircutil.ParseArgAndText(event.Text) | 
				
			|||
			if targetName == "" || text == "" { | 
				
			|||
				client.EmitNonBlocking(irc.NewErrorEvent("input", "Usage: /describe <target> <text...>")) | 
				
			|||
				break | 
				
			|||
			} | 
				
			|||
 | 
				
			|||
			overhead := client.PrivmsgOverhead(targetName, true) | 
				
			|||
			cuts := ircutil.CutMessage(text, overhead) | 
				
			|||
			for _, cut := range cuts { | 
				
			|||
				client.SendCTCP("ACTION", targetName, false, cut) | 
				
			|||
			} | 
				
			|||
 | 
				
			|||
			event.Kill() | 
				
			|||
		} | 
				
			|||
 | 
				
			|||
	// /m is a shorthand for /mode that targets the current channel
 | 
				
			|||
	case "input.m": | 
				
			|||
		{ | 
				
			|||
			if event.Text == "" { | 
				
			|||
				client.EmitNonBlocking(irc.NewErrorEvent("input", "Usage: /m <text...>")) | 
				
			|||
				break | 
				
			|||
			} | 
				
			|||
 | 
				
			|||
			channel := event.ChannelTarget() | 
				
			|||
			if channel != nil { | 
				
			|||
				client.EmitNonBlocking(irc.NewErrorEvent("input", "Target is not a channel")) | 
				
			|||
				break | 
				
			|||
			} | 
				
			|||
 | 
				
			|||
			client.SendQueuedf("MODE %s %s", channel.Name(), event.Text) | 
				
			|||
		} | 
				
			|||
	} | 
				
			|||
} | 
				
			|||
@ -0,0 +1,87 @@ | 
				
			|||
package handlers | 
				
			|||
 | 
				
			|||
import ( | 
				
			|||
	"strings" | 
				
			|||
 | 
				
			|||
	"git.aiterp.net/gisle/irc" | 
				
			|||
	"git.aiterp.net/gisle/irc/ircutil" | 
				
			|||
) | 
				
			|||
 | 
				
			|||
// MRoleplay is a handler that adds commands for cutting NPC commands, as well as cleaning up
 | 
				
			|||
// the input from the server. It's named after Charybdis IRCd's m_roleplay module.
 | 
				
			|||
func MRoleplay(event *irc.Event, client *irc.Client) { | 
				
			|||
	switch event.Name() { | 
				
			|||
	case "packet.privmsg", "ctcp.action": | 
				
			|||
		{ | 
				
			|||
			// Detect m_roleplay
 | 
				
			|||
			if strings.HasPrefix(event.Nick, "\x1F") { | 
				
			|||
				event.Nick = event.Nick[1 : len(event.Nick)-2] | 
				
			|||
				if event.Verb() == "PRIVMSG" { | 
				
			|||
					event.RenderTags["mRoleplay"] = "npc" | 
				
			|||
				} else { | 
				
			|||
					event.RenderTags["mRoleplay"] = "npca" | 
				
			|||
				} | 
				
			|||
			} else if strings.HasPrefix(event.Nick, "=") { | 
				
			|||
				event.RenderTags["mRoleplay"] = "scene" | 
				
			|||
			} else { | 
				
			|||
				break | 
				
			|||
			} | 
				
			|||
 | 
				
			|||
			lastSpace := strings.LastIndex(event.Text, " ") | 
				
			|||
			lastParanthesis := strings.LastIndex(event.Text, "(") | 
				
			|||
			if lastParanthesis != -1 && lastSpace != -1 && lastParanthesis == lastSpace+1 { | 
				
			|||
				event.Text = event.Text[:lastSpace] | 
				
			|||
			} | 
				
			|||
		} | 
				
			|||
	case "input.npcc", "input.npcac": | 
				
			|||
		{ | 
				
			|||
			isAction := event.Verb() == "npcac" | 
				
			|||
			nick, text := ircutil.ParseArgAndText(event.Text) | 
				
			|||
			if nick == "" || text == "" { | 
				
			|||
				client.EmitNonBlocking(irc.NewErrorEvent("input", "Usage: /"+event.Verb()+" <nick> <text...>")) | 
				
			|||
				break | 
				
			|||
			} | 
				
			|||
 | 
				
			|||
			channel := event.ChannelTarget() | 
				
			|||
			if channel == nil { | 
				
			|||
				client.EmitNonBlocking(irc.NewErrorEvent("input", "Target is not a channel")) | 
				
			|||
				break | 
				
			|||
			} | 
				
			|||
 | 
				
			|||
			overhead := ircutil.MessageOverhead("\x1f"+nick+"\x1f", client.Nick(), "npc.fakeuser.invalid", channel.Name(), isAction) | 
				
			|||
			cuts := ircutil.CutMessage(event.Text, overhead) | 
				
			|||
 | 
				
			|||
			for _, cut := range cuts { | 
				
			|||
				npcCommand := "NPCA" | 
				
			|||
				if event.Verb() == "npcc" { | 
				
			|||
					npcCommand = "NPC" | 
				
			|||
				} | 
				
			|||
 | 
				
			|||
				client.SendQueuedf("%s %s :%s", npcCommand, channel.Name(), cut) | 
				
			|||
			} | 
				
			|||
 | 
				
			|||
			event.Kill() | 
				
			|||
		} | 
				
			|||
	case "input.scenec": | 
				
			|||
		{ | 
				
			|||
			if event.Text == "" { | 
				
			|||
				client.EmitNonBlocking(irc.NewErrorEvent("input", "Usage: /scenec <text...>")) | 
				
			|||
				break | 
				
			|||
			} | 
				
			|||
 | 
				
			|||
			channel := event.ChannelTarget() | 
				
			|||
			if channel == nil { | 
				
			|||
				client.EmitNonBlocking(irc.NewErrorEvent("input", "Target is not a channel")) | 
				
			|||
				break | 
				
			|||
			} | 
				
			|||
 | 
				
			|||
			overhead := ircutil.MessageOverhead("=Scene=", client.Nick(), "npc.fakeuser.invalid", channel.Name(), false) | 
				
			|||
			cuts := ircutil.CutMessage(event.Text, overhead) | 
				
			|||
			for _, cut := range cuts { | 
				
			|||
				client.SendQueuedf("SCENE %s :%s", channel.Name(), cut) | 
				
			|||
			} | 
				
			|||
 | 
				
			|||
			event.Kill() | 
				
			|||
		} | 
				
			|||
	} | 
				
			|||
} | 
				
			|||
@ -0,0 +1,4 @@ | 
				
			|||
// Package handlers contain extra handlers that add features to an IRC client that are not
 | 
				
			|||
// always necessary. This includes most input handlers and features. If something adds
 | 
				
			|||
// a IRCv3 cap, it should not be here, however.
 | 
				
			|||
package handlers | 
				
			|||
@ -0,0 +1,17 @@ | 
				
			|||
package ircutil | 
				
			|||
 | 
				
			|||
import ( | 
				
			|||
	"strings" | 
				
			|||
) | 
				
			|||
 | 
				
			|||
// ParseArgAndText parses a text like "#Channel stuff and things" into "#Channel"
 | 
				
			|||
// and "stuff and things". This is commonly used for input commands which has
 | 
				
			|||
// no standard
 | 
				
			|||
func ParseArgAndText(s string) (arg, text string) { | 
				
			|||
	spaceIndex := strings.Index(s, " ") | 
				
			|||
	if spaceIndex == -1 { | 
				
			|||
		return s, "" | 
				
			|||
	} | 
				
			|||
 | 
				
			|||
	return s[:spaceIndex], s[spaceIndex+1:] | 
				
			|||
} | 
				
			|||
						Write
						Preview
					
					
					Loading…
					
					Cancel
						Save
					
		Reference in new issue