package irc
import (
+ "bytes"
"io"
"strings"
)
// Writes the message to a io.Writer, using WriteString if implemented.
// Returns total number of bytes written and first error encountered
func (c Message) WriteTo(w io.Writer) (n int, err error) {
+ buff := new(bytes.Buffer)
+
+ defer buff.Write([]byte{'\r', '\n'})
+ defer io.Copy(w, buff)
+
n = 0
m := 0
write := func(s string) {
if err == nil {
- m, err = io.WriteString(w, s)
+ m, err = io.WriteString(buff, s)
n += m
}
}
case "375":
fallthrough
case "372":
- fallthrough
+ authResp <- msg
case "376":
authResp <- msg
+ close(authResp)
case commands.Notice:
err := msg.ToNoticeError()
if errors.Is(err, messages.LoginFailedError) ||
errors.Is(err, messages.InvalidAuthError) {
authResp <- msg
+ close(authResp)
} else {
res <- msg
}
return msgs, fmt.Errorf("TagsCapability is unavailable: %v", errors.ErrUnsupported)
}
+ res := make(chan messages.Message)
+ capResp := make(chan messages.Message, 2)
+
+ go func() {
+ requestedCount := len(capabilities)
+ receivedCount := 0
+ for msg := range msgs {
+ if requestedCount == receivedCount || !messages.IsCapAckNak(msg) {
+ res <- msg
+ continue
+ }
+ capResp <- msg
+
+ if msg.Params.Trailing != "" {
+ receivedCount += strings.Count(msg.Params.Trailing, " ") + 1
+ if receivedCount == requestedCount {
+ close(capResp)
+ }
+ }
+ }
+ }()
+
c.WriteMessage(messages.Capability(caps))
- msg := <-msgs
-
- if messages.IsAck(msg) {
- fmt.Fprintln(DebugLogger, "Capability request acknowledged")
- } else if messages.IsNak(msg) {
- badCaps := strings.Split(msg.Params.Trailing, " ")
- return msgs, unsupportedCapabilities(badCaps)
- } else {
- // code error
- fmt.Fprintf(DebugLogger, "Unexpected message after capability request: %v\n", msg)
- return msgs, UnexpectedAnswerError
+
+ for msg := range capResp {
+ if messages.IsCapAck(msg) {
+ fmt.Fprintln(DebugLogger, "Capability request acknowledged")
+ } else if messages.IsCapNak(msg) {
+ badCaps := strings.Split(msg.Params.Trailing, " ")
+ return res, UnsupportedCapabilities(badCaps)
+ } else {
+ // code error
+ fmt.Fprintf(DebugLogger, "Unexpected message after capability request: %v\n", msg)
+ return res, UnexpectedAnswerError
+ }
}
- return msgs, nil
+ return res, nil
}
// not documented on the main page
Cap = "CAP"
- Ack = "ACK"
- Nak = "NAK"
// twitch specific IRC messages. These are all receive only
ClearChat = "CLEARCHAT"
}
func DialNoSsl(origin string) (*Conn, error) {
- c, err := irc.Dial("wss://irc-ws.chat.twitch.tv:80", origin)
+ c, err := irc.Dial("ws://irc-ws.chat.twitch.tv:80", origin)
return (*Conn)(c), err
}
func (w *Conn) WriteMessage(m messages.Message) error {
- fmt.Fprintf(DebugLogger, "Writing %v\n", m)
- if _, err := irc.Message(m).WriteTo(w); err != nil {
- return err
- }
-
- // termination bytes
- _, err := w.Write([]byte{'\r', '\n'})
+ fmt.Fprintf(DebugLogger, "Writing %#v\n", irc.Message(m).String())
+ _, err := irc.Message(m).WriteTo(w)
return err
}
msg, err := irc.ParseBytes(bytes)
if err != nil {
fmt.Fprintf(DebugLogger, "Could not parse %#v\n", bytes)
+ } else {
+ fmt.Fprintf(DebugLogger, "Received %s\n", irc.Message(msg))
}
res <- messages.Message(msg)
}
"strings"
)
-type unsupportedCapabilities []string
+type UnsupportedCapabilities []string
var UnexpectedAnswerError = errors.New("Unexpected result")
var UnsupportedCapabilitiesError = errors.New("unsupported capabilities")
-func (u unsupportedCapabilities) Unwrap() error {
+func (u UnsupportedCapabilities) Unwrap() error {
return UnsupportedCapabilitiesError
}
-func (u unsupportedCapabilities) Error() string {
+func (u UnsupportedCapabilities) Error() string {
return fmt.Sprintf("cannot request capabilities %v: %v", strings.Join(u, ", "), UnsupportedCapabilitiesError)
}
-func (u unsupportedCapabilities) Is(target error) bool {
+func (u UnsupportedCapabilities) Is(target error) bool {
return target == UnsupportedCapabilitiesError
}
package core
-import "twitchchat/twitch/core/messages"
+import (
+ "fmt"
+ "twitchchat/twitch/core/messages"
+)
type ChannelMembers map[string][]string
w.WriteMessage(messages.Join(channels...))
- var channelMembers ChannelMembers
+ channelMembers := make(ChannelMembers)
for _, c := range channels {
resp := <-msgs
+ fmt.Fprintf(DebugLogger, "Reading channel members for %s\n", resp)
if messages.IsNotice(resp) {
return msgs, channelMembers, resp.ToNoticeError()
} else if !messages.IsJoin(resp) {
+++ /dev/null
-package messages
-
-import "twitchchat/twitch/core/commands"
-
-func IsAckNak(m Message) bool {
- return IsAck(m) || IsNak(m)
-}
-
-func IsAck(m Message) bool {
- return m.Command == commands.Ack
-}
-
-func IsNak(m Message) bool {
- return m.Command == commands.Nak
-}
irc.Prefix{},
irc.Command(commands.Pass),
irc.Params{
- []string{fmt.Sprintf("oath:%s", nickname)},
+ []string{fmt.Sprintf("oauth:%s", nickname)},
"",
},
}
irc.Prefix{},
irc.Command(commands.Cap),
irc.Params{
- []string{},
+ []string{"REQ"},
capability,
},
}
}
+
+func IsCapAckNak(m Message) bool {
+ return IsCapAck(m) || IsCapNak(m)
+}
+
+func IsCapAck(m Message) bool {
+ return m.Command == commands.Cap &&
+ len(m.Params.Params) == 2 &&
+ m.Params.Params[0] == "*" &&
+ m.Params.Params[1] == "ACK"
+}
+
+func IsCapNak(m Message) bool {
+ return m.Command == commands.Cap &&
+ len(m.Params.Params) == 2 &&
+ m.Params.Params[0] == "*" &&
+ m.Params.Params[1] == "NAK"
+}
import "twitchchat/irc"
type Message irc.Message
+
+func (m Message) String() string {
+ return irc.Message(m).String()
+}
return irc.Message(p).Params.Trailing
}
+func Ping(s string) Message {
+ return Message{
+ irc.Prefix{},
+ irc.Command(commands.Ping),
+ irc.Params{[]string{s}, ""},
+ }
+}
+
func (p Message) ToPong() Message {
return Message{
irc.Prefix{},
irc.Command(commands.Pong),
- irc.Params{[]string{}, p.Text()},
+ irc.Params{[]string{p.Text()}, ""},
}
}
--- /dev/null
+module clichat
+
+go 1.22.3
+
+require golang.org/x/net v0.25.0
--- /dev/null
+golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac=
+golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
--- /dev/null
+package main
+
+import (
+ "errors"
+ "fmt"
+ "os"
+ "twitchchat/twitch/core"
+ "twitchchat/twitch/core/messages"
+)
+
+func main() {
+ conn, err := core.Dial("http://localhost")
+ if err != nil {
+ panic(err)
+ }
+ defer conn.Close()
+
+ core.DebugLogger = os.Stdout
+
+ msgs := conn.ReadMessages()
+
+ conn.WriteMessage(messages.Ping("foobar"))
+ <-msgs
+
+ if msgs, err = conn.Authenticate("fl_gui", "511j9db3a4xqk8jdukj11nh1eok2d2", msgs); err != nil {
+ panic(err)
+ }
+
+ if msgs, err = conn.WithCapability(msgs, core.MembershipCapability, "Unknown", "foobar"); err != nil {
+ if errors.Is(err, core.UnsupportedCapabilitiesError) {
+ fmt.Printf("%#v\n", err.(core.UnsupportedCapabilities))
+ } else {
+ panic(err)
+ }
+ }
+
+ var members core.ChannelMembers
+ if msgs, members, err = conn.Join(msgs, "fl_gui"); err != nil {
+ panic(err)
+ }
+ fmt.Printf("current members: %s", members)
+
+ for m := range msgs {
+ fmt.Println(m)
+ }
+}
--- /dev/null
+signal: interrupt