fchat-rising/chat/conversations.ts

862 lines
38 KiB
TypeScript

import {queuedJoin} from '../fchat/channels';
import {decodeHTML} from '../fchat/common';
import { CharacterCacheRecord } from '../learn/profile-cache';
import { AdManager } from './ads/ad-manager';
import { characterImage, ConversationSettings, EventMessage, Message, messageToString } from './common';
import core from './core';
import {Channel, Character, Conversation as Interfaces} from './interfaces';
import l from './localize';
import {CommandContext, isAction, isCommand, isWarn, parse as parseCommand} from './slash_commands';
import MessageType = Interfaces.Message.Type;
import {EventBus} from './preview/event-bus';
import throat from 'throat';
import Bluebird from 'bluebird';
function createMessage(this: any, type: MessageType, sender: Character, text: string, time?: Date): Message {
if(type === MessageType.Message && isAction(text)) {
type = MessageType.Action;
text = text.substr(text.charAt(4) === ' ' ? 4 : 3);
}
return new Message(type, sender, text, time);
}
function safeAddMessage(this: any, messages: Interfaces.Message[], message: Interfaces.Message, max: number): void {
if(messages.length >= max) messages.shift();
messages.push(message);
}
abstract class Conversation implements Interfaces.Conversation {
abstract enteredText: string;
abstract readonly name: string;
messages: Interfaces.Message[] = [];
errorText = '';
unread = Interfaces.UnreadState.None;
lastRead: Interfaces.Message | undefined = undefined;
infoText = '';
abstract readonly maxMessageLength: number | undefined;
_settings: Interfaces.Settings | undefined;
protected abstract context: CommandContext;
protected maxMessages = 50;
protected allMessages: Interfaces.Message[] = [];
readonly reportMessages: Interfaces.Message[] = [];
private lastSent = '';
adManager: AdManager;
protected static readonly conversationThroat = throat(1); // make sure user posting and ad posting won't get in each others' way
constructor(readonly key: string, public _isPinned: boolean) {
this.adManager = new AdManager(this);
}
get settings(): Interfaces.Settings {
//tslint:disable-next-line:strict-boolean-expressions
return this._settings || (this._settings = state.settings[this.key] || new ConversationSettings());
}
set settings(value: Interfaces.Settings) {
this._settings = value;
state.setSettings(this.key, value); //tslint:disable-line:no-floating-promises
}
get isPinned(): boolean {
return this._isPinned;
}
set isPinned(value: boolean) {
if(value === this._isPinned) return;
this._isPinned = value;
state.savePinned(); //tslint:disable-line:no-floating-promises
}
clearText(): void {
setImmediate(() => this.enteredText = '');
}
async send(): Promise<void> {
if(this.enteredText.length === 0) return;
if(isCommand(this.enteredText)) {
const parsed = parseCommand(this.enteredText, this.context);
if(typeof parsed === 'string') this.errorText = parsed;
else {
parsed.call(this);
this.lastSent = this.enteredText;
this.clearText();
}
} else {
this.lastSent = this.enteredText;
await this.doSend();
}
}
//tslint:disable-next-line:no-async-without-await
abstract async addMessage(message: Interfaces.Message): Promise<void>;
loadLastSent(): void {
this.enteredText = this.lastSent;
}
loadMore(): boolean {
if(this.messages.length >= this.allMessages.length) return false;
this.maxMessages += 50;
this.messages = this.allMessages.slice(-this.maxMessages);
return true;
}
show(): void {
state.show(this);
}
onHide(): void {
this.errorText = '';
this.lastRead = this.messages[this.messages.length - 1];
this.maxMessages = 50;
this.messages = this.allMessages.slice(-this.maxMessages);
}
clear(): void {
this.allMessages = [];
this.messages = [];
}
abstract close(): void;
protected safeAddMessage(message: Interfaces.Message): void {
safeAddMessage(this.reportMessages, message, 500);
safeAddMessage(this.allMessages, message, 500);
safeAddMessage(this.messages, message, this.maxMessages);
}
protected abstract doSend(): void | Promise<void>;
protected static readonly POST_DELAY = 1250;
protected static async testPostDelay(): Promise<void> {
const lastPostDelta = Date.now() - core.cache.getLastPost().getTime();
// console.log('Last Post Delta', lastPostDelta, ((lastPostDelta < Conversation.POST_DELAY) && (lastPostDelta > 0)));
if ((lastPostDelta < Conversation.POST_DELAY) && (lastPostDelta > 0)) {
await Bluebird.delay(Conversation.POST_DELAY - lastPostDelta);
}
}
}
class PrivateConversation extends Conversation implements Interfaces.PrivateConversation {
readonly name = this.character.name;
readonly context = CommandContext.Private;
typingStatus: Interfaces.TypingStatus = 'clear';
readonly maxMessageLength = core.connection.vars.priv_max;
private _enteredText = '';
private ownTypingStatus: Interfaces.TypingStatus = 'clear';
private timer: number | undefined;
private logPromise = core.logs.getBacklog(this).then((messages) => {
this.allMessages.unshift(...messages);
this.reportMessages.unshift(...messages);
this.messages = this.allMessages.slice();
});
constructor(readonly character: Character) {
super(character.name.toLowerCase(), state.pinned.private.indexOf(character.name) !== -1);
this.lastRead = this.messages[this.messages.length - 1];
}
get enteredText(): string {
return this._enteredText;
}
set enteredText(value: string) {
this._enteredText = value;
if(this.timer !== undefined) clearTimeout(this.timer);
if(value.length > 0) {
if(this.ownTypingStatus !== 'typing') this.setOwnTyping('typing');
this.timer = window.setTimeout(() => this.setOwnTyping('paused'), 5000);
} else if(this.ownTypingStatus !== 'clear') this.setOwnTyping('clear');
}
async addMessage(message: Interfaces.Message): Promise<void> {
await this.logPromise;
this.safeAddMessage(message);
if(message.type !== Interfaces.Message.Type.Event) {
if(core.state.settings.logMessages) await core.logs.logMessage(this, message);
if(this.settings.notify !== Interfaces.Setting.False && message.sender !== core.characters.ownCharacter)
await core.notifications.notify(this, message.sender.name, message.text, characterImage(message.sender.name), 'attention');
if(this !== state.selectedConversation || !state.windowFocused)
this.unread = Interfaces.UnreadState.Mention;
this.typingStatus = 'clear';
}
}
async close(): Promise<void> {
state.privateConversations.splice(state.privateConversations.indexOf(this), 1);
delete state.privateMap[this.character.name.toLowerCase()];
await state.savePinned();
if(state.selectedConversation === this) state.show(state.consoleTab);
}
async sort(newIndex: number): Promise<void> {
state.privateConversations.splice(state.privateConversations.indexOf(this), 1);
state.privateConversations.splice(newIndex, 0, this);
return state.savePinned();
}
protected async doSend(): Promise<void> {
await this.logPromise;
if(this.character.status === 'offline') {
this.errorText = l('chat.errorOffline', this.character.name);
return;
}
if(this.character.isIgnored) {
this.errorText = l('chat.errorIgnored', this.character.name);
return;
}
if(this.adManager.isActive()) {
this.errorText = 'Cannot send ads manually while ad auto-posting is active';
return;
}
const messageText = this.enteredText;
this.clearText();
await Conversation.conversationThroat(
async() => {
await Conversation.testPostDelay();
core.connection.send('PRI', {recipient: this.name, message: messageText});
core.cache.markLastPostTime();
const message = createMessage(MessageType.Message, core.characters.ownCharacter, messageText);
this.safeAddMessage(message);
if(core.state.settings.logMessages) await core.logs.logMessage(this, message);
}
);
}
private setOwnTyping(status: Interfaces.TypingStatus): void {
this.ownTypingStatus = status;
core.connection.send('TPN', {character: this.name, status});
}
}
class ChannelConversation extends Conversation implements Interfaces.ChannelConversation {
readonly context = CommandContext.Channel;
readonly name = this.channel.name;
isSendingAds = this.channel.mode === 'ads';
nextAd = 0;
private chat: Interfaces.Message[] = [];
private ads: Interfaces.Message[] = [];
private both: Interfaces.Message[] = [];
private _mode!: Channel.Mode;
private adEnteredText = '';
private chatEnteredText = '';
private logPromise = core.logs.getBacklog(this).then((messages) => {
this.both.unshift(...messages);
this.chat.unshift(...this.both.filter((x) => x.type !== MessageType.Ad));
this.ads.unshift(...this.both.filter((x) => x.type === MessageType.Ad));
this.reportMessages.unshift(...messages);
this.lastRead = this.messages[this.messages.length - 1];
this.messages = this.allMessages.slice(-this.maxMessages);
});
constructor(readonly channel: Channel) {
super(`#${channel.id.replace(/[^\w- ]/gi, '')}`, state.pinned.channels.indexOf(channel.id) !== -1);
core.watch<Channel.Mode | undefined>(function(): Channel.Mode | undefined {
const c = this.channels.getChannel(channel.id);
return c !== undefined ? c.mode : undefined;
}, (value: Channel.Mode | undefined) => {
if(value === undefined) return;
this.mode = value;
if(value !== 'both') this.isSendingAds = value === 'ads';
});
this.mode = channel.mode === 'both' && channel.id in state.modes ? state.modes[channel.id]! : channel.mode;
}
get maxMessageLength(): number {
return core.connection.vars[this.isSendingAds ? 'lfrp_max' : 'chat_max'];
}
get mode(): Channel.Mode {
return this._mode;
}
set mode(mode: Channel.Mode) {
this._mode = mode;
this.maxMessages = 50;
this.allMessages = this[mode];
this.messages = this.allMessages.slice(-this.maxMessages);
if(mode === this.channel.mode && this.channel.id in state.modes) delete state.modes[this.channel.id];
else if(mode !== this.channel.mode && mode !== state.modes[this.channel.id]) state.modes[this.channel.id] = mode;
else return;
state.saveModes(); //tslint:disable-line:no-floating-promises
}
get enteredText(): string {
return this.isSendingAds ? this.adEnteredText : this.chatEnteredText;
}
set enteredText(value: string) {
if(this.isSendingAds) this.adEnteredText = value;
else this.chatEnteredText = value;
}
addModeMessage(mode: Channel.Mode, message: Interfaces.Message): void {
safeAddMessage(this[mode], message, 500);
if(this._mode === mode) safeAddMessage(this.messages, message, this.maxMessages);
}
async addMessage(message: Interfaces.Message): Promise<void> {
await this.logPromise;
if((message.type === MessageType.Message || message.type === MessageType.Ad) && isWarn(message.text)) {
const member = this.channel.members[message.sender.name];
if(member !== undefined && member.rank > Channel.Rank.Member || message.sender.isChatOp)
message = new Message(MessageType.Warn, message.sender, message.text.substr(6), message.time);
}
if(message.type === MessageType.Ad) {
this.addModeMessage('ads', message);
if(core.state.settings.logAds) await core.logs.logMessage(this, message);
} else {
this.addModeMessage('chat', message);
if(message.type !== Interfaces.Message.Type.Event) {
if(message.type === Interfaces.Message.Type.Warn) this.addModeMessage('ads', message);
if(core.state.settings.logMessages) await core.logs.logMessage(this, message);
if(this.unread === Interfaces.UnreadState.None && (this !== state.selectedConversation || !state.windowFocused)
&& this.mode !== 'ads')
this.unread = Interfaces.UnreadState.Unread;
} else this.addModeMessage('ads', message);
}
this.addModeMessage('both', message);
if(message.type !== Interfaces.Message.Type.Event)
safeAddMessage(this.reportMessages, message, 500);
}
clear(): void {
this.messages = [];
this.chat.length = 0;
this.ads.length = 0;
this.both.length = 0;
}
close(): void {
core.connection.send('LCH', {channel: this.channel.id});
}
async sort(newIndex: number): Promise<void> {
state.channelConversations.splice(state.channelConversations.indexOf(this), 1);
state.channelConversations.splice(newIndex, 0, this);
return state.savePinned();
}
protected async doSend(): Promise<void> {
const isAd = this.isSendingAds;
if(this.adManager.isActive()) {
this.errorText = 'Cannot post ads manually while ad auto-posting is active';
return;
}
if(isAd && Date.now() < this.nextAd) {
this.errorText = 'You must wait at least ten minutes between ad posts on this channel';
return;
}
const message = this.enteredText;
if (!isAd) {
this.clearText();
}
await Conversation.conversationThroat(
async() => {
await Conversation.testPostDelay();
core.connection.send(isAd ? 'LRP' : 'MSG', {channel: this.channel.id, message});
core.cache.markLastPostTime();
await this.addMessage(
createMessage(isAd ? MessageType.Ad : MessageType.Message, core.characters.ownCharacter, message, new Date())
);
if(isAd)
this.nextAd = Date.now() + core.connection.vars.lfrp_flood * 1000;
}
);
}
async sendAd(text: string): Promise<void> {
if (text.length < 1)
return;
await Conversation.conversationThroat(
async() => {
await Conversation.testPostDelay();
core.connection.send('LRP', {channel: this.channel.id, message: text});
core.cache.markLastPostTime();
await this.addMessage(
createMessage(MessageType.Ad, core.characters.ownCharacter, text, new Date())
);
this.nextAd = Date.now() + core.connection.vars.lfrp_flood * 1000;
}
);
}
}
class ConsoleConversation extends Conversation {
readonly context = CommandContext.Console;
readonly name = l('chat.consoleTab');
readonly maxMessageLength = undefined;
enteredText = '';
constructor() {
super('_', false);
this.allMessages = [];
}
//tslint:disable-next-line:no-empty
close(): void {
}
async addMessage(message: Interfaces.Message): Promise<void> {
this.safeAddMessage(message);
if(core.state.settings.logMessages) await core.logs.logMessage(this, message);
if(this !== state.selectedConversation || !state.windowFocused) this.unread = Interfaces.UnreadState.Unread;
}
protected doSend(): void {
this.errorText = l('chat.consoleChat');
}
}
class State implements Interfaces.State {
privateConversations: PrivateConversation[] = [];
channelConversations: ChannelConversation[] = [];
privateMap: {[key: string]: PrivateConversation | undefined} = {};
channelMap: {[key: string]: ChannelConversation | undefined} = {};
consoleTab!: ConsoleConversation;
selectedConversation: Conversation = this.consoleTab;
recent: Interfaces.RecentPrivateConversation[] = [];
recentChannels: Interfaces.RecentChannelConversation[] = [];
pinned!: {channels: string[], private: string[]};
settings!: {[key: string]: Interfaces.Settings};
modes!: {[key: string]: Channel.Mode | undefined};
windowFocused = document.hasFocus();
get hasNew(): boolean {
return this.privateConversations.some((x) => x.unread === Interfaces.UnreadState.Mention) ||
this.channelConversations.some((x) => x.unread === Interfaces.UnreadState.Mention);
}
getPrivate(character: Character): PrivateConversation {
const key = character.name.toLowerCase();
let conv = state.privateMap[key];
if(conv !== undefined) return conv;
conv = new PrivateConversation(character);
this.privateConversations.push(conv);
this.privateMap[key] = conv;
const index = this.recent.findIndex((c) => c.character === conv!.name);
if(index !== -1) this.recent.splice(index, 1);
if(this.recent.length >= 50) this.recent.pop();
this.recent.unshift({character: conv.name});
core.settingsStore.set('recent', this.recent); //tslint:disable-line:no-floating-promises
return conv;
}
byKey(key: string): Conversation | undefined {
if(key === '_') return this.consoleTab;
key = key.toLowerCase();
return key[0] === '#' ? this.channelMap[key.substr(1)] : this.privateMap[key];
}
async savePinned(): Promise<void> {
this.pinned.channels = this.channelConversations.filter((x) => x.isPinned).map((x) => x.channel.id);
this.pinned.private = this.privateConversations.filter((x) => x.isPinned).map((x) => x.name);
await core.settingsStore.set('pinned', this.pinned);
}
async saveModes(): Promise<void> {
await core.settingsStore.set('modes', this.modes);
}
async setSettings(key: string, value: Interfaces.Settings): Promise<void> {
this.settings[key] = value;
await core.settingsStore.set('conversationSettings', this.settings);
}
show(conversation: Conversation): void {
if(conversation === this.selectedConversation) return;
this.selectedConversation.onHide();
conversation.unread = Interfaces.UnreadState.None;
this.selectedConversation = conversation;
}
async reloadSettings(): Promise<void> {
//tslint:disable:strict-boolean-expressions
this.pinned = await core.settingsStore.get('pinned') || {private: [], channels: []};
this.modes = await core.settingsStore.get('modes') || {};
for(const conversation of this.channelConversations)
conversation._isPinned = this.pinned.channels.indexOf(conversation.channel.id) !== -1;
for(const conversation of this.privateConversations)
conversation._isPinned = this.pinned.private.indexOf(conversation.name) !== -1;
this.recent = await core.settingsStore.get('recent') || [];
this.recentChannels = await core.settingsStore.get('recentChannels') || [];
const settings = <{[key: string]: ConversationSettings}> await core.settingsStore.get('conversationSettings') || {};
for(const key in settings) {
settings[key] = Object.assign(new ConversationSettings(), settings[key]);
const conv = this.byKey(key);
if(conv !== undefined) conv._settings = settings[key];
}
this.settings = settings;
//tslint:enable
}
}
let state: State;
async function addEventMessage(this: any, message: Interfaces.Message): Promise<void> {
await state.consoleTab.addMessage(message);
if(core.state.settings.eventMessages && state.selectedConversation !== state.consoleTab)
await state.selectedConversation.addMessage(message);
}
function isOfInterest(this: any, character: Character): boolean {
return character.isFriend || character.isBookmarked || state.privateMap[character.name.toLowerCase()] !== undefined;
}
export default function(this: any): Interfaces.State {
state = new State();
window.addEventListener('focus', () => {
state.windowFocused = true;
if(state.selectedConversation !== undefined!) state.selectedConversation.unread = Interfaces.UnreadState.None;
});
window.addEventListener('blur', () => {
state.windowFocused = false;
if(state.selectedConversation !== undefined!)
state.selectedConversation.lastRead = state.selectedConversation.messages[state.selectedConversation.messages.length - 1];
});
const connection = core.connection;
connection.onEvent('connecting', async(isReconnect) => {
state.channelConversations = [];
state.channelMap = {};
if(!isReconnect) {
state.consoleTab = new ConsoleConversation();
state.privateConversations = [];
state.privateMap = {};
} else state.consoleTab.unread = Interfaces.UnreadState.None;
state.selectedConversation = state.consoleTab;
await state.reloadSettings();
});
connection.onEvent('connected', (isReconnect) => {
if(isReconnect) return;
for(const item of state.pinned.private) state.getPrivate(core.characters.get(item));
queuedJoin(state.pinned.channels.slice());
});
core.channels.onEvent(async(type, channel, member) => {
if(type === 'join')
if(member === undefined) {
const conv = new ChannelConversation(channel);
state.channelMap[channel.id] = conv;
state.channelConversations.push(conv);
const index = state.recentChannels.findIndex((c) => c.channel === channel.id);
if(index !== -1) state.recentChannels.splice(index, 1);
if(state.recentChannels.length >= 50) state.recentChannels.pop();
state.recentChannels.unshift({channel: channel.id, name: conv.channel.name});
core.settingsStore.set('recentChannels', state.recentChannels); //tslint:disable-line:no-floating-promises
AdManager.onNewChannelAvailable(conv);
} else {
const conv = state.channelMap[channel.id];
if(conv === undefined) return;
if(conv.settings.joinMessages === Interfaces.Setting.False || conv.settings.joinMessages === Interfaces.Setting.Default &&
!core.state.settings.joinMessages) return;
const text = l('events.channelJoin', `[user]${member.character.name}[/user]`);
await conv.addMessage(new EventMessage(text));
}
else if(member === undefined) {
const conv = state.channelMap[channel.id];
if(conv === undefined) return;
state.channelConversations.splice(state.channelConversations.indexOf(conv), 1);
delete state.channelMap[channel.id];
await state.savePinned();
if(state.selectedConversation === conv) state.show(state.consoleTab);
} else {
const conv = state.channelMap[channel.id];
if(conv === undefined) return;
if(conv.settings.joinMessages === Interfaces.Setting.False || conv.settings.joinMessages === Interfaces.Setting.Default &&
!core.state.settings.joinMessages) return;
const text = l('events.channelLeave', `[user]${member.character.name}[/user]`);
await conv.addMessage(new EventMessage(text));
}
});
connection.onMessage('PRI', async(data, time) => {
const char = core.characters.get(data.character);
if(char.isIgnored) return connection.send('IGN', {action: 'notify', character: data.character});
const message = createMessage(MessageType.Message, char, decodeHTML(data.message), time);
EventBus.$emit('private-message', { message });
const conv = state.getPrivate(char);
await conv.addMessage(message);
});
connection.onMessage('MSG', async(data, time) => {
const char = core.characters.get(data.character);
const conversation = state.channelMap[data.channel.toLowerCase()];
if(conversation === undefined) return core.channels.leave(data.channel);
if(char.isIgnored) return;
const message = createMessage(MessageType.Message, char, decodeHTML(data.message), time);
EventBus.$emit('channel-message', { message, channel: conversation });
await conversation.addMessage(message);
const words = conversation.settings.highlightWords.slice();
if(conversation.settings.defaultHighlights) words.push(...core.state.settings.highlightWords);
if(conversation.settings.highlight === Interfaces.Setting.Default && core.state.settings.highlight ||
conversation.settings.highlight === Interfaces.Setting.True) words.push(core.connection.character);
for(let i = 0; i < words.length; ++i)
words[i] = words[i].replace(/[^\w]/gi, '\\$&');
//tslint:disable-next-line:no-null-keyword
const results = words.length > 0 ? message.text.match(new RegExp(`\\b(${words.join('|')})\\b`, 'i')) : null;
if(results !== null) {
await core.notifications.notify(conversation, data.character, l('chat.highlight', results[0], conversation.name, message.text),
characterImage(data.character), 'attention');
if(conversation !== state.selectedConversation || !state.windowFocused) conversation.unread = Interfaces.UnreadState.Mention;
message.isHighlight = true;
await state.consoleTab.addMessage(new EventMessage(l('events.highlight', `[user]${data.character}[/user]`, results[0],
`[session=${conversation.name}]${data.channel}[/session]`), time));
} else if(conversation.settings.notify === Interfaces.Setting.True) {
await core.notifications.notify(conversation, conversation.name, messageToString(message),
characterImage(data.character), 'attention');
if(conversation !== state.selectedConversation || !state.windowFocused) conversation.unread = Interfaces.UnreadState.Mention;
}
});
connection.onMessage('LRP', async(data, time) => {
const char = core.characters.get(data.character);
const conv = state.channelMap[data.channel.toLowerCase()];
if(conv === undefined) return core.channels.leave(data.channel);
if(char.isIgnored || core.state.hiddenUsers.indexOf(char.name) !== -1) return;
const msg = new Message(MessageType.Ad, char, decodeHTML(data.message), time);
// this is done here so that the message will be rendered correctly when cache is hit
let p: CharacterCacheRecord | undefined;
if (core.characters.ownProfile) {
p = await core.cache.profileCache.get(char.name) || undefined;
if (p) {
msg.score = p.matchScore;
}
}
EventBus.$emit('channel-ad', { message: msg, channel: conv, profile: p });
await conv.addMessage(msg);
});
connection.onMessage('RLL', async(data, time) => {
const sender = core.characters.get(data.character);
let text: string;
if(data.type === 'bottle')
text = l('chat.bottle', `[user]${data.target}[/user]`);
else {
const results = data.results.length > 1 ? `${data.results.join('+')} = ${data.endresult}` : data.endresult.toString();
text = l('chat.roll', data.rolls.join('+'), results);
}
const message = new Message(MessageType.Roll, sender, text, time);
if('channel' in data) {
const channel = (<{channel: string}>data).channel.toLowerCase();
const conversation = state.channelMap[channel];
if(conversation === undefined) return core.channels.leave(channel);
if(sender.isIgnored) return;
if(data.type === 'bottle' && data.target === core.connection.character) {
await core.notifications.notify(conversation, conversation.name, messageToString(message),
characterImage(data.character), 'attention');
if(conversation !== state.selectedConversation || !state.windowFocused)
conversation.unread = Interfaces.UnreadState.Mention;
message.isHighlight = true;
}
await conversation.addMessage(message);
} else {
if(sender.isIgnored) return;
const char = core.characters.get(
data.character === connection.character ? (<{recipient: string}>data).recipient : data.character);
if(char.isIgnored) return connection.send('IGN', {action: 'notify', character: data.character});
const conversation = state.getPrivate(char);
await conversation.addMessage(message);
}
});
connection.onMessage('NLN', async(data, time) => {
const message = new EventMessage(l('events.login', `[user]${data.identity}[/user]`), time);
if(isOfInterest(core.characters.get(data.identity))) await addEventMessage(message);
const conv = state.privateMap[data.identity.toLowerCase()];
if(conv !== undefined && (!core.state.settings.eventMessages || conv !== state.selectedConversation))
await conv.addMessage(message);
});
connection.onMessage('FLN', async(data, time) => {
const message = new EventMessage(l('events.logout', `[user]${data.character}[/user]`), time);
if(isOfInterest(core.characters.get(data.character))) await addEventMessage(message);
const conv = state.privateMap[data.character.toLowerCase()];
if(conv === undefined) return;
conv.typingStatus = 'clear';
if(!core.state.settings.eventMessages || conv !== state.selectedConversation) await conv.addMessage(message);
});
connection.onMessage('TPN', (data) => {
const conv = state.privateMap[data.character.toLowerCase()];
if(conv !== undefined) conv.typingStatus = data.status;
});
connection.onMessage('CBU', async(data, time) => {
const conv = state.channelMap[data.channel.toLowerCase()];
if(conv === undefined) return core.channels.leave(data.channel);
const text = l('events.ban', conv.name, data.character, data.operator);
conv.infoText = text;
return addEventMessage(new EventMessage(text, time));
});
connection.onMessage('CKU', async(data, time) => {
const conv = state.channelMap[data.channel.toLowerCase()];
if(conv === undefined) return core.channels.leave(data.channel);
const text = l('events.kick', conv.name, data.character, data.operator);
conv.infoText = text;
return addEventMessage(new EventMessage(text, time));
});
connection.onMessage('CTU', async(data, time) => {
const conv = state.channelMap[data.channel.toLowerCase()];
if(conv === undefined) return core.channels.leave(data.channel);
const text = l('events.timeout', conv.name, data.character, data.operator, data.length.toString());
conv.infoText = text;
return addEventMessage(new EventMessage(text, time));
});
connection.onMessage('BRO', async(data, time) => {
if(data.character !== undefined) {
const content = decodeHTML(data.message.substr(data.character.length + 24));
const message = new EventMessage(l('events.broadcast', `[user]${data.character}[/user]`, content), time);
await state.consoleTab.addMessage(message);
await core.notifications.notify(state.consoleTab, l('events.broadcast.notification', data.character), content,
characterImage(data.character), 'attention');
for(const conv of (<Conversation[]>state.channelConversations).concat(state.privateConversations))
await conv.addMessage(message);
} else return addEventMessage(new EventMessage(decodeHTML(data.message), time));
});
connection.onMessage('CIU', async(data, time) => {
const text = l('events.invite', `[user]${data.sender}[/user]`, `[session=${data.title}]${data.name}[/session]`);
return addEventMessage(new EventMessage(text, time));
});
connection.onMessage('ERR', async(data, time) => {
state.selectedConversation.errorText = data.message;
return addEventMessage(new EventMessage(`[color=red]${l('events.error', data.message)}[/color]`, time));
});
connection.onMessage('IGN', async(data, time) => {
if(data.action !== 'add' && data.action !== 'delete') return;
const text = l(`events.ignore_${data.action}`, data.character);
state.selectedConversation.infoText = text;
return addEventMessage(new EventMessage(text, time));
});
connection.onMessage('RTB', async(data, time) => {
let url = 'https://www.f-list.net/';
let text: string, character: string;
if(data.type === 'comment') { //tslint:disable-line:prefer-switch
switch(data.target_type) {
case 'newspost':
url += `newspost/${data.target_id}/#Comment${data.id}`;
break;
case 'bugreport':
url += `view_bugreport.php?id=${data.target_id}/#${data.id}`;
break;
case 'changelog':
url += `log.php?id=${data.target_id}/#${data.id}`;
break;
case 'feature':
url += `vote.php?id=${data.target_id}/#${data.id}`;
}
const key = `events.rtbComment${(data.parent_id !== 0 ? 'Reply' : '')}`;
text = l(key, `[user]${data.name}[/user]`, l(`events.rtbComment_${data.target_type}`), `[url=${url}]${data.target}[/url]`);
character = data.name;
} else if(data.type === 'note') {
text = l('events.rtb_note', `[user]${data.sender}[/user]`, `[url=${url}view_note.php?note_id=${data.id}]${data.subject}[/url]`);
character = data.sender;
} else if(data.type === 'friendrequest') {
text = l(`events.rtb_friendrequest`, `[user]${data.name}[/user]`);
character = data.name;
} else {
switch(data.type) {
case 'grouprequest':
url += 'panel/group_requests.php';
break;
case 'bugreport':
url += `view_bugreport.php?id=${data.id}`;
break;
case 'helpdeskticket':
url += `view_ticket.php?id=${data.id}`;
break;
case 'helpdeskreply':
url += `view_ticket.php?id=${data.id}`;
break;
case 'featurerequest':
url += `vote.php?fid=${data.id}`;
break;
default: //TODO
return;
}
text = l(`events.rtb_${data.type}`, `[user]${data.name}[/user]`,
data.title !== undefined ? `[url=${url}]${data.title}[/url]` : url);
character = data.name;
}
await addEventMessage(new EventMessage(text, time));
if(data.type === 'note')
await core.notifications.notify(state.consoleTab, character, text, characterImage(character), 'newnote');
});
const sfcList: Interfaces.SFCMessage[] = [];
connection.onMessage('SFC', async(data, time) => {
let text: string, message: Interfaces.Message;
if(data.action === 'report') {
text = l('events.report', `[user]${data.character}[/user]`, decodeHTML(data.tab), decodeHTML(data.report));
if(!data.old)
await core.notifications.notify(state.consoleTab, data.character, text, characterImage(data.character), 'modalert');
message = new EventMessage(text, time);
safeAddMessage(sfcList, message, 500);
(<Interfaces.SFCMessage>message).sfc = data;
} else {
text = l('events.report.confirmed', `[user]${data.moderator}[/user]`, `[user]${data.character}[/user]`);
for(const item of sfcList)
if(item.sfc.logid === data.logid) {
item.sfc.confirmed = true;
break;
}
message = new EventMessage(text, time);
}
return addEventMessage(message);
});
connection.onMessage('STA', async(data, time) => {
if(data.character === core.connection.character) {
await addEventMessage(new EventMessage(l(data.statusmsg.length > 0 ? 'events.status.ownMessage' : 'events.status.own',
l(`status.${data.status}`), decodeHTML(data.statusmsg)), time));
return;
}
const char = core.characters.get(data.character);
if(!isOfInterest(char)) return;
const status = l(`status.${data.status}`);
const key = data.statusmsg.length > 0 ? 'events.status.message' : 'events.status';
const message = new EventMessage(l(key, `[user]${data.character}[/user]`, status, decodeHTML(data.statusmsg)), time);
await addEventMessage(message);
const conv = state.privateMap[data.character.toLowerCase()];
if(conv !== undefined && (!core.state.settings.eventMessages || conv !== state.selectedConversation))
await conv.addMessage(message);
});
connection.onMessage('SYS', async(data, time) => {
state.selectedConversation.infoText = data.message;
return addEventMessage(new EventMessage(data.message, time));
});
connection.onMessage('UPT', async(data, time) => addEventMessage(new EventMessage(l('events.uptime',
data.startstring, data.channels.toString(), data.users.toString(), data.accepted.toString(), data.maxusers.toString()), time)));
connection.onMessage('ZZZ', async(data, time) => {
state.selectedConversation.infoText = data.message;
return addEventMessage(new EventMessage(data.message, time));
});
return state;
}