BetterDiscordApp-v2/client/src/builtin/EmoteModule.js

256 lines
8.9 KiB
JavaScript
Raw Normal View History

/**
* BetterDiscord Emote Module
* Copyright (c) 2015-present Jiiks/JsSucks - https://github.com/Jiiks / https://github.com/JsSucks
* All rights reserved.
* https://betterdiscord.net
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
2018-03-21 21:47:46 +01:00
import BuiltinModule from './BuiltinModule';
2018-03-22 03:13:32 +01:00
import path from 'path';
2018-08-19 19:34:32 +02:00
import { request } from 'vendor';
import { Utils, FileUtils, ClientLogger as Logger } from 'common';
2018-08-19 19:34:32 +02:00
import { DiscordApi, Settings, Globals, WebpackModules, ReactComponents, MonkeyPatch, Cache, Patcher } from 'modules';
2018-08-17 16:05:23 +02:00
import { VueInjector } from 'ui';
2018-03-21 21:47:46 +01:00
import Emote from './EmoteComponent.js';
2018-08-17 16:05:23 +02:00
import Autocomplete from '../ui/components/common/Autocomplete.vue';
import GlobalAc from '../ui/autocomplete';
const EMOTE_SOURCES = [
'https://static-cdn.jtvnw.net/emoticons/v1/:id/1.0',
'https://cdn.frankerfacez.com/emoticon/:id/1',
'https://cdn.betterttv.net/emote/:id/1x'
]
export default new class EmoteModule extends BuiltinModule {
2018-03-22 03:13:32 +01:00
get dbpath() { return path.join(Globals.getPath('data'), 'emotes.json') }
2018-08-19 19:34:32 +02:00
get database() { return this._db || (this._db = new Map()) }
2018-03-31 02:17:42 +02:00
get favourites() { return this._favourites || (this._favourites = []) }
get mostUsed() { return this._mostUsed || (this._mostUsed = []) }
get settingPath() { return ['emotes', 'default', 'enable'] }
async enabled() {
2018-08-17 16:05:23 +02:00
GlobalAc.add(';', this);
if (!this.database.size) {
await this.loadLocalDb();
2018-03-31 05:37:26 +02:00
}
2018-03-31 02:17:42 +02:00
this.patchMessageContent();
2018-08-19 19:34:32 +02:00
MonkeyPatch('BD:EMOTEMODULE', WebpackModules.getModuleByName('MessageActions')).instead('sendMessage', this.handleSendMessage.bind(this));
MonkeyPatch('BD:EMOTEMODULE', WebpackModules.getModuleByName('MessageActions')).instead('editMessage', this.handleEditMessage.bind(this));
2018-03-31 02:17:42 +02:00
}
async disabled() {
for (const patch of Patcher.getPatchesByCaller('BD:EMOTEMODULE')) patch.unpatch();
2018-08-20 00:56:57 +02:00
GlobalAc.remove(';');
}
addToMostUsed(emote) {
const isMostUsed = this.mostUsed.find(mu => mu.key === emote.name);
if (isMostUsed) {
isMostUsed.useCount += 1;
} else {
this.mostUsed.push({
key: emote.name,
useCount: 1,
value: {
src: EMOTE_SOURCES[emote.type].replace(':id', emote.id),
replaceWith: `;${emote.name};`
}
});
}
2018-03-31 02:17:42 +02:00
}
processMarkup(markup) {
const newMarkup = [];
2018-08-19 19:34:32 +02:00
if (!(markup instanceof Array)) return markup;
const jumboable = !markup.some(child => {
if (typeof child !== 'string') return false;
return / \w+/g.test(child);
});
2018-03-31 02:17:42 +02:00
for (const child of markup) {
2018-03-31 02:17:42 +02:00
if (typeof child !== 'string') {
if (typeof child === 'object') {
const isEmoji = Utils.findInReactTree(child, 'emojiName');
if (isEmoji) child.props.children.props.jumboable = jumboable;
}
newMarkup.push(child);
continue;
}
if (!/:(\w+):/g.test(child)) {
newMarkup.push(child);
continue;
}
2018-05-14 17:55:18 +02:00
const words = child.split(/([^\s]+)([\s]|$)/g).filter(f => f !== '');
let s = '';
for (const word of words) {
const isemote = /:(.*?):/g.exec(word);
if (!isemote) {
s += word;
continue;
}
const emote = this.findByName(isemote[1]);
if (!emote) {
s += word;
continue;
}
newMarkup.push(s);
s = '';
emote.jumboable = jumboable;
newMarkup.push(emote.render());
}
if (s !== '') newMarkup.push(s);
}
2018-08-19 19:34:32 +02:00
if (newMarkup.length === 1) return newMarkup[0];
return newMarkup;
}
async patchMessageContent() {
const selector = `.${WebpackModules.getClassName('container', 'containerCozy', 'containerCompact', 'edited')}`;
const MessageContent = await ReactComponents.getComponent('MessageContent', { selector });
MonkeyPatch('BD:EMOTEMODULE', MessageContent.component.prototype).after('render', this.afterRenderMessageContent.bind(this));
MessageContent.forceUpdateAll();
}
afterRenderMessageContent(component, args, retVal) {
const markup = Utils.findInReactTree(retVal, filter =>
filter &&
filter.className &&
filter.className.includes('markup') &&
filter.children.length >= 2);
2018-03-17 17:05:44 +01:00
if (!markup) return;
markup.children[1] = this.processMarkup(markup.children[1]);
}
2018-03-10 04:29:04 +01:00
2018-08-19 19:34:32 +02:00
handleEditMessage(component, args, orig) {
if (!args.length) return orig(...args);
const { content } = args[2];
if (!content) return orig(...args);
args[2].content = args[2].content.split(' ').map(word => {
const isEmote = /;(.*?);/g.exec(word);
return isEmote ? `:${isEmote[1]}:` : word;
}).join(' ');
2018-08-19 19:34:32 +02:00
return orig(...args);
}
async handleSendMessage(component, args, orig) {
if (!args.length) return orig(...args);
const { content } = args[1];
if (!content) return orig(...args);
const emoteAsImage = Settings.getSetting('emotes', 'default', 'emoteasimage').value &&
(DiscordApi.currentChannel.type === 'DM' || DiscordApi.currentChannel.checkPermissions(DiscordApi.modules.DiscordPermissions.ATTACH_FILES));
if (!emoteAsImage || content.split(' ').length > 1) {
args[1].content = args[1].content.split(' ').map(word => {
const isEmote = /;(.*?);/g.exec(word);
if (isEmote) {
const emote = this.findByName(isEmote[1], true);
if (!emote) return word;
this.addToMostUsed(emote);
return emote ? `:${isEmote[1]}:` : word;
}
return word;
2018-08-19 19:34:32 +02:00
}).join(' ');
return orig(...args);
}
const isEmote = /;(.*?);/g.exec(content);
if (!isEmote) return orig(...args);
const emote = this.findByName(isEmote[1]);
if (!emote) return orig(...args);
this.addToMostUsed(emote);
2018-08-19 19:34:32 +02:00
const FileActions = WebpackModules.getModuleByProps(['makeFile']);
const Uploader = WebpackModules.getModuleByProps(['instantBatchUpload']);
request.get(emote.props.src, { encoding: 'binary' }).then(res => {
const arr = new Uint8Array(new ArrayBuffer(res.length));
for (let i = 0; i < res.length; i++) arr[i] = res.charCodeAt(i);
const suffix = arr[0] === 71 && arr[1] === 73 && arr[2] === 70 ? '.gif' : '.png';
Uploader.upload(args[0], FileActions.makeFile(arr, `${emote.name}${suffix}`));
});
2018-03-10 04:29:04 +01:00
}
2018-03-22 03:13:32 +01:00
async loadLocalDb() {
const emotes = await FileUtils.readJsonFromFile(this.dbpath);
for (const [index, emote] of emotes.entries()) {
const { type, id, src, value } = emote;
if (index % 10000 === 0) await Utils.wait();
this.database.set(id, { id: emote.value.id || value, type });
}
}
findByName(name, simple = false) {
const emote = this.database.get(name);
if (!emote) return null;
return this.parseEmote(name, emote, simple);
}
parseEmote(name, emote, simple = false) {
const { type, id } = emote;
if (type < 0 || type > 2) return null;
return simple ? { type, id, name } : new Emote(type, id, name);
}
2018-08-19 19:34:32 +02:00
acsearch(regex) {
if (regex.length <= 0) {
return {
type: 'imagetext',
title: ['Your most used emotes'],
2018-08-19 20:07:14 +02:00
items: this.mostUsed.sort((a,b) => b.useCount - a.useCount).slice(0, 10).map(mu => {
return {
key: `${mu.key} | ${mu.useCount}`,
value: mu.value
2018-08-19 19:34:32 +02:00
}
})
2018-08-19 19:34:32 +02:00
}
}
2018-08-19 19:34:32 +02:00
const results = this.search(regex);
return {
type: 'imagetext',
title: ['Matching', regex.length],
items: results.map(result => {
result.value.src = EMOTE_SOURCES[result.value.type].replace(':id', result.value.id);
result.value.replaceWith = `;${result.key};`;
return result;
})
}
}
2018-08-17 16:05:23 +02:00
search(regex, limit = 10) {
if (typeof regex === 'string') regex = new RegExp(regex, 'i');
const matching = [];
for (const [key, value] of this.database.entries()) {
if (matching.length >= limit) break;
if (regex.test(key)) matching.push({ key, value })
}
2018-08-19 19:34:32 +02:00
return matching;
2018-08-17 16:05:23 +02:00
}
}