refactor: change project structure

This commit is contained in:
2022-01-24 02:26:24 +02:00
parent cfeadeedd1
commit e34cc39e63
11 changed files with 316 additions and 325 deletions
+176
View File
@@ -0,0 +1,176 @@
// gorc project
// Copyright (C) 2021 IllusionMan1212
//
// This program is free software; you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation; either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License along
// with this program; if not, see https://www.gnu.org/licenses.
package irc
import (
"crypto/tls"
"fmt"
"log"
"net"
"strings"
tea "github.com/charmbracelet/bubbletea"
)
type User struct {
// User prefix in channel
Prefix string
}
type Channel struct {
// Channel name
Name string
// Channel topic
Topic string
// Channel messages history
History string
// Users in this channel
// The map key is the user's nickname
// and the user struct holds data about that user
// such as, prefixes in this channel and etc...
Users map[string]User
}
type Client struct {
// tcp connection
TcpConn net.Conn
// Host that client is connected to
Host string
// Port that client is connected to
Port string
// Nickname currently in use by the user
Nickname string
// Joined channels
Channels []Channel
// Active channel name
ActiveChannel string
// Active channel index
ActiveChannelIndex int
// Reference to the bubbletea program
Tea *tea.Program
}
const CRLF = "\r\n"
func (s *Client) Initialize(host string, port string, tlsEnabled bool) {
addr := fmt.Sprintf("%s:%s", host, port)
if tlsEnabled {
cfg := &tls.Config{ServerName: host}
conn, err := tls.Dial("tcp", addr, cfg)
if err != nil {
// TODO: properly handle the error instead of Fatal-ing (failed to initiate a connection to server)
log.Fatal(err)
}
s.TcpConn = conn
s.Host = host
s.Port = port
s.ActiveChannel = host
s.Channels = make([]Channel, 0)
return
}
addrTCP, err := net.ResolveTCPAddr("tcp", addr)
if err != nil {
log.Fatal(err)
}
conn, err := net.DialTCP("tcp", nil, addrTCP)
if err != nil {
log.Fatal(err)
}
s.TcpConn = conn
s.Host = host
s.Port = port
s.ActiveChannel = host
s.Channels = make([]Channel, 0)
}
func (c *Client) Register(nick string, password string, channel string) {
c.Channels = append(c.Channels, Channel{
Name: c.Host,
Users: make(map[string]User),
})
c.SendCommand("CAP", "LS")
if password != "" {
c.SendCommand("PASS", password)
}
// TODO: check if nickname has spaces and remove them
c.SendCommand("NICK", nick)
c.Nickname = nick
c.SendCommand("USER", nick, "0", "*", nick)
// TODO: CAP REQ :whatever capability the client recognizes and supports
c.SendCommand("CAP", "END")
// joining a channel when registering is optional
if channel != "" {
c.ActiveChannel = channel
c.SendCommand("JOIN", channel)
c.Channels = append(c.Channels, Channel{
Name: c.ActiveChannel,
Users: make(map[string]User),
})
c.ActiveChannelIndex = 1
}
}
func (c Client) SendCommand(cmd string, params ...string) {
if c.TcpConn == nil {
// TODO: properly handle the error instead of Fatal-ing
log.Fatal("Attempted to write data to nil connection")
}
paramsString := ""
// if we have more than 1 param
if len(params) > 1 {
lastParam := params[len(params)-1]
// if the last param is a trailing param
// we prepend a colon to it
if strings.Contains(lastParam, " ") {
lastParam = " :" + lastParam
} else {
lastParam = " " + lastParam
}
paramsString = " " + strings.Join(params[:len(params)-1], " ")
paramsString += lastParam
// if we have exactly 1 param and it's not empty
} else if len(params) == 1 && params[0] != "" {
// if this 1 param contains spaces, we prepend a colon
if strings.Contains(params[0], " ") {
paramsString = " :" + params[0]
} else {
paramsString = " " + params[0]
}
}
c.TcpConn.Write([]byte(cmd + paramsString + CRLF))
}
+374
View File
@@ -0,0 +1,374 @@
// gorc project
// Copyright (C) 2021 IllusionMan1212
//
// This program is free software; you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation; either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License along
// with this program; if not, see https://www.gnu.org/licenses.
package commands
/* --- IRC Ports --- */
const (
INSECURE_PORT = 6667
SECURE_PORT = 6697
)
/* --- Prohibited characters in channel names --- */
const ( // do we need this as a client ????
SPACE = " " // 0x20
CTRL_G = "^G" // 0x07
COMMA = "," // 0x2C
)
/* --- COMMANDS --- */
const (
// Connection messages
CAP = "CAP" // Negotiate capabilities of client and server
AUTHENTICATE = "AUTHENTICATE" // Used for SASL authentication between client and server. needs to have successfully negotiated the 'sasl' capability
PASS = "PASS" // Set a password to use when registering the client.
NICK = "NICK" // Set a nickname to the client
USER = "USER" // Register the client as a user
OPER = "OPER" // Used to obtain operator privileges, needs a <name> and <password> parameters.
QUIT = "QUIT" // Terminate client connection with an optional <reason>.
// Channel operations
JOIN = "JOIN" // Join a channel. can take multiple channels
PART = "PART" // Leave a channel. can take multiple channels with an optional reason.
TOPIC = "TOPIC" // Change the channel topic in a mode +t channel. may need operator privs.
NAMES = "NAMES" // View nicknames of all clients in the provided channel. can take multiple channel params separated by comma ","
LIST = "LIST" // Get list of channels with info about each channel.
INVITE = "INVITE" // Invite a client to an invite-only channel (mode +i)
KICK = "KICK" // Eject a client from the channel
// Server Queries and Commands
MOTD = "MOTD" // Message of the day command returns either the MOTD or ERR_NOMOTD
VERSION = "VERSION" // Query version of software and the RPL_ISUPPORT params of the server.
ADMIN = "ADMIN" // Find name of adminstrator of the server.
CONNECT = "CONNECT" // Force a server to try to establish a new connection to another server. IRC Operators only (do we need this ??)
TIME = "TIME" // Query local time of the server.
STATS = "STATS" // Query statistics of a server.
INFO = "INFO" // Get information about a server. e.g, software name/version, compile date of server, copyright. etc...
MODE = "MODE" // Set or remove modes from a target, either a user(client) target, or a channel target.
// Sending Messages
PRIVMSG = "PRIVMSG" // Sends a "private" message to either a channel or another client
NOTICE = "NOTICE" // Send notices between users and to channels.
// Operator Messages
KILL = "KILL" // CLose connection between a client and the server. IRC Operators only (do we need this ??)
// Optional Messages
AWAY = "AWAY" // Indicate that the client (user) is away/afk/etc..
USERHOST = "USERHOST" // Get information about user with a given nickname. can take up to 5 nickanmes.
// Left behind...
PING = "PING"
PONG = "PONG" // Respond to a server PING
)
/* --- Channel Types --- */
// These are the channel prefixes for different types of channels
const (
Regular = "#" // 0x23
Local = "&" // 0x26
)
/* --- User Modes --- */
const (
Invisible = "+i"
Oper = "+o"
Local_Oper = "+O"
Registered = "+r"
Wallops = "+w"
)
/* --- Channel Modes --- */
const (
Ban = "+b"
Exception = "+e"
Client_Limit = "+l"
Invite_Only = "+i"
Invite_Exception = "+I"
Key = "+k"
Moderated = "+m"
Secret = "+s"
Protected_Topic = "+t"
No_External_Messages = "+n"
)
/* --- Channel membership prefixes --- */
var UserPrefixes = map[string]bool{
"~": true,
"&": true,
"@": true,
"%": true,
"+": true,
}
/* --- Numeric Replies --- */
const (
RPL_WELCOME = "001" // RFC2812 - Implemented
RPL_YOURHOST = "002" // RFC2812 - Implemented
RPL_CREATED = "003" // RFC2812 - Implemented
RPL_MYINFO = "004" // RFC2812 - Implemented
RPL_ISUPPORT = "005" // ??????? - Not Implemented (TODO:)
RPL_BOUNCE = "010" // ??????? - Not Implemented (TODO:)
RPL_TRACELINK = "200" // RFC1459 - Not Implemented (TODO:)
RPL_TRACECONNECTING = "201" // RFC1459 - Not Implemented (TODO:)
RPL_TRACEHANDSHAKE = "202" // RFC1459 - Not Implemented (TODO:)
RPL_TRACEUNKNOWN = "203" // RFC1459 - Not Implemented (TODO:)
RPL_TRACEOPERATOR = "204" // RFC1459 - Not Implemented (TODO:)
RPL_TRACEUSER = "205" // RFC1459 - Not Implemented (TODO:)
RPL_TRACESERVER = "206" // RFC1459 - Not Implemented (TODO:)
RPL_TRACESERVICE = "207" // RFC2812 - Not Implemented (TODO:)
RPL_TRACENEWTYPE = "208" // RFC1459 - Not Implemented (TODO:)
RPL_TRACECLASS = "209" // RFC2812 - Not Implemented (TODO:)
RPL_TRACERECONNECT = "210" // RFC2812 - Not Implemented - Deprecated - Has Conflicts (TODO:)
RPL_STATSLINKINFO = "211" // RFC1459 - Not Implemented (TODO:)
RPL_STATSCOMMANDS = "212" // RFC1459 - Not Implemented (TODO:)
RPL_STATSCLINE = "213" // RFC1459 - Not Implemented (TODO:)
RPL_STATSNLINE = "214" // RFC1459 - Not Implemented - Has Conflicts (TODO:)
RPL_STATSILINE = "215" // RFC1459 - Not Implemented (TODO:)
RPL_STATSKLINE = "216" // RFC1459 - Not Implemented (TODO:)
RPL_STATSQLINE = "217" // RFC1459 - Not Implemented - Has Conflicts (TODO:)
RPL_STATSYLINE = "218" // RFC1459 - Not Implemented (TODO:)
RPL_ENDOFSTATS = "219" // RFC1459 - Not Implemented (TODO:)
RPL_UMODEIS = "221" // RFC1459 - Not Implemented (TODO:)
RPL_SERVICEINFO = "231" // RFC1459 - Not Implemented - Deprecated (TODO:)
RPL_ENDOFSERVICES = "232" // RFC1459 - Not Implemented - Deprecated (TODO:)
RPL_SERVICE = "233" // RFC1459 - Not Implemented - Deprecated (TODO:)
RPL_SERVLIST = "234" // RFC2812 - Not Implemented (TODO:) replaces 231 and 233
RPL_SERVLISTEND = "235" // RFC2812 - Not Implemented (TODO:) replaces 232
RPL_STATSVLINE = "240" // RFC2812 - Not Implemented - Has Conflicts (TODO:)
RPL_STATSLLINE = "241" // RFC1459 - Not Implemented (TODO:)
RPL_STATSUPTIME = "242" // RFC1459 - Not Implemented (TODO:)
RPL_STATSOLINE = "243" // RFC1459 - Not Implemented (TODO:)
RPL_STATSHLINE = "244" // RFC1459 - Not Implemented (TODO:)
RPL_STATSPING = "246" // RFC2812 - Not Implemented - Deprecated (TODO:)
RPL_STATSBLINE = "247" // RFC2812 - Not Implemented - Has Conflicts (TODO:)
RPL_STATSDLINE = "250" // RFC2812 - Not Implemented - Has Conflicts (TODO:)
RPL_LUSERCLIENT = "251" // RFC1459 - Implemented
RPL_LUSEROP = "252" // RFC1459 - Implemented
RPL_LUSERUNKNOWN = "253" // RFC1459 - Implemented
RPL_LUSERCHANNELS = "254" // RFC1459 - Implemented
RPL_LUSERME = "255" // RFC1459 - Implemented
RPL_ADMINME = "256" // RFC1459 - Not Implemented (TODO:)
RPL_ADMINLOC1 = "257" // RFC1459 - Not Implemented (TODO:)
RPL_ADMINLOC2 = "258" // RFC1459 - Not Implemented (TODO:)
RPL_ADMINEMAIL = "259" // RFC1459 - Not Implemented (TODO:)
RPL_TRACELOG = "261" // RFC1459 - Not Implemented (TODO:)
RPL_TRACEEND = "262" // RFC2812 - Not Implemented - Has Conflicts (TODO:)
RPL_TRYAGAIN = "263" // RFC2812 - Not Implemented (TODO:)
RPL_LOCALUSERS = "265" // aircd, Hybrid, Bahamut - Implemented
RPL_GLOBALUSERS = "266" // aircd, Hybird, Bahamut - Implemented
RPL_WHOISCERTFP = "276" // oftc-hybrid - Not Implemented - Has Conflicts (TODO:)
RPL_NONE = "300" // RFC1459 - Not Implemented (TODO:)
RPL_AWAY = "301" // RFC1459 - Not Implemented (TODO:)
RPL_USERHOST = "302" // RFC1459 - Not Implemented (TODO:)
RPL_ISON = "303" // RFC1459 - Not Implemented (TODO:)
RPL_TEXT = "304" // irc2 - Not Implemented (TODO:)
RPL_UNAWAY = "305" // RFC1459 - Not Implemented (TODO:)
RPL_NOWAWAY = "306" // RFC1459 - Not Implemented (TODO:)
RPL_WHOISUSER = "311" // RFC1459 - Not Implemented (TODO:)
RPL_WHOISSERVER = "312" // RFC1459 - Not Implemented (TODO:)
RPL_WHOISOPERATOR = "313" // RFC1459 - Not Implemented (TODO:)
RPL_WHOWASUSER = "314" // RFC1459 - Not Implemented (TODO:)
RPL_ENDOFWHO = "315" // RFC1459 - Not Implemented (TODO:)
RPL_WHOISIDLE = "317" // RFC1459 - Not Implemented (TODO:)
RPL_ENDOFWHOIS = "318" // RFC1459 - Not Implemented (TODO:)
RPL_WHOISCHANNELS = "319" // RFC1459 - Not Implemented (TODO:)
RPL_LISTSTART = "321" // RFC1459 - Not Implemented - Deprecated (TODO:)
RPL_LIST = "322" // RFC1459 - Not Implemented (TODO:)
RPL_LISTEND = "323" // RFC1459 - Not Implemented (TODO:)
RPL_CHANNELMODEIS = "324" // RFC1459 - Not Implemented (TODO:)
RPL_UNIQOPIS = "325" // RFC2812 - Not Implemented - Has Conflicts (TODO:)
RPL_CREATIONTIME = "329" // Bahamut,InspIRCd - Not Implemented (TODO:)
RPL_NOTOPIC = "331" // RFC1459 - Not Implemented (TODO:)
RPL_TOPIC = "332" // RFC1459 - Not Implemented (TODO:)
RPL_TOPICWHOTIME = "333" // ircu,InspIRCd - Not Implemented (TODO:)
RPL_INVITING = "341" // RFC1459 - Not Implemented (TODO:)
RPL_SUMMONING = "342" // RFC1459 - Not Implemented - Deprecated (TODO:)
RPL_INVITELIST = "346" // RFC2812 - Not Implemented (TODO:)
RPL_ENDOFINVITELIST = "347" // RFC2812 - Not Implemented (TODO:)
RPL_EXCEPTLIST = "348" // RFC2812 - Not Implemented (TODO:)
RPL_ENDOFEXCEPTLIST = "349" // RFC2812 - Not Implemented (TODO:)
RPL_WHOISGATEWAY = "350" // InspIRCd - Not Implemented (TODO:)
RPL_VERSION = "351" // RFC1459 - Not Implemented (TODO:)
RPL_WHOREPLY = "352" // RFC1459 - Not Implemented (TODO:)
RPL_NAMREPLY = "353" // RFC1459 - Implemented
RPL_KILLDONE = "361" // RFC1459 - Not Implemented - Deprecated (TODO:)
RPL_CLOSING = "362" // RFC1459 - Not Implemented - Deprecated (TODO:)
RPL_CLOSEEND = "363" // RFC1459 - Not Implemented - Deprecated (TODO:)
RPL_LINKS = "364" // RFC1459 - Not Implemented (TODO:)
RPL_ENDOFLINKS = "365" // RFC1459 - Not Implemented (TODO:)
RPL_ENDOFNAMES = "366" // RFC1459 - Not Implemented (TODO:)
RPL_BANLIST = "367" // RFC1459 - Not Implemented (TODO:)
RPL_ENDOFBANLIST = "368" // RFC1459 - Not Implemented (TODO:)
RPL_ENDOFWHOWAS = "369" // RFC1459 - Not Implemented (TODO:)
RPL_INFO = "371" // RFC1459 - Not Implemented (TODO:)
RPL_MOTD = "372" // RFC1459 - Implemented
RPL_INFOSTART = "373" // RFC1459 - Not Implemented - Deprecated (TODO:)
RPL_ENDOFINFO = "374" // RFC1459 - Not Implemented (TODO:)
RPL_MOTDSTART = "375" // RFC1459 - Implemented
RPL_ENDOFMOTD = "376" // RFC1459 - Not Implemented (TODO:)
RPL_YOUREOPER = "381" // RFC1459 - Not Implemented (TODO:)
RPL_REHASHING = "382" // RFC1459 - Not Implemented (TODO:)
RPL_YOURSERVICE = "383" // RFC2812 - Not Implemented (TODO:)
RPL_MYPORTIS = "384" // RFC1459 - Not Implemented - Deprecated (TODO:)
RPL_TIME = "391" // RFC1459 - Not Implemented - Has Conflicts (TODO:)
RPL_USERSSTART = "392" // RFC1459 - Not Implemented (TODO:)
RPL_USERS = "393" // RFC1459 - Not Implemented (TODO:)
RPL_ENDOFUSERS = "394" // RFC1459 - Not Implemented (TODO:)
RPL_NOUSERS = "395" // RFC1459 - Not Implemented (TODO:)
ERR_UNKNOWNERROR = "400" // ??????? - Not Implemented (TODO:)
ERR_NOSUCHNICK = "401" // RFC1459 - Not Implemented (TODO:)
ERR_NOSUCHSERVER = "402" // RFC1459 - Not Implemented (TODO:)
ERR_NOSUCHCHANNEL = "403" // RFC1459 - Not Implemented (TODO:)
ERR_CANNOTSENDTOCHAN = "404" // RFC1459 - Not Implemented (TODO:)
ERR_TOOMANYCHANNELS = "405" // RFC1459 - Not Implemented (TODO:)
ERR_WASNOSUCHNICK = "406" // RFC1459 - Not Implemented (TODO:)
ERR_NOSUCHSERVICE = "408" // RFC2812 - Not Implemented - Has Conflicts (TODO:)
ERR_NOORIGIN = "409" // RFC1459 - Not Implemented (TODO:)
ERR_NORECIPIENT = "411" // RFC1459 - Not Implemented (TODO:)
ERR_NOTEXTTOSEND = "412" // RFC1459 - Not Implemented (TODO:)
ERR_NOTOPLEVEL = "413" // RFC1459 - Not Implemented (TODO:)
ERR_WILDTOPLEVEL = "414" // RFC1459 - Not Implemented (TODO:)
ERR_BADMASK = "415" // RFC2812 - Not Implemented (TODO:)
ERR_UNKNOWNCOMMAND = "421" // RFC1459 - Not Implemented (TODO:)
ERR_NOMOTD = "422" // RFC1459 - Not Implemented (TODO:)
ERR_NOADMININFO = "423" // RFC1459 - Not Implemented (TODO:)
ERR_FILEERROR = "424" // RFC1459 - Not Implemented (TODO:)
ERR_NONICKNAMEGIVEN = "431" // RFC1459 - Not Implemented (TODO:)
ERR_ERRONEUSNICKNAME = "432" // RFC1459 - Not Implemented (TODO:)
ERR_NICKNAMEINUSE = "433" // RFC1459 - Not Implemented (TODO:)
ERR_NICKCOLLISION = "436" // RFC1459 - Not Implemented (TODO:)
ERR_UNAVAILRESOURCE = "437" // RFC2812 - Not Implemented - Has Conflicts (TODO:)
ERR_USERNOTINCHANNEL = "441" // RFC1459 - Not Implemented (TODO:)
ERR_NOTONCHANNEL = "442" // RFC1459 - Not Implemented (TODO:)
ERR_USERONCHANNEL = "443" // RFC1459 - Not Implemented (TODO:)
ERR_NOLOGIN = "444" // RFC1459 - Not Implemented (TODO:)
ERR_SUMMONDISABLED = "445" // RFC1459 - Not Implemented (TODO:)
ERR_USERSDISABLED = "446" // RFC1459 - Not Implemented (TODO:)
ERR_NOTREGISTERED = "451" // RFC1459 - Not Implemented (TODO:)
ERR_NEEDMOREPARAMS = "461" // RFC1459 - Not Implemented (TODO:)
ERR_ALREADYREGISTERED = "462" // RFC1459 - Not Implemented (TODO:)
ERR_NOPERMFORHOST = "463" // RFC1459 - Not Implemented (TODO:)
ERR_PASSWDMISMATCH = "464" // RFC1459 - Not Implemented (TODO:)
ERR_YOUREBANNEDCREEP = "465" // RFC1459 - Not Implemented (TODO:)
ERR_YOUWILLBEBANNED = "466" // RFC1459 - Not Implemented - Deprecated (TODO:)
ERR_KEYSET = "467" // RFC1459 - Not Implemented (TODO:)
ERR_CHANNELISFULL = "471" // RFC1459 - Not Implemented (TODO:)
ERR_UNKNOWNMODE = "472" // RFC1459 - Not Implemented (TODO:)
ERR_INVITEONLYCHAN = "473" // RFC1459 - Not Implemented (TODO:)
ERR_BANNEDFROMCHAN = "474" // RFC1459 - Not Implemented (TODO:)
ERR_BADCHANNELKEY = "475" // RFC1459 - Not Implemented (TODO:)
ERR_BADCHANMASK = "476" // RFC2812 - Not Implemented (TODO:)
ERR_BANLISTFULL = "478" // RFC2812 - Not Implemented (TODO:)
ERR_NOPRIVILEGES = "481" // RFC1459 - Not Implemented (TODO:)
ERR_CHANOPRIVSNEEDED = "482" // RFC1459 - Not Implemented (TODO:)
ERR_CANTKILLSERVER = "483" // RFC1459 - Not Implemented (TODO:)
ERR_RESTRICTED = "484" // RFC2812 - Not Implemented - Has Conflicts (TODO:)
ERR_UNIQOPRIVSNEEDED = "485" // RFC2812 - Not Implemented (TODO:)
ERR_NOOPERHOST = "491" // RFC1459 - Not Implemented (TODO:)
ERR_NOSERVICEHOST = "492" // RFC1459 - Not Implemented - Deprecated - Has Conflicts (TODO:)
ERR_UMODEUNKNOWNFLAG = "501" // RFC1459 - Not Implemented - Has Conflicts (TODO:)
ERR_USERSDONTMATCH = "502" // RFC1459 - Not Implemented (TODO:)
RPL_STARTTLS = "670" // IRCv3 - Not Implemented (TODO:)
ERR_STARTTLS = "691" // IRCv3 - Not Implemented (TODO:)
ERR_NOPRIVS = "723" // RatBox - Not Implemented (TODO:)
RPL_WHOISKEYVALUE = "760" // IRCv3 - Not Implemented (TODO:)
RPL_KEYVALUE = "761" // IRCv3 - Not Implemented (TODO:)
RPL_METADATAEND = "762" // IRCv3 - Not Implemented (TODO:)
ERR_METADATALIMIT = "764" // IRCv3 - Not Implemented (TODO:)
ERR_TARGETINVALID = "765" // IRCv3 - Not Implemented (TODO:)
ERR_NOMATCHINGKEY = "766" // IRCv3 - Not Implemented (TODO:)
ERR_KEYINVALID = "767" // IRCv3 - Not Implemented (TODO:)
ERR_KEYNOTSET = "768" // IRCv3 - Not Implemented (TODO:)
ERR_KEYNOPERMISSION = "769" // IRCv3 - Not Implemented (TODO:)
RPL_LOGGEDIN = "900" // Charybdis/Atheme,IRCv3 - Not Implemented (TODO:)
RPL_LOGGEDOUT = "901" // Charybdis/Atheme,IRCv3 - Not Implemented (TODO:)
ERR_NICKLOCKED = "902" // Charybdis/Atheme,IRCv3 - Not Implemented (TODO:)
RPL_SASLSUCCESS = "903" // Charybdis/Atheme,IRCv3 - Not Implemented (TODO:)
ERR_SASLFAIL = "904" // Charybdis/Atheme,IRCv3 - Not Implemented (TODO:)
ERR_SASLTOOLONG = "905" // Charybdis/Atheme,IRCv3 - Not Implemented (TODO:)
ERR_SASLABORTED = "906" // Charybdis/Atheme,IRCv3 - Not Implemented (TODO:)
ERR_SASLALREADY = "907" // Charybdis/Atheme,IRCv3 - Not Implemented (TODO:)
RPL_SASLMECHS = "908" // Charybdis/Atheme,IRCv3 - Not Implemented (TODO:)
)
/* --- RPL_ISUPPORT Parameters --- */
var Features = map[string]bool{
"ACCEPT": true,
"AWAYLEN": true,
"BOT": true,
"CALLERID": true,
"CASEMAPPING": true,
"CHANLIMIT": true,
"CHANMODES": true,
"CHANNELLEN": true,
"CHANTYPES": true,
"CHARSET": true, // Deprecated but might still be used
"CLIENTVER": true, // Deprecated but might still be used
"CNOTICE": true,
"CPRIVMSG": true,
"DEAF": true,
"ELIST": true,
"ESILENCE": true,
"ETRACE": true,
"EXCEPTS": true,
"EXTBAN": true,
"FNC": true, // Deprecated but might still be used
"INVEX": true,
"KEYLEN": true,
"KICKLEN": true,
"KNOCK": true,
"LINELEN": true, // Proposed
"MAP": true, // Deprecated but might still be used
"MAXBANS": true, // Deprecated but might still be used
"MAXCHANNELS": true, // Deprecated but might still be used
"MAXLIST": true,
"MAXNICKLEN": true,
"MAXPARA": true, // Deprecated but might still be used
"MAXTARGETS": true,
"METADATA": true,
"MODES": true,
"MONITOR": true,
"NAMESX": true, // Deprecated but might still be used
"NETWORK": true,
"NICKLEN": true,
"OVERRIDE": true,
"PREFIX": true,
"SAFELIST": true,
"SECURELIST": true,
"SILENCE": true,
"SSL": true, // Deprecated but might still be used
"STARTTLS": true, // Deprecated but might still be used
"STATUSMSG": true,
"STD": true, // Deprecated but might still be used
"TARGMAX": true,
"TOPICLEN": true,
"UHNAMES": true, // Deprecated but might still be used
"USERIP": true,
"USERLEN": true, // Proposed
"VBANLIST": true, // Deprecared but might still be used
"VLIST": true,
"WALLCHOPS": true, // Deprecated but might still be used
"WALLVOICES": true, // Deprecated but might still be used
"WATCH": true,
"WHOX": true,
}
// Messages MUST end in CRLF and SHOULD be encoded and decoded using UTF-8 (with fallbacks such as Latin-1).
// Names are casemapped, read the casemapping from the RPL_ISUPPORT the server sends when registration is completed.
// When receiving a numeric reply, client MUST be able to handle any number of parameters on a numeric reply.
+297
View File
@@ -0,0 +1,297 @@
// gorc project
// Copyright (C) 2021 IllusionMan1212
//
// This program is free software; you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation; either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License along
// with this program; if not, see https://www.gnu.org/licenses.
package handler
import (
"bufio"
"fmt"
"io"
"log"
"strings"
"github.com/illusionman1212/gorc/irc"
"github.com/illusionman1212/gorc/irc/commands"
"github.com/illusionman1212/gorc/irc/parser"
"github.com/illusionman1212/gorc/ui/mainscreen"
)
func ReadLoop(client *irc.Client) {
// 512 bytes as a base + 8192 additional bytes for tags
r := bufio.NewReaderSize(client.TcpConn, 8192+512)
for {
msg, err := r.ReadString('\n')
if err != nil {
if err != io.EOF {
log.Println(err)
}
client.TcpConn.Close()
return
}
msg = strings.Replace(msg, "\r\n", "", 1)
if ircMessage, valid := parser.ParseIRCMessage(msg); valid {
HandleCommand(ircMessage, client)
}
}
}
func handlePing(msg parser.IRCMessage, client *irc.Client) {
token := msg.Parameters[0]
client.SendCommand(commands.PONG, token)
}
func handlePrivMsg(msg parser.IRCMessage, client *irc.Client) {
nick := strings.SplitN(msg.Source, "!", 2)[0]
channel := msg.Parameters[0]
msgContent := msg.Parameters[1]
privMsg := fmt.Sprintf("%s: %s", nick, msgContent)
for i, c := range client.Channels {
if c.Name == channel {
client.Channels[i].History += privMsg + irc.CRLF
}
}
}
func handleJoin(msg parser.IRCMessage, client *irc.Client) {
nick := strings.SplitN(msg.Source, "!", 2)[0]
channel := msg.Parameters[0]
joinMsg := fmt.Sprintf("== %s has joined", nick)
for i, c := range client.Channels {
if c.Name == channel {
client.Channels[i].History += joinMsg + irc.CRLF
if _, exists := client.Channels[i].Users[nick]; !exists {
client.Channels[i].Users[nick] = irc.User{}
}
}
}
if channel == client.ActiveChannel {
client.Tea.Send(mainscreen.SwitchChannels())
}
}
func handleQuit(msg parser.IRCMessage, client *irc.Client) {
nick := strings.SplitN(msg.Source, "!", 2)[0]
reason := msg.Parameters[0]
quitMsg := fmt.Sprintf("== %s has quit (%s)", nick, reason)
for i := range client.Channels {
// skip the server "channel"
if i == 0 {
continue
}
client.Channels[i].History += quitMsg + irc.CRLF
delete(client.Channels[i].Users, nick)
}
client.Tea.Send(mainscreen.SwitchChannels())
}
func handlePart(msg parser.IRCMessage, client *irc.Client) {
nick := strings.SplitN(msg.Source, "!", 2)[0]
channel := msg.Parameters[0]
reason := ""
if len(msg.Parameters) > 1 {
reason = msg.Parameters[1]
}
partMsg := fmt.Sprintf("== %s has left %s (%s)", nick, channel, reason)
for i, c := range client.Channels {
if c.Name == channel {
client.Channels[i].History += partMsg + irc.CRLF
delete(client.Channels[i].Users, nick)
}
}
client.Tea.Send(mainscreen.SwitchChannels())
}
func handleWELCOME(msg parser.IRCMessage, client *irc.Client) {
welcomeMsg := msg.Parameters[1]
client.Channels[0].History += welcomeMsg + irc.CRLF
}
func handleYOURHOST(msg parser.IRCMessage, client *irc.Client) {
host := msg.Parameters[1]
client.Channels[0].History += host + irc.CRLF
}
func handleCREATED(msg parser.IRCMessage, client *irc.Client) {
created := msg.Parameters[1]
client.Channels[0].History += created + irc.CRLF
}
func handleMYINFO(msg parser.IRCMessage, client *irc.Client) {
info := strings.Join(msg.Parameters[1:], " ")
client.Channels[0].History += info + irc.CRLF
}
func handleLUSERCLIENT(msg parser.IRCMessage, client *irc.Client) {
message := msg.Parameters[1]
client.Channels[0].History += message + irc.CRLF
}
func handleLUSEROP(msg parser.IRCMessage, client *irc.Client) {
message := strings.Join(msg.Parameters[1:], " ")
client.Channels[0].History += message + irc.CRLF
}
func handleLUSERUNKNOWN(msg parser.IRCMessage, client *irc.Client) {
message := strings.Join(msg.Parameters[1:], " ")
client.Channels[0].History += message + irc.CRLF
}
func handleLUSERCHANNELS(msg parser.IRCMessage, client *irc.Client) {
message := strings.Join(msg.Parameters[1:], " ")
client.Channels[0].History += message + irc.CRLF
}
func handleLUSERME(msg parser.IRCMessage, client *irc.Client) {
message := msg.Parameters[1]
client.Channels[0].History += message + irc.CRLF
}
func handleLOCALUSERS(msg parser.IRCMessage, client *irc.Client) {
message := msg.Parameters[1]
client.Channels[0].History += message + irc.CRLF
}
func handleGLOBALUSERS(msg parser.IRCMessage, client *irc.Client) {
message := msg.Parameters[1]
client.Channels[0].History += message + irc.CRLF
}
func handleNAMREPLY(msg parser.IRCMessage, client *irc.Client) {
// TODO: do i need these
// client := msg.Parameters[0]
// chanSymbol := msg.Parameters[1]
channel := msg.Parameters[2]
nicks := strings.Split(msg.Parameters[3], " ")
for i, c := range client.Channels {
if c.Name == channel {
for _, nick := range nicks {
prefix := ""
_nick := nick
if commands.UserPrefixes[string(nick[0])] {
prefix = string(nick[0])
_nick = nick[1:]
}
client.Channels[i].Users[_nick] = irc.User{
Prefix: prefix,
}
}
}
}
if channel == client.ActiveChannel {
client.Tea.Send(mainscreen.SwitchChannels())
}
}
func handleMOTDStart(msg parser.IRCMessage, client *irc.Client) {
params := strings.Join(msg.Parameters[1:], " ")
client.Channels[0].History += params + irc.CRLF
}
func handleMOTD(msg parser.IRCMessage, client *irc.Client) {
messageLine := msg.Parameters[1]
client.Channels[0].History += messageLine + irc.CRLF
}
func HandleCommand(msg parser.IRCMessage, client *irc.Client) {
// TODO: handle different commands
switch msg.Command {
case commands.PING:
handlePing(msg, client)
case commands.PRIVMSG:
handlePrivMsg(msg, client)
case commands.JOIN:
handleJoin(msg, client)
case commands.QUIT:
handleQuit(msg, client)
case commands.PART:
handlePart(msg, client)
case commands.RPL_WELCOME:
handleWELCOME(msg, client)
case commands.RPL_YOURHOST:
handleYOURHOST(msg, client)
case commands.RPL_CREATED:
handleCREATED(msg, client)
case commands.RPL_MYINFO:
handleMYINFO(msg, client)
case commands.RPL_LUSERCLIENT:
handleLUSERCLIENT(msg, client)
case commands.RPL_LUSEROP:
handleLUSEROP(msg, client)
case commands.RPL_LUSERUNKNOWN:
handleLUSERUNKNOWN(msg, client)
case commands.RPL_LUSERCHANNELS:
handleLUSERCHANNELS(msg, client)
case commands.RPL_LUSERME:
handleLUSERME(msg, client)
case commands.RPL_LOCALUSERS:
handleLOCALUSERS(msg, client)
case commands.RPL_GLOBALUSERS:
handleGLOBALUSERS(msg, client)
case commands.RPL_NAMREPLY:
handleNAMREPLY(msg, client)
case commands.RPL_ENDOFNAMES:
// TODO: toggle a flag on the channel to indicate
// it received all the names correctly.
// start a timeout and update said timeout on every RPL_NAMREPLY
// and log an error if timeout ends without receiving this command.
case commands.RPL_MOTDSTART:
handleMOTDStart(msg, client)
case commands.RPL_MOTD:
handleMOTD(msg, client)
case commands.RPL_ENDOFMOTD:
// TODO: toggle a flag on the client/server to indicate
// it received all the MOTD correctly
// start a timeout and update said timeout on every RPL_MOTD
// and log an error if timeout ends without receiving this command.
default:
fullMsg := fmt.Sprintf("%s %s %s %s", msg.Tags, msg.Source, msg.Command, strings.Join(msg.Parameters, " "))
client.Channels[0].History += fullMsg + irc.CRLF
}
// send a receivedIRCmsg tea message so the ui can update
// we also use this tea message to scroll the viewport down
client.Tea.Send(mainscreen.ReceivedIRCMsg())
}
+107
View File
@@ -0,0 +1,107 @@
// gorc project
// Copyright (C) 2021 IllusionMan1212
//
// This program is free software; you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation; either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License along
// with this program; if not, see https://www.gnu.org/licenses.
package parser
import (
"regexp"
"strings"
)
type IRCMessage struct {
Tags Tags // starts with @ | Optional
Source string // starts with : | Optional
Command string // can either be a string or a numeric value | Required
Parameters []string // Optional (Dependant on command)
}
type Tags map[string]string
func parseTags(rawTags string) Tags {
tags := make(Tags, 0)
rawTagsSlice := strings.Split(rawTags, ";")
for _, tag := range rawTagsSlice {
str := strings.Split(tag, "=")
key := str[0]
value := ""
// we need to make sure a "value" exists before accessing str[1]
if len(str) > 1 {
value = str[1]
}
tags[key] = value
}
return tags
}
func ParseIRCMessage(line string) (IRCMessage, bool) {
ircMessage := IRCMessage{}
multipleSpacesRegex := regexp.MustCompile("[^\\S\\t]+")
trailingParamRegex := regexp.MustCompile("[^\\S\\t]+:")
whitespaceRegex := regexp.MustCompile("^[^\\S\\t]+$")
if len(line) <= 0 {
// TODO: do something here ???
// log.Println("empty message")
return IRCMessage{}, false
}
if line[0] == '@' {
substrs := multipleSpacesRegex.Split(line, 2)
tags := parseTags(substrs[0][1:])
ircMessage.Tags = tags
line = substrs[1]
}
if line[0] == ':' {
substrs := multipleSpacesRegex.Split(line, 2)
ircMessage.Source = substrs[0][1:]
line = substrs[1]
}
substrs := multipleSpacesRegex.Split(line, 2)
// TODO: return error(??) if the irc message is malformed (i.e doesn't contain command)
ircMessage.Command = substrs[0]
// if parameters exist and it's not an empty string and it's not whitespace without ":"
if len(substrs) > 1 && len(substrs[1]) != 0 && !whitespaceRegex.MatchString(substrs[1]) {
params := trailingParamRegex.Split(substrs[1], 2)
parameters := multipleSpacesRegex.Split(params[0], -1)
// if there's a trailing parameter
// this parses empty params as well
if len(params) > 1 {
parameters = append(parameters, params[1])
}
// if we have 1 param and it starts with a colon
if len(params) == 1 && params[0][0] == ':' {
parameters[0] = params[0][1:]
}
ircMessage.Parameters = parameters
}
return ircMessage, true
}
+946
View File
@@ -0,0 +1,946 @@
// gorc project
// Copyright (C) 2021 IllusionMan1212
//
// This program is free software; you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation; either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License along
// with this program; if not, see https://www.gnu.org/licenses.
package parser
import "testing"
// tests provided by https://github.com/ircdocs/parser-tests/blob/master/tests/msg-split.yaml
func TestParser(t *testing.T) {
t.Run("Test simple command", func(t *testing.T) {
testMessage := "foo bar baz asdf"
ircMessage, value := ParseIRCMessage(testMessage)
if !value {
t.Fatal("Invalid irc message")
}
if ircMessage.Command != "foo" {
t.Fatal("Parsing message with only command and params")
}
if ircMessage.Parameters[0] != "bar" {
t.Fatal("Parsing message with only command and params (1st param)")
}
if ircMessage.Parameters[1] != "baz" {
t.Fatal("Parsing message with only command and params (2nd param)")
}
if ircMessage.Parameters[2] != "asdf" {
t.Fatal("Parsing message with only command and params (3rd param)")
}
})
t.Run("Test source", func(t *testing.T) {
testMessage := ":coolguy foo bar baz asdf"
ircMessage, value := ParseIRCMessage(testMessage)
if !value {
t.Fatal("Invalid irc message")
}
if ircMessage.Source != "coolguy" {
t.Fatal("Parsing regular source")
}
if ircMessage.Command != "foo" {
t.Fatal("Parsing command with source")
}
if ircMessage.Parameters[0] != "bar" {
t.Fatal("Parsing 1st param of regular message")
}
if ircMessage.Parameters[1] != "baz" {
t.Fatal("Parsing 2nd param of regular message")
}
if ircMessage.Parameters[2] != "asdf" {
t.Fatal("Parsing 3rd param of regular message")
}
})
t.Run("Test trailing param", func(t *testing.T) {
testMessage := "foo bar baz :asdf quux"
ircMessage, value := ParseIRCMessage(testMessage)
if !value {
t.Fatal("Invalid irc message")
}
if ircMessage.Parameters[0] != "bar" {
t.Fatal("Parsing 1st param in message with trailing param")
}
if ircMessage.Parameters[1] != "baz" {
t.Fatal("Parsing 2nd param in message with trailing param")
}
if ircMessage.Parameters[2] != "asdf quux" {
t.Fatal("Parsing trailing param")
}
})
t.Run("Test empty trailing param", func(t *testing.T) {
testMessage := "foo bar baz :"
ircMessage, value := ParseIRCMessage(testMessage)
if !value {
t.Fatal("Invalid irc message")
}
if ircMessage.Parameters[0] != "bar" {
t.Fatal("Parsing 1st param in message with trailing param")
}
if ircMessage.Parameters[1] != "baz" {
t.Fatal("Parsing 2nd param in message with trailing param")
}
if ircMessage.Parameters[2] != "" {
t.Fatal("Parsing trailing param")
}
})
t.Run("Test multiple colons at trailing param", func(t *testing.T) {
testMessage := "foo bar baz ::asdf"
ircMessage, value := ParseIRCMessage(testMessage)
if !value {
t.Fatal("Invalid irc message")
}
if ircMessage.Parameters[0] != "bar" {
t.Fatal("Parsing 1st param in message with trailing param")
}
if ircMessage.Parameters[1] != "baz" {
t.Fatal("Parsing 2nd param in message with trailing param")
}
if ircMessage.Parameters[2] != ":asdf" {
t.Fatal("Parsing trailing param")
}
})
t.Run("Test source and trailing param", func(t *testing.T) {
testMessage := ":coolguy foo bar baz :asdf quux"
ircMessage, value := ParseIRCMessage(testMessage)
if !value {
t.Fatal("Invalid irc message")
}
if ircMessage.Source != "coolguy" {
t.Fatal("Parsing source")
}
if ircMessage.Command != "foo" {
t.Fatal("Parsing command")
}
if ircMessage.Parameters[0] != "bar" {
t.Fatal("Parsing 1st param")
}
if ircMessage.Parameters[1] != "baz" {
t.Fatal("Parsing 2nd param")
}
if ircMessage.Parameters[2] != "asdf quux" {
t.Fatal("Parsing trailing param")
}
})
t.Run("Test source and trailing param with spaces", func(t *testing.T) {
testMessage := ":coolguy foo bar baz : asdf quux "
ircMessage, value := ParseIRCMessage(testMessage)
if !value {
t.Fatal("Invalid irc message")
}
if ircMessage.Source != "coolguy" {
t.Fatal("Parsing source")
}
if ircMessage.Command != "foo" {
t.Fatal("Parsing command")
}
if ircMessage.Parameters[0] != "bar" {
t.Fatal("Parsing 1st param")
}
if ircMessage.Parameters[1] != "baz" {
t.Fatal("Parsing 2nd param")
}
if ircMessage.Parameters[2] != " asdf quux " {
t.Fatal("Parsing trailing param")
}
})
t.Run("Test source and trailing param 2", func(t *testing.T) {
testMessage := ":coolguy PRIVMSG bar :lol :) "
ircMessage, value := ParseIRCMessage(testMessage)
if !value {
t.Fatal("Invalid irc message")
}
if ircMessage.Source != "coolguy" {
t.Fatal("Parsing source")
}
if ircMessage.Command != "PRIVMSG" {
t.Fatal("Parsing command")
}
if ircMessage.Parameters[0] != "bar" {
t.Fatal("Parsing 1st param")
}
if ircMessage.Parameters[1] != "lol :) " {
t.Fatal("Parsing trailing param")
}
})
t.Run("Test source and empty trailing param", func(t *testing.T) {
testMessage := ":coolguy foo bar baz :"
ircMessage, value := ParseIRCMessage(testMessage)
if !value {
t.Fatal("Invalid irc message")
}
if ircMessage.Source != "coolguy" {
t.Fatal("Parsing source")
}
if ircMessage.Command != "foo" {
t.Fatal("Parsing command")
}
if ircMessage.Parameters[0] != "bar" {
t.Fatal("Parsing 1st param")
}
if ircMessage.Parameters[1] != "baz" {
t.Fatal("Parsing 2nd param")
}
if ircMessage.Parameters[2] != "" {
t.Fatal("Parsing trailing param")
}
})
t.Run("Test source and trailing param with spaces 2", func(t *testing.T) {
testMessage := ":coolguy foo bar baz : "
ircMessage, value := ParseIRCMessage(testMessage)
if !value {
t.Fatal("Invalid irc message")
}
if ircMessage.Source != "coolguy" {
t.Fatal("Parsing source")
}
if ircMessage.Command != "foo" {
t.Fatal("Parsing command")
}
if ircMessage.Parameters[0] != "bar" {
t.Fatal("Parsing 1st param")
}
if ircMessage.Parameters[1] != "baz" {
t.Fatal("Parsing 2nd param")
}
if ircMessage.Parameters[2] != " " {
t.Fatal("Parsing trailing param")
}
})
t.Run("Test tags", func(t *testing.T) {
testMessage := "@id=234AB;rose :dan!d@localhost PRIVMSG #chan :Hey what's up!"
ircMessage, value := ParseIRCMessage(testMessage)
if !value {
t.Fatal("Invalid irc message")
}
if ircMessage.Tags["id"] != "234AB" {
t.Fatal("Parsing regular tag")
}
if ircMessage.Tags["rose"] != "" {
t.Fatal("Parsing value-less tag")
}
if ircMessage.Source != "dan!d@localhost" {
t.Fatal("Parsing source")
}
if ircMessage.Command != "PRIVMSG" {
t.Fatal("Parsing command")
}
if ircMessage.Parameters[0] != "#chan" {
t.Fatal("Parsing 1st param")
}
if ircMessage.Parameters[1] != "Hey what's up!" {
t.Fatal("Parsing 2nd param")
}
})
t.Run("Test tags 2", func(t *testing.T) {
testMessage := "@a=b;c=32;k;rt=q17 foo"
ircMessage, value := ParseIRCMessage(testMessage)
if !value {
t.Fatal("Invalid irc message")
}
if ircMessage.Tags["a"] != "b" {
t.Fatal("Parsing regular tag")
}
if ircMessage.Tags["c"] != "32" {
t.Fatal("Parsing regular tag")
}
if ircMessage.Tags["k"] != "" {
t.Fatal("Parsing value-less tag")
}
if ircMessage.Tags["rt"] != "q17" {
t.Fatal("Parsing regular tag")
}
if ircMessage.Command != "foo" {
t.Fatal("Parsing command")
}
})
// TODO: fails
t.Run("Test escaped tags", func(t *testing.T) {
testMessage := "@a=b\\\\and\\nk;c=72\\s45;d=gh\\:764 foo"
ircMessage, value := ParseIRCMessage(testMessage)
if !value {
t.Fatal("Invalid irc message")
}
if ircMessage.Tags["a"] != "b\\and\nk" {
t.Fatal("Parsing escaped tag")
}
if ircMessage.Tags["c"] != "72 45" {
t.Fatal("Parsing regular tag")
}
if ircMessage.Tags["d"] != "gh;764" {
t.Fatal("Parsing escaped tag")
}
if ircMessage.Command != "foo" {
t.Fatal("Parsing command")
}
})
t.Run("Test tags with source", func(t *testing.T) {
testMessage := "@c;h=;a=b :quux ab cd"
ircMessage, value := ParseIRCMessage(testMessage)
if !value {
t.Fatal("Invalid irc message")
}
if ircMessage.Tags["c"] != "" {
t.Log(ircMessage.Tags)
t.Fatal("Parsing value-less tag")
}
if ircMessage.Tags["h"] != "" {
t.Fatal("Parsing value-less tag")
}
if ircMessage.Tags["a"] != "b" {
t.Fatal("Parsing regular tag")
}
if ircMessage.Source != "quux" {
t.Fatal("Parsing source")
}
if ircMessage.Command != "ab" {
t.Fatal("Parsing command")
}
if ircMessage.Parameters[0] != "cd" {
t.Fatal("Parsing param")
}
})
// TODO: FAIL
t.Run("Test single param with preceeding :", func(t *testing.T) {
testMessage := ":src JOIN :#chan"
ircMessage, value := ParseIRCMessage(testMessage)
if !value {
t.Fatal("Invalid irc message")
}
if ircMessage.Parameters[0] != "#chan" {
t.Fatal("Parsing a single param with a preceeding \":\"")
}
})
t.Run("Test with no space as last param", func(t *testing.T) {
testMessage := ":src AWAY"
ircMessage, value := ParseIRCMessage(testMessage)
if !value {
t.Fatal("Invalid irc message")
}
if ircMessage.Source != "src" {
t.Fatal("Parsing source")
}
if ircMessage.Command != "AWAY" {
t.Fatal("Parsing command")
}
if len(ircMessage.Parameters) > 0 {
t.Fatal("Parsing params")
}
})
t.Run("Test with space as last param", func(t *testing.T) {
testMessage := ":src AWAY "
ircMessage, value := ParseIRCMessage(testMessage)
if !value {
t.Fatal("Invalid irc message")
}
if ircMessage.Source != "src" {
t.Fatal("Parsing source")
}
if ircMessage.Command != "AWAY" {
t.Fatal("Parsing command")
}
if len(ircMessage.Parameters) > 0 {
t.Logf("'%v'", ircMessage.Parameters[0])
t.Fatal("Parsing params")
}
})
t.Run("Test tab as space", func(t *testing.T) {
testMessage := ":cool\tguy foo bar baz"
ircMessage, value := ParseIRCMessage(testMessage)
if !value {
t.Fatal("Invalid irc message")
}
if ircMessage.Source != "cool\tguy" {
t.Fatal("Parsing source")
}
if ircMessage.Command != "foo" {
t.Fatal("Parsing command")
}
if ircMessage.Parameters[0] != "bar" {
t.Fatal("Parsing 1st param")
}
if ircMessage.Parameters[1] != "baz" {
t.Fatal("Parsing 2nd param")
}
})
t.Run("Test weird control codes in source", func(t *testing.T) {
testMessage := ":coolguy!ag@net\x035w\x03ork.admin PRIVMSG foo :bar baz"
ircMessage, value := ParseIRCMessage(testMessage)
if !value {
t.Fatal("Invalid irc message")
}
if ircMessage.Source != "coolguy!ag@net\x035w\x03ork.admin" {
t.Fatal("Parsing source")
}
if ircMessage.Command != "PRIVMSG" {
t.Fatal("Parsing command")
}
if ircMessage.Parameters[0] != "foo" {
t.Fatal("Parsing 1st param")
}
if ircMessage.Parameters[1] != "bar baz" {
t.Fatal("Parsing trailing param")
}
})
t.Run("Test more weird control codes", func(t *testing.T) {
testMessage := ":coolguy!~ag@n\x02et\x0305w\x0fork.admin PRIVMSG foo :bar baz"
ircMessage, value := ParseIRCMessage(testMessage)
if !value {
t.Fatal("Invalid irc message")
}
if ircMessage.Source != "coolguy!~ag@n\x02et\x0305w\x0fork.admin" {
t.Fatal("Parsing source")
}
if ircMessage.Command != "PRIVMSG" {
t.Fatal("Parsing command")
}
if ircMessage.Parameters[0] != "foo" {
t.Fatal("Parsing 1st param")
}
if ircMessage.Parameters[1] != "bar baz" {
t.Fatal("Parsing trailing param")
}
})
t.Run("Test full message", func(t *testing.T) {
testMessage := "@tag1=value1;tag2;vendor1/tag3=value2;vendor2/tag4= :irc.example.com COMMAND param1 param2 :param3 param3"
ircMessage, value := ParseIRCMessage(testMessage)
if !value {
t.Fatal("Invalid irc message")
}
if ircMessage.Tags["tag1"] != "value1" {
t.Fatal("Parsing regular tag")
}
if ircMessage.Tags["tag2"] != "" {
t.Fatal("Parsing value-less tag")
}
if ircMessage.Tags["vendor1/tag3"] != "value2" {
t.Fatal("Parsing regular tag")
}
if ircMessage.Tags["vendor2/tag4"] != "" {
t.Fatal("Parsing value-less tag")
}
if ircMessage.Source != "irc.example.com" {
t.Fatal("Parsing source")
}
if ircMessage.Command != "COMMAND" {
t.Fatal("Parsing command")
}
if ircMessage.Parameters[0] != "param1" {
t.Fatal("Parsing 1st param")
}
if ircMessage.Parameters[1] != "param2" {
t.Fatal("Parsing 2nd param")
}
if ircMessage.Parameters[2] != "param3 param3" {
t.Fatal("Parsing trailing param")
}
})
t.Run("Test message without tags", func(t *testing.T) {
testMessage := ":irc.example.com COMMAND param1 param2 :param3 param3"
ircMessage, value := ParseIRCMessage(testMessage)
if !value {
t.Fatal("Invalid irc message")
}
if ircMessage.Source != "irc.example.com" {
t.Fatal("Parsing source")
}
if ircMessage.Command != "COMMAND" {
t.Fatal("Parsing command")
}
if ircMessage.Parameters[0] != "param1" {
t.Fatal("Parsing 1st param")
}
if ircMessage.Parameters[1] != "param2" {
t.Fatal("Parsing 2nd param")
}
if ircMessage.Parameters[2] != "param3 param3" {
t.Fatal("Parsing trailing param")
}
})
t.Run("Test message without source", func(t *testing.T) {
testMessage := "@tag1=value1;tag2;vendor1/tag3=value2;vendor2/tag4= COMMAND param1 param2 :param3 param3"
ircMessage, value := ParseIRCMessage(testMessage)
if !value {
t.Fatal("Invalid irc message")
}
if ircMessage.Tags["tag1"] != "value1" {
t.Fatal("Parsing regular tag")
}
if ircMessage.Tags["tag2"] != "" {
t.Fatal("Parsing value-less tag")
}
if ircMessage.Tags["vendor1/tag3"] != "value2" {
t.Fatal("Parsing regular tag")
}
if ircMessage.Tags["vendor2/tag4"] != "" {
t.Fatal("Parsing value-less tag")
}
if ircMessage.Source != "" {
t.Fatal("Parsing source")
}
if ircMessage.Command != "COMMAND" {
t.Fatal("Parsing command")
}
if ircMessage.Parameters[0] != "param1" {
t.Fatal("Parsing 1st param")
}
if ircMessage.Parameters[1] != "param2" {
t.Fatal("Parsing 2nd param")
}
if ircMessage.Parameters[2] != "param3 param3" {
t.Fatal("Parsing trailing param")
}
})
// TODO: FAIL
t.Run("Test yaml encoding with slashes", func(t *testing.T) {
testMessage := "@foo=\\\\\\\\\\:\\\\s\\s\\r\\n COMMAND"
ircMessage, value := ParseIRCMessage(testMessage)
if !value {
t.Fatal("Invalid irc message")
}
if ircMessage.Tags["foo"] != "\\\\;\\s \r\n" {
t.Fatal("Parsing escaped tag")
}
if ircMessage.Command != "COMMAND" {
t.Fatal("Parsing command")
}
})
t.Run("Test broken messages 1", func(t *testing.T) {
testMessage := ":gravel.mozilla.org 432 #momo :Erroneous Nickname: Illegal characters"
ircMessage, value := ParseIRCMessage(testMessage)
if !value {
t.Fatal("Invalid irc message")
}
if ircMessage.Source != "gravel.mozilla.org" {
t.Fatal("Parsing source")
}
if ircMessage.Command != "432" {
t.Fatal("Parsing command")
}
if ircMessage.Parameters[0] != "#momo" {
t.Fatal("Parsing 1st param")
}
if ircMessage.Parameters[1] != "Erroneous Nickname: Illegal characters" {
t.Fatal("Parsing trailing param")
}
})
t.Run("Test broken messages 2", func(t *testing.T) {
testMessage := ":gravel.mozilla.org MODE #tckk +n "
ircMessage, value := ParseIRCMessage(testMessage)
if !value {
t.Fatal("Invalid irc message")
}
if ircMessage.Source != "gravel.mozilla.org" {
t.Fatal("Parsing source")
}
if ircMessage.Command != "MODE" {
t.Fatal("Parsing command")
}
if ircMessage.Parameters[0] != "#tckk" {
t.Fatal("Parsing 1st param")
}
if ircMessage.Parameters[1] != "+n" {
t.Fatal("Parsing 2nd param")
}
})
t.Run("Test broken messages 3", func(t *testing.T) {
testMessage := ":services.esper.net MODE #foo-bar +o foobar "
ircMessage, value := ParseIRCMessage(testMessage)
if !value {
t.Fatal("Invalid irc message")
}
if ircMessage.Source != "services.esper.net" {
t.Fatal("Parsing source")
}
if ircMessage.Command != "MODE" {
t.Fatal("Parsing command")
}
if ircMessage.Parameters[0] != "#foo-bar" {
t.Fatal("Parsing 1st param")
}
if ircMessage.Parameters[1] != "+o" {
t.Fatal("Parsing 2nd param")
}
if ircMessage.Parameters[2] != "foobar" {
t.Fatal("Parsing trailing param")
}
})
// TODO: FAIL
t.Run("Test tags. they should be parsed char-at-a-time", func(t *testing.T) {
testMessage := "@tag1=value\\\\ntest COMMAND"
ircMessage, value := ParseIRCMessage(testMessage)
if !value {
t.Fatal("Invalid irc message")
}
if ircMessage.Tags["tag1"] != "value\\ntest" {
t.Fatal("Parsing escaped tag")
}
if ircMessage.Source != "" {
t.Fatal("Parsing source")
}
if ircMessage.Command != "COMMAND" {
t.Fatal("Parsing command")
}
})
// TODO: FAIl
t.Run("Test tag escape for char that doesn't need it", func(t *testing.T) {
testMessage := "@tag1=value\\1 COMMAND"
ircMessage, value := ParseIRCMessage(testMessage)
if !value {
t.Fatal("Invalid irc message")
}
if ircMessage.Tags["tag1"] != "value1" {
t.Fatal("Parsing escaped tag")
}
if ircMessage.Command != "COMMAND" {
t.Fatal("Parsing command")
}
})
// TODO: FAIL
t.Run("Test slash at end of tag", func(t *testing.T) {
testMessage := "@tag1=value1\\ COMMAND"
ircMessage, value := ParseIRCMessage(testMessage)
if !value {
t.Fatal("Invalid irc message")
}
if ircMessage.Tags["tag1"] != "value1" {
t.Fatal("Parsing escaped tag")
}
if ircMessage.Command != "COMMAND" {
t.Fatal("Parsing command")
}
})
t.Run("Test duplicate tags", func(t *testing.T) {
testMessage := "@tag1=1;tag2=3;tag3=4;tag1=5 COMMAND"
ircMessage, value := ParseIRCMessage(testMessage)
if !value {
t.Fatal("Invalid irc message")
}
if ircMessage.Tags["tag1"] != "5" {
t.Fatal("Parsing duplicate tag")
}
if ircMessage.Tags["tag2"] != "3" {
t.Fatal("Parsing regular tag")
}
if ircMessage.Tags["tag3"] != "4" {
t.Fatal("Parsing regular tag")
}
if ircMessage.Command != "COMMAND" {
t.Fatal("Parsing command")
}
})
t.Run("Test vendored tags can have the same name as unvendored tag", func(t *testing.T) {
testMessage := "@tag1=1;tag2=3;tag3=4;tag1=5;vendor/tag2=8 COMMAND"
ircMessage, value := ParseIRCMessage(testMessage)
if !value {
t.Fatal("Invalid irc message")
}
if ircMessage.Tags["tag1"] != "5" {
t.Fatal("Parsing duplicate tag")
}
if ircMessage.Tags["tag2"] != "3" {
t.Fatal("Parsing unvendored tag")
}
if ircMessage.Tags["tag3"] != "4" {
t.Fatal("Parsing regular tag")
}
if ircMessage.Tags["vendor/tag2"] != "8" {
t.Fatal("Parsing vendored tag")
}
if ircMessage.Command != "COMMAND" {
t.Fatal("Parsing command")
}
})
t.Run("Test: some parsers handle /MODE in a special way", func(t *testing.T) {
testMessage := ":SomeOp MODE #channel :+i"
ircMessage, value := ParseIRCMessage(testMessage)
if !value {
t.Fatal("Invalid irc message")
}
if ircMessage.Source != "SomeOp" {
t.Fatal("Parsing source")
}
if ircMessage.Command != "MODE" {
t.Fatal("Parsing command")
}
if ircMessage.Parameters[0] != "#channel" {
t.Fatal("Parsing 1st param")
}
if ircMessage.Parameters[1] != "+i" {
t.Fatal("Parsing trailing param")
}
})
t.Run("Test: some parsers handle /MODE in a special way 2", func(t *testing.T) {
testMessage := ":SomeOp MODE #channel +oo SomeUser :AnotherUser"
ircMessage, value := ParseIRCMessage(testMessage)
if !value {
t.Fatal("Invalid irc message")
}
if ircMessage.Source != "SomeOp" {
t.Fatal("Parsing source")
}
if ircMessage.Command != "MODE" {
t.Fatal("Parsing command")
}
if ircMessage.Parameters[0] != "#channel" {
t.Fatal("Parsing 1st param")
}
if ircMessage.Parameters[1] != "+oo" {
t.Fatal("Parsing 2nd param")
}
if ircMessage.Parameters[2] != "SomeUser" {
t.Fatal("Parsing 3rd param")
}
if ircMessage.Parameters[3] != "AnotherUser" {
t.Fatal("Parsing trailing param")
}
})
t.Run("Test multiple spaces", func(t *testing.T) {
testMessage := ":src JOIN #chan :thing lol haha"
ircMessage, value := ParseIRCMessage(testMessage)
if !value {
t.Fatal("Invalid irc message")
}
if ircMessage.Source != "src" {
t.Fatal("Parsing source with multiple spaces in between")
}
if ircMessage.Command != "JOIN" {
t.Fatal("Parsing command with multiple spaces in between")
}
if ircMessage.Parameters[0] != "#chan" {
t.Fatal("Parsing params with multiple spaces in between")
}
if ircMessage.Parameters[1] != "thing lol haha" {
t.Fatal("Parsing trailing param with multiple spaces in between")
}
})
}