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:
parent
25071d4323
commit
73bf16a21e
app
Resources
Server
Data
Database
Endpoints
5
app/Resources/Tracker/bootstrap.js
vendored
5
app/Resources/Tracker/bootstrap.js
vendored
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -29,29 +29,132 @@ const STATE = (function() {
|
||||
return loadedFileMeta ? loadedFileMeta.users : [];
|
||||
};
|
||||
|
||||
const getServer = function(index) {
|
||||
return loadedFileMeta.servers[index] || { "name": "<unknown>", "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": "<unknown>", "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)));
|
||||
|
@ -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; }
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user