BetterDiscordApp-rauenzi/renderer/src/builtins/emotes/emotes.js

276 lines
12 KiB
JavaScript

import Builtin from "../../structs/builtin";
import {EmoteConfig} from "data";
import {Utilities, WebpackModules, DataStore, DiscordModules, Events, Settings, Strings} from "modules";
import BDEmote from "../../ui/emote";
import Toasts from "../../ui/toasts";
import FormattableString from "../../structs/string";
const request = require("request");
const EmoteURLs = {
TwitchGlobal: new FormattableString(`https://static-cdn.jtvnw.net/emoticons/v1/{{id}}/1.0`),
TwitchSubscriber: new FormattableString(`https://static-cdn.jtvnw.net/emoticons/v1/{{id}}/1.0`),
FrankerFaceZ: new FormattableString(`https://cdn.frankerfacez.com/emoticon/{{id}}/1`),
BTTV: new FormattableString(`https://cdn.betterttv.net/emote/{{id}}/1x`),
};
const Emotes = {
TwitchGlobal: {},
TwitchSubscriber: {},
BTTV: {},
FrankerFaceZ: {}
};
const blocklist = [];
const overrides = ["twitch", "subscriber", "bttv", "ffz"];
const modifiers = ["flip", "spin", "pulse", "spin2", "spin3", "1spin", "2spin", "3spin", "tr", "bl", "br", "shake", "shake2", "shake3", "flap"];
export default new class EmoteModule extends Builtin {
get name() {return "Emotes";}
get collection() {return "settings";}
get category() {return "general";}
get id() {return "emotes";}
get categories() {return Object.keys(Emotes).filter(k => this.isCategoryEnabled(k));}
get shouldDownload() {return Settings.get("emotes", this.category, "download");}
isCategoryEnabled(id) {return super.get("emotes", "categories", id.toLowerCase());}
get(id) {return super.get("emotes", "general", id);}
get MessageComponent() {return WebpackModules.find(m => m.default && m.default.displayName && m.default.displayName == "Message");}
get Emotes() {return Emotes;}
get TwitchGlobal() {return Emotes.TwitchGlobal;}
get TwitchSubscriber() {return Emotes.TwitchSubscriber;}
get BTTV() {return Emotes.BTTV;}
get FrankerFaceZ() {return Emotes.FrankerFaceZ;}
get blocklist() {return blocklist;}
get favorites() {return this.favoriteEmotes;}
getUrl(category, name) {return EmoteURLs[category].format({id: Emotes[category][name]});}
getCategory(category) {return Emotes[category];}
getRemoteFile(category) {return Utilities.repoUrl(`assets/emotes/${category.toLowerCase()}.json`);}
initialize() {
super.initialize();
const storedFavorites = DataStore.getBDData("favoriteEmotes");
this.favoriteEmotes = storedFavorites || {};
this.addFavorite = this.addFavorite.bind(this);
this.removeFavorite = this.removeFavorite.bind(this);
this.onCategoryToggle = this.onCategoryToggle.bind(this);
this.resetEmotes = this.resetEmotes.bind(this);
}
async enabled() {
Settings.registerCollection("emotes", "Emotes", EmoteConfig, {title: Strings.Emotes.clearEmotes, onClick: this.resetEmotes.bind(this)});
await this.getBlocklist();
await this.loadEmoteData();
Events.on("emotes-favorite-added", this.addFavorite);
Events.on("emotes-favorite-removed", this.removeFavorite);
Events.on("setting-updated", this.onCategoryToggle);
this.patchMessageContent();
}
disabled() {
Events.off("setting-updated", this.onCategoryToggle);
Events.off("emotes-favorite-added", this.addFavorite);
Events.off("emotes-favorite-removed", this.removeFavorite);
Settings.removeCollection("emotes");
this.emptyEmotes();
if (!this.cancelEmoteRender) return;
this.cancelEmoteRender();
delete this.cancelEmoteRender;
}
onCategoryToggle(collection, cat, category, enabled) {
if (collection != "emotes" || cat != "categories") return;
if (enabled) return this.loadEmoteData(category);
return this.unloadEmoteData(category);
}
addFavorite(name, url) {
if (!this.favoriteEmotes.hasOwnProperty(name)) this.favoriteEmotes[name] = url;
this.saveFavorites();
}
removeFavorite(name) {
if (!this.favoriteEmotes.hasOwnProperty(name)) return;
delete this.favoriteEmotes[name];
this.saveFavorites();
}
isFavorite(name) {
return this.favoriteEmotes.hasOwnProperty(name);
}
saveFavorites() {
DataStore.setBDData("favoriteEmotes", this.favoriteEmotes);
}
emptyEmotes() {
for (const cat in Emotes) Object.assign(Emotes, {[cat]: {}});
}
patchMessageContent() {
if (this.cancelEmoteRender) return;
this.cancelEmoteRender = this.before(this.MessageComponent, "default", (thisObj, args) => {
const nodes = args[0].childrenMessageContent.props.content;
if (!nodes || !nodes.length) return;
for (let n = 0; n < nodes.length; n++) {
const node = nodes[n];
if (typeof(node) !== "string") continue;
const words = node.split(/([^\s]+)([\s]|$)/g);
for (let c = 0, clen = this.categories.length; c < clen; c++) {
for (let w = 0, wlen = words.length; w < wlen; w++) {
const emote = words[w];
const emoteSplit = emote.split(":");
const emoteName = emoteSplit[0];
let emoteModifier = emoteSplit[1] ? emoteSplit[1] : "";
let emoteOverride = emoteModifier.slice(0);
if (emoteName.length < 4 || blocklist.includes(emoteName)) continue;
if (!modifiers.includes(emoteModifier) || !Settings.get("emotes", "general", "modifiers")) emoteModifier = "";
if (!overrides.includes(emoteOverride)) emoteOverride = "";
else emoteModifier = emoteOverride;
let current = this.categories[c];
if (emoteOverride === "twitch") {
if (Emotes.TwitchGlobal[emoteName]) current = "TwitchGlobal";
else if (Emotes.TwitchSubscriber[emoteName]) current = "TwitchSubscriber";
}
else if (emoteOverride === "subscriber") {
if (Emotes.TwitchSubscriber[emoteName]) current = "TwitchSubscriber";
}
else if (emoteOverride === "bttv") {
if (Emotes.BTTV[emoteName]) current = "BTTV";
}
else if (emoteOverride === "ffz") {
if (Emotes.FrankerFaceZ[emoteName]) current = "FrankerFaceZ";
}
if (!Emotes[current][emoteName]) continue;
const results = nodes[n].match(new RegExp(`([\\s]|^)${Utilities.escape(emoteModifier ? emoteName + ":" + emoteModifier : emoteName)}([\\s]|$)`));
if (!results) continue;
const pre = nodes[n].substring(0, results.index + results[1].length);
const post = nodes[n].substring(results.index + results[0].length - results[2].length);
nodes[n] = pre;
const emoteComponent = DiscordModules.React.createElement(BDEmote, {name: emoteName, url: EmoteURLs[current].format({id: Emotes[current][emoteName]}), modifier: emoteModifier, isFavorite: this.isFavorite(emoteName)});
nodes.splice(n + 1, 0, post);
nodes.splice(n + 1, 0, emoteComponent);
}
}
}
const onlyEmotes = nodes.every(r => {
if (typeof(r) == "string" && r.replace(/\s*/, "") == "") return true;
else if (r.type && r.type.name == "BDEmote") return true;
else if (r.props && r.props.children && r.props.children.props && r.props.children.props.emojiName) return true;
return false;
});
if (!onlyEmotes) return;
for (const node of nodes) {
if (typeof(node) != "object") continue;
if (node.type.name == "BDEmote") node.props.jumboable = true;
else if (node.props && node.props.children && node.props.children.props && node.props.children.props.emojiName) node.props.children.props.jumboable = true;
}
});
}
async getBlocklist() {
try {
const category = "Blocklist";
const exists = DataStore.emotesExist(category);
const valid = await this.isCacheValid(category);
const useCache = (valid) || (!valid && exists && !this.shouldDownload);
const list = useCache ? DataStore.getEmoteData(category) : await this.downloadEmotes(category);
blocklist.push(...list);
}
catch (err) {
// TODO: Log this
}
}
isCacheValid(category) {
return new Promise(resolve => {
const etag = DataStore.getCacheHash("emotes", category);
if (!etag) return resolve(false);
request.head({url: this.getRemoteFile(category), headers: {"If-None-Match": etag}}, (err, resp) => {
resolve(!err && resp.statusCode == 304);
});
});
}
async loadEmoteData(categories) {
if (!categories) categories = this.categories;
if (!Array.isArray(categories)) categories = [categories];
const all = Object.keys(Emotes);
categories = categories.map(k => all.find(c => c.toLowerCase() == k.toLowerCase()));
Toasts.show(Strings.Emotes.loading, {type: "info"});
this.emotesLoaded = false;
for (const category of categories) {
const exists = DataStore.emotesExist(category);
const valid = await this.isCacheValid(category);
const useCache = (valid) || (!valid && exists && !this.shouldDownload);
let data = null;
if (useCache) {
this.log(`Loading ${category} emotes from local cache.`);
const cachedData = DataStore.getEmoteData(category);
const hasData = Object.keys(cachedData).length > 0;
if (hasData) data = cachedData;
}
if (!data) data = await this.downloadEmotes(category);
Object.assign(Emotes[category], data);
await new Promise(r => setTimeout(r, 1000));
}
this.emotesLoaded = true;
Events.dispatch("emotes-loaded");
Toasts.show(Strings.Emotes.loaded, {type: "success"});
}
unloadEmoteData(categories) {
if (!categories) categories = this.categories;
if (!Array.isArray(categories)) categories = [categories];
const all = Object.keys(Emotes);
categories = categories.map(k => all.find(c => c.toLowerCase() == k.toLowerCase()));
for (const category of categories) {
delete Emotes[category];
Emotes[category] = {};
}
}
downloadEmotes(category) {
const url = this.getRemoteFile(category);
this.log(`Downloading ${category} from ${url}`);
const options = {url: url, timeout: 10000, json: true};
return new Promise(resolve => {
request.get(options, (error, response, parsedData) => {
if (error || response.statusCode != 200) {
this.stacktrace(`Could not download ${category} emotes.`, error);
return resolve({});
}
for (const emote in parsedData) {
if (emote.length < 4 || blocklist.includes(emote) || !parsedData[emote]) {
delete parsedData[emote];
continue;
}
// parsedData[emote] = EmoteURLs[category].format({id: parsedData[emote]});
}
DataStore.saveEmoteData(category, parsedData);
DataStore.setCacheHash("emotes", category, response.headers.etag);
resolve(parsedData);
this.log(`Downloaded ${category}`);
});
});
}
resetEmotes() {
const categories = Object.keys(Emotes);
this.unloadEmoteData(categories);
for (const cat of categories) DataStore.invalidateCache("emotes", cat);
this.loadEmoteData();
}
};