import {Guild} from "./guild.js"; import {Channel} from "./channel.js"; import {Direct} from "./direct.js"; import {AVoice} from "./audio/voice.js"; import {User} from "./user.js"; import {getapiurls, SW} from "./utils/utils.js"; import {getBulkInfo, setTheme, Specialuser} from "./utils/utils.js"; import { channeljson, guildjson, mainuserjson, memberjson, memberlistupdatejson, messageCreateJson, messagejson, presencejson, readyjson, startTypingjson, wsjson, } from "./jsontypes.js"; import {Member} from "./member.js"; import {Dialog, Form, FormError, Options, Settings} from "./settings.js"; import {getTextNodeAtPosition, MarkDown} from "./markdown.js"; import {Bot} from "./bot.js"; import {Role} from "./role.js"; import {VoiceFactory} from "./voice.js"; import {I18n, langmap} from "./i18n.js"; 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]); class Localuser { badges = new Map< string, {id: string; description: string; icon: string; link?: string; translate?: boolean} >( badgeArr as [ string, {id: string; description: string; icon: string; link?: string; translate?: boolean}, ][], ); lastSequence: number | null = null; token!: string; userinfo!: Specialuser; serverurls!: Specialuser["serverurls"]; initialized!: boolean; info!: Specialuser["serverurls"]; headers!: {"Content-type": string; Authorization: string}; ready!: readyjson; guilds!: Guild[]; guildids: Map = new Map(); user!: User; status!: string; channelfocus: Channel | undefined; lookingguild: Guild | undefined; guildhtml: Map = new Map(); ws: WebSocket | undefined; connectionSucceed = 0; errorBackoff = 0; channelids: Map = new Map(); readonly userMap: Map = new Map(); voiceFactory?: VoiceFactory; play?: Play; instancePing = { name: "Unknown", }; mfa_enabled!: boolean; get perminfo() { return this.userinfo.localuserStore; } set perminfo(e) { this.userinfo.localuserStore = e; } constructor(userinfo: Specialuser | -1) { Play.playURL("/audio/sounds.jasf").then((_) => { 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; this.headers = { "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; this.initialized = true; this.ready = ready; this.guilds = []; this.guildids = new Map(); this.user = new User(ready.d.user, this); this.user.setstatus("online"); this.resume_gateway_url = ready.d.resume_gateway_url; this.session_id = ready.d.session_id; this.mdBox(); this.voiceFactory = new VoiceFactory({id: this.user.id}); this.handleVoice(); this.mfa_enabled = ready.d.user.mfa_enabled as boolean; this.userinfo.username = this.user.username; this.userinfo.id = this.user.id; this.userinfo.pfpsrc = this.user.getpfpsrc(); this.status = this.ready.d.user_settings.status; this.channelfocus = undefined; this.lookingguild = undefined; this.guildhtml = new Map(); const members: {[key: string]: memberjson} = {}; if (ready.d.merged_members) { for (const thing of ready.d.merged_members) { members[thing[0].guild_id] = thing[0]; } } for (const thing of ready.d.guilds) { const temp = new Guild(thing, this, members[thing.id]); this.guilds.push(temp); this.guildids.set(temp.id, temp); } { const temp = new Direct(ready.d.private_channels, this); this.guilds.push(temp); this.guildids.set(temp.id, temp); } if (ready.d.user_guild_settings) { console.log(ready.d.user_guild_settings.entries); for (const thing of ready.d.user_guild_settings.entries) { (this.guildids.get(thing.guild_id) as Guild).notisetting(thing); } } if (ready.d.read_state) { for (const thing of ready.d.read_state.entries) { const channel = this.channelids.get(thing.channel_id); if (!channel) { continue; } channel.readStateInfo(thing); } } for (const thing of ready.d.relationships) { const user = new User(thing.user, this); user.nickname = thing.nickname; user.relationshipType = thing.type; this.inrelation.add(user); } this.pingEndpoint(); } inrelation = new Set(); outoffocus(): void { const servers = document.getElementById("servers") as HTMLDivElement; servers.innerHTML = ""; const channels = document.getElementById("channels") as HTMLDivElement; channels.innerHTML = ""; if (this.channelfocus) { this.channelfocus.infinite.delete(); } this.lookingguild = undefined; this.channelfocus = undefined; } unload(): void { this.initialized = false; this.outoffocus(); this.guilds = []; this.guildids = new Map(); if (this.ws) { this.ws.close(4040); } } swapped = false; resume_gateway_url?: string; session_id?: string; async initwebsocket(resume = false): Promise { let returny: () => void; if (!this.resume_gateway_url || !this.session_id) { resume = false; } const ws = new WebSocket( (resume ? this.resume_gateway_url : this.serverurls.gateway.toString()) + "?encoding=json&v=9" + (DecompressionStream ? "&compress=zlib-stream" : ""), ); this.ws = ws; let ds: DecompressionStream; let w: WritableStreamDefaultWriter; let r: ReadableStreamDefaultReader; let arr: Uint8Array; let build = ""; if (DecompressionStream) { ds = new DecompressionStream("deflate"); w = ds.writable.getWriter(); r = ds.readable.getReader(); arr = new Uint8Array(); } const promise = new Promise((res) => { returny = res; ws.addEventListener("open", (_event) => { console.log("WebSocket connected"); if (resume) { ws.send( JSON.stringify({ op: 6, d: { token: this.token, session_id: this.session_id, seq: this.lastSequence, }, }), ); this.resume_gateway_url = undefined; this.session_id = undefined; } else { ws.send( JSON.stringify({ op: 2, d: { token: this.token, capabilities: 16381, properties: { browser: "Jank Client", client_build_number: 0, //might update this eventually lol release_channel: "Custom", browser_user_agent: navigator.userAgent, }, compress: Boolean(DecompressionStream), presence: { status: "online", since: null, //new Date().getTime() activities: [], afk: false, }, }, }), ); } }); const textdecode = new TextDecoder(); if (DecompressionStream) { (async () => { while (true) { const read = await r.read(); const data = textdecode.decode(read.value); build += data; try { const temp = JSON.parse(build); build = ""; await this.handleEvent(temp); if (temp.op === 0 && temp.t === "READY") { returny(); } } catch {} } })(); } }); let order = new Promise((res) => res()); ws.addEventListener("message", async (event) => { const temp2 = order; order = new Promise(async (res) => { await temp2; let temp: {op: number; t: string}; try { if (event.data instanceof Blob) { const buff = await event.data.arrayBuffer(); const array = new Uint8Array(buff); const temparr = new Uint8Array(array.length + arr.length); temparr.set(arr, 0); temparr.set(array, arr.length); arr = temparr; const len = array.length; if ( !( array[len - 1] === 255 && array[len - 2] === 255 && array[len - 3] === 0 && array[len - 4] === 0 ) ) { return; } w.write(arr.buffer); arr = new Uint8Array(); return; //had to move the while loop due to me being dumb } else { temp = JSON.parse(event.data); } await this.handleEvent(temp as readyjson); if (temp.op === 0 && temp.t === "READY") { returny(); } } catch (e) { console.error(e); } finally { res(); } }); }); ws.addEventListener("close", async (event) => { this.ws = undefined; console.log("WebSocket closed with code " + event.code); if ( (event.code > 1000 && event.code < 1016 && this.errorBackoff === 0) || (wsCodesRetry.has(event.code) && this.errorBackoff === 0) ) { this.errorBackoff++; this.initwebsocket(true).then(() => { this.loaduser(); }); return; } this.unload(); (document.getElementById("loading") as HTMLElement).classList.remove("doneloading"); (document.getElementById("loading") as HTMLElement).classList.add("loading"); this.fetchingmembers = new Map(); this.noncemap = new Map(); this.noncebuild = new Map(); if ( (event.code > 1000 && event.code < 1016) || wsCodesRetry.has(event.code) || event.code == 4041 ) { if (this.connectionSucceed !== 0 && Date.now() > this.connectionSucceed + 20000) { this.errorBackoff = 0; } else this.errorBackoff++; this.connectionSucceed = 0; const loaddesc = document.getElementById("load-desc") as HTMLElement; loaddesc.innerHTML = ""; loaddesc.append( new MarkDown( I18n.getTranslation("errorReconnect", Math.round(0.2 + this.errorBackoff * 2.8) + ""), ).makeHTML(), ); switch ( this.errorBackoff //try to recover from bad domain ) { case 3: const newurls = await getapiurls(this.info.wellknown); if (newurls) { this.info = newurls; this.serverurls = newurls; this.userinfo.json.serverurls = this.info; break; } break; case 4: { const newurls = await getapiurls(new URL(this.info.wellknown).origin); if (newurls) { this.info = newurls; this.serverurls = newurls; this.userinfo.json.serverurls = this.info; break; } break; } case 5: { const breakappart = new URL(this.info.wellknown).origin.split("."); const url = "https://" + breakappart.at(-2) + "." + breakappart.at(-1); const newurls = await getapiurls(url); if (newurls) { this.info = newurls; this.serverurls = newurls; this.userinfo.json.serverurls = this.info; } break; } } setTimeout( () => { if (this.swapped) return; (document.getElementById("load-desc") as HTMLElement).textContent = I18n.getTranslation("retrying"); this.initwebsocket().then(() => { this.loaduser(); this.init(); const loading = document.getElementById("loading") as HTMLElement; loading.classList.add("doneloading"); loading.classList.remove("loading"); console.log("done loading"); }); }, 200 + this.errorBackoff * 2800, ); } else (document.getElementById("load-desc") as HTMLElement).textContent = I18n.getTranslation("unableToConnect"); }); 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; if (temp.op === 9 && this.ws) { this.errorBackoff = 0; this.ws.close(4041); } if (temp.op == 0) { switch (temp.t) { case "MESSAGE_CREATE": if (this.initialized) { this.messageCreate(temp); } break; case "MESSAGE_DELETE": { temp.d.guild_id ??= "@me"; const channel = this.channelids.get(temp.d.channel_id); if (!channel) break; const message = channel.messages.get(temp.d.id); if (!message) break; message.deleteEvent(); break; } case "READY": await this.gottenReady(temp as readyjson); break; case "MESSAGE_UPDATE": { temp.d.guild_id ??= "@me"; const channel = this.channelids.get(temp.d.channel_id); if (!channel) break; const message = channel.messages.get(temp.d.id); if (!message) break; message.giveData(temp.d); break; } case "TYPING_START": if (this.initialized) { this.typingStart(temp); } break; case "USER_UPDATE": if (this.initialized) { const users = this.userMap.get(temp.d.id); if (users) { users.userupdate(temp.d); } } break; case "CHANNEL_UPDATE": if (this.initialized) { this.updateChannel(temp.d); } break; case "CHANNEL_CREATE": if (this.initialized) { this.createChannel(temp.d); } break; case "CHANNEL_DELETE": if (this.initialized) { this.delChannel(temp.d); } break; case "GUILD_DELETE": { const guildy = this.guildids.get(temp.d.id); if (guildy) { this.guildids.delete(temp.d.id); this.guilds.splice(this.guilds.indexOf(guildy), 1); guildy.html.remove(); } break; } case "GUILD_UPDATE": { const guildy = this.guildids.get(temp.d.id); if (guildy) { guildy.update(temp.d); } break; } case "GUILD_CREATE": (async () => { const guildy = new Guild(temp.d, this, this.user); this.guilds.push(guildy); this.guildids.set(guildy.id, guildy); const divy = guildy.generateGuildIcon(); guildy.HTMLicon = divy; (document.getElementById("servers") as HTMLDivElement).insertBefore( divy, document.getElementById("bottomseparator"), ); })(); break; case "MESSAGE_REACTION_ADD": { temp.d.guild_id ??= "@me"; const guild = this.guildids.get(temp.d.guild_id); if (!guild) break; const channel = this.channelids.get(temp.d.channel_id); if (!channel) break; const message = channel.messages.get(temp.d.message_id); if (!message) break; let thing: Member | {id: string}; if (temp.d.member) { thing = (await Member.new(temp.d.member, guild)) as Member; } else { thing = {id: temp.d.user_id}; } message.reactionAdd(temp.d.emoji, thing); } break; case "MESSAGE_REACTION_REMOVE": { temp.d.guild_id ??= "@me"; const channel = this.channelids.get(temp.d.channel_id); if (!channel) break; const message = channel.messages.get(temp.d.message_id); if (!message) break; message.reactionRemove(temp.d.emoji, temp.d.user_id); } break; case "MESSAGE_REACTION_REMOVE_ALL": { temp.d.guild_id ??= "@me"; const channel = this.channelids.get(temp.d.channel_id); if (!channel) break; const message = channel.messages.get(temp.d.message_id); if (!message) break; message.reactionRemoveAll(); } break; case "MESSAGE_REACTION_REMOVE_EMOJI": { temp.d.guild_id ??= "@me"; const channel = this.channelids.get(temp.d.channel_id); if (!channel) break; const message = channel.messages.get(temp.d.message_id); if (!message) break; message.reactionRemoveEmoji(temp.d.emoji); } break; case "GUILD_MEMBERS_CHUNK": this.gotChunk(temp.d); break; case "GUILD_MEMBER_LIST_UPDATE": { this.memberListUpdate(temp); break; } case "VOICE_STATE_UPDATE": if (this.voiceFactory) { this.voiceFactory.voiceStateUpdate(temp); } break; case "VOICE_SERVER_UPDATE": if (this.voiceFactory) { this.voiceFactory.voiceServerUpdate(temp); } break; case "GUILD_ROLE_CREATE": { const guild = this.guildids.get(temp.d.guild_id); if (!guild) break; guild.newRole(temp.d.role); break; } case "GUILD_ROLE_UPDATE": { const guild = this.guildids.get(temp.d.guild_id); if (!guild) break; guild.updateRole(temp.d.role); break; } case "GUILD_ROLE_DELETE": { const guild = this.guildids.get(temp.d.guild_id); if (!guild) break; guild.deleteRole(temp.d.role_id); break; } case "GUILD_MEMBER_UPDATE": { const guild = this.guildids.get(temp.d.guild_id); if (!guild) break; guild.memberupdate(temp.d); break; } case "RELATIONSHIP_ADD": { const user = new User(temp.d.user, this); user.nickname = null; user.relationshipType = temp.d.type; this.inrelation.add(user); this.relationshipsUpdate(); const me = this.guildids.get("@me"); if (!me) break; me.unreads(); break; } case "RELATIONSHIP_REMOVE": { const user = this.userMap.get(temp.d.id); if (!user) return; user.nickname = null; user.relationshipType = 0; this.inrelation.delete(user); this.relationshipsUpdate(); break; } case "PRESENCE_UPDATE": { if (temp.d.user) { this.presences.set(temp.d.user.id, temp.d); } break; } case "GUILD_MEMBER_ADD": { const guild = this.guildids.get(temp.d.guild_id); if (!guild) break; Member.new(temp.d, guild); break; } case "GUILD_MEMBER_REMOVE": { const guild = this.guildids.get(temp.d.guild_id); if (!guild) break; const user = new User(temp.d.user, this); const member = user.members.get(guild); if (!(member instanceof Member)) break; member.remove(); break; } case "GUILD_EMOJIS_UPDATE": { const guild = this.guildids.get(temp.d.guild_id); if (!guild) break; guild.emojis = temp.d.emojis; guild.onEmojiUpdate(guild.emojis); break; } default: { //@ts-ignore console.warn("Unhandled case " + temp.t, temp); } } } else if (temp.op === 10) { if (!this.ws) return; console.log("heartbeat down"); this.heartbeat_interval = temp.d.heartbeat_interval; this.ws.send(JSON.stringify({op: 1, d: this.lastSequence})); } else if (temp.op === 11) { setTimeout((_: any) => { if (!this.ws) return; if (this.connectionSucceed === 0) this.connectionSucceed = Date.now(); this.ws.send(JSON.stringify({op: 1, d: this.lastSequence})); }, this.heartbeat_interval); } else { console.log("Unhandled case " + temp.d, temp); } } get currentVoice() { return this.voiceFactory?.currentVoice; } async joinVoice(channel: Channel) { if (!this.voiceFactory) return; if (!this.ws) return; this.ws.send(JSON.stringify(this.voiceFactory.joinVoice(channel.id, channel.guild.id))); return undefined; } changeVCStatus(status: string) { const statuselm = document.getElementById("VoiceStatus"); if (!statuselm) throw new Error("Missing status element"); statuselm.textContent = status; } handleVoice() { if (this.voiceFactory) { this.voiceFactory.onJoin = (voice) => { voice.onSatusChange = (status) => { this.changeVCStatus(status); }; }; } } heartbeat_interval: number = 0; updateChannel(json: channeljson): void { const guild = this.guildids.get(json.guild_id); if (guild) { guild.updateChannel(json); if (json.guild_id === this.lookingguild?.id) { this.loadGuild(json.guild_id); } } } createChannel(json: channeljson): undefined | Channel { json.guild_id ??= "@me"; const guild = this.guildids.get(json.guild_id); if (!guild) return; const channel = guild.createChannelpac(json); if (json.guild_id === this.lookingguild?.id) { this.loadGuild(json.guild_id, true); } if (channel.id === this.gotoid) { guild.loadGuild(); guild.loadChannel(channel.id); this.gotoid = undefined; } return channel; // Add this line to return the 'channel' variable } async memberListUpdate(list: memberlistupdatejson | void) { if (this.searching) return; const div = document.getElementById("sideDiv") as HTMLDivElement; div.innerHTML = ""; div.classList.remove("searchDiv"); const guild = this.lookingguild; if (!guild) return; const channel = this.channelfocus; if (!channel) return; if (list) { const counts = new Map(); for (const thing of list.d.ops[0].items) { if ("member" in thing) { await Member.new(thing.member, guild); } else { counts.set(thing.group.id, thing.group.count); } } } const elms: Map = new Map([]); for (const role of guild.roles) { if (role.hoist) { elms.set(role, []); } } elms.set("online", []); elms.set("offline", []); const members = new Set(guild.members); members.forEach((member) => { if (!channel.hasPermission("VIEW_CHANNEL", member)) { members.delete(member); console.log(member, "can't see"); return; } }); for (const [role, list] of elms) { members.forEach((member) => { if (role === "offline") { if (member.user.getStatus() === "offline") { list.push(member); members.delete(member); } return; } if (member.user.getStatus() === "offline") { return; } if (role !== "online" && member.hasRole(role.id)) { list.push(member); members.delete(member); } }); if (!list.length) continue; list.sort((a, b) => { return a.name.toLowerCase() > b.name.toLowerCase() ? 1 : -1; }); } const online = [...members]; online.sort((a, b) => { return a.name.toLowerCase() > b.name.toLowerCase() ? 1 : -1; }); elms.set("online", online); for (const [role, list] of elms) { if (!list.length) continue; const category = document.createElement("div"); category.classList.add("memberList"); let title = document.createElement("h3"); if (role === "offline") { title.textContent = I18n.getTranslation("user.offline"); category.classList.add("offline"); } else if (role === "online") { title.textContent = I18n.getTranslation("user.online"); } else { title.textContent = role.name; } category.append(title); const membershtml = document.createElement("div"); membershtml.classList.add("flexttb"); for (const member of list) { const memberdiv = document.createElement("div"); const pfp = await member.user.buildstatuspfp(member); const username = document.createElement("span"); username.classList.add("ellipsis"); username.textContent = member.name; member.bind(username); member.user.bind(memberdiv, member.guild, false); memberdiv.append(pfp, username); memberdiv.classList.add("flexltr", "liststyle", "memberListStyle"); membershtml.append(memberdiv); } category.append(membershtml); div.append(category); } console.log(elms); } async getSidePannel() { if (this.ws && this.channelfocus) { console.log(this.channelfocus.guild.id); if (this.channelfocus.guild.id === "@me") { this.memberListUpdate(); return; } this.ws.send( JSON.stringify({ d: { channels: {[this.channelfocus.id]: [[0, 99]]}, guild_id: this.channelfocus.guild.id, }, op: 14, }), ); } else { console.log("false? :3"); } } gotoid: string | undefined; async goToChannel(id: string, addstate = true) { const channel = this.channelids.get(id); if (channel) { const guild = channel.guild; guild.loadGuild(); guild.loadChannel(id, addstate); } else { this.gotoid = id; } } delChannel(json: channeljson): void { let guild_id = json.guild_id; guild_id ??= "@me"; const guild = this.guildids.get(guild_id); if (guild) { guild.delChannel(json); } if (json.guild_id === this.lookingguild?.id) { this.loadGuild(json.guild_id, true); } } init(): void { const location = window.location.href.split("/"); this.buildservers(); if (location[3] === "channels") { const guild = this.loadGuild(location[4]); if (!guild) { return; } guild.loadChannel(location[5]); this.channelfocus = this.channelids.get(location[5]); } } loaduser(): void { (document.getElementById("username") as HTMLSpanElement).textContent = this.user.username; (document.getElementById("userpfp") as HTMLImageElement).src = this.user.getpfpsrc(); (document.getElementById("status") as HTMLSpanElement).textContent = this.status; } isAdmin(): boolean { if (this.lookingguild) { return this.lookingguild.isAdmin(); } else { return false; } } loadGuild(id: string, forceReload = false): Guild | undefined { this.searching = false; let guild = this.guildids.get(id); if (!guild) { guild = this.guildids.get("@me"); } console.log(forceReload); if (!forceReload && this.lookingguild === guild) { return guild; } if (this.channelfocus && this.lookingguild !== guild) { this.channelfocus.infinite.delete(); this.channelfocus = undefined; } if (this.lookingguild) { this.lookingguild.html.classList.remove("serveropen"); } if (!guild) return; if (guild.html) { guild.html.classList.add("serveropen"); } this.lookingguild = guild; (document.getElementById("serverName") as HTMLElement).textContent = guild.properties.name; const banner = document.getElementById("servertd"); console.log(guild.banner, banner); if (banner) { if (guild.banner) { //https://cdn.discordapp.com/banners/677271830838640680/fab8570de5bb51365ba8f36d7d3627ae.webp?size=240 banner.style.setProperty( "background-image", `linear-gradient(rgba(0, 0, 0, 1) 0%, rgba(0, 0, 0, 0) 40%), url(${this.info.cdn}/banners/${guild.id}/${guild.banner})`, ); banner.classList.add("Banner"); //background-image: } else { banner.style.removeProperty("background-image"); banner.classList.remove("Banner"); } if (guild.id !== "@me") { banner.style.setProperty("cursor", `pointer`); banner.onclick = (e) => { e.preventDefault(); e.stopImmediatePropagation(); const box = banner.getBoundingClientRect(); Guild.contextmenu.makemenu(box.left + 16, box.bottom + 5, guild, undefined); }; } else { banner.style.removeProperty("cursor"); banner.onclick = () => {}; } } //console.log(this.guildids,id) const channels = document.getElementById("channels") as HTMLDivElement; channels.innerHTML = ""; const html = guild.getHTML(); channels.appendChild(html); return guild; } buildservers(): void { const serverlist = document.getElementById("servers") as HTMLDivElement; // const outdiv = document.createElement("div"); const home: any = document.createElement("span"); const div = document.createElement("div"); div.classList.add("home", "servericon"); home.classList.add("svgicon", "svg-home"); (this.guildids.get("@me") as Guild).html = outdiv; const unread = document.createElement("div"); unread.classList.add("unread"); outdiv.append(unread); outdiv.append(div); div.appendChild(home); outdiv.classList.add("servernoti"); serverlist.append(outdiv); home.onclick = () => { const guild = this.guildids.get("@me"); if (!guild) return; guild.loadGuild(); guild.loadChannel(); }; const sentdms = document.createElement("div"); sentdms.classList.add("sentdms"); serverlist.append(sentdms); sentdms.id = "sentdms"; const br = document.createElement("hr"); br.classList.add("lightbr"); serverlist.appendChild(br); for (const thing of this.guilds) { if (thing instanceof Direct) { (thing as Direct).unreaddms(); continue; } const divy = thing.generateGuildIcon(); thing.HTMLicon = divy; serverlist.append(divy); } { const br = document.createElement("hr"); br.classList.add("lightbr"); serverlist.appendChild(br); br.id = "bottomseparator"; const div = document.createElement("div"); const plus = document.createElement("span"); plus.classList.add("svgicon", "svg-plus"); div.classList.add("home", "servericon"); div.appendChild(plus); serverlist.appendChild(div); div.onclick = (_) => { this.createGuild(); }; const guilddsdiv = document.createElement("div"); const guildDiscoveryContainer = document.createElement("span"); guildDiscoveryContainer.classList.add("svgicon", "svg-explore"); guilddsdiv.classList.add("home", "servericon"); guilddsdiv.appendChild(guildDiscoveryContainer); serverlist.appendChild(guilddsdiv); guildDiscoveryContainer.addEventListener("click", () => { this.guildDiscovery(); }); } this.unreads(); } createGuild() { const full = new Dialog(""); const buttons = full.options.addButtons("", {top: true}); const viacode = buttons.add(I18n.getTranslation("invite.joinUsing")); { const form = viacode.addForm("", async (e: any) => { let parsed = ""; if (e.code.includes("/")) { parsed = e.code.split("/")[e.code.split("/").length - 1]; } else { parsed = e.code; } const json = await ( await fetch(this.info.api + "/invites/" + parsed, { method: "POST", headers: this.headers, }) ).json(); if (json.message) { throw new FormError(text, json.message); } full.hide(); }); const text = form.addTextInput(I18n.getTranslation("invite.inviteLinkCode"), "code"); } const guildcreate = buttons.add(I18n.getTranslation("guild.create")); { const form = guildcreate.addForm("", (fields: any) => { this.makeGuild(fields).then((_) => { if (_.message) { alert(_.errors.name._errors[0].message); } else { full.hide(); } }); }); form.addFileInput(I18n.getTranslation("guild.icon:"), "icon", {files: "one"}); form.addTextInput(I18n.getTranslation("guild.name:"), "name", {required: true}); } full.show(); } async makeGuild(fields: {name: string; icon: string | null}) { return await ( await fetch(this.info.api + "/guilds", { method: "POST", headers: this.headers, body: JSON.stringify(fields), }) ).json(); } async guildDiscovery() { const content = document.createElement("div"); content.classList.add("flexttb", "guildy"); content.textContent = I18n.getTranslation("guild.loadingDiscovery"); const full = new Dialog(""); full.options.addHTMLArea(content); full.show(); const res = await fetch(this.info.api + "/discoverable-guilds?limit=50", { 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 + ""); content.appendChild(title); const guilds = document.createElement("div"); guilds.id = "discovery-guild-content"; json.guilds.forEach((guild: guildjson["properties"]) => { const content = document.createElement("div"); content.classList.add("discovery-guild"); if (guild.banner) { const banner = document.createElement("img"); banner.classList.add("banner"); banner.crossOrigin = "anonymous"; banner.src = this.info.cdn + "/icons/" + guild.id + "/" + guild.banner + ".png?size=256"; banner.alt = ""; content.appendChild(banner); } const nameContainer = document.createElement("div"); nameContainer.classList.add("flex"); const img = document.createElement("img"); img.classList.add("icon"); img.crossOrigin = "anonymous"; img.src = this.info.cdn + (guild.icon ? "/icons/" + guild.id + "/" + guild.icon + ".png?size=48" : "/embed/avatars/3.png"); img.alt = ""; nameContainer.appendChild(img); const name = document.createElement("h3"); name.textContent = guild.name; nameContainer.appendChild(name); content.appendChild(nameContainer); const desc = document.createElement("p"); desc.textContent = guild.description; content.appendChild(desc); content.addEventListener("click", async () => { const joinRes = await fetch(this.info.api + "/guilds/" + guild.id + "/members/@me", { method: "PUT", headers: this.headers, }); if (joinRes.ok) full.hide(); }); guilds.appendChild(content); }); content.appendChild(guilds); } messageCreate(messagep: messageCreateJson): void { messagep.d.guild_id ??= "@me"; const channel = this.channelids.get(messagep.d.channel_id); if (channel) { channel.messageCreate(messagep); this.unreads(); } } unreads(): void { for (const thing of this.guilds) { if (thing.id === "@me") { continue; } const html = this.guildhtml.get(thing.id); thing.unreads(html); } } async typingStart(typing: startTypingjson): Promise { const channel = this.channelids.get(typing.d.channel_id); if (!channel) return; channel.typingStart(typing); } updatepfp(file: Blob): void { const reader = new FileReader(); reader.readAsDataURL(file); reader.onload = () => { fetch(this.info.api + "/users/@me", { method: "PATCH", headers: this.headers, body: JSON.stringify({ avatar: reader.result, }), }); }; } updatebanner(file: Blob | null): void { if (file) { const reader = new FileReader(); reader.readAsDataURL(file); reader.onload = () => { fetch(this.info.api + "/users/@me", { method: "PATCH", headers: this.headers, body: JSON.stringify({ banner: reader.result, }), }); }; } else { fetch(this.info.api + "/users/@me", { method: "PATCH", headers: this.headers, body: JSON.stringify({ banner: null, }), }); } } updateProfile(json: {bio?: string; pronouns?: string; accent_color?: number}) { fetch(this.info.api + "/users/@me/profile", { method: "PATCH", headers: this.headers, body: JSON.stringify(json), }); } async showusersettings() { const settings = new Settings(I18n.getTranslation("localuser.settings")); { const userOptions = settings.addButton(I18n.getTranslation("localuser.userSettings"), { ltr: true, }); const hypotheticalProfile = document.createElement("div"); let file: undefined | File | null; let newpronouns: string | undefined; let newbio: string | undefined; const hypouser = this.user.clone(); let color: string; async function regen() { hypotheticalProfile.textContent = ""; const hypoprofile = await hypouser.buildprofile(-1, -1); hypotheticalProfile.appendChild(hypoprofile); } regen(); const settingsLeft = userOptions.addOptions(""); const settingsRight = userOptions.addOptions(""); settingsRight.addHTMLArea(hypotheticalProfile); const finput = settingsLeft.addFileInput( I18n.getTranslation("uploadPfp"), (_) => { if (file) { this.updatepfp(file); } }, {clear: true}, ); finput.watchForChange((_) => { if (!_) { file = null; hypouser.avatar = null; hypouser.hypotheticalpfp = true; regen(); return; } if (_.length) { file = _[0]; const blob = URL.createObjectURL(file); hypouser.avatar = blob; hypouser.hypotheticalpfp = true; regen(); } }); let bfile: undefined | File | null; const binput = settingsLeft.addFileInput( I18n.getTranslation("uploadBanner"), (_) => { if (bfile !== undefined) { this.updatebanner(bfile); } }, {clear: true}, ); binput.watchForChange((_) => { if (!_) { bfile = null; hypouser.banner = undefined; hypouser.hypotheticalbanner = true; regen(); return; } if (_.length) { bfile = _[0]; const blob = URL.createObjectURL(bfile); hypouser.banner = blob; hypouser.hypotheticalbanner = true; regen(); } }); let changed = false; const pronounbox = settingsLeft.addTextInput( I18n.getTranslation("pronouns"), (_) => { if (newpronouns !== undefined || newbio !== undefined || changed !== undefined) { this.updateProfile({ pronouns: newpronouns, bio: newbio, accent_color: Number.parseInt("0x" + color.substr(1), 16), }); } }, {initText: this.user.pronouns}, ); pronounbox.watchForChange((_) => { hypouser.pronouns = _; newpronouns = _; regen(); }); const bioBox = settingsLeft.addMDInput(I18n.getTranslation("bio"), (_) => {}, { initText: this.user.bio.rawString, }); bioBox.watchForChange((_) => { newbio = _; hypouser.bio = new MarkDown(_, this); regen(); }); if (this.user.accent_color) { color = "#" + this.user.accent_color.toString(16); } else { color = "transparent"; } const colorPicker = settingsLeft.addColorInput( I18n.getTranslation("profileColor"), (_) => {}, {initColor: color}, ); colorPicker.watchForChange((_) => { console.log(); color = _; hypouser.accent_color = Number.parseInt("0x" + _.substr(1), 16); changed = true; regen(); }); } { const tas = settings.addButton(I18n.getTranslation("localuser.themesAndSounds")); { const themes = ["Dark", "WHITE", "Light", "Dark-Accent"]; tas.addSelect( I18n.getTranslation("localuser.theme:"), (_) => { localStorage.setItem("theme", themes[_]); setTheme(); }, themes, { defaultIndex: themes.indexOf(localStorage.getItem("theme") as string), }, ); } { const initArea = (index: number) => { console.log(index === sounds.length - 1); if (index === sounds.length - 1) { const input = document.createElement("input"); input.type = "file"; input.accept = "audio/*"; input.addEventListener("change", () => { if (input.files?.length === 1) { const file = input.files[0]; let reader = new FileReader(); reader.onload = () => { let dataUrl = reader.result; if (typeof dataUrl !== "string") return; this.perminfo.sound = {}; try { this.perminfo.sound.cSound = dataUrl; console.log(this.perminfo.sound.cSound); this.playSound("custom"); } catch (_) { alert(I18n.localuser.soundTooLarge()); } }; reader.readAsDataURL(file); } }); area.append(input); } else { area.innerHTML = ""; } }; const sounds = [...AVoice.sounds, I18n.localuser.customSound()]; const initIndex = sounds.indexOf(this.getNotificationSound()); tas .addSelect( I18n.getTranslation("localuser.notisound"), (index) => { this.setNotificationSound(sounds[index]); }, sounds, {defaultIndex: initIndex}, ) .watchForChange((index) => { initArea(index); this.playSound(sounds[index]); }); const area = document.createElement("div"); initArea(initIndex); tas.addHTMLArea(area); } { let userinfos = getBulkInfo(); tas.addColorInput( I18n.getTranslation("localuser.accentColor"), (_) => { userinfos = getBulkInfo(); userinfos.accent_color = _; localStorage.setItem("userinfos", JSON.stringify(userinfos)); document.documentElement.style.setProperty("--accent-color", userinfos.accent_color); }, {initColor: userinfos.accent_color}, ); } } { const update = settings.addButton(I18n.getTranslation("localuser.updateSettings")); const sw = update.addSelect( I18n.getTranslation("localuser.swSettings"), () => {}, ["SWOff", "SWOffline", "SWOn"].map((e) => I18n.getTranslation("localuser." + e)), { defaultIndex: ["false", "offlineOnly", "true"].indexOf( localStorage.getItem("SWMode") as string, ), }, ); sw.onchange = (e) => { SW.setMode(["false", "offlineOnly", "true"][e] as "false" | "offlineOnly" | "true"); }; update.addButtonInput("", I18n.getTranslation("localuser.CheckUpdate"), () => { SW.checkUpdate(); }); update.addButtonInput("", I18n.getTranslation("localuser.clearCache"), () => { SW.forceClear(); }); } { const security = settings.addButton(I18n.getTranslation("localuser.accountSettings")); const genSecurity = () => { security.removeAll(); if (this.mfa_enabled) { security.addButtonInput("", I18n.getTranslation("localuser.2faDisable"), () => { const form = security.addSubForm( I18n.getTranslation("localuser.2faDisable"), (_: any) => { if (_.message) { switch (_.code) { case 60008: form.error("code", I18n.getTranslation("badCode")); break; } } else { this.mfa_enabled = false; security.returnFromSub(); genSecurity(); } }, { fetchURL: this.info.api + "/users/@me/mfa/totp/disable", headers: this.headers, }, ); form.addTextInput(I18n.getTranslation("localuser.2faCode"), "code", {required: true}); }); } else { security.addButtonInput("", I18n.getTranslation("localuser.2faEnable"), async () => { let secret = ""; for (let i = 0; i < 18; i++) { secret += "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"[Math.floor(Math.random() * 32)]; } const form = security.addSubForm( I18n.getTranslation("localuser.setUp2fa"), (_: any) => { if (_.message) { switch (_.code) { case 60008: form.error("code", I18n.getTranslation("localuser.badCode")); break; case 400: form.error("password", I18n.getTranslation("localuser.badPassword")); break; } } else { genSecurity(); this.mfa_enabled = true; security.returnFromSub(); } }, { fetchURL: this.info.api + "/users/@me/mfa/totp/enable/", headers: this.headers, }, ); form.addTitle(I18n.getTranslation("localuser.setUp2faInstruction")); form.addText(I18n.getTranslation("localuser.2faCodeGive", secret)); form.addTextInput(I18n.getTranslation("localuser.password:"), "password", { required: true, password: true, }); form.addTextInput(I18n.getTranslation("localuser.2faCode"), "code", {required: true}); form.setValue("secret", secret); }); } security.addButtonInput("", I18n.getTranslation("localuser.changeDiscriminator"), () => { const form = security.addSubForm( I18n.getTranslation("localuser.changeDiscriminator"), (_) => { security.returnFromSub(); }, { fetchURL: this.info.api + "/users/@me/", headers: this.headers, method: "PATCH", }, ); form.addTextInput(I18n.getTranslation("localuser.newDiscriminator"), "discriminator"); }); security.addButtonInput("", I18n.getTranslation("localuser.changeEmail"), () => { const form = security.addSubForm( I18n.getTranslation("localuser.changeEmail"), (_) => { security.returnFromSub(); }, { fetchURL: this.info.api + "/users/@me/", headers: this.headers, method: "PATCH", }, ); form.addTextInput(I18n.getTranslation("localuser.password:"), "password", { password: true, }); if (this.mfa_enabled) { form.addTextInput(I18n.getTranslation("localuser.2faCode"), "code"); } form.addTextInput(I18n.getTranslation("localuser.newEmail:"), "email"); }); security.addButtonInput("", I18n.getTranslation("localuser.changeUsername"), () => { const form = security.addSubForm( I18n.getTranslation("localuser.changeUsername"), (_) => { security.returnFromSub(); }, { fetchURL: this.info.api + "/users/@me/", headers: this.headers, method: "PATCH", }, ); form.addTextInput(I18n.getTranslation("localuser.password:"), "password", { password: true, }); if (this.mfa_enabled) { form.addTextInput(I18n.getTranslation("localuser.2faCode"), "code"); } form.addTextInput(I18n.getTranslation("localuser.newUsername"), "username"); }); security.addButtonInput("", I18n.getTranslation("localuser.changePassword"), () => { const form = security.addSubForm( I18n.getTranslation("localuser.changePassword"), (_) => { security.returnFromSub(); }, { fetchURL: this.info.api + "/users/@me/", headers: this.headers, method: "PATCH", }, ); form.addTextInput(I18n.getTranslation("localuser.oldPassword:"), "password", { password: true, }); if (this.mfa_enabled) { form.addTextInput(I18n.getTranslation("localuser.2faCode"), "code"); } let in1 = ""; let in2 = ""; form .addTextInput(I18n.getTranslation("localuser.newPassword:"), "") .watchForChange((text) => { in1 = text; }); const copy = form.addTextInput("New password again:", ""); copy.watchForChange((text) => { in2 = text; }); form.setValue("new_password", () => { if (in1 === in2) { return in1; } else { throw new FormError(copy, I18n.getTranslation("localuser.PasswordsNoMatch")); } }); }); security.addSelect( I18n.getTranslation("localuser.language"), (e) => { I18n.setLanguage(I18n.options()[e]); }, [...langmap.values()], { defaultIndex: I18n.options().indexOf(I18n.lang), }, ); { const box = security.addCheckboxInput( I18n.getTranslation("localuser.enableEVoice"), () => {}, {initState: Boolean(localStorage.getItem("Voice enabled"))}, ); box.onchange = (e) => { if (e) { if (confirm(I18n.getTranslation("localuser.VoiceWarning"))) { localStorage.setItem("Voice enabled", "true"); } else { box.value = false; const checkbox = box.input.deref(); if (checkbox) { checkbox.checked = false; } } } else { localStorage.removeItem("Voice enabled"); } }; const box2 = security.addCheckboxInput("Enable logging of bad stuff", () => {}, { initState: Boolean(localStorage.getItem("logbad")), }); box2.onchange = (e) => { if (e) { if (confirm("this is meant for spacebar devs")) { localStorage.setItem("logbad", "true"); } else { box2.value = false; const checkbox = box2.input.deref(); if (checkbox) { checkbox.checked = false; } } } else { localStorage.removeItem("logbad"); } }; } }; genSecurity(); } { const connections = settings.addButton(I18n.getTranslation("localuser.connections")); const connectionContainer = document.createElement("div"); connectionContainer.id = "connection-container"; fetch(this.info.api + "/connections", { headers: this.headers, }) .then((r) => r.json()) .then((json) => { Object.keys(json) .sort((key) => (json[key].enabled ? -1 : 1)) .forEach((key) => { const connection = json[key]; const container = document.createElement("div"); container.textContent = key.charAt(0).toUpperCase() + key.slice(1); if (connection.enabled) { container.addEventListener("click", async () => { const connectionRes = await fetch( this.info.api + "/connections/" + key + "/authorize", { headers: this.headers, }, ); const connectionJSON = await connectionRes.json(); window.open(connectionJSON.url, "_blank", "noopener noreferrer"); }); } else { container.classList.add("disabled"); container.title = I18n.getTranslation("localuser.PasswordsNoMatch"); } connectionContainer.appendChild(container); }); }); connections.addHTMLArea(connectionContainer); } { const devPortal = settings.addButton(I18n.getTranslation("localuser.devPortal")); fetch(this.info.api + "/teams", { headers: this.headers, }).then(async (teamsRes) => { const teams = await teamsRes.json(); devPortal.addButtonInput("", I18n.getTranslation("localuser.createApp"), () => { const form = devPortal.addSubForm( I18n.getTranslation("localuser.createApp"), (json: any) => { if (json.message) form.error("name", json.message); else { devPortal.returnFromSub(); this.manageApplication(json.id, devPortal); } }, { fetchURL: this.info.api + "/applications", headers: this.headers, method: "POST", }, ); form.addTextInput("Name:", "name", {required: true}); form.addSelect( I18n.getTranslation("localuser.team:"), "team_id", ["Personal", ...teams.map((team: {name: string}) => team.name)], { defaultIndex: 0, }, ); }); const appListContainer = document.createElement("div"); appListContainer.id = "app-list-container"; fetch(this.info.api + "/applications", { headers: this.headers, }) .then((r) => r.json()) .then((json) => { json.forEach( (application: { cover_image: any; icon: any; id: string | undefined; name: string | number; bot: any; }) => { const container = document.createElement("div"); if (application.cover_image || application.icon) { const cover = document.createElement("img"); cover.crossOrigin = "anonymous"; cover.src = this.info.cdn + "/app-icons/" + application.id + "/" + (application.cover_image || application.icon) + ".png?size=256"; cover.alt = ""; cover.loading = "lazy"; container.appendChild(cover); } const name = document.createElement("h2"); name.textContent = application.name + (application.bot ? " (Bot)" : ""); container.appendChild(name); container.addEventListener("click", async () => { this.manageApplication(application.id, devPortal); }); appListContainer.appendChild(container); }, ); }); devPortal.addHTMLArea(appListContainer); }); } { const deleteAccount = settings.addButton(I18n.localuser.deleteAccount()).addForm( "", (e) => { if ("message" in e) { if (typeof e.message === "string") { throw new FormError(password, e.message); } } else { this.userinfo.remove(); window.location.href = "/"; } }, { headers: this.headers, method: "POST", fetchURL: this.info.api + "/users/@me/delete/", traditionalSubmit: false, submitText: I18n.localuser.deleteAccountButton(), }, ); const shrek = deleteAccount.addTextInput( I18n.localuser.areYouSureDelete(I18n.localuser.sillyDeleteConfirmPhrase()), "shrek", ); const password = deleteAccount.addTextInput(I18n.localuser["password:"](), "password", { password: true, }); deleteAccount.addPreprocessor((obj) => { if ("shrek" in obj) { if (obj.shrek !== I18n.localuser.sillyDeleteConfirmPhrase()) { throw new FormError(shrek, I18n.localuser.mustTypePhrase()); } delete obj.shrek; } else { throw new FormError(shrek, I18n.localuser.mustTypePhrase()); } }); } 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); }); } } { const jankInfo = settings.addButton(I18n.jankInfo()); const img = document.createElement("img"); img.src = "/logo.svg"; jankInfo.addHTMLArea(img); img.width = 128; img.height = 128; jankInfo.addMDText( I18n.clientDesc("Jank-Rolling", window.location.origin, this.rights.allow + ""), ); } settings.show(); } readonly botTokens: Map = new Map(); async manageApplication(appId = "", container: Options) { if (this.perminfo.applications) { for (const item of Object.keys(this.perminfo.applications)) { this.botTokens.set(item, this.perminfo.applications[item]); } } const res = await fetch(this.info.api + "/applications/" + appId, { headers: this.headers, }); const json = await res.json(); const form = container.addSubForm(json.name, () => {}, { fetchURL: this.info.api + "/applications/" + appId, method: "PATCH", headers: this.headers, traditionalSubmit: true, }); form.addTextInput(I18n.getTranslation("localuser.appName"), "name", {initText: json.name}); form.addMDInput(I18n.getTranslation("localuser.description"), "description", { initText: json.description, }); form.addFileInput("Icon:", "icon"); form.addTextInput(I18n.getTranslation("localuser.privacyPolcyURL"), "privacy_policy_url", { initText: json.privacy_policy_url, }); form.addTextInput(I18n.getTranslation("localuser.TOSURL"), "terms_of_service_url", { initText: json.terms_of_service_url, }); form.addCheckboxInput(I18n.getTranslation("localuser.publicAvaliable"), "bot_public", { initState: json.bot_public, }); form.addCheckboxInput(I18n.getTranslation("localuser.requireCode"), "bot_require_code_grant", { initState: json.bot_require_code_grant, }); form.addButtonInput( "", I18n.getTranslation("localuser." + (json.bot ? "manageBot" : "addBot")), async () => { if (!json.bot) { if (!confirm(I18n.getTranslation("localuser.confirmAddBot"))) { return; } const updateRes = await fetch(this.info.api + "/applications/" + appId + "/bot", { method: "POST", headers: this.headers, }); const updateJSON = await updateRes.json(); this.botTokens.set(appId, updateJSON.token); } this.manageBot(appId, form); }, ); } async manageBot(appId = "", container: Form) { const res = await fetch(this.info.api + "/applications/" + appId, { headers: this.headers, }); const json = await res.json(); if (!json.bot) { return alert(I18n.getTranslation("localuser.confuseNoBot")); } const bot: mainuserjson = json.bot; const form = container.addSubForm( I18n.getTranslation("localuser.editingBot", bot.username), (out) => { console.log(out); }, { method: "PATCH", fetchURL: this.info.api + "/applications/" + appId + "/bot", headers: this.headers, traditionalSubmit: true, }, ); form.addTextInput(I18n.getTranslation("localuser.botUsername"), "username", { initText: bot.username, }); form.addFileInput(I18n.getTranslation("localuser.botAvatar"), "avatar"); form.addButtonInput("", I18n.getTranslation("localuser.resetToken"), async () => { if (!confirm(I18n.getTranslation("localuser.confirmReset"))) { return; } const updateRes = await fetch(this.info.api + "/applications/" + appId + "/bot/reset", { method: "POST", headers: this.headers, }); const updateJSON = await updateRes.json(); text.setText(I18n.getTranslation("localuser.tokenDisplay", updateJSON.token)); this.botTokens.set(appId, updateJSON.token); if (this.perminfo.applications[appId]) { this.perminfo.applications[appId] = updateJSON.token; } }); const text = form.addText( I18n.getTranslation( "localuser.tokenDisplay", this.botTokens.has(appId) ? (this.botTokens.get(appId) as string) : "*****************", ), ); const check = form.addOptions("", {noSubmit: true}); if (!this.perminfo.applications) { this.perminfo.applications = {}; } const checkbox = check.addCheckboxInput(I18n.getTranslation("localuser.saveToken"), () => {}, { initState: !!this.perminfo.applications[appId], }); checkbox.watchForChange((_) => { if (_) { if (this.botTokens.has(appId)) { this.perminfo.applications[appId] = this.botTokens.get(appId); } else { alert(I18n.getTranslation("localuser.noToken")); checkbox.setState(false); } } else { delete this.perminfo.applications[appId]; } }); form.addButtonInput("", I18n.getTranslation("localuser.advancedBot"), () => { const token = this.botTokens.get(appId); if (token) { const botc = new Bot(bot, token, this); botc.settings(); } }); form.addButtonInput("", I18n.getTranslation("localuser.botInviteCreate"), () => { Bot.InviteMaker(appId, form, this.info); }); } readonly autofillregex = Object.freeze(/[@#:]([a-z0-9 ]*)$/i); mdBox() { interface CustomHTMLDivElement extends HTMLDivElement { markdown: MarkDown; } const typebox = document.getElementById("typebox") as CustomHTMLDivElement; const typeMd = typebox.markdown; typeMd.owner = this; typeMd.onUpdate = (str, pre) => { this.search(document.getElementById("searchOptions") as HTMLDivElement, typeMd, str, pre); }; } MDReplace(replacewith: string, original: string, typebox: MarkDown) { let raw = typebox.rawString; raw = raw.split(original)[1]; if (raw === undefined) return; raw = original.replace(this.autofillregex, "") + replacewith + raw; console.log(raw); console.log(replacewith); console.log(original); typebox.txt = raw.split(""); const match = original.match(this.autofillregex); if (match) { typebox.boxupdate(replacewith.length - match[0].length); } } MDSearchOptions( options: [string, string, void | HTMLElement][], original: string, div: HTMLDivElement, typebox: MarkDown, ) { if (!div) return; div.innerHTML = ""; let i = 0; const htmloptions: HTMLSpanElement[] = []; for (const thing of options) { if (i == 8) { break; } i++; const span = document.createElement("span"); htmloptions.push(span); if (thing[2]) { span.append(thing[2]); } span.append(thing[0]); span.onclick = (e) => { if (e) { const selection = window.getSelection() as Selection; const box = typebox.box.deref(); if (!box) return; if (selection) { console.warn(original); const pos = getTextNodeAtPosition( box, original.length - (original.match(this.autofillregex) as RegExpMatchArray)[0].length + thing[1].length, ); selection.removeAllRanges(); const range = new Range(); range.setStart(pos.node, pos.position); selection.addRange(range); } e.preventDefault(); box.focus(); } this.MDReplace(thing[1], original, typebox); div.innerHTML = ""; remove(); }; div.prepend(span); } const remove = () => { if (div && div.innerHTML === "") { this.keyup = () => false; this.keydown = () => {}; return true; } return false; }; if (htmloptions[0]) { let curindex = 0; let cur = htmloptions[0]; cur.classList.add("selected"); const cancel = new Set(["ArrowUp", "ArrowDown", "Enter", "Tab"]); this.keyup = (event) => { if (remove()) return false; if (cancel.has(event.key)) { switch (event.key) { case "ArrowUp": if (htmloptions[curindex + 1]) { cur.classList.remove("selected"); curindex++; cur = htmloptions[curindex]; cur.classList.add("selected"); } break; case "ArrowDown": if (htmloptions[curindex - 1]) { cur.classList.remove("selected"); curindex--; cur = htmloptions[curindex]; cur.classList.add("selected"); } break; case "Enter": case "Tab": //@ts-ignore cur.onclick(); break; } return true; } return false; }; this.keydown = (event) => { if (remove()) return; if (cancel.has(event.key)) { event.preventDefault(); } }; } else { remove(); } } MDFindChannel(name: string, orginal: string, box: HTMLDivElement, typebox: MarkDown) { const maybe: [number, Channel][] = []; if (this.lookingguild && this.lookingguild.id !== "@me") { for (const channel of this.lookingguild.channels) { const confidence = channel.similar(name); if (confidence > 0) { maybe.push([confidence, channel]); } } } maybe.sort((a, b) => b[0] - a[0]); this.MDSearchOptions( maybe.map((a) => ["# " + a[1].name, `<#${a[1].id}> `, undefined]), orginal, box, typebox, ); } async getUser(id: string) { if (this.userMap.has(id)) { return this.userMap.get(id) as User; } return new User(await (await fetch(this.info.api + "/users/" + id)).json(), this); } MDFineMentionGen(name: string, original: string, box: HTMLDivElement, typebox: MarkDown) { let members: [Member, number][] = []; if (this.lookingguild) { for (const member of this.lookingguild.members) { const rank = member.compare(name); if (rank > 0) { members.push([member, rank]); } } } members.sort((a, b) => b[1] - a[1]); this.MDSearchOptions( members.map((a) => ["@" + a[0].name, `<@${a[0].id}> `, undefined]), original, box, typebox, ); } MDFindMention(name: string, original: string, box: HTMLDivElement, typebox: MarkDown) { if (this.ws && this.lookingguild) { this.MDFineMentionGen(name, original, box, typebox); const nonce = Math.floor(Math.random() * 10 ** 8) + ""; if (this.lookingguild.member_count <= this.lookingguild.members.size) return; this.ws.send( JSON.stringify({ op: 8, d: { guild_id: [this.lookingguild.id], query: name, limit: 8, presences: true, nonce, }, }), ); this.searchMap.set(nonce, async (e) => { console.log(e); if (e.members && e.members[0]) { if (e.members[0].user) { for (const thing of e.members) { await Member.new(thing, this.lookingguild as Guild); } } else { const prom1: Promise[] = []; for (const thing of e.members) { prom1.push(this.getUser(thing.id)); } Promise.all(prom1); for (const thing of e.members) { if (!this.userMap.has(thing.id)) { console.warn("Dumb server bug for this member", thing); continue; } await Member.new(thing, this.lookingguild as Guild); } } this.MDFineMentionGen(name, original, box, typebox); } }); } } findEmoji(search: string, orginal: string, box: HTMLDivElement, typebox: MarkDown) { const emj = Emoji.searchEmoji(search, this, 10); const map = emj.map(([emoji]): [string, string, HTMLElement] => { return [ emoji.name, emoji.id ? `<${emoji.animated ? "a" : ""}:${emoji.name}:${emoji.id}>` : (emoji.emoji as string), emoji.getHTML(), ]; }); this.MDSearchOptions(map, orginal, box, typebox); } search(box: HTMLDivElement, md: MarkDown, str: string, pre: boolean) { if (!pre) { const match = str.match(this.autofillregex); if (match) { const [type, search] = [match[0][0], match[0].split(/@|#|:/)[1]]; switch (type) { case "#": this.MDFindChannel(search, str, box, md); break; case "@": this.MDFindMention(search, str, box, md); break; case ":": if (search.length >= 2) { this.findEmoji(search, str, box, md); } else { this.MDSearchOptions([], "", box, md); } break; } return; } } box.innerHTML = ""; } searching = false; mSearch(query: string) { const p = new URLSearchParams("?"); this.searching = true; p.set("content", query.trim()); fetch(this.info.api + `/guilds/${this.lookingguild?.id}/messages/search/?` + p.toString(), { headers: this.headers, }) .then((_) => _.json()) .then((json: {messages: [messagejson][]; total_results: number}) => { //FIXME total_results shall be ignored as it's known to be bad, spacebar bug. const messages = json.messages .map(([m]) => { const c = this.channelids.get(m.channel_id); if (!c) return; if (c.messages.get(m.id)) { return c.messages.get(m.id); } return new Message(m, c, true); }) .filter((_) => _ !== undefined); const sideDiv = document.getElementById("sideDiv"); const sideContainDiv = document.getElementById("sideContainDiv"); if (!sideDiv || !sideContainDiv) return; sideDiv.innerHTML = ""; sideContainDiv.classList.add("searchDiv"); let channel: Channel | undefined = undefined; for (const message of messages) { if (channel !== message.channel) { channel = message.channel; const h3 = document.createElement("h3"); h3.textContent = channel.name; h3.classList.add("channelSTitle"); sideDiv.append(h3); } const html = message.buildhtml(undefined, true); html.addEventListener("click", async () => { try { await message.channel.focus(message.id); } catch (e) { console.error(e); } }); sideDiv.append(html); } }); } keydown: (event: KeyboardEvent) => unknown = () => {}; keyup: (event: KeyboardEvent) => boolean = () => false; //---------- resolving members code ----------- readonly waitingmembers = new Map< string, Map void> >(); readonly presences: Map = new Map(); async resolvemember(id: string, guildid: string): Promise { if (guildid === "@me") { return undefined; } const guild = this.guildids.get(guildid); const borked = true; if (!guild || (borked && guild.member_count > 250)) { //sorry puyo, I need to fix member resolving while it's broken on large guilds try { const req = await fetch(this.info.api + "/guilds/" + guildid + "/members/" + id, { headers: this.headers, }); if (req.status !== 200) { return undefined; } return await req.json(); } catch { return undefined; } } let guildmap = this.waitingmembers.get(guildid); if (!guildmap) { guildmap = new Map(); this.waitingmembers.set(guildid, guildmap); } const promise: Promise = new Promise((res) => { guildmap.set(id, res); this.getmembers(); }); return await promise; } fetchingmembers: Map = new Map(); noncemap: Map void> = new Map(); noncebuild: Map = new Map(); searchMap = new Map< string, (arg: { chunk_index: number; chunk_count: number; nonce: string; not_found?: string[]; members?: memberjson[]; presences: presencejson[]; }) => unknown >(); async gotChunk(chunk: { chunk_index: number; chunk_count: number; nonce: string; not_found?: string[]; members?: memberjson[]; presences: presencejson[]; }) { for (const thing of chunk.presences) { if (thing.user) { this.presences.set(thing.user.id, thing); } } if (this.searchMap.has(chunk.nonce)) { const func = this.searchMap.get(chunk.nonce); this.searchMap.delete(chunk.nonce); if (func) { func(chunk); return; } } chunk.members ??= []; const arr = this.noncebuild.get(chunk.nonce); if (!arr) return; arr[0] = arr[0].concat(chunk.members); if (chunk.not_found) { arr[1] = chunk.not_found; } arr[2].push(chunk.chunk_index); if (arr[2].length === chunk.chunk_count) { this.noncebuild.delete(chunk.nonce); const func = this.noncemap.get(chunk.nonce); if (!func) return; func([arr[0], arr[1]]); this.noncemap.delete(chunk.nonce); } } async getmembers() { const promise = new Promise((res) => { setTimeout(res, 10); }); await promise; //allow for more to be sent at once :P if (this.ws) { this.waitingmembers.forEach(async (value, guildid) => { const keys = value.keys(); if (this.fetchingmembers.has(guildid)) { return; } const build: string[] = []; for (const key of keys) { build.push(key); if (build.length === 100) { break; } } if (!build.length) { this.waitingmembers.delete(guildid); return; } const promise: Promise<[memberjson[], string[]]> = new Promise((res) => { const nonce = "" + Math.floor(Math.random() * 100000000000); this.noncemap.set(nonce, res); this.noncebuild.set(nonce, [[], [], []]); if (!this.ws) return; this.ws.send( JSON.stringify({ op: 8, d: { user_ids: build, guild_id: guildid, limit: 100, nonce, presences: true, }, }), ); this.fetchingmembers.set(guildid, true); }); const prom = await promise; const data = prom[0]; for (const thing of data) { if (value.has(thing.id)) { const func = value.get(thing.id); if (!func) { value.delete(thing.id); continue; } func(thing); value.delete(thing.id); } } for (const thing of prom[1]) { if (value.has(thing)) { const func = value.get(thing); if (!func) { value.delete(thing); continue; } func(undefined); value.delete(thing); } } this.fetchingmembers.delete(guildid); this.getmembers(); }); } } async pingEndpoint() { const userInfo = getBulkInfo(); if (!userInfo.instances) userInfo.instances = {}; const wellknown = this.info.wellknown; if (!userInfo.instances[wellknown]) { const pingRes = await fetch(this.info.api + "/ping"); const pingJSON = await pingRes.json(); userInfo.instances[wellknown] = pingJSON; localStorage.setItem("userinfos", JSON.stringify(userInfo)); } this.instancePing = userInfo.instances[wellknown].instance; this.pageTitle("Loading..."); } pageTitle(channelName = "", guildName = "") { (document.getElementById("channelname") as HTMLSpanElement).textContent = channelName; (document.getElementsByTagName("title")[0] as HTMLTitleElement).textContent = channelName + (guildName ? " | " + guildName : "") + " | " + this.instancePing.name + " | Jank Client"; } async instanceStats() { const res = await fetch(this.info.api + "/policies/stats", { headers: this.headers, }); const json = await res.json(); const dialog = new Dialog(""); dialog.options.addTitle(I18n.getTranslation("instanceStats.name", this.instancePing.name)); dialog.options.addText(I18n.getTranslation("instanceStats.users", json.counts.user)); dialog.options.addText(I18n.getTranslation("instanceStats.servers", json.counts.guild)); dialog.options.addText(I18n.getTranslation("instanceStats.messages", json.counts.message)); dialog.options.addText(I18n.getTranslation("instanceStats.members", json.counts.members)); dialog.show(); } setNotificationSound(sound: string) { const userinfos = getBulkInfo(); userinfos.preferences.notisound = sound; localStorage.setItem("userinfos", JSON.stringify(userinfos)); } playSound(name = this.getNotificationSound()) { if (this.play) { const voice = this.play.audios.get(name); if (voice) { voice.play(); } else if (this.perminfo.sound && this.perminfo.sound.cSound) { const audio = document.createElement("audio"); audio.src = this.perminfo.sound.cSound; audio.play().catch(); } } } getNotificationSound() { const userinfos = getBulkInfo(); return userinfos.preferences.notisound; } } export {Localuser};