Compare commits

..

No commits in common. "7b555dc9986153af221b344557c1d7b0a28a2078" and "49ae78f98bf67d88b548c41225cc87aeba1975f5" have entirely different histories.

3 changed files with 100 additions and 385 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,38 +1,3 @@
# 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

388
main.go
View File

@ -6,13 +6,11 @@ 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"
@ -33,7 +31,6 @@ 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
@ -124,10 +121,6 @@ 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)
@ -136,27 +129,9 @@ 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)
} }
var ln net.Listener ln, err := net.Listen("tcp", fmt.Sprintf(":%d", cfg.Port))
if *tlsEnabled {
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 { if err != nil {
log.Fatalf("Error listening: %v\n", err) 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)
@ -181,7 +156,6 @@ func handleConn(conn net.Conn) {
conn: conn, conn: conn,
host: host, host: host,
channels: make(map[string]*Channel), channels: make(map[string]*Channel),
monitorTargets: make(map[string]bool),
registered: false, registered: false,
} }
defer client.cleanup() defer client.cleanup()
@ -212,10 +186,6 @@ 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 != "" {
@ -266,14 +236,12 @@ 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 server-time message-tags draft/chathistory batch\r\n") client.fwrite(":server CAP * LS :sasl=PLAIN sasl chathistory\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])
} }
@ -491,105 +459,16 @@ 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) < 1 { if len(msg.Params) < 3 {
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 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 "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) 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
}
subcmd := strings.ToUpper(msg.Params[0])
switch subcmd {
case "+":
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)
@ -599,17 +478,6 @@ 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()
@ -628,29 +496,24 @@ 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, isChannel := channels[target] ch, ok := channels[target]
chMu.Unlock() chMu.Unlock()
if !ok {
var lockCh func() c.fwrite(":server 403 %s %s :No such channel\r\n", c.nick, target)
var unlockCh func() return
if isChannel {
lockCh = ch.mu.RLock
unlockCh = ch.mu.RUnlock
lockCh()
defer unlockCh()
} }
ch.mu.RLock()
defer ch.mu.RUnlock()
var msgs []HistoryMessage var msgs []HistoryMessage
switch subcmd { switch subcmd {
case "LATEST": case "LATEST":
if isChannel && count <= len(ch.history) { if count <= len(ch.history) {
msgs = ch.history[len(ch.history)-count:] msgs = ch.history[len(ch.history)-count:]
} else { } else {
extra := count extra := count - len(ch.history)
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=?
@ -661,15 +524,9 @@ func (c *Client) serveHistory(subcmd, target string, count int, params []string)
return return
} }
defer rows.Close() defer rows.Close()
dbMsgs := scanRows(rows, target) dbMsgs := scanRows(rows, target)
if isChannel {
msgs = append(dbMsgs, ch.history...) 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":
@ -678,6 +535,7 @@ 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 < ?
@ -688,24 +546,7 @@ func (c *Client) serveHistory(subcmd, target string, count int, params []string)
return return
} }
defer rows.Close() defer rows.Close()
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) msgs = scanRows(rows, target)
case "AFTER": case "AFTER":
@ -713,70 +554,21 @@ func (c *Client) serveHistory(subcmd, target string, count int, params []string)
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=")
arg := params[2] rows, err := db.Query(`SELECT msgid, timestamp, prefix, command, msg
var rows *sql.Rows
var err error
if strings.HasPrefix(arg, "msgid=") {
msgid := strings.TrimPrefix(arg, "msgid=")
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 ASC LIMIT ?`, ORDER BY timestamp ASC LIMIT ?`,
target, msgid, count) 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)
@ -805,14 +597,13 @@ func (c *Client) serveHistory(subcmd, target string, count int, params []string)
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.RFC3339Nano)) m.Time.UTC().Format(time.RFC3339))
c.fwrite("%s :%s %s %s :%s\r\n", c.fwrite("%s :%s %s %s :%s\r\n",
tags, tags,
m.Prefix, m.Prefix,
@ -829,12 +620,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 tsInt int64 var ts string
err := row.Scan(&m.MsgID, &tsInt, &m.Prefix, &m.Command, &m.Text) err := row.Scan(&m.MsgID, &ts, &m.Prefix, &m.Command, &m.Text)
if err != nil { if err != nil {
return nil, err return nil, err
} }
m.Time = time.Unix(0, tsInt).UTC() m.Time, _ = time.Parse(time.RFC3339, ts)
m.Params = []string{channel} m.Params = []string{channel}
return &m, nil return &m, nil
} }
@ -851,7 +642,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] && !globalOps[sender.nick] { if !ch.ops[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
} }
@ -885,7 +676,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] && !globalOps[sender.nick] { if !ch.ops[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
} }
@ -937,7 +728,8 @@ 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.nick, c.nick, name) c.fwrite(":server 443 %s %s %s :is already on channel\r\n",
c.nick, c.nick, name)
return return
} }
// clean up stale empty nick // clean up stale empty nick
@ -947,33 +739,34 @@ 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)
ch.mu.RLock() // broadcast JOIN to others
members := make([]*Client, 0, len(ch.members)) for _, member := range 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 members { for _, member := range ch.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()
@ -1005,11 +798,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()
@ -1017,7 +810,6 @@ 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()
@ -1039,12 +831,10 @@ 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.saslComplete { if c.saslRequired && !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))
@ -1057,7 +847,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)
} }
c.fwrite(":server 376 %s :End of MOTD\r\n", c.nick) fmt.Fprintf(c.conn, ":server 376 %s :End of MOTD\r\n", c.nick)
} }
} }
func reloadMOTD() { func reloadMOTD() {
@ -1086,7 +876,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() {
c.fwrite(":server 451 * :You have not registered\r\n") fmt.Fprintf(c.conn, ":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)
@ -1099,9 +889,10 @@ 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 {
c.fwrite(":server 352 %s %s %s server %s H :0 %s\r\n", c.nick, name, member.host, member.nick, member.nick) fmt.Fprintf(c.conn, ":server 352 %s %s %s server %s H :0 %s\r\n",
c.nick, name, member.host, member.nick, member.nick)
} }
c.fwrite(":server 315 %s %s :End of WHO list\r\n", c.nick, name) fmt.Fprintf(c.conn, ":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()
@ -1120,9 +911,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("%019d", time.Now().UnixNano()), MsgID: fmt.Sprintf("%d", time.Now().UnixNano()),
Time: time.Now(), Time: time.Now(),
Prefix: fmt.Sprintf("%s!%s@%s", sender.nick, sender.user, sender.host), Prefix: fmt.Sprintf("%s!%s@%s", sender.nick, sender.user, "host"), // for privacy reasons
Command: "PRIVMSG", Command: "PRIVMSG",
Params: []string{target}, Params: []string{target},
Text: text, Text: text,
@ -1131,34 +922,18 @@ 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,
nick, sender.nick,
msg.Time.UnixNano(), msg.Time.UTC().Format(time.RFC3339),
target, target,
msg.Prefix, msg.Prefix,
msg.Command, msg.Command,
msg.Text, msg.Text,
) )
if err != nil { if err != nil {
log.Printf("persistMessage error: %v", err) log.Printf("storeMessage error: %v", err)
} }
} }
func nicknameExists(nick string) bool { func nicknameExists(nick string) bool {
@ -1171,35 +946,23 @@ 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)
ch.mu.Unlock() for nick, member := range ch.members {
ch.mu.RLock()
snapshot := make([]*Client, 0, len(ch.members))
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) member.fwrite(":%s PART %s\r\n", c.nick, name)
if member == c {
delete(ch.members, nick)
} }
} }
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 {
snapshot = append(snapshot, other)
}
cliMu.RUnlock()
for _, other := range snapshot {
other.fwrite(":%s QUIT :Client disconnected\r\n", c.nick) other.fwrite(":%s QUIT :Client disconnected\r\n", c.nick)
} }
cliMu.RUnlock()
} }
func broadcastNames(ch *Channel) { func broadcastNames(ch *Channel) {
@ -1275,10 +1038,20 @@ 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 INTEGER, timestamp TEXT,
channel TEXT, channel TEXT,
prefix TEXT, prefix TEXT,
command TEXT, command TEXT,
@ -1291,34 +1064,11 @@ 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 tsInt int64 var ts string
if err := rows.Scan(&m.MsgID, &tsInt, &m.Prefix, &m.Command, &m.Text); err != nil { rows.Scan(&m.MsgID, &ts, &m.Prefix, &m.Command, &m.Text)
continue m.Time, _ = time.Parse(time.RFC3339, ts)
}
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)
}
}
}