1
0
mirror of https://github.com/chylex/Discord-History-Tracker.git synced 2025-08-18 13:31:42 +02:00

10 Commits
v31 ... v32

24 changed files with 434 additions and 189 deletions

View File

@@ -0,0 +1,23 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Minify" type="PythonConfigurationType" factoryName="Python">
<module name="rider.module" />
<option name="INTERPRETER_OPTIONS" value="" />
<option name="PARENT_ENVS" value="true" />
<envs>
<env name="PYTHONUNBUFFERED" value="1" />
</envs>
<option name="SDK_HOME" value="" />
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$" />
<option name="IS_MODULE_SDK" value="false" />
<option name="ADD_CONTENT_ROOTS" value="false" />
<option name="ADD_SOURCE_ROOTS" value="false" />
<option name="SCRIPT_NAME" value="$PROJECT_DIR$/minify.py" />
<option name="PARAMETERS" value="" />
<option name="SHOW_COMMAND_LINE" value="false" />
<option name="EMULATE_TERMINAL" value="false" />
<option name="MODULE_MODE" value="false" />
<option name="REDIRECT_INPUT" value="false" />
<option name="INPUT_FILE" value="" />
<method v="2" />
</configuration>
</component>

View File

@@ -12,7 +12,7 @@
<CheckForOverflowUnderflow>true</CheckForOverflowUnderflow> <CheckForOverflowUnderflow>true</CheckForOverflowUnderflow>
<SatelliteResourceLanguages>en</SatelliteResourceLanguages> <SatelliteResourceLanguages>en</SatelliteResourceLanguages>
<AssemblyName>DiscordHistoryTracker</AssemblyName> <AssemblyName>DiscordHistoryTracker</AssemblyName>
<Version>31.0.0.0</Version> <Version>32.0.0.0</Version>
<AssemblyVersion>$(Version)</AssemblyVersion> <AssemblyVersion>$(Version)</AssemblyVersion>
<FileVersion>$(Version)</FileVersion> <FileVersion>$(Version)</FileVersion>
<PackageVersion>$(Version)</PackageVersion> <PackageVersion>$(Version)</PackageVersion>

View File

@@ -3,19 +3,29 @@ using System;
namespace DHT.Desktop.Dialogs { namespace DHT.Desktop.Dialogs {
public static class DialogResult { public static class DialogResult {
public enum All { public enum All {
Ok, Yes, No, Cancel Ok,
Yes,
No,
Cancel
} }
public enum OkCancel { public enum OkCancel {
Closed, Ok, Cancel Closed,
Ok,
Cancel
} }
public enum YesNo { public enum YesNo {
Closed, Yes, No Closed,
Yes,
No
} }
public enum YesNoCancel { public enum YesNoCancel {
Closed, Yes, No, Cancel Closed,
Yes,
No,
Cancel
} }
public static OkCancel ToOkCancel(this All? result) { public static OkCancel ToOkCancel(this All? result) {

View File

@@ -16,4 +16,3 @@ namespace DHT.Desktop.Main {
} }
} }
} }

View File

@@ -97,7 +97,7 @@
const info = DISCORD.getSelectedChannel(); const info = DISCORD.getSelectedChannel();
if (!info) { if (!info) {
GUI.setStatus("Stopped"); GUI.setStatus("Error (Unknown Channel)");
stopTrackingDelayed(); stopTrackingDelayed();
return; return;
} }
@@ -130,14 +130,9 @@
STATE.onTrackingStateChanged(enabled => { STATE.onTrackingStateChanged(enabled => {
if (enabled) { if (enabled) {
if (DISCORD.getSelectedChannel() == null) {
stopTrackingDelayed(() => alert("The selected channel is not visible in the channel list."));
return;
}
const messages = DISCORD.getMessages(); const messages = DISCORD.getMessages();
if (messages == null) { if (messages.length === 0) {
stopTrackingDelayed(() => alert("Cannot see any messages.")); stopTrackingDelayed(() => alert("Cannot see any messages."));
return; return;
} }

File diff suppressed because one or more lines are too long

View File

@@ -1 +1 @@
const STATE=function(){let serverPort=-1;let serverToken="";const post=function(endpoint,json){const aborter=new AbortController;const timeout=window.setTimeout(()=>aborter.abort(),5e3);return new Promise(async(resolve,reject)=>{let r;try{r=await fetch("http://127.0.0.1:"+serverPort+endpoint,{method:"POST",headers:{"Content-Type":"application/json","X-DHT-Token":serverToken},credentials:"omit",redirect:"error",body:JSON.stringify(json),signal:aborter.signal})}catch(e){if(e.name==="AbortError"){reject({status:"DISCONNECTED"});return}else{reject({status:"ERROR",message:e.message});return}}finally{window.clearTimeout(timeout)}if(r.status===200){resolve(r);return}try{const message=await r.text();reject({status:"ERROR",message:message})}catch(e){reject({status:"ERROR",message:e.message})}})};const trackingStateChangedListeners=[];let isTracking=false;const addedChannels=new Set;const addedUsers=new Set;return{setup(port,token){serverPort=port;serverToken=token},onTrackingStateChanged(callback){trackingStateChangedListeners.push(callback);callback(isTracking)},isTracking(){return isTracking},setIsTracking(state){if(isTracking!==state){isTracking=state;if(isTracking){addedChannels.clear();addedUsers.clear()}for(const callback of trackingStateChangedListeners){callback(isTracking)}}},async addDiscordChannel(serverInfo,channelInfo){if(addedChannels.has(channelInfo.id)){return}const server={id:serverInfo.id,name:serverInfo.name,type:serverInfo.type};const channel={id:channelInfo.id,name:channelInfo.name};if("extra"in channelInfo){channel.position=channelInfo.extra.position;channel.topic=channelInfo.extra.topic;channel.nsfw=channelInfo.extra.nsfw}await post("/track-channel",{server:server,channel:channel});addedChannels.add(channelInfo.id)},async addDiscordMessages(channelId,discordMessageArray){const userInfo={};let hasNewUsers=false;for(const msg of discordMessageArray){const user=msg.author;if(!addedUsers.has(user.id)){const obj={id:user.id,name:user.username};if(user.avatar){obj.avatar=user.avatar}if(!user.bot){obj.discriminator=user.discriminator}userInfo[user.id]=obj;hasNewUsers=true}}if(hasNewUsers){await post("/track-users",Object.values(userInfo));for(const id of Object.keys(userInfo)){addedUsers.add(id)}}const response=await post("/track-messages",discordMessageArray.map(msg=>{const obj={id:msg.id,sender:msg.author.id,channel:msg.channel_id,text:msg.content,timestamp:msg.timestamp.toDate().getTime()};if(msg.editedTimestamp!==null){obj.editTimestamp=msg.editedTimestamp.toDate().getTime()}if(msg.messageReference!==null){obj.repliedToId=msg.messageReference.message_id}if(msg.attachments.length>0){obj.attachments=msg.attachments.map(attachment=>{const mapped={id:attachment.id,name:attachment.filename,size:attachment.size,url:attachment.url};if(attachment.content_type){mapped.type=attachment.content_type}return mapped})}if(msg.embeds.length>0){obj.embeds=msg.embeds.map(embed=>{const mapped={};for(const key of Object.keys(embed)){if(key==="id"){continue}if(key==="rawTitle"){mapped["title"]=embed[key]}else if(key==="rawDescription"){mapped["description"]=embed[key]}else{mapped[key]=embed[key]}}return JSON.stringify(mapped)})}if(msg.reactions.length>0){obj.reactions=msg.reactions.map(reaction=>{const emoji=reaction.emoji;const mapped={count:reaction.count};if(emoji.id){mapped.id=emoji.id}if(emoji.name){mapped.name=emoji.name}if(emoji.animated){mapped.isAnimated=emoji.animated}return mapped})}return obj}));const anyNewMessages=await response.text();return anyNewMessages==="1"}}}(); const STATE=function(){let serverPort=-1;let serverToken="";const post=function(endpoint,json){const aborter=new AbortController;const timeout=window.setTimeout(()=>aborter.abort(),5e3);return new Promise(async(resolve,reject)=>{let r;try{r=await fetch("http://127.0.0.1:"+serverPort+endpoint,{method:"POST",headers:{"Content-Type":"application/json","X-DHT-Token":serverToken},credentials:"omit",redirect:"error",body:JSON.stringify(json),signal:aborter.signal})}catch(e){if(e.name==="AbortError"){reject({status:"DISCONNECTED"});return}else{reject({status:"ERROR",message:e.message});return}}finally{window.clearTimeout(timeout)}if(r.status===200){resolve(r);return}try{const message=await r.text();reject({status:"ERROR",message:message})}catch(e){reject({status:"ERROR",message:e.message})}})};const trackingStateChangedListeners=[];let isTracking=false;const addedChannels=new Set;const addedUsers=new Set;return{setup(port,token){serverPort=port;serverToken=token},onTrackingStateChanged(callback){trackingStateChangedListeners.push(callback);callback(isTracking)},isTracking(){return isTracking},setIsTracking(state){if(isTracking!==state){isTracking=state;if(isTracking){addedChannels.clear();addedUsers.clear()}for(const callback of trackingStateChangedListeners){callback(isTracking)}}},async addDiscordChannel(serverInfo,channelInfo){if(addedChannels.has(channelInfo.id)){return}const server={id:serverInfo.id,name:serverInfo.name,type:serverInfo.type};const channel={id:channelInfo.id,name:channelInfo.name};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}await post("/track-channel",{server:server,channel:channel});addedChannels.add(channelInfo.id)},async addDiscordMessages(channelId,discordMessageArray){discordMessageArray=discordMessageArray.filter(msg=>(msg.type===0||msg.type===19||msg.type===21)&&msg.state==="SENT");if(discordMessageArray.length===0){return false}const userInfo={};let hasNewUsers=false;for(const msg of discordMessageArray){const user=msg.author;if(!addedUsers.has(user.id)){const obj={id:user.id,name:user.username};if(user.avatar){obj.avatar=user.avatar}if(!user.bot){obj.discriminator=user.discriminator}userInfo[user.id]=obj;hasNewUsers=true}}if(hasNewUsers){await post("/track-users",Object.values(userInfo));for(const id of Object.keys(userInfo)){addedUsers.add(id)}}const response=await post("/track-messages",discordMessageArray.map(msg=>{const obj={id:msg.id,sender:msg.author.id,channel:msg.channel_id,text:msg.content,timestamp:msg.timestamp.toDate().getTime()};if(msg.editedTimestamp!==null){obj.editTimestamp=msg.editedTimestamp.toDate().getTime()}if(msg.messageReference!==null){obj.repliedToId=msg.messageReference.message_id}if(msg.attachments.length>0){obj.attachments=msg.attachments.map(attachment=>{const mapped={id:attachment.id,name:attachment.filename,size:attachment.size,url:attachment.url};if(attachment.content_type){mapped.type=attachment.content_type}return mapped})}if(msg.embeds.length>0){obj.embeds=msg.embeds.map(embed=>{const mapped={};for(const key of Object.keys(embed)){if(key==="id"){continue}if(key==="rawTitle"){mapped["title"]=embed[key]}else if(key==="rawDescription"){mapped["description"]=embed[key]}else{mapped[key]=embed[key]}}return JSON.stringify(mapped)})}if(msg.reactions.length>0){obj.reactions=msg.reactions.map(reaction=>{const emoji=reaction.emoji;const mapped={count:reaction.count};if(emoji.id){mapped.id=emoji.id}if(emoji.name){mapped.name=emoji.name}if(emoji.animated){mapped.isAnimated=emoji.animated}return mapped})}return obj}));const anyNewMessages=await response.text();return anyNewMessages==="1"}}}();

View File

@@ -1,10 +1,15 @@
// noinspection JSUnresolvedVariable
class DISCORD { class DISCORD {
static getMessageOuterElement() { static getMessageOuterElement() {
return DOM.queryReactClass("messagesWrapper"); return DOM.queryReactClass("messagesWrapper");
} }
static getMessageScrollerElement() { static getMessageScrollerElement() {
return this.getMessageOuterElement().querySelector("[class*='scroller-']"); return DOM.queryReactClass("scroller", this.getMessageOuterElement());
}
static getMessageElements() {
return this.getMessageOuterElement().querySelectorAll("[class*='message-']");
} }
static hasMoreMessages() { static hasMoreMessages() {
@@ -41,7 +46,7 @@ class DISCORD {
return; return;
} }
const anyMessage = this.getMessageOuterElement().querySelector("[class*='message-']"); const anyMessage = DOM.queryReactClass("message", this.getMessageOuterElement());
const messageCount = anyMessage ? anyMessage.parentElement.children.length : 0; const messageCount = anyMessage ? anyMessage.parentElement.children.length : 0;
if (messageCount > 300) { if (messageCount > 300) {
@@ -85,33 +90,74 @@ class DISCORD {
}, 200); }, 200);
} }
/**
* Returns the property object of a message element.
* @returns { null | { message: DiscordMessage, channel: Object } }
*/
static getMessageElementProps(ele) {
const props = DOM.getReactProps(ele);
if (props.children && props.children.length >= 4) {
const childProps = props.children[3].props;
if ("message" in childProps && "channel" in childProps) {
return childProps;
}
}
return null;
}
/**
* Returns an array containing currently loaded messages.
*/
static getMessages() {
try {
const messages = [];
for (const ele of this.getMessageElements()) {
const props = this.getMessageElementProps(ele);
if (props != null) {
messages.push(props.message);
}
}
return messages;
} catch (e) {
console.error(e);
return [];
}
}
/** /**
* Returns an object containing the selected server and channel information. * Returns an object containing the selected server and channel information.
* For types DM and GROUP, the server and channel ids and names are identical. * For types DM and GROUP, the server and channel ids and names are identical.
* @returns {{}|null} * @returns { {} | null }
*/ */
static getSelectedChannel() { static getSelectedChannel() {
try { try {
let obj; let obj;
let channelListEle = DOM.queryReactClass("privateChannels");
if (channelListEle) { for (const ele of this.getMessageElements()) {
const channel = DOM.queryReactClass("selected", channelListEle); const props = this.getMessageElementProps(ele);
if (!channel || !("href" in channel) || !channel.href.includes("/@me/")) { if (props != null) {
obj = props.channel;
break;
}
}
if (!obj) {
return null; return null;
} }
const linkSplit = channel.href.split("/"); const dms = DOM.queryReactClass("privateChannels");
const id = linkSplit[linkSplit.length - 1];
if (!(/^\d+$/.test(id))) {
return null;
}
if (dms) {
let name; let name;
for (const ele of channel.querySelectorAll("[class^='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); const node = Array.prototype.find.call(ele.childNodes, node => node.nodeType === Node.TEXT_NODE);
if (node) { if (node) {
@@ -124,87 +170,49 @@ class DISCORD {
return null; return null;
} }
const icon = channel.querySelector("img[class*='avatar']"); let type;
const iconParent = icon && icon.closest("foreignObject");
const iconMask = iconParent && iconParent.getAttribute("mask");
obj = { // https://discord.com/developers/docs/resources/channel#channel-object-channel-types
"server": { id, name, type: (iconMask && iconMask.includes("#svg-mask-avatar-default")) ? "GROUP" : "DM" }, switch (obj.type) {
"channel": { id, name } case 1: type = "DM"; break;
}; case 3: type = "GROUP"; break;
} default: return null;
else {
channelListEle = document.getElementById("channels");
const channel = channelListEle.querySelector("[class*='modeSelected']").parentElement;
// noinspection JSUnresolvedVariable
const props = DOM.getReactProps(channel).children.props;
if (!props) {
return null;
} }
// noinspection JSUnresolvedVariable const id = obj.id;
const channelObj = props.channel || props.children().props.channel; const server = { id, name, type };
const channel = { id, name };
if (!channelObj) { return { server, channel };
return null;
} }
else if (obj.guild_id) {
// noinspection JSUnresolvedVariable const server = {
obj = { "id": obj.guild_id,
"server": {
"id": channelObj.guild_id,
"name": document.querySelector("nav header > h1").innerText, "name": document.querySelector("nav header > h1").innerText,
"type": "SERVER" "type": "SERVER"
}, };
"channel": {
"id": channelObj.id, const channel = {
"name": channelObj.name, "id": obj.id,
"name": obj.name,
"extra": { "extra": {
"position": channelObj.position, "nsfw": obj.nsfw
"topic": channelObj.topic,
"nsfw": channelObj.nsfw
}
} }
}; };
if (obj.parent_id) {
channel["extra"]["parent"] = obj.parent_id;
}
else {
channel["extra"]["position"] = obj.position;
channel["extra"]["topic"] = obj.topic;
} }
return obj.channel.length === 0 ? null : obj; return { server, channel };
} catch (e) { }
console.error(e); else {
return null; return null;
} }
}
/**
* Returns an array containing currently loaded messages, or null if the messages cannot be retrieved.
*/
static getMessages() {
try {
const scroller = this.getMessageScrollerElement();
const props = DOM.getReactProps(scroller);
let wrappers;
try {
// noinspection JSUnresolvedVariable
wrappers = props.children.props.children.props.children.props.children.find(ele => Array.isArray(ele));
} catch (e) { // old version compatibility
wrappers = props.children.find(ele => Array.isArray(ele));
}
const messages = [];
for (const obj of wrappers) {
// noinspection JSUnresolvedVariable
const nested = obj.props;
if (nested && nested.message) {
messages.push(nested.message);
}
}
return messages;
} catch (e) { } catch (e) {
console.error(e); console.error(e);
return null; return null;
@@ -231,11 +239,16 @@ class DISCORD {
} }
} }
else { else {
const channelIconNormal = "M5.88657 21C5.57547 21 5.3399 20.7189 5.39427 20.4126L6.00001 17H2.59511C2.28449 17 2.04905 16.7198 2.10259 16.4138L2.27759 15.4138C2.31946 15.1746 2.52722 15 2.77011 15H6.35001L7.41001 9H4.00511C3.69449 9 3.45905 8.71977 3.51259 8.41381L3.68759 7.41381C3.72946 7.17456 3.93722 7 4.18011 7H7.76001L8.39677 3.41262C8.43914 3.17391 8.64664 3 8.88907 3H9.87344C10.1845 3 10.4201 3.28107 10.3657 3.58738L9.76001 7H15.76L16.3968 3.41262C16.4391 3.17391 16.6466 3 16.8891 3H17.8734C18.1845 3 18.4201 3.28107 18.3657 3.58738L17.76 7H21.1649C21.4755 7 21.711 7.28023 21.6574 7.58619L21.4824 8.58619C21.4406 8.82544 21.2328 9 20.9899 9H17.41L16.35 15H19.7549C20.0655 15 20.301 15.2802 20.2474 15.5862L20.0724 16.5862C20.0306 16.8254 19.8228 17 19.5799 17H16L15.3632 20.5874C15.3209 20.8261 15.1134 21 14.8709 21H13.8866C13.5755 21 13.3399 20.7189 13.3943 20.4126L14 17H8.00001L7.36325 20.5874C7.32088 20.8261 7.11337 21 6.87094 21H5.88657ZM9.41045 9L8.35045 15H14.3504L15.4104 9H9.41045Z"; const channelIcons = [
const channelIconSpecial = "M14 8C14 7.44772 13.5523 7 13 7H9.76001L10.3657 3.58738C10.4201 3.28107 10.1845 3 9.87344 3H8.88907C8.64664 3 8.43914 3.17391 8.39677 3.41262L7.76001 7H4.18011C3.93722 7 3.72946 7.17456 3.68759 7.41381L3.51259 8.41381C3.45905 8.71977 3.69449 9 4.00511 9H7.41001L6.35001 15H2.77011C2.52722 15 2.31946 15.1746 2.27759 15.4138L2.10259 16.4138C2.04905 16.7198 2.28449 17 2.59511 17H6.00001L5.39427 20.4126C5.3399 20.7189 5.57547 21 5.88657 21H6.87094C7.11337 21 7.32088 20.8261 7.36325 20.5874L8.00001 17H14L13.3943 20.4126C13.3399 20.7189 13.5755 21 13.8866 21H14.8709C15.1134 21 15.3209 20.8261 15.3632 20.5874L16 17H19.5799C19.8228 17 20.0306 16.8254 20.0724 16.5862L20.2474 15.5862C20.301 15.2802 20.0655 15 19.7549 15H16.35L16.6758 13.1558C16.7823 12.5529 16.3186 12 15.7063 12C15.2286 12 14.8199 12.3429 14.7368 12.8133L14.3504 15H8.35045L9.41045 9H13C13.5523 9 14 8.55228 14 8Z"; /* normal */ "M5.88657 21C5.57547 21 5.3399 20.7189 5.39427 20.4126L6.00001 17H2.59511C2.28449 17 2.04905 16.7198 2.10259 16.4138L2.27759 15.4138C2.31946 15.1746 2.52722 15 2.77011 15H6.35001L7.41001 9H4.00511C3.69449 9 3.45905 8.71977 3.51259 8.41381L3.68759 7.41381C3.72946 7.17456 3.93722 7 4.18011 7H7.76001L8.39677 3.41262C8.43914 3.17391 8.64664 3 8.88907 3H9.87344C10.1845 3 10.4201 3.28107 10.3657 3.58738L9.76001 7H15.76L16.3968 3.41262C16.4391 3.17391 16.6466 3 16.8891 3H17.8734C18.1845 3 18.4201 3.28107 18.3657 3.58738L17.76 7H21.1649C21.4755 7 21.711 7.28023 21.6574 7.58619L21.4824 8.58619C21.4406 8.82544 21.2328 9 20.9899 9H17.41L16.35 15H19.7549C20.0655 15 20.301 15.2802 20.2474 15.5862L20.0724 16.5862C20.0306 16.8254 19.8228 17 19.5799 17H16L15.3632 20.5874C15.3209 20.8261 15.1134 21 14.8709 21H13.8866C13.5755 21 13.3399 20.7189 13.3943 20.4126L14 17H8.00001L7.36325 20.5874C7.32088 20.8261 7.11337 21 6.87094 21H5.88657ZM9.41045 9L8.35045 15H14.3504L15.4104 9H9.41045Z",
/* normal + thread */ "M5.43309 21C5.35842 21 5.30189 20.9325 5.31494 20.859L5.99991 17H2.14274C2.06819 17 2.01168 16.9327 2.02453 16.8593L2.33253 15.0993C2.34258 15.0419 2.39244 15 2.45074 15H6.34991L7.40991 9H3.55274C3.47819 9 3.42168 8.93274 3.43453 8.85931L3.74253 7.09931C3.75258 7.04189 3.80244 7 3.86074 7H7.75991L8.45234 3.09903C8.46251 3.04174 8.51231 3 8.57049 3H10.3267C10.4014 3 10.4579 3.06746 10.4449 3.14097L9.75991 7H15.7599L16.4523 3.09903C16.4625 3.04174 16.5123 3 16.5705 3H18.3267C18.4014 3 18.4579 3.06746 18.4449 3.14097L17.7599 7H21.6171C21.6916 7 21.7481 7.06725 21.7353 7.14069L21.4273 8.90069C21.4172 8.95811 21.3674 9 21.3091 9H17.4099L17.0495 11.04H15.05L15.4104 9H9.41035L8.35035 15H10.5599V17H7.99991L7.30749 20.901C7.29732 20.9583 7.24752 21 7.18934 21H5.43309Z",
/* nsfw or private */ "M14 8C14 7.44772 13.5523 7 13 7H9.76001L10.3657 3.58738C10.4201 3.28107 10.1845 3 9.87344 3H8.88907C8.64664 3 8.43914 3.17391 8.39677 3.41262L7.76001 7H4.18011C3.93722 7 3.72946 7.17456 3.68759 7.41381L3.51259 8.41381C3.45905 8.71977 3.69449 9 4.00511 9H7.41001L6.35001 15H2.77011C2.52722 15 2.31946 15.1746 2.27759 15.4138L2.10259 16.4138C2.04905 16.7198 2.28449 17 2.59511 17H6.00001L5.39427 20.4126C5.3399 20.7189 5.57547 21 5.88657 21H6.87094C7.11337 21 7.32088 20.8261 7.36325 20.5874L8.00001 17H14L13.3943 20.4126C13.3399 20.7189 13.5755 21 13.8866 21H14.8709C15.1134 21 15.3209 20.8261 15.3632 20.5874L16 17H19.5799C19.8228 17 20.0306 16.8254 20.0724 16.5862L20.2474 15.5862C20.301 15.2802 20.0655 15 19.7549 15H16.35L16.6758 13.1558C16.7823 12.5529 16.3186 12 15.7063 12C15.2286 12 14.8199 12.3429 14.7368 12.8133L14.3504 15H8.35045L9.41045 9H13C13.5523 9 14 8.55228 14 8Z",
/* nsfw + thread */ "M14.4 7C14.5326 7 14.64 7.10745 14.64 7.24V8.76C14.64 8.89255 14.5326 9 14.4 9H9.41045L8.35045 15H10.56V17H8.00001L7.36325 20.5874C7.32088 20.8261 7.11337 21 6.87094 21H5.88657C5.57547 21 5.3399 20.7189 5.39427 20.4126L6.00001 17H2.59511C2.28449 17 2.04905 16.7198 2.10259 16.4138L2.27759 15.4138C2.31946 15.1746 2.52722 15 2.77011 15H6.35001L7.41001 9H4.00511C3.69449 9 3.45905 8.71977 3.51259 8.41381L3.68759 7.41381C3.72946 7.17456 3.93722 7 4.18011 7H7.76001L8.39677 3.41262C8.43914 3.17391 8.64664 3 8.88907 3H9.87344C10.1845 3 10.4201 3.28107 10.3657 3.58738L9.76001 7H14.4Z",
/* private + thread */ "M15.44 6.99992C15.5725 6.99992 15.68 7.10737 15.68 7.23992V8.75992C15.68 8.89247 15.5725 8.99992 15.44 8.99992H9.41045L8.35045 14.9999H10.56V16.9999H8.00001L7.36325 20.5873C7.32088 20.826 7.11337 20.9999 6.87094 20.9999H5.88657C5.57547 20.9999 5.3399 20.7189 5.39427 20.4125L6.00001 16.9999H2.59511C2.28449 16.9999 2.04905 16.7197 2.10259 16.4137L2.27759 15.4137C2.31946 15.1745 2.52722 14.9999 2.77011 14.9999H6.35001L7.41001 8.99992H4.00511C3.69449 8.99992 3.45905 8.71969 3.51259 8.41373L3.68759 7.41373C3.72946 7.17448 3.93722 6.99992 4.18011 6.99992H7.76001L8.39677 3.41254C8.43914 3.17384 8.64664 2.99992 8.88907 2.99992H9.87344C10.1845 2.99992 10.4201 3.28099 10.3657 3.58731L9.76001 6.99992H15.44Z"
];
const isValidChannelClass = cls => cls.includes("wrapper-") && !cls.includes("clickable-"); const isValidChannelClass = cls => cls.includes("wrapper-") && !cls.includes("clickable-");
const isValidChannelType = ele => !!ele.querySelector("path[d=\"" + channelIconNormal + "\"]") || !!ele.querySelector("path[d=\"" + channelIconSpecial + "\"]"); const isValidChannelType = ele => channelIcons.some(icon => !!ele.querySelector("path[d=\"" + icon + "\"]"));
const isValidChannel = ele => ele.childElementCount > 0 && isValidChannelClass(ele.children[0].className) && isValidChannelType(ele); const isValidChannel = ele => ele.childElementCount > 0 && isValidChannelClass(ele.children[0].className) && isValidChannelType(ele);
const channelListEle = document.querySelector("div[class*='sidebar'] > nav[class*='container'] > div[class*='scroller']"); const channelListEle = document.querySelector("div[class*='sidebar'] > nav[class*='container'] > div[class*='scroller']");

View File

@@ -75,6 +75,8 @@ const STATE = (function() {
* @property {Object[]} embeds * @property {Object[]} embeds
* @property {DiscordMessageReaction[]} [reactions] * @property {DiscordMessageReaction[]} [reactions]
* @property {DiscordMessageReference} [messageReference] * @property {DiscordMessageReference} [messageReference]
* @property {Number} type
* @property {String} state
*/ */
/** /**
@@ -156,9 +158,15 @@ const STATE = (function() {
}; };
if ("extra" in channelInfo) { if ("extra" in channelInfo) {
channel.position = channelInfo.extra.position; const extra = channelInfo.extra;
channel.topic = channelInfo.extra.topic;
channel.nsfw = channelInfo.extra.nsfw; if ("parent" in extra) {
channel.parent = extra.parent;
}
channel.position = extra.position;
channel.topic = extra.topic;
channel.nsfw = extra.nsfw;
} }
await post("/track-channel", { server, channel }); await post("/track-channel", { server, channel });
@@ -170,6 +178,13 @@ const STATE = (function() {
* @param {DiscordMessage[]} discordMessageArray * @param {DiscordMessage[]} discordMessageArray
*/ */
async addDiscordMessages(channelId, discordMessageArray) { async addDiscordMessages(channelId, discordMessageArray) {
// https://discord.com/developers/docs/resources/channel#message-object-message-types
discordMessageArray = discordMessageArray.filter(msg => (msg.type === 0 || msg.type === 19 || msg.type === 21) && msg.state === "SENT");
if (discordMessageArray.length === 0) {
return false;
}
const userInfo = {}; const userInfo = {};
let hasNewUsers = false; let hasNewUsers = false;

View File

@@ -171,7 +171,7 @@ const DISCORD = (function() {
return DOM.getHumanReadableTime(value); return DOM.getHumanReadableTime(value);
} }
else if (property === "contents") { else if (property === "contents") {
return value == null || value.length === 0 ? "" : processMessageContents(value); return value && value.length > 0 ? processMessageContents(value) : "";
} }
else if (property === "embeds") { else if (property === "embeds") {
if (!value) { if (!value) {
@@ -225,8 +225,8 @@ const DISCORD = (function() {
return STATE.hasActiveFilter ? "<span class='info jump' data-jump='" + value + "'>Jump to message</span>" : ""; return STATE.hasActiveFilter ? "<span class='info jump' data-jump='" + value + "'>Jump to message</span>" : "";
} }
else if (property === "reply") { else if (property === "reply") {
if (value === null) { if (!value) {
return ""; return value === null ? "<span class='reply-contents reply-missing'>(replies to an unknown message)</span>" : "";
} }
const user = "<span class='reply-username' title='#" + (value.user.tag ? value.user.tag : "????") + "'>" + value.user.name + "</span>"; const user = "<span class='reply-username' title='#" + (value.user.tag ? value.user.tag : "????") + "'>" + value.user.name + "</span>";
@@ -236,7 +236,7 @@ const DISCORD = (function() {
return "<span class='jump' data-jump='" + value.id + "'>Jump to reply</span><span class='user'>" + avatar + user + "</span>" + contents; return "<span class='jump' data-jump='" + value.id + "'>Jump to reply</span><span class='user'>" + avatar + user + "</span>" + contents;
} }
else if (property === "reactions"){ else if (property === "reactions"){
if (value === null){ if (!value){
return ""; return "";
} }

View File

@@ -118,8 +118,14 @@ const GUI = (function() {
resetActiveFilter(); resetActiveFilter();
const index = STATE.navigateToMessage(jump); const index = STATE.navigateToMessage(jump);
if (index === -1) {
alert("Message not found.");
}
else {
DOM.id("messages").children[index].scrollIntoView(); DOM.id("messages").children[index].scrollIntoView();
} }
}
}); });
DOM.id("overlay").addEventListener("click", () => { DOM.id("overlay").addEventListener("click", () => {

View File

@@ -29,29 +29,132 @@ const STATE = (function() {
return loadedFileMeta ? loadedFileMeta.users : []; return loadedFileMeta ? loadedFileMeta.users : [];
}; };
const getServer = function(index) {
return loadedFileMeta.servers[index] || { "name": "&lt;unknown&gt;", "type": "unknown" };
};
const generateChannelHierarchy = function() {
/**
* @type {Map<string, Set>}
*/
const hierarchy = new Map();
if (!loadedFileMeta) {
return hierarchy;
}
/**
* @returns {Set}
*/
function getChildren(parentId) {
let children = hierarchy.get(parentId);
if (!children) {
children = new Set();
hierarchy.set(parentId, children);
}
return children;
}
for (const [ id, channel ] of Object.entries(loadedFileMeta.channels)) {
getChildren(channel.parent || "").add(id);
}
const unreachableIds = new Set(hierarchy.keys());
function reachIds(parentId) {
unreachableIds.delete(parentId);
const children = hierarchy.get(parentId);
if (children) {
for (const id of children) {
reachIds(id);
}
}
}
reachIds("");
const rootChildren = getChildren("");
for (const unreachableId of unreachableIds) {
for (const id of hierarchy.get(unreachableId)) {
rootChildren.add(id);
}
hierarchy.delete(unreachableId);
}
return hierarchy;
};
const generateChannelOrder = function() {
if (!loadedFileMeta) {
return {};
}
const channels = loadedFileMeta.channels;
const hierarchy = generateChannelHierarchy();
function getSortedSubTree(parentId) {
const children = hierarchy.get(parentId);
if (!children) {
return [];
}
const sortedChildren = Array.from(children);
sortedChildren.sort((id1, id2) => {
const c1 = channels[id1];
const c2 = channels[id2];
const s1 = getServer(c1.server);
const s2 = getServer(c2.server);
return s1.type.localeCompare(s2.type, "en") ||
s1.name.toLocaleLowerCase().localeCompare(s2.name.toLocaleLowerCase(), undefined, { numeric: true }) ||
(c1.position || -1) - (c2.position || -1) ||
c1.name.toLocaleLowerCase().localeCompare(c2.name.toLocaleLowerCase(), undefined, { numeric: true });
});
const subTree = [];
for (const id of sortedChildren) {
subTree.push(id);
subTree.push(...getSortedSubTree(id));
}
return subTree;
}
const orderArray = getSortedSubTree("");
const orderMap = {};
for (let i = 0; i < orderArray.length; i++) {
orderMap[orderArray[i]] = i;
}
return orderMap;
};
const getChannelList = function() { const getChannelList = function() {
if (!loadedFileMeta) { if (!loadedFileMeta) {
return []; return [];
} }
const channels = loadedFileMeta.channels; const channels = loadedFileMeta.channels;
const channelOrder = generateChannelOrder();
return Object.keys(channels).map(key => ({ return Object.keys(channels).map(key => ({
"id": key, "id": key,
"name": channels[key].name, "name": channels[key].name,
"server": loadedFileMeta.servers[channels[key].server] || { "name": "&lt;unknown&gt;", "type": "unknown" }, "server": getServer(channels[key].server),
"msgcount": getFilteredMessageKeys(key).length, "msgcount": getFilteredMessageKeys(key).length,
"topic": channels[key].topic || "", "topic": channels[key].topic || "",
"nsfw": channels[key].nsfw || false, "nsfw": channels[key].nsfw || false,
"position": channels[key].position || -1
})).sort((ac, bc) => { })).sort((ac, bc) => {
const as = ac.server; return channelOrder[ac.id] - channelOrder[bc.id];
const bs = bc.server;
return as.type.localeCompare(bs.type, "en") ||
as.name.toLocaleLowerCase().localeCompare(bs.name.toLocaleLowerCase(), undefined, { numeric: true }) ||
ac.position - bc.position ||
ac.name.toLocaleLowerCase().localeCompare(bc.name.toLocaleLowerCase(), undefined, { numeric: true });
}); });
}; };
@@ -59,6 +162,26 @@ const STATE = (function() {
return loadedFileData[channel] || {}; return loadedFileData[channel] || {};
}; };
const getMessageById = function(id) {
for (const messages of Object.values(loadedFileData)) {
if (id in messages) {
return messages[id];
}
}
return null;
};
const getMessageChannel = function(id) {
for (const [ channel, messages ] of Object.entries(loadedFileData)) {
if (id in messages) {
return channel;
}
}
return null;
};
const getMessageList = function() { const getMessageList = function() {
if (!loadedMessages) { if (!loadedMessages) {
return []; return [];
@@ -83,28 +206,47 @@ const STATE = (function() {
const user = getUser(message.u); const user = getUser(message.u);
const avatar = user.avatar ? { id: getUserId(message.u), path: user.avatar } : null; const avatar = user.avatar ? { id: getUserId(message.u), path: user.avatar } : null;
const reply = ("r" in message && message.r in messages) ? messages[message.r] : null; const obj = {
const replyUser = reply ? getUser(reply.u) : null;
const replyAvatar = replyUser && replyUser.avatar ? { id: getUserId(reply.u), path: replyUser.avatar } : null;
const replyObj = reply ? {
"id": message.r,
"user": replyUser,
"avatar": replyAvatar,
"contents": reply.m
} : null;
return {
user, user,
avatar, avatar,
"timestamp": message.t, "timestamp": message.t,
"contents": ("m" in message) ? message.m : null,
"embeds": ("e" in message) ? message.e.map(embed => JSON.parse(embed)) : [],
"attachments": ("a" in message) ? message.a : [],
"edit": ("te" in message) ? message.te : null,
"jump": key, "jump": key,
"reply": replyObj,
"reactions": ("re" in message) ? message.re : null
}; };
if ("m" in message) {
obj["contents"] = message.m;
}
if ("e" in message) {
obj["embeds"] = message.e.map(embed => JSON.parse(embed));
}
if ("a" in message) {
obj["attachments"] = message.a;
}
if ("te" in message) {
obj["edit"] = message.te;
}
if ("r" in message) {
const replyMessage = getMessageById(message.r);
const replyUser = replyMessage ? getUser(replyMessage.u) : null;
const replyAvatar = replyUser && replyUser.avatar ? { id: getUserId(replyMessage.u), path: replyUser.avatar } : null;
obj["reply"] = replyMessage ? {
"id": message.r,
"user": replyUser,
"avatar": replyAvatar,
"contents": replyMessage.m
} : null;
}
if ("re" in message) {
obj["reactions"] = message.re;
}
return obj;
}); });
}; };
@@ -175,15 +317,18 @@ const STATE = (function() {
}, },
getChannelName(channel) { getChannelName(channel) {
return loadedFileMeta.channels[channel].name || channel; const channelObj = loadedFileMeta.channels[channel];
return (channelObj && channelObj.name) || channel;
}, },
getUserTag(user) { getUserTag(user) {
return loadedFileMeta.users[user].tag; const userObj = loadedFileMeta.users[user];
return (userObj && userObj.tag) || "????";
}, },
getUserName(user) { getUserName(user) {
return loadedFileMeta.users[user].name || user; const userObj = loadedFileMeta.users[user];
return (userObj && userObj.name) || user;
}, },
selectChannel(channel) { selectChannel(channel) {
@@ -247,13 +392,20 @@ const STATE = (function() {
navigateToMessage(id) { navigateToMessage(id) {
if (!loadedMessages) { if (!loadedMessages) {
return 0; return -1;
}
const channel = getMessageChannel(id);
if (channel !== null && channel !== selectedChannel) {
triggerChannelsRefreshed(channel);
this.selectChannel(channel);
} }
const index = loadedMessages.indexOf(id); const index = loadedMessages.indexOf(id);
if (index === -1) { if (index === -1) {
return 0; return -1;
} }
currentPage = Math.max(1, Math.min(this.getPageCount(), 1 + Math.floor(index / messagesPerPage))); currentPage = Math.max(1, Math.min(this.getPageCount(), 1 + Math.floor(index / messagesPerPage)));

View File

@@ -221,6 +221,10 @@
padding: 1px 2px; padding: 1px 2px;
} }
.reply-missing {
color: rgba(255, 255, 255, 0.55);
}
.reactions { .reactions {
margin-top: 4px; margin-top: 4px;
} }

View File

@@ -3,6 +3,7 @@ namespace DHT.Server.Data {
public ulong Id { get; init; } public ulong Id { get; init; }
public ulong Server { get; init; } public ulong Server { get; init; }
public string Name { get; init; } public string Name { get; init; }
public ulong? ParentId { get; init; }
public int? Position { get; init; } public int? Position { get; init; }
public string? Topic { get; init; } public string? Topic { get; init; }
public bool? Nsfw { get; init; } public bool? Nsfw { get; init; }

View File

@@ -71,6 +71,10 @@ namespace DHT.Server.Database.Export {
obj.server = serverIndices[channel.Server]; obj.server = serverIndices[channel.Server];
obj.name = channel.Name; obj.name = channel.Name;
if (channel.ParentId != null) {
obj.parent = channel.ParentId;
}
if (channel.Position != null) { if (channel.Position != null) {
obj.position = channel.Position; obj.position = channel.Position;
} }

View File

@@ -5,7 +5,7 @@ using Microsoft.Data.Sqlite;
namespace DHT.Server.Database.Sqlite { namespace DHT.Server.Database.Sqlite {
internal class Schema { internal class Schema {
internal const int Version = 1; internal const int Version = 2;
private readonly SqliteConnection conn; private readonly SqliteConnection conn;
@@ -64,6 +64,7 @@ namespace DHT.Server.Database.Sqlite {
id INTEGER PRIMARY KEY NOT NULL, id INTEGER PRIMARY KEY NOT NULL,
server INTEGER NOT NULL, server INTEGER NOT NULL,
name TEXT NOT NULL, name TEXT NOT NULL,
parent_id INTEGER,
position INTEGER, position INTEGER,
topic TEXT, topic TEXT,
nsfw INTEGER)"); nsfw INTEGER)");
@@ -105,6 +106,10 @@ namespace DHT.Server.Database.Sqlite {
private void UpgradeSchemas(int dbVersion) { private void UpgradeSchemas(int dbVersion) {
Execute("UPDATE metadata SET value = " + Version + " WHERE key = 'version'"); Execute("UPDATE metadata SET value = " + Version + " WHERE key = 'version'");
if (dbVersion <= 1) {
Execute("ALTER TABLE channels ADD parent_id INTEGER");
}
} }
} }
} }

View File

@@ -72,13 +72,14 @@ namespace DHT.Server.Database.Sqlite {
public void AddChannel(Channel channel) { public void AddChannel(Channel channel) {
using var cmd = conn.Upsert("channels", new[] { using var cmd = conn.Upsert("channels", new[] {
"id", "server", "name", "position", "topic", "nsfw" "id", "server", "name", "parent_id", "position", "topic", "nsfw"
}); });
var channelParams = cmd.Parameters; var channelParams = cmd.Parameters;
channelParams.AddAndSet(":id", channel.Id); channelParams.AddAndSet(":id", channel.Id);
channelParams.AddAndSet(":server", channel.Server); channelParams.AddAndSet(":server", channel.Server);
channelParams.AddAndSet(":name", channel.Name); channelParams.AddAndSet(":name", channel.Name);
channelParams.AddAndSet(":parent_id", channel.ParentId);
channelParams.AddAndSet(":position", channel.Position); channelParams.AddAndSet(":position", channel.Position);
channelParams.AddAndSet(":topic", channel.Topic); channelParams.AddAndSet(":topic", channel.Topic);
channelParams.AddAndSet(":nsfw", channel.Nsfw); channelParams.AddAndSet(":nsfw", channel.Nsfw);
@@ -89,7 +90,7 @@ namespace DHT.Server.Database.Sqlite {
public List<Channel> GetAllChannels() { public List<Channel> GetAllChannels() {
var list = new List<Channel>(); var list = new List<Channel>();
using var cmd = conn.Command("SELECT id, server, name, position, topic, nsfw FROM channels"); using var cmd = conn.Command("SELECT id, server, name, parent_id, position, topic, nsfw FROM channels");
using var reader = cmd.ExecuteReader(); using var reader = cmd.ExecuteReader();
while (reader.Read()) { while (reader.Read()) {
@@ -97,9 +98,10 @@ namespace DHT.Server.Database.Sqlite {
Id = (ulong) reader.GetInt64(0), Id = (ulong) reader.GetInt64(0),
Server = (ulong) reader.GetInt64(1), Server = (ulong) reader.GetInt64(1),
Name = reader.GetString(2), Name = reader.GetString(2),
Position = reader.IsDBNull(3) ? null : reader.GetInt32(3), ParentId = reader.IsDBNull(3) ? null : (ulong) reader.GetInt64(3),
Topic = reader.IsDBNull(4) ? null : reader.GetString(4), Position = reader.IsDBNull(4) ? null : reader.GetInt32(4),
Nsfw = reader.IsDBNull(5) ? null : reader.GetBoolean(5) Topic = reader.IsDBNull(5) ? null : reader.GetString(5),
Nsfw = reader.IsDBNull(6) ? null : reader.GetBoolean(6)
}); });
} }

View File

@@ -27,24 +27,24 @@ namespace DHT.Server.Endpoints {
var requestToken = request.Headers["X-DHT-Token"]; var requestToken = request.Headers["X-DHT-Token"];
if (requestToken.Count != 1 || requestToken[0] != parameters.Token) { if (requestToken.Count != 1 || requestToken[0] != parameters.Token) {
Log.Error("Token: " + (requestToken.Count == 1 ? requestToken[0] : "<missing>")); Log.Error("Token: " + (requestToken.Count == 1 ? requestToken[0] : "<missing>"));
response.StatusCode = (int)HttpStatusCode.Forbidden; response.StatusCode = (int) HttpStatusCode.Forbidden;
return; return;
} }
try { try {
var (statusCode, output) = await Respond(ctx); var (statusCode, output) = await Respond(ctx);
response.StatusCode = (int)statusCode; response.StatusCode = (int) statusCode;
if (output != null) { if (output != null) {
await response.WriteAsJsonAsync(output); await response.WriteAsJsonAsync(output);
} }
} catch (HttpException e) { } catch (HttpException e) {
Log.Error(e); Log.Error(e);
response.StatusCode = (int)e.StatusCode; response.StatusCode = (int) e.StatusCode;
await response.WriteAsync(e.Message); await response.WriteAsync(e.Message);
} catch (Exception e) { } catch (Exception e) {
Log.Error(e); Log.Error(e);
response.StatusCode = (int)HttpStatusCode.InternalServerError; response.StatusCode = (int) HttpStatusCode.InternalServerError;
} }
} }

View File

@@ -32,6 +32,7 @@ namespace DHT.Server.Endpoints {
Id = json.RequireSnowflake("id", path), Id = json.RequireSnowflake("id", path),
Server = serverId, Server = serverId,
Name = json.RequireString("name", path), Name = json.RequireString("name", path),
ParentId = json.HasKey("parent") ? json.RequireSnowflake("parent", path) : null,
Position = json.HasKey("position") ? json.RequireInt("position", path, min: 0) : null, Position = json.HasKey("position") ? json.RequireInt("position", path, min: 0) : null,
Topic = json.HasKey("topic") ? json.RequireString("topic", path) : null, Topic = json.HasKey("topic") ? json.RequireString("topic", path) : null,
Nsfw = json.HasKey("nsfw") ? json.RequireBool("nsfw", path) : null Nsfw = json.HasKey("nsfw") ? json.RequireBool("nsfw", path) : null

View File

@@ -56,7 +56,7 @@ namespace DHT.Server.Endpoints {
Name = ele.RequireString("name", path), Name = ele.RequireString("name", path),
Type = ele.HasKey("type") ? ele.RequireString("type", path) : null, Type = ele.HasKey("type") ? ele.RequireString("type", path) : null,
Url = ele.RequireString("url", path), Url = ele.RequireString("url", path),
Size = (ulong)ele.RequireLong("size", path) Size = (ulong) ele.RequireLong("size", path)
}); });
private static IEnumerable<Embed> ReadEmbeds(JsonElement.ArrayEnumerator array, string path) => array.Select(ele => new Embed { private static IEnumerable<Embed> ReadEmbeds(JsonElement.ArrayEnumerator array, string path) => array.Select(ele => new Embed {

View File

@@ -8,7 +8,7 @@
<Authors>chylex</Authors> <Authors>chylex</Authors>
<Company>DiscordHistoryTracker</Company> <Company>DiscordHistoryTracker</Company>
<Product>DiscordHistoryTrackerServer</Product> <Product>DiscordHistoryTrackerServer</Product>
<Version>31.0.0.0</Version> <Version>32.0.0.0</Version>
<AssemblyVersion>$(Version)</AssemblyVersion> <AssemblyVersion>$(Version)</AssemblyVersion>
<FileVersion>$(Version)</FileVersion> <FileVersion>$(Version)</FileVersion>
<PackageVersion>$(Version)</PackageVersion> <PackageVersion>$(Version)</PackageVersion>

15
app/minify.py Normal file
View File

@@ -0,0 +1,15 @@
# Python 3
import glob
import os
uglifyjs = os.path.abspath("../lib/uglifyjs")
input_dir = os.path.abspath("./Resources/Tracker/scripts")
output_dir = os.path.abspath("./Resources/Tracker/scripts.min")
for file in glob.glob(input_dir + "/*.js"):
name = os.path.basename(file)
print("Minifying {0}...".format(name))
os.system("{0} {1} -o {2}/{3}".format(uglifyjs, file, output_dir, name))
print("Done!")