support managed APIs
This commit is contained in:
parent
5b0c573f65
commit
ec4e8ff39a
3 changed files with 325 additions and 148 deletions
|
@ -1,11 +1,17 @@
|
|||
import { Alerts, Button, ChannelStore, Toasts } from "@webpack/common";
|
||||
import { Message } from "discord-types/general";
|
||||
import { CLONE_LINK_REGEX, clonePlugin, Native, plugins } from ".";
|
||||
/*
|
||||
* Vencord, a Discord client mod
|
||||
* Copyright (c) 2024 Vendicated and contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
const WHITELISTED_SHARE_CHANNELS = ["1256395889354997771", "1032200195582197831", "1301947896601509900"];
|
||||
import { Alerts, Button, ChannelStore } from "@webpack/common";
|
||||
|
||||
import { CLONE_LINK_REGEX, Native, plugins } from ".";
|
||||
|
||||
const WHITELISTED_SHARE_CHANNELS = ["1256395889354997771", "1032200195582197831", "1301947896601509900", "1322935137591365683"];
|
||||
|
||||
export default function UserpluginInstallButton({ props }: any) {
|
||||
const message: Message = props.message;
|
||||
const { message } = props;
|
||||
if (!WHITELISTED_SHARE_CHANNELS.includes(ChannelStore.getChannel(message.channel_id).parent_id) && !WHITELISTED_SHARE_CHANNELS.includes(message.channel_id))
|
||||
return;
|
||||
const gitLink = (props.message.content as string).match(CLONE_LINK_REGEX);
|
||||
|
@ -16,7 +22,19 @@ export default function UserpluginInstallButton({ props }: any) {
|
|||
<Button style={{
|
||||
marginTop: "5px"
|
||||
}}
|
||||
onClick={() => clonePlugin(gitLink)}>
|
||||
onClick={async () => {
|
||||
try {
|
||||
await Native.initPluginInstall(gitLink[0], gitLink[1], gitLink[2], gitLink[3]);
|
||||
|
||||
}
|
||||
catch (e) {
|
||||
if (e.toString().includes("silentStop")) return;
|
||||
Alerts.show({
|
||||
title: "Install error",
|
||||
body: e.toString()
|
||||
});
|
||||
}
|
||||
}}>
|
||||
{installed ? "Reinstall" : "Install"} plugin
|
||||
</Button>
|
||||
{
|
||||
|
@ -26,33 +44,7 @@ export default function UserpluginInstallButton({ props }: any) {
|
|||
}}
|
||||
color={Button.Colors.RED}
|
||||
onClick={() => {
|
||||
Alerts.show({
|
||||
title: "Uninstall plugin",
|
||||
body: `Are you sure that you want to uninstall ${gitLink[1]}?`,
|
||||
cancelText: "Cancel",
|
||||
confirmColor: Button.Colors.RED,
|
||||
confirmText: "Uninstall",
|
||||
async onConfirm() {
|
||||
Toasts.show({
|
||||
id: Toasts.genId(),
|
||||
message: `Uninstalling ${gitLink[1]}...`,
|
||||
type: Toasts.Type.MESSAGE
|
||||
});
|
||||
try {
|
||||
await Native.deleteFolder(`${VesktopNative.fileManager.getVencordDir().replace("\\", "/")}/../src/userplugins/${gitLink[1]}`);
|
||||
await Native.build(VesktopNative.fileManager.getVencordDir().replace("\\", "/"));
|
||||
window.location.reload();
|
||||
}
|
||||
catch {
|
||||
Toasts.pop();
|
||||
return Toasts.show({
|
||||
message: "Something bad has happened while deleting the plugin.",
|
||||
id: Toasts.genId(),
|
||||
type: Toasts.Type.FAILURE
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
}}>
|
||||
Uninstall plugin
|
||||
</Button>
|
||||
|
|
104
index.tsx
104
index.tsx
|
@ -1,57 +1,30 @@
|
|||
import { Notices } from "@api/index";
|
||||
import { addAccessory, removeAccessory } from "@api/MessageAccessories";
|
||||
import { Devs } from "@utils/constants";
|
||||
import definePlugin, { PluginNative } from "@utils/types";
|
||||
import { Alerts, Button, ChannelStore, Forms, TextInput, Toasts, Text } from "@webpack/common";
|
||||
import { Message } from "discord-types/general";
|
||||
import { clone } from "lodash";
|
||||
import UserpluginInstallButton from "./UserpluginInstallButton";
|
||||
import { showInstallModal } from "./UserpluginInstallModal";
|
||||
/*
|
||||
* Vencord, a Discord client mod
|
||||
* Copyright (c) 2024 Vendicated and contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import "./style.css";
|
||||
|
||||
export let plugins: any[] = [];
|
||||
export const CLONE_LINK_REGEX = /https:\/\/(?:git(?:hub|lab)\.com|git\.(?:[a-zA-Z0-9]|\.)+|codeberg\.org)\/(?!user-attachments)(?:[a-zA-Z0-9]|-)+\/((?:[a-zA-Z0-9]|-)+)(?:\.git)?(?:\/)?/;
|
||||
import { Devs } from "@utils/constants";
|
||||
import definePlugin, { PluginNative } from "@utils/types";
|
||||
import { Alerts, TextInput } from "@webpack/common";
|
||||
|
||||
import UserpluginInstallButton from "./UserpluginInstallButton";
|
||||
|
||||
export const plugins: any[] = [];
|
||||
export const CLONE_LINK_REGEX = /https:\/\/((?:git(?:hub|lab)\.com|git\.(?:[a-zA-Z0-9]|\.)+|codeberg\.org))\/(?!user-attachments)((?:[a-zA-Z0-9]|-)+)\/((?:[a-zA-Z0-9]|-)+)(?:\.git)?(?:\/)?/;
|
||||
|
||||
|
||||
// @ts-ignore
|
||||
export const Native = VencordNative.pluginHelpers.UserpluginInstaller as PluginNative<typeof import("./native")>;
|
||||
|
||||
export async function clonePlugin(gitLink: RegExpMatchArray) {
|
||||
Toasts.show({
|
||||
message: "Cloning plugin...",
|
||||
id: Toasts.genId(),
|
||||
type: Toasts.Type.MESSAGE
|
||||
});
|
||||
try {
|
||||
const path = `${VesktopNative.fileManager.getVencordDir().replace("\\", "/")}/../src/userplugins/${gitLink[1]}`;
|
||||
await Native.cloneRepo(gitLink[0], path);
|
||||
const meta = await Native.getPluginMeta(path);
|
||||
console.log(meta);
|
||||
showInstallModal(meta, path);
|
||||
}
|
||||
catch (e) {
|
||||
Toasts.pop();
|
||||
return Toasts.show({
|
||||
message: "Something bad has happened while cloning the plugin, try again later and make sure that the plugin link is valid.",
|
||||
id: Toasts.genId(),
|
||||
type: Toasts.Type.FAILURE
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default definePlugin({
|
||||
name: "UserpluginInstaller",
|
||||
description: "Install userplugins with a simple button click",
|
||||
authors: [Devs.nin0dev],
|
||||
async start() {
|
||||
plugins = await Native.getPlugins(`${VesktopNative.fileManager.getVencordDir().replace("\\", "/")}/../src/userplugins/`);
|
||||
console.log(plugins);
|
||||
addAccessory("userpluginInstallButton", (props: Record<string, any>) => (
|
||||
<UserpluginInstallButton props={props} />
|
||||
), 4);
|
||||
},
|
||||
stop() {
|
||||
removeAccessory("userpluginInstallButton");
|
||||
renderMessageAccessory: (props) => {
|
||||
return <UserpluginInstallButton props={props} />;
|
||||
},
|
||||
toolboxActions: {
|
||||
"Install Plugin": () => {
|
||||
|
@ -62,50 +35,15 @@ export default definePlugin({
|
|||
<TextInput onChange={v => { gitUrl = v; }} placeholder="Git link (https://github.com/...)" />
|
||||
</>,
|
||||
confirmText: "Install",
|
||||
onConfirm() {
|
||||
const fullGitLink = gitUrl.match(CLONE_LINK_REGEX);
|
||||
if (!fullGitLink) return;
|
||||
clonePlugin(fullGitLink);
|
||||
async onConfirm() {
|
||||
const gitLink = gitUrl.match(CLONE_LINK_REGEX)!;
|
||||
await Native.initPluginInstall(gitLink[0], gitLink[1], gitLink[2], gitLink[3]);
|
||||
window.location.reload();
|
||||
}
|
||||
});
|
||||
},
|
||||
"Uninstall Plugin": () => {
|
||||
let name = "";
|
||||
Alerts.show({
|
||||
title: "Uninstall plugin",
|
||||
body: <>
|
||||
<Text>Out of these plugins, which would you like to uninstall?</Text>
|
||||
{
|
||||
plugins.map((item, i) =>
|
||||
<Text style={{ fontWeight: "bold" }}>{item}</Text>
|
||||
)
|
||||
}
|
||||
<TextInput onChange={v => { name = v; }} style={{ marginTop: "10px" }} placeholder="Plugin name as written above" />
|
||||
</>,
|
||||
confirmText: "Uninstall",
|
||||
confirmColor: Button.Colors.RED,
|
||||
async onConfirm() {
|
||||
if (!plugins.includes(name)) return;
|
||||
Toasts.show({
|
||||
id: Toasts.genId(),
|
||||
message: `Uninstalling ${name}...`,
|
||||
type: Toasts.Type.MESSAGE
|
||||
});
|
||||
try {
|
||||
await Native.deleteFolder(`${VesktopNative.fileManager.getVencordDir().replace("\\", "/")}/../src/userplugins/${name}`);
|
||||
await Native.build(VesktopNative.fileManager.getVencordDir().replace("\\", "/"));
|
||||
window.location.reload();
|
||||
}
|
||||
catch {
|
||||
Toasts.pop();
|
||||
return Toasts.show({
|
||||
message: "Something bad has happened while deleting the plugin.",
|
||||
id: Toasts.genId(),
|
||||
type: Toasts.Type.FAILURE
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
311
native.ts
311
native.ts
|
@ -1,24 +1,119 @@
|
|||
/*
|
||||
* Vencord, a Discord client mod
|
||||
* Copyright (c) 2024 Vendicated and contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { exec, spawn } from "child_process";
|
||||
import { IpcMainInvokeEvent } from "electron";
|
||||
import { read, readdir, readdirSync, readFileSync, rmSync } from "fs";
|
||||
import { BrowserView, BrowserWindow, dialog, shell } from "electron";
|
||||
import { existsSync, readdirSync, readFileSync } from "fs";
|
||||
import { rm } from "fs/promises";
|
||||
import { join } from "path";
|
||||
|
||||
const PLUGIN_META_REGEX = /export default definePlugin\((?:\s|\/(?:\/|\*).*)*{\s*(?:\s|\/(?:\/|\*).*)*name:\s*(?:"|'|`)(.*)(?:"|'|`)(?:\s|\/(?:\/|\*).*)*,(?:\s|\/(?:\/|\*).*)*(?:\s|\/(?:\/|\*).*)*description:\s*(?:"|'|`)(.*)(?:"|'|`)(?:\s|\/(?:\/|\*).*)*/;
|
||||
const CLONE_LINK_REGEX = /https:\/\/((?:git(?:hub|lab)\.com|git\.(?:[a-zA-Z0-9]|\.)+|codeberg\.org))\/(?!user-attachments)((?:[a-zA-Z0-9]|-)+)\/((?:[a-zA-Z0-9]|-)+)(?:\.git)?(?:\/)?/;
|
||||
|
||||
export async function cloneRepo(_: IpcMainInvokeEvent, repo: string, clonePath: string): Promise<any> {
|
||||
rmSync(clonePath, { recursive: true, force: true });
|
||||
return new Promise((resolve, reject) => {
|
||||
exec(`git clone ${repo} ${clonePath}`, (error, stdout, stderr) => {
|
||||
if (error) {
|
||||
return reject(
|
||||
stderr
|
||||
);
|
||||
export async function initPluginInstall(_, link: string, source: string, owner: string, repo: string) {
|
||||
const verifiedRegex = link.match(CLONE_LINK_REGEX)!;
|
||||
if (verifiedRegex.length !== 4 || verifiedRegex[0] !== link || verifiedRegex[1] !== source || verifiedRegex[2] !== owner || verifiedRegex[3] !== repo) return;
|
||||
|
||||
// Ask for clone
|
||||
const cloneDialog = await dialog.showMessageBox({
|
||||
title: "Clone userplugin",
|
||||
message: `You are about to clone a userplugin from ${source}.`,
|
||||
type: "question",
|
||||
detail: `The repository name is "${repo}" and it is owned by "${owner}".\nThe repository URL is ${link}\n\n(If you did not request this intentionally, choose Cancel)`,
|
||||
buttons: ["Cancel", "Clone repository and continue install", "Open repository in browser"]
|
||||
});
|
||||
switch (cloneDialog.response) {
|
||||
case 0: {
|
||||
throw "Rejected by user";
|
||||
}
|
||||
case 1: {
|
||||
await cloneRepo(link, repo);
|
||||
break;
|
||||
}
|
||||
case 2: {
|
||||
await shell.openExternal(link);
|
||||
throw "silentStop";
|
||||
}
|
||||
}
|
||||
|
||||
// Get plugin meta
|
||||
const meta = await getPluginMeta(join(__dirname, "..", "src", "userplugins", repo));
|
||||
|
||||
// Review plugin
|
||||
const win = new BrowserWindow({
|
||||
maximizable: false,
|
||||
minimizable: false,
|
||||
width: 560,
|
||||
height: meta.usesNative || meta.usesPreSend ? 650 : 360,
|
||||
resizable: false,
|
||||
webPreferences: {
|
||||
devTools: true
|
||||
},
|
||||
title: "Review userplugin",
|
||||
modal: true,
|
||||
parent: BrowserWindow.getAllWindows()[0],
|
||||
show: false,
|
||||
autoHideMenuBar: true
|
||||
});
|
||||
const reView /* haha got it */ = new BrowserView({
|
||||
webPreferences: {
|
||||
devTools: true,
|
||||
nodeIntegration: true
|
||||
}
|
||||
});
|
||||
win.setBrowserView(reView);
|
||||
win.addBrowserView(reView);
|
||||
win.setTopBrowserView(reView);
|
||||
win.loadURL(generateReviewPluginContent(meta));
|
||||
win.on("page-title-updated", async e => {
|
||||
switch (win.webContents.getTitle() as "abortInstall" | "reviewCode" | "install") {
|
||||
case "abortInstall": {
|
||||
win.close();
|
||||
await rm(join(__dirname, "..", "src", "userplugins", repo), {
|
||||
recursive: true
|
||||
});
|
||||
throw "Rejected by user";
|
||||
break;
|
||||
}
|
||||
resolve(null);
|
||||
case "reviewCode": {
|
||||
win.close();
|
||||
shell.openPath(join(__dirname, "..", "src", "userplugins", repo));
|
||||
throw "A file explorer window should've been opened with your plugin";
|
||||
break;
|
||||
}
|
||||
case "install": {
|
||||
win.close();
|
||||
await build();
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
win.show();
|
||||
}
|
||||
|
||||
async function build(): Promise<any> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const proc = exec("pnpm build", {
|
||||
cwd: __dirname
|
||||
});
|
||||
proc.once("close", async () => {
|
||||
if (proc.exitCode !== 0) {
|
||||
reject("Failed to build");
|
||||
}
|
||||
resolve("Success");
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export async function getPluginMeta(_, path: string): Promise<any> {
|
||||
async function getPluginMeta(path: string): Promise<{
|
||||
name: string;
|
||||
description: string;
|
||||
usesPreSend: boolean;
|
||||
usesNative: boolean;
|
||||
}> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const files = readdirSync(path);
|
||||
let fileToRead: "index.ts" | "index.tsx" | "index.js" | "index.jsx" | undefined;
|
||||
|
@ -28,40 +123,192 @@ export async function getPluginMeta(_, path: string): Promise<any> {
|
|||
if (f === "index.js") fileToRead = "index.js";
|
||||
if (f === "index.jsx") fileToRead = "index.jsx";
|
||||
});
|
||||
if (!fileToRead) reject();
|
||||
if (!fileToRead) reject("Invalid plugin");
|
||||
|
||||
const file = readFileSync(`${path}/${fileToRead}`, "utf8");
|
||||
const rawMeta = file.match(PLUGIN_META_REGEX);
|
||||
resolve({
|
||||
name: rawMeta![1],
|
||||
description: rawMeta![2],
|
||||
usesPreSend: file.includes("PreSendListener")
|
||||
usesPreSend: file.includes("PreSendListener"),
|
||||
usesNative: files.includes("native.ts") || files.includes("native.js")
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export function deleteFolder(_, path: string) {
|
||||
if(path.match(/\.\./g).length > 1) return;
|
||||
rmSync(path, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
export async function build(_: IpcMainInvokeEvent, path: string): Promise<any> {
|
||||
async function cloneRepo(link: string, repo: string): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
exec(`pnpm build`, {
|
||||
cwd: path
|
||||
}, (error, stdout, stderr) => {
|
||||
if (error) {
|
||||
return reject(
|
||||
stderr
|
||||
);
|
||||
const proc = spawn("git", ["clone", link], {
|
||||
cwd: join(__dirname, "..", "src", "userplugins")
|
||||
});
|
||||
proc.once("close", async () => {
|
||||
if (proc.exitCode !== 0) {
|
||||
if (!existsSync(join(__dirname, "..", "src", "userplugins", repo)))
|
||||
return reject("Failed to clone");
|
||||
const deleteReqDialog = await dialog.showMessageBox({
|
||||
title: "Error",
|
||||
message: "Plugin already exists",
|
||||
type: "error",
|
||||
detail: `The plugin that you tried to clone already exists at ${join(__dirname, "..", "src", "userplugins")}.\nWould you like to reclone it? Only do this if you want to reinstall or update the plugin.`,
|
||||
buttons: ["No", "Yes"]
|
||||
});
|
||||
if (deleteReqDialog.response !== 1) return reject("User rejected");
|
||||
await rm(join(__dirname, "..", "src", "userplugins", repo), {
|
||||
recursive: true
|
||||
});
|
||||
await cloneRepo(link, repo);
|
||||
}
|
||||
resolve(null);
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export async function getPlugins(_, path: string): Promise<string[]> {
|
||||
return new Promise((resolve) => {
|
||||
return resolve(readdirSync(path));
|
||||
});
|
||||
function generateReviewPluginContent(meta: {
|
||||
name: string;
|
||||
description: string;
|
||||
usesPreSend: boolean;
|
||||
usesNative: boolean;
|
||||
}): string {
|
||||
const template = `
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Review userplugin</title>
|
||||
<script>
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
document.querySelector("#abort").addEventListener("click", () => {
|
||||
document.title = "abortInstall";
|
||||
})
|
||||
document.querySelector("#review").addEventListener("click", () => {
|
||||
document.title = "reviewCode";
|
||||
})
|
||||
document.querySelector("#install").addEventListener("click", () => {
|
||||
if(!document.querySelector("style").innerHTML.includes("#native-ts-warning { display: none !important; }") && !document.querySelector("input[type='checkbox']").checked) return alert("Make sure to acknowledge all warnings before installing.");
|
||||
document.title = "install";
|
||||
})
|
||||
})
|
||||
</script>
|
||||
<style>
|
||||
* {
|
||||
font-family: sans-serif;
|
||||
color: white;
|
||||
}
|
||||
body {
|
||||
background-color: #202020;
|
||||
padding: 10px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.card {
|
||||
padding: 10px 12px;
|
||||
border: 1px solid grey;
|
||||
border-radius: 10px;
|
||||
margin-bottom: 10px; /* Added margin for better spacing */
|
||||
}
|
||||
.warn-card {
|
||||
border-color: #ffde00;
|
||||
background-color: #ffde0011;
|
||||
}
|
||||
.danger-card {
|
||||
border-color: #ff0000;
|
||||
background-color: #ff000022;
|
||||
h3 {
|
||||
color: #ffaaaa;
|
||||
}
|
||||
}
|
||||
.card * {
|
||||
margin: 0;
|
||||
}
|
||||
.card h3 {
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
input[type="checkbox"] {
|
||||
margin-right: 5px;
|
||||
}
|
||||
#validate {
|
||||
color: #ffaaaa;
|
||||
}
|
||||
#btn-row {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
}
|
||||
#btn-row * {
|
||||
flex:1;
|
||||
margin: 5px;
|
||||
padding: 10px 5px;
|
||||
border: 1px solid white;
|
||||
border-radius: 5px;
|
||||
}
|
||||
#abort {
|
||||
background-color: #aa3030;
|
||||
}
|
||||
#review {
|
||||
background-color: #303030;
|
||||
}
|
||||
#install {
|
||||
background-color: #002000;
|
||||
}
|
||||
#abort:hover {
|
||||
background-color: #bb3030;
|
||||
font-weight: bold;
|
||||
}
|
||||
#review:hover {
|
||||
background-color: #404040;
|
||||
font-weight: bold;
|
||||
}
|
||||
#install:hover {
|
||||
background-color: #003000;
|
||||
font-weight: bold;
|
||||
}
|
||||
%WARNINGHIDER%
|
||||
%NATIVETSHIDER%
|
||||
%PRESENDHIDER%
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h2>Plugin info</h2>
|
||||
<div class="card" id="plugin-info-card">
|
||||
<h3>%PLUGINNAME%</h3>
|
||||
<p>%PLUGINDESC%</p>
|
||||
</div>
|
||||
|
||||
<h2 data-useless="warning">Warnings</h2>
|
||||
<div data-useless="warning" class="card danger-card" id="native-ts-warning">
|
||||
<h3>Uses a native.ts file</h3>
|
||||
<p>
|
||||
Use of this file allows the plugin to escape the browser sandbox and
|
||||
potentially do anything to your system and data.
|
||||
<b
|
||||
>ONLY INSTALL THIS PLUGIN AFTER REVIEWING THE CODE AND BEING 100% SURE
|
||||
THAT NOTHING BAD CAN BE DONE!</b
|
||||
>
|
||||
</p>
|
||||
<br />
|
||||
<input type="checkbox" /> Acknowledge warning (required to allow install)
|
||||
</div>
|
||||
<div data-useless="warning" class="card warn-card" id="pre-send-warning">
|
||||
<h3>Has pre-send listeners</h3>
|
||||
<p>This allows the plugin to edit your messages before they are sent.</p>
|
||||
</div>
|
||||
<p id="validate">
|
||||
Reminder: installing a userplugin can be a destructive action. Make sure that you know and trust the developer before installing any.
|
||||
</p>
|
||||
<div id="btn-row">
|
||||
<button id="abort">
|
||||
Cancel installation
|
||||
</button>
|
||||
<button id="review">
|
||||
Review source code
|
||||
</button>
|
||||
<button id="install">
|
||||
Install plugin
|
||||
</button>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
`.replace("%PLUGINNAME%", meta.name.replaceAll("<", "<")).replace("%PLUGINDESC%", meta.description.replaceAll("<", "<")).replace("%WARNINGHIDER%", !meta.usesNative && !meta.usesPreSend ? "[data-useless=\"warning\"] { display: none !important; }" : "").replace("%NATIVETSHIDER%", meta.usesNative ? "" : "#native-ts-warning { display: none !important; }").replace("%PRESENDHIDER%", meta.usesPreSend ? "" : "#pre-send-warning { display: none !important; }");
|
||||
const buf = Buffer.from(template).toString("base64");
|
||||
return `data:text/html;base64,${buf}`;
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue