Files
gorc/irc/client.go
T

293 lines
6.9 KiB
Go

// gorc project
// Copyright (C) 2022 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"
"time"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
"github.com/illusionman1212/gorc/irc/commands"
"github.com/illusionman1212/gorc/ui"
)
type Message struct {
DateTime time.Time
Tags MessageTags // 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 MessageTags map[string]string
type User struct {
// User prefix in channel
Prefix string
}
type Node[T any] struct {
Next *Node[T]
Prev *Node[T]
Value T
}
type Channel struct {
// Channel name
Name string
// Channel topic
Topic string
// Channel messages history
History string
// Date of the last message we received in this channel
LastUpdatedDate time.Time
// 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
// The root of the doubly linked list of channel
RootChannel *Node[Channel]
// Active channel
ActiveChannel *Node[Channel]
// The channel to join immediately after registration completes.
InitialChannel string
// Reference to the bubbletea program
Tea *tea.Program
// Index of the first visible tab in the tab bar
// FirstTabIndexInTabBar int
// Index of the last visible tab in the tab bar
// LastTabIndexInTabBar int
// The acknowledged capabilities
EnabledCapabilities Capabilities
// The features currently enabled for this client
EnabledFeatures Features
}
type Capabilities map[string]string
type Features map[string]string
type MsgFmtOpts struct {
WithTimestamp bool
// TODO: make these a bitset? they should compliment eachother instead of conflicting or rendering twice.
AsServerMsg bool
AsErrorMsg bool
NotImpl bool
}
var unimpl = lipgloss.NewStyle().Foreground(ui.ErrorColor).Render("[UNIMPL]")
var serverMsgStyle = lipgloss.NewStyle().Foreground(ui.ServerMsgColor)
var errorMsgStyle = lipgloss.NewStyle().Foreground(ui.ErrorColor)
var timestampStyle = serverMsgStyle
var dateStyle = lipgloss.NewStyle().Foreground(ui.DateColor)
const CRLF = "\r\n"
func (m *Message) SetTimestamp() {
if serverTime, ok := m.Tags["time"]; ok {
t, err := time.Parse("2006-01-02T15:04:05.000Z", serverTime)
if err != nil {
// TODO: properly handle error
log.Fatalln(err)
}
m.DateTime = t
} else {
m.DateTime = time.Now()
}
}
func (c *Channel) AppendMsg(datetime time.Time, fullMsg string, opts MsgFmtOpts) {
prefixes := ""
style := ui.DefaultStyle
if opts.WithTimestamp {
str := fmt.Sprintf("[%02d:%02d]", datetime.Local().Hour(), datetime.Local().Minute())
prefixes += timestampStyle.Render(str) + " "
}
if opts.NotImpl {
prefixes += unimpl + " "
}
if opts.AsServerMsg {
prefixes += serverMsgStyle.Render("==") + " "
style = serverMsgStyle
}
if opts.AsErrorMsg {
prefixes += errorMsgStyle.Render("==") + " "
style = errorMsgStyle
}
if datetime.Local().Year() > c.LastUpdatedDate.Local().Year() || datetime.Local().YearDay() > c.LastUpdatedDate.YearDay() {
c.LastUpdatedDate = datetime
dateMsg := fmt.Sprintf("————— %s %d —————", c.LastUpdatedDate.Month().String(), c.LastUpdatedDate.Day())
c.History += dateStyle.Render(dateMsg) + CRLF
}
c.History += prefixes + style.Render(fullMsg) + CRLF
}
func (c *Client) Initialize(host string, port string, tlsEnabled bool) {
addr := fmt.Sprintf("%s:%s", host, port)
c.Host = host
c.Port = port
c.EnabledCapabilities = make(Capabilities, 0)
c.EnabledFeatures = make(Features, 0)
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)
}
c.TCPConn = conn
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)
}
c.TCPConn = conn
}
func (c *Client) Register(nick string, password string, channel string) {
hostChannel := Channel{
Name: c.Host,
Users: make(map[string]User),
}
root := &Node[Channel]{Value: hostChannel}
root.Next = root
root.Prev = root
c.ActiveChannel = root
c.RootChannel = root
c.SendCommand(commands.CAP, "LS", "302")
if password != "" {
c.SendCommand(commands.PASS, password)
}
c.SendCommand(commands.NICK, nick)
// set user-wanted nickname
c.Nickname = nick
c.SendCommand(commands.USER, nick, "0", "*", nick)
}
func (c *Client) AppendChannel(channel Channel) *Node[Channel] {
first := c.RootChannel
last := c.RootChannel.Prev
newNode := &Node[Channel]{
Value: channel,
Next: first,
Prev: last,
}
last.Next = newNode
first.Prev = newNode
return newNode
}
func (c *Client) RemoveChannel(channel *Node[Channel]) {
prev := channel.Prev
next := channel.Next
prev.Next = next
next.Prev = prev
channel.Prev = nil
channel.Next = nil
}
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))
}