3
0
mirror of https://github.com/ergochat/ergo.git synced 2024-12-25 20:22:38 +01:00

cleanup and reorganization

This commit is contained in:
Jeremy Latt 2012-12-09 12:51:50 -08:00
parent 2dba5f4c47
commit 55f7c89468
8 changed files with 254 additions and 196 deletions

View File

@ -11,3 +11,9 @@ I wanted to learn Go.
### What's with the name?
"Ergonomadic" is an anagram of "Go IRC Daemon".
### Helpful Documentation
- [IRC Channel Management](http://tools.ietf.org/html/rfc2811)
- [IRC Client Protocol](http://tools.ietf.org/html/rfc2812)
- [IRC Server Protocol](http://tools.ietf.org/html/rfc2813)

View File

@ -1,5 +1,9 @@
package irc
import (
"sort"
)
type Channel struct {
name string
key string
@ -12,10 +16,19 @@ type Channel struct {
type ChannelSet map[*Channel]bool
// NewChannel creates a new channel from a `Server` and a `name` string, which
// must be unique on the server.
func NewChannel(s *Server, name string) *Channel {
return &Channel{name: name, members: make(ClientSet), invites: make(map[string]bool), server: s}
return &Channel{
name: name,
members: make(ClientSet),
invites: make(map[string]bool),
server: s,
}
}
// Send a `Reply` to all `Client`s of the `Channel`. Skip `fromClient`, if it is
// provided.
func (ch *Channel) Send(reply Reply, fromClient *Client) {
for client := range ch.members {
if client != fromClient {
@ -24,7 +37,20 @@ func (ch *Channel) Send(reply Reply, fromClient *Client) {
}
}
func (ch *Channel) Nicks() []string {
nicks := make([]string, len(ch.members))
i := 0
for member := range ch.members {
nicks[i] = member.Nick()
i++
}
sort.Strings(nicks)
return nicks
}
//
// channel functionality
//
func (ch *Channel) Join(cl *Client, key string) {
if ch.key != key {
@ -42,10 +68,7 @@ func (ch *Channel) Join(cl *Client, key string) {
ch.Send(RplJoin(ch, cl), nil)
ch.GetTopic(cl)
for member := range ch.members {
cl.send <- RplNamReply(ch, member)
}
cl.send <- RplNamReply(ch)
cl.send <- RplEndOfNames(ch.server)
}

View File

@ -16,12 +16,19 @@ type Client struct {
registered bool
invisible bool
channels ChannelSet
server *Server
}
type ClientSet map[*Client]bool
func NewClient(conn net.Conn) *Client {
client := &Client{conn: conn, recv: StringReadChan(conn), channels: make(ChannelSet), hostname: LookupHostname(conn.RemoteAddr())}
func NewClient(server *Server, conn net.Conn) *Client {
client := &Client{
channels: make(ChannelSet),
conn: conn,
hostname: LookupHostname(conn.RemoteAddr()),
recv: StringReadChan(conn),
server: server,
}
client.SetReplyToStringChan()
return client
}
@ -38,12 +45,14 @@ func (c *Client) SetReplyToStringChan() {
}
// Adapt `chan string` to a `chan Message`.
func (c *Client) Communicate(server *Server) {
func (c *Client) Communicate() {
for str := range c.recv {
m := ParseMessage(str)
if m != nil {
server.recv <- &ClientMessage{c, m}
m, err := ParseMessage(str)
if err != nil {
// TODO handle error
return
}
c.server.recv <- &ClientMessage{c, m}
}
}

View File

@ -1,15 +1,30 @@
package irc
import (
"errors"
"fmt"
"regexp"
"strconv"
"strings"
)
type Message interface {
Handle(s *Server, c *Client)
}
var (
ErrNotEnoughArgs = errors.New("not enough arguments")
ErrUModeUnknownFlag = errors.New("unknown umode flag")
)
// unknown
type UnknownMessage struct {
command string
}
// NB: no constructor, created on demand in parser for invalid messages.
func (m *UnknownMessage) Handle(s *Server, c *Client) {
c.send <- ErrUnknownCommand(s, m.command)
}
@ -21,6 +36,17 @@ type PingMessage struct {
server2 string
}
func NewPingMessage(args []string) (Message, error) {
if len(args) < 1 {
return nil, ErrNotEnoughArgs
}
msg := &PingMessage{server: args[0]}
if len(args) > 1 {
msg.server2 = args[1]
}
return msg, nil
}
func (m *PingMessage) Handle(s *Server, c *Client) {
c.send <- RplPong(s)
}
@ -32,8 +58,19 @@ type PongMessage struct {
server2 string
}
func NewPongMessage(args []string) (Message, error) {
if len(args) < 1 {
return nil, ErrNotEnoughArgs
}
message := &PongMessage{server1: args[0]}
if len(args) > 1 {
message.server2 = args[1]
}
return message, nil
}
func (m *PongMessage) Handle(s *Server, c *Client) {
// TODO update client atime
// no-op
}
// NICK
@ -42,6 +79,13 @@ type NickMessage struct {
nickname string
}
func NewNickMessage(args []string) (Message, error) {
if len(args) != 1 {
return nil, ErrNotEnoughArgs
}
return &NickMessage{args[0]}, nil
}
func (m *NickMessage) Handle(s *Server, c *Client) {
s.ChangeNick(c, m.nickname)
}
@ -55,8 +99,24 @@ type UserMessage struct {
realname string
}
func NewUserMessage(args []string) (Message, error) {
if len(args) != 4 {
return nil, ErrNotEnoughArgs
}
msg := &UserMessage{
user: args[0],
unused: args[2],
realname: args[3],
}
mode, err := strconv.ParseUint(args[1], 10, 8)
if err == nil {
msg.mode = uint8(mode)
}
return msg, nil
}
func (m *UserMessage) Handle(s *Server, c *Client) {
s.Register(c, m.user, m.realname)
s.UserLogin(c, m.user, m.realname)
}
// QUIT
@ -65,6 +125,14 @@ type QuitMessage struct {
message string
}
func NewQuitMessage(args []string) (Message, error) {
msg := &QuitMessage{}
if len(args) > 0 {
msg.message = args[0]
}
return msg, nil
}
func (m *QuitMessage) Handle(s *Server, c *Client) {
s.Quit(c, m.message)
}
@ -76,6 +144,28 @@ type ModeMessage struct {
modes []string
}
var MODE_RE = regexp.MustCompile("^[-+][a-zA-Z]+$")
func NewModeMessage(args []string) (Message, error) {
if len(args) < 1 {
return nil, ErrNotEnoughArgs
}
msg := &ModeMessage{
nickname: args[0],
}
for _, arg := range args[1:] {
if !MODE_RE.MatchString(arg) {
return nil, ErrUModeUnknownFlag
}
prefix := arg[0]
for _, c := range arg[1:] {
mode := fmt.Sprintf("%c%c", prefix, c)
msg.modes = append(msg.modes, mode)
}
}
return msg, nil
}
func (m *ModeMessage) Handle(s *Server, c *Client) {
if m.nickname != c.nick {
c.send <- ErrUsersDontMatch(s)
@ -92,6 +182,22 @@ type JoinMessage struct {
zero bool
}
func NewJoinMessage(args []string) (Message, error) {
msg := &JoinMessage{}
if len(args) > 0 {
if args[0] == "0" {
msg.zero = true
} else {
msg.channels = strings.Split(args[0], ",")
}
if len(args) > 1 {
msg.keys = strings.Split(args[1], ",")
}
}
return msg, nil
}
func (m *JoinMessage) Handle(s *Server, c *Client) {
if m.zero {
for channel := range c.channels {
@ -116,6 +222,17 @@ type PartMessage struct {
message string
}
func NewPartMessage(args []string) (Message, error) {
if len(args) < 1 {
return nil, ErrNotEnoughArgs
}
msg := &PartMessage{channels: strings.Split(args[0], ",")}
if len(args) > 1 {
msg.message = args[1]
}
return msg, nil
}
func (m *PartMessage) Handle(s *Server, c *Client) {
for _, chname := range m.channels {
channel := s.channels[chname]
@ -136,6 +253,16 @@ type PrivMsgMessage struct {
message string
}
func NewPrivMsgMessage(args []string) (Message, error) {
if len(args) < 2 {
return nil, ErrNotEnoughArgs
}
return &PrivMsgMessage{
target: args[0],
message: args[1],
}, nil
}
func (m *PrivMsgMessage) TargetIsChannel() bool {
switch m.target[0] {
case '&', '#', '+', '!':
@ -169,6 +296,17 @@ type TopicMessage struct {
topic string
}
func NewTopicMessage(args []string) (Message, error) {
if len(args) < 1 {
return nil, ErrNotEnoughArgs
}
msg := &TopicMessage{channel: args[0]}
if len(args) > 1 {
msg.topic = args[1]
}
return msg, nil
}
func (m *TopicMessage) Handle(s *Server, c *Client) {
channel := s.channels[m.channel]
if channel == nil {

View File

@ -1,6 +1,3 @@
// channel management: http://tools.ietf.org/html/rfc2811
// client protocol: http://tools.ietf.org/html/rfc2812
// server protocol: http://tools.ietf.org/html/rfc2813
package irc
const (
@ -8,16 +5,19 @@ const (
)
const (
RPL_WELCOME = "001"
RPL_YOURHOST = "002"
RPL_CREATED = "003"
RPL_MYINFO = "004"
RPL_UMODEIS = "221"
RPL_NOTOPIC = "331"
RPL_TOPIC = "332"
RPL_NAMREPLY = "353"
RPL_ENDOFNAMES = "366"
RPL_INFO = "371"
// # numeric codes
// ## reply codes
RPL_WELCOME = "001"
RPL_YOURHOST = "002"
RPL_CREATED = "003"
RPL_MYINFO = "004"
RPL_UMODEIS = "221"
RPL_NOTOPIC = "331"
RPL_TOPIC = "332"
RPL_NAMREPLY = "353"
RPL_ENDOFNAMES = "366"
RPL_INFO = "371"
// ## error codes
ERR_NOSUCHNICK = "401"
ERR_NOSUCHSERVER = "402"
ERR_NOSUCHCHANNEL = "403"
@ -29,9 +29,10 @@ const (
ERR_INVITEONLYCHANNEL = "473"
ERR_BADCHANNELKEY = "475"
ERR_USERSDONTMATCH = "502"
RPL_JOIN = "JOIN"
RPL_NICK = "NICK"
RPL_PART = "PART"
RPL_PONG = "PONG"
RPL_PRIVMSG = "PRIVMSG"
// # message codes
RPL_JOIN = "JOIN"
RPL_NICK = "NICK"
RPL_PART = "PART"
RPL_PONG = "PONG"
RPL_PRIVMSG = "PRIVMSG"
)

View File

@ -1,13 +1,11 @@
package irc
import (
"fmt"
"regexp"
"strconv"
"errors"
"strings"
)
var commands = map[string]func([]string) Message{
var commands = map[string]func([]string) (Message, error){
"JOIN": NewJoinMessage,
"MODE": NewModeMessage,
"NICK": NewNickMessage,
@ -20,169 +18,43 @@ var commands = map[string]func([]string) Message{
"USER": NewUserMessage,
}
func ParseMessage(line string) Message {
var (
ErrParseMessage = errors.New("failed to parse message")
)
func ParseMessage(line string) (msg Message, err error) {
command, args := parseLine(line)
constructor, ok := commands[command]
var msg Message
if ok {
msg = constructor(args)
}
if msg == nil {
if !ok {
msg = &UnknownMessage{command}
return
}
return msg
msg, err = constructor(args)
return
}
func parseArg(line string) (string, string) {
func parseArg(line string) (arg string, rest string) {
if line == "" {
return "", ""
return
}
if strings.HasPrefix(line, ":") {
return line[1:], ""
arg = line[1:]
} else {
parts := strings.SplitN(line, " ", 2)
arg = parts[0]
if len(parts) > 1 {
rest = parts[1]
}
}
parts := strings.SplitN(line, " ", 2)
arg := parts[0]
rest := ""
if len(parts) > 1 {
rest = parts[1]
}
return arg, rest
return
}
func parseLine(line string) (string, []string) {
args := make([]string, 0)
func parseLine(line string) (command string, args []string) {
args = make([]string, 0)
for arg, rest := parseArg(line); arg != ""; arg, rest = parseArg(rest) {
args = append(args, arg)
}
return args[0], args[1:]
}
// []string => Message constructors
func NewNickMessage(args []string) Message {
if len(args) != 1 {
return nil
}
return &NickMessage{args[0]}
}
func NewPingMessage(args []string) Message {
if len(args) < 1 {
return nil
}
message := &PingMessage{server: args[0]}
if len(args) > 1 {
message.server2 = args[1]
}
return message
}
func NewPongMessage(args []string) Message {
if len(args) < 1 {
return nil
}
message := &PongMessage{server1: args[0]}
if len(args) > 1 {
message.server2 = args[1]
}
return message
}
func NewQuitMessage(args []string) Message {
msg := QuitMessage{}
if len(args) > 0 {
msg.message = args[0]
}
return &msg
}
func NewUserMessage(args []string) Message {
if len(args) != 4 {
return nil
}
msg := new(UserMessage)
msg.user = args[0]
mode, err := strconv.ParseUint(args[1], 10, 8)
if err == nil {
msg.mode = uint8(mode)
}
msg.unused = args[2]
msg.realname = args[3]
return msg
}
var MODE_RE = regexp.MustCompile("^[-+][a-zA-Z]+$")
func NewModeMessage(args []string) Message {
if len(args) < 1 {
return nil
}
msg := new(ModeMessage)
msg.nickname = args[0]
for _, arg := range args[1:] {
if !MODE_RE.MatchString(arg) {
// TODO invalid args
return nil
}
prefix := arg[0]
for _, c := range arg[1:] {
mode := fmt.Sprintf("%c%c", prefix, c)
msg.modes = append(msg.modes, mode)
}
}
return msg
}
func NewJoinMessage(args []string) Message {
msg := new(JoinMessage)
if len(args) > 0 {
if args[0] == "0" {
msg.zero = true
} else {
msg.channels = strings.Split(args[0], ",")
}
if len(args) > 1 {
msg.keys = strings.Split(args[1], ",")
}
}
return msg
}
func NewPartMessage(args []string) Message {
if len(args) < 1 {
return nil
}
msg := new(PartMessage)
msg.channels = strings.Split(args[0], ",")
if len(args) > 1 {
msg.message = args[1]
}
return msg
}
func NewPrivMsgMessage(args []string) Message {
if len(args) < 2 {
return nil
}
return &PrivMsgMessage{target: args[0], message: args[1]}
}
func NewTopicMessage(args []string) Message {
if len(args) < 1 {
return nil
}
message := &TopicMessage{channel: args[0]}
if len(args) > 1 {
message.topic = args[1]
}
return message
command, args = args[0], args[1:]
return
}

View File

@ -2,6 +2,7 @@ package irc
import (
"fmt"
"strings"
"time"
)
@ -75,7 +76,7 @@ func RplCreated(server *Server) Reply {
}
func RplMyInfo(server *Server) Reply {
return NewReply(server, RPL_MYINFO, server.name+" i ik")
return NewReply(server, RPL_MYINFO, fmt.Sprintf("%s %s i ik", server.name, VERSION))
}
func RplUModeIs(server *Server, client *Client) Reply {
@ -92,9 +93,9 @@ func RplTopic(channel *Channel) Reply {
return &ChannelReply{NewReply(channel.server, RPL_TOPIC, fmt.Sprintf("%s :%s", channel.name, channel.topic)), channel}
}
func RplNamReply(channel *Channel, client *Client) Reply {
func RplNamReply(channel *Channel) Reply {
// TODO multiple names and splitting based on message size
return NewReply(channel.server, RPL_NAMREPLY, fmt.Sprintf("=%s :+%s", channel.name, client.Nick()))
return NewReply(channel.server, RPL_NAMREPLY, fmt.Sprintf("= %s :%s", channel.name, strings.Join(channel.Nicks(), " ")))
}
func RplEndOfNames(source Identifier) Reply {

View File

@ -22,7 +22,13 @@ type ClientMessage struct {
func NewServer(name string) *Server {
recv := make(chan *ClientMessage)
server := &Server{ctime: time.Now(), name: name, recv: recv, nicks: make(map[string]*Client), channels: make(map[string]*Channel)}
server := &Server{
ctime: time.Now(),
name: name,
recv: recv,
nicks: make(map[string]*Client),
channels: make(map[string]*Channel),
}
go func() {
for m := range recv {
m.message.Handle(server, m.client)
@ -47,7 +53,7 @@ func (s *Server) Listen(addr string) {
continue
}
log.Print("Server.Listen: accepted ", conn.RemoteAddr())
go NewClient(conn).Communicate(s)
go NewClient(s, conn).Communicate()
}
}
@ -65,6 +71,7 @@ func (s *Server) GetOrMakeChannel(name string) *Channel {
// Send a message to clients of channels fromClient is a member.
func (s *Server) SendToInterestedClients(fromClient *Client, reply Reply) {
clients := make(map[*Client]bool)
clients[fromClient] = true
for channel := range fromClient.channels {
for client := range channel.members {
clients[client] = true
@ -84,28 +91,29 @@ func (s *Server) ChangeNick(c *Client, newNick string) {
return
}
s.SendToInterestedClients(c, RplNick(c, newNick))
if c.nick != "" {
delete(s.nicks, c.nick)
}
c.nick = newNick
s.nicks[c.nick] = c
s.TryRegister(c)
s.SendToInterestedClients(c, RplNick(c, newNick))
c.nick = newNick
s.tryRegister(c)
}
func (s *Server) Register(c *Client, user string, realName string) {
func (s *Server) UserLogin(c *Client, user string, realName string) {
if c.username != "" {
c.send <- ErrAlreadyRegistered(s)
return
}
c.username, c.realname = user, realName
s.TryRegister(c)
s.tryRegister(c)
}
func (s *Server) TryRegister(c *Client) {
func (s *Server) tryRegister(c *Client) {
if !c.registered && c.HasNick() && c.HasUser() {
c.registered = true
c.send <- RplWelcome(s, c)