From f42d6011c67e4b31438201db62c24b54d653ea1d Mon Sep 17 00:00:00 2001 From: khlieng Date: Wed, 21 Jan 2015 03:06:34 +0100 Subject: [PATCH] Added title bar and basic message and command input --- .gitignore | 3 +- client/package.json | 2 +- client/src/js/actions/channel.js | 11 +++ client/src/js/actions/message.js | 10 ++- client/src/js/actions/server.js | 1 + client/src/js/app.js | 17 ++--- client/src/js/components/App.jsx | 7 +- client/src/js/components/Chat.jsx | 21 ++++++ client/src/js/components/ChatTitle.jsx | 48 +++++++++++++ client/src/js/components/MessageBox.jsx | 9 ++- client/src/js/components/MessageInput.jsx | 65 ++++++++++++++++++ client/src/js/components/UserList.jsx | 12 ++-- client/src/js/socket.js | 3 +- client/src/js/stores/channel.js | 14 ++++ client/src/js/stores/message.js | 27 ++++++-- client/src/js/stores/selectedTab.js | 17 +++++ client/src/js/stores/server.js | 1 + client/src/style.css | 62 +++++++++++++++-- json_types.go | 5 ++ message_handler.go | 30 ++++---- storage/channel.go | 83 ++++++++++++++++------- storage/user.go | 22 ++++-- websocket_handler.go | 12 ++++ 23 files changed, 399 insertions(+), 83 deletions(-) create mode 100644 client/src/js/components/Chat.jsx create mode 100644 client/src/js/components/ChatTitle.jsx create mode 100644 client/src/js/components/MessageInput.jsx diff --git a/.gitignore b/.gitignore index 41772525..bebc1e38 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ bin/ client/dist/ -client/node_modules/ \ No newline at end of file +client/node_modules/ +data.db \ No newline at end of file diff --git a/client/package.json b/client/package.json index ddcc0988..1773f1e9 100644 --- a/client/package.json +++ b/client/package.json @@ -24,4 +24,4 @@ "react-router": "~0.11.6", "react": "~0.12.2" } -} +} \ No newline at end of file diff --git a/client/src/js/actions/channel.js b/client/src/js/actions/channel.js index be6bb2d6..50e726f5 100644 --- a/client/src/js/actions/channel.js +++ b/client/src/js/actions/channel.js @@ -1,4 +1,5 @@ var Reflux = require('reflux'); + var sock = require('../socket.js')('/ws'); var channelActions = Reflux.createActions([ @@ -6,7 +7,9 @@ var channelActions = Reflux.createActions([ 'joined', 'part', 'parted', + 'quit', 'setUsers', + 'setTopic', 'load' ]); @@ -26,10 +29,18 @@ sock.on('part', function(data) { channelActions.parted(data.user, data.server, data.channels[0]); }); +sock.on('quit', function(data) { + channelActions.quit(data.user, data.server); +}); + sock.on('users', function(data) { channelActions.setUsers(data.users, data.server, data.channel); }); +sock.on('topic', function(data) { + channelActions.setTopic(data.topic, data.server, data.channel); +}); + sock.on('channels', function(data) { channelActions.load(data); }); diff --git a/client/src/js/actions/message.js b/client/src/js/actions/message.js index ac2c2606..0512919b 100644 --- a/client/src/js/actions/message.js +++ b/client/src/js/actions/message.js @@ -1,13 +1,19 @@ var Reflux = require('reflux'); +var sock = require('../socket.js')('/ws'); + var messageActions = Reflux.createActions([ 'send', 'add', 'selectTab' ]); -messageActions.send.preEmit = function() { - +messageActions.send.preEmit = function(message, to, server) { + sock.send('chat', { + server: server, + to: to, + message: message + }); }; module.exports = messageActions; \ No newline at end of file diff --git a/client/src/js/actions/server.js b/client/src/js/actions/server.js index 51a7d569..04e50bd6 100644 --- a/client/src/js/actions/server.js +++ b/client/src/js/actions/server.js @@ -1,4 +1,5 @@ var Reflux = require('reflux'); + var sock = require('../socket.js')('/ws'); var serverActions = Reflux.createActions([ diff --git a/client/src/js/app.js b/client/src/js/app.js index 0f78aef9..bd9d7def 100644 --- a/client/src/js/app.js +++ b/client/src/js/app.js @@ -26,7 +26,7 @@ sock.on('connect', function() { channelActions.join({ server: 'irc.freenode.net', - channels: [ '#stuff', '#go-nuts' ] + channels: [ '#stuff' ] }); }); @@ -35,7 +35,8 @@ channelActions.joined.listen(function(user, server, channel) { server: server, from: '', to: channel, - message: user + ' joined the channel' + message: user + ' joined the channel', + type: 'info' }); }); @@ -44,7 +45,8 @@ channelActions.parted.listen(function(user, server, channel) { server: server, from: '', to: channel, - message: user + ' left the channel' + message: user + ' left the channel', + type: 'info' }); }); @@ -56,15 +58,6 @@ 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({ diff --git a/client/src/js/components/App.jsx b/client/src/js/components/App.jsx index 10ae083c..13d294d7 100644 --- a/client/src/js/components/App.jsx +++ b/client/src/js/components/App.jsx @@ -1,15 +1,14 @@ var React = require('react'); + var TabList = require('./TabList.jsx'); -var MessageBox = require('./MessageBox.jsx'); -var UserList = require('./UserList.jsx'); +var Chat = require('./Chat.jsx'); var App = React.createClass({ render: function() { return (
- - +
); } diff --git a/client/src/js/components/Chat.jsx b/client/src/js/components/Chat.jsx new file mode 100644 index 00000000..22f82fef --- /dev/null +++ b/client/src/js/components/Chat.jsx @@ -0,0 +1,21 @@ +var React = require('react'); + +var ChatTitle = require('./ChatTitle.jsx'); +var MessageBox = require('./MessageBox.jsx'); +var MessageInput = require('./MessageInput.jsx'); +var UserList = require('./UserList.jsx'); + +var Chat = React.createClass({ + render: function() { + return ( +
+ + + + +
+ ) + } +}); + +module.exports = Chat; \ No newline at end of file diff --git a/client/src/js/components/ChatTitle.jsx b/client/src/js/components/ChatTitle.jsx new file mode 100644 index 00000000..03886caa --- /dev/null +++ b/client/src/js/components/ChatTitle.jsx @@ -0,0 +1,48 @@ +var React = require('react'); +var Reflux = require('reflux'); + +var channelStore = require('../stores/channel.js'); +var selectedTabStore = require('../stores/selectedTab.js'); + +var ChatTitle = React.createClass({ + mixins: [ + Reflux.connect(channelStore, 'channels'), + Reflux.connect(selectedTabStore, 'selectedTab') + ], + + getInitialState: function() { + return { + channels: channelStore.getState(), + selectedTab: selectedTabStore.getState() + }; + }, + + render: function() { + var tab = this.state.selectedTab; + var title; + + if (tab.channel) { + var channel = this.state.channels[tab.server][tab.channel]; + if (channel) { + title = tab.channel + title += ' ['; + title += channel.users.length; + title += ']'; + + if (channel.topic) { + title += ': ' + channel.topic; + } + } + } else { + title = tab.server; + } + + return ( +
+ {title} +
+ ); + } +}); + +module.exports = ChatTitle; \ No newline at end of file diff --git a/client/src/js/components/MessageBox.jsx b/client/src/js/components/MessageBox.jsx index 1134b0a2..cd4d0584 100644 --- a/client/src/js/components/MessageBox.jsx +++ b/client/src/js/components/MessageBox.jsx @@ -1,6 +1,7 @@ var React = require('react'); var Reflux = require('reflux'); var _ = require('lodash'); + var messageStore = require('../stores/message.js'); var selectedTabStore = require('../stores/selectedTab.js'); @@ -32,7 +33,13 @@ var MessageBox = React.createClass({ render: function() { var tab = this.state.selectedTab.channel || this.state.selectedTab.server; var messages = _.map(this.state.messages[tab], function(message) { - return

{message.from ? message.from + ': ' : null}{message.message}

; + var messageClass = 'message'; + switch (message.type) { + case 'info': + messageClass += ' message-info'; + break; + } + return

{message.from ? message.from + ': ' : null}{message.message}

; }); return ( diff --git a/client/src/js/components/MessageInput.jsx b/client/src/js/components/MessageInput.jsx new file mode 100644 index 00000000..d9d65bbc --- /dev/null +++ b/client/src/js/components/MessageInput.jsx @@ -0,0 +1,65 @@ +var React = require('react'); +var Reflux = require('reflux'); + +var selectedTabStore = require('../stores/selectedTab.js'); +var messageActions = require('../actions/message.js'); +var channelActions = require('../actions/channel.js'); + +function dispatchCommand(cmd, channel, server) { + var params = cmd.slice(1).split(' '); + + switch (params[0].toLowerCase()) { + case 'join': + if (params[1]) { + channelActions.join({ + server: server, + channels: [params[1]] + }); + } + break; + + case 'part': + if (channel) { + channelActions.part({ + server: server, + channels: [channel] + }); + } + break; + } +} + +var MessageInput = React.createClass({ + mixins: [ + Reflux.connect(selectedTabStore, 'selectedTab') + ], + + getInitialState: function() { + return { + selectedTab: selectedTabStore.getState() + }; + }, + + handleKey: function(e) { + if (e.which === 13 && e.target.value) { + var tab = this.state.selectedTab; + + if (e.target.value.charAt(0) === '/') { + dispatchCommand(e.target.value, tab.channel, tab.server); + } else { + messageActions.send(e.target.value, tab.channel, tab.server); + } + e.target.value = ''; + } + }, + + render: function() { + return ( +
+ +
+ ); + } +}); + +module.exports = MessageInput; \ No newline at end of file diff --git a/client/src/js/components/UserList.jsx b/client/src/js/components/UserList.jsx index fb1d3a26..bdf98639 100644 --- a/client/src/js/components/UserList.jsx +++ b/client/src/js/components/UserList.jsx @@ -1,6 +1,7 @@ var React = require('react'); var Reflux = require('reflux'); var _ = require('lodash'); + var channelStore = require('../stores/channel.js'); var selectedTabStore = require('../stores/selectedTab.js'); @@ -20,11 +21,14 @@ var UserList = React.createClass({ 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

{user}

; - }); + var channel = this.state.channels[tab.server][tab.channel]; + if (channel) { + users = _.map(channel.users, function(user) { + return

{user}

; + }); + } } return ( diff --git a/client/src/js/socket.js b/client/src/js/socket.js index dab015a6..60c1381a 100644 --- a/client/src/js/socket.js +++ b/client/src/js/socket.js @@ -1,4 +1,5 @@ var EventEmitter = require('events').EventEmitter; + var _ = require('lodash'); var sockets = {}; @@ -8,7 +9,7 @@ function createSocket(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 })); diff --git a/client/src/js/stores/channel.js b/client/src/js/stores/channel.js index d5960537..24b57f56 100644 --- a/client/src/js/stores/channel.js +++ b/client/src/js/stores/channel.js @@ -1,5 +1,6 @@ var Reflux = require('reflux'); var _ = require('lodash'); + var actions = require('../actions/channel.js'); var channels = {}; @@ -36,16 +37,29 @@ var channelStore = Reflux.createStore({ this.trigger(channels); }, + quit: function(user, server) { + _.each(channels[server], function(channel) { + _.pull(channel.users, user); + }); + this.trigger(channels); + }, + setUsers: function(users, server, channel) { initChannel(server, channel); channels[server][channel].users = users; this.trigger(channels); }, + setTopic: function(topic, server, channel) { + channels[server][channel].topic = topic; + this.trigger(channels); + }, + load: function(storedChannels) { _.each(storedChannels, function(channel) { initChannel(channel.server, channel.name); channels[channel.server][channel.name].users = channel.users; + channels[channel.server][channel.name].topic = channel.topic; }); this.trigger(channels); }, diff --git a/client/src/js/stores/message.js b/client/src/js/stores/message.js index d241630a..74448a34 100644 --- a/client/src/js/stores/message.js +++ b/client/src/js/stores/message.js @@ -1,25 +1,40 @@ var Reflux = require('reflux'); + var actions = require('../actions/message.js'); var messages = {}; +function addMessage(message, dest) { + if (!(dest in messages)) { + messages[dest] = [message]; + } else { + messages[dest].push(message); + } +} + var messageStore = Reflux.createStore({ init: function() { this.listenToMany(actions); }, + send: function(message, to, server) { + addMessage({ + server: server, + from: 'self', + to: to, + message: message + }, to); + + this.trigger(messages); + }, + 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); - } + addMessage(message, dest); this.trigger(messages); }, diff --git a/client/src/js/stores/selectedTab.js b/client/src/js/stores/selectedTab.js index 9745e2a1..2df937d8 100644 --- a/client/src/js/stores/selectedTab.js +++ b/client/src/js/stores/selectedTab.js @@ -1,11 +1,15 @@ var Reflux = require('reflux'); +var _ = require('lodash'); + var actions = require('../actions/tab.js'); +var channelActions = require('../actions/channel.js'); var selectedTab = {}; var selectedTabStore = Reflux.createStore({ init: function() { this.listenToMany(actions); + this.listenTo(channelActions.part, 'part'); }, select: function(server, channel) { @@ -14,6 +18,19 @@ var selectedTabStore = Reflux.createStore({ this.trigger(selectedTab); }, + part: function(data) { + var self = this; + if (data.server === selectedTab.server) { + _.each(data.channels, function(channel) { + if (channel === selectedTab.channel) { + delete selectedTab.channel; + self.trigger(selectedTab); + return; + } + }); + } + }, + getState: function() { return selectedTab; } diff --git a/client/src/js/stores/server.js b/client/src/js/stores/server.js index a8063aba..fba2c59a 100644 --- a/client/src/js/stores/server.js +++ b/client/src/js/stores/server.js @@ -1,4 +1,5 @@ var Reflux = require('reflux'); + var actions = require('../actions/server.js'); var servers = {}; diff --git a/client/src/style.css b/client/src/style.css index 14b812d7..f475f601 100644 --- a/client/src/style.css +++ b/client/src/style.css @@ -1,6 +1,7 @@ * { margin: 0; padding: 0; + box-sizing: border-box; } body { @@ -9,6 +10,14 @@ body { color: #FFF; } +input { + font: 16px Inconsolata, sans-serif; + background: rgba(0,0,0,0.25); + color: #FFF; + outline: none; + border: none; +} + p { line-height: 1.5; } @@ -19,7 +28,7 @@ p { top: 0; bottom: 0; right: 200px; - padding: 20px; + padding: 15px; overflow: auto; } @@ -31,23 +40,64 @@ p { color: #AAA; } -.messagebox { +.chat-title-bar { position: fixed; left: 200px; top: 0; - bottom: 0; right: 200px; - padding: 20px; + height: 50px; + padding: 0 15px; + line-height: 50px; + background: rgba(0,0,0,0.25); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.chat-title { + +} + +.messagebox { + position: fixed; + left: 200px; + top: 50px; + bottom: 50px; + right: 200px; + padding: 15px; overflow: auto; z-index: 1; } +.message { + +} + +.message-info { + color: #666; +} + +.message-input-wrap { + position: fixed; + left: 200px; + bottom: 0px; + right: 0; + height: 50px; + z-index: 1; +} + +.message-input { + width: 100%; + height: 100%; + padding: 15px; +} + .userlist { position: fixed; top: 0; - bottom: 0; + bottom: 50px; right: 0; width: 200px; - padding: 20px; + padding: 15px; overflow: auto; } \ No newline at end of file diff --git a/json_types.go b/json_types.go index 01b58411..1f1f4fc5 100644 --- a/json_types.go +++ b/json_types.go @@ -26,6 +26,11 @@ type Join struct { Channels []string `json:"channels"` } +type Quit struct { + Server string `json:"server"` + User string `json:"user"` +} + type Chat struct { Server string `json:"server"` From string `json:"from"` diff --git a/message_handler.go b/message_handler.go index 6bad58b7..4863f9b3 100644 --- a/message_handler.go +++ b/message_handler.go @@ -2,7 +2,7 @@ package main import ( "bytes" - "fmt" + "log" "strings" "github.com/khlieng/irc/storage" @@ -16,7 +16,7 @@ func handleMessages(irc *IRC, session *Session) { for msg := range irc.Messages { switch msg.Command { case JOIN: - user := parseUser(msg.Prefix) + user := msg.Prefix session.sendJSON("join", Join{ Server: irc.Host, @@ -34,7 +34,7 @@ func handleMessages(irc *IRC, session *Session) { } case PART: - user := parseUser(msg.Prefix) + user := msg.Prefix session.sendJSON("part", Join{ Server: irc.Host, @@ -65,15 +65,18 @@ func handleMessages(irc *IRC, session *Session) { } case QUIT: - /* - session.sendJSON("quit", Quit{ - Server: irc.Host, - User: user, - }) - */ + user := msg.Prefix + + session.sendJSON("quit", Quit{ + Server: irc.Host, + User: user, + }) + + channelStore.RemoveUserAll(user, irc.Host) case RPL_WELCOME, RPL_YOURHOST, + RPL_CREATED, RPL_LUSERCLIENT, RPL_LUSEROP, RPL_LUSERUNKNOWN, @@ -92,6 +95,8 @@ func handleMessages(irc *IRC, session *Session) { Topic: msg.Trailing, }) + channelStore.SetTopic(msg.Trailing, irc.Host, msg.Params[1]) + case RPL_NAMREPLY: users := strings.Split(msg.Trailing, " ") @@ -138,10 +143,5 @@ func handleMessages(irc *IRC, session *Session) { } 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() + log.Println(irc.nick+":", msg.Prefix, msg.Command, msg.Params, msg.Trailing) } diff --git a/storage/channel.go b/storage/channel.go index ec1a1045..1704f596 100644 --- a/storage/channel.go +++ b/storage/channel.go @@ -5,63 +5,98 @@ import ( ) type ChannelStore struct { - data map[string]map[string][]string - lock sync.Mutex + users map[string]map[string][]string + userLock sync.Mutex + + topic map[string]map[string]string + topicLock sync.Mutex } func NewChannelStore() *ChannelStore { return &ChannelStore{ - data: make(map[string]map[string][]string), + users: make(map[string]map[string][]string), + topic: make(map[string]map[string]string), } } func (c *ChannelStore) GetUsers(server, channel string) []string { - c.lock.Lock() + c.userLock.Lock() - users := make([]string, len(c.data[server][channel])) - copy(users, c.data[server][channel]) + users := make([]string, len(c.users[server][channel])) + copy(users, c.users[server][channel]) - c.lock.Unlock() + c.userLock.Unlock() return users } func (c *ChannelStore) SetUsers(users []string, server, channel string) { - c.lock.Lock() + c.userLock.Lock() - if _, ok := c.data[server]; !ok { - c.data[server] = make(map[string][]string) + if _, ok := c.users[server]; !ok { + c.users[server] = make(map[string][]string) } - c.data[server][channel] = users + c.users[server][channel] = users - c.lock.Unlock() + c.userLock.Unlock() } func (c *ChannelStore) AddUser(user, server, channel string) { - c.lock.Lock() + c.userLock.Lock() - if _, ok := c.data[server]; !ok { - c.data[server] = make(map[string][]string) + if _, ok := c.users[server]; !ok { + c.users[server] = make(map[string][]string) } - if users, ok := c.data[server][channel]; ok { - c.data[server][channel] = append(users, user) + if users, ok := c.users[server][channel]; ok { + c.users[server][channel] = append(users, user) } else { - c.data[server][channel] = []string{user} + c.users[server][channel] = []string{user} } - c.lock.Unlock() + c.userLock.Unlock() } func (c *ChannelStore) RemoveUser(user, server, channel string) { - c.lock.Lock() - defer c.lock.Unlock() + c.userLock.Lock() + c.removeUser(user, server, channel) + c.userLock.Unlock() +} - for i, u := range c.data[server][channel] { +func (c *ChannelStore) RemoveUserAll(user, server string) { + c.userLock.Lock() + + for channel, _ := range c.users[server] { + c.removeUser(user, server, channel) + } + + c.userLock.Unlock() +} + +func (c *ChannelStore) GetTopic(server, channel string) string { + c.topicLock.Lock() + defer c.topicLock.Unlock() + + return c.topic[server][channel] +} + +func (c *ChannelStore) SetTopic(topic, server, channel string) { + c.topicLock.Lock() + + if _, ok := c.topic[server]; !ok { + c.topic[server] = make(map[string]string) + } + + c.topic[server][channel] = topic + c.topicLock.Unlock() +} + +func (c *ChannelStore) removeUser(user, server, channel string) { + for i, u := range c.users[server][channel] { if u == user { - users := c.data[server][channel] - c.data[server][channel] = append(users[:i], users[i+1:]...) + users := c.users[server][channel] + c.users[server][channel] = append(users[:i], users[i+1:]...) return } } diff --git a/storage/user.go b/storage/user.go index 05c0765c..2d929c8b 100644 --- a/storage/user.go +++ b/storage/user.go @@ -18,6 +18,7 @@ type Channel struct { Server string `json:"server"` Name string `json:"name"` Users []string `json:"users"` + Topic string `json:"topic,omitempty"` } type User struct { @@ -99,7 +100,7 @@ func (u User) GetChannels() []Channel { } func (u User) AddServer(server Server) { - db.Update(func(tx *bolt.Tx) error { + go db.Update(func(tx *bolt.Tx) error { b := tx.Bucket([]byte("Servers")) data, _ := json.Marshal(server) @@ -110,7 +111,7 @@ func (u User) AddServer(server Server) { } func (u User) AddChannel(channel Channel) { - db.Update(func(tx *bolt.Tx) error { + go db.Update(func(tx *bolt.Tx) error { b := tx.Bucket([]byte("Channels")) data, _ := json.Marshal(channel) @@ -121,16 +122,25 @@ func (u User) AddChannel(channel Channel) { } func (u User) RemoveServer(address string) { - db.Update(func(tx *bolt.Tx) error { - tx.Bucket([]byte("Channels")).Delete([]byte(u.UUID + ":" + address)) + go db.Update(func(tx *bolt.Tx) error { + serverID := []byte(u.UUID + ":" + address) + + tx.Bucket([]byte("Servers")).Delete(serverID) + + b := tx.Bucket([]byte("Channels")) + c := b.Cursor() + + for k, _ := c.Seek(serverID); bytes.HasPrefix(k, serverID); k, _ = c.Next() { + b.Delete(k) + } 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)) + go db.Update(func(tx *bolt.Tx) error { + tx.Bucket([]byte("Channels")).Delete([]byte(u.UUID + ":" + server + ":" + channel)) return nil }) diff --git a/websocket_handler.go b/websocket_handler.go index 9119eeac..83be25d1 100644 --- a/websocket_handler.go +++ b/websocket_handler.go @@ -48,6 +48,7 @@ func handleWS(ws *websocket.Conn) { channels := session.user.GetChannels() for i, channel := range channels { channels[i].Users = channelStore.GetUsers(channel.Server, channel.Name) + channels[i].Topic = channelStore.GetTopic(channel.Server, channel.Name) } session.sendJSON("channels", channels) @@ -106,6 +107,17 @@ func handleWS(ws *websocket.Conn) { irc.Part(data.Channels...) } + case "quit": + var data Quit + + json.Unmarshal(req.Request, &data) + + if irc, ok := session.getIRC(data.Server); ok { + channelStore.RemoveUserAll(irc.nick, data.Server) + session.user.RemoveServer(data.Server) + irc.Quit() + } + case "chat": var data Chat