mirror of
https://github.com/chylex/Discord-History-Tracker.git
synced 2024-11-25 05:42:45 +01:00
Compare commits
1 Commits
50d2288203
...
86dd64119d
Author | SHA1 | Date | |
---|---|---|---|
86dd64119d |
47
app/Resources/Tracker/bootstrap.js
vendored
47
app/Resources/Tracker/bootstrap.js
vendored
@ -10,11 +10,16 @@
|
||||
return;
|
||||
}
|
||||
|
||||
/*[IMPORTS]*/
|
||||
|
||||
if (!DISCORD.isCompatible()) {
|
||||
alert("Discord History Tracker is not compatible with this version of Discord.");
|
||||
return;
|
||||
}
|
||||
|
||||
window.DHT_LOADED = true;
|
||||
window.DHT_ON_UNLOAD = [];
|
||||
|
||||
/*[IMPORTS]*/
|
||||
|
||||
const port = 0; /*[PORT]*/
|
||||
const token = "/*[TOKEN]*/";
|
||||
STATE.setup(port, token);
|
||||
@ -46,7 +51,7 @@
|
||||
return action === null || action === CONSTANTS.AUTOSCROLL_ACTION_NOTHING;
|
||||
};
|
||||
|
||||
const onTrackingContinued = function(anyNewMessages) {
|
||||
const onTrackingContinued = function(anyNewMessages, hasMoreBefore) {
|
||||
if (!STATE.isTracking()) {
|
||||
return;
|
||||
}
|
||||
@ -63,7 +68,7 @@
|
||||
if (SETTINGS.autoscroll) {
|
||||
let action = null;
|
||||
|
||||
if (!DISCORD.hasMoreMessages()) {
|
||||
if (!hasMoreBefore) {
|
||||
console.debug("[DHT] Reached first message.");
|
||||
action = SETTINGS.afterFirstMsg;
|
||||
}
|
||||
@ -84,7 +89,7 @@
|
||||
|
||||
let waitUntilSendingFinishedTimer = null;
|
||||
|
||||
const onMessagesUpdated = async messages => {
|
||||
const onMessagesUpdated = async (server, channel, messages, hasMoreBefore) => {
|
||||
if (!STATE.isTracking() || delayedStopRequests > 0) {
|
||||
return;
|
||||
}
|
||||
@ -94,24 +99,16 @@
|
||||
|
||||
waitUntilSendingFinishedTimer = window.setTimeout(() => {
|
||||
waitUntilSendingFinishedTimer = null;
|
||||
onMessagesUpdated(messages);
|
||||
onMessagesUpdated(server, channel, messages, hasMoreBefore);
|
||||
}, 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);
|
||||
await STATE.addDiscordChannel(server, channel);
|
||||
} catch (e) {
|
||||
onError(e);
|
||||
return;
|
||||
@ -120,32 +117,28 @@
|
||||
try {
|
||||
if (!messages.length) {
|
||||
isSending = false;
|
||||
onTrackingContinued(false);
|
||||
onTrackingContinued(false, hasMoreBefore);
|
||||
}
|
||||
else {
|
||||
const anyNewMessages = await STATE.addDiscordMessages(messages);
|
||||
onTrackingContinued(anyNewMessages);
|
||||
onTrackingContinued(anyNewMessages, hasMoreBefore);
|
||||
}
|
||||
} catch (e) {
|
||||
onError(e);
|
||||
}
|
||||
};
|
||||
|
||||
DISCORD.setupMessageCallback(onMessagesUpdated);
|
||||
const starter = 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);
|
||||
|
||||
if (!starter()) {
|
||||
stopTrackingDelayed(() => alert("Cannot see any messages."));
|
||||
hasJustStarted = false;
|
||||
}
|
||||
}
|
||||
else {
|
||||
isSending = false;
|
||||
|
@ -4,11 +4,26 @@ class DISCORD {
|
||||
|
||||
// https://discord.com/developers/docs/resources/channel#channel-object-channel-types
|
||||
static CHANNEL_TYPE = {
|
||||
GUILD_TEXT: 0,
|
||||
DM: 1,
|
||||
GROUP_DM: 3,
|
||||
GUILD_ANNOUNCEMENT: 5,
|
||||
ANNOUNCEMENT_THREAD: 10,
|
||||
PUBLIC_THREAD: 11,
|
||||
PRIVATE_THREAD: 12
|
||||
PRIVATE_THREAD: 12,
|
||||
|
||||
isPrivate(type) {
|
||||
return type === this.DM
|
||||
|| type === this.GROUP_DM;
|
||||
},
|
||||
|
||||
isNavigableGuildChannel(type) {
|
||||
return type === this.GUILD_TEXT
|
||||
|| type === this.GUILD_ANNOUNCEMENT
|
||||
|| type === this.ANNOUNCEMENT_THREAD
|
||||
|| type === this.PUBLIC_THREAD
|
||||
|| type === this.PRIVATE_THREAD;
|
||||
}
|
||||
};
|
||||
|
||||
// https://discord.com/developers/docs/resources/channel#message-object-message-types
|
||||
@ -18,6 +33,74 @@ class DISCORD {
|
||||
THREAD_STARTER: 21
|
||||
};
|
||||
|
||||
// https://discord.com/developers/docs/topics/permissions#permissions-bitwise-permission-flags
|
||||
static PERMISSION = {
|
||||
VIEW_CHANNEL: 1n << 10n
|
||||
};
|
||||
|
||||
/**
|
||||
* @type {Object}
|
||||
* @property {function(String): ?DiscordGuild} getGuild
|
||||
*/
|
||||
static #guildStore = WEBPACK.findModule(WEBPACK.filterByProps("getGuild", "getGuilds", "getGuildIds"));
|
||||
|
||||
/**
|
||||
* @type {Object}
|
||||
* @property {function(String): Boolean} isOptInEnabled
|
||||
* @property {function(String): Set<String>} getOptedInChannels
|
||||
*/
|
||||
static #guildSettings = WEBPACK.findModule(WEBPACK.filterByProps("isOptInEnabled", "getOptedInChannels"));
|
||||
|
||||
/**
|
||||
* @type {Object}
|
||||
* @property {function(String): ?DiscordChannel} getChannel
|
||||
* @property {function(String): Array<DiscordChannel>} getMutableGuildChannelsForGuild
|
||||
* @property {function(): Array<DiscordChannel>} getSortedPrivateChannels
|
||||
*/
|
||||
static #channelStore = WEBPACK.findModule(WEBPACK.filterByProps("getChannel", "getMutableGuildChannelsForGuild", "getSortedPrivateChannels"));
|
||||
|
||||
/**
|
||||
* @type {function(BigInt, Object): Boolean}
|
||||
*/
|
||||
static #hasPermission = WEBPACK.findFunction("can", [ "getGuildPermissions", "getChannelPermissions" ]);
|
||||
|
||||
/**
|
||||
* @type {function(String): MessageData}
|
||||
*/
|
||||
static #getMessages = WEBPACK.findFunction("getMessages");
|
||||
|
||||
/**
|
||||
* @type {function(String): void}
|
||||
*/
|
||||
static #jumpToMessage = WEBPACK.findFunction("jumpToMessage");
|
||||
|
||||
/**
|
||||
* @type {function(): String}
|
||||
*/
|
||||
static #getCurrentlySelectedChannelId = WEBPACK.findFunction("getCurrentlySelectedChannelId");
|
||||
|
||||
/**
|
||||
* @type {function(String): void}
|
||||
*/
|
||||
static #selectPrivateChannel = WEBPACK.findFunction("selectPrivateChannel", [ "selectChannel" ]);
|
||||
|
||||
/**
|
||||
* @type {function(String, String, String=null): void}
|
||||
*/
|
||||
static #transitionToGuild = WEBPACK.findFunction("transitionToGuild", [ "transitionTo" ]);
|
||||
|
||||
static isCompatible() {
|
||||
return !!this.#guildStore
|
||||
&& !!this.#guildSettings
|
||||
&& !!this.#channelStore
|
||||
&& !!this.#hasPermission
|
||||
&& !!this.#getMessages
|
||||
&& !!this.#jumpToMessage
|
||||
&& !!this.#getCurrentlySelectedChannelId
|
||||
&& !!this.#selectPrivateChannel
|
||||
&& !!this.#transitionToGuild;
|
||||
}
|
||||
|
||||
static getMessageOuterElement() {
|
||||
return DOM.queryReactClass("messagesWrapper");
|
||||
}
|
||||
@ -26,14 +109,6 @@ class DISCORD {
|
||||
return DOM.queryReactClass("scroller", this.getMessageOuterElement());
|
||||
}
|
||||
|
||||
static getMessageElements() {
|
||||
return this.getMessageOuterElement().querySelectorAll("[class*='message_']");
|
||||
}
|
||||
|
||||
static hasMoreMessages() {
|
||||
return document.querySelector("#messagesNavigationDescription + [class^=container]") === null;
|
||||
}
|
||||
|
||||
static loadOlderMessages() {
|
||||
const view = this.getMessageScrollerElement();
|
||||
|
||||
@ -48,20 +123,36 @@ class DISCORD {
|
||||
static setupMessageCallback(callback) {
|
||||
const previousMessages = new Set();
|
||||
|
||||
const onMessageElementsChanged = function() {
|
||||
const messages = DISCORD.getMessages();
|
||||
const hasChanged = messages.some(message => !previousMessages.has(message.id)) || !DISCORD.hasMoreMessages();
|
||||
const onMessageElementsChanged = force => {
|
||||
const channelId = this.#getCurrentlySelectedChannelId();
|
||||
if (!channelId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const messages = this.#getMessages(channelId);
|
||||
if (!messages || !messages.ready || messages.loadingMore) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const channel = this.#channelStore.getChannel(channelId);
|
||||
if (!channel) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const hasChanged = force || !messages.hasMoreBefore || messages.some(message => !previousMessages.has(message.id));
|
||||
if (!hasChanged) {
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
previousMessages.clear();
|
||||
for (const message of messages) {
|
||||
for (const message of messages._array) {
|
||||
previousMessages.add(message.id);
|
||||
}
|
||||
|
||||
callback(messages);
|
||||
const server = this.#guildStore.getGuild(channel.guild_id);
|
||||
|
||||
callback(server, channel, messages._array, messages.hasMoreBefore);
|
||||
return true;
|
||||
};
|
||||
|
||||
let debounceTimer;
|
||||
@ -74,7 +165,7 @@ class DISCORD {
|
||||
debounceTimer = window.setTimeout(onMessageElementsChanged, 100);
|
||||
};
|
||||
|
||||
const observer = new MutationObserver(function () {
|
||||
const observer = new MutationObserver(function() {
|
||||
onMessageElementsChangedLater();
|
||||
});
|
||||
|
||||
@ -112,216 +203,55 @@ class DISCORD {
|
||||
observedElement = null;
|
||||
window.clearInterval(observerTimer);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the message from a message element.
|
||||
* @returns { null | DiscordMessage } }
|
||||
*/
|
||||
static getMessageFromElement(ele) {
|
||||
const props = DOM.getReactProps(ele);
|
||||
|
||||
if (props && Array.isArray(props.children)) {
|
||||
for (const child of props.children) {
|
||||
if (!(child instanceof Object)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const childProps = child.props;
|
||||
if (childProps instanceof Object && "message" in childProps) {
|
||||
return childProps.message;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array containing currently loaded messages.
|
||||
*/
|
||||
static getMessages() {
|
||||
try {
|
||||
const messages = [];
|
||||
|
||||
for (const ele of this.getMessageElements()) {
|
||||
try {
|
||||
const message = this.getMessageFromElement(ele);
|
||||
|
||||
if (message != null) {
|
||||
messages.push(message);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("[DHT] Error extracing message data, skipping it.", e, ele, DOM.tryGetReactProps(ele));
|
||||
}
|
||||
}
|
||||
|
||||
return messages;
|
||||
} catch (e) {
|
||||
console.error("[DHT] Error retrieving messages.", e);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an object containing the selected server and channel information.
|
||||
* For types DM and GROUP, the server and channel ids and names are identical.
|
||||
* @returns { {} | null }
|
||||
*/
|
||||
static getSelectedChannel() {
|
||||
try {
|
||||
let obj = null;
|
||||
|
||||
try {
|
||||
for (const child of DOM.getReactProps(DOM.queryReactClass("chatContent")).children) {
|
||||
if (child && child.props && child.props.channel) {
|
||||
obj = child.props.channel;
|
||||
break;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("[DHT] Error retrieving selected channel from 'chatContent' element.", e);
|
||||
}
|
||||
|
||||
if (!obj || typeof obj.id !== "string") {
|
||||
return null;
|
||||
}
|
||||
|
||||
const dms = DOM.queryReactClass("privateChannels");
|
||||
|
||||
if (dms) {
|
||||
let name;
|
||||
|
||||
for (const ele of dms.querySelectorAll("[class*='channel_'] [class*='selected_'] [class^='name_'] *")) {
|
||||
const node = Array.prototype.find.call(ele.childNodes, node => node.nodeType === Node.TEXT_NODE);
|
||||
|
||||
if (node) {
|
||||
name = node.nodeValue;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!name) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let type;
|
||||
|
||||
// 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;
|
||||
default: return null;
|
||||
}
|
||||
|
||||
const id = obj.id;
|
||||
const server = { id, name, type };
|
||||
const channel = { id, name };
|
||||
|
||||
return { server, channel };
|
||||
}
|
||||
else if (obj.guild_id) {
|
||||
let guild;
|
||||
|
||||
for (const child of DOM.getReactProps(document.querySelector("nav header [class*='headerContent_']")).children) {
|
||||
if (child && child.props && child.props.guild) {
|
||||
guild = child.props.guild;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!guild || typeof guild.name !== "string" || obj.guild_id !== guild.id) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const server = {
|
||||
"id": guild.id,
|
||||
"name": guild.name,
|
||||
"type": "SERVER"
|
||||
};
|
||||
|
||||
const channel = {
|
||||
"id": obj.id,
|
||||
"name": obj.name,
|
||||
"extra": {
|
||||
"nsfw": obj.nsfw
|
||||
}
|
||||
};
|
||||
|
||||
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;
|
||||
}
|
||||
else {
|
||||
channel["extra"]["position"] = obj.position;
|
||||
channel["extra"]["topic"] = obj.topic;
|
||||
}
|
||||
|
||||
return { server, channel };
|
||||
}
|
||||
else {
|
||||
return null;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("[DHT] Error retrieving selected channel.", e);
|
||||
return null;
|
||||
}
|
||||
return () => onMessageElementsChanged(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Selects the next text channel and returns true, otherwise returns false if there are no more channels.
|
||||
*/
|
||||
static selectNextTextChannel() {
|
||||
const dms = DOM.queryReactClass("privateChannels");
|
||||
const currentChannel = this.#channelStore.getChannel(this.#getCurrentlySelectedChannelId());
|
||||
if (!currentChannel) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (dms) {
|
||||
const currentChannel = DOM.queryReactClass("selected", dms);
|
||||
const currentChannelContainer = currentChannel && currentChannel.closest("[class*='channel_']");
|
||||
const nextChannel = currentChannelContainer && currentChannelContainer.nextElementSibling;
|
||||
if (this.CHANNEL_TYPE.isPrivate(currentChannel.type)) {
|
||||
const privateChannel = this.#channelStore.getSortedPrivateChannels();
|
||||
const currentIndex = privateChannel.findIndex(channel => channel.id === currentChannel.id);
|
||||
|
||||
if (!nextChannel || !nextChannel.getAttribute("class").includes("channel_")) {
|
||||
if (currentIndex === -1 || currentIndex === privateChannel.length - 1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const nextChannelLink = nextChannel.querySelector("a[href*='/@me/']");
|
||||
if (!nextChannelLink) {
|
||||
return false;
|
||||
}
|
||||
|
||||
nextChannelLink.click();
|
||||
nextChannelLink.scrollIntoView(true);
|
||||
this.#selectPrivateChannel(privateChannel[currentIndex + 1].id);
|
||||
return true;
|
||||
}
|
||||
else {
|
||||
const channelListEle = document.getElementById("channels");
|
||||
if (!channelListEle) {
|
||||
const guildId = currentChannel.guild_id;
|
||||
|
||||
let isChannelOptedIn;
|
||||
if (this.#guildSettings.isOptInEnabled(guildId)) {
|
||||
const optedInChannels = this.#guildSettings.getOptedInChannels(guildId);
|
||||
isChannelOptedIn = channel => optedInChannels.has(channel.id);
|
||||
}
|
||||
else {
|
||||
isChannelOptedIn = _ => true;
|
||||
}
|
||||
|
||||
const guildChannelMap = this.#channelStore.getMutableGuildChannelsForGuild(guildId);
|
||||
const guildChannels = Object.values(guildChannelMap)
|
||||
.filter(channel => this.CHANNEL_TYPE.isNavigableGuildChannel(channel.type) && isChannelOptedIn(channel) && this.#hasPermission(this.PERMISSION.VIEW_CHANNEL, channel))
|
||||
.sort((a, b) => a.position - b.position);
|
||||
|
||||
debugger;
|
||||
const currentIndex = guildChannels.findIndex(channel => channel.id === currentChannel.id);
|
||||
|
||||
if (currentIndex === -1 || currentIndex === guildChannels.length - 1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
function getLinkElement(channel) {
|
||||
return channel.querySelector("a[href^='/channels/'][role='link']");
|
||||
}
|
||||
|
||||
const allTextChannels = Array.prototype.filter.call(channelListEle.querySelectorAll("[class*='containerDefault']"), ele => getLinkElement(ele) !== null);
|
||||
let nextChannel = null;
|
||||
|
||||
for (let index = 0; index < allTextChannels.length - 1; index++) {
|
||||
if (allTextChannels[index].className.includes("selected_")) {
|
||||
nextChannel = allTextChannels[index + 1];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (nextChannel === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const nextChannelLink = getLinkElement(nextChannel);
|
||||
if (!nextChannelLink) {
|
||||
return false;
|
||||
}
|
||||
|
||||
nextChannelLink.click();
|
||||
nextChannel.scrollIntoView(true);
|
||||
this.#transitionToGuild(guildId, guildChannels[currentIndex + 1].id);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
@ -55,31 +55,4 @@ class DOM {
|
||||
const value = document.cookie.replace(new RegExp("(?:(?:^|.*;\\s*)" + name + "\\s*\\=\\s*([^;]*).*$)|^.*$"), "$1");
|
||||
return value.length ? JSON.parse(decodeURIComponent(value)) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns internal React state object of an element.
|
||||
*/
|
||||
static getReactProps(ele) {
|
||||
const keys = Object.keys(ele || {});
|
||||
let key = keys.find(key => key.startsWith("__reactInternalInstance"));
|
||||
|
||||
if (key) {
|
||||
// noinspection JSUnresolvedVariable
|
||||
return ele[key].memoizedProps;
|
||||
}
|
||||
|
||||
key = keys.find(key => key.startsWith("__reactProps$"));
|
||||
return key ? ele[key] : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns internal React state object of an element, or null if the retrieval throws.
|
||||
*/
|
||||
static tryGetReactProps(ele) {
|
||||
try {
|
||||
return this.getReactProps(ele);
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -58,64 +58,39 @@ const STATE = (function() {
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {DiscordChannel} channel
|
||||
*/
|
||||
const getPrivateChannelName = function(channel) {
|
||||
if (channel.name === "") {
|
||||
return channel.rawRecipients.map(user => user.username).join(", ");
|
||||
}
|
||||
else {
|
||||
return channel.name;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {Number} type
|
||||
*/
|
||||
const getChannelTypeName = function(type) {
|
||||
if (type === DISCORD.CHANNEL_TYPE.DM) {
|
||||
return "DM";
|
||||
}
|
||||
else if (type === DISCORD.CHANNEL_TYPE.GROUP_DM) {
|
||||
return "GROUP";
|
||||
}
|
||||
else {
|
||||
return "SERVER";
|
||||
}
|
||||
};
|
||||
|
||||
const trackingStateChangedListeners = [];
|
||||
let isTracking = false;
|
||||
|
||||
const addedChannels = new Set();
|
||||
const addedUsers = new Set();
|
||||
|
||||
/**
|
||||
* @name DiscordUser
|
||||
* @property {String} id
|
||||
* @property {String} username
|
||||
* @property {String} discriminator
|
||||
* @property {String} [avatar]
|
||||
* @property {Boolean} [bot]
|
||||
*/
|
||||
|
||||
/**
|
||||
* @name DiscordMessage
|
||||
* @property {String} id
|
||||
* @property {String} channel_id
|
||||
* @property {DiscordUser} author
|
||||
* @property {String} content
|
||||
* @property {Date} timestamp
|
||||
* @property {Date|null} editedTimestamp
|
||||
* @property {DiscordAttachment[]} attachments
|
||||
* @property {Object[]} embeds
|
||||
* @property {DiscordMessageReaction[]} [reactions]
|
||||
* @property {DiscordMessageReference} [messageReference]
|
||||
* @property {Number} type
|
||||
* @property {String} state
|
||||
*/
|
||||
|
||||
/**
|
||||
* @name DiscordAttachment
|
||||
* @property {String} id
|
||||
* @property {String} filename
|
||||
* @property {String} [content_type]
|
||||
* @property {String} size
|
||||
* @property {String} url
|
||||
*/
|
||||
|
||||
/**
|
||||
* @name DiscordMessageReaction
|
||||
* @property {DiscordEmoji} emoji
|
||||
* @property {Number} count
|
||||
*/
|
||||
|
||||
/**
|
||||
* @name DiscordMessageReference
|
||||
* @property {String} [message_id]
|
||||
*/
|
||||
|
||||
/**
|
||||
* @name DiscordEmoji
|
||||
* @property {String|null} id
|
||||
* @property {String|null} name
|
||||
* @property {Boolean} animated
|
||||
*/
|
||||
|
||||
return {
|
||||
setup(port, token) {
|
||||
serverPort = port;
|
||||
@ -146,32 +121,51 @@ const STATE = (function() {
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* @param {?DiscordGuild} serverInfo
|
||||
* @param {DiscordChannel} channelInfo
|
||||
*/
|
||||
async addDiscordChannel(serverInfo, channelInfo) {
|
||||
if (addedChannels.has(channelInfo.id)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const server = {
|
||||
id: serverInfo.id,
|
||||
name: serverInfo.name,
|
||||
type: serverInfo.type
|
||||
type: getChannelTypeName(channelInfo.type)
|
||||
};
|
||||
|
||||
const channel = {
|
||||
id: channelInfo.id,
|
||||
name: channelInfo.name
|
||||
extra: {}
|
||||
};
|
||||
|
||||
if ("extra" in channelInfo) {
|
||||
const extra = channelInfo.extra;
|
||||
|
||||
if ("parent" in extra) {
|
||||
channel.parent = extra.parent;
|
||||
}
|
||||
|
||||
channel.position = extra.position;
|
||||
channel.topic = extra.topic;
|
||||
channel.nsfw = extra.nsfw;
|
||||
if (DISCORD.CHANNEL_TYPE.isPrivate(channelInfo.type)) {
|
||||
server.id = channelInfo.id;
|
||||
server.name = channel.name = getPrivateChannelName(channelInfo);
|
||||
}
|
||||
else if (serverInfo) {
|
||||
server.id = serverInfo.id;
|
||||
server.name = serverInfo.name;
|
||||
channel.name = channelInfo.name;
|
||||
}
|
||||
else {
|
||||
return;
|
||||
}
|
||||
|
||||
if ("nsfw" in channelInfo) {
|
||||
channel.extra.nsfw = channelInfo.nsfw;
|
||||
}
|
||||
|
||||
if ("topic" in channelInfo) {
|
||||
channel.extra.topic = channelInfo.topic;
|
||||
}
|
||||
|
||||
if ("position" in channelInfo) {
|
||||
channel.extra.position = channelInfo.position;
|
||||
}
|
||||
|
||||
if (channelInfo.type === DISCORD.CHANNEL_TYPE.ANNOUNCEMENT_THREAD || channelInfo.type === DISCORD.CHANNEL_TYPE.PUBLIC_THREAD || channelInfo.type === DISCORD.CHANNEL_TYPE.PRIVATE_THREAD) {
|
||||
channel.extra.parent = channelInfo.parent_id;
|
||||
}
|
||||
|
||||
await post("/track-channel", { server, channel });
|
||||
|
80
app/Resources/Tracker/scripts/types.js
Normal file
80
app/Resources/Tracker/scripts/types.js
Normal file
@ -0,0 +1,80 @@
|
||||
/**
|
||||
* @name DiscordGuild
|
||||
* @property {String} id
|
||||
* @property {String} name
|
||||
*/
|
||||
|
||||
/**
|
||||
* @name DiscordChannel
|
||||
* @property {String} id
|
||||
* @property {String} name
|
||||
* @property {Number} type
|
||||
* @property {String} [guild_id]
|
||||
* @property {String} [parent_id]
|
||||
* @property {Number} [position]
|
||||
* @property {String} [topic]
|
||||
* @property {Boolean} [nsfw]
|
||||
* @property {DiscordUser[]} [rawRecipients]
|
||||
*/
|
||||
|
||||
/**
|
||||
* @name DiscordUser
|
||||
* @property {String} id
|
||||
* @property {String} username
|
||||
* @property {String} discriminator
|
||||
* @property {String} [avatar]
|
||||
* @property {Boolean} [bot]
|
||||
*/
|
||||
|
||||
/**
|
||||
* @name DiscordMessage
|
||||
* @property {String} id
|
||||
* @property {String} channel_id
|
||||
* @property {DiscordUser} author
|
||||
* @property {String} content
|
||||
* @property {Date} timestamp
|
||||
* @property {Date|null} editedTimestamp
|
||||
* @property {DiscordAttachment[]} attachments
|
||||
* @property {Object[]} embeds
|
||||
* @property {DiscordMessageReaction[]} [reactions]
|
||||
* @property {DiscordMessageReference} [messageReference]
|
||||
* @property {Number} type
|
||||
* @property {String} state
|
||||
*/
|
||||
|
||||
/**
|
||||
* @name DiscordAttachment
|
||||
* @property {String} id
|
||||
* @property {String} filename
|
||||
* @property {String} [content_type]
|
||||
* @property {String} size
|
||||
* @property {String} url
|
||||
*/
|
||||
|
||||
/**
|
||||
* @name DiscordMessageReaction
|
||||
* @property {DiscordEmoji} emoji
|
||||
* @property {Number} count
|
||||
*/
|
||||
|
||||
/**
|
||||
* @name DiscordMessageReference
|
||||
* @property {String} [message_id]
|
||||
*/
|
||||
|
||||
/**
|
||||
* @name DiscordEmoji
|
||||
* @property {String|null} id
|
||||
* @property {String|null} name
|
||||
* @property {Boolean} animated
|
||||
*/
|
||||
|
||||
/**
|
||||
* @name MessageData
|
||||
* @type {Object}
|
||||
* @property {Boolean} ready
|
||||
* @property {Boolean} loadingMore
|
||||
* @property {Boolean} hasMoreAfter
|
||||
* @property {Boolean} hasMoreBefore
|
||||
* @property {Array<DiscordMessage>} _array
|
||||
*/
|
93
app/Resources/Tracker/scripts/webpack.js
Normal file
93
app/Resources/Tracker/scripts/webpack.js
Normal file
@ -0,0 +1,93 @@
|
||||
/**
|
||||
* Parts copied from Better Discord, licensed under Apache License 2.0.
|
||||
*
|
||||
* https://github.com/BetterDiscord/BetterDiscord/blob/78edeb77c60542a57884686c4ba98f997c886fad/renderer/src/modules/webpackmodules.js
|
||||
* https://github.com/BetterDiscord/BetterDiscord/blob/78edeb77c60542a57884686c4ba98f997c886fad/LICENSE
|
||||
*/
|
||||
class WEBPACK {
|
||||
static get require() {
|
||||
if (this._require) {
|
||||
return this._require;
|
||||
}
|
||||
|
||||
/**
|
||||
* @type {Object}
|
||||
* @property {Object} m
|
||||
* @property {Object} c
|
||||
*/
|
||||
let hookedRequire;
|
||||
|
||||
const id = "dht-webpackmodules-" + new Date().getTime();
|
||||
if (typeof (window["webpackChunkdiscord_app"]) !== "undefined") {
|
||||
window["webpackChunkdiscord_app"].push([ [ id ], {}, internalRequire => hookedRequire = internalRequire ]);
|
||||
}
|
||||
|
||||
delete hookedRequire.m[id];
|
||||
delete hookedRequire.c[id];
|
||||
return this._require = hookedRequire;
|
||||
}
|
||||
|
||||
static getAllModules() {
|
||||
return this.require.c;
|
||||
}
|
||||
|
||||
static filterByProps(...props) {
|
||||
return module => props.every(prop => prop in module);
|
||||
}
|
||||
|
||||
static filterByPropsWithPredicate(predicate, ...props) {
|
||||
return module => props.every(prop => prop in module && predicate(module[prop]));
|
||||
}
|
||||
|
||||
static findModules(filter) {
|
||||
const defaultExport = true;
|
||||
const moduleFilter = module => (typeof module === "object" || typeof module === "function") && filter(module);
|
||||
|
||||
const results = [];
|
||||
|
||||
for (const module of Object.values(this.getAllModules())) {
|
||||
/**
|
||||
* @type {Object}
|
||||
* @property [Z]
|
||||
* @property [ZP]
|
||||
* @property [__esModule]
|
||||
* @property [default]
|
||||
*/
|
||||
const exports = module.exports;
|
||||
if (!exports || exports === window || exports === document.documentElement || exports[Symbol.toStringTag] === "DOMTokenList") {
|
||||
continue;
|
||||
}
|
||||
|
||||
let foundModule = null;
|
||||
if (exports.Z && moduleFilter(exports.Z)) {
|
||||
foundModule = defaultExport ? exports.Z : exports;
|
||||
}
|
||||
if (exports.ZP && moduleFilter(exports.ZP)) {
|
||||
foundModule = defaultExport ? exports.ZP : exports;
|
||||
}
|
||||
if (exports.__esModule && exports.default && moduleFilter(exports.default)) {
|
||||
foundModule = defaultExport ? exports.default : exports;
|
||||
}
|
||||
if (moduleFilter(exports)) {
|
||||
foundModule = exports;
|
||||
}
|
||||
|
||||
if (foundModule) {
|
||||
results.push(foundModule);
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
static findModule(filter) {
|
||||
const modules = this.findModules(filter);
|
||||
return modules.length === 1 ? modules[0] : null;
|
||||
}
|
||||
|
||||
static findFunction(name, additionalRequiredProps) {
|
||||
const searchedProps = additionalRequiredProps ? [name, ...additionalRequiredProps] : [name];
|
||||
const matchingModule = this.findModule(this.filterByPropsWithPredicate(prop => typeof(prop) === "function", ...searchedProps));
|
||||
return matchingModule == null ? null : matchingModule[name].bind(matchingModule);
|
||||
}
|
||||
}
|
@ -15,7 +15,7 @@ sealed class GetTrackingScriptEndpoint(IDatabaseFile db, ServerParameters parame
|
||||
string bootstrap = await resources.ReadTextAsync("Tracker/bootstrap.js");
|
||||
string script = bootstrap.Replace("= 0; /*[PORT]*/", "= " + parameters.Port + ";")
|
||||
.Replace("/*[TOKEN]*/", HttpUtility.JavaScriptStringEncode(parameters.Token))
|
||||
.Replace("/*[IMPORTS]*/", await resources.ReadJoinedAsync("Tracker/scripts/", '\n'))
|
||||
.Replace("/*[IMPORTS]*/", await resources.ReadJoinedAsync("Tracker/scripts/", '\n', [ "/webpack.js" ]))
|
||||
.Replace("/*[CSS-CONTROLLER]*/", await resources.ReadTextAsync("Tracker/styles/controller.css"))
|
||||
.Replace("/*[CSS-SETTINGS]*/", await resources.ReadTextAsync("Tracker/styles/settings.css"))
|
||||
.Replace("/*[DEBUGGER]*/", request.Query.ContainsKey("debug") ? "debugger;" : "");
|
||||
|
@ -1,5 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
@ -43,14 +45,26 @@ public sealed class ResourceLoader(Assembly assembly) {
|
||||
return TryGetEmbeddedStream(filename) is {} stream ? await ReadBytesAsync(stream) : null;
|
||||
}
|
||||
|
||||
public async Task<string> ReadJoinedAsync(string path, char separator) {
|
||||
StringBuilder joined = new ();
|
||||
public async Task<string> ReadJoinedAsync(string path, char separator, string[] order) {
|
||||
List<(string, Stream)> resourceNames = [];
|
||||
|
||||
foreach (var embeddedName in assembly.GetManifestResourceNames()) {
|
||||
if (embeddedName.Replace('\\', '/').StartsWith(path)) {
|
||||
joined.Append(await ReadTextAsync(assembly.GetManifestResourceStream(embeddedName)!)).Append(separator);
|
||||
var embeddedNameNormalized = embeddedName.Replace('\\', '/');
|
||||
if (embeddedNameNormalized.StartsWith(path)) {
|
||||
resourceNames.Add((embeddedNameNormalized, assembly.GetManifestResourceStream(embeddedName)!));
|
||||
}
|
||||
}
|
||||
|
||||
StringBuilder joined = new ();
|
||||
|
||||
int GetOrderKey(string name) {
|
||||
int key = Array.FindIndex(order, name.EndsWith);
|
||||
return key == -1 ? order.Length : key;
|
||||
}
|
||||
|
||||
foreach(var (_, stream) in resourceNames.OrderBy(item => GetOrderKey(item.Item1))) {
|
||||
joined.Append(await ReadTextAsync(stream)).Append(separator);
|
||||
}
|
||||
|
||||
return joined.ToString(0, Math.Max(0, joined.Length - 1));
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user