Compare commits

...

10 Commits

Author SHA1 Message Date
meer
7b555dc998 Update main.go 2026-07-26 16:10:26 +00:00
meer
31ea29dbe0 Added TLS 2026-07-26 16:09:34 +00:00
meer
69beb1340e Update main.go 2026-07-26 13:47:38 +00:00
meer
ead0c8e3c9 Patched critical bug I. 2026-07-26 13:44:58 +00:00
meer
7c532f9233 Update main.go 2026-07-26 10:59:03 +00:00
meer
f9adf66d3b Update README.md 2026-07-21 15:46:10 +00:00
meera
55d69d96c1 added ISON and NOTIFY 2026-07-19 23:06:00 +03:00
meera
f921f53669 reduced chance of mutex related panic 2026-07-19 04:22:25 +03:00
meera
c2551972a5 p 2026-07-19 01:57:38 +03:00
meera
330bd96cdf plz audit this commit, what did i fuck up? 2026-07-18 22:19:33 +03:00
3 changed files with 385 additions and 100 deletions

2
.gitignore vendored
View File

@ -24,7 +24,7 @@ profile.cov
# Go workspace file # Go workspace file
go.work go.work
go.work.sum go.work.sum
stresser.go
# env file # env file
.env .env

View File

@ -1,3 +1,38 @@
# Gocirc # Gocirc
a lightweight IRC server written in Golang a lightweight IRC server written in Golang
## setup
1. cloning the repository
```
git clone ssh://git@git-ssh.anarchists.space:222/meer/Gocirc.git
cd Gocirc
```
2. compiling the server
```
chmod +x build.sh
./build.sh
```
3. setting up configuration file
example JSON config
```JSON
{
"port": 6667,
"webapiport": 7880,
"motd": "<motd file>",
"enableserverpassword": true,
"serverpassword": "YOUR_SERVERPASSWORD",
"channels": [
{
"channel": "#main",
"description": "the main channel"
}
],
"ops": [
"admin"
]
}
```
change values as needed

446
main.go
View File

@ -6,11 +6,13 @@ package main
import ( import (
"bufio" "bufio"
"crypto/tls"
"crypto/sha256" "crypto/sha256"
"database/sql" "database/sql"
"encoding/base64" "encoding/base64"
"encoding/hex" "encoding/hex"
"encoding/json" "encoding/json"
"flag"
"fmt" "fmt"
"log" "log"
"net" "net"
@ -31,6 +33,7 @@ type Client struct {
conn net.Conn conn net.Conn
host string host string
channels map[string]*Channel channels map[string]*Channel
monitorTargets map[string]bool
registered bool registered bool
saslRequired bool saslRequired bool
saslComplete bool saslComplete bool
@ -121,6 +124,10 @@ func loadMOTD(filename string) error {
return nil return nil
} }
func main() { func main() {
tlsEnabled := flag.Bool("tls", false, "Enable TLS")
certFile := flag.String("cert", "", "TLS certificate file")
keyFile := flag.String("key", "", "TLS key file")
flag.Parse()
cfg, err := loadConfig() cfg, err := loadConfig()
if err != nil { if err != nil {
log.Fatalf("could not load config file: %v\n", err) log.Fatalf("could not load config file: %v\n", err)
@ -129,10 +136,28 @@ func main() {
if errmotd != nil { if errmotd != nil {
log.Fatalf("could not load message of the day file: %v", err) log.Fatalf("could not load message of the day file: %v", err)
} }
ln, err := net.Listen("tcp", fmt.Sprintf(":%d", cfg.Port)) var ln net.Listener
if err != nil { if *tlsEnabled {
log.Fatalf("Error listening: %v\n", err) if *certFile == "" || *keyFile == "" {
} log.Fatalf("TLS enabled but cert/key not provided")
}
cert, err := tls.LoadX509KeyPair(*certFile, *keyFile)
if err != nil {
log.Fatalf("failed to load TLS cert/key: %v", err)
}
tlsCfg := &tls.Config{Certificates: []tls.Certificate{cert}}
ln, err = tls.Listen("tcp", fmt.Sprintf(":%d", cfg.Port), tlsCfg)
if err != nil {
log.Fatalf("Error listening with TLS: %v\n", err)
}
log.Printf("IRC server listening with TLS on port %d\n", cfg.Port)
} else {
ln, err = net.Listen("tcp", fmt.Sprintf(":%d", cfg.Port))
if err != nil {
log.Fatalf("Error listening: %v\n", err)
}
log.Printf("IRC server listening on port %d\n", cfg.Port)
}
sqlinitDB() sqlinitDB()
log.Printf("IRC server listening on port %d\n", cfg.Port) log.Printf("IRC server listening on port %d\n", cfg.Port)
go startHTTP(*cfg) go startHTTP(*cfg)
@ -153,10 +178,11 @@ func handleConn(conn net.Conn) {
reader := bufio.NewReader(conn) reader := bufio.NewReader(conn)
client := &Client{ client := &Client{
conn: conn, conn: conn,
host: host, host: host,
channels: make(map[string]*Channel), channels: make(map[string]*Channel),
registered: false, monitorTargets: make(map[string]bool),
registered: false,
} }
defer client.cleanup() defer client.cleanup()
for { for {
@ -169,7 +195,7 @@ func handleConn(conn net.Conn) {
if err != nil { if err != nil {
continue continue
} }
//fmt.Printf(" %s\n", line) // fmt.Printf(" %s\n", line)
switch strings.ToUpper(msg.Command) { switch strings.ToUpper(msg.Command) {
case "NICK": case "NICK":
if len(msg.Params) > 0 { if len(msg.Params) > 0 {
@ -186,6 +212,10 @@ func handleConn(conn net.Conn) {
} }
clients[newNick] = client clients[newNick] = client
cliMu.Unlock() cliMu.Unlock()
if oldNick != "" {
notifyMonitorsOffline(oldNick)
}
notifyMonitorsOnline(newNick)
for _, ch := range client.channels { for _, ch := range client.channels {
ch.mu.Lock() ch.mu.Lock()
if oldNick != "" { if oldNick != "" {
@ -236,12 +266,14 @@ func handleConn(conn net.Conn) {
switch subcmd { switch subcmd {
case "LS": case "LS":
// explicitly tell we only support PLAIN SASL, as well as chathistory // explicitly tell we only support PLAIN SASL, as well as chathistory
client.fwrite(":server CAP * LS :sasl=PLAIN sasl chathistory\r\n") client.fwrite(":server CAP * LS :sasl=PLAIN sasl chathistory server-time message-tags draft/chathistory batch\r\n")
case "REQ": case "REQ":
if len(msg.Params) >= 2 && strings.Contains(strings.ToLower(msg.Params[1]), "sasl") { if len(msg.Params) >= 2 && strings.Contains(strings.ToLower(msg.Params[1]), "sasl") {
client.saslRequired = true client.saslRequired = true
client.fwrite(":server CAP * ACK :sasl\r\n") client.fwrite(":server CAP * ACK :sasl\r\n")
} else if len(msg.Params) >= 2 {
client.fwrite(":server CAP * ACK :%s\r\n", msg.Params[1])
} else { } else {
client.fwrite(":server CAP * NAK :%s\r\n", msg.Params[1]) client.fwrite(":server CAP * NAK :%s\r\n", msg.Params[1])
} }
@ -459,16 +491,105 @@ func handleConn(conn net.Conn) {
} else { } else {
fmt.Fprintf(client.conn, ":server 461 %s NAMES :Not enough parameters\r\n", client.nick) fmt.Fprintf(client.conn, ":server 461 %s NAMES :Not enough parameters\r\n", client.nick)
} }
case "ISON":
if len(msg.Params) < 1 {
client.fwrite(":server 461 %s ISON :Not enough parameters\r\n", client.nick)
continue
}
requested := msg.Params
var online []string
cliMu.RLock()
for _, nick := range requested {
if _, ok := clients[nick]; ok {
online = append(online, nick)
}
}
cliMu.RUnlock()
client.fwrite(":server 303 %s :%s\r\n", client.nick, strings.Join(online, " "))
// ircv3 stuff // ircv3 stuff
case "CHATHISTORY": case "CHATHISTORY":
if len(msg.Params) < 3 { if len(msg.Params) < 1 {
client.fwrite(":server 461 %s CHATHISTORY :Not enough parameters\r\n", client.nick) client.fwrite(":server 461 %s CHATHISTORY :Not enough parameters\r\n", client.nick)
continue
}
subcmd := strings.ToUpper(msg.Params[0])
switch subcmd {
case "LATEST", "BEFORE", "AFTER", "AROUND":
if len(msg.Params) < 4 {
client.fwrite(":server 461 %s CHATHISTORY :Not enough parameters\r\n", client.nick)
continue
}
target := historyTargetName(msg.Params[1])
count, convErr := strconv.Atoi(msg.Params[3])
if convErr != nil || count <= 0 {
client.fwrite(":server 461 %s CHATHISTORY :Invalid message count\r\n", client.nick)
continue
}
client.serveHistory(subcmd, target, count, msg.Params)
case "BETWEEN":
if len(msg.Params) < 5 {
client.fwrite(":server 461 %s CHATHISTORY :Not enough parameters\r\n", client.nick)
continue
}
target := historyTargetName(msg.Params[1])
count, convErr := strconv.Atoi(msg.Params[4])
if convErr != nil || count <= 0 {
client.fwrite(":server 461 %s CHATHISTORY :Invalid message count\r\n", client.nick)
continue
}
client.serveHistory(subcmd, target, count, msg.Params)
case "TARGETS":
if len(msg.Params) < 4 {
client.fwrite(":server 461 %s CHATHISTORY :Not enough parameters\r\n", client.nick)
continue
}
count, convErr := strconv.Atoi(msg.Params[3])
if convErr != nil || count <= 0 {
client.fwrite(":server 461 %s CHATHISTORY :Invalid message count\r\n", client.nick)
continue
}
client.serveHistory(subcmd, "", count, msg.Params)
default:
client.fwrite(":server 400 %s CHATHISTORY :Unknown subcommand\r\n", client.nick)
}
case "MONITOR":
if len(msg.Params) < 1 {
client.fwrite(":server 461 %s MONITOR :Not enough parameters\r\n", client.nick)
continue continue
} }
subcmd := strings.ToUpper(msg.Params[0]) subcmd := strings.ToUpper(msg.Params[0])
target := normalizeChannelName(msg.Params[1]) switch subcmd {
count, _ := strconv.Atoi(msg.Params[2]) case "+":
client.serveHistory(subcmd, target, count, msg.Params) for _, nick := range msg.Params[1:] {
client.monitorTargets[nick] = true
cliMu.RLock()
if _, ok := clients[nick]; ok {
client.fwrite(":server 730 %s %s :is online\r\n", client.nick, nick)
}
cliMu.RUnlock()
}
case "-":
for _, nick := range msg.Params[1:] {
delete(client.monitorTargets, nick)
}
case "C":
client.monitorTargets = make(map[string]bool)
case "L":
var nicks []string
for nick := range client.monitorTargets {
nicks = append(nicks, nick)
}
client.fwrite(":server 732 %s :%s\r\n", client.nick, strings.Join(nicks, " "))
client.fwrite(":server 733 %s :End of MONITOR list\r\n", client.nick)
default:
client.fwrite(":server 461 %s MONITOR :Invalid subcommand\r\n", client.nick)
}
default: default:
if client.nick != "" { if client.nick != "" {
fmt.Fprintf(conn, ":server 421 %s %s :Unknown command or command is not implemented\r\n", client.nick, msg.Command) fmt.Fprintf(conn, ":server 421 %s %s :Unknown command or command is not implemented\r\n", client.nick, msg.Command)
@ -478,6 +599,17 @@ func handleConn(conn net.Conn) {
} }
} }
} }
func historyTargetName(name string) string {
if strings.HasPrefix(name, "#") || strings.HasPrefix(name, "&") {
return name
}
if nicknameExists(name) {
return name
}
return name
}
func (c *Client) sendNames(ch *Channel) { func (c *Client) sendNames(ch *Channel) {
ch.mu.Lock() ch.mu.Lock()
defer ch.mu.Unlock() defer ch.mu.Unlock()
@ -496,37 +628,48 @@ func (c *Client) sendNames(ch *Channel) {
} }
func (c *Client) serveHistory(subcmd, target string, count int, params []string) { func (c *Client) serveHistory(subcmd, target string, count int, params []string) {
chMu.Lock() chMu.Lock()
ch, ok := channels[target] ch, isChannel := channels[target]
chMu.Unlock() chMu.Unlock()
if !ok {
c.fwrite(":server 403 %s %s :No such channel\r\n", c.nick, target)
return
}
ch.mu.RLock() var lockCh func()
defer ch.mu.RUnlock() var unlockCh func()
if isChannel {
lockCh = ch.mu.RLock
unlockCh = ch.mu.RUnlock
lockCh()
defer unlockCh()
}
var msgs []HistoryMessage var msgs []HistoryMessage
switch subcmd { switch subcmd {
case "LATEST": case "LATEST":
if count <= len(ch.history) { if isChannel && count <= len(ch.history) {
msgs = ch.history[len(ch.history)-count:] msgs = ch.history[len(ch.history)-count:]
} else { } else {
extra := count - len(ch.history) extra := count
if isChannel {
extra = count - len(ch.history)
}
rows, err := db.Query(`SELECT msgid, timestamp, prefix, command, msg rows, err := db.Query(`SELECT msgid, timestamp, prefix, command, msg
FROM messages FROM messages
WHERE channel=? WHERE channel=?
ORDER BY timestamp DESC LIMIT ?`, ORDER BY timestamp DESC LIMIT ?`,
target, extra) target, extra)
if err != nil { if err != nil {
c.fwrite(":server 718 %s %s :No history available\r\n", c.nick, target) c.fwrite(":server 718 %s %s :No history available\r\n", c.nick, target)
return return
} }
defer rows.Close() defer rows.Close()
dbMsgs := scanRows(rows, target) dbMsgs := scanRows(rows, target)
msgs = append(dbMsgs, ch.history...) if isChannel {
msgs = append(dbMsgs, ch.history...)
} else {
for i, j := 0, len(dbMsgs)-1; i < j; i, j = i+1, j-1 {
dbMsgs[i], dbMsgs[j] = dbMsgs[j], dbMsgs[i]
}
msgs = dbMsgs
}
} }
case "BEFORE": case "BEFORE":
@ -535,40 +678,105 @@ func (c *Client) serveHistory(subcmd, target string, count int, params []string)
return return
} }
msgid := strings.TrimPrefix(params[2], "msgid=") msgid := strings.TrimPrefix(params[2], "msgid=")
rows, err := db.Query(`SELECT msgid, timestamp, prefix, command, msg rows, err := db.Query(`SELECT msgid, timestamp, prefix, command, msg
FROM messages FROM messages
WHERE channel=? AND msgid < ? WHERE channel=? AND msgid < ?
ORDER BY timestamp DESC LIMIT ?`, ORDER BY timestamp DESC LIMIT ?`,
target, msgid, count) target, msgid, count)
if err != nil { if err != nil {
c.fwrite(":server 718 %s %s :No history available\r\n", c.nick, target) c.fwrite(":server 718 %s %s :No history available\r\n", c.nick, target)
return return
} }
defer rows.Close() defer rows.Close()
msgs = scanRows(rows, target) msgs = scanRows(rows, target)
case "BETWEEN":
if len(params) < 5 {
c.fwrite(":server 461 %s CHATHISTORY :Not enough parameters\r\n", c.nick)
return
}
start := strings.TrimPrefix(params[2], "msgid=")
end := strings.TrimPrefix(params[3], "msgid=")
rows, err := db.Query(`SELECT msgid, timestamp, prefix, command, msg
FROM messages
WHERE channel=? AND msgid BETWEEN ? AND ?
ORDER BY timestamp ASC LIMIT ?`,
target, start, end, count)
if err != nil {
c.fwrite(":server 718 %s %s :No history available\r\n", c.nick, target)
return
}
defer rows.Close()
msgs = scanRows(rows, target)
case "AFTER": case "AFTER":
if len(params) < 4 { if len(params) < 4 {
c.fwrite(":server 461 %s CHATHISTORY :Not enough parameters\r\n", c.nick) c.fwrite(":server 461 %s CHATHISTORY :Not enough parameters\r\n", c.nick)
return return
} }
msgid := strings.TrimPrefix(params[2], "msgid=")
rows, err := db.Query(`SELECT msgid, timestamp, prefix, command, msg arg := params[2]
FROM messages var rows *sql.Rows
WHERE channel=? AND msgid > ? var err error
ORDER BY timestamp ASC LIMIT ?`,
target, msgid, count) if strings.HasPrefix(arg, "msgid=") {
msgid := strings.TrimPrefix(arg, "msgid=")
rows, err = db.Query(`SELECT msgid, timestamp, prefix, command, msg
FROM messages
WHERE channel=? AND msgid > ?
ORDER BY timestamp ASC LIMIT ?`,
target, msgid, count)
} else if strings.HasPrefix(arg, "timestamp=") {
tsStr := strings.TrimPrefix(arg, "timestamp=")
t, parseErr := time.Parse(time.RFC3339Nano, tsStr)
if parseErr != nil {
c.fwrite(":server 461 %s CHATHISTORY :Invalid timestamp\r\n", c.nick)
return
}
rows, err = db.Query(`SELECT msgid, timestamp, prefix, command, msg
FROM messages
WHERE channel=? AND timestamp > ?
ORDER BY timestamp ASC LIMIT ?`,
target, t.UnixNano(), count)
} else {
c.fwrite(":server 461 %s CHATHISTORY :Invalid anchor parameter\r\n", c.nick)
return
}
if err != nil { if err != nil {
c.fwrite(":server 718 %s %s :No history available\r\n", c.nick, target) c.fwrite(":server 718 %s %s :No history available\r\n", c.nick, target)
return return
} }
defer rows.Close() defer rows.Close()
msgs = scanRows(rows, target) msgs = scanRows(rows, target)
case "TARGETS":
if len(params) < 4 {
c.fwrite(":server 461 %s CHATHISTORY :Not enough parameters\r\n", c.nick)
return
}
// Parse start and end timestamps
startStr := strings.TrimPrefix(params[1], "timestamp=")
endStr := strings.TrimPrefix(params[2], "timestamp=")
startTime, err1 := time.Parse(time.RFC3339Nano, startStr)
endTime, err2 := time.Parse(time.RFC3339Nano, endStr)
if err1 != nil || err2 != nil {
c.fwrite(":server 461 %s CHATHISTORY :Invalid timestamp\r\n", c.nick)
return
}
rows, err := db.Query(`SELECT msgid, timestamp, prefix, command, msg
FROM messages
WHERE timestamp BETWEEN ? AND ?
ORDER BY timestamp ASC LIMIT ?`,
startTime.UnixNano(), endTime.UnixNano(), count)
if err != nil {
c.fwrite(":server 718 %s * :No history available\r\n", c.nick)
return
}
defer rows.Close()
msgs = scanRows(rows, "")
case "AROUND": case "AROUND":
if len(params) < 4 { if len(params) < 4 {
c.fwrite(":server 461 %s CHATHISTORY :Not enough parameters\r\n", c.nick) c.fwrite(":server 461 %s CHATHISTORY :Not enough parameters\r\n", c.nick)
@ -577,9 +785,9 @@ func (c *Client) serveHistory(subcmd, target string, count int, params []string)
msgid := strings.TrimPrefix(params[2], "msgid=") msgid := strings.TrimPrefix(params[2], "msgid=")
half := count / 2 half := count / 2
beforeRows, _ := db.Query(`SELECT msgid, timestamp, prefix, command, msg beforeRows, _ := db.Query(`SELECT msgid, timestamp, prefix, command, msg
FROM messages FROM messages
WHERE channel=? AND msgid < ? WHERE channel=? AND msgid < ?
ORDER BY timestamp DESC LIMIT ?`, ORDER BY timestamp DESC LIMIT ?`,
target, msgid, half) target, msgid, half)
beforeMsgs := scanRows(beforeRows, target) beforeMsgs := scanRows(beforeRows, target)
anchor, err := findMsgIndex(target, msgid) anchor, err := findMsgIndex(target, msgid)
@ -588,22 +796,23 @@ func (c *Client) serveHistory(subcmd, target string, count int, params []string)
anchorMsg = []HistoryMessage{*anchor} anchorMsg = []HistoryMessage{*anchor}
} }
afterRows, _ := db.Query(`SELECT msgid, timestamp, prefix, command, msg afterRows, _ := db.Query(`SELECT msgid, timestamp, prefix, command, msg
FROM messages FROM messages
WHERE channel=? AND msgid > ? WHERE channel=? AND msgid > ?
ORDER BY timestamp ASC LIMIT ?`, ORDER BY timestamp ASC LIMIT ?`,
target, msgid, half) target, msgid, half)
afterMsgs := scanRows(afterRows, target) afterMsgs := scanRows(afterRows, target)
msgs = append(beforeMsgs, anchorMsg...) msgs = append(beforeMsgs, anchorMsg...)
msgs = append(msgs, afterMsgs...) msgs = append(msgs, afterMsgs...)
} }
batchID := fmt.Sprintf("hist-%d", time.Now().UnixNano()) batchID := fmt.Sprintf("hist-%d", time.Now().UnixNano())
c.fwrite(":server BATCH +%s chathistory %s\r\n", batchID, target) c.fwrite(":server BATCH +%s chathistory %s\r\n", batchID, target)
for _, m := range msgs { for _, m := range msgs {
tags := fmt.Sprintf("@msgid=%s;time=%s", tags := fmt.Sprintf("@msgid=%s;time=%s",
m.MsgID, m.MsgID,
m.Time.UTC().Format(time.RFC3339)) m.Time.UTC().Format(time.RFC3339Nano))
c.fwrite("%s :%s %s %s :%s\r\n", c.fwrite("%s :%s %s %s :%s\r\n",
tags, tags,
m.Prefix, m.Prefix,
@ -620,12 +829,12 @@ func findMsgIndex(channel, msgid string) (*HistoryMessage, error) {
WHERE channel=? AND msgid=?`, channel, msgid) WHERE channel=? AND msgid=?`, channel, msgid)
var m HistoryMessage var m HistoryMessage
var ts string var tsInt int64
err := row.Scan(&m.MsgID, &ts, &m.Prefix, &m.Command, &m.Text) err := row.Scan(&m.MsgID, &tsInt, &m.Prefix, &m.Command, &m.Text)
if err != nil { if err != nil {
return nil, err return nil, err
} }
m.Time, _ = time.Parse(time.RFC3339, ts) m.Time = time.Unix(0, tsInt).UTC()
m.Params = []string{channel} m.Params = []string{channel}
return &m, nil return &m, nil
} }
@ -642,7 +851,7 @@ func kickUser(sender *Client, channelName, targetNick, reason string) {
ch.mu.Lock() ch.mu.Lock()
defer ch.mu.Unlock() defer ch.mu.Unlock()
if !ch.ops[sender.nick] { if !ch.ops[sender.nick] && !globalOps[sender.nick] {
sender.fwrite(":server 482 %s %s :You're not channel operator\r\n", sender.nick, channelName) sender.fwrite(":server 482 %s %s :You're not channel operator\r\n", sender.nick, channelName)
return return
} }
@ -676,7 +885,7 @@ func handleMode(sender *Client, channelName, mode, targetNick string) {
ch.mu.Lock() ch.mu.Lock()
defer ch.mu.Unlock() defer ch.mu.Unlock()
if !ch.ops[sender.nick] { if !ch.ops[sender.nick] && !globalOps[sender.nick] {
sender.fwrite(":server 482 %s %s :You're not channel operator\r\n", sender.nick, channelName) sender.fwrite(":server 482 %s %s :You're not channel operator\r\n", sender.nick, channelName)
return return
} }
@ -728,8 +937,7 @@ func joinChannel(c *Client, name string) {
// reject duplicate join // reject duplicate join
if _, exists := ch.members[c.nick]; exists { if _, exists := ch.members[c.nick]; exists {
ch.mu.Unlock() ch.mu.Unlock()
c.fwrite(":server 443 %s %s %s :is already on channel\r\n", c.fwrite(":server 443 %s %s %s :is already on channel\r\n", c.nick, c.nick, name)
c.nick, c.nick, name)
return return
} }
// clean up stale empty nick // clean up stale empty nick
@ -739,34 +947,33 @@ func joinChannel(c *Client, name string) {
c.channels[name] = ch c.channels[name] = ch
ch.mu.Unlock() ch.mu.Unlock()
// echo JOIN to client
fmt.Fprintf(c.conn, ":%s JOIN %s\r\n", c.nick, name) fmt.Fprintf(c.conn, ":%s JOIN %s\r\n", c.nick, name)
// broadcast JOIN to others ch.mu.RLock()
for _, member := range ch.members { members := make([]*Client, 0, len(ch.members))
for _, m := range ch.members {
members = append(members, m)
}
ch.mu.RUnlock()
for _, member := range members {
if member != c { if member != c {
fmt.Fprintf(member.conn, ":%s JOIN %s\r\n", c.nick, name) fmt.Fprintf(member.conn, ":%s JOIN %s\r\n", c.nick, name)
} }
} }
// topic
if ch.description != "" { if ch.description != "" {
c.fwrite(":server 332 %s %s :%s\r\n", c.nick, name, ch.description) c.fwrite(":server 332 %s %s :%s\r\n", c.nick, name, ch.description)
} else { } else {
c.fwrite(":server 331 %s %s :No topic is set\r\n", c.nick, name) c.fwrite(":server 331 %s %s :No topic is set\r\n", c.nick, name)
} }
// names list
c.sendNames(ch) c.sendNames(ch)
// MODE if op
if ch.ops[c.nick] { if ch.ops[c.nick] {
for _, member := range ch.members { for _, member := range members {
member.fwrite(":server MODE %s +o %s\r\n", name, c.nick) member.fwrite(":server MODE %s +o %s\r\n", name, c.nick)
} }
} }
} }
func (c *Client) partChannel(name string) { func (c *Client) partChannel(name string) {
name = normalizeChannelName(name)
chMu.Lock() chMu.Lock()
ch, ok := channels[name] ch, ok := channels[name]
chMu.Unlock() chMu.Unlock()
@ -781,7 +988,7 @@ func (c *Client) partChannel(name string) {
fmt.Fprintf(member.conn, ":%s PART %s\r\n", c.nick, name) fmt.Fprintf(member.conn, ":%s PART %s\r\n", c.nick, name)
} }
ch.mu.Unlock() ch.mu.Unlock()
// broadcastNames(ch) //broadcastNames(ch)
} }
func addChannel(name string, description string) { func addChannel(name string, description string) {
name = normalizeChannelName(name) name = normalizeChannelName(name)
@ -798,11 +1005,11 @@ func addChannel(name string, description string) {
} }
} }
func sendMessage(sender *Client, target, text string) { func sendMessage(sender *Client, target, text string) {
// TODO: handle when a user sends a message to a channel theyre not in
cliMu.Lock() cliMu.Lock()
if c, ok := clients[target]; ok { if c, ok := clients[target]; ok {
c.fwrite(":%s!%s@%s PRIVMSG %s :%s\r\n", sender.nick, sender.user, sender.host, target, text) c.fwrite(":%s!%s@%s PRIVMSG %s :%s\r\n", sender.nick, sender.user, sender.host, target, text)
cliMu.Unlock() cliMu.Unlock()
storeDirectMessage(sender, target, text)
return return
} }
cliMu.Unlock() cliMu.Unlock()
@ -810,6 +1017,7 @@ func sendMessage(sender *Client, target, text string) {
ch, ok := channels[target] ch, ok := channels[target]
chMu.Unlock() chMu.Unlock()
if !ok { if !ok {
sender.fwrite(":server 401 %s %s :No such nick/channel\r\n", sender.nick, target)
return return
} }
ch.mu.RLock() ch.mu.RLock()
@ -831,10 +1039,12 @@ func sendMessage(sender *Client, target, text string) {
} }
func (c *Client) checkRegistration() { func (c *Client) checkRegistration() {
if c.nick != "" && c.user != "" && !c.registered { if c.nick != "" && c.user != "" && !c.registered {
if c.saslRequired && !c.saslComplete { if !c.saslComplete {
c.fwrite(":server 464 %s :You must authenticate before registering\r\n", c.nick)
return return
} }
c.registered = true c.registered = true
notifyMonitorsOnline(c.nick)
c.fwrite(":server 001 %s :Welcome to Gocirc, %s!\r\n", c.nick, c.nick) c.fwrite(":server 001 %s :Welcome to Gocirc, %s!\r\n", c.nick, c.nick)
c.fwrite(":server 002 %s :Your host is server\r\n", c.nick) c.fwrite(":server 002 %s :Your host is server\r\n", c.nick)
c.fwrite(":server 003 %s :This server has %d users right now\r\n", c.nick, len(clients)) c.fwrite(":server 003 %s :This server has %d users right now\r\n", c.nick, len(clients))
@ -847,7 +1057,7 @@ func (c *Client) checkRegistration() {
} else { } else {
c.fwrite("%s :MOTD File is missing", c.nick) c.fwrite("%s :MOTD File is missing", c.nick)
} }
fmt.Fprintf(c.conn, ":server 376 %s :End of MOTD\r\n", c.nick) c.fwrite(":server 376 %s :End of MOTD\r\n", c.nick)
} }
} }
func reloadMOTD() { func reloadMOTD() {
@ -876,7 +1086,7 @@ func (c *Client) performWhois(nick string) {
c.fwrite(":server 318 %s %s :End of WHOIS list\r\n", c.nick, nick) c.fwrite(":server 318 %s %s :End of WHOIS list\r\n", c.nick, nick)
} }
func (c *Client) sendUnregisteredNotice() { func (c *Client) sendUnregisteredNotice() {
fmt.Fprintf(c.conn, ":server 451 * :You have not registered\r\n") c.fwrite(":server 451 * :You have not registered\r\n")
} }
func showWho(c *Client, name string) { func showWho(c *Client, name string) {
name = normalizeChannelName(name) name = normalizeChannelName(name)
@ -889,10 +1099,9 @@ func showWho(c *Client, name string) {
ch.mu.Lock() ch.mu.Lock()
defer ch.mu.Unlock() defer ch.mu.Unlock()
for _, member := range ch.members { for _, member := range ch.members {
fmt.Fprintf(c.conn, ":server 352 %s %s %s server %s H :0 %s\r\n", c.fwrite(":server 352 %s %s %s server %s H :0 %s\r\n", c.nick, name, member.host, member.nick, member.nick)
c.nick, name, member.host, member.nick, member.nick)
} }
fmt.Fprintf(c.conn, ":server 315 %s %s :End of WHO list\r\n", c.nick, name) c.fwrite(":server 315 %s %s :End of WHO list\r\n", c.nick, name)
} }
func (c *Client) handleList() { func (c *Client) handleList() {
chMu.Lock() chMu.Lock()
@ -911,9 +1120,9 @@ func (c *Client) handleList() {
} }
func storeMessage(ch *Channel, sender *Client, target, text string) { func storeMessage(ch *Channel, sender *Client, target, text string) {
msg := HistoryMessage{ msg := HistoryMessage{
MsgID: fmt.Sprintf("%d", time.Now().UnixNano()), MsgID: fmt.Sprintf("%019d", time.Now().UnixNano()),
Time: time.Now(), Time: time.Now(),
Prefix: fmt.Sprintf("%s!%s@%s", sender.nick, sender.user, "host"), // for privacy reasons Prefix: fmt.Sprintf("%s!%s@%s", sender.nick, sender.user, sender.host),
Command: "PRIVMSG", Command: "PRIVMSG",
Params: []string{target}, Params: []string{target},
Text: text, Text: text,
@ -922,18 +1131,34 @@ func storeMessage(ch *Channel, sender *Client, target, text string) {
if len(ch.history) > 1000 { if len(ch.history) > 1000 {
ch.history = ch.history[1:] // drop oldest ch.history = ch.history[1:] // drop oldest
} }
persistMessage(msg, sender.nick, target)
}
func storeDirectMessage(sender *Client, target, text string) {
msg := HistoryMessage{
MsgID: fmt.Sprintf("%019d", time.Now().UnixNano()),
Time: time.Now(),
Prefix: fmt.Sprintf("%s!%s@%s", sender.nick, sender.user, sender.host),
Command: "PRIVMSG",
Params: []string{target},
Text: text,
}
persistMessage(msg, sender.nick, target)
}
func persistMessage(msg HistoryMessage, nick, target string) {
_, err := db.Exec(`INSERT INTO messages(msgid, nick, timestamp, channel, prefix, command, msg) _, err := db.Exec(`INSERT INTO messages(msgid, nick, timestamp, channel, prefix, command, msg)
VALUES(?, ?, ?, ?, ?, ?, ?)`, VALUES(?, ?, ?, ?, ?, ?, ?)`,
msg.MsgID, msg.MsgID,
sender.nick, nick,
msg.Time.UTC().Format(time.RFC3339), msg.Time.UnixNano(),
target, target,
msg.Prefix, msg.Prefix,
msg.Command, msg.Command,
msg.Text, msg.Text,
) )
if err != nil { if err != nil {
log.Printf("storeMessage error: %v", err) log.Printf("persistMessage error: %v", err)
} }
} }
func nicknameExists(nick string) bool { func nicknameExists(nick string) bool {
@ -946,23 +1171,35 @@ func (c *Client) cleanup() {
cliMu.Lock() cliMu.Lock()
delete(clients, c.nick) delete(clients, c.nick)
cliMu.Unlock() cliMu.Unlock()
notifyMonitorsOffline(c.nick)
for name, ch := range c.channels { for name, ch := range c.channels {
ch.mu.Lock() ch.mu.Lock()
delete(ch.members, c.nick) delete(ch.members, c.nick)
for nick, member := range ch.members { ch.mu.Unlock()
member.fwrite(":%s PART %s\r\n", c.nick, name) ch.mu.RLock()
if member == c { snapshot := make([]*Client, 0, len(ch.members))
delete(ch.members, nick) for _, member := range ch.members {
snapshot = append(snapshot, member)
}
ch.mu.RUnlock()
for _, member := range snapshot {
if member.nick != c.nick {
member.fwrite(":%s PART %s\r\n", c.nick, name)
} }
} }
ch.mu.Unlock()
delete(c.channels, name) delete(c.channels, name)
} }
cliMu.RLock() cliMu.RLock()
snapshot := make([]*Client, 0, len(clients))
for _, other := range clients { for _, other := range clients {
other.fwrite(":%s QUIT :Client disconnected\r\n", c.nick) snapshot = append(snapshot, other)
} }
cliMu.RUnlock() cliMu.RUnlock()
for _, other := range snapshot {
other.fwrite(":%s QUIT :Client disconnected\r\n", c.nick)
}
} }
func broadcastNames(ch *Channel) { func broadcastNames(ch *Channel) {
@ -1038,20 +1275,10 @@ func sqlinitDB() error {
if err != nil { if err != nil {
return err return err
} }
/*
msg := HistoryMessage{
MsgID: fmt.Sprintf("%d", time.Now().UnixNano()),
Time: time.Now(),
Prefix: fmt.Sprintf("%s!%s@%s", sender.nick, sender.user, sender.host),
Command: "PRIVMSG",
Params: []string{target},
Text: text,
}
*/
stmt := `CREATE TABLE IF NOT EXISTS messages( stmt := `CREATE TABLE IF NOT EXISTS messages(
msgid TEXT PRIMARY KEY, msgid TEXT PRIMARY KEY,
nick TEXT, nick TEXT,
timestamp TEXT, timestamp INTEGER,
channel TEXT, channel TEXT,
prefix TEXT, prefix TEXT,
command TEXT, command TEXT,
@ -1064,11 +1291,34 @@ func scanRows(rows *sql.Rows, channel string) []HistoryMessage {
var msgs []HistoryMessage var msgs []HistoryMessage
for rows.Next() { for rows.Next() {
var m HistoryMessage var m HistoryMessage
var ts string var tsInt int64
rows.Scan(&m.MsgID, &ts, &m.Prefix, &m.Command, &m.Text) if err := rows.Scan(&m.MsgID, &tsInt, &m.Prefix, &m.Command, &m.Text); err != nil {
m.Time, _ = time.Parse(time.RFC3339, ts) continue
}
m.Time = time.Unix(0, tsInt).UTC()
m.Params = []string{channel} m.Params = []string{channel}
msgs = append(msgs, m) msgs = append(msgs, m)
} }
return msgs return msgs
} }
// IRC MONITOR command stuff
func notifyMonitorsOnline(nick string) {
cliMu.RLock()
defer cliMu.RUnlock()
for _, c := range clients {
if c.monitorTargets[nick] {
c.fwrite(":server 730 %s %s :is online\r\n", c.nick, nick)
}
}
}
func notifyMonitorsOffline(nick string) {
cliMu.RLock()
defer cliMu.RUnlock()
for _, c := range clients {
if c.monitorTargets[nick] {
c.fwrite(":server 731 %s %s :is offline\r\n", c.nick, nick)
}
}
}