Advertisement
Guest User

Untitled

a guest
Sep 25th, 2019
1,763
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 188.88 KB | None | 0 0
  1. //META{"name":"MessageLoggerV2","source":"https://gitlab.com/_Lighty_/bdstuff/blob/master/MessageLoggerV2.plugin.js","website":"https://_lighty_.gitlab.io/bdstuff/?plugin=MessageLoggerV2"}*//
  2. /*@cc_on
  3. @if (@_jscript)
  4. // Offer to self-install for clueless users that try to run this directly.
  5. var shell = WScript.CreateObject("WScript.Shell");
  6. var fs = new ActiveXObject("Scripting.FileSystemObject");
  7. var pathPlugins = shell.ExpandEnvironmentStrings("%APPDATA%\\BetterDiscord\\plugins");
  8. var pathSelf = WScript.ScriptFullName;
  9. // Put the user at ease by addressing them in the first person
  10. shell.Popup("It looks like you mistakenly tried to run me directly. (don't do that!)", 0, "I'm a plugin for BetterDiscord", 0x30);
  11. if (fs.GetParentFolderName(pathSelf) === fs.GetAbsolutePathName(pathPlugins)) {
  12. shell.Popup("I'm in the correct folder already.\nJust reload Discord with Ctrl+R.", 0, "I'm already installed", 0x40);
  13. } else if (!fs.FolderExists(pathPlugins)) {
  14. shell.Popup("I can't find the BetterDiscord plugins folder.\nAre you sure it's even installed?", 0, "Can't install myself", 0x10);
  15. } else if (shell.Popup("Should I copy myself to BetterDiscord's plugins folder for you?", 0, "Do you need some help?", 0x34) === 6) {
  16. fs.CopyFile(pathSelf, fs.BuildPath(pathPlugins, fs.GetFileName(pathSelf)), true);
  17. // Show the user where to put plugins in the future
  18. shell.Exec("explorer " + pathPlugins);
  19. shell.Popup("I'm installed!\nJust reload Discord with Ctrl+R.", 0, "Successfully installed", 0x40);
  20. }
  21. WScript.Quit();
  22. @else @*/
  23. // extra TODOs:
  24. // special edited message https://i.clouds.tf/guli/mric.png
  25. // modal for checking which servers/channels/users are blacklisted/whitelisted
  26. // option to show all hidden
  27. // menu is unusable in light theme
  28. class MessageLoggerV2 {
  29. getName() {
  30. return 'MessageLoggerV2';
  31. }
  32. getVersion() {
  33. return '1.6.7';
  34. }
  35. getAuthor() {
  36. return 'Lighty';
  37. }
  38. getDescription() {
  39. return "Recode of Metalloriff's MessageLogger plugin. Records all sent messages, message edits and message deletions in the specified servers, all unmuted servers or all servers, and in direct messages.";
  40. }
  41. load() {}
  42. start() {
  43. let onLoaded = () => {
  44. try {
  45. if (!global.ZeresPluginLibrary || !(this.localUser = ZeresPluginLibrary.DiscordModules.UserStore.getCurrentUser())) setTimeout(() => onLoaded(), 1000);
  46. else this.initialize();
  47. } catch (err) {
  48. ZeresPluginLibrary.Logger.stacktrace(this.getName(), 'Failed to start!', err);
  49. ZeresPluginLibrary.Logger.err(this.getName(), `If you cannot solve this yourself, contact ${this.getAuthor()} and provide the errors shown here.`);
  50. this.stop();
  51. this.showToast(`[${this.getName()}] Failed to start! Check console (CTRL + SHIFT + I, click console tab) for more error info.`, { type: 'error', timeout: 10000 });
  52. }
  53. };
  54. const getDir = () => {
  55. // from Zeres Plugin Library, copied here as ZLib may not be available at this point
  56. const process = require('process');
  57. const path = require('path');
  58. if (process.env.injDir) return path.resolve(process.env.injDir, 'plugins/');
  59. switch (process.platform) {
  60. case 'win32':
  61. return path.resolve(process.env.appdata, 'BetterDiscord/plugins/');
  62. case 'darwin':
  63. return path.resolve(process.env.HOME, 'Library/Preferences/', 'BetterDiscord/plugins/');
  64. default:
  65. return path.resolve(process.env.XDG_CONFIG_HOME ? process.env.XDG_CONFIG_HOME : process.env.HOME + '/.config', 'BetterDiscord/plugins/');
  66. }
  67. };
  68. this.pluginDir = getDir();
  69. let libraryOutdated = false;
  70. // I'm sick and tired of people telling me my plugin doesn't work and it's cause zlib is outdated, ffs
  71. if (!global.ZLibrary || !global.ZeresPluginLibrary || (bdplugins.ZeresPluginLibrary && (libraryOutdated = ZeresPluginLibrary.PluginUpdater.defaultComparator(bdplugins.ZeresPluginLibrary.plugin._config.info.version, '1.2.6')))) {
  72. const title = libraryOutdated ? 'Library outdated' : 'Library Missing';
  73. const ModalStack = BdApi.findModuleByProps('push', 'update', 'pop', 'popWithKey');
  74. const TextElement = BdApi.findModuleByProps('Sizes', 'Weights');
  75. const ConfirmationModal = BdApi.findModule(m => m.defaultProps && m.key && m.key() == 'confirm-modal');
  76. const confirmedDownload = () => {
  77. require('request').get('https://rauenzi.github.io/BDPluginLibrary/release/0PluginLibrary.plugin.js', async (error, response, body) => {
  78. if (error) return require('electron').shell.openExternal('https://betterdiscord.net/ghdl?url=https://raw.githubusercontent.com/rauenzi/BDPluginLibrary/master/release/0PluginLibrary.plugin.js');
  79. require('fs').writeFile(require('path').join(this.pluginDir, '0PluginLibrary.plugin.js'), body, () => {
  80. setTimeout(() => {
  81. if (!global.bdplugins.ZeresPluginLibrary) return BdApi.alert('Notice', `Due to you using EnhancedDiscord instead of BetterDiscord, you'll have to reload your Discord before ${this.getName()} starts working. Just press CTRL + R to reload and ${this.getName()} will begin to work!`);
  82. onLoaded();
  83. }, 1000);
  84. });
  85. });
  86. };
  87. if (!ModalStack || !ConfirmationModal || !TextElement) {
  88. BdApi.alert('Uh oh', `Looks like you${libraryOutdated ? 'r Zeres Plugin Library was outdated!' : ' were missing Zeres Plugin Library!'} Also, failed to show a modal, so it has been ${libraryOutdated ? 'updated' : 'downloaded and loaded'} automatically.`);
  89. confirmedDownload();
  90. return;
  91. }
  92. ModalStack.push(props => {
  93. return BdApi.React.createElement(
  94. ConfirmationModal,
  95. Object.assign(
  96. {
  97. header: title,
  98. children: [TextElement({ color: TextElement.Colors.PRIMARY, children: [`The library plugin needed for ${this.getName()} is ${libraryOutdated ? 'outdated' : 'missing'}. Please click Download Now to ${libraryOutdated ? 'update' : 'install'} it.`] })],
  99. red: false,
  100. confirmText: 'Download Now',
  101. cancelText: 'Cancel',
  102. onConfirm: () => confirmedDownload()
  103. },
  104. props
  105. )
  106. );
  107. });
  108. } else onLoaded();
  109. }
  110. stop() {
  111. try {
  112. this.shutdown();
  113. } catch (err) {
  114. ZeresPluginLibrary.Logger.stacktrace(this.getName(), 'Failed to stop!', err);
  115. }
  116. }
  117. getChanges() {
  118. return [
  119. {
  120. title: 'added',
  121. type: 'added',
  122. items: ['Added data file size and severity to corruption to stats modal.']
  123. },
  124. {
  125. title: 'fixes',
  126. type: 'fixed',
  127. items: ['Fixed light theme not being supported fully.', 'Fixed stats modal not showing sent message count.', 'Fixed saved messages cap description not explaining well what it actually does.']
  128. } /* ,
  129. {
  130. title: 'Improved',
  131. type: 'improved',
  132. items: [
  133. 'Links in edited messages are darkened like normal text to differentiate between edit and current message',
  134. 'Edited message color is no longer hardcoded, it is now simply normal text color but darkened, to match theme colors.',
  135. 'Toasts no longer show for local user in DMs (you did it, why show it?).',
  136. 'Ghost ping toast no longer shows if you are in the channel.'
  137. ]
  138. } */ /* ,
  139.  
  140. {
  141. title: 'Gimme some time',
  142. type: 'progress',
  143. items: ['Expand Stats modal to show per server and per channel stats, also show disk space usage.']
  144. } */
  145. ];
  146. }
  147. initialize() {
  148. let defaultSettings = {
  149. obfuscateCSSClasses: false,
  150. autoBackup: false,
  151. dontSaveData: false,
  152. displayUpdateNotes: true,
  153. ignoreMutedGuilds: true,
  154. ignoreMutedChannels: true,
  155. ignoreBots: true,
  156. ignoreSelf: false,
  157. ignoreBlockedUsers: true,
  158. ignoreNSFW: false,
  159. ignoreLocalEdits: false,
  160. ignoreLocalDeletes: false,
  161. alwaysLogGhostPings: false,
  162. showOpenLogsButton: true,
  163. messageCacheCap: 1000,
  164. savedMessagesCap: 1000,
  165. reverseOrder: true,
  166. onlyLogWhitelist: false,
  167. whitelist: [],
  168. blacklist: [],
  169. toastToggles: {
  170. sent: false,
  171. edited: true,
  172. deleted: true,
  173. ghostPings: true
  174. },
  175. toastTogglesDMs: {
  176. sent: false,
  177. edited: true,
  178. deleted: true,
  179. ghostPings: true,
  180. disableToastsForLocal: false
  181. },
  182. blockSpamEdit: false,
  183. disableKeybind: false,
  184. cacheAllImages: true,
  185. dontDeleteCachedImages: false,
  186. aggresiveMessageCaching: true,
  187. openLogKeybind: [
  188. /* 162, 77 */
  189. ], // ctrl + m on windows
  190. openLogFilteredKeybind: [
  191. /* 162, 78 */
  192. ], // ctrl + n on windows
  193. renderCap: 50,
  194. maxShownEdits: 0,
  195. hideNewerEditsFirst: true,
  196. displayDates: true,
  197. deletedMessageColor: '',
  198. editedMessageColor: '',
  199. showEditedMessages: true,
  200. showDeletedMessages: true,
  201. showPurgedMessages: true,
  202. showDeletedCount: true,
  203. showEditedCount: true,
  204. alwaysLogSelected: true,
  205. alwaysLogDM: true,
  206. restoreDeletedMessages: true,
  207. versionInfo: ''
  208. };
  209.  
  210. this.settings = ZeresPluginLibrary.PluginUtilities.loadSettings(this.getName(), defaultSettings);
  211. let settingsChanged = false;
  212.  
  213. if (!this.settings || !Object.keys(this.settings).length) {
  214. this.showToast(`[${this.getName()}] Settings file corrupted! All settings restored to default.`, { type: 'error', timeout: 5000 });
  215. this.settings = defaultSettings; // todo: does defaultSettings get changed?
  216. settingsChanged = true;
  217. }
  218. if (!this.settings.openLogKeybind.length) {
  219. this.settings.openLogKeybind = [162, 77];
  220. settingsChanged = true;
  221. }
  222. if (!this.settings.openLogFilteredKeybind.length) {
  223. this.settings.openLogFilteredKeybind = [162, 78];
  224. settingsChanged = true;
  225. }
  226.  
  227. // force update
  228. ZeresPluginLibrary.PluginUpdater.checkForUpdate(this.getName(), this.getVersion(), 'https://_lighty_.gitlab.io/bdstuff/plugins/MessageLoggerV2.plugin.js');
  229. if (this.settings.versionInfo !== this.getVersion() && this.settings.displayUpdateNotes) {
  230. ZeresPluginLibrary.Modals.showChangelogModal(this.getName() + ' Changelog', this.getVersion(), this.getChanges());
  231. this.settings.versionInfo = this.getVersion();
  232. this.saveSettings();
  233. settingsChanged = false;
  234. }
  235.  
  236. if (settingsChanged) this.saveSettings();
  237.  
  238. this.nodeModules = {
  239. electron: require('electron'),
  240. request: require('request'),
  241. fs: require('fs')
  242. };
  243.  
  244. let defaultConstruct = () => {
  245. return Object.assign(
  246. {},
  247. {
  248. messageRecord: {},
  249. deletedMessageRecord: {},
  250. editedMessageRecord: {},
  251. purgedMessageRecord: {}
  252. }
  253. );
  254. };
  255. let data;
  256. if (this.settings.dontSaveData) {
  257. data = defaultConstruct();
  258. } else {
  259. data = this.loadData(this.getName() + 'Data', defaultConstruct());
  260. const isBad = map => !(map && map.messageRecord && map.editedMessageRecord && map.deletedMessageRecord && map.purgedMessageRecord && typeof map.messageRecord == 'object' && typeof map.editedMessageRecord == 'object' && typeof map.deletedMessageRecord == 'object' && typeof map.purgedMessageRecord == 'object');
  261. if (isBad(data)) {
  262. if (this.settings.autoBackup) {
  263. this.showToast(`[${this.getName()}] Data was corrupted! Loading backup.`, { type: 'info', timeout: 5000 });
  264. data = this.loadData(this.getName() + 'DataBackup', defaultConstruct());
  265. if (isBad(data)) {
  266. this.showToast(`[${this.getName()}] Backup was corrupted! All deleted/edited/purged messages have been erased.`, { type: 'error', timeout: 10000 });
  267. data = defaultConstruct();
  268. }
  269. } else {
  270. this.showToast(`[${this.getName()}] Data was corrupted! Recommended to turn on auto backup in settings! All deleted/edited/purged messages have been erased.`, { type: 'error', timeout: 10000 });
  271. data = defaultConstruct();
  272. }
  273. }
  274. }
  275.  
  276. const dataFileSize = this.nodeModules.fs.statSync(this.pluginDir + '/MessageLoggerV2Data.config.json').size / 1024 / 1024;
  277. // SEVERITY
  278. // 0 OK < 5MiB
  279. // 1 MILD < 10MiB
  280. // 2 DANGER < 20MiB
  281. // 3 EXTREME > 20MiB
  282. this.slowSaveModeStep = dataFileSize > 20 ? 3 : dataFileSize > 10 ? 2 : dataFileSize > 5 ? 1 : 0;
  283. ZLibrary.Logger.info(this.getName(), `Data file size is ${dataFileSize.toFixed(2)}MB`);
  284. if (this.slowSaveModeStep) ZLibrary.Logger.warn(this.getName(), 'Data file is too large, severity level', this.slowSaveModeStep);
  285.  
  286. if (!this.settings.dontSaveData) {
  287. const records = data.messageRecord;
  288. // data structure changed a wee bit, compensate instead of deleting user data or worse, erroring out
  289. for (let a in records) {
  290. const record = records[a];
  291. if (record.deletedata) {
  292. if (record.deletedata.deletetime) {
  293. record.delete_data = {};
  294. record.delete_data.time = record.deletedata.deletetime;
  295. record.delete_data.rel_ids = record.deletedata.relativeids;
  296. }
  297. delete record.deletedata;
  298. }
  299. if (record.editHistory) {
  300. record.edit_history = [];
  301. for (let b in record.editHistory) {
  302. record.edit_history.push({ content: record.editHistory[b].content, time: record.editHistory[b].editedAt });
  303. }
  304. delete record.editHistory;
  305. }
  306. this.fixEmbeds(record.message);
  307. }
  308. }
  309.  
  310. this.cachedMessageRecord = [];
  311. this.messageRecord = data.messageRecord;
  312. this.deletedMessageRecord = data.deletedMessageRecord;
  313. this.editedMessageRecord = data.editedMessageRecord;
  314. this.purgedMessageRecord = data.purgedMessageRecord;
  315. this.tempEditedMessageRecord = {};
  316. this.editHistoryAntiSpam = {};
  317. this.localDeletes = [];
  318.  
  319. defaultConstruct = () => {
  320. return Object.assign(
  321. {},
  322. {
  323. imageCacheRecord: {}
  324. }
  325. );
  326. };
  327.  
  328. this.imageCacheDir = this.pluginDir + '/MLV2_IMAGE_CACHE';
  329.  
  330. if (this.settings.cacheAllImages && !this.nodeModules.fs.existsSync(this.imageCacheDir)) this.nodeModules.fs.mkdirSync(this.imageCacheDir);
  331.  
  332. if (this.settings.dontSaveData || !this.settings.cacheAllImages) {
  333. data = defaultConstruct(); // unused but just in case my stupidity gets in the way
  334. } else {
  335. data = this.loadData('MLV2_IMAGE_CACHE/ImageCache', defaultConstruct());
  336. const isBad = map => !(map && map.imageCacheRecord && typeof map.imageCacheRecord == 'object');
  337. if (isBad(data)) {
  338. data = defaultConstruct();
  339. }
  340. }
  341.  
  342. this.imageCacheRecord = data.imageCacheRecord;
  343.  
  344. defaultConstruct = undefined;
  345.  
  346. this.tools = {
  347. openUserContextMenu: null /* NeatoLib.Modules.get('openUserContextMenu').openUserContextMenu */, // TODO: move here
  348. getMessage: ZeresPluginLibrary.DiscordModules.MessageStore.getMessage,
  349. fetchMessages: ZeresPluginLibrary.DiscordModules.MessageActions.fetchMessages,
  350. transitionTo: null /* NeatoLib.Modules.get('transitionTo').transitionTo */,
  351. getChannel: ZeresPluginLibrary.DiscordModules.ChannelStore.getChannel,
  352. copyToClipboard: this.nodeModules.electron.clipboard.writeText,
  353. getServer: ZeresPluginLibrary.DiscordModules.GuildStore.getGuild,
  354. getUser: ZeresPluginLibrary.DiscordModules.UserStore.getUser,
  355. parse: ZeresPluginLibrary.WebpackModules.getByProps('parserFor', 'parse').parse,
  356. getUserAsync: ZeresPluginLibrary.WebpackModules.getByProps('getUser', 'acceptAgreements').getUser,
  357. isBlocked: ZeresPluginLibrary.WebpackModules.getByProps('isBlocked').isBlocked,
  358. createMomentObject: ZeresPluginLibrary.WebpackModules.getByProps('createFromInputFallback'),
  359. isMentioned: ZeresPluginLibrary.WebpackModules.getByProps('isMentioned').isMentioned
  360. };
  361.  
  362. this.createButton.classes = {
  363. button: (function() {
  364. let buttonData = ZeresPluginLibrary.WebpackModules.getByProps('button', 'colorBrand');
  365. return `${buttonData.button} ${buttonData.lookFilled} ${buttonData.colorBrand} ${buttonData.sizeSmall} ${buttonData.grow}`;
  366. })(),
  367. buttonContents: ZeresPluginLibrary.WebpackModules.getByProps('button', 'colorBrand').contents
  368. };
  369.  
  370. this.createMessageGroup.classes = {
  371. containerBounded: ZeresPluginLibrary.WebpackModules.getByProps('containerCozyBounded').containerCozyBounded,
  372. message: `.${ZeresPluginLibrary.WebpackModules.getByProps('containerCozyBounded').containerCozyBounded.split(/ /g)[0]} > div`,
  373. header: ZeresPluginLibrary.WebpackModules.getByProps('containerCozyBounded').headerCozy,
  374. avatar: ZeresPluginLibrary.WebpackModules.getByProps('containerCozyBounded').avatar,
  375. headerMeta: ZeresPluginLibrary.WebpackModules.getByProps('containerCozyBounded').headerCozyMeta,
  376. username: ZeresPluginLibrary.WebpackModules.getByProps('containerCozyBounded').username,
  377. timestamp: ZeresPluginLibrary.WebpackModules.getByProps('containerCozyBounded').timestampCozy,
  378. timestampSingle: ZeresPluginLibrary.WebpackModules.getByProps('containerCozyBounded').timestampCozy.split(/ /g)[0],
  379. content: ZeresPluginLibrary.WebpackModules.getByProps('containerCozyBounded').contentCozy,
  380. avatarSingle: ZeresPluginLibrary.WebpackModules.getByProps('containerCozyBounded').avatar.split(/ /g)[0],
  381. avatarImg: ZeresPluginLibrary.WebpackModules.getByProps('avatar', 'cursorDefault', 'mask').avatar,
  382. avatarImgSingle: ZeresPluginLibrary.WebpackModules.getByProps('avatar', 'cursorDefault', 'mask').avatar.split(/ /g)[0],
  383. botTag: ZeresPluginLibrary.WebpackModules.getByProps('botTagRegular').botTagRegular + ' ' + ZeresPluginLibrary.WebpackModules.getByProps('botTagCozy').botTagCozy,
  384. markupSingle: ZeresPluginLibrary.WebpackModules.getByProps('markup').markup.split(/ /g)[0]
  385. };
  386.  
  387. this.multiClasses = {
  388. defaultColor: ZeresPluginLibrary.WebpackModules.getByProps('defaultColor').defaultColor,
  389. item: ZeresPluginLibrary.WebpackModules.find(m => m.item && m.selected && m.topPill).item,
  390. tabBarItem: ZeresPluginLibrary.DiscordClassModules.UserModal.tabBarItem,
  391. tabBarContainer: ZeresPluginLibrary.DiscordClassModules.UserModal.tabBarContainer,
  392. tabBar: ZeresPluginLibrary.DiscordClassModules.UserModal.tabBar,
  393. edited: ZeresPluginLibrary.WebpackModules.getByProps('edited').edited,
  394. markup: ZeresPluginLibrary.WebpackModules.getByProps('markup')['markup'],
  395. message: {
  396. cozy: {
  397. containerBounded: ZeresPluginLibrary.WebpackModules.getByProps('containerCozyBounded').containerCozyBounded,
  398. header: ZeresPluginLibrary.WebpackModules.getByProps('containerCozyBounded').headerCozy,
  399. avatar: ZeresPluginLibrary.WebpackModules.getByProps('containerCozyBounded').avatar,
  400. headerMeta: ZeresPluginLibrary.WebpackModules.getByProps('containerCozyBounded').headerCozyMeta,
  401. username: ZeresPluginLibrary.WebpackModules.getByProps('containerCozyBounded').username,
  402. timestamp: ZeresPluginLibrary.WebpackModules.getByProps('containerCozyBounded').timestampCozy,
  403. content: ZeresPluginLibrary.WebpackModules.getByProps('containerCozyBounded').contentCozy
  404. }
  405. }
  406. };
  407.  
  408. this.classes = {
  409. markup: ZeresPluginLibrary.WebpackModules.getByProps('markup')['markup'].split(/ /g)[0],
  410. content: ZeresPluginLibrary.WebpackModules.getByProps('contentCozy')['content'].split(/ /g)[0],
  411. hidden: ZeresPluginLibrary.WebpackModules.getByProps('spoilerText', 'hidden').hidden.split(/ /g)[0],
  412. chat: ZeresPluginLibrary.WebpackModules.getByProps('chat').chat.split(/ /g)[0],
  413. messages: `.${ZLibrary.WebpackModules.getByProps('container', 'containerCompactBounded').container.split(/ /g)[0]} > div:not(.${ZeresPluginLibrary.WebpackModules.getByProps('content', 'marginCompactIndent').content.split(/ /g)[0]})`,
  414. tabBar: ZeresPluginLibrary.DiscordClassModules.UserModal.tabBar.split(/ /g)[0],
  415. containerBounded: ZeresPluginLibrary.WebpackModules.getByProps('containerCozyBounded').containerCozyBounded,
  416. messagesWrapper: ZeresPluginLibrary.WebpackModules.getByProps('messagesWrapper').messagesWrapper.split(/ /g)[0]
  417. };
  418.  
  419. this.muteModule = ZeresPluginLibrary.WebpackModules.find(m => m.isMuted);
  420.  
  421. this.menu = {};
  422. this.menu.classes = {};
  423. this.menu.filter = '';
  424. this.menu.open = false;
  425.  
  426. this.createTextBox.classes = {
  427. inputWrapper: ZeresPluginLibrary.WebpackModules.getByProps('inputWrapper').inputWrapper,
  428. inputMultiInput: ZeresPluginLibrary.WebpackModules.getByProps('input').input + ' ' + ZeresPluginLibrary.WebpackModules.getByProps('multiInput').multiInput,
  429. multiInputFirst: ZeresPluginLibrary.WebpackModules.getByProps('multiInputFirst').multiInputFirst,
  430. inputDefaultMultiInputField: ZeresPluginLibrary.WebpackModules.getByProps('inputDefault').inputDefault + ' ' + ZeresPluginLibrary.WebpackModules.getByProps('multiInputField').multiInputField,
  431. questionMark: ZeresPluginLibrary.WebpackModules.getByProps('questionMark').questionMark,
  432. icon: ZeresPluginLibrary.WebpackModules.getByProps('questionMark').icon,
  433. focused: ZeresPluginLibrary.WebpackModules.getByProps('focused').focused.split(/ /g),
  434. questionMarkSingle: ZeresPluginLibrary.WebpackModules.getByProps('questionMark').questionMark.split(/ /g)[0]
  435. };
  436.  
  437. this.createHeader.classes = {
  438. itemTabBarItem: ZeresPluginLibrary.DiscordClassModules.UserModal.tabBarItem + ' ' + ZeresPluginLibrary.WebpackModules.find(m => m.item && m.selected && m.topPill).item,
  439. tabBarContainer: ZeresPluginLibrary.DiscordClassModules.UserModal.tabBarContainer,
  440. tabBar: ZeresPluginLibrary.DiscordClassModules.UserModal.tabBar,
  441. tabBarSingle: ZeresPluginLibrary.DiscordClassModules.UserModal.tabBar.split(/ /g)[0]
  442. };
  443.  
  444. this.createModal.imageModal = ZeresPluginLibrary.WebpackModules.getByDisplayName('ImageModal');
  445. this.createModal.confirmationModal = ZeresPluginLibrary.DiscordModules.ConfirmationModal;
  446.  
  447. this.observer.chatClass = ZeresPluginLibrary.WebpackModules.getByProps('chat').chat.split(/ /g)[0];
  448. this.observer.containerCozyClass = ZeresPluginLibrary.WebpackModules.getByProps('containerCozyBounded').containerCozyBounded.split(/ /g)[0];
  449.  
  450. this.localUser = ZeresPluginLibrary.DiscordModules.UserStore.getCurrentUser();
  451.  
  452. this.deletedChatMessagesCount = {};
  453. this.editedChatMessagesCount = {};
  454.  
  455. this.channelMessages = ZeresPluginLibrary.WebpackModules.find(m => m._channelMessages)._channelMessages;
  456.  
  457. this.autoBackupSaveInterupts = 0;
  458.  
  459. this.unpatches = [];
  460.  
  461. this.unpatches.push(ZeresPluginLibrary.Patcher.instead(this.getName(), ZeresPluginLibrary.WebpackModules.find(m => m.dispatch), 'dispatch', (_, args, original) => this.onDispatchEvent(args, original)));
  462. this.unpatches.push(
  463. ZeresPluginLibrary.Patcher.instead(this.getName(), ZeresPluginLibrary.DiscordModules.MessageActions, 'startEditMessage', (_, args, original) => {
  464. const channelId = args[0];
  465. const messageId = args[1];
  466. if (!this.editedMessageRecord[channelId] || this.editedMessageRecord[channelId].findIndex(m => m === messageId) == -1) return original(...args);
  467. let record = this.getSavedMessage(messageId);
  468. if (!record) return original(...args);
  469. args[2] = record.message.content;
  470. return original(...args);
  471. })
  472. );
  473.  
  474. // todo: maybe do it in dispatch? or use 'instead'
  475. this.unpatches.push(
  476. ZeresPluginLibrary.Patcher.after(this.getName(), ZeresPluginLibrary.DiscordModules.MessageActions, 'endEditMessage', (_, args, __) => {
  477. if (!args.length) setTimeout(() => this.updateMessages(), 0); // settimeout so it can do it after it was restored
  478. })
  479. );
  480.  
  481. this.style = {};
  482.  
  483. this.style.deleted = this.obfuscatedClass('ml2-deleted');
  484. this.style.edited = this.obfuscatedClass('ml2-edited');
  485. this.style.markupCompact = this.obfuscatedClass('ml2-markup-compact');
  486. this.style.editedCompact = this.obfuscatedClass('ml2-edited-compact');
  487. this.style.tab = this.obfuscatedClass('ml2-tab');
  488. this.style.tabSelected = this.obfuscatedClass('ml2-tab-selected');
  489. this.style.textIndent = this.obfuscatedClass('ml2-help-text-indent');
  490. this.style.menu = this.obfuscatedClass('ML2-MENU');
  491. this.style.openLogs = this.obfuscatedClass('ML2-OL');
  492. this.style.filter = this.obfuscatedClass('ML2-FILTER');
  493. this.style.menuMessages = this.obfuscatedClass('ML2-MENU-MESSAGES');
  494. this.style.menuTabBar = this.obfuscatedClass('ML2-MENU-TABBAR');
  495.  
  496. this.invalidateAllChannelCache();
  497. this.selectedChannel = this.getSelectedTextChannel();
  498. if (this.selectedChannel) {
  499. this.cacheChannelMessages(this.selectedChannel.id);
  500. /* setTimeout(() => */ this.updateMessages(); /* , 20); */ // why is this in a settimeout? TODO find out
  501. }
  502.  
  503. // todo: custom deleted message text color
  504. ZeresPluginLibrary.PluginUtilities.addStyle(
  505. (this.style.css = !this.settings.obfuscateCSSClasses ? 'ML2-CSS' : this.randomString()),
  506. `
  507. html > head + body #app-mount * .bda-links [href*="MessageLogger"]:after{
  508. display: none !important;
  509. }
  510. .da-container .da-markup, .da-container .${this.classes.markup} {
  511. /* transition: color 0.3s; */
  512. }
  513. .${this.style.deleted} .${this.classes.markup}, .${this.style.deleted} .${this.classes.markup} .hljs{
  514. color: #f04747 !important;
  515. }
  516. .theme-dark .${this.classes.markup}.${this.style.edited} .${this.style.edited} {
  517. filter: brightness(70%);
  518. }
  519. .theme-light .${this.classes.markup}.${this.style.edited} .${this.style.edited} {
  520. opacity: 0.5;
  521. }
  522. div > .${ZLibrary.WebpackModules.getByProps('buttonContainer', 'asianCompactTimeStamp')
  523. .buttonContainer.split(' ')
  524. .join('.')} {
  525. z-index: 9999999;
  526. }
  527.  
  528. .${this.style.markupCompact} {
  529. display: grid;
  530. }
  531. .${this.style.editedCompact} {
  532. grid-column: 1;
  533. grid-row: 1;
  534. }
  535.  
  536. .theme-dark .${this.style.deleted}:not(:hover) .${this.classes.content} img, .${this.style.deleted}:not(:hover) .mention, .${this.style.deleted}:not(:hover) .reactions, .${this.style.deleted}:not(:hover) .${this.classes.content} a {
  537. filter: grayscale(100%) !important;
  538. }
  539.  
  540. .${this.style.deleted} .${this.classes.content} img, .${this.style.deleted} .mention, .${this.style.deleted} .reactions, .${this.style.deleted} .${this.classes.content} a {
  541. transition: filter 0.3s !important;
  542. }
  543.  
  544. .theme-dark .${this.style.tab} {
  545. border-color: transparent;
  546. color: rgba(255, 255, 255, 0.4);
  547. }
  548. .theme-light .${this.style.tab} {
  549. border-color: transparent;
  550. color: rgba(0, 0, 0, 0.4);
  551. }
  552.  
  553. .theme-dark .${this.style.tabSelected} {
  554. border-color: rgb(255, 255, 255);
  555. color: rgb(255, 255, 255);
  556. }
  557. .theme-light .${this.style.tabSelected} {
  558. border-color: rgb(0, 0, 0);
  559. color: rgb(0, 0, 0);
  560. }
  561.  
  562. .${this.style.textIndent} {
  563. margin-left: 40px;
  564. }
  565. `
  566. );
  567. this.patchModal();
  568.  
  569. const createKeybindListener = () => {
  570. this.keybindListener = new (ZeresPluginLibrary.WebpackModules.getModule(m => typeof m === 'function' && m.toString().includes('.default.setOnInputEventCallback')))();
  571. this.keybindListener.on('change', e => {
  572. if (this.settings.disableKeybind) return; // todo: destroy if disableKeybind is set to true and don't make one if it was true from the start
  573. // this is the hackiest thing ever but it works xdd
  574. if (!ZeresPluginLibrary.WebpackModules.getByProps('isFocused').isFocused() || document.getElementsByClassName('bda-slist').length) return;
  575. const isKeyBind = keybind => {
  576. if (e.combo.length != keybind.length) return false;
  577. // console.log(e.combo);
  578. for (let i = 0; i < e.combo.length; i++) {
  579. if (e.combo[i][1] != keybind[i]) {
  580. return false;
  581. }
  582. }
  583. return true;
  584. };
  585. const close = () => {
  586. this.menu.filter = '';
  587. this.menu.open = false;
  588. ZeresPluginLibrary.DiscordModules.ModalStack.popWithKey(this.style.menu);
  589. };
  590. if (isKeyBind(this.settings.openLogKeybind)) {
  591. if (this.menu.open) return close();
  592. return this.openWindow();
  593. }
  594. if (isKeyBind(this.settings.openLogFilteredKeybind)) {
  595. if (this.menu.open) return close();
  596. if (!this.selectedChannel) {
  597. this.showToast('No channel selected', { type: 'error' });
  598. return this.openWindow();
  599. }
  600. this.menu.filter = `channel:${this.selectedChannel.id}`;
  601. this.openWindow();
  602. }
  603. });
  604. };
  605.  
  606. this.powerMonitor = ZeresPluginLibrary.WebpackModules.getByProps('remotePowerMonitor').remotePowerMonitor;
  607.  
  608. const refreshKeykindListener = () => {
  609. this.keybindListener.destroy();
  610. createKeybindListener();
  611. };
  612.  
  613. this.keybindListenerInterval = setInterval(() => refreshKeykindListener(), 30 * 1000 * 60); // 10 minutes
  614.  
  615. createKeybindListener();
  616.  
  617. this.powerMonitor.on(
  618. 'resume',
  619. (this.powerMonitorResumeListener = () => {
  620. setTimeout(() => refreshKeykindListener(), 1000);
  621. })
  622. );
  623.  
  624. this.unpatches.push(
  625. ZeresPluginLibrary.Patcher.instead(this.getName(), ZeresPluginLibrary.WebpackModules.getByDisplayName('TextAreaAutosize').prototype, 'focus', (thisObj, args, original) => {
  626. if (this.menu.open) return;
  627. return original(...args);
  628. })
  629. );
  630.  
  631. this.unpatches.push(
  632. ZeresPluginLibrary.Patcher.instead(this.getName(), ZeresPluginLibrary.WebpackModules.getByDisplayName('LazyImage').prototype, 'getSrc', (thisObj, args, original) => {
  633. let indx;
  634. if (((indx = thisObj.props.src.indexOf('?ML2=true')), indx !== -1)) return thisObj.props.src.substr(0, indx);
  635. return original(...args);
  636. })
  637. );
  638.  
  639. this.dataManagerInterval = setInterval(() => {
  640. this.handleMessagesCap();
  641. }, 60 * 1000 * 5); // every 5 minutes, no need to spam it, could be intensive
  642.  
  643. const ChannelContextMenu = ZeresPluginLibrary.WebpackModules.getByDisplayName('ChannelContextMenu');
  644. const GuildContextMenu = ZeresPluginLibrary.WebpackModules.getByDisplayName('GuildContextMenu');
  645. const MessageContextMenu = ZeresPluginLibrary.WebpackModules.getByDisplayName('MessageContextMenu');
  646. const NativeLinkGroup = ZeresPluginLibrary.WebpackModules.getByDisplayName('NativeLinkGroup');
  647. const UserContextMenu = ZeresPluginLibrary.WebpackModules.getByDisplayName('UserContextMenu');
  648.  
  649. this.ContextMenuItem = ZeresPluginLibrary.DiscordModules.ContextMenuItem;
  650. this.ContextMenuActions = ZeresPluginLibrary.DiscordModules.ContextMenuActions;
  651. this.ContextMenuGroup = ZeresPluginLibrary.DiscordModules.ContextMenuItemsGroup;
  652. this.SubMenuItem = ZeresPluginLibrary.WebpackModules.find(m => m.default && m.default.displayName && m.default.displayName.includes('SubMenuItem')).default;
  653.  
  654. const patchContexts = menu => {
  655. this.unpatches.push(ZeresPluginLibrary.Patcher.after(this.getName(), menu.prototype, 'render', (thisObj, args, returnValue) => this.handleContextMenu(thisObj, args, returnValue)));
  656. };
  657.  
  658. for (let menu of [ChannelContextMenu, GuildContextMenu, MessageContextMenu, NativeLinkGroup, UserContextMenu]) patchContexts(menu);
  659.  
  660. this.menu.randomValidChannel = (() => {
  661. const channels = ZeresPluginLibrary.DiscordModules.ChannelStore.getChannels();
  662. var keys = Object.keys(channels);
  663. return channels[keys[(keys.length * Math.random()) << 0]];
  664. })();
  665.  
  666. this.menu.userRequestQueue = [];
  667.  
  668. this.menu.deleteKeyDown = false;
  669. document.addEventListener(
  670. 'keydown',
  671. (this.keydownListener = e => {
  672. if (e.repeat) return;
  673. if (e.keyCode === 46) this.menu.deleteKeyDown = true;
  674. })
  675. );
  676. document.addEventListener(
  677. 'keyup',
  678. (this.keyupListener = e => {
  679. if (e.repeat) return;
  680. if (e.keyCode === 46) this.menu.deleteKeyDown = false;
  681. })
  682. );
  683.  
  684. this.menu.shownMessages = -1;
  685. const iconShit = ZeresPluginLibrary.WebpackModules.getByProps('container', 'children', 'toolbar', 'iconWrapper');
  686. // Icon by font awesome
  687. // https://fontawesome.com/license
  688. this.channelLogButton = this.parseHTML(`<div tabindex="0" class="${iconShit.iconWrapper} ${iconShit.clickable}" id="${this.style.openLogs}" role="button" aria-label="Open Logs">
  689. <svg aria-hidden="true" class="${iconShit.icon}" name="Open Logs" viewBox="0 0 576 512">
  690. <path fill="currentColor" d="M218.17 424.14c-2.95-5.92-8.09-6.52-10.17-6.52s-7.22.59-10.02 6.19l-7.67 15.34c-6.37 12.78-25.03 11.37-29.48-2.09L144 386.59l-10.61 31.88c-5.89 17.66-22.38 29.53-41 29.53H80c-8.84 0-16-7.16-16-16s7.16-16 16-16h12.39c4.83 0 9.11-3.08 10.64-7.66l18.19-54.64c3.3-9.81 12.44-16.41 22.78-16.41s19.48 6.59 22.77 16.41l13.88 41.64c19.75-16.19 54.06-9.7 66 14.16 1.89 3.78 5.49 5.95 9.36 6.26v-82.12l128-127.09V160H248c-13.2 0-24-10.8-24-24V0H24C10.7 0 0 10.7 0 24v464c0 13.3 10.7 24 24 24h336c13.3 0 24-10.7 24-24v-40l-128-.11c-16.12-.31-30.58-9.28-37.83-23.75zM384 121.9c0-6.3-2.5-12.4-7-16.9L279.1 7c-4.5-4.5-10.6-7-17-7H256v128h128v-6.1zm-96 225.06V416h68.99l161.68-162.78-67.88-67.88L288 346.96zm280.54-179.63l-31.87-31.87c-9.94-9.94-26.07-9.94-36.01 0l-27.25 27.25 67.88 67.88 27.25-27.25c9.95-9.94 9.95-26.07 0-36.01z"/>
  691. </svg>
  692. </div>`);
  693. this.channelLogButton.addEventListener('click', () => {
  694. this.openWindow();
  695. });
  696. this.channelLogButton.addEventListener('contextmenu', () => {
  697. if (!this.selectedChannel) return;
  698. this.menu.filter = `channel:${this.selectedChannel.id}`;
  699. this.openWindow();
  700. });
  701. new ZeresPluginLibrary.EmulatedTooltip(this.channelLogButton, 'Open Logs', { side: 'bottom' });
  702.  
  703. if (this.settings.showOpenLogsButton) this.addOpenLogsButton();
  704.  
  705. this.unpatches.push(
  706. ZeresPluginLibrary.Patcher.instead(this.getName(), ZeresPluginLibrary.DiscordModules.MessageActions, 'deleteMessage', (thisObj, args, original) => {
  707. this.localDeletes.push(args[1]);
  708. if (this.localDeletes.length > 10) this.localDeletes.shift();
  709. return original(...args);
  710. })
  711. );
  712.  
  713. this.selfTestInterval = setInterval(() => {
  714. this.selfTestTimeout = setTimeout(() => {
  715. if (this.selfTestFailures > 4) {
  716. clearInterval(this.selfTestInterval);
  717. this.selfTestInterval = 0;
  718. return BdApi.alert(`${this.getName()}: internal error.`, `Self test failure: Failed to hook dispatch. Recommended to reload your discord (CTRL + R) as the plugin may be in a broken state! If you still see this error, open up the devtools console (CTRL + SHIFT + I, click console tab) and report the errors to ${this.getAuthor()} for further assistance.`);
  719. }
  720. ZeresPluginLibrary.Logger.warn(this.getName(), 'Dispatch is not hooked, all our hooks may be invalid, attempting to reload self');
  721. this.selfTestFailures++;
  722. this.stop();
  723. this.start();
  724. }, 3000);
  725. ZeresPluginLibrary.WebpackModules.find(m => m.dispatch).dispatch({
  726. type: 'MESSAGE_LOGGER_V2_SELF_TEST'
  727. });
  728. }, 10000);
  729.  
  730. if (this.selfTestInited) return;
  731. this.selfTestFailures = 0;
  732. this.selfTestInited = true;
  733. }
  734. shutdown() {
  735. const tryUnpatch = fn => {
  736. try {
  737. // things can bug out, best to reload tbh, should maybe warn the user?
  738. fn();
  739. } catch (e) {
  740. ZeresPluginLibrary.Logger.stacktrace(this.getName(), 'Error unpatching', e);
  741. }
  742. };
  743. for (let unpatch of this.unpatches) tryUnpatch(unpatch);
  744. if (this.MessageContextMenuPatch) tryUnpatch(this.MessageContextMenuPatch);
  745. if (this.ChannelContextMenuPatch) tryUnpatch(this.ChannelContextMenuPatch);
  746. if (this.GuildContextMenuPatch) tryUnpatch(this.GuildContextMenuPatch);
  747. if (this.keybindListener) this.keybindListener.destroy();
  748. if (this.style && this.style.css) ZeresPluginLibrary.PluginUtilities.removeStyle(this.style.css);
  749. if (this.dataManagerInterval) clearInterval(this.dataManagerInterval);
  750. if (this.keybindListenerInterval) clearInterval(this.keybindListenerInterval);
  751. if (this.selfTestInterval) clearInterval(this.selfTestInterval);
  752. if (this.selfTestTimeout) clearTimeout(this.selfTestTimeout);
  753. if (this.keydownListener) document.removeEventListener('keydown', this.keydownListener);
  754. if (this.keyupListener) document.removeEventListener('keyup', this.keyupListener);
  755. if (this.powerMonitor) this.powerMonitor.removeListener('resume', this.powerMonitorResumeListener);
  756. if (this.channelLogButton) this.channelLogButton.remove();
  757. // console.log('invalidating cache');
  758. this.invalidateAllChannelCache();
  759. // if (this.selectedChannel) this.cacheChannelMessages(this.selectedChannel.id); // bad idea?
  760. }
  761. observer({ addedNodes }) {
  762. // from NoDeleteMessages by Mega_Mewthree (original), ShiiroSan (edit logging)
  763. let isChat = false;
  764. for (const change of addedNodes) {
  765. if ((isChat = typeof change.className === 'string' && change.className.indexOf(this.observer.chatClass) != -1) || (change.style && change.style.cssText === 'border-radius: 2px; background-color: rgba(114, 137, 218, 0);') || (typeof change.className === 'string' && change.className.indexOf(this.observer.containerCozyClass) !== -1)) {
  766. try {
  767. if (isChat) this.selectedChannel = this.getSelectedTextChannel();
  768. if (!this.selectedChannel) return ZeresPluginLibrary.Logger.warn(this.getName(), 'Chat was loaded but no text channel is selected');
  769. if (isChat && this.settings.showOpenLogsButton) {
  770. const srch = change.querySelector('div[class*="search-"]');
  771. srch.parentElement.insertBefore(this.channelLogButton, srch);
  772. }
  773. this.updateMessages();
  774. const showStuff = (map, name) => {
  775. if (map[this.selectedChannel.id] && map[this.selectedChannel.id]) {
  776. this.showToast(`There are ${map[this.selectedChannel.id]} new ${name} messages in ${this.selectedChannel.name ? '#' + this.selectedChannel.name : 'DM'}`, {
  777. type: 'info',
  778. onClick: () => this.openWindow(name),
  779. timeout: 3000
  780. });
  781. map[this.selectedChannel.id] = 0;
  782. }
  783. };
  784. if (this.settings.showDeletedCount) showStuff(this.deletedChatMessagesCount, 'deleted');
  785. if (this.settings.showEditedCount) showStuff(this.editedChatMessagesCount, 'edited');
  786. } catch (e) {
  787. ZeresPluginLibrary.Logger.stacktrace(this.getName(), 'Error in observer', e);
  788. }
  789. break;
  790. }
  791. }
  792. }
  793. buildSetting(data) {
  794. // compied from ZLib actually
  795. const { name, note, type, value, onChange, id } = data;
  796. let setting = null;
  797. if (type == 'color') {
  798. setting = new ZeresPluginLibrary.Settings.ColorPicker(name, note, value, onChange, { disabled: data.disabled }); // DOESN'T WORK, REEEEEEEEEE
  799. } else if (type == 'dropdown') {
  800. setting = new ZeresPluginLibrary.Settings.Dropdown(name, note, value, data.options, onChange);
  801. } else if (type == 'file') {
  802. setting = new ZeresPluginLibrary.Settings.FilePicker(name, note, onChange);
  803. } else if (type == 'keybind') {
  804. setting = new ZeresPluginLibrary.Settings.Keybind(name, note, value, onChange);
  805. } else if (type == 'radio') {
  806. setting = new ZeresPluginLibrary.Settings.RadioGroup(name, note, value, data.options, onChange, { disabled: data.disabled });
  807. } else if (type == 'slider') {
  808. const options = {};
  809. if (typeof data.markers != 'undefined') options.markers = data.markers;
  810. if (typeof data.stickToMarkers != 'undefined') options.stickToMarkers = data.stickToMarkers;
  811. setting = new ZeresPluginLibrary.Settings.Slider(name, note, data.min, data.max, value, onChange, options);
  812. } else if (type == 'switch') {
  813. setting = new ZeresPluginLibrary.Settings.Switch(name, note, value, onChange, { disabled: data.disabled });
  814. } else if (type == 'textbox') {
  815. setting = new ZeresPluginLibrary.Settings.Textbox(name, note, value, onChange, { placeholder: data.placeholder || '' });
  816. }
  817. if (id) setting.getElement().id = this.obfuscatedClass(id);
  818. return setting;
  819. }
  820. createSetting(data) {
  821. const current = Object.assign({}, data);
  822. if (!current.onChange) {
  823. current.onChange = value => {
  824. this.settings[current.id] = value;
  825. if (current.callback) current.callback(value);
  826. };
  827. }
  828. if (typeof current.value === 'undefined') current.value = this.settings[current.id];
  829. return this.buildSetting(current);
  830. }
  831. createGroup(group) {
  832. const { name, id, collapsible, shown, settings } = group;
  833.  
  834. const list = [];
  835. for (let s = 0; s < settings.length; s++) list.push(this.createSetting(settings[s]));
  836.  
  837. const settingGroup = new ZeresPluginLibrary.Settings.SettingGroup(name, { shown, collapsible }).append(...list);
  838. settingGroup.group.id = id; // should generate the id in here instead?
  839. return settingGroup;
  840. }
  841. getSettingsPanel() {
  842. // todo, sort out the menu
  843. const list = [];
  844. list.push(
  845. this.createGroup({
  846. name: 'Keybinds',
  847. id: this.obfuscatedClass('ml2-settings-keybinds'),
  848. collapsible: true,
  849. shown: false,
  850. settings: [
  851. {
  852. name: 'Open menu keybind',
  853. id: 'openLogKeybind',
  854. type: 'keybind'
  855. },
  856. {
  857. name: 'Open log filtered by selected channel',
  858. id: 'openLogFilteredKeybind',
  859. type: 'keybind'
  860. },
  861. {
  862. name: 'Disable keybinds',
  863. id: 'disableKeybind',
  864. type: 'switch'
  865. }
  866. ]
  867. })
  868. );
  869. list.push(
  870. this.createGroup({
  871. name: 'Ignores and overrides',
  872. id: this.obfuscatedClass('ml2-settings-ignores-overrides'),
  873. collapsible: true,
  874. shown: false,
  875. settings: [
  876. {
  877. name: 'Ignore muted servers',
  878. id: 'ignoreMutedGuilds',
  879. type: 'switch'
  880. },
  881. {
  882. name: 'Ignore muted channels',
  883. id: 'ignoreMutedChannels',
  884. type: 'switch'
  885. },
  886. {
  887. name: 'Ignore bots',
  888. id: 'ignoreBots',
  889. type: 'switch'
  890. },
  891. {
  892. name: 'Ignore messages posted by you',
  893. id: 'ignoreSelf',
  894. type: 'switch'
  895. },
  896. {
  897. name: 'Ignore message edits from you',
  898. id: 'ignoreLocalEdits',
  899. type: 'switch'
  900. },
  901. {
  902. name: 'Ignore message deletes from you',
  903. note: 'Only ignores if you delete your own message.',
  904. id: 'ignoreLocalDeletes',
  905. type: 'switch'
  906. },
  907. {
  908. name: 'Ignore blocked users',
  909. id: 'ignoreBlockedUsers',
  910. type: 'switch'
  911. },
  912. {
  913. name: 'Ignore NSFW channels',
  914. id: 'ignoreNSFW',
  915. type: 'switch'
  916. },
  917. {
  918. name: 'Only log whitelist',
  919. id: 'onlyLogWhitelist',
  920. type: 'switch'
  921. },
  922. {
  923. name: 'Always log selected channel, regardless of whitelist/blacklist',
  924. id: 'alwaysLogSelected',
  925. type: 'switch'
  926. },
  927. {
  928. name: 'Always log DMs, regardless of whitelist/blacklist',
  929. id: 'alwaysLogDM',
  930. type: 'switch'
  931. },
  932. {
  933. name: 'Always log ghost pings, regardless of whitelist/blacklist',
  934. note: 'Messages sent in ignored/muted/blacklisted servers and channels will be logged and shown in sent, but only gets saved if a ghost ping occurs.',
  935. id: 'alwaysLogGhostPings',
  936. type: 'switch'
  937. }
  938. ]
  939. })
  940. );
  941. list.push(
  942. this.createGroup({
  943. name: 'Display settings',
  944. id: this.obfuscatedClass('ml2-settings-display'),
  945. collapsible: true,
  946. shown: false,
  947. settings: [
  948. {
  949. name: 'Display dates with timestamps',
  950. id: 'displayDates',
  951. type: 'switch',
  952. callback: () => {
  953. if (this.selectedChannel) {
  954. // change NOW
  955. this.invalidateAllChannelCache();
  956. this.cacheChannelMessages(this.selectedChannel.id);
  957. }
  958. }
  959. },
  960. {
  961. name: 'Display deleted messages in chat',
  962. id: 'showDeletedMessages',
  963. type: 'switch',
  964. callback: () => {
  965. this.invalidateAllChannelCache();
  966. if (this.selectedChannel) this.cacheChannelMessages(this.selectedChannel.id);
  967. }
  968. },
  969. {
  970. name: 'Display edited messages in chat',
  971. id: 'showEditedMessages',
  972. type: 'switch'
  973. },
  974. {
  975. name: 'Max number of shown edits',
  976. id: 'maxShownEdits',
  977. type: 'textbox',
  978. onChange: val => {
  979. if (isNaN(val)) return this.showToast('Value must be a number!', { type: 'error' });
  980. this.settings.maxShownEdits = parseInt(val);
  981. }
  982. },
  983. {
  984. name: 'Show oldest edit instead of newest if over the shown edits limit',
  985. id: 'hideNewerEditsFirst',
  986. type: 'switch'
  987. },
  988. {
  989. name: 'Display purged messages in chat',
  990. id: 'showPurgedMessages',
  991. type: 'switch',
  992. callback: () => {
  993. this.invalidateAllChannelCache();
  994. if (this.selectedChannel) this.cacheChannelMessages(this.selectedChannel.id);
  995. }
  996. },
  997. {
  998. name: 'Restore deleted messages after reload',
  999. id: 'restoreDeletedMessages',
  1000. type: 'switch',
  1001. callback: val => {
  1002. if (val) {
  1003. this.invalidateAllChannelCache();
  1004. if (this.selectedChannel) this.cacheChannelMessages(this.selectedChannel.id);
  1005. }
  1006. }
  1007. },
  1008. {
  1009. name: 'Show amount of new deleted messages when entering a channel',
  1010. id: 'showDeletedCount',
  1011. type: 'switch'
  1012. },
  1013. {
  1014. name: 'Show amount of new edited messages when entering a channel',
  1015. id: 'showEditedCount',
  1016. type: 'switch'
  1017. },
  1018. {
  1019. name: 'Display update notes',
  1020. id: 'displayUpdateNotes',
  1021. type: 'switch'
  1022. },
  1023. {
  1024. name: 'Menu sort direction',
  1025. id: 'reverseOrder',
  1026. type: 'radio',
  1027. options: [
  1028. {
  1029. name: 'New - old',
  1030. value: false
  1031. },
  1032. {
  1033. name: 'Old - new',
  1034. value: true
  1035. }
  1036. ]
  1037. }
  1038. ]
  1039. })
  1040. );
  1041. list.push(
  1042. this.createGroup({
  1043. name: 'Misc settings',
  1044. id: this.obfuscatedClass('ml2-settings-misc'),
  1045. collapsible: true,
  1046. shown: false,
  1047. settings: [
  1048. {
  1049. name: 'Disable saving data. Logged messages are erased after reload/restart. Disables auto backup.',
  1050. id: 'dontSaveData',
  1051. type: 'switch',
  1052. callback: val => {
  1053. if (!val) this.saveData();
  1054. if (!val && this.settings.autoBackup) this.saveBackup();
  1055. }
  1056. },
  1057. {
  1058. name: "Auto backup data (won't fully prevent losing data, just prevent total data loss)",
  1059. id: 'autoBackup',
  1060. type: 'switch',
  1061. callback: val => {
  1062. if (val && !this.settings.dontSaveData) this.saveBackup();
  1063. }
  1064. } /*
  1065. {
  1066. // no time, TODO!
  1067. name: 'Deleted messages color',
  1068. id: 'deletedMessageColor',
  1069. type: 'color'
  1070. }, */,
  1071. {
  1072. name: 'Aggresive message caching (makes sure we have the data of any deleted or edited messages)',
  1073. id: 'aggresiveMessageCaching',
  1074. type: 'switch'
  1075. },
  1076. {
  1077. name: 'Cache all images by storing them locally in the MLV2_IMAGE_CACHE folder inside the plugins folder',
  1078. id: 'cacheAllImages',
  1079. type: 'switch'
  1080. },
  1081. {
  1082. name: "Don't delete cached images",
  1083. note: "If the message the image is from is erased from data, the cached image will be kept. You'll have to monitor disk usage on your own!",
  1084. id: 'dontDeleteCachedImages',
  1085. type: 'switch'
  1086. },
  1087. {
  1088. name: 'Display open logs button next to the search box top right in channels',
  1089. id: 'showOpenLogsButton',
  1090. type: 'switch',
  1091. callback: val => {
  1092. if (val) return this.addOpenLogsButton();
  1093. this.removeOpenLogsButton();
  1094. }
  1095. },
  1096. {
  1097. name: 'Block spam edit notifications (if enabled)',
  1098. id: 'blockSpamEdit',
  1099. type: 'switch'
  1100. }
  1101. ]
  1102. })
  1103. );
  1104. list.push(
  1105. this.createGroup({
  1106. name: 'Toast notifications for guilds',
  1107. id: this.obfuscatedClass('ml2-settings-toast-guilds'),
  1108. collapsible: true,
  1109. shown: false,
  1110. settings: [
  1111. {
  1112. name: 'Message sent',
  1113. id: 'sent',
  1114. type: 'switch',
  1115. value: this.settings.toastToggles.sent,
  1116. onChange: val => {
  1117. this.settings.toastToggles.sent = val;
  1118. }
  1119. },
  1120. {
  1121. name: 'Message edited',
  1122. id: 'edited',
  1123. type: 'switch',
  1124. value: this.settings.toastToggles.edited,
  1125. onChange: val => {
  1126. this.settings.toastToggles.edited = val;
  1127. }
  1128. },
  1129. {
  1130. name: 'Message deleted',
  1131. id: 'deleted',
  1132. type: 'switch',
  1133. value: this.settings.toastToggles.deleted,
  1134. onChange: val => {
  1135. this.settings.toastToggles.deleted = val;
  1136. }
  1137. },
  1138. {
  1139. name: 'Ghost pings',
  1140. id: 'ghostPings',
  1141. type: 'switch',
  1142. value: this.settings.toastToggles.ghostPings,
  1143. onChange: val => {
  1144. this.settings.toastToggles.ghostPings = val;
  1145. }
  1146. },
  1147. {
  1148. name: 'Disable toasts for local user (yourself)',
  1149. id: 'disableToastsForLocal',
  1150. type: 'switch',
  1151. value: this.settings.toastToggles.disableToastsForLocal,
  1152. onChange: val => {
  1153. this.settings.toastToggles.disableToastsForLocal = val;
  1154. }
  1155. }
  1156. ]
  1157. })
  1158. );
  1159.  
  1160. list.push(
  1161. this.createGroup({
  1162. name: 'Toast notifications for DMs',
  1163. id: this.obfuscatedClass('ml2-settings-toast-dms'),
  1164. collapsible: true,
  1165. shown: false,
  1166. settings: [
  1167. {
  1168. name: 'Message sent',
  1169. id: 'sent',
  1170. type: 'switch',
  1171. value: this.settings.toastTogglesDMs.sent,
  1172. onChange: val => {
  1173. this.settings.toastTogglesDMs.sent = val;
  1174. }
  1175. },
  1176. {
  1177. name: 'Message edited',
  1178. id: 'edited',
  1179. type: 'switch',
  1180. value: this.settings.toastTogglesDMs.edited,
  1181. onChange: val => {
  1182. this.settings.toastTogglesDMs.edited = val;
  1183. }
  1184. },
  1185. {
  1186. name: 'Message deleted',
  1187. id: 'deleted',
  1188. type: 'switch',
  1189. value: this.settings.toastTogglesDMs.deleted,
  1190. onChange: val => {
  1191. this.settings.toastTogglesDMs.deleted = val;
  1192. }
  1193. },
  1194. {
  1195. name: 'Ghost pings',
  1196. id: 'ghostPings',
  1197. type: 'switch',
  1198. value: this.settings.toastTogglesDMs.ghostPings,
  1199. onChange: val => {
  1200. this.settings.toastTogglesDMs.ghostPings = val;
  1201. }
  1202. }
  1203. ]
  1204. })
  1205. );
  1206.  
  1207. list.push(
  1208. this.createGroup({
  1209. name: 'Message caps',
  1210. id: this.obfuscatedClass('ml2-settings-caps'),
  1211. collapsible: true,
  1212. shown: false,
  1213. settings: [
  1214. {
  1215. name: 'Cached messages cap',
  1216. note: 'Max number of sent messages logger should keep track of',
  1217. id: 'messageCacheCap',
  1218. type: 'textbox',
  1219. onChange: val => {
  1220. if (isNaN(val)) return this.showToast('Value must be a number!', { type: 'error' });
  1221. this.settings.messageCacheCap = parseInt(val);
  1222. clearInterval(this.dataManagerInterval);
  1223. this.dataManagerInterval = setInterval(() => {
  1224. this.handleMessagesCap();
  1225. }, 60 * 1000 * 5);
  1226. }
  1227. },
  1228. {
  1229. name: 'Saved messages cap',
  1230. note: "Max number of messages saved to disk, this limit is for deleted, edited and purged INDIVIDUALLY. So if you have it set to 1000, it'll be 1000 edits, 1000 deletes and 1000 purged messages max",
  1231. id: 'savedMessagesCap',
  1232. type: 'textbox',
  1233. onChange: val => {
  1234. if (isNaN(val)) return this.showToast('Value must be a number!', { type: 'error' });
  1235. this.settings.savedMessagesCap = parseInt(val);
  1236. clearInterval(this.dataManagerInterval);
  1237. this.dataManagerInterval = setInterval(() => {
  1238. this.handleMessagesCap();
  1239. }, 60 * 1000 * 5);
  1240. }
  1241. },
  1242. {
  1243. name: 'Menu message render cap',
  1244. note: 'How many messages will show before the LOAD MORE button will show',
  1245. id: 'renderCap',
  1246. type: 'textbox',
  1247. onChange: val => {
  1248. if (isNaN(val)) return this.showToast('Value must be a number!', { type: 'error' });
  1249. this.settings.renderCap = parseInt(val);
  1250. clearInterval(this.dataManagerInterval);
  1251. }
  1252. }
  1253. ]
  1254. })
  1255. );
  1256.  
  1257. list.push(
  1258. this.createGroup({
  1259. name: 'Advanced',
  1260. id: this.obfuscatedClass('ml2-settings-advanced'),
  1261. collapsible: true,
  1262. shown: false,
  1263. settings: [
  1264. {
  1265. name: 'Obfuscate CSS classes',
  1266. note: 'Enable this if some plugin, library or theme is blocking you from using the plugin',
  1267. id: 'obfuscateCSSClasses',
  1268. type: 'switch'
  1269. }
  1270. ]
  1271. })
  1272. );
  1273.  
  1274. const div = document.createElement('div');
  1275. div.id = this.obfuscatedClass('ml2-settings-buttonbox');
  1276. div.style.display = 'inline-flex';
  1277. div.appendChild(this.createButton('Changelog', () => ZeresPluginLibrary.Modals.showChangelogModal(this.getName() + ' Changelog', this.getVersion(), this.getChanges())));
  1278. div.appendChild(this.createButton('Stats', () => this.showStatsModal()));
  1279. div.appendChild(this.createButton('Donate', () => this.nodeModules.electron.shell.openExternal('https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=DZUL9UZDDRLB8&source=url')));
  1280. div.appendChild(this.createButton('Help', () => this.showLoggerHelpModal()));
  1281. let button = div.firstElementChild;
  1282. while (button) {
  1283. button.style.marginRight = button.style.marginLeft = `5px`;
  1284. button = button.nextElementSibling;
  1285. }
  1286.  
  1287. list.push(div);
  1288.  
  1289. return ZeresPluginLibrary.Settings.SettingPanel.build(_ => this.saveSettings(), ...list);
  1290. }
  1291. /* ==================================================-|| START HELPERS ||-================================================== */
  1292. saveSettings() {
  1293. ZeresPluginLibrary.PluginUtilities.saveSettings(this.getName(), this.settings);
  1294. }
  1295. loadData(name, construct = {}) {
  1296. try {
  1297. return $.extend(true, construct, BdApi.getData(name, 'data')); // zeres library turns all arrays into objects.. for.. some reason?
  1298. } catch (e) {
  1299. return {}; // error handling not done here
  1300. }
  1301. }
  1302. handleDataSaving() {
  1303. // saveData/setPluginData is synchronous, can get slow with bigger files
  1304. ZeresPluginLibrary.PluginUtilities.saveData(this.getName() + 'Data', 'data', {
  1305. messageRecord: this.messageRecord,
  1306. deletedMessageRecord: this.deletedMessageRecord,
  1307. editedMessageRecord: this.editedMessageRecord,
  1308. purgedMessageRecord: this.purgedMessageRecord
  1309. });
  1310. if (this.settings.autoBackup) {
  1311. if (this.saveBackupTimeout) this.autoBackupSaveInterupts++;
  1312. if (this.autoBackupSaveInterupts < 4) {
  1313. if (this.saveBackupTimeout) clearTimeout(this.saveBackupTimeout);
  1314. // 10 seconds after, in case shits going down y'know, better not to spam save and corrupt it, don't become the thing you're trying to eliminate
  1315. this.saveBackupTimeout = setTimeout(() => this.saveBackup(), 10 * 1000);
  1316. }
  1317. }
  1318. this.requestedDataSave = 0;
  1319. }
  1320. saveData() {
  1321. if (!this.settings.dontSaveData && !this.requestedDataSave) this.requestedDataSave = setTimeout(() => this.handleDataSaving(), 1000); // needs to be async
  1322. this.updateMessages();
  1323. }
  1324. saveBackup() {
  1325. this.saveBackupTimeout = 0;
  1326. this.autoBackupSaveInterupts = 0;
  1327. ZeresPluginLibrary.PluginUtilities.saveData(this.getName() + 'DataBackup', 'data', {
  1328. messageRecord: this.messageRecord,
  1329. deletedMessageRecord: this.deletedMessageRecord,
  1330. editedMessageRecord: this.editedMessageRecord,
  1331. purgedMessageRecord: this.purgedMessageRecord
  1332. });
  1333. if (!this.loadData(this.getName() + 'DataBackup', 'data').messageRecord) this.saveBackupTimeout = setTimeout(() => this.saveBackup, 300); // don't be taxing
  1334. }
  1335. parseHTML(html) {
  1336. // TODO: drop this func, it's 75% slower than just making the elements manually
  1337. var template = document.createElement('template');
  1338. html = html.trim(); // Never return a text node of whitespace as the result
  1339. template.innerHTML = html;
  1340. return template.content.firstChild;
  1341. }
  1342. randomString() {
  1343. let start = rand();
  1344. while (start[0].toUpperCase() == start[0].toLowerCase()) start = rand();
  1345. return start + '-' + rand();
  1346. function rand() {
  1347. return Math.random()
  1348. .toString(36)
  1349. .substr(2, 7);
  1350. }
  1351. }
  1352. obfuscatedClass(selector) {
  1353. if (this.settings.obfuscateCSSClasses) return this.randomString();
  1354. return selector;
  1355. }
  1356. createTimeStamp(from = undefined, forcedDate = false) {
  1357. // todo: timestamp for edited tooltip
  1358. let date;
  1359. if (from) date = new Date(from);
  1360. else date = new Date();
  1361. return this.settings.displayDates || forcedDate ? `${date.toLocaleTimeString()}, ${date.toLocaleDateString()}` : date.toLocaleTimeString();
  1362. }
  1363. getCachedMessage(id, channelId = 0) {
  1364. let cached = this.cachedMessageRecord.find(m => m.id == id);
  1365. if (cached) return cached;
  1366. if (channelId) return this.tools.getMessage(channelId, id); // if the message isn't cached, it returns undefined
  1367. return null;
  1368. }
  1369. getEditedMessage(messageId, channelId) {
  1370. if (this.editedMessageRecord[channelId] && this.editedMessageRecord[channelId].findIndex(m => m === messageId) != -1) {
  1371. return this.messageRecord[messageId];
  1372. }
  1373. return null;
  1374. }
  1375. getSavedMessage(id) {
  1376. /* DEPRECATED */
  1377. return this.messageRecord[id];
  1378. }
  1379. createMiniFormattedData(message) {
  1380. message = $.extend(true, {}, message);
  1381. let internalMessage = this.tools.getMessage(message.channel_id, message.id);
  1382. let reactions = internalMessage ? internalMessage.reactions : [];
  1383. let guildId = null;
  1384. /* let guild = null; */
  1385. /* const author = this.tools.getUser(message.author.id); */
  1386. const channel = this.tools.getChannel(message.channel_id);
  1387. if (channel) guildId = channel.guild_id;
  1388. /* if (guildId) guild = this.tools.getServer(guildId); */
  1389. const obj = {
  1390. message: {
  1391. author: {
  1392. discriminator: message.author.discriminator,
  1393. username: message.author.username,
  1394. avatar: message.author.avatar,
  1395. id: message.author.id,
  1396. bot: internalMessage && internalMessage.author && internalMessage.author.bot // TODO: is bot checking in menu
  1397. },
  1398. /* channel_name: (channel && channel.name) || 'unknown-channel',
  1399. guild_name: (guild && guild.name) || 'unknown-guild', */
  1400. mention_everyone: message.mention_everyone,
  1401. edited_timestamp: message.edited_timestamp,
  1402. mention_roles: message.mention_roles,
  1403. attachments: message.attachments,
  1404. channel_id: message.channel_id,
  1405. timestamp: message.timestamp,
  1406. mentions: message.mentions,
  1407. content: message.content,
  1408. pinned: message.pinned,
  1409. embeds: message.embeds,
  1410. reactions: reactions,
  1411. type: message.type,
  1412. guild_id: guildId,
  1413. tts: message.tts,
  1414. id: message.id
  1415. },
  1416. local_mentioned: this.tools.isMentioned(message, this.localUser.id),
  1417. /* ghost_pinged: false, */
  1418. delete_data: null /* {
  1419. time: integer,
  1420. rel_ids: [
  1421. string,
  1422. string,
  1423. string
  1424. ],
  1425. hidden: bool
  1426. } */,
  1427. edit_history: null /* [
  1428. {
  1429. content: string,
  1430. timestamp: string
  1431. }
  1432. ],
  1433. edits_hidden: bool */
  1434. };
  1435. this.fixEmbeds(obj.message);
  1436. return obj;
  1437. }
  1438. getSelectedTextChannel() {
  1439. return ZeresPluginLibrary.DiscordModules.ChannelStore.getChannel(ZeresPluginLibrary.DiscordModules.SelectedChannelStore.getChannelId());
  1440. }
  1441. invalidateAllChannelCache() {
  1442. for (let channelId in this.channelMessages) this.invalidateChannelCache(channelId);
  1443. }
  1444. invalidateChannelCache(channelId) {
  1445. if (!this.channelMessages[channelId]) return;
  1446. this.channelMessages[channelId].ready = false;
  1447. }
  1448. cacheChannelMessages(id, relative) {
  1449. // TODO figure out if I can use this to get messages at a certain point
  1450. this.tools.fetchMessages(id, null, null, 50, (relative && { messageId: relative, ML2: true }) || undefined);
  1451. }
  1452. /* UNUSED */
  1453. cachenChannelMessagesRelative(channelId, messageId) {
  1454. ZeresPluginLibrary.DiscordModules.APIModule.get({
  1455. url: ZeresPluginLibrary.DiscordModules.DiscordConstants.Endpoints.MESSAGES(channelId),
  1456. query: {
  1457. before: null,
  1458. after: null,
  1459. limit: 50,
  1460. around: messageId
  1461. }
  1462. })
  1463. .then(res => {
  1464. if (res.status != 200) return;
  1465. const results = res.body;
  1466. const final = results.filter(x => this.cachedMessageRecord.findIndex(m => x.id === m.id) == -1);
  1467. this.cachedMessageRecord.push(...final);
  1468. })
  1469. .catch(err => {
  1470. ZeresPluginLibrary.Logger.stacktrace(this.getName(), `Error caching messages from ${channelId} around ${messageId}`, err);
  1471. });
  1472. }
  1473. formatMarkup(content, channelId) {
  1474. const markup = document.createElement('div');
  1475.  
  1476. const parsed = this.tools.parse(content, true, channelId ? { channelId: channelId } : {});
  1477. // console.log(parsed);
  1478. // error, this render doesn't work with tags
  1479. // TODO: this parser and renderer sucks
  1480. ZeresPluginLibrary.DiscordModules.ReactDOM.render(ZeresPluginLibrary.DiscordModules.React.createElement('div', { className: '' }, parsed), markup);
  1481.  
  1482. const hiddenClass = this.classes.hidden;
  1483.  
  1484. const hidden = markup.getElementsByClassName(hiddenClass);
  1485.  
  1486. for (let i = 0; i < hidden.length; i++) {
  1487. hidden[i].classList.remove(hiddenClass);
  1488. }
  1489.  
  1490. return markup;
  1491. }
  1492. ensureContainer() {
  1493. if (document.querySelector('.toasts')) return;
  1494. if (!this.ensureContainer.container) this.ensureContainer.container = '.' + ZeresPluginLibrary.WebpackModules.getByProps('content', 'guilds').content.split(/ /g)[0] + ' > div:last-child';
  1495. const container = document.querySelector(this.ensureContainer.container);
  1496. const memberlist = container.querySelector(ZeresPluginLibrary['DiscordSelectors'].MemberList.membersWrap);
  1497. const form = container ? container.querySelector('form') : null;
  1498. const left = container ? container.getBoundingClientRect().left : 310;
  1499. const right = memberlist ? memberlist.getBoundingClientRect().left : 0;
  1500. const width = right ? right - container.getBoundingClientRect().left : container.offsetWidth;
  1501. const bottom = form ? form.offsetHeight : 80;
  1502. const toastWrapper = document.createElement('div');
  1503. toastWrapper.classList.add('toasts');
  1504. toastWrapper.style.setProperty('left', left + 'px');
  1505. toastWrapper.style.setProperty('width', width + 'px');
  1506. toastWrapper.style.setProperty('bottom', bottom + 'px');
  1507. document.querySelector('#app-mount').appendChild(toastWrapper);
  1508. }
  1509. async showToast(content, options = {}) {
  1510. // credits to Zere, copied from Zeres Plugin Library
  1511. const { type = '', icon = '', timeout = 3000, onClick = () => {}, onContext = () => {} } = options;
  1512. this.ensureContainer();
  1513. const toast = ZeresPluginLibrary['DOMTools'].parseHTML(ZeresPluginLibrary.Toasts.buildToast(content, ZeresPluginLibrary.Toasts.parseType(type), icon));
  1514. toast.style.pointerEvents = 'auto';
  1515. document.querySelector('.toasts').appendChild(toast);
  1516. const wait = () => {
  1517. toast.classList.add('closing');
  1518. setTimeout(() => {
  1519. toast.remove();
  1520. if (!document.querySelectorAll('.toasts .toast').length) document.querySelector('.toasts').remove();
  1521. }, 300);
  1522. };
  1523. const sto = setTimeout(wait, timeout);
  1524. const toastClicked = () => {
  1525. clearTimeout(sto);
  1526. wait();
  1527. };
  1528. toast.addEventListener('auxclick', () => toastClicked());
  1529. toast.addEventListener('click', () => {
  1530. toastClicked();
  1531. onClick();
  1532. });
  1533. toast.addEventListener('contextmenu', () => {
  1534. toastClicked();
  1535. onContext();
  1536. });
  1537. }
  1538. clamp(val, min, max) {
  1539. // this is so sad, can we hit Metalloriff?
  1540. // his message logger added the func to Math obj and I didn't realize
  1541. return Math.max(min, Math.min(val, max));
  1542. }
  1543. deleteEditedMessageFromRecord(id, editNum) {
  1544. const record = this.messageRecord[id];
  1545. if (!record) return;
  1546.  
  1547. record.edit_history.splice(editNum, 1);
  1548. if (!record.edit_history.length) record.edit_history = null;
  1549. else return this.saveData();
  1550.  
  1551. const channelId = record.message.channel_id;
  1552. const channelMessages = this.editedMessageRecord[channelId];
  1553. channelMessages.splice(channelMessages.findIndex(m => m === id), 1);
  1554. if (this.deletedMessageRecord[channelId] && this.deletedMessageRecord[channelId].findIndex(m => m === id) != -1) return this.saveData();
  1555. if (this.purgedMessageRecord[channelId] && this.purgedMessageRecord[channelId].findIndex(m => m === id) != -1) return this.saveData();
  1556. delete this.messageRecord[id];
  1557. this.saveData();
  1558. }
  1559. jumpToMessage(channelId, messageId, guildId) {
  1560. if (this.menu.open) ZeresPluginLibrary.DiscordModules.ModalStack.popWithKey(this.style.menu);
  1561. ZeresPluginLibrary.DiscordModules.NavigationUtils.transitionTo(`/channels/${guildId || '@me'}/${channelId}${messageId ? '/' + messageId : ''}`);
  1562. }
  1563. isImage(url) {
  1564. return /\.(jpe?g|png|gif|bmp)$/i.test(url);
  1565. }
  1566. fixEmbeds(message) {
  1567. if (!this.fixEmbeds.hex2int) this.fixEmbeds.hex2int = ZeresPluginLibrary.WebpackModules.getByProps('hex2int').hex2int;
  1568.  
  1569. for (let embed of message.embeds) {
  1570. if (typeof embed.title !== 'string' || !embed.title.length) embed.rawTitle && embed.rawTitle.length ? (embed.title = embed.rawTitle) : delete embed.title;
  1571. if (typeof embed.description !== 'string' || !embed.description.length) embed.rawDescription && embed.rawDescription.length ? (embed.description = embed.rawDescription) : delete embed.description;
  1572. if (typeof embed.color === 'string') embed.color = this.fixEmbeds.hex2int(embed.color);
  1573. if (typeof embed.footer === 'object')
  1574. embed.footer = {
  1575. text: embed.footer.text,
  1576. icon_url: embed.footer.iconURL,
  1577. proxy_icon_url: embed.footer.iconProxyURL
  1578. };
  1579. if (typeof embed.author === 'object')
  1580. embed.author = {
  1581. name: embed.author.name,
  1582. url: embed.author.url,
  1583. icon_url: embed.author.iconURL,
  1584. proxy_icon_url: embed.author.iconProxyURL
  1585. };
  1586. if (embed.referenceId) embed.reference_id = embed.referenceId;
  1587. if (embed.fields && embed.fields.length) {
  1588. for (let field of embed.fields) {
  1589. for (let b in field) {
  1590. if (Array.isArray(field[b])) {
  1591. let result = '';
  1592. for (let c of field[b]) {
  1593. if (typeof c === 'string') result += c;
  1594. else if (c.props && c.props.text) result += c.props.text;
  1595. else if (c.props && c.props.title && c.props.href) result += `[${c.props.title}](${c.props.href})`;
  1596. }
  1597. field[b] = result;
  1598. }
  1599. }
  1600. }
  1601. }
  1602. }
  1603. }
  1604. isCompact() {
  1605. return ZeresPluginLibrary.DiscordAPI.UserSettings.displayCompact; // can't get a reference
  1606. }
  1607. /* ==================================================-|| END HELPERS ||-================================================== */
  1608. /* ==================================================-|| START MISC ||-================================================== */
  1609. addOpenLogsButton() {
  1610. if (!this.selectedChannel) return;
  1611. const parent = document.querySelector('div[class*="chat-"] div[class*="toolbar-"]');
  1612. parent.insertBefore(this.channelLogButton, parent.querySelector('div[class*="search-"]'));
  1613. }
  1614. removeOpenLogsButton() {
  1615. const button = document.getElementById(this.style.openLogs);
  1616. button.remove();
  1617. }
  1618. showLoggerHelpModal() {
  1619. this.createModal({
  1620. confirmText: 'OK',
  1621. header: 'Logger help',
  1622. size: ZeresPluginLibrary.Modals.ModalSizes.LARGE,
  1623. children: [
  1624. ZeresPluginLibrary.ReactTools.createWrappedElement([
  1625. this.parseHTML(
  1626. `<div class="${this.multiClasses.defaultColor}">
  1627. <strong>Menu:</strong></br>
  1628. <div class="${this.style.textIndent}">
  1629. DELETE + LEFT-CLICK:</br>
  1630. <div class="${this.style.textIndent}">
  1631. Clicking on a message, deletes the message</br>
  1632. Clicking on an edit deletes that specific edit</br>
  1633. Clicking on the timestamp deletes all messages in that message group
  1634. </div></br>
  1635. RIGHT-CLICK:</br>
  1636. <div class="${this.style.textIndent}">
  1637. Right-clicking the timestamp opens up options for the entire message group
  1638. </div></br>
  1639. </div>
  1640. <strong>Toasts:</strong></br>
  1641. <div class="${this.style.textIndent}">
  1642. Note: Little "notifications" in discord that tell you if a message was edited, deleted, purged etc are called Toasts!</br></br>
  1643. LEFT-CLICK:</br>
  1644. <div class="${this.style.textIndent}">
  1645. Opens menu with the relevant tab</br>
  1646. </div></br>
  1647. RIGHT-CLICK:</br>
  1648. <div class="${this.style.textIndent}">
  1649. Jumps to relevant message in the relevant channel
  1650. </div></br>
  1651. MIDDLE-CLICK/SCROLLWHEEL-CLICK:</br>
  1652. <div class="${this.style.textIndent}">
  1653. Only dismisses/closes the Toast.
  1654. </div></br>
  1655. </div>
  1656. <strong>Open Logs button (top right next to search):</strong></br>
  1657. <div class="${this.style.textIndent}">
  1658. LEFT-CLICK:</br>
  1659. <div class="${this.style.textIndent}">
  1660. Opens menu</br>
  1661. </div></br>
  1662. RIGHT-CLICK:</br>
  1663. <div class="${this.style.textIndent}">
  1664. Opens filtered menu that only shows messages from selected channel</br>
  1665. </div></br>
  1666. </div>
  1667. <strong>Whitelist/blacklist, ignores and overrides:</strong></br>
  1668. <div class="${this.style.textIndent}">
  1669. WHITELIST-ONLY:</br>
  1670. <div class="${this.style.textIndent}">
  1671. All servers are ignored unless whitelisted</br>
  1672. Muted channels in whitelisted servers are ignored unless whitelisted or "Ignore muted channels" is disabled</br>
  1673. All channels in whitelisted servers are logged unless blacklisted, or muted and "Ignore muted channels" is enabled
  1674. </div></br>
  1675. DEFAULT:</br>
  1676. <div class="${this.style.textIndent}">
  1677. All servers are logged unless blacklisted or muted and "Ignore muted servers" is enabled</br>
  1678. Muted channels are ignored unless whitelisted or "Ignore muted channels" is disabled</br>
  1679. Muted servers are ignored unless whitelisted or "Ignore muted servers" is disabled</br>
  1680. Whitelisted channels in muted or blacklisted servers are logged</br>
  1681. </div></br>
  1682. ALL:</br>
  1683. <div class="${this.style.textIndent}">
  1684. Whitelisted channels in blacklisted servers are logged</br>
  1685. Blacklisted channels in whitelisted servers are ignored</br>
  1686. "Always log selected channel" overrides blacklist, whitelist-only mode, NSFW channel ignore, mute</br>
  1687. "Always log DMs" overrides blacklist as well as whitelist-only mode</br>
  1688. Channels marked NSFW and not whitelisted are ignored unless "Ignore NSFW channels" is disabled
  1689. </div></br>
  1690. </div>
  1691. <strong>Chat:</strong></br>
  1692. <div class="${this.style.textIndent}">
  1693. LEFT-CLICK:</br>
  1694. <div class="${this.style.textIndent}">
  1695. Clicking the (Edited) text opens menu of specified message
  1696. </div></br>
  1697. RIGHT-CLICK:</br>
  1698. <div class="${this.style.textIndent}">
  1699. Right-clicking an edit (darkened text) allows you to delete that edit, or hide edits</br>
  1700. Right-clicking on a edited or deleted message gives you the option to hide the deleted message or hide or unhide edits.
  1701. </div></br>
  1702. </div>
  1703. </div>`
  1704. )
  1705. ])
  1706. ],
  1707. red: false
  1708. });
  1709. }
  1710. showStatsModal() {
  1711. const elements = [];
  1712. let totalMessages = Object.keys(this.messageRecord).length;
  1713. let messageCounts = [];
  1714. let spaceUsageMB = 0;
  1715. let cachedImageCount = 0;
  1716. let cachedImagesUsageMB = 0;
  1717.  
  1718. let mostDeletesChannel = { num: 0, id: '' };
  1719. let mostEditsChannel = { num: 0, id: '' };
  1720. let deleteDataTemp = {};
  1721. let editDataTemp = {};
  1722.  
  1723. for (const map of [this.deletedMessageRecord, this.editedMessageRecord, this.cachedMessageRecord]) {
  1724. let messageCount = 0;
  1725. if (!Array.isArray(map)) {
  1726. for (const channelId in map) {
  1727. if (!deleteDataTemp[channelId]) deleteDataTemp[channelId] = [];
  1728. if (!editDataTemp[channelId]) editDataTemp[channelId] = [];
  1729. for (const messageId of map[channelId]) {
  1730. messageCount++;
  1731. const record = this.messageRecord[messageId];
  1732. if (!record) continue; // wtf?
  1733. if (record.delete_data && deleteDataTemp[channelId].findIndex(m => m === messageId)) deleteDataTemp[channelId].push(messageId);
  1734. if (record.edit_history && editDataTemp[channelId].findIndex(m => m === messageId)) editDataTemp[channelId].push(messageId);
  1735. }
  1736. }
  1737. }
  1738. for (const channelId in deleteDataTemp) if (deleteDataTemp[channelId].length > mostDeletesChannel.num) mostDeletesChannel = { num: deleteDataTemp[channelId].length, id: channelId };
  1739. for (const channelId in editDataTemp) if (editDataTemp[channelId].length > mostEditsChannel.num) mostEditsChannel = { num: editDataTemp[channelId].length, id: channelId };
  1740.  
  1741. messageCounts.push(messageCount);
  1742. }
  1743. const addLine = (name, value) => {
  1744. elements.push(this.parseHTML(`<div class="${this.multiClasses.defaultColor}"><strong>${name}</strong>: ${value}</div></br>`));
  1745. };
  1746. addLine('Total messages', totalMessages);
  1747. addLine('Deleted message count', messageCounts[0]);
  1748. addLine('Edited message count', messageCounts[1]);
  1749. addLine('Sent message count', this.cachedMessageRecord.length);
  1750.  
  1751. let channel = this.tools.getChannel(mostDeletesChannel.id);
  1752. if (channel) addLine('Most deletes', mostDeletesChannel.num + ' ' + this.getLiteralName(channel.guild_id, channel.id));
  1753. if (channel) addLine('Most edits', mostEditsChannel.num + ' ' + this.getLiteralName(channel.guild_id, channel.id));
  1754.  
  1755. addLine('Data file size', (this.nodeModules.fs.statSync(this.pluginDir + '/MessageLoggerV2Data.config.json').size / 1024 / 1024).toFixed(2) + 'MB');
  1756. addLine('Data file size severity', this.slowSaveModeStep == 0 ? 'OK' : this.slowSaveModeStep == 1 ? 'MILD' : this.slowSaveModeStep == 2 ? 'BAD' : 'EXTREME');
  1757. this.createModal({
  1758. confirmText: 'OK',
  1759. header: 'Data stats',
  1760. size: ZeresPluginLibrary.Modals.ModalSizes.SMALL,
  1761. children: [ZeresPluginLibrary.ReactTools.createWrappedElement(elements)],
  1762. red: false
  1763. });
  1764. }
  1765. reAddDeletedMessages(messages, id, deletedMessages, purgedMessages) {
  1766. // hack
  1767. if (this.antiinfiniteloop.findIndex(m => m === id) != -1) {
  1768. //this.showToast('Infinite loop detected in data! Faulty ID:' + id, { type: 'error', timeout: 5000 });
  1769. //ZeresPluginLibrary.Logger.err(this.getName(), `Infinite loop detected in data! Cancelled restore of ${id}!`);
  1770. // todo: add code to prevent this bullshit
  1771. return false;
  1772. }
  1773. const record = this.messageRecord[id];
  1774. if (!record || !record.delete_data || record.delete_data.hidden || !record.delete_data.rel_ids) return false;
  1775.  
  1776. this.antiinfiniteloop.push(record.message.id);
  1777.  
  1778. for (let prev in record.delete_data.rel_ids) {
  1779. let relID = record.delete_data.rel_ids[prev];
  1780. let mIDX = (messages.length && messages.length - 1) || 0;
  1781. let addIn = false;
  1782. if (relID != 'CHANNELEND') {
  1783. mIDX = messages.findIndex(m => m.id === relID);
  1784. }
  1785. if (mIDX !== -1) addIn = true;
  1786. else {
  1787. if (deletedMessages && deletedMessages.findIndex(m => m === relID) === -1 && (!this.settings.showPurgedMessages || !purgedMessages || purgedMessages.findIndex(m => m === relID) === -1)) continue;
  1788. if (this.reAddDeletedMessages(messages, relID)) addIn = true;
  1789. }
  1790. if (addIn) {
  1791. mIDX = messages.findIndex(m => m.id === relID);
  1792. //let copy = Object.assign({}, record.message);
  1793. //for (let attachmentIDX in copy.attachments) {
  1794. // let attachment = copy.attachments[attachmentIDX];
  1795. // console.log(attachment.proxy_url);
  1796. // attachment.proxy_url = this.imageCacheIdToDataURL(attachment.proxy_url);
  1797. // console.log(attachment.proxy_url);
  1798. //}
  1799. messages.splice(mIDX, 0, record.message);
  1800. return true;
  1801. }
  1802. }
  1803. return false;
  1804. }
  1805. getLiteralName(guildId, channelId, guildNameBackup = -1, channelNameBackup = -1) {
  1806. // TODO, custom channel server failure text
  1807. const guild = this.tools.getServer(guildId);
  1808. const channel = this.tools.getChannel(channelId); // todo
  1809. /* if (typeof guildNameBackup !== 'number' && guild && guildNameBackup) */ if (guildId) {
  1810. const channelName = channel ? channel.name : 'unknown-channel';
  1811. const guildName = guild ? guild.name : 'unknown-server';
  1812. return `${guildName}, #${channelName}`;
  1813. } else if (channel && channel.name.length) {
  1814. return `group ${channel.name}`;
  1815. } else if (channel && channel.type == 3) {
  1816. let finalGroupName = '';
  1817. for (let i of channel.recipients) {
  1818. const user = this.tools.getUser(i);
  1819. if (!user) continue;
  1820. finalGroupName += ',' + user.username;
  1821. }
  1822. if (!finalGroupName.length) {
  1823. return 'unknown group';
  1824. } else {
  1825. finalGroupName = finalGroupName.substr(1);
  1826. finalGroupName = finalGroupName.length > 10 ? finalGroupName.substr(0, 10 - 1) + '...' : finalGroupName;
  1827. return `group ${finalGroupName}`;
  1828. }
  1829. } else if (channel && channel.recipients) {
  1830. const user = this.tools.getUser(channel.recipients[0]);
  1831. if (!user) return 'DM';
  1832. return `${user.username} DM`;
  1833. } else {
  1834. return 'DM';
  1835. }
  1836. }
  1837. getRelativeMessages(id, channelId) {
  1838. const messages = this.channelMessages[channelId];
  1839. // ready check may be redundant, discord may not save the message if the channel isn't ready
  1840. if (messages && messages.ready && messages._array.length) {
  1841. let foundMessage = false;
  1842. let relMessages = [];
  1843. for (let i = messages._array.length - 1; i >= 0; i--) {
  1844. const message = messages._array[i];
  1845. if (!foundMessage && message.id == id) {
  1846. foundMessage = true;
  1847. continue;
  1848. }
  1849. if (foundMessage) relMessages.push(message.id);
  1850. if (relMessages.length >= 3) return relMessages;
  1851. } // todo, better caching if hasMoreBefore = true
  1852. if (!messages.hasMoreBefore) {
  1853. relMessages.push('CHANNELEND');
  1854. return relMessages;
  1855. }
  1856. }
  1857. return null;
  1858. }
  1859. saveDeletedMessage(message, targetMessageRecord) {
  1860. let result = this.createMiniFormattedData(message);
  1861. result.delete_data = {};
  1862. const id = message.id;
  1863. const channelId = message.channel_id;
  1864. result.delete_data.time = new Date().getTime();
  1865. result.ghost_pinged = result.local_mentioned; // it's simple bruh
  1866. if (!(result.delete_data.rel_ids = this.getRelativeMessages(id, channelId))) {
  1867. //console.error(`[${this.getName()}]: saveDeletedMessage -> Failed to get relative IDs!`);
  1868. //if (this.settings.displayInChat) ZeresPluginLibrary.Toasts.error(`Failed to get relative IDs! Deleted message will not show in chat after reload!`, { timeout: 7500 });
  1869. }
  1870. if (!Array.isArray(targetMessageRecord[channelId])) targetMessageRecord[channelId] = [];
  1871. if (this.messageRecord[id]) {
  1872. const record = this.messageRecord[id];
  1873. record.delete_data = result.delete_data;
  1874. record.ghost_pinged = result.ghost_pinged;
  1875. } else {
  1876. this.messageRecord[id] = result;
  1877. }
  1878. if (this.messageRecord[id].message.attachments) {
  1879. const attachments = this.messageRecord[id].message.attachments;
  1880. for (let i = 0; i < attachments.length; i++) {
  1881. attachments[i].url = attachments[i].proxy_url; // proxy url lasts longer
  1882. }
  1883. }
  1884. if (this.settings.cacheAllImages) this.cacheMessageImages(this.messageRecord[id].message);
  1885. targetMessageRecord[channelId].push(id);
  1886. }
  1887. createButton(label, callback) {
  1888. const classes = this.createButton.classes;
  1889. const ret = this.parseHTML(`<button type="button" class="${classes.button}"><div class="${classes.buttonContents}">${label}</div></button>`);
  1890. if (callback) ret.addEventListener('click', callback);
  1891. return ret;
  1892. }
  1893. createModal(options, image, name) {
  1894. const modal = image ? this.createModal.imageModal : this.createModal.confirmationModal;
  1895. ZeresPluginLibrary.DiscordModules.ModalStack.push(
  1896. function(props) {
  1897. return ZeresPluginLibrary.DiscordModules.React.createElement(modal, Object.assign(options, props));
  1898. },
  1899. undefined,
  1900. name
  1901. );
  1902. }
  1903. getMessageAny(id) {
  1904. const record = this.messageRecord[id];
  1905. if (!record) return this.cachedMessageRecord.find(m => m.id == id);
  1906. return record.message;
  1907. }
  1908. handleImageCacheDataSaving() {
  1909. this.requestedImageCacheDataSave = 0;
  1910. // saveData/setPluginData is synchronous, can get slow with bigger files
  1911. ZeresPluginLibrary.PluginUtilities.saveData('MLV2_IMAGE_CACHE/ImageCache', 'data', {
  1912. imageCacheRecord: this.imageCacheRecord
  1913. });
  1914. }
  1915. saveImageCacheData() {
  1916. if (!this.settings.dontSaveData && !this.requestedImageCacheDataSave) this.requestedImageCacheDataSave = setTimeout(() => this.handleImageCacheDataSaving(), 5000); // needs to be async
  1917. }
  1918. cacheImage(url, attachmentIdx, attachmentId, messageId, channelId, attempts = 0) {
  1919. this.nodeModules.request({ url: url, encoding: null }, (err, res, buffer) => {
  1920. if (err || res.statusCode != 200) {
  1921. if (res.statusCode == 404 || res.statusCode == 403) return;
  1922. attempts++;
  1923. if (attempts > 3) return ZeresPluginLibrary.Logger.warn(this.getName(), `Failed to get image ${attachmentId} for caching, error code ${res.statusCode}`);
  1924. return setTimeout(() => this.cacheImage(url, attachmentIdx, attachmentId, messageId, channelId, attempts), 1000);
  1925. }
  1926. const fileExtension = url.match(/\.[0-9a-z]+$/i)[0];
  1927. this.nodeModules.fs.writeFileSync(this.imageCacheDir + `/${attachmentId}${fileExtension}`, buffer, { encoding: null });
  1928. let reader = new FileReader();
  1929. reader.readAsDataURL(new Blob([buffer], { type: 'image/png' }));
  1930. reader.onload = () => {
  1931. const img = new Image();
  1932. const canvas = document.createElement('canvas');
  1933. img.src = reader.result;
  1934. img.onload = () => {
  1935. const ctx = canvas.getContext('2d');
  1936. let width = img.width;
  1937. let height = img.height;
  1938. const maxSize = 256;
  1939. if (width > maxSize || height > maxSize) {
  1940. if (width > height) {
  1941. const scale = maxSize / width;
  1942. width = maxSize;
  1943. height *= scale;
  1944. } else {
  1945. const scale = maxSize / height;
  1946. height = maxSize;
  1947. width *= scale;
  1948. }
  1949. }
  1950. canvas.width = width;
  1951. canvas.height = height;
  1952. ctx.drawImage(img, 0, 0, width, height);
  1953. let outputData = canvas.toDataURL('image/png');
  1954. outputData = outputData.substr(0, 14) + `;${channelId};${attachmentId}` + outputData.substr(14);
  1955. this.imageCacheRecord[attachmentId] = {
  1956. thumbnail: outputData,
  1957. attachmentIdx: attachmentIdx,
  1958. messageId: messageId,
  1959. extension: fileExtension
  1960. };
  1961. this.saveImageCacheData();
  1962. };
  1963. };
  1964. });
  1965. }
  1966. cacheMessageImages(message) {
  1967. // don't block it, ugly but works, might rework later
  1968. setTimeout(() => {
  1969. for (let i = 0; i < message.attachments.length; i++) {
  1970. const attachment = message.attachments[i];
  1971. if (!this.isImage(attachment.url)) continue;
  1972. this.cacheImage(attachment.url, i, attachment.id, message.id, message.channel_id);
  1973. }
  1974. }, 0);
  1975. }
  1976. /* ==================================================-|| END MISC ||-================================================== */
  1977. /* ==================================================-|| START MESSAGE MANAGMENT ||-================================================== */
  1978. deleteMessageFromRecords(id) {
  1979. const record = this.messageRecord[id];
  1980. if (!record) {
  1981. for (let map of [this.deletedMessageRecord, this.editedMessageRecord, this.purgedMessageRecord]) {
  1982. for (let channelId in map) {
  1983. const index = map[channelId].findIndex(m => m === id);
  1984. if (index == -1) continue;
  1985. map[channelId].splice(index, 1);
  1986. if (!map[channelId].length) delete map[channelId];
  1987. }
  1988. }
  1989. return;
  1990. }
  1991. // console.log('Deleting', record);
  1992. const channelId = record.message.channel_id;
  1993. for (let map of [this.deletedMessageRecord, this.editedMessageRecord, this.purgedMessageRecord]) {
  1994. if (!map[channelId]) continue;
  1995. const index = map[channelId].findIndex(m => m === id);
  1996. if (index == -1) continue;
  1997. map[channelId].splice(index, 1);
  1998. if (!map[channelId].length) delete map[channelId];
  1999. }
  2000. delete this.messageRecord[id];
  2001. }
  2002. handleMessagesCap() {
  2003. try {
  2004. // TODO: add empty record and infinite loop checking for speed improvements
  2005. const extractAllMessageIds = map => {
  2006. let ret = [];
  2007. for (let channelId in map) {
  2008. for (let messageId of map[channelId]) {
  2009. ret.push(messageId);
  2010. }
  2011. }
  2012. return ret;
  2013. };
  2014. if (this.cachedMessageRecord.length > this.settings.messageCacheCap) this.cachedMessageRecord.splice(0, this.cachedMessageRecord.length - this.settings.messageCacheCap);
  2015. let changed = false;
  2016. const deleteMessages = map => {
  2017. this.sortMessagesByAge(map);
  2018. const toDelete = map.length - this.settings.savedMessagesCap;
  2019. for (let i = map.length - 1, deleted = 0; i >= 0 && deleted != toDelete; i--, deleted++) {
  2020. this.deleteMessageFromRecords(map[i]);
  2021. }
  2022. changed = true;
  2023. };
  2024. const handleInvalidEntries = map => {
  2025. for (let channelId in map) {
  2026. for (let messageIdIdx = map[channelId].length - 1; messageIdIdx >= 0; messageIdIdx--) {
  2027. if (!Array.isArray(map[channelId])) {
  2028. delete map[channelId];
  2029. changed = true;
  2030. continue;
  2031. }
  2032. if (!this.messageRecord[map[channelId][messageIdIdx]]) {
  2033. map[channelId].splice(messageIdIdx, 1);
  2034. changed = true;
  2035. }
  2036. }
  2037. if (!map[channelId].length) {
  2038. delete map[channelId];
  2039. changed = true;
  2040. }
  2041. }
  2042. };
  2043. const checkIsInRecords = (channelId, messageId) => {
  2044. for (let map of [this.deletedMessageRecord, this.editedMessageRecord, this.purgedMessageRecord]) if (map[channelId] && map[channelId].findIndex(m => m === messageId) != -1) return true;
  2045. return false;
  2046. };
  2047.  
  2048. for (let map of [this.deletedMessageRecord, this.editedMessageRecord, this.purgedMessageRecord]) handleInvalidEntries(map);
  2049. for (let messageId in this.messageRecord) {
  2050. if (!checkIsInRecords(this.messageRecord[messageId].message.channel_id, messageId)) delete this.messageRecord[messageId];
  2051. }
  2052. let deletedMessages = extractAllMessageIds(this.deletedMessageRecord);
  2053. let editedMessages = extractAllMessageIds(this.editedMessageRecord);
  2054. let purgedMessages = extractAllMessageIds(this.purgedMessageRecord);
  2055. for (let map of [deletedMessages, editedMessages, purgedMessages]) if (map.length > this.settings.savedMessagesCap) deleteMessages(map);
  2056. if (changed) this.saveData();
  2057. if (!this.settings.cacheAllImages) return;
  2058. changed = false;
  2059. for (let attId in this.imageCacheRecord) {
  2060. const record = this.imageCacheRecord[attId];
  2061. const imagePath = `${this.imageCacheDir}/${attId}${record.extension}`;
  2062. if (this.messageRecord[record.messageId] && this.nodeModules.fs.existsSync(imagePath)) continue;
  2063. delete this.imageCacheRecord[attId];
  2064. changed = true;
  2065. }
  2066. if (changed) this.saveImageCacheData();
  2067. if (!this.settings.dontDeleteCachedImages) {
  2068. const savedImages = this.nodeModules.fs.readdirSync(this.imageCacheDir);
  2069. for (let img of savedImages) {
  2070. if (img.indexOf('ImageCache.config.json') != -1) continue;
  2071. const attId = img.match(/(\d*).[a-z]+/i)[1];
  2072. if (this.imageCacheRecord[attId]) continue;
  2073. try {
  2074. this.nodeModules.fs.unlinkSync(`${this.imageCacheDir}/${img}`);
  2075. } catch (e) {
  2076. ZeresPluginLibrary.Logger.err(this.getName(), 'Error deleting unreferenced image, what the shit', e);
  2077. }
  2078. }
  2079. }
  2080. // 10 minutes
  2081. for (let id in this.editHistoryAntiSpam) if (new Date().getTime() - this.editHistoryAntiSpam[id].times[0] < 10 * 60 * 1000) delete this.editHistoryAntiSpam[id];
  2082. } catch (e) {
  2083. ZeresPluginLibrary.Logger.stacktrace(this.getName(), 'Error clearing out data', e);
  2084. }
  2085. }
  2086. /* ==================================================-|| END MESSAGE MANAGMENT ||-================================================== */
  2087. onDispatchEvent(args, callDefault) {
  2088. const dispatch = args[0];
  2089.  
  2090. if (!dispatch) return callDefault(...args);
  2091.  
  2092. try {
  2093. if (dispatch.type === 'MESSAGE_LOGGER_V2_SELF_TEST') {
  2094. clearTimeout(this.selfTestTimeout);
  2095. //console.log('Self test OK');
  2096. this.selfTestFailures = 0;
  2097. return;
  2098. }
  2099. // if (dispatch.type == 'EXPERIMENT_TRIGGER') return callDefault(...args);
  2100. // console.log('INFO: onDispatchEvent -> dispatch', dispatch);
  2101. if (dispatch.type === 'CHANNEL_SELECT') {
  2102. callDefault(...args);
  2103. this.selectedChannel = this.getSelectedTextChannel();
  2104.  
  2105. return;
  2106. }
  2107.  
  2108. if (dispatch.type === 'MODAL_POP') {
  2109. callDefault(...args);
  2110. if (!ZeresPluginLibrary.WebpackModules.getByProps('isModalOpenWithKey').isModalOpenWithKey(this.style.menu)) {
  2111. this.menu.filter = '';
  2112. this.menu.open = false;
  2113. this.menu.shownMessages = -1;
  2114. if (this.menu.messages) this.menu.messages.length = 0;
  2115. }
  2116. return;
  2117. }
  2118.  
  2119. if (dispatch.ML2 && dispatch.type === 'MESSAGE_DELETE') {
  2120. callDefault(...args);
  2121. setTimeout(() => this.updateMessages(), 0); // because this needs to return for it to continue
  2122. return;
  2123. }
  2124.  
  2125. if (dispatch.type === 'LOAD_MESSAGES_SUCCESS_CACHED') {
  2126. callDefault(...args);
  2127. return this.updateMessages();
  2128. }
  2129.  
  2130. if (dispatch.type !== 'MESSAGE_CREATE' && dispatch.type !== 'MESSAGE_DELETE' && dispatch.type !== 'MESSAGE_DELETE_BULK' && dispatch.type !== 'MESSAGE_UPDATE' && dispatch.type !== 'LOAD_MESSAGES_SUCCESS') return callDefault(...args);
  2131.  
  2132. // console.log('INFO: onDispatchEvent -> dispatch', dispatch);
  2133.  
  2134. if (dispatch.message && dispatch.message.type) return callDefault(...args); // anti other shit 1
  2135.  
  2136. const channel = this.tools.getChannel(dispatch.message ? dispatch.message.channel_id : dispatch.channelId);
  2137. if (!channel) return callDefault(...args);
  2138. const guild = channel.guild_id ? this.tools.getServer(channel.guild_id) : false;
  2139.  
  2140. let author = dispatch.message && dispatch.message.author ? this.tools.getUser(dispatch.message.author.id) : false;
  2141. if (!author) author = ((this.channelMessages[channel.id] || { _map: {} })._map[dispatch.message ? dispatch.message.id : dispatch.id] || {}).author;
  2142. if (!author) {
  2143. // last ditch attempt
  2144. let message = this.getCachedMessage(dispatch.id);
  2145. if (message) author = this.tools.getUser(message.author.id);
  2146. }
  2147.  
  2148. if (!author && !(dispatch.type == 'LOAD_MESSAGES_SUCCESS' || dispatch.type == 'MESSAGE_DELETE_BULK')) return callDefault(...args);
  2149.  
  2150. const isLocalUser = author && author.id === this.localUser.id;
  2151.  
  2152. if (author && author.bot && this.settings.ignoreBots) return callDefault(...args);
  2153. if (author && isLocalUser && this.settings.ignoreSelf) return callDefault(...args);
  2154. if (author && this.settings.ignoreBlockedUsers && this.tools.isBlocked(author.id) && !isLocalUser) return callDefault(...args);
  2155. if (author && author.avatar === 'clyde') return callDefault(...args);
  2156.  
  2157. if (this.settings.ignoreLocalEdits && dispatch.type === 'MESSAGE_UPDATE' && isLocalUser) return callDefault(...args);
  2158. if (this.settings.ignoreLocalDeletes && dispatch.type === 'MESSAGE_DELETE' && isLocalUser && this.localDeletes.findIndex(m => m === dispatch.id) !== -1) return callDefault(...args);
  2159.  
  2160. let guildIsMutedReturn = false;
  2161. let channelIgnoreReturn = false;
  2162.  
  2163. const isInWhitelist = id => this.settings.whitelist.findIndex(m => m === id) != -1;
  2164. const isInBlacklist = id => this.settings.blacklist.findIndex(m => m === id) != -1;
  2165. const guildWhitelisted = guild && isInWhitelist(guild.id);
  2166. const channelWhitelisted = isInWhitelist(channel.id);
  2167.  
  2168. const guildBlacklisted = guild && isInBlacklist(guild.id);
  2169. const channelBlacklisted = isInBlacklist(channel.id);
  2170.  
  2171. let doReturn = false;
  2172.  
  2173. if (guild) {
  2174. guildIsMutedReturn = this.settings.ignoreMutedGuilds && this.muteModule.isMuted(guild.id);
  2175. channelIgnoreReturn = (this.settings.ignoreNSFW && channel.nsfw && !channelWhitelisted) || (this.settings.ignoreMutedChannels && (this.muteModule.isChannelMuted(guild.id, channel.id) || (channel.parent_id && this.muteModule.isChannelMuted(channel.parent_id))));
  2176. }
  2177.  
  2178. if (!((this.settings.alwaysLogSelected && this.selectedChannel && this.selectedChannel.id == channel.id) || (this.settings.alwaysLogDM && !guild))) {
  2179. if (guildBlacklisted) {
  2180. if (!channelWhitelisted) doReturn = true; // not whitelisted
  2181. } else if (guildWhitelisted) {
  2182. if (channelBlacklisted) doReturn = true; // channel blacklisted
  2183. if (channelIgnoreReturn && !channelWhitelisted) doReturn = true;
  2184. } else {
  2185. if (this.settings.onlyLogWhitelist) {
  2186. if (!channelWhitelisted) doReturn = true; // guild not in either list, channel not whitelisted
  2187. } else {
  2188. if (channelBlacklisted) doReturn = true; // channel blacklisted
  2189. if (channelIgnoreReturn || guildIsMutedReturn) {
  2190. if (!channelWhitelisted) doReturn = true;
  2191. }
  2192. }
  2193. }
  2194. }
  2195.  
  2196. if (doReturn && this.settings.alwaysLogGhostPings) {
  2197. if (dispatch.type === 'MESSAGE_DELETE') {
  2198. const deleted = this.getCachedMessage(dispatch.id, dispatch.channelId);
  2199. if (!deleted || deleted.type) return callDefault(...args); // nothing we can do past this point..
  2200. if (!this.tools.isMentioned(deleted, this.localUser.id)) return callDefault(...args);
  2201. const record = this.messageRecord[dispatch.id];
  2202. if ((!this.selectedChannel || this.selectedChannel.id != channel.id) && (guild ? this.settings.toastToggles.ghostPings : this.settings.toastTogglesDMs.ghostPings) && (!record || !record.ghost_pinged)) {
  2203. this.showToast(`You got ghost pinged in ${this.getLiteralName(channel.guild_id, channel.id)}`, {
  2204. type: 'warning',
  2205. onClick: () => this.openWindow('ghostpings'),
  2206. onContext: () => this.jumpToMessage(dispatch.channelId, dispatch.id, guild && guild.id),
  2207. timeout: 4500
  2208. });
  2209. }
  2210. this.saveDeletedMessage(deleted, this.deletedMessageRecord);
  2211. this.saveData();
  2212. } else if (dispatch.type === 'MESSAGE_UPDATE') {
  2213. if (!dispatch.message.edited_timestamp) {
  2214. let last = this.getCachedMessage(dispatch.message.id);
  2215. if (last) last.embeds = dispatch.message.embeds;
  2216. return callDefault(...args);
  2217. }
  2218. let isSaved = this.getEditedMessage(dispatch.message.id, channel.id);
  2219. const last = this.getCachedMessage(dispatch.message.id, channel.id);
  2220. const lastEditedSaved = isSaved || this.tempEditedMessageRecord[dispatch.message.id];
  2221.  
  2222. if (isSaved && !lastEditedSaved.local_mentioned) {
  2223. lastEditedSaved.message.content = dispatch.message.content; // don't save history, just the value so we don't confuse the user
  2224. return callDefault(...args);
  2225. }
  2226.  
  2227. // if we have lastEdited then we can still continue as we have all the data we need to process it.
  2228. if (!last && !lastEditedSaved) return callDefault(...args); // nothing we can do past this point..
  2229. let ghostPinged = false;
  2230. if (lastEditedSaved) {
  2231. // last is not needed, we have all the data already saved
  2232. if (lastEditedSaved.message.content === dispatch.message.content) return callDefault(...args); // we don't care about that
  2233. lastEditedSaved.edit_history.push({
  2234. content: lastEditedSaved.message.content,
  2235. time: new Date().getTime()
  2236. });
  2237. lastEditedSaved.message.content = dispatch.message.content;
  2238. ghostPinged = !lastEditedSaved.ghost_pinged && lastEditedSaved.local_mentioned && !this.tools.isMentioned(dispatch.message, this.localUser.id);
  2239. } else {
  2240. if (last.content === dispatch.message.content) return callDefault(...args); // we don't care about that
  2241. let data = this.createMiniFormattedData(last);
  2242. data.edit_history = [
  2243. {
  2244. content: last.content,
  2245. time: new Date().getTime()
  2246. }
  2247. ];
  2248. data.message.content = dispatch.message.content;
  2249. this.tempEditedMessageRecord[data.message.id] = data;
  2250. ghostPinged = this.tools.isMentioned(last, this.localUser.id) && !this.tools.isMentioned(dispatch.message, this.localUser.id);
  2251. }
  2252.  
  2253. if (isSaved) this.saveData();
  2254.  
  2255. if (!ghostPinged) return callDefault(...args);
  2256.  
  2257. if (!isSaved) {
  2258. const data = this.tempEditedMessageRecord[dispatch.message.id];
  2259. data.ghost_pinged = true;
  2260. this.messageRecord[dispatch.message.id] = data;
  2261. if (!this.editedMessageRecord[channel.id]) this.editedMessageRecord[channel.id] = [];
  2262. this.editedMessageRecord[channel.id].push(dispatch.message.id);
  2263. this.saveData();
  2264. } else {
  2265. const lastEdited = this.getEditedMessage(dispatch.message.id, channel.id);
  2266. if (!lastEdited) return callDefault(...args);
  2267. lastEdited.ghost_pinged = true;
  2268. this.saveData();
  2269. }
  2270.  
  2271. if ((!this.selectedChannel || this.selectedChannel.id != channel.id) && (guild ? this.settings.toastToggles.ghostPings : this.settings.toastTogglesDMs.ghostPings)) {
  2272. this.showToast(`You got ghost pinged in ${this.getLiteralName(channel.guild_id, channel.id)}`, {
  2273. type: 'warning',
  2274. onClick: () => this.openWindow('ghostpings'),
  2275. onContext: () => this.jumpToMessage(dispatch.channelId, dispatch.id, guild && guild.id),
  2276. timeout: 4500
  2277. });
  2278. }
  2279. } else if (dispatch.type == 'MESSAGE_CREATE' && dispatch.message && (dispatch.message.content.length || (dispatch.attachments && dispatch.attachments.length) || (dispatch.embeds && dispatch.embeds.length)) && dispatch.message.state != 'SENDING' && !dispatch.optimistic && !dispatch.message.type) {
  2280. if (this.cachedMessageRecord.findIndex(m => m.id === dispatch.message.id) != -1) return callDefault(...args);
  2281. this.cachedMessageRecord.push(dispatch.message);
  2282. }
  2283. }
  2284. if (doReturn) return callDefault(...args);
  2285.  
  2286. if (dispatch.type == 'LOAD_MESSAGES_SUCCESS') {
  2287. if (!this.settings.restoreDeletedMessages) return callDefault(...args);
  2288. if (dispatch.jump && dispatch.jump.ML2) delete dispatch.jump;
  2289. const deletedMessages = this.deletedMessageRecord[channel.id];
  2290. const purgedMessages = this.purgedMessageRecord[channel.id];
  2291. if ((!deletedMessages && !purgedMessages) || (!this.settings.showPurgedMessages && !this.settings.showDeletedMessages)) return callDefault(...args);
  2292. //console.log('Recursively adding deleted messages');
  2293. /* return callDefault(...args); */
  2294. if (this.settings.showDeletedMessages && deletedMessages) {
  2295. for (let messageIDX in deletedMessages) {
  2296. let recordID = deletedMessages[messageIDX];
  2297. if (!this.messageRecord[recordID]) continue;
  2298. if (this.messageRecord[recordID].delete_data.hidden) {
  2299. const mIDX = dispatch.messages.findIndex(m => m.id === recordID);
  2300. if (mIDX != -1) dispatch.messages.splice(mIDX, 1);
  2301. continue;
  2302. }
  2303. if (this.messageRecord[recordID].message.channel_id != dispatch.channelId || dispatch.messages.findIndex(m => m.id === recordID) != -1) continue;
  2304. this.antiinfiniteloop = [];
  2305. this.reAddDeletedMessages(dispatch.messages, recordID, deletedMessages, purgedMessages);
  2306. }
  2307. }
  2308. if (this.settings.showPurgedMessages && purgedMessages) {
  2309. for (let messageIDX in purgedMessages) {
  2310. let recordID = purgedMessages[messageIDX];
  2311. if (!this.messageRecord[recordID]) continue;
  2312. if (this.messageRecord[recordID].delete_data.hidden) {
  2313. const mIDX = dispatch.messages.findIndex(m => m.id === recordID);
  2314. if (mIDX != -1) dispatch.messages.splice(mIDX, 1);
  2315. continue;
  2316. }
  2317. if (this.messageRecord[recordID].message.channel_id != dispatch.channelId || dispatch.messages.findIndex(m => m.id == recordID) != -1) continue;
  2318. this.antiinfiniteloop = [];
  2319. this.reAddDeletedMessages(dispatch.messages, recordID, deletedMessages, purgedMessages);
  2320. }
  2321. }
  2322. callDefault(...args);
  2323. this.updateMessages();
  2324. return;
  2325. }
  2326.  
  2327. if (dispatch.type == 'MESSAGE_DELETE') {
  2328. const deleted = this.getCachedMessage(dispatch.id, dispatch.channelId);
  2329.  
  2330. if (this.settings.aggresiveMessageCaching) {
  2331. const channelMessages = this.channelMessages[channel.id];
  2332. if (!channelMessages || !channelMessages.ready) this.cacheChannelMessages(channel.id);
  2333. }
  2334.  
  2335. if (!deleted) return callDefault(...args); // nothing we can do past this point..
  2336.  
  2337. if (this.deletedMessageRecord[channel.id] && this.deletedMessageRecord[channel.id].findIndex(m => m === deleted.id) != -1) {
  2338. if (!this.settings.showDeletedMessages) callDefault(...args);
  2339. return;
  2340. }
  2341.  
  2342. if (deleted.type) return callDefault(...args);
  2343.  
  2344. if (this.settings.showDeletedCount) {
  2345. if (!this.deletedChatMessagesCount[channel.id]) this.deletedChatMessagesCount[channel.id] = 0;
  2346. if (!this.selectedChannel || this.selectedChannel.id != channel.id) this.deletedChatMessagesCount[channel.id]++;
  2347. }
  2348. if (guild ? this.settings.toastToggles.deleted && ((isLocalUser && !this.settings.toastToggles.disableToastsForLocal) || !isLocalUser) : this.settings.toastTogglesDMs.deleted && !isLocalUser) {
  2349. this.showToast(`Message deleted from ${this.getLiteralName(channel.guild_id, channel.id)}`, {
  2350. type: 'error',
  2351. onClick: () => this.openWindow('deleted'),
  2352. onContext: () => this.jumpToMessage(dispatch.channelId, dispatch.id, guild && guild.id),
  2353. timeout: 4500
  2354. });
  2355. }
  2356.  
  2357. const record = this.messageRecord[dispatch.id];
  2358.  
  2359. if ((!this.selectedChannel || this.selectedChannel.id != channel.id) && (guild ? this.settings.toastToggles.ghostPings : this.settings.toastTogglesDMs.ghostPings) && record && !record.ghost_pinged && this.tools.isMentioned(deleted, this.localUser.id)) {
  2360. this.showToast(`You got ghost pinged in ${this.getLiteralName(channel.guild_id, channel.id)}`, {
  2361. type: 'warning',
  2362. onClick: () => this.openWindow('ghostpings'),
  2363. onContext: () => this.jumpToMessage(dispatch.channelId, dispatch.id, guild && guild.id),
  2364. timeout: 4500
  2365. });
  2366. }
  2367.  
  2368. this.saveDeletedMessage(deleted, this.deletedMessageRecord);
  2369. // if (this.settings.cacheAllImages) this.cacheImages(deleted);
  2370. if (!this.settings.showDeletedMessages) callDefault(...args);
  2371. this.saveData();
  2372. } else if (dispatch.type == 'MESSAGE_DELETE_BULK') {
  2373. if (this.settings.showDeletedCount) {
  2374. if (!this.deletedChatMessagesCount[channel.id]) this.deletedChatMessagesCount[channel.id] = 0;
  2375. if (!this.selectedChannel || this.selectedChannel.id != channel.id) this.deletedChatMessagesCount[channel.id] += dispatch.ids.length;
  2376. }
  2377.  
  2378. let failedMessage = false;
  2379.  
  2380. for (let i = 0; i < dispatch.ids.length; i++) {
  2381. const purged = this.getCachedMessage(dispatch.ids[i], channel.id);
  2382. if (!purged) {
  2383. failedMessage = true;
  2384. continue;
  2385. }
  2386. this.saveDeletedMessage(purged, this.purgedMessageRecord);
  2387. }
  2388.  
  2389. if (failedMessage && this.aggresiveMessageCaching)
  2390. // forcefully cache the channel in case there are active convos there
  2391. this.cacheChannelMessages(channel.id);
  2392. else if (this.settings.aggresiveMessageCaching) {
  2393. const channelMessages = this.channelMessages[channel.id];
  2394. if (!channelMessages || !channelMessages.ready) this.cacheChannelMessages(channel.id);
  2395. }
  2396.  
  2397. if (guild ? this.settings.toastToggles.deleted : this.settings.toastTogglesDMs.deleted) {
  2398. this.showToast(`${dispatch.ids.length} messages bulk deleted from ${this.getLiteralName(channel.guild_id, channel.id)}`, {
  2399. type: 'error',
  2400. onClick: () => this.openWindow('purged'),
  2401. onContext: () => this.jumpToMessage(channel.id, undefined, guild && guild.id),
  2402. timeout: 4500
  2403. });
  2404. }
  2405.  
  2406. if (!this.settings.showPurgedMessages) callDefault(...args);
  2407. this.saveData();
  2408. } else if (dispatch.type == 'MESSAGE_UPDATE') {
  2409. if (!dispatch.message.edited_timestamp) {
  2410. let last = this.getCachedMessage(dispatch.message.id);
  2411. if (last) last.embeds = dispatch.message.embeds;
  2412. return callDefault(...args);
  2413. }
  2414.  
  2415. if (this.settings.showEditedCount) {
  2416. if (!this.editedChatMessagesCount[channel.id]) this.editedChatMessagesCount[channel.id] = 0;
  2417. if (!this.selectedChannel || this.selectedChannel.id != channel.id) this.editedChatMessagesCount[channel.id]++;
  2418. }
  2419.  
  2420. if (this.settings.aggresiveMessageCaching) {
  2421. const channelMessages = this.channelMessages[channel.id];
  2422. if (!channelMessages || !channelMessages.ready) this.cacheChannelMessages(channel.id);
  2423. }
  2424.  
  2425. const last = this.getCachedMessage(dispatch.message.id, channel.id);
  2426. const lastEditedSaved = this.getEditedMessage(dispatch.message.id, channel.id);
  2427.  
  2428. // if we have lastEdited then we can still continue as we have all the data we need to process it.
  2429. if (!last && !lastEditedSaved) return callDefault(...args); // nothing we can do past this point..
  2430. let ghostPinged = false;
  2431. if (lastEditedSaved) {
  2432. // last is not needed, we have all the data already saved
  2433. // console.log(lastEditedSaved.message);
  2434. // console.log(dispatch.message);
  2435. if (lastEditedSaved.message.content === dispatch.message.content) {
  2436. if (!this.settings.showEditedMessages) callDefault(...args);
  2437. return; // we don't care about that
  2438. }
  2439. lastEditedSaved.edit_history.push({
  2440. content: lastEditedSaved.message.content,
  2441. time: new Date().getTime()
  2442. });
  2443. lastEditedSaved.message.content = dispatch.message.content;
  2444. ghostPinged = !lastEditedSaved.ghost_pinged && lastEditedSaved.local_mentioned && !this.tools.isMentioned(dispatch.message, this.localUser.id);
  2445. if (ghostPinged) lastEditedSaved.ghost_pinged = true;
  2446. } else {
  2447. if (last.content === dispatch.message.content) {
  2448. if (!this.settings.showEditedMessages) callDefault(...args);
  2449. return; // we don't care about that
  2450. }
  2451. let data = this.createMiniFormattedData(last);
  2452. data.edit_history = [
  2453. {
  2454. content: last.content,
  2455. time: new Date().getTime()
  2456. }
  2457. ];
  2458. ghostPinged = this.tools.isMentioned(last, this.localUser.id) && !this.tools.isMentioned(dispatch.message, this.localUser.id);
  2459. data.message.content = dispatch.message.content;
  2460. if (ghostPinged) data.ghost_pinged = true;
  2461. this.messageRecord[data.message.id] = data;
  2462. if (!this.editedMessageRecord[channel.id]) this.editedMessageRecord[channel.id] = [];
  2463. this.editedMessageRecord[channel.id].push(data.message.id);
  2464. }
  2465.  
  2466. if (guild ? this.settings.toastToggles.edited && ((isLocalUser && !this.settings.toastToggles.disableToastsForLocal) || !isLocalUser) : this.settings.toastTogglesDMs.edited && !isLocalUser) {
  2467. if (!this.settings.blockSpamEdit) {
  2468. if (!this.editHistoryAntiSpam[author.id]) {
  2469. this.editHistoryAntiSpam[author.id] = {
  2470. blocked: false,
  2471. times: [new Date().getTime()]
  2472. };
  2473. } else {
  2474. this.editHistoryAntiSpam[author.id].times.push(new Date().getTime());
  2475. }
  2476. if (this.editHistoryAntiSpam[author.id].times.length > 10) this.editHistoryAntiSpam[author.id].times.shift();
  2477. if (this.editHistoryAntiSpam[author.id].times.length === 10 && new Date().getTime() - this.editHistoryAntiSpam[author.id].times[0] < 60 * 1000) {
  2478. if (!this.editHistoryAntiSpam[author.id].blocked) {
  2479. this.showToast(`Edit notifications from ${author.username} have been temporarily blocked for 1 minute.`, {
  2480. type: 'warning',
  2481. timeout: 7500
  2482. });
  2483. this.editHistoryAntiSpam[author.id].blocked = true;
  2484. }
  2485. } else if (this.editHistoryAntiSpam[author.id].blocked) {
  2486. this.editHistoryAntiSpam[author.id].blocked = false;
  2487. this.editHistoryAntiSpam[author.id].times = [];
  2488. }
  2489. }
  2490. if (this.settings.blockSpamEdit || !this.editHistoryAntiSpam[author.id].blocked) {
  2491. this.showToast(`Message edited in ${this.getLiteralName(channel.guild_id, channel.id)}`, {
  2492. type: 'info',
  2493. onClick: () => this.openWindow('edited'),
  2494. onContext: () => this.jumpToMessage(channel.id, dispatch.message.id, guild && guild.id),
  2495. timeout: 4500
  2496. });
  2497. }
  2498. }
  2499.  
  2500. if ((!this.selectedChannel || this.selectedChannel.id != channel.id) && (guild ? this.settings.toastToggles.ghostPings : this.settings.toastTogglesDMs.ghostPings) && ghostPinged) {
  2501. this.showToast(`You got ghost pinged in ${this.getLiteralName(channel.guild_id, channel.id)}`, {
  2502. type: 'warning',
  2503. onClick: () => this.openWindow('ghostpings'),
  2504. onContext: () => this.jumpToMessage(dispatch.channelId, dispatch.id, guild && guild.id),
  2505. timeout: 4500
  2506. });
  2507. }
  2508.  
  2509. if (!this.settings.showEditedMessages) callDefault(...args);
  2510. else {
  2511. const msg = this.cachedMessageRecord.find(m => m.id == dispatch.message.id);
  2512. if (msg) msg.content = dispatch.message.content;
  2513. // show new edits afterwards, bruh
  2514. if (lastEditedSaved && lastEditedSaved.edits_hidden) callDefault(...args);
  2515. else {
  2516. const cachedChannel = this.channelMessages[dispatch.message.channel_id];
  2517. if (cachedChannel && cachedChannel.ready) {
  2518. const cachedMessage = cachedChannel._map[dispatch.message.id];
  2519. // wtf?
  2520. if (cachedMessage) {
  2521. cachedMessage.content = dispatch.message.content; // probably bad idea but it works
  2522. cachedMessage.contentParsed = this.tools.parse(dispatch.message.content, true, { channelId: dispatch.message.channel_id }); // omega ghetto
  2523. cachedMessage.editedTimestamp = this.tools.createMomentObject(dispatch.message.edited_timestamp); // don't ask how I found this
  2524. }
  2525. }
  2526. }
  2527. }
  2528. this.saveData();
  2529. } else if (dispatch.type == 'MESSAGE_CREATE' && dispatch.message && (dispatch.message.content.length || (dispatch.attachments && dispatch.attachments.length) || (dispatch.embeds && dispatch.embeds.length)) && dispatch.message.state != 'SENDING' && !dispatch.optimistic && !dispatch.message.type) {
  2530. if (this.cachedMessageRecord.findIndex(m => m.id === dispatch.message.id) != -1) return callDefault(...args);
  2531. this.cachedMessageRecord.push(dispatch.message);
  2532.  
  2533. /* if (this.menu.open && this.menu.selectedTab == 'sent') this.refilterMessages(); */
  2534.  
  2535. if (this.settings.aggresiveMessageCaching) {
  2536. const channelMessages = this.channelMessages[channel.id];
  2537. if (!channelMessages || !channelMessages.ready) this.cacheChannelMessages(channel.id);
  2538. }
  2539. if ((guild ? this.settings.toastToggles.sent : this.settings.toastTogglesDMs.sent) && (!this.selectedChannel || this.selectedChannel.id != channel.id)) {
  2540. this.showToast(`Message sent in ${this.getLiteralName(channel.guild_id, channel.id)}`, { type: 'info', onClick: () => this.openWindow('sent'), onContext: () => this.jumpToMessage(channel.id, dispatch.message.id, guild && guild.id), timeout: 4500 });
  2541. }
  2542.  
  2543. callDefault(...args);
  2544. } else callDefault(...args);
  2545. } catch (err) {
  2546. ZeresPluginLibrary.Logger.stacktrace(this.getName(), 'Error in onDispatchEvent', err);
  2547. }
  2548. }
  2549. updateMessages() {
  2550. if (!this.settings.showEditedMessages && !this.settings.showDeletedMessages && !this.settings.showPurgedMessages) return;
  2551. if (!this.selectedChannel) return;
  2552.  
  2553. const chat = document.querySelector('.' + this.classes.chat + ' .' + this.classes.messagesWrapper);
  2554. if (!chat) return;
  2555. const messages = chat.querySelectorAll(this.classes.messages);
  2556.  
  2557. const onClickEditedTag = e => {
  2558. this.menu.filter = `message:${e.target.messageId}`;
  2559. this.openWindow('edited');
  2560. };
  2561.  
  2562. for (let i = 0; i < messages.length; i++) {
  2563. try {
  2564. const message = messages[i];
  2565. const props = ZeresPluginLibrary.ReactTools.getOwnerInstance(message).props;
  2566. const msg = props.message || (props.style.borderRadius == 2 && props.children && props.children._owner && props.children._owner.memoizedProps.message);
  2567. if (!msg) continue;
  2568. const mid = msg.id;
  2569. const channID = msg.channel_id;
  2570. let markup = message.getElementsByClassName(this.classes.markup)[0];
  2571. if (!markup) continue; // gay
  2572. let record = this.messageRecord[mid];
  2573. if (!record) continue;
  2574. const fuckupdiscord = () => {
  2575. /* HACK */
  2576. if (!markup.fucked) {
  2577. const original = markup.cloneNode(true);
  2578. const parent = markup.parentElement;
  2579. markup.remove();
  2580. parent.appendChild(original);
  2581. markup = original;
  2582. markup.fucked = true;
  2583. }
  2584. /* HACK */
  2585. };
  2586. let wasDeleted = false;
  2587. if (message.className.indexOf(this.style.deleted) === -1 && !message.isDeleted && ((this.deletedMessageRecord[channID] && this.deletedMessageRecord[channID].findIndex(m => m === mid) !== -1) || (this.purgedMessageRecord[channID] && this.purgedMessageRecord[channID].findIndex(m => m === mid) != -1))) {
  2588. if (this.getEditedMessage(mid, channID) && this.settings.showEditedMessages) fuckupdiscord();
  2589. const edits = message.getElementsByClassName(this.style.edited);
  2590. for (let i = 0; i < edits.length; i++) if (edits[i].tooltip) edits[i].tooltip.disabled = true;
  2591. message.classList.add(this.style.deleted);
  2592. new ZeresPluginLibrary.EmulatedTooltip(markup.textContent ? markup : message.getElementsByClassName(this.classes.content)[0].lastChild, 'Deleted at ' + this.createTimeStamp(record.delete_data.time), { side: 'left' });
  2593. wasDeleted = true;
  2594. }
  2595. if (!this.settings.showEditedMessages) continue;
  2596. record = this.getEditedMessage(mid, channID);
  2597. if (record && !record.edits_hidden) {
  2598. fuckupdiscord();
  2599. if (markup.edits === record.edit_history.length) continue;
  2600. markup.edits = record.edit_history.length; // todo, randomize
  2601. markup.classList.add(this.style.edited);
  2602. if (markup.touched) markup.touched = false;
  2603. const headerWidth = this.isCompact() && markup.firstChild && markup.firstChild.getBoundingClientRect ? markup.firstChild.lastChild.getBoundingClientRect().width : 0;
  2604. let header;
  2605. if (this.isCompact()) header = markup.firstChild;
  2606. while (markup.firstChild) markup.removeChild(markup.firstChild);
  2607. let e = 0;
  2608. let max = record.edit_history.length;
  2609. if (this.settings.maxShownEdits) {
  2610. if (record.edit_history.length > this.settings.maxShownEdits) {
  2611. if (this.settings.hideNewerEditsFirst) {
  2612. max = this.settings.maxShownEdits;
  2613. } else {
  2614. e = record.edit_history.length - this.settings.maxShownEdits;
  2615. }
  2616. }
  2617. }
  2618. for (; e < max; e++) {
  2619. const editedMarkup = this.formatMarkup(record.edit_history[e].content, record.message.channel_id);
  2620. const editedTag = document.createElement('time');
  2621. editedTag.className = this.multiClasses.edited;
  2622. editedTag.innerText = '(edited)';
  2623. editedTag.messageId = mid;
  2624. editedTag.addEventListener('click', onClickEditedTag);
  2625. editedMarkup.firstChild.appendChild(editedTag);
  2626. editedMarkup.classList.add(this.style.edited);
  2627. editedMarkup.firstChild.classList.remove(this.style.edited);
  2628. editedMarkup.firstChild.editNum = e;
  2629. const time = record.edit_history[e].time; // compatibility
  2630. if (!wasDeleted) editedMarkup.tooltip = new ZeresPluginLibrary.EmulatedTooltip(editedMarkup, 'Edited at ' + (typeof time === 'string' ? time : this.createTimeStamp(time)), { side: 'left' });
  2631. if (this.isCompact()) {
  2632. if (header) {
  2633. markup.classList.add(this.style.markupCompact);
  2634. header.classList.add(this.style.editedCompact);
  2635. editedMarkup.classList.add(this.style.editedCompact);
  2636. markup.appendChild(header);
  2637. header = undefined;
  2638. }
  2639. editedMarkup.style.textIndent = headerWidth + 'px';
  2640. }
  2641. markup.appendChild(editedMarkup);
  2642. }
  2643. // msg.content is latest, keep it up to date
  2644. // we may not have the time of edit, but at least we can force it to save the newest
  2645. if (new Date().getTime() - record.edit_history[record.edit_history.length - 1].time > 60 * 1000) record.message.content = msg.content;
  2646. const editedMarkup = this.formatMarkup(record.message.content, record.message.channel_id);
  2647. editedMarkup.firstChild.classList.remove(this.classes.markup);
  2648. if (this.isCompact()) {
  2649. editedMarkup.style.textIndent = headerWidth + 'px';
  2650. }
  2651. markup.appendChild(editedMarkup);
  2652. } else if (record && record.edits_hidden) {
  2653. if (markup.className.indexOf(this.style.edited) !== -1) {
  2654. markup.edits = 0;
  2655. const headerWidth = this.isCompact() && markup.firstChild && markup.firstChild.getBoundingClientRect ? markup.firstChild.lastChild.getBoundingClientRect().width : 0;
  2656. let header;
  2657. if (this.isCompact()) header = markup.firstChild;
  2658. while (markup.firstChild) markup.removeChild(markup.firstChild);
  2659. const editedMarkup = this.formatMarkup((record.message.content = msg.content), record.message.channel_id);
  2660. const editedTag = document.createElement('time');
  2661. editedTag.className = this.multiClasses.edited;
  2662. editedTag.innerText = '(edited)';
  2663. editedTag.messageId = mid;
  2664. editedTag.addEventListener('click', onClickEditedTag);
  2665. editedMarkup.firstChild.appendChild(editedTag);
  2666. editedMarkup.firstChild.classList.remove(this.classes.markup);
  2667. if (this.isCompact()) {
  2668. markup.classList.add(this.style.markupCompact);
  2669. header.classList.add(this.style.editedCompact);
  2670. editedMarkup.classList.add(this.style.editedCompact);
  2671. markup.appendChild(header);
  2672. editedMarkup.style.textIndent = headerWidth + 'px';
  2673. }
  2674. markup.appendChild(editedMarkup);
  2675. }
  2676. if (markup.touched) continue;
  2677. const editedText = markup.getElementsByClassName(this.multiClasses.edited.split(/ /g)[0])[0];
  2678. if (!editedText) continue; // dunno
  2679. editedText.messageId = mid;
  2680. editedText.addEventListener('click', onClickEditedTag);
  2681. markup.touched = true;
  2682. }
  2683. } catch (e) {
  2684. ZeresPluginLibrary.Logger.stacktrace(this.getName(), 'Error in updateMessages', e);
  2685. }
  2686. }
  2687. }
  2688. /* ==================================================-|| START MENU ||-================================================== */
  2689. processUserRequestQueue() {
  2690. if (!this.processUserRequestQueue.queueIntervalTime) this.processUserRequestQueue.queueIntervalTime = 500;
  2691. if (this.menu.queueInterval) return;
  2692. const messageDataManager = () => {
  2693. if (!this.menu.userRequestQueue.length) {
  2694. clearInterval(this.menu.queueInterval);
  2695. this.menu.queueInterval = 0;
  2696. return;
  2697. }
  2698. const data = this.menu.userRequestQueue.shift();
  2699. this.tools
  2700. .getUserAsync(data.id)
  2701. .then(res => {
  2702. for (let ss of data.success) ss(res);
  2703. })
  2704. .catch(reason => {
  2705. if (reason.status == 429 && typeof reason.body.retry_after === 'number') {
  2706. clearInterval(this.menu.queueInterval);
  2707. this.menu.queueInterval = 0;
  2708. this.processUserRequestQueue.queueIntervalTime += 50;
  2709. setTimeout(messageDataManager, reason.body.retry_after);
  2710. ZeresPluginLibrary.Logger.warn(this.getName(), 'Rate limited, retrying in', reason.body.retry_after, 'ms');
  2711. this.menu.userRequestQueue.push(data);
  2712. return;
  2713. }
  2714. ZeresPluginLibrary.Logger.warn(this.getName(), `Failed to get info for ${data.username}, reason:`, reason);
  2715. for (let ff of data.fail) ff();
  2716. });
  2717. };
  2718. this.menu.queueInterval = setInterval(() => messageDataManager(), this.processUserRequestQueue.queueIntervalTime);
  2719. }
  2720. patchModal() {
  2721. // REQUIRED
  2722. const onChangeOrder = el => {
  2723. this.settings.reverseOrder = !this.settings.reverseOrder;
  2724. el.target.innerText = 'Sort direction: ' + (!this.settings.reverseOrder ? 'new - old' : 'old - new'); // maybe a func?
  2725. this.saveSettings();
  2726. this.refilterMessages();
  2727. };
  2728.  
  2729. const onClearLog = e => {
  2730. e.preventDefault();
  2731. if (document.getElementById(this.style.filter).parentElement.parentElement.className.indexOf(this.createTextBox.classes.focused[0]) != -1) return;
  2732. this.scrollPosition = document.getElementById(this.style.menuMessages).parentElement.parentElement.parentElement.scrollTop;
  2733. let type = this.menu.selectedTab;
  2734. if (type === 'ghostpings') type = 'ghost pings';
  2735. else {
  2736. type += ' messages';
  2737. }
  2738. ZeresPluginLibrary.Modals.showConfirmationModal('Clear log', `Are you sure you want to delete all ${type}${this.menu.filter.length ? ' that also match filter' : ''}?`, {
  2739. confirmText: 'Confirm',
  2740. onConfirm: () => {
  2741. if (this.menu.selectedTab == 'sent') {
  2742. if (!this.menu.filter.length) for (let id of this.menu.messages) this.cachedMessageRecord.splice(this.cachedMessageRecord.findIndex(m => m.id === id), 1);
  2743. else this.cachedMessageRecord.length = 0; // hack, does it cause a memory leak?
  2744. } else {
  2745. for (let id of this.menu.messages) {
  2746. const record = this.messageRecord[id];
  2747. let isSelected = false;
  2748. if (record) {
  2749. this.invalidateChannelCache(record.message.channel_id);
  2750. if (this.selectedChannel) isSelected = record.message.channel_id === this.selectedChannel.id;
  2751. }
  2752. this.deleteMessageFromRecords(id);
  2753. if (this.selectedChannel && isSelected) this.cacheChannelMessages(this.selectedChannel.id);
  2754. }
  2755. this.saveData();
  2756. }
  2757. this.menu.refilterOnMount = true;
  2758. }
  2759. });
  2760. };
  2761.  
  2762. this.unpatches.push(
  2763. ZeresPluginLibrary.Patcher.instead(this.getName(), ZeresPluginLibrary.DiscordModules.ConfirmationModal.prototype, 'handleClose', (thisObj, args, original) => {
  2764. if (thisObj.props.ml2Data) onChangeOrder(args[0]);
  2765. else original(...args);
  2766. })
  2767. );
  2768.  
  2769. this.unpatches.push(
  2770. ZeresPluginLibrary.Patcher.instead(this.getName(), ZeresPluginLibrary.DiscordModules.ConfirmationModal.prototype, 'handleSubmit', (thisObj, args, original) => {
  2771. if (thisObj.props.ml2Data) onClearLog(args[0]);
  2772. else original(...args);
  2773. })
  2774. );
  2775.  
  2776. this.unpatches.push(
  2777. ZeresPluginLibrary.Patcher.instead(this.getName(), ZeresPluginLibrary.DiscordModules.ConfirmationModal.prototype, 'componentDidMount', (thisObj, args, original) => {
  2778. if (thisObj.props.ml2Data) {
  2779. if (this.menu.refilterOnMount) {
  2780. this.refilterMessages();
  2781. this.menu.refilterOnMount = false;
  2782. }
  2783. document.getElementById(this.style.menuMessages).parentElement.parentElement.parentElement.scrollTop = this.scrollPosition;
  2784. }
  2785. return original(...args);
  2786. })
  2787. );
  2788. }
  2789. // >>-|| POPULATION ||-<<
  2790. createMessageGroup(message) {
  2791. let deleted = false;
  2792. let edited = false;
  2793. let details = 'Sent in';
  2794. let channel = this.tools.getChannel(message.channel_id);
  2795. let timestamp = message.timestamp;
  2796. let author = this.tools.getUser(message.author.id);
  2797. let noUserInfo = false;
  2798. let userInfoBeingRequested = true;
  2799. const isBot = message.author.bot;
  2800. const record = this.messageRecord[message.id];
  2801. if (record) {
  2802. deleted = !!record.delete_data;
  2803. edited = !!record.edit_history;
  2804.  
  2805. if (deleted && edited) {
  2806. details = 'Edited and deleted from';
  2807. timestamp = record.delete_data.time;
  2808. } else if (deleted) {
  2809. details = 'Deleted from';
  2810. timestamp = record.delete_data.time;
  2811. } else if (edited) {
  2812. details = 'Last edit in'; // todo: purged?
  2813. if (typeof record.edit_history[record.edit_history.length - 1].time !== 'string') timestamp = record.edit_history[record.edit_history.length - 1].time;
  2814. }
  2815. }
  2816.  
  2817. details += ` ${this.getLiteralName(message.guild_id || (channel && channel.guild_id), message.channel_id)} `;
  2818.  
  2819. details += `at ${this.createTimeStamp(timestamp, true)}`;
  2820.  
  2821. const classes = this.createMessageGroup.classes;
  2822. const getAvatarOf = user => {
  2823. if (!user.avatar) return '/assets/322c936a8c8be1b803cd94861bdfa868.png';
  2824. return `https://cdn.discordapp.com/avatars/${user.id}/${user.avatar}.png?size=128`;
  2825. };
  2826.  
  2827. // minify?
  2828. // what is wrapper-3t9DeA and how do I get it
  2829. // wtf is avatar-VxgULZ
  2830. const element = this.parseHTML(`<div class="${classes.containerBounded}" style="padding-bottom: 25px; padding-top: 0px">
  2831. <div>
  2832. <div class="${classes.header}">
  2833. <div class="${classes.avatar}">
  2834. <div class="wrapper-3t9DeA da-wrapper" role="img" style="width: 40px; height: 40px;">
  2835. <img src="${getAvatarOf(message.author)}" class="${classes.avatarImg}" aria-hidden="true" style="border-radius: 100%;">
  2836. </div>
  2837. </div>
  2838. <h2 class="${classes.headerMeta}">
  2839. <span class="">
  2840. <span tabindex="0" class="${classes.username}" role="button">
  2841. ${message.author.username}
  2842. </span>
  2843. ${(isBot &&
  2844. `<span class="${classes.botTag}">
  2845. BOT
  2846. </span>`) ||
  2847. ''}
  2848. </span>
  2849. <div class="${classes.timestamp}" style="display:inline">
  2850. ${details}
  2851. </div>
  2852. </h2>
  2853. </div>
  2854. <div class="${classes.content}">
  2855. </div>
  2856. </div>
  2857. </div>`);
  2858. const profImg = element.getElementsByClassName(classes.avatarImgSingle)[0];
  2859. profImg.onerror = () => {
  2860. profImg.src = '/assets/322c936a8c8be1b803cd94861bdfa868.png';
  2861. };
  2862. const verifyProfilePicture = () => {
  2863. if (message.author.avatar != author.avatar && author.avatar) {
  2864. profImg.src = getAvatarOf(author);
  2865. if (record) {
  2866. record.message.author.avatar = author.avatar;
  2867. }
  2868. } else {
  2869. if (record) record.message.author.avatar = null;
  2870. }
  2871. };
  2872. if (!isBot) {
  2873. if (!author) {
  2874. author = message.author;
  2875. if (this.menu.userRequestQueue.findIndex(m => m.id === author.id) == -1) {
  2876. this.menu.userRequestQueue.push({
  2877. id: author.id,
  2878. username: author.username,
  2879. success: [
  2880. res => {
  2881. author = $.extend(true, {}, res);
  2882. verifyProfilePicture();
  2883. userInfoBeingRequested = false;
  2884. }
  2885. ],
  2886. fail: [
  2887. () => {
  2888. noUserInfo = true;
  2889. userInfoBeingRequested = false;
  2890. }
  2891. ]
  2892. });
  2893. } else {
  2894. const dt = this.menu.userRequestQueue.find(m => m.id === author.id);
  2895. dt.success.push(res => {
  2896. author = $.extend(true, {}, res);
  2897. verifyProfilePicture();
  2898. userInfoBeingRequested = false;
  2899. });
  2900. dt.fail.push(() => {
  2901. noUserInfo = true;
  2902. userInfoBeingRequested = false;
  2903. });
  2904. }
  2905. } else {
  2906. userInfoBeingRequested = false;
  2907. verifyProfilePicture();
  2908. }
  2909. }
  2910. const profIcon = element.getElementsByClassName(classes.avatarSingle)[0];
  2911. profIcon.addEventListener('click', () => {
  2912. if (isBot) return this.showToast('User is a bot, this action is not possible on a bot.', { type: 'error', timeout: 5000 });
  2913. if (userInfoBeingRequested) return this.showToast('Please wait, user profile is being requested', { type: 'info', timeout: 5000 });
  2914. if (noUserInfo) return this.showToast('Could not get user info!', { type: 'error' });
  2915. this.scrollPosition = document.getElementById(this.style.menuMessages).parentElement.parentElement.parentElement.scrollTop;
  2916. ZeresPluginLibrary.Popouts.showUserPopout(profIcon, author);
  2917. });
  2918. profIcon.addEventListener('contextmenu', e => {
  2919. if (isBot) return this.showToast('User is a bot, this action is not possible on a bot.', { type: 'error', timeout: 5000 });
  2920. if (userInfoBeingRequested) return this.showToast('Please wait, user profile is being requested', { type: 'info', timeout: 5000 });
  2921. if (noUserInfo) return this.showToast('Could not get user info! You can only delete or copy to clipboard!', { timeout: 5000 });
  2922. ZeresPluginLibrary.WebpackModules.getByProps('openUserContextMenu').openUserContextMenu(e, author, channel || this.menu.randomValidChannel);
  2923. });
  2924. const nameLink = element.getElementsByClassName(classes.username.split(/ /g)[0])[0];
  2925. nameLink.addEventListener('click', () => {
  2926. if (isBot) return this.showToast('User is a bot, this action is not possible on a bot.', { type: 'error', timeout: 5000 });
  2927. if (userInfoBeingRequested) return this.showToast('Please wait, user profile is being requested', { type: 'info', timeout: 5000 });
  2928. if (noUserInfo) return this.showToast('Could not get user info!', { type: 'error' });
  2929. this.scrollPosition = document.getElementById(this.style.menuMessages).parentElement.parentElement.parentElement.scrollTop; // todo: turn this into a function
  2930. ZeresPluginLibrary.Popouts.showUserPopout(nameLink, author);
  2931. });
  2932. nameLink.addEventListener('contextmenu', e => {
  2933. if (isBot) return this.showToast('User is a bot, this action is not possible on a bot.', { type: 'error', timeout: 5000 });
  2934. if (userInfoBeingRequested) return this.showToast('Please wait, user profile is being requested', { type: 'info', timeout: 5000 });
  2935. if (noUserInfo) return this.showToast('Could not get user info! You can only delete or copy to clipboard!', { type: 'error', timeout: 5000 });
  2936. ZeresPluginLibrary.WebpackModules.getByProps('openUserContextMenu').openUserContextMenu(e, author, channel || this.menu.randomValidChannel);
  2937. });
  2938. const timestampEl = element.getElementsByClassName(classes.timestampSingle)[0];
  2939. timestampEl.addEventListener('contextmenu', e => {
  2940. const messages = element.querySelectorAll(`div[class] div[class] div .${classes.markupSingle}`);
  2941. if (!messages.length) return;
  2942. const messageIds = [];
  2943. for (let i = 0; i < messages.length; i++) if (messages[i] && messages[i].messageId) messageIds.push(messages[i].messageId);
  2944. if (!messageIds.length) return;
  2945. const menu = new ZeresPluginLibrary.ContextMenu.Menu();
  2946. const remove = new ZeresPluginLibrary.ContextMenu.TextItem('Remove Group From Log');
  2947. const copyMessage = new ZeresPluginLibrary.ContextMenu.TextItem('Copy Formatted Messages');
  2948. remove.element.addEventListener('click', () => {
  2949. menu.removeMenu();
  2950. let invalidatedChannelCache = false;
  2951. for (let msgid of messageIds) {
  2952. const record = this.messageRecord[msgid];
  2953. if (!record) continue; // the hell
  2954. if ((record.edit_history && !record.edits_hidden) || (record.delete_data && !record.delete_data.hidden)) this.invalidateChannelCache((invalidatedChannelCache = record.message.channel_id));
  2955. this.deleteMessageFromRecords(msgid);
  2956. }
  2957. if (invalidatedChannelCache) this.cacheChannelMessages(invalidatedChannelCache);
  2958. this.refilterMessages(); // I don't like calling that, maybe figure out a way to animate it collapsing on itself smoothly
  2959. this.saveData();
  2960. });
  2961. copyMessage.element.addEventListener('click', () => {
  2962. let result = '';
  2963. for (let msgid of messageIds) {
  2964. const record = this.messageRecord[msgid];
  2965. if (!record) continue;
  2966. if (!result.length) result += `> **${record.message.author.username}** | ${this.createTimeStamp(record.message.timestamp, true)}\n`;
  2967. result += `> ${record.message.content.replace(/\n/g, '\n> ')}\n`;
  2968. }
  2969. this.nodeModules.electron.clipboard.writeText(result);
  2970. this.showToast('Copied!', { type: 'success' });
  2971. menu.removeMenu();
  2972. });
  2973. menu.addItems(copyMessage, remove);
  2974. menu.element.style.position = 'absolute';
  2975. menu.element.style.zIndex = '99999';
  2976. for (let ch of menu.element.children) ch.addClass('clickable-11uBi- da-clickable'); // HACK
  2977. menu.show(e.clientX, e.clientY);
  2978. });
  2979. timestampEl.addEventListener('click', e => {
  2980. if (!this.menu.deleteKeyDown) return;
  2981. const messages = element.querySelectorAll(`div[class] div[class] div .${classes.markupSingle}`);
  2982. if (!messages.length) return;
  2983. const messageIds = [];
  2984. for (let i = 0; i < messages.length; i++) if (messages[i] && messages[i].messageId) messageIds.push(messages[i].messageId);
  2985. if (!messageIds.length) return;
  2986. let invalidatedChannelCache = false;
  2987. for (let msgid of messageIds) {
  2988. const record = this.messageRecord[msgid];
  2989. if (!record) continue; // the hell
  2990. if ((record.edit_history && !record.edits_hidden) || (record.delete_data && !record.delete_data.hidden)) this.invalidateChannelCache((invalidatedChannelCache = record.message.channel_id));
  2991. this.deleteMessageFromRecords(msgid);
  2992. }
  2993. if (invalidatedChannelCache) this.cacheChannelMessages(invalidatedChannelCache);
  2994. this.refilterMessages(); // I don't like calling that, maybe figure out a way to animate it collapsing on itself smoothly
  2995. this.saveData();
  2996. });
  2997. const messageContext = e => {
  2998. let target = e.target;
  2999. if ((!target.classList.contains(this.classes.containerBounded) && !target.classList.contains('mention')) || (target.tagName == 'DIV' && target.classList.contains(ZeresPluginLibrary.WebpackModules.getByProps('imageError').imageError.split(/ /g)[0]))) {
  3000. let isMarkup = false;
  3001. let isEdited = false;
  3002. let isBadImage = target.tagName == 'DIV' && target.classList == ZeresPluginLibrary.WebpackModules.getByProps('imageError').imageError;
  3003. if (!isBadImage) {
  3004. while (!target.classList.contains(this.classes.containerBounded) && !(isMarkup = target.classList.contains(this.classes.markup))) {
  3005. if (target.classList.contains(this.style.edited)) isEdited = target;
  3006. target = target.parentElement;
  3007. }
  3008. }
  3009.  
  3010. if (isMarkup || isBadImage) {
  3011. const messageId = target.messageId;
  3012. if (!messageId) return;
  3013. const record = this.getSavedMessage(messageId);
  3014. if (!record) return;
  3015. let editNum = -1;
  3016. if (isEdited) editNum = isEdited.edit;
  3017. const menu = new ZeresPluginLibrary.ContextMenu.Menu();
  3018. if (channel) {
  3019. let jump = new ZeresPluginLibrary.ContextMenu.TextItem('Jump to Message');
  3020. jump.element.addEventListener('click', () => {
  3021. this.jumpToMessage(message.channel_id, messageId, message.guild_id);
  3022. menu.removeMenu();
  3023. return;
  3024. });
  3025. menu.addItems(jump);
  3026. }
  3027. let remove = new ZeresPluginLibrary.ContextMenu.TextItem('Remove From Log');
  3028. let copyId = new ZeresPluginLibrary.ContextMenu.TextItem('Copy Message ID');
  3029. remove.element.addEventListener('click', () => {
  3030. let invalidatedChannelCache = false;
  3031. if ((record.edit_history && !record.edits_hidden) || (record.delete_data && !record.delete_data.hidden)) this.invalidateChannelCache((invalidatedChannelCache = record.message.channel_id));
  3032. this.deleteMessageFromRecords(messageId);
  3033. this.refilterMessages(); // I don't like calling that, maybe figure out a way to animate it collapsing on itself smoothly
  3034. menu.removeMenu();
  3035. if (invalidatedChannelCache) this.cacheChannelMessages(invalidatedChannelCache);
  3036. this.saveData();
  3037. });
  3038. if (!isBadImage || record.message.content.length) {
  3039. let copyText = new ZeresPluginLibrary.ContextMenu.TextItem('Copy Text');
  3040. let copyMessage = new ZeresPluginLibrary.ContextMenu.TextItem('Copy Formatted Message');
  3041. copyText.element.addEventListener('click', () => {
  3042. this.nodeModules.electron.clipboard.writeText(editNum != -1 ? record.edit_history[editNum].content : record.message.content);
  3043. this.showToast('Copied!', { type: 'success' });
  3044. menu.removeMenu();
  3045. });
  3046. copyMessage.element.addEventListener('click', () => {
  3047. const content = editNum != -1 ? record.edit_history[editNum].content : record.message.content;
  3048. const result = `> **${record.message.author.username}** | ${this.createTimeStamp(record.message.timestamp, true)}\n> ${content.replace(/\n/g, '\n> ')}`;
  3049. this.nodeModules.electron.clipboard.writeText(result);
  3050. this.showToast('Copied!', { type: 'success' });
  3051. menu.removeMenu();
  3052. });
  3053. menu.addItems(copyText, copyMessage);
  3054. }
  3055. copyId.element.addEventListener('click', () => {
  3056. this.nodeModules.electron.clipboard.writeText(messageId); // todo: store electron or writeText somewhere?
  3057. this.showToast('Copied!', { type: 'success' });
  3058. menu.removeMenu();
  3059. });
  3060. if (record.delete_data && record.delete_data.hidden) {
  3061. let unhideDelete = new ZeresPluginLibrary.ContextMenu.TextItem('Unhide Deleted Message');
  3062. unhideDelete.element.addEventListener('click', () => {
  3063. record.delete_data.hidden = false;
  3064. this.invalidateChannelCache(record.message.channel_id); // good idea?
  3065. this.cacheChannelMessages(record.message.channel_id);
  3066. this.saveData();
  3067. this.showToast('Unhidden!', { type: 'success' });
  3068. menu.removeMenu();
  3069. });
  3070. menu.addItems(unhideDelete);
  3071. }
  3072. if (record.edit_history) {
  3073. if (editNum != -1) {
  3074. let deleteEdit = new ZeresPluginLibrary.ContextMenu.TextItem('Delete Edit');
  3075. deleteEdit.element.addEventListener('click', () => {
  3076. this.deleteEditedMessageFromRecord(messageId, editNum);
  3077. this.refilterMessages(); // I don't like calling that, maybe figure out a way to animate it collapsing on itself smoothly
  3078. this.showToast('Deleted!', { type: 'success' });
  3079. menu.removeMenu();
  3080. });
  3081. menu.addItems(deleteEdit);
  3082. }
  3083. if (record.edits_hidden) {
  3084. let unhideEdits = new ZeresPluginLibrary.ContextMenu.TextItem('Unhide Edits');
  3085. unhideEdits.element.addEventListener('click', () => {
  3086. record.edits_hidden = false;
  3087. this.saveData();
  3088. this.showToast('Unhidden!', { type: 'success' });
  3089. menu.removeMenu();
  3090. });
  3091. menu.addItems(unhideEdits);
  3092. }
  3093. }
  3094. menu.addItems(remove, copyId);
  3095. menu.element.style.position = 'absolute';
  3096. menu.element.style.zIndex = '99999';
  3097. for (let ch of menu.element.children) ch.addClass('clickable-11uBi- da-clickable'); // HACK
  3098. menu.show(e.clientX, e.clientY);
  3099. return;
  3100. }
  3101. }
  3102. };
  3103. element.querySelector(classes.message).addEventListener('contextmenu', e => messageContext(e));
  3104. element.querySelector(classes.message).addEventListener('click', e => {
  3105. if (!this.menu.deleteKeyDown) return;
  3106. let target = e.target;
  3107. if (!target.classList.contains(this.classes.containerBounded)) {
  3108. let isMarkup = false;
  3109. let isEdited = false;
  3110. let isBadImage = target.tagName == 'DIV' && target.classList == ZeresPluginLibrary.WebpackModules.getByProps('imageError').imageError;
  3111. if (!isBadImage) {
  3112. while (!target.classList.contains(this.classes.containerBounded) && !(isMarkup = target.classList.contains(this.classes.markup))) {
  3113. if (target.classList.contains(this.style.edited)) isEdited = target;
  3114. target = target.parentElement;
  3115. }
  3116. }
  3117. if (!isMarkup && !isBadImage) return;
  3118. const messageId = target.messageId;
  3119. if (!messageId) return;
  3120. const record = this.messageRecord[messageId];
  3121. if (!record) return;
  3122. this.invalidateChannelCache(record.message.channel_id); // good idea?
  3123. this.cacheChannelMessages(record.message.channel_id);
  3124. if (isEdited) {
  3125. this.deleteEditedMessageFromRecord(messageId, isEdited.edit);
  3126. } else {
  3127. this.deleteMessageFromRecords(messageId);
  3128. }
  3129. this.refilterMessages(); // I don't like calling that, maybe figure out a way to animate it collapsing on itself smoothly
  3130. this.saveData();
  3131. }
  3132. });
  3133. return element;
  3134. }
  3135. populateParent(parent, messages) {
  3136. let lastMessage;
  3137. let lastType; /* unused */
  3138. let messageGroup;
  3139. const registerMentions = markup => {
  3140. const mentions = markup.getElementsByClassName('mention');
  3141. for (let mention of mentions) {
  3142. mention.addEventListener('click', () => {
  3143. this.scrollPosition = document.getElementById(this.style.menuMessages).parentElement.parentElement.parentElement.scrollTop;
  3144. });
  3145. }
  3146. };
  3147. const populate = i => {
  3148. try {
  3149. // todo: maybe make the text red if it's deleted?
  3150. const messageId = messages[i];
  3151. const record = this.getSavedMessage(messageId);
  3152. const message = record ? record.message : this.getMessageAny(messageId);
  3153. if (!message) return;
  3154. // todo: get type and use it
  3155. if (!messageGroup /* || !lastType */ || !lastMessage || lastMessage.channel_id != message.channel_id || lastMessage.author.id != message.author.id || new Date(message.timestamp).getDate() !== new Date(lastMessage.timestamp).getDate() || (message.attachments.length && message.content.length)) {
  3156. messageGroup = this.createMessageGroup(message);
  3157. }
  3158. lastMessage = message;
  3159. const messageContent = messageGroup.getElementsByClassName(this.multiClasses.message.cozy.content.split(/ /g)[0])[0];
  3160. const div = document.createElement('div');
  3161. const divParent = document.createElement('div');
  3162. div.className = this.multiClasses.markup;
  3163. div.messageId = messageId;
  3164. if (record && record.edit_history) {
  3165. div.classList.add(this.style.edited);
  3166. for (let ii = 0; ii < record.edit_history.length; ii++) {
  3167. const hist = record.edit_history[ii];
  3168. const editedMarkup = this.formatMarkup(hist.content, message.channel_id).firstChild;
  3169. editedMarkup.insertAdjacentHTML('beforeend', `<time class="${this.multiClasses.edited}">(edited)</time>`); // TODO, change this
  3170. editedMarkup.tooltip = new ZeresPluginLibrary.EmulatedTooltip(editedMarkup, 'Edited at ' + (typeof hist.time === 'string' ? hist.time : this.createTimeStamp(hist.time)), { side: 'left' });
  3171. editedMarkup.classList.add(this.style.edited);
  3172. editedMarkup.edit = ii;
  3173. registerMentions(editedMarkup);
  3174. div.appendChild(editedMarkup);
  3175. }
  3176. }
  3177. const contentMarkup = this.formatMarkup(message.content, message.channel_id).firstChild;
  3178. contentMarkup.messageId = message.id;
  3179. registerMentions(contentMarkup);
  3180. div.appendChild(contentMarkup);
  3181. if (!record) {
  3182. const channel = this.tools.getChannel(message.channel_id);
  3183. const guild = this.tools.getServer(channel && channel.guild_id);
  3184. divParent.addEventListener('click', () => this.jumpToMessage(message.channel_id, message.id, guild && guild.id));
  3185. }
  3186. divParent.appendChild(div);
  3187. // todo, embeds
  3188. // how do I do embeds?
  3189.  
  3190. // why don't attachments show for sent messages? what's up with that?
  3191. if (message.attachments.length) {
  3192. // const attachmentsContent = this.parseHTML(`<div class="${this.multiClasses.message.cozy.content}"></div>`);
  3193. const attemptToUseCachedImage = (attachmentId, attachmentIdx, hidden) => {
  3194. const cachedImage = this.imageCacheRecord[attachmentId];
  3195. if (!cachedImage) return false;
  3196. const img = document.createElement('img');
  3197. img.classList = ZeresPluginLibrary.WebpackModules.getByProps('clickable').clickable;
  3198. /* img.classList = ZeresPluginLibrary.WebpackModules.getByProps('clickable').clickable; */
  3199. img.messageId = messageId;
  3200. img.idx = attachmentIdx;
  3201. img.id = attachmentId; // USED FOR FINDING THE IMAGE THRU CONTEXT MENUS
  3202. if (hidden) {
  3203. img.src = `https://i.clouds.tf/q2vy/r8q6.png#${record.message.channel_id},${img.id}`;
  3204. img.width = 200;
  3205. } else {
  3206. img.src = cachedImage.thumbnail; // has channel and attachment id embedded
  3207. img.width = 256;
  3208. }
  3209. img.addEventListener('click', e => {
  3210. if (!this.menu.deleteKeyDown) return this.nodeModules.electron.shell.openExternal(`file://${this.imageCacheDir}/${attachmentId}${cachedImage.extension}`);
  3211. this.deleteMessageFromRecords(messageId);
  3212. this.refilterMessages(); // I don't like calling that, maybe figure out a way to animate it collapsing on itself smoothly
  3213. this.saveData();
  3214. });
  3215. divParent.appendChild(img);
  3216. return true;
  3217. };
  3218. const handleCreateImage = (attachment, idx) => {
  3219. if (attachment.url == 'ERROR') {
  3220. if (attemptToUseCachedImage(attachment.id, idx, attachment.hidden)) return;
  3221. const imageErrorDiv = document.createElement('div');
  3222. imageErrorDiv.classList = ZeresPluginLibrary.WebpackModules.getByProps('imageError').imageError;
  3223. imageErrorDiv.messageId = messageId;
  3224. divParent.appendChild(imageErrorDiv);
  3225. } else {
  3226. if (!this.isImage(attachment.url)) return; // bruh
  3227. const img = document.createElement('img');
  3228. img.classList = ZeresPluginLibrary.WebpackModules.getByProps('clickable').clickable;
  3229. img.messageId = messageId;
  3230. img.id = attachment.id; // USED FOR FINDING THE IMAGE THRU CONTEXT MENUS
  3231. img.idx = idx;
  3232. if (attachment.hidden) {
  3233. img.src = `https://i.clouds.tf/q2vy/r8q6.png#${record.message.channel_id},${img.id}`;
  3234. img.width = 200;
  3235. } else {
  3236. img.src = attachment.url;
  3237. img.width = this.clamp(attachment.width, 200, 650);
  3238. }
  3239. // img.style.minHeight = '104px'; // bruh?
  3240. if (record) {
  3241. img.addEventListener('click', () => {
  3242. if (this.menu.deleteKeyDown) {
  3243. this.deleteMessageFromRecords(messageId);
  3244. this.refilterMessages(); // I don't like calling that, maybe figure out a way to animate it collapsing on itself smoothly
  3245. this.saveData();
  3246. return;
  3247. }
  3248. this.scrollPosition = document.getElementById(this.style.menuMessages).parentElement.parentElement.parentElement.scrollTop;
  3249. this.createModal(
  3250. {
  3251. src: attachment.url + '?ML2=true', // self identify
  3252. placeholder: attachment.url, // cute image here
  3253. original: attachment.url,
  3254. width: attachment.width,
  3255. height: attachment.height,
  3256. onClickUntrusted: e => e.openHref()
  3257. },
  3258. true
  3259. );
  3260. });
  3261. }
  3262. img.onerror = () => {
  3263. img.src = '/assets/e0c782560fd96acd7f01fda1f8c6ff24.svg';
  3264. img.width = 200;
  3265. if (record) {
  3266. this.nodeModules.request.head(attachment.url, (err, res) => {
  3267. if (err || res.statusCode != 404) return;
  3268. record.message.attachments[idx].url = 'ERROR';
  3269. attemptToUseCachedImage(attachment.id, idx, attachment.hidden);
  3270. });
  3271. }
  3272. };
  3273. divParent.appendChild(img);
  3274. }
  3275. };
  3276. for (let ii = 0; ii < message.attachments.length; ii++) handleCreateImage(message.attachments[ii], ii);
  3277. }
  3278. if (message.embeds && message.embeds.length) {
  3279. const ddiv = document.createElement('div');
  3280. // TODO: optimize
  3281. ddiv.className = ZLibrary.WebpackModules.getByProps('containerCozy', 'gifFavoriteButton').containerCozy;
  3282. for (const embed of message.embeds) {
  3283. const embedBase = {
  3284. GIFVComponent: ZLibrary.WebpackModules.getByDisplayName('LazyGIFV'),
  3285. ImageComponent: ZLibrary.WebpackModules.getByDisplayName('LazyImageZoomable'),
  3286. LinkComponent: ZLibrary.WebpackModules.getByDisplayName('MaskedLink'),
  3287. VideoComponent: ZLibrary.WebpackModules.getByDisplayName('LazyVideo'),
  3288. allowFullScreen: true,
  3289. autoPlayGif: true,
  3290. backgroundOpacity: '',
  3291. className: ZLibrary.WebpackModules.getByProps('embedWrapper', 'gifFavoriteButton').embedWrapper,
  3292. embed: ZLibrary.WebpackModules.getByProps('sanitizeEmbed').sanitizeEmbed(message.channel_id, message.id, embed),
  3293. hideMedia: false,
  3294. inlineGIFV: true,
  3295. maxMediaHeight: 300,
  3296. maxMediaWidth: 400,
  3297. maxThumbnailHeight: 80,
  3298. maxThumbnailWidth: 80,
  3299. suppressEmbed: false
  3300. };
  3301. ZLibrary.DiscordModules.ReactDOM.render(ZLibrary.DiscordModules.React.createElement(ZLibrary.WebpackModules.getByDisplayName('Embed'), embedBase), ddiv);
  3302. }
  3303. divParent.appendChild(ddiv);
  3304. }
  3305. if (divParent.childElementCount < 2 && !message.content.length) return; // don't bother
  3306. messageContent.appendChild(divParent);
  3307. parent.appendChild(messageGroup);
  3308. } catch (err) {
  3309. ZeresPluginLibrary.Logger.stacktrace(this.getName(), 'Error in populateParent', err);
  3310. }
  3311. };
  3312. let i = 0;
  3313. const addMore = () => {
  3314. for (let added = 0; i < messages.length && (added < this.settings.renderCap || (this.menu.shownMessages != -1 && i < this.menu.shownMessages)); i++, added++) populate(i);
  3315. handleMoreMessages();
  3316. this.menu.shownMessages = i;
  3317. };
  3318. const handleMoreMessages = () => {
  3319. if (i < messages.length) {
  3320. const div = document.createElement('div');
  3321. const moreButton = this.createButton('LOAD MORE', function() {
  3322. this.parentElement.remove();
  3323. addMore();
  3324. });
  3325. moreButton.style.width = '100%';
  3326. moreButton.style.marginBottom = '20px';
  3327. div.appendChild(moreButton);
  3328. parent.appendChild(div);
  3329. }
  3330. };
  3331.  
  3332. if (this.settings.renderCap) addMore();
  3333. else for (; i < messages.length; i++) populate(i);
  3334. this.processUserRequestQueue();
  3335. }
  3336. // >>-|| FILTERING ||-<<
  3337. sortMessagesByAge(map) {
  3338. // sort direction: new - old
  3339. map.sort((a, b) => {
  3340. const recordA = this.messageRecord[a];
  3341. const recordB = this.messageRecord[b];
  3342. if (!recordA || !recordB) return 0;
  3343. let timeA = new Date(recordA.message.timestamp).getTime();
  3344. let timeB = new Date(recordB.message.timestamp).getTime();
  3345. if (recordA.edit_history && typeof recordA.edit_history[recordA.edit_history.length - 1].time !== 'string') timeA = recordA.edit_history[recordA.edit_history.length - 1].time;
  3346. if (recordB.edit_history && typeof recordB.edit_history[recordB.edit_history.length - 1].time !== 'string') timeB = recordB.edit_history[recordB.edit_history.length - 1].time;
  3347. if (recordA.delete_data && recordA.delete_data.time) timeA = recordA.delete_data.time;
  3348. if (recordB.delete_data && recordB.delete_data.time) timeB = recordB.delete_data.time;
  3349. return parseInt(timeB) - parseInt(timeA);
  3350. });
  3351. }
  3352. getFilteredMessages() {
  3353. let messages = [];
  3354.  
  3355. const pushIdsIntoMessages = map => {
  3356. for (let channel in map) {
  3357. for (let messageIdIDX in map[channel]) {
  3358. messages.push(map[channel][messageIdIDX]);
  3359. }
  3360. }
  3361. };
  3362. const checkIsMentioned = map => {
  3363. for (let channel in map) {
  3364. for (let messageIdIDX in map[channel]) {
  3365. const messageId = map[channel][messageIdIDX];
  3366. const record = this.getSavedMessage(messageId);
  3367. if (!record) continue;
  3368. if (record.ghost_pinged) {
  3369. messages.push(messageId);
  3370. }
  3371. }
  3372. }
  3373. };
  3374.  
  3375. if (this.menu.selectedTab == 'sent') {
  3376. for (let i of this.cachedMessageRecord) {
  3377. messages.push(i.id);
  3378. }
  3379. }
  3380. if (this.menu.selectedTab == 'edited') pushIdsIntoMessages(this.editedMessageRecord);
  3381. if (this.menu.selectedTab == 'deleted') pushIdsIntoMessages(this.deletedMessageRecord);
  3382. if (this.menu.selectedTab == 'purged') pushIdsIntoMessages(this.purgedMessageRecord);
  3383. if (this.menu.selectedTab == 'ghostpings') {
  3384. checkIsMentioned(this.deletedMessageRecord);
  3385. checkIsMentioned(this.editedMessageRecord);
  3386. checkIsMentioned(this.purgedMessageRecord);
  3387. }
  3388.  
  3389. const filters = this.menu.filter.split(',');
  3390.  
  3391. for (let i = 0; i < filters.length; i++) {
  3392. const split = filters[i].split(':');
  3393. if (split.length < 2) continue;
  3394.  
  3395. const filterType = split[0].trim().toLowerCase();
  3396. const filter = split[1].trim().toLowerCase();
  3397.  
  3398. if (filterType == 'server' || filterType == 'guild')
  3399. messages = messages.filter(x => {
  3400. const message = this.getMessageAny(x);
  3401. if (!message) return false;
  3402. const channel = this.tools.getChannel(message.channel_id);
  3403. const guild = this.tools.getServer(message.guild_id || (channel && channel.guild_id));
  3404. return (message.guild_id || (channel && channel.guild_id)) == filter || (guild && guild.name.toLowerCase().includes(filter.toLowerCase()));
  3405. });
  3406.  
  3407. if (filterType == 'channel')
  3408. messages = messages.filter(x => {
  3409. const message = this.getMessageAny(x);
  3410. if (!message) return false;
  3411. const channel = this.tools.getChannel(message.channel_id);
  3412. return message.channel_id == filter || (channel && channel.name.toLowerCase().includes(filter.replace('#', '').toLowerCase()));
  3413. });
  3414.  
  3415. if (filterType == 'message' || filterType == 'content')
  3416. messages = messages.filter(x => {
  3417. const message = this.getMessageAny(x);
  3418. return x == filter || (message && message.content.toLowerCase().includes(filter.toLowerCase()));
  3419. });
  3420.  
  3421. if (filterType == 'user')
  3422. messages = messages.filter(x => {
  3423. const message = this.getMessageAny(x);
  3424. if (!message) return false;
  3425. const channel = this.tools.getChannel(message.channel_id);
  3426. const member = ZeresPluginLibrary.DiscordModules.GuildMemberStore.getMember(message.guild_id || (channel && channel.guild_id), message.author.id);
  3427. return message.author.id == filter || message.author.username.toLowerCase().includes(filter.toLowerCase()) || (member && member.nick && member.nick.toLowerCase().includes(filter.toLowerCase()));
  3428. });
  3429. }
  3430.  
  3431. if (this.menu.selectedTab != 'sent') {
  3432. this.sortMessagesByAge(messages);
  3433. if (this.settings.reverseOrder) messages.reverse(); // this gave me a virtual headache
  3434. } else if (!this.settings.reverseOrder) messages.reverse(); // this gave me a virtual headache
  3435.  
  3436. return messages;
  3437. }
  3438. // >>-|| REPOPULATE ||-<<
  3439. refilterMessages() {
  3440. const messagesDIV = document.getElementById(this.style.menuMessages);
  3441. const original = messagesDIV.style.display;
  3442. messagesDIV.style.display = 'none';
  3443. while (messagesDIV.firstChild) messagesDIV.removeChild(messagesDIV.firstChild);
  3444. this.menu.messages = this.getFilteredMessages();
  3445. this.populateParent(messagesDIV, this.menu.messages);
  3446. messagesDIV.style.display = original;
  3447. }
  3448. // >>-|| HEADER ||-<<
  3449. openTab(tab) {
  3450. const tabBar = document.getElementById(this.style.menuTabBar);
  3451. if (!tabBar) return this.showToast(`Error switching to tab ${tab}!`, { type: 'error', timeout: 3000 });
  3452. tabBar.querySelector(`.${this.style.tabSelected}`).classList.remove(this.style.tabSelected);
  3453. tabBar.querySelector('#' + tab).classList.add(this.style.tabSelected);
  3454. this.menu.selectedTab = tab;
  3455. setTimeout(() => this.refilterMessages(), 0);
  3456. }
  3457. createHeader() {
  3458. const classes = this.createHeader.classes;
  3459. const createTab = (title, id) => {
  3460. const tab = this.parseHTML(`<div id="${id}" class="${classes.itemTabBarItem} ${this.style.tab} ${id == this.menu.selectedTab ? this.style.tabSelected : ''}" role="button">${title}</div>`);
  3461. tab.addEventListener('mousedown', () => this.openTab(id));
  3462. return tab;
  3463. };
  3464. const tabBar = this.parseHTML(`<div class="${classes.tabBarContainer}"><div class="${classes.tabBar}" id="${this.style.menuTabBar}"></div></div>`);
  3465. const tabs = tabBar.getElementsByClassName(classes.tabBarSingle)[0];
  3466. tabs.appendChild(createTab('Sent', 'sent'));
  3467. tabs.appendChild(createTab('Deleted', 'deleted'));
  3468. tabs.appendChild(createTab('Edited', 'edited'));
  3469. tabs.appendChild(createTab('Purged', 'purged'));
  3470. tabs.appendChild(createTab('Ghost pings', 'ghostpings'));
  3471. const measureWidth = el => {
  3472. el = el.cloneNode(true);
  3473.  
  3474. el.style.visibility = 'hidden';
  3475. el.style.position = 'absolute';
  3476.  
  3477. document.body.appendChild(el);
  3478. let result = el.getBoundingClientRect().width;
  3479. el.remove();
  3480. return result;
  3481. };
  3482. const totalWidth = measureWidth(tabs) * 2 - 20;
  3483. const wantedTabWidth = totalWidth / tabs.childElementCount;
  3484. const wantedTabMargin = wantedTabWidth / 2;
  3485. let tab = tabs.firstElementChild;
  3486. while (tab) {
  3487. tab.style.marginRight = '0px';
  3488. const tabWidth = measureWidth(tab);
  3489. if (tabWidth > wantedTabWidth) {
  3490. ZeresPluginLibrary.Logger.err(this.getName(), `What the shit? Tab ${tab} is massive!!`);
  3491. tab = tab.nextElementSibling;
  3492. continue;
  3493. }
  3494. tab.style.paddingRight = tab.style.paddingLeft = `${wantedTabMargin - tabWidth / 2}px`;
  3495. tab = tab.nextElementSibling;
  3496. }
  3497. tabBar.style.marginRight = '20px';
  3498. return tabBar;
  3499. }
  3500. createTextBox() {
  3501. const classes = this.createTextBox.classes;
  3502. let textBox = this.parseHTML(
  3503. `<div class="${classes.inputWrapper}"><div class="${classes.inputMultiInput}"><div class="${classes.inputWrapper} ${classes.multiInputFirst}"><input class="${classes.inputDefaultMultiInputField}" name="username" type="text" placeholder="Message filter" maxlength="999" value="${this.menu.filter}" id="${this.style.filter}"></div><span tabindex="0" aria-label="Help!" class="${classes.questionMark}" role="button"><svg name="QuestionMark" class="${classes.icon}" aria-hidden="false" width="16" height="16" viewBox="0 0 24 24"><g fill="currentColor" fill-rule="evenodd" transform="translate(7 4)"><path d="M0 4.3258427C0 5.06741573.616438356 5.68539326 1.35616438 5.68539326 2.09589041 5.68539326 2.71232877 5.06741573 2.71232877 4.3258427 2.71232877 2.84269663 4.31506849 2.78089888 4.5 2.78089888 4.68493151 2.78089888 6.28767123 2.84269663 6.28767123 4.3258427L6.28767123 4.63483146C6.28767123 5.25280899 5.97945205 5.74719101 5.42465753 6.05617978L4.19178082 6.73595506C3.51369863 7.10674157 3.14383562 7.78651685 3.14383562 8.52808989L3.14383562 9.64044944C3.14383562 10.3820225 3.76027397 11 4.5 11 5.23972603 11 5.85616438 10.3820225 5.85616438 9.64044944L5.85616438 8.96067416 6.71917808 8.52808989C8.1369863 7.78651685 9 6.30337079 9 4.69662921L9 4.3258427C9 1.48314607 6.71917808 0 4.5 0 2.21917808 0 0 1.48314607 0 4.3258427zM4.5 12C2.5 12 2.5 15 4.5 15 6.5 15 6.5 12 4.5 12L4.5 12z"></path></g></svg></span></div></div>`
  3504. );
  3505. const inputEl = textBox.getElementsByTagName('input')[0];
  3506. inputEl.addEventListener('focusout', e => {
  3507. DOMTokenList.prototype.remove.apply(e.target.parentElement.parentElement.classList, classes.focused);
  3508. });
  3509. inputEl.addEventListener('focusin', e => {
  3510. DOMTokenList.prototype.add.apply(e.target.parentElement.parentElement.classList, classes.focused);
  3511. });
  3512. const onUpdate = e => {
  3513. if (this.menu.filterSetTimeout) clearTimeout(this.menu.filterSetTimeout);
  3514. this.menu.filter = inputEl.value;
  3515. const filters = this.menu.filter.split(',');
  3516. // console.log(filters);
  3517. if (!filters[0].length) return this.refilterMessages();
  3518. this.menu.filterSetTimeout = setTimeout(() => {
  3519. if (filters[0].length) {
  3520. for (let i = 0; i < filters.length; i++) {
  3521. const split = filters[i].split(':');
  3522. if (split.length < 2) return;
  3523. }
  3524. }
  3525. this.refilterMessages();
  3526. }, 200);
  3527. };
  3528. inputEl.addEventListener('keyup', onUpdate); // maybe I can actually use keydown but it didn't work for me
  3529. inputEl.addEventListener('paste', onUpdate);
  3530. const helpButton = textBox.getElementsByClassName(classes.questionMarkSingle)[0];
  3531. helpButton.addEventListener('click', () => {
  3532. this.scrollPosition = document.getElementById(this.style.menuMessages).parentElement.parentElement.parentElement.scrollTop;
  3533. const extraHelp = this.createButton('Logger help', () => this.showLoggerHelpModal());
  3534. this.createModal({
  3535. confirmText: 'OK',
  3536. header: 'Filter help',
  3537. size: ZeresPluginLibrary.Modals.ModalSizes.LARGE,
  3538. children: [
  3539. ZeresPluginLibrary.ReactTools.createWrappedElement([
  3540. this.parseHTML(
  3541. `<div class="${this.multiClasses.defaultColor}">"server: <servername or serverid>" - Filter results with the specified server name or id.
  3542. "channel: <channelname or channelid>" - Filter results with the specified channel name or id.
  3543. "user: <username, nickname or userid>" - Filter results with the specified username, nickname or userid.
  3544. "message: <search or messageid>" or "content: <search or messageid>" - Filter results with the specified message content.
  3545.  
  3546. Separate the search tags with commas.
  3547. Example: server: tom's bd stuff, message: heck
  3548.  
  3549.  
  3550. Shortcut help:
  3551.  
  3552. "Ctrl + M" (default) - Open message log.
  3553. "Ctrl + N" (default) - Open message log with selected channel filtered.\n\n</div>`.replace(/\n/g, '</br>')
  3554. ),
  3555. extraHelp
  3556. ])
  3557. ],
  3558. red: false
  3559. });
  3560. });
  3561. new ZeresPluginLibrary.EmulatedTooltip(helpButton, 'Help!', { side: 'top' });
  3562. return textBox;
  3563. }
  3564. // >>-|| MENU MODAL CREATION ||-<<
  3565. openWindow(type) {
  3566. if (this.menu.open) {
  3567. this.menu.scrollPosition = 0;
  3568. if (type) this.openTab(type);
  3569. return;
  3570. }
  3571. this.menu.open = true;
  3572. if (type) this.menu.selectedTab = type;
  3573. if (!this.menu.selectedTab) this.menu.selectedTab = 'deleted';
  3574. const messagesDIV = this.parseHTML(`<div id="${this.style.menuMessages}"></div>`);
  3575. const viewportHeight = document.getElementById('app-mount').getBoundingClientRect().height;
  3576. messagesDIV.style.minHeight = viewportHeight * 0.514090909 + 'px'; // hack but ok
  3577. //messagesDIV.style.display = 'none';
  3578. this.createModal(
  3579. {
  3580. confirmText: 'Clear log',
  3581. cancelText: 'Sort direction: ' + (!this.settings.reverseOrder ? 'new - old' : 'old - new'),
  3582. header: ZeresPluginLibrary.ReactTools.createWrappedElement([this.createTextBox(), this.createHeader()]),
  3583. size: ZeresPluginLibrary.Modals.ModalSizes.LARGE,
  3584. children: [ZeresPluginLibrary.ReactTools.createWrappedElement([messagesDIV])],
  3585. ml2Data: true
  3586. },
  3587. false,
  3588. this.style.menu
  3589. );
  3590. let loadAttempts = 0;
  3591. const loadMessages = () => {
  3592. loadAttempts++;
  3593. try {
  3594. this.refilterMessages();
  3595. } catch (e) {
  3596. if (loadAttempts > 4) {
  3597. this.showToast(`Couldn't load menu messages! Report this issue to Lighty, error info is in console`, { type: 'error', timeout: 10000 });
  3598. ZeresPluginLibrary.Logger.stacktrace(this.getName(), 'Failed loading menu', e);
  3599. return;
  3600. }
  3601. setTimeout(() => loadMessages(), 100);
  3602. }
  3603. };
  3604. setTimeout(() => loadMessages(), 100);
  3605. }
  3606. /* ==================================================-|| END MENU ||-================================================== */
  3607. /* ==================================================-|| START CONTEXT MENU ||-================================================== */
  3608. handleContextMenu(thisObj, args, returnValue) {
  3609. if (!returnValue) return returnValue;
  3610. // console.log(thisObj, args, returnValue);
  3611. const type = thisObj.props.type;
  3612. const newItems = [];
  3613. const addElement = (label, callback) => {
  3614. newItems.push(
  3615. ZeresPluginLibrary.DiscordModules.React.createElement(this.ContextMenuItem, {
  3616. label: label,
  3617. action: () => {
  3618. this.ContextMenuActions.closeContextMenu();
  3619. if (callback) callback();
  3620. }
  3621. })
  3622. );
  3623. };
  3624.  
  3625. const handleWhiteBlackList = id => {
  3626. const whitelistIdx = this.settings.whitelist.findIndex(m => m === id);
  3627. const blacklistIdx = this.settings.blacklist.findIndex(m => m === id);
  3628. if (whitelistIdx == -1 && blacklistIdx == -1) {
  3629. addElement(`Add to Whitelist`, () => {
  3630. this.settings.whitelist.push(id);
  3631. this.saveSettings();
  3632. this.showToast('Added!', { type: 'success' });
  3633. });
  3634. addElement(`Add to Blacklist`, () => {
  3635. this.settings.blacklist.push(id);
  3636. this.saveSettings();
  3637. this.showToast('Added!', { type: 'success' });
  3638. });
  3639. } else if (whitelistIdx != -1) {
  3640. addElement(`Remove From Whitelist`, () => {
  3641. this.settings.whitelist.splice(whitelistIdx, 1);
  3642. this.saveSettings();
  3643. this.showToast('Removed!', { type: 'success' });
  3644. });
  3645. addElement(`Move to Blacklist`, () => {
  3646. this.settings.whitelist.splice(whitelistIdx, 1);
  3647. this.settings.blacklist.push(id);
  3648. this.saveSettings();
  3649. this.showToast('Moved!', { type: 'success' });
  3650. });
  3651. } else {
  3652. addElement(`Remove From Blacklist`, () => {
  3653. this.settings.blacklist.splice(blacklistIdx, 1);
  3654. this.saveSettings();
  3655. this.showToast('Removed!', { type: 'success' });
  3656. });
  3657. addElement(`Move to Whitelist`, () => {
  3658. this.settings.blacklist.splice(blacklistIdx, 1);
  3659. this.settings.whitelist.push(id);
  3660. this.saveSettings();
  3661. this.showToast('Moved!', { type: 'success' });
  3662. });
  3663. }
  3664. };
  3665. // image has no type property
  3666. if (!type) {
  3667. if (!this.menu.open) return;
  3668. let matched;
  3669. let isCached = false;
  3670. if (!thisObj.props.src) return;
  3671. if (thisObj.props.src.startsWith('data:image/png')) {
  3672. const cut = thisObj.props.src.substr(0, 100);
  3673. matched = cut.match(/;(\d+);(\d+);/);
  3674. isCached = true;
  3675. } else {
  3676. matched = thisObj.props.src.match(/.*ments\/(\d+)\/(\d+)\//);
  3677. if (!matched) matched = thisObj.props.src.match(/r8q6.png#(\d+),(\d+)/);
  3678. }
  3679. if (!matched) return;
  3680. const channelId = matched[1];
  3681. const attachmentId = matched[2];
  3682. const element = document.getElementById(attachmentId);
  3683. if (!element) return;
  3684. const attachmentIdx = element.idx;
  3685. const record = this.getSavedMessage(element.messageId);
  3686. if (!record) return;
  3687. const attachmentRecord = this.imageCacheRecord[attachmentId];
  3688. addElement('Save to Folder', () => {
  3689. const { dialog } = this.nodeModules.electron.remote;
  3690. const dir = dialog.showSaveDialog({
  3691. defaultPath: record.message.attachments[attachmentIdx].filename
  3692. });
  3693. if (!dir) return;
  3694. const attemptToUseCached = () => {
  3695. const srcFile = `${this.imageCacheDir}/${attachmentId}${(attachmentRecord && attachmentRecord.extension) || 'fuck'}`;
  3696. if (!attachmentRecord || !this.nodeModules.fs.existsSync(srcFile)) return this.showToast('Image does not exist locally!', { type: 'error', timeout: 5000 });
  3697. this.nodeModules.fs.copyFileSync(srcFile, dir);
  3698. this.showToast('Saved!', { type: 'success' });
  3699. };
  3700. if (isCached) {
  3701. attemptToUseCached();
  3702. } else {
  3703. const req = this.nodeModules.request(record.message.attachments[attachmentIdx].url);
  3704. req.on('response', res => {
  3705. if (res.statusCode == 200) {
  3706. req
  3707. .pipe(this.nodeModules.fs.createWriteStream(dir))
  3708. .on('finish', () => this.showToast('Saved!', { type: 'success' }))
  3709. .on('error', () => this.showToast('Failed to save! No permissions.', { type: 'error', timeout: 5000 }));
  3710. } else if (res.statusCode == 404) {
  3711. this.showToast('Image does not exist! Attempting to use local image cache.', { type: 'error' });
  3712. attemptToUseCached();
  3713. } else {
  3714. this.showToast('Unknown error, attempting to use local image cache.', { type: 'error' });
  3715. attemptToUseCached();
  3716. }
  3717. });
  3718. }
  3719. });
  3720. addElement('Copy to Clipboard', () => {
  3721. const { clipboard, nativeImage } = this.nodeModules.electron;
  3722. const attemptToUseCached = () => {
  3723. const srcFile = `${this.imageCacheDir}/${attachmentId}${(attachmentRecord && attachmentRecord.extension) || 'fuck'}`;
  3724. if (!attachmentRecord || !this.nodeModules.fs.existsSync(srcFile)) return this.showToast('Image does not exist locally!', { type: 'error', timeout: 5000 });
  3725. clipboard.write({ image: srcFile });
  3726. this.showToast('Copied!', { type: 'success' });
  3727. };
  3728. if (isCached) {
  3729. attemptToUseCached();
  3730. } else {
  3731. const path = require('path');
  3732. const process = require('process');
  3733. // ImageToClipboard by Zerebos
  3734. this.nodeModules.request({ url: record.message.attachments[attachmentIdx].url, encoding: null }, (error, response, buffer) => {
  3735. if (error || response.statusCode != 200) {
  3736. this.showToast('Failed to copy. Image may not exist. Attempting to use local image cache.', { type: 'error' });
  3737. attemptToUseCached();
  3738. return;
  3739. }
  3740. if (process.platform === 'win32' || process.platform === 'darwin') {
  3741. clipboard.write({ image: nativeImage.createFromBuffer(buffer) });
  3742. } else {
  3743. const file = path.join(process.env.HOME, 'ml2temp.png');
  3744. this.nodeModules.fs.writeFileSync(file, buffer, { encoding: null });
  3745. clipboard.write({ image: file });
  3746. this.nodeModules.fs.unlinkSync(file);
  3747. }
  3748. this.showToast('Copied!', { type: 'success' });
  3749. });
  3750. }
  3751. });
  3752. addElement('Jump to Message', () => {
  3753. this.jumpToMessage(channelId, element.messageId, record.message.guild_id);
  3754. });
  3755. if (record.delete_data && record.delete_data.hidden) {
  3756. addElement('Unhide Deleted Message', () => {
  3757. record.delete_data.hidden = false;
  3758. this.invalidateChannelCache(record.message.channel_id); // good idea?
  3759. this.cacheChannelMessages(record.message.channel_id);
  3760. this.saveData();
  3761. this.showToast('Unhidden!', { type: 'success' });
  3762. });
  3763. }
  3764. if (record.edit_history && record.edits_hidden) {
  3765. addElement('Unhide Deleted Message', () => {
  3766. record.edits_hidden = false;
  3767. this.invalidateChannelCache(record.message.channel_id); // good idea?
  3768. this.cacheChannelMessages(record.message.channel_id);
  3769. this.saveData();
  3770. this.showToast('Unhidden!', { type: 'success' });
  3771. });
  3772. }
  3773. addElement('Remove From Log', () => {
  3774. let invalidatedChannelCache = false;
  3775. if ((record.edit_history && !record.edits_hidden) || record.delete_data) this.invalidateChannelCache((invalidatedChannelCache = record.message.channel_id));
  3776. this.deleteMessageFromRecords(element.messageId);
  3777. this.refilterMessages(); // I don't like calling that, maybe figure out a way to animate it collapsing on itself smoothly
  3778. if (invalidatedChannelCache) this.cacheChannelMessages(invalidatedChannelCache);
  3779. this.saveData();
  3780. });
  3781. if (!thisObj.props.src.startsWith('https://i.clouds.tf/q2vy/r8q6.png')) {
  3782. addElement('Hide Image From Log', () => {
  3783. record.message.attachments[attachmentIdx].hidden = true;
  3784. element.src = `https://i.clouds.tf/q2vy/r8q6.png#${channelId},${attachmentId}`;
  3785. element.width = 200;
  3786. });
  3787. } else {
  3788. addElement('Unhide Image From Log', () => {
  3789. record.message.attachments[attachmentIdx].hidden = false;
  3790. element.src = isCached ? attachmentRecord && attachmentRecord.thumbnail : record.message.attachments[attachmentIdx].url;
  3791. element.width = isCached ? 256 : this.clamp(record.message.attachments[attachmentIdx].width, 200, 650);
  3792. });
  3793. }
  3794. } else if (type == 'MESSAGE_MAIN') {
  3795. addElement('Open Logs', () => this.openWindow());
  3796. const messageId = thisObj.props.message.id;
  3797. const channelId = thisObj.props.channel.id;
  3798. const record = this.messageRecord[messageId];
  3799. if (record) {
  3800. /*
  3801. addElement('Show in menu', () => {
  3802. this.menu.filter = `message:${messageId}`;
  3803. this.openWindow();
  3804. }); */
  3805. if (record.delete_data) {
  3806. const options = returnValue.props.children.find(m => m.props.children && m.props.children.length > 5);
  3807. options.props.children.splice(0, options.props.children.length);
  3808. addElement('Hide Deleted Message', () => {
  3809. ZeresPluginLibrary.WebpackModules.find(m => m.dispatch).dispatch({
  3810. type: 'MESSAGE_DELETE',
  3811. id: messageId,
  3812. channelId: channelId,
  3813. ML2: true // ignore ourselves lol, it's already deleted
  3814. // on a side note, probably does nothing if we don't ignore
  3815. });
  3816. this.showToast('Hidden!', { type: 'success' });
  3817. record.delete_data.hidden = true;
  3818. this.saveData();
  3819. });
  3820. let target = thisObj.props.target;
  3821. if (target) {
  3822. while (target && target.className.indexOf(this.style.deleted) === -1) target = target.parentElement;
  3823. if (target) {
  3824. addElement('Remove Deleted Tint', () => {
  3825. target.isDeleted = true;
  3826. target.classList.remove(this.style.deleted);
  3827. this.showToast('Removed!', { type: 'success' });
  3828. });
  3829. }
  3830. }
  3831. }
  3832. if (record.edit_history) {
  3833. if (record.edits_hidden) {
  3834. addElement('Unhide Edits', () => {
  3835. record.edits_hidden = false;
  3836. this.saveData();
  3837. });
  3838. } else {
  3839. let target = thisObj.props.target;
  3840. if (!target) return;
  3841. while (target.className.indexOf(this.style.edited) === -1) {
  3842. target = target.parentElement;
  3843. if (!target) return;
  3844. }
  3845. const parent = target.parentElement;
  3846. const editNum = target.firstChild.editNum;
  3847. if (typeof editNum === 'number') {
  3848. addElement('Delete Edit', () => {
  3849. target.remove();
  3850. if (parent.childElementCount === 1) {
  3851. const editedTag = document.createElement('time');
  3852. editedTag.className = this.multiClasses.edited;
  3853. editedTag.innerText = '(edited)';
  3854. parent.firstChild.firstChild.appendChild(editedTag);
  3855. }
  3856. this.deleteEditedMessageFromRecord(messageId, editNum);
  3857. });
  3858. }
  3859. addElement('Hide Edits', () => {
  3860. record.edits_hidden = true;
  3861. this.saveData();
  3862. });
  3863. }
  3864. }
  3865. }
  3866. } else if (type == 'GUILD_ICON_BAR') {
  3867. addElement('Open Log For Guild', () => {
  3868. this.menu.filter = `server:${thisObj.props.guild.id}`;
  3869. this.openWindow();
  3870. });
  3871. handleWhiteBlackList(thisObj.props.guild.id);
  3872. } else if (type == 'CHANNEL_LIST_TEXT') {
  3873. addElement('Open Log For Channel', () => {
  3874. this.menu.filter = `channel:${thisObj.props.channel.id}`;
  3875. this.openWindow();
  3876. });
  3877. handleWhiteBlackList(thisObj.props.channel.id);
  3878. } else if (type == 'USER_PRIVATE_CHANNELS_MESSAGE' || type == 'USER_PRIVATE_CHANNELS' || type == 'USER_CHANNEL_MESSAGE') {
  3879. if (!this.menu.open) {
  3880. addElement('Open Logs', () => this.openWindow());
  3881. addElement('Open Log For User', () => {
  3882. this.menu.filter = `user:${thisObj.props.user.id}`;
  3883. this.openWindow();
  3884. });
  3885. addElement(`Open Log For ${type == 'USER_CHANNEL_MESSAGE' ? 'Channel' : 'DM'}`, () => {
  3886. this.menu.filter = `channel:${thisObj.props.channelId}`;
  3887. this.openWindow();
  3888. });
  3889. }
  3890. if (type != 'USER_CHANNEL_MESSAGE') handleWhiteBlackList(thisObj.props.channelId);
  3891. returnValue.props.children.props.children.props.children.push(
  3892. ZeresPluginLibrary.DiscordModules.React.createElement(this.ContextMenuGroup, {
  3893. children: [ZeresPluginLibrary.DiscordModules.React.createElement(this.SubMenuItem, { label: 'Message Logger', render: newItems, invertChildY: true })]
  3894. })
  3895. );
  3896. return;
  3897. }
  3898. if (!newItems.length) return returnValue;
  3899. returnValue.props.children.push(
  3900. ZeresPluginLibrary.DiscordModules.React.createElement(this.ContextMenuGroup, {
  3901. children: [ZeresPluginLibrary.DiscordModules.React.createElement(this.SubMenuItem, { label: 'Message Logger', render: newItems, invertChildY: true })]
  3902. })
  3903. );
  3904. }
  3905. /* ==================================================-|| END CONTEXT MENU ||-================================================== */
  3906. }
  3907. /*@end @*/
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement