MessageLoggerEnhanced: Update
This commit is contained in:
parent
aae801559f
commit
bcb36541f6
28 changed files with 591 additions and 212 deletions
|
|
@ -42,6 +42,7 @@
|
|||
"@ffmpeg/ffmpeg": "^0.12.10",
|
||||
"@ffmpeg/util": "^0.12.1",
|
||||
"@intrnl/xxhash64": "^0.1.2",
|
||||
"@streamparser/json": "^0.0.22",
|
||||
"@types/less": "^3.0.8",
|
||||
"@types/stylus": "^0.48.42",
|
||||
"@types/tinycolor2": "^1.4.6",
|
||||
|
|
@ -53,6 +54,7 @@
|
|||
"jsqr": "1.4.0",
|
||||
"monaco-editor": "^0.54.0",
|
||||
"nanoid": "^5.1.6",
|
||||
"native-file-system-adapter": "^3.0.1",
|
||||
"usercss-meta": "^0.12.0",
|
||||
"virtual-merge": "^1.0.1"
|
||||
},
|
||||
|
|
@ -92,7 +94,7 @@
|
|||
"typescript-transform-paths": "^3.5.5",
|
||||
"zip-local": "^0.3.5"
|
||||
},
|
||||
"packageManager": "pnpm@10.26.2",
|
||||
"packageManager": "pnpm@10.29.2",
|
||||
"pnpm": {
|
||||
"patchedDependencies": {
|
||||
"@types/less@3.0.8": "patches/@types__less@3.0.8.patch"
|
||||
|
|
|
|||
45
pnpm-lock.yaml
generated
45
pnpm-lock.yaml
generated
|
|
@ -25,6 +25,9 @@ importers:
|
|||
'@intrnl/xxhash64':
|
||||
specifier: ^0.1.2
|
||||
version: 0.1.2
|
||||
'@streamparser/json':
|
||||
specifier: ^0.0.22
|
||||
version: 0.0.22
|
||||
'@types/less':
|
||||
specifier: ^3.0.8
|
||||
version: 3.0.8(patch_hash=641e6c93bb737bac7fc283416857bd095cd85bcbcba63becb7a8bbcc78f73076)
|
||||
|
|
@ -58,6 +61,9 @@ importers:
|
|||
nanoid:
|
||||
specifier: ^5.1.6
|
||||
version: 5.1.6
|
||||
native-file-system-adapter:
|
||||
specifier: ^3.0.1
|
||||
version: 3.0.1
|
||||
usercss-meta:
|
||||
specifier: ^0.12.0
|
||||
version: 0.12.0
|
||||
|
|
@ -712,6 +718,9 @@ packages:
|
|||
'@rtsao/scc@1.1.0':
|
||||
resolution: {integrity: sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==}
|
||||
|
||||
'@streamparser/json@0.0.22':
|
||||
resolution: {integrity: sha512-b6gTSBjJ8G8SuO3Gbbj+zXbVx8NSs1EbpbMKpzGLWMdkR+98McH9bEjSz3+0mPJf68c5nxa3CrJHp5EQNXM6zQ==}
|
||||
|
||||
'@stylistic/eslint-plugin@5.6.0':
|
||||
resolution: {integrity: sha512-owEc4B8ME+O/xyZOkLVyLqPMsUgJXIM4XzCm5Vt3WvRXpyoOfYxgA+JkEiFqXPCI8+Nc2BzAT+KGAK7QleGs8Q==}
|
||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||
|
|
@ -1445,6 +1454,10 @@ packages:
|
|||
fd-slicer@1.1.0:
|
||||
resolution: {integrity: sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==}
|
||||
|
||||
fetch-blob@3.2.0:
|
||||
resolution: {integrity: sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==}
|
||||
engines: {node: ^12.20 || >= 14.13}
|
||||
|
||||
fflate@0.8.2:
|
||||
resolution: {integrity: sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==}
|
||||
|
||||
|
|
@ -1959,6 +1972,10 @@ packages:
|
|||
engines: {node: ^18 || >=20}
|
||||
hasBin: true
|
||||
|
||||
native-file-system-adapter@3.0.1:
|
||||
resolution: {integrity: sha512-ocuhsYk2SY0906LPc3QIMW+rCV3MdhqGiy7wV5Bf0e8/5TsMjDdyIwhNiVPiKxzTJLDrLT6h8BoV9ERfJscKhw==}
|
||||
engines: {node: '>=14.8.0'}
|
||||
|
||||
natural-compare@1.4.0:
|
||||
resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==}
|
||||
|
||||
|
|
@ -1969,6 +1986,11 @@ packages:
|
|||
no-case@3.0.4:
|
||||
resolution: {integrity: sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==}
|
||||
|
||||
node-domexception@1.0.0:
|
||||
resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==}
|
||||
engines: {node: '>=10.5.0'}
|
||||
deprecated: Use your platform's native DOMException instead
|
||||
|
||||
normalize-path@3.0.0:
|
||||
resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
|
@ -2143,6 +2165,7 @@ packages:
|
|||
engines: {node: '>=0.6.0', teleport: '>=0.2.0'}
|
||||
deprecated: |-
|
||||
You or someone you depend on is using Q, the JavaScript Promise library that gave JavaScript developers strong feelings about promises. They can almost certainly migrate to the native JavaScript promise now. Thank you literally everyone for joining me in this bet against the odds. Be excellent to each other.
|
||||
|
||||
(For a CapTP with native promises, see @endo/eventual-send and @endo/captp)
|
||||
|
||||
qified@0.5.2:
|
||||
|
|
@ -2534,6 +2557,10 @@ packages:
|
|||
vscode-textmate@5.2.0:
|
||||
resolution: {integrity: sha512-Uw5ooOQxRASHgu6C7GVvUxisKXfSgW4oFlO+aa+PAkgmH89O3CXxEEzNRNtHSqtXFTl0nAC1uYj0GMSH27uwtQ==}
|
||||
|
||||
web-streams-polyfill@3.3.3:
|
||||
resolution: {integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==}
|
||||
engines: {node: '>= 8'}
|
||||
|
||||
webdriver-bidi-protocol@0.3.8:
|
||||
resolution: {integrity: sha512-21Yi2GhGntMc671vNBCjiAeEVknXjVRoyu+k+9xOMShu+ZQfpGQwnBqbNz/Sv4GXZ6JmutlPAi2nIJcrymAWuQ==}
|
||||
|
||||
|
|
@ -2968,6 +2995,8 @@ snapshots:
|
|||
|
||||
'@rtsao/scc@1.1.0': {}
|
||||
|
||||
'@streamparser/json@0.0.22': {}
|
||||
|
||||
'@stylistic/eslint-plugin@5.6.0(eslint@9.39.1)':
|
||||
dependencies:
|
||||
'@eslint-community/eslint-utils': 4.9.0(eslint@9.39.1)
|
||||
|
|
@ -3908,6 +3937,12 @@ snapshots:
|
|||
dependencies:
|
||||
pend: 1.2.0
|
||||
|
||||
fetch-blob@3.2.0:
|
||||
dependencies:
|
||||
node-domexception: 1.0.0
|
||||
web-streams-polyfill: 3.3.3
|
||||
optional: true
|
||||
|
||||
fflate@0.8.2: {}
|
||||
|
||||
file-entry-cache@10.1.4:
|
||||
|
|
@ -4421,6 +4456,10 @@ snapshots:
|
|||
|
||||
nanoid@5.1.6: {}
|
||||
|
||||
native-file-system-adapter@3.0.1:
|
||||
optionalDependencies:
|
||||
fetch-blob: 3.2.0
|
||||
|
||||
natural-compare@1.4.0: {}
|
||||
|
||||
netmask@2.0.2: {}
|
||||
|
|
@ -4430,6 +4469,9 @@ snapshots:
|
|||
lower-case: 2.0.2
|
||||
tslib: 2.6.2
|
||||
|
||||
node-domexception@1.0.0:
|
||||
optional: true
|
||||
|
||||
normalize-path@3.0.0: {}
|
||||
|
||||
object-assign@4.1.1: {}
|
||||
|
|
@ -5134,6 +5176,9 @@ snapshots:
|
|||
|
||||
vscode-textmate@5.2.0: {}
|
||||
|
||||
web-streams-polyfill@3.3.3:
|
||||
optional: true
|
||||
|
||||
webdriver-bidi-protocol@0.3.8: {}
|
||||
|
||||
which-boxed-primitive@1.1.1:
|
||||
|
|
|
|||
|
|
@ -16,15 +16,19 @@
|
|||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { Button } from "@components/Button";
|
||||
import { Heading } from "@components/Heading";
|
||||
import { DEFAULT_IMAGE_CACHE_DIR } from "@equicordplugins/messageLoggerEnhanced/utils/constants";
|
||||
import { classNameFactory } from "@utils/css";
|
||||
import { copyWithToast } from "@utils/discord";
|
||||
import { Button, Toasts } from "@webpack/common";
|
||||
import { classes } from "@utils/misc";
|
||||
import { findCssClassesLazy } from "@webpack";
|
||||
import { Toasts } from "@webpack/common";
|
||||
|
||||
import { Native, settings } from "..";
|
||||
import { DEFAULT_IMAGE_CACHE_DIR } from "../utils/constants";
|
||||
|
||||
const cl = classNameFactory("folder-upload");
|
||||
const inputClasses = findCssClassesLazy("input", "inputWrapper", "editable") as Record<string, string>;
|
||||
|
||||
function createDirSelector(settingKey: "logsDir" | "imageCacheDir", successMessage: string) {
|
||||
return function DirSelector({ option }) {
|
||||
|
|
@ -32,7 +36,7 @@ function createDirSelector(settingKey: "logsDir" | "imageCacheDir", successMessa
|
|||
|
||||
return (
|
||||
<section>
|
||||
<Heading>{option.description}</Heading>
|
||||
<Heading tag="h5">{option.description}</Heading>
|
||||
<SelectFolderInput
|
||||
settingsKey={settingKey}
|
||||
successMessage={successMessage}
|
||||
|
|
@ -79,13 +83,13 @@ export function SelectFolderInput({ settingsKey, successMessage }: Props) {
|
|||
}
|
||||
|
||||
return (
|
||||
<div className={cl("-container")}>
|
||||
<div className={classes(cl("-container"), inputClasses.input)}>
|
||||
<div onClick={() => copyWithToast(path)} className={cl("-input")}>
|
||||
{path == null || path === DEFAULT_IMAGE_CACHE_DIR ? "Choose Folder" : getDirName(path)}
|
||||
</div>
|
||||
<Button
|
||||
className={cl("-button")}
|
||||
size={Button.Sizes.SMALL}
|
||||
size="small"
|
||||
onClick={onFolderSelect}
|
||||
>
|
||||
Browse
|
||||
|
|
|
|||
|
|
@ -5,21 +5,23 @@
|
|||
*/
|
||||
|
||||
import { BaseText } from "@components/BaseText";
|
||||
import { Button } from "@components/Button";
|
||||
import { Flex } from "@components/Flex";
|
||||
import { InfoIcon } from "@components/Icons";
|
||||
import { clearMessagesIDB, DBMessageRecord, deleteMessageIDB, deleteMessagesBulkIDB } from "@equicordplugins/messageLoggerEnhanced/db";
|
||||
import { settings } from "@equicordplugins/messageLoggerEnhanced/index";
|
||||
import { LoggedMessage, LoggedMessageJSON } from "@equicordplugins/messageLoggerEnhanced/types";
|
||||
import { messageJsonToMessageClass } from "@equicordplugins/messageLoggerEnhanced/utils";
|
||||
import { importLogs } from "@equicordplugins/messageLoggerEnhanced/utils/settingsUtils";
|
||||
import { Link } from "@components/Link";
|
||||
import { classNameFactory } from "@utils/css";
|
||||
import { copyWithToast, openUserProfile } from "@utils/discord";
|
||||
import { closeAllModals, ModalContent, ModalFooter, ModalHeader, ModalProps, ModalRoot, ModalSize, openModal } from "@utils/modal";
|
||||
import { LazyComponent } from "@utils/react";
|
||||
import { User } from "@vencord/discord-types";
|
||||
import { type User } from "@vencord/discord-types";
|
||||
import { find, findByCode, findByCodeLazy } from "@webpack";
|
||||
import { Alerts, Button, ChannelStore, ContextMenuApi, FluxDispatcher, GuildStore, Menu, NavigationRouter, React, TabBar, TextInput, Tooltip, useMemo, useRef, useState } from "@webpack/common";
|
||||
import { Alerts, ChannelStore, ContextMenuApi, FluxDispatcher, GuildStore, Menu, NavigationRouter, React, TabBar, TextInput, Tooltip, useMemo, useRef, useState } from "@webpack/common";
|
||||
|
||||
import { clearMessagesIDB, DBMessageRecord, deleteMessageIDB, deleteMessagesBulkIDB } from "../db";
|
||||
import { settings } from "../index";
|
||||
import { LoggedMessage, LoggedMessageJSON } from "../types";
|
||||
import { messageJsonToMessageClass } from "../utils";
|
||||
import { importLogs } from "../utils/settingsUtils";
|
||||
import { useMessages } from "./hooks";
|
||||
|
||||
export interface MessagePreviewProps {
|
||||
|
|
@ -89,7 +91,6 @@ export function LogsModal({ modalProps, initalQuery }: Props) {
|
|||
setCurrentTab(e);
|
||||
setNumDisplayedMessages(settings.store.messagesToDisplayAtOnceInLogs);
|
||||
contentRef.current?.firstElementChild?.scrollTo(0, 0);
|
||||
// forceUpdate();
|
||||
}}
|
||||
>
|
||||
<TabBar.Item
|
||||
|
|
@ -138,14 +139,15 @@ export function LogsModal({ modalProps, initalQuery }: Props) {
|
|||
</ModalContent>
|
||||
}
|
||||
</div>
|
||||
<ModalFooter>
|
||||
<ModalFooter className={cl("footer")}>
|
||||
<Button
|
||||
color={Button.Colors.RED}
|
||||
variant="dangerPrimary"
|
||||
onClick={() => Alerts.show({
|
||||
title: "Clear Logs",
|
||||
body: "Are you sure you want to clear all the logs",
|
||||
confirmText: "Clear",
|
||||
confirmColor: Button.Colors.RED,
|
||||
// @ts-expect-error not typed
|
||||
confirmVariant: "critical-primary",
|
||||
cancelText: "Cancel",
|
||||
onConfirm: async () => {
|
||||
await clearMessagesIDB();
|
||||
|
|
@ -158,13 +160,14 @@ export function LogsModal({ modalProps, initalQuery }: Props) {
|
|||
</Button>
|
||||
<Button
|
||||
style={{ marginRight: "16px" }}
|
||||
color={Button.Colors.BRAND}
|
||||
variant="dangerSecondary"
|
||||
disabled={messages?.length === 0}
|
||||
onClick={() => Alerts.show({
|
||||
title: "Clear Logs",
|
||||
body: `Are you sure you want to clear ${messages.length} logs`,
|
||||
confirmText: "Clear",
|
||||
confirmColor: Button.Colors.RED,
|
||||
// @ts-expect-error not typed
|
||||
confirmVariant: "critical-primary",
|
||||
cancelText: "Cancel",
|
||||
onConfirm: async () => {
|
||||
await deleteMessagesBulkIDB(messages.map(e => e.message_id));
|
||||
|
|
@ -174,9 +177,8 @@ export function LogsModal({ modalProps, initalQuery }: Props) {
|
|||
>
|
||||
Clear Visible Logs
|
||||
</Button>
|
||||
<Button
|
||||
style={{ marginRight: "16px" }}
|
||||
color={Button.Colors.PRIMARY}
|
||||
<Link
|
||||
style={{ marginRight: "1rem" }}
|
||||
onClick={() => {
|
||||
setSortNewest(e => {
|
||||
const val = !e;
|
||||
|
|
@ -187,7 +189,7 @@ export function LogsModal({ modalProps, initalQuery }: Props) {
|
|||
}}
|
||||
>
|
||||
Sort {sortNewest ? "Oldest First" : "Newest First"}
|
||||
</Button>
|
||||
</Link>
|
||||
</ModalFooter>
|
||||
</ModalRoot>
|
||||
);
|
||||
|
|
@ -221,7 +223,7 @@ function LogsContent({ visibleMessages, canLoadMore, sortNewest, tab, reset, han
|
|||
canLoadMore &&
|
||||
<Button
|
||||
style={{ marginTop: "1rem", width: "100%" }}
|
||||
size={Button.Sizes.SMALL} onClick={() => handleLoadMore()}
|
||||
size="small" onClick={() => handleLoadMore()}
|
||||
>
|
||||
Load More
|
||||
</Button>
|
||||
|
|
@ -302,8 +304,6 @@ interface LMessageProps {
|
|||
function LMessage({ log, isGroupStart, reset, }: LMessageProps) {
|
||||
const message = useMemo(() => messageJsonToMessageClass(log), [log]);
|
||||
|
||||
// console.log(message);
|
||||
|
||||
if (!message) return null;
|
||||
|
||||
const channel = ChannelStore.getChannel(message?.channel_id);
|
||||
|
|
|
|||
|
|
@ -4,10 +4,10 @@
|
|||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { countMessagesByStatusIDB, countMessagesIDB, DBMessageRecord, DBMessageStatus, getDateStortedMessagesByStatusIDB } from "@equicordplugins/messageLoggerEnhanced/db";
|
||||
import { doesMatch, tokenizeQuery } from "@equicordplugins/messageLoggerEnhanced/utils/parseQuery";
|
||||
import { useEffect, useState } from "@webpack/common";
|
||||
|
||||
import { countMessagesByStatusIDB, countMessagesIDB, DBMessageRecord, DBMessageStatus, getDateStortedMessagesByStatusIDB } from "../db";
|
||||
import { doesMatch, tokenizeQuery } from "../utils/parseQuery";
|
||||
import { LogTabs } from "./LogsModal";
|
||||
|
||||
function useDebouncedValue<T>(value: T, delay: number): T {
|
||||
|
|
|
|||
|
|
@ -110,6 +110,30 @@ export async function getOldestMessagesIDB(limit: number) {
|
|||
return cacheRecords(await db.getAllFromIndex("messages", "by_timestamp", undefined, limit));
|
||||
}
|
||||
|
||||
export async function* iterateAllMessagesIDB(batchSize = 100) {
|
||||
let lastId: string | undefined;
|
||||
while (true) {
|
||||
const batch: DBMessageRecord[] = [];
|
||||
// new transaction for each batch to avoid timeouts during yield
|
||||
const tx = db.transaction("messages");
|
||||
const range = lastId ? IDBKeyRange.lowerBound(lastId, true) : undefined;
|
||||
let cursor = await tx.store.openCursor(range);
|
||||
|
||||
while (cursor && batch.length < batchSize) {
|
||||
batch.push(cursor.value);
|
||||
cursor = await cursor.continue();
|
||||
}
|
||||
|
||||
if (batch.length === 0) break;
|
||||
|
||||
lastId = batch[batch.length - 1].message_id;
|
||||
|
||||
yield await cacheRecords(batch);
|
||||
|
||||
if (batch.length < batchSize) break;
|
||||
}
|
||||
}
|
||||
|
||||
export async function getOlderThanTimestampIDB(timestamp: string) {
|
||||
const tx = db.transaction("messages", "readonly");
|
||||
const { store } = tx;
|
||||
|
|
|
|||
|
|
@ -40,17 +40,20 @@ let oldGetMessage: typeof MessageStore.getMessage;
|
|||
|
||||
const handledMessageIds = new Set();
|
||||
async function messageDeleteHandler(payload: MessageDeletePayload & { isBulk: boolean; }) {
|
||||
if (payload.mlDeleted) return;
|
||||
if (payload.mlDeleted) {
|
||||
if (settings.store.permanentlyRemoveLogByDefault)
|
||||
await idb.deleteMessageIDB(payload.id);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (handledMessageIds.has(payload.id)) {
|
||||
// Flogger.warn("skipping duplicate message", payload.id);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
handledMessageIds.add(payload.id);
|
||||
|
||||
// @ts-ignore
|
||||
let message: LoggedMessage | LoggedMessageJSON | null =
|
||||
oldGetMessage?.(payload.channelId, payload.id);
|
||||
if (message == null) {
|
||||
|
|
@ -75,7 +78,6 @@ async function messageDeleteHandler(payload: MessageDeletePayload & { isBulk: bo
|
|||
webhookId: message?.webhookId
|
||||
})
|
||||
) {
|
||||
// Flogger.log("IGNORING", message, payload);
|
||||
return FluxDispatcher.dispatch({
|
||||
type: "MESSAGE_DELETE",
|
||||
channelId: payload.channelId,
|
||||
|
|
@ -85,7 +87,6 @@ async function messageDeleteHandler(payload: MessageDeletePayload & { isBulk: bo
|
|||
}
|
||||
|
||||
if (message == null || message.channel_id == null || !message.deleted) return;
|
||||
// Flogger.log("ADDING MESSAGE (DELETED)", message);
|
||||
if (payload.isBulk)
|
||||
return message;
|
||||
|
||||
|
|
@ -138,7 +139,7 @@ async function messageUpdateHandler(payload: MessageUpdatePayload) {
|
|||
message.editHistory = [];
|
||||
cacheThing.commit(cache);
|
||||
}
|
||||
return;// Flogger.log("this message has been ignored", payload);
|
||||
return;
|
||||
}
|
||||
|
||||
let message = oldGetMessage?.(payload.message.channel_id, payload.message.id) as LoggedMessage | LoggedMessageJSON | null;
|
||||
|
|
@ -158,14 +159,12 @@ async function messageUpdateHandler(payload: MessageUpdatePayload) {
|
|||
]
|
||||
};
|
||||
|
||||
// @ts-ignore
|
||||
cacheSentMessages.set(`${payload.message.channel_id},${payload.message.id}`, message);
|
||||
}
|
||||
}
|
||||
|
||||
if (message == null || message.channel_id == null || message.editHistory == null || message.editHistory.length === 0) return;
|
||||
|
||||
// Flogger.log("ADDING MESSAGE (EDITED)", message, payload);
|
||||
const currentChannelId = SelectedChannelStore.getChannelId();
|
||||
await addMessage(message, idb.DBMessageStatus.EDITED, currentChannelId);
|
||||
}
|
||||
|
|
@ -184,7 +183,6 @@ function messageCreateHandler(payload: MessageCreatePayload) {
|
|||
}
|
||||
|
||||
cacheSentMessages.set(`${payload.message.channel_id},${payload.message.id}`, cleanUpCachedMessage(payload.message));
|
||||
// Flogger.log(`cached\nkey:${payload.message.channel_id},${payload.message.id}\nvalue:`, payload.message);
|
||||
}
|
||||
|
||||
async function processMessageFetch(response: FetchMessagesResponse) {
|
||||
|
|
@ -195,13 +193,14 @@ async function processMessageFetch(response: FetchMessagesResponse) {
|
|||
}
|
||||
|
||||
const firstMessage = response.body[response.body.length - 1];
|
||||
// console.time("fetching messages from idb");
|
||||
const messages = await idb.getMessagesByChannelAndAfterTimestampIDB(firstMessage.channel_id, firstMessage.timestamp);
|
||||
// console.timeEnd("fetching messages from idb");
|
||||
|
||||
if (!messages.length) return;
|
||||
|
||||
const deletedMessages = messages.filter(m => m.status === idb.DBMessageStatus.DELETED);
|
||||
const deletedMessages = messages.filter(m =>
|
||||
m.status === idb.DBMessageStatus.DELETED ||
|
||||
m.status === idb.DBMessageStatus.GHOST_PINGED
|
||||
);
|
||||
|
||||
for (const recivedMessage of response.body) {
|
||||
const record = messages.find(m => m.message_id === recivedMessage.id);
|
||||
|
|
@ -223,7 +222,7 @@ async function processMessageFetch(response: FetchMessagesResponse) {
|
|||
|
||||
for (let j = 0, len2 = message.mentions.length; j < len2; j++) {
|
||||
const user = message.mentions[j];
|
||||
const cachedUser = fetchUser(user);
|
||||
const cachedUser = fetchUser((user as any).id || user);
|
||||
if (cachedUser) (message.mentions[j] as any) = cleanupUserObject(cachedUser);
|
||||
}
|
||||
|
||||
|
|
@ -267,14 +266,14 @@ export default definePlugin({
|
|||
replace: "deleted:$self.getDeleted(...arguments), editHistory:$self.getEdited(...arguments),"
|
||||
}
|
||||
},
|
||||
// MessagePreview component in LogsModal
|
||||
{
|
||||
find: "childrenMessageContent:null",
|
||||
find: "=!0,disableInteraction:",
|
||||
replacement: {
|
||||
match: /(\i\]:!\i.{1,50},)childrenHeader:/,
|
||||
replace: "$1childrenAccessories:arguments[0].childrenAccessories || null,childrenHeader:"
|
||||
match: /childrenHeader:.{0,100}childrenMessageContent/,
|
||||
replace: "childrenAccessories:arguments[0].childrenAccessories || null,$&"
|
||||
}
|
||||
},
|
||||
// https://regex101.com/r/S3IVGm/1
|
||||
// fix vidoes failing because there are no thumbnails
|
||||
{
|
||||
find: ".handleImageLoad)",
|
||||
|
|
@ -370,9 +369,20 @@ export default definePlugin({
|
|||
// we have to do this because the original message logger fetches the message from the store now
|
||||
MessageStore.getMessage = (channelId: string, messageId: string) => {
|
||||
const MLMessage = idb.cachedMessages.get(messageId);
|
||||
if (MLMessage) return messageJsonToMessageClass({ message: MLMessage });
|
||||
if (!MLMessage)
|
||||
return this.oldGetMessage(channelId, messageId);
|
||||
|
||||
return this.oldGetMessage(channelId, messageId);
|
||||
if (MLMessage.deleted)
|
||||
return messageJsonToMessageClass({ message: MLMessage });
|
||||
|
||||
// update the edited message with the latest data
|
||||
const latestMessage = this.oldGetMessage(channelId, messageId);
|
||||
return messageJsonToMessageClass({
|
||||
message: {
|
||||
...MLMessage,
|
||||
...(latestMessage ?? {}),
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
Native.init();
|
||||
|
|
|
|||
57
src/equicordplugins/messageLoggerEnhanced/native/export.ts
Normal file
57
src/equicordplugins/messageLoggerEnhanced/native/export.ts
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
/*
|
||||
* Vencord, a Discord client mod
|
||||
* Copyright (c) 2025 Vendicated and contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { randomUUID } from "node:crypto";
|
||||
import { createWriteStream, WriteStream } from "node:fs";
|
||||
|
||||
import { dialog, IpcMainInvokeEvent } from "electron";
|
||||
|
||||
const activeStreams = new Map<string, WriteStream>();
|
||||
|
||||
export async function startNativeLogExport(_event: IpcMainInvokeEvent, filename: string) {
|
||||
const { filePath, canceled } = await dialog.showSaveDialog({
|
||||
defaultPath: filename ?? "message-logger-logs-idb.json",
|
||||
filters: [{ name: "JSON", extensions: ["json"] }]
|
||||
});
|
||||
|
||||
if (canceled || !filePath)
|
||||
throw new Error("No file path selected");
|
||||
|
||||
const stream = createWriteStream(filePath, { flags: "w", encoding: "utf-8" });
|
||||
|
||||
const streamId = randomUUID();
|
||||
activeStreams.set(streamId, stream);
|
||||
|
||||
stream.on("error", err => {
|
||||
console.error("Stream error", err);
|
||||
activeStreams.delete(streamId);
|
||||
});
|
||||
|
||||
return streamId;
|
||||
}
|
||||
|
||||
export async function writeNativeLogChunk(_event: IpcMainInvokeEvent, streamId: string, chunk: string) {
|
||||
const stream = activeStreams.get(streamId);
|
||||
if (!stream) throw new Error("Stream not found or closed");
|
||||
|
||||
// if returns false, buffer is full; wait for drain to prevent RAM spike
|
||||
const canContinue = stream.write(chunk);
|
||||
if (!canContinue) {
|
||||
await new Promise<void>(resolve => stream.once("drain", resolve));
|
||||
}
|
||||
}
|
||||
|
||||
export async function finishNativeLogExport(_event: IpcMainInvokeEvent, streamId: string) {
|
||||
const stream = activeStreams.get(streamId);
|
||||
if (!stream) return;
|
||||
|
||||
return new Promise<void>(resolve => {
|
||||
stream.end(() => {
|
||||
activeStreams.delete(streamId);
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
54
src/equicordplugins/messageLoggerEnhanced/native/import.ts
Normal file
54
src/equicordplugins/messageLoggerEnhanced/native/import.ts
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
/*
|
||||
* Vencord, a Discord client mod
|
||||
* Copyright (c) 2025 Vendicated and contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { randomUUID } from "node:crypto";
|
||||
import { FileHandle, open } from "node:fs/promises";
|
||||
|
||||
import { dialog, IpcMainInvokeEvent } from "electron";
|
||||
|
||||
const activeFiles = new Map<string, FileHandle>();
|
||||
|
||||
export async function startNativeLogImport(_event: IpcMainInvokeEvent, defaultPath?: string) {
|
||||
const res = await dialog.showOpenDialog({
|
||||
title: "Import Logs",
|
||||
filters: [{ name: "Logs", extensions: ["json"] }],
|
||||
properties: ["openFile"],
|
||||
defaultPath
|
||||
});
|
||||
const [path] = res.filePaths;
|
||||
|
||||
if (!path) throw Error("No file selected");
|
||||
|
||||
const fileHandle = await open(path, "r");
|
||||
const fileId = randomUUID();
|
||||
activeFiles.set(fileId, fileHandle);
|
||||
|
||||
return fileId;
|
||||
}
|
||||
|
||||
export async function readNativeLogChunk(_event: IpcMainInvokeEvent, fileId: string, size: number = 64 * 1024): Promise<string | null> {
|
||||
const fileHandle = activeFiles.get(fileId);
|
||||
if (!fileHandle) return null;
|
||||
|
||||
const buffer = Buffer.alloc(size);
|
||||
const { bytesRead } = await fileHandle.read(buffer, 0, size);
|
||||
|
||||
if (bytesRead === 0) {
|
||||
await fileHandle.close();
|
||||
activeFiles.delete(fileId);
|
||||
return null;
|
||||
}
|
||||
|
||||
return buffer.toString("utf-8", 0, bytesRead);
|
||||
}
|
||||
|
||||
export async function closeNativeLogImport(_event: IpcMainInvokeEvent, fileId: string) {
|
||||
const fileHandle = activeFiles.get(fileId);
|
||||
if (fileHandle) {
|
||||
await fileHandle.close();
|
||||
activeFiles.delete(fileId);
|
||||
}
|
||||
}
|
||||
|
|
@ -7,12 +7,15 @@
|
|||
import { readdir, readFile, unlink, writeFile } from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
|
||||
import { LoggedAttachment } from "@equicordplugins/messageLoggerEnhanced/types";
|
||||
import { LOGS_DATA_FILENAME } from "@equicordplugins/messageLoggerEnhanced/utils/constants";
|
||||
import { DATA_DIR } from "@main/utils/constants";
|
||||
import { dialog, IpcMainInvokeEvent, shell } from "electron";
|
||||
|
||||
import { getSettings, saveSettings } from "./settings";
|
||||
export * from "./export";
|
||||
export * from "./import";
|
||||
|
||||
import { LoggedAttachment } from "../types";
|
||||
import { LOGS_DATA_FILENAME } from "../utils/constants";
|
||||
import { ensureDirectoryExists, getAttachmentIdFromFilename, sleep } from "./utils";
|
||||
|
||||
export { getSettings };
|
||||
|
|
|
|||
|
|
@ -25,8 +25,4 @@ export function getAttachmentIdFromFilename(filename: string) {
|
|||
return path.parse(filename).name;
|
||||
}
|
||||
|
||||
export function getAttachmentExtFromFilename(filename: string) {
|
||||
return path.parse(filename).ext;
|
||||
}
|
||||
|
||||
export const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
|
||||
|
|
|
|||
|
|
@ -5,9 +5,10 @@
|
|||
*/
|
||||
|
||||
import { definePluginSettings } from "@api/Settings";
|
||||
import { Button } from "@components/Button";
|
||||
import ErrorBoundary from "@components/ErrorBoundary";
|
||||
import { OptionType } from "@utils/types";
|
||||
import { Alerts, Button } from "@webpack/common";
|
||||
import { Alerts, useState } from "@webpack/common";
|
||||
import { Settings } from "Vencord";
|
||||
|
||||
import { Native } from ".";
|
||||
|
|
@ -18,6 +19,46 @@ import { blockedExts } from "./list";
|
|||
import { DEFAULT_IMAGE_CACHE_DIR } from "./utils/constants";
|
||||
import { exportLogs, importLogs } from "./utils/settingsUtils";
|
||||
|
||||
function ImportLogsButton() {
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
return (
|
||||
<Button
|
||||
disabled={loading}
|
||||
onClick={async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
await importLogs();
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{loading ? "Importing..." : "Import Logs"}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
function ExportLogsButton() {
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
return (
|
||||
<Button
|
||||
disabled={loading}
|
||||
onClick={async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
await exportLogs();
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{loading ? "Exporting..." : "Export Logs"}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
export const settings = definePluginSettings({
|
||||
saveMessages: {
|
||||
default: true,
|
||||
|
|
@ -98,6 +139,12 @@ export const settings = definePluginSettings({
|
|||
description: "Always log current selected channel. Blacklisted channels/users will still be ignored.",
|
||||
},
|
||||
|
||||
permanentlyRemoveLogByDefault: {
|
||||
default: false,
|
||||
type: OptionType.BOOLEAN,
|
||||
description: "Vencord's base MessageLogger remove log button wiil delete logs permanently",
|
||||
},
|
||||
|
||||
hideMessageFromMessageLoggers: {
|
||||
default: false,
|
||||
type: OptionType.BOOLEAN,
|
||||
|
|
@ -204,19 +251,13 @@ export const settings = definePluginSettings({
|
|||
importLogs: {
|
||||
type: OptionType.COMPONENT,
|
||||
description: "Import Logs From File",
|
||||
component: () =>
|
||||
<Button onClick={importLogs}>
|
||||
Import Logs
|
||||
</Button>
|
||||
component: ImportLogsButton
|
||||
},
|
||||
|
||||
exportLogs: {
|
||||
type: OptionType.COMPONENT,
|
||||
description: "Export Logs From IndexedDB",
|
||||
component: () =>
|
||||
<Button onClick={exportLogs}>
|
||||
Export Logs
|
||||
</Button>
|
||||
component: ExportLogsButton
|
||||
},
|
||||
|
||||
openLogs: {
|
||||
|
|
@ -248,11 +289,12 @@ export const settings = definePluginSettings({
|
|||
description: "Clear Logs",
|
||||
component: () =>
|
||||
<Button
|
||||
color={Button.Colors.RED}
|
||||
variant="dangerPrimary"
|
||||
onClick={() => Alerts.show({
|
||||
title: "Clear Logs",
|
||||
body: "Are you sure you want to clear all logs?",
|
||||
confirmColor: Button.Colors.RED,
|
||||
// @ts-expect-error not typed
|
||||
confirmVariant: "critical-primary",
|
||||
confirmText: "Clear",
|
||||
cancelText: "Cancel",
|
||||
onConfirm: () => {
|
||||
|
|
|
|||
|
|
@ -25,6 +25,10 @@
|
|||
/* width: 100%; */
|
||||
}
|
||||
|
||||
.msg-logger-modal-footer {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.msg-logger-modal-content-container {
|
||||
/* max-height: 80vh; */
|
||||
overflow: hidden;
|
||||
|
|
@ -58,17 +62,9 @@
|
|||
justify-content: space-around;
|
||||
}
|
||||
|
||||
.msg-logger-modal-from {
|
||||
font-size: 14px;
|
||||
margin-left: 12px;
|
||||
}
|
||||
|
||||
.msg-logger-modal-from-deleted {
|
||||
color: var(--status-danger);
|
||||
}
|
||||
|
||||
.msg-logger-modal-from-edited {
|
||||
color: var(--white-600);
|
||||
.msg-logger-modal-danger-btn {
|
||||
color: var(--white-500);
|
||||
background-color: var(--button-danger-background);
|
||||
}
|
||||
|
||||
.vc-log-toolbox-btn svg {
|
||||
|
|
@ -83,6 +79,7 @@
|
|||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 0;
|
||||
background-color: var(--background-secondary-alt);
|
||||
}
|
||||
|
||||
|
|
@ -95,4 +92,8 @@
|
|||
.folder-upload-button {
|
||||
margin: 6px;
|
||||
padding: 4px 8px;
|
||||
}
|
||||
|
||||
.vc-updater-modal-content {
|
||||
padding: 1rem 2rem;
|
||||
}
|
||||
|
|
@ -16,7 +16,7 @@
|
|||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { Message, MessageAttachment, MessageJSON, User } from "@vencord/discord-types";
|
||||
import { Message, MessageAttachment, MessageJSON } from "@vencord/discord-types";
|
||||
|
||||
export interface LoggedAttachment extends MessageAttachment {
|
||||
fileExtension?: string | null;
|
||||
|
|
@ -35,14 +35,6 @@ export interface LoggedMessageJSON extends Omit<LoggedMessage, "timestamp"> {
|
|||
ghostPinged?: boolean;
|
||||
timestamp: string;
|
||||
ourCache?: boolean;
|
||||
channel_id: string;
|
||||
id: string;
|
||||
author: User;
|
||||
flags: number;
|
||||
bot: boolean;
|
||||
mentions: string[];
|
||||
content: string;
|
||||
webhookId: string | undefined;
|
||||
referenced_message: RefrencedMessage;
|
||||
message_reference: RefrencedMessage;
|
||||
}
|
||||
|
|
@ -55,14 +47,6 @@ export interface LoggedMessage extends Message {
|
|||
timestamp: string;
|
||||
content: string;
|
||||
}[];
|
||||
channel_id: string;
|
||||
id: string;
|
||||
author: User;
|
||||
flags: number;
|
||||
bot: boolean;
|
||||
mentions: string[];
|
||||
content: string;
|
||||
webhookId: string | undefined;
|
||||
}
|
||||
|
||||
export interface MessageDeletePayload {
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@
|
|||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { settings } from "@equicordplugins/messageLoggerEnhanced/index";
|
||||
import { settings } from "../index";
|
||||
|
||||
export class LimitedMap<K, V> {
|
||||
public map: Map<K, V> = new Map();
|
||||
|
|
@ -24,10 +24,8 @@ export class LimitedMap<K, V> {
|
|||
|
||||
set(key: K, value: V) {
|
||||
if (settings.store.cacheLimit > 0 && this.map.size >= settings.store.cacheLimit) {
|
||||
const firstKey = this.map.keys().next().value;
|
||||
if (firstKey !== undefined) {
|
||||
this.map.delete(firstKey);
|
||||
}
|
||||
// delete the first entry
|
||||
this.map.delete(this.map.keys().next().value!);
|
||||
}
|
||||
this.map.set(key, value);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,10 +16,10 @@
|
|||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { LoggedMessageJSON, RefrencedMessage } from "@equicordplugins/messageLoggerEnhanced/types";
|
||||
import { User } from "@vencord/discord-types";
|
||||
import { MessageStore } from "@webpack/common";
|
||||
|
||||
import { LoggedMessageJSON, RefrencedMessage } from "../types";
|
||||
import { getGuildIdByChannel, isGhostPinged } from "./index";
|
||||
|
||||
export function cleanupMessage(message: any, removeDetails: boolean = true): LoggedMessageJSON {
|
||||
|
|
@ -111,6 +111,6 @@ export function cleanupUserObject(user: User) {
|
|||
avatar: user.avatar,
|
||||
id: user.id,
|
||||
bot: user.bot,
|
||||
public_flags: typeof user.publicFlags !== "undefined" ? user.publicFlags : user.publicFlags
|
||||
public_flags: typeof user.publicFlags !== "undefined" ? user.publicFlags : (user as any).public_flags
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,36 +5,44 @@
|
|||
*/
|
||||
|
||||
import { addContextMenuPatch, NavContextMenuPatchCallback, removeContextMenuPatch } from "@api/ContextMenu";
|
||||
import { openLogModal } from "@equicordplugins/messageLoggerEnhanced/components/LogsModal";
|
||||
import { deleteMessageIDB } from "@equicordplugins/messageLoggerEnhanced/db";
|
||||
import { settings } from "@equicordplugins/messageLoggerEnhanced/index";
|
||||
import { findStoreLazy } from "@webpack";
|
||||
import { FluxDispatcher, Menu, MessageActions, React, Toasts, UserStore } from "@webpack/common";
|
||||
|
||||
import { openLogModal } from "../components/LogsModal";
|
||||
import { deleteMessageIDB } from "../db";
|
||||
import { settings } from "../index";
|
||||
import { addToXAndRemoveFromOpposite, ListType, removeFromX } from ".";
|
||||
|
||||
const SortedGuildStore = findStoreLazy("SortedGuildStore");
|
||||
|
||||
const idFunctions = {
|
||||
Folder: props =>
|
||||
props?.folderId &&
|
||||
SortedGuildStore?.getGuildFolders?.().find(f => f?.folderId === props.folderId)?.guildIds,
|
||||
Server: props => props?.guild?.id,
|
||||
User: props => props?.message?.author?.id || props?.user?.id,
|
||||
Channel: props => props.message?.channel_id || props.channel?.id
|
||||
} as const;
|
||||
} as Record<string, (props: any) => string | string[] | null | undefined>;
|
||||
|
||||
type idKeys = keyof typeof idFunctions;
|
||||
|
||||
function renderListOption(listType: ListType, IdType: idKeys, props: any) {
|
||||
const id = idFunctions[IdType](props);
|
||||
if (!id) return null;
|
||||
const rawId = idFunctions[IdType](props);
|
||||
if (!rawId) return null;
|
||||
|
||||
const isBlocked = settings.store[listType].includes(id);
|
||||
const ids = Array.isArray(rawId) ? rawId : [rawId];
|
||||
|
||||
const isBlocked = ids.every(id => settings.store[listType].includes(id));
|
||||
const oppositeListType = listType === "blacklistedIds" ? "whitelistedIds" : "blacklistedIds";
|
||||
const isOppositeBlocked = settings.store[oppositeListType].includes(id);
|
||||
const isOppositeBlocked = ids.some(id => settings.store[oppositeListType].includes(id));
|
||||
const list = listType === "blacklistedIds" ? "Blacklist" : "Whitelist";
|
||||
|
||||
const addToList = () => addToXAndRemoveFromOpposite(listType, id);
|
||||
const removeFromList = () => removeFromX(listType, id);
|
||||
const addToList = () => ids.forEach(id => addToXAndRemoveFromOpposite(listType, id));
|
||||
const removeFromList = () => ids.forEach(id => removeFromX(listType, id));
|
||||
|
||||
return (
|
||||
<Menu.MenuItem
|
||||
id={`${listType}-${IdType}-${id}`}
|
||||
id={`${listType}-${IdType}-${ids[0]}`}
|
||||
label={
|
||||
isOppositeBlocked
|
||||
? `Move ${IdType} to ${list}`
|
||||
|
|
@ -47,7 +55,8 @@ function renderListOption(listType: ListType, IdType: idKeys, props: any) {
|
|||
|
||||
function renderOpenLogs(idType: idKeys, props: any) {
|
||||
const id = idFunctions[idType](props);
|
||||
if (!id) return null;
|
||||
// TODO: rewrite logs modal to accept arrays
|
||||
if (!id || Array.isArray(id)) return null;
|
||||
|
||||
return (
|
||||
<Menu.MenuItem
|
||||
|
|
@ -62,37 +71,8 @@ export const contextMenuPath: NavContextMenuPatchCallback = (children, props) =>
|
|||
if (!props) return;
|
||||
|
||||
if (!children.some(child => child?.props?.id === "message-logger")) {
|
||||
if (props.navId === "message" && (props.message?.deleted || props.message?.editHistory?.length > 0)) {
|
||||
children.push(
|
||||
<Menu.MenuItem
|
||||
id="remove-message"
|
||||
label={props.message?.deleted ? "Remove Message (Permanent)" : "Remove Message History (Permanent)"}
|
||||
color="danger"
|
||||
action={() =>
|
||||
deleteMessageIDB(props.message.id)
|
||||
.then(() => {
|
||||
if (props.message.deleted) {
|
||||
FluxDispatcher.dispatch({
|
||||
type: "MESSAGE_DELETE",
|
||||
channelId: props.message.channel_id,
|
||||
id: props.message.id,
|
||||
mlDeleted: true
|
||||
});
|
||||
} else {
|
||||
props.message.editHistory = [];
|
||||
}
|
||||
}).catch(() => Toasts.show({
|
||||
type: Toasts.Type.FAILURE,
|
||||
message: "Failed to remove message",
|
||||
id: Toasts.genId()
|
||||
}))
|
||||
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
children.push(
|
||||
<Menu.MenuSeparator />,
|
||||
<Menu.MenuItem
|
||||
id="message-logger"
|
||||
label="Message Logger"
|
||||
|
|
@ -114,6 +94,41 @@ export const contextMenuPath: NavContextMenuPatchCallback = (children, props) =>
|
|||
{renderListOption("whitelistedIds", IdType as idKeys, props)}
|
||||
</React.Fragment>
|
||||
))}
|
||||
|
||||
{
|
||||
props.navId === "message"
|
||||
&& (props.message?.deleted || props.message?.editHistory?.length > 0)
|
||||
&& (
|
||||
<>
|
||||
<Menu.MenuSeparator />
|
||||
<Menu.MenuItem
|
||||
id="remove-message"
|
||||
label={props.message?.deleted ? "Remove Message (Permanent)" : "Remove Message History (Permanent)"}
|
||||
color="danger"
|
||||
action={() =>
|
||||
deleteMessageIDB(props.message.id)
|
||||
.then(() => {
|
||||
if (props.message.deleted) {
|
||||
FluxDispatcher.dispatch({
|
||||
type: "MESSAGE_DELETE",
|
||||
channelId: props.message.channel_id,
|
||||
id: props.message.id,
|
||||
mlDeleted: true
|
||||
});
|
||||
} else {
|
||||
props.message.editHistory = [];
|
||||
}
|
||||
}).catch(() => Toasts.show({
|
||||
type: Toasts.Type.FAILURE,
|
||||
message: "Failed to remove message",
|
||||
id: Toasts.genId()
|
||||
}))
|
||||
}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
{
|
||||
settings.store.hideMessageFromMessageLoggers
|
||||
&& props.navId === "message"
|
||||
|
|
@ -136,7 +151,6 @@ export const contextMenuPath: NavContextMenuPatchCallback = (children, props) =>
|
|||
"validNonShortcutEmojis": []
|
||||
}, { nonce: props.message.id });
|
||||
}}
|
||||
|
||||
/>
|
||||
</>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -17,11 +17,11 @@
|
|||
*/
|
||||
|
||||
import { Settings } from "@api/Settings";
|
||||
import { settings } from "@equicordplugins/messageLoggerEnhanced/index";
|
||||
import { LoggedMessageJSON } from "@equicordplugins/messageLoggerEnhanced/types";
|
||||
import { findStoreLazy } from "@webpack";
|
||||
import { ChannelStore, SelectedChannelStore, UserStore } from "@webpack/common";
|
||||
|
||||
import { settings } from "../index";
|
||||
import { LoggedMessageJSON } from "../types";
|
||||
import { findLastIndex, getGuildIdByChannel } from "./misc";
|
||||
|
||||
export * from "./cleanUp";
|
||||
|
|
@ -168,9 +168,11 @@ export function addToXAndRemoveFromOpposite(list: ListType, id: string) {
|
|||
|
||||
export function addToX(list: ListType, id: string) {
|
||||
const items = settings.store[list] ? settings.store[list].split(",") : [];
|
||||
items.push(id);
|
||||
|
||||
settings.store[list] = items.join(",");
|
||||
if (!items.includes(id)) {
|
||||
items.push(id);
|
||||
settings.store[list] = items.join(",");
|
||||
}
|
||||
}
|
||||
|
||||
export function removeFromX(list: ListType, id: string) {
|
||||
|
|
@ -178,6 +180,6 @@ export function removeFromX(list: ListType, id: string) {
|
|||
const index = items.indexOf(id);
|
||||
if (index !== -1) {
|
||||
items.splice(index, 1);
|
||||
settings.store[list] = items.join(",");
|
||||
}
|
||||
settings.store[list] = items.join(",");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,12 +16,12 @@
|
|||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { DBMessageStatus } from "@equicordplugins/messageLoggerEnhanced/db";
|
||||
import { LoggedMessageJSON } from "@equicordplugins/messageLoggerEnhanced/types";
|
||||
import { PluginNative } from "@utils/types";
|
||||
import { findByCodeLazy, findLazy } from "@webpack";
|
||||
import { ChannelStore, moment, UserStore } from "@webpack/common";
|
||||
|
||||
import { DBMessageStatus } from "../db";
|
||||
import { LoggedMessageJSON } from "../types";
|
||||
import { DEFAULT_IMAGE_CACHE_DIR } from "./constants";
|
||||
import { DISCORD_EPOCH } from "./index";
|
||||
import { memoize } from "./memoize";
|
||||
|
|
@ -156,6 +156,12 @@ export function getNative(): PluginNative<typeof import("../native")> {
|
|||
writeImageNative: async () => { },
|
||||
chooseFile: async () => "",
|
||||
downloadAttachment: async () => ({ error: "web", path: null }),
|
||||
startNativeLogExport: async () => "" as any,
|
||||
finishNativeLogExport: async () => { },
|
||||
writeNativeLogChunk: async () => { },
|
||||
startNativeLogImport: async () => "" as any,
|
||||
readNativeLogChunk: async () => null,
|
||||
closeNativeLogImport: async () => { }
|
||||
} satisfies PluginNative<typeof import("../native")>;
|
||||
|
||||
return Native;
|
||||
|
|
|
|||
|
|
@ -16,9 +16,9 @@
|
|||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { LoggedMessageJSON } from "@equicordplugins/messageLoggerEnhanced/types";
|
||||
import { ChannelStore, GuildStore } from "@webpack/common";
|
||||
|
||||
import { LoggedMessageJSON } from "../types";
|
||||
import { getGuildIdByChannel } from "./index";
|
||||
import { memoize } from "./memoize";
|
||||
|
||||
|
|
|
|||
|
|
@ -23,11 +23,12 @@ import {
|
|||
keys,
|
||||
set,
|
||||
} from "@api/DataStore";
|
||||
import { Flogger, Native } from "@equicordplugins/messageLoggerEnhanced";
|
||||
import { LoggedAttachment } from "@equicordplugins/messageLoggerEnhanced/types";
|
||||
import { DEFAULT_IMAGE_CACHE_DIR } from "@equicordplugins/messageLoggerEnhanced/utils/constants";
|
||||
import { sleep } from "@utils/misc";
|
||||
|
||||
import { Flogger, Native } from "../..";
|
||||
import { LoggedAttachment } from "../../types";
|
||||
import { DEFAULT_IMAGE_CACHE_DIR } from "../constants";
|
||||
|
||||
const ImageStore = createStore("MessageLoggerImageData", "MessageLoggerImageStore");
|
||||
|
||||
interface IDBSavedImage { attachmentId: string, path: string; }
|
||||
|
|
|
|||
|
|
@ -16,11 +16,11 @@
|
|||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { Flogger, settings } from "@equicordplugins/messageLoggerEnhanced";
|
||||
import { LoggedAttachment, LoggedMessage, LoggedMessageJSON } from "@equicordplugins/messageLoggerEnhanced/types";
|
||||
import { memoize } from "@equicordplugins/messageLoggerEnhanced/utils/memoize";
|
||||
import { MessageAttachment } from "@vencord/discord-types";
|
||||
|
||||
import { Flogger, settings } from "../..";
|
||||
import { LoggedAttachment, LoggedMessage, LoggedMessageJSON } from "../../types";
|
||||
import { memoize } from "../memoize";
|
||||
import { deleteImage, downloadAttachment, getImage, } from "./ImageManager";
|
||||
|
||||
export function getFileExtension(str: string) {
|
||||
|
|
|
|||
|
|
@ -16,59 +16,51 @@
|
|||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { addMessagesBulkIDB, DBMessageRecord, getAllMessagesIDB } from "@equicordplugins/messageLoggerEnhanced/db";
|
||||
import { LoggedMessage, LoggedMessageJSON } from "@equicordplugins/messageLoggerEnhanced/types";
|
||||
import JSONParser from "@streamparser/json/jsonparser.js";
|
||||
import { chooseFile as chooseFileWeb } from "@utils/web";
|
||||
import { Toasts } from "@webpack/common";
|
||||
import { showSaveFilePicker } from "native-file-system-adapter";
|
||||
|
||||
import { Native } from "..";
|
||||
|
||||
async function getLogContents(): Promise<string> {
|
||||
if (IS_WEB) {
|
||||
const file = await chooseFileWeb(".json");
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!file) return reject("No file selected");
|
||||
|
||||
const reader = new FileReader();
|
||||
reader.onload = () => resolve(reader.result as string);
|
||||
reader.onerror = reject;
|
||||
reader.readAsText(file);
|
||||
});
|
||||
}
|
||||
|
||||
const settings = await Native.getSettings();
|
||||
return Native.chooseFile("Logs", [{ extensions: ["json"], name: "logs" }], settings.logsDir);
|
||||
}
|
||||
import { addMessagesBulkIDB, iterateAllMessagesIDB } from "../db";
|
||||
import { LoggedMessageJSON } from "../types";
|
||||
|
||||
export async function importLogs() {
|
||||
try {
|
||||
const content = await getLogContents();
|
||||
const data = JSON.parse(content) as { messages: DBMessageRecord[]; };
|
||||
let count = 0;
|
||||
const batchSize = 50;
|
||||
let batch: LoggedMessageJSON[] = [];
|
||||
|
||||
let messages: LoggedMessageJSON[] = [];
|
||||
for await (const item of iterateLogItems()) {
|
||||
const message = item.message || item;
|
||||
if (!message || !message.id || !message.channel_id || !message.timestamp) continue;
|
||||
|
||||
if ((data as any).deletedMessages || (data as any).editedMessages) {
|
||||
messages = Object.values((data as unknown as LoggedMessage)).filter(m => m.message).map(m => m.message) as LoggedMessageJSON[];
|
||||
} else
|
||||
messages = data.messages.map(m => m.message);
|
||||
batch.push(message);
|
||||
|
||||
if (!Array.isArray(messages)) {
|
||||
throw new Error("Invalid log file format");
|
||||
if (batch.length >= batchSize) {
|
||||
await addMessagesBulkIDB(batch);
|
||||
count += batch.length;
|
||||
batch = [];
|
||||
}
|
||||
}
|
||||
|
||||
if (!messages.length) {
|
||||
throw new Error("No messages found in log file");
|
||||
if (batch.length > 0) {
|
||||
await addMessagesBulkIDB(batch);
|
||||
count += batch.length;
|
||||
}
|
||||
|
||||
if (!messages.every(m => m.id && m.channel_id && m.timestamp)) {
|
||||
throw new Error("Invalid message format");
|
||||
if (count === 0) {
|
||||
Toasts.show({
|
||||
id: Toasts.genId(),
|
||||
message: "No messages found in log file",
|
||||
type: Toasts.Type.FAILURE
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
await addMessagesBulkIDB(messages);
|
||||
|
||||
Toasts.show({
|
||||
id: Toasts.genId(),
|
||||
message: "Successfully imported logs",
|
||||
message: `Successfully imported ${count} logs`,
|
||||
type: Toasts.Type.SUCCESS
|
||||
});
|
||||
} catch (e) {
|
||||
|
|
@ -86,22 +78,144 @@ export async function importLogs() {
|
|||
export async function exportLogs() {
|
||||
const filename = "message-logger-logs-idb.json";
|
||||
|
||||
const messages = await getAllMessagesIDB();
|
||||
const data = JSON.stringify({ messages }, null, 2);
|
||||
try {
|
||||
if (!IS_WEB) {
|
||||
const streamId = await Native.startNativeLogExport(filename);
|
||||
|
||||
if (IS_WEB || IS_VESKTOP || IS_EQUIBOP || !DiscordNative) {
|
||||
const file = new File([data], filename, { type: "application/json" });
|
||||
const a = document.createElement("a");
|
||||
a.href = URL.createObjectURL(file);
|
||||
a.download = filename;
|
||||
await Native.writeNativeLogChunk(streamId, '{\n "messages": [\n');
|
||||
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
setImmediate(() => {
|
||||
URL.revokeObjectURL(a.href);
|
||||
document.body.removeChild(a);
|
||||
let first = true;
|
||||
for await (const record of iterateAllMessagesIDB()) {
|
||||
const prefix = first ? "" : ",\n";
|
||||
first = false;
|
||||
|
||||
const chunk = prefix + " " + JSON.stringify(record);
|
||||
|
||||
await Native.writeNativeLogChunk(streamId, chunk);
|
||||
}
|
||||
|
||||
await Native.writeNativeLogChunk(streamId, "\n ]\n}");
|
||||
await Native.finishNativeLogExport(streamId);
|
||||
|
||||
Toasts.show({
|
||||
id: Toasts.genId(),
|
||||
message: "Successfully exported logs",
|
||||
type: Toasts.Type.SUCCESS
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// if check needed so esbuild doenst include native-file-system-adapter in native builds
|
||||
if (IS_WEB) {
|
||||
const handle = await showSaveFilePicker({
|
||||
suggestedName: filename,
|
||||
types: [{
|
||||
description: "JSON File",
|
||||
accept: { "application/json": [".json"] },
|
||||
}],
|
||||
});
|
||||
|
||||
const writable = await handle.createWritable();
|
||||
const writer = writable.getWriter();
|
||||
const encoder = new TextEncoder();
|
||||
|
||||
await writer.write(encoder.encode('{\n "messages": [\n'));
|
||||
|
||||
let first = true;
|
||||
let count = 0;
|
||||
for await (const records of iterateAllMessagesIDB()) {
|
||||
const prefix = first ? "" : ",\n";
|
||||
first = false;
|
||||
const chunk = prefix + " " + JSON.stringify(records);
|
||||
await writer.write(encoder.encode(chunk));
|
||||
count += records.length;
|
||||
}
|
||||
|
||||
await writer.write(encoder.encode("\n ]\n}"));
|
||||
await writer.close();
|
||||
|
||||
Toasts.show({
|
||||
id: Toasts.genId(),
|
||||
message: `Successfully exported ${count} logs`,
|
||||
type: Toasts.Type.SUCCESS
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
|
||||
Toasts.show({
|
||||
id: Toasts.genId(),
|
||||
message: "Error exporting logs. Check the console for more information",
|
||||
type: Toasts.Type.FAILURE
|
||||
});
|
||||
} else {
|
||||
DiscordNative.fileManager.saveWithDialog(data, filename);
|
||||
}
|
||||
}
|
||||
|
||||
async function* parseJsonStream(readChunk: () => Promise<string | null>) {
|
||||
const parser = new JSONParser({
|
||||
paths: ["$.messages.*"],
|
||||
keepStack: false,
|
||||
});
|
||||
const queue: any[] = [];
|
||||
let error: Error | null = null;
|
||||
|
||||
parser.onValue = ({ value }) => {
|
||||
queue.push(value);
|
||||
};
|
||||
|
||||
parser.onError = (err: Error) => {
|
||||
error = err;
|
||||
};
|
||||
|
||||
try {
|
||||
while (true) {
|
||||
if (error) throw error;
|
||||
while (queue.length > 0) {
|
||||
yield queue.shift();
|
||||
}
|
||||
|
||||
const chunk = await readChunk();
|
||||
if (chunk === null) break;
|
||||
|
||||
parser.write(chunk);
|
||||
}
|
||||
} catch (e) {
|
||||
throw e;
|
||||
} finally {
|
||||
if (!parser.isEnded)
|
||||
parser.end();
|
||||
}
|
||||
|
||||
if (error) throw error;
|
||||
while (queue.length > 0) {
|
||||
yield queue.shift();
|
||||
}
|
||||
}
|
||||
|
||||
async function* iterateLogItems(): AsyncGenerator<any> {
|
||||
if (IS_WEB) {
|
||||
const file = await chooseFileWeb(".json");
|
||||
if (!file) throw new Error("No file selected");
|
||||
|
||||
const stream = file.stream();
|
||||
const reader = stream.getReader();
|
||||
const decoder = new TextDecoder();
|
||||
|
||||
yield* parseJsonStream(async () => {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) return null;
|
||||
return decoder.decode(value, { stream: true });
|
||||
});
|
||||
} else {
|
||||
const settings = await Native.getSettings();
|
||||
const fileId = await Native.startNativeLogImport(settings.logsDir);
|
||||
|
||||
try {
|
||||
yield* parseJsonStream(async () => {
|
||||
return await Native.readNativeLogChunk(fileId);
|
||||
});
|
||||
} finally {
|
||||
await Native.closeNativeLogImport(fileId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -37,11 +37,10 @@
|
|||
gap: 4px;
|
||||
}
|
||||
|
||||
.vc-message-peek-activity-row>[class*="textXs"] {
|
||||
flex: 1;
|
||||
.vc-message-peek-activity-row > [class*="textXs"] {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.vc-message-peek-activity-row>[class*="vc-bactivities-row"] {
|
||||
.vc-message-peek-activity-row > [class*="vc-bactivities-row"] {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
|
|
@ -44,6 +44,20 @@ export default definePlugin({
|
|||
match: /decorators:(\i\.isSystemDM\(\)\?.{0,100}:null)/,
|
||||
replace: "decorators:[Vencord.Api.MemberListDecorators.__getDecorators(arguments[0],'dm'),$1]"
|
||||
}
|
||||
},
|
||||
// fix discords styling for now
|
||||
{
|
||||
find: '"AvatarWithText"',
|
||||
replacement: [
|
||||
{
|
||||
match: /(?<=className:.{0,10}),\{.{0,10}\}\)(?=,children:)/,
|
||||
replace: ')+" vc-member-list-decorators-display-names"'
|
||||
},
|
||||
{
|
||||
match: /(?<=className:\i\.\i)(?=,children:\i\}\))/,
|
||||
replace: '+" vc-member-list-decorators-display-names"'
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
});
|
||||
|
|
|
|||
|
|
@ -9,3 +9,10 @@
|
|||
/* Margin to match default Discord decorators */
|
||||
margin-left: 0.25em;
|
||||
}
|
||||
|
||||
.vc-member-list-decorators-display-names {
|
||||
min-width: 0;
|
||||
overflow: visible;
|
||||
text-overflow: clip;
|
||||
white-space: normal
|
||||
}
|
||||
|
|
@ -28,7 +28,7 @@ const symbolPattern = /^[\p{S}\p{P}]{1,3}$/iu;
|
|||
const templatePattern = /(?:\{(?:custom|friend|nick|display|user)(?:,\s*(?:custom|friend|nick|display|user))*\})/iu;
|
||||
|
||||
type CustomNicknameData = Record<string, string>;
|
||||
export let customNicknames: CustomNicknameData = {};
|
||||
let customNicknames: CustomNicknameData = {};
|
||||
|
||||
function adjustHex(color: string, percent: number): string {
|
||||
let hex = color.replace("#", "");
|
||||
|
|
@ -897,7 +897,6 @@ export default definePlugin({
|
|||
tags: ["SMYN", "Nicknames", "Custom Nicknames",],
|
||||
isModified: true,
|
||||
settings,
|
||||
customNicknames,
|
||||
|
||||
patches: [
|
||||
{
|
||||
|
|
@ -1059,6 +1058,7 @@ export default definePlugin({
|
|||
handleHoveringMessage,
|
||||
addHoveringReactionPopout,
|
||||
removeHoveringReactionPopout,
|
||||
getMessageName,
|
||||
getMessageNameText,
|
||||
getMessageNameElement,
|
||||
getMentionNameElement,
|
||||
|
|
|
|||
|
|
@ -109,6 +109,7 @@ export function isEquicordGuild(id: string | null | undefined, isGuildId: boolea
|
|||
if (!id) return false;
|
||||
if (isGuildId) return id === GUILD_ID;
|
||||
const channel = ChannelStore.getChannel(id);
|
||||
if (!channel) return false;
|
||||
return channel.guild_id === GUILD_ID;
|
||||
}
|
||||
|
||||
|
|
@ -121,7 +122,8 @@ export function isEquicordSupport(userId: string | null | undefined): boolean {
|
|||
if (!userId) return false;
|
||||
|
||||
const member = GuildMemberStore.getMember(GUILD_ID, userId);
|
||||
return member?.roles?.includes(EQUICORD_HELPERS) || false;
|
||||
if (!member) return false;
|
||||
return member.roles.includes(EQUICORD_HELPERS) || false;
|
||||
}
|
||||
|
||||
export function removeFromArray<T>(arr: T[], predicate: (e: T) => boolean) {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue