1
0
mirror of https://github.com/chylex/Discord-History-Tracker.git synced 2025-09-15 19:32:09 +02:00

12 Commits

19 changed files with 747 additions and 3622 deletions

View File

@@ -31,6 +31,7 @@ After you've done changes to the source code, you will need to build it. Before
Now open the folder that contains `build.py` in a command line, and run `python build.py` to create a build with default settings. The following files will be created: Now open the folder that contains `build.py` in a command line, and run `python build.py` to create a build with default settings. The following files will be created:
* `bld/track.js` is the raw tracker script that can be pasted into a browser console * `bld/track.js` is the raw tracker script that can be pasted into a browser console
* `bld/track.html` is the tracker script but sanitized for inclusion in HTML (see `web/index.php` for examples)
* `bld/viewer.html` is the complete offline viewer * `bld/viewer.html` is the complete offline viewer
You can tweak the build process using the following flags: You can tweak the build process using the following flags:

File diff suppressed because one or more lines are too long

View File

@@ -1,6 +1,6 @@
// ==UserScript== // ==UserScript==
// @name Discord History Tracker // @name Discord History Tracker
// @version v.31f // @version v.31h
// @license MIT // @license MIT
// @namespace https://chylex.com // @namespace https://chylex.com
// @homepageURL https://dht.chylex.com/ // @homepageURL https://dht.chylex.com/
@@ -12,80 +12,42 @@
const start = function(){ const start = function(){
// NOTE: Currently unused. See `.createStyle()` calls in `gui.js`. Included in branch so a rebase isn't needed for `git` to track this if it's used later. // noinspection JSAnnotator
/*
const css_controller = ` const url = window.location.href;
#app-mount {
height: calc(100% - 48px) !important; if (!url.includes("discord.com/") && !url.includes("discordapp.com/") && !confirm("Could not detect Discord in the URL, do you want to run the script anyway?")) {
return;
} }
#dht-ctrl { if (window.DHT_LOADED) {
position: absolute; alert("Discord History Tracker is already loaded.");
bottom: 0; return;
width: 100%;
height: 48px;
background-color: #fff;
z-index: 1000000;
} }
#dht-ctrl button { window.DHT_LOADED = true;
height: 32px; window.DHT_ON_UNLOAD = [];
margin: 8px 0 8px 8px;
font-size: 16px;
padding: 0 12px;
background-color: #7289da;
color: #fff;
text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.75);
}
#dht-ctrl button:disabled {
background-color: #7a7a7a;
cursor: default;
}
#dht-ctrl p {
display: inline-block;
margin: 14px 12px;
}
`
*/
// NOTE: Currently unused. See `.createStyle()` calls in `gui.js`. Included in branch so a rebase isn't needed for `git` to track this if it's used later.
/*
const css_settings = `
#dht-cfg-overlay {
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
background-color: #000;
opacity: 0.5;
display: block;
z-index: 1000001;
}
#dht-cfg {
position: absolute;
left: 50%;
top: 50%;
width: 800px;
height: 262px;
margin-left: -400px;
margin-top: -131px;
padding: 8px;
background-color: #fff;
z-index: 1000002;
}
#dht-cfg-note {
margin-top: 22px;
}
`
*/
// noinspection JSUnresolvedVariable // noinspection JSUnresolvedVariable
// noinspection LocalVariableNamingConventionJS
class DISCORD { class DISCORD {
// https://discord.com/developers/docs/resources/channel#channel-object-channel-types
static CHANNEL_TYPE = {
DM: 1,
GROUP_DM: 3,
ANNOUNCEMENT_THREAD: 10,
PUBLIC_THREAD: 11,
PRIVATE_THREAD: 12
};
// https://discord.com/developers/docs/resources/channel#message-object-message-types
static MESSAGE_TYPE = {
DEFAULT: 0,
REPLY: 19,
THREAD_STARTER: 21
};
static getMessageOuterElement() { static getMessageOuterElement() {
return DOM.queryReactClass("messagesWrapper"); return DOM.queryReactClass("messagesWrapper");
} }
@@ -114,46 +76,11 @@ class DISCORD {
* Calls the provided function with a list of messages whenever the currently loaded messages change. * Calls the provided function with a list of messages whenever the currently loaded messages change.
*/ */
static setupMessageCallback(callback) { static setupMessageCallback(callback) {
let skipsLeft = 0;
let waitForCleanup = false;
const previousMessages = new Set(); const previousMessages = new Set();
const timer = window.setInterval(() => { const onMessageElementsChanged = function() {
if (skipsLeft > 0) { const messages = DISCORD.getMessages();
--skipsLeft; const hasChanged = messages.some(message => !previousMessages.has(message.id)) || !DISCORD.hasMoreMessages();
return;
}
const view = this.getMessageOuterElement();
if (!view) {
skipsLeft = 2;
return;
}
const anyMessage = DOM.queryReactClass("message", this.getMessageOuterElement());
const messageCount = anyMessage ? anyMessage.parentElement.children.length : 0;
if (messageCount > 300) {
if (waitForCleanup) {
return;
}
skipsLeft = 3;
waitForCleanup = true;
window.setTimeout(() => {
const view = this.getMessageScrollerElement();
// noinspection JSUnusedGlobalSymbols
view.scrollTop = view.scrollHeight / 2;
}, 1);
}
else {
waitForCleanup = false;
}
const messages = this.getMessages();
const hasChanged = messages.some(message => !previousMessages.has(message.id)) || !this.hasMoreMessages();
if (!hasChanged) { if (!hasChanged) {
return; return;
@@ -165,24 +92,74 @@ class DISCORD {
} }
callback(messages); callback(messages);
}, 200); };
window.DHT_ON_UNLOAD.push(() => window.clearInterval(timer)); let debounceTimer;
/**
* Do not trigger the callback too often due to autoscrolling.
*/
const onMessageElementsChangedLater = function() {
window.clearTimeout(debounceTimer);
debounceTimer = window.setTimeout(onMessageElementsChanged, 100);
};
const observer = new MutationObserver(function () {
onMessageElementsChangedLater();
});
let skipsLeft = 0;
let observedElement = null;
const observerTimer = window.setInterval(() => {
if (skipsLeft > 0) {
--skipsLeft;
return;
}
const view = this.getMessageOuterElement();
if (!view) {
skipsLeft = 1;
return;
}
if (observedElement !== null && observedElement.isConnected) {
return;
}
observedElement = view.querySelector("[data-list-id='chat-messages']");
if (observedElement) {
console.debug("[DHT] Observed message container.");
observer.observe(observedElement, { childList: true });
onMessageElementsChangedLater();
}
}, 400);
window.DHT_ON_UNLOAD.push(() => {
observer.disconnect();
observedElement = null;
window.clearInterval(observerTimer);
});
} }
/** /**
* Returns the property object of a message element. * Returns the message from a message element.
* @returns { null | { message: DiscordMessage, channel: Object } } * @returns { null | DiscordMessage } }
*/ */
static getMessageElementProps(ele) { static getMessageFromElement(ele) {
const props = DOM.getReactProps(ele); const props = DOM.getReactProps(ele);
if (props.children && props.children.length) { if (props && Array.isArray(props.children)) {
for (let i = 3; i < props.children.length; i++) { for (const child of props.children) {
const childProps = props.children[i].props; if (!(child instanceof Object)) {
continue;
}
if (childProps && "message" in childProps && "channel" in childProps) { const childProps = child.props;
return childProps; if (childProps instanceof Object && "message" in childProps) {
return childProps.message;
} }
} }
} }
@@ -199,10 +176,10 @@ class DISCORD {
for (const ele of this.getMessageElements()) { for (const ele of this.getMessageElements()) {
try { try {
const props = this.getMessageElementProps(ele); const message = this.getMessageFromElement(ele);
if (props != null) { if (message != null) {
messages.push(props.message); messages.push(message);
} }
} catch (e) { } catch (e) {
console.error("[DHT] Error extracing message data, skipping it.", e, ele, DOM.tryGetReactProps(ele)); console.error("[DHT] Error extracing message data, skipping it.", e, ele, DOM.tryGetReactProps(ele));
@@ -223,7 +200,7 @@ class DISCORD {
*/ */
static getSelectedChannel() { static getSelectedChannel() {
try { try {
let obj; let obj = null;
try { try {
for (const child of DOM.getReactProps(DOM.queryReactClass("chatContent")).children) { for (const child of DOM.getReactProps(DOM.queryReactClass("chatContent")).children) {
@@ -234,15 +211,6 @@ class DISCORD {
} }
} catch (e) { } catch (e) {
console.error("[DHT] Error retrieving selected channel from 'chatContent' element.", e); console.error("[DHT] Error retrieving selected channel from 'chatContent' element.", e);
for (const ele of this.getMessageElements()) {
const props = this.getMessageElementProps(ele);
if (props != null) {
obj = props.channel;
break;
}
}
} }
if (!obj || typeof obj.id !== "string") { if (!obj || typeof obj.id !== "string") {
@@ -271,8 +239,8 @@ class DISCORD {
// https://discord.com/developers/docs/resources/channel#channel-object-channel-types // https://discord.com/developers/docs/resources/channel#channel-object-channel-types
switch (obj.type) { switch (obj.type) {
case 1: type = "DM"; break; case DISCORD.CHANNEL_TYPE.DM: type = "DM"; break;
case 3: type = "GROUP"; break; case DISCORD.CHANNEL_TYPE.GROUP_DM: type = "GROUP"; break;
default: return null; default: return null;
} }
@@ -310,7 +278,7 @@ class DISCORD {
} }
}; };
if (obj.parent_id) { if (obj.type === DISCORD.CHANNEL_TYPE.ANNOUNCEMENT_THREAD || obj.type === DISCORD.CHANNEL_TYPE.PUBLIC_THREAD || obj.type === DISCORD.CHANNEL_TYPE.PRIVATE_THREAD) {
channel["extra"]["parent"] = obj.parent_id; channel["extra"]["parent"] = obj.parent_id;
} }
else { else {
@@ -475,72 +443,6 @@ class DOM {
} }
} }
const CONSTANTS = {
AUTOSCROLL_ACTION_NOTHING: "optNothing",
AUTOSCROLL_ACTION_PAUSE: "optPause",
AUTOSCROLL_ACTION_SWITCH: "optSwitch"
};
let IS_FIRST_RUN = false;
const SETTINGS = (function() {
const settingsChangedEvents = [];
const saveSettings = function() {
DOM.saveToCookie("DHT_SETTINGS", root, 60 * 60 * 24 * 365 * 5);
};
const triggerSettingsChanged = function(property) {
for (const callback of settingsChangedEvents) {
callback(property);
}
saveSettings();
};
const defineTriggeringProperty = function(obj, property, value) {
const name = "_" + property;
Object.defineProperty(obj, property, {
get: (() => obj[name]),
set: (value => {
obj[name] = value;
triggerSettingsChanged(property);
})
});
obj[name] = value;
};
let loaded = DOM.loadFromCookie("DHT_SETTINGS");
if (!loaded) {
loaded = {
"_autoscroll": true,
"_afterFirstMsg": CONSTANTS.AUTOSCROLL_ACTION_PAUSE,
"_afterSavedMsg": CONSTANTS.AUTOSCROLL_ACTION_PAUSE
};
IS_FIRST_RUN = true;
}
const root = {
onSettingsChanged(callback) {
settingsChangedEvents.push(callback);
}
};
defineTriggeringProperty(root, "autoscroll", loaded._autoscroll);
defineTriggeringProperty(root, "afterFirstMsg", loaded._afterFirstMsg);
defineTriggeringProperty(root, "afterSavedMsg", loaded._afterSavedMsg);
if (IS_FIRST_RUN) {
saveSettings();
}
return root;
})();
var GUI = (function(){ var GUI = (function(){
var controller; var controller;
var settings; var settings;
@@ -627,14 +529,44 @@ var GUI = (function(){
// styles // styles
controller.styles = DOM.createStyle(` controller.styles = DOM.createStyle(`#app-mount {
#app-mount div[class*="app-"] { margin-bottom: 48px !important; } height: calc(100% - 48px) !important;
#dht-ctrl { position: absolute; bottom: 0; width: 100%; height: 48px; background-color: #FFF; z-index: 1000000; } }
#dht-ctrl button { height: 32px; margin: 8px 0 8px 8px; font-size: 16px; padding: 0 12px; background-color: #7289DA; color: #FFF; text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.75); }
#dht-ctrl button:disabled { background-color: #7A7A7A; cursor: default; } #dht-ctrl {
#dht-ctrl-close { margin: 8px 8px 8px 0 !important; float: right; } position: absolute;
#dht-ctrl p { display: inline-block; margin: 14px 12px; } bottom: 0;
#dht-ctrl input { display: none; }`); width: 100%;
height: 48px;
background-color: #fff;
z-index: 1000000;
}
#dht-ctrl button {
height: 32px;
margin: 8px 0 8px 8px;
font-size: 16px;
padding: 0 12px;
background-color: #7289da;
color: #fff;
text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.75);
}
#dht-ctrl button:disabled {
background-color: #7a7a7a;
cursor: default;
}
#dht-ctrl p {
display: inline-block;
margin: 14px 12px;
}
#dht-ctrl-close {
margin: 8px 8px 8px 0 !important;
float: right;
}
`);
// main // main
@@ -647,7 +579,7 @@ ${btn("track", "")}
${btn("download", "Download")} ${btn("download", "Download")}
${btn("reset", "Reset")} ${btn("reset", "Reset")}
<p id='dht-ctrl-status'></p> <p id='dht-ctrl-status'></p>
<input id='dht-ctrl-upload-input' type='file' multiple> <input id='dht-ctrl-upload-input' type='file' multiple style="display: none">
${btn("close", "X")}`); ${btn("close", "X")}`);
// elements // elements
@@ -736,11 +668,40 @@ ${btn("close", "X")}`);
// styles // styles
settings.styles = DOM.createStyle(` settings.styles = DOM.createStyle(`#dht-cfg-overlay {
#dht-cfg-overlay { position: absolute; left: 0; top: 0; width: 100%; height: 100%; background-color: #000; opacity: 0.5; display: block; z-index: 1000001; } position: absolute;
#dht-cfg { position: absolute; left: 50%; top: 50%; width: 800px; height: 262px; margin-left: -400px; margin-top: -131px; padding: 8px; background-color: #fff; z-index: 1000002; } left: 0;
#dht-cfg-note { margin-top: 22px; } top: 0;
#dht-cfg sub { color: #666; font-size: 13px; }`); width: 100%;
height: 100%;
background-color: #000;
opacity: 0.5;
display: block;
z-index: 1000001;
}
#dht-cfg {
position: absolute;
left: 50%;
top: 50%;
width: 800px;
height: 262px;
margin-left: -400px;
margin-top: -131px;
padding: 8px;
background-color: #fff;
z-index: 1000002;
}
#dht-cfg-note {
margin-top: 22px;
}
#dht-cfg sub {
color: #666;
font-size: 13px;
}
`);
// overlay // overlay
@@ -758,17 +719,17 @@ ${btn("close", "X")}`);
<label><input id='dht-cfg-autoscroll' type='checkbox'> Autoscroll</label><br> <label><input id='dht-cfg-autoscroll' type='checkbox'> Autoscroll</label><br>
<br> <br>
<label>After reaching the first message in channel...</label><br> <label>After reaching the first message in channel...</label><br>
${radio("afm", "nothing", "Do Nothing")} ${radio("afm", "nothing", "Continue Tracking")}
${radio("afm", "pause", "Pause Tracking")} ${radio("afm", "pause", "Pause Tracking")}
${radio("afm", "switch", "Switch to Next Channel")} ${radio("afm", "switch", "Switch to Next Channel")}
<br> <br>
<label>After reaching a previously saved message...</label><br> <label>After reaching a previously saved message...</label><br>
${radio("asm", "nothing", "Do Nothing")} ${radio("asm", "nothing", "Continue Tracking")}
${radio("asm", "pause", "Pause Tracking")} ${radio("asm", "pause", "Pause Tracking")}
${radio("asm", "switch", "Switch to Next Channel")} ${radio("asm", "switch", "Switch to Next Channel")}
<p id='dht-cfg-note'> <p id='dht-cfg-note'>
It is recommended to disable link and image previews to avoid putting unnecessary strain on your browser.<br><br> It is recommended to disable link and image previews to avoid putting unnecessary strain on your browser.<br><br>
<sub>v.31f, released 20 November 2023</sub> <sub>v.31h, released 03 March 2024</sub>
</p>`); </p>`);
// elements // elements
@@ -817,10 +778,7 @@ It is recommended to disable link and image previews to avoid putting unnecessar
} }
}, },
setStatus: function(state){ setStatus: function(status) {}
console.log("Status: " + state)
// TODO I guess.
}
}; };
return root; return root;
@@ -942,6 +900,16 @@ class SAVEFILE{
return parsedObj && typeof parsedObj.meta === "object" && typeof parsedObj.data === "object"; return parsedObj && typeof parsedObj.meta === "object" && typeof parsedObj.data === "object";
} }
static getDate(date){
if (date instanceof Date) {
return date;
}
else {
// noinspection JSUnresolvedReference
return date.toDate();
}
};
findOrRegisterUser(userId, userName, userDiscriminator, userAvatar){ findOrRegisterUser(userId, userName, userDiscriminator, userAvatar){
var wasPresent = userId in this.meta.users; var wasPresent = userId in this.meta.users;
var userObj = wasPresent ? this.meta.users[userId] : {}; var userObj = wasPresent ? this.meta.users[userId] : {};
@@ -1031,7 +999,7 @@ class SAVEFILE{
var obj = { var obj = {
u: this.findOrRegisterUser(author.id, author.username, author.bot ? null : author.discriminator, author.avatar), u: this.findOrRegisterUser(author.id, author.username, author.bot ? null : author.discriminator, author.avatar),
t: discordMessage.timestamp.toDate().getTime() t: SAVEFILE.getDate(discordMessage.timestamp).getTime()
}; };
if (discordMessage.content.length > 0){ if (discordMessage.content.length > 0){
@@ -1039,7 +1007,7 @@ class SAVEFILE{
} }
if (discordMessage.editedTimestamp !== null){ if (discordMessage.editedTimestamp !== null){
obj.te = discordMessage.editedTimestamp.toDate().getTime(); obj.te = SAVEFILE.getDate(discordMessage.editedTimestamp).getTime();
} }
if (discordMessage.embeds.length > 0){ if (discordMessage.embeds.length > 0){
@@ -1097,11 +1065,9 @@ class SAVEFILE{
addMessagesFromDiscord(discordMessageArray){ addMessagesFromDiscord(discordMessageArray){
var hasNewMessages = false; var hasNewMessages = false;
for(var discordMessage of discordMessageArray){
var type = discordMessage.type;
// https://discord.com/developers/docs/resources/channel#message-object-message-reference-structure for(var discordMessage of discordMessageArray){
if ((type === 0 || type === 19) && discordMessage.state === "SENT" && this.addMessage(discordMessage.channel_id, discordMessage.id, this.convertToMessageObject(discordMessage))){ if (this.addMessage(discordMessage.channel_id, discordMessage.id, this.convertToMessageObject(discordMessage))){
hasNewMessages = true; hasNewMessages = true;
} }
} }
@@ -1168,6 +1134,72 @@ class SAVEFILE{
} }
} }
const CONSTANTS = {
AUTOSCROLL_ACTION_NOTHING: "optNothing",
AUTOSCROLL_ACTION_PAUSE: "optPause",
AUTOSCROLL_ACTION_SWITCH: "optSwitch"
};
let IS_FIRST_RUN = false;
const SETTINGS = (function() {
const settingsChangedEvents = [];
const saveSettings = function() {
DOM.saveToCookie("DHT_SETTINGS", root, 60 * 60 * 24 * 365 * 5);
};
const triggerSettingsChanged = function(property) {
for (const callback of settingsChangedEvents) {
callback(property);
}
saveSettings();
};
const defineTriggeringProperty = function(obj, property, value) {
const name = "_" + property;
Object.defineProperty(obj, property, {
get: (() => obj[name]),
set: (value => {
obj[name] = value;
triggerSettingsChanged(property);
})
});
obj[name] = value;
};
let loaded = DOM.loadFromCookie("DHT_SETTINGS");
if (!loaded) {
loaded = {
"_autoscroll": true,
"_afterFirstMsg": CONSTANTS.AUTOSCROLL_ACTION_PAUSE,
"_afterSavedMsg": CONSTANTS.AUTOSCROLL_ACTION_PAUSE
};
IS_FIRST_RUN = true;
}
const root = {
onSettingsChanged(callback) {
settingsChangedEvents.push(callback);
}
};
defineTriggeringProperty(root, "autoscroll", loaded._autoscroll);
defineTriggeringProperty(root, "afterFirstMsg", loaded._afterFirstMsg);
defineTriggeringProperty(root, "afterSavedMsg", loaded._afterSavedMsg);
if (IS_FIRST_RUN) {
saveSettings();
}
return root;
})();
// noinspection FunctionWithInconsistentReturnsJS // noinspection FunctionWithInconsistentReturnsJS
const STATE = (function() { const STATE = (function() {
@@ -1280,23 +1312,25 @@ const STATE = (function() {
* Registers a Discord server and channel. * Registers a Discord server and channel.
*/ */
addDiscordChannel(serverInfo, channelInfo){ addDiscordChannel(serverInfo, channelInfo){
let serverName = serverInfo.name var serverName = serverInfo.name;
let serverType = serverInfo.type var serverType = serverInfo.type;
let channelId = channelInfo.id var channelId = channelInfo.id;
let channelName = channelInfo.name var channelName = channelInfo.name;
let extraInfo = channelInfo.extra var extraInfo = channelInfo.extra || {};
var serverIndex = this.getSavefile().findOrRegisterServer(serverName, serverType); var serverIndex = this.getSavefile().findOrRegisterServer(serverName, serverType);
if (this.getSavefile().tryRegisterChannel(serverIndex, channelId, channelName, extraInfo) === true){ if (this.getSavefile().tryRegisterChannel(serverIndex, channelId, channelName, extraInfo) === true){
this._triggerStateChanged("data", "channel"); this._triggerStateChanged("data", "channel");
} }
} }
// Right. Upstream desktop `bootstrap.js` expects an `async` here. I think it's fine.
/* /*
* Adds all messages from the array to the specified channel. Returns true if the savefile was updated. * Adds all messages from the array to the specified channel. Returns true if the savefile was updated.
*/ */
addDiscordMessages(discordMessageArray){ addDiscordMessages(discordMessageArray){
discordMessageArray = discordMessageArray.filter(msg => (msg.type === DISCORD.MESSAGE_TYPE.DEFAULT || msg.type === DISCORD.MESSAGE_TYPE.REPLY || msg.type === DISCORD.MESSAGE_TYPE.THREAD_STARTER) && msg.state === "SENT");
if (this.getSavefile().addMessagesFromDiscord(discordMessageArray)){ if (this.getSavefile().addMessagesFromDiscord(discordMessageArray)){
this._triggerStateChanged("data", "messages"); this._triggerStateChanged("data", "messages");
return true; return true;
@@ -1320,40 +1354,11 @@ const STATE = (function() {
this._trackingStateChangedListeners.push(callback); this._trackingStateChangedListeners.push(callback);
callback(this._isTracking); callback(this._isTracking);
} }
/*
* Shim for code from the desktop app.
*/
setup(port, token) {
console.log("Placeholder port and token: " + port + " " + token);
}
} }
return new CLS(); return new CLS();
})(); })();
// NOTE: STOP! This file must be kept in sync with the upstream desktop app branch in order for future changes/fixes to the desktop script to be cleanly merged here.
// Changes for the browser version should be done in another file to maintain mergeablity.
(function() {
const url = window.location.href;
if (!url.includes("discord.com/") && !url.includes("discordapp.com/") && !confirm("Could not detect Discord in the URL, do you want to run the script anyway?")) {
return;
}
if (window.DHT_LOADED) {
alert("Discord History Tracker is already loaded.");
return;
}
window.DHT_LOADED = true;
window.DHT_ON_UNLOAD = [];
/*[IMPORTS]*/
const port = 0; /*[PORT]*/
const token = "/*[TOKEN]*/";
STATE.setup(port, token);
let delayedStopRequests = 0; let delayedStopRequests = 0;
const stopTrackingDelayed = function(callback) { const stopTrackingDelayed = function(callback) {
@@ -1400,9 +1405,11 @@ const STATE = (function() {
let action = null; let action = null;
if (!DISCORD.hasMoreMessages()) { if (!DISCORD.hasMoreMessages()) {
console.debug("[DHT] Reached first message.");
action = SETTINGS.afterFirstMsg; action = SETTINGS.afterFirstMsg;
} }
if (isNoAction(action) && !anyNewMessages) { if (isNoAction(action) && !anyNewMessages) {
console.debug("[DHT] No new messages.");
action = SETTINGS.afterSavedMsg; action = SETTINGS.afterSavedMsg;
} }
@@ -1445,7 +1452,7 @@ const STATE = (function() {
isSending = true; isSending = true;
try { try {
await STATE.addDiscordChannel(info.server, info.channel); STATE.addDiscordChannel(info.server, info.channel);
} catch (e) { } catch (e) {
onError(e); onError(e);
return; return;
@@ -1457,7 +1464,7 @@ const STATE = (function() {
onTrackingContinued(false); onTrackingContinued(false);
} }
else { else {
const anyNewMessages = await STATE.addDiscordMessages(messages); const anyNewMessages = STATE.addDiscordMessages(messages);
onTrackingContinued(anyNewMessages); onTrackingContinued(anyNewMessages);
} }
} catch (e) { } catch (e) {
@@ -1491,8 +1498,6 @@ const STATE = (function() {
if (IS_FIRST_RUN) { if (IS_FIRST_RUN) {
GUI.showSettings(); GUI.showSettings();
} }
})();
/*[DEBUGGER]*/
}; };

File diff suppressed because one or more lines are too long

View File

@@ -8,11 +8,11 @@ import os
import re import re
import distutils.dir_util import distutils.dir_util
VERSION_SHORT = "v.31f" VERSION_SHORT = "v.31h"
VERSION_FULL = VERSION_SHORT + ", released 20 November 2023" VERSION_FULL = VERSION_SHORT + ", released 03 March 2024"
EXEC_UGLIFYJS_WIN = "{2}/lib/uglifyjs.cmd --parse bare_returns --compress --mangle toplevel --mangle-props keep_quoted,reserved=[{3}] --output \"{1}\" \"{0}\"" EXEC_UGLIFYJS_WIN = "{2}/lib/uglifyjs.cmd --parse bare_returns --compress --output \"{1}\" \"{0}\""
EXEC_UGLIFYJS_AUTO = "uglifyjs --parse bare_returns --compress --mangle toplevel --mangle-props keep_quoted,reserved=[{3}] --output \"{1}\" \"{0}\"" EXEC_UGLIFYJS_AUTO = "uglifyjs --parse bare_returns --compress --output \"{1}\" \"{0}\""
USE_UGLIFYJS = "--nominify" not in sys.argv USE_UGLIFYJS = "--nominify" not in sys.argv
USE_MINIFICATION = "--nominify" not in sys.argv USE_MINIFICATION = "--nominify" not in sys.argv
@@ -32,17 +32,13 @@ else:
USE_UGLIFYJS = False USE_UGLIFYJS = False
print("Could not find 'uglifyjs', JS minification will be disabled") print("Could not find 'uglifyjs', JS minification will be disabled")
if USE_UGLIFYJS:
with open("reserve.txt", "r") as reserved:
RESERVED_PROPS = ",".join(line.strip() for line in reserved.readlines())
# File Utilities # File Utilities
def combine_files(input_pattern, output_file): def combine_files(input_pattern, output_file):
is_first_file = True is_first_file = True
with fileinput.input(sorted(glob.glob(input_pattern, recursive=True))) as stream: with fileinput.input(sorted(glob.glob(input_pattern))) as stream:
for line in stream: for line in stream:
if stream.isfirstline(): if stream.isfirstline():
if is_first_file: if is_first_file:
@@ -53,6 +49,23 @@ def combine_files(input_pattern, output_file):
output_file.write(line.replace("{{{version:full}}}", VERSION_FULL)) output_file.write(line.replace("{{{version:full}}}", VERSION_FULL))
def combine_files_to_str(input_pattern):
is_first_file = True
output = []
with fileinput.input(sorted(glob.glob(input_pattern))) as stream:
for line in stream:
if stream.isfirstline():
if is_first_file:
is_first_file = False
else:
output.append("\n")
output.append(line.replace("{{{version:full}}}", VERSION_FULL))
return "".join(output)
def minify_css(input_file, output_file): def minify_css(input_file, output_file):
if not USE_MINIFICATION: if not USE_MINIFICATION:
if input_file != output_file: if input_file != output_file:
@@ -77,24 +90,39 @@ def minify_css(input_file, output_file):
# Build System # Build System
def build_tracker_html(): def build_tracker():
output_file_raw = "bld/track.js" output_file_raw = "bld/track.js"
output_file_html = "bld/track.html" output_file_html = "bld/track.html"
output_file_userscript = "bld/track.user.js"
output_file_tmp = "bld/track.tmp.js" with open("src/tracker/styles/controller.css", "r") as f:
input_pattern = "src/tracker/**/*.js" controller_css = f.read()
with open("src/tracker/styles/settings.css", "r") as f:
settings_css = f.read()
with open("src/tracker/bootstrap.js", "r") as f:
bootstrap_js = f.read()
combined_tracker_js = combine_files_to_str("src/tracker/scripts/*.js")
combined_tracker_js = combined_tracker_js.replace("/*[CSS-CONTROLLER]*/", controller_css)
combined_tracker_js = combined_tracker_js.replace("/*[CSS-SETTINGS]*/", settings_css)
full_tracker_js = bootstrap_js.replace("/*[IMPORTS]*/", combined_tracker_js)
with open(output_file_raw, "w") as out: with open(output_file_raw, "w") as out:
if not USE_UGLIFYJS: if not USE_UGLIFYJS:
out.write("(function(){\n") out.write("(function(){\n")
combine_files(input_pattern, out) out.write(full_tracker_js)
if not USE_UGLIFYJS: if not USE_UGLIFYJS:
out.write("})()") out.write("})()")
if USE_UGLIFYJS: if USE_UGLIFYJS:
os.system(EXEC_UGLIFYJS.format(output_file_raw, output_file_tmp, WORKING_DIR, RESERVED_PROPS)) output_file_tmp = "bld/track.tmp.js"
os.system(EXEC_UGLIFYJS.format(output_file_raw, output_file_tmp, WORKING_DIR))
with open(output_file_raw, "w") as out: with open(output_file_raw, "w") as out:
out.write("javascript:(function(){") out.write("javascript:(function(){")
@@ -107,25 +135,28 @@ def build_tracker_html():
os.remove(output_file_tmp) os.remove(output_file_tmp)
with open(output_file_raw, "r") as raw: with open(output_file_raw, "r") as raw:
script_contents = raw.read().replace("&", "&amp;").replace('"', "&quot;").replace("'", "&#x27;").replace("<", "&lt;").replace(">", "&gt;") minified_tracker_js = raw.read()
with open(output_file_html, "w") as out: write_tracker_html(output_file_html, minified_tracker_js)
out.write(script_contents) write_tracker_userscript(output_file_userscript, full_tracker_js)
def build_tracker_userscript(): def write_tracker_html(output_file, tracker_js):
output_file = "bld/track.user.js" tracker_js = tracker_js.replace("&", "&amp;").replace('"', "&quot;").replace("'", "&#x27;").replace("<", "&lt;").replace(">", "&gt;")
input_pattern = "src/tracker/**/*.js"
userscript_base = "src/base/track.user.js"
with open(userscript_base, "r") as base:
userscript_contents = base.read().replace("{{{version}}}", VERSION_SHORT).split("{{{contents}}}")
with open(output_file, "w") as out: with open(output_file, "w") as out:
out.write(userscript_contents[0]) out.write(tracker_js)
combine_files(input_pattern, out)
out.write(userscript_contents[1])
def write_tracker_userscript(output_file, full_tracker_js):
with open("src/base/track.user.js", "r") as f:
userscript_js = f.read()
userscript_js = userscript_js.replace("{{{version}}}", VERSION_SHORT)
userscript_js = userscript_js.replace("{{{contents}}}", full_tracker_js)
with open(output_file, "w") as out:
out.write(userscript_js)
def build_viewer(): def build_viewer():
@@ -150,7 +181,7 @@ def build_viewer():
combine_files(input_js_pattern, out) combine_files(input_js_pattern, out)
if USE_UGLIFYJS: if USE_UGLIFYJS:
os.system(EXEC_UGLIFYJS.format(tmp_js_file_combined, tmp_js_file_minified, WORKING_DIR, RESERVED_PROPS)) os.system(EXEC_UGLIFYJS.format(tmp_js_file_combined, tmp_js_file_minified, WORKING_DIR))
else: else:
shutil.copyfile(tmp_js_file_combined, tmp_js_file_minified) shutil.copyfile(tmp_js_file_combined, tmp_js_file_minified)
@@ -202,11 +233,8 @@ def build_website():
os.makedirs("bld", exist_ok = True) os.makedirs("bld", exist_ok = True)
print("Building tracker html...") print("Building tracker...")
build_tracker_html() build_tracker()
print("Building tracker userscript...")
build_tracker_userscript()
print("Building viewer...") print("Building viewer...")
build_viewer() build_viewer()

View File

@@ -1,74 +0,0 @@
autoscroll
_autoscroll
afterFirstMsg
_afterFirstMsg
afterSavedMsg
_afterSavedMsg
enableImagePreviews
_enableImagePreviews
enableFormatting
_enableFormatting
enableAnimatedEmoji
_enableAnimatedEmoji
enableUserAvatars
_enableUserAvatars
DHT_LOADED
DHT_EMBEDDED
meta
data
users
userindex
servers
channels
u
t
m
f
e
a
t
te
d
r
re
c
n
an
tag
avatar
author
type
state
name
position
topic
nsfw
id
username
bot
discriminator
timestamp
content
editedTimestamp
mentions
embeds
attachments
title
description
reply
reactions
emoji
count
animated
ext
toDate
memoizedProps
props
children
channel
messages
msSaveBlob
messageReference
message_id
guild_id
guild

View File

@@ -1,5 +0,0 @@
**STOP!**
These files must be kept in sync with the upstream desktop app branch in order for future changes/fixes to the desktop script to be cleanly merged here.
Changes for the browser version should be done in another file to maintain mergeablity.

View File

@@ -1,6 +1,5 @@
// NOTE: STOP! This file must be kept in sync with the upstream desktop app branch in order for future changes/fixes to the desktop script to be cleanly merged here. // noinspection JSAnnotator
// Changes for the browser version should be done in another file to maintain mergeablity.
(function() {
const url = window.location.href; const url = window.location.href;
if (!url.includes("discord.com/") && !url.includes("discordapp.com/") && !confirm("Could not detect Discord in the URL, do you want to run the script anyway?")) { if (!url.includes("discord.com/") && !url.includes("discordapp.com/") && !confirm("Could not detect Discord in the URL, do you want to run the script anyway?")) {
@@ -17,10 +16,6 @@
/*[IMPORTS]*/ /*[IMPORTS]*/
const port = 0; /*[PORT]*/
const token = "/*[TOKEN]*/";
STATE.setup(port, token);
let delayedStopRequests = 0; let delayedStopRequests = 0;
const stopTrackingDelayed = function(callback) { const stopTrackingDelayed = function(callback) {
delayedStopRequests++; delayedStopRequests++;
@@ -66,9 +61,11 @@
let action = null; let action = null;
if (!DISCORD.hasMoreMessages()) { if (!DISCORD.hasMoreMessages()) {
console.debug("[DHT] Reached first message.");
action = SETTINGS.afterFirstMsg; action = SETTINGS.afterFirstMsg;
} }
if (isNoAction(action) && !anyNewMessages) { if (isNoAction(action) && !anyNewMessages) {
console.debug("[DHT] No new messages.");
action = SETTINGS.afterSavedMsg; action = SETTINGS.afterSavedMsg;
} }
@@ -111,7 +108,7 @@
isSending = true; isSending = true;
try { try {
await STATE.addDiscordChannel(info.server, info.channel); STATE.addDiscordChannel(info.server, info.channel);
} catch (e) { } catch (e) {
onError(e); onError(e);
return; return;
@@ -123,7 +120,7 @@
onTrackingContinued(false); onTrackingContinued(false);
} }
else { else {
const anyNewMessages = await STATE.addDiscordMessages(messages); const anyNewMessages = STATE.addDiscordMessages(messages);
onTrackingContinued(anyNewMessages); onTrackingContinued(anyNewMessages);
} }
} catch (e) { } catch (e) {
@@ -157,5 +154,3 @@
if (IS_FIRST_RUN) { if (IS_FIRST_RUN) {
GUI.showSettings(); GUI.showSettings();
} }
})();
/*[DEBUGGER]*/

View File

@@ -1,5 +1,23 @@
// noinspection JSUnresolvedVariable // noinspection JSUnresolvedVariable
// noinspection LocalVariableNamingConventionJS
class DISCORD { class DISCORD {
// https://discord.com/developers/docs/resources/channel#channel-object-channel-types
static CHANNEL_TYPE = {
DM: 1,
GROUP_DM: 3,
ANNOUNCEMENT_THREAD: 10,
PUBLIC_THREAD: 11,
PRIVATE_THREAD: 12
};
// https://discord.com/developers/docs/resources/channel#message-object-message-types
static MESSAGE_TYPE = {
DEFAULT: 0,
REPLY: 19,
THREAD_STARTER: 21
};
static getMessageOuterElement() { static getMessageOuterElement() {
return DOM.queryReactClass("messagesWrapper"); return DOM.queryReactClass("messagesWrapper");
} }
@@ -28,46 +46,11 @@ class DISCORD {
* Calls the provided function with a list of messages whenever the currently loaded messages change. * Calls the provided function with a list of messages whenever the currently loaded messages change.
*/ */
static setupMessageCallback(callback) { static setupMessageCallback(callback) {
let skipsLeft = 0;
let waitForCleanup = false;
const previousMessages = new Set(); const previousMessages = new Set();
const timer = window.setInterval(() => { const onMessageElementsChanged = function() {
if (skipsLeft > 0) { const messages = DISCORD.getMessages();
--skipsLeft; const hasChanged = messages.some(message => !previousMessages.has(message.id)) || !DISCORD.hasMoreMessages();
return;
}
const view = this.getMessageOuterElement();
if (!view) {
skipsLeft = 2;
return;
}
const anyMessage = DOM.queryReactClass("message", this.getMessageOuterElement());
const messageCount = anyMessage ? anyMessage.parentElement.children.length : 0;
if (messageCount > 300) {
if (waitForCleanup) {
return;
}
skipsLeft = 3;
waitForCleanup = true;
window.setTimeout(() => {
const view = this.getMessageScrollerElement();
// noinspection JSUnusedGlobalSymbols
view.scrollTop = view.scrollHeight / 2;
}, 1);
}
else {
waitForCleanup = false;
}
const messages = this.getMessages();
const hasChanged = messages.some(message => !previousMessages.has(message.id)) || !this.hasMoreMessages();
if (!hasChanged) { if (!hasChanged) {
return; return;
@@ -79,24 +62,74 @@ class DISCORD {
} }
callback(messages); callback(messages);
}, 200); };
window.DHT_ON_UNLOAD.push(() => window.clearInterval(timer)); let debounceTimer;
/**
* Do not trigger the callback too often due to autoscrolling.
*/
const onMessageElementsChangedLater = function() {
window.clearTimeout(debounceTimer);
debounceTimer = window.setTimeout(onMessageElementsChanged, 100);
};
const observer = new MutationObserver(function () {
onMessageElementsChangedLater();
});
let skipsLeft = 0;
let observedElement = null;
const observerTimer = window.setInterval(() => {
if (skipsLeft > 0) {
--skipsLeft;
return;
}
const view = this.getMessageOuterElement();
if (!view) {
skipsLeft = 1;
return;
}
if (observedElement !== null && observedElement.isConnected) {
return;
}
observedElement = view.querySelector("[data-list-id='chat-messages']");
if (observedElement) {
console.debug("[DHT] Observed message container.");
observer.observe(observedElement, { childList: true });
onMessageElementsChangedLater();
}
}, 400);
window.DHT_ON_UNLOAD.push(() => {
observer.disconnect();
observedElement = null;
window.clearInterval(observerTimer);
});
} }
/** /**
* Returns the property object of a message element. * Returns the message from a message element.
* @returns { null | { message: DiscordMessage, channel: Object } } * @returns { null | DiscordMessage } }
*/ */
static getMessageElementProps(ele) { static getMessageFromElement(ele) {
const props = DOM.getReactProps(ele); const props = DOM.getReactProps(ele);
if (props.children && props.children.length) { if (props && Array.isArray(props.children)) {
for (let i = 3; i < props.children.length; i++) { for (const child of props.children) {
const childProps = props.children[i].props; if (!(child instanceof Object)) {
continue;
}
if (childProps && "message" in childProps && "channel" in childProps) { const childProps = child.props;
return childProps; if (childProps instanceof Object && "message" in childProps) {
return childProps.message;
} }
} }
} }
@@ -113,10 +146,10 @@ class DISCORD {
for (const ele of this.getMessageElements()) { for (const ele of this.getMessageElements()) {
try { try {
const props = this.getMessageElementProps(ele); const message = this.getMessageFromElement(ele);
if (props != null) { if (message != null) {
messages.push(props.message); messages.push(message);
} }
} catch (e) { } catch (e) {
console.error("[DHT] Error extracing message data, skipping it.", e, ele, DOM.tryGetReactProps(ele)); console.error("[DHT] Error extracing message data, skipping it.", e, ele, DOM.tryGetReactProps(ele));
@@ -137,7 +170,7 @@ class DISCORD {
*/ */
static getSelectedChannel() { static getSelectedChannel() {
try { try {
let obj; let obj = null;
try { try {
for (const child of DOM.getReactProps(DOM.queryReactClass("chatContent")).children) { for (const child of DOM.getReactProps(DOM.queryReactClass("chatContent")).children) {
@@ -148,15 +181,6 @@ class DISCORD {
} }
} catch (e) { } catch (e) {
console.error("[DHT] Error retrieving selected channel from 'chatContent' element.", e); console.error("[DHT] Error retrieving selected channel from 'chatContent' element.", e);
for (const ele of this.getMessageElements()) {
const props = this.getMessageElementProps(ele);
if (props != null) {
obj = props.channel;
break;
}
}
} }
if (!obj || typeof obj.id !== "string") { if (!obj || typeof obj.id !== "string") {
@@ -185,8 +209,8 @@ class DISCORD {
// https://discord.com/developers/docs/resources/channel#channel-object-channel-types // https://discord.com/developers/docs/resources/channel#channel-object-channel-types
switch (obj.type) { switch (obj.type) {
case 1: type = "DM"; break; case DISCORD.CHANNEL_TYPE.DM: type = "DM"; break;
case 3: type = "GROUP"; break; case DISCORD.CHANNEL_TYPE.GROUP_DM: type = "GROUP"; break;
default: return null; default: return null;
} }
@@ -224,7 +248,7 @@ class DISCORD {
} }
}; };
if (obj.parent_id) { if (obj.type === DISCORD.CHANNEL_TYPE.ANNOUNCEMENT_THREAD || obj.type === DISCORD.CHANNEL_TYPE.PUBLIC_THREAD || obj.type === DISCORD.CHANNEL_TYPE.PRIVATE_THREAD) {
channel["extra"]["parent"] = obj.parent_id; channel["extra"]["parent"] = obj.parent_id;
} }
else { else {

View File

@@ -84,14 +84,7 @@ var GUI = (function(){
// styles // styles
controller.styles = DOM.createStyle(` controller.styles = DOM.createStyle(`/*[CSS-CONTROLLER]*/`);
#app-mount div[class*="app-"] { margin-bottom: 48px !important; }
#dht-ctrl { position: absolute; bottom: 0; width: 100%; height: 48px; background-color: #FFF; z-index: 1000000; }
#dht-ctrl button { height: 32px; margin: 8px 0 8px 8px; font-size: 16px; padding: 0 12px; background-color: #7289DA; color: #FFF; text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.75); }
#dht-ctrl button:disabled { background-color: #7A7A7A; cursor: default; }
#dht-ctrl-close { margin: 8px 8px 8px 0 !important; float: right; }
#dht-ctrl p { display: inline-block; margin: 14px 12px; }
#dht-ctrl input { display: none; }`);
// main // main
@@ -104,7 +97,7 @@ ${btn("track", "")}
${btn("download", "Download")} ${btn("download", "Download")}
${btn("reset", "Reset")} ${btn("reset", "Reset")}
<p id='dht-ctrl-status'></p> <p id='dht-ctrl-status'></p>
<input id='dht-ctrl-upload-input' type='file' multiple> <input id='dht-ctrl-upload-input' type='file' multiple style="display: none">
${btn("close", "X")}`); ${btn("close", "X")}`);
// elements // elements
@@ -193,11 +186,7 @@ ${btn("close", "X")}`);
// styles // styles
settings.styles = DOM.createStyle(` settings.styles = DOM.createStyle(`/*[CSS-SETTINGS]*/`);
#dht-cfg-overlay { position: absolute; left: 0; top: 0; width: 100%; height: 100%; background-color: #000; opacity: 0.5; display: block; z-index: 1000001; }
#dht-cfg { position: absolute; left: 50%; top: 50%; width: 800px; height: 262px; margin-left: -400px; margin-top: -131px; padding: 8px; background-color: #fff; z-index: 1000002; }
#dht-cfg-note { margin-top: 22px; }
#dht-cfg sub { color: #666; font-size: 13px; }`);
// overlay // overlay
@@ -215,12 +204,12 @@ ${btn("close", "X")}`);
<label><input id='dht-cfg-autoscroll' type='checkbox'> Autoscroll</label><br> <label><input id='dht-cfg-autoscroll' type='checkbox'> Autoscroll</label><br>
<br> <br>
<label>After reaching the first message in channel...</label><br> <label>After reaching the first message in channel...</label><br>
${radio("afm", "nothing", "Do Nothing")} ${radio("afm", "nothing", "Continue Tracking")}
${radio("afm", "pause", "Pause Tracking")} ${radio("afm", "pause", "Pause Tracking")}
${radio("afm", "switch", "Switch to Next Channel")} ${radio("afm", "switch", "Switch to Next Channel")}
<br> <br>
<label>After reaching a previously saved message...</label><br> <label>After reaching a previously saved message...</label><br>
${radio("asm", "nothing", "Do Nothing")} ${radio("asm", "nothing", "Continue Tracking")}
${radio("asm", "pause", "Pause Tracking")} ${radio("asm", "pause", "Pause Tracking")}
${radio("asm", "switch", "Switch to Next Channel")} ${radio("asm", "switch", "Switch to Next Channel")}
<p id='dht-cfg-note'> <p id='dht-cfg-note'>
@@ -274,10 +263,7 @@ It is recommended to disable link and image previews to avoid putting unnecessar
} }
}, },
setStatus: function(state){ setStatus: function(status) {}
console.log("Status: " + state)
// TODO I guess.
}
}; };
return root; return root;

View File

@@ -114,6 +114,16 @@ class SAVEFILE{
return parsedObj && typeof parsedObj.meta === "object" && typeof parsedObj.data === "object"; return parsedObj && typeof parsedObj.meta === "object" && typeof parsedObj.data === "object";
} }
static getDate(date){
if (date instanceof Date) {
return date;
}
else {
// noinspection JSUnresolvedReference
return date.toDate();
}
};
findOrRegisterUser(userId, userName, userDiscriminator, userAvatar){ findOrRegisterUser(userId, userName, userDiscriminator, userAvatar){
var wasPresent = userId in this.meta.users; var wasPresent = userId in this.meta.users;
var userObj = wasPresent ? this.meta.users[userId] : {}; var userObj = wasPresent ? this.meta.users[userId] : {};
@@ -203,7 +213,7 @@ class SAVEFILE{
var obj = { var obj = {
u: this.findOrRegisterUser(author.id, author.username, author.bot ? null : author.discriminator, author.avatar), u: this.findOrRegisterUser(author.id, author.username, author.bot ? null : author.discriminator, author.avatar),
t: discordMessage.timestamp.toDate().getTime() t: SAVEFILE.getDate(discordMessage.timestamp).getTime()
}; };
if (discordMessage.content.length > 0){ if (discordMessage.content.length > 0){
@@ -211,7 +221,7 @@ class SAVEFILE{
} }
if (discordMessage.editedTimestamp !== null){ if (discordMessage.editedTimestamp !== null){
obj.te = discordMessage.editedTimestamp.toDate().getTime(); obj.te = SAVEFILE.getDate(discordMessage.editedTimestamp).getTime();
} }
if (discordMessage.embeds.length > 0){ if (discordMessage.embeds.length > 0){
@@ -269,11 +279,9 @@ class SAVEFILE{
addMessagesFromDiscord(discordMessageArray){ addMessagesFromDiscord(discordMessageArray){
var hasNewMessages = false; var hasNewMessages = false;
for(var discordMessage of discordMessageArray){
var type = discordMessage.type;
// https://discord.com/developers/docs/resources/channel#message-object-message-reference-structure for(var discordMessage of discordMessageArray){
if ((type === 0 || type === 19) && discordMessage.state === "SENT" && this.addMessage(discordMessage.channel_id, discordMessage.id, this.convertToMessageObject(discordMessage))){ if (this.addMessage(discordMessage.channel_id, discordMessage.id, this.convertToMessageObject(discordMessage))){
hasNewMessages = true; hasNewMessages = true;
} }
} }

View File

@@ -110,23 +110,25 @@ const STATE = (function() {
* Registers a Discord server and channel. * Registers a Discord server and channel.
*/ */
addDiscordChannel(serverInfo, channelInfo){ addDiscordChannel(serverInfo, channelInfo){
let serverName = serverInfo.name var serverName = serverInfo.name;
let serverType = serverInfo.type var serverType = serverInfo.type;
let channelId = channelInfo.id var channelId = channelInfo.id;
let channelName = channelInfo.name var channelName = channelInfo.name;
let extraInfo = channelInfo.extra var extraInfo = channelInfo.extra || {};
var serverIndex = this.getSavefile().findOrRegisterServer(serverName, serverType); var serverIndex = this.getSavefile().findOrRegisterServer(serverName, serverType);
if (this.getSavefile().tryRegisterChannel(serverIndex, channelId, channelName, extraInfo) === true){ if (this.getSavefile().tryRegisterChannel(serverIndex, channelId, channelName, extraInfo) === true){
this._triggerStateChanged("data", "channel"); this._triggerStateChanged("data", "channel");
} }
} }
// Right. Upstream desktop `bootstrap.js` expects an `async` here. I think it's fine.
/* /*
* Adds all messages from the array to the specified channel. Returns true if the savefile was updated. * Adds all messages from the array to the specified channel. Returns true if the savefile was updated.
*/ */
addDiscordMessages(discordMessageArray){ addDiscordMessages(discordMessageArray){
discordMessageArray = discordMessageArray.filter(msg => (msg.type === DISCORD.MESSAGE_TYPE.DEFAULT || msg.type === DISCORD.MESSAGE_TYPE.REPLY || msg.type === DISCORD.MESSAGE_TYPE.THREAD_STARTER) && msg.state === "SENT");
if (this.getSavefile().addMessagesFromDiscord(discordMessageArray)){ if (this.getSavefile().addMessagesFromDiscord(discordMessageArray)){
this._triggerStateChanged("data", "messages"); this._triggerStateChanged("data", "messages");
return true; return true;
@@ -150,13 +152,6 @@ const STATE = (function() {
this._trackingStateChangedListeners.push(callback); this._trackingStateChangedListeners.push(callback);
callback(this._isTracking); callback(this._isTracking);
} }
/*
* Shim for code from the desktop app.
*/
setup(port, token) {
console.log("Placeholder port and token: " + port + " " + token);
}
} }
return new CLS(); return new CLS();

View File

@@ -1,6 +1,3 @@
// NOTE: Currently unused. See `.createStyle()` calls in `gui.js`.
/*
const css_controller = `
#app-mount { #app-mount {
height: calc(100% - 48px) !important; height: calc(100% - 48px) !important;
} }
@@ -33,5 +30,8 @@ const css_controller = `
display: inline-block; display: inline-block;
margin: 14px 12px; margin: 14px 12px;
} }
`
*/ #dht-ctrl-close {
margin: 8px 8px 8px 0 !important;
float: right;
}

View File

@@ -1,6 +1,3 @@
// NOTE: Currently unused. See `.createStyle()` calls in `gui.js`.
/*
const css_settings = `
#dht-cfg-overlay { #dht-cfg-overlay {
position: absolute; position: absolute;
left: 0; left: 0;
@@ -29,5 +26,8 @@ const css_settings = `
#dht-cfg-note { #dht-cfg-note {
margin-top: 22px; margin-top: 22px;
} }
`
*/ #dht-cfg sub {
color: #666;
font-size: 13px;
}

View File

@@ -1,7 +1,8 @@
var DISCORD = (function(){ var DISCORD = (function(){
var REGEX = { var REGEX = {
formatBold: /\*\*([\s\S]+?)\*\*(?!\*)/g, formatBold: /\*\*([\s\S]+?)\*\*(?!\*)/g,
formatItalic: /(.)?\*([\s\S]+?)\*(?!\*)/g, formatItalic1: /\*([\s\S]+?)\*(?!\*)/g,
formatItalic2: /_([\s\S]+?)_(?!_)\b/g,
formatUnderline: /__([\s\S]+?)__(?!_)/g, formatUnderline: /__([\s\S]+?)__(?!_)/g,
formatStrike: /~~([\s\S]+?)~~(?!~)/g, formatStrike: /~~([\s\S]+?)~~(?!~)/g,
formatCodeInline: /(`+)\s*([\s\S]*?[^`])\s*\1(?!`)/g, formatCodeInline: /(`+)\s*([\s\S]*?[^`])\s*\1(?!`)/g,
@@ -54,7 +55,8 @@ var DISCORD = (function(){
.replace(REGEX.specialEscapedSingle, escapeHtmlMatch) .replace(REGEX.specialEscapedSingle, escapeHtmlMatch)
.replace(REGEX.specialEscapedDouble, full => full.replace(/\\/g, "").replace(/(.)/g, escapeHtmlMatch)) .replace(REGEX.specialEscapedDouble, full => full.replace(/\\/g, "").replace(/(.)/g, escapeHtmlMatch))
.replace(REGEX.formatBold, "<b>$1</b>") .replace(REGEX.formatBold, "<b>$1</b>")
.replace(REGEX.formatItalic, (full, pre, match) => pre === '\\' ? full : (pre || "")+"<i>"+match+"</i>") .replace(REGEX.formatItalic1, "<i>$1</i>")
.replace(REGEX.formatItalic2, "<i>$1</i>")
.replace(REGEX.formatUnderline, "<u>$1</u>") .replace(REGEX.formatUnderline, "<u>$1</u>")
.replace(REGEX.formatStrike, "<s>$1</s>"); .replace(REGEX.formatStrike, "<s>$1</s>");
} }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

View File

@@ -13,110 +13,10 @@
</head> </head>
<body> <body>
<div class="inner"> <div class="inner">
<h1>Discord History Tracker <span class="version">{{{version:web}}}</span>&nbsp;<span class="bar">|</span>&nbsp;<span class="notes"><a href="https://github.com/chylex/Discord-History-Tracker/wiki/Release-Notes">Release&nbsp;Notes</a></span></h1> <h1>Discord History Tracker&nbsp;<span class="bar">|</span>&nbsp;<span class="notes"><a href="https://github.com/chylex/Discord-History-Tracker/wiki/Release-Notes">Release&nbsp;Notes</a></span>&nbsp;<span class="bar">|</span>&nbsp;<span class="notes"><a href="https://github.com/chylex/Discord-History-Tracker/wiki/Old-Versions">Old&nbsp;Versions</a></span></h1>
<p>Discord History Tracker lets you save chat history in your servers, groups, and private conversations, and view it offline.</p>
<img src="img/tracker.png" width="851" class="dht bordered"> <img src="img/tracker.png" width="851" class="dht bordered">
<p>This page explains how to use Discord History Tracker entirely in your browser. While this method gets you started quicker and works on any device that has a modern web browser, it has <strong>significant limitations and fewer features</strong> than the <a href="https://dht.chylex.com">desktop app</a>.</p> <p>The browser-only version of Discord History Tracker has been superseded by a <a href="https://dht.chylex.com">desktop app version</a>, and is no longer supported.</p>
<p>Because everything happens in your browser, if the browser tab is closed, or your browser or computer crashes, you will lose all progress. Your browser may also crash or freeze if you have too many messages. If this is a concern, <a href="https://dht.chylex.com">use the desktop app</a> instead.</p> <p>You can import archives from the browser-only version into the desktop app, or view them by downloading the <a href="build/viewer.html">Viewer</a>.</p>
<h2>How to Use</h2>
<p>A tracking script will load messages according to your settings, and temporarily save them in your browser. Once you finish tracking, the browser will create an archive file you can save to your disk, and open in an offline viewer later.</p>
<h3>Setup the Tracking Script</h3>
<h4>Option 1: Userscript</h4>
<div class="quote">
<p><strong>Preferred option.</strong> Requires a browser addon, but DHT will stay up-to-date and be easily accessible on the Discord website.</p>
<ol>
<li>Install a userscript manager addon:
<ul>
<li><a href="https://violentmonkey.github.io/get-it/">Violentmonkey</a> (Chrome)</li>
<li><a href="https://tampermonkey.net/">Tampermonkey</a> (Firefox, Edge, Chrome, Opera)</li>
<li>Due to browser bugs / limitations, DHT will not work in <strong>Firefox</strong> with Greasemonkey / Violentmonkey, and in <strong>Safari</strong> at all</li>
</ul>
</li>
<li>Click <a href="build/track.user.js">Install Userscript</a> to prompt an installation into the userscript manager</li>
<li>Open <a href="https://discord.com/channels/@me" rel="noreferrer">Discord</a>, and view any server, group, or private conversation (it will not appear in Friends list)</li>
<li>Click <strong>DHT</strong> in the top right corner:<br><img src="img/button.png" class="bordered"></li>
</ol>
</div> </div>
<h4>Option 2: Browser / Discord Console</h4>
<div class="quote">
<p>The console is the only way to use DHT directly in the desktop app.</p>
<ol>
<li>Click <a href="javascript:" id="tracker-copy-button" onauxclick="return false;">Copy to Clipboard</a> to copy the tracking script
<noscript> (requires JavaScript)</noscript>
</li>
<li>Press <strong>Ctrl</strong>+<strong>Shift</strong>+<strong>I</strong> in your browser or the Discord app, and select the <strong>Console</strong> tab</li>
<li>Paste the script into the console, and press <strong>Enter</strong> to run it</li>
<li>Press <strong>Ctrl</strong>+<strong>Shift</strong>+<strong>I</strong> again to close the console</li>
</ol>
<p id="tracker-copy-issue">Your browser may not support copying to clipboard, please try copying the script manually:</p>
<textarea id="tracker-copy-contents"><?php include './build/track.html'; ?></textarea>
</div>
<h4>Option 3: Bookmarklet</h4>
<div class="quote">
<p>Requires Firefox 69 or newer.</p>
<ol>
<li>Right-click <a href="<?php include './build/track.html'; ?>" onclick="return false;" onauxclick="return false;">Discord History Tracker</a></li>
<li>Select &laquo;Bookmark This Link&raquo; and save the bookmark</li>
<li>Open <a href="https://discord.com/channels/@me" rel="noreferrer">Discord</a> and click the bookmark to run the script</li>
</ol>
</div>
<h4>Old Versions</h4>
<p>Whenever DHT is updated to work with a new version of Discord, it may no longer work with the previous version of Discord.</p>
<p>If you haven't received that Discord update yet, see <a href="https://github.com/chylex/Discord-History-Tracker/wiki/Release-Notes">Release Notes</a> for information about recent updates, and <a href="https://github.com/chylex/Discord-History-Tracker/wiki/Old-Versions">Old Versions</a> if you need to use an older version of DHT.</p>
<h3>How to Track Messages</h3>
<p>When using the script for the first time, you will see a <strong>Settings</strong> dialog where you can configure the script. These settings will be remembered as long as you don't delete cookies in your browser.</p>
<p>By default, Discord History Tracker is set to automatically scroll up to load the channel history, and pause tracking if it reaches a previously saved message to avoid unnecessary history loading.</p>
<p>Before you <strong>Start Tracking</strong>, you may use <strong>Upload &amp; Combine</strong> to load messages from a previously saved archive file into the browser.</p>
<p>When you click <strong>Download</strong>, the browser will generate an archive file from saved messages, and lets you save it to your computer.</p>
<h3>How to View History</h3>
<p>First, save the <a href="build/viewer.html">Viewer</a> file to your computer. Then you can open the downloaded viewer in your browser, click <strong>Load File</strong>, and select the archive to view.</p>
<h2>External Links</h2>
<p class="links">
<a href="https://github.com/chylex/Discord-History-Tracker/issues">Issues&nbsp;&amp;&nbsp;Suggestions</a>&nbsp;&nbsp;&mdash;&nbsp;
<a href="https://github.com/chylex/Discord-History-Tracker/tree/master-browser-only">Source&nbsp;Code</a>&nbsp;&nbsp;&mdash;&nbsp;
<a href="https://twitter.com/chylexmc">Follow&nbsp;Dev&nbsp;on&nbsp;Twitter</a>&nbsp;&nbsp;&mdash;&nbsp;
<a href="https://www.patreon.com/chylex">Support&nbsp;via&nbsp;Patreon</a>&nbsp;&nbsp;&mdash;&nbsp;
<a href="https://ko-fi.com/chylex">Support&nbsp;via&nbsp;Ko-fi</a>
</p>
</div>
<script type="text/javascript">
var contents = document.getElementById("tracker-copy-contents");
var issue = document.getElementById("tracker-copy-issue");
var button = document.getElementById("tracker-copy-button");
if (document.queryCommandSupported("copy")) {
contents.style.display = "none";
issue.style.display = "none";
}
button.addEventListener("click", function() {
contents.style.display = "block";
issue.style.display = "block";
contents.select();
document.execCommand("copy");
button.innerHTML = "Copied to Clipboard";
contents.style.display = "none";
issue.style.display = "none";
});
contents.addEventListener("click", function() {
contents.select();
});
</script>
</body> </body>
</html> </html>