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, BroadcastMessage, 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'; import log from 'electron-log'; import isChannel = Interfaces.isChannel; 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 insertCount = 0; protected allMessages: Interfaces.Message[] = []; readonly reportMessages: Interfaces.Message[] = []; private lastSent = ''; // private loadedMore = false; adManager: AdManager; public 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 { 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; loadLastSent(): void { this.enteredText = this.lastSent; } loadMore(): boolean { if(this.messages.length >= this.allMessages.length) return false; this.maxMessages += 50; // this.loadedMore = true; this.messages = this.allMessages.slice(-this.maxMessages); EventBus.$emit('conversation-load-more', { conversation: this }); 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); // this.loadedMore = false; this.insertCount = 0; } // Keeps the message-list from re-rendering every time when full, cleaning up after itself every 200 messages stretch(): void { if ((core.conversations.selectedConversation !== this) || (this.messages.length < this.maxMessages)) { return; } if (this.insertCount < 200) { this.maxMessages += 1; this.insertCount += 1; } else { const removed = this.insertCount; this.maxMessages -= removed; this.insertCount = 0; this.messages = this.allMessages.slice(-this.maxMessages); log.debug('conversation.view.cleanup', { channel: this.name, removed, left: this.messages.length, limit: 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; protected static readonly POST_DELAY = 1250; public static async testPostDelay(): Promise { 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); } } isSendingAutomatedAds(): boolean { return this.adManager.isActive(); } toggleAutomatedAds(): void { this.adManager.isActive() ? this.adManager.stop() : this.adManager.start(); } hasAutomatedAds(): boolean { return (this.adManager.getAds().length > 0); } } 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 { await this.logPromise; this.stretch(); 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 { 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 { state.privateConversations.splice(state.privateConversations.indexOf(this), 1); state.privateConversations.splice(newIndex, 0, this); return state.savePinned(); } public async sendMessageEx(messageText: string): Promise { 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; } 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); } ); } protected async doSend(): Promise { 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(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 { await this.logPromise; this.stretch(); 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 { state.channelConversations.splice(state.channelConversations.indexOf(this), 1); state.channelConversations.splice(newIndex, 0, this); return state.savePinned(); } protected async doSend(): Promise { const isAd = this.isSendingAds; if(isAd && 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; // enforces property setter this.settings = { ...this.settings, adSettings: { ...this.settings.adSettings, lastAdTimestamp: Date.now() } }; } } ); } hasAutomatedAds(): boolean { return ((this.mode === 'both') || (this.mode === 'ads')) && super.hasAutomatedAds(); } async sendAd(text: string): Promise { if (text.length < 1) return; const initTime = Date.now(); await Conversation.conversationThroat( async() => { const throatTime = Date.now(); await Promise.all( [ await Conversation.testPostDelay(), await core.adCoordinator.requestTurnToPostAd() ] ); const delayTime = Date.now(); core.connection.send('LRP', {channel: this.channel.id, message: text}); core.cache.markLastPostTime(); log.debug( 'conversation.sendAd', { character: core.characters.ownCharacter?.name, channel: this.channel.name, throatDelta: throatTime - initTime, delayDelta: delayTime - throatTime, totalWait: delayTime - initTime, text } ); await this.addMessage( createMessage(MessageType.Ad, core.characters.ownCharacter, text, new Date()) ); this.nextAd = Date.now() + core.connection.vars.lfrp_flood * 1000; // enforces property setter this.settings = { ...this.settings, adSettings: { ...this.settings.adSettings, lastAdTimestamp: Date.now() } }; } ); } } 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 { 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; getPrivate(character: Character, noCreate: boolean = false): PrivateConversation | undefined { const key = character.name.toLowerCase(); let conv = state.privateMap[key]; if(conv !== undefined) return conv; if (noCreate) { return; } void core.cache.queueForFetching(character.name); 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 { 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 { await core.settingsStore.set('modes', this.modes); } async setSettings(key: string, value: Interfaces.Settings): Promise { 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; EventBus.$emit('select-conversation', { conversation }); } async reloadSettings(): Promise { //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 { 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; } async function withNeutralVisibilityPrivateConversation( character: Character.Character, cb: (p: PrivateConversation, c: Character.Character) => Promise ): Promise { const isVisibleConversation = !!(state.getPrivate as any)(character, true); const conv = state.getPrivate(character); await cb(conv, character); if (!isVisibleConversation) { await conv.close(); } } export async function testSmartFilterForPrivateMessage(fromChar: Character.Character, originalMessage?: Message): Promise { const cachedProfile = core.cache.profileCache.getSync(fromChar.name) || await core.cache.profileCache.get(fromChar.name); const firstTime = cachedProfile && !cachedProfile.match.autoResponded; if ( cachedProfile && cachedProfile.character.character.name !== 'YiffBot 4000' && cachedProfile.match.isFiltered && core.state.settings.risingFilter.autoReply && !cachedProfile.match.autoResponded ) { cachedProfile.match.autoResponded = true; await Conversation.conversationThroat( async() => { log.debug('filter.autoresponse', { name: fromChar.name }); await Conversation.testPostDelay(); // tslint:disable-next-line:prefer-template const message = { recipient: fromChar.name, message: '\n[sub][color=orange][b][AUTOMATED MESSAGE][/b][/color][/sub]\n' + 'Sorry, the player of this character is not interested in characters matching your profile.' + `${core.state.settings.risingFilter.hidePrivateMessages ? ' They did not see your message. To bypass this warning, send your message again.' : ''}\n` + '\n' + '🦄 Need a filter for yourself? Try out [url=https://hearmeneigh.github.io/fchat-rising/]F-Chat Rising[/url]' }; core.connection.send('PRI', message); core.cache.markLastPostTime(); if (core.state.settings.logMessages) { const logMessage = createMessage(Interfaces.Message.Type.Message, core.characters.ownCharacter, message.message, new Date()); await withNeutralVisibilityPrivateConversation( fromChar, async(p) => { // core.logs.logMessage(p, logMessage) await p.addMessage(logMessage); } ); } } ); } if ( cachedProfile && cachedProfile.character.character.name !== 'YiffBot 4000' && cachedProfile.match.isFiltered && core.state.settings.risingFilter.hidePrivateMessages && firstTime // subsequent messages bypass this filter on purpose ) { if (core.state.settings.logMessages && originalMessage && firstTime) { await withNeutralVisibilityPrivateConversation( fromChar, async(p) => core.logs.logMessage(p, originalMessage) ); } return true; } return false; } async function testSmartFilterForChannel(fromChar: Character.Character, conversation: ChannelConversation): Promise { if ( (isChannel(conversation) && conversation.channel.owner === '' && core.state.settings.risingFilter.hidePublicChannelMessages) || (isChannel(conversation) && conversation.channel.owner !== '' && core.state.settings.risingFilter.hidePrivateChannelMessages) ) { const cachedProfile = core.cache.profileCache.getSync(fromChar.name) || await core.cache.profileCache.get(fromChar.name); if (cachedProfile && cachedProfile.match.isFiltered && !fromChar.isChatOp) { return true; } } return false; } 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; EventBus.$emit('select-conversation', { conversation: state.selectedConversation }); 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); if (await testSmartFilterForPrivateMessage(char, message) === true) { return; } 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); if (await testSmartFilterForChannel(char, conversation) === true) { return; } await conversation.addMessage(message); EventBus.$emit('channel-message', { message, channel: conversation }); 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); const p = await core.cache.resolvePScore( (core.conversations.selectedConversation !== conv), char, conv, msg ); 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 char = core.characters.get(data.character); const message = new BroadcastMessage(l('events.broadcast', `[user]${data.character}[/user]`, content), char, 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 (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') { // tslint:disable-next-line:no-unsafe-any core.siteSession.interfaces.notes.incrementNotes(); 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') { // tslint:disable-next-line:no-unsafe-any core.siteSession.interfaces.notes.incrementMessages(); 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); (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; }