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;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.message-events-more {
|
||||||
|
font-weight: 700;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
.message-input-wrap {
|
.message-input-wrap {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 0;
|
left: 0;
|
||||||
|
@ -2,24 +2,23 @@ import React, { memo } from 'react';
|
|||||||
import classnames from 'classnames';
|
import classnames from 'classnames';
|
||||||
import stringToRGB from 'utils/color';
|
import stringToRGB from 'utils/color';
|
||||||
|
|
||||||
const Message = ({ message, coloredNick, style, onNickClick }) => {
|
const Message = ({ message, coloredNick, onNickClick }) => {
|
||||||
const className = classnames('message', {
|
const className = classnames('message', {
|
||||||
[`message-${message.type}`]: message.type
|
[`message-${message.type}`]: message.type
|
||||||
});
|
});
|
||||||
|
|
||||||
if (message.type === 'date') {
|
if (message.type === 'date') {
|
||||||
return (
|
return (
|
||||||
<div className={className} style={style}>
|
<div className={className}>
|
||||||
{message.content}
|
{message.content}
|
||||||
<hr />
|
<hr />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
style = {
|
const style = {
|
||||||
...style,
|
paddingLeft: `${message.indent + 15}px`,
|
||||||
paddingLeft: `${window.messageIndent + 15}px`,
|
textIndent: `-${message.indent}px`
|
||||||
textIndent: `-${window.messageIndent}px`
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const senderStyle = {};
|
const senderStyle = {};
|
||||||
|
@ -247,12 +247,13 @@ export default class MessageBox extends PureComponent {
|
|||||||
const message = messages[index - 1];
|
const message = messages[index - 1];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<div style={style}>
|
||||||
<Message
|
<Message
|
||||||
message={message}
|
message={message}
|
||||||
coloredNick={coloredNicks}
|
coloredNick={coloredNicks}
|
||||||
style={style}
|
|
||||||
onNickClick={onNickClick}
|
onNickClick={onNickClick}
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1,21 +1,17 @@
|
|||||||
import { socketAction } from 'state/actions';
|
import { socketAction } from 'state/actions';
|
||||||
import { setConnected } from 'state/app';
|
import { setConnected } from 'state/app';
|
||||||
import {
|
import {
|
||||||
broadcast,
|
|
||||||
inform,
|
|
||||||
print,
|
print,
|
||||||
addMessage,
|
addMessage,
|
||||||
addMessages
|
addMessages,
|
||||||
|
addEvent,
|
||||||
|
broadcastEvent
|
||||||
} from 'state/messages';
|
} from 'state/messages';
|
||||||
import { openModal } from 'state/modals';
|
import { openModal } from 'state/modals';
|
||||||
import { reconnect } from 'state/servers';
|
import { reconnect } from 'state/servers';
|
||||||
import { select } from 'state/tab';
|
import { select } from 'state/tab';
|
||||||
import { find } from 'utils';
|
import { find } from 'utils';
|
||||||
|
|
||||||
function withReason(message, reason) {
|
|
||||||
return message + (reason ? ` (${reason})` : '');
|
|
||||||
}
|
|
||||||
|
|
||||||
function findChannels(state, server, user) {
|
function findChannels(state, server, user) {
|
||||||
const channels = [];
|
const channels = [];
|
||||||
|
|
||||||
@ -46,37 +42,34 @@ export default function handleSocket({
|
|||||||
},
|
},
|
||||||
|
|
||||||
join({ user, server, channels }) {
|
join({ user, server, channels }) {
|
||||||
dispatch(inform(`${user} joined the channel`, server, channels[0]));
|
dispatch(addEvent(server, channels[0], 'join', user));
|
||||||
},
|
},
|
||||||
|
|
||||||
part({ user, server, channel, reason }) {
|
part({ user, server, channel, reason }) {
|
||||||
dispatch(
|
dispatch(addEvent(server, channel, 'part', user, reason));
|
||||||
inform(withReason(`${user} left the channel`, reason), server, channel)
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
|
|
||||||
quit({ user, server, reason }) {
|
quit({ user, server, reason }) {
|
||||||
const channels = findChannels(getState(), server, user);
|
const channels = findChannels(getState(), server, user);
|
||||||
dispatch(broadcast(withReason(`${user} quit`, reason), server, channels));
|
dispatch(broadcastEvent(server, channels, 'quit', user, reason));
|
||||||
},
|
},
|
||||||
|
|
||||||
nick({ server, oldNick, newNick }) {
|
nick({ server, oldNick, newNick }) {
|
||||||
if (oldNick) {
|
if (oldNick) {
|
||||||
const channels = findChannels(getState(), server, oldNick);
|
const channels = findChannels(getState(), server, oldNick);
|
||||||
dispatch(
|
dispatch(broadcastEvent(server, channels, 'nick', oldNick, newNick));
|
||||||
broadcast(`${oldNick} changed nick to ${newNick}`, server, channels)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
topic({ server, channel, topic, nick }) {
|
topic({ server, channel, topic, nick }) {
|
||||||
if (nick) {
|
if (nick) {
|
||||||
if (topic) {
|
dispatch(addEvent(server, channel, 'topic', nick, topic));
|
||||||
|
/* if (topic) {
|
||||||
dispatch(inform(`${nick} changed the topic to:`, server, channel));
|
dispatch(inform(`${nick} changed the topic to:`, server, channel));
|
||||||
dispatch(print(topic, server, channel));
|
dispatch(print(topic, server, channel));
|
||||||
} else {
|
} else {
|
||||||
dispatch(inform(`${nick} cleared the topic`, server, channel));
|
dispatch(inform(`${nick} cleared the topic`, server, channel));
|
||||||
}
|
} */
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -10,7 +10,6 @@ const smallScreen = 600;
|
|||||||
|
|
||||||
export default function widthUpdates({ store }) {
|
export default function widthUpdates({ store }) {
|
||||||
when(store, getCharWidth, charWidth => {
|
when(store, getCharWidth, charWidth => {
|
||||||
window.messageIndent = 6 * charWidth;
|
|
||||||
const scrollBarWidth = measureScrollBarWidth();
|
const scrollBarWidth = measureScrollBarWidth();
|
||||||
let prevWrapWidth;
|
let prevWrapWidth;
|
||||||
|
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import reducer, { broadcast, getMessageTab } from '../messages';
|
import reducer, { broadcast, getMessageTab } from '../messages';
|
||||||
import * as actions from '../actions';
|
import * as actions from '../actions';
|
||||||
import appReducer from '../app';
|
import appReducer from '../app';
|
||||||
|
import { unix } from 'utils';
|
||||||
|
|
||||||
describe('message reducer', () => {
|
describe('message reducer', () => {
|
||||||
it('adds the message on ADD_MESSAGE', () => {
|
it('adds the message on ADD_MESSAGE', () => {
|
||||||
@ -98,7 +99,7 @@ describe('message reducer', () => {
|
|||||||
it('adds date markers when prepending messages', () => {
|
it('adds date markers when prepending messages', () => {
|
||||||
let state = {
|
let state = {
|
||||||
srv: {
|
srv: {
|
||||||
'#chan1': [{ id: 0, date: new Date(1999, 0, 1) }]
|
'#chan1': [{ id: 0, date: new Date(1990, 0, 3) }]
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -108,8 +109,8 @@ describe('message reducer', () => {
|
|||||||
tab: '#chan1',
|
tab: '#chan1',
|
||||||
prepend: true,
|
prepend: true,
|
||||||
messages: [
|
messages: [
|
||||||
{ id: 1, date: new Date(1990, 0, 2) },
|
{ id: 1, time: unix(new Date(1990, 0, 1)) },
|
||||||
{ id: 2, date: new Date(1990, 0, 3) }
|
{ id: 2, time: unix(new Date(1990, 0, 2)) }
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -150,7 +151,7 @@ describe('message reducer', () => {
|
|||||||
it('adds date markers when adding messages', () => {
|
it('adds date markers when adding messages', () => {
|
||||||
let state = {
|
let state = {
|
||||||
srv: {
|
srv: {
|
||||||
'#chan1': [{ id: 0, date: new Date(1999, 0, 1) }]
|
'#chan1': [{ id: 0, date: new Date(1990, 0, 1) }]
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -159,9 +160,9 @@ describe('message reducer', () => {
|
|||||||
server: 'srv',
|
server: 'srv',
|
||||||
tab: '#chan1',
|
tab: '#chan1',
|
||||||
messages: [
|
messages: [
|
||||||
{ id: 1, date: new Date(1990, 0, 2) },
|
{ id: 1, time: unix(new Date(1990, 0, 2)) },
|
||||||
{ id: 2, date: new Date(1990, 0, 3) },
|
{ id: 2, time: unix(new Date(1990, 0, 3)) },
|
||||||
{ id: 3, date: new Date(1990, 0, 3) }
|
{ id: 3, time: unix(new Date(1990, 0, 3)) }
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import React from 'react';
|
||||||
import { createSelector } from 'reselect';
|
import { createSelector } from 'reselect';
|
||||||
import has from 'lodash/has';
|
import has from 'lodash/has';
|
||||||
import {
|
import {
|
||||||
@ -6,8 +7,10 @@ import {
|
|||||||
linkify,
|
linkify,
|
||||||
timestamp,
|
timestamp,
|
||||||
isChannel,
|
isChannel,
|
||||||
formatDate
|
formatDate,
|
||||||
|
unix
|
||||||
} from 'utils';
|
} from 'utils';
|
||||||
|
import stringToRGB from 'utils/color';
|
||||||
import colorify from 'utils/colorify';
|
import colorify from 'utils/colorify';
|
||||||
import createReducer from 'utils/createReducer';
|
import createReducer from 'utils/createReducer';
|
||||||
import { getApp } from './app';
|
import { getApp } from './app';
|
||||||
@ -45,8 +48,214 @@ function init(state, server, tab) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const collapsedEvents = ['join', 'part', 'quit'];
|
||||||
|
|
||||||
|
function shouldCollapse(msg1, msg2) {
|
||||||
|
return (
|
||||||
|
msg1.events &&
|
||||||
|
msg2.events &&
|
||||||
|
collapsedEvents.indexOf(msg1.events[0].type) !== -1 &&
|
||||||
|
collapsedEvents.indexOf(msg2.events[0].type) !== -1
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const eventVerbs = {
|
||||||
|
join: 'joined the channel',
|
||||||
|
part: 'left the channel',
|
||||||
|
quit: 'quit'
|
||||||
|
};
|
||||||
|
|
||||||
|
function renderNick(nick, type = '') {
|
||||||
|
const style = {
|
||||||
|
color: stringToRGB(nick),
|
||||||
|
fontWeight: 400
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span className="message-sender" style={style} key={`${nick} ${type}`}>
|
||||||
|
{nick}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderMore(count, type) {
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
className="message-events-more"
|
||||||
|
key={`more ${type}`}
|
||||||
|
>{`${count} more`}</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderEvent(event, type, nicks) {
|
||||||
|
const ending = eventVerbs[type];
|
||||||
|
|
||||||
|
if (nicks.length === 1) {
|
||||||
|
event.push(renderNick(nicks[0], type));
|
||||||
|
event.push(` ${ending}`);
|
||||||
|
}
|
||||||
|
if (nicks.length === 2) {
|
||||||
|
event.push(renderNick(nicks[0], type));
|
||||||
|
event.push(' and ');
|
||||||
|
event.push(renderNick(nicks[1], type));
|
||||||
|
event.push(` ${ending}`);
|
||||||
|
}
|
||||||
|
if (nicks.length > 2) {
|
||||||
|
event.push(renderNick(nicks[0], type));
|
||||||
|
event.push(', ');
|
||||||
|
event.push(renderNick(nicks[1], type));
|
||||||
|
event.push(' and ');
|
||||||
|
event.push(renderMore(nicks.length - 2, type));
|
||||||
|
event.push(` ${ending}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderEvents(events) {
|
||||||
|
const first = events[0];
|
||||||
|
if (first.type === 'nick') {
|
||||||
|
const [oldNick, newNick] = first.params;
|
||||||
|
|
||||||
|
return [renderNick(oldNick), ' changed nick to ', renderNick(newNick)];
|
||||||
|
}
|
||||||
|
if (first.type === 'topic') {
|
||||||
|
const [nick, newTopic] = first.params;
|
||||||
|
const topic = colorify(linkify(newTopic));
|
||||||
|
|
||||||
|
if (!topic) {
|
||||||
|
return [renderNick(nick), ' cleared the topic'];
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = [renderNick(nick), ' changed the topic to: '];
|
||||||
|
|
||||||
|
if (Array.isArray(topic)) {
|
||||||
|
result.push(...topic);
|
||||||
|
} else {
|
||||||
|
result.push(topic);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
const byType = {};
|
||||||
|
for (let i = events.length - 1; i >= 0; i--) {
|
||||||
|
const event = events[i];
|
||||||
|
const [nick] = event.params;
|
||||||
|
|
||||||
|
if (!byType[event.type]) {
|
||||||
|
byType[event.type] = [nick];
|
||||||
|
} else if (byType[event.type].indexOf(nick) === -1) {
|
||||||
|
byType[event.type].push(nick);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = [];
|
||||||
|
|
||||||
|
if (byType.join) {
|
||||||
|
renderEvent(result, 'join', byType.join);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (byType.part) {
|
||||||
|
if (result.length > 1) {
|
||||||
|
result[result.length - 1] += ', ';
|
||||||
|
}
|
||||||
|
renderEvent(result, 'part', byType.part);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (byType.quit) {
|
||||||
|
if (result.length > 1) {
|
||||||
|
result[result.length - 1] += ', ';
|
||||||
|
}
|
||||||
|
renderEvent(result, 'quit', byType.quit);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
let nextID = 0;
|
let nextID = 0;
|
||||||
|
|
||||||
|
function initMessage(
|
||||||
|
state,
|
||||||
|
message,
|
||||||
|
server,
|
||||||
|
tab,
|
||||||
|
wrapWidth,
|
||||||
|
charWidth,
|
||||||
|
windowWidth,
|
||||||
|
prepend
|
||||||
|
) {
|
||||||
|
const messages = state[server][tab];
|
||||||
|
|
||||||
|
if (messages.length > 0 && !prepend) {
|
||||||
|
const lastMessage = messages[messages.length - 1];
|
||||||
|
if (shouldCollapse(lastMessage, message)) {
|
||||||
|
lastMessage.events.push(message.events[0]);
|
||||||
|
lastMessage.content = renderEvents(lastMessage.events);
|
||||||
|
|
||||||
|
[lastMessage.breakpoints, lastMessage.length] = findBreakpoints(
|
||||||
|
lastMessage.content
|
||||||
|
);
|
||||||
|
lastMessage.height = messageHeight(
|
||||||
|
lastMessage,
|
||||||
|
wrapWidth,
|
||||||
|
charWidth,
|
||||||
|
6 * charWidth,
|
||||||
|
windowWidth
|
||||||
|
);
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (message.time) {
|
||||||
|
message.date = new Date(message.time * 1000);
|
||||||
|
} else {
|
||||||
|
message.date = new Date();
|
||||||
|
}
|
||||||
|
|
||||||
|
message.time = timestamp(message.date);
|
||||||
|
|
||||||
|
if (!message.id) {
|
||||||
|
message.id = nextID;
|
||||||
|
nextID++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tab.charAt(0) === '#') {
|
||||||
|
message.channel = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (message.events) {
|
||||||
|
message.type = 'info';
|
||||||
|
message.content = renderEvents(message.events);
|
||||||
|
} else {
|
||||||
|
message.content = message.content || '';
|
||||||
|
// Collapse multiple adjacent spaces into a single one
|
||||||
|
message.content = message.content.replace(/\s\s+/g, ' ');
|
||||||
|
|
||||||
|
if (message.content.indexOf('\x01ACTION') === 0) {
|
||||||
|
const { from } = message;
|
||||||
|
message.from = null;
|
||||||
|
message.type = 'action';
|
||||||
|
message.content = from + message.content.slice(7, -1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!message.events) {
|
||||||
|
message.content = colorify(linkify(message.content));
|
||||||
|
}
|
||||||
|
|
||||||
|
[message.breakpoints, message.length] = findBreakpoints(message.content);
|
||||||
|
message.height = messageHeight(
|
||||||
|
message,
|
||||||
|
wrapWidth,
|
||||||
|
charWidth,
|
||||||
|
6 * charWidth,
|
||||||
|
windowWidth
|
||||||
|
);
|
||||||
|
message.indent = 6 * charWidth;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
function createDateMessage(date) {
|
function createDateMessage(date) {
|
||||||
const message = {
|
const message = {
|
||||||
id: nextID,
|
id: nextID,
|
||||||
@ -68,14 +277,34 @@ function isSameDay(d1, d2) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function reducerPrependMessages(messages, server, tab, state) {
|
function reducerPrependMessages(
|
||||||
|
state,
|
||||||
|
messages,
|
||||||
|
server,
|
||||||
|
tab,
|
||||||
|
wrapWidth,
|
||||||
|
charWidth,
|
||||||
|
windowWidth
|
||||||
|
) {
|
||||||
const msgs = [];
|
const msgs = [];
|
||||||
|
|
||||||
for (let i = 0; i < messages.length; i++) {
|
for (let i = 0; i < messages.length; i++) {
|
||||||
if (i > 0 && !isSameDay(messages[i - 1].date, messages[i].date)) {
|
const message = messages[i];
|
||||||
msgs.push(createDateMessage(messages[i].date));
|
initMessage(
|
||||||
|
state,
|
||||||
|
message,
|
||||||
|
server,
|
||||||
|
tab,
|
||||||
|
wrapWidth,
|
||||||
|
charWidth,
|
||||||
|
windowWidth,
|
||||||
|
true
|
||||||
|
);
|
||||||
|
|
||||||
|
if (i > 0 && !isSameDay(messages[i - 1].date, message.date)) {
|
||||||
|
msgs.push(createDateMessage(message.date));
|
||||||
}
|
}
|
||||||
msgs.push(messages[i]);
|
msgs.push(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
const m = state[server][tab];
|
const m = state[server][tab];
|
||||||
@ -110,15 +339,41 @@ function reducerAddMessage(message, server, tab, state) {
|
|||||||
export default createReducer(
|
export default createReducer(
|
||||||
{},
|
{},
|
||||||
{
|
{
|
||||||
[actions.ADD_MESSAGE](state, { server, tab, message }) {
|
[actions.ADD_MESSAGE](
|
||||||
|
state,
|
||||||
|
{ server, tab, message, wrapWidth, charWidth, windowWidth }
|
||||||
|
) {
|
||||||
init(state, server, tab);
|
init(state, server, tab);
|
||||||
|
|
||||||
|
const shouldAdd = initMessage(
|
||||||
|
state,
|
||||||
|
message,
|
||||||
|
server,
|
||||||
|
tab,
|
||||||
|
wrapWidth,
|
||||||
|
charWidth,
|
||||||
|
windowWidth
|
||||||
|
);
|
||||||
|
if (shouldAdd) {
|
||||||
reducerAddMessage(message, server, tab, state);
|
reducerAddMessage(message, server, tab, state);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
[actions.ADD_MESSAGES](state, { server, tab, messages, prepend }) {
|
[actions.ADD_MESSAGES](
|
||||||
|
state,
|
||||||
|
{ server, tab, messages, prepend, wrapWidth, charWidth, windowWidth }
|
||||||
|
) {
|
||||||
if (prepend) {
|
if (prepend) {
|
||||||
init(state, server, tab);
|
init(state, server, tab);
|
||||||
reducerPrependMessages(messages, server, tab, state);
|
reducerPrependMessages(
|
||||||
|
state,
|
||||||
|
messages,
|
||||||
|
server,
|
||||||
|
tab,
|
||||||
|
wrapWidth,
|
||||||
|
charWidth,
|
||||||
|
windowWidth
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
if (!messages[0].tab) {
|
if (!messages[0].tab) {
|
||||||
init(state, server, tab);
|
init(state, server, tab);
|
||||||
@ -128,7 +383,19 @@ export default createReducer(
|
|||||||
if (message.tab) {
|
if (message.tab) {
|
||||||
init(state, server, message.tab);
|
init(state, server, message.tab);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const shouldAdd = initMessage(
|
||||||
|
state,
|
||||||
|
message,
|
||||||
|
server,
|
||||||
|
message.tab || tab,
|
||||||
|
wrapWidth,
|
||||||
|
charWidth,
|
||||||
|
windowWidth
|
||||||
|
);
|
||||||
|
if (shouldAdd) {
|
||||||
reducerAddMessage(message, server, message.tab || tab, state);
|
reducerAddMessage(message, server, message.tab || tab, state);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -184,53 +451,6 @@ export default createReducer(
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
function initMessage(message, tab, state) {
|
|
||||||
if (message.time) {
|
|
||||||
message.date = new Date(message.time * 1000);
|
|
||||||
} else {
|
|
||||||
message.date = new Date();
|
|
||||||
}
|
|
||||||
|
|
||||||
message.time = timestamp(message.date);
|
|
||||||
|
|
||||||
if (!message.id) {
|
|
||||||
message.id = nextID;
|
|
||||||
nextID++;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (tab.charAt(0) === '#') {
|
|
||||||
message.channel = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
message.content = message.content || '';
|
|
||||||
|
|
||||||
// Collapse multiple adjacent spaces into a single one
|
|
||||||
message.content = message.content.replace(/\s\s+/g, ' ');
|
|
||||||
|
|
||||||
if (message.content.indexOf('\x01ACTION') === 0) {
|
|
||||||
const { from } = message;
|
|
||||||
message.from = null;
|
|
||||||
message.type = 'action';
|
|
||||||
message.content = from + message.content.slice(7, -1);
|
|
||||||
}
|
|
||||||
|
|
||||||
const { wrapWidth, charWidth, windowWidth } = getApp(state);
|
|
||||||
|
|
||||||
message.length = message.content.length;
|
|
||||||
message.breakpoints = findBreakpoints(message.content);
|
|
||||||
message.height = messageHeight(
|
|
||||||
message,
|
|
||||||
wrapWidth,
|
|
||||||
charWidth,
|
|
||||||
6 * charWidth,
|
|
||||||
windowWidth
|
|
||||||
);
|
|
||||||
|
|
||||||
message.content = colorify(linkify(message.content));
|
|
||||||
|
|
||||||
return message;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getMessageTab(server, to) {
|
export function getMessageTab(server, to) {
|
||||||
if (!to || to === '*' || (!isChannel(to) && to.indexOf('.') !== -1)) {
|
if (!to || to === '*' || (!isChannel(to) && to.indexOf('.') !== -1)) {
|
||||||
return server;
|
return server;
|
||||||
@ -284,19 +504,19 @@ export function updateMessageHeight(wrapWidth, charWidth, windowWidth) {
|
|||||||
export function sendMessage(content, to, server) {
|
export function sendMessage(content, to, server) {
|
||||||
return (dispatch, getState) => {
|
return (dispatch, getState) => {
|
||||||
const state = getState();
|
const state = getState();
|
||||||
|
const { wrapWidth, charWidth, windowWidth } = getApp(state);
|
||||||
|
|
||||||
dispatch({
|
dispatch({
|
||||||
type: actions.ADD_MESSAGE,
|
type: actions.ADD_MESSAGE,
|
||||||
server,
|
server,
|
||||||
tab: to,
|
tab: to,
|
||||||
message: initMessage(
|
message: {
|
||||||
{
|
|
||||||
from: state.servers[server].nick,
|
from: state.servers[server].nick,
|
||||||
content
|
content
|
||||||
},
|
},
|
||||||
to,
|
wrapWidth,
|
||||||
state
|
charWidth,
|
||||||
),
|
windowWidth,
|
||||||
socket: {
|
socket: {
|
||||||
type: 'message',
|
type: 'message',
|
||||||
data: { content, to, server }
|
data: { content, to, server }
|
||||||
@ -308,13 +528,19 @@ export function sendMessage(content, to, server) {
|
|||||||
export function addMessage(message, server, to) {
|
export function addMessage(message, server, to) {
|
||||||
const tab = getMessageTab(server, to);
|
const tab = getMessageTab(server, to);
|
||||||
|
|
||||||
return (dispatch, getState) =>
|
return (dispatch, getState) => {
|
||||||
|
const { wrapWidth, charWidth, windowWidth } = getApp(getState());
|
||||||
|
|
||||||
dispatch({
|
dispatch({
|
||||||
type: actions.ADD_MESSAGE,
|
type: actions.ADD_MESSAGE,
|
||||||
server,
|
server,
|
||||||
tab,
|
tab,
|
||||||
message: initMessage(message, tab, getState())
|
message,
|
||||||
|
wrapWidth,
|
||||||
|
charWidth,
|
||||||
|
windowWidth
|
||||||
});
|
});
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function addMessages(messages, server, to, prepend, next) {
|
export function addMessages(messages, server, to, prepend, next) {
|
||||||
@ -328,20 +554,57 @@ export function addMessages(messages, server, to, prepend, next) {
|
|||||||
messages[0].next = true;
|
messages[0].next = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
messages.forEach(message =>
|
const { wrapWidth, charWidth, windowWidth } = getApp(state);
|
||||||
initMessage(message, message.tab || tab, state)
|
|
||||||
);
|
|
||||||
|
|
||||||
dispatch({
|
dispatch({
|
||||||
type: actions.ADD_MESSAGES,
|
type: actions.ADD_MESSAGES,
|
||||||
server,
|
server,
|
||||||
tab,
|
tab,
|
||||||
messages,
|
messages,
|
||||||
prepend
|
prepend,
|
||||||
|
wrapWidth,
|
||||||
|
charWidth,
|
||||||
|
windowWidth
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function addEvent(server, tab, type, ...params) {
|
||||||
|
return addMessage(
|
||||||
|
{
|
||||||
|
type: 'info',
|
||||||
|
events: [
|
||||||
|
{
|
||||||
|
type,
|
||||||
|
params,
|
||||||
|
time: unix()
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
server,
|
||||||
|
tab
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function broadcastEvent(server, channels, type, ...params) {
|
||||||
|
const now = unix();
|
||||||
|
|
||||||
|
return addMessages(
|
||||||
|
channels.map(channel => ({
|
||||||
|
type: 'info',
|
||||||
|
tab: channel,
|
||||||
|
events: [
|
||||||
|
{
|
||||||
|
type,
|
||||||
|
params,
|
||||||
|
time: now
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})),
|
||||||
|
server
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export function broadcast(message, server, channels) {
|
export function broadcast(message, server, channels) {
|
||||||
return addMessages(
|
return addMessages(
|
||||||
channels.map(channel => ({
|
channels.map(channel => ({
|
||||||
|
@ -141,6 +141,13 @@ export function timestamp(date = new Date()) {
|
|||||||
const dateFmt = new Intl.DateTimeFormat(window.navigator.language);
|
const dateFmt = new Intl.DateTimeFormat(window.navigator.language);
|
||||||
export const formatDate = dateFmt.format;
|
export const formatDate = dateFmt.format;
|
||||||
|
|
||||||
|
export function unix(date) {
|
||||||
|
if (date) {
|
||||||
|
return Math.floor(date.getTime() / 1000);
|
||||||
|
}
|
||||||
|
return Math.floor(Date.now() / 1000);
|
||||||
|
}
|
||||||
|
|
||||||
const canvas = document.createElement('canvas');
|
const canvas = document.createElement('canvas');
|
||||||
const ctx = canvas.getContext('2d');
|
const ctx = canvas.getContext('2d');
|
||||||
|
|
||||||
|
@ -2,20 +2,40 @@ const lineHeight = 24;
|
|||||||
const userListWidth = 200;
|
const userListWidth = 200;
|
||||||
const smallScreen = 600;
|
const smallScreen = 600;
|
||||||
|
|
||||||
export function findBreakpoints(text) {
|
function findBreakpointsString(text, breakpoints, index) {
|
||||||
const breakpoints = [];
|
|
||||||
|
|
||||||
for (let i = 0; i < text.length; i++) {
|
for (let i = 0; i < text.length; i++) {
|
||||||
const char = text.charAt(i);
|
const char = text.charAt(i);
|
||||||
|
|
||||||
if (char === ' ') {
|
if (char === ' ') {
|
||||||
breakpoints.push({ end: i, next: i + 1 });
|
breakpoints.push({ end: i + index, next: i + 1 + index });
|
||||||
} else if (char === '-' && i !== text.length - 1) {
|
} else if (i !== text.length - 1 && (char === '-' || char === '?')) {
|
||||||
breakpoints.push({ end: i + 1, next: i + 1 });
|
breakpoints.push({ end: i + 1 + index, next: i + 1 + index });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return breakpoints;
|
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, length];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function messageHeight(
|
export function messageHeight(
|
||||||
|
@ -48,7 +48,10 @@ var rootCmd = &cobra.Command{
|
|||||||
|
|
||||||
storage.Initialize(viper.GetString("dir"), viper.GetString("data"), viper.GetString("conf"))
|
storage.Initialize(viper.GetString("dir"), viper.GetString("data"), viper.GetString("conf"))
|
||||||
|
|
||||||
initConfig(storage.Path.Config(), viper.GetBool("reset-config"))
|
err := initConfig(storage.Path.Config(), viper.GetBool("reset-config"))
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
@ -63,6 +66,14 @@ var rootCmd = &cobra.Command{
|
|||||||
}
|
}
|
||||||
defer db.Close()
|
defer db.Close()
|
||||||
|
|
||||||
|
storage.GetMessageStore = func(user *storage.User) (storage.MessageStore, error) {
|
||||||
|
return boltdb.New(storage.Path.Log(user.Username))
|
||||||
|
}
|
||||||
|
|
||||||
|
storage.GetMessageSearchProvider = func(user *storage.User) (storage.MessageSearchProvider, error) {
|
||||||
|
return bleve.New(storage.Path.Index(user.Username))
|
||||||
|
}
|
||||||
|
|
||||||
cfg, cfgUpdated := config.LoadConfig()
|
cfg, cfgUpdated := config.LoadConfig()
|
||||||
dispatch := server.New(cfg)
|
dispatch := server.New(cfg)
|
||||||
|
|
||||||
@ -76,14 +87,6 @@ var rootCmd = &cobra.Command{
|
|||||||
dispatch.Store = db
|
dispatch.Store = db
|
||||||
dispatch.SessionStore = db
|
dispatch.SessionStore = db
|
||||||
|
|
||||||
dispatch.GetMessageStore = func(user *storage.User) (storage.MessageStore, error) {
|
|
||||||
return boltdb.New(storage.Path.Log(user.Username))
|
|
||||||
}
|
|
||||||
|
|
||||||
dispatch.GetMessageSearchProvider = func(user *storage.User) (storage.MessageSearchProvider, error) {
|
|
||||||
return bleve.New(storage.Path.Index(user.Username))
|
|
||||||
}
|
|
||||||
|
|
||||||
dispatch.Run()
|
dispatch.Run()
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@ -119,19 +122,16 @@ func init() {
|
|||||||
viper.SetDefault("dcc.autoget.delete", true)
|
viper.SetDefault("dcc.autoget.delete", true)
|
||||||
}
|
}
|
||||||
|
|
||||||
func initConfig(configPath string, overwrite bool) {
|
func initConfig(configPath string, overwrite bool) error {
|
||||||
if _, err := os.Stat(configPath); overwrite || os.IsNotExist(err) {
|
if _, err := os.Stat(configPath); overwrite || os.IsNotExist(err) {
|
||||||
config, err := assets.Asset("config.default.toml")
|
config, err := assets.Asset("config.default.toml")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Println(err)
|
return err
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Println("Writing default config to", configPath)
|
log.Println("Writing default config to", configPath)
|
||||||
|
|
||||||
err = ioutil.WriteFile(configPath, config, 0600)
|
return ioutil.WriteFile(configPath, config, 0600)
|
||||||
if err != nil {
|
|
||||||
log.Println(err)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -5,6 +5,9 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func (c *Client) HasCapability(name string, values ...string) bool {
|
func (c *Client) HasCapability(name string, values ...string) bool {
|
||||||
|
c.lock.Lock()
|
||||||
|
defer c.lock.Unlock()
|
||||||
|
|
||||||
if capValues, ok := c.enabledCapabilities[name]; ok {
|
if capValues, ok := c.enabledCapabilities[name]; ok {
|
||||||
if len(values) == 0 || capValues == nil {
|
if len(values) == 0 || capValues == nil {
|
||||||
return true
|
return true
|
||||||
|
@ -35,6 +35,8 @@ type Client struct {
|
|||||||
Messages chan *Message
|
Messages chan *Message
|
||||||
ConnectionChanged chan ConnectionState
|
ConnectionChanged chan ConnectionState
|
||||||
Features *Features
|
Features *Features
|
||||||
|
|
||||||
|
state *state
|
||||||
nick string
|
nick string
|
||||||
channels []string
|
channels []string
|
||||||
|
|
||||||
@ -80,18 +82,15 @@ func NewClient(config *Config) *Client {
|
|||||||
wantedCapabilities = append(wantedCapabilities, "sasl")
|
wantedCapabilities = append(wantedCapabilities, "sasl")
|
||||||
}
|
}
|
||||||
|
|
||||||
return &Client{
|
client := &Client{
|
||||||
Config: config,
|
Config: config,
|
||||||
nick: config.Nick,
|
|
||||||
Features: NewFeatures(),
|
|
||||||
Messages: make(chan *Message, 32),
|
Messages: make(chan *Message, 32),
|
||||||
|
ConnectionChanged: make(chan ConnectionState, 4),
|
||||||
|
Features: NewFeatures(),
|
||||||
|
nick: config.Nick,
|
||||||
wantedCapabilities: wantedCapabilities,
|
wantedCapabilities: wantedCapabilities,
|
||||||
requestedCapabilities: map[string][]string{},
|
requestedCapabilities: map[string][]string{},
|
||||||
enabledCapabilities: map[string][]string{},
|
enabledCapabilities: map[string][]string{},
|
||||||
ConnectionChanged: make(chan ConnectionState, 4),
|
|
||||||
out: make(chan string, 32),
|
|
||||||
quit: make(chan struct{}),
|
|
||||||
reconnect: make(chan struct{}),
|
|
||||||
dialer: &net.Dialer{Timeout: 10 * time.Second},
|
dialer: &net.Dialer{Timeout: 10 * time.Second},
|
||||||
recvBuf: make([]byte, 0, 4096),
|
recvBuf: make([]byte, 0, 4096),
|
||||||
backoff: &backoff.Backoff{
|
backoff: &backoff.Backoff{
|
||||||
@ -99,7 +98,13 @@ func NewClient(config *Config) *Client {
|
|||||||
Max: 30 * time.Second,
|
Max: 30 * time.Second,
|
||||||
Jitter: true,
|
Jitter: true,
|
||||||
},
|
},
|
||||||
|
out: make(chan string, 32),
|
||||||
|
quit: make(chan struct{}),
|
||||||
|
reconnect: make(chan struct{}),
|
||||||
}
|
}
|
||||||
|
client.state = newState(client)
|
||||||
|
|
||||||
|
return client
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) GetNick() string {
|
func (c *Client) GetNick() string {
|
||||||
@ -143,6 +148,18 @@ func (c *Client) Host() string {
|
|||||||
return c.Config.Host
|
return c.Config.Host
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *Client) MOTD() []string {
|
||||||
|
return c.state.getMOTD()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) ChannelUsers(channel string) []string {
|
||||||
|
return c.state.getUsers(channel)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) ChannelTopic(channel string) string {
|
||||||
|
return c.state.getTopic(channel)
|
||||||
|
}
|
||||||
|
|
||||||
func (c *Client) Nick(nick string) {
|
func (c *Client) Nick(nick string) {
|
||||||
c.Write("NICK " + nick)
|
c.Write("NICK " + nick)
|
||||||
}
|
}
|
||||||
|
@ -21,7 +21,7 @@ func (c *Client) Connect() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) Reconnect() {
|
func (c *Client) Reconnect() {
|
||||||
close(c.reconnect)
|
c.tryConnect()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) Write(data string) {
|
func (c *Client) Write(data string) {
|
||||||
@ -63,6 +63,7 @@ func (c *Client) run() {
|
|||||||
|
|
||||||
c.sendRecv.Wait()
|
c.sendRecv.Wait()
|
||||||
c.reconnect = make(chan struct{})
|
c.reconnect = make(chan struct{})
|
||||||
|
c.state.reset()
|
||||||
|
|
||||||
time.Sleep(c.backoff.Duration())
|
time.Sleep(c.backoff.Duration())
|
||||||
c.tryConnect()
|
c.tryConnect()
|
||||||
@ -178,7 +179,7 @@ func (c *Client) recv() {
|
|||||||
|
|
||||||
default:
|
default:
|
||||||
c.connChange(false, nil)
|
c.connChange(false, nil)
|
||||||
c.Reconnect()
|
close(c.reconnect)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -195,54 +196,7 @@ func (c *Client) recv() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
switch msg.Command {
|
c.handleMessage(msg)
|
||||||
case PING:
|
|
||||||
go c.write("PONG :" + msg.LastParam())
|
|
||||||
|
|
||||||
case JOIN:
|
|
||||||
if c.Is(msg.Sender) {
|
|
||||||
c.addChannel(msg.Params[0])
|
|
||||||
}
|
|
||||||
|
|
||||||
case NICK:
|
|
||||||
if c.Is(msg.Sender) {
|
|
||||||
c.setNick(msg.LastParam())
|
|
||||||
}
|
|
||||||
|
|
||||||
case PRIVMSG:
|
|
||||||
if ctcp := msg.ToCTCP(); ctcp != nil {
|
|
||||||
c.handleCTCP(ctcp, msg)
|
|
||||||
}
|
|
||||||
|
|
||||||
case CAP:
|
|
||||||
c.handleCAP(msg)
|
|
||||||
|
|
||||||
case RPL_WELCOME:
|
|
||||||
c.setNick(msg.Params[0])
|
|
||||||
c.setRegistered(true)
|
|
||||||
c.flushChannels()
|
|
||||||
|
|
||||||
c.backoff.Reset()
|
|
||||||
c.sendRecv.Add(1)
|
|
||||||
go c.send()
|
|
||||||
|
|
||||||
case RPL_ISUPPORT:
|
|
||||||
c.Features.Parse(msg.Params)
|
|
||||||
|
|
||||||
case ERR_NICKNAMEINUSE, ERR_NICKCOLLISION, ERR_UNAVAILRESOURCE:
|
|
||||||
if c.Config.HandleNickInUse != nil {
|
|
||||||
go c.writeNick(c.Config.HandleNickInUse(msg.Params[1]))
|
|
||||||
}
|
|
||||||
|
|
||||||
case ERROR:
|
|
||||||
c.Messages <- msg
|
|
||||||
c.connChange(false, nil)
|
|
||||||
time.Sleep(5 * time.Second)
|
|
||||||
close(c.quit)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
c.handleSASL(msg)
|
|
||||||
|
|
||||||
c.Messages <- msg
|
c.Messages <- msg
|
||||||
}
|
}
|
||||||
|
@ -147,7 +147,7 @@ func TestRecv(t *testing.T) {
|
|||||||
func TestRecvTriggersReconnect(t *testing.T) {
|
func TestRecvTriggersReconnect(t *testing.T) {
|
||||||
c := NewClient(&Config{})
|
c := NewClient(&Config{})
|
||||||
c.conn = &mockConn{}
|
c.conn = &mockConn{}
|
||||||
c.scan = bufio.NewScanner(bytes.NewBufferString("001 bob\r\n"))
|
c.scan = bufio.NewScanner(bytes.NewBufferString(""))
|
||||||
done := make(chan struct{})
|
done := make(chan struct{})
|
||||||
ok := false
|
ok := false
|
||||||
go func() {
|
go func() {
|
||||||
|
158
pkg/irc/internal.go
Normal file
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
|
Host string
|
||||||
Command string
|
Command string
|
||||||
Params []string
|
Params []string
|
||||||
|
|
||||||
|
meta interface{}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Message) LastParam() string {
|
func (m *Message) LastParam() string {
|
||||||
|
33
pkg/irc/meta.go
Normal file
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
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
messageStore, err := d.GetMessageStore(user)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
user.SetMessageStore(messageStore)
|
|
||||||
|
|
||||||
search, err := d.GetMessageSearchProvider(user)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
user.SetMessageSearchProvider(search)
|
|
||||||
|
|
||||||
log.Println(r.RemoteAddr, "[Auth] New anonymous user | ID:", user.ID)
|
log.Println(r.RemoteAddr, "[Auth] New anonymous user | ID:", user.ID)
|
||||||
|
|
||||||
session, err := session.New(user.ID)
|
session, err := session.New(user.ID)
|
||||||
|
@ -87,7 +87,9 @@ func (d *Dispatch) getIndexData(r *http.Request, state *State) *indexData {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
for i, channel := range channels {
|
for i, channel := range channels {
|
||||||
channels[i].Topic = channelStore.GetTopic(channel.Server, channel.Name)
|
if client, ok := state.getIRC(channel.Server); ok {
|
||||||
|
channels[i].Topic = client.ChannelTopic(channel.Name)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
data.Channels = channels
|
data.Channels = channels
|
||||||
|
|
||||||
@ -106,9 +108,8 @@ func (d *Dispatch) getIndexData(r *http.Request, state *State) *indexData {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (d *indexData) addUsersAndMessages(server, name string, state *State) {
|
func (d *indexData) addUsersAndMessages(server, name string, state *State) {
|
||||||
if isChannel(name) {
|
if i, ok := state.getIRC(server); ok && isChannel(name) {
|
||||||
users := channelStore.GetUsers(server, name)
|
if users := i.ChannelUsers(name); len(users) > 0 {
|
||||||
if len(users) > 0 {
|
|
||||||
d.Users = &Userlist{
|
d.Users = &Userlist{
|
||||||
Server: server,
|
Server: server,
|
||||||
Channel: name,
|
Channel: name,
|
||||||
|
@ -27,7 +27,6 @@ type ircHandler struct {
|
|||||||
state *State
|
state *State
|
||||||
|
|
||||||
whois WhoisReply
|
whois WhoisReply
|
||||||
userBuffers map[string][]string
|
|
||||||
motdBuffer MOTD
|
motdBuffer MOTD
|
||||||
listBuffer storage.ChannelListIndex
|
listBuffer storage.ChannelListIndex
|
||||||
dccProgress chan irc.DownloadProgress
|
dccProgress chan irc.DownloadProgress
|
||||||
@ -39,7 +38,6 @@ func newIRCHandler(client *irc.Client, state *State) *ircHandler {
|
|||||||
i := &ircHandler{
|
i := &ircHandler{
|
||||||
client: client,
|
client: client,
|
||||||
state: state,
|
state: state,
|
||||||
userBuffers: make(map[string][]string),
|
|
||||||
dccProgress: make(chan irc.DownloadProgress, 4),
|
dccProgress: make(chan irc.DownloadProgress, 4),
|
||||||
}
|
}
|
||||||
i.initHandlers()
|
i.initHandlers()
|
||||||
@ -117,41 +115,47 @@ func (i *ircHandler) nick(msg *irc.Message) {
|
|||||||
New: msg.LastParam(),
|
New: msg.LastParam(),
|
||||||
})
|
})
|
||||||
|
|
||||||
channelStore.RenameUser(msg.Sender, msg.LastParam(), i.client.Host())
|
|
||||||
|
|
||||||
if i.client.Is(msg.LastParam()) {
|
if i.client.Is(msg.LastParam()) {
|
||||||
go i.state.user.SetNick(msg.LastParam(), i.client.Host())
|
go i.state.user.SetNick(msg.LastParam(), i.client.Host())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
channels := irc.GetNickChannels(msg)
|
||||||
|
go i.state.user.LogEvent(i.client.Host(), "nick", []string{msg.Sender, msg.LastParam()}, channels...)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (i *ircHandler) join(msg *irc.Message) {
|
func (i *ircHandler) join(msg *irc.Message) {
|
||||||
|
host := i.client.Host()
|
||||||
|
|
||||||
i.state.sendJSON("join", Join{
|
i.state.sendJSON("join", Join{
|
||||||
Server: i.client.Host(),
|
Server: host,
|
||||||
User: msg.Sender,
|
User: msg.Sender,
|
||||||
Channels: msg.Params,
|
Channels: msg.Params,
|
||||||
})
|
})
|
||||||
|
|
||||||
channel := msg.Params[0]
|
channel := msg.Params[0]
|
||||||
channelStore.AddUser(msg.Sender, i.client.Host(), channel)
|
|
||||||
|
|
||||||
if i.client.Is(msg.Sender) {
|
if i.client.Is(msg.Sender) {
|
||||||
// In case no topic is set and there's a cached one that needs to be cleared
|
// In case no topic is set and there's a cached one that needs to be cleared
|
||||||
i.client.Topic(channel)
|
i.client.Topic(channel)
|
||||||
|
|
||||||
i.state.sendLastMessages(i.client.Host(), channel, 50)
|
i.state.sendLastMessages(host, channel, 50)
|
||||||
|
|
||||||
go i.state.user.AddChannel(&storage.Channel{
|
go i.state.user.AddChannel(&storage.Channel{
|
||||||
Server: i.client.Host(),
|
Server: host,
|
||||||
Name: channel,
|
Name: channel,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
go i.state.user.LogEvent(host, "join", []string{msg.Sender}, channel)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (i *ircHandler) part(msg *irc.Message) {
|
func (i *ircHandler) part(msg *irc.Message) {
|
||||||
|
host := i.client.Host()
|
||||||
|
channel := msg.Params[0]
|
||||||
part := Part{
|
part := Part{
|
||||||
Server: i.client.Host(),
|
Server: host,
|
||||||
User: msg.Sender,
|
User: msg.Sender,
|
||||||
Channel: msg.Params[0],
|
Channel: channel,
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(msg.Params) == 2 {
|
if len(msg.Params) == 2 {
|
||||||
@ -160,24 +164,18 @@ func (i *ircHandler) part(msg *irc.Message) {
|
|||||||
|
|
||||||
i.state.sendJSON("part", part)
|
i.state.sendJSON("part", part)
|
||||||
|
|
||||||
channelStore.RemoveUser(msg.Sender, i.client.Host(), part.Channel)
|
|
||||||
|
|
||||||
if i.client.Is(msg.Sender) {
|
if i.client.Is(msg.Sender) {
|
||||||
go i.state.user.RemoveChannel(i.client.Host(), part.Channel)
|
go i.state.user.RemoveChannel(host, part.Channel)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
go i.state.user.LogEvent(host, "part", []string{msg.Sender}, channel)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (i *ircHandler) mode(msg *irc.Message) {
|
func (i *ircHandler) mode(msg *irc.Message) {
|
||||||
target := msg.Params[0]
|
if mode := irc.GetMode(msg); mode != nil {
|
||||||
if len(msg.Params) > 2 && isChannel(target) {
|
i.state.sendJSON("mode", Mode{
|
||||||
mode := parseMode(msg.Params[1])
|
Mode: mode,
|
||||||
mode.Server = i.client.Host()
|
})
|
||||||
mode.Channel = target
|
|
||||||
mode.User = msg.Params[2]
|
|
||||||
|
|
||||||
i.state.sendJSON("mode", mode)
|
|
||||||
|
|
||||||
channelStore.SetMode(i.client.Host(), target, msg.Params[2], mode.Add, mode.Remove)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -215,8 +213,13 @@ func (i *ircHandler) message(msg *irc.Message) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if target != "*" && !msg.IsFromServer() {
|
if target != "*" && !msg.IsFromServer() {
|
||||||
go i.state.user.LogMessage(message.ID,
|
go i.state.user.LogMessage(&storage.Message{
|
||||||
i.client.Host(), msg.Sender, target, msg.LastParam())
|
ID: message.ID,
|
||||||
|
Server: message.Server,
|
||||||
|
From: message.From,
|
||||||
|
To: target,
|
||||||
|
Content: message.Content,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -227,7 +230,9 @@ func (i *ircHandler) quit(msg *irc.Message) {
|
|||||||
Reason: msg.LastParam(),
|
Reason: msg.LastParam(),
|
||||||
})
|
})
|
||||||
|
|
||||||
channelStore.RemoveUserAll(msg.Sender, i.client.Host())
|
channels := irc.GetQuitChannels(msg)
|
||||||
|
|
||||||
|
go i.state.user.LogEvent(i.client.Host(), "quit", []string{msg.Sender, msg.LastParam()}, channels...)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (i *ircHandler) info(msg *irc.Message) {
|
func (i *ircHandler) info(msg *irc.Message) {
|
||||||
@ -298,6 +303,8 @@ func (i *ircHandler) topic(msg *irc.Message) {
|
|||||||
if msg.Command == irc.TOPIC {
|
if msg.Command == irc.TOPIC {
|
||||||
channel = msg.Params[0]
|
channel = msg.Params[0]
|
||||||
nick = msg.Sender
|
nick = msg.Sender
|
||||||
|
|
||||||
|
go i.state.user.LogEvent(i.client.Host(), "topic", []string{nick, msg.LastParam()}, channel)
|
||||||
} else {
|
} else {
|
||||||
channel = msg.Params[1]
|
channel = msg.Params[1]
|
||||||
}
|
}
|
||||||
@ -308,39 +315,21 @@ func (i *ircHandler) topic(msg *irc.Message) {
|
|||||||
Topic: msg.LastParam(),
|
Topic: msg.LastParam(),
|
||||||
Nick: nick,
|
Nick: nick,
|
||||||
})
|
})
|
||||||
|
|
||||||
channelStore.SetTopic(msg.LastParam(), i.client.Host(), channel)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (i *ircHandler) noTopic(msg *irc.Message) {
|
func (i *ircHandler) noTopic(msg *irc.Message) {
|
||||||
channel := msg.Params[1]
|
|
||||||
|
|
||||||
i.state.sendJSON("topic", Topic{
|
i.state.sendJSON("topic", Topic{
|
||||||
Server: i.client.Host(),
|
Server: i.client.Host(),
|
||||||
Channel: channel,
|
Channel: msg.Params[1],
|
||||||
})
|
})
|
||||||
|
|
||||||
channelStore.SetTopic("", i.client.Host(), channel)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (i *ircHandler) names(msg *irc.Message) {
|
|
||||||
users := strings.Split(strings.TrimSuffix(msg.LastParam(), " "), " ")
|
|
||||||
userBuffer := i.userBuffers[msg.Params[2]]
|
|
||||||
i.userBuffers[msg.Params[2]] = append(userBuffer, users...)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (i *ircHandler) namesEnd(msg *irc.Message) {
|
func (i *ircHandler) namesEnd(msg *irc.Message) {
|
||||||
channel := msg.Params[1]
|
|
||||||
users := i.userBuffers[channel]
|
|
||||||
|
|
||||||
i.state.sendJSON("users", Userlist{
|
i.state.sendJSON("users", Userlist{
|
||||||
Server: i.client.Host(),
|
Server: i.client.Host(),
|
||||||
Channel: channel,
|
Channel: msg.Params[1],
|
||||||
Users: users,
|
Users: irc.GetNamreplyUsers(msg),
|
||||||
})
|
})
|
||||||
|
|
||||||
channelStore.SetUsers(users, i.client.Host(), channel)
|
|
||||||
delete(i.userBuffers, channel)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (i *ircHandler) motdStart(msg *irc.Message) {
|
func (i *ircHandler) motdStart(msg *irc.Message) {
|
||||||
@ -363,10 +352,10 @@ func (i *ircHandler) list(msg *irc.Message) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if i.listBuffer != nil {
|
if i.listBuffer != nil {
|
||||||
c, _ := strconv.Atoi(msg.Params[2])
|
userCount, _ := strconv.Atoi(msg.Params[2])
|
||||||
i.listBuffer.Add(&storage.ChannelListItem{
|
i.listBuffer.Add(&storage.ChannelListItem{
|
||||||
Name: msg.Params[1],
|
Name: msg.Params[1],
|
||||||
UserCount: c,
|
UserCount: userCount,
|
||||||
Topic: msg.LastParam(),
|
Topic: msg.LastParam(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -463,7 +452,6 @@ func (i *ircHandler) initHandlers() {
|
|||||||
irc.RPL_ENDOFWHOIS: i.whoisEnd,
|
irc.RPL_ENDOFWHOIS: i.whoisEnd,
|
||||||
irc.RPL_NOTOPIC: i.noTopic,
|
irc.RPL_NOTOPIC: i.noTopic,
|
||||||
irc.RPL_TOPIC: i.topic,
|
irc.RPL_TOPIC: i.topic,
|
||||||
irc.RPL_NAMREPLY: i.names,
|
|
||||||
irc.RPL_ENDOFNAMES: i.namesEnd,
|
irc.RPL_ENDOFNAMES: i.namesEnd,
|
||||||
irc.RPL_MOTDSTART: i.motdStart,
|
irc.RPL_MOTDSTART: i.motdStart,
|
||||||
irc.RPL_MOTD: i.motd,
|
irc.RPL_MOTD: i.motd,
|
||||||
@ -489,29 +477,14 @@ func (i *ircHandler) sendDCCInfo(message string, log bool, a ...interface{}) {
|
|||||||
|
|
||||||
if log {
|
if log {
|
||||||
i.state.user.AddOpenDM(msg.Server, msg.From)
|
i.state.user.AddOpenDM(msg.Server, msg.From)
|
||||||
i.state.user.LogMessage(betterguid.New(), msg.Server, msg.From, msg.From, msg.Content)
|
i.state.user.LogMessage(&storage.Message{
|
||||||
|
Server: msg.Server,
|
||||||
|
From: msg.From,
|
||||||
|
Content: msg.Content,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseMode(mode string) *Mode {
|
|
||||||
m := Mode{}
|
|
||||||
add := false
|
|
||||||
|
|
||||||
for _, c := range mode {
|
|
||||||
if c == '+' {
|
|
||||||
add = true
|
|
||||||
} else if c == '-' {
|
|
||||||
add = false
|
|
||||||
} else if add {
|
|
||||||
m.Add += string(c)
|
|
||||||
} else {
|
|
||||||
m.Remove += string(c)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return &m
|
|
||||||
}
|
|
||||||
|
|
||||||
func isChannel(s string) bool {
|
func isChannel(s string) bool {
|
||||||
return strings.IndexAny(s, "&#+!") == 0
|
return strings.IndexAny(s, "&#+!") == 0
|
||||||
}
|
}
|
||||||
|
@ -6,12 +6,11 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
|
|
||||||
"github.com/khlieng/dispatch/pkg/irc"
|
"github.com/khlieng/dispatch/pkg/irc"
|
||||||
"github.com/khlieng/dispatch/storage"
|
"github.com/khlieng/dispatch/storage"
|
||||||
"github.com/khlieng/dispatch/storage/bleve"
|
"github.com/khlieng/dispatch/storage/bleve"
|
||||||
"github.com/khlieng/dispatch/storage/boltdb"
|
"github.com/khlieng/dispatch/storage/boltdb"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
var user *storage.User
|
var user *storage.User
|
||||||
@ -29,19 +28,17 @@ func TestMain(m *testing.M) {
|
|||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
storage.GetMessageStore = func(_ *storage.User) (storage.MessageStore, error) {
|
||||||
|
return db, nil
|
||||||
|
}
|
||||||
|
storage.GetMessageSearchProvider = func(user *storage.User) (storage.MessageSearchProvider, error) {
|
||||||
|
return bleve.New(storage.Path.Index(user.Username))
|
||||||
|
}
|
||||||
|
|
||||||
user, err = storage.NewUser(db)
|
user, err = storage.NewUser(db)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
user.SetMessageStore(db)
|
|
||||||
|
|
||||||
search, err := bleve.New(storage.Path.Index(user.Username))
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
user.SetMessageSearchProvider(search)
|
|
||||||
|
|
||||||
channelStore = storage.NewChannelStore()
|
|
||||||
|
|
||||||
code := m.Run()
|
code := m.Run()
|
||||||
|
|
||||||
@ -126,21 +123,6 @@ func TestHandleIRCPart(t *testing.T) {
|
|||||||
}, res)
|
}, res)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestHandleIRCMode(t *testing.T) {
|
|
||||||
res := dispatchMessage(&irc.Message{
|
|
||||||
Command: irc.MODE,
|
|
||||||
Params: []string{"#chan", "+o-v", "nick"},
|
|
||||||
})
|
|
||||||
|
|
||||||
checkResponse(t, "mode", &Mode{
|
|
||||||
Server: "host.com",
|
|
||||||
Channel: "#chan",
|
|
||||||
User: "nick",
|
|
||||||
Add: "o",
|
|
||||||
Remove: "v",
|
|
||||||
}, res)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestHandleIRCMessage(t *testing.T) {
|
func TestHandleIRCMessage(t *testing.T) {
|
||||||
res := dispatchMessage(&irc.Message{
|
res := dispatchMessage(&irc.Message{
|
||||||
Command: irc.PRIVMSG,
|
Command: irc.PRIVMSG,
|
||||||
@ -273,35 +255,6 @@ func TestHandleIRCNoTopic(t *testing.T) {
|
|||||||
}, res)
|
}, res)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestHandleIRCNames(t *testing.T) {
|
|
||||||
c := irc.NewClient(&irc.Config{
|
|
||||||
Nick: "nick",
|
|
||||||
Username: "user",
|
|
||||||
Host: "host.com",
|
|
||||||
})
|
|
||||||
s := NewState(nil, nil)
|
|
||||||
i := newIRCHandler(c, s)
|
|
||||||
|
|
||||||
i.dispatchMessage(&irc.Message{
|
|
||||||
Command: irc.RPL_NAMREPLY,
|
|
||||||
Params: []string{"", "", "#chan", "a b c"},
|
|
||||||
})
|
|
||||||
i.dispatchMessage(&irc.Message{
|
|
||||||
Command: irc.RPL_NAMREPLY,
|
|
||||||
Params: []string{"", "", "#chan", "d"},
|
|
||||||
})
|
|
||||||
i.dispatchMessage(&irc.Message{
|
|
||||||
Command: irc.RPL_ENDOFNAMES,
|
|
||||||
Params: []string{"", "#chan"},
|
|
||||||
})
|
|
||||||
|
|
||||||
checkResponse(t, "users", Userlist{
|
|
||||||
Server: "host.com",
|
|
||||||
Channel: "#chan",
|
|
||||||
Users: []string{"a", "b", "c", "d"},
|
|
||||||
}, <-s.broadcast)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestHandleIRCMotd(t *testing.T) {
|
func TestHandleIRCMotd(t *testing.T) {
|
||||||
c := irc.NewClient(&irc.Config{
|
c := irc.NewClient(&irc.Config{
|
||||||
Nick: "nick",
|
Nick: "nick",
|
||||||
|
@ -86,11 +86,7 @@ type Part struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Mode struct {
|
type Mode struct {
|
||||||
Server string
|
*irc.Mode
|
||||||
Channel string
|
|
||||||
User string
|
|
||||||
Add string
|
|
||||||
Remove string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type Quit struct {
|
type Quit struct {
|
||||||
|
@ -1,9 +1,10 @@
|
|||||||
//out.Data: false//v7: false//v24: false// Code generated by easyjson for marshaling/unmarshaling. DO NOT EDIT.
|
//out.Data: false//v7: false//v30: false// Code generated by easyjson for marshaling/unmarshaling. DO NOT EDIT.
|
||||||
|
|
||||||
package server
|
package server
|
||||||
|
|
||||||
import (
|
import (
|
||||||
json "encoding/json"
|
json "encoding/json"
|
||||||
|
irc "github.com/khlieng/dispatch/pkg/irc"
|
||||||
storage "github.com/khlieng/dispatch/storage"
|
storage "github.com/khlieng/dispatch/storage"
|
||||||
easyjson "github.com/mailru/easyjson"
|
easyjson "github.com/mailru/easyjson"
|
||||||
jlexer "github.com/mailru/easyjson/jlexer"
|
jlexer "github.com/mailru/easyjson/jlexer"
|
||||||
@ -1200,6 +1201,29 @@ func easyjson42239ddeDecodeGithubComKhliengDispatchStorage(in *jlexer.Lexer, out
|
|||||||
out.Content = string(in.String())
|
out.Content = string(in.String())
|
||||||
case "time":
|
case "time":
|
||||||
out.Time = int64(in.Int64())
|
out.Time = int64(in.Int64())
|
||||||
|
case "events":
|
||||||
|
if in.IsNull() {
|
||||||
|
in.Skip()
|
||||||
|
out.Events = nil
|
||||||
|
} else {
|
||||||
|
in.Delim('[')
|
||||||
|
if out.Events == nil {
|
||||||
|
if !in.IsDelim(']') {
|
||||||
|
out.Events = make([]storage.Event, 0, 1)
|
||||||
|
} else {
|
||||||
|
out.Events = []storage.Event{}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
out.Events = (out.Events)[:0]
|
||||||
|
}
|
||||||
|
for !in.IsDelim(']') {
|
||||||
|
var v12 storage.Event
|
||||||
|
easyjson42239ddeDecodeGithubComKhliengDispatchStorage1(in, &v12)
|
||||||
|
out.Events = append(out.Events, v12)
|
||||||
|
in.WantComma()
|
||||||
|
}
|
||||||
|
in.Delim(']')
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
in.SkipRecursive()
|
in.SkipRecursive()
|
||||||
}
|
}
|
||||||
@ -1244,6 +1268,122 @@ func easyjson42239ddeEncodeGithubComKhliengDispatchStorage(out *jwriter.Writer,
|
|||||||
}
|
}
|
||||||
out.Int64(int64(in.Time))
|
out.Int64(int64(in.Time))
|
||||||
}
|
}
|
||||||
|
if len(in.Events) != 0 {
|
||||||
|
const prefix string = ",\"events\":"
|
||||||
|
if first {
|
||||||
|
first = false
|
||||||
|
out.RawString(prefix[1:])
|
||||||
|
} else {
|
||||||
|
out.RawString(prefix)
|
||||||
|
}
|
||||||
|
{
|
||||||
|
out.RawByte('[')
|
||||||
|
for v13, v14 := range in.Events {
|
||||||
|
if v13 > 0 {
|
||||||
|
out.RawByte(',')
|
||||||
|
}
|
||||||
|
easyjson42239ddeEncodeGithubComKhliengDispatchStorage1(out, v14)
|
||||||
|
}
|
||||||
|
out.RawByte(']')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out.RawByte('}')
|
||||||
|
}
|
||||||
|
func easyjson42239ddeDecodeGithubComKhliengDispatchStorage1(in *jlexer.Lexer, out *storage.Event) {
|
||||||
|
isTopLevel := in.IsStart()
|
||||||
|
if in.IsNull() {
|
||||||
|
if isTopLevel {
|
||||||
|
in.Consumed()
|
||||||
|
}
|
||||||
|
in.Skip()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
in.Delim('{')
|
||||||
|
for !in.IsDelim('}') {
|
||||||
|
key := in.UnsafeFieldName(false)
|
||||||
|
in.WantColon()
|
||||||
|
if in.IsNull() {
|
||||||
|
in.Skip()
|
||||||
|
in.WantComma()
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
switch key {
|
||||||
|
case "type":
|
||||||
|
out.Type = string(in.String())
|
||||||
|
case "params":
|
||||||
|
if in.IsNull() {
|
||||||
|
in.Skip()
|
||||||
|
out.Params = nil
|
||||||
|
} else {
|
||||||
|
in.Delim('[')
|
||||||
|
if out.Params == nil {
|
||||||
|
if !in.IsDelim(']') {
|
||||||
|
out.Params = make([]string, 0, 4)
|
||||||
|
} else {
|
||||||
|
out.Params = []string{}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
out.Params = (out.Params)[:0]
|
||||||
|
}
|
||||||
|
for !in.IsDelim(']') {
|
||||||
|
var v15 string
|
||||||
|
v15 = string(in.String())
|
||||||
|
out.Params = append(out.Params, v15)
|
||||||
|
in.WantComma()
|
||||||
|
}
|
||||||
|
in.Delim(']')
|
||||||
|
}
|
||||||
|
case "time":
|
||||||
|
out.Time = int64(in.Int64())
|
||||||
|
default:
|
||||||
|
in.SkipRecursive()
|
||||||
|
}
|
||||||
|
in.WantComma()
|
||||||
|
}
|
||||||
|
in.Delim('}')
|
||||||
|
if isTopLevel {
|
||||||
|
in.Consumed()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
func easyjson42239ddeEncodeGithubComKhliengDispatchStorage1(out *jwriter.Writer, in storage.Event) {
|
||||||
|
out.RawByte('{')
|
||||||
|
first := true
|
||||||
|
_ = first
|
||||||
|
if in.Type != "" {
|
||||||
|
const prefix string = ",\"type\":"
|
||||||
|
first = false
|
||||||
|
out.RawString(prefix[1:])
|
||||||
|
out.String(string(in.Type))
|
||||||
|
}
|
||||||
|
if len(in.Params) != 0 {
|
||||||
|
const prefix string = ",\"params\":"
|
||||||
|
if first {
|
||||||
|
first = false
|
||||||
|
out.RawString(prefix[1:])
|
||||||
|
} else {
|
||||||
|
out.RawString(prefix)
|
||||||
|
}
|
||||||
|
{
|
||||||
|
out.RawByte('[')
|
||||||
|
for v16, v17 := range in.Params {
|
||||||
|
if v16 > 0 {
|
||||||
|
out.RawByte(',')
|
||||||
|
}
|
||||||
|
out.String(string(v17))
|
||||||
|
}
|
||||||
|
out.RawByte(']')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if in.Time != 0 {
|
||||||
|
const prefix string = ",\"time\":"
|
||||||
|
if first {
|
||||||
|
first = false
|
||||||
|
out.RawString(prefix[1:])
|
||||||
|
} else {
|
||||||
|
out.RawString(prefix)
|
||||||
|
}
|
||||||
|
out.Int64(int64(in.Time))
|
||||||
|
}
|
||||||
out.RawByte('}')
|
out.RawByte('}')
|
||||||
}
|
}
|
||||||
func easyjson42239ddeDecodeGithubComKhliengDispatchServer10(in *jlexer.Lexer, out *SearchRequest) {
|
func easyjson42239ddeDecodeGithubComKhliengDispatchServer10(in *jlexer.Lexer, out *SearchRequest) {
|
||||||
@ -1627,9 +1767,9 @@ func easyjson42239ddeDecodeGithubComKhliengDispatchServer14(in *jlexer.Lexer, ou
|
|||||||
out.Channels = (out.Channels)[:0]
|
out.Channels = (out.Channels)[:0]
|
||||||
}
|
}
|
||||||
for !in.IsDelim(']') {
|
for !in.IsDelim(']') {
|
||||||
var v12 string
|
var v18 string
|
||||||
v12 = string(in.String())
|
v18 = string(in.String())
|
||||||
out.Channels = append(out.Channels, v12)
|
out.Channels = append(out.Channels, v18)
|
||||||
in.WantComma()
|
in.WantComma()
|
||||||
}
|
}
|
||||||
in.Delim(']')
|
in.Delim(']')
|
||||||
@ -1686,11 +1826,11 @@ func easyjson42239ddeEncodeGithubComKhliengDispatchServer14(out *jwriter.Writer,
|
|||||||
}
|
}
|
||||||
{
|
{
|
||||||
out.RawByte('[')
|
out.RawByte('[')
|
||||||
for v13, v14 := range in.Channels {
|
for v19, v20 := range in.Channels {
|
||||||
if v13 > 0 {
|
if v19 > 0 {
|
||||||
out.RawByte(',')
|
out.RawByte(',')
|
||||||
}
|
}
|
||||||
out.String(string(v14))
|
out.String(string(v20))
|
||||||
}
|
}
|
||||||
out.RawByte(']')
|
out.RawByte(']')
|
||||||
}
|
}
|
||||||
@ -1898,6 +2038,7 @@ func easyjson42239ddeDecodeGithubComKhliengDispatchServer17(in *jlexer.Lexer, ou
|
|||||||
in.Skip()
|
in.Skip()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
out.Mode = new(irc.Mode)
|
||||||
in.Delim('{')
|
in.Delim('{')
|
||||||
for !in.IsDelim('}') {
|
for !in.IsDelim('}') {
|
||||||
key := in.UnsafeFieldName(false)
|
key := in.UnsafeFieldName(false)
|
||||||
@ -2043,9 +2184,9 @@ func easyjson42239ddeDecodeGithubComKhliengDispatchServer18(in *jlexer.Lexer, ou
|
|||||||
out.Messages = (out.Messages)[:0]
|
out.Messages = (out.Messages)[:0]
|
||||||
}
|
}
|
||||||
for !in.IsDelim(']') {
|
for !in.IsDelim(']') {
|
||||||
var v15 storage.Message
|
var v21 storage.Message
|
||||||
easyjson42239ddeDecodeGithubComKhliengDispatchStorage(in, &v15)
|
easyjson42239ddeDecodeGithubComKhliengDispatchStorage(in, &v21)
|
||||||
out.Messages = append(out.Messages, v15)
|
out.Messages = append(out.Messages, v21)
|
||||||
in.WantComma()
|
in.WantComma()
|
||||||
}
|
}
|
||||||
in.Delim(']')
|
in.Delim(']')
|
||||||
@ -2094,11 +2235,11 @@ func easyjson42239ddeEncodeGithubComKhliengDispatchServer18(out *jwriter.Writer,
|
|||||||
}
|
}
|
||||||
{
|
{
|
||||||
out.RawByte('[')
|
out.RawByte('[')
|
||||||
for v16, v17 := range in.Messages {
|
for v22, v23 := range in.Messages {
|
||||||
if v16 > 0 {
|
if v22 > 0 {
|
||||||
out.RawByte(',')
|
out.RawByte(',')
|
||||||
}
|
}
|
||||||
easyjson42239ddeEncodeGithubComKhliengDispatchStorage(out, v17)
|
easyjson42239ddeEncodeGithubComKhliengDispatchStorage(out, v23)
|
||||||
}
|
}
|
||||||
out.RawByte(']')
|
out.RawByte(']')
|
||||||
}
|
}
|
||||||
@ -2315,9 +2456,9 @@ func easyjson42239ddeDecodeGithubComKhliengDispatchServer20(in *jlexer.Lexer, ou
|
|||||||
out.Content = (out.Content)[:0]
|
out.Content = (out.Content)[:0]
|
||||||
}
|
}
|
||||||
for !in.IsDelim(']') {
|
for !in.IsDelim(']') {
|
||||||
var v18 string
|
var v24 string
|
||||||
v18 = string(in.String())
|
v24 = string(in.String())
|
||||||
out.Content = append(out.Content, v18)
|
out.Content = append(out.Content, v24)
|
||||||
in.WantComma()
|
in.WantComma()
|
||||||
}
|
}
|
||||||
in.Delim(']')
|
in.Delim(']')
|
||||||
@ -2362,11 +2503,11 @@ func easyjson42239ddeEncodeGithubComKhliengDispatchServer20(out *jwriter.Writer,
|
|||||||
}
|
}
|
||||||
{
|
{
|
||||||
out.RawByte('[')
|
out.RawByte('[')
|
||||||
for v19, v20 := range in.Content {
|
for v25, v26 := range in.Content {
|
||||||
if v19 > 0 {
|
if v25 > 0 {
|
||||||
out.RawByte(',')
|
out.RawByte(',')
|
||||||
}
|
}
|
||||||
out.String(string(v20))
|
out.String(string(v26))
|
||||||
}
|
}
|
||||||
out.RawByte(']')
|
out.RawByte(']')
|
||||||
}
|
}
|
||||||
@ -2527,9 +2668,9 @@ func easyjson42239ddeDecodeGithubComKhliengDispatchServer22(in *jlexer.Lexer, ou
|
|||||||
out.Channels = (out.Channels)[:0]
|
out.Channels = (out.Channels)[:0]
|
||||||
}
|
}
|
||||||
for !in.IsDelim(']') {
|
for !in.IsDelim(']') {
|
||||||
var v21 string
|
var v27 string
|
||||||
v21 = string(in.String())
|
v27 = string(in.String())
|
||||||
out.Channels = append(out.Channels, v21)
|
out.Channels = append(out.Channels, v27)
|
||||||
in.WantComma()
|
in.WantComma()
|
||||||
}
|
}
|
||||||
in.Delim(']')
|
in.Delim(']')
|
||||||
@ -2574,11 +2715,11 @@ func easyjson42239ddeEncodeGithubComKhliengDispatchServer22(out *jwriter.Writer,
|
|||||||
}
|
}
|
||||||
{
|
{
|
||||||
out.RawByte('[')
|
out.RawByte('[')
|
||||||
for v22, v23 := range in.Channels {
|
for v28, v29 := range in.Channels {
|
||||||
if v22 > 0 {
|
if v28 > 0 {
|
||||||
out.RawByte(',')
|
out.RawByte(',')
|
||||||
}
|
}
|
||||||
out.String(string(v23))
|
out.String(string(v29))
|
||||||
}
|
}
|
||||||
out.RawByte(']')
|
out.RawByte(']')
|
||||||
}
|
}
|
||||||
@ -2916,15 +3057,15 @@ func easyjson42239ddeDecodeGithubComKhliengDispatchServer26(in *jlexer.Lexer, ou
|
|||||||
for !in.IsDelim('}') {
|
for !in.IsDelim('}') {
|
||||||
key := string(in.String())
|
key := string(in.String())
|
||||||
in.WantColon()
|
in.WantColon()
|
||||||
var v24 interface{}
|
var v30 interface{}
|
||||||
if m, ok := v24.(easyjson.Unmarshaler); ok {
|
if m, ok := v30.(easyjson.Unmarshaler); ok {
|
||||||
m.UnmarshalEasyJSON(in)
|
m.UnmarshalEasyJSON(in)
|
||||||
} else if m, ok := v24.(json.Unmarshaler); ok {
|
} else if m, ok := v30.(json.Unmarshaler); ok {
|
||||||
_ = m.UnmarshalJSON(in.Raw())
|
_ = m.UnmarshalJSON(in.Raw())
|
||||||
} else {
|
} else {
|
||||||
v24 = in.Interface()
|
v30 = in.Interface()
|
||||||
}
|
}
|
||||||
(out.Features)[key] = v24
|
(out.Features)[key] = v30
|
||||||
in.WantComma()
|
in.WantComma()
|
||||||
}
|
}
|
||||||
in.Delim('}')
|
in.Delim('}')
|
||||||
@ -2959,21 +3100,21 @@ func easyjson42239ddeEncodeGithubComKhliengDispatchServer26(out *jwriter.Writer,
|
|||||||
}
|
}
|
||||||
{
|
{
|
||||||
out.RawByte('{')
|
out.RawByte('{')
|
||||||
v25First := true
|
v31First := true
|
||||||
for v25Name, v25Value := range in.Features {
|
for v31Name, v31Value := range in.Features {
|
||||||
if v25First {
|
if v31First {
|
||||||
v25First = false
|
v31First = false
|
||||||
} else {
|
} else {
|
||||||
out.RawByte(',')
|
out.RawByte(',')
|
||||||
}
|
}
|
||||||
out.String(string(v25Name))
|
out.String(string(v31Name))
|
||||||
out.RawByte(':')
|
out.RawByte(':')
|
||||||
if m, ok := v25Value.(easyjson.Marshaler); ok {
|
if m, ok := v31Value.(easyjson.Marshaler); ok {
|
||||||
m.MarshalEasyJSON(out)
|
m.MarshalEasyJSON(out)
|
||||||
} else if m, ok := v25Value.(json.Marshaler); ok {
|
} else if m, ok := v31Value.(json.Marshaler); ok {
|
||||||
out.Raw(m.MarshalJSON())
|
out.Raw(m.MarshalJSON())
|
||||||
} else {
|
} else {
|
||||||
out.Raw(json.Marshal(v25Value))
|
out.Raw(json.Marshal(v31Value))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
out.RawByte('}')
|
out.RawByte('}')
|
||||||
@ -3404,17 +3545,17 @@ func easyjson42239ddeDecodeGithubComKhliengDispatchServer31(in *jlexer.Lexer, ou
|
|||||||
out.Results = (out.Results)[:0]
|
out.Results = (out.Results)[:0]
|
||||||
}
|
}
|
||||||
for !in.IsDelim(']') {
|
for !in.IsDelim(']') {
|
||||||
var v26 *storage.ChannelListItem
|
var v32 *storage.ChannelListItem
|
||||||
if in.IsNull() {
|
if in.IsNull() {
|
||||||
in.Skip()
|
in.Skip()
|
||||||
v26 = nil
|
v32 = nil
|
||||||
} else {
|
} else {
|
||||||
if v26 == nil {
|
if v32 == nil {
|
||||||
v26 = new(storage.ChannelListItem)
|
v32 = new(storage.ChannelListItem)
|
||||||
}
|
}
|
||||||
easyjson42239ddeDecodeGithubComKhliengDispatchStorage1(in, v26)
|
easyjson42239ddeDecodeGithubComKhliengDispatchStorage2(in, v32)
|
||||||
}
|
}
|
||||||
out.Results = append(out.Results, v26)
|
out.Results = append(out.Results, v32)
|
||||||
in.WantComma()
|
in.WantComma()
|
||||||
}
|
}
|
||||||
in.Delim(']')
|
in.Delim(']')
|
||||||
@ -3445,14 +3586,14 @@ func easyjson42239ddeEncodeGithubComKhliengDispatchServer31(out *jwriter.Writer,
|
|||||||
out.RawString(prefix[1:])
|
out.RawString(prefix[1:])
|
||||||
{
|
{
|
||||||
out.RawByte('[')
|
out.RawByte('[')
|
||||||
for v27, v28 := range in.Results {
|
for v33, v34 := range in.Results {
|
||||||
if v27 > 0 {
|
if v33 > 0 {
|
||||||
out.RawByte(',')
|
out.RawByte(',')
|
||||||
}
|
}
|
||||||
if v28 == nil {
|
if v34 == nil {
|
||||||
out.RawString("null")
|
out.RawString("null")
|
||||||
} else {
|
} else {
|
||||||
easyjson42239ddeEncodeGithubComKhliengDispatchStorage1(out, *v28)
|
easyjson42239ddeEncodeGithubComKhliengDispatchStorage2(out, *v34)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
out.RawByte(']')
|
out.RawByte(']')
|
||||||
@ -3514,7 +3655,7 @@ func (v *ChannelSearchResult) UnmarshalJSON(data []byte) error {
|
|||||||
func (v *ChannelSearchResult) UnmarshalEasyJSON(l *jlexer.Lexer) {
|
func (v *ChannelSearchResult) UnmarshalEasyJSON(l *jlexer.Lexer) {
|
||||||
easyjson42239ddeDecodeGithubComKhliengDispatchServer31(l, v)
|
easyjson42239ddeDecodeGithubComKhliengDispatchServer31(l, v)
|
||||||
}
|
}
|
||||||
func easyjson42239ddeDecodeGithubComKhliengDispatchStorage1(in *jlexer.Lexer, out *storage.ChannelListItem) {
|
func easyjson42239ddeDecodeGithubComKhliengDispatchStorage2(in *jlexer.Lexer, out *storage.ChannelListItem) {
|
||||||
isTopLevel := in.IsStart()
|
isTopLevel := in.IsStart()
|
||||||
if in.IsNull() {
|
if in.IsNull() {
|
||||||
if isTopLevel {
|
if isTopLevel {
|
||||||
@ -3549,7 +3690,7 @@ func easyjson42239ddeDecodeGithubComKhliengDispatchStorage1(in *jlexer.Lexer, ou
|
|||||||
in.Consumed()
|
in.Consumed()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
func easyjson42239ddeEncodeGithubComKhliengDispatchStorage1(out *jwriter.Writer, in storage.ChannelListItem) {
|
func easyjson42239ddeEncodeGithubComKhliengDispatchStorage2(out *jwriter.Writer, in storage.ChannelListItem) {
|
||||||
out.RawByte('{')
|
out.RawByte('{')
|
||||||
first := true
|
first := true
|
||||||
_ = first
|
_ = first
|
||||||
|
@ -16,16 +16,12 @@ import (
|
|||||||
"github.com/khlieng/dispatch/storage"
|
"github.com/khlieng/dispatch/storage"
|
||||||
)
|
)
|
||||||
|
|
||||||
var channelStore = storage.NewChannelStore()
|
|
||||||
var channelIndexes = storage.NewChannelIndexManager()
|
var channelIndexes = storage.NewChannelIndexManager()
|
||||||
|
|
||||||
type Dispatch struct {
|
type Dispatch struct {
|
||||||
Store storage.Store
|
Store storage.Store
|
||||||
SessionStore storage.SessionStore
|
SessionStore storage.SessionStore
|
||||||
|
|
||||||
GetMessageStore func(*storage.User) (storage.MessageStore, error)
|
|
||||||
GetMessageSearchProvider func(*storage.User) (storage.MessageSearchProvider, error)
|
|
||||||
|
|
||||||
cfg *config.Config
|
cfg *config.Config
|
||||||
upgrader websocket.Upgrader
|
upgrader websocket.Upgrader
|
||||||
states *stateStore
|
states *stateStore
|
||||||
@ -87,18 +83,6 @@ func (d *Dispatch) loadUsers() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (d *Dispatch) loadUser(user *storage.User) {
|
func (d *Dispatch) loadUser(user *storage.User) {
|
||||||
messageStore, err := d.GetMessageStore(user)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
user.SetMessageStore(messageStore)
|
|
||||||
|
|
||||||
search, err := d.GetMessageSearchProvider(user)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
user.SetMessageSearchProvider(search)
|
|
||||||
|
|
||||||
state := NewState(user, d)
|
state := NewState(user, d)
|
||||||
d.states.set(state)
|
d.states.set(state)
|
||||||
go state.run()
|
go state.run()
|
||||||
|
@ -7,7 +7,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/gorilla/websocket"
|
"github.com/gorilla/websocket"
|
||||||
"github.com/kjk/betterguid"
|
"github.com/khlieng/dispatch/storage"
|
||||||
)
|
)
|
||||||
|
|
||||||
type wsHandler struct {
|
type wsHandler struct {
|
||||||
@ -87,11 +87,13 @@ func (h *wsHandler) init(r *http.Request) {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if i, ok := h.state.getIRC(channel.Server); ok {
|
||||||
h.state.sendJSON("users", Userlist{
|
h.state.sendJSON("users", Userlist{
|
||||||
Server: channel.Server,
|
Server: channel.Server,
|
||||||
Channel: channel.Name,
|
Channel: channel.Name,
|
||||||
Users: channelStore.GetUsers(channel.Server, channel.Name),
|
Users: i.ChannelUsers(channel.Name),
|
||||||
})
|
})
|
||||||
|
}
|
||||||
|
|
||||||
h.state.sendLastMessages(channel.Server, channel.Name, 50)
|
h.state.sendLastMessages(channel.Server, channel.Name, 50)
|
||||||
}
|
}
|
||||||
@ -177,8 +179,12 @@ func (h *wsHandler) message(b []byte) {
|
|||||||
if i, ok := h.state.getIRC(data.Server); ok {
|
if i, ok := h.state.getIRC(data.Server); ok {
|
||||||
i.Privmsg(data.To, data.Content)
|
i.Privmsg(data.To, data.Content)
|
||||||
|
|
||||||
go h.state.user.LogMessage(betterguid.New(),
|
go h.state.user.LogMessage(&storage.Message{
|
||||||
data.Server, i.GetNick(), data.To, data.Content)
|
Server: data.Server,
|
||||||
|
From: i.GetNick(),
|
||||||
|
To: data.To,
|
||||||
|
Content: data.Content,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -278,8 +278,7 @@ func (s *BoltStore) RemoveOpenDM(user *storage.User, server, nick string) error
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *BoltStore) LogMessage(message *storage.Message) error {
|
func (s *BoltStore) logMessage(tx *bolt.Tx, message *storage.Message) error {
|
||||||
return s.db.Batch(func(tx *bolt.Tx) error {
|
|
||||||
b, err := tx.Bucket(bucketMessages).CreateBucketIfNotExists([]byte(message.Server + ":" + message.To))
|
b, err := tx.Bucket(bucketMessages).CreateBucketIfNotExists([]byte(message.Server + ":" + message.To))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@ -291,6 +290,24 @@ func (s *BoltStore) LogMessage(message *storage.Message) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return b.Put([]byte(message.ID), data)
|
return b.Put([]byte(message.ID), data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *BoltStore) LogMessage(message *storage.Message) error {
|
||||||
|
return s.db.Batch(func(tx *bolt.Tx) error {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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"
|
"github.com/khlieng/dispatch/pkg/session"
|
||||||
)
|
)
|
||||||
|
|
||||||
var Path directory
|
var (
|
||||||
|
Path directory
|
||||||
|
|
||||||
|
GetMessageStore MessageStoreCreator
|
||||||
|
GetMessageSearchProvider MessageSearchProviderCreator
|
||||||
|
)
|
||||||
|
|
||||||
func Initialize(root, dataRoot, configRoot string) {
|
func Initialize(root, dataRoot, configRoot string) {
|
||||||
if root != DefaultDirectory() {
|
if root != DefaultDirectory() {
|
||||||
@ -52,13 +57,18 @@ type SessionStore interface {
|
|||||||
|
|
||||||
type MessageStore interface {
|
type MessageStore interface {
|
||||||
LogMessage(message *Message) error
|
LogMessage(message *Message) error
|
||||||
|
LogMessages(messages []*Message) error
|
||||||
GetMessages(server, channel string, count int, fromID string) ([]Message, bool, error)
|
GetMessages(server, channel string, count int, fromID string) ([]Message, bool, error)
|
||||||
GetMessagesByID(server, channel string, ids []string) ([]Message, error)
|
GetMessagesByID(server, channel string, ids []string) ([]Message, error)
|
||||||
Close()
|
Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type MessageStoreCreator func(*User) (MessageStore, error)
|
||||||
|
|
||||||
type MessageSearchProvider interface {
|
type MessageSearchProvider interface {
|
||||||
SearchMessages(server, channel, q string) ([]string, error)
|
SearchMessages(server, channel, q string) ([]string, error)
|
||||||
Index(id string, message *Message) error
|
Index(id string, message *Message) error
|
||||||
Close()
|
Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type MessageSearchProviderCreator func(*User) (MessageSearchProvider, error)
|
||||||
|
@ -25,7 +25,6 @@ struct Server {
|
|||||||
struct Channel {
|
struct Channel {
|
||||||
Server string
|
Server string
|
||||||
Name string
|
Name string
|
||||||
Topic string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
struct Message {
|
struct Message {
|
||||||
@ -33,4 +32,11 @@ struct Message {
|
|||||||
From string
|
From string
|
||||||
Content string
|
Content string
|
||||||
Time int64
|
Time int64
|
||||||
|
Events []Event
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Event {
|
||||||
|
Type string
|
||||||
|
Params []string
|
||||||
|
Time int64
|
||||||
}
|
}
|
||||||
|
@ -791,21 +791,6 @@ func (d *Channel) Size() (s uint64) {
|
|||||||
}
|
}
|
||||||
s += l
|
s += l
|
||||||
}
|
}
|
||||||
{
|
|
||||||
l := uint64(len(d.Topic))
|
|
||||||
|
|
||||||
{
|
|
||||||
|
|
||||||
t := l
|
|
||||||
for t >= 0x80 {
|
|
||||||
t >>= 7
|
|
||||||
s++
|
|
||||||
}
|
|
||||||
s++
|
|
||||||
|
|
||||||
}
|
|
||||||
s += l
|
|
||||||
}
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
func (d *Channel) Marshal(buf []byte) ([]byte, error) {
|
func (d *Channel) Marshal(buf []byte) ([]byte, error) {
|
||||||
@ -857,25 +842,6 @@ func (d *Channel) Marshal(buf []byte) ([]byte, error) {
|
|||||||
copy(buf[i+0:], d.Name)
|
copy(buf[i+0:], d.Name)
|
||||||
i += l
|
i += l
|
||||||
}
|
}
|
||||||
{
|
|
||||||
l := uint64(len(d.Topic))
|
|
||||||
|
|
||||||
{
|
|
||||||
|
|
||||||
t := uint64(l)
|
|
||||||
|
|
||||||
for t >= 0x80 {
|
|
||||||
buf[i+0] = byte(t) | 0x80
|
|
||||||
t >>= 7
|
|
||||||
i++
|
|
||||||
}
|
|
||||||
buf[i+0] = byte(t)
|
|
||||||
i++
|
|
||||||
|
|
||||||
}
|
|
||||||
copy(buf[i+0:], d.Topic)
|
|
||||||
i += l
|
|
||||||
}
|
|
||||||
return buf[:i+0], nil
|
return buf[:i+0], nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -922,26 +888,6 @@ func (d *Channel) Unmarshal(buf []byte) (uint64, error) {
|
|||||||
d.Name = string(buf[i+0 : i+0+l])
|
d.Name = string(buf[i+0 : i+0+l])
|
||||||
i += l
|
i += l
|
||||||
}
|
}
|
||||||
{
|
|
||||||
l := uint64(0)
|
|
||||||
|
|
||||||
{
|
|
||||||
|
|
||||||
bs := uint8(7)
|
|
||||||
t := uint64(buf[i+0] & 0x7F)
|
|
||||||
for buf[i+0]&0x80 == 0x80 {
|
|
||||||
i++
|
|
||||||
t |= uint64(buf[i+0]&0x7F) << bs
|
|
||||||
bs += 7
|
|
||||||
}
|
|
||||||
i++
|
|
||||||
|
|
||||||
l = t
|
|
||||||
|
|
||||||
}
|
|
||||||
d.Topic = string(buf[i+0 : i+0+l])
|
|
||||||
i += l
|
|
||||||
}
|
|
||||||
return i + 0, nil
|
return i + 0, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -992,6 +938,29 @@ func (d *Message) Size() (s uint64) {
|
|||||||
}
|
}
|
||||||
s += l
|
s += l
|
||||||
}
|
}
|
||||||
|
{
|
||||||
|
l := uint64(len(d.Events))
|
||||||
|
|
||||||
|
{
|
||||||
|
|
||||||
|
t := l
|
||||||
|
for t >= 0x80 {
|
||||||
|
t >>= 7
|
||||||
|
s++
|
||||||
|
}
|
||||||
|
s++
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
for k0 := range d.Events {
|
||||||
|
|
||||||
|
{
|
||||||
|
s += d.Events[k0].Size()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
s += 8
|
s += 8
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -1068,6 +1037,34 @@ func (d *Message) Marshal(buf []byte) ([]byte, error) {
|
|||||||
*(*int64)(unsafe.Pointer(&buf[i+0])) = d.Time
|
*(*int64)(unsafe.Pointer(&buf[i+0])) = d.Time
|
||||||
|
|
||||||
}
|
}
|
||||||
|
{
|
||||||
|
l := uint64(len(d.Events))
|
||||||
|
|
||||||
|
{
|
||||||
|
|
||||||
|
t := uint64(l)
|
||||||
|
|
||||||
|
for t >= 0x80 {
|
||||||
|
buf[i+8] = byte(t) | 0x80
|
||||||
|
t >>= 7
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
buf[i+8] = byte(t)
|
||||||
|
i++
|
||||||
|
|
||||||
|
}
|
||||||
|
for k0 := range d.Events {
|
||||||
|
|
||||||
|
{
|
||||||
|
nbuf, err := d.Events[k0].Marshal(buf[i+8:])
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
i += uint64(len(nbuf))
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
return buf[:i+8], nil
|
return buf[:i+8], nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1138,6 +1135,251 @@ func (d *Message) Unmarshal(buf []byte) (uint64, error) {
|
|||||||
|
|
||||||
d.Time = *(*int64)(unsafe.Pointer(&buf[i+0]))
|
d.Time = *(*int64)(unsafe.Pointer(&buf[i+0]))
|
||||||
|
|
||||||
|
}
|
||||||
|
{
|
||||||
|
l := uint64(0)
|
||||||
|
|
||||||
|
{
|
||||||
|
|
||||||
|
bs := uint8(7)
|
||||||
|
t := uint64(buf[i+8] & 0x7F)
|
||||||
|
for buf[i+8]&0x80 == 0x80 {
|
||||||
|
i++
|
||||||
|
t |= uint64(buf[i+8]&0x7F) << bs
|
||||||
|
bs += 7
|
||||||
|
}
|
||||||
|
i++
|
||||||
|
|
||||||
|
l = t
|
||||||
|
|
||||||
|
}
|
||||||
|
if uint64(cap(d.Events)) >= l {
|
||||||
|
d.Events = d.Events[:l]
|
||||||
|
} else {
|
||||||
|
d.Events = make([]Event, l)
|
||||||
|
}
|
||||||
|
for k0 := range d.Events {
|
||||||
|
|
||||||
|
{
|
||||||
|
ni, err := d.Events[k0].Unmarshal(buf[i+8:])
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
i += ni
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return i + 8, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Event) Size() (s uint64) {
|
||||||
|
|
||||||
|
{
|
||||||
|
l := uint64(len(d.Type))
|
||||||
|
|
||||||
|
{
|
||||||
|
|
||||||
|
t := l
|
||||||
|
for t >= 0x80 {
|
||||||
|
t >>= 7
|
||||||
|
s++
|
||||||
|
}
|
||||||
|
s++
|
||||||
|
|
||||||
|
}
|
||||||
|
s += l
|
||||||
|
}
|
||||||
|
{
|
||||||
|
l := uint64(len(d.Params))
|
||||||
|
|
||||||
|
{
|
||||||
|
|
||||||
|
t := l
|
||||||
|
for t >= 0x80 {
|
||||||
|
t >>= 7
|
||||||
|
s++
|
||||||
|
}
|
||||||
|
s++
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
for k0 := range d.Params {
|
||||||
|
|
||||||
|
{
|
||||||
|
l := uint64(len(d.Params[k0]))
|
||||||
|
|
||||||
|
{
|
||||||
|
|
||||||
|
t := l
|
||||||
|
for t >= 0x80 {
|
||||||
|
t >>= 7
|
||||||
|
s++
|
||||||
|
}
|
||||||
|
s++
|
||||||
|
|
||||||
|
}
|
||||||
|
s += l
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
s += 8
|
||||||
|
return
|
||||||
|
}
|
||||||
|
func (d *Event) Marshal(buf []byte) ([]byte, error) {
|
||||||
|
size := d.Size()
|
||||||
|
{
|
||||||
|
if uint64(cap(buf)) >= size {
|
||||||
|
buf = buf[:size]
|
||||||
|
} else {
|
||||||
|
buf = make([]byte, size)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
i := uint64(0)
|
||||||
|
|
||||||
|
{
|
||||||
|
l := uint64(len(d.Type))
|
||||||
|
|
||||||
|
{
|
||||||
|
|
||||||
|
t := uint64(l)
|
||||||
|
|
||||||
|
for t >= 0x80 {
|
||||||
|
buf[i+0] = byte(t) | 0x80
|
||||||
|
t >>= 7
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
buf[i+0] = byte(t)
|
||||||
|
i++
|
||||||
|
|
||||||
|
}
|
||||||
|
copy(buf[i+0:], d.Type)
|
||||||
|
i += l
|
||||||
|
}
|
||||||
|
{
|
||||||
|
l := uint64(len(d.Params))
|
||||||
|
|
||||||
|
{
|
||||||
|
|
||||||
|
t := uint64(l)
|
||||||
|
|
||||||
|
for t >= 0x80 {
|
||||||
|
buf[i+0] = byte(t) | 0x80
|
||||||
|
t >>= 7
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
buf[i+0] = byte(t)
|
||||||
|
i++
|
||||||
|
|
||||||
|
}
|
||||||
|
for k0 := range d.Params {
|
||||||
|
|
||||||
|
{
|
||||||
|
l := uint64(len(d.Params[k0]))
|
||||||
|
|
||||||
|
{
|
||||||
|
|
||||||
|
t := uint64(l)
|
||||||
|
|
||||||
|
for t >= 0x80 {
|
||||||
|
buf[i+0] = byte(t) | 0x80
|
||||||
|
t >>= 7
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
buf[i+0] = byte(t)
|
||||||
|
i++
|
||||||
|
|
||||||
|
}
|
||||||
|
copy(buf[i+0:], d.Params[k0])
|
||||||
|
i += l
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
{
|
||||||
|
|
||||||
|
*(*int64)(unsafe.Pointer(&buf[i+0])) = d.Time
|
||||||
|
|
||||||
|
}
|
||||||
|
return buf[:i+8], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Event) Unmarshal(buf []byte) (uint64, error) {
|
||||||
|
i := uint64(0)
|
||||||
|
|
||||||
|
{
|
||||||
|
l := uint64(0)
|
||||||
|
|
||||||
|
{
|
||||||
|
|
||||||
|
bs := uint8(7)
|
||||||
|
t := uint64(buf[i+0] & 0x7F)
|
||||||
|
for buf[i+0]&0x80 == 0x80 {
|
||||||
|
i++
|
||||||
|
t |= uint64(buf[i+0]&0x7F) << bs
|
||||||
|
bs += 7
|
||||||
|
}
|
||||||
|
i++
|
||||||
|
|
||||||
|
l = t
|
||||||
|
|
||||||
|
}
|
||||||
|
d.Type = string(buf[i+0 : i+0+l])
|
||||||
|
i += l
|
||||||
|
}
|
||||||
|
{
|
||||||
|
l := uint64(0)
|
||||||
|
|
||||||
|
{
|
||||||
|
|
||||||
|
bs := uint8(7)
|
||||||
|
t := uint64(buf[i+0] & 0x7F)
|
||||||
|
for buf[i+0]&0x80 == 0x80 {
|
||||||
|
i++
|
||||||
|
t |= uint64(buf[i+0]&0x7F) << bs
|
||||||
|
bs += 7
|
||||||
|
}
|
||||||
|
i++
|
||||||
|
|
||||||
|
l = t
|
||||||
|
|
||||||
|
}
|
||||||
|
if uint64(cap(d.Params)) >= l {
|
||||||
|
d.Params = d.Params[:l]
|
||||||
|
} else {
|
||||||
|
d.Params = make([]string, l)
|
||||||
|
}
|
||||||
|
for k0 := range d.Params {
|
||||||
|
|
||||||
|
{
|
||||||
|
l := uint64(0)
|
||||||
|
|
||||||
|
{
|
||||||
|
|
||||||
|
bs := uint8(7)
|
||||||
|
t := uint64(buf[i+0] & 0x7F)
|
||||||
|
for buf[i+0]&0x80 == 0x80 {
|
||||||
|
i++
|
||||||
|
t |= uint64(buf[i+0]&0x7F) << bs
|
||||||
|
bs += 7
|
||||||
|
}
|
||||||
|
i++
|
||||||
|
|
||||||
|
l = t
|
||||||
|
|
||||||
|
}
|
||||||
|
d.Params[k0] = string(buf[i+0 : i+0+l])
|
||||||
|
i += l
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
{
|
||||||
|
|
||||||
|
d.Time = *(*int64)(unsafe.Pointer(&buf[i+0]))
|
||||||
|
|
||||||
}
|
}
|
||||||
return i + 8, nil
|
return i + 8, nil
|
||||||
}
|
}
|
||||||
|
161
storage/user.go
161
storage/user.go
@ -5,6 +5,8 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/kjk/betterguid"
|
||||||
)
|
)
|
||||||
|
|
||||||
type User struct {
|
type User struct {
|
||||||
@ -15,6 +17,7 @@ type User struct {
|
|||||||
store Store
|
store Store
|
||||||
messageLog MessageStore
|
messageLog MessageStore
|
||||||
messageIndex MessageSearchProvider
|
messageIndex MessageSearchProvider
|
||||||
|
lastMessages map[string]map[string]*Message
|
||||||
clientSettings *ClientSettings
|
clientSettings *ClientSettings
|
||||||
lastIP []byte
|
lastIP []byte
|
||||||
certificate *tls.Certificate
|
certificate *tls.Certificate
|
||||||
@ -25,6 +28,7 @@ func NewUser(store Store) (*User, error) {
|
|||||||
user := &User{
|
user := &User{
|
||||||
store: store,
|
store: store,
|
||||||
clientSettings: DefaultClientSettings(),
|
clientSettings: DefaultClientSettings(),
|
||||||
|
lastMessages: map[string]map[string]*Message{},
|
||||||
}
|
}
|
||||||
|
|
||||||
err := store.SaveUser(user)
|
err := store.SaveUser(user)
|
||||||
@ -32,11 +36,19 @@ func NewUser(store Store) (*User, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
user.messageLog, err = GetMessageStore(user)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
user.messageIndex, err = GetMessageSearchProvider(user)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
err = os.MkdirAll(Path.User(user.Username), 0700)
|
err = os.MkdirAll(Path.User(user.Username), 0700)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
err = os.Mkdir(Path.Downloads(user.Username), 0700)
|
err = os.Mkdir(Path.Downloads(user.Username), 0700)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@ -53,20 +65,35 @@ func LoadUsers(store Store) ([]*User, error) {
|
|||||||
|
|
||||||
for _, user := range users {
|
for _, user := range users {
|
||||||
user.store = store
|
user.store = store
|
||||||
|
user.messageLog, err = GetMessageStore(user)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
user.messageIndex, err = GetMessageSearchProvider(user)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
user.lastMessages = map[string]map[string]*Message{}
|
||||||
user.loadCertificate()
|
user.loadCertificate()
|
||||||
|
|
||||||
|
channels, err := user.GetChannels()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, channel := range channels {
|
||||||
|
messages, _, err := user.GetLastMessages(channel.Server, channel.Name, 1)
|
||||||
|
if err == nil && len(messages) == 1 {
|
||||||
|
user.lastMessages[channel.Server] = map[string]*Message{
|
||||||
|
channel.Name: &messages[0],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return users, nil
|
return users, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *User) SetMessageStore(store MessageStore) {
|
|
||||||
u.messageLog = store
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *User) SetMessageSearchProvider(search MessageSearchProvider) {
|
|
||||||
u.messageIndex = search
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *User) Remove() {
|
func (u *User) Remove() {
|
||||||
u.store.DeleteUser(u)
|
u.store.DeleteUser(u)
|
||||||
if u.messageLog != nil {
|
if u.messageLog != nil {
|
||||||
@ -178,7 +205,6 @@ func (u *User) SetServerName(name, address string) error {
|
|||||||
return u.AddServer(server)
|
return u.AddServer(server)
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Remove topic from disk schema
|
|
||||||
type Channel struct {
|
type Channel struct {
|
||||||
Server string
|
Server string
|
||||||
Name string
|
Name string
|
||||||
@ -221,27 +247,122 @@ type Message struct {
|
|||||||
To string `json:"-" bleve:"to"`
|
To string `json:"-" bleve:"to"`
|
||||||
Content string `bleve:"content"`
|
Content string `bleve:"content"`
|
||||||
Time int64 `bleve:"-"`
|
Time int64 `bleve:"-"`
|
||||||
|
Events []Event `bleve:"-"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m Message) Type() string {
|
func (m Message) Type() string {
|
||||||
return "message"
|
return "message"
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *User) LogMessage(id, server, from, to, content string) error {
|
func (u *User) LogMessage(msg *Message) error {
|
||||||
message := &Message{
|
if msg.Time == 0 {
|
||||||
ID: id,
|
msg.Time = time.Now().Unix()
|
||||||
Server: server,
|
|
||||||
From: from,
|
|
||||||
To: to,
|
|
||||||
Content: content,
|
|
||||||
Time: time.Now().Unix(),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
err := u.messageLog.LogMessage(message)
|
if msg.ID == "" {
|
||||||
|
msg.ID = betterguid.New()
|
||||||
|
}
|
||||||
|
|
||||||
|
if msg.To == "" {
|
||||||
|
msg.To = msg.From
|
||||||
|
}
|
||||||
|
|
||||||
|
u.setLastMessage(msg.Server, msg.To, msg)
|
||||||
|
|
||||||
|
err := u.messageLog.LogMessage(msg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return u.messageIndex.Index(id, message)
|
return u.messageIndex.Index(msg.ID, msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
type Event struct {
|
||||||
|
Type string
|
||||||
|
Params []string
|
||||||
|
Time int64
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *User) LogEvent(server, name string, params []string, channels ...string) error {
|
||||||
|
now := time.Now().Unix()
|
||||||
|
event := Event{
|
||||||
|
Type: name,
|
||||||
|
Params: params,
|
||||||
|
Time: now,
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, channel := range channels {
|
||||||
|
lastMessage := u.getLastMessage(server, channel)
|
||||||
|
|
||||||
|
if lastMessage != nil && shouldCollapse(lastMessage, event) {
|
||||||
|
lastMessage.Events = append(lastMessage.Events, event)
|
||||||
|
u.setLastMessage(server, channel, lastMessage)
|
||||||
|
|
||||||
|
err := u.messageLog.LogMessage(lastMessage)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
msg := &Message{
|
||||||
|
ID: betterguid.New(),
|
||||||
|
Server: server,
|
||||||
|
To: channel,
|
||||||
|
Time: now,
|
||||||
|
Events: []Event{event},
|
||||||
|
}
|
||||||
|
u.setLastMessage(server, channel, msg)
|
||||||
|
|
||||||
|
err := u.messageLog.LogMessage(msg)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var collapsed = []string{"join", "part", "quit"}
|
||||||
|
|
||||||
|
func shouldCollapse(msg *Message, event Event) bool {
|
||||||
|
matches := 0
|
||||||
|
if len(msg.Events) > 0 {
|
||||||
|
for _, collapseType := range collapsed {
|
||||||
|
if msg.Events[0].Type == collapseType {
|
||||||
|
matches++
|
||||||
|
}
|
||||||
|
if event.Type == collapseType {
|
||||||
|
matches++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return matches == 2
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *User) getLastMessage(server, channel string) *Message {
|
||||||
|
u.lock.Lock()
|
||||||
|
defer u.lock.Unlock()
|
||||||
|
|
||||||
|
if _, ok := u.lastMessages[server]; !ok {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
last := u.lastMessages[server][channel]
|
||||||
|
if last != nil {
|
||||||
|
msg := *last
|
||||||
|
return &msg
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *User) setLastMessage(server, channel string, msg *Message) {
|
||||||
|
u.lock.Lock()
|
||||||
|
|
||||||
|
if _, ok := u.lastMessages[server]; !ok {
|
||||||
|
u.lastMessages[server] = map[string]*Message{}
|
||||||
|
}
|
||||||
|
|
||||||
|
u.lastMessages[server][channel] = msg
|
||||||
|
u.lock.Unlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *User) GetMessages(server, channel string, count int, fromID string) ([]Message, bool, error) {
|
func (u *User) GetMessages(server, channel string, count int, fromID string) ([]Message, bool, error) {
|
||||||
|
@ -24,6 +24,13 @@ func TestUser(t *testing.T) {
|
|||||||
db, err := boltdb.New(storage.Path.Database())
|
db, err := boltdb.New(storage.Path.Database())
|
||||||
assert.Nil(t, err)
|
assert.Nil(t, err)
|
||||||
|
|
||||||
|
storage.GetMessageStore = func(_ *storage.User) (storage.MessageStore, error) {
|
||||||
|
return db, nil
|
||||||
|
}
|
||||||
|
storage.GetMessageSearchProvider = func(_ *storage.User) (storage.MessageSearchProvider, error) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
user, err := storage.NewUser(db)
|
user, err := storage.NewUser(db)
|
||||||
assert.Nil(t, err)
|
assert.Nil(t, err)
|
||||||
|
|
||||||
@ -124,17 +131,18 @@ func TestMessages(t *testing.T) {
|
|||||||
db, err := boltdb.New(storage.Path.Database())
|
db, err := boltdb.New(storage.Path.Database())
|
||||||
assert.Nil(t, err)
|
assert.Nil(t, err)
|
||||||
|
|
||||||
|
storage.GetMessageStore = func(_ *storage.User) (storage.MessageStore, error) {
|
||||||
|
return db, nil
|
||||||
|
}
|
||||||
|
storage.GetMessageSearchProvider = func(user *storage.User) (storage.MessageSearchProvider, error) {
|
||||||
|
return bleve.New(storage.Path.Index(user.Username))
|
||||||
|
}
|
||||||
|
|
||||||
user, err := storage.NewUser(db)
|
user, err := storage.NewUser(db)
|
||||||
assert.Nil(t, err)
|
assert.Nil(t, err)
|
||||||
|
|
||||||
os.MkdirAll(storage.Path.User(user.Username), 0700)
|
os.MkdirAll(storage.Path.User(user.Username), 0700)
|
||||||
|
|
||||||
search, err := bleve.New(storage.Path.Index(user.Username))
|
|
||||||
assert.Nil(t, err)
|
|
||||||
|
|
||||||
user.SetMessageStore(db)
|
|
||||||
user.SetMessageSearchProvider(search)
|
|
||||||
|
|
||||||
messages, hasMore, err := user.GetMessages("irc.freenode.net", "#go-nuts", 10, "6")
|
messages, hasMore, err := user.GetMessages("irc.freenode.net", "#go-nuts", 10, "6")
|
||||||
assert.Nil(t, err)
|
assert.Nil(t, err)
|
||||||
assert.False(t, hasMore)
|
assert.False(t, hasMore)
|
||||||
@ -153,7 +161,13 @@ func TestMessages(t *testing.T) {
|
|||||||
for i := 0; i < 5; i++ {
|
for i := 0; i < 5; i++ {
|
||||||
id := betterguid.New()
|
id := betterguid.New()
|
||||||
ids = append(ids, id)
|
ids = append(ids, id)
|
||||||
err = user.LogMessage(id, "irc.freenode.net", "nick", "#go-nuts", "message"+strconv.Itoa(i))
|
err = user.LogMessage(&storage.Message{
|
||||||
|
ID: id,
|
||||||
|
Server: "irc.freenode.net",
|
||||||
|
From: "nick",
|
||||||
|
To: "#go-nuts",
|
||||||
|
Content: "message" + strconv.Itoa(i),
|
||||||
|
})
|
||||||
assert.Nil(t, err)
|
assert.Nil(t, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -196,5 +210,42 @@ func TestMessages(t *testing.T) {
|
|||||||
assert.Nil(t, err)
|
assert.Nil(t, err)
|
||||||
assert.True(t, len(messages) > 0)
|
assert.True(t, len(messages) > 0)
|
||||||
|
|
||||||
|
user.LogEvent("irc.freenode.net", "join", []string{"bob"}, "#go-nuts")
|
||||||
|
messages, hasMore, err = user.GetLastMessages("irc.freenode.net", "#go-nuts", 1)
|
||||||
|
assert.Zero(t, messages[0].Content)
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.True(t, hasMore)
|
||||||
|
assert.Len(t, messages[0].Events, 1)
|
||||||
|
assert.Equal(t, "join", messages[0].Events[0].Type)
|
||||||
|
assert.NotZero(t, messages[0].Events[0].Time)
|
||||||
|
|
||||||
|
user.LogEvent("irc.freenode.net", "part", []string{"bob"}, "#go-nuts")
|
||||||
|
messages, hasMore, err = user.GetLastMessages("irc.freenode.net", "#go-nuts", 1)
|
||||||
|
assert.Zero(t, messages[0].Content)
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.True(t, hasMore)
|
||||||
|
assert.Len(t, messages[0].Events, 2)
|
||||||
|
assert.Equal(t, "part", messages[0].Events[1].Type)
|
||||||
|
assert.NotZero(t, messages[0].Events[0].Time)
|
||||||
|
|
||||||
|
user.LogEvent("irc.freenode.net", "nick", []string{"bob", "rob"}, "#go-nuts")
|
||||||
|
messages, hasMore, err = user.GetLastMessages("irc.freenode.net", "#go-nuts", 1)
|
||||||
|
assert.Zero(t, messages[0].Content)
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.True(t, hasMore)
|
||||||
|
assert.Len(t, messages[0].Events, 1)
|
||||||
|
assert.Equal(t, "nick", messages[0].Events[0].Type)
|
||||||
|
assert.NotZero(t, messages[0].Events[0].Time)
|
||||||
|
|
||||||
|
user.LogEvent("irc.freenode.net", "quit", []string{"rob", "bored"}, "#go-nuts")
|
||||||
|
messages, hasMore, err = user.GetLastMessages("irc.freenode.net", "#go-nuts", 1)
|
||||||
|
assert.Zero(t, messages[0].Content)
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.True(t, hasMore)
|
||||||
|
assert.Len(t, messages[0].Events, 1)
|
||||||
|
assert.Equal(t, "quit", messages[0].Events[0].Type)
|
||||||
|
assert.Equal(t, []string{"rob", "bored"}, messages[0].Events[0].Params)
|
||||||
|
assert.NotZero(t, messages[0].Events[0].Time)
|
||||||
|
|
||||||
db.Close()
|
db.Close()
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user