mirror of
https://github.com/chylex/Discord-History-Tracker.git
synced 2025-09-15 19:32:09 +02:00
Compare commits
3 Commits
master-bro
...
72b23ee4f8
Author | SHA1 | Date | |
---|---|---|---|
![]() |
72b23ee4f8 | ||
![]() |
274f40636e | ||
![]() |
bc438753a3 |
@@ -31,7 +31,6 @@ 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:
|
||||
* `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
|
||||
|
||||
You can tweak the build process using the following flags:
|
||||
|
1484
bld/track.js
1484
bld/track.js
File diff suppressed because one or more lines are too long
@@ -1,6 +1,6 @@
|
||||
// ==UserScript==
|
||||
// @name Discord History Tracker
|
||||
// @version v.31h
|
||||
// @version v.31f
|
||||
// @license MIT
|
||||
// @namespace https://chylex.com
|
||||
// @homepageURL https://dht.chylex.com/
|
||||
@@ -12,42 +12,80 @@
|
||||
|
||||
const start = function(){
|
||||
|
||||
// noinspection JSAnnotator
|
||||
|
||||
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;
|
||||
// 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_controller = `
|
||||
#app-mount {
|
||||
height: calc(100% - 48px) !important;
|
||||
}
|
||||
|
||||
if (window.DHT_LOADED) {
|
||||
alert("Discord History Tracker is already loaded.");
|
||||
return;
|
||||
#dht-ctrl {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
height: 48px;
|
||||
background-color: #fff;
|
||||
z-index: 1000000;
|
||||
}
|
||||
|
||||
window.DHT_LOADED = true;
|
||||
window.DHT_ON_UNLOAD = [];
|
||||
#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;
|
||||
}
|
||||
`
|
||||
*/
|
||||
|
||||
// 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 LocalVariableNamingConventionJS
|
||||
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() {
|
||||
return DOM.queryReactClass("messagesWrapper");
|
||||
}
|
||||
@@ -76,11 +114,46 @@ class DISCORD {
|
||||
* Calls the provided function with a list of messages whenever the currently loaded messages change.
|
||||
*/
|
||||
static setupMessageCallback(callback) {
|
||||
let skipsLeft = 0;
|
||||
let waitForCleanup = false;
|
||||
const previousMessages = new Set();
|
||||
|
||||
const onMessageElementsChanged = function() {
|
||||
const messages = DISCORD.getMessages();
|
||||
const hasChanged = messages.some(message => !previousMessages.has(message.id)) || !DISCORD.hasMoreMessages();
|
||||
const timer = window.setInterval(() => {
|
||||
if (skipsLeft > 0) {
|
||||
--skipsLeft;
|
||||
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) {
|
||||
return;
|
||||
@@ -92,74 +165,24 @@ class DISCORD {
|
||||
}
|
||||
|
||||
callback(messages);
|
||||
};
|
||||
}, 200);
|
||||
|
||||
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);
|
||||
});
|
||||
window.DHT_ON_UNLOAD.push(() => window.clearInterval(timer));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the message from a message element.
|
||||
* @returns { null | DiscordMessage } }
|
||||
* Returns the property object of a message element.
|
||||
* @returns { null | { message: DiscordMessage, channel: Object } }
|
||||
*/
|
||||
static getMessageFromElement(ele) {
|
||||
static getMessageElementProps(ele) {
|
||||
const props = DOM.getReactProps(ele);
|
||||
|
||||
if (props && Array.isArray(props.children)) {
|
||||
for (const child of props.children) {
|
||||
if (!(child instanceof Object)) {
|
||||
continue;
|
||||
}
|
||||
if (props.children && props.children.length) {
|
||||
for (let i = 3; i < props.children.length; i++) {
|
||||
const childProps = props.children[i].props;
|
||||
|
||||
const childProps = child.props;
|
||||
if (childProps instanceof Object && "message" in childProps) {
|
||||
return childProps.message;
|
||||
if (childProps && "message" in childProps && "channel" in childProps) {
|
||||
return childProps;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -176,10 +199,10 @@ class DISCORD {
|
||||
|
||||
for (const ele of this.getMessageElements()) {
|
||||
try {
|
||||
const message = this.getMessageFromElement(ele);
|
||||
const props = this.getMessageElementProps(ele);
|
||||
|
||||
if (message != null) {
|
||||
messages.push(message);
|
||||
if (props != null) {
|
||||
messages.push(props.message);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("[DHT] Error extracing message data, skipping it.", e, ele, DOM.tryGetReactProps(ele));
|
||||
@@ -200,7 +223,7 @@ class DISCORD {
|
||||
*/
|
||||
static getSelectedChannel() {
|
||||
try {
|
||||
let obj = null;
|
||||
let obj;
|
||||
|
||||
try {
|
||||
for (const child of DOM.getReactProps(DOM.queryReactClass("chatContent")).children) {
|
||||
@@ -211,6 +234,15 @@ class DISCORD {
|
||||
}
|
||||
} catch (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") {
|
||||
@@ -239,8 +271,8 @@ class DISCORD {
|
||||
|
||||
// https://discord.com/developers/docs/resources/channel#channel-object-channel-types
|
||||
switch (obj.type) {
|
||||
case DISCORD.CHANNEL_TYPE.DM: type = "DM"; break;
|
||||
case DISCORD.CHANNEL_TYPE.GROUP_DM: type = "GROUP"; break;
|
||||
case 1: type = "DM"; break;
|
||||
case 3: type = "GROUP"; break;
|
||||
default: return null;
|
||||
}
|
||||
|
||||
@@ -278,7 +310,7 @@ class DISCORD {
|
||||
}
|
||||
};
|
||||
|
||||
if (obj.type === DISCORD.CHANNEL_TYPE.ANNOUNCEMENT_THREAD || obj.type === DISCORD.CHANNEL_TYPE.PUBLIC_THREAD || obj.type === DISCORD.CHANNEL_TYPE.PRIVATE_THREAD) {
|
||||
if (obj.parent_id) {
|
||||
channel["extra"]["parent"] = obj.parent_id;
|
||||
}
|
||||
else {
|
||||
@@ -443,6 +475,72 @@ 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 controller;
|
||||
var settings;
|
||||
@@ -529,44 +627,14 @@ var GUI = (function(){
|
||||
|
||||
// styles
|
||||
|
||||
controller.styles = DOM.createStyle(`#app-mount {
|
||||
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 p {
|
||||
display: inline-block;
|
||||
margin: 14px 12px;
|
||||
}
|
||||
|
||||
#dht-ctrl-close {
|
||||
margin: 8px 8px 8px 0 !important;
|
||||
float: right;
|
||||
}
|
||||
`);
|
||||
controller.styles = DOM.createStyle(`
|
||||
#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
|
||||
|
||||
@@ -579,7 +647,7 @@ ${btn("track", "")}
|
||||
${btn("download", "Download")}
|
||||
${btn("reset", "Reset")}
|
||||
<p id='dht-ctrl-status'></p>
|
||||
<input id='dht-ctrl-upload-input' type='file' multiple style="display: none">
|
||||
<input id='dht-ctrl-upload-input' type='file' multiple>
|
||||
${btn("close", "X")}`);
|
||||
|
||||
// elements
|
||||
@@ -668,40 +736,11 @@ ${btn("close", "X")}`);
|
||||
|
||||
// styles
|
||||
|
||||
settings.styles = DOM.createStyle(`#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;
|
||||
}
|
||||
`);
|
||||
settings.styles = DOM.createStyle(`
|
||||
#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
|
||||
|
||||
@@ -719,17 +758,17 @@ ${btn("close", "X")}`);
|
||||
<label><input id='dht-cfg-autoscroll' type='checkbox'> Autoscroll</label><br>
|
||||
<br>
|
||||
<label>After reaching the first message in channel...</label><br>
|
||||
${radio("afm", "nothing", "Continue Tracking")}
|
||||
${radio("afm", "nothing", "Do Nothing")}
|
||||
${radio("afm", "pause", "Pause Tracking")}
|
||||
${radio("afm", "switch", "Switch to Next Channel")}
|
||||
<br>
|
||||
<label>After reaching a previously saved message...</label><br>
|
||||
${radio("asm", "nothing", "Continue Tracking")}
|
||||
${radio("asm", "nothing", "Do Nothing")}
|
||||
${radio("asm", "pause", "Pause Tracking")}
|
||||
${radio("asm", "switch", "Switch to Next Channel")}
|
||||
<p id='dht-cfg-note'>
|
||||
It is recommended to disable link and image previews to avoid putting unnecessary strain on your browser.<br><br>
|
||||
<sub>v.31h, released 03 March 2024</sub>
|
||||
<sub>v.31f, released 20 November 2023</sub>
|
||||
</p>`);
|
||||
|
||||
// elements
|
||||
@@ -778,7 +817,10 @@ It is recommended to disable link and image previews to avoid putting unnecessar
|
||||
}
|
||||
},
|
||||
|
||||
setStatus: function(status) {}
|
||||
setStatus: function(state){
|
||||
console.log("Status: " + state)
|
||||
// TODO I guess.
|
||||
}
|
||||
};
|
||||
|
||||
return root;
|
||||
@@ -899,16 +941,6 @@ class SAVEFILE{
|
||||
static isValid(parsedObj){
|
||||
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){
|
||||
var wasPresent = userId in this.meta.users;
|
||||
@@ -963,15 +995,15 @@ class SAVEFILE{
|
||||
|
||||
channelObj.name = channelName;
|
||||
|
||||
if (extraInfo.position) {
|
||||
if (extraInfo.position){
|
||||
channelObj.position = extraInfo.position;
|
||||
}
|
||||
|
||||
if (extraInfo.topic) {
|
||||
if (extraInfo.topic){
|
||||
channelObj.topic = extraInfo.topic;
|
||||
}
|
||||
|
||||
if (extraInfo.nsfw) {
|
||||
if (extraInfo.nsfw){
|
||||
channelObj.nsfw = extraInfo.nsfw;
|
||||
}
|
||||
|
||||
@@ -999,7 +1031,7 @@ class SAVEFILE{
|
||||
|
||||
var obj = {
|
||||
u: this.findOrRegisterUser(author.id, author.username, author.bot ? null : author.discriminator, author.avatar),
|
||||
t: SAVEFILE.getDate(discordMessage.timestamp).getTime()
|
||||
t: discordMessage.timestamp.toDate().getTime()
|
||||
};
|
||||
|
||||
if (discordMessage.content.length > 0){
|
||||
@@ -1007,7 +1039,7 @@ class SAVEFILE{
|
||||
}
|
||||
|
||||
if (discordMessage.editedTimestamp !== null){
|
||||
obj.te = SAVEFILE.getDate(discordMessage.editedTimestamp).getTime();
|
||||
obj.te = discordMessage.editedTimestamp.toDate().getTime();
|
||||
}
|
||||
|
||||
if (discordMessage.embeds.length > 0){
|
||||
@@ -1065,9 +1097,11 @@ class SAVEFILE{
|
||||
|
||||
addMessagesFromDiscord(discordMessageArray){
|
||||
var hasNewMessages = false;
|
||||
|
||||
for(var discordMessage of discordMessageArray){
|
||||
if (this.addMessage(discordMessage.channel_id, discordMessage.id, this.convertToMessageObject(discordMessage))){
|
||||
var type = discordMessage.type;
|
||||
|
||||
// https://discord.com/developers/docs/resources/channel#message-object-message-reference-structure
|
||||
if ((type === 0 || type === 19) && discordMessage.state === "SENT" && this.addMessage(discordMessage.channel_id, discordMessage.id, this.convertToMessageObject(discordMessage))){
|
||||
hasNewMessages = true;
|
||||
}
|
||||
}
|
||||
@@ -1134,72 +1168,6 @@ 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
|
||||
const STATE = (function() {
|
||||
|
||||
@@ -1312,25 +1280,23 @@ const STATE = (function() {
|
||||
* Registers a Discord server and channel.
|
||||
*/
|
||||
addDiscordChannel(serverInfo, channelInfo){
|
||||
var serverName = serverInfo.name;
|
||||
var serverType = serverInfo.type;
|
||||
var channelId = channelInfo.id;
|
||||
var channelName = channelInfo.name;
|
||||
var extraInfo = channelInfo.extra || {};
|
||||
|
||||
let serverName = serverInfo.name
|
||||
let serverType = serverInfo.type
|
||||
let channelId = channelInfo.id
|
||||
let channelName = channelInfo.name
|
||||
let extraInfo = channelInfo.extra
|
||||
var serverIndex = this.getSavefile().findOrRegisterServer(serverName, serverType);
|
||||
|
||||
if (this.getSavefile().tryRegisterChannel(serverIndex, channelId, channelName, extraInfo) === true){
|
||||
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.
|
||||
*/
|
||||
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)){
|
||||
this._triggerStateChanged("data", "messages");
|
||||
return true;
|
||||
@@ -1354,150 +1320,179 @@ const STATE = (function() {
|
||||
this._trackingStateChangedListeners.push(callback);
|
||||
callback(this._isTracking);
|
||||
}
|
||||
|
||||
/*
|
||||
* Shim for code from the desktop app.
|
||||
*/
|
||||
setup(port, token) {
|
||||
console.log("Placeholder port and token: " + port + " " + token);
|
||||
}
|
||||
}
|
||||
|
||||
return new CLS();
|
||||
})();
|
||||
|
||||
|
||||
let delayedStopRequests = 0;
|
||||
const stopTrackingDelayed = function(callback) {
|
||||
delayedStopRequests++;
|
||||
// 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;
|
||||
|
||||
window.setTimeout(() => {
|
||||
STATE.setIsTracking(false);
|
||||
delayedStopRequests--;
|
||||
|
||||
if (callback) {
|
||||
callback();
|
||||
}
|
||||
}, 200); // give the user visual feedback after clicking the button before switching off
|
||||
};
|
||||
|
||||
let hasJustStarted = false;
|
||||
let isSending = false;
|
||||
|
||||
const onError = function(e) {
|
||||
console.log(e);
|
||||
GUI.setStatus(e.status === "DISCONNECTED" ? "Disconnected" : "Error");
|
||||
stopTrackingDelayed(() => isSending = false);
|
||||
};
|
||||
|
||||
const isNoAction = function(action) {
|
||||
return action === null || action === CONSTANTS.AUTOSCROLL_ACTION_NOTHING;
|
||||
};
|
||||
|
||||
const onTrackingContinued = function(anyNewMessages) {
|
||||
if (!STATE.isTracking()) {
|
||||
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;
|
||||
}
|
||||
|
||||
GUI.setStatus("Tracking");
|
||||
|
||||
if (hasJustStarted) {
|
||||
anyNewMessages = true;
|
||||
hasJustStarted = false;
|
||||
if (window.DHT_LOADED) {
|
||||
alert("Discord History Tracker is already loaded.");
|
||||
return;
|
||||
}
|
||||
|
||||
isSending = false;
|
||||
window.DHT_LOADED = true;
|
||||
window.DHT_ON_UNLOAD = [];
|
||||
|
||||
if (SETTINGS.autoscroll) {
|
||||
let action = null;
|
||||
/*[IMPORTS]*/
|
||||
|
||||
const port = 0; /*[PORT]*/
|
||||
const token = "/*[TOKEN]*/";
|
||||
STATE.setup(port, token);
|
||||
|
||||
let delayedStopRequests = 0;
|
||||
const stopTrackingDelayed = function(callback) {
|
||||
delayedStopRequests++;
|
||||
|
||||
if (!DISCORD.hasMoreMessages()) {
|
||||
console.debug("[DHT] Reached first message.");
|
||||
action = SETTINGS.afterFirstMsg;
|
||||
}
|
||||
if (isNoAction(action) && !anyNewMessages) {
|
||||
console.debug("[DHT] No new messages.");
|
||||
action = SETTINGS.afterSavedMsg;
|
||||
}
|
||||
|
||||
if (isNoAction(action)) {
|
||||
DISCORD.loadOlderMessages();
|
||||
}
|
||||
else if (action === CONSTANTS.AUTOSCROLL_ACTION_PAUSE || (action === CONSTANTS.AUTOSCROLL_ACTION_SWITCH && !DISCORD.selectNextTextChannel())) {
|
||||
GUI.setStatus("Reached End");
|
||||
window.setTimeout(() => {
|
||||
STATE.setIsTracking(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let waitUntilSendingFinishedTimer = null;
|
||||
|
||||
const onMessagesUpdated = async messages => {
|
||||
if (!STATE.isTracking() || delayedStopRequests > 0) {
|
||||
return;
|
||||
}
|
||||
delayedStopRequests--;
|
||||
|
||||
if (callback) {
|
||||
callback();
|
||||
}
|
||||
}, 200); // give the user visual feedback after clicking the button before switching off
|
||||
};
|
||||
|
||||
if (isSending) {
|
||||
window.clearTimeout(waitUntilSendingFinishedTimer);
|
||||
|
||||
waitUntilSendingFinishedTimer = window.setTimeout(() => {
|
||||
waitUntilSendingFinishedTimer = null;
|
||||
onMessagesUpdated(messages);
|
||||
}, 100);
|
||||
|
||||
return;
|
||||
}
|
||||
let hasJustStarted = false;
|
||||
let isSending = false;
|
||||
|
||||
const info = DISCORD.getSelectedChannel();
|
||||
const onError = function(e) {
|
||||
console.log(e);
|
||||
GUI.setStatus(e.status === "DISCONNECTED" ? "Disconnected" : "Error");
|
||||
stopTrackingDelayed(() => isSending = false);
|
||||
};
|
||||
|
||||
if (!info) {
|
||||
GUI.setStatus("Error (Unknown Channel)");
|
||||
stopTrackingDelayed();
|
||||
return;
|
||||
}
|
||||
const isNoAction = function(action) {
|
||||
return action === null || action === CONSTANTS.AUTOSCROLL_ACTION_NOTHING;
|
||||
};
|
||||
|
||||
isSending = true;
|
||||
|
||||
try {
|
||||
STATE.addDiscordChannel(info.server, info.channel);
|
||||
} catch (e) {
|
||||
onError(e);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
if (!messages.length) {
|
||||
isSending = false;
|
||||
onTrackingContinued(false);
|
||||
}
|
||||
else {
|
||||
const anyNewMessages = STATE.addDiscordMessages(messages);
|
||||
onTrackingContinued(anyNewMessages);
|
||||
}
|
||||
} catch (e) {
|
||||
onError(e);
|
||||
}
|
||||
};
|
||||
|
||||
DISCORD.setupMessageCallback(onMessagesUpdated);
|
||||
|
||||
STATE.onTrackingStateChanged(enabled => {
|
||||
if (enabled) {
|
||||
const messages = DISCORD.getMessages();
|
||||
|
||||
if (messages.length === 0) {
|
||||
stopTrackingDelayed(() => alert("Cannot see any messages."));
|
||||
const onTrackingContinued = function(anyNewMessages) {
|
||||
if (!STATE.isTracking()) {
|
||||
return;
|
||||
}
|
||||
|
||||
GUI.setStatus("Starting");
|
||||
hasJustStarted = true;
|
||||
// noinspection JSIgnoredPromiseFromCall
|
||||
onMessagesUpdated(messages);
|
||||
}
|
||||
else {
|
||||
GUI.setStatus("Tracking");
|
||||
|
||||
if (hasJustStarted) {
|
||||
anyNewMessages = true;
|
||||
hasJustStarted = false;
|
||||
}
|
||||
|
||||
isSending = false;
|
||||
|
||||
if (SETTINGS.autoscroll) {
|
||||
let action = null;
|
||||
|
||||
if (!DISCORD.hasMoreMessages()) {
|
||||
action = SETTINGS.afterFirstMsg;
|
||||
}
|
||||
if (isNoAction(action) && !anyNewMessages) {
|
||||
action = SETTINGS.afterSavedMsg;
|
||||
}
|
||||
|
||||
if (isNoAction(action)) {
|
||||
DISCORD.loadOlderMessages();
|
||||
}
|
||||
else if (action === CONSTANTS.AUTOSCROLL_ACTION_PAUSE || (action === CONSTANTS.AUTOSCROLL_ACTION_SWITCH && !DISCORD.selectNextTextChannel())) {
|
||||
GUI.setStatus("Reached End");
|
||||
STATE.setIsTracking(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let waitUntilSendingFinishedTimer = null;
|
||||
|
||||
const onMessagesUpdated = async messages => {
|
||||
if (!STATE.isTracking() || delayedStopRequests > 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isSending) {
|
||||
window.clearTimeout(waitUntilSendingFinishedTimer);
|
||||
|
||||
waitUntilSendingFinishedTimer = window.setTimeout(() => {
|
||||
waitUntilSendingFinishedTimer = null;
|
||||
onMessagesUpdated(messages);
|
||||
}, 100);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const info = DISCORD.getSelectedChannel();
|
||||
|
||||
if (!info) {
|
||||
GUI.setStatus("Error (Unknown Channel)");
|
||||
stopTrackingDelayed();
|
||||
return;
|
||||
}
|
||||
|
||||
isSending = true;
|
||||
|
||||
try {
|
||||
await STATE.addDiscordChannel(info.server, info.channel);
|
||||
} catch (e) {
|
||||
onError(e);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
if (!messages.length) {
|
||||
isSending = false;
|
||||
onTrackingContinued(false);
|
||||
}
|
||||
else {
|
||||
const anyNewMessages = await STATE.addDiscordMessages(messages);
|
||||
onTrackingContinued(anyNewMessages);
|
||||
}
|
||||
} catch (e) {
|
||||
onError(e);
|
||||
}
|
||||
};
|
||||
|
||||
DISCORD.setupMessageCallback(onMessagesUpdated);
|
||||
|
||||
STATE.onTrackingStateChanged(enabled => {
|
||||
if (enabled) {
|
||||
const messages = DISCORD.getMessages();
|
||||
|
||||
if (messages.length === 0) {
|
||||
stopTrackingDelayed(() => alert("Cannot see any messages."));
|
||||
return;
|
||||
}
|
||||
|
||||
GUI.setStatus("Starting");
|
||||
hasJustStarted = true;
|
||||
// noinspection JSIgnoredPromiseFromCall
|
||||
onMessagesUpdated(messages);
|
||||
}
|
||||
else {
|
||||
isSending = false;
|
||||
}
|
||||
});
|
||||
|
||||
GUI.showController();
|
||||
|
||||
if (IS_FIRST_RUN) {
|
||||
GUI.showSettings();
|
||||
}
|
||||
});
|
||||
|
||||
GUI.showController();
|
||||
|
||||
if (IS_FIRST_RUN) {
|
||||
GUI.showSettings();
|
||||
}
|
||||
})();
|
||||
/*[DEBUGGER]*/
|
||||
|
||||
|
||||
};
|
||||
|
1268
bld/viewer.html
1268
bld/viewer.html
File diff suppressed because one or more lines are too long
98
build.py
98
build.py
@@ -8,11 +8,11 @@ import os
|
||||
import re
|
||||
import distutils.dir_util
|
||||
|
||||
VERSION_SHORT = "v.31h"
|
||||
VERSION_FULL = VERSION_SHORT + ", released 03 March 2024"
|
||||
VERSION_SHORT = "v.31f"
|
||||
VERSION_FULL = VERSION_SHORT + ", released 20 November 2023"
|
||||
|
||||
EXEC_UGLIFYJS_WIN = "{2}/lib/uglifyjs.cmd --parse bare_returns --compress --output \"{1}\" \"{0}\""
|
||||
EXEC_UGLIFYJS_AUTO = "uglifyjs --parse bare_returns --compress --output \"{1}\" \"{0}\""
|
||||
EXEC_UGLIFYJS_WIN = "{2}/lib/uglifyjs.cmd --parse bare_returns --compress --mangle toplevel --mangle-props keep_quoted,reserved=[{3}] --output \"{1}\" \"{0}\""
|
||||
EXEC_UGLIFYJS_AUTO = "uglifyjs --parse bare_returns --compress --mangle toplevel --mangle-props keep_quoted,reserved=[{3}] --output \"{1}\" \"{0}\""
|
||||
|
||||
USE_UGLIFYJS = "--nominify" not in sys.argv
|
||||
USE_MINIFICATION = "--nominify" not in sys.argv
|
||||
@@ -32,13 +32,17 @@ else:
|
||||
USE_UGLIFYJS = False
|
||||
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
|
||||
|
||||
def combine_files(input_pattern, output_file):
|
||||
is_first_file = True
|
||||
|
||||
with fileinput.input(sorted(glob.glob(input_pattern))) as stream:
|
||||
with fileinput.input(sorted(glob.glob(input_pattern, recursive=True))) as stream:
|
||||
for line in stream:
|
||||
if stream.isfirstline():
|
||||
if is_first_file:
|
||||
@@ -49,23 +53,6 @@ def combine_files(input_pattern, output_file):
|
||||
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):
|
||||
if not USE_MINIFICATION:
|
||||
if input_file != output_file:
|
||||
@@ -90,39 +77,24 @@ def minify_css(input_file, output_file):
|
||||
|
||||
# Build System
|
||||
|
||||
def build_tracker():
|
||||
def build_tracker_html():
|
||||
output_file_raw = "bld/track.js"
|
||||
output_file_html = "bld/track.html"
|
||||
output_file_userscript = "bld/track.user.js"
|
||||
|
||||
with open("src/tracker/styles/controller.css", "r") as f:
|
||||
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)
|
||||
output_file_tmp = "bld/track.tmp.js"
|
||||
input_pattern = "src/tracker/**/*.js"
|
||||
|
||||
with open(output_file_raw, "w") as out:
|
||||
if not USE_UGLIFYJS:
|
||||
out.write("(function(){\n")
|
||||
|
||||
out.write(full_tracker_js)
|
||||
combine_files(input_pattern, out)
|
||||
|
||||
if not USE_UGLIFYJS:
|
||||
out.write("})()")
|
||||
|
||||
if USE_UGLIFYJS:
|
||||
output_file_tmp = "bld/track.tmp.js"
|
||||
|
||||
os.system(EXEC_UGLIFYJS.format(output_file_raw, output_file_tmp, WORKING_DIR))
|
||||
os.system(EXEC_UGLIFYJS.format(output_file_raw, output_file_tmp, WORKING_DIR, RESERVED_PROPS))
|
||||
|
||||
with open(output_file_raw, "w") as out:
|
||||
out.write("javascript:(function(){")
|
||||
@@ -133,30 +105,27 @@ def build_tracker():
|
||||
out.write("})()")
|
||||
|
||||
os.remove(output_file_tmp)
|
||||
|
||||
|
||||
with open(output_file_raw, "r") as raw:
|
||||
minified_tracker_js = raw.read()
|
||||
script_contents = raw.read().replace("&", "&").replace('"', """).replace("'", "'").replace("<", "<").replace(">", ">")
|
||||
|
||||
write_tracker_html(output_file_html, minified_tracker_js)
|
||||
write_tracker_userscript(output_file_userscript, full_tracker_js)
|
||||
with open(output_file_html, "w") as out:
|
||||
out.write(script_contents)
|
||||
|
||||
|
||||
def write_tracker_html(output_file, tracker_js):
|
||||
tracker_js = tracker_js.replace("&", "&").replace('"', """).replace("'", "'").replace("<", "<").replace(">", ">")
|
||||
def build_tracker_userscript():
|
||||
output_file = "bld/track.user.js"
|
||||
|
||||
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:
|
||||
out.write(tracker_js)
|
||||
|
||||
|
||||
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)
|
||||
out.write(userscript_contents[0])
|
||||
combine_files(input_pattern, out)
|
||||
out.write(userscript_contents[1])
|
||||
|
||||
|
||||
def build_viewer():
|
||||
@@ -181,7 +150,7 @@ def build_viewer():
|
||||
combine_files(input_js_pattern, out)
|
||||
|
||||
if USE_UGLIFYJS:
|
||||
os.system(EXEC_UGLIFYJS.format(tmp_js_file_combined, tmp_js_file_minified, WORKING_DIR))
|
||||
os.system(EXEC_UGLIFYJS.format(tmp_js_file_combined, tmp_js_file_minified, WORKING_DIR, RESERVED_PROPS))
|
||||
else:
|
||||
shutil.copyfile(tmp_js_file_combined, tmp_js_file_minified)
|
||||
|
||||
@@ -233,8 +202,11 @@ def build_website():
|
||||
|
||||
os.makedirs("bld", exist_ok = True)
|
||||
|
||||
print("Building tracker...")
|
||||
build_tracker()
|
||||
print("Building tracker html...")
|
||||
build_tracker_html()
|
||||
|
||||
print("Building tracker userscript...")
|
||||
build_tracker_userscript()
|
||||
|
||||
print("Building viewer...")
|
||||
build_viewer()
|
||||
|
74
reserve.txt
Normal file
74
reserve.txt
Normal file
@@ -0,0 +1,74 @@
|
||||
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
|
5
src/tracker/1_common/DO_NOT_EDIT_THESE.md
Normal file
5
src/tracker/1_common/DO_NOT_EDIT_THESE.md
Normal file
@@ -0,0 +1,5 @@
|
||||
**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.
|
@@ -1,3 +1,6 @@
|
||||
// NOTE: Currently unused. See `.createStyle()` calls in `gui.js`.
|
||||
/*
|
||||
const css_controller = `
|
||||
#app-mount {
|
||||
height: calc(100% - 48px) !important;
|
||||
}
|
||||
@@ -30,8 +33,5 @@
|
||||
display: inline-block;
|
||||
margin: 14px 12px;
|
||||
}
|
||||
|
||||
#dht-ctrl-close {
|
||||
margin: 8px 8px 8px 0 !important;
|
||||
float: right;
|
||||
}
|
||||
`
|
||||
*/
|
@@ -1,3 +1,6 @@
|
||||
// NOTE: Currently unused. See `.createStyle()` calls in `gui.js`.
|
||||
/*
|
||||
const css_settings = `
|
||||
#dht-cfg-overlay {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
@@ -26,8 +29,5 @@
|
||||
#dht-cfg-note {
|
||||
margin-top: 22px;
|
||||
}
|
||||
|
||||
#dht-cfg sub {
|
||||
color: #666;
|
||||
font-size: 13px;
|
||||
}
|
||||
`
|
||||
*/
|
@@ -1,23 +1,5 @@
|
||||
// noinspection JSUnresolvedVariable
|
||||
// noinspection LocalVariableNamingConventionJS
|
||||
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() {
|
||||
return DOM.queryReactClass("messagesWrapper");
|
||||
}
|
||||
@@ -46,11 +28,46 @@ class DISCORD {
|
||||
* Calls the provided function with a list of messages whenever the currently loaded messages change.
|
||||
*/
|
||||
static setupMessageCallback(callback) {
|
||||
let skipsLeft = 0;
|
||||
let waitForCleanup = false;
|
||||
const previousMessages = new Set();
|
||||
|
||||
const onMessageElementsChanged = function() {
|
||||
const messages = DISCORD.getMessages();
|
||||
const hasChanged = messages.some(message => !previousMessages.has(message.id)) || !DISCORD.hasMoreMessages();
|
||||
const timer = window.setInterval(() => {
|
||||
if (skipsLeft > 0) {
|
||||
--skipsLeft;
|
||||
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) {
|
||||
return;
|
||||
@@ -62,74 +79,24 @@ class DISCORD {
|
||||
}
|
||||
|
||||
callback(messages);
|
||||
};
|
||||
}, 200);
|
||||
|
||||
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);
|
||||
});
|
||||
window.DHT_ON_UNLOAD.push(() => window.clearInterval(timer));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the message from a message element.
|
||||
* @returns { null | DiscordMessage } }
|
||||
* Returns the property object of a message element.
|
||||
* @returns { null | { message: DiscordMessage, channel: Object } }
|
||||
*/
|
||||
static getMessageFromElement(ele) {
|
||||
static getMessageElementProps(ele) {
|
||||
const props = DOM.getReactProps(ele);
|
||||
|
||||
if (props && Array.isArray(props.children)) {
|
||||
for (const child of props.children) {
|
||||
if (!(child instanceof Object)) {
|
||||
continue;
|
||||
}
|
||||
if (props.children && props.children.length) {
|
||||
for (let i = 3; i < props.children.length; i++) {
|
||||
const childProps = props.children[i].props;
|
||||
|
||||
const childProps = child.props;
|
||||
if (childProps instanceof Object && "message" in childProps) {
|
||||
return childProps.message;
|
||||
if (childProps && "message" in childProps && "channel" in childProps) {
|
||||
return childProps;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -146,10 +113,10 @@ class DISCORD {
|
||||
|
||||
for (const ele of this.getMessageElements()) {
|
||||
try {
|
||||
const message = this.getMessageFromElement(ele);
|
||||
const props = this.getMessageElementProps(ele);
|
||||
|
||||
if (message != null) {
|
||||
messages.push(message);
|
||||
if (props != null) {
|
||||
messages.push(props.message);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("[DHT] Error extracing message data, skipping it.", e, ele, DOM.tryGetReactProps(ele));
|
||||
@@ -170,7 +137,7 @@ class DISCORD {
|
||||
*/
|
||||
static getSelectedChannel() {
|
||||
try {
|
||||
let obj = null;
|
||||
let obj;
|
||||
|
||||
try {
|
||||
for (const child of DOM.getReactProps(DOM.queryReactClass("chatContent")).children) {
|
||||
@@ -181,6 +148,15 @@ class DISCORD {
|
||||
}
|
||||
} catch (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") {
|
||||
@@ -209,8 +185,8 @@ class DISCORD {
|
||||
|
||||
// https://discord.com/developers/docs/resources/channel#channel-object-channel-types
|
||||
switch (obj.type) {
|
||||
case DISCORD.CHANNEL_TYPE.DM: type = "DM"; break;
|
||||
case DISCORD.CHANNEL_TYPE.GROUP_DM: type = "GROUP"; break;
|
||||
case 1: type = "DM"; break;
|
||||
case 3: type = "GROUP"; break;
|
||||
default: return null;
|
||||
}
|
||||
|
||||
@@ -248,7 +224,7 @@ class DISCORD {
|
||||
}
|
||||
};
|
||||
|
||||
if (obj.type === DISCORD.CHANNEL_TYPE.ANNOUNCEMENT_THREAD || obj.type === DISCORD.CHANNEL_TYPE.PUBLIC_THREAD || obj.type === DISCORD.CHANNEL_TYPE.PRIVATE_THREAD) {
|
||||
if (obj.parent_id) {
|
||||
channel["extra"]["parent"] = obj.parent_id;
|
||||
}
|
||||
else {
|
@@ -84,7 +84,14 @@ var GUI = (function(){
|
||||
|
||||
// styles
|
||||
|
||||
controller.styles = DOM.createStyle(`/*[CSS-CONTROLLER]*/`);
|
||||
controller.styles = DOM.createStyle(`
|
||||
#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
|
||||
|
||||
@@ -97,7 +104,7 @@ ${btn("track", "")}
|
||||
${btn("download", "Download")}
|
||||
${btn("reset", "Reset")}
|
||||
<p id='dht-ctrl-status'></p>
|
||||
<input id='dht-ctrl-upload-input' type='file' multiple style="display: none">
|
||||
<input id='dht-ctrl-upload-input' type='file' multiple>
|
||||
${btn("close", "X")}`);
|
||||
|
||||
// elements
|
||||
@@ -186,7 +193,11 @@ ${btn("close", "X")}`);
|
||||
|
||||
// styles
|
||||
|
||||
settings.styles = DOM.createStyle(`/*[CSS-SETTINGS]*/`);
|
||||
settings.styles = DOM.createStyle(`
|
||||
#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
|
||||
|
||||
@@ -204,12 +215,12 @@ ${btn("close", "X")}`);
|
||||
<label><input id='dht-cfg-autoscroll' type='checkbox'> Autoscroll</label><br>
|
||||
<br>
|
||||
<label>After reaching the first message in channel...</label><br>
|
||||
${radio("afm", "nothing", "Continue Tracking")}
|
||||
${radio("afm", "nothing", "Do Nothing")}
|
||||
${radio("afm", "pause", "Pause Tracking")}
|
||||
${radio("afm", "switch", "Switch to Next Channel")}
|
||||
<br>
|
||||
<label>After reaching a previously saved message...</label><br>
|
||||
${radio("asm", "nothing", "Continue Tracking")}
|
||||
${radio("asm", "nothing", "Do Nothing")}
|
||||
${radio("asm", "pause", "Pause Tracking")}
|
||||
${radio("asm", "switch", "Switch to Next Channel")}
|
||||
<p id='dht-cfg-note'>
|
||||
@@ -263,7 +274,10 @@ It is recommended to disable link and image previews to avoid putting unnecessar
|
||||
}
|
||||
},
|
||||
|
||||
setStatus: function(status) {}
|
||||
setStatus: function(state){
|
||||
console.log("Status: " + state)
|
||||
// TODO I guess.
|
||||
}
|
||||
};
|
||||
|
||||
return root;
|
@@ -113,16 +113,6 @@ class SAVEFILE{
|
||||
static isValid(parsedObj){
|
||||
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){
|
||||
var wasPresent = userId in this.meta.users;
|
||||
@@ -177,15 +167,15 @@ class SAVEFILE{
|
||||
|
||||
channelObj.name = channelName;
|
||||
|
||||
if (extraInfo.position) {
|
||||
if (extraInfo.position){
|
||||
channelObj.position = extraInfo.position;
|
||||
}
|
||||
|
||||
if (extraInfo.topic) {
|
||||
if (extraInfo.topic){
|
||||
channelObj.topic = extraInfo.topic;
|
||||
}
|
||||
|
||||
if (extraInfo.nsfw) {
|
||||
if (extraInfo.nsfw){
|
||||
channelObj.nsfw = extraInfo.nsfw;
|
||||
}
|
||||
|
||||
@@ -213,7 +203,7 @@ class SAVEFILE{
|
||||
|
||||
var obj = {
|
||||
u: this.findOrRegisterUser(author.id, author.username, author.bot ? null : author.discriminator, author.avatar),
|
||||
t: SAVEFILE.getDate(discordMessage.timestamp).getTime()
|
||||
t: discordMessage.timestamp.toDate().getTime()
|
||||
};
|
||||
|
||||
if (discordMessage.content.length > 0){
|
||||
@@ -221,7 +211,7 @@ class SAVEFILE{
|
||||
}
|
||||
|
||||
if (discordMessage.editedTimestamp !== null){
|
||||
obj.te = SAVEFILE.getDate(discordMessage.editedTimestamp).getTime();
|
||||
obj.te = discordMessage.editedTimestamp.toDate().getTime();
|
||||
}
|
||||
|
||||
if (discordMessage.embeds.length > 0){
|
||||
@@ -279,9 +269,11 @@ class SAVEFILE{
|
||||
|
||||
addMessagesFromDiscord(discordMessageArray){
|
||||
var hasNewMessages = false;
|
||||
|
||||
for(var discordMessage of discordMessageArray){
|
||||
if (this.addMessage(discordMessage.channel_id, discordMessage.id, this.convertToMessageObject(discordMessage))){
|
||||
var type = discordMessage.type;
|
||||
|
||||
// https://discord.com/developers/docs/resources/channel#message-object-message-reference-structure
|
||||
if ((type === 0 || type === 19) && discordMessage.state === "SENT" && this.addMessage(discordMessage.channel_id, discordMessage.id, this.convertToMessageObject(discordMessage))){
|
||||
hasNewMessages = true;
|
||||
}
|
||||
}
|
@@ -110,25 +110,23 @@ const STATE = (function() {
|
||||
* Registers a Discord server and channel.
|
||||
*/
|
||||
addDiscordChannel(serverInfo, channelInfo){
|
||||
var serverName = serverInfo.name;
|
||||
var serverType = serverInfo.type;
|
||||
var channelId = channelInfo.id;
|
||||
var channelName = channelInfo.name;
|
||||
var extraInfo = channelInfo.extra || {};
|
||||
|
||||
let serverName = serverInfo.name
|
||||
let serverType = serverInfo.type
|
||||
let channelId = channelInfo.id
|
||||
let channelName = channelInfo.name
|
||||
let extraInfo = channelInfo.extra
|
||||
var serverIndex = this.getSavefile().findOrRegisterServer(serverName, serverType);
|
||||
|
||||
if (this.getSavefile().tryRegisterChannel(serverIndex, channelId, channelName, extraInfo) === true){
|
||||
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.
|
||||
*/
|
||||
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)){
|
||||
this._triggerStateChanged("data", "messages");
|
||||
return true;
|
||||
@@ -152,6 +150,13 @@ const STATE = (function() {
|
||||
this._trackingStateChangedListeners.push(callback);
|
||||
callback(this._isTracking);
|
||||
}
|
||||
|
||||
/*
|
||||
* Shim for code from the desktop app.
|
||||
*/
|
||||
setup(port, token) {
|
||||
console.log("Placeholder port and token: " + port + " " + token);
|
||||
}
|
||||
}
|
||||
|
||||
return new CLS();
|
281
src/tracker/bootstrap.js
vendored
281
src/tracker/bootstrap.js
vendored
@@ -1,156 +1,161 @@
|
||||
// noinspection JSAnnotator
|
||||
|
||||
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]*/
|
||||
|
||||
let delayedStopRequests = 0;
|
||||
const stopTrackingDelayed = function(callback) {
|
||||
delayedStopRequests++;
|
||||
// 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;
|
||||
|
||||
window.setTimeout(() => {
|
||||
STATE.setIsTracking(false);
|
||||
delayedStopRequests--;
|
||||
|
||||
if (callback) {
|
||||
callback();
|
||||
}
|
||||
}, 200); // give the user visual feedback after clicking the button before switching off
|
||||
};
|
||||
|
||||
let hasJustStarted = false;
|
||||
let isSending = false;
|
||||
|
||||
const onError = function(e) {
|
||||
console.log(e);
|
||||
GUI.setStatus(e.status === "DISCONNECTED" ? "Disconnected" : "Error");
|
||||
stopTrackingDelayed(() => isSending = false);
|
||||
};
|
||||
|
||||
const isNoAction = function(action) {
|
||||
return action === null || action === CONSTANTS.AUTOSCROLL_ACTION_NOTHING;
|
||||
};
|
||||
|
||||
const onTrackingContinued = function(anyNewMessages) {
|
||||
if (!STATE.isTracking()) {
|
||||
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;
|
||||
}
|
||||
|
||||
GUI.setStatus("Tracking");
|
||||
|
||||
if (hasJustStarted) {
|
||||
anyNewMessages = true;
|
||||
hasJustStarted = false;
|
||||
if (window.DHT_LOADED) {
|
||||
alert("Discord History Tracker is already loaded.");
|
||||
return;
|
||||
}
|
||||
|
||||
isSending = false;
|
||||
window.DHT_LOADED = true;
|
||||
window.DHT_ON_UNLOAD = [];
|
||||
|
||||
if (SETTINGS.autoscroll) {
|
||||
let action = null;
|
||||
/*[IMPORTS]*/
|
||||
|
||||
const port = 0; /*[PORT]*/
|
||||
const token = "/*[TOKEN]*/";
|
||||
STATE.setup(port, token);
|
||||
|
||||
let delayedStopRequests = 0;
|
||||
const stopTrackingDelayed = function(callback) {
|
||||
delayedStopRequests++;
|
||||
|
||||
if (!DISCORD.hasMoreMessages()) {
|
||||
console.debug("[DHT] Reached first message.");
|
||||
action = SETTINGS.afterFirstMsg;
|
||||
}
|
||||
if (isNoAction(action) && !anyNewMessages) {
|
||||
console.debug("[DHT] No new messages.");
|
||||
action = SETTINGS.afterSavedMsg;
|
||||
}
|
||||
|
||||
if (isNoAction(action)) {
|
||||
DISCORD.loadOlderMessages();
|
||||
}
|
||||
else if (action === CONSTANTS.AUTOSCROLL_ACTION_PAUSE || (action === CONSTANTS.AUTOSCROLL_ACTION_SWITCH && !DISCORD.selectNextTextChannel())) {
|
||||
GUI.setStatus("Reached End");
|
||||
window.setTimeout(() => {
|
||||
STATE.setIsTracking(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let waitUntilSendingFinishedTimer = null;
|
||||
|
||||
const onMessagesUpdated = async messages => {
|
||||
if (!STATE.isTracking() || delayedStopRequests > 0) {
|
||||
return;
|
||||
}
|
||||
delayedStopRequests--;
|
||||
|
||||
if (callback) {
|
||||
callback();
|
||||
}
|
||||
}, 200); // give the user visual feedback after clicking the button before switching off
|
||||
};
|
||||
|
||||
if (isSending) {
|
||||
window.clearTimeout(waitUntilSendingFinishedTimer);
|
||||
|
||||
waitUntilSendingFinishedTimer = window.setTimeout(() => {
|
||||
waitUntilSendingFinishedTimer = null;
|
||||
onMessagesUpdated(messages);
|
||||
}, 100);
|
||||
|
||||
return;
|
||||
}
|
||||
let hasJustStarted = false;
|
||||
let isSending = false;
|
||||
|
||||
const info = DISCORD.getSelectedChannel();
|
||||
const onError = function(e) {
|
||||
console.log(e);
|
||||
GUI.setStatus(e.status === "DISCONNECTED" ? "Disconnected" : "Error");
|
||||
stopTrackingDelayed(() => isSending = false);
|
||||
};
|
||||
|
||||
if (!info) {
|
||||
GUI.setStatus("Error (Unknown Channel)");
|
||||
stopTrackingDelayed();
|
||||
return;
|
||||
}
|
||||
const isNoAction = function(action) {
|
||||
return action === null || action === CONSTANTS.AUTOSCROLL_ACTION_NOTHING;
|
||||
};
|
||||
|
||||
isSending = true;
|
||||
|
||||
try {
|
||||
STATE.addDiscordChannel(info.server, info.channel);
|
||||
} catch (e) {
|
||||
onError(e);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
if (!messages.length) {
|
||||
isSending = false;
|
||||
onTrackingContinued(false);
|
||||
}
|
||||
else {
|
||||
const anyNewMessages = STATE.addDiscordMessages(messages);
|
||||
onTrackingContinued(anyNewMessages);
|
||||
}
|
||||
} catch (e) {
|
||||
onError(e);
|
||||
}
|
||||
};
|
||||
|
||||
DISCORD.setupMessageCallback(onMessagesUpdated);
|
||||
|
||||
STATE.onTrackingStateChanged(enabled => {
|
||||
if (enabled) {
|
||||
const messages = DISCORD.getMessages();
|
||||
|
||||
if (messages.length === 0) {
|
||||
stopTrackingDelayed(() => alert("Cannot see any messages."));
|
||||
const onTrackingContinued = function(anyNewMessages) {
|
||||
if (!STATE.isTracking()) {
|
||||
return;
|
||||
}
|
||||
|
||||
GUI.setStatus("Starting");
|
||||
hasJustStarted = true;
|
||||
// noinspection JSIgnoredPromiseFromCall
|
||||
onMessagesUpdated(messages);
|
||||
}
|
||||
else {
|
||||
GUI.setStatus("Tracking");
|
||||
|
||||
if (hasJustStarted) {
|
||||
anyNewMessages = true;
|
||||
hasJustStarted = false;
|
||||
}
|
||||
|
||||
isSending = false;
|
||||
|
||||
if (SETTINGS.autoscroll) {
|
||||
let action = null;
|
||||
|
||||
if (!DISCORD.hasMoreMessages()) {
|
||||
action = SETTINGS.afterFirstMsg;
|
||||
}
|
||||
if (isNoAction(action) && !anyNewMessages) {
|
||||
action = SETTINGS.afterSavedMsg;
|
||||
}
|
||||
|
||||
if (isNoAction(action)) {
|
||||
DISCORD.loadOlderMessages();
|
||||
}
|
||||
else if (action === CONSTANTS.AUTOSCROLL_ACTION_PAUSE || (action === CONSTANTS.AUTOSCROLL_ACTION_SWITCH && !DISCORD.selectNextTextChannel())) {
|
||||
GUI.setStatus("Reached End");
|
||||
STATE.setIsTracking(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let waitUntilSendingFinishedTimer = null;
|
||||
|
||||
const onMessagesUpdated = async messages => {
|
||||
if (!STATE.isTracking() || delayedStopRequests > 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isSending) {
|
||||
window.clearTimeout(waitUntilSendingFinishedTimer);
|
||||
|
||||
waitUntilSendingFinishedTimer = window.setTimeout(() => {
|
||||
waitUntilSendingFinishedTimer = null;
|
||||
onMessagesUpdated(messages);
|
||||
}, 100);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const info = DISCORD.getSelectedChannel();
|
||||
|
||||
if (!info) {
|
||||
GUI.setStatus("Error (Unknown Channel)");
|
||||
stopTrackingDelayed();
|
||||
return;
|
||||
}
|
||||
|
||||
isSending = true;
|
||||
|
||||
try {
|
||||
await STATE.addDiscordChannel(info.server, info.channel);
|
||||
} catch (e) {
|
||||
onError(e);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
if (!messages.length) {
|
||||
isSending = false;
|
||||
onTrackingContinued(false);
|
||||
}
|
||||
else {
|
||||
const anyNewMessages = await STATE.addDiscordMessages(messages);
|
||||
onTrackingContinued(anyNewMessages);
|
||||
}
|
||||
} catch (e) {
|
||||
onError(e);
|
||||
}
|
||||
};
|
||||
|
||||
DISCORD.setupMessageCallback(onMessagesUpdated);
|
||||
|
||||
STATE.onTrackingStateChanged(enabled => {
|
||||
if (enabled) {
|
||||
const messages = DISCORD.getMessages();
|
||||
|
||||
if (messages.length === 0) {
|
||||
stopTrackingDelayed(() => alert("Cannot see any messages."));
|
||||
return;
|
||||
}
|
||||
|
||||
GUI.setStatus("Starting");
|
||||
hasJustStarted = true;
|
||||
// noinspection JSIgnoredPromiseFromCall
|
||||
onMessagesUpdated(messages);
|
||||
}
|
||||
else {
|
||||
isSending = false;
|
||||
}
|
||||
});
|
||||
|
||||
GUI.showController();
|
||||
|
||||
if (IS_FIRST_RUN) {
|
||||
GUI.showSettings();
|
||||
}
|
||||
});
|
||||
|
||||
GUI.showController();
|
||||
|
||||
if (IS_FIRST_RUN) {
|
||||
GUI.showSettings();
|
||||
}
|
||||
})();
|
||||
/*[DEBUGGER]*/
|
||||
|
@@ -1,8 +1,7 @@
|
||||
var DISCORD = (function(){
|
||||
var REGEX = {
|
||||
formatBold: /\*\*([\s\S]+?)\*\*(?!\*)/g,
|
||||
formatItalic1: /\*([\s\S]+?)\*(?!\*)/g,
|
||||
formatItalic2: /_([\s\S]+?)_(?!_)\b/g,
|
||||
formatItalic: /(.)?\*([\s\S]+?)\*(?!\*)/g,
|
||||
formatUnderline: /__([\s\S]+?)__(?!_)/g,
|
||||
formatStrike: /~~([\s\S]+?)~~(?!~)/g,
|
||||
formatCodeInline: /(`+)\s*([\s\S]*?[^`])\s*\1(?!`)/g,
|
||||
@@ -55,8 +54,7 @@ var DISCORD = (function(){
|
||||
.replace(REGEX.specialEscapedSingle, escapeHtmlMatch)
|
||||
.replace(REGEX.specialEscapedDouble, full => full.replace(/\\/g, "").replace(/(.)/g, escapeHtmlMatch))
|
||||
.replace(REGEX.formatBold, "<b>$1</b>")
|
||||
.replace(REGEX.formatItalic1, "<i>$1</i>")
|
||||
.replace(REGEX.formatItalic2, "<i>$1</i>")
|
||||
.replace(REGEX.formatItalic, (full, pre, match) => pre === '\\' ? full : (pre || "")+"<i>"+match+"</i>")
|
||||
.replace(REGEX.formatUnderline, "<u>$1</u>")
|
||||
.replace(REGEX.formatStrike, "<s>$1</s>");
|
||||
}
|
||||
|
BIN
web/img/button.png
Normal file
BIN
web/img/button.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.7 KiB |
106
web/index.php
106
web/index.php
@@ -13,10 +13,110 @@
|
||||
</head>
|
||||
<body>
|
||||
<div class="inner">
|
||||
<h1>Discord History Tracker <span class="bar">|</span> <span class="notes"><a href="https://github.com/chylex/Discord-History-Tracker/wiki/Release-Notes">Release Notes</a></span> <span class="bar">|</span> <span class="notes"><a href="https://github.com/chylex/Discord-History-Tracker/wiki/Old-Versions">Old Versions</a></span></h1>
|
||||
<h1>Discord History Tracker <span class="version">{{{version:web}}}</span> <span class="bar">|</span> <span class="notes"><a href="https://github.com/chylex/Discord-History-Tracker/wiki/Release-Notes">Release Notes</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">
|
||||
<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>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>
|
||||
<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>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>
|
||||
|
||||
<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>
|
||||
|
||||
<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 «Bookmark This Link» 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 & 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 & Suggestions</a> —
|
||||
<a href="https://github.com/chylex/Discord-History-Tracker/tree/master-browser-only">Source Code</a> —
|
||||
<a href="https://twitter.com/chylexmc">Follow Dev on Twitter</a> —
|
||||
<a href="https://www.patreon.com/chylex">Support via Patreon</a> —
|
||||
<a href="https://ko-fi.com/chylex">Support via 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>
|
||||
</html>
|
||||
|
Reference in New Issue
Block a user