Init
This commit is contained in:
commit
508a04cf4c
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
bin/
|
||||
client/dist/
|
||||
client/node_modules/
|
78
client/gulpfile.js
Normal file
78
client/gulpfile.js
Normal 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
27
client/package.json
Normal 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
11
client/src/index.html
Normal 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>
|
37
client/src/js/actions/channel.js
Normal file
37
client/src/js/actions/channel.js
Normal 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;
|
13
client/src/js/actions/message.js
Normal file
13
client/src/js/actions/message.js
Normal file
@ -0,0 +1,13 @@
|
||||
var Reflux = require('reflux');
|
||||
|
||||
var messageActions = Reflux.createActions([
|
||||
'send',
|
||||
'add',
|
||||
'selectTab'
|
||||
]);
|
||||
|
||||
messageActions.send.preEmit = function() {
|
||||
|
||||
};
|
||||
|
||||
module.exports = messageActions;
|
13
client/src/js/actions/server.js
Normal file
13
client/src/js/actions/server.js
Normal 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;
|
7
client/src/js/actions/tab.js
Normal file
7
client/src/js/actions/tab.js
Normal 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
77
client/src/js/app.js
Normal 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
|
||||
});
|
||||
});
|
||||
});
|
18
client/src/js/components/App.jsx
Normal file
18
client/src/js/components/App.jsx
Normal 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;
|
44
client/src/js/components/MessageBox.jsx
Normal file
44
client/src/js/components/MessageBox.jsx
Normal 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;
|
41
client/src/js/components/TabList.jsx
Normal file
41
client/src/js/components/TabList.jsx
Normal 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;
|
36
client/src/js/components/UserList.jsx
Normal file
36
client/src/js/components/UserList.jsx
Normal 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
40
client/src/js/socket.js
Normal 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;
|
58
client/src/js/stores/channel.js
Normal file
58
client/src/js/stores/channel.js
Normal 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;
|
31
client/src/js/stores/message.js
Normal file
31
client/src/js/stores/message.js
Normal 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;
|
22
client/src/js/stores/selectedTab.js
Normal file
22
client/src/js/stores/selectedTab.js
Normal 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;
|
21
client/src/js/stores/server.js
Normal file
21
client/src/js/stores/server.js
Normal 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
6
client/src/js/util.js
Normal 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
53
client/src/style.css
Normal 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
207
irc.go
Normal 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
52
json_types.go
Normal 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
57
main.go
Normal 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
147
message_handler.go
Normal 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
76
session.go
Normal 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
68
storage/channel.go
Normal 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
22
storage/init.go
Normal 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
137
storage/user.go
Normal 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
24
websocket.go
Normal 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
119
websocket_handler.go
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user