Collapse and log join, part and quit, closes #27, log nick and topic changes, move state into irc package

This commit is contained in:
Ken-Håvard Lieng 2020-06-03 03:04:38 +02:00
parent edd4d6eadb
commit ead3b37cf9
37 changed files with 1980 additions and 969 deletions

File diff suppressed because one or more lines are too long

View File

@ -725,6 +725,11 @@ input.chat-title {
cursor: pointer; cursor: pointer;
} }
.message-events-more {
font-weight: 700;
cursor: pointer;
}
.message-input-wrap { .message-input-wrap {
position: absolute; position: absolute;
left: 0; left: 0;

View File

@ -2,24 +2,23 @@ import React, { memo } from 'react';
import classnames from 'classnames'; import classnames from 'classnames';
import stringToRGB from 'utils/color'; import stringToRGB from 'utils/color';
const Message = ({ message, coloredNick, style, onNickClick }) => { const Message = ({ message, coloredNick, onNickClick }) => {
const className = classnames('message', { const className = classnames('message', {
[`message-${message.type}`]: message.type [`message-${message.type}`]: message.type
}); });
if (message.type === 'date') { if (message.type === 'date') {
return ( return (
<div className={className} style={style}> <div className={className}>
{message.content} {message.content}
<hr /> <hr />
</div> </div>
); );
} }
style = { const style = {
...style, paddingLeft: `${message.indent + 15}px`,
paddingLeft: `${window.messageIndent + 15}px`, textIndent: `-${message.indent}px`
textIndent: `-${window.messageIndent}px`
}; };
const senderStyle = {}; const senderStyle = {};

View File

@ -247,12 +247,13 @@ export default class MessageBox extends PureComponent {
const message = messages[index - 1]; const message = messages[index - 1];
return ( return (
<Message <div style={style}>
message={message} <Message
coloredNick={coloredNicks} message={message}
style={style} coloredNick={coloredNicks}
onNickClick={onNickClick} onNickClick={onNickClick}
/> />
</div>
); );
}; };

View File

@ -1,21 +1,17 @@
import { socketAction } from 'state/actions'; import { socketAction } from 'state/actions';
import { setConnected } from 'state/app'; import { setConnected } from 'state/app';
import { import {
broadcast,
inform,
print, print,
addMessage, addMessage,
addMessages addMessages,
addEvent,
broadcastEvent
} from 'state/messages'; } from 'state/messages';
import { openModal } from 'state/modals'; import { openModal } from 'state/modals';
import { reconnect } from 'state/servers'; import { reconnect } from 'state/servers';
import { select } from 'state/tab'; import { select } from 'state/tab';
import { find } from 'utils'; import { find } from 'utils';
function withReason(message, reason) {
return message + (reason ? ` (${reason})` : '');
}
function findChannels(state, server, user) { function findChannels(state, server, user) {
const channels = []; const channels = [];
@ -46,37 +42,34 @@ export default function handleSocket({
}, },
join({ user, server, channels }) { join({ user, server, channels }) {
dispatch(inform(`${user} joined the channel`, server, channels[0])); dispatch(addEvent(server, channels[0], 'join', user));
}, },
part({ user, server, channel, reason }) { part({ user, server, channel, reason }) {
dispatch( dispatch(addEvent(server, channel, 'part', user, reason));
inform(withReason(`${user} left the channel`, reason), server, channel)
);
}, },
quit({ user, server, reason }) { quit({ user, server, reason }) {
const channels = findChannels(getState(), server, user); const channels = findChannels(getState(), server, user);
dispatch(broadcast(withReason(`${user} quit`, reason), server, channels)); dispatch(broadcastEvent(server, channels, 'quit', user, reason));
}, },
nick({ server, oldNick, newNick }) { nick({ server, oldNick, newNick }) {
if (oldNick) { if (oldNick) {
const channels = findChannels(getState(), server, oldNick); const channels = findChannels(getState(), server, oldNick);
dispatch( dispatch(broadcastEvent(server, channels, 'nick', oldNick, newNick));
broadcast(`${oldNick} changed nick to ${newNick}`, server, channels)
);
} }
}, },
topic({ server, channel, topic, nick }) { topic({ server, channel, topic, nick }) {
if (nick) { if (nick) {
if (topic) { dispatch(addEvent(server, channel, 'topic', nick, topic));
/* if (topic) {
dispatch(inform(`${nick} changed the topic to:`, server, channel)); dispatch(inform(`${nick} changed the topic to:`, server, channel));
dispatch(print(topic, server, channel)); dispatch(print(topic, server, channel));
} else { } else {
dispatch(inform(`${nick} cleared the topic`, server, channel)); dispatch(inform(`${nick} cleared the topic`, server, channel));
} } */
} }
}, },

View File

@ -10,7 +10,6 @@ const smallScreen = 600;
export default function widthUpdates({ store }) { export default function widthUpdates({ store }) {
when(store, getCharWidth, charWidth => { when(store, getCharWidth, charWidth => {
window.messageIndent = 6 * charWidth;
const scrollBarWidth = measureScrollBarWidth(); const scrollBarWidth = measureScrollBarWidth();
let prevWrapWidth; let prevWrapWidth;

View File

@ -1,6 +1,7 @@
import reducer, { broadcast, getMessageTab } from '../messages'; import reducer, { broadcast, getMessageTab } from '../messages';
import * as actions from '../actions'; import * as actions from '../actions';
import appReducer from '../app'; import appReducer from '../app';
import { unix } from 'utils';
describe('message reducer', () => { describe('message reducer', () => {
it('adds the message on ADD_MESSAGE', () => { it('adds the message on ADD_MESSAGE', () => {
@ -98,7 +99,7 @@ describe('message reducer', () => {
it('adds date markers when prepending messages', () => { it('adds date markers when prepending messages', () => {
let state = { let state = {
srv: { srv: {
'#chan1': [{ id: 0, date: new Date(1999, 0, 1) }] '#chan1': [{ id: 0, date: new Date(1990, 0, 3) }]
} }
}; };
@ -108,8 +109,8 @@ describe('message reducer', () => {
tab: '#chan1', tab: '#chan1',
prepend: true, prepend: true,
messages: [ messages: [
{ id: 1, date: new Date(1990, 0, 2) }, { id: 1, time: unix(new Date(1990, 0, 1)) },
{ id: 2, date: new Date(1990, 0, 3) } { id: 2, time: unix(new Date(1990, 0, 2)) }
] ]
}); });
@ -150,7 +151,7 @@ describe('message reducer', () => {
it('adds date markers when adding messages', () => { it('adds date markers when adding messages', () => {
let state = { let state = {
srv: { srv: {
'#chan1': [{ id: 0, date: new Date(1999, 0, 1) }] '#chan1': [{ id: 0, date: new Date(1990, 0, 1) }]
} }
}; };
@ -159,9 +160,9 @@ describe('message reducer', () => {
server: 'srv', server: 'srv',
tab: '#chan1', tab: '#chan1',
messages: [ messages: [
{ id: 1, date: new Date(1990, 0, 2) }, { id: 1, time: unix(new Date(1990, 0, 2)) },
{ id: 2, date: new Date(1990, 0, 3) }, { id: 2, time: unix(new Date(1990, 0, 3)) },
{ id: 3, date: new Date(1990, 0, 3) } { id: 3, time: unix(new Date(1990, 0, 3)) }
] ]
}); });

View File

@ -1,3 +1,4 @@
import React from 'react';
import { createSelector } from 'reselect'; import { createSelector } from 'reselect';
import has from 'lodash/has'; import has from 'lodash/has';
import { import {
@ -6,8 +7,10 @@ import {
linkify, linkify,
timestamp, timestamp,
isChannel, isChannel,
formatDate formatDate,
unix
} from 'utils'; } from 'utils';
import stringToRGB from 'utils/color';
import colorify from 'utils/colorify'; import colorify from 'utils/colorify';
import createReducer from 'utils/createReducer'; import createReducer from 'utils/createReducer';
import { getApp } from './app'; import { getApp } from './app';
@ -45,8 +48,214 @@ function init(state, server, tab) {
} }
} }
const collapsedEvents = ['join', 'part', 'quit'];
function shouldCollapse(msg1, msg2) {
return (
msg1.events &&
msg2.events &&
collapsedEvents.indexOf(msg1.events[0].type) !== -1 &&
collapsedEvents.indexOf(msg2.events[0].type) !== -1
);
}
const eventVerbs = {
join: 'joined the channel',
part: 'left the channel',
quit: 'quit'
};
function renderNick(nick, type = '') {
const style = {
color: stringToRGB(nick),
fontWeight: 400
};
return (
<span className="message-sender" style={style} key={`${nick} ${type}`}>
{nick}
</span>
);
}
function renderMore(count, type) {
return (
<span
className="message-events-more"
key={`more ${type}`}
>{`${count} more`}</span>
);
}
function renderEvent(event, type, nicks) {
const ending = eventVerbs[type];
if (nicks.length === 1) {
event.push(renderNick(nicks[0], type));
event.push(` ${ending}`);
}
if (nicks.length === 2) {
event.push(renderNick(nicks[0], type));
event.push(' and ');
event.push(renderNick(nicks[1], type));
event.push(` ${ending}`);
}
if (nicks.length > 2) {
event.push(renderNick(nicks[0], type));
event.push(', ');
event.push(renderNick(nicks[1], type));
event.push(' and ');
event.push(renderMore(nicks.length - 2, type));
event.push(` ${ending}`);
}
}
function renderEvents(events) {
const first = events[0];
if (first.type === 'nick') {
const [oldNick, newNick] = first.params;
return [renderNick(oldNick), ' changed nick to ', renderNick(newNick)];
}
if (first.type === 'topic') {
const [nick, newTopic] = first.params;
const topic = colorify(linkify(newTopic));
if (!topic) {
return [renderNick(nick), ' cleared the topic'];
}
const result = [renderNick(nick), ' changed the topic to: '];
if (Array.isArray(topic)) {
result.push(...topic);
} else {
result.push(topic);
}
return result;
}
const byType = {};
for (let i = events.length - 1; i >= 0; i--) {
const event = events[i];
const [nick] = event.params;
if (!byType[event.type]) {
byType[event.type] = [nick];
} else if (byType[event.type].indexOf(nick) === -1) {
byType[event.type].push(nick);
}
}
const result = [];
if (byType.join) {
renderEvent(result, 'join', byType.join);
}
if (byType.part) {
if (result.length > 1) {
result[result.length - 1] += ', ';
}
renderEvent(result, 'part', byType.part);
}
if (byType.quit) {
if (result.length > 1) {
result[result.length - 1] += ', ';
}
renderEvent(result, 'quit', byType.quit);
}
return result;
}
let nextID = 0; let nextID = 0;
function initMessage(
state,
message,
server,
tab,
wrapWidth,
charWidth,
windowWidth,
prepend
) {
const messages = state[server][tab];
if (messages.length > 0 && !prepend) {
const lastMessage = messages[messages.length - 1];
if (shouldCollapse(lastMessage, message)) {
lastMessage.events.push(message.events[0]);
lastMessage.content = renderEvents(lastMessage.events);
[lastMessage.breakpoints, lastMessage.length] = findBreakpoints(
lastMessage.content
);
lastMessage.height = messageHeight(
lastMessage,
wrapWidth,
charWidth,
6 * charWidth,
windowWidth
);
return false;
}
}
if (message.time) {
message.date = new Date(message.time * 1000);
} else {
message.date = new Date();
}
message.time = timestamp(message.date);
if (!message.id) {
message.id = nextID;
nextID++;
}
if (tab.charAt(0) === '#') {
message.channel = true;
}
if (message.events) {
message.type = 'info';
message.content = renderEvents(message.events);
} else {
message.content = message.content || '';
// Collapse multiple adjacent spaces into a single one
message.content = message.content.replace(/\s\s+/g, ' ');
if (message.content.indexOf('\x01ACTION') === 0) {
const { from } = message;
message.from = null;
message.type = 'action';
message.content = from + message.content.slice(7, -1);
}
}
if (!message.events) {
message.content = colorify(linkify(message.content));
}
[message.breakpoints, message.length] = findBreakpoints(message.content);
message.height = messageHeight(
message,
wrapWidth,
charWidth,
6 * charWidth,
windowWidth
);
message.indent = 6 * charWidth;
return true;
}
function createDateMessage(date) { function createDateMessage(date) {
const message = { const message = {
id: nextID, id: nextID,
@ -68,14 +277,34 @@ function isSameDay(d1, d2) {
); );
} }
function reducerPrependMessages(messages, server, tab, state) { function reducerPrependMessages(
state,
messages,
server,
tab,
wrapWidth,
charWidth,
windowWidth
) {
const msgs = []; const msgs = [];
for (let i = 0; i < messages.length; i++) { for (let i = 0; i < messages.length; i++) {
if (i > 0 && !isSameDay(messages[i - 1].date, messages[i].date)) { const message = messages[i];
msgs.push(createDateMessage(messages[i].date)); initMessage(
state,
message,
server,
tab,
wrapWidth,
charWidth,
windowWidth,
true
);
if (i > 0 && !isSameDay(messages[i - 1].date, message.date)) {
msgs.push(createDateMessage(message.date));
} }
msgs.push(messages[i]); msgs.push(message);
} }
const m = state[server][tab]; const m = state[server][tab];
@ -110,15 +339,41 @@ function reducerAddMessage(message, server, tab, state) {
export default createReducer( export default createReducer(
{}, {},
{ {
[actions.ADD_MESSAGE](state, { server, tab, message }) { [actions.ADD_MESSAGE](
state,
{ server, tab, message, wrapWidth, charWidth, windowWidth }
) {
init(state, server, tab); init(state, server, tab);
reducerAddMessage(message, server, tab, state);
const shouldAdd = initMessage(
state,
message,
server,
tab,
wrapWidth,
charWidth,
windowWidth
);
if (shouldAdd) {
reducerAddMessage(message, server, tab, state);
}
}, },
[actions.ADD_MESSAGES](state, { server, tab, messages, prepend }) { [actions.ADD_MESSAGES](
state,
{ server, tab, messages, prepend, wrapWidth, charWidth, windowWidth }
) {
if (prepend) { if (prepend) {
init(state, server, tab); init(state, server, tab);
reducerPrependMessages(messages, server, tab, state); reducerPrependMessages(
state,
messages,
server,
tab,
wrapWidth,
charWidth,
windowWidth
);
} else { } else {
if (!messages[0].tab) { if (!messages[0].tab) {
init(state, server, tab); init(state, server, tab);
@ -128,7 +383,19 @@ export default createReducer(
if (message.tab) { if (message.tab) {
init(state, server, message.tab); init(state, server, message.tab);
} }
reducerAddMessage(message, server, message.tab || tab, state);
const shouldAdd = initMessage(
state,
message,
server,
message.tab || tab,
wrapWidth,
charWidth,
windowWidth
);
if (shouldAdd) {
reducerAddMessage(message, server, message.tab || tab, state);
}
}); });
} }
}, },
@ -184,53 +451,6 @@ export default createReducer(
} }
); );
function initMessage(message, tab, state) {
if (message.time) {
message.date = new Date(message.time * 1000);
} else {
message.date = new Date();
}
message.time = timestamp(message.date);
if (!message.id) {
message.id = nextID;
nextID++;
}
if (tab.charAt(0) === '#') {
message.channel = true;
}
message.content = message.content || '';
// Collapse multiple adjacent spaces into a single one
message.content = message.content.replace(/\s\s+/g, ' ');
if (message.content.indexOf('\x01ACTION') === 0) {
const { from } = message;
message.from = null;
message.type = 'action';
message.content = from + message.content.slice(7, -1);
}
const { wrapWidth, charWidth, windowWidth } = getApp(state);
message.length = message.content.length;
message.breakpoints = findBreakpoints(message.content);
message.height = messageHeight(
message,
wrapWidth,
charWidth,
6 * charWidth,
windowWidth
);
message.content = colorify(linkify(message.content));
return message;
}
export function getMessageTab(server, to) { export function getMessageTab(server, to) {
if (!to || to === '*' || (!isChannel(to) && to.indexOf('.') !== -1)) { if (!to || to === '*' || (!isChannel(to) && to.indexOf('.') !== -1)) {
return server; return server;
@ -284,19 +504,19 @@ export function updateMessageHeight(wrapWidth, charWidth, windowWidth) {
export function sendMessage(content, to, server) { export function sendMessage(content, to, server) {
return (dispatch, getState) => { return (dispatch, getState) => {
const state = getState(); const state = getState();
const { wrapWidth, charWidth, windowWidth } = getApp(state);
dispatch({ dispatch({
type: actions.ADD_MESSAGE, type: actions.ADD_MESSAGE,
server, server,
tab: to, tab: to,
message: initMessage( message: {
{ from: state.servers[server].nick,
from: state.servers[server].nick, content
content },
}, wrapWidth,
to, charWidth,
state windowWidth,
),
socket: { socket: {
type: 'message', type: 'message',
data: { content, to, server } data: { content, to, server }
@ -308,13 +528,19 @@ export function sendMessage(content, to, server) {
export function addMessage(message, server, to) { export function addMessage(message, server, to) {
const tab = getMessageTab(server, to); const tab = getMessageTab(server, to);
return (dispatch, getState) => return (dispatch, getState) => {
const { wrapWidth, charWidth, windowWidth } = getApp(getState());
dispatch({ dispatch({
type: actions.ADD_MESSAGE, type: actions.ADD_MESSAGE,
server, server,
tab, tab,
message: initMessage(message, tab, getState()) message,
wrapWidth,
charWidth,
windowWidth
}); });
};
} }
export function addMessages(messages, server, to, prepend, next) { export function addMessages(messages, server, to, prepend, next) {
@ -328,20 +554,57 @@ export function addMessages(messages, server, to, prepend, next) {
messages[0].next = true; messages[0].next = true;
} }
messages.forEach(message => const { wrapWidth, charWidth, windowWidth } = getApp(state);
initMessage(message, message.tab || tab, state)
);
dispatch({ dispatch({
type: actions.ADD_MESSAGES, type: actions.ADD_MESSAGES,
server, server,
tab, tab,
messages, messages,
prepend prepend,
wrapWidth,
charWidth,
windowWidth
}); });
}; };
} }
export function addEvent(server, tab, type, ...params) {
return addMessage(
{
type: 'info',
events: [
{
type,
params,
time: unix()
}
]
},
server,
tab
);
}
export function broadcastEvent(server, channels, type, ...params) {
const now = unix();
return addMessages(
channels.map(channel => ({
type: 'info',
tab: channel,
events: [
{
type,
params,
time: now
}
]
})),
server
);
}
export function broadcast(message, server, channels) { export function broadcast(message, server, channels) {
return addMessages( return addMessages(
channels.map(channel => ({ channels.map(channel => ({

View File

@ -141,6 +141,13 @@ export function timestamp(date = new Date()) {
const dateFmt = new Intl.DateTimeFormat(window.navigator.language); const dateFmt = new Intl.DateTimeFormat(window.navigator.language);
export const formatDate = dateFmt.format; export const formatDate = dateFmt.format;
export function unix(date) {
if (date) {
return Math.floor(date.getTime() / 1000);
}
return Math.floor(Date.now() / 1000);
}
const canvas = document.createElement('canvas'); const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d'); const ctx = canvas.getContext('2d');

View File

@ -2,20 +2,40 @@ const lineHeight = 24;
const userListWidth = 200; const userListWidth = 200;
const smallScreen = 600; const smallScreen = 600;
export function findBreakpoints(text) { function findBreakpointsString(text, breakpoints, index) {
const breakpoints = [];
for (let i = 0; i < text.length; i++) { for (let i = 0; i < text.length; i++) {
const char = text.charAt(i); const char = text.charAt(i);
if (char === ' ') { if (char === ' ') {
breakpoints.push({ end: i, next: i + 1 }); breakpoints.push({ end: i + index, next: i + 1 + index });
} else if (char === '-' && i !== text.length - 1) { } else if (i !== text.length - 1 && (char === '-' || char === '?')) {
breakpoints.push({ end: i + 1, next: i + 1 }); breakpoints.push({ end: i + 1 + index, next: i + 1 + index });
}
}
}
export function findBreakpoints(text) {
const breakpoints = [];
let length = 0;
if (typeof text === 'string') {
findBreakpointsString(text, breakpoints, length);
length = text.length;
} else if (Array.isArray(text)) {
for (let i = 0; i < text.length; i++) {
const node = text[i];
if (typeof node === 'string') {
findBreakpointsString(node, breakpoints, length);
length += node.length;
} else {
findBreakpointsString(node.props.children, breakpoints, length);
length += node.props.children.length;
}
} }
} }
return breakpoints; return [breakpoints, length];
} }
export function messageHeight( export function messageHeight(

View File

@ -48,7 +48,10 @@ var rootCmd = &cobra.Command{
storage.Initialize(viper.GetString("dir"), viper.GetString("data"), viper.GetString("conf")) storage.Initialize(viper.GetString("dir"), viper.GetString("data"), viper.GetString("conf"))
initConfig(storage.Path.Config(), viper.GetBool("reset-config")) err := initConfig(storage.Path.Config(), viper.GetBool("reset-config"))
if err != nil {
log.Fatal(err)
}
}, },
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
@ -63,6 +66,14 @@ var rootCmd = &cobra.Command{
} }
defer db.Close() defer db.Close()
storage.GetMessageStore = func(user *storage.User) (storage.MessageStore, error) {
return boltdb.New(storage.Path.Log(user.Username))
}
storage.GetMessageSearchProvider = func(user *storage.User) (storage.MessageSearchProvider, error) {
return bleve.New(storage.Path.Index(user.Username))
}
cfg, cfgUpdated := config.LoadConfig() cfg, cfgUpdated := config.LoadConfig()
dispatch := server.New(cfg) dispatch := server.New(cfg)
@ -76,14 +87,6 @@ var rootCmd = &cobra.Command{
dispatch.Store = db dispatch.Store = db
dispatch.SessionStore = db dispatch.SessionStore = db
dispatch.GetMessageStore = func(user *storage.User) (storage.MessageStore, error) {
return boltdb.New(storage.Path.Log(user.Username))
}
dispatch.GetMessageSearchProvider = func(user *storage.User) (storage.MessageSearchProvider, error) {
return bleve.New(storage.Path.Index(user.Username))
}
dispatch.Run() dispatch.Run()
}, },
} }
@ -119,19 +122,16 @@ func init() {
viper.SetDefault("dcc.autoget.delete", true) viper.SetDefault("dcc.autoget.delete", true)
} }
func initConfig(configPath string, overwrite bool) { func initConfig(configPath string, overwrite bool) error {
if _, err := os.Stat(configPath); overwrite || os.IsNotExist(err) { if _, err := os.Stat(configPath); overwrite || os.IsNotExist(err) {
config, err := assets.Asset("config.default.toml") config, err := assets.Asset("config.default.toml")
if err != nil { if err != nil {
log.Println(err) return err
return
} }
log.Println("Writing default config to", configPath) log.Println("Writing default config to", configPath)
err = ioutil.WriteFile(configPath, config, 0600) return ioutil.WriteFile(configPath, config, 0600)
if err != nil {
log.Println(err)
}
} }
return nil
} }

View File

@ -5,6 +5,9 @@ import (
) )
func (c *Client) HasCapability(name string, values ...string) bool { func (c *Client) HasCapability(name string, values ...string) bool {
c.lock.Lock()
defer c.lock.Unlock()
if capValues, ok := c.enabledCapabilities[name]; ok { if capValues, ok := c.enabledCapabilities[name]; ok {
if len(values) == 0 || capValues == nil { if len(values) == 0 || capValues == nil {
return true return true

View File

@ -35,8 +35,10 @@ type Client struct {
Messages chan *Message Messages chan *Message
ConnectionChanged chan ConnectionState ConnectionChanged chan ConnectionState
Features *Features Features *Features
nick string
channels []string state *state
nick string
channels []string
wantedCapabilities []string wantedCapabilities []string
requestedCapabilities map[string][]string requestedCapabilities map[string][]string
@ -80,18 +82,15 @@ func NewClient(config *Config) *Client {
wantedCapabilities = append(wantedCapabilities, "sasl") wantedCapabilities = append(wantedCapabilities, "sasl")
} }
return &Client{ client := &Client{
Config: config, Config: config,
nick: config.Nick,
Features: NewFeatures(),
Messages: make(chan *Message, 32), Messages: make(chan *Message, 32),
ConnectionChanged: make(chan ConnectionState, 4),
Features: NewFeatures(),
nick: config.Nick,
wantedCapabilities: wantedCapabilities, wantedCapabilities: wantedCapabilities,
requestedCapabilities: map[string][]string{}, requestedCapabilities: map[string][]string{},
enabledCapabilities: map[string][]string{}, enabledCapabilities: map[string][]string{},
ConnectionChanged: make(chan ConnectionState, 4),
out: make(chan string, 32),
quit: make(chan struct{}),
reconnect: make(chan struct{}),
dialer: &net.Dialer{Timeout: 10 * time.Second}, dialer: &net.Dialer{Timeout: 10 * time.Second},
recvBuf: make([]byte, 0, 4096), recvBuf: make([]byte, 0, 4096),
backoff: &backoff.Backoff{ backoff: &backoff.Backoff{
@ -99,7 +98,13 @@ func NewClient(config *Config) *Client {
Max: 30 * time.Second, Max: 30 * time.Second,
Jitter: true, Jitter: true,
}, },
out: make(chan string, 32),
quit: make(chan struct{}),
reconnect: make(chan struct{}),
} }
client.state = newState(client)
return client
} }
func (c *Client) GetNick() string { func (c *Client) GetNick() string {
@ -143,6 +148,18 @@ func (c *Client) Host() string {
return c.Config.Host return c.Config.Host
} }
func (c *Client) MOTD() []string {
return c.state.getMOTD()
}
func (c *Client) ChannelUsers(channel string) []string {
return c.state.getUsers(channel)
}
func (c *Client) ChannelTopic(channel string) string {
return c.state.getTopic(channel)
}
func (c *Client) Nick(nick string) { func (c *Client) Nick(nick string) {
c.Write("NICK " + nick) c.Write("NICK " + nick)
} }

View File

@ -21,7 +21,7 @@ func (c *Client) Connect() {
} }
func (c *Client) Reconnect() { func (c *Client) Reconnect() {
close(c.reconnect) c.tryConnect()
} }
func (c *Client) Write(data string) { func (c *Client) Write(data string) {
@ -63,6 +63,7 @@ func (c *Client) run() {
c.sendRecv.Wait() c.sendRecv.Wait()
c.reconnect = make(chan struct{}) c.reconnect = make(chan struct{})
c.state.reset()
time.Sleep(c.backoff.Duration()) time.Sleep(c.backoff.Duration())
c.tryConnect() c.tryConnect()
@ -178,7 +179,7 @@ func (c *Client) recv() {
default: default:
c.connChange(false, nil) c.connChange(false, nil)
c.Reconnect() close(c.reconnect)
return return
} }
} }
@ -195,54 +196,7 @@ func (c *Client) recv() {
return return
} }
switch msg.Command { c.handleMessage(msg)
case PING:
go c.write("PONG :" + msg.LastParam())
case JOIN:
if c.Is(msg.Sender) {
c.addChannel(msg.Params[0])
}
case NICK:
if c.Is(msg.Sender) {
c.setNick(msg.LastParam())
}
case PRIVMSG:
if ctcp := msg.ToCTCP(); ctcp != nil {
c.handleCTCP(ctcp, msg)
}
case CAP:
c.handleCAP(msg)
case RPL_WELCOME:
c.setNick(msg.Params[0])
c.setRegistered(true)
c.flushChannels()
c.backoff.Reset()
c.sendRecv.Add(1)
go c.send()
case RPL_ISUPPORT:
c.Features.Parse(msg.Params)
case ERR_NICKNAMEINUSE, ERR_NICKCOLLISION, ERR_UNAVAILRESOURCE:
if c.Config.HandleNickInUse != nil {
go c.writeNick(c.Config.HandleNickInUse(msg.Params[1]))
}
case ERROR:
c.Messages <- msg
c.connChange(false, nil)
time.Sleep(5 * time.Second)
close(c.quit)
return
}
c.handleSASL(msg)
c.Messages <- msg c.Messages <- msg
} }

View File

@ -147,7 +147,7 @@ func TestRecv(t *testing.T) {
func TestRecvTriggersReconnect(t *testing.T) { func TestRecvTriggersReconnect(t *testing.T) {
c := NewClient(&Config{}) c := NewClient(&Config{})
c.conn = &mockConn{} c.conn = &mockConn{}
c.scan = bufio.NewScanner(bytes.NewBufferString("001 bob\r\n")) c.scan = bufio.NewScanner(bytes.NewBufferString(""))
done := make(chan struct{}) done := make(chan struct{})
ok := false ok := false
go func() { go func() {

158
pkg/irc/internal.go Normal file
View File

@ -0,0 +1,158 @@
package irc
import (
"strings"
"time"
)
func (c *Client) handleMessage(msg *Message) {
switch msg.Command {
case CAP:
c.handleCAP(msg)
case PING:
go c.write("PONG :" + msg.LastParam())
case JOIN:
if len(msg.Params) > 0 {
channel := msg.Params[0]
if c.Is(msg.Sender) {
c.addChannel(channel)
}
c.state.addUser(msg.Sender, channel)
}
case PART:
if len(msg.Params) > 0 {
channel := msg.Params[0]
if c.Is(msg.Sender) {
c.state.removeChannel(channel)
} else {
c.state.removeUser(msg.Sender, channel)
}
}
case QUIT:
msg.meta = c.state.removeUserAll(msg.Sender)
case NICK:
if c.Is(msg.Sender) {
c.setNick(msg.LastParam())
}
msg.meta = c.state.renameUser(msg.Sender, msg.LastParam())
case PRIVMSG:
if ctcp := msg.ToCTCP(); ctcp != nil {
c.handleCTCP(ctcp, msg)
}
case MODE:
if len(msg.Params) > 1 {
target := msg.Params[0]
if len(msg.Params) > 2 && isChannel(target) {
mode := ParseMode(msg.Params[1])
mode.Server = c.Host()
mode.Channel = target
mode.User = msg.Params[2]
c.state.setMode(target, msg.Params[2], mode.Add, mode.Remove)
msg.meta = mode
}
}
case TOPIC, RPL_TOPIC:
chIndex := 0
if msg.Command == RPL_TOPIC {
chIndex = 1
}
if len(msg.Params) > chIndex {
c.state.setTopic(msg.LastParam(), msg.Params[chIndex])
}
case RPL_NOTOPIC:
if len(msg.Params) > 1 {
channel := msg.Params[1]
c.state.setTopic("", channel)
}
case RPL_WELCOME:
if len(msg.Params) > 0 {
c.setNick(msg.Params[0])
}
c.setRegistered(true)
c.flushChannels()
c.backoff.Reset()
c.sendRecv.Add(1)
go c.send()
case RPL_ISUPPORT:
c.Features.Parse(msg.Params)
case ERR_NICKNAMEINUSE, ERR_NICKCOLLISION, ERR_UNAVAILRESOURCE:
if c.Config.HandleNickInUse != nil && len(msg.Params) > 1 {
go c.writeNick(c.Config.HandleNickInUse(msg.Params[1]))
}
case RPL_NAMREPLY:
channel := msg.Params[2]
users := strings.Split(strings.TrimSuffix(msg.LastParam(), " "), " ")
userBuffer := c.state.userBuffers[channel]
c.state.userBuffers[channel] = append(userBuffer, users...)
case RPL_ENDOFNAMES:
channel := msg.Params[1]
users := c.state.userBuffers[channel]
c.state.setUsers(users, channel)
delete(c.state.userBuffers, channel)
msg.meta = users
case ERROR:
c.Messages <- msg
c.connChange(false, nil)
time.Sleep(5 * time.Second)
close(c.quit)
return
}
c.handleSASL(msg)
}
type Mode struct {
Server string
Channel string
User string
Add string
Remove string
}
func ParseMode(mode string) *Mode {
m := Mode{}
add := false
for _, c := range mode {
if c == '+' {
add = true
} else if c == '-' {
add = false
} else if add {
m.Add += string(c)
} else {
m.Remove += string(c)
}
}
return &m
}
func isChannel(s string) bool {
return strings.IndexAny(s, "&#+!") == 0
}

37
pkg/irc/internal_test.go Normal file
View File

@ -0,0 +1,37 @@
package irc
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestHandlePing(t *testing.T) {
c, out := testClientSend()
c.handleMessage(&Message{
Command: "PING",
Params: []string{"voi voi"},
})
assert.Equal(t, "PONG :voi voi\r\n", <-out)
}
func TestHandleNamreply(t *testing.T) {
c, _ := testClientSend()
c.handleMessage(&Message{
Command: RPL_NAMREPLY,
Params: []string{"", "", "#chan", "a b c"},
})
c.handleMessage(&Message{
Command: RPL_NAMREPLY,
Params: []string{"", "", "#chan", "d"},
})
endMsg := &Message{
Command: RPL_ENDOFNAMES,
Params: []string{"", "#chan"},
}
c.handleMessage(endMsg)
assert.Equal(t, []string{"a", "b", "c", "d"}, endMsg.meta)
}

View File

@ -11,6 +11,8 @@ type Message struct {
Host string Host string
Command string Command string
Params []string Params []string
meta interface{}
} }
func (m *Message) LastParam() string { func (m *Message) LastParam() string {

33
pkg/irc/meta.go Normal file
View File

@ -0,0 +1,33 @@
package irc
// GetNickChannels returns the channels the client has in common with
// the user that changed nick
func GetNickChannels(msg *Message) []string {
return stringListMeta(msg)
}
// GetQuitChannels returns the channels the client has in common with
// the user that quit
func GetQuitChannels(msg *Message) []string {
return stringListMeta(msg)
}
func GetMode(msg *Message) *Mode {
if mode, ok := msg.meta.(*Mode); ok {
return mode
}
return nil
}
// GetNamreplyUsers returns all RPL_NAMREPLY users
// when passed a RPL_ENDOFNAMES message
func GetNamreplyUsers(msg *Message) []string {
return stringListMeta(msg)
}
func stringListMeta(msg *Message) []string {
if list, ok := msg.meta.([]string); ok {
return list
}
return nil
}

236
pkg/irc/state.go Normal file
View File

@ -0,0 +1,236 @@
package irc
import (
"strings"
"sync"
)
type state struct {
client *Client
users map[string][]*User
topic map[string]string
userBuffers map[string][]string
motd []string
lock sync.Mutex
}
const userModePrefixes = "~&@%+"
const userModeChars = "qaohv"
type User struct {
nick string
modes string
prefix string
}
func NewUser(nick string) *User {
user := &User{nick: nick}
if i := strings.IndexAny(nick, userModePrefixes); i == 0 {
i = strings.Index(userModePrefixes, string(nick[0]))
user.modes = string(userModeChars[i])
user.nick = nick[1:]
user.updatePrefix()
}
return user
}
func (u *User) String() string {
return u.prefix + u.nick
}
func (u *User) AddModes(modes string) {
for _, mode := range modes {
if strings.Contains(u.modes, string(mode)) {
continue
}
u.modes += string(mode)
}
u.updatePrefix()
}
func (u *User) RemoveModes(modes string) {
for _, mode := range modes {
u.modes = strings.Replace(u.modes, string(mode), "", 1)
}
u.updatePrefix()
}
func (u *User) updatePrefix() {
for i, mode := range userModeChars {
if strings.Contains(u.modes, string(mode)) {
u.prefix = string(userModePrefixes[i])
return
}
}
u.prefix = ""
}
func newState(client *Client) *state {
return &state{
client: client,
users: make(map[string][]*User),
topic: make(map[string]string),
userBuffers: make(map[string][]string),
}
}
func (s *state) reset() {
s.lock.Lock()
s.users = make(map[string][]*User)
s.topic = make(map[string]string)
s.userBuffers = make(map[string][]string)
s.motd = []string{}
s.lock.Unlock()
}
func (s *state) removeChannel(channel string) {
s.lock.Lock()
delete(s.users, channel)
delete(s.topic, channel)
s.lock.Unlock()
}
func (s *state) getUsers(channel string) []string {
s.lock.Lock()
users := make([]string, len(s.users[channel]))
for i, user := range s.users[channel] {
users[i] = user.String()
}
s.lock.Unlock()
return users
}
func (s *state) setUsers(users []string, channel string) {
s.lock.Lock()
s.users[channel] = make([]*User, len(users))
for i, nick := range users {
s.users[channel][i] = NewUser(nick)
}
s.lock.Unlock()
}
func (s *state) addUser(user, channel string) {
s.lock.Lock()
if users, ok := s.users[channel]; ok {
for _, u := range users {
if u.nick == user {
s.lock.Unlock()
return
}
}
s.users[channel] = append(users, NewUser(user))
} else {
s.users[channel] = []*User{NewUser(user)}
}
s.lock.Unlock()
}
func (s *state) removeUser(user, channel string) {
s.lock.Lock()
s.internalRemoveUser(user, channel)
s.lock.Unlock()
}
func (s *state) removeUserAll(user string) []string {
channels := []string{}
s.lock.Lock()
for channel := range s.users {
if s.internalRemoveUser(user, channel) {
channels = append(channels, channel)
}
}
s.lock.Unlock()
return channels
}
func (s *state) renameUser(oldNick, newNick string) []string {
s.lock.Lock()
channels := s.renameAll(oldNick, newNick)
s.lock.Unlock()
return channels
}
func (s *state) setMode(channel, user, add, remove string) {
s.lock.Lock()
for _, u := range s.users[channel] {
if u.nick == user {
u.AddModes(add)
u.RemoveModes(remove)
s.lock.Unlock()
return
}
}
s.lock.Unlock()
}
func (s *state) getTopic(channel string) string {
s.lock.Lock()
topic := s.topic[channel]
s.lock.Unlock()
return topic
}
func (s *state) setTopic(topic, channel string) {
s.lock.Lock()
s.topic[channel] = topic
s.lock.Unlock()
}
func (s *state) getMOTD() []string {
s.lock.Lock()
motd := s.motd
s.lock.Unlock()
return motd
}
func (s *state) rename(channel, oldNick, newNick string) bool {
for _, user := range s.users[channel] {
if user.nick == oldNick {
user.nick = newNick
return true
}
}
return false
}
func (s *state) renameAll(oldNick, newNick string) []string {
channels := []string{}
for channel := range s.users {
if s.rename(channel, oldNick, newNick) {
channels = append(channels, channel)
}
}
return channels
}
func (s *state) internalRemoveUser(user, channel string) bool {
for i, u := range s.users[channel] {
if u.nick == user {
users := s.users[channel]
s.users[channel] = append(users[:i], users[i+1:]...)
return true
}
}
return false
}

89
pkg/irc/state_test.go Normal file
View File

@ -0,0 +1,89 @@
package irc
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestStateGetSetUsers(t *testing.T) {
state := newState(NewClient(&Config{}))
users := []string{"a", "b"}
state.setUsers(users, "#chan")
assert.Equal(t, users, state.getUsers("#chan"))
state.setUsers(users, "#chan")
assert.Equal(t, users, state.getUsers("#chan"))
}
func TestStateAddRemoveUser(t *testing.T) {
state := newState(NewClient(&Config{}))
state.addUser("user", "#chan")
state.addUser("user", "#chan")
assert.Len(t, state.getUsers("#chan"), 1)
state.addUser("user2", "#chan")
assert.Equal(t, []string{"user", "user2"}, state.getUsers("#chan"))
state.removeUser("user", "#chan")
assert.Equal(t, []string{"user2"}, state.getUsers("#chan"))
}
func TestStateRemoveUserAll(t *testing.T) {
state := newState(NewClient(&Config{}))
state.addUser("user", "#chan1")
state.addUser("user", "#chan2")
state.removeUserAll("user")
assert.Empty(t, state.getUsers("#chan1"))
assert.Empty(t, state.getUsers("#chan2"))
}
func TestStateRenameUser(t *testing.T) {
state := newState(NewClient(&Config{}))
state.addUser("user", "#chan1")
state.addUser("user", "#chan2")
state.renameUser("user", "new")
assert.Equal(t, []string{"new"}, state.getUsers("#chan1"))
assert.Equal(t, []string{"new"}, state.getUsers("#chan2"))
state.addUser("@gotop", "#chan3")
state.renameUser("gotop", "stillgotit")
assert.Equal(t, []string{"@stillgotit"}, state.getUsers("#chan3"))
}
func TestStateMode(t *testing.T) {
state := newState(NewClient(&Config{}))
state.addUser("+user", "#chan")
state.setMode("#chan", "user", "o", "v")
assert.Equal(t, []string{"@user"}, state.getUsers("#chan"))
state.setMode("#chan", "user", "v", "")
assert.Equal(t, []string{"@user"}, state.getUsers("#chan"))
state.setMode("#chan", "user", "", "o")
assert.Equal(t, []string{"+user"}, state.getUsers("#chan"))
state.setMode("#chan", "user", "q", "")
assert.Equal(t, []string{"~user"}, state.getUsers("#chan"))
}
func TestStateTopic(t *testing.T) {
state := newState(NewClient(&Config{}))
assert.Equal(t, "", state.getTopic("#chan"))
state.setTopic("the topic", "#chan")
assert.Equal(t, "the topic", state.getTopic("#chan"))
}
func TestStateChannelUserMode(t *testing.T) {
user := NewUser("&test")
assert.Equal(t, "test", user.nick)
assert.Equal(t, "a", string(user.modes[0]))
assert.Equal(t, "&test", user.String())
user.RemoveModes("a")
assert.Equal(t, "test", user.String())
user.AddModes("o")
assert.Equal(t, "@test", user.String())
user.AddModes("q")
assert.Equal(t, "~test", user.String())
user.AddModes("v")
assert.Equal(t, "~test", user.String())
user.RemoveModes("qo")
assert.Equal(t, "+test", user.String())
user.RemoveModes("v")
assert.Equal(t, "test", user.String())
}

View File

@ -63,18 +63,6 @@ func (d *Dispatch) newUser(w http.ResponseWriter, r *http.Request) (*State, erro
return nil, err return nil, err
} }
messageStore, err := d.GetMessageStore(user)
if err != nil {
return nil, err
}
user.SetMessageStore(messageStore)
search, err := d.GetMessageSearchProvider(user)
if err != nil {
return nil, err
}
user.SetMessageSearchProvider(search)
log.Println(r.RemoteAddr, "[Auth] New anonymous user | ID:", user.ID) log.Println(r.RemoteAddr, "[Auth] New anonymous user | ID:", user.ID)
session, err := session.New(user.ID) session, err := session.New(user.ID)

View File

@ -87,7 +87,9 @@ func (d *Dispatch) getIndexData(r *http.Request, state *State) *indexData {
return nil return nil
} }
for i, channel := range channels { for i, channel := range channels {
channels[i].Topic = channelStore.GetTopic(channel.Server, channel.Name) if client, ok := state.getIRC(channel.Server); ok {
channels[i].Topic = client.ChannelTopic(channel.Name)
}
} }
data.Channels = channels data.Channels = channels
@ -106,9 +108,8 @@ func (d *Dispatch) getIndexData(r *http.Request, state *State) *indexData {
} }
func (d *indexData) addUsersAndMessages(server, name string, state *State) { func (d *indexData) addUsersAndMessages(server, name string, state *State) {
if isChannel(name) { if i, ok := state.getIRC(server); ok && isChannel(name) {
users := channelStore.GetUsers(server, name) if users := i.ChannelUsers(name); len(users) > 0 {
if len(users) > 0 {
d.Users = &Userlist{ d.Users = &Userlist{
Server: server, Server: server,
Channel: name, Channel: name,

View File

@ -27,7 +27,6 @@ type ircHandler struct {
state *State state *State
whois WhoisReply whois WhoisReply
userBuffers map[string][]string
motdBuffer MOTD motdBuffer MOTD
listBuffer storage.ChannelListIndex listBuffer storage.ChannelListIndex
dccProgress chan irc.DownloadProgress dccProgress chan irc.DownloadProgress
@ -39,7 +38,6 @@ func newIRCHandler(client *irc.Client, state *State) *ircHandler {
i := &ircHandler{ i := &ircHandler{
client: client, client: client,
state: state, state: state,
userBuffers: make(map[string][]string),
dccProgress: make(chan irc.DownloadProgress, 4), dccProgress: make(chan irc.DownloadProgress, 4),
} }
i.initHandlers() i.initHandlers()
@ -117,41 +115,47 @@ func (i *ircHandler) nick(msg *irc.Message) {
New: msg.LastParam(), New: msg.LastParam(),
}) })
channelStore.RenameUser(msg.Sender, msg.LastParam(), i.client.Host())
if i.client.Is(msg.LastParam()) { if i.client.Is(msg.LastParam()) {
go i.state.user.SetNick(msg.LastParam(), i.client.Host()) go i.state.user.SetNick(msg.LastParam(), i.client.Host())
} }
channels := irc.GetNickChannels(msg)
go i.state.user.LogEvent(i.client.Host(), "nick", []string{msg.Sender, msg.LastParam()}, channels...)
} }
func (i *ircHandler) join(msg *irc.Message) { func (i *ircHandler) join(msg *irc.Message) {
host := i.client.Host()
i.state.sendJSON("join", Join{ i.state.sendJSON("join", Join{
Server: i.client.Host(), Server: host,
User: msg.Sender, User: msg.Sender,
Channels: msg.Params, Channels: msg.Params,
}) })
channel := msg.Params[0] channel := msg.Params[0]
channelStore.AddUser(msg.Sender, i.client.Host(), channel)
if i.client.Is(msg.Sender) { if i.client.Is(msg.Sender) {
// In case no topic is set and there's a cached one that needs to be cleared // In case no topic is set and there's a cached one that needs to be cleared
i.client.Topic(channel) i.client.Topic(channel)
i.state.sendLastMessages(i.client.Host(), channel, 50) i.state.sendLastMessages(host, channel, 50)
go i.state.user.AddChannel(&storage.Channel{ go i.state.user.AddChannel(&storage.Channel{
Server: i.client.Host(), Server: host,
Name: channel, Name: channel,
}) })
} }
go i.state.user.LogEvent(host, "join", []string{msg.Sender}, channel)
} }
func (i *ircHandler) part(msg *irc.Message) { func (i *ircHandler) part(msg *irc.Message) {
host := i.client.Host()
channel := msg.Params[0]
part := Part{ part := Part{
Server: i.client.Host(), Server: host,
User: msg.Sender, User: msg.Sender,
Channel: msg.Params[0], Channel: channel,
} }
if len(msg.Params) == 2 { if len(msg.Params) == 2 {
@ -160,24 +164,18 @@ func (i *ircHandler) part(msg *irc.Message) {
i.state.sendJSON("part", part) i.state.sendJSON("part", part)
channelStore.RemoveUser(msg.Sender, i.client.Host(), part.Channel)
if i.client.Is(msg.Sender) { if i.client.Is(msg.Sender) {
go i.state.user.RemoveChannel(i.client.Host(), part.Channel) go i.state.user.RemoveChannel(host, part.Channel)
} }
go i.state.user.LogEvent(host, "part", []string{msg.Sender}, channel)
} }
func (i *ircHandler) mode(msg *irc.Message) { func (i *ircHandler) mode(msg *irc.Message) {
target := msg.Params[0] if mode := irc.GetMode(msg); mode != nil {
if len(msg.Params) > 2 && isChannel(target) { i.state.sendJSON("mode", Mode{
mode := parseMode(msg.Params[1]) Mode: mode,
mode.Server = i.client.Host() })
mode.Channel = target
mode.User = msg.Params[2]
i.state.sendJSON("mode", mode)
channelStore.SetMode(i.client.Host(), target, msg.Params[2], mode.Add, mode.Remove)
} }
} }
@ -215,8 +213,13 @@ func (i *ircHandler) message(msg *irc.Message) {
} }
if target != "*" && !msg.IsFromServer() { if target != "*" && !msg.IsFromServer() {
go i.state.user.LogMessage(message.ID, go i.state.user.LogMessage(&storage.Message{
i.client.Host(), msg.Sender, target, msg.LastParam()) ID: message.ID,
Server: message.Server,
From: message.From,
To: target,
Content: message.Content,
})
} }
} }
@ -227,7 +230,9 @@ func (i *ircHandler) quit(msg *irc.Message) {
Reason: msg.LastParam(), Reason: msg.LastParam(),
}) })
channelStore.RemoveUserAll(msg.Sender, i.client.Host()) channels := irc.GetQuitChannels(msg)
go i.state.user.LogEvent(i.client.Host(), "quit", []string{msg.Sender, msg.LastParam()}, channels...)
} }
func (i *ircHandler) info(msg *irc.Message) { func (i *ircHandler) info(msg *irc.Message) {
@ -298,6 +303,8 @@ func (i *ircHandler) topic(msg *irc.Message) {
if msg.Command == irc.TOPIC { if msg.Command == irc.TOPIC {
channel = msg.Params[0] channel = msg.Params[0]
nick = msg.Sender nick = msg.Sender
go i.state.user.LogEvent(i.client.Host(), "topic", []string{nick, msg.LastParam()}, channel)
} else { } else {
channel = msg.Params[1] channel = msg.Params[1]
} }
@ -308,39 +315,21 @@ func (i *ircHandler) topic(msg *irc.Message) {
Topic: msg.LastParam(), Topic: msg.LastParam(),
Nick: nick, Nick: nick,
}) })
channelStore.SetTopic(msg.LastParam(), i.client.Host(), channel)
} }
func (i *ircHandler) noTopic(msg *irc.Message) { func (i *ircHandler) noTopic(msg *irc.Message) {
channel := msg.Params[1]
i.state.sendJSON("topic", Topic{ i.state.sendJSON("topic", Topic{
Server: i.client.Host(), Server: i.client.Host(),
Channel: channel, Channel: msg.Params[1],
}) })
channelStore.SetTopic("", i.client.Host(), channel)
}
func (i *ircHandler) names(msg *irc.Message) {
users := strings.Split(strings.TrimSuffix(msg.LastParam(), " "), " ")
userBuffer := i.userBuffers[msg.Params[2]]
i.userBuffers[msg.Params[2]] = append(userBuffer, users...)
} }
func (i *ircHandler) namesEnd(msg *irc.Message) { func (i *ircHandler) namesEnd(msg *irc.Message) {
channel := msg.Params[1]
users := i.userBuffers[channel]
i.state.sendJSON("users", Userlist{ i.state.sendJSON("users", Userlist{
Server: i.client.Host(), Server: i.client.Host(),
Channel: channel, Channel: msg.Params[1],
Users: users, Users: irc.GetNamreplyUsers(msg),
}) })
channelStore.SetUsers(users, i.client.Host(), channel)
delete(i.userBuffers, channel)
} }
func (i *ircHandler) motdStart(msg *irc.Message) { func (i *ircHandler) motdStart(msg *irc.Message) {
@ -363,10 +352,10 @@ func (i *ircHandler) list(msg *irc.Message) {
} }
if i.listBuffer != nil { if i.listBuffer != nil {
c, _ := strconv.Atoi(msg.Params[2]) userCount, _ := strconv.Atoi(msg.Params[2])
i.listBuffer.Add(&storage.ChannelListItem{ i.listBuffer.Add(&storage.ChannelListItem{
Name: msg.Params[1], Name: msg.Params[1],
UserCount: c, UserCount: userCount,
Topic: msg.LastParam(), Topic: msg.LastParam(),
}) })
} }
@ -463,7 +452,6 @@ func (i *ircHandler) initHandlers() {
irc.RPL_ENDOFWHOIS: i.whoisEnd, irc.RPL_ENDOFWHOIS: i.whoisEnd,
irc.RPL_NOTOPIC: i.noTopic, irc.RPL_NOTOPIC: i.noTopic,
irc.RPL_TOPIC: i.topic, irc.RPL_TOPIC: i.topic,
irc.RPL_NAMREPLY: i.names,
irc.RPL_ENDOFNAMES: i.namesEnd, irc.RPL_ENDOFNAMES: i.namesEnd,
irc.RPL_MOTDSTART: i.motdStart, irc.RPL_MOTDSTART: i.motdStart,
irc.RPL_MOTD: i.motd, irc.RPL_MOTD: i.motd,
@ -489,29 +477,14 @@ func (i *ircHandler) sendDCCInfo(message string, log bool, a ...interface{}) {
if log { if log {
i.state.user.AddOpenDM(msg.Server, msg.From) i.state.user.AddOpenDM(msg.Server, msg.From)
i.state.user.LogMessage(betterguid.New(), msg.Server, msg.From, msg.From, msg.Content) i.state.user.LogMessage(&storage.Message{
Server: msg.Server,
From: msg.From,
Content: msg.Content,
})
} }
} }
func parseMode(mode string) *Mode {
m := Mode{}
add := false
for _, c := range mode {
if c == '+' {
add = true
} else if c == '-' {
add = false
} else if add {
m.Add += string(c)
} else {
m.Remove += string(c)
}
}
return &m
}
func isChannel(s string) bool { func isChannel(s string) bool {
return strings.IndexAny(s, "&#+!") == 0 return strings.IndexAny(s, "&#+!") == 0
} }

View File

@ -6,12 +6,11 @@ import (
"os" "os"
"testing" "testing"
"github.com/stretchr/testify/assert"
"github.com/khlieng/dispatch/pkg/irc" "github.com/khlieng/dispatch/pkg/irc"
"github.com/khlieng/dispatch/storage" "github.com/khlieng/dispatch/storage"
"github.com/khlieng/dispatch/storage/bleve" "github.com/khlieng/dispatch/storage/bleve"
"github.com/khlieng/dispatch/storage/boltdb" "github.com/khlieng/dispatch/storage/boltdb"
"github.com/stretchr/testify/assert"
) )
var user *storage.User var user *storage.User
@ -29,19 +28,17 @@ func TestMain(m *testing.M) {
log.Fatal(err) log.Fatal(err)
} }
storage.GetMessageStore = func(_ *storage.User) (storage.MessageStore, error) {
return db, nil
}
storage.GetMessageSearchProvider = func(user *storage.User) (storage.MessageSearchProvider, error) {
return bleve.New(storage.Path.Index(user.Username))
}
user, err = storage.NewUser(db) user, err = storage.NewUser(db)
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
user.SetMessageStore(db)
search, err := bleve.New(storage.Path.Index(user.Username))
if err != nil {
log.Fatal(err)
}
user.SetMessageSearchProvider(search)
channelStore = storage.NewChannelStore()
code := m.Run() code := m.Run()
@ -126,21 +123,6 @@ func TestHandleIRCPart(t *testing.T) {
}, res) }, res)
} }
func TestHandleIRCMode(t *testing.T) {
res := dispatchMessage(&irc.Message{
Command: irc.MODE,
Params: []string{"#chan", "+o-v", "nick"},
})
checkResponse(t, "mode", &Mode{
Server: "host.com",
Channel: "#chan",
User: "nick",
Add: "o",
Remove: "v",
}, res)
}
func TestHandleIRCMessage(t *testing.T) { func TestHandleIRCMessage(t *testing.T) {
res := dispatchMessage(&irc.Message{ res := dispatchMessage(&irc.Message{
Command: irc.PRIVMSG, Command: irc.PRIVMSG,
@ -273,35 +255,6 @@ func TestHandleIRCNoTopic(t *testing.T) {
}, res) }, res)
} }
func TestHandleIRCNames(t *testing.T) {
c := irc.NewClient(&irc.Config{
Nick: "nick",
Username: "user",
Host: "host.com",
})
s := NewState(nil, nil)
i := newIRCHandler(c, s)
i.dispatchMessage(&irc.Message{
Command: irc.RPL_NAMREPLY,
Params: []string{"", "", "#chan", "a b c"},
})
i.dispatchMessage(&irc.Message{
Command: irc.RPL_NAMREPLY,
Params: []string{"", "", "#chan", "d"},
})
i.dispatchMessage(&irc.Message{
Command: irc.RPL_ENDOFNAMES,
Params: []string{"", "#chan"},
})
checkResponse(t, "users", Userlist{
Server: "host.com",
Channel: "#chan",
Users: []string{"a", "b", "c", "d"},
}, <-s.broadcast)
}
func TestHandleIRCMotd(t *testing.T) { func TestHandleIRCMotd(t *testing.T) {
c := irc.NewClient(&irc.Config{ c := irc.NewClient(&irc.Config{
Nick: "nick", Nick: "nick",

View File

@ -86,11 +86,7 @@ type Part struct {
} }
type Mode struct { type Mode struct {
Server string *irc.Mode
Channel string
User string
Add string
Remove string
} }
type Quit struct { type Quit struct {

View File

@ -1,9 +1,10 @@
//out.Data: false//v7: false//v24: false// Code generated by easyjson for marshaling/unmarshaling. DO NOT EDIT. //out.Data: false//v7: false//v30: false// Code generated by easyjson for marshaling/unmarshaling. DO NOT EDIT.
package server package server
import ( import (
json "encoding/json" json "encoding/json"
irc "github.com/khlieng/dispatch/pkg/irc"
storage "github.com/khlieng/dispatch/storage" storage "github.com/khlieng/dispatch/storage"
easyjson "github.com/mailru/easyjson" easyjson "github.com/mailru/easyjson"
jlexer "github.com/mailru/easyjson/jlexer" jlexer "github.com/mailru/easyjson/jlexer"
@ -1200,6 +1201,29 @@ func easyjson42239ddeDecodeGithubComKhliengDispatchStorage(in *jlexer.Lexer, out
out.Content = string(in.String()) out.Content = string(in.String())
case "time": case "time":
out.Time = int64(in.Int64()) out.Time = int64(in.Int64())
case "events":
if in.IsNull() {
in.Skip()
out.Events = nil
} else {
in.Delim('[')
if out.Events == nil {
if !in.IsDelim(']') {
out.Events = make([]storage.Event, 0, 1)
} else {
out.Events = []storage.Event{}
}
} else {
out.Events = (out.Events)[:0]
}
for !in.IsDelim(']') {
var v12 storage.Event
easyjson42239ddeDecodeGithubComKhliengDispatchStorage1(in, &v12)
out.Events = append(out.Events, v12)
in.WantComma()
}
in.Delim(']')
}
default: default:
in.SkipRecursive() in.SkipRecursive()
} }
@ -1244,6 +1268,122 @@ func easyjson42239ddeEncodeGithubComKhliengDispatchStorage(out *jwriter.Writer,
} }
out.Int64(int64(in.Time)) out.Int64(int64(in.Time))
} }
if len(in.Events) != 0 {
const prefix string = ",\"events\":"
if first {
first = false
out.RawString(prefix[1:])
} else {
out.RawString(prefix)
}
{
out.RawByte('[')
for v13, v14 := range in.Events {
if v13 > 0 {
out.RawByte(',')
}
easyjson42239ddeEncodeGithubComKhliengDispatchStorage1(out, v14)
}
out.RawByte(']')
}
}
out.RawByte('}')
}
func easyjson42239ddeDecodeGithubComKhliengDispatchStorage1(in *jlexer.Lexer, out *storage.Event) {
isTopLevel := in.IsStart()
if in.IsNull() {
if isTopLevel {
in.Consumed()
}
in.Skip()
return
}
in.Delim('{')
for !in.IsDelim('}') {
key := in.UnsafeFieldName(false)
in.WantColon()
if in.IsNull() {
in.Skip()
in.WantComma()
continue
}
switch key {
case "type":
out.Type = string(in.String())
case "params":
if in.IsNull() {
in.Skip()
out.Params = nil
} else {
in.Delim('[')
if out.Params == nil {
if !in.IsDelim(']') {
out.Params = make([]string, 0, 4)
} else {
out.Params = []string{}
}
} else {
out.Params = (out.Params)[:0]
}
for !in.IsDelim(']') {
var v15 string
v15 = string(in.String())
out.Params = append(out.Params, v15)
in.WantComma()
}
in.Delim(']')
}
case "time":
out.Time = int64(in.Int64())
default:
in.SkipRecursive()
}
in.WantComma()
}
in.Delim('}')
if isTopLevel {
in.Consumed()
}
}
func easyjson42239ddeEncodeGithubComKhliengDispatchStorage1(out *jwriter.Writer, in storage.Event) {
out.RawByte('{')
first := true
_ = first
if in.Type != "" {
const prefix string = ",\"type\":"
first = false
out.RawString(prefix[1:])
out.String(string(in.Type))
}
if len(in.Params) != 0 {
const prefix string = ",\"params\":"
if first {
first = false
out.RawString(prefix[1:])
} else {
out.RawString(prefix)
}
{
out.RawByte('[')
for v16, v17 := range in.Params {
if v16 > 0 {
out.RawByte(',')
}
out.String(string(v17))
}
out.RawByte(']')
}
}
if in.Time != 0 {
const prefix string = ",\"time\":"
if first {
first = false
out.RawString(prefix[1:])
} else {
out.RawString(prefix)
}
out.Int64(int64(in.Time))
}
out.RawByte('}') out.RawByte('}')
} }
func easyjson42239ddeDecodeGithubComKhliengDispatchServer10(in *jlexer.Lexer, out *SearchRequest) { func easyjson42239ddeDecodeGithubComKhliengDispatchServer10(in *jlexer.Lexer, out *SearchRequest) {
@ -1627,9 +1767,9 @@ func easyjson42239ddeDecodeGithubComKhliengDispatchServer14(in *jlexer.Lexer, ou
out.Channels = (out.Channels)[:0] out.Channels = (out.Channels)[:0]
} }
for !in.IsDelim(']') { for !in.IsDelim(']') {
var v12 string var v18 string
v12 = string(in.String()) v18 = string(in.String())
out.Channels = append(out.Channels, v12) out.Channels = append(out.Channels, v18)
in.WantComma() in.WantComma()
} }
in.Delim(']') in.Delim(']')
@ -1686,11 +1826,11 @@ func easyjson42239ddeEncodeGithubComKhliengDispatchServer14(out *jwriter.Writer,
} }
{ {
out.RawByte('[') out.RawByte('[')
for v13, v14 := range in.Channels { for v19, v20 := range in.Channels {
if v13 > 0 { if v19 > 0 {
out.RawByte(',') out.RawByte(',')
} }
out.String(string(v14)) out.String(string(v20))
} }
out.RawByte(']') out.RawByte(']')
} }
@ -1898,6 +2038,7 @@ func easyjson42239ddeDecodeGithubComKhliengDispatchServer17(in *jlexer.Lexer, ou
in.Skip() in.Skip()
return return
} }
out.Mode = new(irc.Mode)
in.Delim('{') in.Delim('{')
for !in.IsDelim('}') { for !in.IsDelim('}') {
key := in.UnsafeFieldName(false) key := in.UnsafeFieldName(false)
@ -2043,9 +2184,9 @@ func easyjson42239ddeDecodeGithubComKhliengDispatchServer18(in *jlexer.Lexer, ou
out.Messages = (out.Messages)[:0] out.Messages = (out.Messages)[:0]
} }
for !in.IsDelim(']') { for !in.IsDelim(']') {
var v15 storage.Message var v21 storage.Message
easyjson42239ddeDecodeGithubComKhliengDispatchStorage(in, &v15) easyjson42239ddeDecodeGithubComKhliengDispatchStorage(in, &v21)
out.Messages = append(out.Messages, v15) out.Messages = append(out.Messages, v21)
in.WantComma() in.WantComma()
} }
in.Delim(']') in.Delim(']')
@ -2094,11 +2235,11 @@ func easyjson42239ddeEncodeGithubComKhliengDispatchServer18(out *jwriter.Writer,
} }
{ {
out.RawByte('[') out.RawByte('[')
for v16, v17 := range in.Messages { for v22, v23 := range in.Messages {
if v16 > 0 { if v22 > 0 {
out.RawByte(',') out.RawByte(',')
} }
easyjson42239ddeEncodeGithubComKhliengDispatchStorage(out, v17) easyjson42239ddeEncodeGithubComKhliengDispatchStorage(out, v23)
} }
out.RawByte(']') out.RawByte(']')
} }
@ -2315,9 +2456,9 @@ func easyjson42239ddeDecodeGithubComKhliengDispatchServer20(in *jlexer.Lexer, ou
out.Content = (out.Content)[:0] out.Content = (out.Content)[:0]
} }
for !in.IsDelim(']') { for !in.IsDelim(']') {
var v18 string var v24 string
v18 = string(in.String()) v24 = string(in.String())
out.Content = append(out.Content, v18) out.Content = append(out.Content, v24)
in.WantComma() in.WantComma()
} }
in.Delim(']') in.Delim(']')
@ -2362,11 +2503,11 @@ func easyjson42239ddeEncodeGithubComKhliengDispatchServer20(out *jwriter.Writer,
} }
{ {
out.RawByte('[') out.RawByte('[')
for v19, v20 := range in.Content { for v25, v26 := range in.Content {
if v19 > 0 { if v25 > 0 {
out.RawByte(',') out.RawByte(',')
} }
out.String(string(v20)) out.String(string(v26))
} }
out.RawByte(']') out.RawByte(']')
} }
@ -2527,9 +2668,9 @@ func easyjson42239ddeDecodeGithubComKhliengDispatchServer22(in *jlexer.Lexer, ou
out.Channels = (out.Channels)[:0] out.Channels = (out.Channels)[:0]
} }
for !in.IsDelim(']') { for !in.IsDelim(']') {
var v21 string var v27 string
v21 = string(in.String()) v27 = string(in.String())
out.Channels = append(out.Channels, v21) out.Channels = append(out.Channels, v27)
in.WantComma() in.WantComma()
} }
in.Delim(']') in.Delim(']')
@ -2574,11 +2715,11 @@ func easyjson42239ddeEncodeGithubComKhliengDispatchServer22(out *jwriter.Writer,
} }
{ {
out.RawByte('[') out.RawByte('[')
for v22, v23 := range in.Channels { for v28, v29 := range in.Channels {
if v22 > 0 { if v28 > 0 {
out.RawByte(',') out.RawByte(',')
} }
out.String(string(v23)) out.String(string(v29))
} }
out.RawByte(']') out.RawByte(']')
} }
@ -2916,15 +3057,15 @@ func easyjson42239ddeDecodeGithubComKhliengDispatchServer26(in *jlexer.Lexer, ou
for !in.IsDelim('}') { for !in.IsDelim('}') {
key := string(in.String()) key := string(in.String())
in.WantColon() in.WantColon()
var v24 interface{} var v30 interface{}
if m, ok := v24.(easyjson.Unmarshaler); ok { if m, ok := v30.(easyjson.Unmarshaler); ok {
m.UnmarshalEasyJSON(in) m.UnmarshalEasyJSON(in)
} else if m, ok := v24.(json.Unmarshaler); ok { } else if m, ok := v30.(json.Unmarshaler); ok {
_ = m.UnmarshalJSON(in.Raw()) _ = m.UnmarshalJSON(in.Raw())
} else { } else {
v24 = in.Interface() v30 = in.Interface()
} }
(out.Features)[key] = v24 (out.Features)[key] = v30
in.WantComma() in.WantComma()
} }
in.Delim('}') in.Delim('}')
@ -2959,21 +3100,21 @@ func easyjson42239ddeEncodeGithubComKhliengDispatchServer26(out *jwriter.Writer,
} }
{ {
out.RawByte('{') out.RawByte('{')
v25First := true v31First := true
for v25Name, v25Value := range in.Features { for v31Name, v31Value := range in.Features {
if v25First { if v31First {
v25First = false v31First = false
} else { } else {
out.RawByte(',') out.RawByte(',')
} }
out.String(string(v25Name)) out.String(string(v31Name))
out.RawByte(':') out.RawByte(':')
if m, ok := v25Value.(easyjson.Marshaler); ok { if m, ok := v31Value.(easyjson.Marshaler); ok {
m.MarshalEasyJSON(out) m.MarshalEasyJSON(out)
} else if m, ok := v25Value.(json.Marshaler); ok { } else if m, ok := v31Value.(json.Marshaler); ok {
out.Raw(m.MarshalJSON()) out.Raw(m.MarshalJSON())
} else { } else {
out.Raw(json.Marshal(v25Value)) out.Raw(json.Marshal(v31Value))
} }
} }
out.RawByte('}') out.RawByte('}')
@ -3404,17 +3545,17 @@ func easyjson42239ddeDecodeGithubComKhliengDispatchServer31(in *jlexer.Lexer, ou
out.Results = (out.Results)[:0] out.Results = (out.Results)[:0]
} }
for !in.IsDelim(']') { for !in.IsDelim(']') {
var v26 *storage.ChannelListItem var v32 *storage.ChannelListItem
if in.IsNull() { if in.IsNull() {
in.Skip() in.Skip()
v26 = nil v32 = nil
} else { } else {
if v26 == nil { if v32 == nil {
v26 = new(storage.ChannelListItem) v32 = new(storage.ChannelListItem)
} }
easyjson42239ddeDecodeGithubComKhliengDispatchStorage1(in, v26) easyjson42239ddeDecodeGithubComKhliengDispatchStorage2(in, v32)
} }
out.Results = append(out.Results, v26) out.Results = append(out.Results, v32)
in.WantComma() in.WantComma()
} }
in.Delim(']') in.Delim(']')
@ -3445,14 +3586,14 @@ func easyjson42239ddeEncodeGithubComKhliengDispatchServer31(out *jwriter.Writer,
out.RawString(prefix[1:]) out.RawString(prefix[1:])
{ {
out.RawByte('[') out.RawByte('[')
for v27, v28 := range in.Results { for v33, v34 := range in.Results {
if v27 > 0 { if v33 > 0 {
out.RawByte(',') out.RawByte(',')
} }
if v28 == nil { if v34 == nil {
out.RawString("null") out.RawString("null")
} else { } else {
easyjson42239ddeEncodeGithubComKhliengDispatchStorage1(out, *v28) easyjson42239ddeEncodeGithubComKhliengDispatchStorage2(out, *v34)
} }
} }
out.RawByte(']') out.RawByte(']')
@ -3514,7 +3655,7 @@ func (v *ChannelSearchResult) UnmarshalJSON(data []byte) error {
func (v *ChannelSearchResult) UnmarshalEasyJSON(l *jlexer.Lexer) { func (v *ChannelSearchResult) UnmarshalEasyJSON(l *jlexer.Lexer) {
easyjson42239ddeDecodeGithubComKhliengDispatchServer31(l, v) easyjson42239ddeDecodeGithubComKhliengDispatchServer31(l, v)
} }
func easyjson42239ddeDecodeGithubComKhliengDispatchStorage1(in *jlexer.Lexer, out *storage.ChannelListItem) { func easyjson42239ddeDecodeGithubComKhliengDispatchStorage2(in *jlexer.Lexer, out *storage.ChannelListItem) {
isTopLevel := in.IsStart() isTopLevel := in.IsStart()
if in.IsNull() { if in.IsNull() {
if isTopLevel { if isTopLevel {
@ -3549,7 +3690,7 @@ func easyjson42239ddeDecodeGithubComKhliengDispatchStorage1(in *jlexer.Lexer, ou
in.Consumed() in.Consumed()
} }
} }
func easyjson42239ddeEncodeGithubComKhliengDispatchStorage1(out *jwriter.Writer, in storage.ChannelListItem) { func easyjson42239ddeEncodeGithubComKhliengDispatchStorage2(out *jwriter.Writer, in storage.ChannelListItem) {
out.RawByte('{') out.RawByte('{')
first := true first := true
_ = first _ = first

View File

@ -16,16 +16,12 @@ import (
"github.com/khlieng/dispatch/storage" "github.com/khlieng/dispatch/storage"
) )
var channelStore = storage.NewChannelStore()
var channelIndexes = storage.NewChannelIndexManager() var channelIndexes = storage.NewChannelIndexManager()
type Dispatch struct { type Dispatch struct {
Store storage.Store Store storage.Store
SessionStore storage.SessionStore SessionStore storage.SessionStore
GetMessageStore func(*storage.User) (storage.MessageStore, error)
GetMessageSearchProvider func(*storage.User) (storage.MessageSearchProvider, error)
cfg *config.Config cfg *config.Config
upgrader websocket.Upgrader upgrader websocket.Upgrader
states *stateStore states *stateStore
@ -87,18 +83,6 @@ func (d *Dispatch) loadUsers() {
} }
func (d *Dispatch) loadUser(user *storage.User) { func (d *Dispatch) loadUser(user *storage.User) {
messageStore, err := d.GetMessageStore(user)
if err != nil {
log.Fatal(err)
}
user.SetMessageStore(messageStore)
search, err := d.GetMessageSearchProvider(user)
if err != nil {
log.Fatal(err)
}
user.SetMessageSearchProvider(search)
state := NewState(user, d) state := NewState(user, d)
d.states.set(state) d.states.set(state)
go state.run() go state.run()

View File

@ -7,7 +7,7 @@ import (
"strings" "strings"
"github.com/gorilla/websocket" "github.com/gorilla/websocket"
"github.com/kjk/betterguid" "github.com/khlieng/dispatch/storage"
) )
type wsHandler struct { type wsHandler struct {
@ -87,11 +87,13 @@ func (h *wsHandler) init(r *http.Request) {
continue continue
} }
h.state.sendJSON("users", Userlist{ if i, ok := h.state.getIRC(channel.Server); ok {
Server: channel.Server, h.state.sendJSON("users", Userlist{
Channel: channel.Name, Server: channel.Server,
Users: channelStore.GetUsers(channel.Server, channel.Name), Channel: channel.Name,
}) Users: i.ChannelUsers(channel.Name),
})
}
h.state.sendLastMessages(channel.Server, channel.Name, 50) h.state.sendLastMessages(channel.Server, channel.Name, 50)
} }
@ -177,8 +179,12 @@ func (h *wsHandler) message(b []byte) {
if i, ok := h.state.getIRC(data.Server); ok { if i, ok := h.state.getIRC(data.Server); ok {
i.Privmsg(data.To, data.Content) i.Privmsg(data.To, data.Content)
go h.state.user.LogMessage(betterguid.New(), go h.state.user.LogMessage(&storage.Message{
data.Server, i.GetNick(), data.To, data.Content) Server: data.Server,
From: i.GetNick(),
To: data.To,
Content: data.Content,
})
} }
} }

View File

@ -278,19 +278,36 @@ func (s *BoltStore) RemoveOpenDM(user *storage.User, server, nick string) error
}) })
} }
func (s *BoltStore) logMessage(tx *bolt.Tx, message *storage.Message) error {
b, err := tx.Bucket(bucketMessages).CreateBucketIfNotExists([]byte(message.Server + ":" + message.To))
if err != nil {
return err
}
data, err := message.Marshal(nil)
if err != nil {
return err
}
return b.Put([]byte(message.ID), data)
}
func (s *BoltStore) LogMessage(message *storage.Message) error { func (s *BoltStore) LogMessage(message *storage.Message) error {
return s.db.Batch(func(tx *bolt.Tx) error { return s.db.Batch(func(tx *bolt.Tx) error {
b, err := tx.Bucket(bucketMessages).CreateBucketIfNotExists([]byte(message.Server + ":" + message.To)) return s.logMessage(tx, message)
if err != nil { })
return err }
func (s *BoltStore) LogMessages(messages []*storage.Message) error {
return s.db.Batch(func(tx *bolt.Tx) error {
for _, message := range messages {
err := s.logMessage(tx, message)
if err != nil {
return err
}
} }
data, err := message.Marshal(nil) return nil
if err != nil {
return err
}
return b.Put([]byte(message.ID), data)
}) })
} }

View File

@ -1,206 +0,0 @@
package storage
import (
"strings"
"sync"
)
type ChannelStore struct {
users map[string]map[string][]*ChannelStoreUser
userLock sync.Mutex
topic map[string]map[string]string
topicLock sync.Mutex
}
const userModePrefixes = "~&@%+"
const userModeChars = "qaohv"
type ChannelStoreUser struct {
nick string
modes string
prefix string
}
func NewChannelStoreUser(nick string) *ChannelStoreUser {
user := &ChannelStoreUser{nick: nick}
if i := strings.IndexAny(nick, userModePrefixes); i == 0 {
i = strings.Index(userModePrefixes, string(nick[0]))
user.modes = string(userModeChars[i])
user.nick = nick[1:]
user.updatePrefix()
}
return user
}
func (c *ChannelStoreUser) String() string {
return c.prefix + c.nick
}
func (c *ChannelStoreUser) addModes(modes string) {
for _, mode := range modes {
if strings.Contains(c.modes, string(mode)) {
continue
}
c.modes += string(mode)
}
c.updatePrefix()
}
func (c *ChannelStoreUser) removeModes(modes string) {
for _, mode := range modes {
c.modes = strings.Replace(c.modes, string(mode), "", 1)
}
c.updatePrefix()
}
func (c *ChannelStoreUser) updatePrefix() {
for i, mode := range userModeChars {
if strings.Contains(c.modes, string(mode)) {
c.prefix = string(userModePrefixes[i])
return
}
}
c.prefix = ""
}
func NewChannelStore() *ChannelStore {
return &ChannelStore{
users: make(map[string]map[string][]*ChannelStoreUser),
topic: make(map[string]map[string]string),
}
}
func (c *ChannelStore) GetUsers(server, channel string) []string {
c.userLock.Lock()
users := make([]string, len(c.users[server][channel]))
for i, user := range c.users[server][channel] {
users[i] = user.String()
}
c.userLock.Unlock()
return users
}
func (c *ChannelStore) SetUsers(users []string, server, channel string) {
c.userLock.Lock()
if _, ok := c.users[server]; !ok {
c.users[server] = make(map[string][]*ChannelStoreUser)
}
c.users[server][channel] = make([]*ChannelStoreUser, len(users))
for i, nick := range users {
c.users[server][channel][i] = NewChannelStoreUser(nick)
}
c.userLock.Unlock()
}
func (c *ChannelStore) AddUser(user, server, channel string) {
c.userLock.Lock()
if _, ok := c.users[server]; !ok {
c.users[server] = make(map[string][]*ChannelStoreUser)
}
if users, ok := c.users[server][channel]; ok {
for _, u := range users {
if u.nick == user {
c.userLock.Unlock()
return
}
}
c.users[server][channel] = append(users, NewChannelStoreUser(user))
} else {
c.users[server][channel] = []*ChannelStoreUser{NewChannelStoreUser(user)}
}
c.userLock.Unlock()
}
func (c *ChannelStore) RemoveUser(user, server, channel string) {
c.userLock.Lock()
c.removeUser(user, server, channel)
c.userLock.Unlock()
}
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) RenameUser(oldNick, newNick, server string) {
c.userLock.Lock()
c.renameAll(server, oldNick, newNick)
c.userLock.Unlock()
}
func (c *ChannelStore) SetMode(server, channel, user, add, remove string) {
c.userLock.Lock()
for _, u := range c.users[server][channel] {
if u.nick == user {
u.addModes(add)
u.removeModes(remove)
c.userLock.Unlock()
return
}
}
c.userLock.Unlock()
}
func (c *ChannelStore) GetTopic(server, channel string) string {
c.topicLock.Lock()
topic := c.topic[server][channel]
c.topicLock.Unlock()
return topic
}
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) rename(server, channel, oldNick, newNick string) {
for _, user := range c.users[server][channel] {
if user.nick == oldNick {
user.nick = newNick
return
}
}
}
func (c *ChannelStore) renameAll(server, oldNick, newNick string) {
for channel := range c.users[server] {
c.rename(server, channel, oldNick, newNick)
}
}
func (c *ChannelStore) removeUser(user, server, channel string) {
for i, u := range c.users[server][channel] {
if u.nick == user {
users := c.users[server][channel]
c.users[server][channel] = append(users[:i], users[i+1:]...)
return
}
}
}

View File

@ -1,89 +0,0 @@
package storage
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestGetSetUsers(t *testing.T) {
channelStore := NewChannelStore()
users := []string{"a", "b"}
channelStore.SetUsers(users, "srv", "#chan")
assert.Equal(t, users, channelStore.GetUsers("srv", "#chan"))
channelStore.SetUsers(users, "srv", "#chan")
assert.Equal(t, users, channelStore.GetUsers("srv", "#chan"))
}
func TestAddRemoveUser(t *testing.T) {
channelStore := NewChannelStore()
channelStore.AddUser("user", "srv", "#chan")
channelStore.AddUser("user", "srv", "#chan")
assert.Len(t, channelStore.GetUsers("srv", "#chan"), 1)
channelStore.AddUser("user2", "srv", "#chan")
assert.Equal(t, []string{"user", "user2"}, channelStore.GetUsers("srv", "#chan"))
channelStore.RemoveUser("user", "srv", "#chan")
assert.Equal(t, []string{"user2"}, channelStore.GetUsers("srv", "#chan"))
}
func TestRemoveUserAll(t *testing.T) {
channelStore := NewChannelStore()
channelStore.AddUser("user", "srv", "#chan1")
channelStore.AddUser("user", "srv", "#chan2")
channelStore.RemoveUserAll("user", "srv")
assert.Empty(t, channelStore.GetUsers("srv", "#chan1"))
assert.Empty(t, channelStore.GetUsers("srv", "#chan2"))
}
func TestRenameUser(t *testing.T) {
channelStore := NewChannelStore()
channelStore.AddUser("user", "srv", "#chan1")
channelStore.AddUser("user", "srv", "#chan2")
channelStore.RenameUser("user", "new", "srv")
assert.Equal(t, []string{"new"}, channelStore.GetUsers("srv", "#chan1"))
assert.Equal(t, []string{"new"}, channelStore.GetUsers("srv", "#chan2"))
channelStore.AddUser("@gotop", "srv", "#chan3")
channelStore.RenameUser("gotop", "stillgotit", "srv")
assert.Equal(t, []string{"@stillgotit"}, channelStore.GetUsers("srv", "#chan3"))
}
func TestMode(t *testing.T) {
channelStore := NewChannelStore()
channelStore.AddUser("+user", "srv", "#chan")
channelStore.SetMode("srv", "#chan", "user", "o", "v")
assert.Equal(t, []string{"@user"}, channelStore.GetUsers("srv", "#chan"))
channelStore.SetMode("srv", "#chan", "user", "v", "")
assert.Equal(t, []string{"@user"}, channelStore.GetUsers("srv", "#chan"))
channelStore.SetMode("srv", "#chan", "user", "", "o")
assert.Equal(t, []string{"+user"}, channelStore.GetUsers("srv", "#chan"))
channelStore.SetMode("srv", "#chan", "user", "q", "")
assert.Equal(t, []string{"~user"}, channelStore.GetUsers("srv", "#chan"))
}
func TestTopic(t *testing.T) {
channelStore := NewChannelStore()
assert.Equal(t, "", channelStore.GetTopic("srv", "#chan"))
channelStore.SetTopic("the topic", "srv", "#chan")
assert.Equal(t, "the topic", channelStore.GetTopic("srv", "#chan"))
}
func TestChannelUserMode(t *testing.T) {
user := NewChannelStoreUser("&test")
assert.Equal(t, "test", user.nick)
assert.Equal(t, "a", string(user.modes[0]))
assert.Equal(t, "&test", user.String())
user.removeModes("a")
assert.Equal(t, "test", user.String())
user.addModes("o")
assert.Equal(t, "@test", user.String())
user.addModes("q")
assert.Equal(t, "~test", user.String())
user.addModes("v")
assert.Equal(t, "~test", user.String())
user.removeModes("qo")
assert.Equal(t, "+test", user.String())
user.removeModes("v")
assert.Equal(t, "test", user.String())
}

View File

@ -7,7 +7,12 @@ import (
"github.com/khlieng/dispatch/pkg/session" "github.com/khlieng/dispatch/pkg/session"
) )
var Path directory var (
Path directory
GetMessageStore MessageStoreCreator
GetMessageSearchProvider MessageSearchProviderCreator
)
func Initialize(root, dataRoot, configRoot string) { func Initialize(root, dataRoot, configRoot string) {
if root != DefaultDirectory() { if root != DefaultDirectory() {
@ -52,13 +57,18 @@ type SessionStore interface {
type MessageStore interface { type MessageStore interface {
LogMessage(message *Message) error LogMessage(message *Message) error
LogMessages(messages []*Message) error
GetMessages(server, channel string, count int, fromID string) ([]Message, bool, error) GetMessages(server, channel string, count int, fromID string) ([]Message, bool, error)
GetMessagesByID(server, channel string, ids []string) ([]Message, error) GetMessagesByID(server, channel string, ids []string) ([]Message, error)
Close() Close()
} }
type MessageStoreCreator func(*User) (MessageStore, error)
type MessageSearchProvider interface { type MessageSearchProvider interface {
SearchMessages(server, channel, q string) ([]string, error) SearchMessages(server, channel, q string) ([]string, error)
Index(id string, message *Message) error Index(id string, message *Message) error
Close() Close()
} }
type MessageSearchProviderCreator func(*User) (MessageSearchProvider, error)

View File

@ -25,7 +25,6 @@ struct Server {
struct Channel { struct Channel {
Server string Server string
Name string Name string
Topic string
} }
struct Message { struct Message {
@ -33,4 +32,11 @@ struct Message {
From string From string
Content string Content string
Time int64 Time int64
Events []Event
}
struct Event {
Type string
Params []string
Time int64
} }

View File

@ -791,21 +791,6 @@ func (d *Channel) Size() (s uint64) {
} }
s += l s += l
} }
{
l := uint64(len(d.Topic))
{
t := l
for t >= 0x80 {
t >>= 7
s++
}
s++
}
s += l
}
return return
} }
func (d *Channel) Marshal(buf []byte) ([]byte, error) { func (d *Channel) Marshal(buf []byte) ([]byte, error) {
@ -857,25 +842,6 @@ func (d *Channel) Marshal(buf []byte) ([]byte, error) {
copy(buf[i+0:], d.Name) copy(buf[i+0:], d.Name)
i += l i += l
} }
{
l := uint64(len(d.Topic))
{
t := uint64(l)
for t >= 0x80 {
buf[i+0] = byte(t) | 0x80
t >>= 7
i++
}
buf[i+0] = byte(t)
i++
}
copy(buf[i+0:], d.Topic)
i += l
}
return buf[:i+0], nil return buf[:i+0], nil
} }
@ -922,26 +888,6 @@ func (d *Channel) Unmarshal(buf []byte) (uint64, error) {
d.Name = string(buf[i+0 : i+0+l]) d.Name = string(buf[i+0 : i+0+l])
i += l i += l
} }
{
l := uint64(0)
{
bs := uint8(7)
t := uint64(buf[i+0] & 0x7F)
for buf[i+0]&0x80 == 0x80 {
i++
t |= uint64(buf[i+0]&0x7F) << bs
bs += 7
}
i++
l = t
}
d.Topic = string(buf[i+0 : i+0+l])
i += l
}
return i + 0, nil return i + 0, nil
} }
@ -992,6 +938,29 @@ func (d *Message) Size() (s uint64) {
} }
s += l s += l
} }
{
l := uint64(len(d.Events))
{
t := l
for t >= 0x80 {
t >>= 7
s++
}
s++
}
for k0 := range d.Events {
{
s += d.Events[k0].Size()
}
}
}
s += 8 s += 8
return return
} }
@ -1068,6 +1037,34 @@ func (d *Message) Marshal(buf []byte) ([]byte, error) {
*(*int64)(unsafe.Pointer(&buf[i+0])) = d.Time *(*int64)(unsafe.Pointer(&buf[i+0])) = d.Time
} }
{
l := uint64(len(d.Events))
{
t := uint64(l)
for t >= 0x80 {
buf[i+8] = byte(t) | 0x80
t >>= 7
i++
}
buf[i+8] = byte(t)
i++
}
for k0 := range d.Events {
{
nbuf, err := d.Events[k0].Marshal(buf[i+8:])
if err != nil {
return nil, err
}
i += uint64(len(nbuf))
}
}
}
return buf[:i+8], nil return buf[:i+8], nil
} }
@ -1138,6 +1135,251 @@ func (d *Message) Unmarshal(buf []byte) (uint64, error) {
d.Time = *(*int64)(unsafe.Pointer(&buf[i+0])) d.Time = *(*int64)(unsafe.Pointer(&buf[i+0]))
}
{
l := uint64(0)
{
bs := uint8(7)
t := uint64(buf[i+8] & 0x7F)
for buf[i+8]&0x80 == 0x80 {
i++
t |= uint64(buf[i+8]&0x7F) << bs
bs += 7
}
i++
l = t
}
if uint64(cap(d.Events)) >= l {
d.Events = d.Events[:l]
} else {
d.Events = make([]Event, l)
}
for k0 := range d.Events {
{
ni, err := d.Events[k0].Unmarshal(buf[i+8:])
if err != nil {
return 0, err
}
i += ni
}
}
}
return i + 8, nil
}
func (d *Event) Size() (s uint64) {
{
l := uint64(len(d.Type))
{
t := l
for t >= 0x80 {
t >>= 7
s++
}
s++
}
s += l
}
{
l := uint64(len(d.Params))
{
t := l
for t >= 0x80 {
t >>= 7
s++
}
s++
}
for k0 := range d.Params {
{
l := uint64(len(d.Params[k0]))
{
t := l
for t >= 0x80 {
t >>= 7
s++
}
s++
}
s += l
}
}
}
s += 8
return
}
func (d *Event) Marshal(buf []byte) ([]byte, error) {
size := d.Size()
{
if uint64(cap(buf)) >= size {
buf = buf[:size]
} else {
buf = make([]byte, size)
}
}
i := uint64(0)
{
l := uint64(len(d.Type))
{
t := uint64(l)
for t >= 0x80 {
buf[i+0] = byte(t) | 0x80
t >>= 7
i++
}
buf[i+0] = byte(t)
i++
}
copy(buf[i+0:], d.Type)
i += l
}
{
l := uint64(len(d.Params))
{
t := uint64(l)
for t >= 0x80 {
buf[i+0] = byte(t) | 0x80
t >>= 7
i++
}
buf[i+0] = byte(t)
i++
}
for k0 := range d.Params {
{
l := uint64(len(d.Params[k0]))
{
t := uint64(l)
for t >= 0x80 {
buf[i+0] = byte(t) | 0x80
t >>= 7
i++
}
buf[i+0] = byte(t)
i++
}
copy(buf[i+0:], d.Params[k0])
i += l
}
}
}
{
*(*int64)(unsafe.Pointer(&buf[i+0])) = d.Time
}
return buf[:i+8], nil
}
func (d *Event) Unmarshal(buf []byte) (uint64, error) {
i := uint64(0)
{
l := uint64(0)
{
bs := uint8(7)
t := uint64(buf[i+0] & 0x7F)
for buf[i+0]&0x80 == 0x80 {
i++
t |= uint64(buf[i+0]&0x7F) << bs
bs += 7
}
i++
l = t
}
d.Type = string(buf[i+0 : i+0+l])
i += l
}
{
l := uint64(0)
{
bs := uint8(7)
t := uint64(buf[i+0] & 0x7F)
for buf[i+0]&0x80 == 0x80 {
i++
t |= uint64(buf[i+0]&0x7F) << bs
bs += 7
}
i++
l = t
}
if uint64(cap(d.Params)) >= l {
d.Params = d.Params[:l]
} else {
d.Params = make([]string, l)
}
for k0 := range d.Params {
{
l := uint64(0)
{
bs := uint8(7)
t := uint64(buf[i+0] & 0x7F)
for buf[i+0]&0x80 == 0x80 {
i++
t |= uint64(buf[i+0]&0x7F) << bs
bs += 7
}
i++
l = t
}
d.Params[k0] = string(buf[i+0 : i+0+l])
i += l
}
}
}
{
d.Time = *(*int64)(unsafe.Pointer(&buf[i+0]))
} }
return i + 8, nil return i + 8, nil
} }

View File

@ -5,6 +5,8 @@ import (
"os" "os"
"sync" "sync"
"time" "time"
"github.com/kjk/betterguid"
) )
type User struct { type User struct {
@ -15,6 +17,7 @@ type User struct {
store Store store Store
messageLog MessageStore messageLog MessageStore
messageIndex MessageSearchProvider messageIndex MessageSearchProvider
lastMessages map[string]map[string]*Message
clientSettings *ClientSettings clientSettings *ClientSettings
lastIP []byte lastIP []byte
certificate *tls.Certificate certificate *tls.Certificate
@ -25,6 +28,7 @@ func NewUser(store Store) (*User, error) {
user := &User{ user := &User{
store: store, store: store,
clientSettings: DefaultClientSettings(), clientSettings: DefaultClientSettings(),
lastMessages: map[string]map[string]*Message{},
} }
err := store.SaveUser(user) err := store.SaveUser(user)
@ -32,11 +36,19 @@ func NewUser(store Store) (*User, error) {
return nil, err return nil, err
} }
user.messageLog, err = GetMessageStore(user)
if err != nil {
return nil, err
}
user.messageIndex, err = GetMessageSearchProvider(user)
if err != nil {
return nil, err
}
err = os.MkdirAll(Path.User(user.Username), 0700) err = os.MkdirAll(Path.User(user.Username), 0700)
if err != nil { if err != nil {
return nil, err return nil, err
} }
err = os.Mkdir(Path.Downloads(user.Username), 0700) err = os.Mkdir(Path.Downloads(user.Username), 0700)
if err != nil { if err != nil {
return nil, err return nil, err
@ -53,20 +65,35 @@ func LoadUsers(store Store) ([]*User, error) {
for _, user := range users { for _, user := range users {
user.store = store user.store = store
user.messageLog, err = GetMessageStore(user)
if err != nil {
return nil, err
}
user.messageIndex, err = GetMessageSearchProvider(user)
if err != nil {
return nil, err
}
user.lastMessages = map[string]map[string]*Message{}
user.loadCertificate() user.loadCertificate()
channels, err := user.GetChannels()
if err != nil {
return nil, err
}
for _, channel := range channels {
messages, _, err := user.GetLastMessages(channel.Server, channel.Name, 1)
if err == nil && len(messages) == 1 {
user.lastMessages[channel.Server] = map[string]*Message{
channel.Name: &messages[0],
}
}
}
} }
return users, nil return users, nil
} }
func (u *User) SetMessageStore(store MessageStore) {
u.messageLog = store
}
func (u *User) SetMessageSearchProvider(search MessageSearchProvider) {
u.messageIndex = search
}
func (u *User) Remove() { func (u *User) Remove() {
u.store.DeleteUser(u) u.store.DeleteUser(u)
if u.messageLog != nil { if u.messageLog != nil {
@ -178,7 +205,6 @@ func (u *User) SetServerName(name, address string) error {
return u.AddServer(server) return u.AddServer(server)
} }
// TODO: Remove topic from disk schema
type Channel struct { type Channel struct {
Server string Server string
Name string Name string
@ -215,33 +241,128 @@ func (u *User) RemoveOpenDM(server, nick string) error {
} }
type Message struct { type Message struct {
ID string `json:"-" bleve:"-"` ID string `json:"-" bleve:"-"`
Server string `json:"-" bleve:"server"` Server string `json:"-" bleve:"server"`
From string `bleve:"-"` From string `bleve:"-"`
To string `json:"-" bleve:"to"` To string `json:"-" bleve:"to"`
Content string `bleve:"content"` Content string `bleve:"content"`
Time int64 `bleve:"-"` Time int64 `bleve:"-"`
Events []Event `bleve:"-"`
} }
func (m Message) Type() string { func (m Message) Type() string {
return "message" return "message"
} }
func (u *User) LogMessage(id, server, from, to, content string) error { func (u *User) LogMessage(msg *Message) error {
message := &Message{ if msg.Time == 0 {
ID: id, msg.Time = time.Now().Unix()
Server: server,
From: from,
To: to,
Content: content,
Time: time.Now().Unix(),
} }
err := u.messageLog.LogMessage(message) if msg.ID == "" {
msg.ID = betterguid.New()
}
if msg.To == "" {
msg.To = msg.From
}
u.setLastMessage(msg.Server, msg.To, msg)
err := u.messageLog.LogMessage(msg)
if err != nil { if err != nil {
return err return err
} }
return u.messageIndex.Index(id, message) return u.messageIndex.Index(msg.ID, msg)
}
type Event struct {
Type string
Params []string
Time int64
}
func (u *User) LogEvent(server, name string, params []string, channels ...string) error {
now := time.Now().Unix()
event := Event{
Type: name,
Params: params,
Time: now,
}
for _, channel := range channels {
lastMessage := u.getLastMessage(server, channel)
if lastMessage != nil && shouldCollapse(lastMessage, event) {
lastMessage.Events = append(lastMessage.Events, event)
u.setLastMessage(server, channel, lastMessage)
err := u.messageLog.LogMessage(lastMessage)
if err != nil {
return err
}
} else {
msg := &Message{
ID: betterguid.New(),
Server: server,
To: channel,
Time: now,
Events: []Event{event},
}
u.setLastMessage(server, channel, msg)
err := u.messageLog.LogMessage(msg)
if err != nil {
return err
}
}
}
return nil
}
var collapsed = []string{"join", "part", "quit"}
func shouldCollapse(msg *Message, event Event) bool {
matches := 0
if len(msg.Events) > 0 {
for _, collapseType := range collapsed {
if msg.Events[0].Type == collapseType {
matches++
}
if event.Type == collapseType {
matches++
}
}
}
return matches == 2
}
func (u *User) getLastMessage(server, channel string) *Message {
u.lock.Lock()
defer u.lock.Unlock()
if _, ok := u.lastMessages[server]; !ok {
return nil
}
last := u.lastMessages[server][channel]
if last != nil {
msg := *last
return &msg
}
return nil
}
func (u *User) setLastMessage(server, channel string, msg *Message) {
u.lock.Lock()
if _, ok := u.lastMessages[server]; !ok {
u.lastMessages[server] = map[string]*Message{}
}
u.lastMessages[server][channel] = msg
u.lock.Unlock()
} }
func (u *User) GetMessages(server, channel string, count int, fromID string) ([]Message, bool, error) { func (u *User) GetMessages(server, channel string, count int, fromID string) ([]Message, bool, error) {

View File

@ -24,6 +24,13 @@ func TestUser(t *testing.T) {
db, err := boltdb.New(storage.Path.Database()) db, err := boltdb.New(storage.Path.Database())
assert.Nil(t, err) assert.Nil(t, err)
storage.GetMessageStore = func(_ *storage.User) (storage.MessageStore, error) {
return db, nil
}
storage.GetMessageSearchProvider = func(_ *storage.User) (storage.MessageSearchProvider, error) {
return nil, nil
}
user, err := storage.NewUser(db) user, err := storage.NewUser(db)
assert.Nil(t, err) assert.Nil(t, err)
@ -124,17 +131,18 @@ func TestMessages(t *testing.T) {
db, err := boltdb.New(storage.Path.Database()) db, err := boltdb.New(storage.Path.Database())
assert.Nil(t, err) assert.Nil(t, err)
storage.GetMessageStore = func(_ *storage.User) (storage.MessageStore, error) {
return db, nil
}
storage.GetMessageSearchProvider = func(user *storage.User) (storage.MessageSearchProvider, error) {
return bleve.New(storage.Path.Index(user.Username))
}
user, err := storage.NewUser(db) user, err := storage.NewUser(db)
assert.Nil(t, err) assert.Nil(t, err)
os.MkdirAll(storage.Path.User(user.Username), 0700) os.MkdirAll(storage.Path.User(user.Username), 0700)
search, err := bleve.New(storage.Path.Index(user.Username))
assert.Nil(t, err)
user.SetMessageStore(db)
user.SetMessageSearchProvider(search)
messages, hasMore, err := user.GetMessages("irc.freenode.net", "#go-nuts", 10, "6") messages, hasMore, err := user.GetMessages("irc.freenode.net", "#go-nuts", 10, "6")
assert.Nil(t, err) assert.Nil(t, err)
assert.False(t, hasMore) assert.False(t, hasMore)
@ -153,7 +161,13 @@ func TestMessages(t *testing.T) {
for i := 0; i < 5; i++ { for i := 0; i < 5; i++ {
id := betterguid.New() id := betterguid.New()
ids = append(ids, id) ids = append(ids, id)
err = user.LogMessage(id, "irc.freenode.net", "nick", "#go-nuts", "message"+strconv.Itoa(i)) err = user.LogMessage(&storage.Message{
ID: id,
Server: "irc.freenode.net",
From: "nick",
To: "#go-nuts",
Content: "message" + strconv.Itoa(i),
})
assert.Nil(t, err) assert.Nil(t, err)
} }
@ -196,5 +210,42 @@ func TestMessages(t *testing.T) {
assert.Nil(t, err) assert.Nil(t, err)
assert.True(t, len(messages) > 0) assert.True(t, len(messages) > 0)
user.LogEvent("irc.freenode.net", "join", []string{"bob"}, "#go-nuts")
messages, hasMore, err = user.GetLastMessages("irc.freenode.net", "#go-nuts", 1)
assert.Zero(t, messages[0].Content)
assert.Nil(t, err)
assert.True(t, hasMore)
assert.Len(t, messages[0].Events, 1)
assert.Equal(t, "join", messages[0].Events[0].Type)
assert.NotZero(t, messages[0].Events[0].Time)
user.LogEvent("irc.freenode.net", "part", []string{"bob"}, "#go-nuts")
messages, hasMore, err = user.GetLastMessages("irc.freenode.net", "#go-nuts", 1)
assert.Zero(t, messages[0].Content)
assert.Nil(t, err)
assert.True(t, hasMore)
assert.Len(t, messages[0].Events, 2)
assert.Equal(t, "part", messages[0].Events[1].Type)
assert.NotZero(t, messages[0].Events[0].Time)
user.LogEvent("irc.freenode.net", "nick", []string{"bob", "rob"}, "#go-nuts")
messages, hasMore, err = user.GetLastMessages("irc.freenode.net", "#go-nuts", 1)
assert.Zero(t, messages[0].Content)
assert.Nil(t, err)
assert.True(t, hasMore)
assert.Len(t, messages[0].Events, 1)
assert.Equal(t, "nick", messages[0].Events[0].Type)
assert.NotZero(t, messages[0].Events[0].Time)
user.LogEvent("irc.freenode.net", "quit", []string{"rob", "bored"}, "#go-nuts")
messages, hasMore, err = user.GetLastMessages("irc.freenode.net", "#go-nuts", 1)
assert.Zero(t, messages[0].Content)
assert.Nil(t, err)
assert.True(t, hasMore)
assert.Len(t, messages[0].Events, 1)
assert.Equal(t, "quit", messages[0].Events[0].Type)
assert.Equal(t, []string{"rob", "bored"}, messages[0].Events[0].Params)
assert.NotZero(t, messages[0].Events[0].Time)
db.Close() db.Close()
} }