Show last IRC connection error in status tab, log IRC connection errors

This commit is contained in:
Ken-Håvard Lieng 2017-07-02 03:31:00 +02:00
parent 786d8013b9
commit 18aff3ded6
19 changed files with 294 additions and 189 deletions

File diff suppressed because one or more lines are too long

View File

@ -39,6 +39,14 @@ i[class^="icon-"]:before, i[class*=" icon-"]:before {
margin: 0;
}
.success {
color: #6BB758 !important;
}
.error {
color: #F6546A !important;
}
.wrap {
position: fixed;
top: 0;

View File

@ -12,13 +12,14 @@ export default class TabList extends PureComponent {
const tabs = [];
channels.forEach((server, address) => {
const srv = servers.get(address);
tabs.push(
<TabListItem
key={address}
server={address}
content={servers.getIn([address, 'name'])}
content={srv.name}
selected={tab.server === address && tab.name === null}
connected={servers.getIn([address, 'connected'])}
connected={srv.status.connected}
onClick={this.handleTabClick}
/>
);

View File

@ -15,9 +15,9 @@ export default class TabListItem extends PureComponent {
classes.push('tab-server');
if (connected) {
style.color = '#6BB758';
classes.push('success');
} else {
style.color = '#F6546A';
classes.push('error');
}
}

View File

@ -55,6 +55,7 @@ export default class Chat extends Component {
nick,
search,
showUserList,
status,
tab,
title,
users,
@ -81,6 +82,7 @@ export default class Chat extends Component {
<div className={chatClass}>
<ChatTitle
channel={channel}
status={status}
tab={tab}
title={title}
onCloseClick={this.handleCloseClick}

View File

@ -7,7 +7,7 @@ import { linkify } from 'util';
export default class ChatTitle extends PureComponent {
render() {
const { title, tab, channel, onTitleChange,
const { status, title, tab, channel, onTitleChange,
onToggleSearch, onToggleUserList, onCloseClick } = this.props;
let closeTitle;
@ -19,6 +19,11 @@ export default class ChatTitle extends PureComponent {
closeTitle = 'Disconnect';
}
let serverError = null;
if (!tab.name && status.error) {
serverError = <span className="error">Error! {status.error}</span>;
}
return (
<div>
<div className="chat-title-bar">
@ -34,6 +39,7 @@ export default class ChatTitle extends PureComponent {
</Editable>
<div className="chat-topic-wrap">
<span className="chat-topic">{linkify(channel.get('topic')) || null}</span>
{serverError}
</div>
<i className="icon-search" title="Search" onClick={onToggleSearch} />
<i

View File

@ -10,7 +10,7 @@ import { getSelectedMessages, getHasMoreMessages,
runCommand, sendMessage, fetchMessages, addFetchedMessages } from 'state/messages';
import { openPrivateChat, closePrivateChat } from 'state/privateChats';
import { getSearch, searchMessages, toggleSearch } from 'state/search';
import { getCurrentNick, disconnect, setNick, setServerName } from 'state/servers';
import { getCurrentNick, getCurrentServerStatus, disconnect, setNick, setServerName } from 'state/servers';
import { getSelectedTab, select } from 'state/tab';
import { getShowUserList, toggleUserList } from 'state/ui';
@ -22,6 +22,7 @@ const mapState = createStructuredSelector({
nick: getCurrentNick,
search: getSearch,
showUserList: getShowUserList,
status: getCurrentServerStatus,
tab: getSelectedTab,
title: getSelectedTabTitle,
users: getSelectedChannelUsers

View File

@ -1,4 +1,26 @@
import { setServerName } from '../servers';
import { connect, setServerName } from '../servers';
describe('connect()', () => {
it('sets host and port correctly', () => {
expect(connect('cake.com:1881', '', {})).toMatchObject({
socket: {
data: {
host: 'cake.com',
port: '1881'
}
}
});
expect(connect('cake.com', '', {})).toMatchObject({
socket: {
data: {
host: 'cake.com',
port: undefined
}
}
});
});
});
describe('setServerName()', () => {
it('passes valid names to the server', () => {

View File

@ -8,10 +8,13 @@ describe('server reducer', () => {
expect(state.toJS()).toEqual({
'127.0.0.1': {
connected: false,
name: '127.0.0.1',
nick: 'nick',
editedNick: null
editedNick: null,
status: {
connected: false,
error: null
}
}
});
@ -19,10 +22,13 @@ describe('server reducer', () => {
expect(state.toJS()).toEqual({
'127.0.0.1': {
connected: false,
name: '127.0.0.1',
nick: 'nick',
editedNick: null
editedNick: null,
status: {
connected: false,
error: null
}
}
});
@ -32,16 +38,22 @@ describe('server reducer', () => {
expect(state.toJS()).toEqual({
'127.0.0.1': {
connected: false,
name: '127.0.0.1',
nick: 'nick',
editedNick: null
editedNick: null,
status: {
connected: false,
error: null
}
},
'127.0.0.2': {
connected: false,
name: 'srv',
nick: 'nick',
editedNick: null
editedNick: null,
status: {
connected: false,
error: null
}
}
});
});
@ -87,9 +99,8 @@ describe('server reducer', () => {
editing: true
});
expect(state.toJS()).toEqual({
expect(state.toJS()).toMatchObject({
'127.0.0.1': {
connected: false,
name: '127.0.0.1',
nick: 'nick',
editedNick: 'nick2'
@ -111,9 +122,8 @@ describe('server reducer', () => {
nick: ''
});
expect(state.toJS()).toEqual({
expect(state.toJS()).toMatchObject({
'127.0.0.1': {
connected: false,
name: '127.0.0.1',
nick: 'nick',
editedNick: null
@ -130,9 +140,8 @@ describe('server reducer', () => {
newNick: 'nick2'
});
expect(state.toJS()).toEqual({
expect(state.toJS()).toMatchObject({
'127.0.0.1': {
connected: false,
name: '127.0.0.1',
nick: 'nick2',
editedNick: null
@ -153,9 +162,8 @@ describe('server reducer', () => {
server: '127.0.0.1'
});
expect(state.toJS()).toEqual({
expect(state.toJS()).toMatchObject({
'127.0.0.1': {
connected: false,
name: '127.0.0.1',
nick: 'nick',
editedNick: null
@ -171,13 +179,17 @@ describe('server reducer', () => {
host: '127.0.0.1',
name: 'stuff',
nick: 'nick',
connected: true
status: {
connected: true
}
},
{
host: '127.0.0.2',
name: 'stuffz',
nick: 'nick2',
connected: false
status: {
connected: false
}
},
]
});
@ -187,13 +199,19 @@ describe('server reducer', () => {
name: 'stuff',
nick: 'nick',
editedNick: null,
connected: true
status: {
connected: true,
error: null
}
},
'127.0.0.2': {
name: 'stuffz',
nick: 'nick2',
editedNick: null,
connected: false
status: {
connected: false,
error: null
}
}
});
});
@ -202,7 +220,8 @@ describe('server reducer', () => {
let state = reducer(undefined, connect('127.0.0.1:1337', 'nick', {}));
state = reducer(state, {
type: actions.socket.CONNECTION_UPDATE,
'127.0.0.1': true
server: '127.0.0.1',
connected: true
});
expect(state.toJS()).toEqual({
@ -210,7 +229,29 @@ describe('server reducer', () => {
name: '127.0.0.1',
nick: 'nick',
editedNick: null,
connected: true
status: {
connected: true,
error: null
}
}
});
state = reducer(state, {
type: actions.socket.CONNECTION_UPDATE,
server: '127.0.0.1',
connected: false,
error: 'Bad stuff happened'
});
expect(state.toJS()).toEqual({
'127.0.0.1': {
name: '127.0.0.1',
nick: 'nick',
editedNick: null,
status: {
connected: false,
error: 'Bad stuff happened'
}
}
});
});

View File

@ -4,11 +4,16 @@ import createReducer from 'util/createReducer';
import { getSelectedTab, updateSelection } from './tab';
import * as actions from './actions';
const Status = Record({
connected: false,
error: null
});
const Server = Record({
nick: '',
editedNick: null,
name: '',
connected: false
status: new Status()
});
export const getServers = state => state.servers;
@ -31,6 +36,12 @@ export const getCurrentServerName = createSelector(
(servers, tab) => servers.getIn([tab.server, 'name'])
);
export const getCurrentServerStatus = createSelector(
getServers,
getSelectedTab,
(servers, tab) => servers.getIn([tab.server, 'status'])
);
export default createReducer(Map(), {
[actions.CONNECT](state, { host, nick, options }) {
if (!state.has(host)) {
@ -81,27 +92,27 @@ export default createReducer(Map(), {
return state.withMutations(s => {
data.forEach(server => {
server.status = new Status(server.status);
s.set(server.host, new Server(server));
});
});
},
[actions.socket.CONNECTION_UPDATE](state, action) {
return state.withMutations(s =>
Object.keys(action).forEach(server => {
if (s.has(server)) {
s.setIn([server, 'connected'], action[server]);
}
})
);
if (state.has(action.server)) {
return state.setIn([action.server, 'status'], new Status(action));
}
return state;
}
});
export function connect(server, nick, options) {
let host = server;
let port;
const i = server.indexOf(':');
if (i > 0) {
host = server.slice(0, i);
port = server.slice(i + 1);
}
return {
@ -112,7 +123,8 @@ export function connect(server, nick, options) {
socket: {
type: 'connect',
data: {
server,
host,
port,
nick,
username: options.username || nick,
password: options.password,

View File

@ -20,7 +20,7 @@ type Client struct {
Username string
Realname string
Messages chan *Message
ConnectionChanged chan bool
ConnectionChanged chan ConnectionState
HandleNickInUse func(string) string
nick string
@ -47,7 +47,7 @@ func NewClient(nick, username string) *Client {
Username: username,
Realname: nick,
Messages: make(chan *Message, 32),
ConnectionChanged: make(chan bool, 16),
ConnectionChanged: make(chan ConnectionState, 16),
out: make(chan string, 32),
quit: make(chan struct{}),
reconnect: make(chan struct{}),

View File

@ -10,8 +10,6 @@ import (
)
func (c *Client) Connect(address string) {
c.ConnectionChanged <- false
if idx := strings.Index(address, ":"); idx < 0 {
c.Host = address
@ -26,6 +24,7 @@ func (c *Client) Connect(address string) {
c.Server = address
c.dialer = &net.Dialer{Timeout: 10 * time.Second}
c.connChange(false, nil)
go c.run()
}
@ -71,8 +70,20 @@ func (c *Client) run() {
}
}
type ConnectionState struct {
Connected bool
Error error
}
func (c *Client) connChange(connected bool, err error) {
c.ConnectionChanged <- ConnectionState{
Connected: connected,
Error: err,
}
}
func (c *Client) disconnect() {
c.ConnectionChanged <- false
c.connChange(false, nil)
c.lock.Lock()
c.connected = false
c.lock.Unlock()
@ -91,7 +102,9 @@ func (c *Client) tryConnect() {
}
err := c.connect()
if err == nil {
if err != nil {
c.connChange(false, err)
} else {
c.backoff.Reset()
c.flushChannels()
@ -123,7 +136,7 @@ func (c *Client) connect() error {
}
c.connected = true
c.ConnectionChanged <- true
c.connChange(true, nil)
c.reader = bufio.NewReader(c.conn)
c.register()

View File

@ -20,7 +20,7 @@ type connectDefaults struct {
type indexData struct {
Defaults connectDefaults `json:"defaults"`
Servers []storage.Server `json:"servers,omitempty"`
Servers []Server `json:"servers,omitempty"`
Channels []storage.Channel `json:"channels,omitempty"`
// Users in the selected channel
@ -57,32 +57,32 @@ func (d *indexData) addUsersAndMessages(server, channel string, session *Session
}
func getIndexData(r *http.Request, session *Session) *indexData {
data := indexData{}
servers := session.user.GetServers()
connections := session.getConnectionStates()
for i, server := range servers {
servers[i].Connected = connections[server.Host]
servers[i].Port = ""
servers[i].TLS = false
servers[i].Password = ""
servers[i].Username = ""
servers[i].Realname = ""
for _, server := range servers {
server.Password = ""
server.Username = ""
server.Realname = ""
data.Servers = append(data.Servers, Server{
Server: server,
Status: newConnectionUpdate(server.Host, connections[server.Host]),
})
}
channels := session.user.GetChannels()
for i, channel := range channels {
channels[i].Topic = channelStore.GetTopic(channel.Server, channel.Name)
}
data.Channels = channels
data := indexData{
Defaults: connectDefaults{
Name: viper.GetString("defaults.name"),
Address: viper.GetString("defaults.address"),
Channels: viper.GetStringSlice("defaults.channels"),
Password: viper.GetString("defaults.password") != "",
SSL: viper.GetBool("defaults.ssl"),
},
Servers: servers,
Channels: channels,
data.Defaults = connectDefaults{
Name: viper.GetString("defaults.name"),
Address: viper.GetString("defaults.address"),
Channels: viper.GetStringSlice("defaults.channels"),
Password: viper.GetString("defaults.password") != "",
SSL: viper.GetBool("defaults.ssl"),
}
server, channel := getTabFromPath(r.URL.EscapedPath())

View File

@ -27,31 +27,7 @@ func reconnectIRC() {
channels := user.GetChannels()
for _, server := range user.GetServers() {
i := irc.NewClient(server.Nick, server.Username)
i.TLS = server.TLS
i.Password = server.Password
i.Realname = server.Realname
i.HandleNickInUse = createNickInUseHandler(i, session)
if i.TLS {
i.TLSConfig = &tls.Config{
InsecureSkipVerify: !viper.GetBool("verify_certificates"),
}
if cert := user.GetCertificate(); cert != nil {
i.TLSConfig.Certificates = []tls.Certificate{*cert}
}
}
session.setIRC(server.Host, i)
if server.Port != "" {
i.Connect(net.JoinHostPort(server.Host, server.Port))
} else {
i.Connect(server.Host)
}
go newIRCHandler(i, session).run()
i := connectIRC(server, session)
var joining []string
for _, channel := range channels {
@ -63,3 +39,39 @@ func reconnectIRC() {
}
}
}
func connectIRC(server storage.Server, session *Session) *irc.Client {
i := irc.NewClient(server.Nick, server.Username)
i.TLS = server.TLS
i.Realname = server.Realname
i.HandleNickInUse = createNickInUseHandler(i, session)
address := server.Host
if server.Port != "" {
address = net.JoinHostPort(server.Host, server.Port)
}
if server.Password == "" &&
viper.GetString("defaults.password") != "" &&
address == viper.GetString("defaults.address") {
i.Password = viper.GetString("defaults.password")
} else {
i.Password = server.Password
}
if i.TLS {
i.TLSConfig = &tls.Config{
InsecureSkipVerify: !viper.GetBool("verify_certificates"),
}
if cert := session.user.GetCertificate(); cert != nil {
i.TLSConfig.Certificates = []tls.Certificate{*cert}
}
}
session.setIRC(server.Host, i)
i.Connect(address)
go newIRCHandler(i, session).run()
return i
}

View File

@ -1,6 +1,7 @@
package server
import (
"fmt"
"log"
"strings"
"unicode"
@ -37,6 +38,7 @@ func newIRCHandler(client *irc.Client, session *Session) *ircHandler {
}
func (i *ircHandler) run() {
var lastConnErr error
for {
select {
case msg, ok := <-i.client.Messages:
@ -47,11 +49,17 @@ func (i *ircHandler) run() {
i.dispatchMessage(msg)
case connected := <-i.client.ConnectionChanged:
i.session.sendJSON("connection_update", map[string]bool{
i.client.Host: connected,
})
i.session.setConnectionState(i.client.Host, connected)
case state := <-i.client.ConnectionChanged:
i.session.sendJSON("connection_update", newConnectionUpdate(i.client.Host, state))
i.session.setConnectionState(i.client.Host, state)
if state.Error != nil && (lastConnErr == nil ||
state.Error.Error() != lastConnErr.Error()) {
lastConnErr = state.Error
i.log("Connection error:", state.Error)
} else if state.Connected {
i.log("Connected")
}
}
}
}
@ -312,6 +320,11 @@ func (i *ircHandler) initHandlers() {
}
}
func (i *ircHandler) log(v ...interface{}) {
s := fmt.Sprintln(v...)
log.Println("[IRC]", i.session.user.ID, i.client.Host, s[:len(s)-1])
}
func parseMode(mode string) *Mode {
m := Mode{}
add := false

View File

@ -3,6 +3,7 @@ package server
import (
"encoding/json"
"github.com/khlieng/dispatch/irc"
"github.com/khlieng/dispatch/storage"
)
@ -16,14 +17,31 @@ type WSResponse struct {
Data interface{} `json:"data"`
}
type Connect struct {
Name string `json:"name"`
Server string `json:"server"`
TLS bool `json:"tls"`
Password string `json:"password"`
Nick string `json:"nick"`
Username string `json:"username"`
Realname string `json:"realname"`
type Server struct {
storage.Server
Status ConnectionUpdate `json:"status"`
}
type ServerName struct {
Server string `json:"server"`
Name string `json:"name"`
}
type ConnectionUpdate struct {
Server string `json:"server"`
Connected bool `json:"connected"`
Error string `json:"error,omitempty"`
}
func newConnectionUpdate(server string, state irc.ConnectionState) ConnectionUpdate {
status := ConnectionUpdate{
Server: server,
Connected: state.Connected,
}
if state.Error != nil {
status.Error = state.Error.Error()
}
return status
}
type Nick struct {

View File

@ -16,7 +16,7 @@ const (
type Session struct {
irc map[string]*irc.Client
connectionState map[string]bool
connectionState map[string]irc.ConnectionState
ircLock sync.Mutex
ws map[string]*wsConn
@ -31,7 +31,7 @@ type Session struct {
func NewSession(user *storage.User) *Session {
return &Session{
irc: make(map[string]*irc.Client),
connectionState: make(map[string]bool),
connectionState: make(map[string]irc.ConnectionState),
ws: make(map[string]*wsConn),
broadcast: make(chan WSResponse, 32),
user: user,
@ -51,7 +51,9 @@ func (s *Session) getIRC(server string) (*irc.Client, bool) {
func (s *Session) setIRC(server string, i *irc.Client) {
s.ircLock.Lock()
s.irc[server] = i
s.connectionState[server] = false
s.connectionState[server] = irc.ConnectionState{
Connected: false,
}
s.ircLock.Unlock()
s.reset <- 0
@ -74,9 +76,9 @@ func (s *Session) numIRC() int {
return n
}
func (s *Session) getConnectionStates() map[string]bool {
func (s *Session) getConnectionStates() map[string]irc.ConnectionState {
s.ircLock.Lock()
state := make(map[string]bool, len(s.connectionState))
state := make(map[string]irc.ConnectionState, len(s.connectionState))
for k, v := range s.connectionState {
state[k] = v
@ -86,9 +88,9 @@ func (s *Session) getConnectionStates() map[string]bool {
return state
}
func (s *Session) setConnectionState(server string, connected bool) {
func (s *Session) setConnectionState(server string, state irc.ConnectionState) {
s.ircLock.Lock()
s.connectionState[server] = connected
s.connectionState[server] = state
s.ircLock.Unlock()
}

View File

@ -1,19 +1,13 @@
package server
import (
"crypto/tls"
"encoding/json"
"log"
"net"
"net/http"
"strings"
"github.com/gorilla/websocket"
"github.com/kjk/betterguid"
"github.com/spf13/viper"
"github.com/khlieng/dispatch/irc"
"github.com/khlieng/dispatch/storage"
)
type wsHandler struct {
@ -87,56 +81,17 @@ func (h *wsHandler) init(r *http.Request) {
}
func (h *wsHandler) connect(b []byte) {
var data Connect
var data Server
json.Unmarshal(b, &data)
host, port, err := net.SplitHostPort(data.Server)
if err != nil {
host = data.Server
}
if _, ok := h.session.getIRC(host); !ok {
if _, ok := h.session.getIRC(data.Host); !ok {
log.Println(h.addr, "[IRC] Add server", data.Server)
i := irc.NewClient(data.Nick, data.Username)
i.TLS = data.TLS
i.Realname = data.Realname
i.HandleNickInUse = createNickInUseHandler(i, h.session)
connectIRC(data.Server, h.session)
if data.Password == "" &&
viper.GetString("defaults.password") != "" &&
data.Server == viper.GetString("defaults.address") {
i.Password = viper.GetString("defaults.password")
} else {
i.Password = data.Password
}
if i.TLS {
i.TLSConfig = &tls.Config{
InsecureSkipVerify: !viper.GetBool("verify_certificates"),
}
if cert := h.session.user.GetCertificate(); cert != nil {
i.TLSConfig.Certificates = []tls.Certificate{*cert}
}
}
h.session.setIRC(host, i)
i.Connect(data.Server)
go newIRCHandler(i, h.session).run()
go h.session.user.AddServer(storage.Server{
Name: data.Name,
Host: host,
Port: port,
TLS: data.TLS,
Password: data.Password,
Nick: data.Nick,
Username: data.Username,
Realname: data.Realname,
})
go h.session.user.AddServer(data.Server)
} else {
log.Println(h.addr, "[IRC]", data.Server, "already added")
log.Println(h.addr, "[IRC]", data.Host, "already added")
}
}
@ -287,7 +242,7 @@ func (h *wsHandler) fetchMessages(b []byte) {
}
func (h *wsHandler) setServerName(b []byte) {
var data Connect
var data ServerName
json.Unmarshal(b, &data)
if isValidServerName(data.Name) {

View File

@ -23,15 +23,14 @@ type User struct {
}
type Server struct {
Name string `json:"name"`
Host string `json:"host"`
Port string `json:"port,omitempty"`
TLS bool `json:"tls,omitempty"`
Password string `json:"password,omitempty"`
Nick string `json:"nick"`
Username string `json:"username,omitempty"`
Realname string `json:"realname,omitempty"`
Connected bool `json:"connected"`
Name string `json:"name"`
Host string `json:"host"`
Port string `json:"port"`
TLS bool `json:"tls,omitempty"`
Password string `json:"password,omitempty"`
Nick string `json:"nick"`
Username string `json:"username,omitempty"`
Realname string `json:"realname,omitempty"`
}
type Channel struct {