Files

293 lines
6.8 KiB
Go
Raw Permalink Normal View History

2021-10-20 03:13:57 +03:00
// gorc project
2022-01-24 04:24:58 +02:00
// Copyright (C) 2022 IllusionMan1212
2021-10-20 03:13:57 +03:00
//
// 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.
2022-01-24 02:26:24 +02:00
package irc
2021-10-20 03:13:57 +03:00
import (
"crypto/tls"
"fmt"
"log"
2021-12-09 03:48:42 +02:00
"net"
"strings"
2022-02-19 16:24:39 +02:00
"time"
tea "github.com/charmbracelet/bubbletea"
2022-02-19 16:24:39 +02:00
"github.com/charmbracelet/lipgloss"
"github.com/illusionman1212/gorc/irc/commands"
"github.com/illusionman1212/gorc/ui"
2021-10-20 03:13:57 +03:00
)
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
2022-01-09 04:30:22 +02:00
type User struct {
// User prefix in channel
Prefix string
2022-01-09 04:30:22 +02:00
}
2025-08-18 21:10:04 +02:00
type Node[T any] struct {
Next *Node[T]
Prev *Node[T]
Value T
}
2022-01-09 04:30:22 +02:00
type Channel struct {
2022-01-20 05:08:39 +02:00
// Channel name
Name string
2022-01-09 04:30:22 +02:00
// Channel topic
Topic string
// Channel messages history
History string
// Date of the last message we received in this channel
LastUpdatedDate time.Time
2022-01-09 04:30:22 +02:00
// 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
2022-01-09 04:30:22 +02:00
}
2021-12-09 03:48:42 +02:00
2022-01-09 04:30:22 +02:00
type Client struct {
2021-10-20 03:13:57 +03:00
// tcp connection
TCPConn net.Conn
2022-01-09 04:30:22 +02:00
2022-01-20 05:08:39 +02:00
// 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
2025-08-18 21:10:04 +02:00
// The root of the doubly linked list of channel
RootChannel *Node[Channel]
2022-01-20 05:08:39 +02:00
2025-08-18 21:10:04 +02:00
// Active channel
ActiveChannel *Node[Channel]
2022-01-20 05:08:39 +02:00
// The channel to join immediately after registration completes.
InitialChannel string
2022-01-20 05:08:39 +02:00
// Reference to the bubbletea program
Tea *tea.Program
// Index of the first visible tab in the tab bar
2025-08-18 21:10:04 +02:00
// FirstTabIndexInTabBar int
// Index of the last visible tab in the tab bar
2025-08-18 21:10:04 +02:00
// LastTabIndexInTabBar int
// The acknowledged capabilities
EnabledCapabilities Capabilities
// The features currently enabled for this client
EnabledFeatures Features
2021-10-20 03:13:57 +03:00
}
type Capabilities map[string]string
type Features map[string]string
2022-02-19 16:24:39 +02:00
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
2022-02-19 16:24:39 +02:00
}
var unimpl = lipgloss.NewStyle().Foreground(ui.ErrorColor).Render("[UNIMPL]")
2022-02-19 16:24:39 +02:00
var serverMsgStyle = lipgloss.NewStyle().Foreground(ui.ServerMsgColor)
var errorMsgStyle = lipgloss.NewStyle().Foreground(ui.ErrorColor)
var timestampStyle = serverMsgStyle
2022-02-19 16:24:39 +02:00
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)
}
2025-09-02 06:53:29 +02:00
m.DateTime = t.Local()
} else {
m.DateTime = time.Now()
}
}
func (c *Channel) AppendMsg(datetime time.Time, fullMsg string, opts MsgFmtOpts) {
2022-02-19 16:24:39 +02:00
prefixes := ""
style := ui.DefaultStyle
if opts.WithTimestamp {
2025-09-02 06:53:29 +02:00
str := fmt.Sprintf("[%02d:%02d]", datetime.Hour(), datetime.Minute())
prefixes += timestampStyle.Render(str) + " "
2022-02-19 16:24:39 +02:00
}
if opts.NotImpl {
prefixes += unimpl + " "
2022-02-19 16:24:39 +02:00
}
if opts.AsServerMsg {
prefixes += serverMsgStyle.Render("==") + " "
style = serverMsgStyle
}
if opts.AsErrorMsg {
prefixes += errorMsgStyle.Render("==") + " "
style = errorMsgStyle
}
2025-09-02 06:53:29 +02:00
if datetime.Year() > c.LastUpdatedDate.Year() || datetime.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
2022-02-19 16:24:39 +02:00
}
c.History += prefixes + style.Render(fullMsg) + CRLF
}
func (c *Client) Initialize(host string, port string, tlsEnabled bool) {
2021-10-20 03:13:57 +03:00
addr := fmt.Sprintf("%s:%s", host, port)
c.Host = host
c.Port = port
c.EnabledCapabilities = make(Capabilities, 0)
c.EnabledFeatures = make(Features, 0)
2021-12-09 03:48:42 +02:00
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
2021-12-09 03:48:42 +02:00
}
addrTCP, err := net.ResolveTCPAddr("tcp", addr)
if err != nil {
log.Fatal(err)
}
conn, err := net.DialTCP("tcp", nil, addrTCP)
2021-10-20 03:13:57 +03:00
if err != nil {
log.Fatal(err)
}
c.TCPConn = conn
}
2022-01-09 04:30:22 +02:00
func (c *Client) Register(nick string, password string, channel string) {
2025-08-18 21:10:04 +02:00
hostChannel := Channel{
2022-01-20 05:08:39 +02:00
Name: c.Host,
Users: make(map[string]User),
2025-08-18 21:10:04 +02:00
}
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 != "" {
2022-02-19 16:24:39 +02:00
c.SendCommand(commands.PASS, password)
}
2022-02-19 16:24:39 +02:00
c.SendCommand(commands.NICK, nick)
// set user-wanted nickname
2022-01-09 04:30:22 +02:00
c.Nickname = nick
2022-02-19 16:24:39 +02:00
c.SendCommand(commands.USER, nick, "0", "*", nick)
2021-10-20 03:13:57 +03:00
}
2025-08-18 21:10:04 +02:00
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 {
2021-12-09 03:48:42 +02:00
// TODO: properly handle the error instead of Fatal-ing
log.Fatal("Attempted to write data to nil connection")
}
paramsString := ""
2022-01-03 23:59:52 +02:00
// if we have more than 1 param
if len(params) > 1 {
2022-01-03 23:59:52 +02:00
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
2022-01-09 04:30:22 +02:00
// if we have exactly 1 param and it's not empty
} else if len(params) == 1 && params[0] != "" {
2022-01-03 23:59:52 +02:00
// 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))
}