diff --git a/src/webpage/localuser.ts b/src/webpage/localuser.ts index e7d0fd5..bb68195 100644 --- a/src/webpage/localuser.ts +++ b/src/webpage/localuser.ts @@ -29,6 +29,7 @@ import {Emoji} from "./emoji.js"; import {Play} from "./audio/play.js"; import {Message} from "./message.js"; import {badgeArr} from "./Dbadges.js"; +import {Rights} from "./rights.js"; const wsCodesRetry = new Set([4000, 4001, 4002, 4003, 4005, 4007, 4008, 4009]); @@ -79,11 +80,13 @@ class Localuser { this.play = _; }); if (userinfo === -1) { + this.rights = new Rights(""); return; } this.token = userinfo.token; this.userinfo = userinfo; this.perminfo.guilds ??= {}; + this.perminfo.user ??= {}; this.serverurls = this.userinfo.serverurls; this.initialized = false; this.info = this.serverurls; @@ -91,6 +94,8 @@ class Localuser { "Content-type": "application/json; charset=UTF-8", Authorization: this.userinfo.token, }; + const rights = this.perminfo.user.rights || "875069521787904"; + this.rights = new Rights(rights); } async gottenReady(ready: readyjson): Promise { await I18n.done; @@ -406,6 +411,11 @@ class Localuser { await promise; } relationshipsUpdate = () => {}; + rights: Rights; + updateRights(rights: string | number) { + this.rights.update(rights); + this.perminfo.user.rights = rights; + } async handleEvent(temp: wsjson) { console.debug(temp); if (temp.s) this.lastSequence = temp.s; @@ -1776,6 +1786,135 @@ class Localuser { } }); } + if ( + this.rights.hasPermission("OPERATOR") || + this.rights.hasPermission("CREATE_REGISTRATION_TOKENS") + ) { + const manageInstance = settings.addButton(I18n.localuser.manageInstance()); + if (this.rights.hasPermission("OPERATOR")) { + manageInstance.addButtonInput("", I18n.manageInstance.stop(), () => { + const menu = new Dialog(""); + const options = menu.float.options; + options.addTitle(I18n.manageInstance.AreYouSureStop()); + const yesno = options.addOptions("", {ltr: true}); + yesno.addButtonInput("", I18n.yes(), () => { + fetch(this.info.api + "/stop", {headers: this.headers, method: "POST"}); + menu.hide(); + }); + yesno.addButtonInput("", I18n.no(), () => { + menu.hide(); + }); + menu.show(); + }); + } + if (this.rights.hasPermission("CREATE_REGISTRATION_TOKENS")) { + manageInstance.addButtonInput("", I18n.manageInstance.createTokens(), () => { + const tokens = manageInstance.addSubOptions(I18n.manageInstance.createTokens(), { + noSubmit: true, + }); + const count = tokens.addTextInput(I18n.manageInstance.count(), () => {}, { + initText: "1", + }); + const length = tokens.addTextInput(I18n.manageInstance.length(), () => {}, { + initText: "32", + }); + const format = tokens.addSelect( + I18n.manageInstance.format(), + () => {}, + [ + I18n.manageInstance.TokenFormats.JSON(), + I18n.manageInstance.TokenFormats.plain(), + I18n.manageInstance.TokenFormats.URLs(), + ], + { + defaultIndex: 2, + }, + ); + format.watchForChange((e) => { + if (e !== 2) { + urlOption.removeAll(); + } else { + makeURLMenu(); + } + }); + const urlOption = tokens.addOptions(""); + const urlOptionsJSON = { + url: window.location.origin, + type: "Jank", + }; + function makeURLMenu() { + urlOption + .addTextInput(I18n.manageInstance.clientURL(), () => {}, { + initText: urlOptionsJSON.url, + }) + .watchForChange((str) => { + urlOptionsJSON.url = str; + }); + urlOption + .addSelect( + I18n.manageInstance.regType(), + () => {}, + ["Jank", I18n.manageInstance.genericType()], + { + defaultIndex: ["Jank", "generic"].indexOf(urlOptionsJSON.type), + }, + ) + .watchForChange((i) => { + urlOptionsJSON.type = ["Jank", "generic"][i]; + }); + } + makeURLMenu(); + tokens.addButtonInput("", I18n.manageInstance.create(), async () => { + const params = new URLSearchParams(); + params.set("count", count.value); + params.set("length", length.value); + const json = (await ( + await fetch( + this.info.api + "/auth/generate-registration-tokens?" + params.toString(), + { + headers: this.headers, + }, + ) + ).json()) as {tokens: string[]}; + if (format.index === 0) { + pre.textContent = JSON.stringify(json.tokens); + } else if (format.index === 1) { + pre.textContent = json.tokens.join("\n"); + } else if (format.index === 2) { + if (urlOptionsJSON.type === "Jank") { + const options = new URLSearchParams(); + options.set("instance", this.info.wellknown); + pre.textContent = json.tokens + .map((token) => { + options.set("token", token); + return `${urlOptionsJSON.url}/register?` + options.toString(); + }) + .join("\n"); + } else { + const options = new URLSearchParams(); + pre.textContent = json.tokens + .map((token) => { + options.set("token", token); + return `${urlOptionsJSON.url}/register?` + options.toString(); + }) + .join("\n"); + } + } + }); + tokens.addButtonInput("", I18n.manageInstance.copy(), async () => { + try { + if (pre.textContent) { + await navigator.clipboard.writeText(pre.textContent); + } + } catch (err) { + console.error(err); + } + }); + const pre = document.createElement("pre"); + tokens.addHTMLArea(pre); + }); + } + } settings.show(); } readonly botTokens: Map = new Map(); diff --git a/src/webpage/login.ts b/src/webpage/login.ts index c24a51c..d931886 100644 --- a/src/webpage/login.ts +++ b/src/webpage/login.ts @@ -88,7 +88,10 @@ if (instancein) { } timeout = setTimeout(() => checkInstance((instancein as HTMLInputElement).value), 1000); }); - if (localStorage.getItem("instanceinfo")) { + if ( + localStorage.getItem("instanceinfo") && + !new URLSearchParams(window.location.search).get("instance") + ) { const json = JSON.parse(localStorage.getItem("instanceinfo")!); if (json.value) { (instancein as HTMLInputElement).value = json.value; diff --git a/src/webpage/register.ts b/src/webpage/register.ts index a3d3b08..adc3f39 100644 --- a/src/webpage/register.ts +++ b/src/webpage/register.ts @@ -42,9 +42,13 @@ async function registertry(e: Event) { const instanceInfo = JSON.parse(localStorage.getItem("instanceinfo") ?? "{}"); const apiurl = new URL(instanceInfo.api); - + let add = ""; + const token = new URLSearchParams(window.location.search).get("token"); + if (token) { + add = "?" + new URLSearchParams([["token", token]]).toString(); + } try { - const response = await fetch(apiurl + "/auth/register", { + const response = await fetch(apiurl + "/auth/register" + add, { body: JSON.stringify({ date_of_birth: dateofbirth, email, diff --git a/src/webpage/rights.ts b/src/webpage/rights.ts new file mode 100644 index 0000000..21afeae --- /dev/null +++ b/src/webpage/rights.ts @@ -0,0 +1,116 @@ +import {I18n} from "./i18n.js"; + +class Rights { + allow!: bigint; + constructor(allow: string | number) { + this.update(allow); + } + update(allow: string | number) { + try { + this.allow = BigInt(allow); + } catch { + this.allow = 875069521787904n; + console.error( + `Something really stupid happened with a permission with allow being ${allow}, execution will still happen, but something really stupid happened, please report if you know what caused this.`, + ); + } + } + getPermissionbit(b: number, big: bigint): boolean { + return Boolean((big >> BigInt(b)) & 1n); + } + setPermissionbit(b: number, state: boolean, big: bigint): bigint { + const bit = 1n << BigInt(b); + return (big & ~bit) | (BigInt(state) << BigInt(b)); //thanks to geotale for this code :3 + } + static *info(): Generator<{name: string; readableName: string; description: string}> { + throw new Error("Isn't implemented"); + for (const thing of this.permisions) { + yield { + name: thing, + readableName: I18n.getTranslation("permissions.readableNames." + thing), + description: I18n.getTranslation("permissions.descriptions." + thing), + }; + } + } + static readonly permisions = [ + "OPERATOR", + "MANAGE_APPLICATIONS", + "MANAGE_GUILDS", + "MANAGE_MESSAGES", + "MANAGE_RATE_LIMITS", + "MANAGE_ROUTING", + "MANAGE_TICKETS", + "MANAGE_USERS", + "ADD_MEMBERS", + "BYPASS_RATE_LIMITS", + "CREATE_APPLICATIONS", + "CREATE_CHANNELS", + "CREATE_DMS", + "CREATE_DM_GROUPS", + "CREATE_GUILDS", + "CREATE_INVITES", + "CREATE_ROLES", + "CREATE_TEMPLATES", + "CREATE_WEBHOOKS", + "JOIN_GUILDS", + "PIN_MESSAGES", + "SELF_ADD_REACTIONS", + "SELF_DELETE_MESSAGES", + "SELF_EDIT_MESSAGES", + "SELF_EDIT_NAME", + "SEND_MESSAGES", + "USE_ACTIVITIES", + "USE_VIDEO", + "USE_VOICE", + "INVITE_USERS", + "SELF_DELETE_DISABLE", + "DEBTABLE", + "CREDITABLE", + "KICK_BAN_MEMBERS", + "SELF_LEAVE_GROUPS", + "PRESENCE", + "SELF_ADD_DISCOVERABLE", + "MANAGE_GUILD_DIRECTORY", + "POGGERS", + "USE_ACHIEVEMENTS", + "INITIATE_INTERACTIONS", + "RESPOND_TO_INTERACTIONS", + "SEND_BACKDATED_EVENTS", + "USE_MASS_INVITES", + "ACCEPT_INVITES", + "SELF_EDIT_FLAGS", + "EDIT_FLAGS", + "MANAGE_GROUPS", + "VIEW_SERVER_STATS", + "RESEND_VERIFICATION_EMAIL", + "CREATE_REGISTRATION_TOKENS", + ]; + getPermission(name: string): boolean { + if (undefined === Rights.permisions.indexOf(name)) { + console.error(name + " is not found in map", Rights.permisions); + } + return this.getPermissionbit(Rights.permisions.indexOf(name), this.allow); + } + hasPermission(name: string, adminOverride = true): boolean { + if (this.getPermissionbit(Rights.permisions.indexOf(name), this.allow)) return true; + if (name !== "OPERATOR" && adminOverride) return this.hasPermission("OPERATOR"); + return false; + } + setPermission(name: string, setto: number): void { + const bit = Rights.permisions.indexOf(name); + if (bit === undefined) { + return console.error( + "Tried to set permission to " + setto + " for " + name + " but it doesn't exist", + ); + } + + if (setto === 0) { + this.allow = this.setPermissionbit(bit, false, this.allow); + } else if (setto === 1) { + this.allow = this.setPermissionbit(bit, true, this.allow); + } else { + console.error("invalid number entered:" + setto); + } + } +} +export {Rights}; diff --git a/src/webpage/role.ts b/src/webpage/role.ts index d39c050..968baa3 100644 --- a/src/webpage/role.ts +++ b/src/webpage/role.ts @@ -4,6 +4,11 @@ import {Guild} from "./guild.js"; import {SnowFlake} from "./snowflake.js"; import {rolesjson} from "./jsontypes.js"; import {Search} from "./search.js"; +import {OptionsElement, Buttons} from "./settings.js"; +import {Contextmenu} from "./contextmenu.js"; +import {Channel} from "./channel.js"; +import {I18n} from "./i18n.js"; + class Role extends SnowFlake { permissions: Permissions; owner: Guild; @@ -135,10 +140,7 @@ class PermissionToggle implements OptionsElement { } submit() {} } -import {OptionsElement, Buttons} from "./settings.js"; -import {Contextmenu} from "./contextmenu.js"; -import {Channel} from "./channel.js"; -import {I18n} from "./i18n.js"; + class RoleList extends Buttons { permissions: [Role, Permissions][]; permission: Permissions; diff --git a/src/webpage/user.ts b/src/webpage/user.ts index 7ed3554..6d47bb3 100644 --- a/src/webpage/user.ts +++ b/src/webpage/user.ts @@ -10,6 +10,7 @@ import {Search} from "./search.js"; import {I18n} from "./i18n.js"; import {Direct} from "./direct.js"; import {Hover} from "./hover.js"; +import {Dialog} from "./settings.js"; class User extends SnowFlake { owner: Localuser; @@ -69,6 +70,10 @@ class User extends SnowFlake { "totp_secret", "webauthn_enabled", ]); + if (!this.localuser.rights.getPermission("OPERATOR")) { + //Unless the user is an operator, we really shouldn't ever see this + bad.add("rights"); + } for (const thing of bad) { if (json.hasOwnProperty(thing)) { console.error(thing + " should not be exposed to the client"); @@ -346,6 +351,36 @@ class User extends SnowFlake { navigator.clipboard.writeText(this.id); }, ); + + this.contextmenu.addSeperator(); + + this.contextmenu.addButton( + () => I18n.user.instanceBan(), + function (this: User) { + const menu = new Dialog(""); + const options = menu.float.options; + options.addTitle(I18n.user.confirmInstBan(this.name)); + const opt = options.addOptions("", {ltr: true}); + opt.addButtonInput("", I18n.yes(), () => { + fetch(this.info.api + "/users/" + this.id, { + headers: this.localuser.headers, + method: "POST", + }); + menu.hide(); + }); + opt.addButtonInput("", I18n.no(), () => { + menu.hide(); + }); + menu.show(); + }, + { + visable: function () { + return this.localuser.rights.hasPermission("MANAGE_USERS"); + }, + color: "red", + }, + ); + console.warn("this ran"); } static checkuser(user: User | userjson, owner: Localuser): User { @@ -461,6 +496,14 @@ class User extends SnowFlake { } (this as any)[key] = (json as any)[key]; } + if ("rights" in this) { + if ( + this === this.localuser.user && + (typeof this.rights == "string" || typeof this.rights == "number") + ) { + this.localuser.updateRights(this.rights); + } + } } bind(html: HTMLElement, guild: Guild | null = null, error = true): void { diff --git a/translations/en.json b/translations/en.json index b95739c..31542e2 100644 --- a/translations/en.json +++ b/translations/en.json @@ -321,7 +321,26 @@ "areYouSureDelete": "Are you sure you want to delete your account? If so enter the phrase $1", "sillyDeleteConfirmPhrase": "Shrek is love, Shrek is life", "deleteAccountButton": "Delete account", - "mustTypePhrase": "To delete your account you must type the phrase" + "mustTypePhrase": "To delete your account you must type the phrase", + "manageInstance": "Manage Instance" + }, + "manageInstance": { + "stop": "Stop instance", + "AreYouSureStop": "Are you sure you want to stop this instance?", + "createTokens": "Create Registration Tokens", + "count": "Count:", + "length": "Length:", + "format": "Format:", + "TokenFormats": { + "plain": "Plain", + "JSON": "JSON formatted", + "URLs": "Invite URLs" + }, + "create": "Create", + "clientURL": "Client URL:", + "regType": "Register token URL type", + "genericType": "Generic", + "copy": "Copy" }, "message": { "reactionAdd": "Add reaction", @@ -403,7 +422,9 @@ "ban": "Ban member", "addRole": "Add roles", "removeRole": "Remove roles", - "editServerProfile": "Edit server profile" + "editServerProfile": "Edit server profile", + "instanceBan": "Instance ban", + "confirmInstBan": "Are you sure you want to instance ban $1?" }, "login": { "checking": "Checking Instance", @@ -453,6 +474,8 @@ "name:": "Name:", "confirmDel": "Are you sure you want to delete this emoji?" }, + "incorrectURLS": "## This instance has likely sent the incorrect URLs.\n### If you're the instance owner please see [here](https://docs.spacebar.chat/setup/server/) under *Connecting from remote machines* to correct the issue.\n Would you like Jank Client to automatically try to fix this error to let you connect to the instance?", + "uploadFilesText": "Upload your files here!", "errorReconnect": "Unable to connect to the server, retrying in **$1** seconds...", "retrying": "Retrying...",