From 8bc009dc196245c52dcdb81c6d2bb640eeeab6fa Mon Sep 17 00:00:00 2001 From: MathMan05 Date: Tue, 15 Apr 2025 12:17:51 -0500 Subject: [PATCH] marge inital template support --- src/index.ts | 4 ++ src/webpage/guild.ts | 86 +++++++++++++++++++++++++++- src/webpage/index.ts | 4 ++ src/webpage/jsontypes.ts | 32 +++++++++++ src/webpage/localuser.ts | 60 +++++++++++++++++++- src/webpage/settings.ts | 2 +- src/webpage/style.css | 13 +++++ src/webpage/template.html | 35 ++++++++++++ src/webpage/templatePage.ts | 110 ++++++++++++++++++++++++++++++++++++ translations/en.json | 16 ++++++ 10 files changed, 359 insertions(+), 3 deletions(-) create mode 100644 src/webpage/template.html create mode 100644 src/webpage/templatePage.ts diff --git a/src/index.ts b/src/index.ts index 01caaf5..0bc7ac0 100644 --- a/src/index.ts +++ b/src/index.ts @@ -177,6 +177,10 @@ app.use("/", async (req: Request, res: Response) => { res.sendFile(path.join(__dirname, "webpage", "invite.html")); return; } + if (req.path.startsWith("/template/")) { + res.sendFile(path.join(__dirname, "webpage", "template.html")); + return; + } const filePath = await combinePath("/webpage/" + req.path); res.sendFile(filePath); }); diff --git a/src/webpage/guild.ts b/src/webpage/guild.ts index a1d124e..0222224 100644 --- a/src/webpage/guild.ts +++ b/src/webpage/guild.ts @@ -16,6 +16,7 @@ import { extendedProperties, banObj, addInfoBan, + templateSkim, } from "./jsontypes.js"; import {User} from "./user.js"; import {I18n} from "./i18n.js"; @@ -24,6 +25,7 @@ import {webhookMenu} from "./webhooks.js"; import {createImg} from "./utils/utils.js"; import {Sticker} from "./sticker.js"; import {ProgessiveDecodeJSON} from "./utils/progessiveLoad.js"; +import {getApiUrls} from "../utils.js"; class Guild extends SnowFlake { owner!: Localuser; @@ -581,7 +583,89 @@ class Guild extends SnowFlake { })(); const webhooks = settings.addButton(I18n.webhooks.base()); webhookMenu(this, this.info.api + `/guilds/${this.id}/webhooks`, webhooks); - console.log(this.properties.features, this.properties.features.includes("COMMUNITY")); + const template = settings.addButton(I18n.guild.templates()); + (async () => { + template.addText(I18n.guild.templcateMetaDesc()); + const generateTemplateArea = (temp: templateSkim) => { + const div = document.createElement("div"); + div.classList.add("flexltr", "templateMiniBox"); + const code = document.createElement("span"); + + code.textContent = temp.code + ` (${temp.name})`; + + const edit = document.createElement("button"); + edit.textContent = I18n.edit(); + edit.onclick = () => { + const form = template.addSubForm( + I18n.guild.editingTemplate(temp.name), + (tempy) => { + const template = tempy as templateSkim; + temp.name = template.name; + temp.description = template.description; + }, + { + fetchURL: this.info.api + "/guilds/" + this.id + "/templates/" + temp.code, + method: "PATCH", + headers: this.headers, + }, + ); + const search = new URLSearchParams([["instance", this.info.wellknown]]); + form.addMDText( + I18n.guild.templateURL( + window.location.origin + "/template/" + temp.code + "?" + search, + ), + ); + + const name = form.addTextInput(I18n.guild.templateName(), "name", { + initText: temp.name, + }); + form.addMDInput(I18n.guild.templateDesc(), "description", { + initText: temp.description, + }); + User.resolve(temp.creator_id, this.localuser).then((_) => { + form.addText(I18n.guild.tempCreatedBy()); + form.addHTMLArea(_.createWidget(this)); + }); + form.addText(I18n.guild.tempUseCount((temp.usage_count || 0) + "")); + form.addPreprocessor(() => { + if (name.value.length < 2) { + throw new FormError(name, I18n.guild.templateNameShort()); + } + }); + }; + + div.append(code, edit); + template.addHTMLArea(div); + }; + template.addButtonInput("", I18n.guild.createNewTemplate(), () => { + const form = template.addSubForm( + I18n.guild.createNewTemplate(), + (code) => { + template.returnFromSub(); + generateTemplateArea(code as templateSkim); + }, + { + fetchURL: this.info.api + "/guilds/" + this.id + "/templates", + method: "POST", + headers: this.headers, + }, + ); + form.addText(I18n.guild.templcateMetaDesc()); + const name = form.addTextInput(I18n.guild.templateName(), "name"); + form.addMDInput(I18n.guild.templateDesc(), "description"); + form.addPreprocessor(() => { + if (name.value.length < 2) { + throw new FormError(name, I18n.guild.templateNameShort()); + } + }); + }); + const templates = (await ( + await fetch(this.info.api + "/guilds/" + this.id + "/templates", {headers: this.headers}) + ).json()) as templateSkim[]; + for (const temp of templates.reverse()) { + generateTemplateArea(temp); + } + })(); let com = false; if (this.properties.features.includes("COMMUNITY")) { this.addCommunity(settings, textChannels); diff --git a/src/webpage/index.ts b/src/webpage/index.ts index 62e7a77..f68e1e8 100644 --- a/src/webpage/index.ts +++ b/src/webpage/index.ts @@ -7,6 +7,7 @@ import {Message} from "./message.js"; import {File} from "./file.js"; import {I18n} from "./i18n.js"; (async () => { + let templateID = new URLSearchParams(window.location.search).get("templateID"); await I18n.done; if (!Localuser.users.currentuser) { @@ -54,6 +55,9 @@ import {I18n} from "./i18n.js"; loading.classList.add("doneloading"); loading.classList.remove("loading"); console.log("done loading"); + if (templateID) { + thisUser.passTemplateID(templateID); + } }); } catch (e) { console.error(e); diff --git a/src/webpage/jsontypes.ts b/src/webpage/jsontypes.ts index c3eb533..49cdc98 100644 --- a/src/webpage/jsontypes.ts +++ b/src/webpage/jsontypes.ts @@ -131,6 +131,37 @@ interface banObj { public_flags: number; }; } +interface templateSkim { + id: string; + code: string; + name: string; + description: string; + usage_count: null | number; + creator_id: string; + created_at: string; + updated_at: string; + source_guild_id: string; + serialized_source_guild: { + id: string; + afk_channel_id: null | string; + afk_timeout: number; + default_message_notifications: number; + description: null | "string"; + explicit_content_filter: number; + features: string[]; + icon: null | string; + large: boolean; + name: string; + preferred_locale: string; + region: string; + system_channel_id: null | string; + system_channel_flags: number; + verification_level: number; + widget_enabled: boolean; + nsfw: boolean; + premium_progress_bar_enabled: boolean; + }; +} interface addInfoBan { id: string; user_id: string; @@ -838,4 +869,5 @@ export { stickerJson, banObj, addInfoBan, + templateSkim, }; diff --git a/src/webpage/localuser.ts b/src/webpage/localuser.ts index f530bc0..719b772 100644 --- a/src/webpage/localuser.ts +++ b/src/webpage/localuser.ts @@ -1046,6 +1046,7 @@ class Localuser { return false; } } + loadGuild(id: string, forceReload = false): Guild | undefined { this.searching = false; let guild = this.guildids.get(id); @@ -1172,7 +1173,10 @@ class Localuser { } this.unreads(); } - createGuild() { + passTemplateID(id: string) { + this.createGuild(id); + } + createGuild(templateID?: string) { const full = new Dialog(""); const buttons = full.options.addButtons("", {top: true}); const viacode = buttons.add(I18n.getTranslation("invite.joinUsing")); @@ -1224,7 +1228,61 @@ class Localuser { full.hide(); }); } + const guildcreateFromTemplate = buttons.add(I18n.guild.createFromTemplate()); + { + const form = guildcreateFromTemplate.addForm( + "", + (_: any) => { + if (_.message) { + loading.hide(); + full.show(); + alert(_.message); + const htmlarea = buttons.htmlarea.deref(); + if (htmlarea) buttons.generateHTMLArea(guildcreateFromTemplate, htmlarea); + } else { + loading.hide(); + full.hide(); + } + }, + { + method: "POST", + headers: this.headers, + }, + ); + const template = form.addTextInput(I18n.guild.template(), "template", { + initText: templateID || "", + }); + form.addFileInput(I18n.getTranslation("guild.icon:"), "icon", {files: "one"}); + form.addTextInput(I18n.getTranslation("guild.name:"), "name", {required: true}); + + const loading = new Dialog(""); + loading.float.options.addTitle(I18n.guild.creating()); + form.onFormError = () => { + loading.hide(); + full.show(); + }; + form.addPreprocessor((e) => { + loading.show(); + full.hide(); + if ("template" in e) delete e.template; + let code: string; + if (URL.canParse(template.value)) { + const url = new URL(template.value); + code = url.pathname.split("/").at(-1) as string; + if (url.host === "discord.com") { + code = "discord:" + code; + } + } else { + code = template.value; + } + form.fetchURL = this.info.api + "/guilds/templates/" + code; + }); + } full.show(); + if (templateID) { + const htmlarea = buttons.htmlarea.deref(); + if (htmlarea) buttons.generateHTMLArea(guildcreateFromTemplate, htmlarea); + } } async makeGuild(fields: {name: string; icon: string | null}) { return await ( diff --git a/src/webpage/settings.ts b/src/webpage/settings.ts index 29a9c49..1e4953b 100644 --- a/src/webpage/settings.ts +++ b/src/webpage/settings.ts @@ -1095,7 +1095,7 @@ class Form implements OptionsElement { readonly names: Map> = new Map(); readonly required: WeakSet> = new WeakSet(); readonly submitText: string; - readonly fetchURL: string; + fetchURL: string; readonly headers = {}; readonly method: string; value!: object; diff --git a/src/webpage/style.css b/src/webpage/style.css index c305e21..a3acfd8 100644 --- a/src/webpage/style.css +++ b/src/webpage/style.css @@ -59,6 +59,18 @@ body { margin-left: 8px; } } +.templateMiniBox { + display: flex; + align-items: center; + padding: 6px; + border-radius: 4px; + width: fit-content; + background: var(--secondary-bg); + + button { + margin-left: 4px; + } +} .flexltr { min-height: 0; display: flex; @@ -1952,6 +1964,7 @@ img.bigembedimg { border-radius: 8px; overflow: hidden; align-items: flex-start; + z-index: 3; } .hypoprofile { position: relative; diff --git a/src/webpage/template.html b/src/webpage/template.html new file mode 100644 index 0000000..193c462 --- /dev/null +++ b/src/webpage/template.html @@ -0,0 +1,35 @@ + + + + + + Jank Client + + + + + + + + + + +
+
+

Use Template Name

+

+ +
+
+ + + diff --git a/src/webpage/templatePage.ts b/src/webpage/templatePage.ts new file mode 100644 index 0000000..59e9bf2 --- /dev/null +++ b/src/webpage/templatePage.ts @@ -0,0 +1,110 @@ +import {I18n} from "./i18n.js"; +import {templateSkim} from "./jsontypes.js"; +import {getapiurls} from "./utils/utils.js"; +import {getBulkUsers, Specialuser} from "./utils/utils.js"; + +(async () => { + const users = getBulkUsers(); + const well = new URLSearchParams(window.location.search).get("instance"); + const joinable: Specialuser[] = []; + + for (const key in users.users) { + if (Object.prototype.hasOwnProperty.call(users.users, key)) { + const user: Specialuser = users.users[key]; + if (well && user.serverurls.wellknown.includes(well)) { + joinable.push(user); + } + console.log(user); + } + } + + let urls: {api: string; cdn: string} | undefined; + + if (!joinable.length && well) { + const out = await getapiurls(well); + if (out) { + urls = out; + for (const key in users.users) { + if (Object.prototype.hasOwnProperty.call(users.users, key)) { + const user: Specialuser = users.users[key]; + if (user.serverurls.api.includes(out.api)) { + joinable.push(user); + } + console.log(user); + } + } + } else { + throw new Error("Someone needs to handle the case where the servers don't exist"); + } + } else { + urls = joinable[0].serverurls; + } + await I18n.done; + if (!joinable.length) { + document.getElementById("usetemplate")!.textContent = I18n.htmlPages.noAccount(); + } + + const code = window.location.pathname.split("/")[2]; + + fetch(`${urls!.api}/guilds/templates/${code}`, { + method: "GET", + headers: { + Authorization: joinable[0].token, + }, + }) + .then((response) => response.json()) + .then((json) => { + const template = json as templateSkim; + document.getElementById("templatename")!.textContent = I18n.useTemplate(template.name); + document.getElementById("templatedescription")!.textContent = template.description; + }); + + function showAccounts(): void { + const table = document.createElement("dialog"); + for (const user of joinable) { + console.log(user.pfpsrc); + + const userinfo = document.createElement("div"); + userinfo.classList.add("flexltr", "switchtable"); + + const pfp = document.createElement("img"); + pfp.src = user.pfpsrc; + pfp.classList.add("pfp"); + userinfo.append(pfp); + + const userDiv = document.createElement("div"); + userDiv.classList.add("userinfo"); + userDiv.textContent = user.username; + userDiv.append(document.createElement("br")); + + const span = document.createElement("span"); + span.textContent = user.serverurls.wellknown.replace("https://", "").replace("http://", ""); + span.classList.add("serverURL"); + userDiv.append(span); + + userinfo.append(userDiv); + table.append(userinfo); + + userinfo.addEventListener("click", () => { + const search = new URLSearchParams(); + search.set("templateID", code); + sessionStorage.setItem("currentuser", user.uid); + window.location.assign("/channels/@me?" + search); + }); + } + + if (!joinable.length) { + const l = new URLSearchParams("?"); + l.set("goback", window.location.href); + l.set("instance", well!); + window.location.href = "/login?" + l.toString(); + } + + table.classList.add("flexttb", "accountSwitcher"); + console.log(table); + document.body.append(table); + } + + document.getElementById("usetemplate")!.addEventListener("click", showAccounts); + document.getElementById("usetemplate")!.textContent = I18n.useTemplateButton(); +})(); diff --git a/translations/en.json b/translations/en.json index c2c37af..6c6e519 100644 --- a/translations/en.json +++ b/translations/en.json @@ -230,6 +230,8 @@ "box3title": "Contribute to Jank Client", "box3description": "We always appreciate some help, whether that be in the form of bug reports, or code, or even just pointing out some typos." }, + "useTemplate": "Use $1 as a template", + "useTemplateButton": "Use Template", "register": { "passwordError:": "Password: $1", "usernameError": "Username: $1", @@ -244,7 +246,21 @@ "goThereTrust": "Go there and trust in the future", "nevermind": "Nevermind", "submit": "submit", + "edit": "Edit", "guild": { + "template": "Template:", + "viewTemplate": "View Template", + "createFromTemplate": "Create From Template", + "tempUseCount": "Template has been used $1 {{PLURAL:$1|time|times}}", + "tempCreatedBy": "Template created by:", + "editingTemplate": "Editing $1", + "createNewTemplate": "Create New Template", + "templates": "Templates", + "templateName": "Template Name:", + "templateDesc": "Template Description:", + "templcateMetaDesc": "A template allows others to use this guild as a base for their own guilds, it'll copy the channels, roles, and settings of this guild, but not the messages from within the guild, the bots, or the guilds icon.", + "templateNameShort": "Template name must at least be 2 characters long", + "templateURL": "Template URL: $1", "bannedBy": "Banned by:", "banReason": "Ban reason: $1", "bans": "Bans",