Add cobra, move the server code into its own package

This commit is contained in:
khlieng 2015-05-01 22:59:46 +02:00
parent bb729a5c8e
commit e63c012aad
57 changed files with 6910 additions and 145 deletions

589
server/bindata.go Normal file

File diff suppressed because one or more lines are too long

37
server/bindata_fs.go Normal file
View file

@ -0,0 +1,37 @@
package server
import (
"bytes"
"net/http"
"os"
)
type BindataFileSystem struct{}
func (f BindataFileSystem) Open(name string) (http.File, error) {
path := "dist" + name
data, err := Asset(path)
if err != nil {
return nil, err
}
return &BindataFile{bytes.NewReader(data), path}, nil
}
type BindataFile struct {
*bytes.Reader
path string
}
func (f *BindataFile) Close() error {
return nil
}
func (f *BindataFile) Readdir(count int) ([]os.FileInfo, error) {
return make([]os.FileInfo, 0), nil
}
func (f *BindataFile) Stat() (os.FileInfo, error) {
return AssetInfo(f.path)
}

293
server/irc.go Normal file
View file

@ -0,0 +1,293 @@
package server
import (
"bufio"
"crypto/tls"
"fmt"
"log"
"net"
"strings"
"sync"
"time"
)
const (
PING = "PING"
NICK = "NICK"
JOIN = "JOIN"
PART = "PART"
MODE = "MODE"
PRIVMSG = "PRIVMSG"
NOTICE = "NOTICE"
TOPIC = "TOPIC"
QUIT = "QUIT"
RPL_WELCOME = "001"
RPL_YOURHOST = "002"
RPL_CREATED = "003"
RPL_LUSERCLIENT = "251"
RPL_LUSEROP = "252"
RPL_LUSERUNKNOWN = "253"
RPL_LUSERCHANNELS = "254"
RPL_LUSERME = "255"
RPL_AWAY = "301"
RPL_WHOISUSER = "311"
RPL_WHOISSERVER = "312"
RPL_WHOISOPERATOR = "313"
RPL_WHOISIDLE = "317"
RPL_ENDOFWHOIS = "318"
RPL_WHOISCHANNELS = "319"
RPL_TOPIC = "332"
RPL_NAMREPLY = "353"
RPL_ENDOFNAMES = "366"
RPL_MOTD = "372"
RPL_MOTDSTART = "375"
RPL_ENDOFMOTD = "376"
)
type Message struct {
Prefix string
Command string
Params []string
Trailing string
}
type IRC struct {
conn net.Conn
reader *bufio.Reader
out chan string
ready sync.WaitGroup
nick string
nickLock sync.Mutex
Messages chan *Message
Server string
Host string
TLS bool
TLSConfig *tls.Config
Password string
Username string
Realname string
}
func NewIRC(nick, username string) *IRC {
return &IRC{
nick: nick,
Username: username,
Realname: nick,
Messages: make(chan *Message, 32),
out: make(chan string, 32),
}
}
func (i *IRC) Connect(address string) error {
if idx := strings.Index(address, ":"); idx < 0 {
i.Host = address
if i.TLS {
address += ":6697"
} else {
address += ":6667"
}
} else {
i.Host = address[:idx]
}
i.Server = address
dialer := &net.Dialer{Timeout: 30 * time.Second}
if i.TLS {
if i.TLSConfig == nil {
i.TLSConfig = &tls.Config{InsecureSkipVerify: true}
}
if conn, err := tls.DialWithDialer(dialer, "tcp", address, i.TLSConfig); err != nil {
return err
} else {
i.conn = conn
}
} else {
if conn, err := dialer.Dial("tcp", address); err != nil {
return err
} else {
i.conn = conn
}
}
i.reader = bufio.NewReader(i.conn)
if i.Password != "" {
i.Pass(i.Password)
}
i.Nick(i.nick)
i.User(i.Username, i.Realname)
i.ready.Add(1)
go i.send()
go i.recv()
return nil
}
func (i *IRC) Pass(password string) {
i.write("PASS " + password)
}
func (i *IRC) Nick(nick string) {
i.write("NICK " + nick)
i.nickLock.Lock()
i.nick = nick
i.nickLock.Unlock()
}
func (i *IRC) User(username, realname string) {
i.writef("USER %s 0 * :%s", username, realname)
}
func (i *IRC) Oper(name, password string) {
i.Write("OPER " + name + " " + password)
}
func (i *IRC) Mode(target, modes, params string) {
i.Write(strings.TrimRight("MODE "+target+" "+modes+" "+params, " "))
}
func (i *IRC) Quit() {
go func() {
i.ready.Wait()
i.write("QUIT")
i.conn.Close()
}()
}
func (i *IRC) Join(channels ...string) {
i.Write("JOIN " + strings.Join(channels, ","))
}
func (i *IRC) Part(channels ...string) {
i.Write("PART " + strings.Join(channels, ","))
}
func (i *IRC) Topic(channel string) {
i.Write("TOPIC " + channel)
}
func (i *IRC) Invite(nick, channel string) {
i.Write("INVITE " + nick + " " + channel)
}
func (i *IRC) Kick(channel string, users ...string) {
i.Write("KICK " + channel + " " + strings.Join(users, ","))
}
func (i *IRC) Privmsg(target, msg string) {
i.Writef("PRIVMSG %s :%s", target, msg)
}
func (i *IRC) Notice(target, msg string) {
i.Writef("NOTICE %s :%s", target, msg)
}
func (i *IRC) Whois(nick string) {
i.Write("WHOIS " + nick)
}
func (i *IRC) Away(message string) {
i.Write("AWAY :" + message)
}
func (i *IRC) GetNick() string {
i.nickLock.Lock()
defer i.nickLock.Unlock()
return i.nick
}
func (i *IRC) Write(data string) {
i.out <- data + "\r\n"
}
func (i *IRC) Writef(format string, a ...interface{}) {
i.out <- fmt.Sprintf(format+"\r\n", a...)
}
func (i *IRC) write(data string) {
i.conn.Write([]byte(data + "\r\n"))
}
func (i *IRC) writef(format string, a ...interface{}) {
fmt.Fprintf(i.conn, format+"\r\n", a...)
}
func (i *IRC) send() {
i.ready.Wait()
for {
i.conn.Write([]byte(<-i.out))
}
}
func (i *IRC) recv() {
defer i.conn.Close()
for {
line, err := i.reader.ReadString('\n')
if err != nil {
log.Println("IRC connection to", i.Server, "died")
return
}
msg := parseMessage(line)
msg.Prefix = parseUser(msg.Prefix)
switch msg.Command {
case PING:
i.write("PONG :" + msg.Trailing)
case RPL_WELCOME:
i.ready.Done()
}
i.Messages <- msg
}
}
func parseMessage(line string) *Message {
line = strings.Trim(line, "\r\n")
msg := Message{}
cmdStart := 0
cmdEnd := len(line)
if strings.HasPrefix(line, ":") {
cmdStart = strings.Index(line, " ") + 1
msg.Prefix = line[1 : cmdStart-1]
}
if i := strings.Index(line, " :"); i > 0 {
cmdEnd = i
msg.Trailing = line[i+2:]
}
cmd := strings.Split(line[cmdStart:cmdEnd], " ")
msg.Command = cmd[0]
if len(cmd) > 1 {
msg.Params = cmd[1:]
}
if msg.Trailing != "" {
msg.Params = append(msg.Params, msg.Trailing)
}
return &msg
}
func parseUser(user string) string {
if i := strings.Index(user, "!"); i > 0 {
return user[:i]
}
return user
}

117
server/json_types.go Normal file
View file

@ -0,0 +1,117 @@
package server
import (
"encoding/json"
)
type WSRequest struct {
Type string `json:"type"`
Request json.RawMessage `json:"request"`
}
type WSResponse struct {
Type string `json:"type"`
Response *json.RawMessage `json:"response"`
}
type Connect struct {
Name string `json:"name"`
Server string `json:"server"`
TLS bool `json:"tls"`
Password string `json:"password"`
Nick string `json:"nick"`
Username string `json:"username"`
Realname string `json:"realname"`
}
type Nick struct {
Server string `json:"server"`
Old string `json:"old"`
New string `json:"new"`
}
type Join struct {
Server string `json:"server"`
User string `json:"user"`
Channels []string `json:"channels"`
}
type Part struct {
Join
Reason string `json:"reason,omitempty"`
}
type Mode struct {
Server string `json:"server"`
Channel string `json:"channel"`
User string `json:"user"`
Add string `json:"add"`
Remove string `json:"remove"`
}
type Quit struct {
Server string `json:"server"`
User string `json:"user"`
Reason string `json:"reason,omitempty"`
}
type Chat struct {
Server string `json:"server"`
From string `json:"from"`
To string `json:"to,omitempty"`
Message string `json:"message"`
}
type Topic struct {
Server string `json:"server"`
Channel string `json:"channel"`
Topic string `json:"topic"`
}
type Userlist struct {
Server string `json:"server"`
Channel string `json:"channel"`
Users []string `json:"users"`
}
type MOTD struct {
Server string `json:"server"`
Title string `json:"title"`
Content []string `json:"content"`
}
type Invite struct {
Server string `json:"server"`
Channel string `json:"channel"`
User string `json:"user"`
}
type Kick struct {
Server string `json:"server"`
Channel string `json:"channel"`
User string `json:"user"`
}
type Whois struct {
Server string `json:"server"`
User string `json:"user"`
}
type WhoisReply struct {
Nick string `json:"nick"`
Username string `json:"username"`
Host string `json:"host"`
Realname string `json:"realname"`
Server string `json:"server"`
Channels []string `json:"channels"`
}
type Away struct {
Server string `json:"server"`
Message string `json:"message"`
}
type Error struct {
Server string `json:"server"`
Message string `json:"message"`
}

207
server/message_handler.go Normal file
View file

@ -0,0 +1,207 @@
package server
import (
"log"
"strings"
"github.com/khlieng/name_pending/storage"
)
func handleMessages(irc *IRC, session *Session) {
var whois WhoisReply
userBuffers := make(map[string][]string)
var motd MOTD
for msg := range irc.Messages {
switch msg.Command {
case NICK:
session.sendJSON("nick", Nick{
Server: irc.Host,
Old: msg.Prefix,
New: msg.Trailing,
})
channelStore.RenameUser(msg.Prefix, msg.Trailing, irc.Host)
case JOIN:
user := msg.Prefix
session.sendJSON("join", Join{
Server: irc.Host,
User: user,
Channels: msg.Params,
})
channelStore.AddUser(user, irc.Host, msg.Params[0])
if user == irc.GetNick() {
session.user.AddChannel(storage.Channel{
Server: irc.Host,
Name: msg.Params[0],
})
}
case PART:
user := msg.Prefix
session.sendJSON("part", Part{
Join: Join{
Server: irc.Host,
User: user,
Channels: msg.Params,
},
Reason: msg.Trailing,
})
channelStore.RemoveUser(user, irc.Host, msg.Params[0])
if user == irc.GetNick() {
session.user.RemoveChannel(irc.Host, msg.Params[0])
}
case MODE:
target := msg.Params[0]
if len(msg.Params) > 2 && isChannel(target) {
mode := parseMode(msg.Params[1])
mode.Server = irc.Host
mode.Channel = target
mode.User = msg.Params[2]
session.sendJSON("mode", mode)
channelStore.SetMode(irc.Host, target, msg.Params[2], mode.Add, mode.Remove)
}
case PRIVMSG, NOTICE:
if msg.Params[0] == irc.GetNick() {
session.sendJSON("pm", Chat{
Server: irc.Host,
From: msg.Prefix,
Message: msg.Trailing,
})
} else {
session.sendJSON("message", Chat{
Server: irc.Host,
From: msg.Prefix,
To: msg.Params[0],
Message: msg.Trailing,
})
}
if msg.Params[0] != "*" {
session.user.LogMessage(irc.Host, msg.Prefix, msg.Params[0], msg.Trailing)
}
case QUIT:
user := msg.Prefix
session.sendJSON("quit", Quit{
Server: irc.Host,
User: user,
Reason: msg.Trailing,
})
channelStore.RemoveUserAll(user, irc.Host)
case RPL_WELCOME,
RPL_YOURHOST,
RPL_CREATED,
RPL_LUSERCLIENT,
RPL_LUSEROP,
RPL_LUSERUNKNOWN,
RPL_LUSERCHANNELS,
RPL_LUSERME:
session.sendJSON("pm", Chat{
Server: irc.Host,
From: msg.Prefix,
Message: strings.Join(msg.Params[1:], " "),
})
case RPL_WHOISUSER:
whois.Nick = msg.Params[1]
whois.Username = msg.Params[2]
whois.Host = msg.Params[3]
whois.Realname = msg.Params[5]
case RPL_WHOISSERVER:
whois.Server = msg.Params[2]
case RPL_WHOISCHANNELS:
whois.Channels = append(whois.Channels, strings.Split(strings.TrimRight(msg.Trailing, " "), " ")...)
case RPL_ENDOFWHOIS:
session.sendJSON("whois", whois)
whois = WhoisReply{}
case RPL_TOPIC:
session.sendJSON("topic", Topic{
Server: irc.Host,
Channel: msg.Params[1],
Topic: msg.Trailing,
})
channelStore.SetTopic(msg.Trailing, irc.Host, msg.Params[1])
case RPL_NAMREPLY:
users := strings.Split(msg.Trailing, " ")
userBuffer := userBuffers[msg.Params[2]]
userBuffers[msg.Params[2]] = append(userBuffer, users...)
case RPL_ENDOFNAMES:
channel := msg.Params[1]
users := userBuffers[channel]
session.sendJSON("users", Userlist{
Server: irc.Host,
Channel: channel,
Users: users,
})
channelStore.SetUsers(users, irc.Host, channel)
delete(userBuffers, channel)
case RPL_MOTDSTART:
motd.Server = irc.Host
motd.Title = msg.Trailing
case RPL_MOTD:
motd.Content = append(motd.Content, msg.Trailing)
case RPL_ENDOFMOTD:
session.sendJSON("motd", motd)
motd = MOTD{}
default:
printMessage(msg, irc)
}
}
}
func parseMode(mode string) *Mode {
m := Mode{}
add := false
for _, c := range mode {
if c == '+' {
add = true
} else if c == '-' {
add = false
} else if add {
m.Add += string(c)
} else {
m.Remove += string(c)
}
}
return &m
}
func isChannel(s string) bool {
return strings.IndexAny(s, "&#+!") == 0
}
func printMessage(msg *Message, irc *IRC) {
log.Println(irc.GetNick()+":", msg.Prefix, msg.Command, msg.Params, msg.Trailing)
}

124
server/server.go Normal file
View file

@ -0,0 +1,124 @@
package server
import (
"log"
"net/http"
"strconv"
"strings"
"sync"
"github.com/khlieng/name_pending/Godeps/_workspace/src/github.com/julienschmidt/httprouter"
"github.com/khlieng/name_pending/Godeps/_workspace/src/golang.org/x/net/websocket"
"github.com/khlieng/name_pending/storage"
)
var (
channelStore *storage.ChannelStore
sessions map[string]*Session
sessionLock sync.Mutex
fs http.Handler
files []File
)
type File struct {
Path string
ContentType string
}
func Run(port int, development bool) {
defer storage.Cleanup()
channelStore = storage.NewChannelStore()
sessions = make(map[string]*Session)
fs = http.FileServer(BindataFileSystem{})
files = []File{
File{"/bundle.js", "text/javascript"},
File{"/css/style.css", "text/css"},
File{"/css/fontello.css", "text/css"},
File{"/font/fontello.eot", "application/vnd.ms-fontobject"},
File{"/font/fontello.svg", "image/svg+xml"},
File{"/font/fontello.ttf", "application/x-font-ttf"},
File{"/font/fontello.woff", "application/font-woff"},
}
if !development {
reconnect()
}
router := httprouter.New()
router.Handler("GET", "/ws", websocket.Handler(handleWS))
router.NotFound = serveFiles
log.Println("Listening on port", port)
log.Fatal(http.ListenAndServe(":"+strconv.Itoa(port), router))
}
func reconnect() {
for _, user := range storage.LoadUsers() {
session := NewSession()
session.user = user
sessions[user.UUID] = session
go session.write()
channels := user.GetChannels()
for _, server := range user.GetServers() {
irc := NewIRC(server.Nick, server.Username)
irc.TLS = server.TLS
irc.Password = server.Password
irc.Realname = server.Realname
go func() {
err := irc.Connect(server.Address)
if err != nil {
log.Println(err)
} else {
session.setIRC(irc.Host, irc)
go handleMessages(irc, session)
var joining []string
for _, channel := range channels {
if channel.Server == server.Address {
joining = append(joining, channel.Name)
}
}
irc.Join(joining...)
}
}()
}
}
}
func serveFiles(w http.ResponseWriter, r *http.Request) {
var ext string
if strings.Contains(r.Header.Get("Accept-Encoding"), "gzip") {
w.Header().Set("Content-Encoding", "gzip")
ext = ".gz"
}
if r.URL.Path == "/" {
w.Header().Set("Content-Type", "text/html")
r.URL.Path = "/index.html" + ext
fs.ServeHTTP(w, r)
return
}
for _, file := range files {
if strings.HasSuffix(r.URL.Path, file.Path) {
w.Header().Set("Content-Type", file.ContentType)
r.URL.Path = file.Path + ext
fs.ServeHTTP(w, r)
return
}
}
w.Header().Set("Content-Type", "text/html")
r.URL.Path = "/index.html" + ext
fs.ServeHTTP(w, r)
}

96
server/session.go Normal file
View file

@ -0,0 +1,96 @@
package server
import (
"encoding/json"
"sync"
"github.com/khlieng/name_pending/Godeps/_workspace/src/golang.org/x/net/websocket"
"github.com/khlieng/name_pending/storage"
)
type Session struct {
irc map[string]*IRC
ircLock sync.Mutex
ws map[string]*WebSocket
wsLock sync.Mutex
out chan []byte
user *storage.User
}
func NewSession() *Session {
return &Session{
irc: make(map[string]*IRC),
ws: make(map[string]*WebSocket),
out: make(chan []byte, 32),
}
}
func (s *Session) getIRC(server string) (*IRC, bool) {
s.ircLock.Lock()
irc, ok := s.irc[server]
s.ircLock.Unlock()
return irc, ok
}
func (s *Session) setIRC(server string, irc *IRC) {
s.ircLock.Lock()
s.irc[server] = irc
s.ircLock.Unlock()
}
func (s *Session) deleteIRC(server string) {
s.ircLock.Lock()
delete(s.irc, server)
s.ircLock.Unlock()
}
func (s *Session) numIRC() int {
s.ircLock.Lock()
n := len(s.irc)
s.ircLock.Unlock()
return n
}
func (s *Session) setWS(addr string, ws *websocket.Conn) {
socket := NewWebSocket(ws)
go socket.write()
s.wsLock.Lock()
s.ws[addr] = socket
s.wsLock.Unlock()
}
func (s *Session) deleteWS(addr string) {
s.wsLock.Lock()
delete(s.ws, addr)
s.wsLock.Unlock()
}
func (s *Session) sendJSON(t string, v interface{}) {
data, _ := json.Marshal(v)
raw := json.RawMessage(data)
res, _ := json.Marshal(WSResponse{Type: t, Response: &raw})
s.out <- res
}
func (s *Session) sendError(err error, server string) {
s.sendJSON("error", Error{
Server: server,
Message: err.Error(),
})
}
func (s *Session) write() {
for res := range s.out {
s.wsLock.Lock()
for _, ws := range s.ws {
ws.Out <- res
}
s.wsLock.Unlock()
}
}

24
server/websocket.go Normal file
View file

@ -0,0 +1,24 @@
package server
import (
"github.com/khlieng/name_pending/Godeps/_workspace/src/golang.org/x/net/websocket"
)
type WebSocket struct {
conn *websocket.Conn
Out chan []byte
}
func NewWebSocket(ws *websocket.Conn) *WebSocket {
return &WebSocket{
conn: ws,
Out: make(chan []byte, 32),
}
}
func (w *WebSocket) write() {
for {
w.conn.Write(<-w.Out)
}
}

204
server/websocket_handler.go Normal file
View file

@ -0,0 +1,204 @@
package server
import (
"encoding/json"
"log"
"strings"
"github.com/khlieng/name_pending/Godeps/_workspace/src/golang.org/x/net/websocket"
"github.com/khlieng/name_pending/storage"
)
func handleWS(ws *websocket.Conn) {
defer ws.Close()
var session *Session
var UUID string
var req WSRequest
addr := ws.Request().RemoteAddr
log.Println(addr, "connected")
for {
err := websocket.JSON.Receive(ws, &req)
if err != nil {
if session != nil {
session.deleteWS(addr)
}
log.Println(addr, "disconnected")
return
}
switch req.Type {
case "uuid":
json.Unmarshal(req.Request, &UUID)
log.Println(addr, "set UUID", UUID)
sessionLock.Lock()
if storedSession, exists := sessions[UUID]; exists {
sessionLock.Unlock()
session = storedSession
log.Println(addr, "attached to", session.numIRC(), "existing IRC connections")
channels := session.user.GetChannels()
for i, channel := range channels {
channels[i].Topic = channelStore.GetTopic(channel.Server, channel.Name)
}
session.sendJSON("channels", channels)
session.sendJSON("servers", session.user.GetServers())
for _, channel := range channels {
session.sendJSON("users", Userlist{
Server: channel.Server,
Channel: channel.Name,
Users: channelStore.GetUsers(channel.Server, channel.Name),
})
}
} else {
session = NewSession()
session.user = storage.NewUser(UUID)
sessions[UUID] = session
sessionLock.Unlock()
go session.write()
}
session.setWS(addr, ws)
case "connect":
var data Connect
json.Unmarshal(req.Request, &data)
if _, ok := session.getIRC(data.Server); !ok {
log.Println(addr, "connecting to", data.Server)
irc := NewIRC(data.Nick, data.Username)
irc.TLS = data.TLS
irc.Password = data.Password
irc.Realname = data.Realname
if idx := strings.Index(data.Server, ":"); idx < 0 {
session.setIRC(data.Server, irc)
} else {
session.setIRC(data.Server[:idx], irc)
}
go func() {
err := irc.Connect(data.Server)
if err != nil {
session.deleteIRC(irc.Host)
session.sendError(err, irc.Host)
log.Println(err)
} else {
go handleMessages(irc, session)
session.user.AddServer(storage.Server{
Name: data.Name,
Address: irc.Host,
TLS: data.TLS,
Password: data.Password,
Nick: data.Nick,
Username: data.Username,
Realname: data.Realname,
})
}
}()
} else {
log.Println(addr, "already connected to", data.Server)
}
case "join":
var data Join
json.Unmarshal(req.Request, &data)
if irc, ok := session.getIRC(data.Server); ok {
irc.Join(data.Channels...)
}
case "part":
var data Part
json.Unmarshal(req.Request, &data)
if irc, ok := session.getIRC(data.Server); ok {
irc.Part(data.Channels...)
}
case "quit":
var data Quit
json.Unmarshal(req.Request, &data)
if irc, ok := session.getIRC(data.Server); ok {
irc.Quit()
session.deleteIRC(data.Server)
channelStore.RemoveUserAll(irc.GetNick(), data.Server)
session.user.RemoveServer(data.Server)
}
case "chat":
var data Chat
json.Unmarshal(req.Request, &data)
if irc, ok := session.getIRC(data.Server); ok {
irc.Privmsg(data.To, data.Message)
}
case "nick":
var data Nick
json.Unmarshal(req.Request, &data)
if irc, ok := session.getIRC(data.Server); ok {
irc.Nick(data.New)
session.user.SetNick(data.New, data.Server)
}
case "invite":
var data Invite
json.Unmarshal(req.Request, &data)
if irc, ok := session.getIRC(data.Server); ok {
irc.Invite(data.User, data.Channel)
}
case "kick":
var data Invite
json.Unmarshal(req.Request, &data)
if irc, ok := session.getIRC(data.Server); ok {
irc.Kick(data.Channel, data.User)
}
case "whois":
var data Whois
json.Unmarshal(req.Request, &data)
if irc, ok := session.getIRC(data.Server); ok {
irc.Whois(data.User)
}
case "away":
var data Away
json.Unmarshal(req.Request, &data)
if irc, ok := session.getIRC(data.Server); ok {
irc.Away(data.Message)
}
}
}
}