webhooks and bug fixes

does not allow for editing/deleting them yet
This commit is contained in:
MathMan05 2025-03-21 11:45:23 -05:00
parent 7068b03757
commit 995961749e
11 changed files with 400 additions and 15 deletions

View file

@ -16,6 +16,7 @@ import {
messagejson, messagejson,
readyjson, readyjson,
startTypingjson, startTypingjson,
webhookType,
} from "./jsontypes.js"; } from "./jsontypes.js";
import {MarkDown} from "./markdown.js"; import {MarkDown} from "./markdown.js";
import {Member} from "./member.js"; import {Member} from "./member.js";
@ -222,7 +223,7 @@ class Channel extends SnowFlake {
this.sortPerms(); this.sortPerms();
const settings = new Settings(I18n.getTranslation("channel.settingsFor", this.name)); 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("", () => {}, { const form = gensettings.addForm("", () => {}, {
fetchURL: this.info.api + "/channels/" + this.id, fetchURL: this.info.api + "/channels/" + this.id,
method: "PATCH", 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( s1.options.push(
new RoleList( new RoleList(
this.permission_overwritesar, this.permission_overwritesar,
@ -265,6 +266,131 @@ class Channel extends SnowFlake {
this, 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(); settings.show();
} }
sortPerms() { sortPerms() {
@ -799,7 +925,9 @@ class Channel extends SnowFlake {
parent_id: this.id, parent_id: this.id,
permission_overwrites: [], permission_overwrites: [],
}), }),
}); })
.then((_) => _.json())
.then((_) => this.guild.goToChannelDelay(_.id));
} }
deleteChannel() { deleteChannel() {
fetch(this.info.api + "/channels/" + this.id, { fetch(this.info.api + "/channels/" + this.id, {

View file

@ -14,6 +14,7 @@ import {
rolesjson, rolesjson,
emojipjson, emojipjson,
extendedProperties, extendedProperties,
webhookType,
} from "./jsontypes.js"; } from "./jsontypes.js";
import {User} from "./user.js"; import {User} from "./user.js";
import {I18n} from "./i18n.js"; import {I18n} from "./i18n.js";
@ -316,6 +317,137 @@ class Guild extends SnowFlake {
genDiv(); genDiv();
emoji.addHTMLArea(containdiv); 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(); settings.show();
} }
makeInviteMenu(options: Options, valid: void | Channel[]) { makeInviteMenu(options: Options, valid: void | Channel[]) {
@ -970,6 +1102,14 @@ class Guild extends SnowFlake {
this.printServers(); this.printServers();
return thischannel; 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)) { createchannels(func = this.createChannel.bind(this)) {
const options = ["text", "announcement", "voice"].map((e) => const options = ["text", "announcement", "voice"].map((e) =>
I18n.getTranslation("channel." + e), I18n.getTranslation("channel." + e),
@ -1036,7 +1176,9 @@ class Guild extends SnowFlake {
method: "POST", method: "POST",
headers: this.headers, headers: this.headers,
body: JSON.stringify({name, type}), body: JSON.stringify({name, type}),
}); })
.then((_) => _.json())
.then((_) => this.goToChannelDelay(_.id));
} }
async createRole(name: string) { async createRole(name: string) {
const fetched = await fetch(this.info.api + "/guilds/" + this.id + "roles", { const fetched = await fetch(this.info.api + "/guilds/" + this.id + "roles", {

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 180 180"><path fill="none" stroke="red" stroke-linecap="round" stroke-linejoin="round" stroke-width="29" d="m58 165 64-75-64-75"/></svg>

After

Width:  |  Height:  |  Size: 189 B

View file

@ -148,6 +148,8 @@ type userjson = {
theme_colors: string; theme_colors: string;
pronouns?: string; pronouns?: string;
badge_ids: string[]; badge_ids: string[];
webhook?: webhookInfo;
uid?: string;
}; };
type memberjson = { type memberjson = {
index?: number; index?: number;
@ -300,6 +302,30 @@ type dirrectjson = {
recipients: userjson[]; recipients: userjson[];
is_spam: boolean; 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 = { type messagejson = {
id: string; id: string;
channel_id: string; channel_id: string;
@ -323,6 +349,7 @@ type messagejson = {
nonce: string; nonce: string;
pinned: boolean; pinned: boolean;
type: number; type: number;
webhook: webhookInfo;
}; };
type filejson = { type filejson = {
id: string; id: string;
@ -760,4 +787,6 @@ export {
opRTC12, opRTC12,
emojipjson, emojipjson,
extendedProperties, extendedProperties,
webhookInfo,
webhookType,
}; };

View file

@ -892,7 +892,7 @@ class Localuser {
if (!forceReload && this.lookingguild === guild) { if (!forceReload && this.lookingguild === guild) {
return guild; return guild;
} }
if (this.channelfocus) { if (this.channelfocus && this.lookingguild !== guild) {
this.channelfocus.infinite.delete(); this.channelfocus.infinite.delete();
this.channelfocus = undefined; this.channelfocus = undefined;
} }
@ -1070,7 +1070,11 @@ class Localuser {
headers: this.headers, headers: this.headers,
}); });
const json = await res.json(); 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 = ""; content.innerHTML = "";
const title = document.createElement("h2"); const title = document.createElement("h2");
title.textContent = I18n.getTranslation("guild.disoveryTitle", json.guilds.length + ""); title.textContent = I18n.getTranslation("guild.disoveryTitle", json.guilds.length + "");

View file

@ -386,6 +386,7 @@ class Member extends SnowFlake {
); );
} }
static async resolveMember(user: User, guild: Guild): Promise<Member | undefined> { static async resolveMember(user: User, guild: Guild): Promise<Member | undefined> {
if (user.webhook) return undefined;
const maybe = user.members.get(guild); const maybe = user.members.get(guild);
if (!user.members.has(guild)) { if (!user.members.has(guild)) {
const membpromise = guild.localuser.resolvemember(user.id, guild.id); const membpromise = guild.localuser.resolvemember(user.id, guild.id);

View file

@ -207,8 +207,11 @@ class Message extends SnowFlake {
if (messagejson.reactions?.length) { if (messagejson.reactions?.length) {
console.log(messagejson.reactions, ":3"); console.log(messagejson.reactions, ":3");
} }
console.log(messagejson.webhook);
this.author = new User(messagejson.author, this.localuser); if (messagejson.webhook) {
messagejson.author.webhook = messagejson.webhook;
}
this.author = new User(messagejson.author, this.localuser, false);
for (const thing in messagejson.mentions) { for (const thing in messagejson.mentions) {
this.mentions[thing] = new User(messagejson.mentions[thing], this.localuser); this.mentions[thing] = new User(messagejson.mentions[thing], this.localuser);
} }
@ -592,7 +595,7 @@ class Message extends SnowFlake {
if (this.author.bot) { if (this.author.bot) {
const username = document.createElement("span"); const username = document.createElement("span");
username.classList.add("bot"); username.classList.add("bot");
username.textContent = "BOT"; username.textContent = this.author.webhook ? I18n.webhook() : I18n.bot();
userwrap.appendChild(username); userwrap.appendChild(username);
} }
const time = document.createElement("span"); const time = document.createElement("span");

View file

@ -1043,6 +1043,9 @@ class Form implements OptionsElement<object> {
} }
return this.options.addSubOptions(name, {ltr, noSubmit}); return this.options.addSubOptions(name, {ltr, noSubmit});
} }
addHTMLArea(html: (() => HTMLElement) | HTMLElement, onSubmit = () => {}) {
return this.options.addHTMLArea(html, onSubmit);
}
addSubForm( addSubForm(
name: string, name: string,
onSubmit: (arg1: object, sent: object) => void, onSubmit: (arg1: object, sent: object) => void,
@ -1168,6 +1171,9 @@ class Form implements OptionsElement<object> {
addText(str: string) { addText(str: string) {
return this.options.addText(str); return this.options.addText(str);
} }
addMDText(str: string) {
return this.options.addMDText(str);
}
addHR() { addHR() {
return this.options.addHR(); return this.options.addHR();
} }
@ -1263,6 +1269,9 @@ class Form implements OptionsElement<object> {
}; };
}); });
promises.push(promise); promises.push(promise);
continue;
} else if (input.value === undefined) {
continue;
} }
} else { } else {
console.error(options.files + " is not currently implemented"); console.error(options.files + " is not currently implemented");

View file

@ -12,6 +12,22 @@ body {
height: 100svh; height: 100svh;
background: var(--primary-bg); 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 { .flexltr {
min-height: 0; min-height: 0;
display: flex; display: flex;
@ -268,6 +284,9 @@ textarea {
.svg-category { .svg-category {
mask: url(/icons/category.svg); mask: url(/icons/category.svg);
} }
.svg-intoMenu {
mask: url(/icons/intoMenu.svg);
}
.svg-channel { .svg-channel {
mask: url(/icons/channel.svg); mask: url(/icons/channel.svg);
} }
@ -2033,6 +2052,29 @@ fieldset input[type="radio"] {
.FormSettings { .FormSettings {
padding-bottom: 32px; 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, .optionElement,
.FormSettings > button { .FormSettings > button {
margin: 16px 16px 0 16px; margin: 16px 16px 0 16px;

View file

@ -4,7 +4,7 @@ import {Contextmenu} from "./contextmenu.js";
import {Localuser} from "./localuser.js"; import {Localuser} from "./localuser.js";
import {Guild} from "./guild.js"; import {Guild} from "./guild.js";
import {SnowFlake} from "./snowflake.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 {Role} from "./role.js";
import {Search} from "./search.js"; import {Search} from "./search.js";
import {I18n} from "./i18n.js"; import {I18n} from "./i18n.js";
@ -16,6 +16,7 @@ class User extends SnowFlake {
owner: Localuser; owner: Localuser;
hypotheticalpfp!: boolean; hypotheticalpfp!: boolean;
avatar!: string | null; avatar!: string | null;
uid: string;
username!: string; username!: string;
nickname: string | null = null; nickname: string | null = null;
relationshipType: 0 | 1 | 2 | 3 | 4 | 5 | 6 = 0; relationshipType: 0 | 1 | 2 | 3 | 4 | 5 | 6 = 0;
@ -24,6 +25,7 @@ class User extends SnowFlake {
pronouns?: string; pronouns?: string;
bot!: boolean; bot!: boolean;
public_flags!: number; public_flags!: number;
webhook?: webhookInfo;
accent_color!: number; accent_color!: number;
banner: string | undefined; banner: string | undefined;
hypotheticalbanner!: boolean; hypotheticalbanner!: boolean;
@ -35,7 +37,7 @@ class User extends SnowFlake {
status!: string; status!: string;
resolving: false | Promise<any> = false; resolving: false | Promise<any> = false;
constructor(userjson: userjson, owner: Localuser, dontclone = false) { constructor(userjson: userjson, owner: Localuser, dontclone: boolean = false) {
super(userjson.id); super(userjson.id);
this.owner = owner; this.owner = owner;
if (localStorage.getItem("logbad") && owner.user && owner.user.id !== userjson.id) { if (localStorage.getItem("logbad") && owner.user && owner.user.id !== userjson.id) {
@ -44,6 +46,12 @@ class User extends SnowFlake {
if (!owner) { if (!owner) {
console.error("missing localuser"); 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) { if (dontclone) {
this.userupdate(userjson); this.userupdate(userjson);
this.hypotheticalpfp = false; this.hypotheticalpfp = false;
@ -384,7 +392,7 @@ class User extends SnowFlake {
} }
static checkuser(user: User | userjson, owner: Localuser): User { 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 (tempUser) {
if (!(user instanceof User)) { if (!(user instanceof User)) {
tempUser.userupdate(user); tempUser.userupdate(user);
@ -392,7 +400,7 @@ class User extends SnowFlake {
return tempUser; return tempUser;
} else { } else {
const tempuser = new User(user as userjson, owner, true); 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; return tempuser;
} }
} }
@ -512,6 +520,7 @@ class User extends SnowFlake {
.then((member) => { .then((member) => {
User.contextmenu.bindContextmenu(html, this, member); User.contextmenu.bindContextmenu(html, this, member);
if (member === undefined && error) { if (member === undefined && error) {
if (this.webhook) return;
const errorSpan = document.createElement("span"); const errorSpan = document.createElement("span");
errorSpan.textContent = "!"; errorSpan.textContent = "!";
errorSpan.classList.add("membererror"); errorSpan.classList.add("membererror");

View file

@ -152,7 +152,22 @@
"selectName": "Name of channel", "selectName": "Name of channel",
"selectCatName": "Name of category", "selectCatName": "Name of category",
"createChannel": "Create channel", "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 ⇌", "switchAccounts": "Switch accounts ⇌",
"accountNotStart": "Account unable to start", "accountNotStart": "Account unable to start",
@ -483,5 +498,7 @@
"uploadFilesText": "Upload your files here!", "uploadFilesText": "Upload your files here!",
"errorReconnect": "Unable to connect to the server, retrying in **$1** seconds...", "errorReconnect": "Unable to connect to the server, retrying in **$1** seconds...",
"retrying": "Retrying...", "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"
} }