Collapse and log join, part and quit, closes #27, log nick and topic changes, move state into irc package
This commit is contained in:
parent
edd4d6eadb
commit
ead3b37cf9
File diff suppressed because one or more lines are too long
@ -725,6 +725,11 @@ input.chat-title {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.message-events-more {
|
||||
font-weight: 700;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.message-input-wrap {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
|
@ -2,24 +2,23 @@ import React, { memo } from 'react';
|
||||
import classnames from 'classnames';
|
||||
import stringToRGB from 'utils/color';
|
||||
|
||||
const Message = ({ message, coloredNick, style, onNickClick }) => {
|
||||
const Message = ({ message, coloredNick, onNickClick }) => {
|
||||
const className = classnames('message', {
|
||||
[`message-${message.type}`]: message.type
|
||||
});
|
||||
|
||||
if (message.type === 'date') {
|
||||
return (
|
||||
<div className={className} style={style}>
|
||||
<div className={className}>
|
||||
{message.content}
|
||||
<hr />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
style = {
|
||||
...style,
|
||||
paddingLeft: `${window.messageIndent + 15}px`,
|
||||
textIndent: `-${window.messageIndent}px`
|
||||
const style = {
|
||||
paddingLeft: `${message.indent + 15}px`,
|
||||
textIndent: `-${message.indent}px`
|
||||
};
|
||||
|
||||
const senderStyle = {};
|
||||
|
@ -247,12 +247,13 @@ export default class MessageBox extends PureComponent {
|
||||
const message = messages[index - 1];
|
||||
|
||||
return (
|
||||
<Message
|
||||
message={message}
|
||||
coloredNick={coloredNicks}
|
||||
style={style}
|
||||
onNickClick={onNickClick}
|
||||
/>
|
||||
<div style={style}>
|
||||
<Message
|
||||
message={message}
|
||||
coloredNick={coloredNicks}
|
||||
onNickClick={onNickClick}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -1,21 +1,17 @@
|
||||
import { socketAction } from 'state/actions';
|
||||
import { setConnected } from 'state/app';
|
||||
import {
|
||||
broadcast,
|
||||
inform,
|
||||
print,
|
||||
addMessage,
|
||||
addMessages
|
||||
addMessages,
|
||||
addEvent,
|
||||
broadcastEvent
|
||||
} from 'state/messages';
|
||||
import { openModal } from 'state/modals';
|
||||
import { reconnect } from 'state/servers';
|
||||
import { select } from 'state/tab';
|
||||
import { find } from 'utils';
|
||||
|
||||
function withReason(message, reason) {
|
||||
return message + (reason ? ` (${reason})` : '');
|
||||
}
|
||||
|
||||
function findChannels(state, server, user) {
|
||||
const channels = [];
|
||||
|
||||
@ -46,37 +42,34 @@ export default function handleSocket({
|
||||
},
|
||||
|
||||
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 }) {
|
||||
dispatch(
|
||||
inform(withReason(`${user} left the channel`, reason), server, channel)
|
||||
);
|
||||
dispatch(addEvent(server, channel, 'part', user, reason));
|
||||
},
|
||||
|
||||
quit({ user, server, reason }) {
|
||||
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 }) {
|
||||
if (oldNick) {
|
||||
const channels = findChannels(getState(), server, oldNick);
|
||||
dispatch(
|
||||
broadcast(`${oldNick} changed nick to ${newNick}`, server, channels)
|
||||
);
|
||||
dispatch(broadcastEvent(server, channels, 'nick', oldNick, newNick));
|
||||
}
|
||||
},
|
||||
|
||||
topic({ server, channel, topic, nick }) {
|
||||
if (nick) {
|
||||
if (topic) {
|
||||
dispatch(addEvent(server, channel, 'topic', nick, topic));
|
||||
/* if (topic) {
|
||||
dispatch(inform(`${nick} changed the topic to:`, server, channel));
|
||||
dispatch(print(topic, server, channel));
|
||||
} else {
|
||||
dispatch(inform(`${nick} cleared the topic`, server, channel));
|
||||
}
|
||||
} */
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -10,7 +10,6 @@ const smallScreen = 600;
|
||||
|
||||
export default function widthUpdates({ store }) {
|
||||
when(store, getCharWidth, charWidth => {
|
||||
window.messageIndent = 6 * charWidth;
|
||||
const scrollBarWidth = measureScrollBarWidth();
|
||||
let prevWrapWidth;
|
||||
|
||||
|
@ -1,6 +1,7 @@
|
||||
import reducer, { broadcast, getMessageTab } from '../messages';
|
||||
import * as actions from '../actions';
|
||||
import appReducer from '../app';
|
||||
import { unix } from 'utils';
|
||||
|
||||
describe('message reducer', () => {
|
||||
it('adds the message on ADD_MESSAGE', () => {
|
||||
@ -98,7 +99,7 @@ describe('message reducer', () => {
|
||||
it('adds date markers when prepending messages', () => {
|
||||
let state = {
|
||||
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',
|
||||
prepend: true,
|
||||
messages: [
|
||||
{ id: 1, date: new Date(1990, 0, 2) },
|
||||
{ id: 2, date: new Date(1990, 0, 3) }
|
||||
{ id: 1, time: unix(new Date(1990, 0, 1)) },
|
||||
{ id: 2, time: unix(new Date(1990, 0, 2)) }
|
||||
]
|
||||
});
|
||||
|
||||
@ -150,7 +151,7 @@ describe('message reducer', () => {
|
||||
it('adds date markers when adding messages', () => {
|
||||
let state = {
|
||||
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',
|
||||
tab: '#chan1',
|
||||
messages: [
|
||||
{ id: 1, date: new Date(1990, 0, 2) },
|
||||
{ id: 2, date: new Date(1990, 0, 3) },
|
||||
{ id: 3, date: new Date(1990, 0, 3) }
|
||||
{ id: 1, time: unix(new Date(1990, 0, 2)) },
|
||||
{ id: 2, time: unix(new Date(1990, 0, 3)) },
|
||||
{ id: 3, time: unix(new Date(1990, 0, 3)) }
|
||||
]
|
||||
});
|
||||
|
||||
|
@ -1,3 +1,4 @@
|
||||
import React from 'react';
|
||||
import { createSelector } from 'reselect';
|
||||
import has from 'lodash/has';
|
||||
import {
|
||||
@ -6,8 +7,10 @@ import {
|
||||
linkify,
|
||||
timestamp,
|
||||
isChannel,
|
||||
formatDate
|
||||
formatDate,
|
||||
unix
|
||||
} from 'utils';
|
||||
import stringToRGB from 'utils/color';
|
||||
import colorify from 'utils/colorify';
|
||||
import createReducer from 'utils/createReducer';
|
||||
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;
|
||||
|
||||
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) {
|
||||
const message = {
|
||||
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 = [];
|
||||
|
||||
for (let i = 0; i < messages.length; i++) {
|
||||
if (i > 0 && !isSameDay(messages[i - 1].date, messages[i].date)) {
|
||||
msgs.push(createDateMessage(messages[i].date));
|
||||
const message = messages[i];
|
||||
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];
|
||||
@ -110,15 +339,41 @@ function reducerAddMessage(message, server, tab, state) {
|
||||
export default createReducer(
|
||||
{},
|
||||
{
|
||||
[actions.ADD_MESSAGE](state, { server, tab, message }) {
|
||||
[actions.ADD_MESSAGE](
|
||||
state,
|
||||
{ server, tab, message, wrapWidth, charWidth, windowWidth }
|
||||
) {
|
||||
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) {
|
||||
init(state, server, tab);
|
||||
reducerPrependMessages(messages, server, tab, state);
|
||||
reducerPrependMessages(
|
||||
state,
|
||||
messages,
|
||||
server,
|
||||
tab,
|
||||
wrapWidth,
|
||||
charWidth,
|
||||
windowWidth
|
||||
);
|
||||
} else {
|
||||
if (!messages[0].tab) {
|
||||
init(state, server, tab);
|
||||
@ -128,7 +383,19 @@ export default createReducer(
|
||||
if (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) {
|
||||
if (!to || to === '*' || (!isChannel(to) && to.indexOf('.') !== -1)) {
|
||||
return server;
|
||||
@ -284,19 +504,19 @@ export function updateMessageHeight(wrapWidth, charWidth, windowWidth) {
|
||||
export function sendMessage(content, to, server) {
|
||||
return (dispatch, getState) => {
|
||||
const state = getState();
|
||||
const { wrapWidth, charWidth, windowWidth } = getApp(state);
|
||||
|
||||
dispatch({
|
||||
type: actions.ADD_MESSAGE,
|
||||
server,
|
||||
tab: to,
|
||||
message: initMessage(
|
||||
{
|
||||
from: state.servers[server].nick,
|
||||
content
|
||||
},
|
||||
to,
|
||||
state
|
||||
),
|
||||
message: {
|
||||
from: state.servers[server].nick,
|
||||
content
|
||||
},
|
||||
wrapWidth,
|
||||
charWidth,
|
||||
windowWidth,
|
||||
socket: {
|
||||
type: 'message',
|
||||
data: { content, to, server }
|
||||
@ -308,13 +528,19 @@ export function sendMessage(content, to, server) {
|
||||
export function addMessage(message, server, to) {
|
||||
const tab = getMessageTab(server, to);
|
||||
|
||||
return (dispatch, getState) =>
|
||||
return (dispatch, getState) => {
|
||||
const { wrapWidth, charWidth, windowWidth } = getApp(getState());
|
||||
|
||||
dispatch({
|
||||
type: actions.ADD_MESSAGE,
|
||||
server,
|
||||
tab,
|
||||
message: initMessage(message, tab, getState())
|
||||
message,
|
||||
wrapWidth,
|
||||
charWidth,
|
||||
windowWidth
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
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.forEach(message =>
|
||||
initMessage(message, message.tab || tab, state)
|
||||
);
|
||||
const { wrapWidth, charWidth, windowWidth } = getApp(state);
|
||||
|
||||
dispatch({
|
||||
type: actions.ADD_MESSAGES,
|
||||
server,
|
||||
tab,
|
||||
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) {
|
||||
return addMessages(
|
||||
channels.map(channel => ({
|
||||
|
@ -141,6 +141,13 @@ export function timestamp(date = new Date()) {
|
||||
const dateFmt = new Intl.DateTimeFormat(window.navigator.language);
|
||||
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 ctx = canvas.getContext('2d');
|
||||
|
||||
|
@ -2,20 +2,40 @@ const lineHeight = 24;
|
||||
const userListWidth = 200;
|
||||
const smallScreen = 600;
|
||||
|
||||
export function findBreakpoints(text) {
|
||||
const breakpoints = [];
|
||||
|
||||
function findBreakpointsString(text, breakpoints, index) {
|
||||
for (let i = 0; i < text.length; i++) {
|
||||
const char = text.charAt(i);
|
||||
|
||||
if (char === ' ') {
|
||||
breakpoints.push({ end: i, next: i + 1 });
|
||||
} else if (char === '-' && i !== text.length - 1) {
|
||||
breakpoints.push({ end: i + 1, next: i + 1 });
|
||||
breakpoints.push({ end: i + index, next: i + 1 + index });
|
||||
} else if (i !== text.length - 1 && (char === '-' || char === '?')) {
|
||||
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(
|
||||
|
@ -48,7 +48,10 @@ var rootCmd = &cobra.Command{
|
||||
|
||||
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) {
|
||||
@ -63,6 +66,14 @@ var rootCmd = &cobra.Command{
|
||||
}
|
||||
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()
|
||||
dispatch := server.New(cfg)
|
||||
|
||||
@ -76,14 +87,6 @@ var rootCmd = &cobra.Command{
|
||||
dispatch.Store = 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()
|
||||
},
|
||||
}
|
||||
@ -119,19 +122,16 @@ func init() {
|
||||
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) {
|
||||
config, err := assets.Asset("config.default.toml")
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
return
|
||||
return err
|
||||
}
|
||||
|
||||
log.Println("Writing default config to", configPath)
|
||||
|
||||
err = ioutil.WriteFile(configPath, config, 0600)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
}
|
||||
return ioutil.WriteFile(configPath, config, 0600)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
@ -5,6 +5,9 @@ import (
|
||||
)
|
||||
|
||||
func (c *Client) HasCapability(name string, values ...string) bool {
|
||||
c.lock.Lock()
|
||||
defer c.lock.Unlock()
|
||||
|
||||
if capValues, ok := c.enabledCapabilities[name]; ok {
|
||||
if len(values) == 0 || capValues == nil {
|
||||
return true
|
||||
|
@ -35,8 +35,10 @@ type Client struct {
|
||||
Messages chan *Message
|
||||
ConnectionChanged chan ConnectionState
|
||||
Features *Features
|
||||
nick string
|
||||
channels []string
|
||||
|
||||
state *state
|
||||
nick string
|
||||
channels []string
|
||||
|
||||
wantedCapabilities []string
|
||||
requestedCapabilities map[string][]string
|
||||
@ -80,18 +82,15 @@ func NewClient(config *Config) *Client {
|
||||
wantedCapabilities = append(wantedCapabilities, "sasl")
|
||||
}
|
||||
|
||||
return &Client{
|
||||
client := &Client{
|
||||
Config: config,
|
||||
nick: config.Nick,
|
||||
Features: NewFeatures(),
|
||||
Messages: make(chan *Message, 32),
|
||||
ConnectionChanged: make(chan ConnectionState, 4),
|
||||
Features: NewFeatures(),
|
||||
nick: config.Nick,
|
||||
wantedCapabilities: wantedCapabilities,
|
||||
requestedCapabilities: 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},
|
||||
recvBuf: make([]byte, 0, 4096),
|
||||
backoff: &backoff.Backoff{
|
||||
@ -99,7 +98,13 @@ func NewClient(config *Config) *Client {
|
||||
Max: 30 * time.Second,
|
||||
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 {
|
||||
@ -143,6 +148,18 @@ func (c *Client) Host() string {
|
||||
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) {
|
||||
c.Write("NICK " + nick)
|
||||
}
|
||||
|
@ -21,7 +21,7 @@ func (c *Client) Connect() {
|
||||
}
|
||||
|
||||
func (c *Client) Reconnect() {
|
||||
close(c.reconnect)
|
||||
c.tryConnect()
|
||||
}
|
||||
|
||||
func (c *Client) Write(data string) {
|
||||
@ -63,6 +63,7 @@ func (c *Client) run() {
|
||||
|
||||
c.sendRecv.Wait()
|
||||
c.reconnect = make(chan struct{})
|
||||
c.state.reset()
|
||||
|
||||
time.Sleep(c.backoff.Duration())
|
||||
c.tryConnect()
|
||||
@ -178,7 +179,7 @@ func (c *Client) recv() {
|
||||
|
||||
default:
|
||||
c.connChange(false, nil)
|
||||
c.Reconnect()
|
||||
close(c.reconnect)
|
||||
return
|
||||
}
|
||||
}
|
||||
@ -195,54 +196,7 @@ func (c *Client) recv() {
|
||||
return
|
||||
}
|
||||
|
||||
switch msg.Command {
|
||||
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.handleMessage(msg)
|
||||
|
||||
c.Messages <- msg
|
||||
}
|
||||
|
@ -147,7 +147,7 @@ func TestRecv(t *testing.T) {
|
||||
func TestRecvTriggersReconnect(t *testing.T) {
|
||||
c := NewClient(&Config{})
|
||||
c.conn = &mockConn{}
|
||||
c.scan = bufio.NewScanner(bytes.NewBufferString("001 bob\r\n"))
|
||||
c.scan = bufio.NewScanner(bytes.NewBufferString(""))
|
||||
done := make(chan struct{})
|
||||
ok := false
|
||||
go func() {
|
||||
|
158
pkg/irc/internal.go
Normal file
158
pkg/irc/internal.go
Normal 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
37
pkg/irc/internal_test.go
Normal 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)
|
||||
}
|
@ -11,6 +11,8 @@ type Message struct {
|
||||
Host string
|
||||
Command string
|
||||
Params []string
|
||||
|
||||
meta interface{}
|
||||
}
|
||||
|
||||
func (m *Message) LastParam() string {
|
||||
|
33
pkg/irc/meta.go
Normal file
33
pkg/irc/meta.go
Normal 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
236
pkg/irc/state.go
Normal 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
89
pkg/irc/state_test.go
Normal 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())
|
||||
}
|
@ -63,18 +63,6 @@ func (d *Dispatch) newUser(w http.ResponseWriter, r *http.Request) (*State, erro
|
||||
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)
|
||||
|
||||
session, err := session.New(user.ID)
|
||||
|
@ -87,7 +87,9 @@ func (d *Dispatch) getIndexData(r *http.Request, state *State) *indexData {
|
||||
return nil
|
||||
}
|
||||
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
|
||||
|
||||
@ -106,9 +108,8 @@ func (d *Dispatch) getIndexData(r *http.Request, state *State) *indexData {
|
||||
}
|
||||
|
||||
func (d *indexData) addUsersAndMessages(server, name string, state *State) {
|
||||
if isChannel(name) {
|
||||
users := channelStore.GetUsers(server, name)
|
||||
if len(users) > 0 {
|
||||
if i, ok := state.getIRC(server); ok && isChannel(name) {
|
||||
if users := i.ChannelUsers(name); len(users) > 0 {
|
||||
d.Users = &Userlist{
|
||||
Server: server,
|
||||
Channel: name,
|
||||
|
@ -27,7 +27,6 @@ type ircHandler struct {
|
||||
state *State
|
||||
|
||||
whois WhoisReply
|
||||
userBuffers map[string][]string
|
||||
motdBuffer MOTD
|
||||
listBuffer storage.ChannelListIndex
|
||||
dccProgress chan irc.DownloadProgress
|
||||
@ -39,7 +38,6 @@ func newIRCHandler(client *irc.Client, state *State) *ircHandler {
|
||||
i := &ircHandler{
|
||||
client: client,
|
||||
state: state,
|
||||
userBuffers: make(map[string][]string),
|
||||
dccProgress: make(chan irc.DownloadProgress, 4),
|
||||
}
|
||||
i.initHandlers()
|
||||
@ -117,41 +115,47 @@ func (i *ircHandler) nick(msg *irc.Message) {
|
||||
New: msg.LastParam(),
|
||||
})
|
||||
|
||||
channelStore.RenameUser(msg.Sender, msg.LastParam(), i.client.Host())
|
||||
|
||||
if i.client.Is(msg.LastParam()) {
|
||||
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) {
|
||||
host := i.client.Host()
|
||||
|
||||
i.state.sendJSON("join", Join{
|
||||
Server: i.client.Host(),
|
||||
Server: host,
|
||||
User: msg.Sender,
|
||||
Channels: msg.Params,
|
||||
})
|
||||
|
||||
channel := msg.Params[0]
|
||||
channelStore.AddUser(msg.Sender, i.client.Host(), channel)
|
||||
|
||||
if i.client.Is(msg.Sender) {
|
||||
// In case no topic is set and there's a cached one that needs to be cleared
|
||||
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{
|
||||
Server: i.client.Host(),
|
||||
Server: host,
|
||||
Name: channel,
|
||||
})
|
||||
}
|
||||
|
||||
go i.state.user.LogEvent(host, "join", []string{msg.Sender}, channel)
|
||||
}
|
||||
|
||||
func (i *ircHandler) part(msg *irc.Message) {
|
||||
host := i.client.Host()
|
||||
channel := msg.Params[0]
|
||||
part := Part{
|
||||
Server: i.client.Host(),
|
||||
Server: host,
|
||||
User: msg.Sender,
|
||||
Channel: msg.Params[0],
|
||||
Channel: channel,
|
||||
}
|
||||
|
||||
if len(msg.Params) == 2 {
|
||||
@ -160,24 +164,18 @@ func (i *ircHandler) part(msg *irc.Message) {
|
||||
|
||||
i.state.sendJSON("part", part)
|
||||
|
||||
channelStore.RemoveUser(msg.Sender, i.client.Host(), part.Channel)
|
||||
|
||||
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) {
|
||||
target := msg.Params[0]
|
||||
if len(msg.Params) > 2 && isChannel(target) {
|
||||
mode := parseMode(msg.Params[1])
|
||||
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)
|
||||
if mode := irc.GetMode(msg); mode != nil {
|
||||
i.state.sendJSON("mode", Mode{
|
||||
Mode: mode,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@ -215,8 +213,13 @@ func (i *ircHandler) message(msg *irc.Message) {
|
||||
}
|
||||
|
||||
if target != "*" && !msg.IsFromServer() {
|
||||
go i.state.user.LogMessage(message.ID,
|
||||
i.client.Host(), msg.Sender, target, msg.LastParam())
|
||||
go i.state.user.LogMessage(&storage.Message{
|
||||
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(),
|
||||
})
|
||||
|
||||
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) {
|
||||
@ -298,6 +303,8 @@ func (i *ircHandler) topic(msg *irc.Message) {
|
||||
if msg.Command == irc.TOPIC {
|
||||
channel = msg.Params[0]
|
||||
nick = msg.Sender
|
||||
|
||||
go i.state.user.LogEvent(i.client.Host(), "topic", []string{nick, msg.LastParam()}, channel)
|
||||
} else {
|
||||
channel = msg.Params[1]
|
||||
}
|
||||
@ -308,39 +315,21 @@ func (i *ircHandler) topic(msg *irc.Message) {
|
||||
Topic: msg.LastParam(),
|
||||
Nick: nick,
|
||||
})
|
||||
|
||||
channelStore.SetTopic(msg.LastParam(), i.client.Host(), channel)
|
||||
}
|
||||
|
||||
func (i *ircHandler) noTopic(msg *irc.Message) {
|
||||
channel := msg.Params[1]
|
||||
|
||||
i.state.sendJSON("topic", Topic{
|
||||
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) {
|
||||
channel := msg.Params[1]
|
||||
users := i.userBuffers[channel]
|
||||
|
||||
i.state.sendJSON("users", Userlist{
|
||||
Server: i.client.Host(),
|
||||
Channel: channel,
|
||||
Users: users,
|
||||
Channel: msg.Params[1],
|
||||
Users: irc.GetNamreplyUsers(msg),
|
||||
})
|
||||
|
||||
channelStore.SetUsers(users, i.client.Host(), channel)
|
||||
delete(i.userBuffers, channel)
|
||||
}
|
||||
|
||||
func (i *ircHandler) motdStart(msg *irc.Message) {
|
||||
@ -363,10 +352,10 @@ func (i *ircHandler) list(msg *irc.Message) {
|
||||
}
|
||||
|
||||
if i.listBuffer != nil {
|
||||
c, _ := strconv.Atoi(msg.Params[2])
|
||||
userCount, _ := strconv.Atoi(msg.Params[2])
|
||||
i.listBuffer.Add(&storage.ChannelListItem{
|
||||
Name: msg.Params[1],
|
||||
UserCount: c,
|
||||
UserCount: userCount,
|
||||
Topic: msg.LastParam(),
|
||||
})
|
||||
}
|
||||
@ -463,7 +452,6 @@ func (i *ircHandler) initHandlers() {
|
||||
irc.RPL_ENDOFWHOIS: i.whoisEnd,
|
||||
irc.RPL_NOTOPIC: i.noTopic,
|
||||
irc.RPL_TOPIC: i.topic,
|
||||
irc.RPL_NAMREPLY: i.names,
|
||||
irc.RPL_ENDOFNAMES: i.namesEnd,
|
||||
irc.RPL_MOTDSTART: i.motdStart,
|
||||
irc.RPL_MOTD: i.motd,
|
||||
@ -489,29 +477,14 @@ func (i *ircHandler) sendDCCInfo(message string, log bool, a ...interface{}) {
|
||||
|
||||
if log {
|
||||
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 {
|
||||
return strings.IndexAny(s, "&#+!") == 0
|
||||
}
|
||||
|
@ -6,12 +6,11 @@ import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/khlieng/dispatch/pkg/irc"
|
||||
"github.com/khlieng/dispatch/storage"
|
||||
"github.com/khlieng/dispatch/storage/bleve"
|
||||
"github.com/khlieng/dispatch/storage/boltdb"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
var user *storage.User
|
||||
@ -29,19 +28,17 @@ func TestMain(m *testing.M) {
|
||||
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)
|
||||
if err != nil {
|
||||
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()
|
||||
|
||||
@ -126,21 +123,6 @@ func TestHandleIRCPart(t *testing.T) {
|
||||
}, 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) {
|
||||
res := dispatchMessage(&irc.Message{
|
||||
Command: irc.PRIVMSG,
|
||||
@ -273,35 +255,6 @@ func TestHandleIRCNoTopic(t *testing.T) {
|
||||
}, 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) {
|
||||
c := irc.NewClient(&irc.Config{
|
||||
Nick: "nick",
|
||||
|
@ -86,11 +86,7 @@ type Part struct {
|
||||
}
|
||||
|
||||
type Mode struct {
|
||||
Server string
|
||||
Channel string
|
||||
User string
|
||||
Add string
|
||||
Remove string
|
||||
*irc.Mode
|
||||
}
|
||||
|
||||
type Quit struct {
|
||||
|
@ -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
|
||||
|
||||
import (
|
||||
json "encoding/json"
|
||||
irc "github.com/khlieng/dispatch/pkg/irc"
|
||||
storage "github.com/khlieng/dispatch/storage"
|
||||
easyjson "github.com/mailru/easyjson"
|
||||
jlexer "github.com/mailru/easyjson/jlexer"
|
||||
@ -1200,6 +1201,29 @@ func easyjson42239ddeDecodeGithubComKhliengDispatchStorage(in *jlexer.Lexer, out
|
||||
out.Content = string(in.String())
|
||||
case "time":
|
||||
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:
|
||||
in.SkipRecursive()
|
||||
}
|
||||
@ -1244,6 +1268,122 @@ func easyjson42239ddeEncodeGithubComKhliengDispatchStorage(out *jwriter.Writer,
|
||||
}
|
||||
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('}')
|
||||
}
|
||||
func easyjson42239ddeDecodeGithubComKhliengDispatchServer10(in *jlexer.Lexer, out *SearchRequest) {
|
||||
@ -1627,9 +1767,9 @@ func easyjson42239ddeDecodeGithubComKhliengDispatchServer14(in *jlexer.Lexer, ou
|
||||
out.Channels = (out.Channels)[:0]
|
||||
}
|
||||
for !in.IsDelim(']') {
|
||||
var v12 string
|
||||
v12 = string(in.String())
|
||||
out.Channels = append(out.Channels, v12)
|
||||
var v18 string
|
||||
v18 = string(in.String())
|
||||
out.Channels = append(out.Channels, v18)
|
||||
in.WantComma()
|
||||
}
|
||||
in.Delim(']')
|
||||
@ -1686,11 +1826,11 @@ func easyjson42239ddeEncodeGithubComKhliengDispatchServer14(out *jwriter.Writer,
|
||||
}
|
||||
{
|
||||
out.RawByte('[')
|
||||
for v13, v14 := range in.Channels {
|
||||
if v13 > 0 {
|
||||
for v19, v20 := range in.Channels {
|
||||
if v19 > 0 {
|
||||
out.RawByte(',')
|
||||
}
|
||||
out.String(string(v14))
|
||||
out.String(string(v20))
|
||||
}
|
||||
out.RawByte(']')
|
||||
}
|
||||
@ -1898,6 +2038,7 @@ func easyjson42239ddeDecodeGithubComKhliengDispatchServer17(in *jlexer.Lexer, ou
|
||||
in.Skip()
|
||||
return
|
||||
}
|
||||
out.Mode = new(irc.Mode)
|
||||
in.Delim('{')
|
||||
for !in.IsDelim('}') {
|
||||
key := in.UnsafeFieldName(false)
|
||||
@ -2043,9 +2184,9 @@ func easyjson42239ddeDecodeGithubComKhliengDispatchServer18(in *jlexer.Lexer, ou
|
||||
out.Messages = (out.Messages)[:0]
|
||||
}
|
||||
for !in.IsDelim(']') {
|
||||
var v15 storage.Message
|
||||
easyjson42239ddeDecodeGithubComKhliengDispatchStorage(in, &v15)
|
||||
out.Messages = append(out.Messages, v15)
|
||||
var v21 storage.Message
|
||||
easyjson42239ddeDecodeGithubComKhliengDispatchStorage(in, &v21)
|
||||
out.Messages = append(out.Messages, v21)
|
||||
in.WantComma()
|
||||
}
|
||||
in.Delim(']')
|
||||
@ -2094,11 +2235,11 @@ func easyjson42239ddeEncodeGithubComKhliengDispatchServer18(out *jwriter.Writer,
|
||||
}
|
||||
{
|
||||
out.RawByte('[')
|
||||
for v16, v17 := range in.Messages {
|
||||
if v16 > 0 {
|
||||
for v22, v23 := range in.Messages {
|
||||
if v22 > 0 {
|
||||
out.RawByte(',')
|
||||
}
|
||||
easyjson42239ddeEncodeGithubComKhliengDispatchStorage(out, v17)
|
||||
easyjson42239ddeEncodeGithubComKhliengDispatchStorage(out, v23)
|
||||
}
|
||||
out.RawByte(']')
|
||||
}
|
||||
@ -2315,9 +2456,9 @@ func easyjson42239ddeDecodeGithubComKhliengDispatchServer20(in *jlexer.Lexer, ou
|
||||
out.Content = (out.Content)[:0]
|
||||
}
|
||||
for !in.IsDelim(']') {
|
||||
var v18 string
|
||||
v18 = string(in.String())
|
||||
out.Content = append(out.Content, v18)
|
||||
var v24 string
|
||||
v24 = string(in.String())
|
||||
out.Content = append(out.Content, v24)
|
||||
in.WantComma()
|
||||
}
|
||||
in.Delim(']')
|
||||
@ -2362,11 +2503,11 @@ func easyjson42239ddeEncodeGithubComKhliengDispatchServer20(out *jwriter.Writer,
|
||||
}
|
||||
{
|
||||
out.RawByte('[')
|
||||
for v19, v20 := range in.Content {
|
||||
if v19 > 0 {
|
||||
for v25, v26 := range in.Content {
|
||||
if v25 > 0 {
|
||||
out.RawByte(',')
|
||||
}
|
||||
out.String(string(v20))
|
||||
out.String(string(v26))
|
||||
}
|
||||
out.RawByte(']')
|
||||
}
|
||||
@ -2527,9 +2668,9 @@ func easyjson42239ddeDecodeGithubComKhliengDispatchServer22(in *jlexer.Lexer, ou
|
||||
out.Channels = (out.Channels)[:0]
|
||||
}
|
||||
for !in.IsDelim(']') {
|
||||
var v21 string
|
||||
v21 = string(in.String())
|
||||
out.Channels = append(out.Channels, v21)
|
||||
var v27 string
|
||||
v27 = string(in.String())
|
||||
out.Channels = append(out.Channels, v27)
|
||||
in.WantComma()
|
||||
}
|
||||
in.Delim(']')
|
||||
@ -2574,11 +2715,11 @@ func easyjson42239ddeEncodeGithubComKhliengDispatchServer22(out *jwriter.Writer,
|
||||
}
|
||||
{
|
||||
out.RawByte('[')
|
||||
for v22, v23 := range in.Channels {
|
||||
if v22 > 0 {
|
||||
for v28, v29 := range in.Channels {
|
||||
if v28 > 0 {
|
||||
out.RawByte(',')
|
||||
}
|
||||
out.String(string(v23))
|
||||
out.String(string(v29))
|
||||
}
|
||||
out.RawByte(']')
|
||||
}
|
||||
@ -2916,15 +3057,15 @@ func easyjson42239ddeDecodeGithubComKhliengDispatchServer26(in *jlexer.Lexer, ou
|
||||
for !in.IsDelim('}') {
|
||||
key := string(in.String())
|
||||
in.WantColon()
|
||||
var v24 interface{}
|
||||
if m, ok := v24.(easyjson.Unmarshaler); ok {
|
||||
var v30 interface{}
|
||||
if m, ok := v30.(easyjson.Unmarshaler); ok {
|
||||
m.UnmarshalEasyJSON(in)
|
||||
} else if m, ok := v24.(json.Unmarshaler); ok {
|
||||
} else if m, ok := v30.(json.Unmarshaler); ok {
|
||||
_ = m.UnmarshalJSON(in.Raw())
|
||||
} else {
|
||||
v24 = in.Interface()
|
||||
v30 = in.Interface()
|
||||
}
|
||||
(out.Features)[key] = v24
|
||||
(out.Features)[key] = v30
|
||||
in.WantComma()
|
||||
}
|
||||
in.Delim('}')
|
||||
@ -2959,21 +3100,21 @@ func easyjson42239ddeEncodeGithubComKhliengDispatchServer26(out *jwriter.Writer,
|
||||
}
|
||||
{
|
||||
out.RawByte('{')
|
||||
v25First := true
|
||||
for v25Name, v25Value := range in.Features {
|
||||
if v25First {
|
||||
v25First = false
|
||||
v31First := true
|
||||
for v31Name, v31Value := range in.Features {
|
||||
if v31First {
|
||||
v31First = false
|
||||
} else {
|
||||
out.RawByte(',')
|
||||
}
|
||||
out.String(string(v25Name))
|
||||
out.String(string(v31Name))
|
||||
out.RawByte(':')
|
||||
if m, ok := v25Value.(easyjson.Marshaler); ok {
|
||||
if m, ok := v31Value.(easyjson.Marshaler); ok {
|
||||
m.MarshalEasyJSON(out)
|
||||
} else if m, ok := v25Value.(json.Marshaler); ok {
|
||||
} else if m, ok := v31Value.(json.Marshaler); ok {
|
||||
out.Raw(m.MarshalJSON())
|
||||
} else {
|
||||
out.Raw(json.Marshal(v25Value))
|
||||
out.Raw(json.Marshal(v31Value))
|
||||
}
|
||||
}
|
||||
out.RawByte('}')
|
||||
@ -3404,17 +3545,17 @@ func easyjson42239ddeDecodeGithubComKhliengDispatchServer31(in *jlexer.Lexer, ou
|
||||
out.Results = (out.Results)[:0]
|
||||
}
|
||||
for !in.IsDelim(']') {
|
||||
var v26 *storage.ChannelListItem
|
||||
var v32 *storage.ChannelListItem
|
||||
if in.IsNull() {
|
||||
in.Skip()
|
||||
v26 = nil
|
||||
v32 = nil
|
||||
} else {
|
||||
if v26 == nil {
|
||||
v26 = new(storage.ChannelListItem)
|
||||
if v32 == nil {
|
||||
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.Delim(']')
|
||||
@ -3445,14 +3586,14 @@ func easyjson42239ddeEncodeGithubComKhliengDispatchServer31(out *jwriter.Writer,
|
||||
out.RawString(prefix[1:])
|
||||
{
|
||||
out.RawByte('[')
|
||||
for v27, v28 := range in.Results {
|
||||
if v27 > 0 {
|
||||
for v33, v34 := range in.Results {
|
||||
if v33 > 0 {
|
||||
out.RawByte(',')
|
||||
}
|
||||
if v28 == nil {
|
||||
if v34 == nil {
|
||||
out.RawString("null")
|
||||
} else {
|
||||
easyjson42239ddeEncodeGithubComKhliengDispatchStorage1(out, *v28)
|
||||
easyjson42239ddeEncodeGithubComKhliengDispatchStorage2(out, *v34)
|
||||
}
|
||||
}
|
||||
out.RawByte(']')
|
||||
@ -3514,7 +3655,7 @@ func (v *ChannelSearchResult) UnmarshalJSON(data []byte) error {
|
||||
func (v *ChannelSearchResult) UnmarshalEasyJSON(l *jlexer.Lexer) {
|
||||
easyjson42239ddeDecodeGithubComKhliengDispatchServer31(l, v)
|
||||
}
|
||||
func easyjson42239ddeDecodeGithubComKhliengDispatchStorage1(in *jlexer.Lexer, out *storage.ChannelListItem) {
|
||||
func easyjson42239ddeDecodeGithubComKhliengDispatchStorage2(in *jlexer.Lexer, out *storage.ChannelListItem) {
|
||||
isTopLevel := in.IsStart()
|
||||
if in.IsNull() {
|
||||
if isTopLevel {
|
||||
@ -3549,7 +3690,7 @@ func easyjson42239ddeDecodeGithubComKhliengDispatchStorage1(in *jlexer.Lexer, ou
|
||||
in.Consumed()
|
||||
}
|
||||
}
|
||||
func easyjson42239ddeEncodeGithubComKhliengDispatchStorage1(out *jwriter.Writer, in storage.ChannelListItem) {
|
||||
func easyjson42239ddeEncodeGithubComKhliengDispatchStorage2(out *jwriter.Writer, in storage.ChannelListItem) {
|
||||
out.RawByte('{')
|
||||
first := true
|
||||
_ = first
|
||||
|
@ -16,16 +16,12 @@ import (
|
||||
"github.com/khlieng/dispatch/storage"
|
||||
)
|
||||
|
||||
var channelStore = storage.NewChannelStore()
|
||||
var channelIndexes = storage.NewChannelIndexManager()
|
||||
|
||||
type Dispatch struct {
|
||||
Store storage.Store
|
||||
SessionStore storage.SessionStore
|
||||
|
||||
GetMessageStore func(*storage.User) (storage.MessageStore, error)
|
||||
GetMessageSearchProvider func(*storage.User) (storage.MessageSearchProvider, error)
|
||||
|
||||
cfg *config.Config
|
||||
upgrader websocket.Upgrader
|
||||
states *stateStore
|
||||
@ -87,18 +83,6 @@ func (d *Dispatch) loadUsers() {
|
||||
}
|
||||
|
||||
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)
|
||||
d.states.set(state)
|
||||
go state.run()
|
||||
|
@ -7,7 +7,7 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/gorilla/websocket"
|
||||
"github.com/kjk/betterguid"
|
||||
"github.com/khlieng/dispatch/storage"
|
||||
)
|
||||
|
||||
type wsHandler struct {
|
||||
@ -87,11 +87,13 @@ func (h *wsHandler) init(r *http.Request) {
|
||||
continue
|
||||
}
|
||||
|
||||
h.state.sendJSON("users", Userlist{
|
||||
Server: channel.Server,
|
||||
Channel: channel.Name,
|
||||
Users: channelStore.GetUsers(channel.Server, channel.Name),
|
||||
})
|
||||
if i, ok := h.state.getIRC(channel.Server); ok {
|
||||
h.state.sendJSON("users", Userlist{
|
||||
Server: channel.Server,
|
||||
Channel: channel.Name,
|
||||
Users: i.ChannelUsers(channel.Name),
|
||||
})
|
||||
}
|
||||
|
||||
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 {
|
||||
i.Privmsg(data.To, data.Content)
|
||||
|
||||
go h.state.user.LogMessage(betterguid.New(),
|
||||
data.Server, i.GetNick(), data.To, data.Content)
|
||||
go h.state.user.LogMessage(&storage.Message{
|
||||
Server: data.Server,
|
||||
From: i.GetNick(),
|
||||
To: data.To,
|
||||
Content: data.Content,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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 {
|
||||
return s.db.Batch(func(tx *bolt.Tx) error {
|
||||
b, err := tx.Bucket(bucketMessages).CreateBucketIfNotExists([]byte(message.Server + ":" + message.To))
|
||||
if err != nil {
|
||||
return err
|
||||
return s.logMessage(tx, message)
|
||||
})
|
||||
}
|
||||
|
||||
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)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return b.Put([]byte(message.ID), data)
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
@ -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())
|
||||
}
|
@ -7,7 +7,12 @@ import (
|
||||
"github.com/khlieng/dispatch/pkg/session"
|
||||
)
|
||||
|
||||
var Path directory
|
||||
var (
|
||||
Path directory
|
||||
|
||||
GetMessageStore MessageStoreCreator
|
||||
GetMessageSearchProvider MessageSearchProviderCreator
|
||||
)
|
||||
|
||||
func Initialize(root, dataRoot, configRoot string) {
|
||||
if root != DefaultDirectory() {
|
||||
@ -52,13 +57,18 @@ type SessionStore interface {
|
||||
|
||||
type MessageStore interface {
|
||||
LogMessage(message *Message) error
|
||||
LogMessages(messages []*Message) error
|
||||
GetMessages(server, channel string, count int, fromID string) ([]Message, bool, error)
|
||||
GetMessagesByID(server, channel string, ids []string) ([]Message, error)
|
||||
Close()
|
||||
}
|
||||
|
||||
type MessageStoreCreator func(*User) (MessageStore, error)
|
||||
|
||||
type MessageSearchProvider interface {
|
||||
SearchMessages(server, channel, q string) ([]string, error)
|
||||
Index(id string, message *Message) error
|
||||
Close()
|
||||
}
|
||||
|
||||
type MessageSearchProviderCreator func(*User) (MessageSearchProvider, error)
|
||||
|
@ -25,7 +25,6 @@ struct Server {
|
||||
struct Channel {
|
||||
Server string
|
||||
Name string
|
||||
Topic string
|
||||
}
|
||||
|
||||
struct Message {
|
||||
@ -33,4 +32,11 @@ struct Message {
|
||||
From string
|
||||
Content string
|
||||
Time int64
|
||||
Events []Event
|
||||
}
|
||||
|
||||
struct Event {
|
||||
Type string
|
||||
Params []string
|
||||
Time int64
|
||||
}
|
||||
|
@ -791,21 +791,6 @@ func (d *Channel) Size() (s uint64) {
|
||||
}
|
||||
s += l
|
||||
}
|
||||
{
|
||||
l := uint64(len(d.Topic))
|
||||
|
||||
{
|
||||
|
||||
t := l
|
||||
for t >= 0x80 {
|
||||
t >>= 7
|
||||
s++
|
||||
}
|
||||
s++
|
||||
|
||||
}
|
||||
s += l
|
||||
}
|
||||
return
|
||||
}
|
||||
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)
|
||||
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
|
||||
}
|
||||
|
||||
@ -922,26 +888,6 @@ func (d *Channel) Unmarshal(buf []byte) (uint64, error) {
|
||||
d.Name = 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
|
||||
|
||||
}
|
||||
d.Topic = string(buf[i+0 : i+0+l])
|
||||
i += l
|
||||
}
|
||||
return i + 0, nil
|
||||
}
|
||||
|
||||
@ -992,6 +938,29 @@ func (d *Message) Size() (s uint64) {
|
||||
}
|
||||
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
|
||||
return
|
||||
}
|
||||
@ -1068,6 +1037,34 @@ func (d *Message) Marshal(buf []byte) ([]byte, error) {
|
||||
*(*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
|
||||
}
|
||||
|
||||
@ -1138,6 +1135,251 @@ func (d *Message) Unmarshal(buf []byte) (uint64, error) {
|
||||
|
||||
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
|
||||
}
|
||||
|
173
storage/user.go
173
storage/user.go
@ -5,6 +5,8 @@ import (
|
||||
"os"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/kjk/betterguid"
|
||||
)
|
||||
|
||||
type User struct {
|
||||
@ -15,6 +17,7 @@ type User struct {
|
||||
store Store
|
||||
messageLog MessageStore
|
||||
messageIndex MessageSearchProvider
|
||||
lastMessages map[string]map[string]*Message
|
||||
clientSettings *ClientSettings
|
||||
lastIP []byte
|
||||
certificate *tls.Certificate
|
||||
@ -25,6 +28,7 @@ func NewUser(store Store) (*User, error) {
|
||||
user := &User{
|
||||
store: store,
|
||||
clientSettings: DefaultClientSettings(),
|
||||
lastMessages: map[string]map[string]*Message{},
|
||||
}
|
||||
|
||||
err := store.SaveUser(user)
|
||||
@ -32,11 +36,19 @@ func NewUser(store Store) (*User, error) {
|
||||
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)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = os.Mkdir(Path.Downloads(user.Username), 0700)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@ -53,20 +65,35 @@ func LoadUsers(store Store) ([]*User, error) {
|
||||
|
||||
for _, user := range users {
|
||||
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()
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
func (u *User) SetMessageStore(store MessageStore) {
|
||||
u.messageLog = store
|
||||
}
|
||||
|
||||
func (u *User) SetMessageSearchProvider(search MessageSearchProvider) {
|
||||
u.messageIndex = search
|
||||
}
|
||||
|
||||
func (u *User) Remove() {
|
||||
u.store.DeleteUser(u)
|
||||
if u.messageLog != nil {
|
||||
@ -178,7 +205,6 @@ func (u *User) SetServerName(name, address string) error {
|
||||
return u.AddServer(server)
|
||||
}
|
||||
|
||||
// TODO: Remove topic from disk schema
|
||||
type Channel struct {
|
||||
Server string
|
||||
Name string
|
||||
@ -215,33 +241,128 @@ func (u *User) RemoveOpenDM(server, nick string) error {
|
||||
}
|
||||
|
||||
type Message struct {
|
||||
ID string `json:"-" bleve:"-"`
|
||||
Server string `json:"-" bleve:"server"`
|
||||
From string `bleve:"-"`
|
||||
To string `json:"-" bleve:"to"`
|
||||
Content string `bleve:"content"`
|
||||
Time int64 `bleve:"-"`
|
||||
ID string `json:"-" bleve:"-"`
|
||||
Server string `json:"-" bleve:"server"`
|
||||
From string `bleve:"-"`
|
||||
To string `json:"-" bleve:"to"`
|
||||
Content string `bleve:"content"`
|
||||
Time int64 `bleve:"-"`
|
||||
Events []Event `bleve:"-"`
|
||||
}
|
||||
|
||||
func (m Message) Type() string {
|
||||
return "message"
|
||||
}
|
||||
|
||||
func (u *User) LogMessage(id, server, from, to, content string) error {
|
||||
message := &Message{
|
||||
ID: id,
|
||||
Server: server,
|
||||
From: from,
|
||||
To: to,
|
||||
Content: content,
|
||||
Time: time.Now().Unix(),
|
||||
func (u *User) LogMessage(msg *Message) error {
|
||||
if msg.Time == 0 {
|
||||
msg.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 {
|
||||
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) {
|
||||
|
@ -24,6 +24,13 @@ func TestUser(t *testing.T) {
|
||||
db, err := boltdb.New(storage.Path.Database())
|
||||
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)
|
||||
assert.Nil(t, err)
|
||||
|
||||
@ -124,17 +131,18 @@ func TestMessages(t *testing.T) {
|
||||
db, err := boltdb.New(storage.Path.Database())
|
||||
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)
|
||||
assert.Nil(t, err)
|
||||
|
||||
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")
|
||||
assert.Nil(t, err)
|
||||
assert.False(t, hasMore)
|
||||
@ -153,7 +161,13 @@ func TestMessages(t *testing.T) {
|
||||
for i := 0; i < 5; i++ {
|
||||
id := betterguid.New()
|
||||
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)
|
||||
}
|
||||
|
||||
@ -196,5 +210,42 @@ func TestMessages(t *testing.T) {
|
||||
assert.Nil(t, err)
|
||||
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()
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user