mirror of
https://github.com/gissleh/irc.git
synced 2026-09-29 00:05:34 +00:00
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.go
This commit is contained in:
@@ -293,6 +293,22 @@ func (client *Client) SendQueuedf(format string, a ...interface{}) {
|
||||
client.SendQueued(fmt.Sprintf(format, a...))
|
||||
}
|
||||
|
||||
// SendCTCP sends a queued message with the following CTCP verb and text. If reply is true,
|
||||
// it will use a NOTICE instead of PRIVMSG.
|
||||
func (client *Client) SendCTCP(verb, targetName string, reply bool, text string) {
|
||||
ircVerb := "PRIVMSG"
|
||||
if reply {
|
||||
ircVerb = "NOTICE"
|
||||
}
|
||||
|
||||
client.SendQueuedf("%s %s :\x01%s %s\x01", ircVerb, targetName, verb, text)
|
||||
}
|
||||
|
||||
// SendCTCPf is SendCTCP with a fmt.Sprintf
|
||||
func (client *Client) SendCTCPf(verb, targetName string, reply bool, format string, a ...interface{}) {
|
||||
client.SendCTCP(verb, targetName, reply, fmt.Sprintf(format, a...))
|
||||
}
|
||||
|
||||
// Emit sends an event through the client's event, and it will return immediately
|
||||
// unless the internal channel is filled up. The returned context can be used to
|
||||
// wait for the event, or the client's destruction.
|
||||
@@ -340,6 +356,25 @@ func (client *Client) EmitSync(ctx context.Context, event Event) (err error) {
|
||||
}
|
||||
}
|
||||
|
||||
// EmitInput emits an input event parsed from the line.
|
||||
func (client *Client) EmitInput(line string, target Target) context.Context {
|
||||
event := ParseInput(line)
|
||||
|
||||
if target != nil {
|
||||
client.mutex.RLock()
|
||||
event.targets = append(event.targets, target)
|
||||
event.targetIds[target] = client.targteIds[target]
|
||||
client.mutex.RUnlock()
|
||||
} else {
|
||||
client.mutex.RLock()
|
||||
event.targets = append(event.targets, client.status)
|
||||
event.targetIds[client.status] = client.targteIds[client.status]
|
||||
client.mutex.RUnlock()
|
||||
}
|
||||
|
||||
return client.Emit(event)
|
||||
}
|
||||
|
||||
// Value gets a client value.
|
||||
func (client *Client) Value(key string) (v interface{}, ok bool) {
|
||||
client.mutex.RLock()
|
||||
@@ -512,6 +547,11 @@ func (client *Client) handleEventLoop() {
|
||||
client.handleEvent(event)
|
||||
emit(event, client)
|
||||
|
||||
// Turn an unhandled input into a raw command.
|
||||
if event.kind == "input" && !event.Killed() {
|
||||
client.SendQueued(strings.ToUpper(event.verb) + " " + event.Text)
|
||||
}
|
||||
|
||||
event.cancel()
|
||||
}
|
||||
case <-ticker.C:
|
||||
@@ -560,7 +600,7 @@ func (client *Client) handleSendLoop() {
|
||||
time.Sleep(time.Second - deltaTime)
|
||||
lastRefresh = now
|
||||
|
||||
queue = 0
|
||||
queue = 1
|
||||
}
|
||||
} else {
|
||||
lastRefresh = now
|
||||
|
||||
+38
-8
@@ -6,10 +6,14 @@ import (
|
||||
"testing"
|
||||
|
||||
"git.aiterp.net/gisle/irc"
|
||||
"git.aiterp.net/gisle/irc/handlers"
|
||||
"git.aiterp.net/gisle/irc/internal/irctest"
|
||||
)
|
||||
|
||||
func TestClient(t *testing.T) {
|
||||
irc.Handle(handlers.Input)
|
||||
irc.Handle(handlers.MRoleplay)
|
||||
|
||||
client := irc.New(context.Background(), irc.Config{
|
||||
Nick: "Test",
|
||||
User: "Tester",
|
||||
@@ -71,8 +75,8 @@ func TestClient(t *testing.T) {
|
||||
{Kind: 'S', Data: ":Gisle!~irce@10.32.0.1 MODE #Test +N-s "},
|
||||
{Kind: 'S', Data: ":Test1234!~test2@172.17.37.1 JOIN #Test Test1234"},
|
||||
{Kind: 'S', Data: ":Gisle!~irce@10.32.0.1 MODE #Test +v Test1234"},
|
||||
{Kind: 'S', Data: "PING :archgisle.lan"}, // Ping/Pong to sync.
|
||||
{Kind: 'C', Data: "PONG :archgisle.lan"},
|
||||
{Kind: 'S', Data: "PING :testserver.example.com"}, // Ping/Pong to sync.
|
||||
{Kind: 'C', Data: "PONG :testserver.example.com"},
|
||||
{Callback: func() error {
|
||||
channel := client.Channel("#Test")
|
||||
if channel == nil {
|
||||
@@ -98,8 +102,8 @@ func TestClient(t *testing.T) {
|
||||
{Kind: 'S', Data: ":Hunter2!~test2@172.17.37.1 AWAY :Doing stuff"},
|
||||
{Kind: 'S', Data: ":Gisle!~irce@10.32.0.1 AWAY"},
|
||||
{Kind: 'S', Data: ":Gisle!~irce@10.32.0.1 PART #Test :Leaving the channel"},
|
||||
{Kind: 'S', Data: "PING :archgisle.lan"}, // Ping/Pong to sync.
|
||||
{Kind: 'C', Data: "PONG :archgisle.lan"},
|
||||
{Kind: 'S', Data: "PING :testserver.example.com"}, // Ping/Pong to sync.
|
||||
{Kind: 'C', Data: "PONG :testserver.example.com"},
|
||||
{Callback: func() error {
|
||||
channel := client.Channel("#Test")
|
||||
if channel == nil {
|
||||
@@ -133,8 +137,8 @@ func TestClient(t *testing.T) {
|
||||
return nil
|
||||
}},
|
||||
{Kind: 'S', Data: ":Hunter2!~test2@172.17.37.1 PRIVMSG Test768 :Hello, World"},
|
||||
{Kind: 'S', Data: "PING :archgisle.lan"}, // Ping/Pong to sync.
|
||||
{Kind: 'C', Data: "PONG :archgisle.lan"},
|
||||
{Kind: 'S', Data: "PING :testserver.example.com"}, // Ping/Pong to sync.
|
||||
{Kind: 'C', Data: "PONG :testserver.example.com"},
|
||||
{Callback: func() error {
|
||||
query := client.Query("Hunter2")
|
||||
if query == nil {
|
||||
@@ -144,8 +148,8 @@ func TestClient(t *testing.T) {
|
||||
return nil
|
||||
}},
|
||||
{Kind: 'S', Data: ":Hunter2!~test2@172.17.37.1 NICK SevenAsterisks"},
|
||||
{Kind: 'S', Data: "PING :archgisle.lan"}, // Ping/Pong to sync.
|
||||
{Kind: 'C', Data: "PONG :archgisle.lan"},
|
||||
{Kind: 'S', Data: "PING :testserver.example.com"}, // Ping/Pong to sync.
|
||||
{Kind: 'C', Data: "PONG :testserver.example.com"},
|
||||
{Callback: func() error {
|
||||
oldQuerry := client.Query("Hunter2")
|
||||
if oldQuerry != nil {
|
||||
@@ -159,6 +163,32 @@ func TestClient(t *testing.T) {
|
||||
|
||||
return nil
|
||||
}},
|
||||
{Callback: func() error {
|
||||
client.EmitInput("/invalidcommand stuff and things", nil)
|
||||
return nil
|
||||
}},
|
||||
{Kind: 'C', Data: "INVALIDCOMMAND stuff and things"},
|
||||
{Kind: 'S', Data: ":testserver.example.com 421 Test768 INVALIDCOMMAND :Unknown command"},
|
||||
{Callback: func() error {
|
||||
channel := client.Channel("#Test")
|
||||
if channel == nil {
|
||||
return errors.New("Channel #Test not found")
|
||||
}
|
||||
|
||||
client.EmitInput("/me does stuff", channel)
|
||||
client.EmitInput("/describe #Test describes stuff", channel)
|
||||
client.EmitInput("/text Hello, World", channel)
|
||||
client.EmitInput("Hello again", channel)
|
||||
return nil
|
||||
}},
|
||||
{Kind: 'C', Data: "PRIVMSG #Test :\x01ACTION does stuff\x01"},
|
||||
{Kind: 'C', Data: "PRIVMSG #Test :\x01ACTION describes stuff\x01"},
|
||||
{Kind: 'C', Data: "PRIVMSG #Test :Hello, World"},
|
||||
{Kind: 'C', Data: "PRIVMSG #Test :Hello again"},
|
||||
{Kind: 'S', Data: ":Test768!~test@127.0.0.1 PRIVMSG #Test :\x01ACTION does stuff\x01"},
|
||||
{Kind: 'S', Data: ":Test768!~test@127.0.0.1 PRIVMSG #Test :\x01ACTION describes stuff\x01"},
|
||||
{Kind: 'S', Data: ":Test768!~test@127.0.0.1 PRIVMSG #Test :Hello, World"},
|
||||
{Kind: 'S', Data: ":Test768!~test@127.0.0.1 PRIVMSG #Test :Hello again"},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -91,6 +91,9 @@ func (event *Event) Context() context.Context {
|
||||
|
||||
// Kill stops propagation of the event. The context will be killed once
|
||||
// the current event handler returns.
|
||||
//
|
||||
// A use case for this is to prevent the default input handler from firing
|
||||
// on an already prcoessed input event.
|
||||
func (event *Event) Kill() {
|
||||
event.killed = true
|
||||
}
|
||||
@@ -125,6 +128,59 @@ func (event *Event) Arg(index int) string {
|
||||
return event.Args[index]
|
||||
}
|
||||
|
||||
// Target finds the first target with one of the kinds specified. If none
|
||||
// are specified, the first target will be returned. If more than one
|
||||
// is provided, the first kinds are prioritized.
|
||||
func (event *Event) Target(kinds ...string) Target {
|
||||
if len(event.targets) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
if len(kinds) == 0 || kinds[0] == "" {
|
||||
return event.targets[0]
|
||||
}
|
||||
|
||||
for _, kind := range kinds {
|
||||
for _, target := range event.targets {
|
||||
if target.Kind() == kind {
|
||||
return target
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ChannelTarget gets the first channel target.
|
||||
func (event *Event) ChannelTarget() *Channel {
|
||||
target := event.Target("channel")
|
||||
if target == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return target.(*Channel)
|
||||
}
|
||||
|
||||
// QueryTarget gets the first query target.
|
||||
func (event *Event) QueryTarget() *Query {
|
||||
target := event.Target("query")
|
||||
if target == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return target.(*Query)
|
||||
}
|
||||
|
||||
// StatusTarget gets the first status target.
|
||||
func (event *Event) StatusTarget() *Status {
|
||||
target := event.Target("status")
|
||||
if target == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return target.(*Status)
|
||||
}
|
||||
|
||||
// MarshalJSON makes a JSON object from the event.
|
||||
func (event *Event) MarshalJSON() ([]byte, error) {
|
||||
data := eventJSONData{
|
||||
|
||||
@@ -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
-1
@@ -8,7 +8,7 @@ import (
|
||||
|
||||
var unescapeTags = strings.NewReplacer("\\\\", "\\", "\\:", ";", "\\s", " ", "\\r", "\r", "\\n", "\n")
|
||||
|
||||
// ParsePacket parses and irc line and returns an event that's either of kind `packet`, `ctcp` or `ctcpreply`
|
||||
// ParsePacket parses an irc line and returns an event that's either of kind `packet`, `ctcp` or `ctcpreply`
|
||||
func ParsePacket(line string) (Event, error) {
|
||||
event := NewEvent("packet", "")
|
||||
event.Time = time.Now()
|
||||
|
||||
@@ -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
|
||||
@@ -63,7 +63,7 @@ func (interaction *Interaction) Listen() (addr string, err error) {
|
||||
}
|
||||
case 'C':
|
||||
{
|
||||
conn.SetReadDeadline(time.Now().Add(time.Second))
|
||||
conn.SetReadDeadline(time.Now().Add(time.Second * 2))
|
||||
input, err := reader.ReadString('\n')
|
||||
if err != nil {
|
||||
interaction.Failure = &InteractionFailure{
|
||||
|
||||
@@ -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:]
|
||||
}
|
||||
Reference in New Issue
Block a user