This commit is contained in:
khlieng 2015-01-17 02:37:21 +01:00
commit 508a04cf4c
30 changed files with 1545 additions and 0 deletions

3
.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
bin/
client/dist/
client/node_modules/

78
client/gulpfile.js Normal file
View File

@ -0,0 +1,78 @@
var gulp = require('gulp');
var gulpif = require('gulp-if');
var minifyHTML = require('gulp-minify-html');
var minifyCSS = require('gulp-minify-css');
var autoprefixer = require('gulp-autoprefixer');
var uglify = require('gulp-uglify');
var browserify = require('browserify');
var source = require('vinyl-source-stream');
var streamify = require('gulp-streamify');
var reactify = require('reactify');
var strictify = require('strictify');
var watchify = require('watchify');
var argv = require('yargs')
.alias('p', 'production')
.argv;
if (argv.production) {
process.env['NODE_ENV'] = 'production';
}
gulp.task('html', function() {
gulp.src('./src/*.html')
.pipe(minifyHTML())
.pipe(gulp.dest('./dist'));
});
gulp.task('css', function() {
gulp.src('./src/*.css')
.pipe(autoprefixer())
.pipe(minifyCSS())
.pipe(gulp.dest('./dist'));
});
gulp.task('js', function() {
return js(false);
});
function js(watch) {
var bundler, rebundle;
bundler = browserify('./src/js/app.js', {
debug: !argv.production,
cache: {},
packageCache: {},
fullPaths: watch
});
if (watch) {
bundler = watchify(bundler);
}
bundler
.transform(reactify)
.transform(strictify);
rebundle = function() {
var stream = bundler.bundle();
stream.on('error', console.log);
return stream
.pipe(source('bundle.js'))
.pipe(gulpif(argv.production, streamify(uglify())))
.pipe(gulp.dest('./dist'));
};
bundler.on('time', function(time) {
console.log('JS bundle: ' + time + ' ms');
});
bundler.on('update', rebundle);
return rebundle();
}
gulp.task('watch', ['default'], function() {
gulp.watch('./src/*.html', ['html']);
gulp.watch('./src/*.css', ['css']);
return js(true);
});
gulp.task('default', ['html', 'css', 'js']);

27
client/package.json Normal file
View File

@ -0,0 +1,27 @@
{
"name": "irc",
"version": "0.0.0",
"description": "",
"main": "index.js",
"devDependencies": {
"yargs": "~1.3.3",
"strictify": "~0.2.0",
"vinyl-source-stream": "~1.0.0",
"gulp-if": "~1.2.5",
"gulp": "~3.8.10",
"gulp-uglify": "~1.0.2",
"gulp-minify-css": "~0.3.11",
"gulp-streamify": "0.0.5",
"gulp-minify-html": "~0.1.8",
"watchify": "~2.2.1",
"browserify": "~8.0.3",
"gulp-autoprefixer": "~2.0.0",
"reactify": "~0.17.1"
},
"dependencies": {
"lodash": "~2.4.1",
"reflux": "~0.2.2",
"react-router": "~0.11.6",
"react": "~0.12.2"
}
}

11
client/src/index.html Normal file
View File

@ -0,0 +1,11 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>IRC</title>
<link href='http://fonts.googleapis.com/css?family=Inconsolata' rel='stylesheet' type='text/css'>
<link href="style.css" rel="stylesheet">
</head>
<body>
<script src="bundle.js"></script>
</body>
</html>

View File

@ -0,0 +1,37 @@
var Reflux = require('reflux');
var sock = require('../socket.js')('/ws');
var channelActions = Reflux.createActions([
'join',
'joined',
'part',
'parted',
'setUsers',
'load'
]);
channelActions.join.preEmit = function(data) {
sock.send('join', data);
};
channelActions.part.preEmit = function(data) {
sock.send('part', data);
};
sock.on('join', function(data) {
channelActions.joined(data.user, data.server, data.channels[0]);
});
sock.on('part', function(data) {
channelActions.parted(data.user, data.server, data.channels[0]);
});
sock.on('users', function(data) {
channelActions.setUsers(data.users, data.server, data.channel);
});
sock.on('channels', function(data) {
channelActions.load(data);
});
module.exports = channelActions;

View File

@ -0,0 +1,13 @@
var Reflux = require('reflux');
var messageActions = Reflux.createActions([
'send',
'add',
'selectTab'
]);
messageActions.send.preEmit = function() {
};
module.exports = messageActions;

View File

@ -0,0 +1,13 @@
var Reflux = require('reflux');
var sock = require('../socket.js')('/ws');
var serverActions = Reflux.createActions([
'connect',
'disconnect'
]);
serverActions.connect.preEmit = function(data) {
sock.send('connect', data);
};
module.exports = serverActions;

View File

@ -0,0 +1,7 @@
var Reflux = require('reflux');
var tabActions = Reflux.createActions([
'select'
]);
module.exports = tabActions;

77
client/src/js/app.js Normal file
View File

@ -0,0 +1,77 @@
var React = require('react');
var _ = require('lodash');
var sock = require('./socket')('/ws');
var util = require('./util');
var App = require('./components/App.jsx');
var messageActions = require('./actions/message.js');
var tabActions = require('./actions/tab.js');
var serverActions = require('./actions/server.js');
var channelActions = require('./actions/channel.js');
React.render(<App />, document.body);
var uuid = localStorage.uuid || (localStorage.uuid = util.UUID());
tabActions.select('irc.freenode.net');
sock.on('connect', function() {
sock.send('uuid', uuid);
serverActions.connect({
server: 'irc.freenode.net',
nick: 'test' + Math.floor(Math.random() * 99999),
username: 'user'
});
channelActions.join({
server: 'irc.freenode.net',
channels: [ '#stuff', '#go-nuts' ]
});
});
channelActions.joined.listen(function(user, server, channel) {
messageActions.add({
server: server,
from: '',
to: channel,
message: user + ' joined the channel'
});
});
channelActions.parted.listen(function(user, server, channel) {
messageActions.add({
server: server,
from: '',
to: channel,
message: user + ' left the channel'
});
});
sock.on('message', function(data) {
messageActions.add(data);
});
sock.on('pm', function(data) {
messageActions.add(data);
});
sock.on('topic', function(data) {
messageActions.add({
server: data.server,
from: '',
to: data.channel,
message: data.topic
});
});
sock.on('motd', function(data) {
_.each(data.content.split('\n'), function(line) {
messageActions.add({
server: data.server,
from: '',
to: data.server,
message: line
});
});
});

View File

@ -0,0 +1,18 @@
var React = require('react');
var TabList = require('./TabList.jsx');
var MessageBox = require('./MessageBox.jsx');
var UserList = require('./UserList.jsx');
var App = React.createClass({
render: function() {
return (
<div>
<TabList />
<MessageBox />
<UserList />
</div>
);
}
});
module.exports = App;

View File

@ -0,0 +1,44 @@
var React = require('react');
var Reflux = require('reflux');
var _ = require('lodash');
var messageStore = require('../stores/message.js');
var selectedTabStore = require('../stores/selectedTab.js');
var MessageBox = React.createClass({
mixins: [
Reflux.connect(messageStore, 'messages'),
Reflux.connect(selectedTabStore, 'selectedTab')
],
getInitialState: function() {
return {
messages: messageStore.getState(),
selectedTab: selectedTabStore.getState()
};
},
componentWillUpdate: function() {
var el = this.getDOMNode();
this.autoScroll = el.scrollTop + el.offsetHeight === el.scrollHeight;
},
componentDidUpdate: function() {
if (this.autoScroll) {
var el = this.getDOMNode();
el.scrollTop = el.scrollHeight;
}
},
render: function() {
var tab = this.state.selectedTab.channel || this.state.selectedTab.server;
var messages = _.map(this.state.messages[tab], function(message) {
return <p>{message.from ? message.from + ': ' : null}{message.message}</p>;
});
return (
<div className="messagebox">{messages}</div>
);
}
});
module.exports = MessageBox;

View File

@ -0,0 +1,41 @@
var React = require('react');
var Reflux = require('reflux');
var _ = require('lodash');
var serverStore = require('../stores/server.js');
var channelStore = require('../stores/channel.js');
var selectedTabStore = require('../stores/selectedTab.js');
var tabActions = require('../actions/tab.js');
var TabList = React.createClass({
mixins: [
Reflux.connect(serverStore, 'servers'),
Reflux.connect(channelStore, 'channels'),
Reflux.connect(selectedTabStore, 'selectedTab')
],
getInitialState: function() {
return {
servers: serverStore.getState(),
channels: channelStore.getState(),
selectedTab: selectedTabStore.getState()
};
},
render: function() {
var self = this;
var tabs = _.map(this.state.channels, function(server, address) {
var channels = _.map(server, function(channel, name) {
return <p onClick={tabActions.select.bind(null, address, name)}>{name}</p>;
});
channels.unshift(<p onClick={tabActions.select.bind(null, address, null)}>{address}</p>);
return channels;
});
return (
<div className="tablist">{tabs}</div>
);
}
});
module.exports = TabList;

View File

@ -0,0 +1,36 @@
var React = require('react');
var Reflux = require('reflux');
var _ = require('lodash');
var channelStore = require('../stores/channel.js');
var selectedTabStore = require('../stores/selectedTab.js');
var UserList = React.createClass({
mixins: [
Reflux.connect(channelStore, 'channels'),
Reflux.connect(selectedTabStore, 'selectedTab')
],
getInitialState: function() {
return {
channels: channelStore.getState(),
selectedTab: selectedTabStore.getState()
};
},
render: function() {
var users = null;
var tab = this.state.selectedTab;
if (tab.channel) {
users = _.map(this.state.channels[tab.server][tab.channel].users, function(user) {
return <p>{user}</p>;
});
}
return (
<div className="userlist">{users}</div>
);
}
});
module.exports = UserList;

40
client/src/js/socket.js Normal file
View File

@ -0,0 +1,40 @@
var EventEmitter = require('events').EventEmitter;
var _ = require('lodash');
var sockets = {};
function createSocket(path) {
if (sockets[path]) {
return sockets[path];
} else {
var ws = new WebSocket('ws://' + window.location.host + path);
var sock = {
send: function(type, data) {
ws.send(JSON.stringify({ type: type, request: data }));
}
};
_.extend(sock, EventEmitter.prototype);
sockets[path] = sock;
ws.onopen = function() {
sock.emit('connect');
};
ws.onclose = function() {
sock.emit('disconnect');
};
ws.onmessage = function(e) {
var msg = JSON.parse(e.data);
sock.emit(msg.type, msg.response);
};
return sock;
}
}
module.exports = createSocket;

View File

@ -0,0 +1,58 @@
var Reflux = require('reflux');
var _ = require('lodash');
var actions = require('../actions/channel.js');
var channels = {};
function initChannel(server, channel) {
if (!(server in channels)) {
channels[server] = {};
channels[server][channel] = { users: [] };
} else if (!(channel in channels[server])) {
channels[server][channel] = { users: [] };
}
}
var channelStore = Reflux.createStore({
init: function() {
this.listenToMany(actions);
},
joined: function(user, server, channel) {
initChannel(server, channel);
channels[server][channel].users.push(user);
this.trigger(channels);
},
part: function(data) {
_.each(data.channels, function(channel) {
delete channels[data.server][channel];
});
this.trigger(channels);
},
parted: function(user, server, channel) {
_.pull(channels[server][channel].users, user);
this.trigger(channels);
},
setUsers: function(users, server, channel) {
initChannel(server, channel);
channels[server][channel].users = users;
this.trigger(channels);
},
load: function(storedChannels) {
_.each(storedChannels, function(channel) {
initChannel(channel.server, channel.name);
channels[channel.server][channel.name].users = channel.users;
});
this.trigger(channels);
},
getState: function() {
return channels;
}
});
module.exports = channelStore;

View File

@ -0,0 +1,31 @@
var Reflux = require('reflux');
var actions = require('../actions/message.js');
var messages = {};
var messageStore = Reflux.createStore({
init: function() {
this.listenToMany(actions);
},
add: function(message) {
var dest = message.to || message.from;
if (message.from.indexOf('.') !== -1) {
dest = message.server;
}
if (!(dest in messages)) {
messages[dest] = [message];
} else {
messages[dest].push(message);
}
this.trigger(messages);
},
getState: function() {
return messages;
}
});
module.exports = messageStore;

View File

@ -0,0 +1,22 @@
var Reflux = require('reflux');
var actions = require('../actions/tab.js');
var selectedTab = {};
var selectedTabStore = Reflux.createStore({
init: function() {
this.listenToMany(actions);
},
select: function(server, channel) {
selectedTab.server = server;
selectedTab.channel = channel;
this.trigger(selectedTab);
},
getState: function() {
return selectedTab;
}
});
module.exports = selectedTabStore;

View File

@ -0,0 +1,21 @@
var Reflux = require('reflux');
var actions = require('../actions/server.js');
var servers = {};
var serverStore = Reflux.createStore({
init: function() {
this.listenToMany(actions);
},
connect: function(data) {
servers[data.server] = data;
this.trigger(servers);
},
getState: function() {
return servers;
}
});
module.exports = serverStore;

6
client/src/js/util.js Normal file
View File

@ -0,0 +1,6 @@
exports.UUID = function() {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
var r = Math.random()*16|0, v = c == 'x' ? r : (r&0x3|0x8);
return v.toString(16);
});
};

53
client/src/style.css Normal file
View File

@ -0,0 +1,53 @@
* {
margin: 0;
padding: 0;
}
body {
font-family: Inconsolata, sans-serif;
background: #111;
color: #FFF;
}
p {
line-height: 1.5;
}
.tablist {
position: fixed;
left: 0;
top: 0;
bottom: 0;
right: 200px;
padding: 20px;
overflow: auto;
}
.tablist p {
cursor: pointer;
}
.tablist p:hover {
color: #AAA;
}
.messagebox {
position: fixed;
left: 200px;
top: 0;
bottom: 0;
right: 200px;
padding: 20px;
overflow: auto;
z-index: 1;
}
.userlist {
position: fixed;
top: 0;
bottom: 0;
right: 0;
width: 200px;
padding: 20px;
overflow: auto;
}

207
irc.go Normal file
View File

@ -0,0 +1,207 @@
package main
import (
"bufio"
"crypto/tls"
"fmt"
"net"
"strings"
)
const (
PING = "PING"
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_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
Messages chan *Message
Server string
Host string
TLS bool
TLSConfig *tls.Config
nick string
Username string
Realname string
}
func NewIRC(nick, username string) *IRC {
return &IRC{
nick: nick,
Username: username,
Realname: nick,
Messages: make(chan *Message, 32),
}
}
func (i *IRC) Connect(address string) {
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
if i.TLS {
if i.TLSConfig == nil {
i.TLSConfig = &tls.Config{InsecureSkipVerify: true}
}
i.conn, _ = tls.Dial("tcp", address, i.TLSConfig)
} else {
i.conn, _ = net.Dial("tcp", address)
}
i.reader = bufio.NewReader(i.conn)
i.Nick(i.nick)
i.User(i.Username, i.Realname)
go i.recv()
}
func (i *IRC) Pass(password string) {
i.Write("PASS " + password)
}
func (i *IRC) Nick(nick string) {
i.Write("NICK " + nick)
}
func (i *IRC) User(username, realname string) {
i.Writef("USER %s 0 * :%s", username, realname)
}
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) 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) Topic(channel string) {
i.Write("TOPIC " + channel)
}
func (i *IRC) Whois(nick string) {
i.Write("WHOIS " + nick)
}
func (i *IRC) Quit() {
i.Write("QUIT")
i.conn.Close()
}
func (i *IRC) Write(data string) {
fmt.Fprint(i.conn, data+"\r\n")
}
func (i *IRC) Writef(format string, a ...interface{}) {
fmt.Fprintf(i.conn, format+"\r\n", a...)
}
func (i *IRC) recv() {
defer i.conn.Close()
for {
line, err := i.reader.ReadString('\n')
if err != nil {
return
}
msg := parseMessage(line)
msg.Prefix = parseUser(msg.Prefix)
switch msg.Command {
case PING:
i.Write("PONG :" + msg.Trailing)
}
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.LastIndex(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:]
}
return &msg
}
func parseUser(user string) string {
if i := strings.Index(user, "!"); i > 0 {
return user[:i]
}
return user
}

52
json_types.go Normal file
View File

@ -0,0 +1,52 @@
package main
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 {
Server string `json:"server"`
Nick string `json:"nick"`
Username string `json:"username"`
}
type Join struct {
Server string `json:"server"`
User string `json:"user"`
Channels []string `json:"channels"`
}
type Chat struct {
Server string `json:"server"`
From string `json:"from"`
To string `json:"to"`
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"`
}

57
main.go Normal file
View File

@ -0,0 +1,57 @@
package main
import (
"log"
"net/http"
"sync"
"golang.org/x/net/websocket"
"github.com/khlieng/irc/storage"
)
var (
channelStore *storage.ChannelStore
sessions map[string]*Session
sessionLock sync.Mutex
)
func main() {
defer storage.Cleanup()
channelStore = storage.NewChannelStore()
sessions = make(map[string]*Session)
/*for _, user := range storage.LoadUsers() {
channels := user.GetChannels()
for _, server := range user.GetServers() {
session := NewSession()
session.user = user
sessions[user.UUID] = session
irc := NewIRC(server.Nick, server.Username)
irc.TLS = true
irc.Connect(server.Address)
session.setIRC(irc.Host, irc)
go session.write()
go handleMessages(irc, session)
var joining []string
for _, channel := range channels {
if channel.Server == server.Address {
joining = append(joining, channel.Name)
}
}
irc.Join(joining...)
}
}*/
http.Handle("/", http.FileServer(http.Dir("client/dist")))
http.Handle("/ws", websocket.Handler(handleWS))
log.Println("Listening on port 1337")
http.ListenAndServe(":1337", nil)
}

147
message_handler.go Normal file
View File

@ -0,0 +1,147 @@
package main
import (
"bytes"
"fmt"
"strings"
"github.com/khlieng/irc/storage"
)
func handleMessages(irc *IRC, session *Session) {
userBuffers := make(map[string][]string)
var motd MOTD
var motdContent bytes.Buffer
for msg := range irc.Messages {
switch msg.Command {
case JOIN:
user := parseUser(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.nick {
session.user.AddChannel(storage.Channel{
Server: irc.Host,
Name: msg.Params[0],
})
}
case PART:
user := parseUser(msg.Prefix)
session.sendJSON("part", Join{
Server: irc.Host,
User: user,
Channels: msg.Params,
})
channelStore.RemoveUser(user, irc.Host, msg.Params[0])
if user == irc.nick {
session.user.RemoveChannel(irc.Host, msg.Params[0])
}
case PRIVMSG, NOTICE:
if msg.Params[0] == irc.nick {
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,
})
}
case QUIT:
/*
session.sendJSON("quit", Quit{
Server: irc.Host,
User: user,
})
*/
case RPL_WELCOME,
RPL_YOURHOST,
RPL_LUSERCLIENT,
RPL_LUSEROP,
RPL_LUSERUNKNOWN,
RPL_LUSERCHANNELS,
RPL_LUSERME:
session.sendJSON("pm", Chat{
Server: irc.Host,
From: msg.Prefix,
Message: strings.Join(append(msg.Params[1:], msg.Trailing), " "),
})
case RPL_TOPIC:
session.sendJSON("topic", Topic{
Server: irc.Host,
Channel: msg.Params[1],
Topic: msg.Trailing,
})
case RPL_NAMREPLY:
users := strings.Split(msg.Trailing, " ")
for i, user := range users {
users[i] = strings.TrimLeft(user, "@+")
}
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:
motdContent.WriteString(msg.Trailing)
motdContent.WriteRune('\n')
case RPL_ENDOFMOTD:
motd.Content = motdContent.String()
session.sendJSON("motd", motd)
motdContent.Reset()
motd = MOTD{}
default:
printMessage(msg, irc)
}
}
}
func printMessage(msg *Message, irc *IRC) {
fmt.Printf("%s: %s %s %s\n", irc.nick, msg.Prefix, msg.Command, msg.Params)
if msg.Trailing != "" {
fmt.Println(msg.Trailing)
}
fmt.Println()
}

76
session.go Normal file
View File

@ -0,0 +1,76 @@
package main
import (
"encoding/json"
"sync"
"golang.org/x/net/websocket"
"github.com/khlieng/irc/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()
defer s.ircLock.Unlock()
irc, ok := s.irc[server]
return irc, ok
}
func (s *Session) setIRC(server string, irc *IRC) {
s.ircLock.Lock()
s.irc[server] = irc
s.ircLock.Unlock()
}
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) write() {
for res := range s.out {
s.wsLock.Lock()
for _, ws := range s.ws {
ws.In <- res
}
s.wsLock.Unlock()
}
}

68
storage/channel.go Normal file
View File

@ -0,0 +1,68 @@
package storage
import (
"sync"
)
type ChannelStore struct {
data map[string]map[string][]string
lock sync.Mutex
}
func NewChannelStore() *ChannelStore {
return &ChannelStore{
data: make(map[string]map[string][]string),
}
}
func (c *ChannelStore) GetUsers(server, channel string) []string {
c.lock.Lock()
users := make([]string, len(c.data[server][channel]))
copy(users, c.data[server][channel])
c.lock.Unlock()
return users
}
func (c *ChannelStore) SetUsers(users []string, server, channel string) {
c.lock.Lock()
if _, ok := c.data[server]; !ok {
c.data[server] = make(map[string][]string)
}
c.data[server][channel] = users
c.lock.Unlock()
}
func (c *ChannelStore) AddUser(user, server, channel string) {
c.lock.Lock()
if _, ok := c.data[server]; !ok {
c.data[server] = make(map[string][]string)
}
if users, ok := c.data[server][channel]; ok {
c.data[server][channel] = append(users, user)
} else {
c.data[server][channel] = []string{user}
}
c.lock.Unlock()
}
func (c *ChannelStore) RemoveUser(user, server, channel string) {
c.lock.Lock()
defer c.lock.Unlock()
for i, u := range c.data[server][channel] {
if u == user {
users := c.data[server][channel]
c.data[server][channel] = append(users[:i], users[i+1:]...)
return
}
}
}

22
storage/init.go Normal file
View File

@ -0,0 +1,22 @@
package storage
import (
"github.com/boltdb/bolt"
)
var db *bolt.DB
func init() {
db, _ = bolt.Open("data.db", 0600, nil)
db.Update(func(tx *bolt.Tx) error {
tx.CreateBucketIfNotExists([]byte("Users"))
tx.CreateBucketIfNotExists([]byte("Servers"))
tx.CreateBucketIfNotExists([]byte("Channels"))
return nil
})
}
func Cleanup() {
db.Close()
}

137
storage/user.go Normal file
View File

@ -0,0 +1,137 @@
package storage
import (
"bytes"
"encoding/json"
"github.com/boltdb/bolt"
)
type Server struct {
Address string
Nick string
Username string
Realname string
}
type Channel struct {
Server string `json:"server"`
Name string `json:"name"`
Users []string `json:"users"`
}
type User struct {
UUID string
}
func NewUser(uuid string) User {
user := User{
UUID: uuid,
}
go db.Update(func(tx *bolt.Tx) error {
b := tx.Bucket([]byte("Users"))
data, _ := json.Marshal(user)
b.Put([]byte(uuid), data)
return nil
})
return user
}
func LoadUsers() []User {
var users []User
db.View(func(tx *bolt.Tx) error {
b := tx.Bucket([]byte("Users"))
b.ForEach(func(k, v []byte) error {
users = append(users, User{string(k)})
return nil
})
return nil
})
return users
}
func (u User) GetServers() []Server {
var servers []Server
db.View(func(tx *bolt.Tx) error {
c := tx.Bucket([]byte("Servers")).Cursor()
prefix := []byte(u.UUID)
for k, v := c.Seek(prefix); bytes.HasPrefix(k, prefix); k, v = c.Next() {
var server Server
json.Unmarshal(v, &server)
servers = append(servers, server)
}
return nil
})
return servers
}
func (u User) GetChannels() []Channel {
var channels []Channel
db.View(func(tx *bolt.Tx) error {
c := tx.Bucket([]byte("Channels")).Cursor()
prefix := []byte(u.UUID)
for k, v := c.Seek(prefix); bytes.HasPrefix(k, prefix); k, v = c.Next() {
var channel Channel
json.Unmarshal(v, &channel)
channels = append(channels, channel)
}
return nil
})
return channels
}
func (u User) AddServer(server Server) {
db.Update(func(tx *bolt.Tx) error {
b := tx.Bucket([]byte("Servers"))
data, _ := json.Marshal(server)
b.Put([]byte(u.UUID+":"+server.Address), data)
return nil
})
}
func (u User) AddChannel(channel Channel) {
db.Update(func(tx *bolt.Tx) error {
b := tx.Bucket([]byte("Channels"))
data, _ := json.Marshal(channel)
b.Put([]byte(u.UUID+":"+channel.Server+":"+channel.Name), data)
return nil
})
}
func (u User) RemoveServer(address string) {
db.Update(func(tx *bolt.Tx) error {
tx.Bucket([]byte("Channels")).Delete([]byte(u.UUID + ":" + address))
return nil
})
}
func (u User) RemoveChannel(server, channel string) {
db.Update(func(tx *bolt.Tx) error {
tx.Bucket([]byte("Servers")).Delete([]byte(u.UUID + ":" + server + ":" + channel))
return nil
})
}

24
websocket.go Normal file
View File

@ -0,0 +1,24 @@
package main
import (
"golang.org/x/net/websocket"
)
type WebSocket struct {
conn *websocket.Conn
In chan []byte
}
func NewWebSocket(ws *websocket.Conn) *WebSocket {
return &WebSocket{
conn: ws,
In: make(chan []byte, 32),
}
}
func (w *WebSocket) write() {
for data := range w.In {
w.conn.Write(data)
}
}

119
websocket_handler.go Normal file
View File

@ -0,0 +1,119 @@
package main
import (
"encoding/json"
"log"
"golang.org/x/net/websocket"
"github.com/khlieng/irc/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 existing IRC connections")
channels := session.user.GetChannels()
for i, channel := range channels {
channels[i].Users = channelStore.GetUsers(channel.Server, channel.Name)
}
session.sendJSON("channels", channels)
} 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 = true
irc.Connect(data.Server)
session.setIRC(irc.Host, irc)
go handleMessages(irc, session)
session.user.AddServer(storage.Server{
Address: irc.Host,
Nick: data.Nick,
Username: data.Username,
})
} 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 Join
json.Unmarshal(req.Request, &data)
if irc, ok := session.getIRC(data.Server); ok {
irc.Part(data.Channels...)
}
case "chat":
var data Chat
json.Unmarshal(req.Request, &data)
if irc, ok := session.getIRC(data.Server); ok {
irc.Privmsg(data.To, data.Message)
}
}
}
}