1
0
mirror of https://github.com/chylex/Discord-History-Tracker.git synced 2025-04-10 17:15:43 +02:00

Add support for tracking threads (app)

This commit is contained in:
chylex 2021-08-15 02:09:41 +02:00
parent 25071d4323
commit 73bf16a21e
Signed by: chylex
GPG Key ID: 4DE42C8F19A80548
10 changed files with 230 additions and 41 deletions

View File

@ -131,7 +131,10 @@
STATE.onTrackingStateChanged(enabled => {
if (enabled) {
if (DISCORD.getSelectedChannel() == null) {
stopTrackingDelayed(() => alert("The selected channel is not visible in the channel list."));
const message = document.querySelector("div[class*='modeSelected-'][class*='typeThread-']") == null
? "Cannot find selected channel. Ensure it is visible in the channel list."
: "Cannot find selected thread. Ensure it is visible in the channel list. If it is, try switching to a different server and back, or restarting Discord.";
stopTrackingDelayed(() => alert(message));
return;
}

View File

@ -1,3 +1,4 @@
// noinspection JSUnresolvedVariable
class DISCORD {
static getMessageOuterElement() {
return DOM.queryReactClass("messagesWrapper");
@ -92,7 +93,6 @@ class DISCORD {
*/
static getSelectedChannel() {
try {
let obj;
let channelListEle = DOM.queryReactClass("privateChannels");
if (channelListEle) {
@ -128,7 +128,7 @@ class DISCORD {
const iconParent = icon && icon.closest("foreignObject");
const iconMask = iconParent && iconParent.getAttribute("mask");
obj = {
return {
"server": { id, name, type: (iconMask && iconMask.includes("#svg-mask-avatar-default")) ? "GROUP" : "DM" },
"channel": { id, name }
};
@ -137,40 +137,71 @@ class DISCORD {
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 channelObj = props.channel || props.children().props.channel;
let channelObj;
if (!channelObj) {
try {
channelObj = props.channel || props.children().props.channel;
} catch (ignored) {
channelObj = null;
}
if (channelObj) {
return {
"server": {
"id": channelObj.guild_id,
"name": document.querySelector("nav header > h1").innerText,
"type": "SERVER"
},
"channel": {
"id": channelObj.id,
"name": channelObj.name,
"extra": {
"position": channelObj.position,
"topic": channelObj.topic,
"nsfw": channelObj.nsfw
}
}
};
}
const parentProps = DOM.getReactProps(channel.parentElement);
const parentPropsChildren = parentProps.children;
if (!Array.isArray(parentPropsChildren) || parentPropsChildren.length < 3) {
return null;
}
// noinspection JSUnresolvedVariable
obj = {
const threadItems = parentPropsChildren[2];
const threadItem = Array.isArray(threadItems) && threadItems.find(item => item.props && item.props.thread && channel.querySelector("div[data-list-item-id='channels___" + item.props.thread.id + "']") !== null);
if (!threadItem) {
return null;
}
const thread = threadItem.props.thread;
return {
"server": {
"id": channelObj.guild_id,
"id": thread.guild_id,
"name": document.querySelector("nav header > h1").innerText,
"type": "SERVER"
},
"channel": {
"id": channelObj.id,
"name": channelObj.name,
"id": thread.id,
"name": thread.name,
"extra": {
"position": channelObj.position,
"topic": channelObj.topic,
"nsfw": channelObj.nsfw
"parent": thread.parent_id,
"nsfw": thread.nsfw
}
}
};
}
return obj.channel.length === 0 ? null : obj;
} catch (e) {
console.error(e);
return null;

View File

@ -158,9 +158,15 @@ const STATE = (function() {
};
if ("extra" in channelInfo) {
channel.position = channelInfo.extra.position;
channel.topic = channelInfo.extra.topic;
channel.nsfw = channelInfo.extra.nsfw;
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, channel });
@ -172,8 +178,8 @@ const STATE = (function() {
* @param {DiscordMessage[]} discordMessageArray
*/
async addDiscordMessages(channelId, discordMessageArray) {
// https://discord.com/developers/docs/resources/channel#message-object-message-reference-structure
discordMessageArray = discordMessageArray.filter(msg => (msg.type === 0 || msg.type === 19) && msg.state === "SENT");
// 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;

View File

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

View File

@ -29,29 +29,132 @@ const STATE = (function() {
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() {
if (!loadedFileMeta) {
return [];
}
const channels = loadedFileMeta.channels;
const channelOrder = generateChannelOrder();
return Object.keys(channels).map(key => ({
"id": key,
"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,
"topic": channels[key].topic || "",
"nsfw": channels[key].nsfw || false,
"position": channels[key].position || -1
})).sort((ac, bc) => {
const as = ac.server;
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 });
return channelOrder[ac.id] - channelOrder[bc.id];
});
};
@ -59,6 +162,26 @@ const STATE = (function() {
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() {
if (!loadedMessages) {
return [];
@ -83,7 +206,7 @@ const STATE = (function() {
const user = getUser(message.u);
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 reply = "r" in message ? getMessageById(message.r) : null;
const replyUser = reply ? getUser(reply.u) : null;
const replyAvatar = replyUser && replyUser.avatar ? { id: getUserId(reply.u), path: replyUser.avatar } : null;
const replyObj = reply ? {
@ -247,13 +370,20 @@ const STATE = (function() {
navigateToMessage(id) {
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);
if (index === -1) {
return 0;
return -1;
}
currentPage = Math.max(1, Math.min(this.getPageCount(), 1 + Math.floor(index / messagesPerPage)));

View File

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

View File

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

View File

@ -5,7 +5,7 @@ using Microsoft.Data.Sqlite;
namespace DHT.Server.Database.Sqlite {
internal class Schema {
internal const int Version = 1;
internal const int Version = 2;
private readonly SqliteConnection conn;
@ -64,6 +64,7 @@ namespace DHT.Server.Database.Sqlite {
id INTEGER PRIMARY KEY NOT NULL,
server INTEGER NOT NULL,
name TEXT NOT NULL,
parent_id INTEGER,
position INTEGER,
topic TEXT,
nsfw INTEGER)");
@ -105,6 +106,10 @@ namespace DHT.Server.Database.Sqlite {
private void UpgradeSchemas(int dbVersion) {
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) {
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;
channelParams.AddAndSet(":id", channel.Id);
channelParams.AddAndSet(":server", channel.Server);
channelParams.AddAndSet(":name", channel.Name);
channelParams.AddAndSet(":parent_id", channel.ParentId);
channelParams.AddAndSet(":position", channel.Position);
channelParams.AddAndSet(":topic", channel.Topic);
channelParams.AddAndSet(":nsfw", channel.Nsfw);
@ -89,7 +90,7 @@ namespace DHT.Server.Database.Sqlite {
public List<Channel> GetAllChannels() {
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();
while (reader.Read()) {
@ -97,9 +98,10 @@ namespace DHT.Server.Database.Sqlite {
Id = (ulong) reader.GetInt64(0),
Server = (ulong) reader.GetInt64(1),
Name = reader.GetString(2),
Position = reader.IsDBNull(3) ? null : reader.GetInt32(3),
Topic = reader.IsDBNull(4) ? null : reader.GetString(4),
Nsfw = reader.IsDBNull(5) ? null : reader.GetBoolean(5)
ParentId = reader.IsDBNull(3) ? null : (ulong) reader.GetInt64(3),
Position = reader.IsDBNull(4) ? null : reader.GetInt32(4),
Topic = reader.IsDBNull(5) ? null : reader.GetString(5),
Nsfw = reader.IsDBNull(6) ? null : reader.GetBoolean(6)
});
}

View File

@ -32,6 +32,7 @@ namespace DHT.Server.Endpoints {
Id = json.RequireSnowflake("id", path),
Server = serverId,
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,
Topic = json.HasKey("topic") ? json.RequireString("topic", path) : null,
Nsfw = json.HasKey("nsfw") ? json.RequireBool("nsfw", path) : null