From 995961749e9da4bd0d75c4aea740edd72dda1889 Mon Sep 17 00:00:00 2001 From: MathMan05 Date: Fri, 21 Mar 2025 11:45:23 -0500 Subject: [PATCH] webhooks and bug fixes does not allow for editing/deleting them yet --- src/webpage/channel.ts | 134 +++++++++++++++++++++++++++++- src/webpage/guild.ts | 144 ++++++++++++++++++++++++++++++++- src/webpage/icons/intoMenu.svg | 1 + src/webpage/jsontypes.ts | 29 +++++++ src/webpage/localuser.ts | 8 +- src/webpage/member.ts | 1 + src/webpage/message.ts | 9 ++- src/webpage/settings.ts | 9 +++ src/webpage/style.css | 42 ++++++++++ src/webpage/user.ts | 17 +++- translations/en.json | 21 ++++- 11 files changed, 400 insertions(+), 15 deletions(-) create mode 100644 src/webpage/icons/intoMenu.svg diff --git a/src/webpage/channel.ts b/src/webpage/channel.ts index d56c773..9f8a4aa 100644 --- a/src/webpage/channel.ts +++ b/src/webpage/channel.ts @@ -16,6 +16,7 @@ import { messagejson, readyjson, startTypingjson, + webhookType, } from "./jsontypes.js"; import {MarkDown} from "./markdown.js"; import {Member} from "./member.js"; @@ -222,7 +223,7 @@ class Channel extends SnowFlake { this.sortPerms(); const settings = new Settings(I18n.getTranslation("channel.settingsFor", this.name)); { - const gensettings = settings.addButton("Settings"); + const gensettings = settings.addButton(I18n.channel.settings()); const form = gensettings.addForm("", () => {}, { fetchURL: this.info.api + "/channels/" + this.id, method: "PATCH", @@ -256,7 +257,7 @@ class Channel extends SnowFlake { }); } } - const s1 = settings.addButton("Permissions"); + const s1 = settings.addButton(I18n.channel.permissions()); s1.options.push( new RoleList( this.permission_overwritesar, @@ -265,6 +266,131 @@ class Channel extends SnowFlake { this, ), ); + + const webhooks = settings.addButton(I18n.webhooks.base()); + (async () => { + const hooks = (await ( + await fetch(this.info.api + `/channels/${this.id}/webhooks`, {headers: this.headers}) + ).json()) as webhookType[]; + webhooks.addButtonInput("", I18n.webhooks.newWebHook(), () => { + const nameBox = new Dialog(I18n.webhooks.EnterWebhookName()); + const options = nameBox.float.options; + options.addTextInput(I18n.webhooks.name(), async (name) => { + const json = await ( + await fetch(`${this.info.api}/channels/${this.id}/webhooks/`, { + method: "POST", + headers: this.headers, + body: JSON.stringify({name}), + }) + ).json(); + makeHook(json); + }); + options.addButtonInput("", I18n.submit(), () => { + options.submit(); + nameBox.hide(); + }); + nameBox.show(); + }); + + const makeHook = (hook: webhookType) => { + const div = document.createElement("div"); + div.classList.add("flexltr", "webhookArea"); + const pfp = document.createElement("img"); + if (hook.avatar) { + pfp.src = `${this.info.cdn}/avatars/${hook.id}/${hook.avatar}`; + } else { + const int = Number((BigInt(hook.id) >> 22n) % 6n); + pfp.src = `${this.info.cdn}/embed/avatars/${int}.png`; + } + pfp.classList.add("webhookpfppreview"); + + const namePlate = document.createElement("div"); + namePlate.classList.add("flexttb"); + + const name = document.createElement("b"); + name.textContent = hook.name; + + const createdAt = document.createElement("span"); + createdAt.textContent = I18n.webhooks.createdAt( + new Intl.DateTimeFormat(I18n.lang).format(SnowFlake.stringToUnixTime(hook.id)), + ); + + namePlate.append(name, createdAt); + + const icon = document.createElement("span"); + icon.classList.add("svg-intoMenu", "svgicon"); + + div.append(pfp, namePlate, icon); + + div.onclick = () => { + const form = webhooks.addSubForm( + hook.name, + (e) => { + console.log(e); + }, + {traditionalSubmit: true}, + ); + form.addTextInput(I18n.webhooks.name(), "name", {initText: hook.name}); + form.addFileInput(I18n.webhooks.avatar(), "avatar", {clear: true}); + + const moveChannels = this.guild.channels.filter( + (_) => _.hasPermission("MANAGE_WEBHOOKS") && _.type !== 4, + ); + form.addSelect( + I18n.webhooks.channel(), + "channel_id", + moveChannels.map((_) => _.name), + { + defaultIndex: moveChannels.findIndex((_) => _.id === this.id), + }, + moveChannels.map((_) => _.id), + ); + + form.addMDText(I18n.webhooks.token(hook.token)); + form.addMDText(I18n.webhooks.url(hook.url)); + form.addButtonInput("", I18n.webhooks.copyURL(), () => { + navigator.clipboard.writeText(hook.url); + }); + + form.addText(I18n.webhooks.createdBy()); + + try { + const div = document.createElement("div"); + div.classList.add("flexltr", "createdWebhook"); + //TODO make sure this is something I can actually do here + const user = new User(hook.user, this.localuser); + const name = document.createElement("b"); + name.textContent = user.name; + const nameBox = document.createElement("div"); + nameBox.classList.add("flexttb"); + nameBox.append(name); + const pfp = user.buildpfp(); + div.append(pfp, nameBox); + form.addHTMLArea(div); + + Member.resolveMember(user, this.guild).then((_) => { + if (_) { + name.textContent = _.name; + pfp.src = _.getpfpsrc(); + } else { + const notFound = document.createElement("span"); + notFound.textContent = I18n.webhooks.notFound(); + nameBox.append(notFound); + } + }); + user.bind(div, this.guild); + } catch {} + }; + + console.log(hook); + + webhooks.addHTMLArea(div); + }; + for (const hook of hooks) { + makeHook(hook); + } + })(); + settings.show(); } sortPerms() { @@ -799,7 +925,9 @@ class Channel extends SnowFlake { parent_id: this.id, permission_overwrites: [], }), - }); + }) + .then((_) => _.json()) + .then((_) => this.guild.goToChannelDelay(_.id)); } deleteChannel() { fetch(this.info.api + "/channels/" + this.id, { diff --git a/src/webpage/guild.ts b/src/webpage/guild.ts index 23d3320..64a7de7 100644 --- a/src/webpage/guild.ts +++ b/src/webpage/guild.ts @@ -14,6 +14,7 @@ import { rolesjson, emojipjson, extendedProperties, + webhookType, } from "./jsontypes.js"; import {User} from "./user.js"; import {I18n} from "./i18n.js"; @@ -316,6 +317,137 @@ class Guild extends SnowFlake { genDiv(); emoji.addHTMLArea(containdiv); } + const webhooks = settings.addButton(I18n.webhooks.base()); + (async () => { + const moveChannels = this.channels.filter( + (_) => _.hasPermission("MANAGE_WEBHOOKS") && _.type !== 4, + ); + const hooks = (await ( + await fetch(this.info.api + `/guilds/${this.id}/webhooks`, {headers: this.headers}) + ).json()) as webhookType[]; + webhooks.addButtonInput("", I18n.webhooks.newWebHook(), () => { + const nameBox = new Dialog(I18n.webhooks.EnterWebhookName()); + const options = nameBox.float.options; + options.addTextInput(I18n.webhooks.name(), async (name) => { + const json = await ( + await fetch(`${this.info.api}/channels/${moveChannels[select.index].id}/webhooks/`, { + method: "POST", + headers: this.headers, + body: JSON.stringify({name}), + }) + ).json(); + makeHook(json); + }); + const select = options.addSelect( + I18n.webhooks.channel(), + () => {}, + moveChannels.map((_) => _.name), + { + defaultIndex: 0, + }, + ); + options.addButtonInput("", I18n.submit(), () => { + options.submit(); + nameBox.hide(); + }); + nameBox.show(); + }); + + const makeHook = (hook: webhookType) => { + const div = document.createElement("div"); + div.classList.add("flexltr", "webhookArea"); + const pfp = document.createElement("img"); + if (hook.avatar) { + pfp.src = `${this.info.cdn}/avatars/${hook.id}/${hook.avatar}`; + } else { + const int = Number((BigInt(hook.id) >> 22n) % 6n); + pfp.src = `${this.info.cdn}/embed/avatars/${int}.png`; + } + pfp.classList.add("webhookpfppreview"); + + const namePlate = document.createElement("div"); + namePlate.classList.add("flexttb"); + + const name = document.createElement("b"); + name.textContent = hook.name; + + const createdAt = document.createElement("span"); + createdAt.textContent = I18n.webhooks.createdAt( + new Intl.DateTimeFormat(I18n.lang).format(SnowFlake.stringToUnixTime(hook.id)), + ); + + namePlate.append(name, createdAt); + + const icon = document.createElement("span"); + icon.classList.add("svg-intoMenu", "svgicon"); + + div.append(pfp, namePlate, icon); + + div.onclick = () => { + const form = webhooks.addSubForm( + hook.name, + (e) => { + console.log(e); + }, + {traditionalSubmit: true}, + ); + form.addTextInput(I18n.webhooks.name(), "name", {initText: hook.name}); + form.addFileInput(I18n.webhooks.avatar(), "avatar", {clear: true}); + + form.addSelect( + I18n.webhooks.channel(), + "channel_id", + moveChannels.map((_) => _.name), + { + defaultIndex: moveChannels.findIndex((_) => _.id === hook.channel_id), + }, + moveChannels.map((_) => _.id), + ); + + form.addMDText(I18n.webhooks.token(hook.token)); + form.addMDText(I18n.webhooks.url(hook.url)); + form.addButtonInput("", I18n.webhooks.copyURL(), () => { + navigator.clipboard.writeText(hook.url); + }); + + form.addText(I18n.webhooks.createdBy()); + + try { + const div = document.createElement("div"); + div.classList.add("flexltr", "createdWebhook"); + //TODO make sure this is something I can actually do here + const user = new User(hook.user, this.localuser); + const name = document.createElement("b"); + name.textContent = user.name; + const nameBox = document.createElement("div"); + nameBox.classList.add("flexttb"); + nameBox.append(name); + const pfp = user.buildpfp(); + div.append(pfp, nameBox); + form.addHTMLArea(div); + + Member.resolveMember(user, this).then((_) => { + if (_) { + name.textContent = _.name; + pfp.src = _.getpfpsrc(); + } else { + const notFound = document.createElement("span"); + notFound.textContent = I18n.webhooks.notFound(); + nameBox.append(notFound); + } + }); + user.bind(div, this); + } catch {} + }; + + console.log(hook); + + webhooks.addHTMLArea(div); + }; + for (const hook of hooks) { + makeHook(hook); + } + })(); settings.show(); } makeInviteMenu(options: Options, valid: void | Channel[]) { @@ -970,6 +1102,14 @@ class Guild extends SnowFlake { this.printServers(); return thischannel; } + goToChannelDelay(id: string) { + const channel = this.channels.find((_) => _.id == id); + if (channel) { + this.loadChannel(channel.id); + } else { + this.localuser.gotoid = id; + } + } createchannels(func = this.createChannel.bind(this)) { const options = ["text", "announcement", "voice"].map((e) => I18n.getTranslation("channel." + e), @@ -1036,7 +1176,9 @@ class Guild extends SnowFlake { method: "POST", headers: this.headers, body: JSON.stringify({name, type}), - }); + }) + .then((_) => _.json()) + .then((_) => this.goToChannelDelay(_.id)); } async createRole(name: string) { const fetched = await fetch(this.info.api + "/guilds/" + this.id + "roles", { diff --git a/src/webpage/icons/intoMenu.svg b/src/webpage/icons/intoMenu.svg new file mode 100644 index 0000000..c534cd2 --- /dev/null +++ b/src/webpage/icons/intoMenu.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/webpage/jsontypes.ts b/src/webpage/jsontypes.ts index 67e0617..b9d6dc7 100644 --- a/src/webpage/jsontypes.ts +++ b/src/webpage/jsontypes.ts @@ -148,6 +148,8 @@ type userjson = { theme_colors: string; pronouns?: string; badge_ids: string[]; + webhook?: webhookInfo; + uid?: string; }; type memberjson = { index?: number; @@ -300,6 +302,30 @@ type dirrectjson = { recipients: userjson[]; is_spam: boolean; }; +type webhookType = { + application_id: null | string; + avatar: null | string; + channel_id: string; + guild_id: string; + id: string; + name: string; + type: 1; + user: userjson; + token: string; + url: string; +}; +type webhookInfo = { + id: string; + type: 1; + name: string; + avatar: null | string; + guild_id: string; + channel_id: string; + application_id: null | string; + user_id: string; + source_guild_id: string; + source_channel_id: string; +}; type messagejson = { id: string; channel_id: string; @@ -323,6 +349,7 @@ type messagejson = { nonce: string; pinned: boolean; type: number; + webhook: webhookInfo; }; type filejson = { id: string; @@ -760,4 +787,6 @@ export { opRTC12, emojipjson, extendedProperties, + webhookInfo, + webhookType, }; diff --git a/src/webpage/localuser.ts b/src/webpage/localuser.ts index e8ccf5c..c2c775e 100644 --- a/src/webpage/localuser.ts +++ b/src/webpage/localuser.ts @@ -892,7 +892,7 @@ class Localuser { if (!forceReload && this.lookingguild === guild) { return guild; } - if (this.channelfocus) { + if (this.channelfocus && this.lookingguild !== guild) { this.channelfocus.infinite.delete(); this.channelfocus = undefined; } @@ -1070,7 +1070,11 @@ class Localuser { headers: this.headers, }); const json = await res.json(); - + console.log([...json.guilds], json.guilds); + //@ts-ignore + json.guilds = json.guilds.sort((a, b) => { + return b.member_count - a.member_count; + }); content.innerHTML = ""; const title = document.createElement("h2"); title.textContent = I18n.getTranslation("guild.disoveryTitle", json.guilds.length + ""); diff --git a/src/webpage/member.ts b/src/webpage/member.ts index ae96e2b..cd0b2f6 100644 --- a/src/webpage/member.ts +++ b/src/webpage/member.ts @@ -386,6 +386,7 @@ class Member extends SnowFlake { ); } static async resolveMember(user: User, guild: Guild): Promise { + if (user.webhook) return undefined; const maybe = user.members.get(guild); if (!user.members.has(guild)) { const membpromise = guild.localuser.resolvemember(user.id, guild.id); diff --git a/src/webpage/message.ts b/src/webpage/message.ts index e0119aa..212c6ec 100644 --- a/src/webpage/message.ts +++ b/src/webpage/message.ts @@ -207,8 +207,11 @@ class Message extends SnowFlake { if (messagejson.reactions?.length) { console.log(messagejson.reactions, ":3"); } - - this.author = new User(messagejson.author, this.localuser); + console.log(messagejson.webhook); + if (messagejson.webhook) { + messagejson.author.webhook = messagejson.webhook; + } + this.author = new User(messagejson.author, this.localuser, false); for (const thing in messagejson.mentions) { this.mentions[thing] = new User(messagejson.mentions[thing], this.localuser); } @@ -592,7 +595,7 @@ class Message extends SnowFlake { if (this.author.bot) { const username = document.createElement("span"); username.classList.add("bot"); - username.textContent = "BOT"; + username.textContent = this.author.webhook ? I18n.webhook() : I18n.bot(); userwrap.appendChild(username); } const time = document.createElement("span"); diff --git a/src/webpage/settings.ts b/src/webpage/settings.ts index d7cec7e..d991c37 100644 --- a/src/webpage/settings.ts +++ b/src/webpage/settings.ts @@ -1043,6 +1043,9 @@ class Form implements OptionsElement { } return this.options.addSubOptions(name, {ltr, noSubmit}); } + addHTMLArea(html: (() => HTMLElement) | HTMLElement, onSubmit = () => {}) { + return this.options.addHTMLArea(html, onSubmit); + } addSubForm( name: string, onSubmit: (arg1: object, sent: object) => void, @@ -1168,6 +1171,9 @@ class Form implements OptionsElement { addText(str: string) { return this.options.addText(str); } + addMDText(str: string) { + return this.options.addMDText(str); + } addHR() { return this.options.addHR(); } @@ -1263,6 +1269,9 @@ class Form implements OptionsElement { }; }); promises.push(promise); + continue; + } else if (input.value === undefined) { + continue; } } else { console.error(options.files + " is not currently implemented"); diff --git a/src/webpage/style.css b/src/webpage/style.css index 5e622ec..80cfef5 100644 --- a/src/webpage/style.css +++ b/src/webpage/style.css @@ -12,6 +12,22 @@ body { height: 100svh; background: var(--primary-bg); } +.createdWebhook { + display: flex; + align-items: center; + width: fit-content; + padding: 0.1in; + border-radius: 0.1in; + background: var(--secondary-bg); + user-select: none; + cursor: pointer; + + .pfp { + width: 0.5in; + height: 0.5in; + margin-right: 0.15in; + } +} .flexltr { min-height: 0; display: flex; @@ -268,6 +284,9 @@ textarea { .svg-category { mask: url(/icons/category.svg); } +.svg-intoMenu { + mask: url(/icons/intoMenu.svg); +} .svg-channel { mask: url(/icons/channel.svg); } @@ -2033,6 +2052,29 @@ fieldset input[type="radio"] { .FormSettings { padding-bottom: 32px; } +.webhookArea { + background: var(--secondary-bg); + padding: 0.2in; + display: flex; + align-items: center; + border-radius: 0.1in; + user-select: none; + cursor: pointer; + span { + color: var(--secondary-text-soft); + } + .svgicon { + width: 0.4in; + height: 0.4in; + margin-left: auto; + } +} +.webhookpfppreview { + width: 0.8in; + height: 0.8in; + border-radius: 1in; + margin-right: 0.2in; +} .optionElement, .FormSettings > button { margin: 16px 16px 0 16px; diff --git a/src/webpage/user.ts b/src/webpage/user.ts index 6d47bb3..3aee913 100644 --- a/src/webpage/user.ts +++ b/src/webpage/user.ts @@ -4,7 +4,7 @@ import {Contextmenu} from "./contextmenu.js"; import {Localuser} from "./localuser.js"; import {Guild} from "./guild.js"; import {SnowFlake} from "./snowflake.js"; -import {presencejson, userjson} from "./jsontypes.js"; +import {presencejson, userjson, webhookInfo} from "./jsontypes.js"; import {Role} from "./role.js"; import {Search} from "./search.js"; import {I18n} from "./i18n.js"; @@ -16,6 +16,7 @@ class User extends SnowFlake { owner: Localuser; hypotheticalpfp!: boolean; avatar!: string | null; + uid: string; username!: string; nickname: string | null = null; relationshipType: 0 | 1 | 2 | 3 | 4 | 5 | 6 = 0; @@ -24,6 +25,7 @@ class User extends SnowFlake { pronouns?: string; bot!: boolean; public_flags!: number; + webhook?: webhookInfo; accent_color!: number; banner: string | undefined; hypotheticalbanner!: boolean; @@ -35,7 +37,7 @@ class User extends SnowFlake { status!: string; resolving: false | Promise = false; - constructor(userjson: userjson, owner: Localuser, dontclone = false) { + constructor(userjson: userjson, owner: Localuser, dontclone: boolean = false) { super(userjson.id); this.owner = owner; if (localStorage.getItem("logbad") && owner.user && owner.user.id !== userjson.id) { @@ -44,6 +46,12 @@ class User extends SnowFlake { if (!owner) { console.error("missing localuser"); } + this.uid = userjson.id; + if (userjson.webhook) { + this.uid += ":::" + userjson.username; + console.log(this.uid); + } + userjson.uid = this.uid; if (dontclone) { this.userupdate(userjson); this.hypotheticalpfp = false; @@ -384,7 +392,7 @@ class User extends SnowFlake { } static checkuser(user: User | userjson, owner: Localuser): User { - const tempUser = owner.userMap.get(user.id); + const tempUser = owner.userMap.get(user.uid || user.id); if (tempUser) { if (!(user instanceof User)) { tempUser.userupdate(user); @@ -392,7 +400,7 @@ class User extends SnowFlake { return tempUser; } else { const tempuser = new User(user as userjson, owner, true); - owner.userMap.set(user.id, tempuser); + owner.userMap.set(user.uid || user.id, tempuser); return tempuser; } } @@ -512,6 +520,7 @@ class User extends SnowFlake { .then((member) => { User.contextmenu.bindContextmenu(html, this, member); if (member === undefined && error) { + if (this.webhook) return; const errorSpan = document.createElement("span"); errorSpan.textContent = "!"; errorSpan.classList.add("membererror"); diff --git a/translations/en.json b/translations/en.json index 09d3961..6f82144 100644 --- a/translations/en.json +++ b/translations/en.json @@ -152,7 +152,22 @@ "selectName": "Name of channel", "selectCatName": "Name of category", "createChannel": "Create channel", - "createCatagory": "Create category" + "createCatagory": "Create category", + "permissions": "Permissions" + }, + "webhooks": { + "createdAt": "Created at $1", + "name": "Name:", + "token": "Webhook token: `$1`", + "url": "Webhook url: `$1`", + "avatar": "Avatar", + "createdBy": "Created by:", + "notFound": "User no longer is in the guild", + "channel": "Channel", + "copyURL": "Copy Webhook URL", + "newWebHook": "New Webhook", + "EnterWebhookName": "Enter Webhook name", + "base": "Webhooks" }, "switchAccounts": "Switch accounts ⇌", "accountNotStart": "Account unable to start", @@ -483,5 +498,7 @@ "uploadFilesText": "Upload your files here!", "errorReconnect": "Unable to connect to the server, retrying in **$1** seconds...", "retrying": "Retrying...", - "unableToConnect": "Unable to connect to the Spacebar server. Please try logging out and back in." + "unableToConnect": "Unable to connect to the Spacebar server. Please try logging out and back in.", + "bot": "BOT", + "webhook": "WEBHOOK" }