commit
358b1e8bf3
|
@ -13,7 +13,7 @@ import path from 'path';
|
|||
import { request } from 'vendor';
|
||||
|
||||
import { Utils, FileUtils, ClientLogger as Logger } from 'common';
|
||||
import { DiscordApi, Settings, Globals, WebpackModules, ReactComponents, MonkeyPatch, Cache, Patcher } from 'modules';
|
||||
import { DiscordApi, Settings, Globals, WebpackModules, ReactComponents, MonkeyPatch, Cache, Patcher, Database } from 'modules';
|
||||
import { VueInjector } from 'ui';
|
||||
|
||||
import Emote from './EmoteComponent.js';
|
||||
|
@ -29,33 +29,159 @@ const EMOTE_SOURCES = [
|
|||
|
||||
export default new class EmoteModule extends BuiltinModule {
|
||||
|
||||
/**
|
||||
* @returns {String} Path to local emote database
|
||||
*/
|
||||
get dbpath() { return path.join(Globals.getPath('data'), 'emotes.json') }
|
||||
|
||||
/**
|
||||
* @returns {Map} Cached raw emote database
|
||||
*/
|
||||
get database() { return this._db || (this._db = new Map()) }
|
||||
|
||||
/**
|
||||
* @returns {Array} Favourite emotes
|
||||
*/
|
||||
get favourites() { return this._favourites || (this._favourites = []) }
|
||||
|
||||
/**
|
||||
* @returns {Array} Most used emotes
|
||||
*/
|
||||
get mostUsed() { return this._mostUsed || (this._mostUsed = []) }
|
||||
|
||||
get settingPath() { return ['emotes', 'default', 'enable'] }
|
||||
|
||||
async enabled() {
|
||||
// Add ; prefix for autocomplete
|
||||
GlobalAc.add(';', this);
|
||||
|
||||
if (!this.database.size) {
|
||||
await this.loadLocalDb();
|
||||
}
|
||||
|
||||
// Read favourites and most used from database
|
||||
const userData = await Database.findOne({ 'id': 'EmoteModule' });
|
||||
if (userData) {
|
||||
if (userData.hasOwnProperty('favourites')) this._favourites = userData.favourites;
|
||||
if (userData.hasOwnProperty('mostused')) this._mostUsed = userData.mostused;
|
||||
}
|
||||
|
||||
this.patchMessageContent();
|
||||
this.patchSendAndEdit();
|
||||
}
|
||||
|
||||
async disabled() {
|
||||
// Unpatch all patches
|
||||
for (const patch of Patcher.getPatchesByCaller('BD:EMOTEMODULE')) patch.unpatch();
|
||||
// Remove ; prefix from autocomplete
|
||||
GlobalAc.remove(';');
|
||||
}
|
||||
|
||||
/**
|
||||
* Load emotes from local database
|
||||
*/
|
||||
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 });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Patches MessageContent render method
|
||||
*/
|
||||
async patchMessageContent() {
|
||||
const MessageContent = await ReactComponents.getComponent('MessageContent', { selector: WebpackModules.getSelector('container', 'containerCozy', 'containerCompact', 'edited') });
|
||||
MonkeyPatch('BD:EMOTEMODULE', MessageContent.component.prototype).after('render', this.afterRenderMessageContent.bind(this));
|
||||
MessageContent.forceUpdateAll();
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle message render
|
||||
*/
|
||||
afterRenderMessageContent(component, args, retVal) {
|
||||
const markup = Utils.findInReactTree(retVal, filter =>
|
||||
filter &&
|
||||
filter.className &&
|
||||
filter.className.includes('markup') &&
|
||||
filter.children.length >= 2);
|
||||
|
||||
if (!markup) return;
|
||||
markup.children[1] = this.processMarkup(markup.children[1]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Patches MessageActions send and edit
|
||||
*/
|
||||
patchSendAndEdit() {
|
||||
MonkeyPatch('BD:EMOTEMODULE', WebpackModules.getModuleByName('MessageActions')).instead('sendMessage', this.handleSendMessage.bind(this));
|
||||
MonkeyPatch('BD:EMOTEMODULE', WebpackModules.getModuleByName('MessageActions')).instead('editMessage', this.handleEditMessage.bind(this));
|
||||
}
|
||||
|
||||
async disabled() {
|
||||
for (const patch of Patcher.getPatchesByCaller('BD:EMOTEMODULE')) patch.unpatch();
|
||||
GlobalAc.remove(';');
|
||||
/**
|
||||
* Handle send message
|
||||
*/
|
||||
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;
|
||||
}).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);
|
||||
|
||||
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}`));
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle edit message
|
||||
*/
|
||||
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(' ');
|
||||
return orig(...args);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add/update emote to most used
|
||||
* @param {Object} emote emote to add/update
|
||||
*/
|
||||
addToMostUsed(emote) {
|
||||
const isMostUsed = this.mostUsed.find(mu => mu.key === emote.name);
|
||||
if (isMostUsed) {
|
||||
|
@ -63,15 +189,19 @@ export default new class EmoteModule extends BuiltinModule {
|
|||
} else {
|
||||
this.mostUsed.push({
|
||||
key: emote.name,
|
||||
useCount: 1,
|
||||
value: {
|
||||
src: EMOTE_SOURCES[emote.type].replace(':id', emote.id),
|
||||
replaceWith: `;${emote.name};`
|
||||
}
|
||||
id: emote.id,
|
||||
type: emote.type,
|
||||
useCount: 1
|
||||
});
|
||||
}
|
||||
// Save most used to database
|
||||
// TODO only save first n
|
||||
Database.insertOrUpdate({ 'id': 'EmoteModule' }, { 'id': 'EmoteModule', favourites: this.favourites, mostused: this.mostUsed })
|
||||
}
|
||||
|
||||
/**
|
||||
* Inject emotes into markup
|
||||
*/
|
||||
processMarkup(markup) {
|
||||
const newMarkup = [];
|
||||
if (!(markup instanceof Array)) return markup;
|
||||
|
@ -123,97 +253,35 @@ export default new class EmoteModule extends BuiltinModule {
|
|||
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);
|
||||
|
||||
if (!markup) return;
|
||||
markup.children[1] = this.processMarkup(markup.children[1]);
|
||||
}
|
||||
|
||||
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(' ');
|
||||
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;
|
||||
}).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);
|
||||
|
||||
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}`));
|
||||
});
|
||||
}
|
||||
|
||||
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 });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Find an emote by name
|
||||
* @param {String} name Emote name
|
||||
* @param {Boolean} simple Simple object or Emote instance
|
||||
* @returns {Object|Emote}
|
||||
*/
|
||||
findByName(name, simple = false) {
|
||||
const emote = this.database.get(name);
|
||||
if (!emote) return null;
|
||||
return this.parseEmote(name, emote, simple);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse emote object
|
||||
* @param {String} name Emote name
|
||||
* @param {Object} emote Emote object
|
||||
* @param {Boolean} simple Simple object or Emote instance
|
||||
* @returns {Object|Emote}
|
||||
*/
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
* Search for autocomplete
|
||||
* @param {any} regex
|
||||
*/
|
||||
acsearch(regex) {
|
||||
if (regex.length <= 0) {
|
||||
return {
|
||||
|
@ -222,7 +290,10 @@ export default new class EmoteModule extends BuiltinModule {
|
|||
items: this.mostUsed.sort((a,b) => b.useCount - a.useCount).slice(0, 10).map(mu => {
|
||||
return {
|
||||
key: `${mu.key} | ${mu.useCount}`,
|
||||
value: mu.value
|
||||
value: {
|
||||
src: EMOTE_SOURCES[mu.type].replace(':id', mu.id),
|
||||
replaceWith: `;${mu.key};`
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
@ -240,6 +311,11 @@ export default new class EmoteModule extends BuiltinModule {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Search for anything else
|
||||
* @param {any} regex
|
||||
* @param {any} limit
|
||||
*/
|
||||
search(regex, limit = 10) {
|
||||
if (typeof regex === 'string') regex = new RegExp(regex, 'i');
|
||||
const matching = [];
|
||||
|
|
|
@ -43,4 +43,19 @@ export default class {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Find first in the database
|
||||
* @param {Object} args The record to find
|
||||
* @return {Promise} null if record was not found
|
||||
*/
|
||||
static async findOne(args) {
|
||||
try {
|
||||
const find = await this.find(args);
|
||||
if (find && find.length) return find[0];
|
||||
return null;
|
||||
} catch (err) {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue