diff --git a/client.go b/client.go new file mode 100644 index 0000000..9ec2486 --- /dev/null +++ b/client.go @@ -0,0 +1,79 @@ +// gorc project +// Copyright (C) 2021 IllusionMan1212 and contributors +// +// 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 main + +import ( + "crypto/tls" + "fmt" + "log" +) + +type Client struct { + // tcp connection + conn *tls.Conn + + // channel to receive data that comes through from the connection + receive chan string +} + +func NewClient(host string, port string) *Client { + // TODO: allow for insecure connections thru a checkbox (needed for irc servers that maybe don't have encryption on) + // TODO: distinguish between secure and insecure connections + cfg := &tls.Config{ServerName: host} + addr := fmt.Sprintf("%s:%s", host, port) + 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) + } + + // TODO: read nickname and channel from user input (stdin ?? or TUI) + conn.Write([]byte("CAP LS\r\n")) + // conn.Write([]byte("PASS password")) + conn.Write([]byte("NICK illusion434\r\n")) + conn.Write([]byte("USER illusion434 0 * :illusion434\r\n")) + // TODO: CAP REQ :whatever capability the client recognizes and supports + conn.Write([]byte("CAP END\r\n")) + conn.Write([]byte("JOIN ##programming\r\n")) + + return &Client{ + conn: conn, + receive: make(chan string), + } +} + +func (c Client) handleCommand(message IRCMessage) { + fmt.Printf("Source: %s\n", message.Source) + fmt.Printf("Command: %s\n", message.Command) + fmt.Printf("Params: %s\n", message.Parameters) + + switch message.Command { + case "PING": + c.conn.Write([]byte("PONG\r\n")) + break + } +} + +func (c Client) Run() { + for { + select { + case line := <-c.receive: + messages := parseIRCMessage(line) + c.handleCommand(messages) + } + } +} diff --git a/constants.go b/constants.go index 91a02c3..6b436eb 100644 --- a/constants.go +++ b/constants.go @@ -77,11 +77,11 @@ const ( ) /* --- IRC Message --- */ -type message struct { - Tags []string // starts with @ | Optional - Prefix string // starts with : | Optional - Command string // can either be a string or a numeric value | Required - Parameters []string // Optional (Dependant on command) +type IRCMessage struct { + Tags map[string]string // 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) } /* --- Channel Types --- */ @@ -234,16 +234,5 @@ const ( // Names are casemapped, read the casemapping from the RPL_ISUPPORT the server sends when registration is completed. // Silently ignore empty messages and only parse messages when you encounter a CRLF. -// Messages format -// [@tags] [:source|prefix] -// tags: Optional metadata on a message, starting with "@" (0x40) -// source|prefix: Optional note of where the message came from, starting with ":" (0x3A) -// command: The specific command this message represents -// parameters: if it exists, data relevant to this specific command - -// Message parts are separated by one or more ASCII SPACE characters " " (0x20) -// Messages are limited to 512 bytes including the CRLF characters. - -// Client MUST be able to process a message whether it contains a source|prefix or not. // Client SHOULD NOT include a source when sending a message, but if included it MUST be the nickname of the client. // When receiving a numeric reply, client MUST be able to handle any number of parameters on a numeric reply. diff --git a/main.go b/main.go index 865719d..18f4333 100644 --- a/main.go +++ b/main.go @@ -17,47 +17,24 @@ package main import ( - "crypto/tls" - "fmt" - "io" + "bufio" "log" ) func main() { - // TOOD: read the server (and port?) from user input (stdin ?? or TUI) - // TODO: allow for insecure connections thru a checkbox (needed for irc servers that maybe don't have encryption on) - cfg := &tls.Config{ServerName: "irc.libera.chat"} - conn, err := tls.Dial("tcp", "irc.libera.chat:6697", cfg) - if err != nil { - // TODO: properly handle the error instead of Fatal-ing (failed to initiate a connection to server) - log.Fatal(err) - } - defer conn.Close() - - // TODO: read nickname and channel from user input (stdin ?? or TUI) - conn.Write([]byte("CAP LS\r\n")) - // TODO: PASS if it requires a password - conn.Write([]byte("NICK illusion\r\n")) - conn.Write([]byte("USER illusion 0 * :illusion\r\n")) - // TODO: CAP REQ :whatever capability the client recognizes and supports - conn.Write([]byte("CAP END\r\n")) - // conn.Write([]byte("JOIN #libera\r\n")) + // create new client with the provided host and port + client := NewClient("irc.libera.chat", "6697") + defer client.conn.Close() + go client.Run() + // main loop + r := bufio.NewReaderSize(client.conn, 512) for { - buf := make([]byte, 1024) - - _, err := conn.Read(buf) + msg, err := r.ReadString('\n') if err != nil { - if err == io.EOF { - log.Print("End of file received\n") - return - } + log.Print(err) } - // TODO: parse received data according to the protocol - - // TOOD: reply to PINGs with PONGs to keep the connection alive - - fmt.Printf("Data: %s\n", buf) + client.receive <- msg } } diff --git a/parser.go b/parser.go new file mode 100644 index 0000000..e37c274 --- /dev/null +++ b/parser.go @@ -0,0 +1,108 @@ +// gorc project +// Copyright (C) 2021 IllusionMan1212 and contributors +// +// 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 main + +import ( + "fmt" + "strings" +) + +func parseIRCMessage(line string) IRCMessage { + ircMessage := &IRCMessage{} + + switch line[0] { + case '@': + substrs := strings.SplitN(line, " ", 4) + + tagsSlice := make(map[string]string) + + tags := strings.Split(substrs[0][1:], ";") + + for _, tag := range tags { + splitTag := strings.Split(tag, "=") + + // TOOD: some tags don't have an "=" and they're basically considered "true" + tagsSlice[splitTag[0]] = splitTag[1] + } + + ircMessage := IRCMessage{ + Tags: tagsSlice, + Source: substrs[1][1:], + Command: substrs[2], + } + + ircMessage.Tags = tagsSlice + ircMessage.Source = substrs[1][1:] + ircMessage.Command = substrs[2] + + fmt.Printf("%s\n", ircMessage.Tags) + fmt.Printf("%s\n", ircMessage.Source) + fmt.Printf("%s\n", ircMessage.Command) + fmt.Printf("%s\n", ircMessage.Parameters) + // TODO: + + break + case ':': + substrs := strings.SplitN(line, " ", 3) + + parameters := make([]string, 0) + + // if parameters exist + if len(substrs) > 2 { + params := strings.SplitN(substrs[2], " :", 2) + + parameters = strings.Split(params[0], " ") + // TODO: parameters can be empty. this is denoted by a ":" + + // if there's a final space-included param + if len(params) > 1 { + parameters = append(parameters, params[1]) + } + } + + ircMessage.Source = substrs[0][1:] + ircMessage.Command = substrs[1] + ircMessage.Parameters = parameters + + break + default: + substrs := strings.SplitN(line, " ", 2) + + parameters := make([]string, 0) + + // if parameters exist + if len(substrs) > 1 { + params := strings.SplitN(substrs[1], " :", 2) + + parameters = strings.Split(params[0], " ") + // TODO: parameters can be empty. this is denoted by a ":" + + // if there's a trailing parameter + if len(params) > 1 { + parameters = append(parameters, params[1]) + } + } + + ircMessage.Source = "" + ircMessage.Command = substrs[0] + ircMessage.Parameters = parameters + + break + } + + return *ircMessage +}