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