summaryrefslogtreecommitdiff
path: root/irc
diff options
context:
space:
mode:
Diffstat (limited to 'irc')
-rw-r--r--irc/channel.go40
-rw-r--r--irc/events.go35
-rw-r--r--irc/session.go (renamed from irc/states.go)769
-rw-r--r--irc/tokens.go24
-rw-r--r--irc/typing.go13
5 files changed, 305 insertions, 576 deletions
diff --git a/irc/channel.go b/irc/channel.go
new file mode 100644
index 0000000..c4a9e6f
--- /dev/null
+++ b/irc/channel.go
@@ -0,0 +1,40 @@
+package irc
+
+import (
+ "bufio"
+ "fmt"
+ "net"
+)
+
+const chanCapacity = 64
+
+func ChanInOut(conn net.Conn) (in <-chan Message, out chan<- Message) {
+ in_ := make(chan Message, chanCapacity)
+ out_ := make(chan Message, chanCapacity)
+
+ go func() {
+ r := bufio.NewScanner(conn)
+ for r.Scan() {
+ line := r.Text()
+ msg, err := ParseMessage(line)
+ if err != nil {
+ continue
+ }
+ in_ <- msg
+ }
+ close(in_)
+ }()
+
+ go func() {
+ for msg := range out_ {
+ // TODO send messages by batches
+ _, err := fmt.Fprintf(conn, "%s\r\n", msg.String())
+ if err != nil {
+ break
+ }
+ }
+ _ = conn.Close()
+ }()
+
+ return in_, out_
+}
diff --git a/irc/events.go b/irc/events.go
index a241232..bdd6914 100644
--- a/irc/events.go
+++ b/irc/events.go
@@ -1,17 +1,9 @@
package irc
-import (
- "time"
-)
+import "time"
type Event interface{}
-type RawMessageEvent struct {
- Message string
- Outgoing bool
- IsValid bool
-}
-
type ErrorEvent struct {
Severity Severity
Code string
@@ -22,13 +14,11 @@ type RegisteredEvent struct{}
type SelfNickEvent struct {
FormerNick string
- Time time.Time
}
type UserNickEvent struct {
- User *Prefix
+ User string
FormerNick string
- Time time.Time
}
type SelfJoinEvent struct {
@@ -36,9 +26,8 @@ type SelfJoinEvent struct {
}
type UserJoinEvent struct {
- User *Prefix
+ User string
Channel string
- Time time.Time
}
type SelfPartEvent struct {
@@ -46,26 +35,22 @@ type SelfPartEvent struct {
}
type UserPartEvent struct {
- User *Prefix
+ User string
Channel string
- Time time.Time
}
type UserQuitEvent struct {
- User *Prefix
+ User string
Channels []string
- Time time.Time
}
type TopicChangeEvent struct {
- User *Prefix
Channel string
Topic string
- Time time.Time
}
type MessageEvent struct {
- User *Prefix
+ User string
Target string
TargetIsChannel bool
Command string
@@ -73,14 +58,6 @@ type MessageEvent struct {
Time time.Time
}
-type TagEvent struct {
- User *Prefix
- Target string
- TargetIsChannel bool
- Typing int
- Time time.Time
-}
-
type HistoryEvent struct {
Target string
Messages []Event
diff --git a/irc/states.go b/irc/session.go
index b2a129a..69d29aa 100644
--- a/irc/states.go
+++ b/irc/session.go
@@ -1,21 +1,17 @@
package irc
import (
- "bufio"
"bytes"
"encoding/base64"
"errors"
"fmt"
- "net"
"strconv"
"strings"
- "sync/atomic"
"time"
+ "unicode"
"unicode/utf8"
)
-const writeDeadline = 10 * time.Second
-
type SASLClient interface {
Handshake() (mech string)
Respond(challenge string) (res string, err error)
@@ -74,62 +70,6 @@ const (
TypingDone
)
-// action contains the arguments of a user action.
-//
-// To keep connection reads and writes in a single coroutine, the library
-// interface functions like Join("#channel") or PrivMsg("target", "message")
-// don't interact with the IRC session directly. Instead, they push an action
-// in the action channel. This action is then processed by the correct
-// coroutine.
-type action interface{}
-
-type (
- actionSendRaw struct {
- raw string
- }
-
- actionChangeNick struct {
- Nick string
- }
- actionChangeMode struct {
- Channel string
- Flags string
- Args []string
- }
-
- actionJoin struct {
- Channel string
- }
- actionPart struct {
- Channel string
- Reason string
- }
- actionSetTopic struct {
- Channel string
- Topic string
- }
- actionQuit struct {
- Reason string
- }
-
- actionPrivMsg struct {
- Target string
- Content string
- }
-
- actionTyping struct {
- Channel string
- }
- actionTypingStop struct {
- Channel string
- }
-
- actionRequestHistory struct {
- Target string
- Before time.Time
- }
-)
-
// User is a known IRC user (we share a channel with it).
type User struct {
Name *Prefix // the nick, user and hostname of the user if known.
@@ -155,20 +95,11 @@ type SessionParams struct {
RealName string
Auth SASLClient
-
- Debug bool // whether the Session should report all messages it sends and receive.
}
-// Session is an IRC session/connection/whatever.
type Session struct {
- conn net.Conn
- msgs chan Message // incoming messages.
- acts chan action // user actions.
- evts chan Event // events sent to the user.
-
- debug bool
-
- running atomic.Value // bool
+ out chan<- Message
+ closed bool
registered bool
typings *Typings // incoming typing notifications.
typingStamps map[string]time.Time // user typing instants.
@@ -185,9 +116,12 @@ type Session struct {
enabledCaps map[string]struct{}
// ISUPPORT features
- casemap func(string) string
- chantypes string
- linelen int
+ casemap func(string) string
+ chantypes string
+ linelen int
+ historyLimit int
+ prefixSymbols string
+ prefixModes string
users map[string]*User // known users.
channels map[string]Channel // joined channels.
@@ -195,18 +129,9 @@ type Session struct {
chReqs map[string]struct{} // set of targets for which history is currently requested.
}
-// NewSession starts an IRC session from the given connection and session
-// parameters.
-//
-// It returns an error when the paramaters are invalid, or when it cannot write
-// to the connection.
-func NewSession(conn net.Conn, params SessionParams) (*Session, error) {
+func NewSession(out chan<- Message, params SessionParams) *Session {
s := &Session{
- conn: conn,
- msgs: make(chan Message, 64),
- acts: make(chan action, 64),
- evts: make(chan Event, 64),
- debug: params.Debug,
+ out: out,
typings: NewTypings(),
typingStamps: map[string]time.Time{},
nick: params.Nickname,
@@ -219,66 +144,28 @@ func NewSession(conn net.Conn, params SessionParams) (*Session, error) {
casemap: CasemapRFC1459,
chantypes: "#&",
linelen: 512,
+ historyLimit: 100,
+ prefixSymbols: "@+",
+ prefixModes: "ov",
users: map[string]*User{},
channels: map[string]Channel{},
chBatches: map[string]HistoryEvent{},
chReqs: map[string]struct{}{},
}
- s.running.Store(true)
-
- go func() {
- r := bufio.NewScanner(conn)
+ s.out <- NewMessage("CAP", "LS", "302")
+ s.out <- NewMessage("NICK", s.nick)
+ s.out <- NewMessage("USER", s.user, "0", "*", s.real)
- for r.Scan() {
- line := r.Text()
- msg, err := ParseMessage(line)
- if err != nil {
- continue
- }
- valid := msg.IsValid()
- if s.debug {
- s.evts <- RawMessageEvent{Message: line, IsValid: valid}
- }
- if valid {
- s.msgs <- msg
- }
- }
-
- s.Stop()
- }()
-
- err := s.send("CAP LS 302\r\nNICK %s\r\nUSER %s 0 * :%s\r\n", s.nick, s.user, s.real)
- if err != nil {
- return nil, err
- }
-
- go s.run()
-
- return s, nil
-}
-
-// Running reports whether we are still connected to the server.
-func (s *Session) Running() bool {
- return s.running.Load().(bool)
+ return s
}
-// Stop stops the session and closes the connection.
-func (s *Session) Stop() {
- if !s.Running() {
+func (s *Session) Close() {
+ if s.closed {
return
}
- s.running.Store(false)
- _ = s.conn.Close()
- close(s.acts)
- close(s.evts)
- close(s.msgs)
- s.typings.Stop()
-}
-
-// Poll returns the event channel where incoming events are reported.
-func (s *Session) Poll() (events <-chan Event) {
- return s.evts
+ s.closed = true
+ close(s.out)
}
// HasCapability reports whether the given capability has been negociated
@@ -297,6 +184,10 @@ func (s *Session) NickCf() string {
return s.nickCf
}
+func (s *Session) IsMe(nick string) bool {
+ return s.nickCf == s.casemap(nick)
+}
+
func (s *Session) IsChannel(name string) bool {
return strings.IndexAny(name, s.chantypes) == 0
}
@@ -332,11 +223,14 @@ func (s *Session) Names(channel string) []Member {
// Typings returns the list of nickname who are currently typing.
func (s *Session) Typings(target string) []string {
- targetCf := s.Casemap(target)
- var res []string
- for t := range s.typings.targets {
- if targetCf == t.Target && s.Casemap(t.Name) != s.NickCf() {
- res = append(res, s.users[t.Name].Name.Name)
+ targetCf := s.casemap(target)
+ res := s.typings.List(targetCf)
+ for i := 0; i < len(res); i++ {
+ if s.IsMe(res[i]) {
+ res = append(res[:i], res[i+1:]...)
+ i--
+ } else if u, ok := s.users[res[i]]; ok {
+ res[i] = u.Name.Name
}
}
return res
@@ -368,50 +262,34 @@ func (s *Session) Topic(channel string) (topic string, who *Prefix, at time.Time
return
}
-// SendRaw sends its given argument verbatim to the server.
func (s *Session) SendRaw(raw string) {
- s.acts <- actionSendRaw{raw}
-}
-
-func (s *Session) sendRaw(act actionSendRaw) (err error) {
- err = s.send("%s\r\n", act.raw)
- return
+ s.out <- NewMessage(raw)
}
func (s *Session) Join(channel string) {
- s.acts <- actionJoin{channel}
-}
-
-func (s *Session) join(act actionJoin) (err error) {
- err = s.send("JOIN %s\r\n", act.Channel)
- return
+ // TODO support keys
+ s.out <- NewMessage("JOIN", channel)
}
func (s *Session) Part(channel, reason string) {
- s.acts <- actionPart{channel, reason}
+ s.out <- NewMessage("PART", channel, reason)
}
-func (s *Session) part(act actionPart) (err error) {
- err = s.send("PART %s :%s\r\n", act.Channel, act.Reason)
- return
+func (s *Session) ChangeTopic(channel, topic string) {
+ s.out <- NewMessage("TOPIC", channel, topic)
}
-func (s *Session) SetTopic(channel, topic string) {
- s.acts <- actionSetTopic{channel, topic}
-}
-
-func (s *Session) setTopic(act actionSetTopic) (err error) {
- err = s.send("TOPIC %s :%s\r\n", act.Channel, act.Topic)
- return
+func (s *Session) Quit(reason string) {
+ s.out <- NewMessage("QUIT", reason)
}
-func (s *Session) Quit(reason string) {
- s.acts <- actionQuit{reason}
+func (s *Session) ChangeNick(nick string) {
+ s.out <- NewMessage("NICK", nick)
}
-func (s *Session) quit(act actionQuit) (err error) {
- err = s.send("QUIT :%s\r\n", act.Reason)
- return
+func (s *Session) ChangeMode(channel, flags string, args []string) {
+ args = append([]string{channel, flags}, args...)
+ s.out <- NewMessage("MODE", args...)
}
func splitChunks(s string, chunkLen int) (chunks []string) {
@@ -433,34 +311,7 @@ func splitChunks(s string, chunkLen int) (chunks []string) {
return
}
-func (s *Session) ChangeNick(nick string) {
- s.acts <- actionChangeNick{nick}
-}
-
-func (s *Session) changeNick(act actionChangeNick) (err error) {
- err = s.send("NICK %s\r\n", act.Nick)
- return
-}
-
-func (s *Session) ChangeMode(channel string, flags string, args []string) {
- s.acts <- actionChangeMode{channel, flags, args}
-}
-
-func (s *Session) changeMode(act actionChangeMode) (err error) {
- if strings.IndexAny(act.Channel, s.chantypes) == 0 {
- err = s.send("MODE %s %s %s\r\n",
- act.Channel, act.Flags, strings.Join(act.Args, " "))
- } else {
- err = s.send("MODE %s %s\r\n", act.Channel, act.Flags)
- }
- return
-}
-
func (s *Session) PrivMsg(target, content string) {
- s.acts <- actionPrivMsg{target, content}
-}
-
-func (s *Session) privMsg(act actionPrivMsg) (err error) {
hostLen := len(s.host)
if hostLen == 0 {
hostLen = len("255.255.255.255")
@@ -470,173 +321,115 @@ func (s *Session) privMsg(act actionPrivMsg) (err error) {
len(s.nick) -
len(s.user) -
hostLen -
- len(act.Target)
- chunks := splitChunks(act.Content, maxMessageLen)
+ len(target)
+ chunks := splitChunks(content, maxMessageLen)
for _, chunk := range chunks {
- err = s.send("PRIVMSG %s :%s\r\n", act.Target, chunk)
- if err != nil {
- return
- }
+ s.out <- NewMessage("PRIVMSG", target, chunk)
}
- target := s.Casemap(act.Target)
- delete(s.typingStamps, target)
- return
-}
-
-func (s *Session) Typing(channel string) {
- s.acts <- actionTyping{channel}
+ targetCf := s.Casemap(target)
+ delete(s.typingStamps, targetCf)
}
-func (s *Session) typing(act actionTyping) (err error) {
- if _, ok := s.enabledCaps["message-tags"]; !ok {
+func (s *Session) Typing(target string) {
+ if !s.HasCapability("message-tags") {
return
}
-
- to := s.Casemap(act.Channel)
+ targetCf := s.casemap(target)
now := time.Now()
-
- if t, ok := s.typingStamps[to]; ok && now.Sub(t).Seconds() < 3.0 {
+ if t, ok := s.typingStamps[targetCf]; ok && now.Sub(t).Seconds() < 3.0 {
return
}
-
- s.typingStamps[to] = now
-
- err = s.send("@+typing=active TAGMSG %s\r\n", act.Channel)
- return
-}
-
-func (s *Session) TypingStop(channel string) {
- s.acts <- actionTypingStop{channel}
+ s.typingStamps[targetCf] = now
+ s.out <- NewMessage("TAGMSG", target).WithTag("+typing", "active")
}
-func (s *Session) typingStop(act actionTypingStop) (err error) {
- if _, ok := s.enabledCaps["message-tags"]; !ok {
+func (s *Session) TypingStop(target string) {
+ if !s.HasCapability("message-tags") {
return
}
+ s.out <- NewMessage("TAGMSG", target).WithTag("+typing", "done")
+}
- err = s.send("@+typing=done TAGMSG %s\r\n", act.Channel)
- return
+type HistoryRequest struct {
+ s *Session
+ target string
+ command string
+ bounds []string
+ limit int
}
-func (s *Session) RequestHistory(target string, before time.Time) {
- s.acts <- actionRequestHistory{target, before}
+func formatTimestamp(t time.Time) string {
+ return fmt.Sprintf("timestamp=%04d-%02d-%02dT%02d:%02d:%02d.%03dZ",
+ t.Year(), t.Month(), t.Day(), t.Hour(), t.Minute(), t.Second(), t.Nanosecond()/1e6)
}
-func (s *Session) requestHistory(act actionRequestHistory) (err error) {
- if _, ok := s.enabledCaps["draft/chathistory"]; !ok {
- return
+func (r *HistoryRequest) WithLimit(limit int) *HistoryRequest {
+ if limit < r.s.historyLimit {
+ r.limit = limit
+ } else {
+ r.limit = r.s.historyLimit
}
+ return r
+}
- target := s.Casemap(act.Target)
- if _, ok := s.chReqs[target]; ok {
+func (r *HistoryRequest) doRequest() {
+ if !r.s.HasCapability("draft/chathistory") {
return
}
- s.chReqs[target] = struct{}{}
- t := act.Before.UTC().Add(1 * time.Second)
- err = s.send("CHATHISTORY BEFORE %s timestamp=%04d-%02d-%02dT%02d:%02d:%02d.%03dZ 100\r\n", act.Target, t.Year(), t.Month(), t.Day(), t.Hour(), t.Minute(), t.Second(), t.Nanosecond()/1e6)
+ targetCf := r.s.casemap(r.target)
+ if _, ok := r.s.chReqs[targetCf]; ok {
+ return
+ }
+ r.s.chReqs[targetCf] = struct{}{}
- return
+ args := make([]string, 0, len(r.bounds)+3)
+ args = append(args, r.command)
+ args = append(args, r.target)
+ args = append(args, r.bounds...)
+ args = append(args, strconv.Itoa(r.limit))
+ r.s.out <- NewMessage("CHATHISTORY", args...)
}
-func (s *Session) run() {
- for s.Running() {
- var err error
+func (r *HistoryRequest) Before(t time.Time) {
+ r.command = "BEFORE"
+ r.bounds = []string{formatTimestamp(t)}
+ r.doRequest()
+}
- select {
- case act, ok := <-s.acts:
- if !ok {
- break
- }
- switch act := act.(type) {
- case actionSendRaw:
- err = s.sendRaw(act)
- case actionChangeNick:
- err = s.changeNick(act)
- case actionChangeMode:
- err = s.changeMode(act)
- case actionJoin:
- err = s.join(act)
- case actionPart:
- err = s.part(act)
- case actionSetTopic:
- err = s.setTopic(act)
- case actionQuit:
- err = s.quit(act)
- case actionPrivMsg:
- err = s.privMsg(act)
- case actionTyping:
- err = s.typing(act)
- case actionTypingStop:
- err = s.typingStop(act)
- case actionRequestHistory:
- err = s.requestHistory(act)
- }
- case msg, ok := <-s.msgs:
- if !ok {
- break
- }
- if s.registered {
- err = s.handle(msg)
- } else {
- err = s.handleStart(msg)
- }
- case t, ok := <-s.typings.Stops():
- if !ok {
- break
- }
- u, ok := s.users[t.Name]
- if !ok {
- break
- }
- c, ok := s.channels[t.Target]
- if !ok {
- break
- }
- s.evts <- TagEvent{
- User: u.Name,
- Target: c.Name,
- Typing: TypingDone,
- Time: time.Now(),
- }
- }
+func (s *Session) NewHistoryRequest(target string) *HistoryRequest {
+ return &HistoryRequest{
+ s: s,
+ target: target,
+ limit: s.historyLimit,
+ }
+}
- if err != nil {
- s.evts <- err
- }
+func (s *Session) HandleMessage(msg Message) Event {
+ if s.registered {
+ return s.handleRegistered(msg)
+ } else {
+ return s.handleUnregistered(msg)
}
}
-func (s *Session) handleStart(msg Message) (err error) {
+func (s *Session) handleUnregistered(msg Message) Event {
switch msg.Command {
case "AUTHENTICATE":
if s.auth != nil {
- var res string
-
- res, err = s.auth.Respond(msg.Params[0])
+ res, err := s.auth.Respond(msg.Params[0])
if err != nil {
- err = s.send("AUTHENTICATE *\r\n")
- return
- }
-
- err = s.send("AUTHENTICATE %s\r\n", res)
- if err != nil {
- return
+ s.out <- NewMessage("AUTHENTICATE", "*")
+ } else {
+ s.out <- NewMessage("AUTHENTICATE", res)
}
}
case rplLoggedin:
- err = s.send("CAP END\r\n")
- if err != nil {
- return
- }
-
+ s.out <- NewMessage("CAP", "END")
s.acct = msg.Params[2]
s.host = ParsePrefix(msg.Params[1]).Host
case errNicklocked, errSaslfail, errSasltoolong, errSaslaborted, errSaslalready, rplSaslmechs:
- err = s.send("CAP END\r\n")
- if err != nil {
- return
- }
+ s.out <- NewMessage("CAP", "END")
case "CAP":
switch msg.Params[1] {
case "LS":
@@ -652,57 +445,44 @@ func (s *Session) handleStart(msg Message) (err error) {
}
for _, c := range ParseCaps(ls) {
- if c.Enable {
- s.availableCaps[c.Name] = c.Value
- } else {
- delete(s.availableCaps, c.Name)
- }
+ s.availableCaps[c.Name] = c.Value
}
if !willContinue {
- var req strings.Builder
-
for c := range s.availableCaps {
if _, ok := SupportedCapabilities[c]; !ok {
continue
}
-
- _, _ = fmt.Fprintf(&req, "CAP REQ %s\r\n", c)
+ s.out <- NewMessage("CAP", "REQ", c)
}
_, ok := s.availableCaps["sasl"]
if s.auth == nil || !ok {
- _, _ = fmt.Fprintf(&req, "CAP END\r\n")
- }
-
- err = s.send(req.String())
- if err != nil {
- return
+ s.out <- NewMessage("CAP", "END")
}
}
default:
- s.handle(msg)
+ return s.handleRegistered(msg)
}
case errNicknameinuse:
- err = s.send("NICK %s_\r\n", msg.Params[1])
- if err != nil {
- return
- }
+ s.out <- NewMessage("NICK", msg.Params[1]+"_")
+ case rplSaslsuccess:
+ // do nothing
default:
- err = s.handle(msg)
+ return s.handleRegistered(msg)
}
-
- return
+ return nil
}
-func (s *Session) handle(msg Message) (err error) {
+func (s *Session) handleRegistered(msg Message) Event {
if id, ok := msg.Tags["batch"]; ok {
if b, ok := s.chBatches[id]; ok {
+ ev := s.newMessageEvent(msg)
s.chBatches[id] = HistoryEvent{
Target: b.Target,
- Messages: append(b.Messages, s.privmsgToEvent(msg)),
+ Messages: append(b.Messages, ev),
}
- return
+ return nil
}
}
@@ -714,14 +494,10 @@ func (s *Session) handle(msg Message) (err error) {
s.users[s.nickCf] = &User{Name: &Prefix{
Name: s.nick, User: s.user, Host: s.host,
}}
- s.evts <- RegisteredEvent{}
-
if s.host == "" {
- err = s.send("WHO %s\r\n", s.nick)
- if err != nil {
- return
- }
+ s.out <- NewMessage("WHO", s.nick)
}
+ return RegisteredEvent{}
case rplIsupport:
s.updateFeatures(msg.Params[1 : len(msg.Params)-1])
case rplWhoreply:
@@ -731,106 +507,49 @@ func (s *Session) handle(msg Message) (err error) {
case "CAP":
switch msg.Params[1] {
case "ACK":
- for _, c := range strings.Split(msg.Params[2], " ") {
- s.enabledCaps[c] = struct{}{}
+ for _, c := range ParseCaps(msg.Params[2]) {
+ if c.Enable {
+ s.enabledCaps[c.Name] = struct{}{}
+ } else {
+ delete(s.enabledCaps, c.Name)
+ }
- if s.auth != nil && c == "sasl" {
+ if s.auth != nil && c.Name == "sasl" {
h := s.auth.Handshake()
- err = s.send("AUTHENTICATE %s\r\n", h)
- if err != nil {
- return
- }
- } else if len(s.channels) != 0 && c == "multi-prefix" {
+ s.out <- NewMessage("AUTHENTICATE", h)
+ } else if len(s.channels) != 0 && c.Name == "multi-prefix" {
// TODO merge NAMES commands
- var sb strings.Builder
- sb.Grow(512)
- for _, c := range s.channels {
- sb.WriteString("NAMES ")
- sb.WriteString(c.Name)
- sb.WriteString("\r\n")
- }
- err = s.send(sb.String())
- if err != nil {
- return
+ for channel := range s.channels {
+ s.out <- NewMessage("NAMES", channel)
}
}
}
case "NAK":
- for _, c := range strings.Split(msg.Params[2], " ") {
- delete(s.enabledCaps, c)
- }
+ // do nothing
case "NEW":
- diff := ParseCaps(msg.Params[2])
-
- for _, c := range diff {
- if c.Enable {
- s.availableCaps[c.Name] = c.Value
- } else {
- delete(s.availableCaps, c.Name)
- }
- }
-
- var req strings.Builder
-
- for _, c := range diff {
+ for _, c := range ParseCaps(msg.Params[2]) {
+ s.availableCaps[c.Name] = c.Value
_, ok := SupportedCapabilities[c.Name]
- if !c.Enable || !ok {
+ if !ok {
continue
}
-
- _, _ = fmt.Fprintf(&req, "CAP REQ %s\r\n", c.Name)
+ s.out <- NewMessage("CAP", "REQ", c.Name)
}
_, ok := s.availableCaps["sasl"]
if s.acct == "" && ok {
// TODO authenticate
}
-
- err = s.send(req.String())
- if err != nil {
- return
- }
case "DEL":
- diff := ParseCaps(msg.Params[2])
-
- for i := range diff {
- diff[i].Enable = !diff[i].Enable
- }
-
- for _, c := range diff {
- if c.Enable {
- s.availableCaps[c.Name] = c.Value
- } else {
- delete(s.availableCaps, c.Name)
- }
- }
-
- var req strings.Builder
-
- for _, c := range diff {
- _, ok := SupportedCapabilities[c.Name]
- if !c.Enable || !ok {
- continue
- }
-
- _, _ = fmt.Fprintf(&req, "CAP REQ %s\r\n", c.Name)
- }
-
- _, ok := s.availableCaps["sasl"]
- if s.acct == "" && ok {
- // TODO authenticate
- }
-
- err = s.send(req.String())
- if err != nil {
- return
+ for _, c := range ParseCaps(msg.Params[2]) {
+ delete(s.availableCaps, c.Name)
+ delete(s.enabledCaps, c.Name)
}
}
case "JOIN":
nickCf := s.Casemap(msg.Prefix.Name)
channelCf := s.Casemap(msg.Params[0])
-
- if nickCf == s.nickCf {
+ if s.IsMe(msg.Prefix.Name) {
s.channels[channelCf] = Channel{
Name: msg.Params[0],
Members: map[*User]string{},
@@ -840,61 +559,56 @@ func (s *Session) handle(msg Message) (err error) {
s.users[nickCf] = &User{Name: msg.Prefix.Copy()}
}
c.Members[s.users[nickCf]] = ""
- t := msg.TimeOrNow()
-
- s.evts <- UserJoinEvent{
- User: msg.Prefix.Copy(),
+ return UserJoinEvent{
+ User: msg.Prefix.Name,
Channel: c.Name,
- Time: t,
}
}
case "PART":
nickCf := s.Casemap(msg.Prefix.Name)
channelCf := s.Casemap(msg.Params[0])
-
- if nickCf == s.nickCf {
+ if s.IsMe(msg.Prefix.Name) {
if c, ok := s.channels[channelCf]; ok {
delete(s.channels, channelCf)
for u := range c.Members {
s.cleanUser(u)
}
- s.evts <- SelfPartEvent{Channel: c.Name}
+ return SelfPartEvent{
+ Channel: c.Name,
+ }
}
} else if c, ok := s.channels[channelCf]; ok {
if u, ok := s.users[nickCf]; ok {
delete(c.Members, u)
s.cleanUser(u)
s.typings.Done(channelCf, nickCf)
-
- s.evts <- UserPartEvent{
- User: msg.Prefix.Copy(),
+ return UserPartEvent{
+ User: u.Name.Name,
Channel: c.Name,
- Time: msg.TimeOrNow(),
}
}
}
case "KICK":
- channelCf := s.Casemap(msg.Params[0])
nickCf := s.Casemap(msg.Params[1])
-
- if nickCf == s.nickCf {
+ channelCf := s.Casemap(msg.Params[0])
+ if s.IsMe(msg.Prefix.Name) {
if c, ok := s.channels[channelCf]; ok {
delete(s.channels, channelCf)
for u := range c.Members {
s.cleanUser(u)
}
- s.evts <- SelfPartEvent{Channel: c.Name}
+ return SelfPartEvent{
+ Channel: c.Name,
+ }
}
} else if c, ok := s.channels[channelCf]; ok {
if u, ok := s.users[nickCf]; ok {
delete(c.Members, u)
s.cleanUser(u)
s.typings.Done(channelCf, nickCf)
-
- s.evts <- UserPartEvent{
- User: u.Name.Copy(),
+ return UserPartEvent{
+ User: u.Name.Name,
Channel: c.Name,
- Time: msg.TimeOrNow(),
}
}
}
@@ -911,11 +625,9 @@ func (s *Session) handle(msg Message) (err error) {
s.typings.Done(channelCf, nickCf)
}
}
-
- s.evts <- UserQuitEvent{
- User: msg.Prefix.Copy(),
+ return UserQuitEvent{
+ User: u.Name.Name,
Channels: channels,
- Time: msg.TimeOrNow(),
}
}
case rplNamreply:
@@ -924,8 +636,7 @@ func (s *Session) handle(msg Message) (err error) {
if c, ok := s.channels[channelCf]; ok {
c.Secret = msg.Params[1] == "@"
- // TODO compute CHANTYPES
- for _, name := range ParseNameReply(msg.Params[3], "~&@%+") {
+ for _, name := range ParseNameReply(msg.Params[3], s.prefixSymbols) {
nickCf := s.Casemap(name.Name.Name)
if _, ok := s.users[nickCf]; !ok {
@@ -941,7 +652,9 @@ func (s *Session) handle(msg Message) (err error) {
if c, ok := s.channels[channelCf]; ok && !c.complete {
c.complete = true
s.channels[channelCf] = c
- s.evts <- SelfJoinEvent{Channel: c.Name}
+ return SelfJoinEvent{
+ Channel: c.Name,
+ }
}
case rplTopic:
channelCf := s.Casemap(msg.Params[1])
@@ -970,51 +683,34 @@ func (s *Session) handle(msg Message) (err error) {
c.TopicWho = msg.Prefix.Copy()
c.TopicTime = msg.TimeOrNow()
s.channels[channelCf] = c
- s.evts <- TopicChangeEvent{
- User: msg.Prefix.Copy(),
+ return TopicChangeEvent{
Channel: c.Name,
Topic: c.Topic,
- Time: c.TopicTime,
}
}
case "PRIVMSG", "NOTICE":
- s.evts <- s.privmsgToEvent(msg)
+ targetCf := s.casemap(msg.Params[0])
+ nickCf := s.casemap(msg.Prefix.Name)
+ s.typings.Done(targetCf, nickCf)
+ return s.newMessageEvent(msg)
case "TAGMSG":
nickCf := s.Casemap(msg.Prefix.Name)
targetCf := s.Casemap(msg.Params[0])
- if nickCf == s.nickCf {
+ if s.IsMe(msg.Prefix.Name) {
// TAGMSG from self
break
}
- typing := TypingUnspec
if t, ok := msg.Tags["+typing"]; ok {
if t == "active" {
- typing = TypingActive
s.typings.Active(targetCf, nickCf)
} else if t == "paused" {
- typing = TypingPaused
- s.typings.Active(targetCf, nickCf)
+ s.typings.Done(targetCf, nickCf)
} else if t == "done" {
- typing = TypingDone
s.typings.Done(targetCf, nickCf)
}
- } else {
- break
- }
-
- ev := TagEvent{
- User: msg.Prefix.Copy(), // TODO correctly casemap
- Target: msg.Params[0], // TODO correctly casemap
- Typing: typing,
- Time: msg.TimeOrNow(),
}
- if c, ok := s.channels[targetCf]; ok {
- ev.Target = c.Name
- ev.TargetIsChannel = true
- }
- s.evts <- ev
case "BATCH":
batchStart := msg.Params[0][0] == '+'
id := msg.Params[0][1:]
@@ -1022,90 +718,74 @@ func (s *Session) handle(msg Message) (err error) {
if batchStart && msg.Params[1] == "chathistory" {
s.chBatches[id] = HistoryEvent{Target: msg.Params[2]}
} else if b, ok := s.chBatches[id]; ok {
- s.evts <- b
delete(s.chBatches, id)
delete(s.chReqs, s.Casemap(b.Target))
+ return b
}
case "NICK":
nickCf := s.Casemap(msg.Prefix.Name)
newNick := msg.Params[0]
newNickCf := s.Casemap(newNick)
- t := msg.TimeOrNow()
- var u *Prefix
if formerUser, ok := s.users[nickCf]; ok {
formerUser.Name.Name = newNick
delete(s.users, nickCf)
s.users[newNickCf] = formerUser
- u = formerUser.Name.Copy()
} else {
break
}
- if nickCf == s.nickCf {
- s.evts <- SelfNickEvent{
- FormerNick: s.nick,
- Time: t,
- }
+ if s.IsMe(msg.Prefix.Name) {
s.nick = newNick
s.nickCf = newNickCf
+ return SelfNickEvent{
+ FormerNick: msg.Prefix.Name,
+ }
} else {
- s.evts <- UserNickEvent{
- User: u,
+ return UserNickEvent{
+ User: msg.Params[0],
FormerNick: msg.Prefix.Name,
- Time: t,
}
}
+ case "PING":
+ s.out <- NewMessage("PONG", msg.Params[0])
+ case "ERROR":
+ s.Close()
case "FAIL":
- s.evts <- ErrorEvent{
+ return ErrorEvent{
Severity: SeverityFail,
Code: msg.Params[1],
- Message: msg.Params[len(msg.Params)-1],
+ Message: strings.Join(msg.Params[2:], " "),
}
case "WARN":
- s.evts <- ErrorEvent{
+ return ErrorEvent{
Severity: SeverityWarn,
Code: msg.Params[1],
- Message: msg.Params[len(msg.Params)-1],
+ Message: strings.Join(msg.Params[2:], " "),
}
case "NOTE":
- s.evts <- ErrorEvent{
+ return ErrorEvent{
Severity: SeverityNote,
Code: msg.Params[1],
- Message: msg.Params[len(msg.Params)-1],
- }
- case "PING":
- err = s.send("PONG :%s\r\n", msg.Params[0])
- if err != nil {
- return
- }
- case "ERROR":
- err = errors.New("connection terminated")
- if len(msg.Params) > 0 {
- err = fmt.Errorf("connection terminated: %s", msg.Params[0])
+ Message: strings.Join(msg.Params[2:], " "),
}
- _ = s.conn.Close()
default:
- // reply handling
- if ReplySeverity(msg.Command) == SeverityFail {
- s.evts <- ErrorEvent{
- Severity: SeverityFail,
+ if msg.IsReply() {
+ return ErrorEvent{
+ Severity: ReplySeverity(msg.Command),
Code: msg.Command,
- Message: msg.Params[len(msg.Params)-1],
+ Message: strings.Join(msg.Params[1:], " "),
}
}
}
-
- return
+ return nil
}
-func (s *Session) privmsgToEvent(msg Message) (ev MessageEvent) {
+func (s *Session) newMessageEvent(msg Message) MessageEvent {
targetCf := s.Casemap(msg.Params[0])
-
- s.typings.Done(targetCf, s.Casemap(msg.Prefix.Name))
- ev = MessageEvent{
- User: msg.Prefix.Copy(), // TODO correctly casemap
- Target: msg.Params[0], // TODO correctly casemap
+ ev := MessageEvent{
+ User: msg.Prefix.Name, // TODO correctly casemap
+ Target: msg.Params[0], // TODO correctly casemap
Command: msg.Command,
Content: msg.Params[1],
Time: msg.TimeOrNow(),
@@ -1114,8 +794,7 @@ func (s *Session) privmsgToEvent(msg Message) (ev MessageEvent) {
ev.Target = c.Name
ev.TargetIsChannel = true
}
-
- return
+ return ev
}
func (s *Session) cleanUser(parted *User) {
@@ -1157,6 +836,7 @@ func (s *Session) updateFeatures(features []string) {
continue
}
+ Switch:
switch key {
case "CASEMAPPING":
switch value {
@@ -1167,31 +847,32 @@ func (s *Session) updateFeatures(features []string) {
}
case "CHANTYPES":
s.chantypes = value
+ case "CHATHISTORY":
+ historyLimit, err := strconv.Atoi(value)
+ if err == nil {
+ s.historyLimit = historyLimit
+ }
case "LINELEN":
linelen, err := strconv.Atoi(value)
if err == nil && linelen != 0 {
s.linelen = linelen
}
- }
- }
-}
-
-func (s *Session) send(format string, args ...interface{}) (err error) {
- msg := fmt.Sprintf(format, args...)
-
- s.conn.SetWriteDeadline(time.Now().Add(writeDeadline))
- _, err = s.conn.Write([]byte(msg))
-
- if s.debug {
- for _, line := range strings.Split(msg, "\r\n") {
- if line != "" {
- s.evts <- RawMessageEvent{
- Message: line,
- Outgoing: true,
+ case "PREFIX":
+ if value == "" {
+ s.prefixModes = ""
+ s.prefixSymbols = ""
+ }
+ if len(value)%2 != 0 {
+ break Switch
+ }
+ for i := 0; i < len(value); i++ {
+ if unicode.MaxASCII < value[i] {
+ break Switch
}
}
+ numPrefixes := len(value)/2 - 1
+ s.prefixModes = value[1 : numPrefixes+1]
+ s.prefixSymbols = value[numPrefixes+2:]
}
}
-
- return
}
diff --git a/irc/tokens.go b/irc/tokens.go
index 86e6539..9c669bb 100644
--- a/irc/tokens.go
+++ b/irc/tokens.go
@@ -229,6 +229,10 @@ type Message struct {
Params []string
}
+func NewMessage(command string, params ...string) Message {
+ return Message{Command: command, Params: params}
+}
+
// ParseMessage parses the message from the given string, which must be trimmed
// of "\r\n" beforehand.
func ParseMessage(line string) (msg Message, err error) {
@@ -282,6 +286,14 @@ func ParseMessage(line string) (msg Message, err error) {
return
}
+func (msg Message) WithTag(key, value string) Message {
+ if msg.Tags == nil {
+ msg.Tags = map[string]string{}
+ }
+ msg.Tags[key] = escapeTagValue(value)
+ return msg
+}
+
// IsReply reports whether the message command is a server reply.
func (msg *Message) IsReply() bool {
if len(msg.Command) != 3 {
@@ -326,9 +338,15 @@ func (msg *Message) String() string {
sb.WriteRune(' ')
sb.WriteString(p)
}
- sb.WriteRune(' ')
- sb.WriteRune(':')
- sb.WriteString(msg.Params[len(msg.Params)-1])
+ lastParam := msg.Params[len(msg.Params)-1]
+ if !strings.ContainsRune(lastParam, ' ') && !strings.HasPrefix(lastParam, ":") {
+ sb.WriteRune(' ')
+ sb.WriteString(lastParam)
+ } else {
+ sb.WriteRune(' ')
+ sb.WriteRune(':')
+ sb.WriteString(lastParam)
+ }
}
return sb.String()
diff --git a/irc/typing.go b/irc/typing.go
index bfadd64..5f028e4 100644
--- a/irc/typing.go
+++ b/irc/typing.go
@@ -73,3 +73,16 @@ func (ts *Typings) Done(target, name string) {
delete(ts.targets, Typing{target, name})
ts.l.Unlock()
}
+
+func (ts *Typings) List(target string) []string {
+ ts.l.Lock()
+ defer ts.l.Unlock()
+
+ var res []string
+ for t := range ts.targets {
+ if target == t.Target {
+ res = append(res, t.Name)
+ }
+ }
+ return res
+}