diff --git a/.dist/audio.js b/.dist/audio.js deleted file mode 100644 index ea8fa37..0000000 --- a/.dist/audio.js +++ /dev/null @@ -1,159 +0,0 @@ -import { getBulkInfo } from "./login.js"; -class Voice { - audioCtx; - info; - playing; - myArrayBuffer; - gainNode; - buffer; - source; - constructor(wave, freq, volume = 1) { - this.audioCtx = new (window.AudioContext)(); - this.info = { wave, freq }; - this.playing = false; - this.myArrayBuffer = this.audioCtx.createBuffer(1, this.audioCtx.sampleRate, this.audioCtx.sampleRate); - this.gainNode = this.audioCtx.createGain(); - this.gainNode.gain.value = volume; - this.gainNode.connect(this.audioCtx.destination); - this.buffer = this.myArrayBuffer.getChannelData(0); - this.source = this.audioCtx.createBufferSource(); - this.source.buffer = this.myArrayBuffer; - this.source.loop = true; - this.source.start(); - this.updateWave(); - } - get wave() { - return this.info.wave; - } - get freq() { - return this.info.freq; - } - set wave(wave) { - this.info.wave = wave; - this.updateWave(); - } - set freq(freq) { - this.info.freq = freq; - this.updateWave(); - } - updateWave() { - const func = this.waveFunction(); - for (let i = 0; i < this.buffer.length; i++) { - this.buffer[i] = func(i / this.audioCtx.sampleRate, this.freq); - } - } - waveFunction() { - if (typeof this.wave === "function") { - return this.wave; - } - switch (this.wave) { - case "sin": - return (t, freq) => { - return Math.sin(t * Math.PI * 2 * freq); - }; - case "triangle": - return (t, freq) => { - return Math.abs((4 * t * freq) % 4 - 2) - 1; - }; - case "sawtooth": - return (t, freq) => { - return ((t * freq) % 1) * 2 - 1; - }; - case "square": - return (t, freq) => { - return (t * freq) % 2 < 1 ? 1 : -1; - }; - case "white": - return (_t, _freq) => { - return Math.random() * 2 - 1; - }; - case "noise": - return (_t, _freq) => { - return 0; - }; - } - return new Function(); - } - play() { - if (this.playing) { - return; - } - this.source.connect(this.gainNode); - this.playing = true; - } - stop() { - if (this.playing) { - this.source.disconnect(); - this.playing = false; - } - } - static noises(noise) { - switch (noise) { - case "three": { - const voicy = new Voice("sin", 800); - voicy.play(); - setTimeout(_ => { - voicy.freq = 1000; - }, 50); - setTimeout(_ => { - voicy.freq = 1300; - }, 100); - setTimeout(_ => { - voicy.stop(); - }, 150); - break; - } - case "zip": { - const voicy = new Voice((t, freq) => { - return Math.sin(((t + 2) ** (Math.cos(t * 4))) * Math.PI * 2 * freq); - }, 700); - voicy.play(); - setTimeout(_ => { - voicy.stop(); - }, 150); - break; - } - case "square": { - const voicy = new Voice("square", 600, 0.4); - voicy.play(); - setTimeout(_ => { - voicy.freq = 800; - }, 50); - setTimeout(_ => { - voicy.freq = 1000; - }, 100); - setTimeout(_ => { - voicy.stop(); - }, 150); - break; - } - case "beep": { - const voicy = new Voice("sin", 800); - voicy.play(); - setTimeout(_ => { - voicy.stop(); - }, 50); - setTimeout(_ => { - voicy.play(); - }, 100); - setTimeout(_ => { - voicy.stop(); - }, 150); - break; - } - } - } - static get sounds() { - return ["three", "zip", "square", "beep"]; - } - static setNotificationSound(sound) { - const userinfos = getBulkInfo(); - userinfos.preferences.notisound = sound; - localStorage.setItem("userinfos", JSON.stringify(userinfos)); - } - static getNotificationSound() { - const userinfos = getBulkInfo(); - return userinfos.preferences.notisound; - } -} -export { Voice }; diff --git a/.dist/channel.js b/.dist/channel.js deleted file mode 100644 index c06d558..0000000 --- a/.dist/channel.js +++ /dev/null @@ -1,1201 +0,0 @@ -"use strict"; -import { Message } from "./message.js"; -import { Voice } from "./audio.js"; -import { Contextmenu } from "./contextmenu.js"; -import { Dialog } from "./dialog.js"; -import { Permissions } from "./permissions.js"; -import { Settings } from "./settings.js"; -import { RoleList } from "./role.js"; -import { InfiniteScroller } from "./infiniteScroller.js"; -import { SnowFlake } from "./snowflake.js"; -import { MarkDown } from "./markdown.js"; -import { Member } from "./member.js"; -class Channel extends SnowFlake { - editing; - type; - owner; - headers; - name; - parent_id; - parent; - children; - guild_id; - permission_overwrites; - permission_overwritesar; - topic; - nsfw; - position = 0; - lastreadmessageid; - lastmessageid; - mentions; - lastpin; - move_id; - typing; - message_notifications; - allthewayup; - static contextmenu = new Contextmenu("channel menu"); - replyingto; - infinite; - idToPrev = new Map(); - idToNext = new Map(); - messages = new Map(); - static setupcontextmenu() { - this.contextmenu.addbutton("Copy channel id", function () { - navigator.clipboard.writeText(this.id); - }); - this.contextmenu.addbutton("Mark as read", function () { - this.readbottom(); - }); - this.contextmenu.addbutton("Settings[temp]", function () { - this.generateSettings(); - }); - this.contextmenu.addbutton("Delete channel", function () { - this.deleteChannel(); - }, null, function () { - return this.isAdmin(); - }); - this.contextmenu.addbutton("Edit channel", function () { - this.editChannel(); - }, null, function () { - return this.isAdmin(); - }); - this.contextmenu.addbutton("Make invite", function () { - this.createInvite(); - }, null, function () { - return this.hasPermission("CREATE_INSTANT_INVITE") && this.type !== 4; - }); - /* - this.contextmenu.addbutton("Test button",function(){ - this.localuser.ws.send(JSON.stringify({ - "op": 14, - "d": { - "guild_id": this.guild.id, - "channels": { - [this.id]: [ - [ - 0, - 99 - ] - ] - } - } - })) - },null); - /**/ - } - createInvite() { - const div = document.createElement("div"); - div.classList.add("invitediv"); - const text = document.createElement("span"); - div.append(text); - let uses = 0; - let expires = 1800; - const copycontainer = document.createElement("div"); - copycontainer.classList.add("copycontainer"); - const copy = document.createElement("span"); - copy.classList.add("copybutton", "svgtheme", "svg-copy"); - copycontainer.append(copy); - copycontainer.onclick = _ => { - if (text.textContent) { - navigator.clipboard.writeText(text.textContent); - } - }; - div.append(copycontainer); - const update = () => { - fetch(`${this.info.api}/channels/${this.id}/invites`, { - method: "POST", - headers: this.headers, - body: JSON.stringify({ - flags: 0, - target_type: null, - target_user_id: null, - max_age: expires + "", - max_uses: uses, - temporary: uses !== 0 - }) - }).then(_ => _.json()).then(json => { - const params = new URLSearchParams(""); - params.set("instance", this.info.wellknown); - const encoded = params.toString(); - text.textContent = `${location.origin}/invite/${json.code}?${encoded}`; - }); - }; - update(); - new Dialog(["vdiv", - ["title", "Invite people"], - ["text", `to #${this.name} in ${this.guild.properties.name}`], - ["select", "Expire after:", ["30 Minutes", "1 Hour", "6 Hours", "12 Hours", "1 Day", "7 Days", "30 Days", "Never"], function (e) { - expires = [1800, 3600, 21600, 43200, 86400, 604800, 2592000, 0][e.srcElement.selectedIndex]; - update(); - }, 0], - ["select", "Max uses:", ["No limit", "1 use", "5 uses", "10 uses", "25 uses", "50 uses", "100 uses"], function (e) { - uses = [0, 1, 5, 10, 25, 50, 100][e.srcElement.selectedIndex]; - update(); - }, 0], - ["html", div] - ]).show(); - } - generateSettings() { - this.sortPerms(); - const settings = new Settings("Settings for " + this.name); - const s1 = settings.addButton("roles"); - s1.options.push(new RoleList(this.permission_overwritesar, this.guild, this.updateRolePermissions.bind(this), true)); - settings.show(); - } - sortPerms() { - this.permission_overwritesar.sort((a, b) => { - return this.guild.roles.findIndex(_ => _ === a[0]) - this.guild.roles.findIndex(_ => _ === b[0]); - }); - } - setUpInfiniteScroller() { - this.infinite = new InfiniteScroller((async (id, offset) => { - if (offset === 1) { - if (this.idToPrev.has(id)) { - return this.idToPrev.get(id); - } - else { - await this.grabBefore(id); - return this.idToPrev.get(id); - } - } - else { - if (this.idToNext.has(id)) { - return this.idToNext.get(id); - } - else if (this.lastmessage?.id !== id) { - await this.grabAfter(id); - return this.idToNext.get(id); - } - else { - } - } - }), (async (id) => { - //await new Promise(_=>{setTimeout(_,Math.random()*10)}) - const messgage = this.messages.get(id); - try { - if (messgage) { - return messgage.buildhtml(); - } - else { - console.error(id + " not found"); - } - } - catch (e) { - console.error(e); - } - return document.createElement("div"); - }), (async (id) => { - const message = this.messages.get(id); - try { - if (message) { - message.deleteDiv(); - return true; - } - } - catch (e) { - console.error(e); - } - finally { } - return false; - }), this.readbottom.bind(this)); - } - constructor(json, owner, id = json === -1 ? "" : json.id) { - super(id); - if (json === -1) { - return; - } - this.editing; - this.type = json.type; - this.owner = owner; - this.headers = this.owner.headers; - this.name = json.name; - if (json.parent_id) { - this.parent_id = json.parent_id; - } - this.parent = undefined; - this.children = []; - this.guild_id = json.guild_id; - this.permission_overwrites = new Map(); - this.permission_overwritesar = []; - for (const thing of json.permission_overwrites) { - if (thing.id === "1182819038095799904" || thing.id === "1182820803700625444") { - continue; - } - if (!this.permission_overwrites.has(thing.id)) { //either a bug in the server requires this, or the API is cursed - this.permission_overwrites.set(thing.id, new Permissions(thing.allow, thing.deny)); - const permission = this.permission_overwrites.get(thing.id); - if (permission) { - const role = this.guild.roleids.get(thing.id); - if (role) { - this.permission_overwritesar.push([role, permission]); - } - } - } - } - this.topic = json.topic; - this.nsfw = json.nsfw; - this.position = json.position; - this.lastreadmessageid = undefined; - if (json.last_message_id) { - this.lastmessageid = json.last_message_id; - } - else { - this.lastmessageid = undefined; - } - this.setUpInfiniteScroller(); - this.perminfo ??= {}; - } - get perminfo() { - return this.guild.perminfo.channels[this.id]; - } - set perminfo(e) { - this.guild.perminfo.channels[this.id] = e; - } - isAdmin() { - return this.guild.isAdmin(); - } - get guild() { - return this.owner; - } - get localuser() { - return this.guild.localuser; - } - get info() { - return this.owner.info; - } - readStateInfo(json) { - this.lastreadmessageid = json.last_message_id; - this.mentions = json.mention_count; - this.mentions ??= 0; - this.lastpin = json.last_pin_timestamp; - } - get hasunreads() { - if (!this.hasPermission("VIEW_CHANNEL")) { - return false; - } - return (!!this.lastmessageid) && - ((!this.lastreadmessageid) || - SnowFlake.stringToUnixTime(this.lastmessageid) > SnowFlake.stringToUnixTime(this.lastreadmessageid)) - && this.type !== 4; - } - hasPermission(name, member = this.guild.member) { - if (member.isAdmin()) { - return true; - } - for (const thing of member.roles) { - let premission = this.permission_overwrites.get(thing.id); - if (premission) { - const perm = premission.getPermission(name); - if (perm) { - return perm === 1; - } - } - if (thing.permissions.getPermission(name)) { - return true; - } - } - return false; - } - get canMessage() { - if ((this.permission_overwritesar.length === 0) && this.hasPermission("MANAGE_CHANNELS")) { - const role = this.guild.roles.find(_ => _.name === "@everyone"); - if (role) { - this.addRoleToPerms(role); - } - } - return this.hasPermission("SEND_MESSAGES"); - } - sortchildren() { - this.children.sort((a, b) => { - return a.position - b.position; - }); - } - resolveparent(guild) { - const parentid = this.parent_id; - if (!parentid) - return false; - this.parent = this.localuser.channelids.get(parentid); - this.parent ??= undefined; - if (this.parent !== undefined) { - this.parent.children.push(this); - } - return this.parent !== undefined; - } - calculateReorder() { - let position = -1; - const build = []; - for (const thing of this.children) { - const thisthing = { id: thing.id, position: undefined, parent_id: undefined }; - if (thing.position < position) { - thing.position = thisthing.position = position + 1; - } - position = thing.position; - if (thing.move_id && thing.move_id !== thing.parent_id) { - thing.parent_id = thing.move_id; - thisthing.parent_id = thing.parent?.id; - thing.move_id = undefined; - //console.log(this.guild.channelids[thisthing.parent_id.id]); - } - if (thisthing.position || thisthing.parent_id) { - build.push(thisthing); - } - } - return build; - } - static dragged = []; - html; - get visable() { - return this.hasPermission("VIEW_CHANNEL"); - } - createguildHTML(admin = false) { - const div = document.createElement("div"); - this.html = new WeakRef(div); - if (!this.visable) { - let quit = true; - for (const thing of this.children) { - if (thing.visable) { - quit = false; - } - } - if (quit) { - return div; - } - } - div["all"] = this; - div.draggable = admin; - div.addEventListener("dragstart", e => { - Channel.dragged = [this, div]; - e.stopImmediatePropagation(); - }); - div.addEventListener("dragend", () => { - Channel.dragged = []; - }); - if (this.type === 4) { - this.sortchildren(); - const caps = document.createElement("div"); - const decdiv = document.createElement("div"); - const decoration = document.createElement("span"); - decoration.classList.add("svgtheme", "collapse-icon", "svg-category"); - decdiv.appendChild(decoration); - const myhtml = document.createElement("p2"); - myhtml.textContent = this.name; - decdiv.appendChild(myhtml); - caps.appendChild(decdiv); - const childrendiv = document.createElement("div"); - if (admin) { - const addchannel = document.createElement("span"); - addchannel.textContent = "+"; - addchannel.classList.add("addchannel"); - caps.appendChild(addchannel); - addchannel.onclick = _ => { - this.guild.createchannels(this.createChannel.bind(this)); - }; - this.coatDropDiv(decdiv, childrendiv); - } - div.appendChild(caps); - caps.classList.add("capsflex"); - decdiv.classList.add("channeleffects"); - decdiv.classList.add("channel"); - Channel.contextmenu.bindContextmenu(decdiv, this, undefined); - decdiv["all"] = this; - for (const channel of this.children) { - childrendiv.appendChild(channel.createguildHTML(admin)); - } - childrendiv.classList.add("channels"); - setTimeout(_ => { - if (!this.perminfo.collapsed) { - childrendiv.style.height = childrendiv.scrollHeight + "px"; - } - }, 100); - div.appendChild(childrendiv); - if (this.perminfo.collapsed) { - decoration.classList.add("hiddencat"); - childrendiv.style.height = "0px"; - } - decdiv.onclick = () => { - if (childrendiv.style.height !== "0px") { - decoration.classList.add("hiddencat"); - this.perminfo.collapsed = true; - this.localuser.userinfo.updateLocal(); - childrendiv.style.height = "0px"; - } - else { - decoration.classList.remove("hiddencat"); - this.perminfo.collapsed = false; - this.localuser.userinfo.updateLocal(); - childrendiv.style.height = childrendiv.scrollHeight + "px"; - } - }; - } - else { - div.classList.add("channel"); - if (this.hasunreads) { - div.classList.add("cunread"); - } - Channel.contextmenu.bindContextmenu(div, this, undefined); - if (admin) { - this.coatDropDiv(div); - } - div["all"] = this; - const myhtml = document.createElement("span"); - myhtml.textContent = this.name; - if (this.type === 0) { - const decoration = document.createElement("span"); - div.appendChild(decoration); - decoration.classList.add("space", "svgtheme", "svg-channel"); - } - else if (this.type === 2) { // - const decoration = document.createElement("span"); - div.appendChild(decoration); - decoration.classList.add("space", "svgtheme", "svg-voice"); - } - else if (this.type === 5) { // - const decoration = document.createElement("span"); - div.appendChild(decoration); - decoration.classList.add("space", "svgtheme", "svg-announce"); - } - else { - console.log(this.type); - } - div.appendChild(myhtml); - div.onclick = _ => { - this.getHTML(); - }; - } - return div; - } - get myhtml() { - if (this.html) { - return this.html.deref(); - } - else { - return undefined; - } - } - readbottom() { - if (!this.hasunreads) { - return; - } - fetch(this.info.api + "/channels/" + this.id + "/messages/" + this.lastmessageid + "/ack", { - method: "POST", - headers: this.headers, - body: JSON.stringify({}) - }); - this.lastreadmessageid = this.lastmessageid; - this.guild.unreads(); - if (this.myhtml) { - this.myhtml.classList.remove("cunread"); - } - } - coatDropDiv(div, container = false) { - div.addEventListener("dragenter", event => { - console.log("enter"); - event.preventDefault(); - }); - div.addEventListener("dragover", event => { - event.preventDefault(); - }); - div.addEventListener("drop", event => { - const that = Channel.dragged[0]; - if (!that) - return; - event.preventDefault(); - if (container) { - that.move_id = this.id; - if (that.parent) { - that.parent.children.splice(that.parent.children.indexOf(that), 1); - } - that.parent = this; - container.prepend(Channel.dragged[1]); - this.children.unshift(that); - } - else { - console.log(this, Channel.dragged); - that.move_id = this.parent_id; - if (that.parent) { - that.parent.children.splice(that.parent.children.indexOf(that), 1); - } - else { - this.guild.headchannels.splice(this.guild.headchannels.indexOf(that), 1); - } - that.parent = this.parent; - if (that.parent) { - const build = []; - for (let i = 0; i < that.parent.children.length; i++) { - build.push(that.parent.children[i]); - if (that.parent.children[i] === this) { - build.push(that); - } - } - that.parent.children = build; - } - else { - const build = []; - for (let i = 0; i < this.guild.headchannels.length; i++) { - build.push(this.guild.headchannels[i]); - if (this.guild.headchannels[i] === this) { - build.push(that); - } - } - this.guild.headchannels = build; - } - if (Channel.dragged[1]) { - div.after(Channel.dragged[1]); - } - } - this.guild.calculateReorder(); - }); - return div; - } - createChannel(name, type) { - fetch(this.info.api + "/guilds/" + this.guild.id + "/channels", { - method: "POST", - headers: this.headers, - body: JSON.stringify({ - name, - type, - parent_id: this.id, - permission_overwrites: [], - }) - }); - } - editChannel() { - let name = this.name; - let topic = this.topic; - let nsfw = this.nsfw; - const thisid = this.id; - const thistype = this.type; - const full = new Dialog(["hdiv", - ["vdiv", - ["textbox", "Channel name:", this.name, function () { - name = this.value; - }], - ["mdbox", "Channel topic:", this.topic, function () { - topic = this.value; - }], - ["checkbox", "NSFW Channel", this.nsfw, function () { - nsfw = this.checked; - }], - ["button", "", "submit", () => { - fetch(this.info.api + "/channels/" + thisid, { - method: "PATCH", - headers: this.headers, - body: JSON.stringify({ - name, - type: thistype, - topic, - bitrate: 64000, - user_limit: 0, - nsfw, - flags: 0, - rate_limit_per_user: 0 - }) - }); - console.log(full); - full.hide(); - }]] - ]); - full.show(); - console.log(full); - } - deleteChannel() { - fetch(this.info.api + "/channels/" + this.id, { - method: "DELETE", - headers: this.headers - }); - } - setReplying(message) { - if (this.replyingto?.div) { - this.replyingto.div.classList.remove("replying"); - } - this.replyingto = message; - if (!this.replyingto?.div) - return; - console.log(message); - this.replyingto.div.classList.add("replying"); - this.makereplybox(); - } - makereplybox() { - const replybox = document.getElementById("replybox"); - if (this.replyingto) { - replybox.innerHTML = ""; - const span = document.createElement("span"); - span.textContent = "Replying to " + this.replyingto.author.username; - const X = document.createElement("button"); - X.onclick = _ => { - if (this.replyingto?.div) { - this.replyingto.div.classList.remove("replying"); - } - replybox.classList.add("hideReplyBox"); - this.replyingto = null; - replybox.innerHTML = ""; - }; - replybox.classList.remove("hideReplyBox"); - X.textContent = "⦻"; - X.classList.add("cancelReply"); - replybox.append(span); - replybox.append(X); - } - else { - replybox.classList.add("hideReplyBox"); - } - } - async getmessage(id) { - const message = this.messages.get(id); - if (message) { - return message; - } - else { - const gety = await fetch(this.info.api + "/channels/" + this.id + "/messages?limit=1&around=" + id, { headers: this.headers }); - const json = await gety.json(); - return new Message(json[0], this); - } - } - static genid = 0; - async getHTML() { - const id = ++Channel.genid; - if (this.localuser.channelfocus) { - this.localuser.channelfocus.infinite.delete(); - } - if (this.guild !== this.localuser.lookingguild) { - this.guild.loadGuild(); - } - if (this.localuser.channelfocus && this.localuser.channelfocus.myhtml) { - this.localuser.channelfocus.myhtml.classList.remove("viewChannel"); - } - if (this.myhtml) { - this.myhtml.classList.add("viewChannel"); - } - this.guild.prevchannel = this; - this.guild.perminfo.prevchannel = this.id; - this.localuser.userinfo.updateLocal(); - this.localuser.channelfocus = this; - const prom = this.infinite.delete(); - history.pushState(null, "", "/channels/" + this.guild_id + "/" + this.id); - this.localuser.pageTitle("#" + this.name); - const channelTopic = document.getElementById("channelTopic"); - if (this.topic) { - channelTopic.innerHTML = new MarkDown(this.topic, this).makeHTML().innerHTML; - channelTopic.removeAttribute("hidden"); - } - else - channelTopic.setAttribute("hidden", ""); - const loading = document.getElementById("loadingdiv"); - Channel.regenLoadingMessages(); - loading.classList.add("loading"); - this.rendertyping(); - await this.putmessages(); - await prom; - if (id !== Channel.genid) { - return; - } - this.makereplybox(); - await this.buildmessages(); - //loading.classList.remove("loading"); - document.getElementById("typebox").contentEditable = "" + this.canMessage; - } - typingmap = new Map(); - async typingStart(typing) { - const memb = await Member.new(typing.d.member, this.guild); - if (!memb) - return; - if (memb.id === this.localuser.user.id) { - console.log("you is typing"); - return; - } - console.log("user is typing and you should see it"); - this.typingmap.set(memb, Date.now()); - setTimeout(this.rendertyping.bind(this), 10000); - this.rendertyping(); - } - rendertyping() { - const typingtext = document.getElementById("typing"); - let build = ""; - let showing = false; - let i = 0; - const curtime = Date.now() - 5000; - for (const thing of this.typingmap.keys()) { - if (this.typingmap.get(thing) > curtime) { - if (i !== 0) { - build += ", "; - } - i++; - if (thing.nick) { - build += thing.nick; - } - else { - build += thing.user.username; - } - showing = true; - } - else { - this.typingmap.delete(thing); - } - } - if (i > 1) { - build += " are typing"; - } - else { - build += " is typing"; - } - if (this.localuser.channelfocus === this) { - if (showing) { - typingtext.classList.remove("hidden"); - const typingtext2 = document.getElementById("typingtext"); - typingtext2.textContent = build; - } - else { - typingtext.classList.add("hidden"); - } - } - } - static regenLoadingMessages() { - const loading = document.getElementById("loadingdiv"); - loading.innerHTML = ""; - for (let i = 0; i < 15; i++) { - const div = document.createElement("div"); - div.classList.add("loadingmessage"); - if (Math.random() < 0.5) { - const pfp = document.createElement("div"); - pfp.classList.add("loadingpfp"); - const username = document.createElement("div"); - username.style.width = Math.floor(Math.random() * 96 * 1.5 + 40) + "px"; - username.classList.add("loadingcontent"); - div.append(pfp, username); - } - const content = document.createElement("div"); - content.style.width = Math.floor(Math.random() * 96 * 3 + 40) + "px"; - content.style.height = Math.floor(Math.random() * 3 + 1) * 20 + "px"; - content.classList.add("loadingcontent"); - div.append(content); - loading.append(div); - } - } - lastmessage; - async putmessages() { - if (this.allthewayup) { - return; - } - if (this.lastreadmessageid && this.messages.has(this.lastreadmessageid)) { - return; - } - const j = await fetch(this.info.api + "/channels/" + this.id + "/messages?limit=100", { - headers: this.headers, - }); - const response = await j.json(); - if (response.length !== 100) { - this.allthewayup = true; - } - let prev; - for (const thing of response) { - const message = new Message(thing, this); - if (prev) { - this.idToNext.set(message.id, prev.id); - this.idToPrev.set(prev.id, message.id); - } - else { - this.lastmessage = message; - this.lastmessageid = message.id; - } - prev = message; - } - } - delChannel(json) { - const build = []; - for (const thing of this.children) { - if (thing.id !== json.id) { - build.push(thing); - } - } - this.children = build; - } - async grabAfter(id) { - if (id === this.lastmessage?.id) { - return; - } - await fetch(this.info.api + "/channels/" + this.id + "/messages?limit=100&after=" + id, { - headers: this.headers - }).then(j => { - return j.json(); - }).then(response => { - let previd = id; - for (const i in response) { - let messager; - let willbreak = false; - if (this.messages.has(response[i].id)) { - messager = this.messages.get(response[i].id); - willbreak = true; - } - else { - messager = new Message(response[i], this); - } - this.idToPrev.set(messager.id, previd); - this.idToNext.set(previd, messager.id); - previd = messager.id; - if (willbreak) { - break; - } - } - //out.buildmessages(); - }); - } - topid; - async grabBefore(id) { - if (this.topid && id === this.topid) { - return; - } - await fetch(this.info.api + "/channels/" + this.id + "/messages?before=" + id + "&limit=100", { - headers: this.headers - }).then(j => { - return j.json(); - }).then((response) => { - if (response.length < 100) { - this.allthewayup = true; - if (response.length === 0) { - this.topid = id; - } - } - let previd = id; - for (const i in response) { - let messager; - let willbreak = false; - if (this.messages.has(response[i].id)) { - console.log("flaky"); - messager = this.messages.get(response[i].id); - willbreak = true; - } - else { - messager = new Message(response[i], this); - } - this.idToNext.set(messager.id, previd); - this.idToPrev.set(previd, messager.id); - previd = messager.id; - if (Number(i) === response.length - 1 && response.length < 100) { - this.topid = previd; - } - if (willbreak) { - break; - } - } - }); - } - /** - * Please dont use this, its not implemented. - * @deprecated - * @todo - **/ - async grabArround(id) { - throw new Error("please don't call this, no one has implemented it :P"); - } - async buildmessages() { - this.infinitefocus = false; - this.tryfocusinfinate(); - } - infinitefocus = false; - async tryfocusinfinate() { - if (this.infinitefocus) - return; - this.infinitefocus = true; - const messages = document.getElementById("channelw"); - for (const thing of messages.getElementsByClassName("messagecontainer")) { - thing.remove(); - } - const loading = document.getElementById("loadingdiv"); - const removetitle = document.getElementById("removetitle"); - //messages.innerHTML=""; - let id; - if (this.lastreadmessageid && this.messages.has(this.lastreadmessageid)) { - id = this.lastreadmessageid; - } - else if (this.lastreadmessageid && (id = this.findClosest(this.lastreadmessageid))) { - } - else if (this.lastmessageid && this.messages.has(this.lastmessageid)) { - id = this.goBackIds(this.lastmessageid, 50); - } - if (!id) { - if (!removetitle) { - const title = document.createElement("h2"); - title.id = "removetitle"; - title.textContent = "No messages appear to be here, be the first to say something!"; - title.classList.add("titlespace"); - messages.append(title); - } - this.infinitefocus = false; - loading.classList.remove("loading"); - return; - } - else if (removetitle) { - removetitle.remove(); - } - if (this.localuser.channelfocus !== this) { - return; - } - for (const elm of messages.getElementsByClassName("scroller")) { - elm.remove(); - console.warn("rouge element detected and removed"); - } - messages.append(await this.infinite.getDiv(id)); - this.infinite.updatestuff(); - this.infinite.watchForChange().then(async (_) => { - //await new Promise(resolve => setTimeout(resolve, 0)); - this.infinite.focus(id, false); //if someone could figure out how to make this work correctly without this, that's be great :P - loading.classList.remove("loading"); - }); - //this.infinite.focus(id.id,false); - } - goBackIds(id, back, returnifnotexistant = true) { - while (back !== 0) { - const nextid = this.idToPrev.get(id); - if (nextid) { - id = nextid; - back--; - } - else { - if (returnifnotexistant) { - break; - } - else { - return undefined; - } - } - } - return id; - } - findClosest(id) { - if (!this.lastmessageid || !id) - return; - let flake = this.lastmessageid; - const time = SnowFlake.stringToUnixTime(id); - let flaketime = SnowFlake.stringToUnixTime(flake); - while (flake && time < flaketime) { - flake = this.idToPrev.get(flake); - if (!flake) { - return; - } - flaketime = SnowFlake.stringToUnixTime(flake); - } - return flake; - } - updateChannel(json) { - this.type = json.type; - this.name = json.name; - const parent = this.localuser.channelids.get(json.parent_id); - if (parent) { - this.parent = parent; - this.parent_id = parent.id; - } - else { - this.parent = undefined; - this.parent_id = undefined; - } - this.children = []; - this.guild_id = json.guild_id; - this.permission_overwrites = new Map(); - for (const thing of json.permission_overwrites) { - if (thing.id === "1182819038095799904" || thing.id === "1182820803700625444") { - continue; - } - this.permission_overwrites.set(thing.id, new Permissions(thing.allow, thing.deny)); - const permisions = this.permission_overwrites.get(thing.id); - if (permisions) { - const role = this.guild.roleids.get(thing.id); - if (role) { - this.permission_overwritesar.push([role, permisions]); - } - } - } - this.topic = json.topic; - this.nsfw = json.nsfw; - } - typingstart() { - if (this.typing > Date.now()) { - return; - } - this.typing = Date.now() + 6000; - fetch(this.info.api + "/channels/" + this.id + "/typing", { - method: "POST", - headers: this.headers - }); - } - get notification() { - let notinumber = this.message_notifications; - if (Number(notinumber) === 3) { - notinumber = null; - } - notinumber ??= this.guild.message_notifications; - switch (Number(notinumber)) { - case 0: - return "all"; - case 1: - return "mentions"; - case 2: - return "none"; - case 3: - return "default"; - } - } - async sendMessage(content, { attachments = [], embeds = [], replyingto = null }) { - let replyjson; - if (replyingto) { - replyjson = - { - guild_id: replyingto.guild.id, - channel_id: replyingto.channel.id, - message_id: replyingto.id, - }; - } - if (attachments.length === 0) { - const body = { - content, - nonce: Math.floor(Math.random() * 1000000000), - message_reference: undefined - }; - if (replyjson) { - body.message_reference = replyjson; - } - return await fetch(this.info.api + "/channels/" + this.id + "/messages", { - method: "POST", - headers: this.headers, - body: JSON.stringify(body) - }); - } - else { - const formData = new FormData(); - const body = { - content, - nonce: Math.floor(Math.random() * 1000000000), - message_reference: undefined - }; - if (replyjson) { - body.message_reference = replyjson; - } - formData.append("payload_json", JSON.stringify(body)); - for (const i in attachments) { - formData.append("files[" + i + "]", attachments[i]); - } - return await fetch(this.info.api + "/channels/" + this.id + "/messages", { - method: "POST", - body: formData, - headers: { Authorization: this.headers.Authorization } - }); - } - } - messageCreate(messagep) { - if (!this.hasPermission("VIEW_CHANNEL")) { - return; - } - const messagez = new Message(messagep.d, this); - this.lastmessage = messagez; - if (this.lastmessageid) { - this.idToNext.set(this.lastmessageid, messagez.id); - this.idToPrev.set(messagez.id, this.lastmessageid); - } - this.lastmessageid = messagez.id; - if (messagez.author === this.localuser.user) { - this.lastreadmessageid = messagez.id; - if (this.myhtml) { - this.myhtml.classList.remove("cunread"); - } - } - else { - if (this.myhtml) { - this.myhtml.classList.add("cunread"); - } - } - this.guild.unreads(); - if (this === this.localuser.channelfocus) { - if (!this.infinitefocus) { - this.tryfocusinfinate(); - } - this.infinite.addedBottom(); - } - if (messagez.author === this.localuser.user) { - return; - } - if (this.localuser.lookingguild?.prevchannel === this && document.hasFocus()) { - return; - } - if (this.notification === "all") { - this.notify(messagez); - } - else if (this.notification === "mentions" && messagez.mentionsuser(this.localuser.user)) { - this.notify(messagez); - } - } - notititle(message) { - return message.author.username + " > " + this.guild.properties.name + " > " + this.name; - } - notify(message, deep = 0) { - Voice.noises(Voice.getNotificationSound()); - if (!("Notification" in window)) { - } - else if (Notification.permission === "granted") { - let noticontent = message.content.textContent; - if (message.embeds[0]) { - noticontent ||= message.embeds[0].json.title; - noticontent ||= message.content.textContent; - } - noticontent ||= "Blank Message"; - let imgurl = null; - const images = message.getimages(); - if (images.length) { - const image = images[0]; - if (image.proxy_url) { - imgurl ||= image.proxy_url; - } - imgurl ||= image.url; - } - const notification = new Notification(this.notititle(message), { - body: noticontent, - icon: message.author.getpfpsrc(), - image: imgurl, - }); - notification.addEventListener("click", _ => { - window.focus(); - this.getHTML(); - }); - } - else if (Notification.permission !== "denied") { - Notification.requestPermission().then(() => { - if (deep === 3) { - return; - } - this.notify(message, deep + 1); - }); - } - } - async addRoleToPerms(role) { - await fetch(this.info.api + "/channels/" + this.id + "/permissions/" + role.id, { - method: "PUT", - headers: this.headers, - body: JSON.stringify({ - allow: "0", - deny: "0", - id: role.id, - type: 0 - }) - }); - const perm = new Permissions("0", "0"); - this.permission_overwrites.set(role.id, perm); - this.permission_overwritesar.push([role, perm]); - } - async updateRolePermissions(id, perms) { - const permission = this.permission_overwrites.get(id); - if (permission) { - permission.allow = perms.allow; - permission.deny = perms.deny; - await fetch(this.info.api + "/channels/" + this.id + "/permissions/" + id, { - method: "PUT", - headers: this.headers, - body: JSON.stringify({ - allow: permission.allow.toString(), - deny: permission.deny.toString(), - id, - type: 0 - }) - }); - } - } -} -Channel.setupcontextmenu(); -export { Channel }; diff --git a/.dist/contextmenu.js b/.dist/contextmenu.js deleted file mode 100644 index 57905a5..0000000 --- a/.dist/contextmenu.js +++ /dev/null @@ -1,86 +0,0 @@ -class Contextmenu { - static currentmenu; - name; - buttons; - div; - static setup() { - Contextmenu.currentmenu = ""; - document.addEventListener("click", event => { - if (Contextmenu.currentmenu === "") { - return; - } - if (!Contextmenu.currentmenu.contains(event.target)) { - Contextmenu.currentmenu.remove(); - Contextmenu.currentmenu = ""; - } - }); - } - constructor(name) { - this.name = name; - this.buttons = []; - } - addbutton(text, onclick, img = null, shown = _ => true, enabled = _ => true) { - this.buttons.push([text, onclick, img, shown, enabled, "button"]); - return {}; - } - addsubmenu(text, onclick, img = null, shown = _ => true, enabled = _ => true) { - this.buttons.push([text, onclick, img, shown, enabled, "submenu"]); - return {}; - } - makemenu(x, y, addinfo, other) { - const div = document.createElement("div"); - div.classList.add("contextmenu", "flexttb"); - let visibleButtons = 0; - for (const thing of this.buttons) { - if (!thing[3].bind(addinfo)(other)) - continue; - visibleButtons++; - const intext = document.createElement("button"); - intext.disabled = !thing[4].bind(addinfo)(other); - intext.classList.add("contextbutton"); - intext.textContent = thing[0]; - console.log(thing); - if (thing[5] === "button" || thing[5] === "submenu") { - intext.onclick = thing[1].bind(addinfo, other); - } - div.appendChild(intext); - } - if (visibleButtons == 0) - return; - if (Contextmenu.currentmenu != "") { - Contextmenu.currentmenu.remove(); - } - div.style.top = y + "px"; - div.style.left = x + "px"; - document.body.appendChild(div); - Contextmenu.keepOnScreen(div); - console.log(div); - Contextmenu.currentmenu = div; - return this.div; - } - bindContextmenu(obj, addinfo, other) { - const func = event => { - event.preventDefault(); - event.stopImmediatePropagation(); - this.makemenu(event.clientX, event.clientY, addinfo, other); - }; - obj.addEventListener("contextmenu", func); - return func; - } - static keepOnScreen(obj) { - const html = document.documentElement.getBoundingClientRect(); - const docheight = html.height; - const docwidth = html.width; - const box = obj.getBoundingClientRect(); - console.log(box, docheight, docwidth); - if (box.right > docwidth) { - console.log("test"); - obj.style.left = docwidth - box.width + "px"; - } - if (box.bottom > docheight) { - obj.style.top = docheight - box.height + "px"; - } - } -} -Contextmenu.setup(); -export { Contextmenu }; diff --git a/.dist/dialog.js b/.dist/dialog.js deleted file mode 100644 index c7ccdfe..0000000 --- a/.dist/dialog.js +++ /dev/null @@ -1,244 +0,0 @@ -class Dialog { - layout; - onclose; - onopen; - html; - background; - constructor(layout, onclose = _ => { }, onopen = _ => { }) { - this.layout = layout; - this.onclose = onclose; - this.onopen = onopen; - const div = document.createElement("div"); - div.appendChild(this.tohtml(layout)); - this.html = div; - this.html.classList.add("centeritem"); - if (!(layout[0] === "img")) { - this.html.classList.add("nonimagecenter"); - } - } - tohtml(array) { - switch (array[0]) { - case "img": - const img = document.createElement("img"); - img.src = array[1]; - if (array[2] != undefined) { - if (array[2].length === 2) { - img.width = array[2][0]; - img.height = array[2][1]; - } - else if (array[2][0] === "fit") { - img.classList.add("imgfit"); - } - } - return img; - case "hdiv": - const hdiv = document.createElement("div"); - hdiv.classList.add("flexltr"); - for (const thing of array) { - if (thing === "hdiv") { - continue; - } - hdiv.appendChild(this.tohtml(thing)); - } - return hdiv; - case "vdiv": - const vdiv = document.createElement("div"); - vdiv.classList.add("flexttb"); - for (const thing of array) { - if (thing === "vdiv") { - continue; - } - vdiv.appendChild(this.tohtml(thing)); - } - return vdiv; - case "checkbox": - { - const div = document.createElement("div"); - const checkbox = document.createElement("input"); - div.appendChild(checkbox); - const label = document.createElement("span"); - checkbox.checked = array[2]; - label.textContent = array[1]; - div.appendChild(label); - checkbox.addEventListener("change", array[3]); - checkbox.type = "checkbox"; - return div; - } - case "button": - { - const div = document.createElement("div"); - const input = document.createElement("button"); - const label = document.createElement("span"); - input.textContent = array[2]; - label.textContent = array[1]; - div.appendChild(label); - div.appendChild(input); - input.addEventListener("click", array[3]); - return div; - } - case "mdbox": - { - const div = document.createElement("div"); - const input = document.createElement("textarea"); - input.value = array[2]; - const label = document.createElement("span"); - label.textContent = array[1]; - input.addEventListener("input", array[3]); - div.appendChild(label); - div.appendChild(document.createElement("br")); - div.appendChild(input); - return div; - } - case "textbox": - { - const div = document.createElement("div"); - const input = document.createElement("input"); - input.value = array[2]; - input.type = "text"; - const label = document.createElement("span"); - label.textContent = array[1]; - console.log(array[3]); - input.addEventListener("input", array[3]); - div.appendChild(label); - div.appendChild(input); - return div; - } - case "fileupload": - { - const div = document.createElement("div"); - const input = document.createElement("input"); - input.type = "file"; - const label = document.createElement("span"); - label.textContent = array[1]; - div.appendChild(label); - div.appendChild(input); - input.addEventListener("change", array[2]); - console.log(array); - return div; - } - case "text": { - const span = document.createElement("span"); - span.textContent = array[1]; - return span; - } - case "title": { - const span = document.createElement("span"); - span.classList.add("title"); - span.textContent = array[1]; - return span; - } - case "radio": { - const div = document.createElement("div"); - const fieldset = document.createElement("fieldset"); - fieldset.addEventListener("change", () => { - let i = -1; - for (const thing of fieldset.children) { - i++; - if (i === 0) { - continue; - } - const checkbox = thing.children[0].children[0]; - if (checkbox.checked) { - array[3](checkbox.value); - } - } - }); - const legend = document.createElement("legend"); - legend.textContent = array[1]; - fieldset.appendChild(legend); - let i = 0; - for (const thing of array[2]) { - const div = document.createElement("div"); - const input = document.createElement("input"); - input.classList.add("radio"); - input.type = "radio"; - input.name = array[1]; - input.value = thing; - if (i === array[4]) { - input.checked = true; - } - const label = document.createElement("label"); - label.appendChild(input); - const span = document.createElement("span"); - span.textContent = thing; - label.appendChild(span); - div.appendChild(label); - fieldset.appendChild(div); - i++; - } - div.appendChild(fieldset); - return div; - } - case "html": - return array[1]; - case "select": { - const div = document.createElement("div"); - const label = document.createElement("label"); - const select = document.createElement("select"); - label.textContent = array[1]; - div.append(label); - div.appendChild(select); - for (const thing of array[2]) { - const option = document.createElement("option"); - option.textContent = thing; - select.appendChild(option); - } - select.selectedIndex = array[4]; - select.addEventListener("change", array[3]); - return div; - } - case "tabs": { - const table = document.createElement("div"); - table.classList.add("flexttb"); - const tabs = document.createElement("div"); - tabs.classList.add("flexltr"); - tabs.classList.add("tabbed-head"); - table.appendChild(tabs); - const content = document.createElement("div"); - content.classList.add("tabbed-content"); - table.appendChild(content); - let shown; - for (const thing of array[1]) { - const button = document.createElement("button"); - button.textContent = thing[0]; - tabs.appendChild(button); - const html = this.tohtml(thing[1]); - content.append(html); - if (!shown) { - shown = html; - } - else { - html.style.display = "none"; - } - button.addEventListener("click", _ => { - if (shown) { - shown.style.display = "none"; - } - html.style.display = ""; - shown = html; - }); - } - return table; - } - default: - console.error("can't find element:" + array[0], " full element:", array); - return document.createElement("span"); - } - } - show() { - this.onopen(); - console.log("fullscreen"); - this.background = document.createElement("div"); - this.background.classList.add("background"); - document.body.appendChild(this.background); - document.body.appendChild(this.html); - this.background.onclick = _ => { - this.hide(); - }; - } - hide() { - document.body.removeChild(this.background); - document.body.removeChild(this.html); - } -} -export { Dialog }; diff --git a/.dist/direct.js b/.dist/direct.js deleted file mode 100644 index cdcad07..0000000 --- a/.dist/direct.js +++ /dev/null @@ -1,283 +0,0 @@ -import { Guild } from "./guild.js"; -import { Channel } from "./channel.js"; -import { Message } from "./message.js"; -import { User } from "./user.js"; -import { Permissions } from "./permissions.js"; -import { SnowFlake } from "./snowflake.js"; -import { Contextmenu } from "./contextmenu.js"; -class Direct extends Guild { - getUnixTime() { - throw new Error("Do not call this for Direct, it does not make sense"); - } - constructor(json, owner) { - super(-1, owner, null); - this.message_notifications = 0; - this.owner = owner; - if (!this.localuser) { - console.error("Owner was not included, please fix"); - } - this.headers = this.localuser.headers; - this.channels = []; - this.channelids = {}; - this.properties = {}; - this.roles = []; - this.roleids = new Map(); - this.prevchannel = undefined; - this.properties.name = "Direct Messages"; - for (const thing of json) { - const temp = new Group(thing, this); - this.channels.push(temp); - this.channelids[temp.id] = temp; - } - this.headchannels = this.channels; - } - createChannelpac(json) { - const thischannel = new Group(json, this); - this.channelids[thischannel.id] = thischannel; - this.channels.push(thischannel); - this.sortchannels(); - this.printServers(); - return thischannel; - } - delChannel(json) { - const channel = this.channelids[json.id]; - super.delChannel(json); - if (channel) { - channel.del(); - } - } - giveMember(_member) { - console.error("not a real guild, can't give member object"); - } - getRole(ID) { - return null; - } - hasRole(r) { - return false; - } - isAdmin() { - return false; - } - unreaddms() { - for (const thing of this.channels) { - thing.unreads(); - } - } -} -const dmPermissions = new Permissions("0"); -dmPermissions.setPermission("ADD_REACTIONS", 1); -dmPermissions.setPermission("VIEW_CHANNEL", 1); -dmPermissions.setPermission("SEND_MESSAGES", 1); -dmPermissions.setPermission("EMBED_LINKS", 1); -dmPermissions.setPermission("ATTACH_FILES", 1); -dmPermissions.setPermission("READ_MESSAGE_HISTORY", 1); -dmPermissions.setPermission("MENTION_EVERYONE", 1); -dmPermissions.setPermission("USE_EXTERNAL_EMOJIS", 1); -dmPermissions.setPermission("USE_APPLICATION_COMMANDS", 1); -dmPermissions.setPermission("USE_EXTERNAL_STICKERS", 1); -dmPermissions.setPermission("USE_EMBEDDED_ACTIVITIES", 1); -dmPermissions.setPermission("USE_SOUNDBOARD", 1); -dmPermissions.setPermission("USE_EXTERNAL_SOUNDS", 1); -dmPermissions.setPermission("SEND_VOICE_MESSAGES", 1); -dmPermissions.setPermission("SEND_POLLS", 1); -dmPermissions.setPermission("USE_EXTERNAL_APPS", 1); -dmPermissions.setPermission("CONNECT", 1); -dmPermissions.setPermission("SPEAK", 1); -dmPermissions.setPermission("STREAM", 1); -dmPermissions.setPermission("USE_VAD", 1); -class Group extends Channel { - user; - static contextmenu = new Contextmenu("channel menu"); - static setupcontextmenu() { - this.contextmenu.addbutton("Copy DM id", function () { - navigator.clipboard.writeText(this.id); - }); - this.contextmenu.addbutton("Mark as read", function () { - this.readbottom(); - }); - this.contextmenu.addbutton("Close DM", function () { - this.deleteChannel(); - }); - this.contextmenu.addbutton("Copy user ID", function () { - navigator.clipboard.writeText(this.user.id); - }); - } - constructor(json, owner) { - super(-1, owner, json.id); - this.owner = owner; - this.headers = this.guild.headers; - this.name = json.recipients[0]?.username; - if (json.recipients[0]) { - this.user = new User(json.recipients[0], this.localuser); - } - else { - this.user = this.localuser.user; - } - this.name ??= this.localuser.user.username; - this.parent_id = undefined; - this.parent = null; - this.children = []; - this.guild_id = "@me"; - this.permission_overwrites = new Map(); - this.lastmessageid = json.last_message_id; - this.mentions = 0; - this.setUpInfiniteScroller(); - this.updatePosition(); - } - updatePosition() { - if (this.lastmessageid) { - this.position = SnowFlake.stringToUnixTime(this.lastmessageid); - } - else { - this.position = 0; - } - this.position = -Math.max(this.position, this.getUnixTime()); - } - createguildHTML() { - const div = document.createElement("div"); - Group.contextmenu.bindContextmenu(div, this, undefined); - this.html = new WeakRef(div); - div.classList.add("channeleffects"); - const myhtml = document.createElement("span"); - myhtml.textContent = this.name; - div.appendChild(this.user.buildpfp()); - div.appendChild(myhtml); - div["myinfo"] = this; - div.onclick = _ => { - this.getHTML(); - }; - return div; - } - async getHTML() { - const id = ++Channel.genid; - if (this.localuser.channelfocus) { - this.localuser.channelfocus.infinite.delete(); - } - if (this.guild !== this.localuser.lookingguild) { - this.guild.loadGuild(); - } - this.guild.prevchannel = this; - this.localuser.channelfocus = this; - const prom = this.infinite.delete(); - history.pushState(null, "", "/channels/" + this.guild_id + "/" + this.id); - this.localuser.pageTitle("@" + this.name); - document.getElementById("channelTopic").setAttribute("hidden", ""); - const loading = document.getElementById("loadingdiv"); - Channel.regenLoadingMessages(); - loading.classList.add("loading"); - this.rendertyping(); - await this.putmessages(); - await prom; - if (id !== Channel.genid) { - return; - } - this.buildmessages(); - document.getElementById("typebox").contentEditable = "" + true; - } - messageCreate(messagep) { - const messagez = new Message(messagep.d, this); - if (this.lastmessageid) { - this.idToNext.set(this.lastmessageid, messagez.id); - this.idToPrev.set(messagez.id, this.lastmessageid); - } - this.lastmessageid = messagez.id; - if (messagez.author === this.localuser.user) { - this.lastreadmessageid = messagez.id; - if (this.myhtml) { - this.myhtml.classList.remove("cunread"); - } - } - else { - if (this.myhtml) { - this.myhtml.classList.add("cunread"); - } - } - this.unreads(); - this.updatePosition(); - this.infinite.addedBottom(); - this.guild.sortchannels(); - if (this.myhtml) { - const parrent = this.myhtml.parentElement; - parrent.prepend(this.myhtml); - } - if (this === this.localuser.channelfocus) { - if (!this.infinitefocus) { - this.tryfocusinfinate(); - } - this.infinite.addedBottom(); - } - this.unreads(); - if (messagez.author === this.localuser.user) { - return; - } - if (this.localuser.lookingguild?.prevchannel === this && document.hasFocus()) { - return; - } - if (this.notification === "all") { - this.notify(messagez); - } - else if (this.notification === "mentions" && messagez.mentionsuser(this.localuser.user)) { - this.notify(messagez); - } - } - notititle(message) { - return message.author.username; - } - readbottom() { - super.readbottom(); - this.unreads(); - } - all = new WeakRef(document.createElement("div")); - noti = new WeakRef(document.createElement("div")); - del() { - const all = this.all.deref(); - if (all) { - all.remove(); - } - if (this.myhtml) { - this.myhtml.remove(); - } - } - unreads() { - const sentdms = document.getElementById("sentdms"); //Need to change sometime - const current = this.all.deref(); - if (this.hasunreads) { - { - const noti = this.noti.deref(); - if (noti) { - noti.textContent = this.mentions + ""; - return; - } - } - const div = document.createElement("div"); - div.classList.add("servernoti"); - const noti = document.createElement("div"); - noti.classList.add("unread", "notiunread", "pinged"); - noti.textContent = "" + this.mentions; - this.noti = new WeakRef(noti); - div.append(noti); - const buildpfp = this.user.buildpfp(); - this.all = new WeakRef(div); - buildpfp.classList.add("mentioned"); - div.append(buildpfp); - sentdms.append(div); - div.onclick = _ => { - this.guild.loadGuild(); - this.getHTML(); - }; - } - else if (current) { - current.remove(); - } - else { - } - } - isAdmin() { - return false; - } - hasPermission(name) { - return dmPermissions.hasPermission(name); - } -} -export { Direct, Group }; -Group.setupcontextmenu(); diff --git a/.dist/embed.js b/.dist/embed.js deleted file mode 100644 index 89bcbbf..0000000 --- a/.dist/embed.js +++ /dev/null @@ -1,385 +0,0 @@ -import { Dialog } from "./dialog.js"; -import { MarkDown } from "./markdown.js"; -import { getapiurls, getInstances } from "./login.js"; -import { Guild } from "./guild.js"; -class Embed { - type; - owner; - json; - constructor(json, owner) { - this.type = this.getType(json); - this.owner = owner; - this.json = json; - } - getType(json) { - const instances = getInstances(); - if (instances && json.type === "link" && json.url && URL.canParse(json.url)) { - const Url = new URL(json.url); - for (const instance of instances) { - if (instance.url && URL.canParse(instance.url)) { - const IUrl = new URL(instance.url); - const params = new URLSearchParams(Url.search); - let host; - if (params.has("instance")) { - const url = params.get("instance"); - if (URL.canParse(url)) { - host = new URL(url).host; - } - else { - host = Url.host; - } - } - else { - host = Url.host; - } - if (IUrl.host === host) { - const code = Url.pathname.split("/")[Url.pathname.split("/").length - 1]; - json.invite = { - url: instance.url, - code - }; - return "invite"; - } - } - } - } - return json.type || "rich"; - } - generateHTML() { - switch (this.type) { - case "rich": - return this.generateRich(); - case "image": - return this.generateImage(); - case "invite": - return this.generateInvite(); - case "link": - return this.generateLink(); - case "video": - case "article": - return this.generateArticle(); - default: - console.warn(`unsupported embed type ${this.type}, please add support dev :3`, this.json); - return document.createElement("div"); //prevent errors by giving blank div - } - } - get message() { - return this.owner; - } - get channel() { - return this.message.channel; - } - get guild() { - return this.channel.guild; - } - get localuser() { - return this.guild.localuser; - } - generateRich() { - const div = document.createElement("div"); - if (this.json.color) { - div.style.backgroundColor = "#" + this.json.color.toString(16); - } - div.classList.add("embed-color"); - const embed = document.createElement("div"); - embed.classList.add("embed"); - div.append(embed); - if (this.json.author) { - const authorline = document.createElement("div"); - if (this.json.author.icon_url) { - const img = document.createElement("img"); - img.classList.add("embedimg"); - img.src = this.json.author.icon_url; - authorline.append(img); - } - const a = document.createElement("a"); - a.textContent = this.json.author.name; - if (this.json.author.url) { - MarkDown.safeLink(a, this.json.author.url); - } - a.classList.add("username"); - authorline.append(a); - embed.append(authorline); - } - if (this.json.title) { - const title = document.createElement("a"); - title.append(new MarkDown(this.json.title, this.channel).makeHTML()); - if (this.json.url) { - MarkDown.safeLink(title, this.json.url); - } - title.classList.add("embedtitle"); - embed.append(title); - } - if (this.json.description) { - const p = document.createElement("p"); - p.append(new MarkDown(this.json.description, this.channel).makeHTML()); - embed.append(p); - } - embed.append(document.createElement("br")); - if (this.json.fields) { - for (const thing of this.json.fields) { - const div = document.createElement("div"); - const b = document.createElement("b"); - b.textContent = thing.name; - div.append(b); - const p = document.createElement("p"); - p.append(new MarkDown(thing.value, this.channel).makeHTML()); - p.classList.add("embedp"); - div.append(p); - if (thing.inline) { - div.classList.add("inline"); - } - embed.append(div); - } - } - if (this.json.footer || this.json.timestamp) { - const footer = document.createElement("div"); - if (this.json?.footer?.icon_url) { - const img = document.createElement("img"); - img.src = this.json.footer.icon_url; - img.classList.add("embedicon"); - footer.append(img); - } - if (this.json?.footer?.text) { - const span = document.createElement("span"); - span.textContent = this.json.footer.text; - span.classList.add("spaceright"); - footer.append(span); - } - if (this.json?.footer && this.json?.timestamp) { - const span = document.createElement("span"); - span.textContent = "•"; - span.classList.add("spaceright"); - footer.append(span); - } - if (this.json?.timestamp) { - const span = document.createElement("span"); - span.textContent = new Date(this.json.timestamp).toLocaleString(); - footer.append(span); - } - embed.append(footer); - } - return div; - } - generateImage() { - const img = document.createElement("img"); - img.classList.add("messageimg"); - img.onclick = function () { - const full = new Dialog(["img", img.src, ["fit"]]); - full.show(); - }; - img.src = this.json.thumbnail.proxy_url; - if (this.json.thumbnail.width) { - let scale = 1; - const max = 96 * 3; - scale = Math.max(scale, this.json.thumbnail.width / max); - scale = Math.max(scale, this.json.thumbnail.height / max); - this.json.thumbnail.width /= scale; - this.json.thumbnail.height /= scale; - } - img.style.width = this.json.thumbnail.width + "px"; - img.style.height = this.json.thumbnail.height + "px"; - console.log(this.json, "Image fix"); - return img; - } - generateLink() { - const table = document.createElement("table"); - table.classList.add("embed", "linkembed"); - const trtop = document.createElement("tr"); - table.append(trtop); - if (this.json.url && this.json.title) { - const td = document.createElement("td"); - const a = document.createElement("a"); - MarkDown.safeLink(a, this.json.url); - a.textContent = this.json.title; - td.append(a); - trtop.append(td); - } - { - const td = document.createElement("td"); - const img = document.createElement("img"); - if (this.json.thumbnail) { - img.classList.add("embedimg"); - img.onclick = function () { - const full = new Dialog(["img", img.src, ["fit"]]); - full.show(); - }; - img.src = this.json.thumbnail.proxy_url; - td.append(img); - } - trtop.append(td); - } - const bottomtr = document.createElement("tr"); - const td = document.createElement("td"); - if (this.json.description) { - const span = document.createElement("span"); - span.textContent = this.json.description; - td.append(span); - } - bottomtr.append(td); - table.append(bottomtr); - return table; - } - invcache; - generateInvite() { - if (this.invcache && (!this.json.invite || !this.localuser)) { - return this.generateLink(); - } - const div = document.createElement("div"); - div.classList.add("embed", "inviteEmbed", "flexttb"); - const json1 = this.json.invite; - (async () => { - let json; - let info; - if (!this.invcache) { - if (!json1) { - div.append(this.generateLink()); - return; - } - const tempinfo = await getapiurls(json1.url); - ; - if (!tempinfo) { - div.append(this.generateLink()); - return; - } - info = tempinfo; - const res = await fetch(info.api + "/invites/" + json1.code); - if (!res.ok) { - div.append(this.generateLink()); - } - json = await res.json(); - this.invcache = [json, info]; - } - else { - [json, info] = this.invcache; - } - if (!json) { - div.append(this.generateLink()); - return; - } - if (json.guild.banner) { - const banner = document.createElement("img"); - banner.src = this.localuser.info.cdn + "/icons/" + json.guild.id + "/" + json.guild.banner + ".png?size=256"; - banner.classList.add("banner"); - div.append(banner); - } - const guild = json.guild; - guild.info = info; - const icon = Guild.generateGuildIcon(guild); - const iconrow = document.createElement("div"); - iconrow.classList.add("flexltr", "flexstart"); - iconrow.append(icon); - { - const guildinfo = document.createElement("div"); - guildinfo.classList.add("flexttb", "invguildinfo"); - const name = document.createElement("b"); - name.textContent = guild.name; - guildinfo.append(name); - const members = document.createElement("span"); - members.innerText = "#" + json.channel.name + " • Members: " + guild.member_count; - guildinfo.append(members); - members.classList.add("subtext"); - iconrow.append(guildinfo); - } - div.append(iconrow); - const h2 = document.createElement("h2"); - h2.textContent = `You've been invited by ${json.inviter.username}`; - div.append(h2); - const button = document.createElement("button"); - button.textContent = "Accept"; - if (this.localuser.info.api.startsWith(info.api)) { - if (this.localuser.guildids.has(guild.id)) { - button.textContent = "Already joined"; - button.disabled = true; - } - } - button.classList.add("acceptinvbutton"); - div.append(button); - button.onclick = _ => { - if (this.localuser.info.api.startsWith(info.api)) { - fetch(this.localuser.info.api + "/invites/" + json.code, { - method: "POST", - headers: this.localuser.headers, - }).then(r => r.json()).then(_ => { - if (_.message) { - alert(_.message); - } - }); - } - else { - if (this.json.invite) { - const params = new URLSearchParams(""); - params.set("instance", this.json.invite.url); - const encoded = params.toString(); - const url = `${location.origin}/invite/${this.json.invite.code}?${encoded}`; - window.open(url, "_blank"); - } - } - }; - })(); - return div; - } - generateArticle() { - const colordiv = document.createElement("div"); - colordiv.style.backgroundColor = "#000000"; - colordiv.classList.add("embed-color"); - const div = document.createElement("div"); - div.classList.add("embed"); - if (this.json.provider) { - const provider = document.createElement("p"); - provider.classList.add("provider"); - provider.textContent = this.json.provider.name; - div.append(provider); - } - const a = document.createElement("a"); - if (this.json.url && this.json.url) { - MarkDown.safeLink(a, this.json.url); - a.textContent = this.json.url; - div.append(a); - } - if (this.json.description) { - const description = document.createElement("p"); - description.textContent = this.json.description; - div.append(description); - } - if (this.json.thumbnail) { - const img = document.createElement("img"); - if (this.json.thumbnail.width && this.json.thumbnail.width) { - let scale = 1; - const inch = 96; - scale = Math.max(scale, this.json.thumbnail.width / inch / 4); - scale = Math.max(scale, this.json.thumbnail.height / inch / 3); - this.json.thumbnail.width /= scale; - this.json.thumbnail.height /= scale; - img.style.width = this.json.thumbnail.width + "px"; - img.style.height = this.json.thumbnail.height + "px"; - } - img.classList.add("bigembedimg"); - if (this.json.video) { - img.onclick = async () => { - if (this.json.video) { - img.remove(); - const iframe = document.createElement("iframe"); - iframe.src = this.json.video.url + "?autoplay=1"; - if (this.json.thumbnail.width && this.json.thumbnail.width) { - iframe.style.width = this.json.thumbnail.width + "px"; - iframe.style.height = this.json.thumbnail.height + "px"; - } - div.append(iframe); - } - }; - } - else { - img.onclick = async () => { - const full = new Dialog(["img", img.src, ["fit"]]); - full.show(); - }; - } - img.src = this.json.thumbnail.proxy_url || this.json.thumbnail.url; - div.append(img); - } - colordiv.append(div); - return colordiv; - } -} -export { Embed }; diff --git a/.dist/emoji.js b/.dist/emoji.js deleted file mode 100644 index d475720..0000000 --- a/.dist/emoji.js +++ /dev/null @@ -1,205 +0,0 @@ -import { Contextmenu } from "./contextmenu.js"; -import { Guild } from "./guild.js"; -class Emoji { - static emojis; - name; - id; - animated; - owner; - get guild() { - if (this.owner instanceof Guild) { - return this.owner; - } - } - get localuser() { - if (this.owner instanceof Guild) { - return this.owner.localuser; - } - else { - return this.owner; - } - } - get info() { - return this.owner.info; - } - constructor(json, owner) { - this.name = json.name; - this.id = json.id; - this.animated = json.animated; - this.owner = owner; - } - getHTML(bigemoji = false) { - const emojiElem = document.createElement("img"); - emojiElem.classList.add("md-emoji"); - emojiElem.classList.add(bigemoji ? "bigemoji" : "smallemoji"); - emojiElem.crossOrigin = "anonymous"; - emojiElem.src = this.info.cdn + "/emojis/" + this.id + "." + (this.animated ? "gif" : "png") + "?size=32"; - emojiElem.alt = this.name; - emojiElem.loading = "lazy"; - return emojiElem; - } - static decodeEmojiList(buffer) { - const view = new DataView(buffer, 0); - let i = 0; - function read16() { - const int = view.getUint16(i); - i += 2; - return int; - } - function read8() { - const int = view.getUint8(i); - i += 1; - return int; - } - function readString8() { - return readStringNo(read8()); - } - function readString16() { - return readStringNo(read16()); - } - function readStringNo(length) { - const array = new Uint8Array(length); - for (let i = 0; i < length; i++) { - array[i] = read8(); - } - //console.log(array); - return new TextDecoder("utf-8").decode(array.buffer); - } - const build = []; - let cats = read16(); - for (; cats !== 0; cats--) { - const name = readString16(); - const emojis = []; - let emojinumber = read16(); - for (; emojinumber !== 0; emojinumber--) { - //console.log(emojis); - const name = readString8(); - const len = read8(); - const skin_tone_support = len > 127; - const emoji = readStringNo(len - (Number(skin_tone_support) * 128)); - emojis.push({ - name, - skin_tone_support, - emoji - }); - } - build.push({ - name, - emojis - }); - } - this.emojis = build; - console.log(build); - } - static grabEmoji() { - fetch("/emoji.bin").then(e => { - return e.arrayBuffer(); - }).then(e => { - Emoji.decodeEmojiList(e); - }); - } - static async emojiPicker(x, y, localuser) { - let res; - const promise = new Promise(r => { - res = r; - }); - const menu = document.createElement("div"); - menu.classList.add("flexttb", "emojiPicker"); - menu.style.top = y + "px"; - menu.style.left = x + "px"; - const title = document.createElement("h2"); - title.textContent = Emoji.emojis[0].name; - title.classList.add("emojiTitle"); - menu.append(title); - const selection = document.createElement("div"); - selection.classList.add("flexltr", "dontshrink", "emojirow"); - const body = document.createElement("div"); - body.classList.add("emojiBody"); - let isFirst = true; - localuser.guilds.filter(guild => guild.id != "@me" && guild.emojis.length > 0).forEach(guild => { - const select = document.createElement("div"); - select.classList.add("emojiSelect"); - if (guild.properties.icon) { - const img = document.createElement("img"); - img.classList.add("pfp", "servericon", "emoji-server"); - img.crossOrigin = "anonymous"; - img.src = localuser.info.cdn + "/icons/" + guild.properties.id + "/" + guild.properties.icon + ".png?size=48"; - img.alt = "Server: " + guild.properties.name; - select.appendChild(img); - } - else { - const div = document.createElement("span"); - div.textContent = guild.properties.name.replace(/'s /g, " ").replace(/\w+/g, word => word[0]).replace(/\s/g, ""); - select.append(div); - } - selection.append(select); - const clickEvent = () => { - title.textContent = guild.properties.name; - body.innerHTML = ""; - for (const emojit of guild.emojis) { - const emojiElem = document.createElement("div"); - emojiElem.classList.add("emojiSelect"); - const emojiClass = new Emoji({ - id: emojit.id, - name: emojit.name, - animated: emojit.animated - }, localuser); - emojiElem.append(emojiClass.getHTML()); - body.append(emojiElem); - emojiElem.addEventListener("click", () => { - res(emojiClass); - if (Contextmenu.currentmenu !== "") { - Contextmenu.currentmenu.remove(); - } - }); - } - }; - select.addEventListener("click", clickEvent); - if (isFirst) { - clickEvent(); - isFirst = false; - } - }); - setTimeout(() => { - if (Contextmenu.currentmenu != "") { - Contextmenu.currentmenu.remove(); - } - document.body.append(menu); - Contextmenu.currentmenu = menu; - Contextmenu.keepOnScreen(menu); - }, 10); - let i = 0; - for (const thing of Emoji.emojis) { - const select = document.createElement("div"); - select.textContent = thing.emojis[0].emoji; - select.classList.add("emojiSelect"); - selection.append(select); - const clickEvent = () => { - title.textContent = thing.name; - body.innerHTML = ""; - for (const emojit of thing.emojis) { - const emoji = document.createElement("div"); - emoji.classList.add("emojiSelect"); - emoji.textContent = emojit.emoji; - body.append(emoji); - emoji.onclick = _ => { - res(emojit.emoji); - if (Contextmenu.currentmenu !== "") { - Contextmenu.currentmenu.remove(); - } - }; - } - }; - select.onclick = clickEvent; - if (i === 0) { - clickEvent(); - } - i++; - } - menu.append(selection); - menu.append(body); - return promise; - } -} -Emoji.grabEmoji(); -export { Emoji }; diff --git a/.dist/file.js b/.dist/file.js deleted file mode 100644 index b70f062..0000000 --- a/.dist/file.js +++ /dev/null @@ -1,145 +0,0 @@ -import { Dialog } from "./dialog.js"; -class File { - owner; - id; - filename; - content_type; - width; - height; - proxy_url; - url; - size; - constructor(fileJSON, owner) { - this.owner = owner; - this.id = fileJSON.id; - this.filename = fileJSON.filename; - this.content_type = fileJSON.content_type; - this.width = fileJSON.width; - this.height = fileJSON.height; - this.url = fileJSON.url; - this.proxy_url = fileJSON.proxy_url; - this.content_type = fileJSON.content_type; - this.size = fileJSON.size; - } - getHTML(temp = false) { - const src = this.proxy_url || this.url; - if (this.width && this.height) { - let scale = 1; - const max = 96 * 3; - scale = Math.max(scale, this.width / max); - scale = Math.max(scale, this.height / max); - this.width /= scale; - this.height /= scale; - } - if (this.content_type.startsWith("image/")) { - const div = document.createElement("div"); - const img = document.createElement("img"); - img.classList.add("messageimg"); - div.classList.add("messageimgdiv"); - img.onclick = function () { - const full = new Dialog(["img", img.src, ["fit"]]); - full.show(); - }; - img.src = src; - div.append(img); - if (this.width) { - div.style.width = this.width + "px"; - div.style.height = this.height + "px"; - } - console.log(img); - console.log(this.width, this.height); - return div; - } - else if (this.content_type.startsWith("video/")) { - const video = document.createElement("video"); - const source = document.createElement("source"); - source.src = src; - video.append(source); - source.type = this.content_type; - video.controls = !temp; - if (this.width && this.height) { - video.width = this.width; - video.height = this.height; - } - return video; - } - else if (this.content_type.startsWith("audio/")) { - const audio = document.createElement("audio"); - const source = document.createElement("source"); - source.src = src; - audio.append(source); - source.type = this.content_type; - audio.controls = !temp; - return audio; - } - else { - return this.createunknown(); - } - } - upHTML(files, file) { - const div = document.createElement("div"); - const contained = this.getHTML(true); - div.classList.add("containedFile"); - div.append(contained); - const controls = document.createElement("div"); - const garbage = document.createElement("button"); - garbage.textContent = "🗑"; - garbage.onclick = _ => { - div.remove(); - files.splice(files.indexOf(file), 1); - }; - controls.classList.add("controls"); - div.append(controls); - controls.append(garbage); - return div; - } - static initFromBlob(file) { - return new File({ - filename: file.name, - size: file.size, - id: "null", - content_type: file.type, - width: undefined, - height: undefined, - url: URL.createObjectURL(file), - proxy_url: undefined - }, null); - } - createunknown() { - console.log("🗎"); - const src = this.proxy_url || this.url; - const div = document.createElement("table"); - div.classList.add("unknownfile"); - const nametr = document.createElement("tr"); - div.append(nametr); - const fileicon = document.createElement("td"); - nametr.append(fileicon); - fileicon.append("🗎"); - fileicon.classList.add("fileicon"); - fileicon.rowSpan = 2; - const nametd = document.createElement("td"); - if (src) { - const a = document.createElement("a"); - a.href = src; - a.textContent = this.filename; - nametd.append(a); - } - else { - nametd.textContent = this.filename; - } - nametd.classList.add("filename"); - nametr.append(nametd); - const sizetr = document.createElement("tr"); - const size = document.createElement("td"); - sizetr.append(size); - size.textContent = "Size:" + File.filesizehuman(this.size); - size.classList.add("filesize"); - div.appendChild(sizetr); - return div; - } - static filesizehuman(fsize) { - const i = fsize == 0 ? 0 : Math.floor(Math.log(fsize) / Math.log(1024)); - return Number((fsize / Math.pow(1024, i)).toFixed(2)) * 1 + " " + ["Bytes", "Kilobytes", "Megabytes", "Gigabytes", "Terabytes"][i]; - } -} -export { File }; diff --git a/.dist/guild.js b/.dist/guild.js deleted file mode 100644 index d774d06..0000000 --- a/.dist/guild.js +++ /dev/null @@ -1,604 +0,0 @@ -import { Channel } from "./channel.js"; -import { Contextmenu } from "./contextmenu.js"; -import { Role, RoleList } from "./role.js"; -import { Dialog } from "./dialog.js"; -import { Member } from "./member.js"; -import { Settings } from "./settings.js"; -import { SnowFlake } from "./snowflake.js"; -import { User } from "./user.js"; -class Guild extends SnowFlake { - owner; - headers; - channels; - properties; - member_count; - roles; - roleids; - prevchannel; - message_notifications; - headchannels; - position; - parent_id; - member; - html; - emojis; - large; - static contextmenu = new Contextmenu("guild menu"); - static setupcontextmenu() { - Guild.contextmenu.addbutton("Copy Guild id", function () { - navigator.clipboard.writeText(this.id); - }); - Guild.contextmenu.addbutton("Mark as read", function () { - this.markAsRead(); - }); - Guild.contextmenu.addbutton("Notifications", function () { - this.setnotifcation(); - }); - Guild.contextmenu.addbutton("Leave guild", function () { - this.confirmleave(); - }, null, function (_) { - return this.properties.owner_id !== this.member.user.id; - }); - Guild.contextmenu.addbutton("Delete guild", function () { - this.confirmDelete(); - }, null, function (_) { - return this.properties.owner_id === this.member.user.id; - }); - Guild.contextmenu.addbutton("Create invite", function () { - }, null, _ => true, _ => false); - Guild.contextmenu.addbutton("Settings", function () { - this.generateSettings(); - }); - /* -----things left for later----- - guild.contextmenu.addbutton("Leave Guild",function(){ - console.log(this) - this.deleteChannel(); - },null,_=>{return thisuser.isAdmin()}) - - guild.contextmenu.addbutton("Mute Guild",function(){ - editchannelf(this); - },null,_=>{return thisuser.isAdmin()}) - */ - } - generateSettings() { - const settings = new Settings("Settings for " + this.properties.name); - { - const overview = settings.addButton("Overview"); - const form = overview.addForm("", _ => { }, { - headers: this.headers, - traditionalSubmit: true, - fetchURL: this.info.api + "/guilds/" + this.id, - method: "PATCH" - }); - form.addTextInput("Name:", "name", { initText: this.properties.name }); - form.addMDInput("Description:", "description", { initText: this.properties.description }); - form.addFileInput("Banner:", "banner", { clear: true }); - form.addFileInput("Icon:", "icon", { clear: true }); - let region = this.properties.region; - if (!region) { - region = ""; - } - form.addTextInput("Region:", "region", { initText: region }); - } - const s1 = settings.addButton("roles"); - const permlist = []; - for (const thing of this.roles) { - permlist.push([thing, thing.permissions]); - } - s1.options.push(new RoleList(permlist, this, this.updateRolePermissions.bind(this))); - settings.show(); - } - constructor(json, owner, member) { - if (json === -1 || member === null) { - super("@me"); - return; - } - if (json.stickers.length) { - console.log(json.stickers, ":3"); - } - super(json.id); - this.large = json.large; - this.member_count = json.member_count; - this.emojis = json.emojis; - this.owner = owner; - this.headers = this.owner.headers; - this.channels = []; - this.properties = json.properties; - this.roles = []; - this.roleids = new Map(); - this.message_notifications = 0; - for (const roley of json.roles) { - const roleh = new Role(roley, this); - this.roles.push(roleh); - this.roleids.set(roleh.id, roleh); - } - if (member instanceof User) { - Member.resolveMember(member, this).then(_ => { - if (_) { - this.member = _; - } - else { - console.error("Member was unable to resolve"); - } - }); - } - else { - Member.new(member, this).then(_ => { - if (_) { - this.member = _; - } - }); - } - this.perminfo ??= { channels: {} }; - for (const thing of json.channels) { - const temp = new Channel(thing, this); - this.channels.push(temp); - this.localuser.channelids.set(temp.id, temp); - } - this.headchannels = []; - for (const thing of this.channels) { - const parent = thing.resolveparent(this); - if (!parent) { - this.headchannels.push(thing); - } - } - this.prevchannel = this.localuser.channelids.get(this.perminfo.prevchannel); - } - get perminfo() { - return this.localuser.perminfo.guilds[this.id]; - } - set perminfo(e) { - this.localuser.perminfo.guilds[this.id] = e; - } - notisetting(settings) { - this.message_notifications = settings.message_notifications; - } - setnotifcation() { - let noti = this.message_notifications; - const notiselect = new Dialog(["vdiv", - ["radio", "select notifications type", - ["all", "only mentions", "none"], - function (e) { - noti = ["all", "only mentions", "none"].indexOf(e); - }, - noti], - ["button", "", "submit", _ => { - // - fetch(this.info.api + `/users/@me/guilds/${this.id}/settings/`, { - method: "PATCH", - headers: this.headers, - body: JSON.stringify({ - message_notifications: noti - }) - }); - this.message_notifications = noti; - }]]); - notiselect.show(); - } - confirmleave() { - const full = new Dialog([ - "vdiv", - ["title", - "Are you sure you want to leave?" - ], - ["hdiv", - ["button", - "", - "Yes, I'm sure", - _ => { - this.leave().then(_ => { - full.hide(); - }); - } - ], - ["button", - "", - "Nevermind", - _ => { - full.hide(); - } - ] - ] - ]); - full.show(); - } - async leave() { - return fetch(this.info.api + "/users/@me/guilds/" + this.id, { - method: "DELETE", - headers: this.headers - }); - } - printServers() { - let build = ""; - for (const thing of this.headchannels) { - build += (thing.name + ":" + thing.position) + "\n"; - for (const thingy of thing.children) { - build += (" " + thingy.name + ":" + thingy.position) + "\n"; - } - } - console.log(build); - } - calculateReorder() { - let position = -1; - const build = []; - for (const thing of this.headchannels) { - const thisthing = { id: thing.id, position: undefined, parent_id: undefined }; - if (thing.position <= position) { - thing.position = (thisthing.position = position + 1); - } - position = thing.position; - console.log(position); - if (thing.move_id && thing.move_id !== thing.parent_id) { - thing.parent_id = thing.move_id; - thisthing.parent_id = thing.parent?.id; - thing.move_id = undefined; - } - if (thisthing.position || thisthing.parent_id) { - build.push(thisthing); - } - if (thing.children.length > 0) { - const things = thing.calculateReorder(); - for (const thing of things) { - build.push(thing); - } - } - } - console.log(build); - this.printServers(); - if (build.length === 0) { - return; - } - const serverbug = false; - if (serverbug) { - for (const thing of build) { - console.log(build, thing); - fetch(this.info.api + "/guilds/" + this.id + "/channels", { - method: "PATCH", - headers: this.headers, - body: JSON.stringify([thing]) - }); - } - } - else { - fetch(this.info.api + "/guilds/" + this.id + "/channels", { - method: "PATCH", - headers: this.headers, - body: JSON.stringify(build) - }); - } - } - get localuser() { - return this.owner; - } - get info() { - return this.owner.info; - } - sortchannels() { - this.headchannels.sort((a, b) => { - return a.position - b.position; - }); - } - static generateGuildIcon(guild) { - const divy = document.createElement("div"); - divy.classList.add("servernoti"); - const noti = document.createElement("div"); - noti.classList.add("unread"); - divy.append(noti); - if (guild instanceof Guild) { - guild.localuser.guildhtml.set(guild.id, divy); - } - let icon; - if (guild instanceof Guild) { - icon = guild.properties.icon; - } - else { - icon = guild.icon; - } - if (icon !== null) { - const img = document.createElement("img"); - img.classList.add("pfp", "servericon"); - img.src = guild.info.cdn + "/icons/" + guild.id + "/" + icon + ".png"; - divy.appendChild(img); - if (guild instanceof Guild) { - img.onclick = () => { - console.log(guild.loadGuild); - guild.loadGuild(); - guild.loadChannel(); - }; - Guild.contextmenu.bindContextmenu(img, guild, undefined); - } - } - else { - const div = document.createElement("div"); - let name; - if (guild instanceof Guild) { - name = guild.properties.name; - } - else { - name = guild.name; - } - const build = name.replace(/'s /g, " ").replace(/\w+/g, word => word[0]).replace(/\s/g, ""); - div.textContent = build; - div.classList.add("blankserver", "servericon"); - divy.appendChild(div); - if (guild instanceof Guild) { - div.onclick = () => { - guild.loadGuild(); - guild.loadChannel(); - }; - Guild.contextmenu.bindContextmenu(div, guild, undefined); - } - } - return divy; - } - generateGuildIcon() { - return Guild.generateGuildIcon(this); - } - confirmDelete() { - let confirmname = ""; - const full = new Dialog([ - "vdiv", - ["title", - "Are you sure you want to delete " + this.properties.name + "?" - ], - ["textbox", - "Name of server:", - "", - function () { - confirmname = this.value; - } - ], - ["hdiv", - ["button", - "", - "Yes, I'm sure", - _ => { - console.log(confirmname); - if (confirmname !== this.properties.name) { - return; - } - this.delete().then(_ => { - full.hide(); - }); - } - ], - ["button", - "", - "Nevermind", - _ => { - full.hide(); - } - ] - ] - ]); - full.show(); - } - async delete() { - return fetch(this.info.api + "/guilds/" + this.id + "/delete", { - method: "POST", - headers: this.headers, - }); - } - unreads(html) { - if (html) { - this.html = html; - } - else { - html = this.html; - } - let read = true; - for (const thing of this.channels) { - if (thing.hasunreads) { - console.log(thing); - read = false; - break; - } - } - if (!html) { - return; - } - if (read) { - html.children[0].classList.remove("notiunread"); - } - else { - html.children[0].classList.add("notiunread"); - } - } - getHTML() { - //this.printServers(); - this.sortchannels(); - this.printServers(); - const build = document.createElement("div"); - for (const thing of this.headchannels) { - build.appendChild(thing.createguildHTML(this.isAdmin())); - } - return build; - } - isAdmin() { - return this.member.isAdmin(); - } - async markAsRead() { - const build = { read_states: [] }; - for (const thing of this.channels) { - if (thing.hasunreads) { - build.read_states.push({ channel_id: thing.id, message_id: thing.lastmessageid, read_state_type: 0 }); - thing.lastreadmessageid = thing.lastmessageid; - if (!thing.myhtml) - continue; - thing.myhtml.classList.remove("cunread"); - } - } - this.unreads(); - fetch(this.info.api + "/read-states/ack-bulk", { - method: "POST", - headers: this.headers, - body: JSON.stringify(build) - }); - } - hasRole(r) { - console.log("this should run"); - if (r instanceof Role) { - r = r.id; - } - return this.member.hasRole(r); - } - loadChannel(ID) { - if (ID) { - const channel = this.localuser.channelids.get(ID); - if (channel) { - channel.getHTML(); - return; - } - } - if (this.prevchannel) { - console.log(this.prevchannel); - this.prevchannel.getHTML(); - return; - } - for (const thing of this.channels) { - if (thing.children.length === 0) { - thing.getHTML(); - return; - } - } - } - loadGuild() { - this.localuser.loadGuild(this.id); - } - updateChannel(json) { - const channel = this.localuser.channelids.get(json.id); - if (channel) { - channel.updateChannel(json); - this.headchannels = []; - for (const thing of this.channels) { - thing.children = []; - } - this.headchannels = []; - for (const thing of this.channels) { - const parent = thing.resolveparent(this); - if (!parent) { - this.headchannels.push(thing); - } - } - this.printServers(); - } - } - createChannelpac(json) { - const thischannel = new Channel(json, this); - this.localuser.channelids.set(json.id, thischannel); - this.channels.push(thischannel); - thischannel.resolveparent(this); - if (!thischannel.parent) { - this.headchannels.push(thischannel); - } - this.calculateReorder(); - this.printServers(); - return thischannel; - } - createchannels(func = this.createChannel) { - let name = ""; - let category = 0; - const channelselect = new Dialog(["vdiv", - ["radio", "select channel type", - ["voice", "text", "announcement"], - function (e) { - console.log(e); - category = { text: 0, voice: 2, announcement: 5, category: 4 }[e]; - }, - 1 - ], - ["textbox", "Name of channel", "", function () { - name = this.value; - }], - ["button", "", "submit", function () { - console.log(name, category); - func(name, category); - channelselect.hide(); - }]]); - channelselect.show(); - } - createcategory() { - let name = ""; - const category = 4; - const channelselect = new Dialog(["vdiv", - ["textbox", "Name of category", "", function () { - name = this.value; - }], - ["button", "", "submit", () => { - console.log(name, category); - this.createChannel(name, category); - channelselect.hide(); - }]]); - channelselect.show(); - } - delChannel(json) { - const channel = this.localuser.channelids.get(json.id); - this.localuser.channelids.delete(json.id); - if (!channel) - return; - this.channels.splice(this.channels.indexOf(channel), 1); - const indexy = this.headchannels.indexOf(channel); - if (indexy !== -1) { - this.headchannels.splice(indexy, 1); - } - /* - const build=[]; - for(const thing of this.channels){ - console.log(thing.id); - if(thing!==channel){ - build.push(thing) - }else{ - console.log("fail"); - if(thing.parent){ - thing.parent.delChannel(json); - } - } - } - this.channels=build; - */ - this.printServers(); - } - createChannel(name, type) { - fetch(this.info.api + "/guilds/" + this.id + "/channels", { - method: "POST", - headers: this.headers, - body: JSON.stringify({ name, type }) - }); - } - async createRole(name) { - const fetched = await fetch(this.info.api + "/guilds/" + this.id + "roles", { - method: "POST", - headers: this.headers, - body: JSON.stringify({ - name, - color: 0, - permissions: "0" - }) - }); - const json = await fetched.json(); - const role = new Role(json, this); - this.roleids.set(role.id, role); - this.roles.push(role); - return role; - } - async updateRolePermissions(id, perms) { - const role = this.roleids[id]; - role.permissions.allow = perms.allow; - role.permissions.deny = perms.deny; - await fetch(this.info.api + "/guilds/" + this.id + "/roles/" + role.id, { - method: "PATCH", - headers: this.headers, - body: JSON.stringify({ - color: role.color, - hoist: role.hoist, - icon: role.icon, - mentionable: role.mentionable, - name: role.name, - permissions: role.permissions.allow.toString(), - unicode_emoji: role.unicode_emoji, - }) - }); - } -} -Guild.setupcontextmenu(); -export { Guild }; diff --git a/.dist/home.js b/.dist/home.js deleted file mode 100644 index 40f28cc..0000000 --- a/.dist/home.js +++ /dev/null @@ -1,62 +0,0 @@ -import { mobile } from "./login.js"; -console.log(mobile); -const serverbox = document.getElementById("instancebox"); -fetch("/instances.json").then(_ => _.json()).then((json) => { - console.warn(json); - for (const instance of json) { - if (instance.display === false) { - continue; - } - const div = document.createElement("div"); - div.classList.add("flexltr", "instance"); - if (instance.image) { - const img = document.createElement("img"); - img.src = instance.image; - div.append(img); - } - const statbox = document.createElement("div"); - statbox.classList.add("flexttb"); - { - const textbox = document.createElement("div"); - textbox.classList.add("flexttb", "instatancetextbox"); - const title = document.createElement("h2"); - title.innerText = instance.name; - if (instance.online !== undefined) { - const status = document.createElement("span"); - status.innerText = instance.online ? "Online" : "Offline"; - status.classList.add("instanceStatus"); - title.append(status); - } - textbox.append(title); - if (instance.description || instance.descriptionLong) { - const p = document.createElement("p"); - if (instance.descriptionLong) { - p.innerText = instance.descriptionLong; - } - else if (instance.description) { - p.innerText = instance.description; - } - textbox.append(p); - } - statbox.append(textbox); - } - if (instance.uptime) { - const stats = document.createElement("div"); - stats.classList.add("flexltr"); - const span = document.createElement("span"); - span.innerText = `Uptime: All time: ${Math.round(instance.uptime.alltime * 100)}% This week: ${Math.round(instance.uptime.weektime * 100)}% Today: ${Math.round(instance.uptime.daytime * 100)}%`; - stats.append(span); - statbox.append(stats); - } - div.append(statbox); - div.onclick = _ => { - if (instance.online) { - window.location.href = "/register.html?instance=" + encodeURI(instance.name); - } - else { - alert("Instance is offline, can't connect"); - } - }; - serverbox.append(div); - } -}); diff --git a/.dist/index.js b/.dist/index.js deleted file mode 100644 index f87945c..0000000 --- a/.dist/index.js +++ /dev/null @@ -1,212 +0,0 @@ -import { Localuser } from "./localuser.js"; -import { Contextmenu } from "./contextmenu.js"; -import { mobile, getBulkUsers, setTheme } from "./login.js"; -import { MarkDown } from "./markdown.js"; -import { File } from "./file.js"; -(async () => { - async function waitforload() { - let res; - new Promise(r => { - res = r; - }); - document.addEventListener("DOMContentLoaded", () => { - res(); - }); - await res; - } - await waitforload(); - const users = getBulkUsers(); - if (!users.currentuser) { - window.location.href = "/login.html"; - } - function showAccountSwitcher() { - const table = document.createElement("div"); - for (const thing of Object.values(users.users)) { - const specialuser = thing; - console.log(specialuser.pfpsrc); - const userinfo = document.createElement("div"); - userinfo.classList.add("flexltr", "switchtable"); - const pfp = document.createElement("img"); - userinfo.append(pfp); - const user = document.createElement("div"); - userinfo.append(user); - user.append(specialuser.username); - user.append(document.createElement("br")); - const span = document.createElement("span"); - span.textContent = specialuser.serverurls.wellknown.replace("https://", "").replace("http://", ""); - user.append(span); - user.classList.add("userinfo"); - span.classList.add("serverURL"); - pfp.src = specialuser.pfpsrc; - pfp.classList.add("pfp"); - table.append(userinfo); - userinfo.addEventListener("click", _ => { - thisuser.unload(); - thisuser.swapped = true; - const loading = document.getElementById("loading"); - loading.classList.remove("doneloading"); - loading.classList.add("loading"); - thisuser = new Localuser(specialuser); - users.currentuser = specialuser.uid; - localStorage.setItem("userinfos", JSON.stringify(users)); - thisuser.initwebsocket().then(_ => { - thisuser.loaduser(); - thisuser.init(); - loading.classList.add("doneloading"); - loading.classList.remove("loading"); - console.log("done loading"); - }); - userinfo.remove(); - }); - } - { - const td = document.createElement("div"); - td.classList.add("switchtable"); - td.append("Switch accounts ⇌"); - td.addEventListener("click", _ => { - window.location.href = "/login.html"; - }); - table.append(td); - } - table.classList.add("accountSwitcher"); - if (Contextmenu.currentmenu != "") { - Contextmenu.currentmenu.remove(); - } - Contextmenu.currentmenu = table; - console.log(table); - document.body.append(table); - } - { - const userinfo = document.getElementById("userinfo"); - userinfo.addEventListener("click", _ => { - _.stopImmediatePropagation(); - showAccountSwitcher(); - }); - const switchaccounts = document.getElementById("switchaccounts"); - switchaccounts.addEventListener("click", _ => { - _.stopImmediatePropagation(); - showAccountSwitcher(); - }); - console.log("this ran"); - } - let thisuser; - try { - console.log(users.users, users.currentuser); - thisuser = new Localuser(users.users[users.currentuser]); - thisuser.initwebsocket().then(_ => { - thisuser.loaduser(); - thisuser.init(); - const loading = document.getElementById("loading"); - loading.classList.add("doneloading"); - loading.classList.remove("loading"); - console.log("done loading"); - }); - } - catch (e) { - console.error(e); - document.getElementById("load-desc").textContent = "Account unable to start"; - thisuser = new Localuser(-1); - } - { - const menu = new Contextmenu("create rightclick"); //Really should go into the localuser class, but that's a later thing - menu.addbutton("Create channel", () => { - if (thisuser.lookingguild) { - thisuser.lookingguild.createchannels(); - } - }, null, _ => { - return thisuser.isAdmin(); - }); - menu.addbutton("Create category", () => { - if (thisuser.lookingguild) { - thisuser.lookingguild.createcategory(); - } - }, null, _ => { - return thisuser.isAdmin(); - }); - menu.bindContextmenu(document.getElementById("channels"), 0, 0); - } - const pasteimage = document.getElementById("pasteimage"); - let replyingto = null; - async function enter(event) { - const channel = thisuser.channelfocus; - if (!channel || !thisuser.channelfocus) - return; - channel.typingstart(); - if (event.key === "Enter" && !event.shiftKey) { - event.preventDefault(); - if (channel.editing) { - channel.editing.edit(markdown.rawString); - channel.editing = null; - } - else { - replyingto = thisuser.channelfocus.replyingto; - const replying = replyingto; - if (replyingto?.div) { - replyingto.div.classList.remove("replying"); - } - thisuser.channelfocus.replyingto = null; - channel.sendMessage(markdown.rawString, { - attachments: images, - embeds: [], - replyingto: replying - }); - thisuser.channelfocus.makereplybox(); - } - while (images.length != 0) { - images.pop(); - pasteimage.removeChild(imageshtml.pop()); - } - typebox.innerHTML = ""; - } - } - const typebox = document.getElementById("typebox"); - const markdown = new MarkDown("", thisuser); - markdown.giveBox(typebox); - typebox["markdown"] = markdown; - typebox.addEventListener("keyup", enter); - typebox.addEventListener("keydown", event => { - if (event.key === "Enter" && !event.shiftKey) - event.preventDefault(); - }); - console.log(typebox); - typebox.onclick = console.log; - /* - function getguildinfo(){ - const path=window.location.pathname.split("/"); - const channel=path[3]; - this.ws.send(JSON.stringify({op: 14, d: {guild_id: path[2], channels: {[channel]: [[0, 99]]}}})); - } - */ - const images = []; - const imageshtml = []; - document.addEventListener("paste", async (e) => { - if (!e.clipboardData) - return; - Array.from(e.clipboardData.files).forEach(async (f) => { - const file = File.initFromBlob(f); - e.preventDefault(); - const html = file.upHTML(images, f); - pasteimage.appendChild(html); - images.push(f); - imageshtml.push(html); - }); - }); - setTheme(); - function userSettings() { - thisuser.showusersettings(); - } - document.getElementById("settings").onclick = userSettings; - if (mobile) { - document.getElementById("channelw").onclick = () => { - document.getElementById("channels").parentNode.classList.add("collapse"); - document.getElementById("servertd").classList.add("collapse"); - document.getElementById("servers").classList.add("collapse"); - }; - document.getElementById("mobileback").textContent = "#"; - document.getElementById("mobileback").onclick = () => { - document.getElementById("channels").parentNode.classList.remove("collapse"); - document.getElementById("servertd").classList.remove("collapse"); - document.getElementById("servers").classList.remove("collapse"); - }; - } -})(); diff --git a/.dist/infiniteScroller.js b/.dist/infiniteScroller.js deleted file mode 100644 index 9eac035..0000000 --- a/.dist/infiniteScroller.js +++ /dev/null @@ -1,311 +0,0 @@ -class InfiniteScroller { - getIDFromOffset; - getHTMLFromID; - destroyFromID; - reachesBottom; - minDist = 2000; - fillDist = 3000; - maxDist = 6000; - HTMLElements = []; - div; - constructor(getIDFromOffset, getHTMLFromID, destroyFromID, reachesBottom = () => { }) { - this.getIDFromOffset = getIDFromOffset; - this.getHTMLFromID = getHTMLFromID; - this.destroyFromID = destroyFromID; - this.reachesBottom = reachesBottom; - } - timeout; - async getDiv(initialId, bottom = true) { - //div.classList.add("flexttb") - if (this.div) { - throw new Error("Div already exists, exiting."); - } - const scroll = document.createElement("div"); - scroll.classList.add("flexttb", "scroller"); - this.beenloaded = false; - //this.interval=setInterval(this.updatestuff.bind(this,true),100); - this.div = scroll; - this.div.addEventListener("scroll", _ => { - this.checkscroll(); - if (this.scrollBottom < 5) { - this.scrollBottom = 5; - } - if (this.timeout === null) { - this.timeout = setTimeout(this.updatestuff.bind(this), 300); - } - this.watchForChange(); - }); - { - let oldheight = 0; - new ResizeObserver(_ => { - this.checkscroll(); - const func = this.snapBottom(); - this.updatestuff(); - const change = oldheight - scroll.offsetHeight; - if (change > 0 && this.div) { - this.div.scrollTop += change; - } - oldheight = scroll.offsetHeight; - this.watchForChange(); - func(); - }).observe(scroll); - } - new ResizeObserver(this.watchForChange.bind(this)).observe(scroll); - await this.firstElement(initialId); - this.updatestuff(); - await this.watchForChange().then(_ => { - this.updatestuff(); - this.beenloaded = true; - }); - return scroll; - } - beenloaded = false; - scrollBottom; - scrollTop; - needsupdate = true; - averageheight = 60; - checkscroll() { - if (this.beenloaded && this.div && !document.body.contains(this.div)) { - console.warn("not in document"); - this.div = null; - } - } - async updatestuff() { - this.timeout = null; - if (!this.div) - return; - this.scrollBottom = this.div.scrollHeight - this.div.scrollTop - this.div.clientHeight; - this.averageheight = this.div.scrollHeight / this.HTMLElements.length; - if (this.averageheight < 10) { - this.averageheight = 60; - } - this.scrollTop = this.div.scrollTop; - if (!this.scrollBottom && !await this.watchForChange()) { - this.reachesBottom(); - } - if (!this.scrollTop) { - await this.watchForChange(); - } - this.needsupdate = false; - //this.watchForChange(); - } - async firstElement(id) { - if (!this.div) - return; - const html = await this.getHTMLFromID(id); - this.div.appendChild(html); - this.HTMLElements.push([html, id]); - } - async addedBottom() { - await this.updatestuff(); - const func = this.snapBottom(); - await this.watchForChange(); - func(); - } - snapBottom() { - const scrollBottom = this.scrollBottom; - return () => { - if (this.div && scrollBottom < 4) { - this.div.scrollTop = this.div.scrollHeight; - } - }; - } - async watchForTop(already = false, fragement = new DocumentFragment()) { - if (!this.div) - return false; - try { - let again = false; - if (this.scrollTop < (already ? this.fillDist : this.minDist)) { - let nextid; - const firstelm = this.HTMLElements.at(0); - if (firstelm) { - const previd = firstelm[1]; - nextid = await this.getIDFromOffset(previd, 1); - } - if (!nextid) { - } - else { - const html = await this.getHTMLFromID(nextid); - if (!html) { - this.destroyFromID(nextid); - return false; - } - again = true; - fragement.prepend(html); - this.HTMLElements.unshift([html, nextid]); - this.scrollTop += this.averageheight; - } - } - if (this.scrollTop > this.maxDist) { - const html = this.HTMLElements.shift(); - if (html) { - again = true; - await this.destroyFromID(html[1]); - this.scrollTop -= this.averageheight; - } - } - if (again) { - await this.watchForTop(true, fragement); - } - return again; - } - finally { - if (!already) { - if (this.div.scrollTop === 0) { - this.scrollTop = 1; - this.div.scrollTop = 10; - } - this.div.prepend(fragement, fragement); - } - } - } - async watchForBottom(already = false, fragement = new DocumentFragment()) { - let func; - if (!already) - func = this.snapBottom(); - if (!this.div) - return false; - try { - let again = false; - const scrollBottom = this.scrollBottom; - if (scrollBottom < (already ? this.fillDist : this.minDist)) { - let nextid; - const lastelm = this.HTMLElements.at(-1); - if (lastelm) { - const previd = lastelm[1]; - nextid = await this.getIDFromOffset(previd, -1); - } - if (!nextid) { - } - else { - again = true; - const html = await this.getHTMLFromID(nextid); - fragement.appendChild(html); - this.HTMLElements.push([html, nextid]); - this.scrollBottom += this.averageheight; - } - } - if (scrollBottom > this.maxDist) { - const html = this.HTMLElements.pop(); - if (html) { - await this.destroyFromID(html[1]); - this.scrollBottom -= this.averageheight; - again = true; - } - } - if (again) { - await this.watchForBottom(true, fragement); - } - return again; - } - finally { - if (!already) { - this.div.append(fragement); - if (func) { - func(); - } - } - } - } - watchtime = false; - changePromise; - async watchForChange() { - if (this.changePromise) { - this.watchtime = true; - return await this.changePromise; - } - else { - this.watchtime = false; - } - this.changePromise = new Promise(async (res) => { - try { - try { - if (!this.div) { - res(false); - return false; - } - const out = await Promise.allSettled([this.watchForTop(), this.watchForBottom()]); - const changed = (out[0].value || out[1].value); - if (this.timeout === null && changed) { - this.timeout = setTimeout(this.updatestuff.bind(this), 300); - } - if (!this.changePromise) { - console.error("something really bad happened"); - } - res(Boolean(changed)); - return Boolean(changed); - } - catch (e) { - console.error(e); - } - res(false); - return false; - } - catch (e) { - throw e; - } - finally { - setTimeout(_ => { - this.changePromise = undefined; - if (this.watchtime) { - this.watchForChange(); - } - }, 300); - } - }); - return await this.changePromise; - } - async focus(id, flash = true) { - let element; - for (const thing of this.HTMLElements) { - if (thing[1] === id) { - element = thing[0]; - } - } - if (element) { - if (flash) { - element.scrollIntoView({ - behavior: "smooth", - block: "center" - }); - await new Promise(resolve => setTimeout(resolve, 1000)); - element.classList.remove("jumped"); - await new Promise(resolve => setTimeout(resolve, 100)); - element.classList.add("jumped"); - } - else { - element.scrollIntoView(); - } - } - else { - for (const thing of this.HTMLElements) { - await this.destroyFromID(thing[1]); - } - this.HTMLElements = []; - await this.firstElement(id); - this.updatestuff(); - await this.watchForChange(); - await new Promise(resolve => setTimeout(resolve, 100)); - await this.focus(id, true); - } - } - async delete() { - if (this.div) { - this.div.remove(); - this.div = null; - } - try { - for (const thing of this.HTMLElements) { - await this.destroyFromID(thing[1]); - } - } - catch (e) { - console.error(e); - } - this.HTMLElements = []; - if (this.timeout) { - clearTimeout(this.timeout); - } - } -} -export { InfiniteScroller }; diff --git a/.dist/invite.js b/.dist/invite.js deleted file mode 100644 index 91001ed..0000000 --- a/.dist/invite.js +++ /dev/null @@ -1,118 +0,0 @@ -import { getBulkUsers, getapiurls } from "./login.js"; -(async () => { - const users = getBulkUsers(); - const well = new URLSearchParams(window.location.search).get("instance"); - const joinable = []; - for (const thing in users.users) { - const user = users.users[thing]; - if (user.serverurls.wellknown.includes(well)) { - joinable.push(user); - } - console.log(users.users[thing]); - } - let urls; - if (!joinable.length && well) { - const out = await getapiurls(well); - if (out) { - urls = out; - for (const thing in users.users) { - const user = users.users[thing]; - if (user.serverurls.api.includes(out.api)) { - joinable.push(user); - } - console.log(users.users[thing]); - } - } - else { - throw new Error("someone needs to handle the case where the servers don't exist"); - } - } - else { - urls = joinable[0].serverurls; - } - if (!joinable.length) { - document.getElementById("AcceptInvite").textContent = "Create an account to accept the invite"; - } - const code = window.location.pathname.split("/")[2]; - let guildinfo; - fetch(`${urls.api}/invites/${code}`, { - method: "GET" - }).then(_ => _.json()).then(json => { - const guildjson = json.guild; - guildinfo = guildjson; - document.getElementById("invitename").textContent = guildjson.name; - document.getElementById("invitedescription").textContent = - `${json.inviter.username} invited you to join ${guildjson.name}`; - if (guildjson.icon) { - const img = document.createElement("img"); - img.src = `${urls.cdn}/icons/${guildjson.id}/${guildjson.icon}.png`; - img.classList.add("inviteGuild"); - document.getElementById("inviteimg").append(img); - } - else { - const txt = guildjson.name.replace(/'s /g, " ").replace(/\w+/g, word => word[0]).replace(/\s/g, ""); - const div = document.createElement("div"); - div.textContent = txt; - div.classList.add("inviteGuild"); - document.getElementById("inviteimg").append(div); - } - }); - function showAccounts() { - const table = document.createElement("dialog"); - for (const thing of Object.values(joinable)) { - const specialuser = thing; - console.log(specialuser.pfpsrc); - const userinfo = document.createElement("div"); - userinfo.classList.add("flexltr", "switchtable"); - const pfp = document.createElement("img"); - userinfo.append(pfp); - const user = document.createElement("div"); - userinfo.append(user); - user.append(specialuser.username); - user.append(document.createElement("br")); - const span = document.createElement("span"); - span.textContent = specialuser.serverurls.wellknown.replace("https://", "").replace("http://", ""); - user.append(span); - user.classList.add("userinfo"); - span.classList.add("serverURL"); - pfp.src = specialuser.pfpsrc; - pfp.classList.add("pfp"); - table.append(userinfo); - userinfo.addEventListener("click", _ => { - console.log(thing); - fetch(`${urls.api}/invites/${code}`, { - method: "POST", - headers: { - Authorization: thing.token - } - }).then(_ => { - users.currentuser = specialuser.uid; - localStorage.setItem("userinfos", JSON.stringify(users)); - window.location.href = "/channels/" + guildinfo.id; - }); - }); - } - { - const td = document.createElement("div"); - td.classList.add("switchtable"); - td.append("Login or create an account ⇌"); - td.addEventListener("click", _ => { - const l = new URLSearchParams("?"); - l.set("goback", window.location.href); - l.set("instance", well); - window.location.href = "/login?" + l.toString(); - }); - if (!joinable.length) { - const l = new URLSearchParams("?"); - l.set("goback", window.location.href); - l.set("instance", well); - window.location.href = "/login?" + l.toString(); - } - table.append(td); - } - table.classList.add("accountSwitcher"); - console.log(table); - document.body.append(table); - } - document.getElementById("AcceptInvite").addEventListener("click", showAccounts); -})(); diff --git a/.dist/jsontypes.js b/.dist/jsontypes.js deleted file mode 100644 index cb0ff5c..0000000 --- a/.dist/jsontypes.js +++ /dev/null @@ -1 +0,0 @@ -export {}; diff --git a/.dist/localuser.js b/.dist/localuser.js deleted file mode 100644 index affe080..0000000 --- a/.dist/localuser.js +++ /dev/null @@ -1,1470 +0,0 @@ -import { Guild } from "./guild.js"; -import { Direct } from "./direct.js"; -import { Voice } from "./audio.js"; -import { User } from "./user.js"; -import { Dialog } from "./dialog.js"; -import { getapiurls, getBulkInfo, setTheme } from "./login.js"; -import { Member } from "./member.js"; -import { FormError, Settings } from "./settings.js"; -import { MarkDown } from "./markdown.js"; -const wsCodesRetry = new Set([4000, 4003, 4005, 4007, 4008, 4009]); -class Localuser { - badges = new Map(); - lastSequence = null; - token; - userinfo; - serverurls; - initialized; - info; - headers; - userConnections; - devPortal; - ready; - guilds; - guildids; - user; - status; - channelfocus; - lookingguild; - guildhtml; - ws; - connectionSucceed = 0; - errorBackoff = 0; - channelids = new Map(); - userMap = new Map(); - instancePing = { - name: "Unknown", - }; - mfa_enabled; - get perminfo() { - return this.userinfo.localuserStore; - } - set perminfo(e) { - this.userinfo.localuserStore = e; - } - constructor(userinfo) { - if (userinfo === -1) { - return; - } - this.token = userinfo.token; - this.userinfo = userinfo; - this.perminfo.guilds ??= {}; - 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 }; - } - gottenReady(ready) { - 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.mfa_enabled = ready.d.user.mfa_enabled; - this.userinfo.username = this.user.username; - 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 = {}; - 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); - } - console.log(ready.d.user_guild_settings.entries); - for (const thing of ready.d.user_guild_settings.entries) { - this.guildids.get(thing.guild_id).notisetting(thing); - } - 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.pingEndpoint(); - this.userinfo.updateLocal(); - } - outoffocus() { - const servers = document.getElementById("servers"); - servers.innerHTML = ""; - const channels = document.getElementById("channels"); - channels.innerHTML = ""; - if (this.channelfocus) { - this.channelfocus.infinite.delete(); - } - this.lookingguild = undefined; - this.channelfocus = undefined; - } - unload() { - this.initialized = false; - this.outoffocus(); - this.guilds = []; - this.guildids = new Map(); - if (this.ws) { - this.ws.close(4001); - } - } - swapped = false; - async initwebsocket() { - let returny; - const ws = new WebSocket(this.serverurls.gateway.toString() + "?encoding=json&v=9" + (DecompressionStream ? "&compress=zlib-stream" : "")); - this.ws = ws; - let ds; - let w; - let r; - let arr; - 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"); - 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 = ""; - if (temp.op === 0 && temp.t === "READY") { - returny(); - } - await this.handleEvent(temp); - } - catch { } - } - })(); - } - }); - let order = new Promise(res => (res())); - ws.addEventListener("message", async (event) => { - const temp2 = order; - order = new Promise(async (res) => { - await temp2; - let temp; - 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); - } - if (temp.op === 0 && temp.t === "READY") { - returny(); - } - await this.handleEvent(temp); - } - catch (e) { - console.error(e); - } - finally { - res(); - } - }); - }); - ws.addEventListener("close", async (event) => { - this.ws = undefined; - console.log("WebSocket closed with code " + event.code); - this.unload(); - document.getElementById("loading").classList.remove("doneloading"); - document.getElementById("loading").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))) { - if (this.connectionSucceed !== 0 && Date.now() > this.connectionSucceed + 20000) - this.errorBackoff = 0; - else - this.errorBackoff++; - this.connectionSucceed = 0; - document.getElementById("load-desc").innerHTML = "Unable to connect to the Spacebar server, retrying in " + Math.round(0.2 + (this.errorBackoff * 2.8)) + " seconds..."; - 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; - this.userinfo.updateLocal(); - 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; - this.userinfo.updateLocal(); - 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; - this.userinfo.updateLocal(); - } - break; - } - } - setTimeout(() => { - if (this.swapped) - return; - document.getElementById("load-desc").textContent = "Retrying..."; - this.initwebsocket().then(() => { - this.loaduser(); - this.init(); - const loading = document.getElementById("loading"); - loading.classList.add("doneloading"); - loading.classList.remove("loading"); - console.log("done loading"); - }); - }, 200 + (this.errorBackoff * 2800)); - } - else - document.getElementById("load-desc").textContent = "Unable to connect to the Spacebar server. Please try logging out and back in."; - }); - await promise; - } - async handleEvent(temp) { - console.debug(temp); - if (temp.s) - this.lastSequence = temp.s; - 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": - this.gottenReady(temp); - 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_CREATE": - { - const guildy = new Guild(temp.d, this, this.user); - this.guilds.push(guildy); - this.guildids.set(guildy.id, guildy); - document.getElementById("servers").insertBefore(guildy.generateGuildIcon(), 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; - if (temp.d.member) { - thing = await Member.new(temp.d.member, guild); - } - 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; - } - } - 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(_ => { - 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); - } - } - heartbeat_interval; - updateChannel(json) { - 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) { - 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); - } - if (channel.id === this.gotoid) { - guild.loadGuild(); - guild.loadChannel(channel.id); - this.gotoid = undefined; - } - } - gotoid; - async goToChannel(id) { - const channel = this.channelids.get(id); - if (channel) { - const guild = channel.guild; - guild.loadGuild(); - guild.loadChannel(id); - } - else { - this.gotoid = id; - } - } - delChannel(json) { - 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); - } - } - init() { - 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() { - document.getElementById("username").textContent = this.user.username; - document.getElementById("userpfp").src = this.user.getpfpsrc(); - document.getElementById("status").textContent = this.status; - } - isAdmin() { - if (this.lookingguild) { - return this.lookingguild.isAdmin(); - } - else { - return false; - } - } - loadGuild(id) { - let guild = this.guildids.get(id); - if (!guild) { - guild = this.guildids.get("@me"); - } - if (this.lookingguild === guild) { - return guild; - } - if (this.channelfocus) { - 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").textContent = guild.properties.name; - //console.log(this.guildids,id) - const channels = document.getElementById("channels"); - channels.innerHTML = ""; - const html = guild.getHTML(); - channels.appendChild(html); - return guild; - } - buildservers() { - const serverlist = document.getElementById("servers"); // - const outdiv = document.createElement("div"); - const home = document.createElement("span"); - const div = document.createElement("div"); - div.classList.add("home", "servericon"); - home.classList.add("svgtheme", "svgicon", "svg-home"); - home["all"] = this.guildids.get("@me"); - this.guildids.get("@me").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 = function () { - this["all"].loadGuild(); - this["all"].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.unreaddms(); - continue; - } - const divy = thing.generateGuildIcon(); - serverlist.append(divy); - } - { - const br = document.createElement("hr"); - br.classList.add("lightbr"); - serverlist.appendChild(br); - br.id = "bottomseparator"; - const div = document.createElement("div"); - div.textContent = "+"; - div.classList.add("home", "servericon"); - serverlist.appendChild(div); - div.onclick = _ => { - this.createGuild(); - }; - const guilddsdiv = document.createElement("div"); - const guildDiscoveryContainer = document.createElement("span"); - guildDiscoveryContainer.classList.add("svgtheme", "svgicon", "svg-explore"); - guilddsdiv.classList.add("home", "servericon"); - guilddsdiv.appendChild(guildDiscoveryContainer); - serverlist.appendChild(guilddsdiv); - guildDiscoveryContainer.addEventListener("click", () => { - this.guildDiscovery(); - }); - } - this.unreads(); - } - createGuild() { - let inviteurl = ""; - const error = document.createElement("span"); - const fields = { - name: "", - icon: null, - }; - const full = new Dialog(["tabs", [ - ["Join using invite", [ - "vdiv", - ["textbox", - "Invite Link/Code", - "", - function () { - inviteurl = this.value; - } - ], - ["html", error], - ["button", - "", - "Submit", - _ => { - let parsed = ""; - if (inviteurl.includes("/")) { - parsed = inviteurl.split("/")[inviteurl.split("/").length - 1]; - } - else { - parsed = inviteurl; - } - fetch(this.info.api + "/invites/" + parsed, { - method: "POST", - headers: this.headers, - }).then(r => r.json()).then(_ => { - if (_.message) { - error.textContent = _.message; - } - }); - } - ] - ]], - ["Create Guild", - ["vdiv", - ["title", "Create a guild"], - ["fileupload", "Icon:", function (event) { - const target = event.target; - if (!target.files) - return; - const reader = new FileReader(); - reader.readAsDataURL(target.files[0]); - reader.onload = () => { - fields.icon = reader.result; - }; - }], - ["textbox", "Name:", "", function (event) { - const target = event.target; - fields.name = target.value; - }], - ["button", "", "submit", () => { - this.makeGuild(fields).then(_ => { - if (_.message) { - alert(_.errors.name._errors[0].message); - } - else { - full.hide(); - } - }); - }]]] - ]]); - full.show(); - } - async makeGuild(fields) { - 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("guildy"); - content.textContent = "Loading..."; - const full = new Dialog(["html", content]); - full.show(); - const res = await fetch(this.info.api + "/discoverable-guilds?limit=50", { - headers: this.headers - }); - const json = await res.json(); - content.innerHTML = ""; - const title = document.createElement("h2"); - title.textContent = "Guild discovery (" + json.total + " entries)"; - content.appendChild(title); - const guilds = document.createElement("div"); - guilds.id = "discovery-guild-content"; - json.guilds.forEach(guild => { - 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) { - messagep.d.guild_id ??= "@me"; - const channel = this.channelids.get(messagep.d.channel_id); - if (channel) { - channel.messageCreate(messagep); - this.unreads(); - } - } - unreads() { - for (const thing of this.guilds) { - if (thing.id === "@me") { - continue; - } - const html = this.guildhtml.get(thing.id); - thing.unreads(html); - } - } - async typingStart(typing) { - // - const channel = this.channelids.get(typing.d.channel_id); - if (!channel) - return; - channel.typingStart(typing); - //this.typing.set(memb,Date.now()); - } - updatepfp(file) { - 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) { - 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) { - fetch(this.info.api + "/users/@me/profile", { - method: "PATCH", - headers: this.headers, - body: JSON.stringify(json) - }); - } - async showusersettings() { - const settings = new Settings("Settings"); - { - const userOptions = settings.addButton("User Settings", { ltr: true }); - const hypotheticalProfile = document.createElement("div"); - let file; - let newpronouns; - let newbio; - const hypouser = this.user.clone(); - let color; - 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("Upload pfp:", _ => { - 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; - const binput = settingsLeft.addFileInput("Upload banner:", _ => { - 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("Pronouns", _ => { - if (newpronouns || newbio || changed) { - 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("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("Profile color", _ => { }, { initColor: color }); - colorPicker.watchForChange(_ => { - console.log(); - color = _; - hypouser.accent_color = Number.parseInt("0x" + _.substr(1), 16); - changed = true; - regen(); - }); - } - { - const tas = settings.addButton("Themes & sounds"); - { - const themes = ["Dark", "WHITE", "Light"]; - tas.addSelect("Theme:", _ => { - localStorage.setItem("theme", themes[_]); - setTheme(); - }, themes, { defaultIndex: themes.indexOf(localStorage.getItem("theme")) }); - } - { - const sounds = Voice.sounds; - tas.addSelect("Notification sound:", _ => { - Voice.setNotificationSound(sounds[_]); - }, sounds, { defaultIndex: sounds.indexOf(Voice.getNotificationSound()) }).watchForChange(_ => { - Voice.noises(sounds[_]); - }); - } - { - const userinfos = getBulkInfo(); - tas.addColorInput("Accent color:", _ => { - userinfos.accent_color = _; - localStorage.setItem("userinfos", JSON.stringify(userinfos)); - document.documentElement.style.setProperty("--accent-color", userinfos.accent_color); - }, { initColor: userinfos.accent_color }); - } - } - { - const security = settings.addButton("Account Settings"); - const genSecurity = () => { - security.removeAll(); - if (this.mfa_enabled) { - security.addButtonInput("", "Disable 2FA", () => { - const form = security.addSubForm("2FA Disable", (_) => { - if (_.message) { - switch (_.code) { - case 60008: - form.error("code", "Invalid code"); - break; - } - } - else { - this.mfa_enabled = false; - security.returnFromSub(); - genSecurity(); - } - }, { - fetchURL: (this.info.api + "/users/@me/mfa/totp/disable"), - headers: this.headers - }); - form.addTextInput("Code:", "code", { required: true }); - }); - } - else { - security.addButtonInput("", "Enable 2FA", async () => { - let secret = ""; - for (let i = 0; i < 18; i++) { - secret += "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"[Math.floor(Math.random() * 32)]; - } - const form = security.addSubForm("2FA Setup", (_) => { - if (_.message) { - switch (_.code) { - case 60008: - form.error("code", "Invalid code"); - break; - case 400: - form.error("password", "Incorrect password"); - break; - } - } - else { - genSecurity(); - this.mfa_enabled = true; - security.returnFromSub(); - } - }, { - fetchURL: (this.info.api + "/users/@me/mfa/totp/enable/"), - headers: this.headers - }); - form.addTitle("Copy this secret into your totp(time-based one time password) app"); - form.addText(`Your secret is: ${secret} and it's 6 digits, with a 30 second token period`); - form.addTextInput("Account Password:", "password", { required: true, password: true }); - form.addTextInput("Code:", "code", { required: true }); - form.setValue("secret", secret); - }); - } - security.addButtonInput("", "Change discriminator", () => { - const form = security.addSubForm("Change Discriminator", _ => { - security.returnFromSub(); - }, { - fetchURL: (this.info.api + "/users/@me/"), - headers: this.headers, - method: "PATCH" - }); - form.addTextInput("New discriminator:", "discriminator"); - }); - security.addButtonInput("", "Change email", () => { - const form = security.addSubForm("Change Email", _ => { - security.returnFromSub(); - }, { - fetchURL: (this.info.api + "/users/@me/"), - headers: this.headers, - method: "PATCH" - }); - form.addTextInput("Password:", "password", { password: true }); - if (this.mfa_enabled) { - form.addTextInput("Code:", "code"); - } - form.addTextInput("New email:", "email"); - }); - security.addButtonInput("", "Change username", () => { - const form = security.addSubForm("Change Username", _ => { - security.returnFromSub(); - }, { - fetchURL: (this.info.api + "/users/@me/"), - headers: this.headers, - method: "PATCH" - }); - form.addTextInput("Password:", "password", { password: true }); - if (this.mfa_enabled) { - form.addTextInput("Code:", "code"); - } - form.addTextInput("New username:", "username"); - }); - security.addButtonInput("", "Change password", () => { - const form = security.addSubForm("Change Password", _ => { - security.returnFromSub(); - }, { - fetchURL: (this.info.api + "/users/@me/"), - headers: this.headers, - method: "PATCH" - }); - form.addTextInput("Old password:", "password", { password: true }); - if (this.mfa_enabled) { - form.addTextInput("Code:", "code"); - } - let in1 = ""; - let in2 = ""; - form.addTextInput("New password:", "").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, "Passwords don't match"); - } - }); - }); - }; - genSecurity(); - } - { - const connections = settings.addButton("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 = "This connection has been disabled server-side."; - } - connectionContainer.appendChild(container); - }); - }); - connections.addHTMLArea(connectionContainer); - } - { - const devPortal = settings.addButton("Developer Portal"); - const teamsRes = await fetch(this.info.api + "/teams", { - headers: this.headers - }); - const teams = await teamsRes.json(); - devPortal.addButtonInput("", "Create application", () => { - const form = devPortal.addSubForm("Create application", (json) => { - if (json.message) - form.error("name", json.message); - else { - devPortal.returnFromSub(); - this.manageApplication(json.id); - } - }, { - fetchURL: this.info.api + "/applications", - headers: this.headers, - method: "POST" - }); - form.addTextInput("Name", "name", { required: true }); - form.addSelect("Team", "team_id", ["Personal", ...teams.map(team => 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 => { - 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); - }); - appListContainer.appendChild(container); - }); - }); - devPortal.addHTMLArea(appListContainer); - } - settings.show(); - } - async manageApplication(appId = "") { - const res = await fetch(this.info.api + "/applications/" + appId, { - headers: this.headers - }); - const json = await res.json(); - const fields = {}; - const appDialog = new Dialog(["vdiv", - ["title", - "Editing " + json.name - ], - ["vdiv", - ["textbox", "Application name:", json.name, event => { - fields.name = event.target.value; - }], - ["mdbox", "Description:", json.description, event => { - fields.description = event.target.value; - }], - ["vdiv", - json.icon ? ["img", this.info.cdn + "/app-icons/" + appId + "/" + json.icon + ".png?size=128", [128, 128]] : ["text", "No icon"], - ["fileupload", "Application icon:", event => { - const reader = new FileReader(); - const files = event.target.files; - if (files) { - reader.readAsDataURL(files[0]); - reader.onload = () => { - fields.icon = reader.result; - }; - } - }] - ] - ], - ["hdiv", - ["textbox", "Privacy policy URL:", json.privacy_policy_url || "", event => { - fields.privacy_policy_url = event.target.value; - }], - ["textbox", "Terms of Service URL:", json.terms_of_service_url || "", event => { - fields.terms_of_service_url = event.target.value; - }] - ], - ["hdiv", - ["checkbox", "Make bot publicly inviteable?", json.bot_public, event => { - fields.bot_public = event.target.checked; - }], - ["checkbox", "Require code grant to invite the bot?", json.bot_require_code_grant, event => { - fields.bot_require_code_grant = event.target.checked; - }] - ], - ["hdiv", - ["button", - "", - "Save changes", - async () => { - const updateRes = await fetch(this.info.api + "/applications/" + appId, { - method: "PATCH", - headers: this.headers, - body: JSON.stringify(fields) - }); - if (updateRes.ok) - appDialog.hide(); - else { - const updateJSON = await updateRes.json(); - alert("An error occurred: " + updateJSON.message); - } - } - ], - ["button", - "", - (json.bot ? "Manage" : "Add") + " bot", - async () => { - if (!json.bot) { - if (!confirm("Are you sure you want to add a bot to this application? There's no going back.")) - return; - const updateRes = await fetch(this.info.api + "/applications/" + appId + "/bot", { - method: "POST", - headers: this.headers - }); - const updateJSON = await updateRes.json(); - alert("Bot token:\n" + updateJSON.token); - } - appDialog.hide(); - this.manageBot(appId); - } - ] - ]]); - appDialog.show(); - } - async manageBot(appId = "") { - const res = await fetch(this.info.api + "/applications/" + appId, { - headers: this.headers - }); - const json = await res.json(); - if (!json.bot) - return alert("For some reason, this application doesn't have a bot (yet)."); - const fields = { - username: json.bot.username, - avatar: json.bot.avatar ? (this.info.cdn + "/app-icons/" + appId + "/" + json.bot.avatar + ".png?size=256") : "" - }; - const botDialog = new Dialog(["vdiv", - ["title", - "Editing bot: " + json.bot.username - ], - ["hdiv", - ["textbox", "Bot username:", json.bot.username, event => { - fields.username = event.target.value; - }], - ["vdiv", - fields.avatar ? ["img", fields.avatar, [128, 128]] : ["text", "No avatar"], - ["fileupload", "Bot avatar:", event => { - const reader = new FileReader(); - const files = event.target.files; - if (files) { - const file = files[0]; - reader.readAsDataURL(file); - reader.onload = () => { - fields.avatar = reader.result; - }; - } - }] - ] - ], - ["hdiv", - ["button", - "", - "Save changes", - async () => { - const updateRes = await fetch(this.info.api + "/applications/" + appId + "/bot", { - method: "PATCH", - headers: this.headers, - body: JSON.stringify(fields) - }); - if (updateRes.ok) - botDialog.hide(); - else { - const updateJSON = await updateRes.json(); - alert("An error occurred: " + updateJSON.message); - } - } - ], - ["button", - "", - "Reset token", - async () => { - if (!confirm("Are you sure you want to reset the bot token? Your bot will stop working until you update it.")) - return; - const updateRes = await fetch(this.info.api + "/applications/" + appId + "/bot/reset", { - method: "POST", - headers: this.headers - }); - const updateJSON = await updateRes.json(); - alert("New token:\n" + updateJSON.token); - botDialog.hide(); - } - ] - ]]); - botDialog.show(); - } - //---------- resolving members code ----------- - waitingmembers = new Map(); - presences = new Map(); - async resolvemember(id, guildid) { - if (guildid === "@me") { - return undefined; - } - const guild = this.guildids.get(guildid); - const borked = true; - if (borked && guild && 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/" + guild.id + "/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 = new Promise(res => { - guildmap.set(id, res); - this.getmembers(); - }); - return await promise; - } - fetchingmembers = new Map(); - noncemap = new Map(); - noncebuild = new Map(); - async gotChunk(chunk) { - for (const thing of chunk.presences) { - if (thing.user) { - this.presences.set(thing.user.id, thing); - } - } - 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 = []; - for (const key of keys) { - build.push(key); - if (build.length === 100) { - break; - } - } - if (!build.length) { - this.waitingmembers.delete(guildid); - return; - } - const promise = 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").textContent = channelName; - document.getElementsByTagName("title")[0].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(["vdiv", - ["title", "Instance stats: " + this.instancePing.name], - ["text", "Registered users: " + json.counts.user], - ["text", "Servers: " + json.counts.guild], - ["text", "Messages: " + json.counts.message], - ["text", "Members: " + json.counts.members] - ]); - dialog.show(); - } -} -export { Localuser }; diff --git a/.dist/login.js b/.dist/login.js deleted file mode 100644 index 43c25a8..0000000 --- a/.dist/login.js +++ /dev/null @@ -1,461 +0,0 @@ -import { Dialog } from "./dialog.js"; -const mobile = /iPhone|iPad|iPod|Android/i.test(navigator.userAgent); -function setTheme() { - let name = localStorage.getItem("theme"); - if (!name) { - localStorage.setItem("theme", "Dark"); - name = "Dark"; - } - document.body.className = name + "-theme"; -} -let instances; -setTheme(); -function getBulkUsers() { - const json = getBulkInfo(); - for (const thing in json.users) { - json.users[thing] = new Specialuser(json.users[thing]); - } - return json; -} -function trimswitcher() { - const json = getBulkInfo(); - const map = new Map(); - for (const thing in json.users) { - const user = json.users[thing]; - let wellknown = user.serverurls.wellknown; - if (wellknown.at(-1) !== "/") { - wellknown += "/"; - } - wellknown += user.username; - if (map.has(wellknown)) { - const otheruser = map.get(wellknown); - if (otheruser[1].serverurls.wellknown.at(-1) === "/") { - delete json.users[otheruser[0]]; - map.set(wellknown, [thing, user]); - } - else { - delete json.users[thing]; - } - } - else { - map.set(wellknown, [thing, user]); - } - } - for (const thing in json.users) { - if (thing.at(-1) === "/") { - const user = json.users[thing]; - delete json.users[thing]; - json.users[thing.slice(0, -1)] = user; - } - } - localStorage.setItem("userinfos", JSON.stringify(json)); - console.log(json); -} -function getBulkInfo() { - return JSON.parse(localStorage.getItem("userinfos")); -} -function setDefaults() { - let userinfos = getBulkInfo(); - if (!userinfos) { - localStorage.setItem("userinfos", JSON.stringify({ - currentuser: null, - users: {}, - preferences: { - theme: "Dark", - notifications: false, - notisound: "three", - }, - })); - userinfos = getBulkInfo(); - } - if (userinfos.users === undefined) { - userinfos.users = {}; - } - if (userinfos.accent_color === undefined) { - userinfos.accent_color = "#242443"; - } - document.documentElement.style.setProperty("--accent-color", userinfos.accent_color); - if (userinfos.preferences === undefined) { - userinfos.preferences = { - theme: "Dark", - notifications: false, - notisound: "three", - }; - } - if (userinfos.preferences && (userinfos.preferences.notisound === undefined)) { - userinfos.preferences.notisound = "three"; - } - localStorage.setItem("userinfos", JSON.stringify(userinfos)); -} -setDefaults(); -class Specialuser { - serverurls; - email; - token; - loggedin; - json; - constructor(json) { - if (json instanceof Specialuser) { - console.error("specialuser can't construct from another specialuser"); - } - this.serverurls = json.serverurls; - let apistring = new URL(json.serverurls.api).toString(); - apistring = apistring.replace(/\/(v\d+\/?)?$/, "") + "/v9"; - this.serverurls.api = apistring; - this.serverurls.cdn = new URL(json.serverurls.cdn).toString().replace(/\/$/, ""); - this.serverurls.gateway = new URL(json.serverurls.gateway).toString().replace(/\/$/, ""); - this.serverurls.wellknown = new URL(json.serverurls.wellknown).toString().replace(/\/$/, ""); - this.serverurls.login = new URL(json.serverurls.login).toString().replace(/\/$/, ""); - this.email = json.email; - this.token = json.token; - this.loggedin = json.loggedin; - this.json = json; - this.json.localuserStore ??= {}; - if (!this.serverurls || !this.email || !this.token) { - console.error("There are fundamentally missing pieces of info missing from this user"); - } - } - set pfpsrc(e) { - this.json.pfpsrc = e; - this.updateLocal(); - } - get pfpsrc() { - return this.json.pfpsrc; - } - set username(e) { - this.json.username = e; - this.updateLocal(); - } - get username() { - return this.json.username; - } - set localuserStore(e) { - this.json.localuserStore = e; - this.updateLocal(); - } - get localuserStore() { - return this.json.localuserStore; - } - get uid() { - return this.email + this.serverurls.wellknown; - } - toJSON() { - return this.json; - } - updateLocal() { - const info = getBulkInfo(); - info.users[this.uid] = this.toJSON(); - localStorage.setItem("userinfos", JSON.stringify(info)); - } -} -function adduser(user) { - user = new Specialuser(user); - const info = getBulkInfo(); - info.users[user.uid] = user; - info.currentuser = user.uid; - localStorage.setItem("userinfos", JSON.stringify(info)); - return user; -} -const instancein = document.getElementById("instancein"); -let timeout; -let instanceinfo; -const stringURLMap = new Map(); -const stringURLsMap = new Map(); -async function getapiurls(str) { - if (!URL.canParse(str)) { - const val = stringURLMap.get(str); - if (val) { - str = val; - } - else { - const val = stringURLsMap.get(str); - if (val) { - const responce = await fetch(val.api + val.api.endsWith("/") ? "" : "/" + "ping"); - if (responce.ok) { - if (val.login) { - return val; - } - else { - val.login = val.api; - return val; - } - } - } - } - } - if (str.at(-1) !== "/") { - str += "/"; - } - let api; - try { - const info = await fetch(`${str}/.well-known/spacebar`).then(x => x.json()); - api = info.api; - } - catch { - return false; - } - const url = new URL(api); - try { - const info = await fetch(`${api}${url.pathname.includes("api") ? "" : "api"}/policies/instance/domains`).then(x => x.json()); - return { - api: info.apiEndpoint, - gateway: info.gateway, - cdn: info.cdn, - wellknown: str, - login: url.toString() - }; - } - catch { - const val = stringURLsMap.get(str); - if (val) { - const responce = await fetch(val.api + val.api.endsWith("/") ? "" : "/" + "ping"); - if (responce.ok) { - if (val.login) { - return val; - } - else { - val.login = val.api; - return val; - } - } - } - return false; - } -} -async function checkInstance(e) { - const verify = document.getElementById("verify"); - try { - verify.textContent = "Checking Instance"; - const instanceinfo = await getapiurls(instancein.value); - if (instanceinfo) { - instanceinfo.value = instancein.value; - localStorage.setItem("instanceinfo", JSON.stringify(instanceinfo)); - verify.textContent = "Instance is all good"; - if (checkInstance.alt) { - checkInstance.alt(); - } - setTimeout(_ => { - console.log(verify.textContent); - verify.textContent = ""; - }, 3000); - } - else { - verify.textContent = "Invalid Instance, try again"; - } - } - catch { - console.log("catch"); - verify.textContent = "Invalid Instance, try again"; - } -} -if (instancein) { - console.log(instancein); - instancein.addEventListener("keydown", e => { - const verify = document.getElementById("verify"); - verify.textContent = "Waiting to check Instance"; - clearTimeout(timeout); - timeout = setTimeout(checkInstance, 1000); - }); - if (localStorage.getItem("instanceinfo")) { - const json = JSON.parse(localStorage.getItem("instanceinfo")); - if (json.value) { - instancein.value = json.value; - } - else { - instancein.value = json.wellknown; - } - } - else { - checkInstance("https://spacebar.chat/"); - } -} -async function login(username, password, captcha) { - if (captcha === "") { - captcha = undefined; - } - const options = { - method: "POST", - body: JSON.stringify({ - login: username, - password, - undelete: false, - captcha_key: captcha - }), - headers: { - "Content-type": "application/json; charset=UTF-8", - } - }; - try { - const info = JSON.parse(localStorage.getItem("instanceinfo")); - const api = info.login + (info.login.startsWith("/") ? "/" : ""); - return await fetch(api + "/auth/login", options).then(response => response.json()) - .then(response => { - console.log(response, response.message); - if (response.message === "Invalid Form Body") { - return response.errors.login._errors[0].message; - console.log("test"); - } - //this.serverurls||!this.email||!this.token - console.log(response); - if (response.captcha_sitekey) { - const capt = document.getElementById("h-captcha"); - if (!capt.children.length) { - const capty = document.createElement("div"); - capty.classList.add("h-captcha"); - capty.setAttribute("data-sitekey", response.captcha_sitekey); - const script = document.createElement("script"); - script.src = "https://js.hcaptcha.com/1/api.js"; - capt.append(script); - capt.append(capty); - } - else { - eval("hcaptcha.reset()"); - } - } - else { - console.log(response); - if (response.ticket) { - let onetimecode = ""; - new Dialog(["vdiv", ["title", "2FA code:"], ["textbox", "", "", function () { - onetimecode = this.value; - }], ["button", "", "Submit", function () { - fetch(api + "/auth/mfa/totp", { - method: "POST", - headers: { - "Content-Type": "application/json" - }, - body: JSON.stringify({ - code: onetimecode, - ticket: response.ticket, - }) - }).then(r => r.json()).then(response => { - if (response.message) { - alert(response.message); - } - else { - console.warn(response); - if (!response.token) - return; - adduser({ serverurls: JSON.parse(localStorage.getItem("instanceinfo")), email: username, token: response.token }).username = username; - const redir = new URLSearchParams(window.location.search).get("goback"); - if (redir) { - window.location.href = redir; - } - else { - window.location.href = "/channels/@me"; - } - } - }); - }]]).show(); - } - else { - console.warn(response); - if (!response.token) - return; - adduser({ serverurls: JSON.parse(localStorage.getItem("instanceinfo")), email: username, token: response.token }).username = username; - const redir = new URLSearchParams(window.location.search).get("goback"); - if (redir) { - window.location.href = redir; - } - else { - window.location.href = "/channels/@me"; - } - return ""; - } - } - }); - } - catch (error) { - console.error("Error:", error); - } -} -async function check(e) { - e.preventDefault(); - const h = await login(e.srcElement[1].value, e.srcElement[2].value, e.srcElement[3].value); - document.getElementById("wrong").textContent = h; - console.log(h); -} -if (document.getElementById("form")) { - document.getElementById("form").addEventListener("submit", check); -} -//this currently does not work, and need to be implemented better at some time. -/* -if ("serviceWorker" in navigator){ - navigator.serviceWorker.register("/service.js", { - scope: "/", - }).then((registration) => { - let serviceWorker:ServiceWorker; - if (registration.installing) { - serviceWorker = registration.installing; - console.log("installing"); - } else if (registration.waiting) { - serviceWorker = registration.waiting; - console.log("waiting"); - } else if (registration.active) { - serviceWorker = registration.active; - console.log("active"); - } - if (serviceWorker) { - console.log(serviceWorker.state); - serviceWorker.addEventListener("statechange", (e) => { - console.log(serviceWorker.state); - }); - } - }) -} -*/ -const switchurl = document.getElementById("switch"); -if (switchurl) { - switchurl.href += window.location.search; - const instance = new URLSearchParams(window.location.search).get("instance"); - console.log(instance); - if (instance) { - instancein.value = instance; - checkInstance(""); - } -} -export { checkInstance }; -trimswitcher(); -export { mobile, getBulkUsers, getBulkInfo, setTheme, Specialuser, getapiurls, adduser }; -const datalist = document.getElementById("instances"); -console.warn(datalist); -export function getInstances() { - return instances; -} -fetch("/instances.json").then(_ => _.json()).then((json) => { - instances = json; - if (datalist) { - console.warn(json); - if (instancein && instancein.value === "") { - instancein.value = json[0].name; - } - for (const instance of json) { - if (instance.display === false) { - continue; - } - const option = document.createElement("option"); - option.disabled = !instance.online; - option.value = instance.name; - if (instance.url) { - stringURLMap.set(option.value, instance.url); - if (instance.urls) { - stringURLsMap.set(instance.url, instance.urls); - } - } - else if (instance.urls) { - stringURLsMap.set(option.value, instance.urls); - } - else { - option.disabled = true; - } - if (instance.description) { - option.label = instance.description; - } - else { - option.label = instance.name; - } - datalist.append(option); - } - checkInstance(""); - } -}); diff --git a/.dist/markdown.js b/.dist/markdown.js deleted file mode 100644 index 7675cbc..0000000 --- a/.dist/markdown.js +++ /dev/null @@ -1,761 +0,0 @@ -import { Channel } from "./channel.js"; -import { Dialog } from "./dialog.js"; -import { Emoji } from "./emoji.js"; -import { Localuser } from "./localuser.js"; -import { Member } from "./member.js"; -class MarkDown { - txt; - keep; - stdsize; - owner; - info; - constructor(text, owner, { keep = false, stdsize = false } = {}) { - if ((typeof text) === (typeof "")) { - this.txt = text.split(""); - } - else { - this.txt = text; - } - if (this.txt === undefined) { - this.txt = []; - } - this.info = owner.info; - this.keep = keep; - this.owner = owner; - this.stdsize = stdsize; - } - get localuser() { - if (this.owner instanceof Localuser) { - return this.owner; - } - else { - return this.owner.localuser; - } - } - get rawString() { - return this.txt.join(""); - } - get textContent() { - return this.makeHTML().textContent; - } - makeHTML({ keep = this.keep, stdsize = this.stdsize } = {}) { - return this.markdown(this.txt, { keep, stdsize }); - } - markdown(text, { keep = false, stdsize = false } = {}) { - let txt; - if ((typeof text) === (typeof "")) { - txt = text.split(""); - } - else { - txt = text; - } - if (txt === undefined) { - txt = []; - } - const span = document.createElement("span"); - let current = document.createElement("span"); - function appendcurrent() { - if (current.innerHTML !== "") { - span.append(current); - current = document.createElement("span"); - } - } - for (let i = 0; i < txt.length; i++) { - if (txt[i] === "\n" || i === 0) { - const first = i === 0; - if (first) { - i--; - } - let element = document.createElement("span"); - let keepys = ""; - if (txt[i + 1] === "#") { - if (txt[i + 2] === "#") { - if (txt[i + 3] === "#" && txt[i + 4] === " ") { - element = document.createElement("h3"); - keepys = "### "; - i += 5; - } - else if (txt[i + 3] === " ") { - element = document.createElement("h2"); - element.classList.add("h2md"); - keepys = "## "; - i += 4; - } - } - else if (txt[i + 2] === " ") { - element = document.createElement("h1"); - keepys = "# "; - i += 3; - } - } - else if (txt[i + 1] === ">" && txt[i + 2] === " ") { - element = document.createElement("div"); - const line = document.createElement("div"); - line.classList.add("quoteline"); - element.append(line); - element.classList.add("quote"); - keepys = "> "; - i += 3; - } - if (keepys) { - appendcurrent(); - if (!first && !stdsize) { - span.appendChild(document.createElement("br")); - } - const build = []; - for (; txt[i] !== "\n" && txt[i] !== undefined; i++) { - build.push(txt[i]); - } - try { - if (stdsize) { - element = document.createElement("span"); - } - if (keep) { - element.append(keepys); - //span.appendChild(document.createElement("br")); - } - element.appendChild(this.markdown(build, { keep, stdsize })); - span.append(element); - } - finally { - i -= 1; - continue; - } - } - if (first) { - i++; - } - } - if (txt[i] === "\n") { - if (!stdsize) { - appendcurrent(); - span.append(document.createElement("br")); - } - continue; - } - if (txt[i] === "`") { - let count = 1; - if (txt[i + 1] === "`") { - count++; - if (txt[i + 2] === "`") { - count++; - } - } - let build = ""; - if (keep) { - build += "`".repeat(count); - } - let find = 0; - let j = i + count; - let init = true; - for (; txt[j] !== undefined && (txt[j] !== "\n" || count === 3) && find !== count; j++) { - if (txt[j] === "`") { - find++; - } - else { - if (find !== 0) { - build += "`".repeat(find); - find = 0; - } - if (init && count === 3) { - if (txt[j] === " " || txt[j] === "\n") { - init = false; - } - if (keep) { - build += txt[j]; - } - continue; - } - build += txt[j]; - } - } - if (stdsize) { - build = build.replaceAll("\n", ""); - } - if (find === count) { - appendcurrent(); - i = j; - if (keep) { - build += "`".repeat(find); - } - if (count !== 3 && !stdsize) { - const samp = document.createElement("samp"); - samp.textContent = build; - span.appendChild(samp); - } - else { - const pre = document.createElement("pre"); - if (build.at(-1) === "\n") { - build = build.substring(0, build.length - 1); - } - if (txt[i] === "\n") { - i++; - } - pre.textContent = build; - span.appendChild(pre); - } - i--; - continue; - } - } - if (txt[i] === "*") { - let count = 1; - if (txt[i + 1] === "*") { - count++; - if (txt[i + 2] === "*") { - count++; - } - } - let build = []; - let find = 0; - let j = i + count; - for (; txt[j] !== undefined && find !== count; j++) { - if (txt[j] === "*") { - find++; - } - else { - build.push(txt[j]); - if (find !== 0) { - build = build.concat(new Array(find).fill("*")); - find = 0; - } - } - } - if (find === count && (count != 1 || txt[i + 1] !== " ")) { - appendcurrent(); - i = j; - const stars = "*".repeat(count); - if (count === 1) { - const i = document.createElement("i"); - if (keep) { - i.append(stars); - } - i.appendChild(this.markdown(build, { keep, stdsize })); - if (keep) { - i.append(stars); - } - span.appendChild(i); - } - else if (count === 2) { - const b = document.createElement("b"); - if (keep) { - b.append(stars); - } - b.appendChild(this.markdown(build, { keep, stdsize })); - if (keep) { - b.append(stars); - } - span.appendChild(b); - } - else { - const b = document.createElement("b"); - const i = document.createElement("i"); - if (keep) { - b.append(stars); - } - b.appendChild(this.markdown(build, { keep, stdsize })); - if (keep) { - b.append(stars); - } - i.appendChild(b); - span.appendChild(i); - } - i--; - continue; - } - } - if (txt[i] === "_") { - let count = 1; - if (txt[i + 1] === "_") { - count++; - if (txt[i + 2] === "_") { - count++; - } - } - let build = []; - let find = 0; - let j = i + count; - for (; txt[j] !== undefined && find !== count; j++) { - if (txt[j] === "_") { - find++; - } - else { - build.push(txt[j]); - if (find !== 0) { - build = build.concat(new Array(find).fill("_")); - find = 0; - } - } - } - if (find === count && (count != 1 || (txt[j + 1] === " " || txt[j + 1] === "\n" || txt[j + 1] === undefined))) { - appendcurrent(); - i = j; - const underscores = "_".repeat(count); - if (count === 1) { - const i = document.createElement("i"); - if (keep) { - i.append(underscores); - } - i.appendChild(this.markdown(build, { keep, stdsize })); - if (keep) { - i.append(underscores); - } - span.appendChild(i); - } - else if (count === 2) { - const u = document.createElement("u"); - if (keep) { - u.append(underscores); - } - u.appendChild(this.markdown(build, { keep, stdsize })); - if (keep) { - u.append(underscores); - } - span.appendChild(u); - } - else { - const u = document.createElement("u"); - const i = document.createElement("i"); - if (keep) { - i.append(underscores); - } - i.appendChild(this.markdown(build, { keep, stdsize })); - if (keep) { - i.append(underscores); - } - u.appendChild(i); - span.appendChild(u); - } - i--; - continue; - } - } - if (txt[i] === "~" && txt[i + 1] === "~") { - const count = 2; - let build = []; - let find = 0; - let j = i + 2; - for (; txt[j] !== undefined && find !== count; j++) { - if (txt[j] === "~") { - find++; - } - else { - build.push(txt[j]); - if (find !== 0) { - build = build.concat(new Array(find).fill("~")); - find = 0; - } - } - } - if (find === count) { - appendcurrent(); - i = j - 1; - const tildes = "~~"; - if (count === 2) { - const s = document.createElement("s"); - if (keep) { - s.append(tildes); - } - s.appendChild(this.markdown(build, { keep, stdsize })); - if (keep) { - s.append(tildes); - } - span.appendChild(s); - } - continue; - } - } - if (txt[i] === "|" && txt[i + 1] === "|") { - const count = 2; - let build = []; - let find = 0; - let j = i + 2; - for (; txt[j] !== undefined && find !== count; j++) { - if (txt[j] === "|") { - find++; - } - else { - build.push(txt[j]); - if (find !== 0) { - build = build.concat(new Array(find).fill("~")); - find = 0; - } - } - } - if (find === count) { - appendcurrent(); - i = j - 1; - const pipes = "||"; - if (count === 2) { - const j = document.createElement("j"); - if (keep) { - j.append(pipes); - } - j.appendChild(this.markdown(build, { keep, stdsize })); - j.classList.add("spoiler"); - j.onclick = MarkDown.unspoil; - if (keep) { - j.append(pipes); - } - span.appendChild(j); - } - continue; - } - } - if ((!keep) && txt[i] === "h" && txt[i + 1] === "t" && txt[i + 2] === "t" && txt[i + 3] === "p") { - let build = "http"; - let j = i + 4; - const endchars = new Set(["\\", "<", ">", "|", "]", " "]); - for (; txt[j] !== undefined; j++) { - const char = txt[j]; - if (endchars.has(char)) { - break; - } - build += char; - } - if (URL.canParse(build)) { - appendcurrent(); - const a = document.createElement("a"); - //a.href=build; - MarkDown.safeLink(a, build); - a.textContent = build; - a.target = "_blank"; - i = j - 1; - span.appendChild(a); - continue; - } - } - if (txt[i] === "<" && (txt[i + 1] === "@" || txt[i + 1] === "#")) { - let id = ""; - let j = i + 2; - const numbers = new Set(["0", "1", "2", "3", "4", "5", "6", "7", "8", "9"]); - for (; txt[j] !== undefined; j++) { - const char = txt[j]; - if (!numbers.has(char)) { - break; - } - id += char; - } - if (txt[j] === ">") { - appendcurrent(); - const mention = document.createElement("span"); - mention.classList.add("mentionMD"); - mention.contentEditable = "false"; - const char = txt[i + 1]; - i = j; - switch (char) { - case "@": - const user = this.localuser.userMap.get(id); - if (user) { - mention.textContent = `@${user.name}`; - let guild = null; - if (this.owner instanceof Channel) { - guild = this.owner.guild; - } - if (!keep) { - user.bind(mention, guild); - } - if (guild) { - Member.resolveMember(user, guild).then(member => { - if (member) { - mention.textContent = `@${member.name}`; - } - }); - } - } - else { - mention.textContent = `@unknown`; - } - break; - case "#": - const channel = this.localuser.channelids.get(id); - if (channel) { - mention.textContent = `#${channel.name}`; - if (!keep) { - mention.onclick = _ => { - this.localuser.goToChannel(id); - }; - } - } - else { - mention.textContent = `#unknown`; - } - break; - } - span.appendChild(mention); - mention.setAttribute("real", `<${char}${id}>`); - continue; - } - } - if (txt[i] === "<" && txt[i + 1] === "t" && txt[i + 2] === ":") { - let found = false; - const build = ["<", "t", ":"]; - let j = i + 3; - for (; txt[j] !== void 0; j++) { - build.push(txt[j]); - if (txt[j] === ">") { - found = true; - break; - } - } - if (found) { - appendcurrent(); - i = j; - const parts = build.join("").match(/^$/); - const dateInput = new Date(Number.parseInt(parts[1]) * 1000); - let time = ""; - if (Number.isNaN(dateInput.getTime())) - time = build.join(""); - else { - if (parts[3] === "d") - time = dateInput.toLocaleString(void 0, { day: "2-digit", month: "2-digit", year: "numeric" }); - else if (parts[3] === "D") - time = dateInput.toLocaleString(void 0, { day: "numeric", month: "long", year: "numeric" }); - else if (!parts[3] || parts[3] === "f") - time = dateInput.toLocaleString(void 0, { day: "numeric", month: "long", year: "numeric" }) + " " + - dateInput.toLocaleString(void 0, { hour: "2-digit", minute: "2-digit" }); - else if (parts[3] === "F") - time = dateInput.toLocaleString(void 0, { day: "numeric", month: "long", year: "numeric", weekday: "long" }) + " " + - dateInput.toLocaleString(void 0, { hour: "2-digit", minute: "2-digit" }); - else if (parts[3] === "t") - time = dateInput.toLocaleString(void 0, { hour: "2-digit", minute: "2-digit" }); - else if (parts[3] === "T") - time = dateInput.toLocaleString(void 0, { hour: "2-digit", minute: "2-digit", second: "2-digit" }); - else if (parts[3] === "R") - time = Math.round((Date.now() - (Number.parseInt(parts[1]) * 1000)) / 1000 / 60) + " minutes ago"; - } - const timeElem = document.createElement("span"); - timeElem.classList.add("markdown-timestamp"); - timeElem.textContent = time; - span.appendChild(timeElem); - continue; - } - } - if (txt[i] === "<" && (txt[i + 1] === ":" || (txt[i + 1] === "a" && txt[i + 2] === ":"))) { - let found = false; - const build = txt[i + 1] === "a" ? ["<", "a", ":"] : ["<", ":"]; - let j = i + build.length; - for (; txt[j] !== void 0; j++) { - build.push(txt[j]); - if (txt[j] === ">") { - found = true; - break; - } - } - if (found) { - const buildjoin = build.join(""); - const parts = buildjoin.match(/^<(a)?:\w+:(\d{10,30})>$/); - if (parts && parts[2]) { - appendcurrent(); - i = j; - const isEmojiOnly = txt.join("").trim() === buildjoin.trim(); - const owner = (this.owner instanceof Channel) ? this.owner.guild : this.owner; - const emoji = new Emoji({ name: buildjoin, id: parts[2], animated: Boolean(parts[1]) }, owner); - span.appendChild(emoji.getHTML(isEmojiOnly)); - continue; - } - } - } - if (txt[i] == "[" && !keep) { - let partsFound = 0; - let j = i + 1; - const build = ["["]; - for (; txt[j] !== void 0; j++) { - build.push(txt[j]); - if (partsFound === 0 && txt[j] === "]") { - if (txt[j + 1] === "(" && - txt[j + 2] === "h" && txt[j + 3] === "t" && txt[j + 4] === "t" && txt[j + 5] === "p" && (txt[j + 6] === "s" || txt[j + 6] === ":")) { - partsFound++; - } - else { - break; - } - } - else if (partsFound === 1 && txt[j] === ")") { - partsFound++; - break; - } - } - if (partsFound === 2) { - appendcurrent(); - const parts = build.join("").match(/^\[(.+)\]\((https?:.+?)( ('|").+('|"))?\)$/); - if (parts) { - const linkElem = document.createElement("a"); - if (URL.canParse(parts[2])) { - i = j; - MarkDown.safeLink(linkElem, parts[2]); - linkElem.textContent = parts[1]; - linkElem.target = "_blank"; - linkElem.rel = "noopener noreferrer"; - linkElem.title = (parts[3] ? parts[3].substring(2, parts[3].length - 1) + "\n\n" : "") + parts[2]; - span.appendChild(linkElem); - continue; - } - } - } - } - current.textContent += txt[i]; - } - appendcurrent(); - return span; - } - static unspoil(e) { - e.target.classList.remove("spoiler"); - e.target.classList.add("unspoiled"); - } - giveBox(box) { - box.onkeydown = _ => { - //console.log(_); - }; - let prevcontent = ""; - box.onkeyup = _ => { - const content = MarkDown.gatherBoxText(box); - if (content !== prevcontent) { - prevcontent = content; - this.txt = content.split(""); - this.boxupdate(box); - } - }; - box.onpaste = _ => { - if (!_.clipboardData) - return; - console.log(_.clipboardData.types); - const data = _.clipboardData.getData("text"); - document.execCommand("insertHTML", false, data); - _.preventDefault(); - if (!box.onkeyup) - return; - box.onkeyup(new KeyboardEvent("_")); - }; - } - boxupdate(box) { - const restore = saveCaretPosition(box); - box.innerHTML = ""; - box.append(this.makeHTML({ keep: true })); - if (restore) { - restore(); - } - } - static gatherBoxText(element) { - if (element.tagName.toLowerCase() === "img") { - return element.alt; - } - if (element.tagName.toLowerCase() === "br") { - return "\n"; - } - if (element.hasAttribute("real")) { - return element.getAttribute("real"); - } - let build = ""; - for (const thing of element.childNodes) { - if (thing instanceof Text) { - const text = thing.textContent; - build += text; - continue; - } - const text = this.gatherBoxText(thing); - if (text) { - build += text; - } - } - return build; - } - static trustedDomains = new Set([location.host]); - static safeLink(elm, url) { - if (URL.canParse(url)) { - const Url = new URL(url); - if (elm instanceof HTMLAnchorElement && this.trustedDomains.has(Url.host)) { - elm.href = url; - elm.target = "_blank"; - return; - } - elm.onmouseup = _ => { - if (_.button === 2) - return; - console.log(":3"); - function open() { - const proxy = window.open(url, '_blank'); - if (proxy && _.button === 1) { - proxy.focus(); - } - else if (proxy) { - window.focus(); - } - } - if (this.trustedDomains.has(Url.host)) { - open(); - } - else { - const full = new Dialog([ - "vdiv", - ["title", "You're leaving spacebar"], - ["text", "You're going to " + Url.host + " are you sure you want to go there?"], - ["hdiv", - ["button", "", "Nevermind", _ => full.hide()], - ["button", "", "Go there", _ => { open(); full.hide(); }], - ["button", "", "Go there and trust in the future", _ => { - open(); - full.hide(); - this.trustedDomains.add(Url.host); - }] - ] - ]); - full.show(); - } - }; - } - else { - throw Error(url + " is not a valid URL"); - } - } - static replace(base, newelm) { - const basechildren = base.children; - const newchildren = newelm.children; - for (const thing of newchildren) { - } - } -} -//solution from https://stackoverflow.com/questions/4576694/saving-and-restoring-caret-position-for-contenteditable-div -let text = ""; -function saveCaretPosition(context) { - const selection = window.getSelection(); - if (!selection) - return; - const range = selection.getRangeAt(0); - range.setStart(context, 0); - text = selection.toString(); - let len = text.length + 1; - for (const str in text.split("\n")) { - if (str.length !== 0) { - len--; - } - } - len += +(text[text.length - 1] === "\n"); - return function restore() { - if (!selection) - return; - const pos = getTextNodeAtPosition(context, len); - selection.removeAllRanges(); - const range = new Range(); - range.setStart(pos.node, pos.position); - selection.addRange(range); - }; -} -function getTextNodeAtPosition(root, index) { - const NODE_TYPE = NodeFilter.SHOW_TEXT; - const treeWalker = document.createTreeWalker(root, NODE_TYPE, elem => { - if (!elem.textContent) - return 0; - if (index > elem.textContent.length) { - index -= elem.textContent.length; - return NodeFilter.FILTER_REJECT; - } - return NodeFilter.FILTER_ACCEPT; - }); - const c = treeWalker.nextNode(); - return { - node: c ? c : root, - position: index - }; -} -export { MarkDown }; diff --git a/.dist/member.js b/.dist/member.js deleted file mode 100644 index 9bc8d13..0000000 --- a/.dist/member.js +++ /dev/null @@ -1,221 +0,0 @@ -import { User } from "./user.js"; -import { SnowFlake } from "./snowflake.js"; -import { Dialog } from "./dialog.js"; -class Member extends SnowFlake { - static already = {}; - owner; - user; - roles = []; - nick; - constructor(memberjson, owner) { - super(memberjson.id); - this.owner = owner; - if (this.localuser.userMap.has(memberjson.id)) { - this.user = this.localuser.userMap.get(memberjson.id); - } - else if (memberjson.user) { - this.user = new User(memberjson.user, owner.localuser); - } - else { - throw new Error("Missing user object of this member"); - } - for (const thing of Object.keys(memberjson)) { - if (thing === "guild") { - continue; - } - if (thing === "owner") { - continue; - } - if (thing === "roles") { - for (const strrole of memberjson.roles) { - const role = this.guild.roleids.get(strrole); - if (!role) - continue; - this.roles.push(role); - } - continue; - } - this[thing] = memberjson[thing]; - } - if (this.localuser.userMap.has(this?.id)) { - this.user = this.localuser.userMap.get(this?.id); - } - this.roles.sort((a, b) => { return (this.guild.roles.indexOf(a) - this.guild.roles.indexOf(b)); }); - } - get guild() { - return this.owner; - } - get localuser() { - return this.guild.localuser; - } - get info() { - return this.owner.info; - } - static async new(memberjson, owner) { - let user; - if (owner.localuser.userMap.has(memberjson.id)) { - user = owner.localuser.userMap.get(memberjson.id); - } - else if (memberjson.user) { - user = new User(memberjson.user, owner.localuser); - } - else { - throw new Error("missing user object of this member"); - } - if (user.members.has(owner)) { - let memb = user.members.get(owner); - if (memb === undefined) { - memb = new Member(memberjson, owner); - user.members.set(owner, memb); - return memb; - } - else if (memb instanceof Promise) { - return await memb; //I should do something else, though for now this is "good enough" - } - else { - return memb; - } - } - else { - const memb = new Member(memberjson, owner); - user.members.set(owner, memb); - return memb; - } - } - static async resolveMember(user, guild) { - const maybe = user.members.get(guild); - if (!user.members.has(guild)) { - const membpromise = guild.localuser.resolvemember(user.id, guild.id); - const promise = new Promise(async (res) => { - const membjson = await membpromise; - if (membjson === undefined) { - res(undefined); - } - else { - const member = new Member(membjson, guild); - const map = guild.localuser.presences; - member.getPresence(map.get(member.id)); - map.delete(member.id); - res(member); - return member; - } - }); - user.members.set(guild, promise); - } - if (maybe instanceof Promise) { - return await maybe; - } - else { - return maybe; - } - } - getPresence(presence) { - this.user.getPresence(presence); - } - /** - * @todo - */ - highInfo() { - fetch(this.info.api + "/users/" + this.id + "/profile?with_mutual_guilds=true&with_mutual_friends_count=true&guild_id=" + this.guild.id, { headers: this.guild.headers }); - } - hasRole(ID) { - console.log(this.roles, ID); - for (const thing of this.roles) { - if (thing.id === ID) { - return true; - } - } - return false; - } - getColor() { - for (const thing of this.roles) { - const color = thing.getColor(); - if (color) { - return color; - } - } - return ""; - } - isAdmin() { - for (const role of this.roles) { - if (role.permissions.getPermission("ADMINISTRATOR")) { - return true; - } - } - return this.guild.properties.owner_id === this.user.id; - } - bind(html) { - if (html.tagName === "SPAN") { - if (!this) { - return; - } - /* - if(this.error){ - - } - */ - html.style.color = this.getColor(); - } - //this.profileclick(html); - } - profileclick(html) { - //to be implemented - } - get name() { - return this.nick || this.user.username; - } - kick() { - let reason = ""; - const menu = new Dialog(["vdiv", - ["title", "Kick " + this.name + " from " + this.guild.properties.name], - ["textbox", "Reason:", "", function (e) { - reason = e.target.value; - }], - ["button", "", "submit", () => { - this.kickAPI(reason); - menu.hide(); - }]]); - menu.show(); - } - kickAPI(reason) { - const headers = structuredClone(this.guild.headers); - headers["x-audit-log-reason"] = reason; - fetch(`${this.info.api}/guilds/${this.guild.id}/members/${this.id}`, { - method: "DELETE", - headers, - }); - } - ban() { - let reason = ""; - const menu = new Dialog(["vdiv", - ["title", "Ban " + this.name + " from " + this.guild.properties.name], - ["textbox", "Reason:", "", function (e) { - reason = e.target.value; - }], - ["button", "", "submit", () => { - this.banAPI(reason); - menu.hide(); - }]]); - menu.show(); - } - banAPI(reason) { - const headers = structuredClone(this.guild.headers); - headers["x-audit-log-reason"] = reason; - fetch(`${this.info.api}/guilds/${this.guild.id}/bans/${this.id}`, { - method: "PUT", - headers - }); - } - hasPermission(name) { - if (this.isAdmin()) { - return true; - } - for (const thing of this.roles) { - if (thing.permissions.getPermission(name)) { - return true; - } - } - return false; - } -} -export { Member }; diff --git a/.dist/message.js b/.dist/message.js deleted file mode 100644 index 794eebf..0000000 --- a/.dist/message.js +++ /dev/null @@ -1,699 +0,0 @@ -import { Contextmenu } from "./contextmenu.js"; -import { User } from "./user.js"; -import { Member } from "./member.js"; -import { MarkDown } from "./markdown.js"; -import { Embed } from "./embed.js"; -import { File } from "./file.js"; -import { SnowFlake } from "./snowflake.js"; -import { Emoji } from "./emoji.js"; -import { Dialog } from "./dialog.js"; -class Message extends SnowFlake { - static contextmenu = new Contextmenu("message menu"); - owner; - headers; - embeds; - author; - mentions; - mention_roles; - attachments; //probably should be its own class tbh, should be Attachments[] - message_reference; - type; - timestamp; - content; - static del; - static resolve; - /* - weakdiv:WeakRef; - set div(e:HTMLDivElement){ - if(!e){ - this.weakdiv=null; - return; - } - this.weakdiv=new WeakRef(e); - } - get div(){ - return this.weakdiv?.deref(); - } - //*/ - div; - member; - reactions; - static setup() { - this.del = new Promise(_ => { - this.resolve = _; - }); - Message.setupcmenu(); - } - static setupcmenu() { - Message.contextmenu.addbutton("Copy raw text", function () { - navigator.clipboard.writeText(this.content.rawString); - }); - Message.contextmenu.addbutton("Reply", function () { - this.channel.setReplying(this); - }); - Message.contextmenu.addbutton("Copy message id", function () { - navigator.clipboard.writeText(this.id); - }); - Message.contextmenu.addsubmenu("Add reaction", function (arg, e) { - Emoji.emojiPicker(e.x, e.y, this.localuser).then(_ => { - this.reactionToggle(_); - }); - }); - Message.contextmenu.addbutton("Edit", function () { - this.setEdit(); - }, null, function () { - return this.author.id === this.localuser.user.id; - }); - Message.contextmenu.addbutton("Delete message", function () { - this.delete(); - }, null, function () { - return this.canDelete(); - }); - } - setEdit() { - this.channel.editing = this; - const markdown = document.getElementById("typebox")["markdown"]; - markdown.txt = this.content.rawString.split(""); - markdown.boxupdate(document.getElementById("typebox")); - } - constructor(messagejson, owner) { - super(messagejson.id); - this.owner = owner; - this.headers = this.owner.headers; - this.giveData(messagejson); - this.owner.messages.set(this.id, this); - } - reactionToggle(emoji) { - let remove = false; - for (const thing of this.reactions) { - if (thing.emoji.name === emoji) { - remove = thing.me; - break; - } - } - let reactiontxt; - if (emoji instanceof Emoji) { - reactiontxt = `${emoji.name}:${emoji.id}`; - } - else { - reactiontxt = encodeURIComponent(emoji); - } - fetch(`${this.info.api}/channels/${this.channel.id}/messages/${this.id}/reactions/${reactiontxt}/@me`, { - method: remove ? "DELETE" : "PUT", - headers: this.headers - }); - } - giveData(messagejson) { - const func = this.channel.infinite.snapBottom(); - for (const thing of Object.keys(messagejson)) { - if (thing === "attachments") { - this.attachments = []; - for (const thing of messagejson.attachments) { - this.attachments.push(new File(thing, this)); - } - continue; - } - else if (thing === "content") { - this.content = new MarkDown(messagejson[thing], this.channel); - continue; - } - else if (thing === "id") { - continue; - } - else if (thing === "member") { - Member.new(messagejson.member, this.guild).then(_ => { - this.member = _; - }); - continue; - } - else if (thing === "embeds") { - this.embeds = []; - for (const thing in messagejson.embeds) { - this.embeds[thing] = new Embed(messagejson.embeds[thing], this); - } - continue; - } - this[thing] = messagejson[thing]; - } - if (messagejson.reactions?.length) { - console.log(messagejson.reactions, ":3"); - } - this.author = new User(messagejson.author, this.localuser); - for (const thing in messagejson.mentions) { - this.mentions[thing] = new User(messagejson.mentions[thing], this.localuser); - } - if (!this.member && this.guild.id !== "@me") { - this.author.resolvemember(this.guild).then(_ => { - this.member = _; - }); - } - if (this.mentions.length || this.mention_roles.length) { //currently mention_roles isn't implemented on the spacebar servers - console.log(this.mentions, this.mention_roles); - } - if (this.mentionsuser(this.localuser.user)) { - console.log(this); - } - if (this.div) { - this.generateMessage(); - } - func(); - } - canDelete() { - return this.channel.hasPermission("MANAGE_MESSAGES") || this.author === this.localuser.user; - } - get channel() { - return this.owner; - } - get guild() { - return this.owner.guild; - } - get localuser() { - return this.owner.localuser; - } - get info() { - return this.owner.info; - } - messageevents(obj) { - const func = Message.contextmenu.bindContextmenu(obj, this, undefined); - this.div = obj; - obj.classList.add("messagediv"); - } - deleteDiv() { - if (!this.div) - return; - try { - this.div.remove(); - this.div = undefined; - } - catch (e) { - console.error(e); - } - } - mentionsuser(userd) { - if (userd instanceof User) { - return this.mentions.includes(userd); - } - else if (userd instanceof Member) { - return this.mentions.includes(userd.user); - } - } - getimages() { - const build = []; - for (const thing of this.attachments) { - if (thing.content_type.startsWith("image/")) { - build.push(thing); - } - } - return build; - } - async edit(content) { - return await fetch(this.info.api + "/channels/" + this.channel.id + "/messages/" + this.id, { - method: "PATCH", - headers: this.headers, - body: JSON.stringify({ content }) - }); - } - delete() { - fetch(`${this.info.api}/channels/${this.channel.id}/messages/${this.id}`, { - headers: this.headers, - method: "DELETE", - }); - } - deleteEvent() { - console.log("deleted"); - if (this.div) { - this.div.remove(); - this.div.innerHTML = ""; - this.div = undefined; - } - const prev = this.channel.idToPrev.get(this.id); - const next = this.channel.idToNext.get(this.id); - this.channel.idToPrev.delete(this.id); - this.channel.idToNext.delete(this.id); - this.channel.messages.delete(this.id); - if (prev && next) { - this.channel.idToPrev.set(next, prev); - this.channel.idToNext.set(prev, next); - } - else if (prev) { - this.channel.idToNext.delete(prev); - } - else if (next) { - this.channel.idToPrev.delete(next); - } - if (prev) { - const prevmessage = this.channel.messages.get(prev); - if (prevmessage) { - prevmessage.generateMessage(); - } - } - if (this.channel.lastmessage === this || this.channel.lastmessageid === this.id) { - if (prev) { - this.channel.lastmessage = this.channel.messages.get(prev); - this.channel.lastmessageid = prev; - } - else { - this.channel.lastmessage = undefined; - this.channel.lastmessageid = undefined; - } - } - if (this.channel.lastreadmessageid === this.id) { - if (prev) { - this.channel.lastreadmessageid = prev; - } - else { - this.channel.lastreadmessageid = undefined; - } - } - console.log("deleted done"); - } - reactdiv; - blockedPropigate() { - const previd = this.channel.idToPrev.get(this.id); - if (!previd) { - this.generateMessage(); - return; - } - const premessage = this.channel.messages.get(previd); - if (premessage?.author === this.author) { - premessage.blockedPropigate(); - } - else { - this.generateMessage(); - } - } - generateMessage(premessage, ignoredblock = false) { - if (!this.div) - return; - if (!premessage) { - premessage = this.channel.messages.get(this.channel.idToPrev.get(this.id)); - } - const div = this.div; - for (const user of this.mentions) { - if (user === this.localuser.user) { - div.classList.add("mentioned"); - } - } - if (this === this.channel.replyingto) { - div.classList.add("replying"); - } - div.innerHTML = ""; - const build = document.createElement("div"); - build.classList.add("flexltr", "message"); - div.classList.remove("zeroheight"); - if (this.author.relationshipType === 2) { - if (ignoredblock) { - if (premessage?.author !== this.author) { - const span = document.createElement("span"); - span.textContent = "You have this user blocked, click to hide these messages."; - div.append(span); - span.classList.add("blocked"); - span.onclick = _ => { - const scroll = this.channel.infinite.scrollTop; - let next = this; - while (next?.author === this.author) { - next.generateMessage(); - next = this.channel.messages.get(this.channel.idToNext.get(next.id)); - } - if (this.channel.infinite.scollDiv && scroll) { - this.channel.infinite.scollDiv.scrollTop = scroll; - } - }; - } - } - else { - div.classList.remove("topMessage"); - if (premessage?.author === this.author) { - div.classList.add("zeroheight"); - premessage.blockedPropigate(); - div.appendChild(build); - return div; - } - else { - build.classList.add("blocked", "topMessage"); - const span = document.createElement("span"); - let count = 1; - let next = this.channel.messages.get(this.channel.idToNext.get(this.id)); - while (next?.author === this.author) { - count++; - next = this.channel.messages.get(this.channel.idToNext.get(next.id)); - } - span.textContent = `You have this user blocked, click to see the ${count} blocked messages.`; - build.append(span); - span.onclick = _ => { - const scroll = this.channel.infinite.scrollTop; - const func = this.channel.infinite.snapBottom(); - let next = this; - while (next?.author === this.author) { - next.generateMessage(undefined, true); - next = this.channel.messages.get(this.channel.idToNext.get(next.id)); - console.log("loopy"); - } - if (this.channel.infinite.scollDiv && scroll) { - func(); - this.channel.infinite.scollDiv.scrollTop = scroll; - } - }; - div.appendChild(build); - return div; - } - } - } - if (this.message_reference) { - const replyline = document.createElement("div"); - const line = document.createElement("hr"); - const minipfp = document.createElement("img"); - minipfp.classList.add("replypfp"); - replyline.appendChild(line); - replyline.appendChild(minipfp); - const username = document.createElement("span"); - replyline.appendChild(username); - const reply = document.createElement("div"); - username.classList.add("username"); - reply.classList.add("replytext"); - replyline.appendChild(reply); - const line2 = document.createElement("hr"); - replyline.appendChild(line2); - line2.classList.add("reply"); - line.classList.add("startreply"); - replyline.classList.add("replyflex"); - this.channel.getmessage(this.message_reference.message_id).then(message => { - if (message.author.relationshipType === 2) { - username.textContent = "Blocked user"; - return; - } - const author = message.author; - reply.appendChild(message.content.makeHTML({ stdsize: true })); - minipfp.src = author.getpfpsrc(); - author.bind(minipfp, this.guild); - username.textContent = author.username; - author.bind(username, this.guild); - }); - reply.onclick = _ => { - this.channel.infinite.focus(this.message_reference.message_id); - }; - div.appendChild(replyline); - } - div.appendChild(build); - if ({ 0: true, 19: true }[this.type] || this.attachments.length !== 0) { - const pfpRow = document.createElement("div"); - pfpRow.classList.add("flexltr"); - let pfpparent, current; - if (premessage != null) { - pfpparent ??= premessage; - let pfpparent2 = pfpparent.all; - pfpparent2 ??= pfpparent; - const old = (new Date(pfpparent2.timestamp).getTime()) / 1000; - const newt = (new Date(this.timestamp).getTime()) / 1000; - current = (newt - old) > 600; - } - const combine = (premessage?.author != this.author) || (current) || this.message_reference; - if (combine) { - const pfp = this.author.buildpfp(); - this.author.bind(pfp, this.guild, false); - pfpRow.appendChild(pfp); - } - else { - div["pfpparent"] = pfpparent; - } - pfpRow.classList.add("pfprow"); - build.appendChild(pfpRow); - const text = document.createElement("div"); - text.classList.add("flexttb"); - const texttxt = document.createElement("div"); - texttxt.classList.add("commentrow", "flexttb"); - text.appendChild(texttxt); - if (combine) { - const username = document.createElement("span"); - username.classList.add("username"); - this.author.bind(username, this.guild); - div.classList.add("topMessage"); - username.textContent = this.author.username; - const userwrap = document.createElement("div"); - userwrap.classList.add("flexltr"); - userwrap.appendChild(username); - if (this.author.bot) { - const username = document.createElement("span"); - username.classList.add("bot"); - username.textContent = "BOT"; - userwrap.appendChild(username); - } - const time = document.createElement("span"); - time.textContent = " " + formatTime(new Date(this.timestamp)); - time.classList.add("timestamp"); - userwrap.appendChild(time); - texttxt.appendChild(userwrap); - } - else { - div.classList.remove("topMessage"); - } - const messaged = this.content.makeHTML(); - div["txt"] = messaged; - const messagedwrap = document.createElement("div"); - messagedwrap.classList.add("flexttb"); - messagedwrap.appendChild(messaged); - texttxt.appendChild(messagedwrap); - build.appendChild(text); - if (this.attachments.length) { - console.log(this.attachments); - const attach = document.createElement("div"); - attach.classList.add("flexltr"); - for (const thing of this.attachments) { - attach.appendChild(thing.getHTML()); - } - messagedwrap.appendChild(attach); - } - if (this.embeds.length) { - const embeds = document.createElement("div"); - embeds.classList.add("flexltr"); - for (const thing of this.embeds) { - embeds.appendChild(thing.generateHTML()); - } - messagedwrap.appendChild(embeds); - } - // - } - else if (this.type === 7) { - const text = document.createElement("div"); - text.classList.add("flexttb"); - const texttxt = document.createElement("div"); - text.appendChild(texttxt); - build.appendChild(text); - texttxt.classList.add("flexltr"); - const messaged = document.createElement("span"); - div["txt"] = messaged; - messaged.textContent = "welcome: "; - texttxt.appendChild(messaged); - const username = document.createElement("span"); - username.textContent = this.author.username; - //this.author.profileclick(username); - this.author.bind(username, this.guild); - texttxt.appendChild(username); - username.classList.add("username"); - const time = document.createElement("span"); - time.textContent = " " + formatTime(new Date(this.timestamp)); - time.classList.add("timestamp"); - texttxt.append(time); - div.classList.add("topMessage"); - } - const reactions = document.createElement("div"); - reactions.classList.add("flexltr", "reactiondiv"); - this.reactdiv = new WeakRef(reactions); - this.updateReactions(); - div.append(reactions); - this.bindButtonEvent(); - return (div); - } - bindButtonEvent() { - if (this.div) { - let buttons; - this.div.onmouseenter = _ => { - if (buttons) { - buttons.remove(); - buttons = undefined; - } - if (this.div) { - buttons = document.createElement("div"); - buttons.classList.add("messageButtons", "flexltr"); - if (this.channel.hasPermission("SEND_MESSAGES")) { - const container = document.createElement("div"); - const reply = document.createElement("span"); - reply.classList.add("svgtheme", "svg-reply", "svgicon"); - container.append(reply); - buttons.append(container); - container.onclick = _ => { - this.channel.setReplying(this); - }; - } - if (this.author === this.localuser.user) { - const container = document.createElement("div"); - const edit = document.createElement("span"); - edit.classList.add("svgtheme", "svg-edit", "svgicon"); - container.append(edit); - buttons.append(container); - container.onclick = _ => { - this.setEdit(); - }; - } - if (this.canDelete()) { - const container = document.createElement("div"); - const reply = document.createElement("span"); - reply.classList.add("svgtheme", "svg-delete", "svgicon"); - container.append(reply); - buttons.append(container); - container.onclick = _ => { - if (_.shiftKey) { - this.delete(); - return; - } - const diaolog = new Dialog(["hdiv", ["title", "are you sure you want to delete this?"], ["button", "", "yes", () => { this.delete(); diaolog.hide(); }], ["button", "", "no", () => { diaolog.hide(); }]]); - diaolog.show(); - }; - } - if (buttons.childNodes.length !== 0) { - this.div.append(buttons); - } - } - }; - this.div.onmouseleave = _ => { - if (buttons) { - buttons.remove(); - buttons = undefined; - } - }; - } - } - updateReactions() { - const reactdiv = this.reactdiv.deref(); - if (!reactdiv) - return; - const func = this.channel.infinite.snapBottom(); - reactdiv.innerHTML = ""; - for (const thing of this.reactions) { - const reaction = document.createElement("div"); - reaction.classList.add("reaction"); - if (thing.me) { - reaction.classList.add("meReacted"); - } - let emoji; - if (thing.emoji.id || /\d{17,21}/.test(thing.emoji.name)) { - if (/\d{17,21}/.test(thing.emoji.name)) - thing.emoji.id = thing.emoji.name; //Should stop being a thing once the server fixes this bug - const emo = new Emoji(thing.emoji, this.guild); - emoji = emo.getHTML(false); - } - else { - emoji = document.createElement("p"); - emoji.textContent = thing.emoji.name; - } - const count = document.createElement("p"); - count.textContent = "" + thing.count; - count.classList.add("reactionCount"); - reaction.append(count); - reaction.append(emoji); - reactdiv.append(reaction); - reaction.onclick = _ => { - this.reactionToggle(thing.emoji.name); - }; - } - func(); - } - reactionAdd(data, member) { - for (const thing of this.reactions) { - if (thing.emoji.name === data.name) { - thing.count++; - if (member.id === this.localuser.user.id) { - thing.me = true; - this.updateReactions(); - return; - } - } - } - this.reactions.push({ - count: 1, - emoji: data, - me: member.id === this.localuser.user.id - }); - this.updateReactions(); - } - reactionRemove(data, id) { - console.log("test"); - for (const i in this.reactions) { - const thing = this.reactions[i]; - console.log(thing, data); - if (thing.emoji.name === data.name) { - thing.count--; - if (thing.count === 0) { - this.reactions.splice(Number(i), 1); - this.updateReactions(); - return; - } - if (id === this.localuser.user.id) { - thing.me = false; - this.updateReactions(); - return; - } - } - } - } - reactionRemoveAll() { - this.reactions = []; - this.updateReactions(); - } - reactionRemoveEmoji(emoji) { - for (const i in this.reactions) { - const reaction = this.reactions[i]; - if ((reaction.emoji.id && reaction.emoji.id == emoji.id) || (!reaction.emoji.id && reaction.emoji.name == emoji.name)) { - this.reactions.splice(Number(i), 1); - this.updateReactions(); - break; - } - } - } - buildhtml(premessage) { - if (this.div) { - console.error(`HTML for ${this.id} already exists, aborting`); - return this.div; - } - try { - const div = document.createElement("div"); - this.div = div; - this.messageevents(div); - return this.generateMessage(premessage); - } - catch (e) { - console.error(e); - } - return this.div; - } -} -let now; -let yesterdayStr; -function formatTime(date) { - updateTimes(); - const datestring = date.toLocaleDateString(); - const formatTime = (date) => date.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }); - if (datestring === now) { - return `Today at ${formatTime(date)}`; - } - else if (datestring === yesterdayStr) { - return `Yesterday at ${formatTime(date)}`; - } - else { - return `${date.toLocaleDateString()} at ${formatTime(date)}`; - } -} -let tomorrow = 0; -updateTimes(); -function updateTimes() { - if (tomorrow < Date.now()) { - const d = new Date(); - tomorrow = d.setHours(24, 0, 0, 0); - now = new Date().toLocaleDateString(); - const yesterday = new Date(now); - yesterday.setDate(new Date().getDate() - 1); - yesterdayStr = yesterday.toLocaleDateString(); - } -} -Message.setup(); -export { Message }; diff --git a/.dist/permissions.js b/.dist/permissions.js deleted file mode 100644 index e52a770..0000000 --- a/.dist/permissions.js +++ /dev/null @@ -1,328 +0,0 @@ -class Permissions { - allow; - deny; - hasDeny; - constructor(allow, deny = "") { - this.hasDeny = Boolean(deny); - try { - this.allow = BigInt(allow); - this.deny = BigInt(deny); - } - catch { - this.allow = 0n; - this.deny = 0n; - console.error(`Something really stupid happened with a permission with allow being ${allow} and deny being, ${deny}, execution will still happen, but something really stupid happened, please report if you know what caused this.`); - } - } - getPermissionbit(b, big) { - return Boolean((big >> BigInt(b)) & 1n); - } - setPermissionbit(b, state, big) { - const bit = 1n << BigInt(b); - return (big & ~bit) | (BigInt(state) << BigInt(b)); //thanks to geotale for this code :3 - } - static map; - static info; - static makeMap() { - Permissions.info = [ - { - name: "CREATE_INSTANT_INVITE", - readableName: "Create invite", - description: "Allows the user to create invites for the guild" - }, - { - name: "KICK_MEMBERS", - readableName: "Kick members", - description: "Allows the user to kick members from the guild" - }, - { - name: "BAN_MEMBERS", - readableName: "Ban members", - description: "Allows the user to ban members from the guild" - }, - { - name: "ADMINISTRATOR", - readableName: "Administrator", - description: "Allows all permissions and bypasses channel permission overwrites. This is a dangerous permission!" - }, - { - name: "MANAGE_CHANNELS", - readableName: "Manage channels", - description: "Allows the user to manage and edit channels" - }, - { - name: "MANAGE_GUILD", - readableName: "Manage guild", - description: "Allows management and editing of the guild" - }, - { - name: "ADD_REACTIONS", - readableName: "Add reactions", - description: "Allows user to add reactions to messages" - }, - { - name: "VIEW_AUDIT_LOG", - readableName: "View audit log", - description: "Allows the user to view the audit log" - }, - { - name: "PRIORITY_SPEAKER", - readableName: "Priority speaker", - description: "Allows for using priority speaker in a voice channel" - }, - { - name: "STREAM", - readableName: "Video", - description: "Allows the user to stream" - }, - { - name: "VIEW_CHANNEL", - readableName: "View channels", - description: "Allows the user to view the channel" - }, - { - name: "SEND_MESSAGES", - readableName: "Send messages", - description: "Allows user to send messages" - }, - { - name: "SEND_TTS_MESSAGES", - readableName: "Send text-to-speech messages", - description: "Allows the user to send text-to-speech messages" - }, - { - name: "MANAGE_MESSAGES", - readableName: "Manage messages", - description: "Allows the user to delete messages that aren't their own" - }, - { - name: "EMBED_LINKS", - readableName: "Embed links", - description: "Allow links sent by this user to auto-embed" - }, - { - name: "ATTACH_FILES", - readableName: "Attach files", - description: "Allows the user to attach files" - }, - { - name: "READ_MESSAGE_HISTORY", - readableName: "Read message history", - description: "Allows user to read the message history" - }, - { - name: "MENTION_EVERYONE", - readableName: "Mention @everyone, @here and all roles", - description: "Allows the user to mention everyone" - }, - { - name: "USE_EXTERNAL_EMOJIS", - readableName: "Use external emojis", - description: "Allows the user to use external emojis" - }, - { - name: "VIEW_GUILD_INSIGHTS", - readableName: "View guild insights", - description: "Allows the user to see guild insights" - }, - { - name: "CONNECT", - readableName: "Connect", - description: "Allows the user to connect to a voice channel" - }, - { - name: "SPEAK", - readableName: "Speak", - description: "Allows the user to speak in a voice channel" - }, - { - name: "MUTE_MEMBERS", - readableName: "Mute members", - description: "Allows user to mute other members" - }, - { - name: "DEAFEN_MEMBERS", - readableName: "Deafen members", - description: "Allows user to deafen other members" - }, - { - name: "MOVE_MEMBERS", - readableName: "Move members", - description: "Allows the user to move members between voice channels" - }, - { - name: "USE_VAD", - readableName: "Use voice activity detection", - description: "Allows users to speak in a voice channel by simply talking" - }, - { - name: "CHANGE_NICKNAME", - readableName: "Change nickname", - description: "Allows the user to change their own nickname" - }, - { - name: "MANAGE_NICKNAMES", - readableName: "Manage nicknames", - description: "Allows user to change nicknames of other members" - }, - { - name: "MANAGE_ROLES", - readableName: "Manage roles", - description: "Allows user to edit and manage roles" - }, - { - name: "MANAGE_WEBHOOKS", - readableName: "Manage webhooks", - description: "Allows management and editing of webhooks" - }, - { - name: "MANAGE_GUILD_EXPRESSIONS", - readableName: "Manage expressions", - description: "Allows for managing emoji, stickers, and soundboards" - }, - { - name: "USE_APPLICATION_COMMANDS", - readableName: "Use application commands", - description: "Allows the user to use application commands" - }, - { - name: "REQUEST_TO_SPEAK", - readableName: "Request to speak", - description: "Allows user to request to speak in stage channel" - }, - { - name: "MANAGE_EVENTS", - readableName: "Manage events", - description: "Allows user to edit and manage events" - }, - { - name: "MANAGE_THREADS", - readableName: "Manage threads", - description: "Allows the user to delete and archive threads and view all private threads" - }, - { - name: "CREATE_PUBLIC_THREADS", - readableName: "Create public threads", - description: "Allows the user to create public threads" - }, - { - name: "CREATE_PRIVATE_THREADS", - readableName: "Create private threads", - description: "Allows the user to create private threads" - }, - { - name: "USE_EXTERNAL_STICKERS", - readableName: "Use external stickers", - description: "Allows user to use external stickers" - }, - { - name: "SEND_MESSAGES_IN_THREADS", - readableName: "Send messages in threads", - description: "Allows the user to send messages in threads" - }, - { - name: "USE_EMBEDDED_ACTIVITIES", - readableName: "Use activities", - description: "Allows the user to use embedded activities" - }, - { - name: "MODERATE_MEMBERS", - readableName: "Timeout members", - description: "Allows the user to time out other users to prevent them from sending or reacting to messages in chat and threads, and from speaking in voice and stage channels" - }, - { - name: "VIEW_CREATOR_MONETIZATION_ANALYTICS", - readableName: "View creator monetization analytics", - description: "Allows for viewing role subscription insights" - }, - { - name: "USE_SOUNDBOARD", - readableName: "Use soundboard", - description: "Allows for using soundboard in a voice channel" - }, - { - name: "CREATE_GUILD_EXPRESSIONS", - readableName: "Create expressions", - description: "Allows for creating emojis, stickers, and soundboard sounds, and editing and deleting those created by the current user." - }, - { - name: "CREATE_EVENTS", - readableName: "Create events", - description: "Allows for creating scheduled events, and editing and deleting those created by the current user." - }, - { - name: "USE_EXTERNAL_SOUNDS", - readableName: "Use external sounds", - description: "Allows the usage of custom soundboard sounds from other servers" - }, - { - name: "SEND_VOICE_MESSAGES", - readableName: "Send voice messages", - description: "Allows sending voice messages" - }, - { - name: "SEND_POLLS", - readableName: "Create polls", - description: "Allows sending polls" - }, - { - name: "USE_EXTERNAL_APPS", - readableName: "Use external apps", - description: "Allows user-installed apps to send public responses. " + - "When disabled, users will still be allowed to use their apps but the responses will be ephemeral. " + - "This only applies to apps not also installed to the server." - }, - ]; - Permissions.map = {}; - let i = 0; - for (const thing of Permissions.info) { - Permissions.map[i] = thing; - Permissions.map[thing.name] = i; - i++; - } - } - getPermission(name) { - if (this.getPermissionbit(Permissions.map[name], this.allow)) { - return 1; - } - else if (this.getPermissionbit(Permissions.map[name], this.deny)) { - return -1; - } - else { - return 0; - } - } - hasPermission(name) { - if (this.deny) { - console.warn("This function may of been used in error, think about using getPermision instead"); - } - if (this.getPermissionbit(Permissions.map[name], this.allow)) - return true; - if (name != "ADMINISTRATOR") - return this.hasPermission("ADMINISTRATOR"); - return false; - } - setPermission(name, setto) { - const bit = Permissions.map[name]; - if (!bit) { - return console.error("Tried to set permission to " + setto + " for " + name + " but it doesn't exist"); - } - if (setto === 0) { - this.deny = this.setPermissionbit(bit, false, this.deny); - this.allow = this.setPermissionbit(bit, false, this.allow); - } - else if (setto === 1) { - this.deny = this.setPermissionbit(bit, false, this.deny); - this.allow = this.setPermissionbit(bit, true, this.allow); - } - else if (setto === -1) { - this.deny = this.setPermissionbit(bit, true, this.deny); - this.allow = this.setPermissionbit(bit, false, this.allow); - } - else { - console.error("invalid number entered:" + setto); - } - } -} -Permissions.makeMap(); -export { Permissions }; diff --git a/.dist/register.js b/.dist/register.js deleted file mode 100644 index 8f8dc58..0000000 --- a/.dist/register.js +++ /dev/null @@ -1,118 +0,0 @@ -import { checkInstance, adduser } from "./login.js"; -if (document.getElementById("register")) { - document.getElementById("register").addEventListener("submit", registertry); -} -async function registertry(e) { - e.preventDefault(); - const elements = e.srcElement; - const email = elements[1].value; - const username = elements[2].value; - if (elements[3].value !== elements[4].value) { - document.getElementById("wrong").textContent = "Passwords don't match"; - return; - } - const password = elements[3].value; - const dateofbirth = elements[5].value; - const apiurl = new URL(JSON.parse(localStorage.getItem("instanceinfo")).api); - await fetch(apiurl + "/auth/register", { - body: JSON.stringify({ - date_of_birth: dateofbirth, - email, - username, - password, - consent: elements[6].checked, - captcha_key: elements[7]?.value - }), - headers: { - "content-type": "application/json" - }, - method: "POST" - }).then(e => { - e.json().then(e => { - if (e.captcha_sitekey) { - const capt = document.getElementById("h-captcha"); - if (!capt.children.length) { - const capty = document.createElement("div"); - capty.classList.add("h-captcha"); - capty.setAttribute("data-sitekey", e.captcha_sitekey); - const script = document.createElement("script"); - script.src = "https://js.hcaptcha.com/1/api.js"; - capt.append(script); - capt.append(capty); - } - else { - eval("hcaptcha.reset()"); - } - return; - } - if (!e.token) { - console.log(e); - if (e.errors.consent) { - error(elements[6], e.errors.consent._errors[0].message); - } - else if (e.errors.password) { - error(elements[3], "Password: " + e.errors.password._errors[0].message); - } - else if (e.errors.username) { - error(elements[2], "Username: " + e.errors.username._errors[0].message); - } - else if (e.errors.email) { - error(elements[1], "Email: " + e.errors.email._errors[0].message); - } - else if (e.errors.date_of_birth) { - error(elements[5], "Date of Birth: " + e.errors.date_of_birth._errors[0].message); - } - else { - document.getElementById("wrong").textContent = e.errors[Object.keys(e.errors)[0]]._errors[0].message; - } - } - else { - adduser({ serverurls: JSON.parse(localStorage.getItem("instanceinfo")), email, token: e.token }).username = username; - localStorage.setItem("token", e.token); - const redir = new URLSearchParams(window.location.search).get("goback"); - if (redir) { - window.location.href = redir; - } - else { - window.location.href = "/channels/@me"; - } - } - }); - }); - //document.getElementById("wrong").textContent=h; - // console.log(h); -} -function error(e, message) { - const p = e.parentElement; - let element = p.getElementsByClassName("suberror")[0]; - if (!element) { - const div = document.createElement("div"); - div.classList.add("suberror", "suberrora"); - p.append(div); - element = div; - } - else { - element.classList.remove("suberror"); - setTimeout(_ => { - element.classList.add("suberror"); - }, 100); - } - element.textContent = message; -} -let TOSa = document.getElementById("TOSa"); -async function tosLogic() { - const apiurl = new URL(JSON.parse(localStorage.getItem("instanceinfo")).api); - const tosPage = (await (await fetch(apiurl.toString() + "/ping")).json()).instance.tosPage; - if (tosPage) { - document.getElementById("TOSbox").innerHTML = "I agree to the Terms of Service:"; - TOSa = document.getElementById("TOSa"); - TOSa.href = tosPage; - } - else { - document.getElementById("TOSbox").textContent = "This instance has no Terms of Service, accept ToS anyways:"; - TOSa = null; - } - console.log(tosPage); -} -tosLogic(); -checkInstance["alt"] = tosLogic; diff --git a/.dist/role.js b/.dist/role.js deleted file mode 100644 index f1c0e70..0000000 --- a/.dist/role.js +++ /dev/null @@ -1,160 +0,0 @@ -import { Permissions } from "./permissions.js"; -import { SnowFlake } from "./snowflake.js"; -class Role extends SnowFlake { - permissions; - owner; - color; - name; - info; - hoist; - icon; - mentionable; - unicode_emoji; - headers; - constructor(json, owner) { - super(json.id); - this.headers = owner.headers; - this.info = owner.info; - for (const thing of Object.keys(json)) { - if (thing === "id") { - continue; - } - this[thing] = json[thing]; - } - this.permissions = new Permissions(json.permissions); - this.owner = owner; - } - get guild() { - return this.owner; - } - get localuser() { - return this.guild.localuser; - } - getColor() { - if (this.color === 0) { - return null; - } - return `#${this.color.toString(16)}`; - } -} -export { Role }; -import { Options } from "./settings.js"; -class PermissionToggle { - rolejson; - permissions; - owner; - value; - constructor(roleJSON, permissions, owner) { - this.rolejson = roleJSON; - this.permissions = permissions; - this.owner = owner; - } - watchForChange() { } - generateHTML() { - const div = document.createElement("div"); - div.classList.add("setting"); - const name = document.createElement("span"); - name.textContent = this.rolejson.readableName; - name.classList.add("settingsname"); - div.append(name); - div.append(this.generateCheckbox()); - const p = document.createElement("p"); - p.textContent = this.rolejson.description; - div.appendChild(p); - return div; - } - generateCheckbox() { - const div = document.createElement("div"); - div.classList.add("tritoggle"); - const state = this.permissions.getPermission(this.rolejson.name); - const on = document.createElement("input"); - on.type = "radio"; - on.name = this.rolejson.name; - div.append(on); - if (state === 1) { - on.checked = true; - } - on.onclick = _ => { - this.permissions.setPermission(this.rolejson.name, 1); - this.owner.changed(); - }; - const no = document.createElement("input"); - no.type = "radio"; - no.name = this.rolejson.name; - div.append(no); - if (state === 0) { - no.checked = true; - } - no.onclick = _ => { - this.permissions.setPermission(this.rolejson.name, 0); - this.owner.changed(); - }; - if (this.permissions.hasDeny) { - const off = document.createElement("input"); - off.type = "radio"; - off.name = this.rolejson.name; - div.append(off); - if (state === -1) { - off.checked = true; - } - off.onclick = _ => { - this.permissions.setPermission(this.rolejson.name, -1); - this.owner.changed(); - }; - } - return div; - } - submit() { - } -} -import { Buttons } from "./settings.js"; -class RoleList extends Buttons { - permissions; - permission; - guild; - channel; - options; - onchange; - curid; - constructor(permissions, guild, onchange, channel = false) { - super("Roles"); - this.guild = guild; - this.permissions = permissions; - this.channel = channel; - this.onchange = onchange; - const options = new Options("", this); - if (channel) { - this.permission = new Permissions("0", "0"); - } - else { - this.permission = new Permissions("0"); - } - for (const thing of Permissions.info) { - options.options.push(new PermissionToggle(thing, this.permission, options)); - } - for (const i of permissions) { - console.log(i); - this.buttons.push([i[0].name, i[0].id]); - } - this.options = options; - } - handleString(str) { - this.curid = str; - const arr = this.permissions.find(_ => _[0].id === str); - if (arr) { - const perm = arr[1]; - this.permission.deny = perm.deny; - this.permission.allow = perm.allow; - const role = this.permissions.find(e => e[0].id === str); - if (role) { - this.options.name = role[0].name; - this.options.haschanged = false; - } - } - return this.options.generateHTML(); - } - save() { - this.onchange(this.curid, this.permission); - } -} -export { RoleList }; diff --git a/.dist/service.js b/.dist/service.js deleted file mode 100644 index 335d72a..0000000 --- a/.dist/service.js +++ /dev/null @@ -1,94 +0,0 @@ -function deleteoldcache() { - caches.delete("cache"); - console.log("this ran :P"); -} -async function putInCache(request, response) { - console.log(request, response); - const cache = await caches.open("cache"); - console.log("Grabbed"); - try { - console.log(await cache.put(request, response)); - } - catch (error) { - console.error(error); - } -} -console.log("test"); -let lastcache; -self.addEventListener("activate", async (event) => { - console.log("test2"); - checkCache(); -}); -async function checkCache() { - if (checkedrecently) { - return; - } - const promise = await caches.match("/getupdates"); - if (promise) { - lastcache = await promise.text(); - } - console.log(lastcache); - fetch("/getupdates").then(async (data) => { - const text = await data.clone().text(); - console.log(text, lastcache); - if (lastcache !== text) { - deleteoldcache(); - putInCache("/getupdates", data.clone()); - } - checkedrecently = true; - setTimeout(_ => { - checkedrecently = false; - }, 1000 * 60 * 30); - }); -} -var checkedrecently = false; -function samedomain(url) { - return new URL(url).origin === self.origin; -} -function isindexhtml(url) { - console.log(url); - if (new URL(url).pathname.startsWith("/channels")) { - return true; - } - return false; -} -async function getfile(event) { - checkCache(); - if (!samedomain(event.request.url)) { - return await fetch(event.request.clone()); - } - const responseFromCache = await caches.match(event.request.url); - console.log(responseFromCache, caches); - if (responseFromCache) { - console.log("cache hit"); - return responseFromCache; - } - if (isindexhtml(event.request.url)) { - console.log("is index.html"); - const responseFromCache = await caches.match("/index.html"); - if (responseFromCache) { - console.log("cache hit"); - return responseFromCache; - } - const responseFromNetwork = await fetch("/index.html"); - await putInCache("/index.html", responseFromNetwork.clone()); - return responseFromNetwork; - } - const responseFromNetwork = await fetch(event.request.clone()); - console.log(event.request.clone()); - await putInCache(event.request.clone(), responseFromNetwork.clone()); - try { - return responseFromNetwork; - } - catch (e) { - console.error(e); - } -} -self.addEventListener("fetch", (event) => { - try { - event.respondWith(getfile(event)); - } - catch (e) { - console.error(e); - } -}); diff --git a/.dist/settings.js b/.dist/settings.js deleted file mode 100644 index f92c485..0000000 --- a/.dist/settings.js +++ /dev/null @@ -1,937 +0,0 @@ -//future me stuff -class Buttons { - name; - buttons; - buttonList; - warndiv; - value; - constructor(name) { - this.buttons = []; - this.name = name; - } - add(name, thing) { - if (!thing) { - thing = new Options(name, this); - } - this.buttons.push([name, thing]); - return thing; - } - generateHTML() { - const buttonList = document.createElement("div"); - buttonList.classList.add("Buttons"); - buttonList.classList.add("flexltr"); - this.buttonList = buttonList; - const htmlarea = document.createElement("div"); - htmlarea.classList.add("flexgrow"); - const buttonTable = document.createElement("div"); - buttonTable.classList.add("flexttb", "settingbuttons"); - for (const thing of this.buttons) { - const button = document.createElement("button"); - button.classList.add("SettingsButton"); - button.textContent = thing[0]; - button.onclick = _ => { - this.generateHTMLArea(thing[1], htmlarea); - if (this.warndiv) { - this.warndiv.remove(); - } - }; - buttonTable.append(button); - } - this.generateHTMLArea(this.buttons[0][1], htmlarea); - buttonList.append(buttonTable); - buttonList.append(htmlarea); - return buttonList; - } - handleString(str) { - const div = document.createElement("span"); - div.textContent = str; - return div; - } - generateHTMLArea(buttonInfo, htmlarea) { - let html; - if (buttonInfo instanceof Options) { - buttonInfo.subOptions = undefined; - html = buttonInfo.generateHTML(); - } - else { - html = this.handleString(buttonInfo); - } - htmlarea.innerHTML = ""; - htmlarea.append(html); - } - changed(html) { - this.warndiv = html; - this.buttonList.append(html); - } - watchForChange() { } - save() { } - submit() { - } -} -class TextInput { - label; - owner; - onSubmit; - value; - input; - password; - constructor(label, onSubmit, owner, { initText = "", password = false } = {}) { - this.label = label; - this.value = initText; - this.owner = owner; - this.onSubmit = onSubmit; - this.password = password; - } - generateHTML() { - const div = document.createElement("div"); - const span = document.createElement("span"); - span.textContent = this.label; - div.append(span); - const input = document.createElement("input"); - input.value = this.value; - input.type = this.password ? "password" : "text"; - input.oninput = this.onChange.bind(this); - this.input = new WeakRef(input); - div.append(input); - return div; - } - onChange(ev) { - this.owner.changed(); - const input = this.input.deref(); - if (input) { - const value = input.value; - this.onchange(value); - this.value = value; - } - } - onchange = _ => { }; - watchForChange(func) { - this.onchange = func; - } - submit() { - this.onSubmit(this.value); - } -} -class SettingsText { - onSubmit; - value; - text; - constructor(text) { - this.text = text; - } - generateHTML() { - const span = document.createElement("span"); - span.innerText = this.text; - return span; - } - watchForChange() { } - submit() { } -} -class SettingsTitle { - onSubmit; - value; - text; - constructor(text) { - this.text = text; - } - generateHTML() { - const span = document.createElement("h2"); - span.innerText = this.text; - return span; - } - watchForChange() { } - submit() { } -} -class CheckboxInput { - label; - owner; - onSubmit; - value; - input; - constructor(label, onSubmit, owner, { initState = false } = {}) { - this.label = label; - this.value = initState; - this.owner = owner; - this.onSubmit = onSubmit; - } - generateHTML() { - const div = document.createElement("div"); - const span = document.createElement("span"); - span.textContent = this.label; - div.append(span); - const input = document.createElement("input"); - input.type = "checkbox"; - input.checked = this.value; - input.oninput = this.onChange.bind(this); - this.input = new WeakRef(input); - div.append(input); - return div; - } - onChange(ev) { - this.owner.changed(); - const input = this.input.deref(); - if (input) { - const value = input.checked; - this.onchange(value); - this.value = value; - } - } - onchange = _ => { }; - watchForChange(func) { - this.onchange = func; - } - submit() { - this.onSubmit(this.value); - } -} -class ButtonInput { - label; - owner; - onClick; - textContent; - value; - constructor(label, textContent, onClick, owner, {} = {}) { - this.label = label; - this.owner = owner; - this.onClick = onClick; - this.textContent = textContent; - } - generateHTML() { - const div = document.createElement("div"); - const span = document.createElement("span"); - span.textContent = this.label; - div.append(span); - const button = document.createElement("button"); - button.textContent = this.textContent; - button.onclick = this.onClickEvent.bind(this); - div.append(button); - return div; - } - onClickEvent(ev) { - this.onClick(); - } - watchForChange() { } - submit() { } -} -class ColorInput { - label; - owner; - onSubmit; - colorContent; - input; - value; - constructor(label, onSubmit, owner, { initColor = "" } = {}) { - this.label = label; - this.colorContent = initColor; - this.owner = owner; - this.onSubmit = onSubmit; - } - generateHTML() { - const div = document.createElement("div"); - const span = document.createElement("span"); - span.textContent = this.label; - div.append(span); - const input = document.createElement("input"); - input.value = this.colorContent; - input.type = "color"; - input.oninput = this.onChange.bind(this); - this.input = new WeakRef(input); - div.append(input); - return div; - } - onChange(ev) { - this.owner.changed(); - const input = this.input.deref(); - if (input) { - const value = input.value; - this.value = value; - this.onchange(value); - this.colorContent = value; - } - } - onchange = _ => { }; - watchForChange(func) { - this.onchange = func; - } - submit() { - this.onSubmit(this.colorContent); - } -} -class SelectInput { - label; - owner; - onSubmit; - options; - index; - select; - get value() { - return this.index; - } - constructor(label, onSubmit, options, owner, { defaultIndex = 0 } = {}) { - this.label = label; - this.index = defaultIndex; - this.owner = owner; - this.onSubmit = onSubmit; - this.options = options; - } - generateHTML() { - const div = document.createElement("div"); - const span = document.createElement("span"); - span.textContent = this.label; - div.append(span); - const select = document.createElement("select"); - select.onchange = this.onChange.bind(this); - for (const thing of this.options) { - const option = document.createElement("option"); - option.textContent = thing; - select.appendChild(option); - } - this.select = new WeakRef(select); - select.selectedIndex = this.index; - div.append(select); - return div; - } - onChange(ev) { - this.owner.changed(); - const select = this.select.deref(); - if (select) { - const value = select.selectedIndex; - this.onchange(value); - this.index = value; - } - } - onchange = _ => { }; - watchForChange(func) { - this.onchange = func; - } - submit() { - this.onSubmit(this.index); - } -} -class MDInput { - label; - owner; - onSubmit; - value; - input; - constructor(label, onSubmit, owner, { initText = "" } = {}) { - this.label = label; - this.value = initText; - this.owner = owner; - this.onSubmit = onSubmit; - } - generateHTML() { - const div = document.createElement("div"); - const span = document.createElement("span"); - span.textContent = this.label; - div.append(span); - div.append(document.createElement("br")); - const input = document.createElement("textarea"); - input.value = this.value; - input.oninput = this.onChange.bind(this); - this.input = new WeakRef(input); - div.append(input); - return div; - } - onChange(ev) { - this.owner.changed(); - const input = this.input.deref(); - if (input) { - const value = input.value; - this.onchange(value); - this.value = value; - } - } - onchange = _ => { }; - watchForChange(func) { - this.onchange = func; - } - submit() { - this.onSubmit(this.value); - } -} -class FileInput { - label; - owner; - onSubmit; - input; - value; - clear; - constructor(label, onSubmit, owner, { clear = false } = {}) { - this.label = label; - this.owner = owner; - this.onSubmit = onSubmit; - this.clear = clear; - } - generateHTML() { - const div = document.createElement("div"); - const span = document.createElement("span"); - span.textContent = this.label; - div.append(span); - const input = document.createElement("input"); - input.type = "file"; - input.oninput = this.onChange.bind(this); - this.input = new WeakRef(input); - div.append(input); - if (this.clear) { - const button = document.createElement("button"); - button.textContent = "Clear"; - button.onclick = _ => { - if (this.onchange) { - this.onchange(null); - } - this.value = null; - this.owner.changed(); - }; - div.append(button); - } - return div; - } - onChange(ev) { - this.owner.changed(); - const input = this.input.deref(); - if (input) { - this.value = input.files; - if (this.onchange) { - this.onchange(input.files); - } - } - } - onchange = null; - watchForChange(func) { - this.onchange = func; - } - submit() { - const input = this.input.deref(); - if (input) { - this.onSubmit(input.files); - } - } -} -class HtmlArea { - submit; - html; - value; - constructor(html, submit) { - this.submit = submit; - this.html = html; - } - generateHTML() { - if (this.html instanceof Function) { - return this.html(); - } - else { - return this.html; - } - } - watchForChange() { } -} -class Options { - name; - haschanged = false; - options; - owner; - ltr; - value; - html = new WeakMap(); - container = new WeakRef(document.createElement("div")); - constructor(name, owner, { ltr = false } = {}) { - this.name = name; - this.options = []; - this.owner = owner; - this.ltr = ltr; - } - removeAll() { - while (this.options.length) { - this.options.pop(); - } - const container = this.container.deref(); - if (container) { - container.innerHTML = ""; - } - } - watchForChange() { } - addOptions(name, { ltr = false } = {}) { - const options = new Options(name, this, { ltr }); - this.options.push(options); - this.generate(options); - return options; - } - subOptions; - addSubOptions(name, { ltr = false } = {}) { - const options = new Options(name, this, { ltr }); - this.subOptions = options; - const container = this.container.deref(); - if (container) { - this.generateContainter(); - } - else { - throw new Error("Tried to make a subOptions when the options weren't rendered"); - } - return options; - } - addSubForm(name, onSubmit, { ltr = false, submitText = "Submit", fetchURL = "", headers = {}, method = "POST", traditionalSubmit = false } = {}) { - const options = new Form(name, this, onSubmit, { ltr, submitText, fetchURL, headers, method, traditionalSubmit }); - this.subOptions = options; - const container = this.container.deref(); - if (container) { - this.generateContainter(); - } - else { - throw new Error("Tried to make a subForm when the options weren't rendered"); - } - return options; - } - returnFromSub() { - this.subOptions = undefined; - this.generateContainter(); - } - addSelect(label, onSubmit, selections, { defaultIndex = 0 } = {}) { - const select = new SelectInput(label, onSubmit, selections, this, { defaultIndex }); - this.options.push(select); - this.generate(select); - return select; - } - addFileInput(label, onSubmit, { clear = false } = {}) { - const FI = new FileInput(label, onSubmit, this, { clear }); - this.options.push(FI); - this.generate(FI); - return FI; - } - addTextInput(label, onSubmit, { initText = "", password = false } = {}) { - const textInput = new TextInput(label, onSubmit, this, { initText, password }); - this.options.push(textInput); - this.generate(textInput); - return textInput; - } - addColorInput(label, onSubmit, { initColor = "" } = {}) { - const colorInput = new ColorInput(label, onSubmit, this, { initColor }); - this.options.push(colorInput); - this.generate(colorInput); - return colorInput; - } - addMDInput(label, onSubmit, { initText = "" } = {}) { - const mdInput = new MDInput(label, onSubmit, this, { initText }); - this.options.push(mdInput); - this.generate(mdInput); - return mdInput; - } - addHTMLArea(html, submit = () => { }) { - const htmlarea = new HtmlArea(html, submit); - this.options.push(htmlarea); - this.generate(htmlarea); - return htmlarea; - } - addButtonInput(label, textContent, onSubmit) { - const button = new ButtonInput(label, textContent, onSubmit, this); - this.options.push(button); - this.generate(button); - return button; - } - addCheckboxInput(label, onSubmit, { initState = false } = {}) { - const box = new CheckboxInput(label, onSubmit, this, { initState }); - this.options.push(box); - this.generate(box); - return box; - } - addText(str) { - const text = new SettingsText(str); - this.options.push(text); - this.generate(text); - return text; - } - addTitle(str) { - const text = new SettingsTitle(str); - this.options.push(text); - this.generate(text); - return text; - } - addForm(name, onSubmit, { ltr = false, submitText = "Submit", fetchURL = "", headers = {}, method = "POST", traditionalSubmit = false } = {}) { - const options = new Form(name, this, onSubmit, { ltr, submitText, fetchURL, headers, method, traditionalSubmit }); - this.options.push(options); - this.generate(options); - return options; - } - generate(elm) { - const container = this.container.deref(); - if (container) { - const div = document.createElement("div"); - if (!(elm instanceof Options)) { - div.classList.add("optionElement"); - } - const html = elm.generateHTML(); - div.append(html); - this.html.set(elm, new WeakRef(div)); - container.append(div); - } - } - title = new WeakRef(document.createElement("h2")); - generateHTML() { - const div = document.createElement("div"); - div.classList.add("titlediv"); - const title = document.createElement("h2"); - title.textContent = this.name; - div.append(title); - if (this.name !== "") - title.classList.add("settingstitle"); - this.title = new WeakRef(title); - const container = document.createElement("div"); - this.container = new WeakRef(container); - container.classList.add(this.ltr ? "flexltr" : "flexttb", "flexspace"); - this.generateContainter(); - div.append(container); - return div; - } - generateContainter() { - const container = this.container.deref(); - if (container) { - const title = this.title.deref(); - if (title) - title.innerHTML = ""; - container.innerHTML = ""; - if (this.subOptions) { - container.append(this.subOptions.generateHTML()); //more code needed, though this is enough for now - if (title) { - const name = document.createElement("span"); - name.innerText = this.name; - name.classList.add("clickable"); - name.onclick = () => { - this.returnFromSub(); - }; - title.append(name, " > ", this.subOptions.name); - } - } - else { - for (const thing of this.options) { - this.generate(thing); - } - if (title) { - title.innerText = this.name; - } - } - if (title && title.innerText !== "") { - title.classList.add("settingstitle"); - } - else if (title) { - title.classList.remove("settingstitle"); - } - } - else { - console.warn("tried to generate container, but it did not exist"); - } - } - changed() { - if (this.owner instanceof Options || this.owner instanceof Form) { - this.owner.changed(); - return; - } - if (!this.haschanged) { - const div = document.createElement("div"); - div.classList.add("flexltr", "savediv"); - const span = document.createElement("span"); - div.append(span); - span.textContent = "Careful, you have unsaved changes"; - const button = document.createElement("button"); - button.textContent = "Save changes"; - div.append(button); - this.haschanged = true; - this.owner.changed(div); - button.onclick = _ => { - if (this.owner instanceof Buttons) { - this.owner.save(); - } - div.remove(); - this.submit(); - }; - } - } - submit() { - this.haschanged = false; - for (const thing of this.options) { - thing.submit(); - } - } -} -class FormError extends Error { - elem; - message; - constructor(elem, message) { - super(message); - this.message = message; - this.elem = elem; - } -} -export { FormError }; -class Form { - name; - options; - owner; - ltr; - names = new Map(); - required = new WeakSet(); - submitText; - fetchURL; - headers = {}; - method; - value; - traditionalSubmit; - values = {}; - constructor(name, owner, onSubmit, { ltr = false, submitText = "Submit", fetchURL = "", headers = {}, method = "POST", traditionalSubmit = false } = {}) { - this.traditionalSubmit = traditionalSubmit; - this.name = name; - this.method = method; - this.submitText = submitText; - this.options = new Options("", this, { ltr }); - this.owner = owner; - this.fetchURL = fetchURL; - this.headers = headers; - this.ltr = ltr; - this.onSubmit = onSubmit; - } - setValue(key, value) { - this.values[key] = value; - } - addSelect(label, formName, selections, { defaultIndex = 0, required = false } = {}) { - const select = this.options.addSelect(label, _ => { }, selections, { defaultIndex }); - this.names.set(formName, select); - if (required) { - this.required.add(select); - } - return select; - } - fileOptions = new Map(); - addFileInput(label, formName, { required = false, files = "one", clear = false } = {}) { - const FI = this.options.addFileInput(label, _ => { }, { clear }); - if (files !== "one" && files !== "multi") - throw new Error("files should equal one or multi"); - this.fileOptions.set(FI, { files }); - this.names.set(formName, FI); - if (required) { - this.required.add(FI); - } - return FI; - } - addTextInput(label, formName, { initText = "", required = false, password = false } = {}) { - const textInput = this.options.addTextInput(label, _ => { }, { initText, password }); - this.names.set(formName, textInput); - if (required) { - this.required.add(textInput); - } - return textInput; - } - addColorInput(label, formName, { initColor = "", required = false } = {}) { - const colorInput = this.options.addColorInput(label, _ => { }, { initColor }); - this.names.set(formName, colorInput); - if (required) { - this.required.add(colorInput); - } - return colorInput; - } - addMDInput(label, formName, { initText = "", required = false } = {}) { - const mdInput = this.options.addMDInput(label, _ => { }, { initText }); - this.names.set(formName, mdInput); - if (required) { - this.required.add(mdInput); - } - return mdInput; - } - addCheckboxInput(label, formName, { initState = false, required = false } = {}) { - const box = this.options.addCheckboxInput(label, _ => { }, { initState }); - this.names.set(formName, box); - if (required) { - this.required.add(box); - } - return box; - } - addText(str) { - this.options.addText(str); - } - addTitle(str) { - this.options.addTitle(str); - } - generateHTML() { - const div = document.createElement("div"); - div.append(this.options.generateHTML()); - div.classList.add("FormSettings"); - if (!this.traditionalSubmit) { - const button = document.createElement("button"); - button.onclick = _ => { - this.submit(); - }; - button.textContent = this.submitText; - div.append(button); - } - return div; - } - onSubmit; - watchForChange(func) { - this.onSubmit = func; - } - changed() { - if (this.traditionalSubmit) { - this.owner.changed(); - } - } - async submit() { - const build = {}; - for (const key of Object.keys(this.values)) { - const thing = this.values[key]; - if (thing instanceof Function) { - try { - build[key] = thing(); - } - catch (e) { - if (e instanceof FormError) { - const elm = this.options.html.get(e.elem); - if (elm) { - const html = elm.deref(); - if (html) { - this.makeError(html, e.message); - } - } - } - return; - } - } - else { - build[key] = thing; - } - } - const promises = []; - for (const thing of this.names.keys()) { - if (thing === "") - continue; - const input = this.names.get(thing); - if (input instanceof SelectInput) { - build[thing] = input.options[input.value]; - continue; - } - else if (input instanceof FileInput) { - const options = this.fileOptions.get(input); - if (!options) { - throw new Error("FileInput without its options is in this form, this should never happen."); - } - if (options.files === "one") { - console.log(input.value); - if (input.value) { - const reader = new FileReader(); - reader.readAsDataURL(input.value[0]); - const promise = new Promise((res) => { - reader.onload = () => { - build[thing] = reader.result; - res(); - }; - }); - promises.push(promise); - } - } - else { - console.error(options.files + " is not currently implemented"); - } - } - build[thing] = input.value; - } - await Promise.allSettled(promises); - if (this.fetchURL !== "") { - fetch(this.fetchURL, { - method: this.method, - body: JSON.stringify(build), - headers: this.headers - }).then(_ => _.json()).then(json => { - if (json.errors && this.errors(json.errors)) - return; - this.onSubmit(json); - }); - } - else { - this.onSubmit(build); - } - console.warn("needs to be implemented"); - } - errors(errors) { - if (!(errors instanceof Object)) { - return; - } - for (const error of Object.keys(errors)) { - const elm = this.names.get(error); - if (elm) { - const ref = this.options.html.get(elm); - if (ref && ref.deref()) { - const html = ref.deref(); - this.makeError(html, errors[error]._errors[0].message); - return true; - } - } - } - return false; - } - error(formElm, errorMessage) { - const elm = this.names.get(formElm); - if (elm) { - const htmlref = this.options.html.get(elm); - if (htmlref) { - const html = htmlref.deref(); - if (html) { - this.makeError(html, errorMessage); - } - } - } - else { - console.warn(formElm + " is not a valid form property"); - } - } - makeError(e, message) { - let element = e.getElementsByClassName("suberror")[0]; - if (!element) { - const div = document.createElement("div"); - div.classList.add("suberror", "suberrora"); - e.append(div); - element = div; - } - else { - element.classList.remove("suberror"); - setTimeout(_ => { - element.classList.add("suberror"); - }, 100); - } - element.textContent = message; - } -} -class Settings extends Buttons { - static Buttons = Buttons; - static Options = Options; - html; - constructor(name) { - super(name); - } - addButton(name, { ltr = false } = {}) { - const options = new Options(name, this, { ltr }); - this.add(name, options); - return options; - } - show() { - const background = document.createElement("div"); - background.classList.add("background"); - const title = document.createElement("h2"); - title.textContent = this.name; - title.classList.add("settingstitle"); - background.append(title); - background.append(this.generateHTML()); - const exit = document.createElement("span"); - exit.textContent = "✖"; - exit.classList.add("exitsettings"); - background.append(exit); - exit.onclick = _ => { - this.hide(); - }; - document.body.append(background); - this.html = background; - } - hide() { - if (this.html) { - this.html.remove(); - this.html = null; - } - } -} -export { Settings, Buttons, Options }; diff --git a/.dist/snowflake.js b/.dist/snowflake.js deleted file mode 100644 index ff5bf9c..0000000 --- a/.dist/snowflake.js +++ /dev/null @@ -1,19 +0,0 @@ -class SnowFlake { - id; - constructor(id) { - this.id = id; - } - getUnixTime() { - return SnowFlake.stringToUnixTime(this.id); - } - static stringToUnixTime(str) { - try { - return Number((BigInt(str) >> 22n) + 1420070400000n); - } - catch { - console.error(`The ID is corrupted, it's ${str} when it should be some number.`); - return 0; - } - } -} -export { SnowFlake }; diff --git a/.dist/user.js b/.dist/user.js deleted file mode 100644 index 051f2c7..0000000 --- a/.dist/user.js +++ /dev/null @@ -1,431 +0,0 @@ -//const usercache={}; -import { Member } from "./member.js"; -import { MarkDown } from "./markdown.js"; -import { Contextmenu } from "./contextmenu.js"; -import { SnowFlake } from "./snowflake.js"; -class User extends SnowFlake { - owner; - hypotheticalpfp; - avatar; - username; - nickname = null; - relationshipType = 0; - bio; - discriminator; - pronouns; - bot; - public_flags; - accent_color; - banner; - hypotheticalbanner; - premium_since; - premium_type; - theme_colors; - badge_ids; - members = new WeakMap(); - status; - clone() { - return new User({ - username: this.username, - id: this.id + "#clone", - public_flags: this.public_flags, - discriminator: this.discriminator, - avatar: this.avatar, - accent_color: this.accent_color, - banner: this.banner, - bio: this.bio.rawString, - premium_since: this.premium_since, - premium_type: this.premium_type, - bot: this.bot, - theme_colors: this.theme_colors, - pronouns: this.pronouns, - badge_ids: this.badge_ids - }, this.owner); - } - getPresence(presence) { - if (presence) { - this.setstatus(presence.status); - } - else { - this.setstatus("offline"); - } - } - setstatus(status) { - this.status = status; - } - async getStatus() { - if (this.status) { - return this.status; - } - else { - return "offline"; - } - } - static contextmenu = new Contextmenu("User Menu"); - static setUpContextMenu() { - this.contextmenu.addbutton("Copy user id", function () { - navigator.clipboard.writeText(this.id); - }); - this.contextmenu.addbutton("Message user", function () { - fetch(this.info.api + "/users/@me/channels", { method: "POST", - body: JSON.stringify({ recipients: [this.id] }), - headers: this.localuser.headers - }).then(_ => _.json()).then(json => { - this.localuser.goToChannel(json.id); - }); - }); - this.contextmenu.addbutton("Block user", function () { - this.block(); - }, null, function () { - return this.relationshipType !== 2; - }); - this.contextmenu.addbutton("Unblock user", function () { - this.unblock(); - }, null, function () { - return this.relationshipType === 2; - }); - this.contextmenu.addbutton("Friend request", function () { - fetch(`${this.info.api}/users/@me/relationships/${this.id}`, { - method: "PUT", - headers: this.owner.headers, - body: JSON.stringify({ - type: 1 - }) - }); - }); - this.contextmenu.addbutton("Kick member", function (member) { - member.kick(); - }, null, member => { - if (!member) - return false; - const us = member.guild.member; - if (member.id === us.id) { - return false; - } - if (member.id === member.guild.properties.owner_id) { - return false; - } - return (us.hasPermission("KICK_MEMBERS")) || false; - }); - this.contextmenu.addbutton("Ban member", function (member) { - member.ban(); - }, null, member => { - if (!member) - return false; - const us = member.guild.member; - if (member.id === us.id) { - return false; - } - if (member.id === member.guild.properties.owner_id) { - return false; - } - return (us.hasPermission("BAN_MEMBERS")) || false; - }); - } - static checkuser(user, owner) { - if (owner.userMap.has(user.id)) { - return owner.userMap.get(user.id); - } - else { - const tempuser = new User(user, owner, true); - owner.userMap.set(user.id, tempuser); - return tempuser; - } - } - get info() { - return this.owner.info; - } - get localuser() { - return this.owner; - } - get name() { - return this.username; - } - constructor(userjson, owner, dontclone = false) { - super(userjson.id); - this.owner = owner; - if (!owner) { - console.error("missing localuser"); - } - if (dontclone) { - for (const thing of Object.keys(userjson)) { - if (thing === "bio") { - this.bio = new MarkDown(userjson[thing], this.localuser); - continue; - } - if (thing === "id") { - continue; - } - this[thing] = userjson[thing]; - } - this.hypotheticalpfp = false; - } - else { - return User.checkuser(userjson, owner); - } - } - async resolvemember(guild) { - return await Member.resolveMember(this, guild); - } - async getUserProfile() { - return (await fetch(`${this.info.api}/users/${this.id.replace("#clone", "")}/profile?with_mutual_guilds=true&with_mutual_friends=true`, { - headers: this.localuser.headers - })).json(); - } - resolving = false; - async getBadge(id) { - if (this.localuser.badges.has(id)) { - return this.localuser.badges.get(id); - } - else { - if (this.resolving) { - await this.resolving; - return this.localuser.badges.get(id); - } - const prom = await this.getUserProfile(); - this.resolving = prom; - const badges = prom.badges; - this.resolving = false; - for (const thing of badges) { - this.localuser.badges.set(thing.id, thing); - } - return this.localuser.badges.get(id); - } - } - buildpfp() { - const pfp = document.createElement("img"); - pfp.loading = "lazy"; - pfp.src = this.getpfpsrc(); - pfp.classList.add("pfp"); - pfp.classList.add("userid:" + this.id); - return pfp; - } - async buildstatuspfp() { - const div = document.createElement("div"); - div.style.position = "relative"; - const pfp = this.buildpfp(); - div.append(pfp); - { - const status = document.createElement("div"); - status.classList.add("statusDiv"); - switch (await this.getStatus()) { - case "offline": - status.classList.add("offlinestatus"); - break; - case "online": - default: - status.classList.add("onlinestatus"); - break; - } - div.append(status); - } - return div; - } - userupdate(json) { - if (json.avatar !== this.avatar) { - console.log; - this.changepfp(json.avatar); - } - } - bind(html, guild = null, error = true) { - if (guild && guild.id !== "@me") { - Member.resolveMember(this, guild).then(_ => { - User.contextmenu.bindContextmenu(html, this, _); - if (_ === undefined && error) { - const error = document.createElement("span"); - error.textContent = "!"; - error.classList.add("membererror"); - html.after(error); - return; - } - if (_) { - _.bind(html); - } - }).catch(_ => { - console.log(_); - }); - } - if (guild) { - this.profileclick(html, guild); - } - else { - this.profileclick(html); - } - } - static async resolve(id, localuser) { - const json = await fetch(localuser.info.api.toString() + "/users/" + id + "/profile", { headers: localuser.headers }).then(_ => _.json()); - return new User(json, localuser); - } - changepfp(update) { - this.avatar = update; - this.hypotheticalpfp = false; - const src = this.getpfpsrc(); - console.log(src); - for (const thing of document.getElementsByClassName("userid:" + this.id)) { - thing.src = src; - } - } - block() { - fetch(`${this.info.api}/users/@me/relationships/${this.id}`, { - method: "PUT", - headers: this.owner.headers, - body: JSON.stringify({ - type: 2 - }) - }); - this.relationshipType = 2; - const channel = this.localuser.channelfocus; - if (channel) { - for (const thing of channel.messages) { - thing[1].generateMessage(); - } - } - } - unblock() { - fetch(`${this.info.api}/users/@me/relationships/${this.id}`, { - method: "DELETE", - headers: this.owner.headers, - }); - this.relationshipType = 0; - const channel = this.localuser.channelfocus; - if (channel) { - for (const thing of channel.messages) { - thing[1].generateMessage(); - } - } - } - getpfpsrc() { - if (this.hypotheticalpfp && this.avatar) { - return this.avatar; - } - if (this.avatar !== null) { - return this.info.cdn + "/avatars/" + this.id.replace("#clone", "") + "/" + this.avatar + ".png"; - } - else { - const int = new Number((BigInt(this.id.replace("#clone", "")) >> 22n) % 6n); - return this.info.cdn + `/embed/avatars/${int}.png`; - } - } - createjankpromises() { - new Promise(_ => { }); - } - async buildprofile(x, y, guild = null) { - if (Contextmenu.currentmenu != "") { - Contextmenu.currentmenu.remove(); - } - const div = document.createElement("div"); - if (this.accent_color) { - div.style.setProperty("--accent_color", "#" + this.accent_color.toString(16).padStart(6, "0")); - } - else { - div.style.setProperty("--accent_color", "transparent"); - } - if (this.banner) { - const banner = document.createElement("img"); - let src; - if (!this.hypotheticalbanner) { - src = this.info.cdn + "/avatars/" + this.id.replace("#clone", "") + "/" + this.banner + ".png"; - } - else { - src = this.banner; - } - console.log(src, this.banner); - banner.src = src; - banner.classList.add("banner"); - div.append(banner); - } - if (x !== -1) { - div.style.left = x + "px"; - div.style.top = y + "px"; - div.classList.add("profile", "flexttb"); - } - else { - this.setstatus("online"); - div.classList.add("hypoprofile", "flexttb"); - } - const badgediv = document.createElement("div"); - badgediv.classList.add("badges"); - (async () => { - if (!this.badge_ids) - return; - for (const id of this.badge_ids) { - const badgejson = await this.getBadge(id); - if (badgejson) { - const badge = document.createElement(badgejson.link ? "a" : "div"); - badge.classList.add("badge"); - const img = document.createElement("img"); - img.src = badgejson.icon; - badge.append(img); - const span = document.createElement("span"); - span.textContent = badgejson.description; - badge.append(span); - if (badge instanceof HTMLAnchorElement) { - badge.href = badgejson.link; - } - badgediv.append(badge); - } - } - })(); - { - const pfp = await this.buildstatuspfp(); - div.appendChild(pfp); - } - { - const userbody = document.createElement("div"); - userbody.classList.add("infosection"); - div.appendChild(userbody); - const usernamehtml = document.createElement("h2"); - usernamehtml.textContent = this.username; - userbody.appendChild(usernamehtml); - userbody.appendChild(badgediv); - const discrimatorhtml = document.createElement("h3"); - discrimatorhtml.classList.add("tag"); - discrimatorhtml.textContent = this.username + "#" + this.discriminator; - userbody.appendChild(discrimatorhtml); - const pronounshtml = document.createElement("p"); - pronounshtml.textContent = this.pronouns; - pronounshtml.classList.add("pronouns"); - userbody.appendChild(pronounshtml); - const rule = document.createElement("hr"); - userbody.appendChild(rule); - const biohtml = this.bio.makeHTML(); - userbody.appendChild(biohtml); - if (guild) { - Member.resolveMember(this, guild).then(member => { - if (!member) - return; - const roles = document.createElement("div"); - roles.classList.add("rolesbox"); - for (const role of member.roles) { - const div = document.createElement("div"); - div.classList.add("rolediv"); - const color = document.createElement("div"); - div.append(color); - color.style.setProperty("--role-color", "#" + role.color.toString(16).padStart(6, "0")); - color.classList.add("colorrolediv"); - const span = document.createElement("span"); - div.append(span); - span.textContent = role.name; - roles.append(div); - } - userbody.append(roles); - }); - } - } - console.log(div); - if (x !== -1) { - Contextmenu.currentmenu = div; - document.body.appendChild(div); - Contextmenu.keepOnScreen(div); - } - return div; - } - profileclick(obj, guild) { - obj.onclick = e => { - this.buildprofile(e.clientX, e.clientY, guild); - e.stopPropagation(); - }; - } -} -User.setUpContextMenu(); -export { User }; diff --git a/.gitignore b/.gitignore index 48a8706..e9f9a82 100644 --- a/.gitignore +++ b/.gitignore @@ -134,3 +134,5 @@ testAccount.json CC uptime.json .directory +.dist/ +bun.lockb \ No newline at end of file diff --git a/gulpfile.cjs b/gulpfile.cjs new file mode 100644 index 0000000..2e9f80f --- /dev/null +++ b/gulpfile.cjs @@ -0,0 +1,34 @@ +const gulp = require("gulp"); +const ts = require("gulp-typescript"); +const tsProject = ts.createProject("tsconfig.json"); + +// Task to compile TypeScript files +gulp.task("scripts", () => { + return tsProject.src().pipe(tsProject()).js.pipe(gulp.dest("dist")); +}); + +// Task to copy HTML files +gulp.task("copy-html", () => { + return gulp.src("src/**/*.html").pipe(gulp.dest("dist")); +}); + +// Task to copy other static assets (e.g., CSS, images) +gulp.task("copy-assets", () => { + return gulp + .src([ + "src/**/*.css", + "src/**/*.bin", + "src/**/*.ico", + "src/**/*.json", + "src/**/*.js", + "src/**/*.png", + "src/**/*.jpg", + "src/**/*.jpeg", + "src/**/*.gif", + "src/**/*.svg", + ]) + .pipe(gulp.dest("dist")); +}); + +// Default task to run all tasks +gulp.task("default", gulp.series("scripts", "copy-html", "copy-assets")); diff --git a/index.js b/index.js deleted file mode 100755 index 737a1d5..0000000 --- a/index.js +++ /dev/null @@ -1,197 +0,0 @@ -#! /usr/bin/env node -const compression = require("compression"); - -const express = require("express"); -const fs = require("node:fs"); -const app = express(); -const instances=require("./webpage/instances.json"); -const stats=require("./stats.js"); -const instancenames=new Map(); -for(const instance of instances){ - instancenames.set(instance.name,instance); -} -app.use(compression()); -fetch("https://raw.githubusercontent.com/spacebarchat/spacebarchat/master/instances/instances.json").then(_=>_.json()).then(json=>{ - for(const instance of json){ - if(!instancenames.has(instance.name)){ - instances.push(instance); - }else{ - const ofinst=instancenames.get(instance.name); - for(const key of Object.keys(instance)){ - if(!ofinst[key]){ - ofinst[key]=instance[key]; - } - } - } - } - stats.observe(instances); -}); - -app.use("/getupdates",(req, res)=>{ - const out=fs.statSync(`${__dirname}/webpage`); - res.send(out.mtimeMs+""); -}); -const debugging=true;//Do not turn this off, the service worker is all kinds of jank as is, it'll really mess your day up if you disable this -function isembed(str){ - return str.includes("discord")||str.includes("Spacebar"); -} -async function getapiurls(str){ - if(str.at(-1)!=="/"){ - str+="/"; - } - let api; - try{ - const info=await fetch(`${str}/.well-known/spacebar`).then(x=>x.json()); - api=info.api; - }catch{ - return false; - } - const url = new URL(api); - try{ - const info=await fetch(`${api}${url.pathname.includes("api") ? "" : "api"}/policies/instance/domains`).then(x=>x.json()); - return{ - api: info.apiEndpoint, - gateway: info.gateway, - cdn: info.cdn, - wellknown: str, - }; - }catch{ - return false; - } -} - -async function inviteres(req,res){ - let url; - if(URL.canParse(req.query.url)){ - url=new URL(req.query.url); - }else{ - const scheme = req.secure ? "https" : "http"; - const host=`${scheme}://${req.get("Host")}`; - url=new URL(host); - } - try{ - if(url.pathname.startsWith("invite")){ - throw-1; - } - const code=url.pathname.split("/")[2]; - let title=""; - let description=""; - let thumbnail=""; - const urls=await getapiurls(url.searchParams.get("instance")); - await fetch(`${urls.api}/invites/${code}`,{ - method: "GET" - }).then(_=>_.json()).then(json=>{ - title=json.guild.name; - if(json.inviter){ - description=json.inviter.username+" Has invited you to "+json.guild.name+(json.guild.description?json.guild.description+"\n":""); - }else{ - description="you've been invited to "+json.guild.name+(json.guild.description?json.guild.description+"\n":""); - } - if(json.guild.icon){ - thumbnail=`${urls.cdn}/icons/${json.guild.id}/${json.guild.icon}.png`; - } - }); - const json={ - type: "link", - version: "1.0", - title, - thumbnail, - description, - }; - res.send(JSON.stringify(json)); - }catch(e){ - console.error(e); - const json={ - type: "link", - version: "1.0", - title: "Jank Client", - thumbnail: "/logo.webp", - description: "A spacebar client that has DMs, replying and more", - url: url.toString() - }; - res.send(JSON.stringify(json)); - } -} -/* - function htmlEnc(s) {//https://stackoverflow.com/a/11561642 - return s.replaceAll(/&/g, '&') - .replaceAll(//g, '>') - .replaceAll(/'/g, ''') - .replaceAll(/"/g, '"'); - } - function strEscape(s){ - return JSON.stringify(s); - } - html= - ``+ - ``+ - ``+ - `${htmlEnc(title)}`+ - ``+ - ``+ - ``+ - ``+ - `` - res.type('html'); - res.send(html); - return true; - }catch(e){ - console.error(e); - } - return false; - */ - -app.use("/services/oembed", (req, res)=>{ - inviteres(req, res); -}); -app.use("/uptime",(req,res)=>{ - console.log(req.query.name); - const uptime=stats.uptime[req.query.name]; - console.log(req.query.name,uptime,stats.uptime); - res.send(uptime); -}); -app.use("/", async (req, res)=>{ - const scheme = req.secure ? "https" : "http"; - const host=`${scheme}://${req.get("Host")}`; - const ref=host+req.originalUrl; - if(host&&ref){ - const link=`${host}/services/oembed?url=${encodeURIComponent(ref)}`; - res.set("Link",`<${link}>; rel="alternate"; type="application/json+oembed"; title="Jank Client oEmbed format"`); - }else{ - console.log(req); - } - if(req.path==="/"){ - res.sendFile("./webpage/home.html", {root: __dirname}); - return; - } - if(debugging&&req.path.startsWith("/service.js")){ - res.send("dud"); - return; - } - if(req.path.startsWith("/instances.json")){ - res.send(JSON.stringify(instances)); - return; - } - if(req.path.startsWith("/invite/")){ - res.sendFile("./webpage/invite.html", {root: __dirname}); - return; - } - if(fs.existsSync(`${__dirname}/webpage${req.path}`)){ - res.sendFile(`./webpage${req.path}`, {root: __dirname}); - }else if(req.path.endsWith(".js") && fs.existsSync(`${__dirname}/.dist${req.path}`)){ - const dir=`./.dist${req.path}`; - res.sendFile(dir, {root: __dirname}); - }else if(fs.existsSync(`${__dirname}/webpage${req.path}.html`)){ - res.sendFile(`./webpage${req.path}.html`, {root: __dirname}); - }else{ - res.sendFile("./webpage/index.html", {root: __dirname}); - } -}); - - -const PORT = process.env.PORT || Number(process.argv[1]) || 8080; -app.listen(PORT, ()=>{}); -console.log("this ran :P"); - -exports.getapiurls=getapiurls; diff --git a/package-lock.json b/package-lock.json index 56b687f..0cf41b2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,8 @@ "version": "0.1.0", "license": "GPL-3.0", "dependencies": { + "@types/express": "^4.17.21", + "@types/node-fetch": "^2.6.11", "compression": "^1.7.4", "express": "^4.19.2", "ts-to-jsdoc": "^2.2.0" @@ -18,11 +20,15 @@ "@html-eslint/eslint-plugin": "^0.25.0", "@html-eslint/parser": "^0.25.0", "@stylistic/eslint-plugin": "^2.3.0", + "@types/compression": "^1.7.5", "@types/eslint__js": "^8.42.3", "eslint": "^8.57.0", "eslint-plugin-html": "^8.1.1", "eslint-plugin-sonarjs": "^1.0.4", "eslint-plugin-unicorn": "^55.0.0", + "gulp": "^5.0.0", + "gulp-copy": "^5.0.0", + "gulp-typescript": "^6.0.0-alpha.1", "ts-node": "^10.9.2", "typescript": "^5.5.4", "typescript-eslint": "^7.17.0" @@ -291,6 +297,29 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, + "node_modules/@gulpjs/messages": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@gulpjs/messages/-/messages-1.1.0.tgz", + "integrity": "sha512-Ys9sazDatyTgZVb4xPlDufLweJ/Os2uHWOv+Caxvy2O85JcnT4M3vc73bi8pdLWlv3fdWQz3pdI9tVwo8rQQSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/@gulpjs/to-absolute-glob": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@gulpjs/to-absolute-glob/-/to-absolute-glob-4.0.0.tgz", + "integrity": "sha512-kjotm7XJrJ6v+7knhPaRgaT6q8F8K2jiafwYdNHLzmV0uGLuZY43FK6smNSHUPrhq5kX2slCUy+RGG/xGqmIKA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-negated-glob": "^1.0.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, "node_modules/@html-eslint/eslint-plugin": { "version": "0.25.0", "resolved": "https://registry.npmjs.org/@html-eslint/eslint-plugin/-/eslint-plugin-0.25.0.tgz", @@ -577,6 +606,35 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/body-parser": { + "version": "1.19.5", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz", + "integrity": "sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==", + "license": "MIT", + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/compression": { + "version": "1.7.5", + "resolved": "https://registry.npmjs.org/@types/compression/-/compression-1.7.5.tgz", + "integrity": "sha512-AAQvK5pxMpaT+nDvhHrsBhLSYG5yQdtkaJE1WYieSNY2mVFKAgmU4ks65rkZD5oqnGCFLyQpUr1CqI4DmUMyDg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/express": "*" + } + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/eslint": { "version": "8.56.11", "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.56.11.tgz", @@ -602,29 +660,106 @@ "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==", "dev": true }, + "node_modules/@types/express": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.21.tgz", + "integrity": "sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==", + "license": "MIT", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^4.17.33", + "@types/qs": "*", + "@types/serve-static": "*" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "4.19.5", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.5.tgz", + "integrity": "sha512-y6W03tvrACO72aijJ5uF02FRq5cgDR9lUxddQ8vyF+GvmjJQqbzDcJngEjURc+ZsG31VI3hODNZJ2URj86pzmg==", + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/http-errors": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz", + "integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==", + "license": "MIT" + }, "node_modules/@types/json-schema": { "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", "dev": true }, + "node_modules/@types/mime": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", + "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", + "license": "MIT" + }, "node_modules/@types/node": { "version": "20.14.8", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.8.tgz", "integrity": "sha512-DO+2/jZinXfROG7j7WKFn/3C6nFwxy2lLpgLjEXJz+0XKphZlTLJ14mo8Vfg8X5BWN6XjyESXq+LcYdT7tR3bA==", - "dev": true, "license": "MIT", - "peer": true, "dependencies": { "undici-types": "~5.26.4" } }, + "node_modules/@types/node-fetch": { + "version": "2.6.11", + "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.11.tgz", + "integrity": "sha512-24xFj9R5+rfQJLRyM56qh+wnVSYhyXC2tkoBndtY0U+vubqNsYXGjufB2nn8Q6gt0LrARwL6UBtMCSVCwl4B1g==", + "license": "MIT", + "dependencies": { + "@types/node": "*", + "form-data": "^4.0.0" + } + }, "node_modules/@types/normalize-package-data": { "version": "2.4.4", "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.4.tgz", "integrity": "sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==", "dev": true }, + "node_modules/@types/qs": { + "version": "6.9.16", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.16.tgz", + "integrity": "sha512-7i+zxXdPD0T4cKDuxCUXJ4wHcsJLwENa6Z3dCu8cfCK743OGy5Nu1RmAGqDPsoTDINVEcdXKRvR/zre+P2Ku1A==", + "license": "MIT" + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "license": "MIT" + }, + "node_modules/@types/send": { + "version": "0.17.4", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz", + "integrity": "sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==", + "license": "MIT", + "dependencies": { + "@types/mime": "^1", + "@types/node": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "1.15.7", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.7.tgz", + "integrity": "sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==", + "license": "MIT", + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*", + "@types/send": "*" + } + }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "7.17.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.17.0.tgz", @@ -961,6 +1096,16 @@ "url": "https://github.com/sponsors/epoberezkin" } }, + "node_modules/ansi-colors": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", + "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/ansi-regex": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", @@ -985,6 +1130,56 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/ansi-wrap": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/ansi-wrap/-/ansi-wrap-0.1.0.tgz", + "integrity": "sha512-ZyznvL8k/FZeQHr2T6LzcJ/+vBApDnMNZvfVFy3At0knswWd6rJ3/0Hhmpu8oqa6C92npmozs890sX9Dl6q+Qw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/anymatch/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/append-buffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/append-buffer/-/append-buffer-1.0.2.tgz", + "integrity": "sha512-WLbYiXzD3y/ATLZFufV/rZvWdZOs+Z/+5v1rBZ463Jn398pa6kcde27cvozYnBoxXblGZTFfoPpsaEw0orU5BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-equal": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/arg": { "version": "4.1.3", "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", @@ -998,12 +1193,52 @@ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", "dev": true }, + "node_modules/arr-diff": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", + "integrity": "sha512-YVIQ82gZPGBebQV/a8dar4AitzCQs0jjXwMPZllpXMaGjXPYVUawSxQrRsjhjupyVxEvbHgUmIhKVlND+j02kA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/arr-union": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/arr-union/-/arr-union-3.1.0.tgz", + "integrity": "sha512-sKpyeERZ02v1FeCZT8lrfJq5u6goHCtpTAzPwJYe7c8SPFOboNjNg1vz2L4VTn9T4PQxEx13TbXLmYUcS6Ug7Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/array-each": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/array-each/-/array-each-1.0.1.tgz", + "integrity": "sha512-zHjL5SZa68hkKHBFBK6DJCTtr9sfTCPCaph/L7tMSLcTFgy+zX7E+6q5UArbtOtMBCtxdICpfTCspRse+ywyXA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/array-flatten": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", "license": "MIT" }, + "node_modules/array-slice": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/array-slice/-/array-slice-1.1.0.tgz", + "integrity": "sha512-B1qMD3RBP7O8o0H2KbrXDyB0IccejMF15+87Lvlor12ONPRHP6gTjXMNkt/d3ZuOGbAe66hFmaCfECI24Ufp6w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/array-union": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", @@ -1013,11 +1248,146 @@ "node": ">=8" } }, + "node_modules/assign-symbols": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz", + "integrity": "sha512-Q+JC7Whu8HhmTdBph/Tq59IoRtoy6KAm5zzPv00WdujX82lbAL8K7WVjne7vdCsAmbF4AYaDOPyO3k0kl8qIrw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/async-done": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/async-done/-/async-done-2.0.0.tgz", + "integrity": "sha512-j0s3bzYq9yKIVLKGE/tWlCpa3PfFLcrDZLTSVdnnCTGagXuXBJO4SsY9Xdk/fQBirCkH4evW5xOeJXqlAQFdsw==", + "dev": true, + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.4.4", + "once": "^1.4.0", + "stream-exhaust": "^1.0.2" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/async-settle": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/async-settle/-/async-settle-2.0.0.tgz", + "integrity": "sha512-Obu/KE8FurfQRN6ODdHN9LuXqwC+JFIM9NRyZqJJ4ZfLJmIYN9Rg0/kb+wF70VV5+fJusTMQlJ1t5rF7J/ETdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "async-done": "^2.0.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/b4a": { + "version": "1.6.6", + "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.6.6.tgz", + "integrity": "sha512-5Tk1HLk6b6ctmjIkAcU/Ujv/1WqiDl0F0JdRCR80VsOcUlHcu7pWeWRlOqQLHfDEsVx9YH/aif5AG4ehoCtTmg==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/bach": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/bach/-/bach-2.0.1.tgz", + "integrity": "sha512-A7bvGMGiTOxGMpNupYl9HQTf0FFDNF4VCmks4PJpFyN1AX2pdKuxuwdvUz2Hu388wcgp+OvGFNsumBfFNkR7eg==", + "dev": true, + "license": "MIT", + "dependencies": { + "async-done": "^2.0.0", + "async-settle": "^2.0.0", + "now-and-later": "^3.0.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" }, + "node_modules/bare-events": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.4.2.tgz", + "integrity": "sha512-qMKFd2qG/36aA4GwvKq8MxnPgCQAmBWmSyLWsJcbn8v03wvIPQ/hG1Ms8bPzndZxMDoHpxez5VOS+gC9Yi24/Q==", + "dev": true, + "license": "Apache-2.0", + "optional": true + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/bl": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-5.1.0.tgz", + "integrity": "sha512-tv1ZJHLfTDnXE6tMHv73YgSJaWR2AFuPwMntBe7XL/GBFHnT0CLnsHMogfk5+GzCDC5ZWarSCYaIGATZt9dNsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer": "^6.0.3", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/bl/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/body-parser": { "version": "1.20.3", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", @@ -1093,6 +1463,44 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, + "node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/buffer-equal": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal/-/buffer-equal-1.0.1.tgz", + "integrity": "sha512-QoV3ptgEaQpvVwbXdSO39iqPQTCxSF7A5U99AxbHYqUdCizL/lH2Z0A2y6nbZucxMEOtNyZfG2s6gsVugGpKkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/builtin-modules": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.3.0.tgz", @@ -1178,6 +1586,44 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chokidar/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/ci-info": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.0.0.tgz", @@ -1214,6 +1660,57 @@ "node": ">=0.8.0" } }, + "node_modules/cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "node_modules/clone": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", + "integrity": "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/clone-buffer": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/clone-buffer/-/clone-buffer-1.0.0.tgz", + "integrity": "sha512-KLLTJWrvwIP+OPfMn0x2PheDEP20RPUcGXj/ERegTgdmPEZylALQldygiqrPPu8P45uNuPs7ckmReLY6v/iA5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/clone-stats": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/clone-stats/-/clone-stats-1.0.0.tgz", + "integrity": "sha512-au6ydSpg6nsrigcZ4m8Bc9hxjeW+GJ8xh5G3BJCMt4WXe1H10UNaVOamqQTmrx1kjVuxAHIQSNU6hY4Nsn9/ag==", + "dev": true, + "license": "MIT" + }, + "node_modules/cloneable-readable": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/cloneable-readable/-/cloneable-readable-1.1.3.tgz", + "integrity": "sha512-2EF8zTQOxYq70Y4XKtorQupqF0m49MBz2/yf5Bj+MHjvpG3Hy7sImifnqD6UA+TKYxeSV+u6qqQPawN5UvnpKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.1", + "process-nextick-args": "^2.0.0", + "readable-stream": "^2.3.5" + } + }, "node_modules/code-block-writer": { "version": "12.0.0", "resolved": "https://registry.npmjs.org/code-block-writer/-/code-block-writer-12.0.0.tgz", @@ -1238,6 +1735,18 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/compressible": { "version": "2.0.18", "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", @@ -1310,6 +1819,13 @@ "node": ">= 0.6" } }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, "node_modules/cookie": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", @@ -1325,6 +1841,20 @@ "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", "license": "MIT" }, + "node_modules/copy-props": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/copy-props/-/copy-props-4.0.0.tgz", + "integrity": "sha512-bVWtw1wQLzzKiYROtvNlbJgxgBYt2bMJpkCbKmXM3xyijvcjjWXEk5nyrrT3bgJ7ODb19ZohE2T0Y3FgNPyoTw==", + "dev": true, + "license": "MIT", + "dependencies": { + "each-props": "^3.0.0", + "is-plain-object": "^5.0.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, "node_modules/core-js-compat": { "version": "3.37.1", "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.37.1.tgz", @@ -1338,6 +1868,13 @@ "url": "https://opencollective.com/core-js" } }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "dev": true, + "license": "MIT" + }, "node_modules/create-require": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", @@ -1391,6 +1928,33 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -1410,6 +1974,16 @@ "npm": "1.2.8000 || >= 1.4.16" } }, + "node_modules/detect-file": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/detect-file/-/detect-file-1.0.0.tgz", + "integrity": "sha512-DtCOLG98P007x7wiiOmfI0fi3eIKyWiLTGJ2MDnVi/E04lWGbf+JzrRHMm0rgIIZJGtHpKpbVgLWHrv8xXpc3Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/diff": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", @@ -1499,6 +2073,33 @@ "url": "https://github.com/fb55/domutils?sponsor=1" } }, + "node_modules/duplexify": { + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-3.7.1.tgz", + "integrity": "sha512-07z8uv2wMyS51kKhD1KsdXJg5WQ6t93RneqRxUHnskXVtlYYkLqM0gqStQZ3pj073g687jPCHrqNfCzawLYh5g==", + "dev": true, + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.0.0", + "inherits": "^2.0.1", + "readable-stream": "^2.0.0", + "stream-shift": "^1.0.0" + } + }, + "node_modules/each-props": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/each-props/-/each-props-3.0.0.tgz", + "integrity": "sha512-IYf1hpuWrdzse/s/YJOrFmU15lyhSzxelNVAHTEG3DtP4QsLTWZUzcUL3HMXmKQxXpa4EIrBPpwRgj0aehdvAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-plain-object": "^5.0.0", + "object.defaults": "^1.1.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, "node_modules/ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", @@ -1511,6 +2112,13 @@ "integrity": "sha512-FKbOCOQ5QRB3VlIbl1LZQefWIYwszlBloaXcY2rbfpu9ioJnNh3TK03YtIDKDo3WKBi8u+YV4+Fn2CkEozgf4w==", "dev": true }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, "node_modules/encodeurl": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", @@ -1520,6 +2128,16 @@ "node": ">= 0.8" } }, + "node_modules/end-of-stream": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", + "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, "node_modules/entities": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", @@ -1898,6 +2516,19 @@ "node": ">= 0.6" } }, + "node_modules/expand-tilde": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/expand-tilde/-/expand-tilde-2.0.2.tgz", + "integrity": "sha512-A5EmesHW6rfnZ9ysHQjPdJRni0SRar0tjtG5MNtm9n5TUvsYU8oozprtRD4AqHxcZWWlVuAmQo2nWKfN9oyjTw==", + "dev": true, + "license": "MIT", + "dependencies": { + "homedir-polyfill": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/express": { "version": "4.21.0", "resolved": "https://registry.npmjs.org/express/-/express-4.21.0.tgz", @@ -1940,12 +2571,40 @@ "node": ">= 0.10.0" } }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "dev": true, + "license": "MIT" + }, + "node_modules/extend-shallow": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", + "integrity": "sha512-BwY5b5Ql4+qZoefgMj2NUmx+tehVTH/Kf4k1ZEtOHNFcm2wSxMRo992l6X3TIgni2eZVTZ85xMOjF31fwZAj6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "assign-symbols": "^1.0.0", + "is-extendable": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", "dev": true }, + "node_modules/fast-fifo": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", + "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==", + "dev": true, + "license": "MIT" + }, "node_modules/fast-glob": { "version": "3.3.2", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", @@ -1984,6 +2643,16 @@ "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", "dev": true }, + "node_modules/fastest-levenshtein": { + "version": "1.0.16", + "resolved": "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz", + "integrity": "sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4.9.1" + } + }, "node_modules/fastq": { "version": "1.17.1", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", @@ -2049,6 +2718,49 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/findup-sync": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/findup-sync/-/findup-sync-5.0.0.tgz", + "integrity": "sha512-MzwXju70AuyflbgeOhzvQWAvvQdo1XL0A9bVvlXsYcFEBM87WR4OakL4OfZq+QRmr+duJubio+UtNQCPsVESzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "detect-file": "^1.0.0", + "is-glob": "^4.0.3", + "micromatch": "^4.0.4", + "resolve-dir": "^1.0.1" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/fined": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fined/-/fined-2.0.0.tgz", + "integrity": "sha512-OFRzsL6ZMHz5s0JrsEr+TpdGNCtrVtnuG3x1yzGNiQHT0yaDnXAj8V/lWcpJVrnoDpcwXcASxAZYbuXda2Y82A==", + "dev": true, + "license": "MIT", + "dependencies": { + "expand-tilde": "^2.0.2", + "is-plain-object": "^5.0.0", + "object.defaults": "^1.1.0", + "object.pick": "^1.3.0", + "parse-filepath": "^1.0.2" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/flagged-respawn": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/flagged-respawn/-/flagged-respawn-2.0.0.tgz", + "integrity": "sha512-Gq/a6YCi8zexmGHMuJwahTGzXlAZAOsbCVKduWXC6TlLCjjFRlExMJc4GC2NYPYZ0r/brw9P7CpRgQmlPVeOoA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.13.0" + } + }, "node_modules/flat-cache": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", @@ -2069,6 +2781,54 @@ "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==", "dev": true }, + "node_modules/flush-write-stream": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/flush-write-stream/-/flush-write-stream-1.1.1.tgz", + "integrity": "sha512-3Z4XhFZ3992uIq0XOqb9AreonueSYphE6oYbpt5+3u06JWklbsPkNv3ZKkP9Bz/r+1MWCaMoSQ28P85+1Yc77w==", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "readable-stream": "^2.3.6" + } + }, + "node_modules/for-in": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", + "integrity": "sha512-7EwmXrOjyL+ChxMhmG5lnW9MPt1aIeZEwKhQzoBUdTV0N3zuwWDZYVJatDvZ2OyzPUvdIAZDsCetk3coyMfcnQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/for-own": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/for-own/-/for-own-1.0.0.tgz", + "integrity": "sha512-0OABksIGrxKK8K4kynWkQ7y1zounQxP+CWnyclVwj81KW3vlLlGUx57DKGcP/LH216GzqnstnPocF16Nxs0Ycg==", + "dev": true, + "license": "MIT", + "dependencies": { + "for-in": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -2087,12 +2847,41 @@ "node": ">= 0.6" } }, + "node_modules/fs-mkdirp-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/fs-mkdirp-stream/-/fs-mkdirp-stream-2.0.1.tgz", + "integrity": "sha512-UTOY+59K6IA94tec8Wjqm0FSh5OVudGNB0NL/P6fB3HiE3bYOY3VYBGijsnOHNkQSwC1FKkU77pmq7xp9CskLw==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.8", + "streamx": "^2.12.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", "dev": true }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/function-bind": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", @@ -2102,6 +2891,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, "node_modules/get-intrinsic": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", @@ -2154,6 +2953,40 @@ "node": ">=10.13.0" } }, + "node_modules/glob-stream": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/glob-stream/-/glob-stream-8.0.2.tgz", + "integrity": "sha512-R8z6eTB55t3QeZMmU1C+Gv+t5UnNRkA55c5yo67fAVfxODxieTwsjNG7utxS/73NdP1NbDgCrhVEg2h00y4fFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@gulpjs/to-absolute-glob": "^4.0.0", + "anymatch": "^3.1.3", + "fastq": "^1.13.0", + "glob-parent": "^6.0.2", + "is-glob": "^4.0.3", + "is-negated-glob": "^1.0.0", + "normalize-path": "^3.0.0", + "streamx": "^2.12.5" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/glob-watcher": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/glob-watcher/-/glob-watcher-6.0.0.tgz", + "integrity": "sha512-wGM28Ehmcnk2NqRORXFOTOR064L4imSw3EeOqU5bIwUf62eXGwg89WivH6VMahL8zlQHeodzvHpXplrqzrz3Nw==", + "dev": true, + "license": "MIT", + "dependencies": { + "async-done": "^2.0.0", + "chokidar": "^3.5.3" + }, + "engines": { + "node": ">= 10.13.0" + } + }, "node_modules/glob/node_modules/brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -2176,6 +3009,51 @@ "node": "*" } }, + "node_modules/global-modules": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/global-modules/-/global-modules-1.0.0.tgz", + "integrity": "sha512-sKzpEkf11GpOFuw0Zzjzmt4B4UZwjOcG757PPvrfhxcLFbq0wpsgpOqxpxtxFiCG4DtG93M6XRVbF2oGdev7bg==", + "dev": true, + "license": "MIT", + "dependencies": { + "global-prefix": "^1.0.1", + "is-windows": "^1.0.1", + "resolve-dir": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/global-prefix": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/global-prefix/-/global-prefix-1.0.2.tgz", + "integrity": "sha512-5lsx1NUDHtSjfg0eHlmYvZKv8/nVqX4ckFbM+FrGcQ+04KWcWFo9P5MxPZYSzUvyzmdTbI7Eix8Q4IbELDqzKg==", + "dev": true, + "license": "MIT", + "dependencies": { + "expand-tilde": "^2.0.2", + "homedir-polyfill": "^1.0.1", + "ini": "^1.3.4", + "is-windows": "^1.0.1", + "which": "^1.2.14" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/global-prefix/node_modules/which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "which": "bin/which" + } + }, "node_modules/globals": { "version": "13.24.0", "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", @@ -2211,6 +3089,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/glogg": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/glogg/-/glogg-2.2.0.tgz", + "integrity": "sha512-eWv1ds/zAlz+M1ioHsyKJomfY7jbDDPpwSkv14KQj89bycx1nvK5/2Cj/T9g7kzJcX5Bc7Yv22FjfBZS/jl94A==", + "dev": true, + "license": "MIT", + "dependencies": { + "sparkles": "^2.1.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, "node_modules/gopd": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", @@ -2223,12 +3114,376 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, "node_modules/graphemer": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", "dev": true }, + "node_modules/gulp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/gulp/-/gulp-5.0.0.tgz", + "integrity": "sha512-S8Z8066SSileaYw1S2N1I64IUc/myI2bqe2ihOBzO6+nKpvNSg7ZcWJt/AwF8LC/NVN+/QZ560Cb/5OPsyhkhg==", + "dev": true, + "license": "MIT", + "dependencies": { + "glob-watcher": "^6.0.0", + "gulp-cli": "^3.0.0", + "undertaker": "^2.0.0", + "vinyl-fs": "^4.0.0" + }, + "bin": { + "gulp": "bin/gulp.js" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/gulp-cli": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/gulp-cli/-/gulp-cli-3.0.0.tgz", + "integrity": "sha512-RtMIitkT8DEMZZygHK2vEuLPqLPAFB4sntSxg4NoDta7ciwGZ18l7JuhCTiS5deOJi2IoK0btE+hs6R4sfj7AA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@gulpjs/messages": "^1.1.0", + "chalk": "^4.1.2", + "copy-props": "^4.0.0", + "gulplog": "^2.2.0", + "interpret": "^3.1.1", + "liftoff": "^5.0.0", + "mute-stdout": "^2.0.0", + "replace-homedir": "^2.0.0", + "semver-greatest-satisfied-range": "^2.0.0", + "string-width": "^4.2.3", + "v8flags": "^4.0.0", + "yargs": "^16.2.0" + }, + "bin": { + "gulp": "bin/gulp.js" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/gulp-copy": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/gulp-copy/-/gulp-copy-5.0.0.tgz", + "integrity": "sha512-XgTPwevIxr5bPITtrq24euacqASBMOy2R30IEUXX1mLsbd/GoOo0AM3T9qjF9L2Yp+muRr/spdRskVUjUCDqkg==", + "dev": true, + "license": "MIT", + "dependencies": { + "plugin-error": "^2.0.1", + "through2": "^2.0.3" + }, + "peerDependencies": { + "gulp": "^4.0.1 || ^5.0.0" + } + }, + "node_modules/gulp-typescript": { + "version": "6.0.0-alpha.1", + "resolved": "https://registry.npmjs.org/gulp-typescript/-/gulp-typescript-6.0.0-alpha.1.tgz", + "integrity": "sha512-KoT0TTfjfT7w3JItHkgFH1T/zK4oXWC+a8xxKfniRfVcA0Fa1bKrIhztYelYmb+95RB80OLMBreknYkdwzdi2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-colors": "^4.1.1", + "plugin-error": "^1.0.1", + "source-map": "^0.7.3", + "through2": "^3.0.1", + "vinyl": "^2.2.0", + "vinyl-fs": "^3.0.3" + }, + "engines": { + "node": ">= 8" + }, + "peerDependencies": { + "typescript": "~2.7.1 || >=2.8.0-dev || >=2.9.0-dev || ~3.0.0 || >=3.0.0-dev || >=3.1.0-dev || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.7.0-dev " + } + }, + "node_modules/gulp-typescript/node_modules/convert-source-map": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", + "dev": true, + "license": "MIT" + }, + "node_modules/gulp-typescript/node_modules/fs-mkdirp-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-mkdirp-stream/-/fs-mkdirp-stream-1.0.0.tgz", + "integrity": "sha512-+vSd9frUnapVC2RZYfL3FCB2p3g4TBhaUmrsWlSudsGdnxIuUvBB2QM1VZeBtc49QFwrp+wQLrDs3+xxDgI5gQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.1.11", + "through2": "^2.0.3" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/gulp-typescript/node_modules/fs-mkdirp-stream/node_modules/through2": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", + "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "readable-stream": "~2.3.6", + "xtend": "~4.0.1" + } + }, + "node_modules/gulp-typescript/node_modules/glob-parent": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz", + "integrity": "sha512-E8Ak/2+dZY6fnzlR7+ueWvhsH1SjHr4jjss4YS/h4py44jY9MhK/VFdaZJAWDz6BbL21KeteKxFSFpq8OS5gVA==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^3.1.0", + "path-dirname": "^1.0.0" + } + }, + "node_modules/gulp-typescript/node_modules/glob-stream": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/glob-stream/-/glob-stream-6.1.0.tgz", + "integrity": "sha512-uMbLGAP3S2aDOHUDfdoYcdIePUCfysbAd0IAoWVZbeGU/oNQ8asHVSshLDJUPWxfzj8zsCG7/XeHPHTtow0nsw==", + "dev": true, + "license": "MIT", + "dependencies": { + "extend": "^3.0.0", + "glob": "^7.1.1", + "glob-parent": "^3.1.0", + "is-negated-glob": "^1.0.0", + "ordered-read-streams": "^1.0.0", + "pumpify": "^1.3.5", + "readable-stream": "^2.1.5", + "remove-trailing-separator": "^1.0.1", + "to-absolute-glob": "^2.0.0", + "unique-stream": "^2.0.2" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/gulp-typescript/node_modules/is-glob": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz", + "integrity": "sha512-UFpDDrPgM6qpnFNI+rh/p3bUaq9hKLZN8bMUWzxmcnZVS3omf4IPK+BrewlnWjO1WmUsMYuSjKh4UJuV4+Lqmw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/gulp-typescript/node_modules/lead": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/lead/-/lead-1.0.0.tgz", + "integrity": "sha512-IpSVCk9AYvLHo5ctcIXxOBpMWUe+4TKN3VPWAKUbJikkmsGp0VrSM8IttVc32D6J4WUsiPE6aEFRNmIoF/gdow==", + "dev": true, + "license": "MIT", + "dependencies": { + "flush-write-stream": "^1.0.2" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/gulp-typescript/node_modules/normalize-path": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", + "integrity": "sha512-3pKJwH184Xo/lnH6oyP1q2pMd7HcypqqmRs91/6/i2CGtWwIKGCkOOMTm/zXbgTEWHw1uNpNi/igc3ePOYHb6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "remove-trailing-separator": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/gulp-typescript/node_modules/now-and-later": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/now-and-later/-/now-and-later-2.0.1.tgz", + "integrity": "sha512-KGvQ0cB70AQfg107Xvs/Fbu+dGmZoTRJp2TaPwcwQm3/7PteUyN2BCgk8KBMPGBUXZdVwyWS8fDCGFygBm19UQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "once": "^1.3.2" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/gulp-typescript/node_modules/plugin-error": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/plugin-error/-/plugin-error-1.0.1.tgz", + "integrity": "sha512-L1zP0dk7vGweZME2i+EeakvUNqSrdiI3F91TwEoYiGrAfUXmVv6fJIq4g82PAXxNsWOp0J7ZqQy/3Szz0ajTxA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-colors": "^1.0.1", + "arr-diff": "^4.0.0", + "arr-union": "^3.1.0", + "extend-shallow": "^3.0.2" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/gulp-typescript/node_modules/plugin-error/node_modules/ansi-colors": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-1.1.0.tgz", + "integrity": "sha512-SFKX67auSNoVR38N3L+nvsPjOE0bybKTYbkf5tRvushrAPQ9V75huw0ZxBkKVeRU9kqH3d6HA4xTckbwZ4ixmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-wrap": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/gulp-typescript/node_modules/resolve-options": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/resolve-options/-/resolve-options-1.1.0.tgz", + "integrity": "sha512-NYDgziiroVeDC29xq7bp/CacZERYsA9bXYd1ZmcJlF3BcrZv5pTb4NG7SjdyKDnXZ84aC4vo2u6sNKIA1LCu/A==", + "dev": true, + "license": "MIT", + "dependencies": { + "value-or-function": "^3.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/gulp-typescript/node_modules/through2": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/through2/-/through2-3.0.2.tgz", + "integrity": "sha512-enaDQ4MUyP2W6ZyT6EsMzqBPZaM/avg8iuo+l2d3QCs0J+6RaqkHV/2/lOwDTueBHeJ/2LG9lrLW3d5rWPucuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.4", + "readable-stream": "2 || 3" + } + }, + "node_modules/gulp-typescript/node_modules/to-through": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-through/-/to-through-2.0.0.tgz", + "integrity": "sha512-+QIz37Ly7acM4EMdw2PRN389OneM5+d844tirkGp4dPKzI5OE72V9OsbFp+CIYJDahZ41ZV05hNtcPAQUAm9/Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "through2": "^2.0.3" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/gulp-typescript/node_modules/to-through/node_modules/through2": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", + "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "readable-stream": "~2.3.6", + "xtend": "~4.0.1" + } + }, + "node_modules/gulp-typescript/node_modules/value-or-function": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/value-or-function/-/value-or-function-3.0.0.tgz", + "integrity": "sha512-jdBB2FrWvQC/pnPtIqcLsMaQgjhdb6B7tk1MMyTKapox+tQZbdRP4uLxu/JY0t7fbfDCUMnuelzEYv5GsxHhdg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/gulp-typescript/node_modules/vinyl-fs": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/vinyl-fs/-/vinyl-fs-3.0.3.tgz", + "integrity": "sha512-vIu34EkyNyJxmP0jscNzWBSygh7VWhqun6RmqVfXePrOwi9lhvRs//dOaGOTRUQr4tx7/zd26Tk5WeSVZitgng==", + "dev": true, + "license": "MIT", + "dependencies": { + "fs-mkdirp-stream": "^1.0.0", + "glob-stream": "^6.1.0", + "graceful-fs": "^4.0.0", + "is-valid-glob": "^1.0.0", + "lazystream": "^1.0.0", + "lead": "^1.0.0", + "object.assign": "^4.0.4", + "pumpify": "^1.3.5", + "readable-stream": "^2.3.3", + "remove-bom-buffer": "^3.0.0", + "remove-bom-stream": "^1.2.0", + "resolve-options": "^1.1.0", + "through2": "^2.0.0", + "to-through": "^2.0.0", + "value-or-function": "^3.0.0", + "vinyl": "^2.0.0", + "vinyl-sourcemap": "^1.1.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/gulp-typescript/node_modules/vinyl-fs/node_modules/through2": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", + "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "readable-stream": "~2.3.6", + "xtend": "~4.0.1" + } + }, + "node_modules/gulp-typescript/node_modules/vinyl-sourcemap": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/vinyl-sourcemap/-/vinyl-sourcemap-1.1.0.tgz", + "integrity": "sha512-NiibMgt6VJGJmyw7vtzhctDcfKch4e4n9TBeoWlirb7FMg9/1Ov9k+A5ZRAtywBpRPiyECvQRQllYM8dECegVA==", + "dev": true, + "license": "MIT", + "dependencies": { + "append-buffer": "^1.0.2", + "convert-source-map": "^1.5.0", + "graceful-fs": "^4.1.6", + "normalize-path": "^2.1.1", + "now-and-later": "^2.0.0", + "remove-bom-buffer": "^3.0.0", + "vinyl": "^2.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/gulplog": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/gulplog/-/gulplog-2.2.0.tgz", + "integrity": "sha512-V2FaKiOhpR3DRXZuYdRLn/qiY0yI5XmqbTKrYbdemJ+xOh2d2MOweI/XFgMzd/9+1twdvMwllnZbWZNJ+BOm4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "glogg": "^2.2.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, "node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", @@ -2286,6 +3541,19 @@ "node": ">= 0.4" } }, + "node_modules/homedir-polyfill": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/homedir-polyfill/-/homedir-polyfill-1.0.3.tgz", + "integrity": "sha512-eSmmWE5bZTK2Nou4g0AI3zZ9rswp7GRKoKXS1BLUkvPviOqs4YTN1djQIqrXy9k5gEtdLPy86JjRwsNM9tnDcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "parse-passwd": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/hosted-git-info": { "version": "2.8.9", "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", @@ -2339,6 +3607,27 @@ "node": ">=0.10.0" } }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, "node_modules/ignore": { "version": "5.3.1", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz", @@ -2399,6 +3688,23 @@ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "license": "ISC" }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "dev": true, + "license": "ISC" + }, + "node_modules/interpret": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/interpret/-/interpret-3.1.1.tgz", + "integrity": "sha512-6xwYfHbajpoF0xLW+iwLkhwgvLoZDfjYfoFNu8ftMoXINzwuymNLd9u/KmwtdT2GbR+/Cz66otEGEVVUHX9QLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.13.0" + } + }, "node_modules/ipaddr.js": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", @@ -2408,12 +3714,46 @@ "node": ">= 0.10" } }, + "node_modules/is-absolute": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-absolute/-/is-absolute-1.0.0.tgz", + "integrity": "sha512-dOWoqflvcydARa360Gvv18DZ/gRuHKi2NU/wU5X1ZFzdYfH29nkiNZsF3mp4OJ3H4yo9Mx8A/uAGNzpzPN3yBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-relative": "^1.0.0", + "is-windows": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/is-arrayish": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", "dev": true }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-buffer": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", + "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", + "dev": true, + "license": "MIT" + }, "node_modules/is-builtin-module": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/is-builtin-module/-/is-builtin-module-3.2.1.tgz", @@ -2444,6 +3784,32 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-extendable": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", + "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-plain-object": "^2.0.4" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-extendable/node_modules/is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "dev": true, + "license": "MIT", + "dependencies": { + "isobject": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -2452,6 +3818,16 @@ "node": ">=0.10.0" } }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/is-glob": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", @@ -2463,6 +3839,16 @@ "node": ">=0.10.0" } }, + "node_modules/is-negated-glob": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-negated-glob/-/is-negated-glob-1.0.0.tgz", + "integrity": "sha512-czXVVn/QEmgvej1f50BZ648vUI+em0xqMq2Sn+QncCLN4zj1UAxlT+kw/6ggQTOaZPd1HqKQGEqbpQVtJucWug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/is-number": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", @@ -2480,12 +3866,92 @@ "node": ">=8" } }, + "node_modules/is-plain-object": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", + "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-relative": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-relative/-/is-relative-1.0.0.tgz", + "integrity": "sha512-Kw/ReK0iqwKeu0MITLFuj0jbPAmEiOsIwyIXvvbfa6QfmN9pkD1M+8pdk7Rl/dTKbH34/XBFMbgD4iMJhLQbGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-unc-path": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-unc-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-unc-path/-/is-unc-path-1.0.0.tgz", + "integrity": "sha512-mrGpVd0fs7WWLfVsStvgF6iEJnbjDFZh9/emhRDcGWTduTfNHd9CHeUwH3gYIjdbwo4On6hunkztwOaAw0yllQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "unc-path-regex": "^0.1.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-utf8": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-utf8/-/is-utf8-0.2.1.tgz", + "integrity": "sha512-rMYPYvCzsXywIsldgLaSoPlw5PfoB/ssr7hY4pLfcodrA5M/eArza1a9VmTiNIBNMjOGr1Ow9mTyU2o69U6U9Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-valid-glob": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-valid-glob/-/is-valid-glob-1.0.0.tgz", + "integrity": "sha512-AhiROmoEFDSsjx8hW+5sGwgKVIORcXnrlAx/R0ZSeaPw70Vw0CqkGBBhHGL58Uox2eXnU1AnvXJl1XlyedO5bA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-windows": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", + "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "dev": true, + "license": "MIT" + }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", "dev": true }, + "node_modules/isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -2549,6 +4015,39 @@ "json-buffer": "3.0.1" } }, + "node_modules/last-run": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/last-run/-/last-run-2.0.0.tgz", + "integrity": "sha512-j+y6WhTLN4Itnf9j5ZQos1BGPCS8DAwmgMroR3OzfxAsBxam0hMw7J8M3KqZl0pLQJ1jNnwIexg5DYpC/ctwEQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/lazystream": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.1.tgz", + "integrity": "sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "readable-stream": "^2.0.5" + }, + "engines": { + "node": ">= 0.6.3" + } + }, + "node_modules/lead": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/lead/-/lead-4.0.0.tgz", + "integrity": "sha512-DpMa59o5uGUWWjruMp71e6knmwKU3jRBBn1kjuLWN9EeIOxNeSAwvHf03WIl8g/ZMR2oSQC9ej3yeLBwdDc/pg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.13.0" + } + }, "node_modules/levn": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", @@ -2562,6 +4061,25 @@ "node": ">= 0.8.0" } }, + "node_modules/liftoff": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/liftoff/-/liftoff-5.0.0.tgz", + "integrity": "sha512-a5BQjbCHnB+cy+gsro8lXJ4kZluzOijzJ1UVVfyJYZC+IP2pLv1h4+aysQeKuTmyO8NAqfyQAk4HWaP/HjcKTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "extend": "^3.0.2", + "findup-sync": "^5.0.0", + "fined": "^2.0.0", + "flagged-respawn": "^2.0.0", + "is-plain-object": "^5.0.0", + "rechoir": "^0.8.0", + "resolve": "^1.20.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, "node_modules/lines-and-columns": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", @@ -2596,6 +4114,16 @@ "dev": true, "license": "ISC" }, + "node_modules/map-cache": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz", + "integrity": "sha512-8y/eV9QQZCiyn1SprXSrCmqJN0yNRATe+PO8ztwqrvrbdRLA3eYJF0yaR0YayLWkMbsQSKWS9N2gPcGEc4UsZg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/media-typer": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", @@ -2732,6 +4260,16 @@ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "license": "MIT" }, + "node_modules/mute-stdout": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mute-stdout/-/mute-stdout-2.0.0.tgz", + "integrity": "sha512-32GSKM3Wyc8dg/p39lWPKYu8zci9mJFzV1Np9Of0ZEpe6Fhssn/FbI7ywAMd40uX+p3ZKh3T5EeCFv81qS3HmQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.13.0" + } + }, "node_modules/natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", @@ -2774,6 +4312,29 @@ "semver": "bin/semver" } }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/now-and-later": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/now-and-later/-/now-and-later-3.0.0.tgz", + "integrity": "sha512-pGO4pzSdaxhWTGkfSfHx3hVzJVslFPwBp2Myq9MYN/ChfJZF87ochMAXnvz6/58RJSf5ik2q9tXprBBrk2cpcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, "node_modules/object-inspect": { "version": "1.13.2", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.2.tgz", @@ -2786,6 +4347,64 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.assign": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.5.tgz", + "integrity": "sha512-byy+U7gp+FVwmyzKPYhW2h5l3crpmGsxl7X2s8y43IgxvG4g3QZ6CffDtsNQy1WsmZpQbO+ybo0AlW7TY6DcBQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.5", + "define-properties": "^1.2.1", + "has-symbols": "^1.0.3", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.defaults": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/object.defaults/-/object.defaults-1.1.0.tgz", + "integrity": "sha512-c/K0mw/F11k4dEUBMW8naXUuBuhxRCfG7W+yFy8EcijU/rSmazOUd1XAEEe6bC0OuXY4HUKjTJv7xbxIMqdxrA==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-each": "^1.0.1", + "array-slice": "^1.0.0", + "for-own": "^1.0.0", + "isobject": "^3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object.pick": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/object.pick/-/object.pick-1.3.0.tgz", + "integrity": "sha512-tqa/UMy/CCoYmj+H5qc07qvSL9dqcs/WZENZ1JbtWBlATP+iVOe778gE6MSijnyCnORzDuX6hU+LA4SZ09YjFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "isobject": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/on-finished": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", @@ -2833,6 +4452,16 @@ "node": ">= 0.8.0" } }, + "node_modules/ordered-read-streams": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ordered-read-streams/-/ordered-read-streams-1.0.1.tgz", + "integrity": "sha512-Z87aSjx3r5c0ZB7bcJqIgIRX5bxR7A4aSzvIbaxd0oTkWBCOoKfuGHiKj60CHVUgg1Phm5yMZzBdt8XqRs73Mw==", + "dev": true, + "license": "MIT", + "dependencies": { + "readable-stream": "^2.0.1" + } + }, "node_modules/p-limit": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", @@ -2884,6 +4513,21 @@ "node": ">=6" } }, + "node_modules/parse-filepath": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/parse-filepath/-/parse-filepath-1.0.2.tgz", + "integrity": "sha512-FwdRXKCohSVeXqwtYonZTXtbGJKrn+HNyWDYVcp5yuJlesTwNH4rsmRZ+GrKAPJ5bLpRxESMeS+Rl0VCHRvB2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-absolute": "^1.0.0", + "map-cache": "^0.2.0", + "path-root": "^0.1.1" + }, + "engines": { + "node": ">=0.8" + } + }, "node_modules/parse-json": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", @@ -2902,6 +4546,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/parse-passwd": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/parse-passwd/-/parse-passwd-1.0.0.tgz", + "integrity": "sha512-1Y1A//QUXEZK7YKz+rD9WydcE1+EuPr6ZBgKecAB8tmoW6UFv0NREVJe1p+jRxtThkcbbKkfwIbWJe/IeE6m2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", @@ -2917,6 +4571,13 @@ "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==", "license": "MIT" }, + "node_modules/path-dirname": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/path-dirname/-/path-dirname-1.0.2.tgz", + "integrity": "sha512-ALzNPpyNq9AqXMBjeymIjFDAkAFH06mHJH/cSBHAgU0s4vfpBn6b2nf8tiRLvagKD8RbTpq2FKTBg7cl9l3c7Q==", + "dev": true, + "license": "MIT" + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -2950,6 +4611,29 @@ "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", "dev": true }, + "node_modules/path-root": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/path-root/-/path-root-0.1.1.tgz", + "integrity": "sha512-QLcPegTHF11axjfojBIoDygmS2E3Lf+8+jI6wOVmNVenrKSo3mFdSGiIgdSHenczw3wPtlVMQaFVwGmM7BJdtg==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-root-regex": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-root-regex": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/path-root-regex/-/path-root-regex-0.1.2.tgz", + "integrity": "sha512-4GlJ6rZDhQZFE0DPVKh0e9jmZ5egZfxTkp7bcRDuPlJXbAwhxcl2dINPUAsjLdejqaLsCeg8axcLjIbvBjN4pQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/path-to-regexp": { "version": "0.1.10", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.10.tgz", @@ -2983,6 +4667,32 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/plugin-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/plugin-error/-/plugin-error-2.0.1.tgz", + "integrity": "sha512-zMakqvIDyY40xHOvzXka0kUvf40nYIuwRE8dWhti2WtjQZ31xAgBZBhxsK7vK3QbRXS1Xms/LO7B5cuAsfB2Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-colors": "^1.0.1" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/plugin-error/node_modules/ansi-colors": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-1.1.0.tgz", + "integrity": "sha512-SFKX67auSNoVR38N3L+nvsPjOE0bybKTYbkf5tRvushrAPQ9V75huw0ZxBkKVeRU9kqH3d6HA4xTckbwZ4ixmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-wrap": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/pluralize": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-8.0.0.tgz", @@ -3001,6 +4711,13 @@ "node": ">= 0.8.0" } }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "dev": true, + "license": "MIT" + }, "node_modules/proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", @@ -3014,6 +4731,29 @@ "node": ">= 0.10" } }, + "node_modules/pump": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pump/-/pump-2.0.1.tgz", + "integrity": "sha512-ruPMNRkN3MHP1cWJc9OWr+T/xDP0jhXYCLfJcBuX54hhfIBnaQmAUMfDcG4DM5UMWByBbJY69QSphm3jtDKIkA==", + "dev": true, + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/pumpify": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/pumpify/-/pumpify-1.5.1.tgz", + "integrity": "sha512-oClZI37HvuUJJxSKKrC17bZ9Cu0ZYhEAGPsPUy9KlMUmv9dKX2o77RUmq7f3XjIxbwyGwYzbzQ1L2Ks8sIradQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "duplexify": "^3.6.0", + "inherits": "^2.0.3", + "pump": "^2.0.0" + } + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -3057,6 +4797,13 @@ } ] }, + "node_modules/queue-tick": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/queue-tick/-/queue-tick-1.0.1.tgz", + "integrity": "sha512-kJt5qhMxoszgU/62PLP1CJytzd2NKetjSRnyuj31fDd3Rlcz3fzlFdFLD1SItunPwyqEOkca6GbV612BWfaBag==", + "dev": true, + "license": "MIT" + }, "node_modules/range-parser": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", @@ -3183,6 +4930,68 @@ "node": ">=8" } }, + "node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/readable-stream/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true, + "license": "MIT" + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/readdirp/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/rechoir": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.8.0.tgz", + "integrity": "sha512-/vxpCXddiX8NGfGO/mTafwjq4aFa/71pvamip0++IQk3zG8cbCj0fifNPrjjF1XMXUne91jL9OoxmdykoEtifQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve": "^1.20.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, "node_modules/regexp-tree": { "version": "0.1.27", "resolved": "https://registry.npmjs.org/regexp-tree/-/regexp-tree-0.1.27.tgz", @@ -3213,6 +5022,72 @@ "jsesc": "bin/jsesc" } }, + "node_modules/remove-bom-buffer": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/remove-bom-buffer/-/remove-bom-buffer-3.0.0.tgz", + "integrity": "sha512-8v2rWhaakv18qcvNeli2mZ/TMTL2nEyAKRvzo1WtnZBl15SHyEhrCu2/xKlJyUFKHiHgfXIyuY6g2dObJJycXQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-buffer": "^1.1.5", + "is-utf8": "^0.2.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/remove-bom-stream": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/remove-bom-stream/-/remove-bom-stream-1.2.0.tgz", + "integrity": "sha512-wigO8/O08XHb8YPzpDDT+QmRANfW6vLqxfaXm1YXhnFf3AkSLyjfG3GEFg4McZkmgL7KvCj5u2KczkvSP6NfHA==", + "dev": true, + "license": "MIT", + "dependencies": { + "remove-bom-buffer": "^3.0.0", + "safe-buffer": "^5.1.0", + "through2": "^2.0.3" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/remove-trailing-separator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz", + "integrity": "sha512-/hS+Y0u3aOfIETiaiirUFwDBDzmXPvO+jAfKTitUngIPzdKc6Z0LoFjM/CK5PL4C+eKwHohlHAb6H0VFfmmUsw==", + "dev": true, + "license": "ISC" + }, + "node_modules/replace-ext": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/replace-ext/-/replace-ext-1.0.1.tgz", + "integrity": "sha512-yD5BHCe7quCgBph4rMQ+0KkIRKwWCrHDOX1p1Gp6HwjPM5kVoCdKGNhN7ydqqsX6lJEnQDKZ/tFMiEdQ1dvPEw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/replace-homedir": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/replace-homedir/-/replace-homedir-2.0.0.tgz", + "integrity": "sha512-bgEuQQ/BHW0XkkJtawzrfzHFSN70f/3cNOiHa2QsYxqrjaC30X1k74FJ6xswVBP0sr0SpGIdVFuPwfrYziVeyw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/resolve": { "version": "1.22.8", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", @@ -3230,6 +5105,20 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/resolve-dir": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/resolve-dir/-/resolve-dir-1.0.1.tgz", + "integrity": "sha512-R7uiTjECzvOsWSfdM0QKFNBVFcK27aHOUwdvK53BcW8zqnGdYp0Fbj82cy54+2A4P2tFM22J5kRfe1R+lM/1yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "expand-tilde": "^2.0.0", + "global-modules": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/resolve-from": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", @@ -3239,6 +5128,19 @@ "node": ">=4" } }, + "node_modules/resolve-options": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/resolve-options/-/resolve-options-2.0.0.tgz", + "integrity": "sha512-/FopbmmFOQCfsCx77BRFdKOniglTiHumLgwvd6IDPihy1GKkadZbgQJBcTb2lMzSR1pndzd96b1nZrreZ7+9/A==", + "dev": true, + "license": "MIT", + "dependencies": { + "value-or-function": "^4.0.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, "node_modules/reusify": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", @@ -3324,6 +5226,19 @@ "node": ">=10" } }, + "node_modules/semver-greatest-satisfied-range": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/semver-greatest-satisfied-range/-/semver-greatest-satisfied-range-2.0.0.tgz", + "integrity": "sha512-lH3f6kMbwyANB7HuOWRMlLCa2itaCrZJ+SAqqkSZrZKO/cAsk2EOyaKHUtNkVLFyFW9pct22SFesFp3Z7zpA0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "sver": "^1.8.3" + }, + "engines": { + "node": ">= 10.13.0" + } + }, "node_modules/send": { "version": "0.19.0", "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", @@ -3449,6 +5364,26 @@ "node": ">=8" } }, + "node_modules/source-map": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz", + "integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">= 8" + } + }, + "node_modules/sparkles": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/sparkles/-/sparkles-2.1.0.tgz", + "integrity": "sha512-r7iW1bDw8R/cFifrD3JnQJX0K1jqT0kprL48BiBpLZLJPmAm34zsVBsK5lc7HirZYZqMW65dOXZgbAGt/I6frg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.13.0" + } + }, "node_modules/spdx-correct": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz", @@ -3490,6 +5425,77 @@ "node": ">= 0.8" } }, + "node_modules/stream-composer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/stream-composer/-/stream-composer-1.0.2.tgz", + "integrity": "sha512-bnBselmwfX5K10AH6L4c8+S5lgZMWI7ZYrz2rvYjCPB2DIMC4Ig8OpxGpNJSxRZ58oti7y1IcNvjBAz9vW5m4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "streamx": "^2.13.2" + } + }, + "node_modules/stream-exhaust": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/stream-exhaust/-/stream-exhaust-1.0.2.tgz", + "integrity": "sha512-b/qaq/GlBK5xaq1yrK9/zFcyRSTNxmcZwFLGSTG0mXgZl/4Z6GgiyYOXOvY7N3eEvFRAG1bkDRz5EPGSvPYQlw==", + "dev": true, + "license": "MIT" + }, + "node_modules/stream-shift": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.3.tgz", + "integrity": "sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/streamx": { + "version": "2.20.1", + "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.20.1.tgz", + "integrity": "sha512-uTa0mU6WUC65iUvzKH4X9hEdvSW7rbPxPtwfWiLMSj3qTdQbAiUboZTxauKfpFuGIGa1C2BYijZ7wgdUXICJhA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-fifo": "^1.3.2", + "queue-tick": "^1.0.1", + "text-decoder": "^1.1.0" + }, + "optionalDependencies": { + "bare-events": "^2.2.0" + } + }, + "node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/string_decoder/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true, + "license": "MIT" + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/strip-ansi": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", @@ -3550,12 +5556,89 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/sver": { + "version": "1.8.4", + "resolved": "https://registry.npmjs.org/sver/-/sver-1.8.4.tgz", + "integrity": "sha512-71o1zfzyawLfIWBOmw8brleKyvnbn73oVHNCsu51uPMz/HWiKkkXsI31JjHW5zqXEqnPYkIiHd8ZmL7FCimLEA==", + "dev": true, + "license": "MIT", + "optionalDependencies": { + "semver": "^6.3.0" + } + }, + "node_modules/sver/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "optional": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/teex": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/teex/-/teex-1.0.1.tgz", + "integrity": "sha512-eYE6iEI62Ni1H8oIa7KlDU6uQBtqr4Eajni3wX7rpfXD8ysFx8z0+dri+KWEPWpBsxXfxu58x/0jvTVT1ekOSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "streamx": "^2.12.5" + } + }, + "node_modules/text-decoder": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.0.tgz", + "integrity": "sha512-n1yg1mOj9DNpk3NeZOx7T6jchTbyJS3i3cucbNN6FcdPriMZx7NsgrGpWWdWZZGxD7ES1XB+3uoqHMgOKaN+fg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "b4a": "^1.6.4" + } + }, "node_modules/text-table": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", "dev": true }, + "node_modules/through2": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", + "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "readable-stream": "~2.3.6", + "xtend": "~4.0.1" + } + }, + "node_modules/through2-filter": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/through2-filter/-/through2-filter-3.0.0.tgz", + "integrity": "sha512-jaRjI2WxN3W1V8/FMZ9HKIBXixtiqs3SQSX4/YGIiP3gL6djW48VoZq9tDqeCWs3MT8YY5wb/zli8VW8snY1CA==", + "dev": true, + "license": "MIT", + "dependencies": { + "through2": "~2.0.0", + "xtend": "~4.0.0" + } + }, + "node_modules/to-absolute-glob": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/to-absolute-glob/-/to-absolute-glob-2.0.2.tgz", + "integrity": "sha512-rtwLUQEwT8ZeKQbyFJyomBRYXyE16U5VKuy0ftxLMK/PZb2fkOsg5r9kHdauuVDbsNdIBoC/HCthpidamQFXYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-absolute": "^1.0.0", + "is-negated-glob": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -3567,6 +5650,19 @@ "node": ">=8.0" } }, + "node_modules/to-through": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/to-through/-/to-through-3.0.0.tgz", + "integrity": "sha512-y8MN937s/HVhEoBU1SxfHC+wxCHkV1a9gW8eAdTadYh/bGyesZIVcbjI+mSpFbSVwQici/XjBjuUyri1dnXwBw==", + "dev": true, + "license": "MIT", + "dependencies": { + "streamx": "^2.12.5" + }, + "engines": { + "node": ">=10.13.0" + } + }, "node_modules/toidentifier": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", @@ -3737,13 +5833,68 @@ } } }, + "node_modules/unc-path-regex": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/unc-path-regex/-/unc-path-regex-0.1.2.tgz", + "integrity": "sha512-eXL4nmJT7oCpkZsHZUOJo8hcX3GbsiDOa0Qu9F646fi8dT3XuSVopVqAcEiVzSKKH7UoDti23wNX3qGFxcW5Qg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/undertaker": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/undertaker/-/undertaker-2.0.0.tgz", + "integrity": "sha512-tO/bf30wBbTsJ7go80j0RzA2rcwX6o7XPBpeFcb+jzoeb4pfMM2zUeSDIkY1AWqeZabWxaQZ/h8N9t35QKDLPQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "bach": "^2.0.1", + "fast-levenshtein": "^3.0.0", + "last-run": "^2.0.0", + "undertaker-registry": "^2.0.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/undertaker-registry": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/undertaker-registry/-/undertaker-registry-2.0.0.tgz", + "integrity": "sha512-+hhVICbnp+rlzZMgxXenpvTxpuvA67Bfgtt+O9WOE5jo7w/dyiF1VmoZVIHvP2EkUjsyKyTwYKlLhA+j47m1Ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/undertaker/node_modules/fast-levenshtein": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-3.0.0.tgz", + "integrity": "sha512-hKKNajm46uNmTlhHSyZkmToAc56uZJwYq7yrciZjqOxnlfQwERDQJmHPUp7m1m9wx8vgOe8IaCKZ5Kv2k1DdCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fastest-levenshtein": "^1.0.7" + } + }, "node_modules/undici-types": { "version": "5.26.5", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "license": "MIT" + }, + "node_modules/unique-stream": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/unique-stream/-/unique-stream-2.3.1.tgz", + "integrity": "sha512-2nY4TnBE70yoxHkDli7DMazpWiP7xMdCYqU2nBRO0UB+ZpEkGsSija7MvmvnZFUeC+mrgiUfcHSr3LmRFIg4+A==", "dev": true, "license": "MIT", - "peer": true + "dependencies": { + "json-stable-stringify-without-jsonify": "^1.0.1", + "through2-filter": "^3.0.0" + } }, "node_modules/unpipe": { "version": "1.0.0", @@ -3793,6 +5944,13 @@ "punycode": "^2.1.0" } }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, + "license": "MIT" + }, "node_modules/utils-merge": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", @@ -3809,6 +5967,16 @@ "dev": true, "license": "MIT" }, + "node_modules/v8flags": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/v8flags/-/v8flags-4.0.1.tgz", + "integrity": "sha512-fcRLaS4H/hrZk9hYwbdRM35D0U8IYMfEClhXxCivOojl+yTRAZH3Zy2sSy6qVCiGbV9YAtPssP6jaChqC9vPCg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.13.0" + } + }, "node_modules/validate-npm-package-license": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", @@ -3819,6 +5987,16 @@ "spdx-expression-parse": "^3.0.0" } }, + "node_modules/value-or-function": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/value-or-function/-/value-or-function-4.0.0.tgz", + "integrity": "sha512-aeVK81SIuT6aMJfNo9Vte8Dw0/FZINGBV8BfCraGtqVxIeLAEhJyoWs8SmvRVmXfGss2PmmOwZCuBPbZR+IYWg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.13.0" + } + }, "node_modules/vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", @@ -3828,6 +6006,176 @@ "node": ">= 0.8" } }, + "node_modules/vinyl": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-2.2.1.tgz", + "integrity": "sha512-LII3bXRFBZLlezoG5FfZVcXflZgWP/4dCwKtxd5ky9+LOtM4CS3bIRQsmR1KMnMW07jpE8fqR2lcxPZ+8sJIcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "clone": "^2.1.1", + "clone-buffer": "^1.0.0", + "clone-stats": "^1.0.0", + "cloneable-readable": "^1.0.0", + "remove-trailing-separator": "^1.0.1", + "replace-ext": "^1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/vinyl-contents": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/vinyl-contents/-/vinyl-contents-2.0.0.tgz", + "integrity": "sha512-cHq6NnGyi2pZ7xwdHSW1v4Jfnho4TEGtxZHw01cmnc8+i7jgR6bRnED/LbrKan/Q7CvVLbnvA5OepnhbpjBZ5Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "bl": "^5.0.0", + "vinyl": "^3.0.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/vinyl-contents/node_modules/replace-ext": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/replace-ext/-/replace-ext-2.0.0.tgz", + "integrity": "sha512-UszKE5KVK6JvyD92nzMn9cDapSk6w/CaFZ96CnmDMUqH9oowfxF/ZjRITD25H4DnOQClLA4/j7jLGXXLVKxAug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, + "node_modules/vinyl-contents/node_modules/vinyl": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-3.0.0.tgz", + "integrity": "sha512-rC2VRfAVVCGEgjnxHUnpIVh3AGuk62rP3tqVrn+yab0YH7UULisC085+NYH+mnqf3Wx4SpSi1RQMwudL89N03g==", + "dev": true, + "license": "MIT", + "dependencies": { + "clone": "^2.1.2", + "clone-stats": "^1.0.0", + "remove-trailing-separator": "^1.1.0", + "replace-ext": "^2.0.0", + "teex": "^1.0.1" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/vinyl-fs": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/vinyl-fs/-/vinyl-fs-4.0.0.tgz", + "integrity": "sha512-7GbgBnYfaquMk3Qu9g22x000vbYkOex32930rBnc3qByw6HfMEAoELjCjoJv4HuEQxHAurT+nvMHm6MnJllFLw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fs-mkdirp-stream": "^2.0.1", + "glob-stream": "^8.0.0", + "graceful-fs": "^4.2.11", + "iconv-lite": "^0.6.3", + "is-valid-glob": "^1.0.0", + "lead": "^4.0.0", + "normalize-path": "3.0.0", + "resolve-options": "^2.0.0", + "stream-composer": "^1.0.2", + "streamx": "^2.14.0", + "to-through": "^3.0.0", + "value-or-function": "^4.0.0", + "vinyl": "^3.0.0", + "vinyl-sourcemap": "^2.0.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/vinyl-fs/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/vinyl-fs/node_modules/replace-ext": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/replace-ext/-/replace-ext-2.0.0.tgz", + "integrity": "sha512-UszKE5KVK6JvyD92nzMn9cDapSk6w/CaFZ96CnmDMUqH9oowfxF/ZjRITD25H4DnOQClLA4/j7jLGXXLVKxAug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, + "node_modules/vinyl-fs/node_modules/vinyl": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-3.0.0.tgz", + "integrity": "sha512-rC2VRfAVVCGEgjnxHUnpIVh3AGuk62rP3tqVrn+yab0YH7UULisC085+NYH+mnqf3Wx4SpSi1RQMwudL89N03g==", + "dev": true, + "license": "MIT", + "dependencies": { + "clone": "^2.1.2", + "clone-stats": "^1.0.0", + "remove-trailing-separator": "^1.1.0", + "replace-ext": "^2.0.0", + "teex": "^1.0.1" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/vinyl-sourcemap": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/vinyl-sourcemap/-/vinyl-sourcemap-2.0.0.tgz", + "integrity": "sha512-BAEvWxbBUXvlNoFQVFVHpybBbjW1r03WhohJzJDSfgrrK5xVYIDTan6xN14DlyImShgDRv2gl9qhM6irVMsV0Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "convert-source-map": "^2.0.0", + "graceful-fs": "^4.2.10", + "now-and-later": "^3.0.0", + "streamx": "^2.12.5", + "vinyl": "^3.0.0", + "vinyl-contents": "^2.0.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/vinyl-sourcemap/node_modules/replace-ext": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/replace-ext/-/replace-ext-2.0.0.tgz", + "integrity": "sha512-UszKE5KVK6JvyD92nzMn9cDapSk6w/CaFZ96CnmDMUqH9oowfxF/ZjRITD25H4DnOQClLA4/j7jLGXXLVKxAug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, + "node_modules/vinyl-sourcemap/node_modules/vinyl": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-3.0.0.tgz", + "integrity": "sha512-rC2VRfAVVCGEgjnxHUnpIVh3AGuk62rP3tqVrn+yab0YH7UULisC085+NYH+mnqf3Wx4SpSi1RQMwudL89N03g==", + "dev": true, + "license": "MIT", + "dependencies": { + "clone": "^2.1.2", + "clone-stats": "^1.0.0", + "remove-trailing-separator": "^1.1.0", + "replace-ext": "^2.0.0", + "teex": "^1.0.1" + }, + "engines": { + "node": ">=10.13.0" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -3852,12 +6200,79 @@ "node": ">=0.10.0" } }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "dev": true }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs-parser": { + "version": "20.2.9", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, "node_modules/yn": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", diff --git a/package.json b/package.json index 04e6780..06ec2f8 100644 --- a/package.json +++ b/package.json @@ -2,14 +2,18 @@ "name": "jankclient", "version": "0.1.0", "description": "A SpaceBar Client written in JS HTML and CSS to run, clone the repo and do either `node index.js` or `bun index.js` both bun and node are supported, and both should function as expected, if there are any problems with Jank Client on things that aren't linux, please let me know. To access Jank Client after init simply go to http://localhost:8080/login and login with your username and password.", - "main": "index.js", + "main": ".dist/index.js", + "type": "module", "scripts": { "lint": "eslint .", - "start": "node index.js" + "build": "npx gulp", + "start": "node dist/index.js" }, "author": "MathMan05", "license": "GPL-3.0", "dependencies": { + "@types/express": "^4.17.21", + "@types/node-fetch": "^2.6.11", "compression": "^1.7.4", "express": "^4.19.2", "ts-to-jsdoc": "^2.2.0" @@ -19,13 +23,17 @@ "@html-eslint/eslint-plugin": "^0.25.0", "@html-eslint/parser": "^0.25.0", "@stylistic/eslint-plugin": "^2.3.0", + "@types/compression": "^1.7.5", "@types/eslint__js": "^8.42.3", "eslint": "^8.57.0", "eslint-plugin-html": "^8.1.1", "eslint-plugin-sonarjs": "^1.0.4", "eslint-plugin-unicorn": "^55.0.0", + "gulp": "^5.0.0", + "gulp-copy": "^5.0.0", + "gulp-typescript": "^6.0.0-alpha.1", "ts-node": "^10.9.2", "typescript": "^5.5.4", "typescript-eslint": "^7.17.0" } -} +} \ No newline at end of file diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..db88eb0 --- /dev/null +++ b/src/index.ts @@ -0,0 +1,116 @@ +#!/usr/bin/env node + +import compression from "compression"; +import express, { Request, Response } from "express"; +import fs from "node:fs"; +import fetch from "node-fetch"; +import path from "path"; +import { observe, uptime } from "./stats.js"; +import { getApiUrls, inviteResponse } from "./utils.js"; + +interface Instance { + name: string; + [key: string]: any; +} + +const app = express(); +import instances from "./webpage/instances.json"; +const instanceNames = new Map(); + +for (const instance of instances) { + instanceNames.set(instance.name, instance); +} + +app.use(compression()); + +async function updateInstances(): Promise { + try { + const response = await fetch( + "https://raw.githubusercontent.com/spacebarchat/spacebarchat/master/instances/instances.json" + ); + const json: Instance[] = await response.json(); + for (const instance of json) { + if (!instanceNames.has(instance.name)) { + instances.push(instance as any); + } else { + const existingInstance = instanceNames.get(instance.name); + if (existingInstance) { + for (const key of Object.keys(instance)) { + if (!existingInstance[key]) { + existingInstance[key] = instance[key]; + } + } + } + } + } + observe(instances); + } catch (error) { + console.error("Error updating instances:", error); + } +} + +updateInstances(); + +app.use("/getupdates", (_req: Request, res: Response) => { + try { + const stats = fs.statSync(path.join(__dirname, "webpage")); + res.send(stats.mtimeMs.toString()); + } catch (error) { + console.error("Error getting updates:", error); + res.status(500).send("Error getting updates"); + } +}); + +app.use("/services/oembed", (req: Request, res: Response) => { + inviteResponse(req, res); +}); + +app.use("/uptime", (req: Request, res: Response) => { + const instanceUptime = uptime[req.query.name as string]; + res.send(instanceUptime); +}); + +app.use("/", async (req: Request, res: Response) => { + const scheme = req.secure ? "https" : "http"; + const host = `${scheme}://${req.get("Host")}`; + const ref = host + req.originalUrl; + + if (host && ref) { + const link = `${host}/services/oembed?url=${encodeURIComponent(ref)}`; + res.set( + "Link", + `<${link}>; rel="alternate"; type="application/json+oembed"; title="Jank Client oEmbed format"` + ); + } + + if (req.path === "/") { + res.sendFile(path.join(__dirname, "webpage", "home.html")); + return; + } + + if (req.path.startsWith("/instances.json")) { + res.json(instances); + return; + } + + if (req.path.startsWith("/invite/")) { + res.sendFile(path.join(__dirname, "webpage", "invite.html")); + return; + } + + const filePath = path.join(__dirname, "webpage", req.path); + if (fs.existsSync(filePath)) { + res.sendFile(filePath); + } else if (fs.existsSync(`${filePath}.html`)) { + res.sendFile(`${filePath}.html`); + } else { + res.sendFile(path.join(__dirname, "webpage", "index.html")); + } +}); + +const PORT = process.env.PORT || Number(process.argv[2]) || 8080; +app.listen(PORT, () => { + console.log(`Server running on port ${PORT}`); +}); + +export { getApiUrls }; diff --git a/src/stats.ts b/src/stats.ts new file mode 100644 index 0000000..af790eb --- /dev/null +++ b/src/stats.ts @@ -0,0 +1,247 @@ +import fs from "node:fs"; +import path from "path"; +import fetch from "node-fetch"; +import { getApiUrls } from "./utils.js"; + +interface UptimeEntry { + time: number; + online: boolean; +} + +interface UptimeObject { + [key: string]: UptimeEntry[]; +} + +interface Instance { + name: string; + urls?: { api: string }; + url?: string; + online?: boolean; + uptime?: { + daytime: number; + weektime: number; + alltime: number; + }; +} + +let uptimeObject: UptimeObject = loadUptimeObject(); +export { uptimeObject as uptime }; + +function loadUptimeObject(): UptimeObject { + const filePath = path.join(__dirname, "..", "uptime.json"); + if (fs.existsSync(filePath)) { + try { + return JSON.parse(fs.readFileSync(filePath, "utf8")); + } catch (error) { + console.error("Error reading uptime.json:", error); + return {}; + } + } + return {}; +} + +function saveUptimeObject(): void { + fs.writeFile( + `${__dirname}/uptime.json`, + JSON.stringify(uptimeObject), + (error) => { + if (error) { + console.error("Error saving uptime.json:", error); + } + } + ); +} + +function removeUndefinedKey(): void { + if (uptimeObject.undefined) { + delete uptimeObject.undefined; + saveUptimeObject(); + } +} + +removeUndefinedKey(); + +export async function observe(instances: Instance[]): Promise { + const activeInstances = new Set(); + const instancePromises = instances.map((instance) => + resolveInstance(instance, activeInstances) + ); + await Promise.allSettled(instancePromises); + updateInactiveInstances(activeInstances); +} + +async function resolveInstance( + instance: Instance, + activeInstances: Set +): Promise { + try { + calcStats(instance); + const api = await getApiUrl(instance); + if (!api) { + handleUnresolvedApi(instance); + return; + } + activeInstances.add(instance.name); + scheduleHealthCheck(instance, api); + } catch (error) { + console.error("Error resolving instance:", error); + } +} + +async function getApiUrl(instance: Instance): Promise { + if (instance.urls) { + return instance.urls.api; + } + if (instance.url) { + const urls = await getApiUrls(instance.url); + return urls ? urls.api : null; + } + return null; +} + +function handleUnresolvedApi(instance: Instance): void { + setStatus(instance, false); + console.warn(`${instance.name} does not resolve api URL`, instance); + setTimeout(() => resolveInstance(instance, new Set()), 1000 * 60 * 30); +} + +function scheduleHealthCheck(instance: Instance, api: string): void { + const checkInterval = 1000 * 60 * 30; + const initialDelay = Math.random() * 1000 * 60 * 10; + setTimeout(() => { + checkHealth(instance, api); + setInterval(() => checkHealth(instance, api), checkInterval); + }, initialDelay); +} + +async function checkHealth( + instance: Instance, + api: string, + tries = 0 +): Promise { + try { + const response = await fetch(`${api}ping`, { method: "HEAD" }); + if (response.ok || tries > 3) { + setStatus(instance, response.ok); + } else { + retryHealthCheck(instance, api, tries); + } + } catch (error) { + console.error("Error checking health:", error); + if (tries > 3) { + setStatus(instance, false); + } else { + retryHealthCheck(instance, api, tries); + } + } +} + +function retryHealthCheck( + instance: Instance, + api: string, + tries: number +): void { + setTimeout(() => checkHealth(instance, api, tries + 1), 30000); +} + +function updateInactiveInstances(activeInstances: Set): void { + for (const key of Object.keys(uptimeObject)) { + if (!activeInstances.has(key)) { + setStatus(key, false); + } + } +} + +function calcStats(instance: Instance): void { + const obj = uptimeObject[instance.name]; + if (!obj) return; + + const now = Date.now(); + const day = now - 1000 * 60 * 60 * 24; + const week = now - 1000 * 60 * 60 * 24 * 7; + + let totalTimePassed = 0; + let alltime = 0; + let daytime = 0; + let weektime = 0; + let online = false; + + for (let i = 0; i < obj.length; i++) { + const entry = obj[i]; + online = entry.online; + const stamp = entry.time; + const nextStamp = obj[i + 1]?.time || now; + const timePassed = nextStamp - stamp; + + totalTimePassed += timePassed; + alltime += Number(online) * timePassed; + + if (stamp + timePassed > week) { + const weekTimePassed = Math.min(timePassed, nextStamp - week); + weektime += Number(online) * weekTimePassed; + + if (stamp + timePassed > day) { + const dayTimePassed = Math.min(weekTimePassed, nextStamp - day); + daytime += Number(online) * dayTimePassed; + } + } + } + + instance.online = online; + instance.uptime = calculateUptimeStats( + totalTimePassed, + alltime, + daytime, + weektime, + online + ); +} + +function calculateUptimeStats( + totalTimePassed: number, + alltime: number, + daytime: number, + weektime: number, + online: boolean +): { daytime: number; weektime: number; alltime: number } { + const dayInMs = 1000 * 60 * 60 * 24; + const weekInMs = dayInMs * 7; + + alltime /= totalTimePassed; + + if (totalTimePassed > dayInMs) { + daytime = daytime || (online ? dayInMs : 0); + daytime /= dayInMs; + + if (totalTimePassed > weekInMs) { + weektime = weektime || (online ? weekInMs : 0); + weektime /= weekInMs; + } else { + weektime = alltime; + } + } else { + weektime = alltime; + daytime = alltime; + } + + return { daytime, weektime, alltime }; +} + +function setStatus(instance: string | Instance, status: boolean): void { + const name = typeof instance === "string" ? instance : instance.name; + let obj = uptimeObject[name]; + + if (!obj) { + obj = []; + uptimeObject[name] = obj; + } + + if (obj.at(-1)?.online !== status) { + obj.push({ time: Date.now(), online: status }); + saveUptimeObject(); + } + + if (typeof instance !== "string") { + calcStats(instance); + } +} diff --git a/src/utils.ts b/src/utils.ts new file mode 100644 index 0000000..5445419 --- /dev/null +++ b/src/utils.ts @@ -0,0 +1,114 @@ +import fetch from "node-fetch"; +import { Request, Response } from "express"; + +interface ApiUrls { + api: string; + gateway: string; + cdn: string; + wellknown: string; +} + +interface Invite { + guild: { + name: string; + description?: string; + icon?: string; + id: string; + }; + inviter?: { + username: string; + }; +} + +export async function getApiUrls(url: string): Promise { + if (!url.endsWith("/")) { + url += "/"; + } + try { + const info = await fetch(`${url}.well-known/spacebar`).then((res) => + res.json() + ); + const api = info.api; + const apiUrl = new URL(api); + const policies = await fetch( + `${api}${ + apiUrl.pathname.includes("api") ? "" : "api" + }/policies/instance/domains` + ).then((res) => res.json()); + return { + api: policies.apiEndpoint, + gateway: policies.gateway, + cdn: policies.cdn, + wellknown: url, + }; + } catch (error) { + console.error("Error fetching API URLs:", error); + return null; + } +} + +export async function inviteResponse( + req: Request, + res: Response +): Promise { + let url: URL; + if (URL.canParse(req.query.url as string)) { + url = new URL(req.query.url as string); + } else { + const scheme = req.secure ? "https" : "http"; + const host = `${scheme}://${req.get("Host")}`; + url = new URL(host); + } + + try { + if (url.pathname.startsWith("invite")) { + throw new Error("Invalid invite URL"); + } + + const code = url.pathname.split("/")[2]; + const instance = url.searchParams.get("instance"); + if (!instance) { + throw new Error("Instance not specified"); + } + const urls = await getApiUrls(instance); + if (!urls) { + throw new Error("Failed to get API URLs"); + } + + const invite = await fetch(`${urls.api}/invites/${code}`).then( + (res) => res.json() as Promise + ); + const title = invite.guild.name; + const description = invite.inviter + ? `${invite.inviter.username} has invited you to ${invite.guild.name}${ + invite.guild.description ? `\n${invite.guild.description}` : "" + }` + : `You've been invited to ${invite.guild.name}${ + invite.guild.description ? `\n${invite.guild.description}` : "" + }`; + const thumbnail = invite.guild.icon + ? `${urls.cdn}/icons/${invite.guild.id}/${invite.guild.icon}.png` + : ""; + + const jsonResponse = { + type: "link", + version: "1.0", + title, + thumbnail, + description, + }; + + res.json(jsonResponse); + } catch (error) { + console.error("Error processing invite response:", error); + const jsonResponse = { + type: "link", + version: "1.0", + title: "Jank Client", + thumbnail: "/logo.webp", + description: "A spacebar client that has DMs, replying and more", + url: url.toString(), + }; + res.json(jsonResponse); + } +} diff --git a/src/webpage/audio.ts b/src/webpage/audio.ts new file mode 100644 index 0000000..dfdac5d --- /dev/null +++ b/src/webpage/audio.ts @@ -0,0 +1,164 @@ +import { getBulkInfo } from "./login.js"; + +class Voice { + audioCtx: AudioContext; + info: { wave: string | Function; freq: number }; + playing: boolean; + myArrayBuffer: AudioBuffer; + gainNode: GainNode; + buffer: Float32Array; + source: AudioBufferSourceNode; + constructor(wave: string | Function, freq: number, volume = 1) { + this.audioCtx = new window.AudioContext(); + this.info = { wave, freq }; + this.playing = false; + this.myArrayBuffer = this.audioCtx.createBuffer( + 1, + this.audioCtx.sampleRate, + this.audioCtx.sampleRate + ); + this.gainNode = this.audioCtx.createGain(); + this.gainNode.gain.value = volume; + this.gainNode.connect(this.audioCtx.destination); + this.buffer = this.myArrayBuffer.getChannelData(0); + this.source = this.audioCtx.createBufferSource(); + this.source.buffer = this.myArrayBuffer; + this.source.loop = true; + this.source.start(); + this.updateWave(); + } + get wave(): string | Function { + return this.info.wave; + } + get freq(): number { + return this.info.freq; + } + set wave(wave: string | Function) { + this.info.wave = wave; + this.updateWave(); + } + set freq(freq: number) { + this.info.freq = freq; + this.updateWave(); + } + updateWave(): void { + const func = this.waveFunction(); + for (let i = 0; i < this.buffer.length; i++) { + this.buffer[i] = func(i / this.audioCtx.sampleRate, this.freq); + } + } + waveFunction(): Function { + if (typeof this.wave === "function") { + return this.wave; + } + switch (this.wave) { + case "sin": + return (t: number, freq: number) => { + return Math.sin(t * Math.PI * 2 * freq); + }; + case "triangle": + return (t: number, freq: number) => { + return Math.abs(((4 * t * freq) % 4) - 2) - 1; + }; + case "sawtooth": + return (t: number, freq: number) => { + return ((t * freq) % 1) * 2 - 1; + }; + case "square": + return (t: number, freq: number) => { + return (t * freq) % 2 < 1 ? 1 : -1; + }; + case "white": + return (_t: number, _freq: number) => { + return Math.random() * 2 - 1; + }; + case "noise": + return (_t: number, _freq: number) => { + return 0; + }; + } + return new Function(); + } + play(): void { + if (this.playing) { + return; + } + this.source.connect(this.gainNode); + this.playing = true; + } + stop(): void { + if (this.playing) { + this.source.disconnect(); + this.playing = false; + } + } + static noises(noise: string): void { + switch (noise) { + case "three": { + const voicy = new Voice("sin", 800); + voicy.play(); + setTimeout((_) => { + voicy.freq = 1000; + }, 50); + setTimeout((_) => { + voicy.freq = 1300; + }, 100); + setTimeout((_) => { + voicy.stop(); + }, 150); + break; + } + case "zip": { + const voicy = new Voice((t: number, freq: number) => { + return Math.sin((t + 2) ** Math.cos(t * 4) * Math.PI * 2 * freq); + }, 700); + voicy.play(); + setTimeout((_) => { + voicy.stop(); + }, 150); + break; + } + case "square": { + const voicy = new Voice("square", 600, 0.4); + voicy.play(); + setTimeout((_) => { + voicy.freq = 800; + }, 50); + setTimeout((_) => { + voicy.freq = 1000; + }, 100); + setTimeout((_) => { + voicy.stop(); + }, 150); + break; + } + case "beep": { + const voicy = new Voice("sin", 800); + voicy.play(); + setTimeout((_) => { + voicy.stop(); + }, 50); + setTimeout((_) => { + voicy.play(); + }, 100); + setTimeout((_) => { + voicy.stop(); + }, 150); + break; + } + } + } + static get sounds() { + return ["three", "zip", "square", "beep"]; + } + static setNotificationSound(sound: string) { + const userinfos = getBulkInfo(); + userinfos.preferences.notisound = sound; + localStorage.setItem("userinfos", JSON.stringify(userinfos)); + } + static getNotificationSound() { + const userinfos = getBulkInfo(); + return userinfos.preferences.notisound; + } +} +export { Voice }; diff --git a/src/webpage/channel.ts b/src/webpage/channel.ts new file mode 100644 index 0000000..4e12262 --- /dev/null +++ b/src/webpage/channel.ts @@ -0,0 +1,1415 @@ +"use strict"; +import { Message } from "./message.js"; +import { Voice } from "./audio.js"; +import { Contextmenu } from "./contextmenu.js"; +import { Dialog } from "./dialog.js"; +import { Guild } from "./guild.js"; +import { Localuser } from "./localuser.js"; +import { Permissions } from "./permissions.js"; +import { Settings } from "./settings.js"; +import { Role, RoleList } from "./role.js"; +import { InfiniteScroller } from "./infiniteScroller.js"; +import { SnowFlake } from "./snowflake.js"; +import { + channeljson, + embedjson, + messageCreateJson, + messagejson, + readyjson, + startTypingjson, +} from "./jsontypes.js"; +import { MarkDown } from "./markdown.js"; +import { Member } from "./member.js"; + +declare global { + interface NotificationOptions { + image?: string | null | undefined; + } +} +class Channel extends SnowFlake { + editing!: Message | null; + type!: number; + owner!: Guild; + headers!: Localuser["headers"]; + name!: string; + parent_id?: string; + parent: Channel | undefined; + children!: Channel[]; + guild_id!: string; + permission_overwrites!: Map; + permission_overwritesar!: [Role, Permissions][]; + topic!: string; + nsfw!: boolean; + position: number = 0; + lastreadmessageid: string | undefined; + lastmessageid: string | undefined; + mentions!: number; + lastpin!: string; + move_id?: string; + typing!: number; + message_notifications!: number; + allthewayup!: boolean; + static contextmenu = new Contextmenu("channel menu"); + replyingto!: Message | null; + infinite!: InfiniteScroller; + idToPrev: Map = new Map(); + idToNext: Map = new Map(); + messages: Map = new Map(); + static setupcontextmenu() { + this.contextmenu.addbutton("Copy channel id", function (this: Channel) { + navigator.clipboard.writeText(this.id); + }); + + this.contextmenu.addbutton("Mark as read", function (this: Channel) { + this.readbottom(); + }); + + this.contextmenu.addbutton("Settings[temp]", function (this: Channel) { + this.generateSettings(); + }); + + this.contextmenu.addbutton( + "Delete channel", + function (this: Channel) { + this.deleteChannel(); + }, + null, + function () { + return this.isAdmin(); + } + ); + + this.contextmenu.addbutton( + "Edit channel", + function (this: Channel) { + this.editChannel(); + }, + null, + function () { + return this.isAdmin(); + } + ); + + this.contextmenu.addbutton( + "Make invite", + function (this: Channel) { + this.createInvite(); + }, + null, + function () { + return this.hasPermission("CREATE_INSTANT_INVITE") && this.type !== 4; + } + ); + /* + this.contextmenu.addbutton("Test button",function(){ + this.localuser.ws.send(JSON.stringify({ + "op": 14, + "d": { + "guild_id": this.guild.id, + "channels": { + [this.id]: [ + [ + 0, + 99 + ] + ] + } + } + })) + },null); + /**/ + } + createInvite() { + const div = document.createElement("div"); + div.classList.add("invitediv"); + const text = document.createElement("span"); + div.append(text); + let uses = 0; + let expires = 1800; + const copycontainer = document.createElement("div"); + copycontainer.classList.add("copycontainer"); + const copy = document.createElement("span"); + copy.classList.add("copybutton", "svgtheme", "svg-copy"); + copycontainer.append(copy); + copycontainer.onclick = (_) => { + if (text.textContent) { + navigator.clipboard.writeText(text.textContent); + } + }; + div.append(copycontainer); + const update = () => { + fetch(`${this.info.api}/channels/${this.id}/invites`, { + method: "POST", + headers: this.headers, + body: JSON.stringify({ + flags: 0, + target_type: null, + target_user_id: null, + max_age: expires + "", + max_uses: uses, + temporary: uses !== 0, + }), + }) + .then((_) => _.json()) + .then((json) => { + const params = new URLSearchParams(""); + params.set("instance", this.info.wellknown); + const encoded = params.toString(); + text.textContent = `${location.origin}/invite/${json.code}?${encoded}`; + }); + }; + update(); + new Dialog([ + "vdiv", + ["title", "Invite people"], + ["text", `to #${this.name} in ${this.guild.properties.name}`], + [ + "select", + "Expire after:", + [ + "30 Minutes", + "1 Hour", + "6 Hours", + "12 Hours", + "1 Day", + "7 Days", + "30 Days", + "Never", + ], + function (_e: Event) { + /* + let expires: number = [ + 1800, 3600, 21600, 43200, 86400, 604800, 2592000, 0, + ][(e.srcElement as HTMLSelectElement).selectedIndex]; + */ + update(); + }, + 0, + ], + [ + "select", + "Max uses:", + [ + "No limit", + "1 use", + "5 uses", + "10 uses", + "25 uses", + "50 uses", + "100 uses", + ], + function (_e: Event) { + /* + let uses: number = [0, 1, 5, 10, 25, 50, 100][ + (e.srcElement as HTMLSelectElement).selectedIndex + ]; + */ + update(); + }, + 0, + ], + ["html", div], + ]).show(); + } + generateSettings() { + this.sortPerms(); + const settings = new Settings("Settings for " + this.name); + + const s1 = settings.addButton("roles"); + + s1.options.push( + new RoleList( + this.permission_overwritesar, + this.guild, + this.updateRolePermissions.bind(this), + true + ) + ); + settings.show(); + } + sortPerms() { + this.permission_overwritesar.sort((a, b) => { + return ( + this.guild.roles.findIndex((_) => _ === a[0]) - + this.guild.roles.findIndex((_) => _ === b[0]) + ); + }); + } + setUpInfiniteScroller() { + this.infinite = new InfiniteScroller( + async (id: string, offset: number): Promise => { + if (offset === 1) { + if (this.idToPrev.has(id)) { + return this.idToPrev.get(id); + } else { + await this.grabBefore(id); + return this.idToPrev.get(id); + } + } else { + if (this.idToNext.has(id)) { + return this.idToNext.get(id); + } else if (this.lastmessage?.id !== id) { + await this.grabAfter(id); + return this.idToNext.get(id); + } else { + return; + } + } + }, + async (id: string): Promise => { + //await new Promise(_=>{setTimeout(_,Math.random()*10)}) + const messgage = this.messages.get(id); + try { + if (messgage) { + return messgage.buildhtml(); + } else { + console.error(id + " not found"); + } + } catch (e) { + console.error(e); + } + return document.createElement("div"); + }, + async (id: string) => { + const message = this.messages.get(id); + try { + if (message) { + message.deleteDiv(); + return true; + } + } catch (e) { + console.error(e); + } finally { + } + return false; + }, + this.readbottom.bind(this) + ); + } + constructor( + json: channeljson | -1, + owner: Guild, + id: string = json === -1 ? "" : json.id + ) { + super(id); + if (json === -1) { + return; + } + this.editing; + this.type = json.type; + this.owner = owner; + this.headers = this.owner.headers; + this.name = json.name; + if (json.parent_id) { + this.parent_id = json.parent_id; + } + this.parent = undefined; + this.children = []; + this.guild_id = json.guild_id; + this.permission_overwrites = new Map(); + this.permission_overwritesar = []; + for (const thing of json.permission_overwrites) { + if ( + thing.id === "1182819038095799904" || + thing.id === "1182820803700625444" + ) { + continue; + } + if (!this.permission_overwrites.has(thing.id)) { + //either a bug in the server requires this, or the API is cursed + this.permission_overwrites.set( + thing.id, + new Permissions(thing.allow, thing.deny) + ); + const permission = this.permission_overwrites.get(thing.id); + if (permission) { + const role = this.guild.roleids.get(thing.id); + if (role) { + this.permission_overwritesar.push([role, permission]); + } + } + } + } + + this.topic = json.topic; + this.nsfw = json.nsfw; + this.position = json.position; + this.lastreadmessageid = undefined; + if (json.last_message_id) { + this.lastmessageid = json.last_message_id; + } else { + this.lastmessageid = undefined; + } + this.setUpInfiniteScroller(); + this.perminfo ??= {}; + } + get perminfo() { + return this.guild.perminfo.channels[this.id]; + } + set perminfo(e) { + this.guild.perminfo.channels[this.id] = e; + } + isAdmin() { + return this.guild.isAdmin(); + } + get guild() { + return this.owner; + } + get localuser() { + return this.guild.localuser; + } + get info() { + return this.owner.info; + } + readStateInfo(json: readyjson["d"]["read_state"]["entries"][0]) { + this.lastreadmessageid = json.last_message_id; + this.mentions = json.mention_count; + this.mentions ??= 0; + this.lastpin = json.last_pin_timestamp; + } + get hasunreads(): boolean { + if (!this.hasPermission("VIEW_CHANNEL")) { + return false; + } + return ( + !!this.lastmessageid && + (!this.lastreadmessageid || + SnowFlake.stringToUnixTime(this.lastmessageid) > + SnowFlake.stringToUnixTime(this.lastreadmessageid)) && + this.type !== 4 + ); + } + hasPermission(name: string, member = this.guild.member): boolean { + if (member.isAdmin()) { + return true; + } + for (const thing of member.roles) { + let premission = this.permission_overwrites.get(thing.id); + if (premission) { + const perm = premission.getPermission(name); + if (perm) { + return perm === 1; + } + } + if (thing.permissions.getPermission(name)) { + return true; + } + } + return false; + } + get canMessage(): boolean { + if ( + this.permission_overwritesar.length === 0 && + this.hasPermission("MANAGE_CHANNELS") + ) { + const role = this.guild.roles.find((_) => _.name === "@everyone"); + if (role) { + this.addRoleToPerms(role); + } + } + return this.hasPermission("SEND_MESSAGES"); + } + sortchildren() { + this.children.sort((a, b) => { + return a.position - b.position; + }); + } + resolveparent(_guild: Guild) { + const parentid = this.parent_id; + if (!parentid) return false; + this.parent = this.localuser.channelids.get(parentid); + this.parent ??= undefined; + if (this.parent !== undefined) { + this.parent.children.push(this); + } + return this.parent !== undefined; + } + calculateReorder() { + let position = -1; + const build: { + id: string; + position: number | undefined; + parent_id: string | undefined; + }[] = []; + for (const thing of this.children) { + const thisthing: { + id: string; + position: number | undefined; + parent_id: string | undefined; + } = { id: thing.id, position: undefined, parent_id: undefined }; + if (thing.position < position) { + thing.position = thisthing.position = position + 1; + } + position = thing.position; + if (thing.move_id && thing.move_id !== thing.parent_id) { + thing.parent_id = thing.move_id; + thisthing.parent_id = thing.parent?.id; + thing.move_id = undefined; + //console.log(this.guild.channelids[thisthing.parent_id.id]); + } + if (thisthing.position || thisthing.parent_id) { + build.push(thisthing); + } + } + return build; + } + static dragged: [Channel, HTMLDivElement] | [] = []; + html: WeakRef | undefined; + get visable() { + return this.hasPermission("VIEW_CHANNEL"); + } + createguildHTML(admin = false): HTMLDivElement { + const div = document.createElement("div"); + this.html = new WeakRef(div); + if (!this.visable) { + let quit = true; + for (const thing of this.children) { + if (thing.visable) { + quit = false; + } + } + if (quit) { + return div; + } + } + // @ts-ignore I dont wanna deal with this + div["all"] = this; + div.draggable = admin; + div.addEventListener("dragstart", (e) => { + Channel.dragged = [this, div]; + e.stopImmediatePropagation(); + }); + div.addEventListener("dragend", () => { + Channel.dragged = []; + }); + if (this.type === 4) { + this.sortchildren(); + const caps = document.createElement("div"); + + const decdiv = document.createElement("div"); + const decoration = document.createElement("span"); + decoration.classList.add("svgtheme", "collapse-icon", "svg-category"); + decdiv.appendChild(decoration); + + const myhtml = document.createElement("p2"); + myhtml.textContent = this.name; + decdiv.appendChild(myhtml); + caps.appendChild(decdiv); + const childrendiv = document.createElement("div"); + if (admin) { + const addchannel = document.createElement("span"); + addchannel.textContent = "+"; + addchannel.classList.add("addchannel"); + caps.appendChild(addchannel); + addchannel.onclick = (_) => { + this.guild.createchannels(this.createChannel.bind(this)); + }; + this.coatDropDiv(decdiv, childrendiv); + } + div.appendChild(caps); + caps.classList.add("capsflex"); + decdiv.classList.add("channeleffects"); + decdiv.classList.add("channel"); + + Channel.contextmenu.bindContextmenu(decdiv, this, undefined); + // @ts-ignore I dont wanna deal with this + decdiv["all"] = this; + + for (const channel of this.children) { + childrendiv.appendChild(channel.createguildHTML(admin)); + } + childrendiv.classList.add("channels"); + setTimeout((_: any) => { + if (!this.perminfo.collapsed) { + childrendiv.style.height = childrendiv.scrollHeight + "px"; + } + }, 100); + div.appendChild(childrendiv); + if (this.perminfo.collapsed) { + decoration.classList.add("hiddencat"); + childrendiv.style.height = "0px"; + } + decdiv.onclick = () => { + if (childrendiv.style.height !== "0px") { + decoration.classList.add("hiddencat"); + this.perminfo.collapsed = true; + this.localuser.userinfo.updateLocal(); + childrendiv.style.height = "0px"; + } else { + decoration.classList.remove("hiddencat"); + this.perminfo.collapsed = false; + this.localuser.userinfo.updateLocal(); + childrendiv.style.height = childrendiv.scrollHeight + "px"; + } + }; + } else { + div.classList.add("channel"); + if (this.hasunreads) { + div.classList.add("cunread"); + } + Channel.contextmenu.bindContextmenu(div, this, undefined); + if (admin) { + this.coatDropDiv(div); + } + // @ts-ignore I dont wanna deal with this + div["all"] = this; + const myhtml = document.createElement("span"); + myhtml.textContent = this.name; + if (this.type === 0) { + const decoration = document.createElement("span"); + div.appendChild(decoration); + decoration.classList.add("space", "svgtheme", "svg-channel"); + } else if (this.type === 2) { + // + const decoration = document.createElement("span"); + div.appendChild(decoration); + decoration.classList.add("space", "svgtheme", "svg-voice"); + } else if (this.type === 5) { + // + const decoration = document.createElement("span"); + div.appendChild(decoration); + decoration.classList.add("space", "svgtheme", "svg-announce"); + } else { + console.log(this.type); + } + div.appendChild(myhtml); + div.onclick = (_) => { + this.getHTML(); + }; + } + return div; + } + get myhtml() { + if (this.html) { + return this.html.deref(); + } else { + return undefined; + } + } + readbottom() { + if (!this.hasunreads) { + return; + } + fetch( + this.info.api + + "/channels/" + + this.id + + "/messages/" + + this.lastmessageid + + "/ack", + { + method: "POST", + headers: this.headers, + body: JSON.stringify({}), + } + ); + this.lastreadmessageid = this.lastmessageid; + this.guild.unreads(); + if (this.myhtml) { + this.myhtml.classList.remove("cunread"); + } + } + coatDropDiv(div: HTMLDivElement, container: HTMLElement | boolean = false) { + div.addEventListener("dragenter", (event) => { + console.log("enter"); + event.preventDefault(); + }); + + div.addEventListener("dragover", (event) => { + event.preventDefault(); + }); + + div.addEventListener("drop", (event) => { + const that = Channel.dragged[0]; + if (!that) return; + event.preventDefault(); + if (container) { + that.move_id = this.id; + if (that.parent) { + that.parent.children.splice(that.parent.children.indexOf(that), 1); + } + that.parent = this; + (container as HTMLElement).prepend( + Channel.dragged[1] as HTMLDivElement + ); + this.children.unshift(that); + } else { + console.log(this, Channel.dragged); + that.move_id = this.parent_id; + if (that.parent) { + that.parent.children.splice(that.parent.children.indexOf(that), 1); + } else { + this.guild.headchannels.splice( + this.guild.headchannels.indexOf(that), + 1 + ); + } + that.parent = this.parent; + if (that.parent) { + const build: Channel[] = []; + for (let i = 0; i < that.parent.children.length; i++) { + build.push(that.parent.children[i]); + if (that.parent.children[i] === this) { + build.push(that); + } + } + that.parent.children = build; + } else { + const build: Channel[] = []; + for (let i = 0; i < this.guild.headchannels.length; i++) { + build.push(this.guild.headchannels[i]); + if (this.guild.headchannels[i] === this) { + build.push(that); + } + } + this.guild.headchannels = build; + } + if (Channel.dragged[1]) { + div.after(Channel.dragged[1]); + } + } + this.guild.calculateReorder(); + }); + + return div; + } + createChannel(name: string, type: number) { + fetch(this.info.api + "/guilds/" + this.guild.id + "/channels", { + method: "POST", + headers: this.headers, + body: JSON.stringify({ + name, + type, + parent_id: this.id, + permission_overwrites: [], + }), + }); + } + editChannel() { + let name = this.name; + let topic = this.topic; + let nsfw = this.nsfw; + const thisid = this.id; + const thistype = this.type; + const full = new Dialog([ + "hdiv", + [ + "vdiv", + [ + "textbox", + "Channel name:", + this.name, + function (this: HTMLInputElement) { + name = this.value; + }, + ], + [ + "mdbox", + "Channel topic:", + this.topic, + function (this: HTMLTextAreaElement) { + topic = this.value; + }, + ], + [ + "checkbox", + "NSFW Channel", + this.nsfw, + function (this: HTMLInputElement) { + nsfw = this.checked; + }, + ], + [ + "button", + "", + "submit", + () => { + fetch(this.info.api + "/channels/" + thisid, { + method: "PATCH", + headers: this.headers, + body: JSON.stringify({ + name, + type: thistype, + topic, + bitrate: 64000, + user_limit: 0, + nsfw, + flags: 0, + rate_limit_per_user: 0, + }), + }); + console.log(full); + full.hide(); + }, + ], + ], + ]); + full.show(); + console.log(full); + } + deleteChannel() { + fetch(this.info.api + "/channels/" + this.id, { + method: "DELETE", + headers: this.headers, + }); + } + setReplying(message: Message) { + if (this.replyingto?.div) { + this.replyingto.div.classList.remove("replying"); + } + this.replyingto = message; + if (!this.replyingto?.div) return; + console.log(message); + this.replyingto.div.classList.add("replying"); + this.makereplybox(); + } + makereplybox() { + const replybox = document.getElementById("replybox") as HTMLElement; + if (this.replyingto) { + replybox.innerHTML = ""; + const span = document.createElement("span"); + span.textContent = "Replying to " + this.replyingto.author.username; + const X = document.createElement("button"); + X.onclick = (_) => { + if (this.replyingto?.div) { + this.replyingto.div.classList.remove("replying"); + } + replybox.classList.add("hideReplyBox"); + this.replyingto = null; + replybox.innerHTML = ""; + }; + replybox.classList.remove("hideReplyBox"); + X.textContent = "⦻"; + X.classList.add("cancelReply"); + replybox.append(span); + replybox.append(X); + } else { + replybox.classList.add("hideReplyBox"); + } + } + async getmessage(id: string): Promise { + const message = this.messages.get(id); + if (message) { + return message; + } else { + const gety = await fetch( + this.info.api + + "/channels/" + + this.id + + "/messages?limit=1&around=" + + id, + { headers: this.headers } + ); + const json = await gety.json(); + return new Message(json[0], this); + } + } + static genid: number = 0; + async getHTML() { + const id = ++Channel.genid; + if (this.localuser.channelfocus) { + this.localuser.channelfocus.infinite.delete(); + } + if (this.guild !== this.localuser.lookingguild) { + this.guild.loadGuild(); + } + if (this.localuser.channelfocus && this.localuser.channelfocus.myhtml) { + this.localuser.channelfocus.myhtml.classList.remove("viewChannel"); + } + if (this.myhtml) { + this.myhtml.classList.add("viewChannel"); + } + this.guild.prevchannel = this; + this.guild.perminfo.prevchannel = this.id; + this.localuser.userinfo.updateLocal(); + this.localuser.channelfocus = this; + const prom = this.infinite.delete(); + history.pushState(null, "", "/channels/" + this.guild_id + "/" + this.id); + + this.localuser.pageTitle("#" + this.name); + const channelTopic = document.getElementById( + "channelTopic" + ) as HTMLSpanElement; + if (this.topic) { + channelTopic.innerHTML = new MarkDown( + this.topic, + this + ).makeHTML().innerHTML; + channelTopic.removeAttribute("hidden"); + } else channelTopic.setAttribute("hidden", ""); + + const loading = document.getElementById("loadingdiv") as HTMLDivElement; + Channel.regenLoadingMessages(); + loading.classList.add("loading"); + this.rendertyping(); + await this.putmessages(); + await prom; + if (id !== Channel.genid) { + return; + } + this.makereplybox(); + + await this.buildmessages(); + //loading.classList.remove("loading"); + (document.getElementById("typebox") as HTMLDivElement).contentEditable = + "" + this.canMessage; + } + typingmap: Map = new Map(); + async typingStart(typing: startTypingjson): Promise { + const memb = await Member.new(typing.d.member!, this.guild); + if (!memb) return; + if (memb.id === this.localuser.user.id) { + console.log("you is typing"); + return; + } + console.log("user is typing and you should see it"); + this.typingmap.set(memb, Date.now()); + setTimeout(this.rendertyping.bind(this), 10000); + this.rendertyping(); + } + rendertyping(): void { + const typingtext = document.getElementById("typing") as HTMLDivElement; + let build = ""; + let showing = false; + let i = 0; + const curtime = Date.now() - 5000; + for (const thing of this.typingmap.keys()) { + if ((this.typingmap.get(thing) as number) > curtime) { + if (i !== 0) { + build += ", "; + } + i++; + if (thing.nick) { + build += thing.nick; + } else { + build += thing.user.username; + } + showing = true; + } else { + this.typingmap.delete(thing); + } + } + if (i > 1) { + build += " are typing"; + } else { + build += " is typing"; + } + if (this.localuser.channelfocus === this) { + if (showing) { + typingtext.classList.remove("hidden"); + const typingtext2 = document.getElementById( + "typingtext" + ) as HTMLDivElement; + typingtext2.textContent = build; + } else { + typingtext.classList.add("hidden"); + } + } + } + static regenLoadingMessages() { + const loading = document.getElementById("loadingdiv") as HTMLDivElement; + loading.innerHTML = ""; + for (let i = 0; i < 15; i++) { + const div = document.createElement("div"); + div.classList.add("loadingmessage"); + if (Math.random() < 0.5) { + const pfp = document.createElement("div"); + pfp.classList.add("loadingpfp"); + const username = document.createElement("div"); + username.style.width = Math.floor(Math.random() * 96 * 1.5 + 40) + "px"; + username.classList.add("loadingcontent"); + div.append(pfp, username); + } + const content = document.createElement("div"); + content.style.width = Math.floor(Math.random() * 96 * 3 + 40) + "px"; + content.style.height = Math.floor(Math.random() * 3 + 1) * 20 + "px"; + content.classList.add("loadingcontent"); + div.append(content); + loading.append(div); + } + } + lastmessage: Message | undefined; + async putmessages() { + if (this.allthewayup) { + return; + } + if (this.lastreadmessageid && this.messages.has(this.lastreadmessageid)) { + return; + } + const j = await fetch( + this.info.api + "/channels/" + this.id + "/messages?limit=100", + { + headers: this.headers, + } + ); + + const response = await j.json(); + if (response.length !== 100) { + this.allthewayup = true; + } + let prev: Message | undefined; + for (const thing of response) { + const message = new Message(thing, this); + if (prev) { + this.idToNext.set(message.id, prev.id); + this.idToPrev.set(prev.id, message.id); + } else { + this.lastmessage = message; + this.lastmessageid = message.id; + } + prev = message; + } + } + delChannel(json: channeljson) { + const build: Channel[] = []; + for (const thing of this.children) { + if (thing.id !== json.id) { + build.push(thing); + } + } + this.children = build; + } + async grabAfter(id: string) { + if (id === this.lastmessage?.id) { + return; + } + await fetch( + this.info.api + + "/channels/" + + this.id + + "/messages?limit=100&after=" + + id, + { + headers: this.headers, + } + ) + .then((j) => { + return j.json(); + }) + .then((response) => { + let previd: string = id; + for (const i in response) { + let messager: Message; + let willbreak = false; + if (this.messages.has(response[i].id)) { + messager = this.messages.get(response[i].id) as Message; + willbreak = true; + } else { + messager = new Message(response[i], this); + } + this.idToPrev.set(messager.id, previd); + this.idToNext.set(previd, messager.id); + previd = messager.id; + if (willbreak) { + break; + } + } + //out.buildmessages(); + }); + } + topid!: string; + async grabBefore(id: string) { + if (this.topid && id === this.topid) { + return; + } + + await fetch( + this.info.api + + "/channels/" + + this.id + + "/messages?before=" + + id + + "&limit=100", + { + headers: this.headers, + } + ) + .then((j) => { + return j.json(); + }) + .then((response: messagejson[]) => { + if (response.length < 100) { + this.allthewayup = true; + if (response.length === 0) { + this.topid = id; + } + } + let previd = id; + for (const i in response) { + let messager: Message; + let willbreak = false; + if (this.messages.has(response[i].id)) { + console.log("flaky"); + messager = this.messages.get(response[i].id) as Message; + willbreak = true; + } else { + messager = new Message(response[i], this); + } + + this.idToNext.set(messager.id, previd); + this.idToPrev.set(previd, messager.id); + previd = messager.id; + + if (Number(i) === response.length - 1 && response.length < 100) { + this.topid = previd; + } + if (willbreak) { + break; + } + } + }); + } + /** + * Please dont use this, its not implemented. + * @deprecated + * @todo + **/ + async grabArround(/* id: string */) { + //currently unused and no plans to use it yet + throw new Error("please don't call this, no one has implemented it :P"); + } + async buildmessages() { + this.infinitefocus = false; + this.tryfocusinfinate(); + } + infinitefocus = false; + async tryfocusinfinate() { + if (this.infinitefocus) return; + this.infinitefocus = true; + const messages = document.getElementById("channelw") as HTMLDivElement; + const messageContainers = Array.from( + messages.getElementsByClassName("messagecontainer") + ); + for (const thing of messageContainers) { + thing.remove(); + } + const loading = document.getElementById("loadingdiv") as HTMLDivElement; + const removetitle = document.getElementById("removetitle"); + //messages.innerHTML=""; + let id: string | undefined; + if (this.lastreadmessageid && this.messages.has(this.lastreadmessageid)) { + id = this.lastreadmessageid; + } else if ( + this.lastreadmessageid && + (id = this.findClosest(this.lastreadmessageid)) + ) { + } else if (this.lastmessageid && this.messages.has(this.lastmessageid)) { + id = this.goBackIds(this.lastmessageid, 50); + } + if (!id) { + if (!removetitle) { + const title = document.createElement("h2"); + title.id = "removetitle"; + title.textContent = + "No messages appear to be here, be the first to say something!"; + title.classList.add("titlespace"); + messages.append(title); + } + this.infinitefocus = false; + loading.classList.remove("loading"); + return; + } else if (removetitle) { + removetitle.remove(); + } + if (this.localuser.channelfocus !== this) { + return; + } + const elements = Array.from(messages.getElementsByClassName("scroller")); + for (const elm of elements) { + elm.remove(); + console.warn("rouge element detected and removed"); + } + messages.append(await this.infinite.getDiv(id)); + this.infinite.updatestuff(); + this.infinite.watchForChange().then(async (_) => { + //await new Promise(resolve => setTimeout(resolve, 0)); + this.infinite.focus(id, false); //if someone could figure out how to make this work correctly without this, that's be great :P + loading.classList.remove("loading"); + }); + //this.infinite.focus(id.id,false); + } + private goBackIds( + id: string, + back: number, + returnifnotexistant = true + ): string | undefined { + while (back !== 0) { + const nextid = this.idToPrev.get(id); + if (nextid) { + id = nextid; + back--; + } else { + if (returnifnotexistant) { + break; + } else { + return undefined; + } + } + } + return id; + } + private findClosest(id: string | undefined) { + if (!this.lastmessageid || !id) return; + let flake: string | undefined = this.lastmessageid; + const time = SnowFlake.stringToUnixTime(id); + let flaketime = SnowFlake.stringToUnixTime(flake); + while (flake && time < flaketime) { + flake = this.idToPrev.get(flake); + + if (!flake) { + return; + } + flaketime = SnowFlake.stringToUnixTime(flake); + } + return flake; + } + updateChannel(json: channeljson) { + this.type = json.type; + this.name = json.name; + const parent = this.localuser.channelids.get(json.parent_id); + if (parent) { + this.parent = parent; + this.parent_id = parent.id; + } else { + this.parent = undefined; + this.parent_id = undefined; + } + + this.children = []; + this.guild_id = json.guild_id; + this.permission_overwrites = new Map(); + for (const thing of json.permission_overwrites) { + if ( + thing.id === "1182819038095799904" || + thing.id === "1182820803700625444" + ) { + continue; + } + this.permission_overwrites.set( + thing.id, + new Permissions(thing.allow, thing.deny) + ); + const permisions = this.permission_overwrites.get(thing.id); + if (permisions) { + const role = this.guild.roleids.get(thing.id); + if (role) { + this.permission_overwritesar.push([role, permisions]); + } + } + } + this.topic = json.topic; + this.nsfw = json.nsfw; + } + typingstart() { + if (this.typing > Date.now()) { + return; + } + this.typing = Date.now() + 6000; + fetch(this.info.api + "/channels/" + this.id + "/typing", { + method: "POST", + headers: this.headers, + }); + } + get notification() { + let notinumber: number | null = this.message_notifications; + if (Number(notinumber) === 3) { + notinumber = null; + } + notinumber ??= this.guild.message_notifications; + switch (Number(notinumber)) { + case 0: + return "all"; + case 1: + return "mentions"; + case 2: + return "none"; + case 3: + default: + return "default"; + } + } + async sendMessage( + content: string, + { + attachments = [], + replyingto = null, + }: { attachments: Blob[]; embeds: embedjson; replyingto: Message | null } + ) { + let replyjson: any; + if (replyingto) { + replyjson = { + guild_id: replyingto.guild.id, + channel_id: replyingto.channel.id, + message_id: replyingto.id, + }; + } + if (attachments.length === 0) { + const body = { + content, + nonce: Math.floor(Math.random() * 1000000000), + message_reference: undefined, + }; + if (replyjson) { + body.message_reference = replyjson; + } + return await fetch(this.info.api + "/channels/" + this.id + "/messages", { + method: "POST", + headers: this.headers, + body: JSON.stringify(body), + }); + } else { + const formData = new FormData(); + const body = { + content, + nonce: Math.floor(Math.random() * 1000000000), + message_reference: undefined, + }; + if (replyjson) { + body.message_reference = replyjson; + } + formData.append("payload_json", JSON.stringify(body)); + for (const i in attachments) { + formData.append("files[" + i + "]", attachments[i]); + } + return await fetch(this.info.api + "/channels/" + this.id + "/messages", { + method: "POST", + body: formData, + headers: { Authorization: this.headers.Authorization }, + }); + } + } + messageCreate(messagep: messageCreateJson): void { + if (!this.hasPermission("VIEW_CHANNEL")) { + return; + } + const messagez = new Message(messagep.d, this); + this.lastmessage = messagez; + if (this.lastmessageid) { + this.idToNext.set(this.lastmessageid, messagez.id); + this.idToPrev.set(messagez.id, this.lastmessageid); + } + + this.lastmessageid = messagez.id; + + if (messagez.author === this.localuser.user) { + this.lastreadmessageid = messagez.id; + if (this.myhtml) { + this.myhtml.classList.remove("cunread"); + } + } else { + if (this.myhtml) { + this.myhtml.classList.add("cunread"); + } + } + this.guild.unreads(); + if (this === this.localuser.channelfocus) { + if (!this.infinitefocus) { + this.tryfocusinfinate(); + } + this.infinite.addedBottom(); + } + if (messagez.author === this.localuser.user) { + return; + } + if ( + this.localuser.lookingguild?.prevchannel === this && + document.hasFocus() + ) { + return; + } + if (this.notification === "all") { + this.notify(messagez); + } else if ( + this.notification === "mentions" && + messagez.mentionsuser(this.localuser.user) + ) { + this.notify(messagez); + } + } + notititle(message: Message): string { + return ( + message.author.username + + " > " + + this.guild.properties.name + + " > " + + this.name + ); + } + notify(message: Message, deep = 0) { + Voice.noises(Voice.getNotificationSound()); + if (!("Notification" in window)) { + } else if (Notification.permission === "granted") { + let noticontent: string | undefined | null = message.content.textContent; + if (message.embeds[0]) { + noticontent ||= message.embeds[0]?.json.title; + noticontent ||= message.content.textContent; + } + noticontent ||= "Blank Message"; + let imgurl: null | string = null; + const images = message.getimages(); + if (images.length) { + const image = images[0]; + if (image.proxy_url) { + imgurl ||= image.proxy_url; + } + imgurl ||= image.url; + } + const notification = new Notification(this.notititle(message), { + body: noticontent, + icon: message.author.getpfpsrc(), + image: imgurl, + }); + notification.addEventListener("click", (_) => { + window.focus(); + this.getHTML(); + }); + } else if (Notification.permission !== "denied") { + Notification.requestPermission().then(() => { + if (deep === 3) { + return; + } + this.notify(message, deep + 1); + }); + } + } + async addRoleToPerms(role: Role) { + await fetch( + this.info.api + "/channels/" + this.id + "/permissions/" + role.id, + { + method: "PUT", + headers: this.headers, + body: JSON.stringify({ + allow: "0", + deny: "0", + id: role.id, + type: 0, + }), + } + ); + const perm = new Permissions("0", "0"); + this.permission_overwrites.set(role.id, perm); + this.permission_overwritesar.push([role, perm]); + } + async updateRolePermissions(id: string, perms: Permissions) { + const permission = this.permission_overwrites.get(id); + if (permission) { + permission.allow = perms.allow; + permission.deny = perms.deny; + await fetch( + this.info.api + "/channels/" + this.id + "/permissions/" + id, + { + method: "PUT", + headers: this.headers, + body: JSON.stringify({ + allow: permission.allow.toString(), + deny: permission.deny.toString(), + id, + type: 0, + }), + } + ); + } + } +} +Channel.setupcontextmenu(); +export { Channel }; diff --git a/src/webpage/contextmenu.ts b/src/webpage/contextmenu.ts new file mode 100644 index 0000000..fe28d2e --- /dev/null +++ b/src/webpage/contextmenu.ts @@ -0,0 +1,107 @@ +class Contextmenu { + static currentmenu: HTMLElement | ""; + name: string; + buttons: [ + string, + (this: x, arg: y, e: MouseEvent) => void, + string | null, + (this: x, arg: y) => boolean, + (this: x, arg: y) => boolean, + string + ][]; + div!: HTMLDivElement; + static setup() { + Contextmenu.currentmenu = ""; + document.addEventListener("click", (event) => { + if (Contextmenu.currentmenu === "") { + return; + } + if (!Contextmenu.currentmenu.contains(event.target as Node)) { + Contextmenu.currentmenu.remove(); + Contextmenu.currentmenu = ""; + } + }); + } + constructor(name: string) { + this.name = name; + this.buttons = []; + } + addbutton( + text: string, + onclick: (this: x, arg: y, e: MouseEvent) => void, + img: null | string = null, + shown: (this: x, arg: y) => boolean = (_) => true, + enabled: (this: x, arg: y) => boolean = (_) => true + ) { + this.buttons.push([text, onclick, img, shown, enabled, "button"]); + return {}; + } + addsubmenu( + text: string, + onclick: (this: x, arg: y, e: MouseEvent) => void, + img = null, + shown: (this: x, arg: y) => boolean = (_) => true, + enabled: (this: x, arg: y) => boolean = (_) => true + ) { + this.buttons.push([text, onclick, img, shown, enabled, "submenu"]); + return {}; + } + private makemenu(x: number, y: number, addinfo: x, other: y) { + const div = document.createElement("div"); + div.classList.add("contextmenu", "flexttb"); + + let visibleButtons = 0; + for (const thing of this.buttons) { + if (!thing[3].bind(addinfo).call(addinfo, other)) continue; + visibleButtons++; + + const intext = document.createElement("button"); + intext.disabled = !thing[4].bind(addinfo).call(addinfo, other); + intext.classList.add("contextbutton"); + intext.textContent = thing[0]; + console.log(thing); + if (thing[5] === "button" || thing[5] === "submenu") { + intext.onclick = thing[1].bind(addinfo, other); + } + + div.appendChild(intext); + } + if (visibleButtons == 0) return; + + if (Contextmenu.currentmenu != "") { + Contextmenu.currentmenu.remove(); + } + div.style.top = y + "px"; + div.style.left = x + "px"; + document.body.appendChild(div); + Contextmenu.keepOnScreen(div); + console.log(div); + Contextmenu.currentmenu = div; + return this.div; + } + bindContextmenu(obj: HTMLElement, addinfo: x, other: y) { + const func = (event: MouseEvent) => { + event.preventDefault(); + event.stopImmediatePropagation(); + this.makemenu(event.clientX, event.clientY, addinfo, other); + }; + obj.addEventListener("contextmenu", func); + return func; + } + static keepOnScreen(obj: HTMLElement) { + const html = document.documentElement.getBoundingClientRect(); + const docheight = html.height; + const docwidth = html.width; + const box = obj.getBoundingClientRect(); + console.log(box, docheight, docwidth); + if (box.right > docwidth) { + console.log("test"); + obj.style.left = docwidth - box.width + "px"; + } + if (box.bottom > docheight) { + obj.style.top = docheight - box.height + "px"; + } + } +} +Contextmenu.setup(); +export { Contextmenu }; diff --git a/src/webpage/dialog.ts b/src/webpage/dialog.ts new file mode 100644 index 0000000..ca15cf1 --- /dev/null +++ b/src/webpage/dialog.ts @@ -0,0 +1,273 @@ +type dialogjson = + | ["hdiv", ...dialogjson[]] + | ["vdiv", ...dialogjson[]] + | ["img", string, [number, number] | undefined | ["fit"]] + | ["checkbox", string, boolean, (this: HTMLInputElement, e: Event) => unknown] + | ["button", string, string, (this: HTMLButtonElement, e: Event) => unknown] + | ["mdbox", string, string, (this: HTMLTextAreaElement, e: Event) => unknown] + | ["textbox", string, string, (this: HTMLInputElement, e: Event) => unknown] + | ["fileupload", string, (this: HTMLInputElement, e: Event) => unknown] + | ["text", string] + | ["title", string] + | ["radio", string, string[], (this: unknown, e: string) => unknown, number] + | ["html", HTMLElement] + | [ + "select", + string, + string[], + (this: HTMLSelectElement, e: Event) => unknown, + number + ] + | ["tabs", [string, dialogjson][]]; +class Dialog { + layout: dialogjson; + onclose: Function; + onopen: Function; + html: HTMLDivElement; + background!: HTMLDivElement; + constructor( + layout: dialogjson, + onclose = (_: any) => {}, + onopen = (_: any) => {} + ) { + this.layout = layout; + this.onclose = onclose; + this.onopen = onopen; + const div = document.createElement("div"); + div.appendChild(this.tohtml(layout)); + this.html = div; + this.html.classList.add("centeritem"); + if (!(layout[0] === "img")) { + this.html.classList.add("nonimagecenter"); + } + } + tohtml(array: dialogjson): HTMLElement { + switch (array[0]) { + case "img": + const img = document.createElement("img"); + img.src = array[1]; + if (array[2] != undefined) { + if (array[2].length === 2) { + img.width = array[2][0]; + img.height = array[2][1]; + } else if (array[2][0] === "fit") { + img.classList.add("imgfit"); + } + } + return img; + case "hdiv": + const hdiv = document.createElement("div"); + hdiv.classList.add("flexltr"); + + for (const thing of array) { + if (thing === "hdiv") { + continue; + } + hdiv.appendChild(this.tohtml(thing)); + } + return hdiv; + case "vdiv": + const vdiv = document.createElement("div"); + vdiv.classList.add("flexttb"); + for (const thing of array) { + if (thing === "vdiv") { + continue; + } + vdiv.appendChild(this.tohtml(thing)); + } + return vdiv; + case "checkbox": { + const div = document.createElement("div"); + const checkbox = document.createElement("input"); + div.appendChild(checkbox); + const label = document.createElement("span"); + checkbox.checked = array[2]; + label.textContent = array[1]; + div.appendChild(label); + checkbox.addEventListener("change", array[3]); + checkbox.type = "checkbox"; + return div; + } + case "button": { + const div = document.createElement("div"); + const input = document.createElement("button"); + + const label = document.createElement("span"); + input.textContent = array[2]; + label.textContent = array[1]; + div.appendChild(label); + div.appendChild(input); + input.addEventListener("click", array[3]); + return div; + } + case "mdbox": { + const div = document.createElement("div"); + const input = document.createElement("textarea"); + input.value = array[2]; + const label = document.createElement("span"); + label.textContent = array[1]; + input.addEventListener("input", array[3]); + div.appendChild(label); + div.appendChild(document.createElement("br")); + div.appendChild(input); + return div; + } + case "textbox": { + const div = document.createElement("div"); + const input = document.createElement("input"); + input.value = array[2]; + input.type = "text"; + const label = document.createElement("span"); + label.textContent = array[1]; + console.log(array[3]); + input.addEventListener("input", array[3]); + div.appendChild(label); + div.appendChild(input); + return div; + } + case "fileupload": { + const div = document.createElement("div"); + const input = document.createElement("input"); + input.type = "file"; + const label = document.createElement("span"); + label.textContent = array[1]; + div.appendChild(label); + div.appendChild(input); + input.addEventListener("change", array[2]); + console.log(array); + return div; + } + case "text": { + const span = document.createElement("span"); + span.textContent = array[1]; + return span; + } + case "title": { + const span = document.createElement("span"); + span.classList.add("title"); + span.textContent = array[1]; + return span; + } + case "radio": { + const div = document.createElement("div"); + const fieldset = document.createElement("fieldset"); + fieldset.addEventListener("change", () => { + let i = -1; + for (const thing of Array.from(fieldset.children)) { + i++; + if (i === 0) { + continue; + } + const checkbox = thing.children[0].children[0] as HTMLInputElement; + if (checkbox.checked) { + array[3](checkbox.value); + } + } + }); + const legend = document.createElement("legend"); + legend.textContent = array[1]; + fieldset.appendChild(legend); + let i = 0; + for (const thing of array[2]) { + const div = document.createElement("div"); + const input = document.createElement("input"); + input.classList.add("radio"); + input.type = "radio"; + input.name = array[1]; + input.value = thing; + if (i === array[4]) { + input.checked = true; + } + const label = document.createElement("label"); + + label.appendChild(input); + const span = document.createElement("span"); + span.textContent = thing; + label.appendChild(span); + div.appendChild(label); + fieldset.appendChild(div); + i++; + } + div.appendChild(fieldset); + return div; + } + case "html": + return array[1]; + + case "select": { + const div = document.createElement("div"); + const label = document.createElement("label"); + const select = document.createElement("select"); + + label.textContent = array[1]; + div.append(label); + div.appendChild(select); + for (const thing of array[2]) { + const option = document.createElement("option"); + option.textContent = thing; + select.appendChild(option); + } + select.selectedIndex = array[4]; + select.addEventListener("change", array[3]); + return div; + } + case "tabs": { + const table = document.createElement("div"); + table.classList.add("flexttb"); + const tabs = document.createElement("div"); + tabs.classList.add("flexltr"); + tabs.classList.add("tabbed-head"); + table.appendChild(tabs); + const content = document.createElement("div"); + content.classList.add("tabbed-content"); + table.appendChild(content); + + let shown: HTMLElement | undefined; + for (const thing of array[1]) { + const button = document.createElement("button"); + button.textContent = thing[0]; + tabs.appendChild(button); + + const html = this.tohtml(thing[1]); + content.append(html); + if (!shown) { + shown = html; + } else { + html.style.display = "none"; + } + button.addEventListener("click", (_) => { + if (shown) { + shown.style.display = "none"; + } + html.style.display = ""; + shown = html; + }); + } + return table; + } + default: + console.error( + "can't find element:" + array[0], + " full element:", + array + ); + return document.createElement("span"); + } + } + show() { + this.onopen(); + console.log("fullscreen"); + this.background = document.createElement("div"); + this.background.classList.add("background"); + document.body.appendChild(this.background); + document.body.appendChild(this.html); + this.background.onclick = (_) => { + this.hide(); + }; + } + hide() { + document.body.removeChild(this.background); + document.body.removeChild(this.html); + } +} +export { Dialog }; diff --git a/src/webpage/direct.ts b/src/webpage/direct.ts new file mode 100644 index 0000000..131143e --- /dev/null +++ b/src/webpage/direct.ts @@ -0,0 +1,306 @@ +import { Guild } from "./guild.js"; +import { Channel } from "./channel.js"; +import { Message } from "./message.js"; +import { Localuser } from "./localuser.js"; +import { User } from "./user.js"; +import { + channeljson, + dirrectjson, + memberjson, + messagejson, +} from "./jsontypes.js"; +import { Permissions } from "./permissions.js"; +import { SnowFlake } from "./snowflake.js"; +import { Contextmenu } from "./contextmenu.js"; + +class Direct extends Guild { + declare channelids: { [key: string]: Group }; + getUnixTime(): number { + throw new Error("Do not call this for Direct, it does not make sense"); + } + constructor(json: dirrectjson[], owner: Localuser) { + super(-1, owner, null); + this.message_notifications = 0; + this.owner = owner; + if (!this.localuser) { + console.error("Owner was not included, please fix"); + } + this.headers = this.localuser.headers; + this.channels = []; + this.channelids = {}; + // @ts-ignore + this.properties = {}; + this.roles = []; + this.roleids = new Map(); + this.prevchannel = undefined; + this.properties.name = "Direct Messages"; + for (const thing of json) { + const temp = new Group(thing, this); + this.channels.push(temp); + this.channelids[temp.id] = temp; + } + this.headchannels = this.channels; + } + createChannelpac(json: any) { + const thischannel = new Group(json, this); + this.channelids[thischannel.id] = thischannel; + this.channels.push(thischannel); + this.sortchannels(); + this.printServers(); + return thischannel; + } + delChannel(json: channeljson) { + const channel = this.channelids[json.id]; + super.delChannel(json); + if (channel) { + channel.del(); + } + } + giveMember(_member: memberjson) { + console.error("not a real guild, can't give member object"); + } + getRole(/* ID: string */) { + return null; + } + hasRole(/* r: string */) { + return false; + } + isAdmin() { + return false; + } + unreaddms() { + for (const thing of this.channels) { + (thing as Group).unreads(); + } + } +} + +const dmPermissions = new Permissions("0"); +dmPermissions.setPermission("ADD_REACTIONS", 1); +dmPermissions.setPermission("VIEW_CHANNEL", 1); +dmPermissions.setPermission("SEND_MESSAGES", 1); +dmPermissions.setPermission("EMBED_LINKS", 1); +dmPermissions.setPermission("ATTACH_FILES", 1); +dmPermissions.setPermission("READ_MESSAGE_HISTORY", 1); +dmPermissions.setPermission("MENTION_EVERYONE", 1); +dmPermissions.setPermission("USE_EXTERNAL_EMOJIS", 1); +dmPermissions.setPermission("USE_APPLICATION_COMMANDS", 1); +dmPermissions.setPermission("USE_EXTERNAL_STICKERS", 1); +dmPermissions.setPermission("USE_EMBEDDED_ACTIVITIES", 1); +dmPermissions.setPermission("USE_SOUNDBOARD", 1); +dmPermissions.setPermission("USE_EXTERNAL_SOUNDS", 1); +dmPermissions.setPermission("SEND_VOICE_MESSAGES", 1); +dmPermissions.setPermission("SEND_POLLS", 1); +dmPermissions.setPermission("USE_EXTERNAL_APPS", 1); + +dmPermissions.setPermission("CONNECT", 1); +dmPermissions.setPermission("SPEAK", 1); +dmPermissions.setPermission("STREAM", 1); +dmPermissions.setPermission("USE_VAD", 1); + +// @ts-ignore +class Group extends Channel { + user: User; + static contextmenu = new Contextmenu("channel menu"); + static setupcontextmenu() { + this.contextmenu.addbutton("Copy DM id", function (this: Group) { + navigator.clipboard.writeText(this.id); + }); + + this.contextmenu.addbutton("Mark as read", function (this: Group) { + this.readbottom(); + }); + + this.contextmenu.addbutton("Close DM", function (this: Group) { + this.deleteChannel(); + }); + + this.contextmenu.addbutton("Copy user ID", function () { + navigator.clipboard.writeText(this.user.id); + }); + } + constructor(json: dirrectjson, owner: Direct) { + super(-1, owner, json.id); + this.owner = owner; + this.headers = this.guild.headers; + this.name = json.recipients[0]?.username; + if (json.recipients[0]) { + this.user = new User(json.recipients[0], this.localuser); + } else { + this.user = this.localuser.user; + } + this.name ??= this.localuser.user.username; + this.parent_id!; + this.parent!; + this.children = []; + this.guild_id = "@me"; + this.permission_overwrites = new Map(); + this.lastmessageid = json.last_message_id; + this.mentions = 0; + this.setUpInfiniteScroller(); + this.updatePosition(); + } + updatePosition() { + if (this.lastmessageid) { + this.position = SnowFlake.stringToUnixTime(this.lastmessageid); + } else { + this.position = 0; + } + this.position = -Math.max(this.position, this.getUnixTime()); + } + createguildHTML() { + const div = document.createElement("div"); + Group.contextmenu.bindContextmenu(div, this, undefined); + this.html = new WeakRef(div); + div.classList.add("channeleffects"); + const myhtml = document.createElement("span"); + myhtml.textContent = this.name; + div.appendChild(this.user.buildpfp()); + div.appendChild(myhtml); + (div as any)["myinfo"] = this; + div.onclick = (_) => { + this.getHTML(); + }; + + return div; + } + async getHTML() { + const id = ++Channel.genid; + if (this.localuser.channelfocus) { + this.localuser.channelfocus.infinite.delete(); + } + if (this.guild !== this.localuser.lookingguild) { + this.guild.loadGuild(); + } + this.guild.prevchannel = this; + this.localuser.channelfocus = this; + const prom = this.infinite.delete(); + history.pushState(null, "", "/channels/" + this.guild_id + "/" + this.id); + this.localuser.pageTitle("@" + this.name); + (document.getElementById("channelTopic") as HTMLElement).setAttribute( + "hidden", + "" + ); + + const loading = document.getElementById("loadingdiv") as HTMLDivElement; + Channel.regenLoadingMessages(); + loading.classList.add("loading"); + this.rendertyping(); + await this.putmessages(); + await prom; + if (id !== Channel.genid) { + return; + } + this.buildmessages(); + (document.getElementById("typebox") as HTMLDivElement).contentEditable = + "" + true; + } + messageCreate(messagep: { d: messagejson }) { + const messagez = new Message(messagep.d, this); + if (this.lastmessageid) { + this.idToNext.set(this.lastmessageid, messagez.id); + this.idToPrev.set(messagez.id, this.lastmessageid); + } + this.lastmessageid = messagez.id; + if (messagez.author === this.localuser.user) { + this.lastreadmessageid = messagez.id; + if (this.myhtml) { + this.myhtml.classList.remove("cunread"); + } + } else { + if (this.myhtml) { + this.myhtml.classList.add("cunread"); + } + } + this.unreads(); + this.updatePosition(); + this.infinite.addedBottom(); + this.guild.sortchannels(); + if (this.myhtml) { + const parrent = this.myhtml.parentElement as HTMLElement; + parrent.prepend(this.myhtml); + } + if (this === this.localuser.channelfocus) { + if (!this.infinitefocus) { + this.tryfocusinfinate(); + } + this.infinite.addedBottom(); + } + this.unreads(); + if (messagez.author === this.localuser.user) { + return; + } + if ( + this.localuser.lookingguild?.prevchannel === this && + document.hasFocus() + ) { + return; + } + if (this.notification === "all") { + this.notify(messagez); + } else if ( + this.notification === "mentions" && + messagez.mentionsuser(this.localuser.user) + ) { + this.notify(messagez); + } + } + notititle(message: Message) { + return message.author.username; + } + readbottom() { + super.readbottom(); + this.unreads(); + } + all: WeakRef = new WeakRef(document.createElement("div")); + noti: WeakRef = new WeakRef(document.createElement("div")); + del() { + const all = this.all.deref(); + if (all) { + all.remove(); + } + if (this.myhtml) { + this.myhtml.remove(); + } + } + unreads() { + const sentdms = document.getElementById("sentdms") as HTMLDivElement; //Need to change sometime + const current = this.all.deref(); + if (this.hasunreads) { + { + const noti = this.noti.deref(); + if (noti) { + noti.textContent = this.mentions + ""; + return; + } + } + const div = document.createElement("div"); + div.classList.add("servernoti"); + const noti = document.createElement("div"); + noti.classList.add("unread", "notiunread", "pinged"); + noti.textContent = "" + this.mentions; + this.noti = new WeakRef(noti); + div.append(noti); + const buildpfp = this.user.buildpfp(); + this.all = new WeakRef(div); + buildpfp.classList.add("mentioned"); + div.append(buildpfp); + sentdms.append(div); + div.onclick = (_) => { + this.guild.loadGuild(); + this.getHTML(); + }; + } else if (current) { + current.remove(); + } else { + } + } + isAdmin(): boolean { + return false; + } + hasPermission(name: string): boolean { + return dmPermissions.hasPermission(name); + } +} +export { Direct, Group }; +Group.setupcontextmenu(); diff --git a/src/webpage/embed.ts b/src/webpage/embed.ts new file mode 100644 index 0000000..772a835 --- /dev/null +++ b/src/webpage/embed.ts @@ -0,0 +1,411 @@ +import { Dialog } from "./dialog.js"; +import { Message } from "./message.js"; +import { MarkDown } from "./markdown.js"; +import { embedjson, invitejson } from "./jsontypes.js"; +import { getapiurls, getInstances } from "./login.js"; +import { Guild } from "./guild.js"; + +class Embed { + type: string; + owner: Message; + json: embedjson; + constructor(json: embedjson, owner: Message) { + this.type = this.getType(json); + this.owner = owner; + this.json = json; + } + getType(json: embedjson) { + const instances = getInstances(); + if ( + instances && + json.type === "link" && + json.url && + URL.canParse(json.url) + ) { + const Url = new URL(json.url); + for (const instance of instances) { + if (instance.url && URL.canParse(instance.url)) { + const IUrl = new URL(instance.url); + const params = new URLSearchParams(Url.search); + let host: string; + if (params.has("instance")) { + const url = params.get("instance") as string; + if (URL.canParse(url)) { + host = new URL(url).host; + } else { + host = Url.host; + } + } else { + host = Url.host; + } + if (IUrl.host === host) { + const code = + Url.pathname.split("/")[Url.pathname.split("/").length - 1]; + json.invite = { + url: instance.url, + code, + }; + return "invite"; + } + } + } + } + return json.type || "rich"; + } + generateHTML() { + switch (this.type) { + case "rich": + return this.generateRich(); + case "image": + return this.generateImage(); + case "invite": + return this.generateInvite(); + case "link": + return this.generateLink(); + case "video": + case "article": + return this.generateArticle(); + default: + console.warn( + `unsupported embed type ${this.type}, please add support dev :3`, + this.json + ); + return document.createElement("div"); //prevent errors by giving blank div + } + } + get message() { + return this.owner; + } + get channel() { + return this.message.channel; + } + get guild() { + return this.channel.guild; + } + get localuser() { + return this.guild.localuser; + } + generateRich() { + const div = document.createElement("div"); + if (this.json.color) { + div.style.backgroundColor = "#" + this.json.color.toString(16); + } + div.classList.add("embed-color"); + + const embed = document.createElement("div"); + embed.classList.add("embed"); + div.append(embed); + + if (this.json.author) { + const authorline = document.createElement("div"); + if (this.json.author.icon_url) { + const img = document.createElement("img"); + img.classList.add("embedimg"); + img.src = this.json.author.icon_url; + authorline.append(img); + } + const a = document.createElement("a"); + a.textContent = this.json.author.name as string; + if (this.json.author.url) { + MarkDown.safeLink(a, this.json.author.url); + } + a.classList.add("username"); + authorline.append(a); + embed.append(authorline); + } + if (this.json.title) { + const title = document.createElement("a"); + title.append(new MarkDown(this.json.title, this.channel).makeHTML()); + if (this.json.url) { + MarkDown.safeLink(title, this.json.url); + } + title.classList.add("embedtitle"); + embed.append(title); + } + if (this.json.description) { + const p = document.createElement("p"); + p.append(new MarkDown(this.json.description, this.channel).makeHTML()); + embed.append(p); + } + + embed.append(document.createElement("br")); + if (this.json.fields) { + for (const thing of this.json.fields) { + const div = document.createElement("div"); + const b = document.createElement("b"); + b.textContent = thing.name; + div.append(b); + const p = document.createElement("p"); + p.append(new MarkDown(thing.value, this.channel).makeHTML()); + p.classList.add("embedp"); + div.append(p); + + if (thing.inline) { + div.classList.add("inline"); + } + embed.append(div); + } + } + if (this.json.footer || this.json.timestamp) { + const footer = document.createElement("div"); + if (this.json?.footer?.icon_url) { + const img = document.createElement("img"); + img.src = this.json.footer.icon_url; + img.classList.add("embedicon"); + footer.append(img); + } + if (this.json?.footer?.text) { + const span = document.createElement("span"); + span.textContent = this.json.footer.text; + span.classList.add("spaceright"); + footer.append(span); + } + if (this.json?.footer && this.json?.timestamp) { + const span = document.createElement("span"); + span.textContent = "•"; + span.classList.add("spaceright"); + footer.append(span); + } + if (this.json?.timestamp) { + const span = document.createElement("span"); + span.textContent = new Date(this.json.timestamp).toLocaleString(); + footer.append(span); + } + embed.append(footer); + } + return div; + } + generateImage() { + const img = document.createElement("img"); + img.classList.add("messageimg"); + img.onclick = function () { + const full = new Dialog(["img", img.src, ["fit"]]); + full.show(); + }; + img.src = this.json.thumbnail.proxy_url; + if (this.json.thumbnail.width) { + let scale = 1; + const max = 96 * 3; + scale = Math.max(scale, this.json.thumbnail.width / max); + scale = Math.max(scale, this.json.thumbnail.height / max); + this.json.thumbnail.width /= scale; + this.json.thumbnail.height /= scale; + } + img.style.width = this.json.thumbnail.width + "px"; + img.style.height = this.json.thumbnail.height + "px"; + console.log(this.json, "Image fix"); + return img; + } + generateLink() { + const table = document.createElement("table"); + table.classList.add("embed", "linkembed"); + const trtop = document.createElement("tr"); + table.append(trtop); + if (this.json.url && this.json.title) { + const td = document.createElement("td"); + const a = document.createElement("a"); + MarkDown.safeLink(a, this.json.url); + a.textContent = this.json.title; + td.append(a); + trtop.append(td); + } + { + const td = document.createElement("td"); + const img = document.createElement("img"); + if (this.json.thumbnail) { + img.classList.add("embedimg"); + img.onclick = function () { + const full = new Dialog(["img", img.src, ["fit"]]); + full.show(); + }; + img.src = this.json.thumbnail.proxy_url; + td.append(img); + } + trtop.append(td); + } + const bottomtr = document.createElement("tr"); + const td = document.createElement("td"); + if (this.json.description) { + const span = document.createElement("span"); + span.textContent = this.json.description; + td.append(span); + } + bottomtr.append(td); + table.append(bottomtr); + return table; + } + invcache: [invitejson, { cdn: string; api: string }] | undefined; + generateInvite() { + if (this.invcache && (!this.json.invite || !this.localuser)) { + return this.generateLink(); + } + const div = document.createElement("div"); + div.classList.add("embed", "inviteEmbed", "flexttb"); + const json1 = this.json.invite; + (async () => { + let json: invitejson; + let info: { cdn: string; api: string }; + if (!this.invcache) { + if (!json1) { + div.append(this.generateLink()); + return; + } + const tempinfo = await getapiurls(json1.url); + + if (!tempinfo) { + div.append(this.generateLink()); + return; + } + info = tempinfo; + const res = await fetch(info.api + "/invites/" + json1.code); + if (!res.ok) { + div.append(this.generateLink()); + } + json = (await res.json()) as invitejson; + this.invcache = [json, info]; + } else { + [json, info] = this.invcache; + } + if (!json) { + div.append(this.generateLink()); + return; + } + if (json.guild.banner) { + const banner = document.createElement("img"); + banner.src = + this.localuser.info.cdn + + "/icons/" + + json.guild.id + + "/" + + json.guild.banner + + ".png?size=256"; + banner.classList.add("banner"); + div.append(banner); + } + const guild: invitejson["guild"] & { info?: { cdn: string } } = + json.guild; + guild.info = info; + const icon = Guild.generateGuildIcon( + guild as invitejson["guild"] & { info: { cdn: string } } + ); + const iconrow = document.createElement("div"); + iconrow.classList.add("flexltr", "flexstart"); + iconrow.append(icon); + { + const guildinfo = document.createElement("div"); + guildinfo.classList.add("flexttb", "invguildinfo"); + const name = document.createElement("b"); + name.textContent = guild.name; + guildinfo.append(name); + + const members = document.createElement("span"); + members.innerText = + "#" + json.channel.name + " • Members: " + guild.member_count; + guildinfo.append(members); + members.classList.add("subtext"); + iconrow.append(guildinfo); + } + + div.append(iconrow); + const h2 = document.createElement("h2"); + h2.textContent = `You've been invited by ${json.inviter.username}`; + div.append(h2); + const button = document.createElement("button"); + button.textContent = "Accept"; + if (this.localuser.info.api.startsWith(info.api)) { + if (this.localuser.guildids.has(guild.id)) { + button.textContent = "Already joined"; + button.disabled = true; + } + } + button.classList.add("acceptinvbutton"); + div.append(button); + button.onclick = (_) => { + if (this.localuser.info.api.startsWith(info.api)) { + fetch(this.localuser.info.api + "/invites/" + json.code, { + method: "POST", + headers: this.localuser.headers, + }) + .then((r) => r.json()) + .then((_) => { + if (_.message) { + alert(_.message); + } + }); + } else { + if (this.json.invite) { + const params = new URLSearchParams(""); + params.set("instance", this.json.invite.url); + const encoded = params.toString(); + const url = `${location.origin}/invite/${this.json.invite.code}?${encoded}`; + window.open(url, "_blank"); + } + } + }; + })(); + return div; + } + generateArticle() { + const colordiv = document.createElement("div"); + colordiv.style.backgroundColor = "#000000"; + colordiv.classList.add("embed-color"); + + const div = document.createElement("div"); + div.classList.add("embed"); + if (this.json.provider) { + const provider = document.createElement("p"); + provider.classList.add("provider"); + provider.textContent = this.json.provider.name; + div.append(provider); + } + const a = document.createElement("a"); + if (this.json.url && this.json.url) { + MarkDown.safeLink(a, this.json.url); + a.textContent = this.json.url; + div.append(a); + } + if (this.json.description) { + const description = document.createElement("p"); + description.textContent = this.json.description; + div.append(description); + } + if (this.json.thumbnail) { + const img = document.createElement("img"); + if (this.json.thumbnail.width && this.json.thumbnail.width) { + let scale = 1; + const inch = 96; + scale = Math.max(scale, this.json.thumbnail.width / inch / 4); + scale = Math.max(scale, this.json.thumbnail.height / inch / 3); + this.json.thumbnail.width /= scale; + this.json.thumbnail.height /= scale; + img.style.width = this.json.thumbnail.width + "px"; + img.style.height = this.json.thumbnail.height + "px"; + } + img.classList.add("bigembedimg"); + if (this.json.video) { + img.onclick = async () => { + if (this.json.video) { + img.remove(); + const iframe = document.createElement("iframe"); + iframe.src = this.json.video.url + "?autoplay=1"; + if (this.json.thumbnail.width && this.json.thumbnail.width) { + iframe.style.width = this.json.thumbnail.width + "px"; + iframe.style.height = this.json.thumbnail.height + "px"; + } + div.append(iframe); + } + }; + } else { + img.onclick = async () => { + const full = new Dialog(["img", img.src, ["fit"]]); + full.show(); + }; + } + img.src = this.json.thumbnail.proxy_url || this.json.thumbnail.url; + div.append(img); + } + colordiv.append(div); + return colordiv; + } +} +export { Embed }; diff --git a/webpage/emoji.bin b/src/webpage/emoji.bin similarity index 100% rename from webpage/emoji.bin rename to src/webpage/emoji.bin diff --git a/src/webpage/emoji.ts b/src/webpage/emoji.ts new file mode 100644 index 0000000..2e61a93 --- /dev/null +++ b/src/webpage/emoji.ts @@ -0,0 +1,259 @@ +import { Contextmenu } from "./contextmenu.js"; +import { Guild } from "./guild.js"; +import { Localuser } from "./localuser.js"; + +class Emoji { + static emojis: { + name: string; + emojis: { + name: string; + emoji: string; + }[]; + }[]; + name: string; + id: string; + animated: boolean; + owner: Guild | Localuser; + get guild() { + if (this.owner instanceof Guild) { + return this.owner; + } + return; + } + get localuser() { + if (this.owner instanceof Guild) { + return this.owner.localuser; + } else { + return this.owner; + } + } + get info() { + return this.owner.info; + } + constructor( + json: { name: string; id: string; animated: boolean }, + owner: Guild | Localuser + ) { + this.name = json.name; + this.id = json.id; + this.animated = json.animated; + this.owner = owner; + } + getHTML(bigemoji: boolean = false) { + const emojiElem = document.createElement("img"); + emojiElem.classList.add("md-emoji"); + emojiElem.classList.add(bigemoji ? "bigemoji" : "smallemoji"); + emojiElem.crossOrigin = "anonymous"; + emojiElem.src = + this.info.cdn + + "/emojis/" + + this.id + + "." + + (this.animated ? "gif" : "png") + + "?size=32"; + + emojiElem.alt = this.name; + emojiElem.loading = "lazy"; + return emojiElem; + } + static decodeEmojiList(buffer: ArrayBuffer) { + const view = new DataView(buffer, 0); + let i = 0; + function read16() { + const int = view.getUint16(i); + i += 2; + return int; + } + function read8() { + const int = view.getUint8(i); + i += 1; + return int; + } + function readString8() { + return readStringNo(read8()); + } + function readString16() { + return readStringNo(read16()); + } + function readStringNo(length: number) { + const array = new Uint8Array(length); + + for (let i = 0; i < length; i++) { + array[i] = read8(); + } + //console.log(array); + return new TextDecoder("utf-8").decode(array.buffer); + } + const build: { name: string; emojis: { name: string; emoji: string }[] }[] = + []; + let cats = read16(); + + for (; cats !== 0; cats--) { + const name = readString16(); + const emojis: { + name: string; + skin_tone_support: boolean; + emoji: string; + }[] = []; + let emojinumber = read16(); + for (; emojinumber !== 0; emojinumber--) { + //console.log(emojis); + const name = readString8(); + const len = read8(); + const skin_tone_support = len > 127; + const emoji = readStringNo(len - Number(skin_tone_support) * 128); + emojis.push({ + name, + skin_tone_support, + emoji, + }); + } + build.push({ + name, + emojis, + }); + } + this.emojis = build; + console.log(build); + } + static grabEmoji() { + fetch("/emoji.bin") + .then((e) => { + return e.arrayBuffer(); + }) + .then((e) => { + Emoji.decodeEmojiList(e); + }); + } + static async emojiPicker( + x: number, + y: number, + localuser: Localuser + ): Promise { + let res: (r: Emoji | string) => void; + const promise: Promise = new Promise((r) => { + res = r; + }); + const menu = document.createElement("div"); + menu.classList.add("flexttb", "emojiPicker"); + menu.style.top = y + "px"; + menu.style.left = x + "px"; + + const title = document.createElement("h2"); + title.textContent = Emoji.emojis[0].name; + title.classList.add("emojiTitle"); + menu.append(title); + const selection = document.createElement("div"); + selection.classList.add("flexltr", "dontshrink", "emojirow"); + const body = document.createElement("div"); + body.classList.add("emojiBody"); + + let isFirst = true; + localuser.guilds + .filter((guild) => guild.id != "@me" && guild.emojis.length > 0) + .forEach((guild) => { + const select = document.createElement("div"); + select.classList.add("emojiSelect"); + + if (guild.properties.icon) { + const img = document.createElement("img"); + img.classList.add("pfp", "servericon", "emoji-server"); + img.crossOrigin = "anonymous"; + img.src = + localuser.info.cdn + + "/icons/" + + guild.properties.id + + "/" + + guild.properties.icon + + ".png?size=48"; + img.alt = "Server: " + guild.properties.name; + select.appendChild(img); + } else { + const div = document.createElement("span"); + div.textContent = guild.properties.name + .replace(/'s /g, " ") + .replace(/\w+/g, (word) => word[0]) + .replace(/\s/g, ""); + select.append(div); + } + + selection.append(select); + + const clickEvent = () => { + title.textContent = guild.properties.name; + body.innerHTML = ""; + for (const emojit of guild.emojis) { + const emojiElem = document.createElement("div"); + emojiElem.classList.add("emojiSelect"); + + const emojiClass = new Emoji( + { + id: emojit.id as string, + name: emojit.name, + animated: emojit.animated as boolean, + }, + localuser + ); + emojiElem.append(emojiClass.getHTML()); + body.append(emojiElem); + + emojiElem.addEventListener("click", () => { + res(emojiClass); + if (Contextmenu.currentmenu !== "") { + Contextmenu.currentmenu.remove(); + } + }); + } + }; + + select.addEventListener("click", clickEvent); + if (isFirst) { + clickEvent(); + isFirst = false; + } + }); + + setTimeout(() => { + if (Contextmenu.currentmenu != "") { + Contextmenu.currentmenu.remove(); + } + document.body.append(menu); + Contextmenu.currentmenu = menu; + Contextmenu.keepOnScreen(menu); + }, 10); + + let i = 0; + for (const thing of Emoji.emojis) { + const select = document.createElement("div"); + select.textContent = thing.emojis[0].emoji; + select.classList.add("emojiSelect"); + selection.append(select); + const clickEvent = () => { + title.textContent = thing.name; + body.innerHTML = ""; + for (const emojit of thing.emojis) { + const emoji = document.createElement("div"); + emoji.classList.add("emojiSelect"); + emoji.textContent = emojit.emoji; + body.append(emoji); + emoji.onclick = (_) => { + res(emojit.emoji); + if (Contextmenu.currentmenu !== "") { + Contextmenu.currentmenu.remove(); + } + }; + } + }; + select.onclick = clickEvent; + if (i === 0) { + clickEvent(); + } + i++; + } + menu.append(selection); + menu.append(body); + return promise; + } +} +Emoji.grabEmoji(); +export { Emoji }; diff --git a/webpage/favicon.ico b/src/webpage/favicon.ico similarity index 100% rename from webpage/favicon.ico rename to src/webpage/favicon.ico diff --git a/src/webpage/file.ts b/src/webpage/file.ts new file mode 100644 index 0000000..e0f2311 --- /dev/null +++ b/src/webpage/file.ts @@ -0,0 +1,152 @@ +import { Message } from "./message.js"; +import { Dialog } from "./dialog.js"; +import { filejson } from "./jsontypes.js"; + +class File { + owner: Message | null; + id: string; + filename: string; + content_type: string; + width: number | undefined; + height: number | undefined; + proxy_url: string | undefined; + url: string; + size: number; + constructor(fileJSON: filejson, owner: Message | null) { + this.owner = owner; + this.id = fileJSON.id; + this.filename = fileJSON.filename; + this.content_type = fileJSON.content_type; + this.width = fileJSON.width; + this.height = fileJSON.height; + this.url = fileJSON.url; + this.proxy_url = fileJSON.proxy_url; + this.content_type = fileJSON.content_type; + this.size = fileJSON.size; + } + getHTML(temp: boolean = false): HTMLElement { + const src = this.proxy_url || this.url; + if (this.width && this.height) { + let scale = 1; + const max = 96 * 3; + scale = Math.max(scale, this.width / max); + scale = Math.max(scale, this.height / max); + this.width /= scale; + this.height /= scale; + } + if (this.content_type.startsWith("image/")) { + const div = document.createElement("div"); + const img = document.createElement("img"); + img.classList.add("messageimg"); + div.classList.add("messageimgdiv"); + img.onclick = function () { + const full = new Dialog(["img", img.src, ["fit"]]); + full.show(); + }; + img.src = src; + div.append(img); + if (this.width) { + div.style.width = this.width + "px"; + div.style.height = this.height + "px"; + } + console.log(img); + console.log(this.width, this.height); + return div; + } else if (this.content_type.startsWith("video/")) { + const video = document.createElement("video"); + const source = document.createElement("source"); + source.src = src; + video.append(source); + source.type = this.content_type; + video.controls = !temp; + if (this.width && this.height) { + video.width = this.width; + video.height = this.height; + } + return video; + } else if (this.content_type.startsWith("audio/")) { + const audio = document.createElement("audio"); + const source = document.createElement("source"); + source.src = src; + audio.append(source); + source.type = this.content_type; + audio.controls = !temp; + return audio; + } else { + return this.createunknown(); + } + } + upHTML(files: Blob[], file: globalThis.File): HTMLElement { + const div = document.createElement("div"); + const contained = this.getHTML(true); + div.classList.add("containedFile"); + div.append(contained); + const controls = document.createElement("div"); + const garbage = document.createElement("button"); + garbage.textContent = "🗑"; + garbage.onclick = (_) => { + div.remove(); + files.splice(files.indexOf(file), 1); + }; + controls.classList.add("controls"); + div.append(controls); + controls.append(garbage); + return div; + } + static initFromBlob(file: globalThis.File) { + return new File( + { + filename: file.name, + size: file.size, + id: "null", + content_type: file.type, + width: undefined, + height: undefined, + url: URL.createObjectURL(file), + proxy_url: undefined, + }, + null + ); + } + createunknown(): HTMLElement { + console.log("🗎"); + const src = this.proxy_url || this.url; + const div = document.createElement("table"); + div.classList.add("unknownfile"); + const nametr = document.createElement("tr"); + div.append(nametr); + const fileicon = document.createElement("td"); + nametr.append(fileicon); + fileicon.append("🗎"); + fileicon.classList.add("fileicon"); + fileicon.rowSpan = 2; + const nametd = document.createElement("td"); + if (src) { + const a = document.createElement("a"); + a.href = src; + a.textContent = this.filename; + nametd.append(a); + } else { + nametd.textContent = this.filename; + } + + nametd.classList.add("filename"); + nametr.append(nametd); + const sizetr = document.createElement("tr"); + const size = document.createElement("td"); + sizetr.append(size); + size.textContent = "Size:" + File.filesizehuman(this.size); + size.classList.add("filesize"); + div.appendChild(sizetr); + return div; + } + static filesizehuman(fsize: number) { + const i = fsize == 0 ? 0 : Math.floor(Math.log(fsize) / Math.log(1024)); + return ( + Number((fsize / Math.pow(1024, i)).toFixed(2)) * 1 + + " " + + ["Bytes", "Kilobytes", "Megabytes", "Gigabytes", "Terabytes"][i] + ); + } +} +export { File }; diff --git a/src/webpage/guild.ts b/src/webpage/guild.ts new file mode 100644 index 0000000..3c2ec7a --- /dev/null +++ b/src/webpage/guild.ts @@ -0,0 +1,724 @@ +import { Channel } from "./channel.js"; +import { Localuser } from "./localuser.js"; +import { Contextmenu } from "./contextmenu.js"; +import { Role, RoleList } from "./role.js"; +import { Dialog } from "./dialog.js"; +import { Member } from "./member.js"; +import { Settings } from "./settings.js"; +import { Permissions } from "./permissions.js"; +import { SnowFlake } from "./snowflake.js"; +import { + channeljson, + guildjson, + emojijson, + memberjson, + invitejson, +} from "./jsontypes.js"; +import { User } from "./user.js"; + +class Guild extends SnowFlake { + owner!: Localuser; + headers!: Localuser["headers"]; + channels!: Channel[]; + properties!: guildjson["properties"]; + member_count!: number; + roles!: Role[]; + roleids!: Map; + prevchannel: Channel | undefined; + banner!: string; + message_notifications!: number; + headchannels!: Channel[]; + position!: number; + parent_id!: string; + member!: Member; + html!: HTMLElement; + emojis!: emojijson[]; + large!: boolean; + static contextmenu = new Contextmenu("guild menu"); + static setupcontextmenu() { + Guild.contextmenu.addbutton("Copy Guild id", function (this: Guild) { + navigator.clipboard.writeText(this.id); + }); + + Guild.contextmenu.addbutton("Mark as read", function (this: Guild) { + this.markAsRead(); + }); + + Guild.contextmenu.addbutton("Notifications", function (this: Guild) { + this.setnotifcation(); + }); + + Guild.contextmenu.addbutton( + "Leave guild", + function (this: Guild) { + this.confirmleave(); + }, + null, + function (_) { + return this.properties.owner_id !== this.member.user.id; + } + ); + + Guild.contextmenu.addbutton( + "Delete guild", + function (this: Guild) { + this.confirmDelete(); + }, + null, + function (_) { + return this.properties.owner_id === this.member.user.id; + } + ); + + Guild.contextmenu.addbutton( + "Create invite", + function (this: Guild) {}, + null, + (_) => true, + (_) => false + ); + Guild.contextmenu.addbutton("Settings", function (this: Guild) { + this.generateSettings(); + }); + /* -----things left for later----- + guild.contextmenu.addbutton("Leave Guild",function(){ + console.log(this) + this.deleteChannel(); + },null,_=>{return thisuser.isAdmin()}) + + guild.contextmenu.addbutton("Mute Guild",function(){ + editchannelf(this); + },null,_=>{return thisuser.isAdmin()}) + */ + } + generateSettings() { + const settings = new Settings("Settings for " + this.properties.name); + { + const overview = settings.addButton("Overview"); + const form = overview.addForm("", (_) => {}, { + headers: this.headers, + traditionalSubmit: true, + fetchURL: this.info.api + "/guilds/" + this.id, + method: "PATCH", + }); + form.addTextInput("Name:", "name", { initText: this.properties.name }); + form.addMDInput("Description:", "description", { + initText: this.properties.description, + }); + form.addFileInput("Banner:", "banner", { clear: true }); + form.addFileInput("Icon:", "icon", { clear: true }); + let region = this.properties.region; + if (!region) { + region = ""; + } + form.addTextInput("Region:", "region", { initText: region }); + } + const s1 = settings.addButton("roles"); + const permlist: [Role, Permissions][] = []; + for (const thing of this.roles) { + permlist.push([thing, thing.permissions]); + } + s1.options.push( + new RoleList(permlist, this, this.updateRolePermissions.bind(this)) + ); + settings.show(); + } + constructor( + json: guildjson | -1, + owner: Localuser, + member: memberjson | User | null + ) { + if (json === -1 || member === null) { + super("@me"); + return; + } + if (json.stickers.length) { + console.log(json.stickers, ":3"); + } + super(json.id); + this.large = json.large; + this.member_count = json.member_count; + this.emojis = json.emojis; + this.owner = owner; + this.headers = this.owner.headers; + this.channels = []; + this.properties = json.properties; + this.roles = []; + this.roleids = new Map(); + + this.message_notifications = 0; + for (const roley of json.roles) { + const roleh = new Role(roley, this); + this.roles.push(roleh); + this.roleids.set(roleh.id, roleh); + } + if (member instanceof User) { + Member.resolveMember(member, this).then((_) => { + if (_) { + this.member = _; + } else { + console.error("Member was unable to resolve"); + } + }); + } else { + Member.new(member, this).then((_) => { + if (_) { + this.member = _; + } + }); + } + this.perminfo ??= { channels: {} }; + for (const thing of json.channels) { + const temp = new Channel(thing, this); + this.channels.push(temp); + this.localuser.channelids.set(temp.id, temp); + } + this.headchannels = []; + for (const thing of this.channels) { + const parent = thing.resolveparent(this); + if (!parent) { + this.headchannels.push(thing); + } + } + this.prevchannel = this.localuser.channelids.get(this.perminfo.prevchannel); + } + get perminfo() { + return this.localuser.perminfo.guilds[this.id]; + } + set perminfo(e) { + this.localuser.perminfo.guilds[this.id] = e; + } + notisetting(settings: { + channel_overrides?: unknown[]; + message_notifications: any; + flags?: number; + hide_muted_channels?: boolean; + mobile_push?: boolean; + mute_config?: null; + mute_scheduled_events?: boolean; + muted?: boolean; + notify_highlights?: number; + suppress_everyone?: boolean; + suppress_roles?: boolean; + version?: number; + guild_id?: string; + }) { + this.message_notifications = settings.message_notifications; + } + setnotifcation() { + let noti = this.message_notifications; + const notiselect = new Dialog([ + "vdiv", + [ + "radio", + "select notifications type", + ["all", "only mentions", "none"], + function (e: string /* "all" | "only mentions" | "none" */) { + noti = ["all", "only mentions", "none"].indexOf(e); + }, + noti, + ], + [ + "button", + "", + "submit", + (_: any) => { + // + fetch(this.info.api + `/users/@me/guilds/${this.id}/settings/`, { + method: "PATCH", + headers: this.headers, + body: JSON.stringify({ + message_notifications: noti, + }), + }); + this.message_notifications = noti; + }, + ], + ]); + notiselect.show(); + } + confirmleave() { + const full = new Dialog([ + "vdiv", + ["title", "Are you sure you want to leave?"], + [ + "hdiv", + [ + "button", + "", + "Yes, I'm sure", + (_: any) => { + this.leave().then((_) => { + full.hide(); + }); + }, + ], + [ + "button", + "", + "Nevermind", + (_: any) => { + full.hide(); + }, + ], + ], + ]); + full.show(); + } + async leave() { + return fetch(this.info.api + "/users/@me/guilds/" + this.id, { + method: "DELETE", + headers: this.headers, + }); + } + printServers() { + let build = ""; + for (const thing of this.headchannels) { + build += thing.name + ":" + thing.position + "\n"; + for (const thingy of thing.children) { + build += " " + thingy.name + ":" + thingy.position + "\n"; + } + } + console.log(build); + } + calculateReorder() { + let position = -1; + const build: { + id: string; + position: number | undefined; + parent_id: string | undefined; + }[] = []; + for (const thing of this.headchannels) { + const thisthing: { + id: string; + position: number | undefined; + parent_id: string | undefined; + } = { id: thing.id, position: undefined, parent_id: undefined }; + if (thing.position <= position) { + thing.position = thisthing.position = position + 1; + } + position = thing.position; + console.log(position); + if (thing.move_id && thing.move_id !== thing.parent_id) { + thing.parent_id = thing.move_id; + thisthing.parent_id = thing.parent?.id; + thing.move_id = undefined; + } + if (thisthing.position || thisthing.parent_id) { + build.push(thisthing); + } + if (thing.children.length > 0) { + const things = thing.calculateReorder(); + for (const thing of things) { + build.push(thing); + } + } + } + console.log(build); + this.printServers(); + if (build.length === 0) { + return; + } + const serverbug = false; + if (serverbug) { + for (const thing of build) { + console.log(build, thing); + fetch(this.info.api + "/guilds/" + this.id + "/channels", { + method: "PATCH", + headers: this.headers, + body: JSON.stringify([thing]), + }); + } + } else { + fetch(this.info.api + "/guilds/" + this.id + "/channels", { + method: "PATCH", + headers: this.headers, + body: JSON.stringify(build), + }); + } + } + get localuser() { + return this.owner; + } + get info() { + return this.owner.info; + } + sortchannels() { + this.headchannels.sort((a, b) => { + return a.position - b.position; + }); + } + static generateGuildIcon( + guild: Guild | (invitejson["guild"] & { info: { cdn: string } }) + ) { + const divy = document.createElement("div"); + divy.classList.add("servernoti"); + + const noti = document.createElement("div"); + noti.classList.add("unread"); + divy.append(noti); + if (guild instanceof Guild) { + guild.localuser.guildhtml.set(guild.id, divy); + } + let icon: string | null; + if (guild instanceof Guild) { + icon = guild.properties.icon; + } else { + icon = guild.icon; + } + if (icon !== null) { + const img = document.createElement("img"); + img.classList.add("pfp", "servericon"); + img.src = guild.info.cdn + "/icons/" + guild.id + "/" + icon + ".png"; + divy.appendChild(img); + if (guild instanceof Guild) { + img.onclick = () => { + console.log(guild.loadGuild); + guild.loadGuild(); + guild.loadChannel(); + }; + Guild.contextmenu.bindContextmenu(img, guild, undefined); + } + } else { + const div = document.createElement("div"); + let name: string; + if (guild instanceof Guild) { + name = guild.properties.name; + } else { + name = guild.name; + } + const build = name + .replace(/'s /g, " ") + .replace(/\w+/g, (word) => word[0]) + .replace(/\s/g, ""); + div.textContent = build; + div.classList.add("blankserver", "servericon"); + divy.appendChild(div); + if (guild instanceof Guild) { + div.onclick = () => { + guild.loadGuild(); + guild.loadChannel(); + }; + Guild.contextmenu.bindContextmenu(div, guild, undefined); + } + } + return divy; + } + generateGuildIcon() { + return Guild.generateGuildIcon(this); + } + confirmDelete() { + let confirmname = ""; + const full = new Dialog([ + "vdiv", + [ + "title", + "Are you sure you want to delete " + this.properties.name + "?", + ], + [ + "textbox", + "Name of server:", + "", + function (this: HTMLInputElement) { + confirmname = this.value; + }, + ], + [ + "hdiv", + [ + "button", + "", + "Yes, I'm sure", + (_: any) => { + console.log(confirmname); + if (confirmname !== this.properties.name) { + return; + } + this.delete().then((_) => { + full.hide(); + }); + }, + ], + [ + "button", + "", + "Nevermind", + (_: any) => { + full.hide(); + }, + ], + ], + ]); + full.show(); + } + async delete() { + return fetch(this.info.api + "/guilds/" + this.id + "/delete", { + method: "POST", + headers: this.headers, + }); + } + unreads(html?: HTMLElement | undefined) { + if (html) { + this.html = html; + } else { + html = this.html; + } + let read = true; + for (const thing of this.channels) { + if (thing.hasunreads) { + console.log(thing); + read = false; + break; + } + } + if (!html) { + return; + } + if (read) { + html.children[0].classList.remove("notiunread"); + } else { + html.children[0].classList.add("notiunread"); + } + } + getHTML() { + //this.printServers(); + this.sortchannels(); + this.printServers(); + const build = document.createElement("div"); + + for (const thing of this.headchannels) { + build.appendChild(thing.createguildHTML(this.isAdmin())); + } + return build; + } + isAdmin() { + return this.member.isAdmin(); + } + async markAsRead() { + const build: { + read_states: { + channel_id: string; + message_id: string | null | undefined; + read_state_type: number; + }[]; + } = { read_states: [] }; + for (const thing of this.channels) { + if (thing.hasunreads) { + build.read_states.push({ + channel_id: thing.id, + message_id: thing.lastmessageid, + read_state_type: 0, + }); + thing.lastreadmessageid = thing.lastmessageid; + if (!thing.myhtml) continue; + thing.myhtml.classList.remove("cunread"); + } + } + this.unreads(); + fetch(this.info.api + "/read-states/ack-bulk", { + method: "POST", + headers: this.headers, + body: JSON.stringify(build), + }); + } + hasRole(r: Role | string) { + console.log("this should run"); + if (r instanceof Role) { + r = r.id; + } + return this.member.hasRole(r); + } + loadChannel(ID?: string | undefined) { + if (ID) { + const channel = this.localuser.channelids.get(ID); + if (channel) { + channel.getHTML(); + return; + } + } + if (this.prevchannel) { + console.log(this.prevchannel); + this.prevchannel.getHTML(); + return; + } + for (const thing of this.channels) { + if (thing.children.length === 0) { + thing.getHTML(); + return; + } + } + } + loadGuild() { + this.localuser.loadGuild(this.id); + } + updateChannel(json: channeljson) { + const channel = this.localuser.channelids.get(json.id); + if (channel) { + channel.updateChannel(json); + this.headchannels = []; + for (const thing of this.channels) { + thing.children = []; + } + this.headchannels = []; + for (const thing of this.channels) { + const parent = thing.resolveparent(this); + if (!parent) { + this.headchannels.push(thing); + } + } + this.printServers(); + } + } + createChannelpac(json: channeljson) { + const thischannel = new Channel(json, this); + this.localuser.channelids.set(json.id, thischannel); + this.channels.push(thischannel); + thischannel.resolveparent(this); + if (!thischannel.parent) { + this.headchannels.push(thischannel); + } + this.calculateReorder(); + this.printServers(); + return thischannel; + } + createchannels(func = this.createChannel) { + let name = ""; + let category = 0; + const channelselect = new Dialog([ + "vdiv", + [ + "radio", + "select channel type", + ["voice", "text", "announcement"], + function (radio: string) { + console.log(radio); + category = + { text: 0, voice: 2, announcement: 5, category: 4 }[radio] || 0; + }, + 1, + ], + [ + "textbox", + "Name of channel", + "", + function (this: HTMLInputElement) { + name = this.value; + }, + ], + [ + "button", + "", + "submit", + function () { + console.log(name, category); + func(name, category); + channelselect.hide(); + }, + ], + ]); + channelselect.show(); + } + createcategory() { + let name = ""; + const category = 4; + const channelselect = new Dialog([ + "vdiv", + [ + "textbox", + "Name of category", + "", + function (this: HTMLInputElement) { + name = this.value; + }, + ], + [ + "button", + "", + "submit", + () => { + console.log(name, category); + this.createChannel(name, category); + channelselect.hide(); + }, + ], + ]); + channelselect.show(); + } + delChannel(json: channeljson) { + const channel = this.localuser.channelids.get(json.id); + this.localuser.channelids.delete(json.id); + if (!channel) return; + this.channels.splice(this.channels.indexOf(channel), 1); + const indexy = this.headchannels.indexOf(channel); + if (indexy !== -1) { + this.headchannels.splice(indexy, 1); + } + + /* + const build=[]; + for(const thing of this.channels){ + console.log(thing.id); + if(thing!==channel){ + build.push(thing) + }else{ + console.log("fail"); + if(thing.parent){ + thing.parent.delChannel(json); + } + } + } + this.channels=build; + */ + this.printServers(); + } + createChannel(name: string, type: number) { + fetch(this.info.api + "/guilds/" + this.id + "/channels", { + method: "POST", + headers: this.headers, + body: JSON.stringify({ name, type }), + }); + } + async createRole(name: string) { + const fetched = await fetch( + this.info.api + "/guilds/" + this.id + "roles", + { + method: "POST", + headers: this.headers, + body: JSON.stringify({ + name, + color: 0, + permissions: "0", + }), + } + ); + const json = await fetched.json(); + const role = new Role(json, this); + this.roleids.set(role.id, role); + this.roles.push(role); + return role; + } + async updateRolePermissions(id: string, perms: Permissions) { + const role = this.roleids.get(id); + if (!role) { + return; + } + role.permissions.allow = perms.allow; + role.permissions.deny = perms.deny; + + await fetch(this.info.api + "/guilds/" + this.id + "/roles/" + role.id, { + method: "PATCH", + headers: this.headers, + body: JSON.stringify({ + color: role.color, + hoist: role.hoist, + icon: role.icon, + mentionable: role.mentionable, + name: role.name, + permissions: role.permissions.allow.toString(), + unicode_emoji: role.unicode_emoji, + }), + }); + } +} +Guild.setupcontextmenu(); +export { Guild }; diff --git a/webpage/home.html b/src/webpage/home.html similarity index 79% rename from webpage/home.html rename to src/webpage/home.html index c54b337..4266bd6 100644 --- a/webpage/home.html +++ b/src/webpage/home.html @@ -4,8 +4,7 @@ Jank Client - - + @@ -14,10 +13,12 @@
diff --git a/src/webpage/home.ts b/src/webpage/home.ts new file mode 100644 index 0000000..7301cd9 --- /dev/null +++ b/src/webpage/home.ts @@ -0,0 +1,89 @@ +import { mobile } from "./login.js"; +console.log(mobile); +const serverbox = document.getElementById("instancebox") as HTMLDivElement; + +fetch("/instances.json") + .then((_) => _.json()) + .then( + ( + json: { + name: string; + description?: string; + descriptionLong?: string; + image?: string; + url?: string; + display?: boolean; + online?: boolean; + uptime: { alltime: number; daytime: number; weektime: number }; + urls: { + wellknown: string; + api: string; + cdn: string; + gateway: string; + login?: string; + }; + }[] + ) => { + console.warn(json); + for (const instance of json) { + if (instance.display === false) { + continue; + } + const div = document.createElement("div"); + div.classList.add("flexltr", "instance"); + if (instance.image) { + const img = document.createElement("img"); + img.src = instance.image; + div.append(img); + } + const statbox = document.createElement("div"); + statbox.classList.add("flexttb"); + + { + const textbox = document.createElement("div"); + textbox.classList.add("flexttb", "instatancetextbox"); + const title = document.createElement("h2"); + title.innerText = instance.name; + if (instance.online !== undefined) { + const status = document.createElement("span"); + status.innerText = instance.online ? "Online" : "Offline"; + status.classList.add("instanceStatus"); + title.append(status); + } + textbox.append(title); + if (instance.description || instance.descriptionLong) { + const p = document.createElement("p"); + if (instance.descriptionLong) { + p.innerText = instance.descriptionLong; + } else if (instance.description) { + p.innerText = instance.description; + } + textbox.append(p); + } + statbox.append(textbox); + } + if (instance.uptime) { + const stats = document.createElement("div"); + stats.classList.add("flexltr"); + const span = document.createElement("span"); + span.innerText = `Uptime: All time: ${Math.round( + instance.uptime.alltime * 100 + )}% This week: ${Math.round( + instance.uptime.weektime * 100 + )}% Today: ${Math.round(instance.uptime.daytime * 100)}%`; + stats.append(span); + statbox.append(stats); + } + div.append(statbox); + div.onclick = (_) => { + if (instance.online) { + window.location.href = + "/register.html?instance=" + encodeURI(instance.name); + } else { + alert("Instance is offline, can't connect"); + } + }; + serverbox.append(div); + } + } + ); diff --git a/webpage/icons/announce.svg b/src/webpage/icons/announce.svg similarity index 100% rename from webpage/icons/announce.svg rename to src/webpage/icons/announce.svg diff --git a/webpage/icons/category.svg b/src/webpage/icons/category.svg similarity index 100% rename from webpage/icons/category.svg rename to src/webpage/icons/category.svg diff --git a/webpage/icons/channel.svg b/src/webpage/icons/channel.svg similarity index 100% rename from webpage/icons/channel.svg rename to src/webpage/icons/channel.svg diff --git a/webpage/icons/copy.svg b/src/webpage/icons/copy.svg similarity index 100% rename from webpage/icons/copy.svg rename to src/webpage/icons/copy.svg diff --git a/webpage/icons/delete.svg b/src/webpage/icons/delete.svg similarity index 100% rename from webpage/icons/delete.svg rename to src/webpage/icons/delete.svg diff --git a/webpage/icons/edit.svg b/src/webpage/icons/edit.svg similarity index 100% rename from webpage/icons/edit.svg rename to src/webpage/icons/edit.svg diff --git a/webpage/icons/explore.svg b/src/webpage/icons/explore.svg similarity index 100% rename from webpage/icons/explore.svg rename to src/webpage/icons/explore.svg diff --git a/webpage/icons/home.svg b/src/webpage/icons/home.svg similarity index 100% rename from webpage/icons/home.svg rename to src/webpage/icons/home.svg diff --git a/webpage/icons/reply.svg b/src/webpage/icons/reply.svg similarity index 100% rename from webpage/icons/reply.svg rename to src/webpage/icons/reply.svg diff --git a/webpage/icons/settings.svg b/src/webpage/icons/settings.svg similarity index 100% rename from webpage/icons/settings.svg rename to src/webpage/icons/settings.svg diff --git a/webpage/icons/voice.svg b/src/webpage/icons/voice.svg similarity index 100% rename from webpage/icons/voice.svg rename to src/webpage/icons/voice.svg diff --git a/webpage/index.html b/src/webpage/index.html similarity index 88% rename from webpage/index.html rename to src/webpage/index.html index 6b466fa..616a1ea 100644 --- a/webpage/index.html +++ b/src/webpage/index.html @@ -2,7 +2,8 @@ - + + Jank Client @@ -19,10 +20,10 @@
- + Logo

Jank Client is loading

This shouldn't take long

-

Switch Accounts

+

Switch Accounts

@@ -36,7 +37,7 @@
- + User Profile Picture

USERNAME

diff --git a/src/webpage/index.ts b/src/webpage/index.ts new file mode 100644 index 0000000..18f2d40 --- /dev/null +++ b/src/webpage/index.ts @@ -0,0 +1,259 @@ +import { Localuser } from "./localuser.js"; +import { Contextmenu } from "./contextmenu.js"; +import { mobile, getBulkUsers, setTheme, Specialuser } from "./login.js"; +import { MarkDown } from "./markdown.js"; +import { Message } from "./message.js"; +import { File } from "./file.js"; + +(async () => { + async function waitForLoad(): Promise { + return new Promise((resolve) => { + document.addEventListener("DOMContentLoaded", (_) => resolve()); + }); + } + + await waitForLoad(); + + const users = getBulkUsers(); + if (!users.currentuser) { + window.location.href = "/login.html"; + return; + } + + function showAccountSwitcher(): void { + const table = document.createElement("div"); + table.classList.add("accountSwitcher"); + + for (const user of Object.values(users.users)) { + const specialUser = user as Specialuser; + const userInfo = document.createElement("div"); + userInfo.classList.add("flexltr", "switchtable"); + + const pfp = document.createElement("img"); + pfp.src = specialUser.pfpsrc; + pfp.classList.add("pfp"); + userInfo.append(pfp); + + const userDiv = document.createElement("div"); + userDiv.classList.add("userinfo"); + userDiv.textContent = specialUser.username; + userDiv.append(document.createElement("br")); + + const span = document.createElement("span"); + span.textContent = specialUser.serverurls.wellknown + .replace("https://", "") + .replace("http://", ""); + span.classList.add("serverURL"); + userDiv.append(span); + + userInfo.append(userDiv); + table.append(userInfo); + + userInfo.addEventListener("click", () => { + thisUser.unload(); + thisUser.swapped = true; + const loading = document.getElementById("loading") as HTMLDivElement; + loading.classList.remove("doneloading"); + loading.classList.add("loading"); + + thisUser = new Localuser(specialUser); + users.currentuser = specialUser.uid; + localStorage.setItem("userinfos", JSON.stringify(users)); + + thisUser.initwebsocket().then(() => { + thisUser.loaduser(); + thisUser.init(); + loading.classList.add("doneloading"); + loading.classList.remove("loading"); + console.log("done loading"); + }); + + userInfo.remove(); + }); + } + + const switchAccountDiv = document.createElement("div"); + switchAccountDiv.classList.add("switchtable"); + switchAccountDiv.textContent = "Switch accounts ⇌"; + switchAccountDiv.addEventListener("click", () => { + window.location.href = "/login.html"; + }); + table.append(switchAccountDiv); + + if (Contextmenu.currentmenu) { + Contextmenu.currentmenu.remove(); + } + Contextmenu.currentmenu = table; + document.body.append(table); + } + + const userInfoElement = document.getElementById("userinfo") as HTMLDivElement; + userInfoElement.addEventListener("click", (event) => { + event.stopImmediatePropagation(); + showAccountSwitcher(); + }); + + const switchAccountsElement = document.getElementById( + "switchaccounts" + ) as HTMLDivElement; + switchAccountsElement.addEventListener("click", (event) => { + event.stopImmediatePropagation(); + showAccountSwitcher(); + }); + + let thisUser: Localuser; + try { + console.log(users.users, users.currentuser); + thisUser = new Localuser(users.users[users.currentuser]); + thisUser.initwebsocket().then(() => { + thisUser.loaduser(); + thisUser.init(); + const loading = document.getElementById("loading") as HTMLDivElement; + loading.classList.add("doneloading"); + loading.classList.remove("loading"); + console.log("done loading"); + }); + } catch (e) { + console.error(e); + (document.getElementById("load-desc") as HTMLSpanElement).textContent = + "Account unable to start"; + thisUser = new Localuser(-1); + } + + const menu = new Contextmenu("create rightclick"); + menu.addbutton( + "Create channel", + () => { + if (thisUser.lookingguild) { + thisUser.lookingguild.createchannels(); + } + }, + null, + () => thisUser.isAdmin() + ); + + menu.addbutton( + "Create category", + () => { + if (thisUser.lookingguild) { + thisUser.lookingguild.createcategory(); + } + }, + null, + () => thisUser.isAdmin() + ); + + menu.bindContextmenu( + document.getElementById("channels") as HTMLDivElement, + 0, + 0 + ); + + const pasteImageElement = document.getElementById( + "pasteimage" + ) as HTMLDivElement; + let replyingTo: Message | null = null; + + async function handleEnter(event: KeyboardEvent): Promise { + const channel = thisUser.channelfocus; + if (!channel) return; + + channel.typingstart(); + + if (event.key === "Enter" && !event.shiftKey) { + event.preventDefault(); + + if (channel.editing) { + channel.editing.edit(markdown.rawString); + channel.editing = null; + } else { + replyingTo = thisUser.channelfocus + ? thisUser.channelfocus.replyingto + : null; + if (replyingTo?.div) { + replyingTo.div.classList.remove("replying"); + } + if (thisUser.channelfocus) { + thisUser.channelfocus.replyingto = null; + } + channel.sendMessage(markdown.rawString, { + attachments: images, + // @ts-ignore This is valid according to the API + embeds: [], // Add an empty array for the embeds property + replyingto: replyingTo, + }); + if (thisUser.channelfocus) { + thisUser.channelfocus.makereplybox(); + } + } + + while (images.length) { + images.pop(); + pasteImageElement.removeChild(imagesHtml.pop() as HTMLElement); + } + + typebox.innerHTML = ""; + } + } + + interface CustomHTMLDivElement extends HTMLDivElement { + markdown: MarkDown; + } + + const typebox = document.getElementById("typebox") as CustomHTMLDivElement; + const markdown = new MarkDown("", thisUser); + typebox.markdown = markdown; + typebox.addEventListener("keyup", handleEnter); + typebox.addEventListener("keydown", (event) => { + if (event.key === "Enter" && !event.shiftKey) event.preventDefault(); + }); + markdown.giveBox(typebox); + + const images: Blob[] = []; + const imagesHtml: HTMLElement[] = []; + + document.addEventListener("paste", async (e: ClipboardEvent) => { + if (!e.clipboardData) return; + + for (const file of Array.from(e.clipboardData.files)) { + const fileInstance = File.initFromBlob(file); + e.preventDefault(); + const html = fileInstance.upHTML(images, file); + pasteImageElement.appendChild(html); + images.push(file); + imagesHtml.push(html); + } + }); + + setTheme(); + + function userSettings(): void { + thisUser.showusersettings(); + } + + (document.getElementById("settings") as HTMLImageElement).onclick = + userSettings; + + if (mobile) { + const channelWrapper = document.getElementById( + "channelw" + ) as HTMLDivElement; + channelWrapper.onclick = () => { + ( + document.getElementById("channels")!.parentNode as HTMLElement + ).classList.add("collapse"); + document.getElementById("servertd")!.classList.add("collapse"); + document.getElementById("servers")!.classList.add("collapse"); + }; + + const mobileBack = document.getElementById("mobileback") as HTMLDivElement; + mobileBack.textContent = "#"; + mobileBack.onclick = () => { + ( + document.getElementById("channels")!.parentNode as HTMLElement + ).classList.remove("collapse"); + document.getElementById("servertd")!.classList.remove("collapse"); + document.getElementById("servers")!.classList.remove("collapse"); + }; + } +})(); diff --git a/src/webpage/infiniteScroller.ts b/src/webpage/infiniteScroller.ts new file mode 100644 index 0000000..0020cb6 --- /dev/null +++ b/src/webpage/infiniteScroller.ts @@ -0,0 +1,323 @@ +class InfiniteScroller { + readonly getIDFromOffset: ( + ID: string, + offset: number + ) => Promise; + readonly getHTMLFromID: (ID: string) => Promise; + readonly destroyFromID: (ID: string) => Promise; + readonly reachesBottom: () => void; + private readonly minDist = 2000; + private readonly fillDist = 3000; + private readonly maxDist = 6000; + HTMLElements: [HTMLElement, string][] = []; + div: HTMLDivElement | null = null; + timeout: NodeJS.Timeout | null = null; + beenloaded = false; + scrollBottom = 0; + scrollTop = 0; + needsupdate = true; + averageheight = 60; + watchtime = false; + changePromise: Promise | undefined; + scollDiv!: { scrollTop: number; scrollHeight: number; clientHeight: number }; + + constructor( + getIDFromOffset: InfiniteScroller["getIDFromOffset"], + getHTMLFromID: InfiniteScroller["getHTMLFromID"], + destroyFromID: InfiniteScroller["destroyFromID"], + reachesBottom: InfiniteScroller["reachesBottom"] = () => {} + ) { + this.getIDFromOffset = getIDFromOffset; + this.getHTMLFromID = getHTMLFromID; + this.destroyFromID = destroyFromID; + this.reachesBottom = reachesBottom; + } + + async getDiv(initialId: string): Promise { + if (this.div) { + throw new Error("Div already exists, exiting."); + } + + const scroll = document.createElement("div"); + scroll.classList.add("flexttb", "scroller"); + this.div = scroll; + + this.div.addEventListener("scroll", () => { + this.checkscroll(); + if (this.scrollBottom < 5) { + this.scrollBottom = 5; + } + if (this.timeout === null) { + this.timeout = setTimeout(this.updatestuff.bind(this), 300); + } + this.watchForChange(); + }); + + let oldheight = 0; + new ResizeObserver(() => { + this.checkscroll(); + const func = this.snapBottom(); + this.updatestuff(); + const change = oldheight - scroll.offsetHeight; + if (change > 0 && this.div) { + this.div.scrollTop += change; + } + oldheight = scroll.offsetHeight; + this.watchForChange(); + func(); + }).observe(scroll); + + new ResizeObserver(this.watchForChange.bind(this)).observe(scroll); + + await this.firstElement(initialId); + this.updatestuff(); + await this.watchForChange().then(() => { + this.updatestuff(); + this.beenloaded = true; + }); + + return scroll; + } + + checkscroll(): void { + if (this.beenloaded && this.div && !document.body.contains(this.div)) { + console.warn("not in document"); + this.div = null; + } + } + + async updatestuff(): Promise { + this.timeout = null; + if (!this.div) return; + + this.scrollBottom = + this.div.scrollHeight - this.div.scrollTop - this.div.clientHeight; + this.averageheight = this.div.scrollHeight / this.HTMLElements.length; + if (this.averageheight < 10) { + this.averageheight = 60; + } + this.scrollTop = this.div.scrollTop; + + if (!this.scrollBottom && !(await this.watchForChange())) { + this.reachesBottom(); + } + if (!this.scrollTop) { + await this.watchForChange(); + } + this.needsupdate = false; + } + + async firstElement(id: string): Promise { + if (!this.div) return; + const html = await this.getHTMLFromID(id); + this.div.appendChild(html); + this.HTMLElements.push([html, id]); + } + + async addedBottom(): Promise { + await this.updatestuff(); + const func = this.snapBottom(); + await this.watchForChange(); + func(); + } + + snapBottom(): () => void { + const scrollBottom = this.scrollBottom; + return () => { + if (this.div && scrollBottom < 4) { + this.div.scrollTop = this.div.scrollHeight; + } + }; + } + + private async watchForTop( + already = false, + fragment = new DocumentFragment() + ): Promise { + if (!this.div) return false; + try { + let again = false; + if (this.scrollTop < (already ? this.fillDist : this.minDist)) { + let nextid: string | undefined; + const firstelm = this.HTMLElements.at(0); + if (firstelm) { + const previd = firstelm[1]; + nextid = await this.getIDFromOffset(previd, 1); + } + + if (nextid) { + const html = await this.getHTMLFromID(nextid); + if (!html) { + this.destroyFromID(nextid); + return false; + } + again = true; + fragment.prepend(html); + this.HTMLElements.unshift([html, nextid]); + this.scrollTop += this.averageheight; + } + } + if (this.scrollTop > this.maxDist) { + const html = this.HTMLElements.shift(); + if (html) { + again = true; + await this.destroyFromID(html[1]); + this.scrollTop -= this.averageheight; + } + } + if (again) { + await this.watchForTop(true, fragment); + } + return again; + } finally { + if (!already) { + if (this.div.scrollTop === 0) { + this.scrollTop = 1; + this.div.scrollTop = 10; + } + this.div.prepend(fragment, fragment); + } + } + } + + async watchForBottom( + already = false, + fragment = new DocumentFragment() + ): Promise { + let func: Function | undefined; + if (!already) func = this.snapBottom(); + if (!this.div) return false; + try { + let again = false; + const scrollBottom = this.scrollBottom; + if (scrollBottom < (already ? this.fillDist : this.minDist)) { + let nextid: string | undefined; + const lastelm = this.HTMLElements.at(-1); + if (lastelm) { + const previd = lastelm[1]; + nextid = await this.getIDFromOffset(previd, -1); + } + if (nextid) { + again = true; + const html = await this.getHTMLFromID(nextid); + fragment.appendChild(html); + this.HTMLElements.push([html, nextid]); + this.scrollBottom += this.averageheight; + } + } + if (scrollBottom > this.maxDist) { + const html = this.HTMLElements.pop(); + if (html) { + await this.destroyFromID(html[1]); + this.scrollBottom -= this.averageheight; + again = true; + } + } + if (again) { + await this.watchForBottom(true, fragment); + } + return again; + } finally { + if (!already) { + this.div.append(fragment); + if (func) { + func(); + } + } + } + } + + async watchForChange(): Promise { + if (this.changePromise) { + this.watchtime = true; + return await this.changePromise; + } else { + this.watchtime = false; + } + + this.changePromise = new Promise(async (res) => { + try { + if (!this.div) { + res(false); + return false; + } + const out = (await Promise.allSettled([ + this.watchForTop(), + this.watchForBottom(), + ])) as { value: boolean }[]; + const changed = out[0].value || out[1].value; + if (this.timeout === null && changed) { + this.timeout = setTimeout(this.updatestuff.bind(this), 300); + } + res(Boolean(changed)); + return Boolean(changed); + } catch (e) { + console.error(e); + res(false); + return false; + } finally { + setTimeout(() => { + this.changePromise = undefined; + if (this.watchtime) { + this.watchForChange(); + } + }, 300); + } + }); + + return await this.changePromise; + } + + async focus(id: string, flash = true): Promise { + let element: HTMLElement | undefined; + for (const thing of this.HTMLElements) { + if (thing[1] === id) { + element = thing[0]; + } + } + if (element) { + if (flash) { + element.scrollIntoView({ + behavior: "smooth", + block: "center", + }); + await new Promise((resolve) => setTimeout(resolve, 1000)); + element.classList.remove("jumped"); + await new Promise((resolve) => setTimeout(resolve, 100)); + element.classList.add("jumped"); + } else { + element.scrollIntoView(); + } + } else { + for (const thing of this.HTMLElements) { + await this.destroyFromID(thing[1]); + } + this.HTMLElements = []; + await this.firstElement(id); + this.updatestuff(); + await this.watchForChange(); + await new Promise((resolve) => setTimeout(resolve, 100)); + await this.focus(id, true); + } + } + + async delete(): Promise { + if (this.div) { + this.div.remove(); + this.div = null; + } + try { + for (const thing of this.HTMLElements) { + await this.destroyFromID(thing[1]); + } + } catch (e) { + console.error(e); + } + this.HTMLElements = []; + if (this.timeout) { + clearTimeout(this.timeout); + } + } +} + +export { InfiniteScroller }; diff --git a/webpage/instances.json b/src/webpage/instances.json similarity index 71% rename from webpage/instances.json rename to src/webpage/instances.json index 43ad86d..328384c 100644 --- a/webpage/instances.json +++ b/src/webpage/instances.json @@ -1,9 +1,9 @@ [ { - "name":"Spacebar", - "description":"The official Spacebar instance.", - "image":"https://raw.githubusercontent.com/spacebarchat/spacebarchat/master/branding/png/Spacebar__Icon-Discord.png", - "url":"https://spacebar.chat" + "name": "Spacebar", + "description": "The official Spacebar instance.", + "image": "https://raw.githubusercontent.com/spacebarchat/spacebarchat/master/branding/png/Spacebar__Icon-Discord.png", + "url": "https://spacebar.chat" }, { "name": "Fastbar", @@ -13,16 +13,16 @@ "language": "en", "country": "US", "display": true, - "urls":{ + "urls": { "wellknown": "https://greysilly7.xyz", "api": "https://spacebar.greysilly7.xyz/api/v9", "cdn": "https://spacebar.greysilly7.xyz", "gateway": "wss://spacebar.greysilly7.xyz" }, - "contactInfo":{ + "contactInfo": { "dicord": "greysilly7", "github": "https://github.com/greysilly7", "email": "greysilly7@gmail.com" } } -] +] \ No newline at end of file diff --git a/webpage/invite.html b/src/webpage/invite.html similarity index 88% rename from webpage/invite.html rename to src/webpage/invite.html index 314a22e..b1c0299 100644 --- a/webpage/invite.html +++ b/src/webpage/invite.html @@ -4,7 +4,7 @@ Jank Client - + diff --git a/src/webpage/invite.ts b/src/webpage/invite.ts new file mode 100644 index 0000000..331fcc0 --- /dev/null +++ b/src/webpage/invite.ts @@ -0,0 +1,147 @@ +import { getBulkUsers, Specialuser, getapiurls } from "./login.js"; + +(async () => { + const users = getBulkUsers(); + const well = new URLSearchParams(window.location.search).get("instance"); + const joinable: Specialuser[] = []; + + for (const key in users.users) { + if (Object.prototype.hasOwnProperty.call(users.users, key)) { + const user: Specialuser = users.users[key]; + if (well && user.serverurls.wellknown.includes(well)) { + joinable.push(user); + } + console.log(user); + } + } + + let urls: { api: string; cdn: string } | undefined; + + if (!joinable.length && well) { + const out = await getapiurls(well); + if (out) { + urls = out; + for (const key in users.users) { + if (Object.prototype.hasOwnProperty.call(users.users, key)) { + const user: Specialuser = users.users[key]; + if (user.serverurls.api.includes(out.api)) { + joinable.push(user); + } + console.log(user); + } + } + } else { + throw new Error( + "Someone needs to handle the case where the servers don't exist" + ); + } + } else { + urls = joinable[0].serverurls; + } + + if (!joinable.length) { + document.getElementById("AcceptInvite")!.textContent = + "Create an account to accept the invite"; + } + + const code = window.location.pathname.split("/")[2]; + let guildinfo: any; + + fetch(`${urls!.api}/invites/${code}`, { + method: "GET", + }) + .then((response) => response.json()) + .then((json) => { + const guildjson = json.guild; + guildinfo = guildjson; + document.getElementById("invitename")!.textContent = guildjson.name; + document.getElementById( + "invitedescription" + )!.textContent = `${json.inviter.username} invited you to join ${guildjson.name}`; + if (guildjson.icon) { + const img = document.createElement("img"); + img.src = `${urls!.cdn}/icons/${guildjson.id}/${guildjson.icon}.png`; + img.classList.add("inviteGuild"); + document.getElementById("inviteimg")!.append(img); + } else { + const txt = guildjson.name + .replace(/'s /g, " ") + .replace(/\w+/g, (word: any[]) => word[0]) + .replace(/\s/g, ""); + const div = document.createElement("div"); + div.textContent = txt; + div.classList.add("inviteGuild"); + document.getElementById("inviteimg")!.append(div); + } + }); + + function showAccounts(): void { + const table = document.createElement("dialog"); + for (const user of joinable) { + console.log(user.pfpsrc); + + const userinfo = document.createElement("div"); + userinfo.classList.add("flexltr", "switchtable"); + + const pfp = document.createElement("img"); + pfp.src = user.pfpsrc; + pfp.classList.add("pfp"); + userinfo.append(pfp); + + const userDiv = document.createElement("div"); + userDiv.classList.add("userinfo"); + userDiv.textContent = user.username; + userDiv.append(document.createElement("br")); + + const span = document.createElement("span"); + span.textContent = user.serverurls.wellknown + .replace("https://", "") + .replace("http://", ""); + span.classList.add("serverURL"); + userDiv.append(span); + + userinfo.append(userDiv); + table.append(userinfo); + + userinfo.addEventListener("click", () => { + console.log(user); + fetch(`${urls!.api}/invites/${code}`, { + method: "POST", + headers: { + Authorization: user.token, + }, + }).then(() => { + users.currentuser = user.uid; + localStorage.setItem("userinfos", JSON.stringify(users)); + window.location.href = "/channels/" + guildinfo.id; + }); + }); + } + + const td = document.createElement("div"); + td.classList.add("switchtable"); + td.textContent = "Login or create an account ⇌"; + td.addEventListener("click", () => { + const l = new URLSearchParams("?"); + l.set("goback", window.location.href); + l.set("instance", well!); + window.location.href = "/login?" + l.toString(); + }); + + if (!joinable.length) { + const l = new URLSearchParams("?"); + l.set("goback", window.location.href); + l.set("instance", well!); + window.location.href = "/login?" + l.toString(); + } + + table.append(td); + table.classList.add("accountSwitcher"); + console.log(table); + document.body.append(table); + } + + document + .getElementById("AcceptInvite")! + .addEventListener("click", showAccounts); +})(); diff --git a/src/webpage/jsontypes.ts b/src/webpage/jsontypes.ts new file mode 100644 index 0000000..1bbeffc --- /dev/null +++ b/src/webpage/jsontypes.ts @@ -0,0 +1,501 @@ +type readyjson = { + op: 0; + t: "READY"; + s: number; + d: { + v: number; + user: mainuserjson; + user_settings: { + index: number; + afk_timeout: number; + allow_accessibility_detection: boolean; + animate_emoji: boolean; + animate_stickers: number; + contact_sync_enabled: boolean; + convert_emoticons: boolean; + custom_status: string; + default_guilds_restricted: boolean; + detect_platform_accounts: boolean; + developer_mode: boolean; + disable_games_tab: boolean; + enable_tts_command: boolean; + explicit_content_filter: 0; + friend_discovery_flags: 0; + friend_source_flags: { + all: boolean; + }; //might be missing things here + gateway_connected: boolean; + gif_auto_play: boolean; + guild_folders: []; //need an example of this not empty + guild_positions: []; //need an example of this not empty + inline_attachment_media: boolean; + inline_embed_media: boolean; + locale: string; + message_display_compact: boolean; + native_phone_integration_enabled: boolean; + render_embeds: boolean; + render_reactions: boolean; + restricted_guilds: []; //need an example of this not empty + show_current_game: boolean; + status: string; + stream_notifications_enabled: boolean; + theme: string; + timezone_offset: number; + view_nsfw_guilds: boolean; + }; + guilds: guildjson[]; + relationships: { + id: string; + type: 0 | 1 | 2 | 3 | 4; + nickname: string | null; + user: userjson; + }[]; + read_state: { + entries: { + id: string; + channel_id: string; + last_message_id: string; + last_pin_timestamp: string; + mention_count: number; //in theory, the server doesn't actually send this as far as I'm aware + }[]; + partial: boolean; + version: number; + }; + user_guild_settings: { + entries: { + channel_overrides: unknown[]; //will have to find example + message_notifications: number; + flags: number; + hide_muted_channels: boolean; + mobile_push: boolean; + mute_config: null; + mute_scheduled_events: boolean; + muted: boolean; + notify_highlights: number; + suppress_everyone: boolean; + suppress_roles: boolean; + version: number; + guild_id: string; + }[]; + partial: boolean; + version: number; + }; + private_channels: dirrectjson[]; + session_id: string; + country_code: string; + users: userjson[]; + merged_members: [memberjson][]; + sessions: { + active: boolean; + activities: []; //will need to find example of this + client_info: { + version: number; + }; + session_id: string; + status: string; + }[]; + resume_gateway_url: string; + consents: { + personalization: { + consented: boolean; + }; + }; + experiments: []; //not sure if I need to do this :P + guild_join_requests: []; //need to get examples + connected_accounts: []; //need to get examples + guild_experiments: []; //need to get examples + geo_ordered_rtc_regions: []; //need to get examples + api_code_version: number; + friend_suggestion_count: number; + analytics_token: string; + tutorial: boolean; + session_type: string; + auth_session_id_hash: string; + notification_settings: { + flags: number; + }; + }; +}; +type mainuserjson = userjson & { + flags: number; + mfa_enabled?: boolean; + email?: string; + phone?: string; + verified: boolean; + nsfw_allowed: boolean; + premium: boolean; + purchased_flags: number; + premium_usage_flags: number; + disabled: boolean; +}; +type userjson = { + username: string; + discriminator: string; + id: string; + public_flags: number; + avatar: string | null; + accent_color: number; + banner?: string; + bio: string; + bot: boolean; + premium_since: string; + premium_type: number; + theme_colors: string; + pronouns: string; + badge_ids: string[]; +}; +type memberjson = { + index?: number; + id: string; + user: userjson | null; + guild_id: string; + guild: { + id: string; + } | null; + nick?: string; + roles: string[]; + joined_at: string; + premium_since: string; + deaf: boolean; + mute: boolean; + pending: boolean; + last_message_id?: boolean; //What??? +}; +type emojijson = { + name: string; + id?: string; + animated?: boolean; +}; + +type guildjson = { + application_command_counts: { [key: string]: number }; + channels: channeljson[]; + data_mode: string; + emojis: emojijson[]; + guild_scheduled_events: []; + id: string; + large: boolean; + lazy: boolean; + member_count: number; + premium_subscription_count: number; + properties: { + region: string | null; + name: string; + description: string; + icon: string; + splash: string; + banner: string; + features: string[]; + preferred_locale: string; + owner_id: string; + application_id: string; + afk_channel_id: string; + afk_timeout: number; + member_count: number; + system_channel_id: string; + verification_level: number; + explicit_content_filter: number; + default_message_notifications: number; + mfa_level: number; + vanity_url_code: number; + premium_tier: number; + premium_progress_bar_enabled: boolean; + system_channel_flags: number; + discovery_splash: string; + rules_channel_id: string; + public_updates_channel_id: string; + max_video_channel_users: number; + max_members: number; + nsfw_level: number; + hub_type: null; + home_header: null; + id: string; + latest_onboarding_question_id: string; + max_stage_video_channel_users: number; + nsfw: boolean; + safety_alerts_channel_id: string; + }; + roles: rolesjson[]; + stage_instances: []; + stickers: []; + threads: []; + version: string; + guild_hashes: {}; + joined_at: string; +}; +type startTypingjson = { + d: { + channel_id: string; + guild_id?: string; + user_id: string; + timestamp: number; + member?: memberjson; + }; +}; +type channeljson = { + id: string; + created_at: string; + name: string; + icon: string; + type: number; + last_message_id: string; + guild_id: string; + parent_id: string; + last_pin_timestamp: string; + default_auto_archive_duration: number; + permission_overwrites: { + id: string; + allow: string; + deny: string; + }[]; + video_quality_mode: null; + nsfw: boolean; + topic: string; + retention_policy_id: string; + flags: number; + default_thread_rate_limit_per_user: number; + position: number; +}; +type rolesjson = { + id: string; + guild_id: string; + color: number; + hoist: boolean; + managed: boolean; + mentionable: boolean; + name: string; + permissions: string; + position: number; + icon: string; + unicode_emoji: string; + flags: number; +}; +type dirrectjson = { + id: string; + flags: number; + last_message_id: string; + type: number; + recipients: userjson[]; + is_spam: boolean; +}; +type messagejson = { + id: string; + channel_id: string; + guild_id: string; + author: userjson; + member?: memberjson; + content: string; + timestamp: string; + edited_timestamp: string; + tts: boolean; + mention_everyone: boolean; + mentions: []; //need examples to fix + mention_roles: []; //need examples to fix + attachments: filejson[]; + embeds: embedjson[]; + reactions: { + count: number; + emoji: emojijson; //very likely needs expanding + me: boolean; + }[]; + nonce: string; + pinned: boolean; + type: number; +}; +type filejson = { + id: string; + filename: string; + content_type: string; + width?: number; + height?: number; + proxy_url: string | undefined; + url: string; + size: number; +}; +type embedjson = { + type: string | null; + color?: number; + author: { + icon_url?: string; + name?: string; + url?: string; + title?: string; + }; + title?: string; + url?: string; + description?: string; + fields?: { + name: string; + value: string; + inline: boolean; + }[]; + footer?: { + icon_url?: string; + text?: string; + thumbnail?: string; + }; + timestamp?: string; + thumbnail: { + proxy_url: string; + url: string; + width: number; + height: number; + }; + provider: { + name: string; + }; + video?: { + url: string; + width?: number | null; + height?: number | null; + proxy_url?: string; + }; + invite?: { + url: string; + code: string; + }; +}; +type invitejson = { + code: string; + temporary: boolean; + uses: number; + max_use: number; + max_age: number; + created_at: string; + expires_at: string; + guild_id: string; + channel_id: string; + inviter_id: string; + target_user_id: string | null; + target_user_type: string | null; + vanity_url: string | null; + flags: number; + guild: guildjson["properties"]; + channel: channeljson; + inviter: userjson; +}; +type presencejson = { + status: string; + since: number | null; + activities: any[]; //bit more complicated but not now + afk: boolean; + user?: userjson; +}; +type messageCreateJson = { + op: 0; + d: { + guild_id?: string; + channel_id?: string; + } & messagejson; + s: number; + t: "MESSAGE_CREATE"; +}; +type wsjson = + | { + op: 0; + d: any; + s: number; + t: + | "TYPING_START" + | "USER_UPDATE" + | "CHANNEL_UPDATE" + | "CHANNEL_CREATE" + | "CHANNEL_DELETE" + | "GUILD_DELETE" + | "GUILD_CREATE" + | "MESSAGE_REACTION_REMOVE_ALL" + | "MESSAGE_REACTION_REMOVE_EMOJI"; + } + | { + op: 0; + t: "GUILD_MEMBERS_CHUNK"; + d: memberChunk; + s: number; + } + | { + op: 0; + d: { + id: string; + guild_id?: string; + channel_id: string; + }; + s: number; + t: "MESSAGE_DELETE"; + } + | { + op: 0; + d: { + guild_id?: string; + channel_id: string; + } & messagejson; + s: number; + t: "MESSAGE_UPDATE"; + } + | messageCreateJson + | readyjson + | { + op: 11; + s: undefined; + d: {}; + } + | { + op: 10; + s: undefined; + d: { + heartbeat_interval: number; + }; + } + | { + op: 0; + t: "MESSAGE_REACTION_ADD"; + d: { + user_id: string; + channel_id: string; + message_id: string; + guild_id?: string; + emoji: emojijson; + member?: memberjson; + }; + s: number; + } + | { + op: 0; + t: "MESSAGE_REACTION_REMOVE"; + d: { + user_id: string; + channel_id: string; + message_id: string; + guild_id: string; + emoji: emojijson; + }; + s: 3; + }; +type memberChunk = { + guild_id: string; + nonce: string; + members: memberjson[]; + presences: presencejson[]; + chunk_index: number; + chunk_count: number; + not_found: string[]; +}; +export { + readyjson, + dirrectjson, + startTypingjson, + channeljson, + guildjson, + rolesjson, + userjson, + memberjson, + mainuserjson, + messagejson, + filejson, + embedjson, + emojijson, + presencejson, + wsjson, + messageCreateJson, + memberChunk, + invitejson, +}; diff --git a/src/webpage/localuser.ts b/src/webpage/localuser.ts new file mode 100644 index 0000000..40caa46 --- /dev/null +++ b/src/webpage/localuser.ts @@ -0,0 +1,1824 @@ +import { Guild } from "./guild.js"; +import { Channel } from "./channel.js"; +import { Direct } from "./direct.js"; +import { Voice } from "./audio.js"; +import { User } from "./user.js"; +import { Dialog } from "./dialog.js"; +import { getapiurls, getBulkInfo, setTheme, Specialuser } from "./login.js"; +import { + channeljson, + guildjson, + memberjson, + messageCreateJson, + presencejson, + readyjson, + startTypingjson, + wsjson, +} from "./jsontypes.js"; +import { Member } from "./member.js"; +import { FormError, Settings } from "./settings.js"; +import { MarkDown } from "./markdown.js"; + +const wsCodesRetry = new Set([4000, 4003, 4005, 4007, 4008, 4009]); + +class Localuser { + badges: Map< + string, + { id: string; description: string; icon: string; link: string } + > = new Map(); + lastSequence: number | null = null; + token!: string; + userinfo!: Specialuser; + serverurls!: Specialuser["serverurls"]; + initialized!: boolean; + info!: Specialuser["serverurls"]; + headers!: { "Content-type": string; Authorization: string }; + userConnections!: Dialog; + devPortal!: Dialog; + 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(); + instancePing = { + name: "Unknown", + }; + mfa_enabled!: boolean; + get perminfo() { + return this.userinfo.localuserStore; + } + set perminfo(e) { + this.userinfo.localuserStore = e; + } + constructor(userinfo: Specialuser | -1) { + if (userinfo === -1) { + return; + } + this.token = userinfo.token; + this.userinfo = userinfo; + this.perminfo.guilds ??= {}; + 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, + }; + } + gottenReady(ready: readyjson): void { + 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.mfa_enabled = ready.d.user.mfa_enabled as boolean; + this.userinfo.username = this.user.username; + 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 } = {}; + 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); + } + 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); + } + + 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.pingEndpoint(); + this.userinfo.updateLocal(); + } + 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(4001); + } + } + swapped = false; + async initwebsocket(): Promise { + let returny: () => void; + const ws = new WebSocket( + 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"); + 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 = ""; + if (temp.op === 0 && temp.t === "READY") { + returny(); + } + await this.handleEvent(temp); + } 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); + } + if (temp.op === 0 && temp.t === "READY") { + returny(); + } + await this.handleEvent(temp as readyjson); + } catch (e) { + console.error(e); + } finally { + res(); + } + }); + }); + + ws.addEventListener("close", async (event) => { + this.ws = undefined; + console.log("WebSocket closed with code " + event.code); + + 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) + ) { + if ( + this.connectionSucceed !== 0 && + Date.now() > this.connectionSucceed + 20000 + ) + this.errorBackoff = 0; + else this.errorBackoff++; + this.connectionSucceed = 0; + + (document.getElementById("load-desc") as HTMLElement).innerHTML = + "Unable to connect to the Spacebar server, retrying in " + + Math.round(0.2 + this.errorBackoff * 2.8) + + " seconds..."; + 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; + this.userinfo.updateLocal(); + 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; + this.userinfo.updateLocal(); + 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; + this.userinfo.updateLocal(); + } + break; + } + } + setTimeout(() => { + if (this.swapped) return; + (document.getElementById("load-desc") as HTMLElement).textContent = + "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 = + "Unable to connect to the Spacebar server. Please try logging out and back in."; + }); + + await promise; + } + async handleEvent(temp: wsjson) { + console.debug(temp); + if (temp.s) this.lastSequence = temp.s; + 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": + 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_CREATE": { + const guildy = new Guild(temp.d, this, this.user); + this.guilds.push(guildy); + this.guildids.set(guildy.id, guildy); + (document.getElementById("servers") as HTMLDivElement).insertBefore( + guildy.generateGuildIcon(), + 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; + } + } 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); + } + } + 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); + } + if (channel.id === this.gotoid) { + guild.loadGuild(); + guild.loadChannel(channel.id); + this.gotoid = undefined; + } + return channel; // Add this line to return the 'channel' variable + } + gotoid: string | undefined; + async goToChannel(id: string) { + const channel = this.channelids.get(id); + if (channel) { + const guild = channel.guild; + guild.loadGuild(); + guild.loadChannel(id); + } 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); + } + } + 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): Guild | undefined { + let guild = this.guildids.get(id); + if (!guild) { + guild = this.guildids.get("@me"); + } + if (this.lookingguild === guild) { + return guild; + } + if (this.channelfocus) { + 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; + //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("svgtheme", "svgicon", "svg-home"); + home["all"] = this.guildids.get("@me"); + (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 = function () { + this["all"].loadGuild(); + this["all"].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(); + serverlist.append(divy); + } + { + const br = document.createElement("hr"); + br.classList.add("lightbr"); + serverlist.appendChild(br); + br.id = "bottomseparator"; + + const div = document.createElement("div"); + div.textContent = "+"; + div.classList.add("home", "servericon"); + serverlist.appendChild(div); + div.onclick = (_) => { + this.createGuild(); + }; + const guilddsdiv = document.createElement("div"); + const guildDiscoveryContainer = document.createElement("span"); + guildDiscoveryContainer.classList.add( + "svgtheme", + "svgicon", + "svg-explore" + ); + guilddsdiv.classList.add("home", "servericon"); + guilddsdiv.appendChild(guildDiscoveryContainer); + serverlist.appendChild(guilddsdiv); + guildDiscoveryContainer.addEventListener("click", () => { + this.guildDiscovery(); + }); + } + this.unreads(); + } + createGuild() { + let inviteurl = ""; + const error = document.createElement("span"); + const fields: { name: string; icon: string | null } = { + name: "", + icon: null, + }; + const full = new Dialog([ + "tabs", + [ + [ + "Join using invite", + [ + "vdiv", + [ + "textbox", + "Invite Link/Code", + "", + function (this: HTMLInputElement) { + inviteurl = this.value; + }, + ], + ["html", error], + [ + "button", + "", + "Submit", + (_: any) => { + let parsed = ""; + if (inviteurl.includes("/")) { + parsed = + inviteurl.split("/")[inviteurl.split("/").length - 1]; + } else { + parsed = inviteurl; + } + fetch(this.info.api + "/invites/" + parsed, { + method: "POST", + headers: this.headers, + }) + .then((r) => r.json()) + .then((_) => { + if (_.message) { + error.textContent = _.message; + } + }); + }, + ], + ], + ], + [ + "Create Guild", + [ + "vdiv", + ["title", "Create a guild"], + [ + "fileupload", + "Icon:", + function (event: Event) { + const target = event.target as HTMLInputElement; + if (!target.files) return; + const reader = new FileReader(); + reader.readAsDataURL(target.files[0]); + reader.onload = () => { + fields.icon = reader.result as string; + }; + }, + ], + [ + "textbox", + "Name:", + "", + function (this: HTMLInputElement, event: Event) { + const target = event.target as HTMLInputElement; + fields.name = target.value; + }, + ], + [ + "button", + "", + "submit", + () => { + this.makeGuild(fields).then((_) => { + if (_.message) { + alert(_.errors.name._errors[0].message); + } else { + full.hide(); + } + }); + }, + ], + ], + ], + ], + ]); + 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("guildy"); + content.textContent = "Loading..."; + const full = new Dialog(["html", content]); + full.show(); + + const res = await fetch(this.info.api + "/discoverable-guilds?limit=50", { + headers: this.headers, + }); + const json = await res.json(); + + content.innerHTML = ""; + const title = document.createElement("h2"); + title.textContent = "Guild discovery (" + json.total + " entries)"; + 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("Settings"); + { + const userOptions = settings.addButton("User Settings", { 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( + "Upload pfp:", + (_) => { + 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( + "Upload banner:", + (_) => { + 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( + "Pronouns", + (_) => { + if (newpronouns || newbio || changed) { + 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("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( + "Profile color", + (_) => {}, + { initColor: color } + ); + colorPicker.watchForChange((_) => { + console.log(); + color = _; + hypouser.accent_color = Number.parseInt("0x" + _.substr(1), 16); + changed = true; + regen(); + }); + } + { + const tas = settings.addButton("Themes & sounds"); + { + const themes = ["Dark", "WHITE", "Light"]; + tas.addSelect( + "Theme:", + (_) => { + localStorage.setItem("theme", themes[_]); + setTheme(); + }, + themes, + { + defaultIndex: themes.indexOf( + localStorage.getItem("theme") as string + ), + } + ); + } + { + const sounds = Voice.sounds; + tas + .addSelect( + "Notification sound:", + (_) => { + Voice.setNotificationSound(sounds[_]); + }, + sounds, + { defaultIndex: sounds.indexOf(Voice.getNotificationSound()) } + ) + .watchForChange((_) => { + Voice.noises(sounds[_]); + }); + } + + { + const userinfos = getBulkInfo(); + tas.addColorInput( + "Accent color:", + (_) => { + userinfos.accent_color = _; + localStorage.setItem("userinfos", JSON.stringify(userinfos)); + document.documentElement.style.setProperty( + "--accent-color", + userinfos.accent_color + ); + }, + { initColor: userinfos.accent_color } + ); + } + } + { + const security = settings.addButton("Account Settings"); + const genSecurity = () => { + security.removeAll(); + if (this.mfa_enabled) { + security.addButtonInput("", "Disable 2FA", () => { + const form = security.addSubForm( + "2FA Disable", + (_: any) => { + if (_.message) { + switch (_.code) { + case 60008: + form.error("code", "Invalid code"); + break; + } + } else { + this.mfa_enabled = false; + security.returnFromSub(); + genSecurity(); + } + }, + { + fetchURL: this.info.api + "/users/@me/mfa/totp/disable", + headers: this.headers, + } + ); + form.addTextInput("Code:", "code", { required: true }); + }); + } else { + security.addButtonInput("", "Enable 2FA", async () => { + let secret = ""; + for (let i = 0; i < 18; i++) { + secret += "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"[ + Math.floor(Math.random() * 32) + ]; + } + const form = security.addSubForm( + "2FA Setup", + (_: any) => { + if (_.message) { + switch (_.code) { + case 60008: + form.error("code", "Invalid code"); + break; + case 400: + form.error("password", "Incorrect password"); + break; + } + } else { + genSecurity(); + this.mfa_enabled = true; + security.returnFromSub(); + } + }, + { + fetchURL: this.info.api + "/users/@me/mfa/totp/enable/", + headers: this.headers, + } + ); + form.addTitle( + "Copy this secret into your totp(time-based one time password) app" + ); + form.addText( + `Your secret is: ${secret} and it's 6 digits, with a 30 second token period` + ); + form.addTextInput("Account Password:", "password", { + required: true, + password: true, + }); + form.addTextInput("Code:", "code", { required: true }); + form.setValue("secret", secret); + }); + } + security.addButtonInput("", "Change discriminator", () => { + const form = security.addSubForm( + "Change Discriminator", + (_) => { + security.returnFromSub(); + }, + { + fetchURL: this.info.api + "/users/@me/", + headers: this.headers, + method: "PATCH", + } + ); + form.addTextInput("New discriminator:", "discriminator"); + }); + security.addButtonInput("", "Change email", () => { + const form = security.addSubForm( + "Change Email", + (_) => { + security.returnFromSub(); + }, + { + fetchURL: this.info.api + "/users/@me/", + headers: this.headers, + method: "PATCH", + } + ); + form.addTextInput("Password:", "password", { password: true }); + if (this.mfa_enabled) { + form.addTextInput("Code:", "code"); + } + form.addTextInput("New email:", "email"); + }); + security.addButtonInput("", "Change username", () => { + const form = security.addSubForm( + "Change Username", + (_) => { + security.returnFromSub(); + }, + { + fetchURL: this.info.api + "/users/@me/", + headers: this.headers, + method: "PATCH", + } + ); + form.addTextInput("Password:", "password", { password: true }); + if (this.mfa_enabled) { + form.addTextInput("Code:", "code"); + } + form.addTextInput("New username:", "username"); + }); + security.addButtonInput("", "Change password", () => { + const form = security.addSubForm( + "Change Password", + (_) => { + security.returnFromSub(); + }, + { + fetchURL: this.info.api + "/users/@me/", + headers: this.headers, + method: "PATCH", + } + ); + form.addTextInput("Old password:", "password", { password: true }); + if (this.mfa_enabled) { + form.addTextInput("Code:", "code"); + } + let in1 = ""; + let in2 = ""; + form.addTextInput("New password:", "").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, "Passwords don't match"); + } + }); + }); + }; + genSecurity(); + } + { + const connections = settings.addButton("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 = + "This connection has been disabled server-side."; + } + + connectionContainer.appendChild(container); + }); + }); + connections.addHTMLArea(connectionContainer); + } + { + const devPortal = settings.addButton("Developer Portal"); + + const teamsRes = await fetch(this.info.api + "/teams", { + headers: this.headers, + }); + const teams = await teamsRes.json(); + + devPortal.addButtonInput("", "Create application", () => { + const form = devPortal.addSubForm( + "Create application", + (json: any) => { + if (json.message) form.error("name", json.message); + else { + devPortal.returnFromSub(); + this.manageApplication(json.id); + } + }, + { + fetchURL: this.info.api + "/applications", + headers: this.headers, + method: "POST", + } + ); + + form.addTextInput("Name", "name", { required: true }); + form.addSelect( + "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); + }); + appListContainer.appendChild(container); + } + ); + }); + devPortal.addHTMLArea(appListContainer); + } + settings.show(); + } + async manageApplication(appId = "") { + const res = await fetch(this.info.api + "/applications/" + appId, { + headers: this.headers, + }); + const json = await res.json(); + + const fields: any = {}; + const appDialog = new Dialog([ + "vdiv", + ["title", "Editing " + json.name], + [ + "vdiv", + [ + "textbox", + "Application name:", + json.name, + (event: Event) => { + const target = event.target as HTMLInputElement; + fields.name = target.value; + }, + ], + [ + "mdbox", + "Description:", + json.description, + (event: Event) => { + const target = event.target as HTMLInputElement; + fields.description = target.value; + }, + ], + [ + "vdiv", + json.icon + ? [ + "img", + this.info.cdn + + "/app-icons/" + + appId + + "/" + + json.icon + + ".png?size=128", + [128, 128], + ] + : ["text", "No icon"], + [ + "fileupload", + "Application icon:", + (event) => { + const reader = new FileReader(); + const files = (event.target as HTMLInputElement).files; + if (files) { + reader.readAsDataURL(files[0]); + reader.onload = () => { + fields.icon = reader.result; + }; + } + }, + ], + ], + ], + [ + "hdiv", + [ + "textbox", + "Privacy policy URL:", + json.privacy_policy_url || "", + (event: Event) => { + const target = event.target as HTMLInputElement; + fields.privacy_policy_url = target.value; + }, + ], + [ + "textbox", + "Terms of Service URL:", + json.terms_of_service_url || "", + (event: Event) => { + const target = event.target as HTMLInputElement; + fields.terms_of_service_url = target.value; + }, + ], + ], + [ + "hdiv", + [ + "checkbox", + "Make bot publicly inviteable?", + json.bot_public, + (event: Event) => { + const target = event.target as HTMLInputElement; + fields.bot_public = target.checked; + }, + ], + [ + "checkbox", + "Require code grant to invite the bot?", + json.bot_require_code_grant, + (event: Event) => { + const target = event.target as HTMLInputElement; + fields.bot_require_code_grant = target.checked; + }, + ], + ], + [ + "hdiv", + [ + "button", + "", + "Save changes", + async () => { + const updateRes = await fetch( + this.info.api + "/applications/" + appId, + { + method: "PATCH", + headers: this.headers, + body: JSON.stringify(fields), + } + ); + if (updateRes.ok) appDialog.hide(); + else { + const updateJSON = await updateRes.json(); + alert("An error occurred: " + updateJSON.message); + } + }, + ], + [ + "button", + "", + (json.bot ? "Manage" : "Add") + " bot", + async () => { + if (!json.bot) { + if ( + !confirm( + "Are you sure you want to add a bot to this application? There's no going back." + ) + ) + return; + + const updateRes = await fetch( + this.info.api + "/applications/" + appId + "/bot", + { + method: "POST", + headers: this.headers, + } + ); + const updateJSON = await updateRes.json(); + alert("Bot token:\n" + updateJSON.token); + } + + appDialog.hide(); + this.manageBot(appId); + }, + ], + ], + ]); + appDialog.show(); + } + async manageBot(appId = "") { + const res = await fetch(this.info.api + "/applications/" + appId, { + headers: this.headers, + }); + const json = await res.json(); + if (!json.bot) + return alert( + "For some reason, this application doesn't have a bot (yet)." + ); + + const fields: any = { + username: json.bot.username, + avatar: json.bot.avatar + ? this.info.cdn + + "/app-icons/" + + appId + + "/" + + json.bot.avatar + + ".png?size=256" + : "", + }; + const botDialog = new Dialog([ + "vdiv", + ["title", "Editing bot: " + json.bot.username], + [ + "hdiv", + [ + "textbox", + "Bot username:", + json.bot.username, + (event: Event) => { + const target = event.target as HTMLInputElement; + fields.username = target.value; + }, + ], + [ + "vdiv", + fields.avatar + ? ["img", fields.avatar, [128, 128]] + : ["text", "No avatar"], + [ + "fileupload", + "Bot avatar:", + (event) => { + const reader = new FileReader(); + const files = (event.target as HTMLInputElement).files; + if (files) { + const file = files[0]; + reader.readAsDataURL(file); + reader.onload = () => { + fields.avatar = reader.result; + }; + } + }, + ], + ], + ], + [ + "hdiv", + [ + "button", + "", + "Save changes", + async () => { + const updateRes = await fetch( + this.info.api + "/applications/" + appId + "/bot", + { + method: "PATCH", + headers: this.headers, + body: JSON.stringify(fields), + } + ); + if (updateRes.ok) botDialog.hide(); + else { + const updateJSON = await updateRes.json(); + alert("An error occurred: " + updateJSON.message); + } + }, + ], + [ + "button", + "", + "Reset token", + async () => { + if ( + !confirm( + "Are you sure you want to reset the bot token? Your bot will stop working until you update it." + ) + ) + return; + + const updateRes = await fetch( + this.info.api + "/applications/" + appId + "/bot/reset", + { + method: "POST", + headers: this.headers, + } + ); + const updateJSON = await updateRes.json(); + alert("New token:\n" + updateJSON.token); + botDialog.hide(); + }, + ], + ], + ]); + botDialog.show(); + } + + //---------- resolving members code ----------- + readonly waitingmembers: Map< + string, + Map void> + > = new Map(); + 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 (borked && guild && 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/" + guild.id + "/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(); + 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); + } + } + 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([ + "vdiv", + ["title", "Instance stats: " + this.instancePing.name], + ["text", "Registered users: " + json.counts.user], + ["text", "Servers: " + json.counts.guild], + ["text", "Messages: " + json.counts.message], + ["text", "Members: " + json.counts.members], + ]); + dialog.show(); + } +} +export { Localuser }; diff --git a/src/webpage/login.html b/src/webpage/login.html new file mode 100644 index 0000000..16b2c3a --- /dev/null +++ b/src/webpage/login.html @@ -0,0 +1,62 @@ + + + + + Jank Client + + + + + + + +
+

Login

+
+
+
+

+

+ +
+

+ +
+



+

+ +
+ +
+ Don't have an account? +
+ + + diff --git a/src/webpage/login.ts b/src/webpage/login.ts new file mode 100644 index 0000000..5e53c99 --- /dev/null +++ b/src/webpage/login.ts @@ -0,0 +1,625 @@ +import { Dialog } from "./dialog.js"; + +const mobile = /iPhone|iPad|iPod|Android/i.test(navigator.userAgent); + +function setTheme() { + let name = localStorage.getItem("theme"); + if (!name) { + localStorage.setItem("theme", "Dark"); + name = "Dark"; + } + document.body.className = name + "-theme"; +} +let instances: + | { + name: string; + description?: string; + descriptionLong?: string; + image?: string; + url?: string; + display?: boolean; + online?: boolean; + uptime: { alltime: number; daytime: number; weektime: number }; + urls: { + wellknown: string; + api: string; + cdn: string; + gateway: string; + login?: string; + }; + }[] + | null; + +setTheme(); +function getBulkUsers() { + const json = getBulkInfo(); + for (const thing in json.users) { + json.users[thing] = new Specialuser(json.users[thing]); + } + return json; +} +function trimswitcher() { + const json = getBulkInfo(); + const map = new Map(); + for (const thing in json.users) { + const user = json.users[thing]; + let wellknown = user.serverurls.wellknown; + if (wellknown.at(-1) !== "/") { + wellknown += "/"; + } + wellknown += user.username; + if (map.has(wellknown)) { + const otheruser = map.get(wellknown); + if (otheruser[1].serverurls.wellknown.at(-1) === "/") { + delete json.users[otheruser[0]]; + map.set(wellknown, [thing, user]); + } else { + delete json.users[thing]; + } + } else { + map.set(wellknown, [thing, user]); + } + } + for (const thing in json.users) { + if (thing.at(-1) === "/") { + const user = json.users[thing]; + delete json.users[thing]; + json.users[thing.slice(0, -1)] = user; + } + } + localStorage.setItem("userinfos", JSON.stringify(json)); + console.log(json); +} + +function getBulkInfo() { + return JSON.parse(localStorage.getItem("userinfos")!); +} +function setDefaults() { + let userinfos = getBulkInfo(); + if (!userinfos) { + localStorage.setItem( + "userinfos", + JSON.stringify({ + currentuser: null, + users: {}, + preferences: { + theme: "Dark", + notifications: false, + notisound: "three", + }, + }) + ); + userinfos = getBulkInfo(); + } + if (userinfos.users === undefined) { + userinfos.users = {}; + } + if (userinfos.accent_color === undefined) { + userinfos.accent_color = "#242443"; + } + document.documentElement.style.setProperty( + "--accent-color", + userinfos.accent_color + ); + if (userinfos.preferences === undefined) { + userinfos.preferences = { + theme: "Dark", + notifications: false, + notisound: "three", + }; + } + if (userinfos.preferences && userinfos.preferences.notisound === undefined) { + userinfos.preferences.notisound = "three"; + } + localStorage.setItem("userinfos", JSON.stringify(userinfos)); +} +setDefaults(); +class Specialuser { + serverurls: { + api: string; + cdn: string; + gateway: string; + wellknown: string; + login: string; + }; + email: string; + token: string; + loggedin; + json; + constructor(json: any) { + if (json instanceof Specialuser) { + console.error("specialuser can't construct from another specialuser"); + } + this.serverurls = json.serverurls; + let apistring = new URL(json.serverurls.api).toString(); + apistring = apistring.replace(/\/(v\d+\/?)?$/, "") + "/v9"; + this.serverurls.api = apistring; + this.serverurls.cdn = new URL(json.serverurls.cdn) + .toString() + .replace(/\/$/, ""); + this.serverurls.gateway = new URL(json.serverurls.gateway) + .toString() + .replace(/\/$/, ""); + this.serverurls.wellknown = new URL(json.serverurls.wellknown) + .toString() + .replace(/\/$/, ""); + this.serverurls.login = new URL(json.serverurls.login) + .toString() + .replace(/\/$/, ""); + this.email = json.email; + this.token = json.token; + this.loggedin = json.loggedin; + this.json = json; + this.json.localuserStore ??= {}; + if (!this.serverurls || !this.email || !this.token) { + console.error( + "There are fundamentally missing pieces of info missing from this user" + ); + } + } + set pfpsrc(e) { + this.json.pfpsrc = e; + this.updateLocal(); + } + get pfpsrc() { + return this.json.pfpsrc; + } + set username(e) { + this.json.username = e; + this.updateLocal(); + } + get username() { + return this.json.username; + } + set localuserStore(e) { + this.json.localuserStore = e; + this.updateLocal(); + } + get localuserStore() { + return this.json.localuserStore; + } + get uid() { + return this.email + this.serverurls.wellknown; + } + toJSON() { + return this.json; + } + updateLocal() { + const info = getBulkInfo(); + info.users[this.uid] = this.toJSON(); + localStorage.setItem("userinfos", JSON.stringify(info)); + } +} +function adduser(user: typeof Specialuser.prototype.json) { + user = new Specialuser(user); + const info = getBulkInfo(); + info.users[user.uid] = user; + info.currentuser = user.uid; + localStorage.setItem("userinfos", JSON.stringify(info)); + return user; +} +const instancein = document.getElementById("instancein") as HTMLInputElement; +let timeout: string | number | NodeJS.Timeout | undefined; +// let instanceinfo; +const stringURLMap = new Map(); + +const stringURLsMap = new Map< + string, + { + wellknown: string; + api: string; + cdn: string; + gateway: string; + login?: string; + } +>(); +async function getapiurls(str: string): Promise< + | { + api: string; + cdn: string; + gateway: string; + wellknown: string; + login: string; + } + | false +> { + if (!URL.canParse(str)) { + const val = stringURLMap.get(str); + if (val) { + str = val; + } else { + const val = stringURLsMap.get(str); + if (val) { + const responce = await fetch( + val.api + val.api.endsWith("/") ? "" : "/" + "ping" + ); + if (responce.ok) { + if (val.login) { + return val as { + wellknown: string; + api: string; + cdn: string; + gateway: string; + login: string; + }; + } else { + val.login = val.api; + return val as { + wellknown: string; + api: string; + cdn: string; + gateway: string; + login: string; + }; + } + } + } + } + } + if (str.at(-1) !== "/") { + str += "/"; + } + let api: string; + try { + const info = await fetch(`${str}/.well-known/spacebar`).then((x) => + x.json() + ); + api = info.api; + } catch { + return false; + } + const url = new URL(api); + try { + const info = await fetch( + `${api}${ + url.pathname.includes("api") ? "" : "api" + }/policies/instance/domains` + ).then((x) => x.json()); + return { + api: info.apiEndpoint, + gateway: info.gateway, + cdn: info.cdn, + wellknown: str, + login: url.toString(), + }; + } catch { + const val = stringURLsMap.get(str); + if (val) { + const responce = await fetch( + val.api + val.api.endsWith("/") ? "" : "/" + "ping" + ); + if (responce.ok) { + if (val.login) { + return val as { + wellknown: string; + api: string; + cdn: string; + gateway: string; + login: string; + }; + } else { + val.login = val.api; + return val as { + wellknown: string; + api: string; + cdn: string; + gateway: string; + login: string; + }; + } + } + } + return false; + } +} +async function checkInstance(instance?: string) { + const verify = document.getElementById("verify"); + try { + verify!.textContent = "Checking Instance"; + const instanceValue = instance || (instancein as HTMLInputElement).value; + const instanceinfo = (await getapiurls(instanceValue)) as { + wellknown: string; + api: string; + cdn: string; + gateway: string; + login: string; + value: string; + }; + if (instanceinfo) { + instanceinfo.value = instanceValue; + localStorage.setItem("instanceinfo", JSON.stringify(instanceinfo)); + verify!.textContent = "Instance is all good"; + // @ts-ignore + if (checkInstance.alt) { + // @ts-ignore + checkInstance.alt(); + } + setTimeout((_: any) => { + console.log(verify!.textContent); + verify!.textContent = ""; + }, 3000); + } else { + verify!.textContent = "Invalid Instance, try again"; + } + } catch { + console.log("catch"); + verify!.textContent = "Invalid Instance, try again"; + } +} + +if (instancein) { + console.log(instancein); + instancein.addEventListener("keydown", (_) => { + const verify = document.getElementById("verify"); + verify!.textContent = "Waiting to check Instance"; + clearTimeout(timeout); + timeout = setTimeout(() => checkInstance(), 1000); + }); + if (localStorage.getItem("instanceinfo")) { + const json = JSON.parse(localStorage.getItem("instanceinfo")!); + if (json.value) { + (instancein as HTMLInputElement).value = json.value; + } else { + (instancein as HTMLInputElement).value = json.wellknown; + } + } else { + checkInstance("https://spacebar.chat/"); + } +} + +async function login(username: string, password: string, captcha: string) { + if (captcha === "") { + captcha = ""; + } + const options = { + method: "POST", + body: JSON.stringify({ + login: username, + password, + undelete: false, + captcha_key: captcha, + }), + headers: { + "Content-type": "application/json; charset=UTF-8", + }, + }; + try { + const info = JSON.parse(localStorage.getItem("instanceinfo")!); + const api = info.login + (info.login.startsWith("/") ? "/" : ""); + return await fetch(api + "/auth/login", options) + .then((response) => response.json()) + .then((response) => { + console.log(response, response.message); + if (response.message === "Invalid Form Body") { + return response.errors.login._errors[0].message; + console.log("test"); + } + //this.serverurls||!this.email||!this.token + console.log(response); + + if (response.captcha_sitekey) { + const capt = document.getElementById("h-captcha"); + if (!capt!.children.length) { + const capty = document.createElement("div"); + capty.classList.add("h-captcha"); + + capty.setAttribute("data-sitekey", response.captcha_sitekey); + const script = document.createElement("script"); + script.src = "https://js.hcaptcha.com/1/api.js"; + capt!.append(script); + capt!.append(capty); + } else { + eval("hcaptcha.reset()"); + } + } else { + console.log(response); + if (response.ticket) { + let onetimecode = ""; + new Dialog([ + "vdiv", + ["title", "2FA code:"], + [ + "textbox", + "", + "", + function (this: HTMLInputElement) { + onetimecode = this.value; + }, + ], + [ + "button", + "", + "Submit", + function () { + fetch(api + "/auth/mfa/totp", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + code: onetimecode, + ticket: response.ticket, + }), + }) + .then((r) => r.json()) + .then((response) => { + if (response.message) { + alert(response.message); + } else { + console.warn(response); + if (!response.token) return; + adduser({ + serverurls: JSON.parse( + localStorage.getItem("instanceinfo")! + ), + email: username, + token: response.token, + }).username = username; + const redir = new URLSearchParams( + window.location.search + ).get("goback"); + if (redir) { + window.location.href = redir; + } else { + window.location.href = "/channels/@me"; + } + } + }); + }, + ], + ]).show(); + } else { + console.warn(response); + if (!response.token) return; + adduser({ + serverurls: JSON.parse(localStorage.getItem("instanceinfo")!), + email: username, + token: response.token, + }).username = username; + const redir = new URLSearchParams(window.location.search).get( + "goback" + ); + if (redir) { + window.location.href = redir; + } else { + window.location.href = "/channels/@me"; + } + return ""; + } + } + }); + } catch (error) { + console.error("Error:", error); + } +} + +async function check(e: SubmitEvent) { + e.preventDefault(); + const target = e.target as HTMLFormElement; + const h = await login( + (target[1] as HTMLInputElement).value, + (target[2] as HTMLInputElement).value, + (target[3] as HTMLInputElement).value + ); + const wrongElement = document.getElementById("wrong"); + if (wrongElement) { + wrongElement.textContent = h; + } + console.log(h); +} +if (document.getElementById("form")) { + const form = document.getElementById("form"); + if (form) { + form.addEventListener("submit", (e: SubmitEvent) => check(e)); + } +} +//this currently does not work, and need to be implemented better at some time. +/* +if ("serviceWorker" in navigator){ + navigator.serviceWorker.register("/service.js", { + scope: "/", + }).then((registration) => { + let serviceWorker:ServiceWorker; + if (registration.installing) { + serviceWorker = registration.installing; + console.log("installing"); + } else if (registration.waiting) { + serviceWorker = registration.waiting; + console.log("waiting"); + } else if (registration.active) { + serviceWorker = registration.active; + console.log("active"); + } + if (serviceWorker) { + console.log(serviceWorker.state); + serviceWorker.addEventListener("statechange", (e) => { + console.log(serviceWorker.state); + }); + } + }) +} +*/ +const switchurl = document.getElementById("switch") as HTMLAreaElement; +if (switchurl) { + switchurl.href += window.location.search; + const instance = new URLSearchParams(window.location.search).get("instance"); + console.log(instance); + if (instance) { + instancein.value = instance; + checkInstance(""); + } +} +export { checkInstance }; +trimswitcher(); +export { + mobile, + getBulkUsers, + getBulkInfo, + setTheme, + Specialuser, + getapiurls, + adduser, +}; + +const datalist = document.getElementById("instances"); +console.warn(datalist); +export function getInstances() { + return instances; +} + +fetch("/instances.json") + .then((_) => _.json()) + .then( + ( + json: { + name: string; + description?: string; + descriptionLong?: string; + image?: string; + url?: string; + display?: boolean; + online?: boolean; + uptime: { alltime: number; daytime: number; weektime: number }; + urls: { + wellknown: string; + api: string; + cdn: string; + gateway: string; + login?: string; + }; + }[] + ) => { + instances = json; + if (datalist) { + console.warn(json); + if (instancein && instancein.value === "") { + instancein.value = json[0].name; + } + for (const instance of json) { + if (instance.display === false) { + continue; + } + const option = document.createElement("option"); + option.disabled = !instance.online; + option.value = instance.name; + if (instance.url) { + stringURLMap.set(option.value, instance.url); + if (instance.urls) { + stringURLsMap.set(instance.url, instance.urls); + } + } else if (instance.urls) { + stringURLsMap.set(option.value, instance.urls); + } else { + option.disabled = true; + } + if (instance.description) { + option.label = instance.description; + } else { + option.label = instance.name; + } + datalist.append(option); + } + checkInstance(""); + } + } + ); diff --git a/webpage/logo.svg b/src/webpage/logo.svg similarity index 100% rename from webpage/logo.svg rename to src/webpage/logo.svg diff --git a/webpage/logo.webp b/src/webpage/logo.webp similarity index 100% rename from webpage/logo.webp rename to src/webpage/logo.webp diff --git a/src/webpage/manifest.json b/src/webpage/manifest.json new file mode 100644 index 0000000..dca0dc2 --- /dev/null +++ b/src/webpage/manifest.json @@ -0,0 +1,12 @@ +{ + "name": "Jank Client", + "icons": [ + { + "src": "/logo.svg", + "sizes": "512x512" + } + ], + "start_url": "/channels/@me", + "display": "standalone", + "theme_color": "#05050a" +} \ No newline at end of file diff --git a/src/webpage/markdown.ts b/src/webpage/markdown.ts new file mode 100644 index 0000000..444ea24 --- /dev/null +++ b/src/webpage/markdown.ts @@ -0,0 +1,870 @@ +import { Channel } from "./channel.js"; +import { Dialog } from "./dialog.js"; +import { Emoji } from "./emoji.js"; +import { Guild } from "./guild.js"; +import { Localuser } from "./localuser.js"; +import { Member } from "./member.js"; + +class MarkDown { + txt: string[]; + keep: boolean; + stdsize: boolean; + owner: Localuser | Channel; + info: Localuser["info"]; + constructor( + text: string | string[], + owner: MarkDown["owner"], + { keep = false, stdsize = false } = {} + ) { + if (typeof text === typeof "") { + this.txt = (text as string).split(""); + } else { + this.txt = text as string[]; + } + if (this.txt === undefined) { + this.txt = []; + } + this.info = owner.info; + this.keep = keep; + this.owner = owner; + this.stdsize = stdsize; + } + get localuser() { + if (this.owner instanceof Localuser) { + return this.owner; + } else { + return this.owner.localuser; + } + } + get rawString() { + return this.txt.join(""); + } + get textContent() { + return this.makeHTML().textContent; + } + makeHTML({ keep = this.keep, stdsize = this.stdsize } = {}) { + return this.markdown(this.txt, { keep, stdsize }); + } + markdown(text: string | string[], { keep = false, stdsize = false } = {}) { + let txt: string[]; + if (typeof text === typeof "") { + txt = (text as string).split(""); + } else { + txt = text as string[]; + } + if (txt === undefined) { + txt = []; + } + const span = document.createElement("span"); + let current = document.createElement("span"); + function appendcurrent() { + if (current.innerHTML !== "") { + span.append(current); + current = document.createElement("span"); + } + } + for (let i = 0; i < txt.length; i++) { + if (txt[i] === "\n" || i === 0) { + const first = i === 0; + if (first) { + i--; + } + let element: HTMLElement = document.createElement("span"); + let keepys = ""; + + if (txt[i + 1] === "#") { + if (txt[i + 2] === "#") { + if (txt[i + 3] === "#" && txt[i + 4] === " ") { + element = document.createElement("h3"); + keepys = "### "; + i += 5; + } else if (txt[i + 3] === " ") { + element = document.createElement("h2"); + element.classList.add("h2md"); + keepys = "## "; + i += 4; + } + } else if (txt[i + 2] === " ") { + element = document.createElement("h1"); + keepys = "# "; + i += 3; + } + } else if (txt[i + 1] === ">" && txt[i + 2] === " ") { + element = document.createElement("div"); + const line = document.createElement("div"); + line.classList.add("quoteline"); + element.append(line); + element.classList.add("quote"); + keepys = "> "; + i += 3; + } + if (keepys) { + appendcurrent(); + if (!first && !stdsize) { + span.appendChild(document.createElement("br")); + } + const build: string[] = []; + for (; txt[i] !== "\n" && txt[i] !== undefined; i++) { + build.push(txt[i]); + } + try { + if (stdsize) { + element = document.createElement("span"); + } + if (keep) { + element.append(keepys); + //span.appendChild(document.createElement("br")); + } + element.appendChild(this.markdown(build, { keep, stdsize })); + span.append(element); + } finally { + i -= 1; + continue; + } + } + if (first) { + i++; + } + } + if (txt[i] === "\n") { + if (!stdsize) { + appendcurrent(); + span.append(document.createElement("br")); + } + continue; + } + if (txt[i] === "`") { + let count = 1; + if (txt[i + 1] === "`") { + count++; + if (txt[i + 2] === "`") { + count++; + } + } + let build = ""; + if (keep) { + build += "`".repeat(count); + } + let find = 0; + let j = i + count; + let init = true; + for ( + ; + txt[j] !== undefined && + (txt[j] !== "\n" || count === 3) && + find !== count; + j++ + ) { + if (txt[j] === "`") { + find++; + } else { + if (find !== 0) { + build += "`".repeat(find); + find = 0; + } + if (init && count === 3) { + if (txt[j] === " " || txt[j] === "\n") { + init = false; + } + if (keep) { + build += txt[j]; + } + continue; + } + build += txt[j]; + } + } + if (stdsize) { + build = build.replaceAll("\n", ""); + } + if (find === count) { + appendcurrent(); + i = j; + if (keep) { + build += "`".repeat(find); + } + if (count !== 3 && !stdsize) { + const samp = document.createElement("samp"); + samp.textContent = build; + span.appendChild(samp); + } else { + const pre = document.createElement("pre"); + if (build.at(-1) === "\n") { + build = build.substring(0, build.length - 1); + } + if (txt[i] === "\n") { + i++; + } + pre.textContent = build; + span.appendChild(pre); + } + i--; + continue; + } + } + + if (txt[i] === "*") { + let count = 1; + if (txt[i + 1] === "*") { + count++; + if (txt[i + 2] === "*") { + count++; + } + } + let build: string[] = []; + let find = 0; + let j = i + count; + for (; txt[j] !== undefined && find !== count; j++) { + if (txt[j] === "*") { + find++; + } else { + build.push(txt[j]); + if (find !== 0) { + build = build.concat(new Array(find).fill("*")); + find = 0; + } + } + } + if (find === count && (count != 1 || txt[i + 1] !== " ")) { + appendcurrent(); + i = j; + + const stars = "*".repeat(count); + if (count === 1) { + const i = document.createElement("i"); + if (keep) { + i.append(stars); + } + i.appendChild(this.markdown(build, { keep, stdsize })); + if (keep) { + i.append(stars); + } + span.appendChild(i); + } else if (count === 2) { + const b = document.createElement("b"); + if (keep) { + b.append(stars); + } + b.appendChild(this.markdown(build, { keep, stdsize })); + if (keep) { + b.append(stars); + } + span.appendChild(b); + } else { + const b = document.createElement("b"); + const i = document.createElement("i"); + if (keep) { + b.append(stars); + } + b.appendChild(this.markdown(build, { keep, stdsize })); + if (keep) { + b.append(stars); + } + i.appendChild(b); + span.appendChild(i); + } + i--; + continue; + } + } + + if (txt[i] === "_") { + let count = 1; + if (txt[i + 1] === "_") { + count++; + if (txt[i + 2] === "_") { + count++; + } + } + let build: string[] = []; + let find = 0; + let j = i + count; + for (; txt[j] !== undefined && find !== count; j++) { + if (txt[j] === "_") { + find++; + } else { + build.push(txt[j]); + if (find !== 0) { + build = build.concat(new Array(find).fill("_")); + find = 0; + } + } + } + if ( + find === count && + (count != 1 || + txt[j + 1] === " " || + txt[j + 1] === "\n" || + txt[j + 1] === undefined) + ) { + appendcurrent(); + i = j; + const underscores = "_".repeat(count); + if (count === 1) { + const i = document.createElement("i"); + if (keep) { + i.append(underscores); + } + i.appendChild(this.markdown(build, { keep, stdsize })); + if (keep) { + i.append(underscores); + } + span.appendChild(i); + } else if (count === 2) { + const u = document.createElement("u"); + if (keep) { + u.append(underscores); + } + u.appendChild(this.markdown(build, { keep, stdsize })); + if (keep) { + u.append(underscores); + } + span.appendChild(u); + } else { + const u = document.createElement("u"); + const i = document.createElement("i"); + if (keep) { + i.append(underscores); + } + i.appendChild(this.markdown(build, { keep, stdsize })); + if (keep) { + i.append(underscores); + } + u.appendChild(i); + span.appendChild(u); + } + i--; + continue; + } + } + + if (txt[i] === "~" && txt[i + 1] === "~") { + const count = 2; + let build: string[] = []; + let find = 0; + let j = i + 2; + for (; txt[j] !== undefined && find !== count; j++) { + if (txt[j] === "~") { + find++; + } else { + build.push(txt[j]); + if (find !== 0) { + build = build.concat(new Array(find).fill("~")); + find = 0; + } + } + } + if (find === count) { + appendcurrent(); + i = j - 1; + const tildes = "~~"; + if (count === 2) { + const s = document.createElement("s"); + if (keep) { + s.append(tildes); + } + s.appendChild(this.markdown(build, { keep, stdsize })); + if (keep) { + s.append(tildes); + } + span.appendChild(s); + } + continue; + } + } + if (txt[i] === "|" && txt[i + 1] === "|") { + const count = 2; + let build: string[] = []; + let find = 0; + let j = i + 2; + for (; txt[j] !== undefined && find !== count; j++) { + if (txt[j] === "|") { + find++; + } else { + build.push(txt[j]); + if (find !== 0) { + build = build.concat(new Array(find).fill("~")); + find = 0; + } + } + } + if (find === count) { + appendcurrent(); + i = j - 1; + const pipes = "||"; + if (count === 2) { + const j = document.createElement("j"); + if (keep) { + j.append(pipes); + } + j.appendChild(this.markdown(build, { keep, stdsize })); + j.classList.add("spoiler"); + j.onclick = MarkDown.unspoil; + if (keep) { + j.append(pipes); + } + span.appendChild(j); + } + continue; + } + } + if ( + !keep && + txt[i] === "h" && + txt[i + 1] === "t" && + txt[i + 2] === "t" && + txt[i + 3] === "p" + ) { + let build = "http"; + let j = i + 4; + const endchars = new Set(["\\", "<", ">", "|", "]", " "]); + for (; txt[j] !== undefined; j++) { + const char = txt[j]; + if (endchars.has(char)) { + break; + } + build += char; + } + if (URL.canParse(build)) { + appendcurrent(); + const a = document.createElement("a"); + //a.href=build; + MarkDown.safeLink(a, build); + a.textContent = build; + a.target = "_blank"; + i = j - 1; + span.appendChild(a); + continue; + } + } + if (txt[i] === "<" && (txt[i + 1] === "@" || txt[i + 1] === "#")) { + let id = ""; + let j = i + 2; + const numbers = new Set([ + "0", + "1", + "2", + "3", + "4", + "5", + "6", + "7", + "8", + "9", + ]); + for (; txt[j] !== undefined; j++) { + const char = txt[j]; + if (!numbers.has(char)) { + break; + } + id += char; + } + + if (txt[j] === ">") { + appendcurrent(); + const mention = document.createElement("span"); + mention.classList.add("mentionMD"); + mention.contentEditable = "false"; + const char = txt[i + 1]; + i = j; + switch (char) { + case "@": + const user = this.localuser.userMap.get(id); + if (user) { + mention.textContent = `@${user.name}`; + let guild: null | Guild = null; + if (this.owner instanceof Channel) { + guild = this.owner.guild; + } + if (!keep) { + user.bind(mention, guild); + } + if (guild) { + Member.resolveMember(user, guild).then((member) => { + if (member) { + mention.textContent = `@${member.name}`; + } + }); + } + } else { + mention.textContent = `@unknown`; + } + break; + case "#": + const channel = this.localuser.channelids.get(id); + if (channel) { + mention.textContent = `#${channel.name}`; + if (!keep) { + mention.onclick = (_) => { + this.localuser.goToChannel(id); + }; + } + } else { + mention.textContent = `#unknown`; + } + break; + } + span.appendChild(mention); + mention.setAttribute("real", `<${char}${id}>`); + continue; + } + } + if (txt[i] === "<" && txt[i + 1] === "t" && txt[i + 2] === ":") { + let found = false; + const build = ["<", "t", ":"]; + let j = i + 3; + for (; txt[j] !== void 0; j++) { + build.push(txt[j]); + + if (txt[j] === ">") { + found = true; + break; + } + } + + if (found) { + appendcurrent(); + i = j; + const parts = build + .join("") + .match(/^$/) as RegExpMatchArray; + const dateInput = new Date(Number.parseInt(parts[1]) * 1000); + let time = ""; + if (Number.isNaN(dateInput.getTime())) time = build.join(""); + else { + if (parts[3] === "d") + time = dateInput.toLocaleString(void 0, { + day: "2-digit", + month: "2-digit", + year: "numeric", + }); + else if (parts[3] === "D") + time = dateInput.toLocaleString(void 0, { + day: "numeric", + month: "long", + year: "numeric", + }); + else if (!parts[3] || parts[3] === "f") + time = + dateInput.toLocaleString(void 0, { + day: "numeric", + month: "long", + year: "numeric", + }) + + " " + + dateInput.toLocaleString(void 0, { + hour: "2-digit", + minute: "2-digit", + }); + else if (parts[3] === "F") + time = + dateInput.toLocaleString(void 0, { + day: "numeric", + month: "long", + year: "numeric", + weekday: "long", + }) + + " " + + dateInput.toLocaleString(void 0, { + hour: "2-digit", + minute: "2-digit", + }); + else if (parts[3] === "t") + time = dateInput.toLocaleString(void 0, { + hour: "2-digit", + minute: "2-digit", + }); + else if (parts[3] === "T") + time = dateInput.toLocaleString(void 0, { + hour: "2-digit", + minute: "2-digit", + second: "2-digit", + }); + else if (parts[3] === "R") + time = + Math.round( + (Date.now() - Number.parseInt(parts[1]) * 1000) / 1000 / 60 + ) + " minutes ago"; + } + + const timeElem = document.createElement("span"); + timeElem.classList.add("markdown-timestamp"); + timeElem.textContent = time; + span.appendChild(timeElem); + continue; + } + } + + if ( + txt[i] === "<" && + (txt[i + 1] === ":" || (txt[i + 1] === "a" && txt[i + 2] === ":")) + ) { + let found = false; + const build = txt[i + 1] === "a" ? ["<", "a", ":"] : ["<", ":"]; + let j = i + build.length; + for (; txt[j] !== void 0; j++) { + build.push(txt[j]); + + if (txt[j] === ">") { + found = true; + break; + } + } + + if (found) { + const buildjoin = build.join(""); + const parts = buildjoin.match(/^<(a)?:\w+:(\d{10,30})>$/); + if (parts && parts[2]) { + appendcurrent(); + i = j; + const isEmojiOnly = txt.join("").trim() === buildjoin.trim(); + const owner = + this.owner instanceof Channel ? this.owner.guild : this.owner; + const emoji = new Emoji( + { name: buildjoin, id: parts[2], animated: Boolean(parts[1]) }, + owner + ); + span.appendChild(emoji.getHTML(isEmojiOnly)); + + continue; + } + } + } + + if (txt[i] == "[" && !keep) { + let partsFound = 0; + let j = i + 1; + const build = ["["]; + for (; txt[j] !== void 0; j++) { + build.push(txt[j]); + + if (partsFound === 0 && txt[j] === "]") { + if ( + txt[j + 1] === "(" && + txt[j + 2] === "h" && + txt[j + 3] === "t" && + txt[j + 4] === "t" && + txt[j + 5] === "p" && + (txt[j + 6] === "s" || txt[j + 6] === ":") + ) { + partsFound++; + } else { + break; + } + } else if (partsFound === 1 && txt[j] === ")") { + partsFound++; + break; + } + } + + if (partsFound === 2) { + appendcurrent(); + + const parts = build + .join("") + .match(/^\[(.+)\]\((https?:.+?)( ('|").+('|"))?\)$/); + if (parts) { + const linkElem = document.createElement("a"); + if (URL.canParse(parts[2])) { + i = j; + MarkDown.safeLink(linkElem, parts[2]); + linkElem.textContent = parts[1]; + linkElem.target = "_blank"; + linkElem.rel = "noopener noreferrer"; + linkElem.title = + (parts[3] + ? parts[3].substring(2, parts[3].length - 1) + "\n\n" + : "") + parts[2]; + span.appendChild(linkElem); + + continue; + } + } + } + } + + current.textContent += txt[i]; + } + appendcurrent(); + return span; + } + static unspoil(e: any): void { + e.target.classList.remove("spoiler"); + e.target.classList.add("unspoiled"); + } + giveBox(box: HTMLDivElement) { + box.onkeydown = (_) => { + //console.log(_); + }; + let prevcontent = ""; + box.onkeyup = (_) => { + const content = MarkDown.gatherBoxText(box); + if (content !== prevcontent) { + prevcontent = content; + this.txt = content.split(""); + this.boxupdate(box); + } + }; + box.onpaste = (_) => { + if (!_.clipboardData) return; + console.log(_.clipboardData.types); + const data = _.clipboardData.getData("text"); + + document.execCommand("insertHTML", false, data); + _.preventDefault(); + if (!box.onkeyup) return; + box.onkeyup(new KeyboardEvent("_")); + }; + } + boxupdate(box: HTMLElement) { + const restore = saveCaretPosition(box); + box.innerHTML = ""; + box.append(this.makeHTML({ keep: true })); + if (restore) { + restore(); + } + } + static gatherBoxText(element: HTMLElement): string { + if (element.tagName.toLowerCase() === "img") { + return (element as HTMLImageElement).alt; + } + if (element.tagName.toLowerCase() === "br") { + return "\n"; + } + if (element.hasAttribute("real")) { + return element.getAttribute("real") as string; + } + let build = ""; + for (const thing of Array.from(element.childNodes)) { + if (thing instanceof Text) { + const text = thing.textContent; + build += text; + continue; + } + const text = this.gatherBoxText(thing as HTMLElement); + if (text) { + build += text; + } + } + return build; + } + static readonly trustedDomains = new Set([location.host]); + static safeLink(elm: HTMLElement, url: string) { + if (URL.canParse(url)) { + const Url = new URL(url); + if ( + elm instanceof HTMLAnchorElement && + this.trustedDomains.has(Url.host) + ) { + elm.href = url; + elm.target = "_blank"; + return; + } + elm.onmouseup = (_) => { + if (_.button === 2) return; + console.log(":3"); + function open() { + const proxy = window.open(url, "_blank"); + if (proxy && _.button === 1) { + proxy.focus(); + } else if (proxy) { + window.focus(); + } + } + if (this.trustedDomains.has(Url.host)) { + open(); + } else { + const full: Dialog = new Dialog([ + "vdiv", + ["title", "You're leaving spacebar"], + [ + "text", + "You're going to " + + Url.host + + " are you sure you want to go there?", + ], + [ + "hdiv", + ["button", "", "Nevermind", (_: any) => full.hide()], + [ + "button", + "", + "Go there", + (_: any) => { + open(); + full.hide(); + }, + ], + [ + "button", + "", + "Go there and trust in the future", + (_: any) => { + open(); + full.hide(); + this.trustedDomains.add(Url.host); + }, + ], + ], + ]); + full.show(); + } + }; + } else { + throw Error(url + " is not a valid URL"); + } + } + /* + static replace(base: HTMLElement, newelm: HTMLElement) { + const basechildren = base.children; + const newchildren = newelm.children; + for (const thing of Array.from(newchildren)) { + base.append(thing); + } + } + */ +} + +//solution from https://stackoverflow.com/questions/4576694/saving-and-restoring-caret-position-for-contenteditable-div +let text = ""; +function saveCaretPosition(context: Node) { + const selection = window.getSelection(); + if (!selection) return; + const range = selection.getRangeAt(0); + range.setStart(context, 0); + text = selection.toString(); + let len = text.length + 1; + for (const str in text.split("\n")) { + if (str.length !== 0) { + len--; + } + } + len += +(text[text.length - 1] === "\n"); + + return function restore() { + if (!selection) return; + const pos = getTextNodeAtPosition(context, len); + selection.removeAllRanges(); + const range = new Range(); + range.setStart(pos.node, pos.position); + selection.addRange(range); + }; +} + +function getTextNodeAtPosition(root: Node, index: number) { + const NODE_TYPE = NodeFilter.SHOW_TEXT; + const treeWalker = document.createTreeWalker(root, NODE_TYPE, (elem) => { + if (!elem.textContent) return 0; + if (index > elem.textContent.length) { + index -= elem.textContent.length; + return NodeFilter.FILTER_REJECT; + } + return NodeFilter.FILTER_ACCEPT; + }); + const c = treeWalker.nextNode(); + return { + node: c ? c : root, + position: index, + }; +} +export { MarkDown }; diff --git a/src/webpage/member.ts b/src/webpage/member.ts new file mode 100644 index 0000000..d291f5e --- /dev/null +++ b/src/webpage/member.ts @@ -0,0 +1,256 @@ +import { User } from "./user.js"; +import { Role } from "./role.js"; +import { Guild } from "./guild.js"; +import { SnowFlake } from "./snowflake.js"; +import { memberjson, presencejson } from "./jsontypes.js"; +import { Dialog } from "./dialog.js"; + +class Member extends SnowFlake { + static already = {}; + owner: Guild; + user: User; + roles: Role[] = []; + nick!: string; + [key: string]: any; + + private constructor(memberjson: memberjson, owner: Guild) { + super(memberjson.id); + this.owner = owner; + if (this.localuser.userMap.has(memberjson.id)) { + this.user = this.localuser.userMap.get(memberjson.id) as User; + } else if (memberjson.user) { + this.user = new User(memberjson.user, owner.localuser); + } else { + throw new Error("Missing user object of this member"); + } + + for (const key of Object.keys(memberjson)) { + if (key === "guild" || key === "owner") { + continue; + } + + if (key === "roles") { + for (const strrole of memberjson.roles) { + const role = this.guild.roleids.get(strrole); + if (!role) continue; + this.roles.push(role); + } + continue; + } + (this as any)[key] = (memberjson as any)[key]; + } + if (this.localuser.userMap.has(this?.id)) { + this.user = this.localuser.userMap.get(this?.id) as User; + } + this.roles.sort((a, b) => { + return this.guild.roles.indexOf(a) - this.guild.roles.indexOf(b); + }); + } + get guild() { + return this.owner; + } + get localuser() { + return this.guild.localuser; + } + get info() { + return this.owner.info; + } + static async new( + memberjson: memberjson, + owner: Guild + ): Promise { + let user: User; + if (owner.localuser.userMap.has(memberjson.id)) { + user = owner.localuser.userMap.get(memberjson.id) as User; + } else if (memberjson.user) { + user = new User(memberjson.user, owner.localuser); + } else { + throw new Error("missing user object of this member"); + } + if (user.members.has(owner)) { + let memb = user.members.get(owner); + if (memb === undefined) { + memb = new Member(memberjson, owner); + user.members.set(owner, memb); + return memb; + } else if (memb instanceof Promise) { + return await memb; //I should do something else, though for now this is "good enough" + } else { + return memb; + } + } else { + const memb = new Member(memberjson, owner); + user.members.set(owner, memb); + return memb; + } + } + static async resolveMember( + user: User, + guild: Guild + ): Promise { + const maybe = user.members.get(guild); + if (!user.members.has(guild)) { + const membpromise = guild.localuser.resolvemember(user.id, guild.id); + const promise = new Promise(async (res) => { + const membjson = await membpromise; + if (membjson === undefined) { + return res(undefined); + } else { + const member = new Member(membjson, guild); + const map = guild.localuser.presences; + member.getPresence(map.get(member.id)); + map.delete(member.id); + res(member); + return member; + } + }); + user.members.set(guild, promise); + } + if (maybe instanceof Promise) { + return await maybe; + } else { + return maybe; + } + } + public getPresence(presence: presencejson | undefined) { + this.user.getPresence(presence); + } + /** + * @todo + */ + highInfo() { + fetch( + this.info.api + + "/users/" + + this.id + + "/profile?with_mutual_guilds=true&with_mutual_friends_count=true&guild_id=" + + this.guild.id, + { headers: this.guild.headers } + ); + } + hasRole(ID: string) { + console.log(this.roles, ID); + for (const thing of this.roles) { + if (thing.id === ID) { + return true; + } + } + return false; + } + getColor() { + for (const thing of this.roles) { + const color = thing.getColor(); + if (color) { + return color; + } + } + return ""; + } + isAdmin() { + for (const role of this.roles) { + if (role.permissions.getPermission("ADMINISTRATOR")) { + return true; + } + } + return this.guild.properties.owner_id === this.user.id; + } + bind(html: HTMLElement) { + if (html.tagName === "SPAN") { + if (!this) { + return; + } + /* + if(this.error){ + + } + */ + html.style.color = this.getColor(); + } + + //this.profileclick(html); + } + profileclick(/* html: HTMLElement */) { + //to be implemented + } + get name() { + return this.nick || this.user.username; + } + kick() { + let reason = ""; + const menu = new Dialog([ + "vdiv", + ["title", "Kick " + this.name + " from " + this.guild.properties.name], + [ + "textbox", + "Reason:", + "", + function (e: Event) { + reason = (e.target as HTMLInputElement).value; + }, + ], + [ + "button", + "", + "submit", + () => { + this.kickAPI(reason); + menu.hide(); + }, + ], + ]); + menu.show(); + } + kickAPI(reason: string) { + const headers = structuredClone(this.guild.headers); + (headers as any)["x-audit-log-reason"] = reason; + fetch(`${this.info.api}/guilds/${this.guild.id}/members/${this.id}`, { + method: "DELETE", + headers, + }); + } + ban() { + let reason = ""; + const menu = new Dialog([ + "vdiv", + ["title", "Ban " + this.name + " from " + this.guild.properties.name], + [ + "textbox", + "Reason:", + "", + function (e: Event) { + reason = (e.target as HTMLInputElement).value; + }, + ], + [ + "button", + "", + "submit", + () => { + this.banAPI(reason); + menu.hide(); + }, + ], + ]); + menu.show(); + } + banAPI(reason: string) { + const headers = structuredClone(this.guild.headers); + (headers as any)["x-audit-log-reason"] = reason; + fetch(`${this.info.api}/guilds/${this.guild.id}/bans/${this.id}`, { + method: "PUT", + headers, + }); + } + hasPermission(name: string): boolean { + if (this.isAdmin()) { + return true; + } + for (const thing of this.roles) { + if (thing.permissions.getPermission(name)) { + return true; + } + } + return false; + } +} +export { Member }; diff --git a/src/webpage/message.ts b/src/webpage/message.ts new file mode 100644 index 0000000..486ea88 --- /dev/null +++ b/src/webpage/message.ts @@ -0,0 +1,769 @@ +import { Contextmenu } from "./contextmenu.js"; +import { User } from "./user.js"; +import { Member } from "./member.js"; +import { MarkDown } from "./markdown.js"; +import { Embed } from "./embed.js"; +import { Channel } from "./channel.js"; +import { Localuser } from "./localuser.js"; +import { Role } from "./role.js"; +import { File } from "./file.js"; +import { SnowFlake } from "./snowflake.js"; +import { memberjson, messagejson } from "./jsontypes.js"; +import { Emoji } from "./emoji.js"; +import { Dialog } from "./dialog.js"; + +class Message extends SnowFlake { + static contextmenu = new Contextmenu("message menu"); + owner: Channel; + headers: Localuser["headers"]; + embeds!: Embed[]; + author!: User; + mentions!: User[]; + mention_roles!: Role[]; + attachments!: File[]; //probably should be its own class tbh, should be Attachments[] + message_reference!: messagejson; + type!: number; + timestamp!: number; + content!: MarkDown; + static del: Promise; + static resolve: Function; + /* + weakdiv:WeakRef; + set div(e:HTMLDivElement){ + if(!e){ + this.weakdiv=null; + return; + } + this.weakdiv=new WeakRef(e); + } + get div(){ + return this.weakdiv?.deref(); + } + //*/ + div: + | (HTMLDivElement & { pfpparent?: Message | undefined; txt?: HTMLElement }) + | undefined; + member: Member | undefined; + reactions!: messagejson["reactions"]; + static setup() { + this.del = new Promise((_) => { + this.resolve = _; + }); + Message.setupcmenu(); + } + static setupcmenu() { + Message.contextmenu.addbutton("Copy raw text", function (this: Message) { + navigator.clipboard.writeText(this.content.rawString); + }); + Message.contextmenu.addbutton("Reply", function (this: Message) { + this.channel.setReplying(this); + }); + Message.contextmenu.addbutton("Copy message id", function (this: Message) { + navigator.clipboard.writeText(this.id); + }); + Message.contextmenu.addsubmenu( + "Add reaction", + function (this: Message, _, e: MouseEvent) { + Emoji.emojiPicker(e.x, e.y, this.localuser).then((_) => { + this.reactionToggle(_); + }); + } + ); + Message.contextmenu.addbutton( + "Edit", + function (this: Message) { + this.setEdit(); + }, + null, + function () { + return this.author.id === this.localuser.user.id; + } + ); + Message.contextmenu.addbutton( + "Delete message", + function (this: Message) { + this.delete(); + }, + null, + function () { + return this.canDelete(); + } + ); + } + setEdit() { + this.channel.editing = this; + const markdown = ( + document.getElementById("typebox") as HTMLDivElement & { + markdown: MarkDown; + } + )["markdown"] as MarkDown; + markdown.txt = this.content.rawString.split(""); + markdown.boxupdate(document.getElementById("typebox") as HTMLDivElement); + } + constructor(messagejson: messagejson, owner: Channel) { + super(messagejson.id); + this.owner = owner; + this.headers = this.owner.headers; + this.giveData(messagejson); + this.owner.messages.set(this.id, this); + } + reactionToggle(emoji: string | Emoji) { + let remove = false; + for (const thing of this.reactions) { + if (thing.emoji.name === emoji) { + remove = thing.me; + break; + } + } + let reactiontxt: string; + if (emoji instanceof Emoji) { + reactiontxt = `${emoji.name}:${emoji.id}`; + } else { + reactiontxt = encodeURIComponent(emoji); + } + fetch( + `${this.info.api}/channels/${this.channel.id}/messages/${this.id}/reactions/${reactiontxt}/@me`, + { + method: remove ? "DELETE" : "PUT", + headers: this.headers, + } + ); + } + giveData(messagejson: messagejson) { + const func = this.channel.infinite.snapBottom(); + for (const thing of Object.keys(messagejson)) { + if (thing === "attachments") { + this.attachments = []; + for (const thing of messagejson.attachments) { + this.attachments.push(new File(thing, this)); + } + continue; + } else if (thing === "content") { + this.content = new MarkDown(messagejson[thing], this.channel); + continue; + } else if (thing === "id") { + continue; + } else if (thing === "member") { + Member.new(messagejson.member as memberjson, this.guild).then((_) => { + this.member = _ as Member; + }); + continue; + } else if (thing === "embeds") { + this.embeds = []; + for (const thing in messagejson.embeds) { + this.embeds[thing] = new Embed(messagejson.embeds[thing], this); + } + continue; + } + (this as any)[thing] = (messagejson as any)[thing]; + } + if (messagejson.reactions?.length) { + console.log(messagejson.reactions, ":3"); + } + + this.author = new User(messagejson.author, this.localuser); + for (const thing in messagejson.mentions) { + this.mentions[thing] = new User( + messagejson.mentions[thing], + this.localuser + ); + } + if (!this.member && this.guild.id !== "@me") { + this.author.resolvemember(this.guild).then((_) => { + this.member = _; + }); + } + if (this.mentions.length || this.mention_roles.length) { + //currently mention_roles isn't implemented on the spacebar servers + console.log(this.mentions, this.mention_roles); + } + if (this.mentionsuser(this.localuser.user)) { + console.log(this); + } + if (this.div) { + this.generateMessage(); + } + func(); + } + canDelete() { + return ( + this.channel.hasPermission("MANAGE_MESSAGES") || + this.author === this.localuser.user + ); + } + get channel() { + return this.owner; + } + get guild() { + return this.owner.guild; + } + get localuser() { + return this.owner.localuser; + } + get info() { + return this.owner.info; + } + messageevents(obj: HTMLDivElement) { + // const func = Message.contextmenu.bindContextmenu(obj, this, undefined); + this.div = obj; + obj.classList.add("messagediv"); + } + deleteDiv() { + if (!this.div) return; + try { + this.div.remove(); + this.div = undefined; + } catch (e) { + console.error(e); + } + } + mentionsuser(userd: User | Member) { + if (userd instanceof User) { + return this.mentions.includes(userd); + } else if (userd instanceof Member) { + return this.mentions.includes(userd.user); + } else { + return; + } + } + getimages() { + const build: File[] = []; + for (const thing of this.attachments) { + if (thing.content_type.startsWith("image/")) { + build.push(thing); + } + } + return build; + } + async edit(content: string) { + return await fetch( + this.info.api + "/channels/" + this.channel.id + "/messages/" + this.id, + { + method: "PATCH", + headers: this.headers, + body: JSON.stringify({ content }), + } + ); + } + delete() { + fetch(`${this.info.api}/channels/${this.channel.id}/messages/${this.id}`, { + headers: this.headers, + method: "DELETE", + }); + } + deleteEvent() { + console.log("deleted"); + if (this.div) { + this.div.remove(); + this.div.innerHTML = ""; + this.div = undefined; + } + const prev = this.channel.idToPrev.get(this.id); + const next = this.channel.idToNext.get(this.id); + this.channel.idToPrev.delete(this.id); + this.channel.idToNext.delete(this.id); + this.channel.messages.delete(this.id); + if (prev && next) { + this.channel.idToPrev.set(next, prev); + this.channel.idToNext.set(prev, next); + } else if (prev) { + this.channel.idToNext.delete(prev); + } else if (next) { + this.channel.idToPrev.delete(next); + } + if (prev) { + const prevmessage = this.channel.messages.get(prev); + if (prevmessage) { + prevmessage.generateMessage(); + } + } + if ( + this.channel.lastmessage === this || + this.channel.lastmessageid === this.id + ) { + if (prev) { + this.channel.lastmessage = this.channel.messages.get(prev); + this.channel.lastmessageid = prev; + } else { + this.channel.lastmessage = undefined; + this.channel.lastmessageid = undefined; + } + } + if (this.channel.lastreadmessageid === this.id) { + if (prev) { + this.channel.lastreadmessageid = prev; + } else { + this.channel.lastreadmessageid = undefined; + } + } + console.log("deleted done"); + } + reactdiv!: WeakRef; + blockedPropigate() { + const previd = this.channel.idToPrev.get(this.id); + if (!previd) { + this.generateMessage(); + return; + } + const premessage = this.channel.messages.get(previd); + if (premessage?.author === this.author) { + premessage.blockedPropigate(); + } else { + this.generateMessage(); + } + } + generateMessage(premessage?: Message | undefined, ignoredblock = false) { + if (!this.div) return; + if (!premessage) { + premessage = this.channel.messages.get( + this.channel.idToPrev.get(this.id) as string + ); + } + const div = this.div; + for (const user of this.mentions) { + if (user === this.localuser.user) { + div.classList.add("mentioned"); + } + } + if (this === this.channel.replyingto) { + div.classList.add("replying"); + } + div.innerHTML = ""; + const build = document.createElement("div"); + + build.classList.add("flexltr", "message"); + div.classList.remove("zeroheight"); + if (this.author.relationshipType === 2) { + if (ignoredblock) { + if (premessage?.author !== this.author) { + const span = document.createElement("span"); + span.textContent = + "You have this user blocked, click to hide these messages."; + div.append(span); + span.classList.add("blocked"); + span.onclick = (_) => { + const scroll = this.channel.infinite.scrollTop; + let next: Message | undefined = this; + while (next?.author === this.author) { + next.generateMessage(); + next = this.channel.messages.get( + this.channel.idToNext.get(next.id) as string + ); + } + if (this.channel.infinite.scollDiv && scroll) { + this.channel.infinite.scollDiv.scrollTop = scroll; + } + }; + } + } else { + div.classList.remove("topMessage"); + if (premessage?.author === this.author) { + div.classList.add("zeroheight"); + premessage.blockedPropigate(); + div.appendChild(build); + return div; + } else { + build.classList.add("blocked", "topMessage"); + const span = document.createElement("span"); + let count = 1; + let next = this.channel.messages.get( + this.channel.idToNext.get(this.id) as string + ); + while (next?.author === this.author) { + count++; + next = this.channel.messages.get( + this.channel.idToNext.get(next.id) as string + ); + } + span.textContent = `You have this user blocked, click to see the ${count} blocked messages.`; + build.append(span); + span.onclick = (_) => { + const scroll = this.channel.infinite.scrollTop; + const func = this.channel.infinite.snapBottom(); + let next: Message | undefined = this; + while (next?.author === this.author) { + next.generateMessage(undefined, true); + next = this.channel.messages.get( + this.channel.idToNext.get(next.id) as string + ); + console.log("loopy"); + } + if (this.channel.infinite.scollDiv && scroll) { + func(); + this.channel.infinite.scollDiv.scrollTop = scroll; + } + }; + div.appendChild(build); + return div; + } + } + } + if (this.message_reference) { + const replyline = document.createElement("div"); + const line = document.createElement("hr"); + const minipfp = document.createElement("img"); + minipfp.classList.add("replypfp"); + replyline.appendChild(line); + replyline.appendChild(minipfp); + const username = document.createElement("span"); + replyline.appendChild(username); + const reply = document.createElement("div"); + username.classList.add("username"); + reply.classList.add("replytext"); + replyline.appendChild(reply); + const line2 = document.createElement("hr"); + replyline.appendChild(line2); + line2.classList.add("reply"); + line.classList.add("startreply"); + replyline.classList.add("replyflex"); + // TODO: Fix this + this.channel.getmessage(this.message_reference.id).then((message) => { + if (message.author.relationshipType === 2) { + username.textContent = "Blocked user"; + return; + } + const author = message.author; + reply.appendChild(message.content.makeHTML({ stdsize: true })); + minipfp.src = author.getpfpsrc(); + author.bind(minipfp, this.guild); + username.textContent = author.username; + author.bind(username, this.guild); + }); + reply.onclick = (_) => { + // TODO: FIX this + this.channel.infinite.focus(this.message_reference.id); + }; + div.appendChild(replyline); + } + div.appendChild(build); + if ({ 0: true, 19: true }[this.type] || this.attachments.length !== 0) { + const pfpRow = document.createElement("div"); + pfpRow.classList.add("flexltr"); + let pfpparent, current; + if (premessage != null) { + pfpparent ??= premessage; + // @ts-ignore + // TODO: type this + let pfpparent2 = pfpparent.all; + pfpparent2 ??= pfpparent; + const old = new Date(pfpparent2.timestamp).getTime() / 1000; + const newt = new Date(this.timestamp).getTime() / 1000; + current = newt - old > 600; + } + const combine = + premessage?.author != this.author || current || this.message_reference; + if (combine) { + const pfp = this.author.buildpfp(); + this.author.bind(pfp, this.guild, false); + pfpRow.appendChild(pfp); + } else { + div["pfpparent"] = pfpparent; + } + pfpRow.classList.add("pfprow"); + build.appendChild(pfpRow); + const text = document.createElement("div"); + text.classList.add("flexttb"); + const texttxt = document.createElement("div"); + texttxt.classList.add("commentrow", "flexttb"); + text.appendChild(texttxt); + if (combine) { + const username = document.createElement("span"); + username.classList.add("username"); + this.author.bind(username, this.guild); + div.classList.add("topMessage"); + username.textContent = this.author.username; + const userwrap = document.createElement("div"); + userwrap.classList.add("flexltr"); + userwrap.appendChild(username); + if (this.author.bot) { + const username = document.createElement("span"); + username.classList.add("bot"); + username.textContent = "BOT"; + userwrap.appendChild(username); + } + const time = document.createElement("span"); + time.textContent = " " + formatTime(new Date(this.timestamp)); + time.classList.add("timestamp"); + userwrap.appendChild(time); + + texttxt.appendChild(userwrap); + } else { + div.classList.remove("topMessage"); + } + const messaged = this.content.makeHTML(); + (div as any)["txt"] = messaged; + const messagedwrap = document.createElement("div"); + messagedwrap.classList.add("flexttb"); + messagedwrap.appendChild(messaged); + texttxt.appendChild(messagedwrap); + + build.appendChild(text); + if (this.attachments.length) { + console.log(this.attachments); + const attach = document.createElement("div"); + attach.classList.add("flexltr"); + for (const thing of this.attachments) { + attach.appendChild(thing.getHTML()); + } + messagedwrap.appendChild(attach); + } + if (this.embeds.length) { + const embeds = document.createElement("div"); + embeds.classList.add("flexltr"); + for (const thing of this.embeds) { + embeds.appendChild(thing.generateHTML()); + } + messagedwrap.appendChild(embeds); + } + // + } else if (this.type === 7) { + const text = document.createElement("div"); + text.classList.add("flexttb"); + const texttxt = document.createElement("div"); + text.appendChild(texttxt); + build.appendChild(text); + texttxt.classList.add("flexltr"); + const messaged = document.createElement("span"); + div["txt"] = messaged; + messaged.textContent = "welcome: "; + texttxt.appendChild(messaged); + + const username = document.createElement("span"); + username.textContent = this.author.username; + //this.author.profileclick(username); + this.author.bind(username, this.guild); + texttxt.appendChild(username); + username.classList.add("username"); + + const time = document.createElement("span"); + time.textContent = " " + formatTime(new Date(this.timestamp)); + time.classList.add("timestamp"); + texttxt.append(time); + div.classList.add("topMessage"); + } + const reactions = document.createElement("div"); + reactions.classList.add("flexltr", "reactiondiv"); + this.reactdiv = new WeakRef(reactions); + this.updateReactions(); + div.append(reactions); + this.bindButtonEvent(); + return div; + } + bindButtonEvent() { + if (this.div) { + let buttons: HTMLDivElement | undefined; + this.div.onmouseenter = (_) => { + if (buttons) { + buttons.remove(); + buttons = undefined; + } + if (this.div) { + buttons = document.createElement("div"); + buttons.classList.add("messageButtons", "flexltr"); + if (this.channel.hasPermission("SEND_MESSAGES")) { + const container = document.createElement("div"); + const reply = document.createElement("span"); + reply.classList.add("svgtheme", "svg-reply", "svgicon"); + container.append(reply); + buttons.append(container); + container.onclick = (_) => { + this.channel.setReplying(this); + }; + } + if (this.author === this.localuser.user) { + const container = document.createElement("div"); + const edit = document.createElement("span"); + edit.classList.add("svgtheme", "svg-edit", "svgicon"); + container.append(edit); + buttons.append(container); + container.onclick = (_) => { + this.setEdit(); + }; + } + if (this.canDelete()) { + const container = document.createElement("div"); + const reply = document.createElement("span"); + reply.classList.add("svgtheme", "svg-delete", "svgicon"); + container.append(reply); + buttons.append(container); + container.onclick = (_) => { + if (_.shiftKey) { + this.delete(); + return; + } + const diaolog = new Dialog([ + "hdiv", + ["title", "are you sure you want to delete this?"], + [ + "button", + "", + "yes", + () => { + this.delete(); + diaolog.hide(); + }, + ], + [ + "button", + "", + "no", + () => { + diaolog.hide(); + }, + ], + ]); + diaolog.show(); + }; + } + if (buttons.childNodes.length !== 0) { + this.div.append(buttons); + } + } + }; + this.div.onmouseleave = (_) => { + if (buttons) { + buttons.remove(); + buttons = undefined; + } + }; + } + } + updateReactions() { + const reactdiv = this.reactdiv.deref(); + if (!reactdiv) return; + const func = this.channel.infinite.snapBottom(); + reactdiv.innerHTML = ""; + for (const thing of this.reactions) { + const reaction = document.createElement("div"); + reaction.classList.add("reaction"); + if (thing.me) { + reaction.classList.add("meReacted"); + } + let emoji: HTMLElement; + if (thing.emoji.id || /\d{17,21}/.test(thing.emoji.name)) { + if (/\d{17,21}/.test(thing.emoji.name)) + thing.emoji.id = thing.emoji.name; //Should stop being a thing once the server fixes this bug + const emo = new Emoji( + thing.emoji as { name: string; id: string; animated: boolean }, + this.guild + ); + emoji = emo.getHTML(false); + } else { + emoji = document.createElement("p"); + emoji.textContent = thing.emoji.name; + } + const count = document.createElement("p"); + count.textContent = "" + thing.count; + count.classList.add("reactionCount"); + reaction.append(count); + reaction.append(emoji); + reactdiv.append(reaction); + + reaction.onclick = (_) => { + this.reactionToggle(thing.emoji.name); + }; + } + func(); + } + reactionAdd(data: { name: string }, member: Member | { id: string }) { + for (const thing of this.reactions) { + if (thing.emoji.name === data.name) { + thing.count++; + if (member.id === this.localuser.user.id) { + thing.me = true; + this.updateReactions(); + return; + } + } + } + this.reactions.push({ + count: 1, + emoji: data, + me: member.id === this.localuser.user.id, + }); + this.updateReactions(); + } + reactionRemove(data: { name: string }, id: string) { + console.log("test"); + for (const i in this.reactions) { + const thing = this.reactions[i]; + console.log(thing, data); + if (thing.emoji.name === data.name) { + thing.count--; + if (thing.count === 0) { + this.reactions.splice(Number(i), 1); + this.updateReactions(); + return; + } + if (id === this.localuser.user.id) { + thing.me = false; + this.updateReactions(); + return; + } + } + } + } + reactionRemoveAll() { + this.reactions = []; + this.updateReactions(); + } + reactionRemoveEmoji(emoji: Emoji) { + for (const i in this.reactions) { + const reaction = this.reactions[i]; + if ( + (reaction.emoji.id && reaction.emoji.id == emoji.id) || + (!reaction.emoji.id && reaction.emoji.name == emoji.name) + ) { + this.reactions.splice(Number(i), 1); + this.updateReactions(); + break; + } + } + } + buildhtml(premessage?: Message | undefined): HTMLElement { + if (this.div) { + console.error(`HTML for ${this.id} already exists, aborting`); + return this.div; + } + try { + const div = document.createElement("div"); + this.div = div; + this.messageevents(div); + return this.generateMessage(premessage) as HTMLElement; + } catch (e) { + console.error(e); + } + return this.div as HTMLElement; + } +} +let now: string; +let yesterdayStr: string; + +function formatTime(date: Date) { + updateTimes(); + const datestring = date.toLocaleDateString(); + const formatTime = (date: Date) => + date.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }); + + if (datestring === now) { + return `Today at ${formatTime(date)}`; + } else if (datestring === yesterdayStr) { + return `Yesterday at ${formatTime(date)}`; + } else { + return `${date.toLocaleDateString()} at ${formatTime(date)}`; + } +} +let tomorrow = 0; +updateTimes(); +function updateTimes() { + if (tomorrow < Date.now()) { + const d = new Date(); + tomorrow = d.setHours(24, 0, 0, 0); + now = new Date().toLocaleDateString(); + const yesterday = new Date(now); + yesterday.setDate(new Date().getDate() - 1); + yesterdayStr = yesterday.toLocaleDateString(); + } +} +Message.setup(); +export { Message }; diff --git a/src/webpage/permissions.ts b/src/webpage/permissions.ts new file mode 100644 index 0000000..308ffab --- /dev/null +++ b/src/webpage/permissions.ts @@ -0,0 +1,347 @@ +class Permissions { + allow: bigint; + deny: bigint; + readonly hasDeny: boolean; + constructor(allow: string, deny: string = "") { + this.hasDeny = Boolean(deny); + try { + this.allow = BigInt(allow); + this.deny = BigInt(deny); + } catch { + this.allow = 0n; + this.deny = 0n; + console.error( + `Something really stupid happened with a permission with allow being ${allow} and deny being, ${deny}, 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 map: { + [key: number | string]: + | { name: string; readableName: string; description: string } + | number; + }; + static info: { name: string; readableName: string; description: string }[]; + static makeMap() { + Permissions.info = [ + //for people in the future, do not reorder these, the creation of the map realize on the order + { + name: "CREATE_INSTANT_INVITE", + readableName: "Create invite", + description: "Allows the user to create invites for the guild", + }, + { + name: "KICK_MEMBERS", + readableName: "Kick members", + description: "Allows the user to kick members from the guild", + }, + { + name: "BAN_MEMBERS", + readableName: "Ban members", + description: "Allows the user to ban members from the guild", + }, + { + name: "ADMINISTRATOR", + readableName: "Administrator", + description: + "Allows all permissions and bypasses channel permission overwrites. This is a dangerous permission!", + }, + { + name: "MANAGE_CHANNELS", + readableName: "Manage channels", + description: "Allows the user to manage and edit channels", + }, + { + name: "MANAGE_GUILD", + readableName: "Manage guild", + description: "Allows management and editing of the guild", + }, + { + name: "ADD_REACTIONS", + readableName: "Add reactions", + description: "Allows user to add reactions to messages", + }, + { + name: "VIEW_AUDIT_LOG", + readableName: "View audit log", + description: "Allows the user to view the audit log", + }, + { + name: "PRIORITY_SPEAKER", + readableName: "Priority speaker", + description: "Allows for using priority speaker in a voice channel", + }, + { + name: "STREAM", + readableName: "Video", + description: "Allows the user to stream", + }, + { + name: "VIEW_CHANNEL", + readableName: "View channels", + description: "Allows the user to view the channel", + }, + { + name: "SEND_MESSAGES", + readableName: "Send messages", + description: "Allows user to send messages", + }, + { + name: "SEND_TTS_MESSAGES", + readableName: "Send text-to-speech messages", + description: "Allows the user to send text-to-speech messages", + }, + { + name: "MANAGE_MESSAGES", + readableName: "Manage messages", + description: "Allows the user to delete messages that aren't their own", + }, + { + name: "EMBED_LINKS", + readableName: "Embed links", + description: "Allow links sent by this user to auto-embed", + }, + { + name: "ATTACH_FILES", + readableName: "Attach files", + description: "Allows the user to attach files", + }, + { + name: "READ_MESSAGE_HISTORY", + readableName: "Read message history", + description: "Allows user to read the message history", + }, + { + name: "MENTION_EVERYONE", + readableName: "Mention @everyone, @here and all roles", + description: "Allows the user to mention everyone", + }, + { + name: "USE_EXTERNAL_EMOJIS", + readableName: "Use external emojis", + description: "Allows the user to use external emojis", + }, + { + name: "VIEW_GUILD_INSIGHTS", + readableName: "View guild insights", + description: "Allows the user to see guild insights", + }, + { + name: "CONNECT", + readableName: "Connect", + description: "Allows the user to connect to a voice channel", + }, + { + name: "SPEAK", + readableName: "Speak", + description: "Allows the user to speak in a voice channel", + }, + { + name: "MUTE_MEMBERS", + readableName: "Mute members", + description: "Allows user to mute other members", + }, + { + name: "DEAFEN_MEMBERS", + readableName: "Deafen members", + description: "Allows user to deafen other members", + }, + { + name: "MOVE_MEMBERS", + readableName: "Move members", + description: "Allows the user to move members between voice channels", + }, + { + name: "USE_VAD", + readableName: "Use voice activity detection", + description: + "Allows users to speak in a voice channel by simply talking", + }, + { + name: "CHANGE_NICKNAME", + readableName: "Change nickname", + description: "Allows the user to change their own nickname", + }, + { + name: "MANAGE_NICKNAMES", + readableName: "Manage nicknames", + description: "Allows user to change nicknames of other members", + }, + { + name: "MANAGE_ROLES", + readableName: "Manage roles", + description: "Allows user to edit and manage roles", + }, + { + name: "MANAGE_WEBHOOKS", + readableName: "Manage webhooks", + description: "Allows management and editing of webhooks", + }, + { + name: "MANAGE_GUILD_EXPRESSIONS", + readableName: "Manage expressions", + description: "Allows for managing emoji, stickers, and soundboards", + }, + { + name: "USE_APPLICATION_COMMANDS", + readableName: "Use application commands", + description: "Allows the user to use application commands", + }, + { + name: "REQUEST_TO_SPEAK", + readableName: "Request to speak", + description: "Allows user to request to speak in stage channel", + }, + { + name: "MANAGE_EVENTS", + readableName: "Manage events", + description: "Allows user to edit and manage events", + }, + { + name: "MANAGE_THREADS", + readableName: "Manage threads", + description: + "Allows the user to delete and archive threads and view all private threads", + }, + { + name: "CREATE_PUBLIC_THREADS", + readableName: "Create public threads", + description: "Allows the user to create public threads", + }, + { + name: "CREATE_PRIVATE_THREADS", + readableName: "Create private threads", + description: "Allows the user to create private threads", + }, + { + name: "USE_EXTERNAL_STICKERS", + readableName: "Use external stickers", + description: "Allows user to use external stickers", + }, + { + name: "SEND_MESSAGES_IN_THREADS", + readableName: "Send messages in threads", + description: "Allows the user to send messages in threads", + }, + { + name: "USE_EMBEDDED_ACTIVITIES", + readableName: "Use activities", + description: "Allows the user to use embedded activities", + }, + { + name: "MODERATE_MEMBERS", + readableName: "Timeout members", + description: + "Allows the user to time out other users to prevent them from sending or reacting to messages in chat and threads, and from speaking in voice and stage channels", + }, + { + name: "VIEW_CREATOR_MONETIZATION_ANALYTICS", + readableName: "View creator monetization analytics", + description: "Allows for viewing role subscription insights", + }, + { + name: "USE_SOUNDBOARD", + readableName: "Use soundboard", + description: "Allows for using soundboard in a voice channel", + }, + { + name: "CREATE_GUILD_EXPRESSIONS", + readableName: "Create expressions", + description: + "Allows for creating emojis, stickers, and soundboard sounds, and editing and deleting those created by the current user.", + }, + { + name: "CREATE_EVENTS", + readableName: "Create events", + description: + "Allows for creating scheduled events, and editing and deleting those created by the current user.", + }, + { + name: "USE_EXTERNAL_SOUNDS", + readableName: "Use external sounds", + description: + "Allows the usage of custom soundboard sounds from other servers", + }, + { + name: "SEND_VOICE_MESSAGES", + readableName: "Send voice messages", + description: "Allows sending voice messages", + }, + { + name: "SEND_POLLS", + readableName: "Create polls", + description: "Allows sending polls", + }, + { + name: "USE_EXTERNAL_APPS", + readableName: "Use external apps", + description: + "Allows user-installed apps to send public responses. " + + "When disabled, users will still be allowed to use their apps but the responses will be ephemeral. " + + "This only applies to apps not also installed to the server.", + }, + ]; + Permissions.map = {}; + let i = 0; + for (const thing of Permissions.info) { + Permissions.map[i] = thing; + Permissions.map[thing.name] = i; + i++; + } + } + getPermission(name: string): number { + if (this.getPermissionbit(Permissions.map[name] as number, this.allow)) { + return 1; + } else if ( + this.getPermissionbit(Permissions.map[name] as number, this.deny) + ) { + return -1; + } else { + return 0; + } + } + hasPermission(name: string): boolean { + if (this.deny) { + console.warn( + "This function may of been used in error, think about using getPermision instead" + ); + } + if (this.getPermissionbit(Permissions.map[name] as number, this.allow)) + return true; + if (name != "ADMINISTRATOR") return this.hasPermission("ADMINISTRATOR"); + return false; + } + setPermission(name: string, setto: number): void { + const bit = Permissions.map[name] as number; + if (!bit) { + return console.error( + "Tried to set permission to " + + setto + + " for " + + name + + " but it doesn't exist" + ); + } + + if (setto === 0) { + this.deny = this.setPermissionbit(bit, false, this.deny); + this.allow = this.setPermissionbit(bit, false, this.allow); + } else if (setto === 1) { + this.deny = this.setPermissionbit(bit, false, this.deny); + this.allow = this.setPermissionbit(bit, true, this.allow); + } else if (setto === -1) { + this.deny = this.setPermissionbit(bit, true, this.deny); + this.allow = this.setPermissionbit(bit, false, this.allow); + } else { + console.error("invalid number entered:" + setto); + } + } +} +Permissions.makeMap(); +export { Permissions }; diff --git a/webpage/register.html b/src/webpage/register.html similarity index 100% rename from webpage/register.html rename to src/webpage/register.html diff --git a/src/webpage/register.ts b/src/webpage/register.ts new file mode 100644 index 0000000..f36f46d --- /dev/null +++ b/src/webpage/register.ts @@ -0,0 +1,152 @@ +import { checkInstance, adduser } from "./login.js"; + +const registerElement = document.getElementById("register"); +if (registerElement) { + registerElement.addEventListener("submit", registertry); +} + +async function registertry(e: Event) { + e.preventDefault(); + const elements = (e.target as HTMLFormElement) + .elements as HTMLFormControlsCollection; + const email = (elements[1] as HTMLInputElement).value; + const username = (elements[2] as HTMLInputElement).value; + const password = (elements[3] as HTMLInputElement).value; + const confirmPassword = (elements[4] as HTMLInputElement).value; + const dateofbirth = (elements[5] as HTMLInputElement).value; + const consent = (elements[6] as HTMLInputElement).checked; + const captchaKey = (elements[7] as HTMLInputElement)?.value; + + if (password !== confirmPassword) { + (document.getElementById("wrong") as HTMLElement).textContent = + "Passwords don't match"; + return; + } + + const instanceInfo = JSON.parse(localStorage.getItem("instanceinfo") ?? "{}"); + const apiurl = new URL(instanceInfo.api); + + try { + const response = await fetch(apiurl + "/auth/register", { + body: JSON.stringify({ + date_of_birth: dateofbirth, + email, + username, + password, + consent, + captcha_key: captchaKey, + }), + headers: { + "content-type": "application/json", + }, + method: "POST", + }); + + const data = await response.json(); + + if (data.captcha_sitekey) { + const capt = document.getElementById("h-captcha"); + if (capt && !capt.children.length) { + const capty = document.createElement("div"); + capty.classList.add("h-captcha"); + capty.setAttribute("data-sitekey", data.captcha_sitekey); + const script = document.createElement("script"); + script.src = "https://js.hcaptcha.com/1/api.js"; + capt.append(script); + capt.append(capty); + } else { + eval("hcaptcha.reset()"); + } + return; + } + + if (!data.token) { + handleErrors(data.errors, elements); + } else { + adduser({ + serverurls: instanceInfo, + email, + token: data.token, + }).username = username; + localStorage.setItem("token", data.token); + const redir = new URLSearchParams(window.location.search).get("goback"); + window.location.href = redir ? redir : "/channels/@me"; + } + } catch (error) { + console.error("Registration failed:", error); + } +} + +function handleErrors(errors: any, elements: HTMLFormControlsCollection) { + if (errors.consent) { + showError(elements[6] as HTMLElement, errors.consent._errors[0].message); + } else if (errors.password) { + showError( + elements[3] as HTMLElement, + "Password: " + errors.password._errors[0].message + ); + } else if (errors.username) { + showError( + elements[2] as HTMLElement, + "Username: " + errors.username._errors[0].message + ); + } else if (errors.email) { + showError( + elements[1] as HTMLElement, + "Email: " + errors.email._errors[0].message + ); + } else if (errors.date_of_birth) { + showError( + elements[5] as HTMLElement, + "Date of Birth: " + errors.date_of_birth._errors[0].message + ); + } else { + (document.getElementById("wrong") as HTMLElement).textContent = + errors[Object.keys(errors)[0]]._errors[0].message; + } +} + +function showError(element: HTMLElement, message: string) { + const parent = element.parentElement!; + let errorElement = parent.getElementsByClassName( + "suberror" + )[0] as HTMLElement; + if (!errorElement) { + const div = document.createElement("div"); + div.classList.add("suberror", "suberrora"); + parent.append(div); + errorElement = div; + } else { + errorElement.classList.remove("suberror"); + setTimeout(() => { + errorElement.classList.add("suberror"); + }, 100); + } + errorElement.textContent = message; +} + +let TOSa = document.getElementById("TOSa") as HTMLAnchorElement | null; + +async function tosLogic() { + const instanceInfo = JSON.parse(localStorage.getItem("instanceinfo") ?? "{}"); + const apiurl = new URL(instanceInfo.api); + const response = await fetch(apiurl.toString() + "/ping"); + const data = await response.json(); + const tosPage = data.instance.tosPage; + + if (tosPage) { + document.getElementById("TOSbox")!.innerHTML = + 'I agree to the Terms of Service:'; + TOSa = document.getElementById("TOSa") as HTMLAnchorElement; + TOSa.href = tosPage; + } else { + document.getElementById("TOSbox")!.textContent = + "This instance has no Terms of Service, accept ToS anyways:"; + TOSa = null; + } + console.log(tosPage); +} + +tosLogic(); + +(checkInstance as any)["alt"] = tosLogic; diff --git a/src/webpage/role.ts b/src/webpage/role.ts new file mode 100644 index 0000000..a024aae --- /dev/null +++ b/src/webpage/role.ts @@ -0,0 +1,180 @@ +import { Permissions } from "./permissions.js"; +import { Localuser } from "./localuser.js"; +import { Guild } from "./guild.js"; +import { SnowFlake } from "./snowflake.js"; +import { rolesjson } from "./jsontypes.js"; +class Role extends SnowFlake { + permissions: Permissions; + owner: Guild; + color!: number; + name!: string; + info: Guild["info"]; + hoist!: boolean; + icon!: string; + mentionable!: boolean; + unicode_emoji!: string; + headers: Guild["headers"]; + constructor(json: rolesjson, owner: Guild) { + super(json.id); + this.headers = owner.headers; + this.info = owner.info; + for (const thing of Object.keys(json)) { + if (thing === "id") { + continue; + } + (this as any)[thing] = (json as any)[thing]; + } + this.permissions = new Permissions(json.permissions); + this.owner = owner; + } + get guild(): Guild { + return this.owner; + } + get localuser(): Localuser { + return this.guild.localuser; + } + getColor(): string | null { + if (this.color === 0) { + return null; + } + return `#${this.color.toString(16)}`; + } +} +export { Role }; +import { Options } from "./settings.js"; +class PermissionToggle implements OptionsElement { + readonly rolejson: { + name: string; + readableName: string; + description: string; + }; + permissions: Permissions; + owner: Options; + value!: number; + constructor( + roleJSON: PermissionToggle["rolejson"], + permissions: Permissions, + owner: Options + ) { + this.rolejson = roleJSON; + this.permissions = permissions; + this.owner = owner; + } + watchForChange() {} + generateHTML(): HTMLElement { + const div = document.createElement("div"); + div.classList.add("setting"); + const name = document.createElement("span"); + name.textContent = this.rolejson.readableName; + name.classList.add("settingsname"); + div.append(name); + + div.append(this.generateCheckbox()); + const p = document.createElement("p"); + p.textContent = this.rolejson.description; + div.appendChild(p); + return div; + } + generateCheckbox(): HTMLElement { + const div = document.createElement("div"); + div.classList.add("tritoggle"); + const state = this.permissions.getPermission(this.rolejson.name); + + const on = document.createElement("input"); + on.type = "radio"; + on.name = this.rolejson.name; + div.append(on); + if (state === 1) { + on.checked = true; + } + on.onclick = (_) => { + this.permissions.setPermission(this.rolejson.name, 1); + this.owner.changed(); + }; + + const no = document.createElement("input"); + no.type = "radio"; + no.name = this.rolejson.name; + div.append(no); + if (state === 0) { + no.checked = true; + } + no.onclick = (_) => { + this.permissions.setPermission(this.rolejson.name, 0); + this.owner.changed(); + }; + if (this.permissions.hasDeny) { + const off = document.createElement("input"); + off.type = "radio"; + off.name = this.rolejson.name; + div.append(off); + if (state === -1) { + off.checked = true; + } + off.onclick = (_) => { + this.permissions.setPermission(this.rolejson.name, -1); + this.owner.changed(); + }; + } + return div; + } + submit() {} +} +import { OptionsElement, Buttons } from "./settings.js"; +class RoleList extends Buttons { + readonly permissions: [Role, Permissions][]; + permission: Permissions; + readonly guild: Guild; + readonly channel: boolean; + declare readonly buttons: [string, string][]; + readonly options: Options; + onchange: Function; + curid!: string; + constructor( + permissions: [Role, Permissions][], + guild: Guild, + onchange: Function, + channel = false + ) { + super("Roles"); + this.guild = guild; + this.permissions = permissions; + this.channel = channel; + this.onchange = onchange; + const options = new Options("", this); + if (channel) { + this.permission = new Permissions("0", "0"); + } else { + this.permission = new Permissions("0"); + } + for (const thing of Permissions.info) { + options.options.push( + new PermissionToggle(thing, this.permission, options) + ); + } + for (const i of permissions) { + console.log(i); + this.buttons.push([i[0].name, i[0].id]); + } + this.options = options; + } + handleString(str: string): HTMLElement { + this.curid = str; + const arr = this.permissions.find((_) => _[0].id === str); + if (arr) { + const perm = arr[1]; + this.permission.deny = perm.deny; + this.permission.allow = perm.allow; + const role = this.permissions.find((e) => e[0].id === str); + if (role) { + this.options.name = role[0].name; + this.options.haschanged = false; + } + } + return this.options.generateHTML(); + } + save() { + this.onchange(this.curid, this.permission); + } +} +export { RoleList }; diff --git a/src/webpage/service.ts b/src/webpage/service.ts new file mode 100644 index 0000000..67256c6 --- /dev/null +++ b/src/webpage/service.ts @@ -0,0 +1,96 @@ +function deleteoldcache() { + caches.delete("cache"); + console.log("this ran :P"); +} + +async function putInCache(request: URL | RequestInfo, response: Response) { + console.log(request, response); + const cache = await caches.open("cache"); + console.log("Grabbed"); + try { + console.log(await cache.put(request, response)); + } catch (error) { + console.error(error); + } +} +console.log("test"); + +let lastcache: string; +self.addEventListener("activate", async () => { + console.log("test2"); + checkCache(); +}); +async function checkCache() { + if (checkedrecently) { + return; + } + const promise = await caches.match("/getupdates"); + if (promise) { + lastcache = await promise.text(); + } + console.log(lastcache); + fetch("/getupdates").then(async (data) => { + const text = await data.clone().text(); + console.log(text, lastcache); + if (lastcache !== text) { + deleteoldcache(); + putInCache("/getupdates", data.clone()); + } + checkedrecently = true; + setTimeout((_: any) => { + checkedrecently = false; + }, 1000 * 60 * 30); + }); +} +var checkedrecently = false; +function samedomain(url: string | URL) { + return new URL(url).origin === self.origin; +} +function isindexhtml(url: string | URL) { + console.log(url); + if (new URL(url).pathname.startsWith("/channels")) { + return true; + } + return false; +} +async function getfile(event: { + request: { url: URL | RequestInfo; clone: () => string | URL | Request }; +}) { + checkCache(); + if (!samedomain(event.request.url.toString())) { + return await fetch(event.request.clone()); + } + const responseFromCache = await caches.match(event.request.url); + console.log(responseFromCache, caches); + if (responseFromCache) { + console.log("cache hit"); + return responseFromCache; + } + if (isindexhtml(event.request.url.toString())) { + console.log("is index.html"); + const responseFromCache = await caches.match("/index.html"); + if (responseFromCache) { + console.log("cache hit"); + return responseFromCache; + } + const responseFromNetwork = await fetch("/index.html"); + await putInCache("/index.html", responseFromNetwork.clone()); + return responseFromNetwork; + } + const responseFromNetwork = await fetch(event.request.clone()); + console.log(event.request.clone()); + await putInCache(event.request.clone(), responseFromNetwork.clone()); + try { + return responseFromNetwork; + } catch (e) { + console.error(e); + return e; + } +} +self.addEventListener("fetch", (event: any) => { + try { + event.respondWith(getfile(event)); + } catch (e) { + console.error(e); + } +}); diff --git a/src/webpage/settings.ts b/src/webpage/settings.ts new file mode 100644 index 0000000..83e66d2 --- /dev/null +++ b/src/webpage/settings.ts @@ -0,0 +1,1113 @@ +interface OptionsElement { + // + generateHTML(): HTMLElement; + submit: () => void; + readonly watchForChange: (func: (arg1: x) => void) => void; + value: x; +} +//future me stuff +class Buttons implements OptionsElement { + readonly name: string; + readonly buttons: [string, Options | string][]; + buttonList!: HTMLDivElement; + warndiv!: HTMLElement; + value: unknown; + constructor(name: string) { + this.buttons = []; + this.name = name; + } + add(name: string, thing?: Options | undefined) { + if (!thing) { + thing = new Options(name, this); + } + this.buttons.push([name, thing]); + return thing; + } + generateHTML() { + const buttonList = document.createElement("div"); + buttonList.classList.add("Buttons"); + buttonList.classList.add("flexltr"); + this.buttonList = buttonList; + const htmlarea = document.createElement("div"); + htmlarea.classList.add("flexgrow"); + const buttonTable = document.createElement("div"); + buttonTable.classList.add("flexttb", "settingbuttons"); + for (const thing of this.buttons) { + const button = document.createElement("button"); + button.classList.add("SettingsButton"); + button.textContent = thing[0]; + button.onclick = (_) => { + this.generateHTMLArea(thing[1], htmlarea); + if (this.warndiv) { + this.warndiv.remove(); + } + }; + buttonTable.append(button); + } + this.generateHTMLArea(this.buttons[0][1], htmlarea); + buttonList.append(buttonTable); + buttonList.append(htmlarea); + return buttonList; + } + handleString(str: string): HTMLElement { + const div = document.createElement("span"); + div.textContent = str; + return div; + } + private generateHTMLArea( + buttonInfo: Options | string, + htmlarea: HTMLElement + ) { + let html: HTMLElement; + if (buttonInfo instanceof Options) { + buttonInfo.subOptions = undefined; + html = buttonInfo.generateHTML(); + } else { + html = this.handleString(buttonInfo); + } + htmlarea.innerHTML = ""; + htmlarea.append(html); + } + changed(html: HTMLElement) { + this.warndiv = html; + this.buttonList.append(html); + } + watchForChange() {} + save() {} + submit() {} +} + +class TextInput implements OptionsElement { + readonly label: string; + readonly owner: Options; + readonly onSubmit: (str: string) => void; + value: string; + input!: WeakRef; + password: boolean; + constructor( + label: string, + onSubmit: (str: string) => void, + owner: Options, + { initText = "", password = false } = {} + ) { + this.label = label; + this.value = initText; + this.owner = owner; + this.onSubmit = onSubmit; + this.password = password; + } + generateHTML(): HTMLDivElement { + const div = document.createElement("div"); + const span = document.createElement("span"); + span.textContent = this.label; + div.append(span); + const input = document.createElement("input"); + input.value = this.value; + input.type = this.password ? "password" : "text"; + input.oninput = this.onChange.bind(this); + this.input = new WeakRef(input); + div.append(input); + return div; + } + private onChange() { + this.owner.changed(); + const input = this.input.deref(); + if (input) { + const value = input.value as string; + this.onchange(value); + this.value = value; + } + } + onchange: (str: string) => void = (_) => {}; + watchForChange(func: (str: string) => void) { + this.onchange = func; + } + submit() { + this.onSubmit(this.value); + } +} + +class SettingsText implements OptionsElement { + readonly onSubmit!: (str: string) => void; + value!: void; + readonly text: string; + constructor(text: string) { + this.text = text; + } + generateHTML(): HTMLSpanElement { + const span = document.createElement("span"); + span.innerText = this.text; + return span; + } + watchForChange() {} + submit() {} +} +class SettingsTitle implements OptionsElement { + readonly onSubmit!: (str: string) => void; + value!: void; + readonly text: string; + constructor(text: string) { + this.text = text; + } + generateHTML(): HTMLSpanElement { + const span = document.createElement("h2"); + span.innerText = this.text; + return span; + } + watchForChange() {} + submit() {} +} +class CheckboxInput implements OptionsElement { + readonly label: string; + readonly owner: Options; + readonly onSubmit: (str: boolean) => void; + value: boolean; + input!: WeakRef; + constructor( + label: string, + onSubmit: (str: boolean) => void, + owner: Options, + { initState = false } = {} + ) { + this.label = label; + this.value = initState; + this.owner = owner; + this.onSubmit = onSubmit; + } + generateHTML(): HTMLDivElement { + const div = document.createElement("div"); + const span = document.createElement("span"); + span.textContent = this.label; + div.append(span); + const input = document.createElement("input"); + input.type = "checkbox"; + input.checked = this.value; + input.oninput = this.onChange.bind(this); + this.input = new WeakRef(input); + div.append(input); + return div; + } + private onChange() { + this.owner.changed(); + const input = this.input.deref(); + if (input) { + const value = input.checked as boolean; + this.onchange(value); + this.value = value; + } + } + onchange: (str: boolean) => void = (_) => {}; + watchForChange(func: (str: boolean) => void) { + this.onchange = func; + } + submit() { + this.onSubmit(this.value); + } +} + +class ButtonInput implements OptionsElement { + readonly label: string; + readonly owner: Options; + readonly onClick: () => void; + textContent: string; + value!: void; + constructor( + label: string, + textContent: string, + onClick: () => void, + owner: Options, + {} = {} + ) { + this.label = label; + this.owner = owner; + this.onClick = onClick; + this.textContent = textContent; + } + generateHTML(): HTMLDivElement { + const div = document.createElement("div"); + const span = document.createElement("span"); + span.textContent = this.label; + div.append(span); + const button = document.createElement("button"); + button.textContent = this.textContent; + button.onclick = this.onClickEvent.bind(this); + div.append(button); + return div; + } + private onClickEvent() { + this.onClick(); + } + watchForChange() {} + submit() {} +} + +class ColorInput implements OptionsElement { + readonly label: string; + readonly owner: Options; + readonly onSubmit: (str: string) => void; + colorContent: string; + input!: WeakRef; + value!: string; + constructor( + label: string, + onSubmit: (str: string) => void, + owner: Options, + { initColor = "" } = {} + ) { + this.label = label; + this.colorContent = initColor; + this.owner = owner; + this.onSubmit = onSubmit; + } + generateHTML(): HTMLDivElement { + const div = document.createElement("div"); + const span = document.createElement("span"); + span.textContent = this.label; + div.append(span); + const input = document.createElement("input"); + input.value = this.colorContent; + input.type = "color"; + input.oninput = this.onChange.bind(this); + this.input = new WeakRef(input); + div.append(input); + return div; + } + private onChange() { + this.owner.changed(); + const input = this.input.deref(); + if (input) { + const value = input.value as string; + this.value = value; + this.onchange(value); + this.colorContent = value; + } + } + onchange: (str: string) => void = (_) => {}; + watchForChange(func: (str: string) => void) { + this.onchange = func; + } + submit() { + this.onSubmit(this.colorContent); + } +} + +class SelectInput implements OptionsElement { + readonly label: string; + readonly owner: Options; + readonly onSubmit: (str: number) => void; + options: string[]; + index: number; + select!: WeakRef; + get value() { + return this.index; + } + constructor( + label: string, + onSubmit: (str: number) => void, + options: string[], + owner: Options, + { defaultIndex = 0 } = {} + ) { + this.label = label; + this.index = defaultIndex; + this.owner = owner; + this.onSubmit = onSubmit; + this.options = options; + } + generateHTML(): HTMLDivElement { + const div = document.createElement("div"); + const span = document.createElement("span"); + span.textContent = this.label; + div.append(span); + const select = document.createElement("select"); + + select.onchange = this.onChange.bind(this); + for (const thing of this.options) { + const option = document.createElement("option"); + option.textContent = thing; + select.appendChild(option); + } + this.select = new WeakRef(select); + select.selectedIndex = this.index; + div.append(select); + return div; + } + private onChange() { + this.owner.changed(); + const select = this.select.deref(); + if (select) { + const value = select.selectedIndex; + this.onchange(value); + this.index = value; + } + } + onchange: (str: number) => void = (_) => {}; + watchForChange(func: (str: number) => void) { + this.onchange = func; + } + submit() { + this.onSubmit(this.index); + } +} +class MDInput implements OptionsElement { + readonly label: string; + readonly owner: Options; + readonly onSubmit: (str: string) => void; + value: string; + input!: WeakRef; + constructor( + label: string, + onSubmit: (str: string) => void, + owner: Options, + { initText = "" } = {} + ) { + this.label = label; + this.value = initText; + this.owner = owner; + this.onSubmit = onSubmit; + } + generateHTML(): HTMLDivElement { + const div = document.createElement("div"); + const span = document.createElement("span"); + span.textContent = this.label; + div.append(span); + div.append(document.createElement("br")); + const input = document.createElement("textarea"); + input.value = this.value; + input.oninput = this.onChange.bind(this); + this.input = new WeakRef(input); + div.append(input); + return div; + } + onChange() { + this.owner.changed(); + const input = this.input.deref(); + if (input) { + const value = input.value as string; + this.onchange(value); + this.value = value; + } + } + onchange: (str: string) => void = (_) => {}; + watchForChange(func: (str: string) => void) { + this.onchange = func; + } + submit() { + this.onSubmit(this.value); + } +} +class FileInput implements OptionsElement { + readonly label: string; + readonly owner: Options; + readonly onSubmit: (str: FileList | null) => void; + input!: WeakRef; + value!: FileList | null; + clear: boolean; + constructor( + label: string, + onSubmit: (str: FileList | null) => void, + owner: Options, + { clear = false } = {} + ) { + this.label = label; + this.owner = owner; + this.onSubmit = onSubmit; + this.clear = clear; + } + generateHTML(): HTMLDivElement { + const div = document.createElement("div"); + const span = document.createElement("span"); + span.textContent = this.label; + div.append(span); + const input = document.createElement("input"); + input.type = "file"; + input.oninput = this.onChange.bind(this); + this.input = new WeakRef(input); + div.append(input); + if (this.clear) { + const button = document.createElement("button"); + button.textContent = "Clear"; + button.onclick = (_) => { + if (this.onchange) { + this.onchange(null); + } + this.value = null; + this.owner.changed(); + }; + div.append(button); + } + return div; + } + onChange() { + this.owner.changed(); + const input = this.input.deref(); + if (input) { + this.value = input.files; + if (this.onchange) { + this.onchange(input.files); + } + } + } + onchange: ((str: FileList | null) => void) | null = null; + watchForChange(func: (str: FileList | null) => void) { + this.onchange = func; + } + submit() { + const input = this.input.deref(); + if (input) { + this.onSubmit(input.files); + } + } +} + +class HtmlArea implements OptionsElement { + submit: () => void; + html: (() => HTMLElement) | HTMLElement; + value!: void; + constructor(html: (() => HTMLElement) | HTMLElement, submit: () => void) { + this.submit = submit; + this.html = html; + } + generateHTML(): HTMLElement { + if (this.html instanceof Function) { + return this.html(); + } else { + return this.html; + } + } + watchForChange() {} +} +class Options implements OptionsElement { + name: string; + haschanged = false; + readonly options: OptionsElement[]; + readonly owner: Buttons | Options | Form; + readonly ltr: boolean; + value!: void; + readonly html: WeakMap, WeakRef> = + new WeakMap(); + container: WeakRef = new WeakRef( + document.createElement("div") + ); + constructor( + name: string, + owner: Buttons | Options | Form, + { ltr = false } = {} + ) { + this.name = name; + this.options = []; + this.owner = owner; + this.ltr = ltr; + } + removeAll() { + while (this.options.length) { + this.options.pop(); + } + const container = this.container.deref(); + if (container) { + container.innerHTML = ""; + } + } + watchForChange() {} + addOptions(name: string, { ltr = false } = {}) { + const options = new Options(name, this, { ltr }); + this.options.push(options); + this.generate(options); + return options; + } + subOptions: Options | Form | undefined; + addSubOptions(name: string, { ltr = false } = {}) { + const options = new Options(name, this, { ltr }); + this.subOptions = options; + const container = this.container.deref(); + if (container) { + this.generateContainter(); + } else { + throw new Error( + "Tried to make a subOptions when the options weren't rendered" + ); + } + return options; + } + addSubForm( + name: string, + onSubmit: (arg1: object) => void, + { + ltr = false, + submitText = "Submit", + fetchURL = "", + headers = {}, + method = "POST", + traditionalSubmit = false, + } = {} + ) { + const options = new Form(name, this, onSubmit, { + ltr, + submitText, + fetchURL, + headers, + method, + traditionalSubmit, + }); + this.subOptions = options; + const container = this.container.deref(); + if (container) { + this.generateContainter(); + } else { + throw new Error( + "Tried to make a subForm when the options weren't rendered" + ); + } + return options; + } + returnFromSub() { + this.subOptions = undefined; + this.generateContainter(); + } + addSelect( + label: string, + onSubmit: (str: number) => void, + selections: string[], + { defaultIndex = 0 } = {} + ) { + const select = new SelectInput(label, onSubmit, selections, this, { + defaultIndex, + }); + this.options.push(select); + this.generate(select); + return select; + } + addFileInput( + label: string, + onSubmit: (files: FileList | null) => void, + { clear = false } = {} + ) { + const FI = new FileInput(label, onSubmit, this, { clear }); + this.options.push(FI); + this.generate(FI); + return FI; + } + addTextInput( + label: string, + onSubmit: (str: string) => void, + { initText = "", password = false } = {} + ) { + const textInput = new TextInput(label, onSubmit, this, { + initText, + password, + }); + this.options.push(textInput); + this.generate(textInput); + return textInput; + } + addColorInput( + label: string, + onSubmit: (str: string) => void, + { initColor = "" } = {} + ) { + const colorInput = new ColorInput(label, onSubmit, this, { initColor }); + this.options.push(colorInput); + this.generate(colorInput); + return colorInput; + } + addMDInput( + label: string, + onSubmit: (str: string) => void, + { initText = "" } = {} + ) { + const mdInput = new MDInput(label, onSubmit, this, { initText }); + this.options.push(mdInput); + this.generate(mdInput); + return mdInput; + } + addHTMLArea( + html: (() => HTMLElement) | HTMLElement, + submit: () => void = () => {} + ) { + const htmlarea = new HtmlArea(html, submit); + this.options.push(htmlarea); + this.generate(htmlarea); + return htmlarea; + } + addButtonInput(label: string, textContent: string, onSubmit: () => void) { + const button = new ButtonInput(label, textContent, onSubmit, this); + this.options.push(button); + this.generate(button); + return button; + } + addCheckboxInput( + label: string, + onSubmit: (str: boolean) => void, + { initState = false } = {} + ) { + const box = new CheckboxInput(label, onSubmit, this, { initState }); + this.options.push(box); + this.generate(box); + return box; + } + addText(str: string) { + const text = new SettingsText(str); + this.options.push(text); + this.generate(text); + return text; + } + addTitle(str: string) { + const text = new SettingsTitle(str); + this.options.push(text); + this.generate(text); + return text; + } + addForm( + name: string, + onSubmit: (arg1: object) => void, + { + ltr = false, + submitText = "Submit", + fetchURL = "", + headers = {}, + method = "POST", + traditionalSubmit = false, + } = {} + ) { + const options = new Form(name, this, onSubmit, { + ltr, + submitText, + fetchURL, + headers, + method, + traditionalSubmit, + }); + this.options.push(options); + this.generate(options); + return options; + } + generate(elm: OptionsElement) { + const container = this.container.deref(); + if (container) { + const div = document.createElement("div"); + if (!(elm instanceof Options)) { + div.classList.add("optionElement"); + } + const html = elm.generateHTML(); + div.append(html); + this.html.set(elm, new WeakRef(div)); + container.append(div); + } + } + title: WeakRef = new WeakRef(document.createElement("h2")); + generateHTML(): HTMLElement { + const div = document.createElement("div"); + div.classList.add("titlediv"); + const title = document.createElement("h2"); + title.textContent = this.name; + div.append(title); + if (this.name !== "") title.classList.add("settingstitle"); + this.title = new WeakRef(title); + const container = document.createElement("div"); + this.container = new WeakRef(container); + container.classList.add(this.ltr ? "flexltr" : "flexttb", "flexspace"); + this.generateContainter(); + div.append(container); + return div; + } + generateContainter() { + const container = this.container.deref(); + if (container) { + const title = this.title.deref(); + if (title) title.innerHTML = ""; + container.innerHTML = ""; + if (this.subOptions) { + container.append(this.subOptions.generateHTML()); //more code needed, though this is enough for now + if (title) { + const name = document.createElement("span"); + name.innerText = this.name; + name.classList.add("clickable"); + name.onclick = () => { + this.returnFromSub(); + }; + title.append(name, " > ", this.subOptions.name); + } + } else { + for (const thing of this.options) { + this.generate(thing); + } + if (title) { + title.innerText = this.name; + } + } + if (title && title.innerText !== "") { + title.classList.add("settingstitle"); + } else if (title) { + title.classList.remove("settingstitle"); + } + } else { + console.warn("tried to generate container, but it did not exist"); + } + } + changed() { + if (this.owner instanceof Options || this.owner instanceof Form) { + this.owner.changed(); + return; + } + if (!this.haschanged) { + const div = document.createElement("div"); + div.classList.add("flexltr", "savediv"); + const span = document.createElement("span"); + div.append(span); + span.textContent = "Careful, you have unsaved changes"; + const button = document.createElement("button"); + button.textContent = "Save changes"; + div.append(button); + this.haschanged = true; + this.owner.changed(div); + + button.onclick = (_) => { + if (this.owner instanceof Buttons) { + this.owner.save(); + } + div.remove(); + this.submit(); + }; + } + } + submit() { + this.haschanged = false; + for (const thing of this.options) { + thing.submit(); + } + } +} +class FormError extends Error { + elem: OptionsElement; + message: string; + constructor(elem: OptionsElement, message: string) { + super(message); + this.message = message; + this.elem = elem; + } +} +export { FormError }; +class Form implements OptionsElement { + name: string; + readonly options: Options; + readonly owner: Options; + readonly ltr: boolean; + readonly names: Map> = new Map(); + readonly required: WeakSet> = new WeakSet(); + readonly submitText: string; + readonly fetchURL: string; + readonly headers = {}; + readonly method: string; + value!: object; + traditionalSubmit: boolean; + values: { [key: string]: any } = {}; + constructor( + name: string, + owner: Options, + onSubmit: (arg1: object) => void, + { + ltr = false, + submitText = "Submit", + fetchURL = "", + headers = {}, + method = "POST", + traditionalSubmit = false, + } = {} + ) { + this.traditionalSubmit = traditionalSubmit; + this.name = name; + this.method = method; + this.submitText = submitText; + this.options = new Options("", this, { ltr }); + this.owner = owner; + this.fetchURL = fetchURL; + this.headers = headers; + this.ltr = ltr; + this.onSubmit = onSubmit; + } + setValue(key: string, value: any) { + //the value can't really be anything, but I don't care enough to fix this + this.values[key] = value; + } + addSelect( + label: string, + formName: string, + selections: string[], + { defaultIndex = 0, required = false } = {} + ) { + const select = this.options.addSelect(label, (_) => {}, selections, { + defaultIndex, + }); + this.names.set(formName, select); + if (required) { + this.required.add(select); + } + return select; + } + readonly fileOptions: Map = new Map(); + addFileInput( + label: string, + formName: string, + { required = false, files = "one", clear = false } = {} + ) { + const FI = this.options.addFileInput(label, (_) => {}, { clear }); + if (files !== "one" && files !== "multi") + throw new Error("files should equal one or multi"); + this.fileOptions.set(FI, { files }); + this.names.set(formName, FI); + if (required) { + this.required.add(FI); + } + return FI; + } + + addTextInput( + label: string, + formName: string, + { initText = "", required = false, password = false } = {} + ) { + const textInput = this.options.addTextInput(label, (_) => {}, { + initText, + password, + }); + this.names.set(formName, textInput); + if (required) { + this.required.add(textInput); + } + return textInput; + } + addColorInput( + label: string, + formName: string, + { initColor = "", required = false } = {} + ) { + const colorInput = this.options.addColorInput(label, (_) => {}, { + initColor, + }); + this.names.set(formName, colorInput); + if (required) { + this.required.add(colorInput); + } + return colorInput; + } + + addMDInput( + label: string, + formName: string, + { initText = "", required = false } = {} + ) { + const mdInput = this.options.addMDInput(label, (_) => {}, { initText }); + this.names.set(formName, mdInput); + if (required) { + this.required.add(mdInput); + } + return mdInput; + } + + addCheckboxInput( + label: string, + formName: string, + { initState = false, required = false } = {} + ) { + const box = this.options.addCheckboxInput(label, (_) => {}, { initState }); + this.names.set(formName, box); + if (required) { + this.required.add(box); + } + return box; + } + addText(str: string) { + this.options.addText(str); + } + addTitle(str: string) { + this.options.addTitle(str); + } + generateHTML(): HTMLElement { + const div = document.createElement("div"); + div.append(this.options.generateHTML()); + div.classList.add("FormSettings"); + if (!this.traditionalSubmit) { + const button = document.createElement("button"); + button.onclick = (_) => { + this.submit(); + }; + button.textContent = this.submitText; + div.append(button); + } + return div; + } + onSubmit: (arg1: object) => void; + watchForChange(func: (arg1: object) => void) { + this.onSubmit = func; + } + changed() { + if (this.traditionalSubmit) { + this.owner.changed(); + } + } + async submit() { + const build = {}; + for (const key of Object.keys(this.values)) { + const thing = this.values[key]; + if (thing instanceof Function) { + try { + (build as any)[key] = thing(); + } catch (e: any) { + if (e instanceof FormError) { + const elm = this.options.html.get(e.elem); + if (elm) { + const html = elm.deref(); + if (html) { + this.makeError(html, e.message); + } + } + } + return; + } + } else { + (build as any)[thing] = thing; + } + } + const promises: Promise[] = []; + for (const thing of this.names.keys()) { + if (thing === "") continue; + const input = this.names.get(thing) as OptionsElement; + if (input instanceof SelectInput) { + (build as any)[thing] = input.options[input.value]; + continue; + } else if (input instanceof FileInput) { + const options = this.fileOptions.get(input); + if (!options) { + throw new Error( + "FileInput without its options is in this form, this should never happen." + ); + } + if (options.files === "one") { + console.log(input.value); + if (input.value) { + const reader = new FileReader(); + reader.readAsDataURL(input.value[0]); + const promise = new Promise((res) => { + reader.onload = () => { + (build as any)[thing] = reader.result; + res(); + }; + }); + promises.push(promise); + } + } else { + console.error(options.files + " is not currently implemented"); + } + } + (build as any)[thing] = input.value; + } + await Promise.allSettled(promises); + if (this.fetchURL !== "") { + fetch(this.fetchURL, { + method: this.method, + body: JSON.stringify(build), + headers: this.headers, + }) + .then((_) => _.json()) + .then((json) => { + if (json.errors && this.errors(json.errors)) return; + this.onSubmit(json); + }); + } else { + this.onSubmit(build); + } + console.warn("needs to be implemented"); + } + errors(errors: { + code: number; + message: string; + errors: { [key: string]: { _errors: { message: string; code: string } } }; + }) { + if (!(errors instanceof Object)) { + return; + } + for (const error of Object.keys(errors)) { + const elm = this.names.get(error); + if (elm) { + const ref = this.options.html.get(elm); + if (ref && ref.deref()) { + const html = ref.deref() as HTMLDivElement; + this.makeError(html, errors["errors"][error]._errors.message); + return true; + } + } + } + return false; + } + error(formElm: string, errorMessage: string) { + const elm = this.names.get(formElm); + if (elm) { + const htmlref = this.options.html.get(elm); + if (htmlref) { + const html = htmlref.deref(); + if (html) { + this.makeError(html, errorMessage); + } + } + } else { + console.warn(formElm + " is not a valid form property"); + } + } + makeError(e: HTMLDivElement, message: string) { + let element = e.getElementsByClassName("suberror")[0] as HTMLElement; + if (!element) { + const div = document.createElement("div"); + div.classList.add("suberror", "suberrora"); + e.append(div); + element = div; + } else { + element.classList.remove("suberror"); + setTimeout((_) => { + element.classList.add("suberror"); + }, 100); + } + element.textContent = message; + } +} +class Settings extends Buttons { + static readonly Buttons = Buttons; + static readonly Options = Options; + html!: HTMLElement | null; + constructor(name: string) { + super(name); + } + addButton(name: string, { ltr = false } = {}): Options { + const options = new Options(name, this, { ltr }); + this.add(name, options); + return options; + } + show() { + const background = document.createElement("div"); + background.classList.add("background"); + + const title = document.createElement("h2"); + title.textContent = this.name; + title.classList.add("settingstitle"); + background.append(title); + + background.append(this.generateHTML()); + + const exit = document.createElement("span"); + exit.textContent = "✖"; + exit.classList.add("exitsettings"); + background.append(exit); + exit.onclick = (_) => { + this.hide(); + }; + document.body.append(background); + this.html = background; + } + hide() { + if (this.html) { + this.html.remove(); + this.html = null; + } + } +} + +export { Settings, OptionsElement, Buttons, Options }; diff --git a/src/webpage/snowflake.ts b/src/webpage/snowflake.ts new file mode 100644 index 0000000..516ec67 --- /dev/null +++ b/src/webpage/snowflake.ts @@ -0,0 +1,20 @@ +abstract class SnowFlake { + public readonly id: string; + constructor(id: string) { + this.id = id; + } + getUnixTime(): number { + return SnowFlake.stringToUnixTime(this.id); + } + static stringToUnixTime(str: string) { + try { + return Number((BigInt(str) >> 22n) + 1420070400000n); + } catch { + console.error( + `The ID is corrupted, it's ${str} when it should be some number.` + ); + return 0; + } + } +} +export { SnowFlake }; diff --git a/webpage/style.css b/src/webpage/style.css similarity index 100% rename from webpage/style.css rename to src/webpage/style.css diff --git a/webpage/themes.css b/src/webpage/themes.css similarity index 100% rename from webpage/themes.css rename to src/webpage/themes.css diff --git a/src/webpage/user.ts b/src/webpage/user.ts new file mode 100644 index 0000000..d3fcff4 --- /dev/null +++ b/src/webpage/user.ts @@ -0,0 +1,489 @@ +import { Member } from "./member.js"; +import { MarkDown } from "./markdown.js"; +import { Contextmenu } from "./contextmenu.js"; +import { Localuser } from "./localuser.js"; +import { Guild } from "./guild.js"; +import { SnowFlake } from "./snowflake.js"; +import { presencejson, userjson } from "./jsontypes.js"; + +class User extends SnowFlake { + owner: Localuser; + hypotheticalpfp!: boolean; + avatar!: string | null; + username!: string; + nickname: string | null = null; + relationshipType: 0 | 1 | 2 | 3 | 4 = 0; + bio!: MarkDown; + discriminator!: string; + pronouns!: string; + bot!: boolean; + public_flags!: number; + accent_color!: number; + banner: string | undefined; + hypotheticalbanner!: boolean; + premium_since!: string; + premium_type!: number; + theme_colors!: string; + badge_ids!: string[]; + members: WeakMap> = + new WeakMap(); + private status!: string; + resolving: false | Promise = false; + + constructor(userjson: userjson, owner: Localuser, dontclone = false) { + super(userjson.id); + this.owner = owner; + if (!owner) { + console.error("missing localuser"); + } + if (dontclone) { + for (const key of Object.keys(userjson)) { + if (key === "bio") { + this.bio = new MarkDown(userjson[key], this.localuser); + continue; + } + if (key === "id") { + continue; + } + (this as any)[key] = (userjson as any)[key]; + } + this.hypotheticalpfp = false; + } else { + return User.checkuser(userjson, owner); + } + } + + clone(): User { + return new User( + { + username: this.username, + id: this.id + "#clone", + public_flags: this.public_flags, + discriminator: this.discriminator, + avatar: this.avatar, + accent_color: this.accent_color, + banner: this.banner, + bio: this.bio.rawString, + premium_since: this.premium_since, + premium_type: this.premium_type, + bot: this.bot, + theme_colors: this.theme_colors, + pronouns: this.pronouns, + badge_ids: this.badge_ids, + }, + this.owner + ); + } + + public getPresence(presence: presencejson | undefined): void { + if (presence) { + this.setstatus(presence.status); + } else { + this.setstatus("offline"); + } + } + + setstatus(status: string): void { + this.status = status; + } + + async getStatus(): Promise { + return this.status || "offline"; + } + + static contextmenu = new Contextmenu("User Menu"); + + static setUpContextMenu(): void { + this.contextmenu.addbutton("Copy user id", function (this: User) { + navigator.clipboard.writeText(this.id); + }); + this.contextmenu.addbutton("Message user", function (this: User) { + fetch(this.info.api + "/users/@me/channels", { + method: "POST", + body: JSON.stringify({ recipients: [this.id] }), + headers: this.localuser.headers, + }) + .then((res) => res.json()) + .then((json) => { + this.localuser.goToChannel(json.id); + }); + }); + this.contextmenu.addbutton( + "Block user", + function (this: User) { + this.block(); + }, + null, + function () { + return this.relationshipType !== 2; + } + ); + + this.contextmenu.addbutton( + "Unblock user", + function (this: User) { + this.unblock(); + }, + null, + function () { + return this.relationshipType === 2; + } + ); + this.contextmenu.addbutton("Friend request", function (this: User) { + fetch(`${this.info.api}/users/@me/relationships/${this.id}`, { + method: "PUT", + headers: this.owner.headers, + body: JSON.stringify({ + type: 1, + }), + }); + }); + this.contextmenu.addbutton( + "Kick member", + function (this: User, member: Member | undefined) { + member?.kick(); + }, + null, + (member) => { + if (!member) return false; + const us = member.guild.member; + if (member.id === us.id) { + return false; + } + if (member.id === member.guild.properties.owner_id) { + return false; + } + return us.hasPermission("KICK_MEMBERS") || false; + } + ); + this.contextmenu.addbutton( + "Ban member", + function (this: User, member: Member | undefined) { + member?.ban(); + }, + null, + (member) => { + if (!member) return false; + const us = member.guild.member; + if (member.id === us.id) { + return false; + } + if (member.id === member.guild.properties.owner_id) { + return false; + } + return us.hasPermission("BAN_MEMBERS") || false; + } + ); + } + + static checkuser(user: User | userjson, owner: Localuser): User { + if (owner.userMap.has(user.id)) { + return owner.userMap.get(user.id) as User; + } else { + const tempuser = new User(user as userjson, owner, true); + owner.userMap.set(user.id, tempuser); + return tempuser; + } + } + + get info() { + return this.owner.info; + } + + get localuser() { + return this.owner; + } + + get name() { + return this.username; + } + + async resolvemember(guild: Guild): Promise { + return await Member.resolveMember(this, guild); + } + + async getUserProfile(): Promise { + return await fetch( + `${this.info.api}/users/${this.id.replace( + "#clone", + "" + )}/profile?with_mutual_guilds=true&with_mutual_friends=true`, + { + headers: this.localuser.headers, + } + ).then((res) => res.json()); + } + + async getBadge(id: string): Promise { + if (this.localuser.badges.has(id)) { + return this.localuser.badges.get(id); + } else { + if (this.resolving) { + await this.resolving; + return this.localuser.badges.get(id); + } + + const prom = await this.getUserProfile(); + this.resolving = prom; + const badges = prom.badges; + this.resolving = false; + for (const badge of badges) { + this.localuser.badges.set(badge.id, badge); + } + return this.localuser.badges.get(id); + } + } + + buildpfp(): HTMLImageElement { + const pfp = document.createElement("img"); + pfp.loading = "lazy"; + pfp.src = this.getpfpsrc(); + pfp.classList.add("pfp"); + pfp.classList.add("userid:" + this.id); + return pfp; + } + + async buildstatuspfp(): Promise { + const div = document.createElement("div"); + div.style.position = "relative"; + const pfp = this.buildpfp(); + div.append(pfp); + const status = document.createElement("div"); + status.classList.add("statusDiv"); + switch (await this.getStatus()) { + case "offline": + status.classList.add("offlinestatus"); + break; + case "online": + default: + status.classList.add("onlinestatus"); + break; + } + div.append(status); + return div; + } + + userupdate(json: userjson): void { + if (json.avatar !== this.avatar) { + this.changepfp(json.avatar); + } + } + + bind(html: HTMLElement, guild: Guild | null = null, error = true): void { + if (guild && guild.id !== "@me") { + Member.resolveMember(this, guild) + .then((member) => { + User.contextmenu.bindContextmenu(html, this, member); + if (member === undefined && error) { + const errorSpan = document.createElement("span"); + errorSpan.textContent = "!"; + errorSpan.classList.add("membererror"); + html.after(errorSpan); + return; + } + if (member) { + member.bind(html); + } + }) + .catch((err) => { + console.log(err); + }); + } + if (guild) { + this.profileclick(html, guild); + } else { + this.profileclick(html); + } + } + + static async resolve(id: string, localuser: Localuser): Promise { + const json = await fetch( + localuser.info.api.toString() + "/users/" + id + "/profile", + { headers: localuser.headers } + ).then((res) => res.json()); + return new User(json, localuser); + } + + changepfp(update: string | null): void { + this.avatar = update; + this.hypotheticalpfp = false; + const src = this.getpfpsrc(); + Array.from(document.getElementsByClassName("userid:" + this.id)).forEach( + (element) => { + (element as HTMLImageElement).src = src; + } + ); + } + + block(): void { + fetch(`${this.info.api}/users/@me/relationships/${this.id}`, { + method: "PUT", + headers: this.owner.headers, + body: JSON.stringify({ + type: 2, + }), + }); + this.relationshipType = 2; + const channel = this.localuser.channelfocus; + if (channel) { + for (const message of channel.messages) { + message[1].generateMessage(); + } + } + } + + unblock(): void { + fetch(`${this.info.api}/users/@me/relationships/${this.id}`, { + method: "DELETE", + headers: this.owner.headers, + }); + this.relationshipType = 0; + const channel = this.localuser.channelfocus; + if (channel) { + for (const message of channel.messages) { + message[1].generateMessage(); + } + } + } + + getpfpsrc(): string { + if (this.hypotheticalpfp && this.avatar) { + return this.avatar; + } + if (this.avatar !== null) { + return `${this.info.cdn}/avatars/${this.id.replace("#clone", "")}/${ + this.avatar + }.png`; + } else { + const int = Number((BigInt(this.id.replace("#clone", "")) >> 22n) % 6n); + return `${this.info.cdn}/embed/avatars/${int}.png`; + } + } + + async buildprofile( + x: number, + y: number, + guild: Guild | null = null + ): Promise { + if (Contextmenu.currentmenu != "") { + Contextmenu.currentmenu.remove(); + } + + const div = document.createElement("div"); + + if (this.accent_color) { + div.style.setProperty( + "--accent_color", + `#${this.accent_color.toString(16).padStart(6, "0")}` + ); + } else { + div.style.setProperty("--accent_color", "transparent"); + } + if (this.banner) { + const banner = document.createElement("img"); + let src: string; + if (!this.hypotheticalbanner) { + src = `${this.info.cdn}/avatars/${this.id.replace("#clone", "")}/${ + this.banner + }.png`; + } else { + src = this.banner; + } + banner.src = src; + banner.classList.add("banner"); + div.append(banner); + } + if (x !== -1) { + div.style.left = `${x}px`; + div.style.top = `${y}px`; + div.classList.add("profile", "flexttb"); + } else { + this.setstatus("online"); + div.classList.add("hypoprofile", "flexttb"); + } + const badgediv = document.createElement("div"); + badgediv.classList.add("badges"); + (async () => { + if (!this.badge_ids) return; + for (const id of this.badge_ids) { + const badgejson = await this.getBadge(id); + if (badgejson) { + const badge = document.createElement(badgejson.link ? "a" : "div"); + badge.classList.add("badge"); + const img = document.createElement("img"); + img.src = badgejson.icon; + badge.append(img); + const span = document.createElement("span"); + span.textContent = badgejson.description; + badge.append(span); + if (badge instanceof HTMLAnchorElement) { + badge.href = badgejson.link; + } + badgediv.append(badge); + } + } + })(); + const pfp = await this.buildstatuspfp(); + div.appendChild(pfp); + const userbody = document.createElement("div"); + userbody.classList.add("infosection"); + div.appendChild(userbody); + const usernamehtml = document.createElement("h2"); + usernamehtml.textContent = this.username; + userbody.appendChild(usernamehtml); + userbody.appendChild(badgediv); + const discrimatorhtml = document.createElement("h3"); + discrimatorhtml.classList.add("tag"); + discrimatorhtml.textContent = `${this.username}#${this.discriminator}`; + userbody.appendChild(discrimatorhtml); + + const pronounshtml = document.createElement("p"); + pronounshtml.textContent = this.pronouns; + pronounshtml.classList.add("pronouns"); + userbody.appendChild(pronounshtml); + + const rule = document.createElement("hr"); + userbody.appendChild(rule); + const biohtml = this.bio.makeHTML(); + userbody.appendChild(biohtml); + if (guild) { + Member.resolveMember(this, guild).then((member) => { + if (!member) return; + const roles = document.createElement("div"); + roles.classList.add("rolesbox"); + for (const role of member.roles) { + const roleDiv = document.createElement("div"); + roleDiv.classList.add("rolediv"); + const color = document.createElement("div"); + roleDiv.append(color); + color.style.setProperty( + "--role-color", + `#${role.color.toString(16).padStart(6, "0")}` + ); + color.classList.add("colorrolediv"); + const span = document.createElement("span"); + roleDiv.append(span); + span.textContent = role.name; + roles.append(roleDiv); + } + userbody.append(roles); + }); + } + if (x !== -1) { + Contextmenu.currentmenu = div; + document.body.appendChild(div); + Contextmenu.keepOnScreen(div); + } + return div; + } + + profileclick(obj: HTMLElement, guild?: Guild): void { + obj.onclick = (e: MouseEvent) => { + this.buildprofile(e.clientX, e.clientY, guild); + e.stopPropagation(); + }; + } +} + +User.setUpContextMenu(); +export { User }; diff --git a/stats.js b/stats.js deleted file mode 100644 index 1ccf0fe..0000000 --- a/stats.js +++ /dev/null @@ -1,179 +0,0 @@ -const index = require("./index.js"); -const fs=require("node:fs"); -let uptimeObject={}; -if(fs.existsSync(__dirname+"/uptime.json")){ - try{ - uptimeObject=JSON.parse(fs.readFileSync(__dirname+"/uptime.json", "utf8")); - }catch{ - uptimeObject={}; - } -} -if(uptimeObject.undefined){ - delete uptimeObject.undefined; - updatejson(); -} -async function observe(instances){ - const active=new Set(); - async function resolveinstance(instance){ - try{ - calcStats(instance); - }catch(e){ - console.error(e); - } - let api; - if(instance.urls){ - api=instance.urls.api; - }else if(instance.url){ - const urls=await index.getapiurls(instance.url); - if(urls){ - api=urls.api; - } - } - if(!api||api===""){ - setStatus(instance,false); - console.warn(instance.name+" does not resolve api URL",instance); - setTimeout(_=>{ - resolveinstance(instance); - },1000*60*30,); - return; - } - active.add(instance.name); - api+=api.endsWith("/")?"":"/"; - async function check(tries=0){ - try{ - const req=await fetch(api+"ping",{method: "HEAD"}) - if(tries>3||req.ok){ - setStatus(instance,req.ok); - }else{ - setTimeout(()=>{ - check(tries+1); - },30000) - } - }catch{ - if(tries>3){ - setStatus(instance,false); - }else{ - setTimeout(()=>{ - check(tries+1); - },30000) - } - } - } - setTimeout( - _=>{ - check(); - setInterval(_=>{ - check(); - },1000*60*30); - },Math.random()*1000*60*10 - ); - } - const promlist=[]; - for(const instance of instances){ - promlist.push(resolveinstance(instance)); - } - await Promise.allSettled(promlist); - for(const key of Object.keys(uptimeObject)){ - if(!active.has(key)){ - setStatus(key,false); - } - } -} -function calcStats(instance){ - const obj=uptimeObject[instance.name]; - if(!obj)return; - const day=Date.now()-1000*60*60*24; - const week=Date.now()-1000*60*60*24*7; - let alltime=-1; - let totalTimePassed=0; - let daytime=-1; - let weektime=-1; - let online=false; - let i=0; - for(const thing of obj){ - online=thing.online; - let stamp=thing.time; - if(alltime===-1){ - alltime=0; - } - let timepassed; - if(obj[i+1]){ - timepassed=obj[i+1].time-stamp; - }else{ - timepassed=Date.now()-stamp; - } - totalTimePassed+=timepassed; - alltime+=online*timepassed; - if(stamp+timepassed>week){ - if(stampday){ - if(stamp1000*60*60*24){ - if(daytime===-1){ - daytime=online*1000*60*60*24; - } - daytime/=1000*60*60*24; - if(totalTimePassed>1000*60*60*24*7){ - if(weektime===-1){ - weektime=online*1000*60*60*24*7; - } - weektime/=1000*60*60*24*7; - }else{ - weektime=alltime; - } - }else{ - weektime=alltime; - daytime=alltime; - } - instance.uptime={daytime,weektime,alltime}; -} -/** - * @param {string|Object} instance - * @param {boolean} status - */ -function setStatus(instance,status){ - let name=instance.name; - if(typeof instance==="string"){ - name=instance; - } - - let obj=uptimeObject[name]; - let needSetting=false; - if(!obj){ - obj=[]; - uptimeObject[name]=obj; - needSetting=true; - }else{ - if(obj.at(-1).online!==status){ - needSetting=true; - } - } - if(needSetting){ - obj.push({time: Date.now(),online: status}); - updatejson(); - } - if(typeof instance!=="string"){ - calcStats(instance); - } -} -function updatejson(){ - fs.writeFile(__dirname+"/uptime.json",JSON.stringify(uptimeObject),_=>{}); -} -exports.observe=observe; -exports.uptime=uptimeObject; diff --git a/tsconfig.json b/tsconfig.json index e59ead1..416f654 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,20 +1,39 @@ - { +{ "compilerOptions": { - "target": "es2022", - "moduleResolution": "Bundler", - "module":"es2022", - "strict": false, + "allowSyntheticDefaultImports": true, + "alwaysStrict": true, + "declaration": true, + "declarationMap": true, "esModuleInterop": true, - "outDir": "./.dist", + "importHelpers": false, + "incremental": true, + "lib": [ + "esnext", + "DOM" + ], + "module": "ES2022", + "moduleResolution": "Bundler", + "newLine": "lf", + "noEmitHelpers": false, + "noFallthroughCasesInSwitch": true, + "noImplicitReturns": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "preserveConstEnums": true, + "pretty": true, "removeComments": false, - "noImplicitThis":true, - "useUnknownInCatchVariables":true, - "strictNullChecks":true + "resolveJsonModule": true, + "sourceMap": true, + "strict": true, + "target": "ES2022", + "useDefineForClassFields": true, + "resolvePackageJsonImports": true, + "outDir": "./dist", }, "include": [ - "./webpage/*.ts" + "src/**/*.ts" ], "exclude": [ "node_modules" ] -} +} \ No newline at end of file diff --git a/webpage/audio.ts b/webpage/audio.ts deleted file mode 100644 index 2b259ab..0000000 --- a/webpage/audio.ts +++ /dev/null @@ -1,164 +0,0 @@ -import{getBulkInfo}from"./login.js"; - -class Voice{ - audioCtx:AudioContext; - info:{wave:string|Function,freq:number}; - playing:boolean; - myArrayBuffer:AudioBuffer; - gainNode:GainNode; - buffer:Float32Array; - source:AudioBufferSourceNode; - constructor(wave:string|Function,freq:number,volume=1){ - this.audioCtx = new (window.AudioContext)(); - this.info={wave,freq}; - this.playing=false; - this.myArrayBuffer=this.audioCtx.createBuffer( - 1, - this.audioCtx.sampleRate, - this.audioCtx.sampleRate, - ); - this.gainNode = this.audioCtx.createGain(); - this.gainNode.gain.value=volume; - this.gainNode.connect(this.audioCtx.destination); - this.buffer=this.myArrayBuffer.getChannelData(0); - this.source = this.audioCtx.createBufferSource(); - this.source.buffer = this.myArrayBuffer; - this.source.loop=true; - this.source.start(); - this.updateWave(); - } - get wave():string|Function{ - return this.info.wave; - } - get freq():number{ - return this.info.freq; - } - set wave(wave:string|Function){ - this.info.wave=wave; - this.updateWave(); - } - set freq(freq:number){ - this.info.freq=freq; - this.updateWave(); - } - updateWave():void{ - const func=this.waveFunction(); - for(let i = 0; i < this.buffer.length; i++){ - this.buffer[i]=func(i/this.audioCtx.sampleRate,this.freq); - } - } - waveFunction():Function{ - if(typeof this.wave === "function"){ - return this.wave; - } - switch(this.wave){ - case"sin": - return(t:number,freq:number)=>{ - return Math.sin(t*Math.PI*2*freq); - }; - case"triangle": - return(t:number,freq:number)=>{ - return Math.abs((4*t*freq)%4-2)-1; - }; - case"sawtooth": - return(t:number,freq:number)=>{ - return((t*freq)%1)*2-1; - }; - case"square": - return(t:number,freq:number)=>{ - return(t*freq)%2<1?1:-1; - }; - case"white": - return(_t:number,_freq:number)=>{ - return Math.random()*2-1; - }; - case"noise": - return(_t:number,_freq:number)=>{ - return 0; - }; - } - return new Function(); - } - play():void{ - if(this.playing){ - return; - } - this.source.connect(this.gainNode); - this.playing=true; - } - stop():void{ - if(this.playing){ - this.source.disconnect(); - this.playing=false; - } - } - static noises(noise:string):void{ - switch(noise){ - case"three":{ - const voicy=new Voice("sin",800); - voicy.play(); - setTimeout(_=>{ - voicy.freq=1000; - },50); - setTimeout(_=>{ - voicy.freq=1300; - },100); - setTimeout(_=>{ - voicy.stop(); - },150); - break; - } - case"zip":{ - const voicy=new Voice((t:number,freq:number)=>{ - return Math.sin(((t+2)**(Math.cos(t*4)))*Math.PI*2*freq); - },700); - voicy.play(); - setTimeout(_=>{ - voicy.stop(); - },150); - break; - } - case"square":{ - const voicy=new Voice("square",600,0.4); - voicy.play(); - setTimeout(_=>{ - voicy.freq=800; - },50); - setTimeout(_=>{ - voicy.freq=1000; - },100); - setTimeout(_=>{ - voicy.stop(); - },150); - break; - } - case"beep":{ - const voicy=new Voice("sin",800); - voicy.play(); - setTimeout(_=>{ - voicy.stop(); - },50); - setTimeout(_=>{ - voicy.play(); - },100); - setTimeout(_=>{ - voicy.stop(); - },150); - break; - } - } - } - static get sounds(){ - return["three","zip","square","beep"]; - } - static setNotificationSound(sound:string){ - const userinfos=getBulkInfo(); - userinfos.preferences.notisound=sound; - localStorage.setItem("userinfos",JSON.stringify(userinfos)); - } - static getNotificationSound(){ - const userinfos=getBulkInfo(); - return userinfos.preferences.notisound; - } -} -export{Voice}; diff --git a/webpage/channel.ts b/webpage/channel.ts deleted file mode 100644 index 2e61250..0000000 --- a/webpage/channel.ts +++ /dev/null @@ -1,1204 +0,0 @@ -"use strict"; -import{ Message }from"./message.js"; -import{Voice}from"./audio.js"; -import{Contextmenu}from"./contextmenu.js"; -import{Dialog}from"./dialog.js"; -import{Guild}from"./guild.js"; -import{ Localuser }from"./localuser.js"; -import{ Permissions }from"./permissions.js"; -import{ Settings }from"./settings.js"; -import{ Role,RoleList }from"./role.js"; -import{InfiniteScroller}from"./infiniteScroller.js"; -import{ SnowFlake }from"./snowflake.js"; -import{ channeljson, messageCreateJson, messagejson, readyjson }from"./jsontypes.js"; -import{ MarkDown }from"./markdown.js"; -import { Member } from "./member.js"; - -declare global { - interface NotificationOptions { - image?: string|null|undefined - } -} -class Channel extends SnowFlake{ - editing:Message|null; - type:number; - owner:Guild; - headers:Localuser["headers"]; - name:string; - parent_id?:string; - parent:Channel|undefined; - children:Channel[]; - guild_id:string; - permission_overwrites:Map; - permission_overwritesar:[Role,Permissions][]; - topic:string; - nsfw:boolean; - position:number=0; - lastreadmessageid:string|undefined; - lastmessageid:string|undefined; - mentions:number; - lastpin:string; - move_id?:string; - typing:number; - message_notifications:number; - allthewayup:boolean; - static contextmenu=new Contextmenu("channel menu"); - replyingto:Message|null; - infinite:InfiniteScroller; - idToPrev:Map=new Map(); - idToNext:Map=new Map(); - messages:Map=new Map(); - static setupcontextmenu(){ - this.contextmenu.addbutton("Copy channel id",function(this:Channel){ - navigator.clipboard.writeText(this.id); - }); - - this.contextmenu.addbutton("Mark as read",function(this:Channel){ - this.readbottom(); - }); - - this.contextmenu.addbutton("Settings[temp]",function(this:Channel){ - this.generateSettings(); - }); - - this.contextmenu.addbutton("Delete channel",function(this:Channel){ - this.deleteChannel(); - },null,function(){ - return this.isAdmin(); - }); - - this.contextmenu.addbutton("Edit channel",function(this:Channel){ - this.editChannel(); - },null,function(){ - return this.isAdmin(); - }); - - this.contextmenu.addbutton("Make invite",function(this:Channel){ - this.createInvite(); - },null,function(){ - return this.hasPermission("CREATE_INSTANT_INVITE")&&this.type!==4; - }); - /* - this.contextmenu.addbutton("Test button",function(){ - this.localuser.ws.send(JSON.stringify({ - "op": 14, - "d": { - "guild_id": this.guild.id, - "channels": { - [this.id]: [ - [ - 0, - 99 - ] - ] - } - } - })) - },null); - /**/ - } - createInvite(){ - const div=document.createElement("div"); - div.classList.add("invitediv"); - const text=document.createElement("span"); - div.append(text); - let uses=0; - let expires=1800; - const copycontainer=document.createElement("div"); - copycontainer.classList.add("copycontainer"); - const copy=document.createElement("span"); - copy.classList.add("copybutton","svgtheme","svg-copy"); - copycontainer.append(copy); - copycontainer.onclick=_=>{ - if(text.textContent){ - navigator.clipboard.writeText(text.textContent); - } - }; - div.append(copycontainer); - const update=()=>{ - fetch(`${this.info.api}/channels/${this.id}/invites`,{ - method: "POST", - headers: this.headers, - body: JSON.stringify({ - flags: 0, - target_type: null, - target_user_id: null, - max_age: expires+"", - max_uses: uses, - temporary: uses!==0 - }) - }).then(_=>_.json()).then(json=>{ - const params=new URLSearchParams(""); - params.set("instance",this.info.wellknown); - const encoded=params.toString(); - text.textContent=`${location.origin}/invite/${json.code}?${encoded}`; - }); - }; - update(); - new Dialog(["vdiv", - ["title","Invite people"], - ["text",`to #${this.name} in ${this.guild.properties.name}`], - ["select","Expire after:",["30 Minutes","1 Hour","6 Hours","12 Hours","1 Day","7 Days","30 Days","Never"],function(e){ - expires=[1800,3600,21600,43200,86400,604800,2592000,0][e.srcElement.selectedIndex]; - update(); - },0], - ["select","Max uses:",["No limit","1 use","5 uses","10 uses","25 uses","50 uses","100 uses"],function(e){ - uses=[0,1,5,10,25,50,100][e.srcElement.selectedIndex]; - update(); - },0], - ["html",div] - ]).show(); - } - generateSettings(){ - this.sortPerms(); - const settings=new Settings("Settings for "+this.name); - - const s1=settings.addButton("roles"); - - s1.options.push(new RoleList(this.permission_overwritesar,this.guild,this.updateRolePermissions.bind(this),true)); - settings.show(); - } - sortPerms(){ - this.permission_overwritesar.sort((a,b)=>{ - return this.guild.roles.findIndex(_=>_===a[0])-this.guild.roles.findIndex(_=>_===b[0]); - }); - } - setUpInfiniteScroller(){ - this.infinite=new InfiniteScroller((async (id:string,offset:number):Promise=>{ - if(offset===1){ - if(this.idToPrev.has(id)){ - return this.idToPrev.get(id); - }else{ - await this.grabBefore(id); - return this.idToPrev.get(id); - } - }else{ - if(this.idToNext.has(id)){ - return this.idToNext.get(id); - }else if(this.lastmessage?.id!==id){ - await this.grabAfter(id); - return this.idToNext.get(id); - }else{ - } - } - }), - (async (id:string):Promise=>{ - //await new Promise(_=>{setTimeout(_,Math.random()*10)}) - const messgage=this.messages.get(id); - try{ - if(messgage){ - return messgage.buildhtml(); - }else{ - console.error(id+" not found"); - } - }catch(e){ - console.error(e); - } - return document.createElement("div"); - }), - (async (id:string)=>{ - const message=this.messages.get(id); - try{ - if(message){ - message.deleteDiv(); - return true; - } - }catch(e){ - console.error(e); - }finally{} - return false; - }), - this.readbottom.bind(this) - ); - } - constructor(json:channeljson|-1,owner:Guild,id:string=json===-1?"":json.id){ - super(id); - if(json===-1){ - return; - } - this.editing; - this.type=json.type; - this.owner=owner; - this.headers=this.owner.headers; - this.name=json.name; - if(json.parent_id){ - this.parent_id=json.parent_id; - } - this.parent=undefined; - this.children=[]; - this.guild_id=json.guild_id; - this.permission_overwrites=new Map(); - this.permission_overwritesar=[]; - for(const thing of json.permission_overwrites){ - if(thing.id==="1182819038095799904"||thing.id==="1182820803700625444"){ - continue; - } - if(!this.permission_overwrites.has(thing.id)){//either a bug in the server requires this, or the API is cursed - this.permission_overwrites.set(thing.id,new Permissions(thing.allow,thing.deny)); - const permission=this.permission_overwrites.get(thing.id); - if(permission){ - const role=this.guild.roleids.get(thing.id); - if(role){ - this.permission_overwritesar.push([role,permission]); - } - } - } - } - - this.topic=json.topic; - this.nsfw=json.nsfw; - this.position=json.position; - this.lastreadmessageid=undefined; - if(json.last_message_id){ - this.lastmessageid=json.last_message_id; - }else{ - this.lastmessageid=undefined; - } - this.setUpInfiniteScroller(); - this.perminfo??={}; - } - get perminfo(){ - return this.guild.perminfo.channels[this.id]; - } - set perminfo(e){ - this.guild.perminfo.channels[this.id]=e; - } - isAdmin(){ - return this.guild.isAdmin(); - } - get guild(){ - return this.owner; - } - get localuser(){ - return this.guild.localuser; - } - get info(){ - return this.owner.info; - } - readStateInfo(json:readyjson["d"]["read_state"]["entries"][0]){ - this.lastreadmessageid=json.last_message_id; - this.mentions=json.mention_count; - this.mentions??=0; - this.lastpin=json.last_pin_timestamp; - } - get hasunreads():boolean{ - if(!this.hasPermission("VIEW_CHANNEL")){ - return false; - } - return (!!this.lastmessageid)&& - ( - (!this.lastreadmessageid)|| - SnowFlake.stringToUnixTime(this.lastmessageid)>SnowFlake.stringToUnixTime(this.lastreadmessageid) - ) - &&this.type!==4; - } - hasPermission(name:string,member=this.guild.member):boolean{ - if(member.isAdmin()){ - return true; - } - for(const thing of member.roles){ - let premission=this.permission_overwrites.get(thing.id); - if(premission){ - const perm=premission.getPermission(name); - if(perm){ - return perm===1; - } - } - if(thing.permissions.getPermission(name)){ - return true; - } - } - return false; - } - get canMessage():boolean{ - if((this.permission_overwritesar.length===0)&&this.hasPermission("MANAGE_CHANNELS")){ - const role=this.guild.roles.find(_=>_.name==="@everyone"); - if(role){ - this.addRoleToPerms(role); - } - } - return this.hasPermission("SEND_MESSAGES"); - } - sortchildren(){ - this.children.sort((a,b)=>{ - return a.position-b.position; - }); - } - resolveparent(guild:Guild){ - const parentid=this.parent_id; - if(!parentid)return false; - this.parent=this.localuser.channelids.get(parentid); - this.parent??=undefined; - if(this.parent!==undefined){ - this.parent.children.push(this); - } - return this.parent!==undefined; - } - calculateReorder(){ - let position=-1; - const build:{id:string,position:number|undefined,parent_id:string|undefined}[]=[]; - for(const thing of this.children){ - const thisthing:{id:string,position:number|undefined,parent_id:string|undefined}={id: thing.id,position: undefined,parent_id: undefined}; - if(thing.position|undefined; - get visable(){ - return this.hasPermission("VIEW_CHANNEL") - } - createguildHTML(admin=false):HTMLDivElement{ - const div=document.createElement("div"); - this.html=new WeakRef(div); - if(!this.visable){ - let quit=true; - for(const thing of this.children){ - if(thing.visable){ - quit=false; - } - } - if(quit){ - return div; - } - } - div["all"]=this; - div.draggable=admin; - div.addEventListener("dragstart",e=>{ - Channel.dragged=[this,div];e.stopImmediatePropagation(); - }); - div.addEventListener("dragend",()=>{ - Channel.dragged=[]; - }); - if(this.type===4){ - this.sortchildren(); - const caps=document.createElement("div"); - - const decdiv=document.createElement("div"); - const decoration=document.createElement("span"); - decoration.classList.add("svgtheme","collapse-icon","svg-category"); - decdiv.appendChild(decoration); - - const myhtml=document.createElement("p2"); - myhtml.textContent=this.name; - decdiv.appendChild(myhtml); - caps.appendChild(decdiv); - const childrendiv=document.createElement("div"); - if(admin){ - const addchannel=document.createElement("span"); - addchannel.textContent="+"; - addchannel.classList.add("addchannel"); - caps.appendChild(addchannel); - addchannel.onclick=_=>{ - this.guild.createchannels(this.createChannel.bind(this)); - }; - this.coatDropDiv(decdiv,childrendiv); - } - div.appendChild(caps); - caps.classList.add("capsflex"); - decdiv.classList.add("channeleffects"); - decdiv.classList.add("channel"); - - Channel.contextmenu.bindContextmenu(decdiv,this,undefined); - decdiv["all"]=this; - - - for(const channel of this.children){ - childrendiv.appendChild(channel.createguildHTML(admin)); - } - childrendiv.classList.add("channels"); - setTimeout(_=>{ - if(!this.perminfo.collapsed){ - childrendiv.style.height = childrendiv.scrollHeight + "px"; - } - },100); - div.appendChild(childrendiv); - if(this.perminfo.collapsed){ - decoration.classList.add("hiddencat"); - childrendiv.style.height = "0px"; - } - decdiv.onclick=()=>{ - if(childrendiv.style.height!=="0px"){ - decoration.classList.add("hiddencat"); - this.perminfo.collapsed=true; - this.localuser.userinfo.updateLocal(); - childrendiv.style.height = "0px"; - }else{ - decoration.classList.remove("hiddencat"); - this.perminfo.collapsed=false; - this.localuser.userinfo.updateLocal(); - childrendiv.style.height = childrendiv.scrollHeight + "px"; - } - }; - - }else{ - div.classList.add("channel"); - if(this.hasunreads){ - div.classList.add("cunread"); - } - Channel.contextmenu.bindContextmenu(div,this,undefined); - if(admin){ - this.coatDropDiv(div); - } - div["all"]=this; - const myhtml=document.createElement("span"); - myhtml.textContent=this.name; - if(this.type===0){ - const decoration=document.createElement("span"); - div.appendChild(decoration); - decoration.classList.add("space","svgtheme","svg-channel"); - }else if(this.type===2){// - const decoration=document.createElement("span"); - div.appendChild(decoration); - decoration.classList.add("space","svgtheme","svg-voice"); - }else if(this.type===5){// - const decoration=document.createElement("span"); - div.appendChild(decoration); - decoration.classList.add("space","svgtheme","svg-announce"); - }else{ - console.log(this.type); - } - div.appendChild(myhtml); - div.onclick=_=>{ - this.getHTML(); - }; - } - return div; - } - get myhtml(){ - if(this.html){ - return this.html.deref(); - }else{ - return undefined - } - } - readbottom(){ - if(!this.hasunreads){ - return; - } - fetch(this.info.api+"/channels/"+this.id+"/messages/"+this.lastmessageid+"/ack",{ - method: "POST", - headers: this.headers, - body: JSON.stringify({}) - }); - this.lastreadmessageid=this.lastmessageid; - this.guild.unreads(); - if(this.myhtml){ - this.myhtml.classList.remove("cunread"); - } - } - coatDropDiv(div:HTMLDivElement,container:HTMLElement|boolean=false){ - div.addEventListener("dragenter", event=>{ - console.log("enter"); - event.preventDefault(); - }); - - div.addEventListener("dragover", event=>{ - event.preventDefault(); - }); - - div.addEventListener("drop", event=>{ - const that=Channel.dragged[0]; - if(!that)return; - event.preventDefault(); - if(container){ - that.move_id=this.id; - if(that.parent){ - that.parent.children.splice(that.parent.children.indexOf(that),1); - } - that.parent=this; - (container as HTMLElement).prepend(Channel.dragged[1] as HTMLDivElement); - this.children.unshift(that); - }else{ - console.log(this,Channel.dragged); - that.move_id=this.parent_id; - if(that.parent){ - that.parent.children.splice(that.parent.children.indexOf(that),1); - }else{ - this.guild.headchannels.splice(this.guild.headchannels.indexOf(that),1); - } - that.parent=this.parent; - if(that.parent){ - const build:Channel[]=[]; - for(let i=0;i{ - fetch(this.info.api+"/channels/"+thisid,{ - method: "PATCH", - headers: this.headers, - body: JSON.stringify({ - name, - type: thistype, - topic, - bitrate: 64000, - user_limit: 0, - nsfw, - flags: 0, - rate_limit_per_user: 0 - }) - }); - console.log(full); - full.hide(); - }] - ] - - ]); - full.show(); - console.log(full); - } - deleteChannel(){ - fetch(this.info.api+"/channels/"+this.id,{ - method: "DELETE", - headers: this.headers - }); - } - setReplying(message:Message){ - if(this.replyingto?.div){ - this.replyingto.div.classList.remove("replying"); - } - this.replyingto=message; - if(!this.replyingto?.div)return; - console.log(message); - this.replyingto.div.classList.add("replying"); - this.makereplybox(); - } - makereplybox(){ - const replybox=document.getElementById("replybox") as HTMLElement; - if(this.replyingto){ - replybox.innerHTML=""; - const span=document.createElement("span"); - span.textContent="Replying to "+this.replyingto.author.username; - const X=document.createElement("button"); - X.onclick=_=>{ - if(this.replyingto?.div){ - this.replyingto.div.classList.remove("replying"); - } - replybox.classList.add("hideReplyBox"); - this.replyingto=null; - replybox.innerHTML=""; - }; - replybox.classList.remove("hideReplyBox"); - X.textContent="⦻"; - X.classList.add("cancelReply"); - replybox.append(span); - replybox.append(X); - }else{ - replybox.classList.add("hideReplyBox"); - } - } - async getmessage(id:string):Promise{ - const message=this.messages.get(id); - if(message){ - return message; - }else{ - const gety=await fetch(this.info.api+"/channels/"+this.id+"/messages?limit=1&around="+id,{headers: this.headers}); - const json=await gety.json(); - return new Message(json[0],this); - } - } - static genid:number=0; - async getHTML(){ - const id=++Channel.genid; - if(this.localuser.channelfocus){ - this.localuser.channelfocus.infinite.delete(); - } - if(this.guild!==this.localuser.lookingguild){ - this.guild.loadGuild(); - } - if(this.localuser.channelfocus&&this.localuser.channelfocus.myhtml){ - this.localuser.channelfocus.myhtml.classList.remove("viewChannel"); - } - if(this.myhtml){ - this.myhtml.classList.add("viewChannel"); - } - this.guild.prevchannel=this; - this.guild.perminfo.prevchannel=this.id; - this.localuser.userinfo.updateLocal(); - this.localuser.channelfocus=this; - const prom=this.infinite.delete(); - history.pushState(null, "","/channels/"+this.guild_id+"/"+this.id); - - this.localuser.pageTitle("#"+this.name); - const channelTopic=document.getElementById("channelTopic") as HTMLSpanElement; - if(this.topic){ - channelTopic.innerHTML=new MarkDown(this.topic, this).makeHTML().innerHTML; - channelTopic.removeAttribute("hidden"); - }else channelTopic.setAttribute("hidden",""); - - const loading=document.getElementById("loadingdiv") as HTMLDivElement; - Channel.regenLoadingMessages(); - loading.classList.add("loading"); - this.rendertyping(); - await this.putmessages(); - await prom; - if(id!==Channel.genid){ - return; - } - this.makereplybox(); - - await this.buildmessages(); - //loading.classList.remove("loading"); - (document.getElementById("typebox") as HTMLDivElement).contentEditable=""+this.canMessage; - } - typingmap:Map=new Map(); - async typingStart(typing):Promise{ - const memb=await Member.new(typing.d.member,this.guild); - if(!memb)return; - if(memb.id===this.localuser.user.id){ - console.log("you is typing"); - return; - } - console.log("user is typing and you should see it"); - this.typingmap.set(memb,Date.now()); - setTimeout(this.rendertyping.bind(this),10000); - this.rendertyping(); - } - rendertyping():void{ - const typingtext=document.getElementById("typing") as HTMLDivElement; - let build=""; - let showing=false; - let i=0; - const curtime=Date.now()-5000; - for(const thing of this.typingmap.keys()){ - if(this.typingmap.get(thing) as number>curtime){ - if(i!==0){ - build+=", "; - } - i++; - if(thing.nick){ - build+=thing.nick; - }else{ - build+=thing.user.username; - } - showing=true; - }else{ - this.typingmap.delete(thing); - } - } - if(i>1){ - build+=" are typing"; - }else{ - build+=" is typing"; - } - if(this.localuser.channelfocus===this){ - if(showing){ - typingtext.classList.remove("hidden"); - const typingtext2=document.getElementById("typingtext") as HTMLDivElement; - typingtext2.textContent=build; - }else{ - typingtext.classList.add("hidden"); - } - } - } - static regenLoadingMessages(){ - const loading=document.getElementById("loadingdiv") as HTMLDivElement; - loading.innerHTML=""; - for(let i=0;i<15;i++){ - const div=document.createElement("div"); - div.classList.add("loadingmessage"); - if(Math.random()<0.5){ - const pfp=document.createElement("div"); - pfp.classList.add("loadingpfp"); - const username=document.createElement("div"); - username.style.width=Math.floor(Math.random()*96*1.5+40)+"px"; - username.classList.add("loadingcontent"); - div.append(pfp,username); - } - const content=document.createElement("div"); - content.style.width=Math.floor(Math.random()*96*3+40)+"px"; - content.style.height=Math.floor(Math.random()*3+1)*20+"px"; - content.classList.add("loadingcontent"); - div.append(content); - loading.append(div); - } - } - lastmessage:Message|undefined; - async putmessages(){ - if(this.allthewayup){ - return; - } - if(this.lastreadmessageid&&this.messages.has(this.lastreadmessageid)){ - return; - } - const j=await fetch(this.info.api+"/channels/"+this.id+"/messages?limit=100",{ - headers: this.headers, - }); - - const response=await j.json(); - if(response.length!==100){ - this.allthewayup=true; - } - let prev:Message|undefined; - for(const thing of response){ - const message=new Message(thing,this); - if(prev){ - this.idToNext.set(message.id,prev.id); - this.idToPrev.set(prev.id,message.id); - }else{ - this.lastmessage=message; - this.lastmessageid=message.id; - } - prev=message; - } - } - delChannel(json:channeljson){ - const build:Channel[]=[]; - for(const thing of this.children){ - if(thing.id!==json.id){ - build.push(thing); - } - } - this.children=build; - } - async grabAfter(id:string){ - if(id===this.lastmessage?.id){ - return; - } - await fetch(this.info.api+"/channels/"+this.id+"/messages?limit=100&after="+id,{ - headers: this.headers - }).then(j=>{ - return j.json(); - }).then(response=>{ - let previd:string=id; - for(const i in response){ - let messager:Message; - let willbreak=false; - if(this.messages.has(response[i].id)){ - messager=this.messages.get(response[i].id) as Message; - willbreak=true; - }else{ - messager=new Message(response[i],this); - } - this.idToPrev.set(messager.id,previd); - this.idToNext.set(previd,messager.id); - previd=messager.id; - if(willbreak){ - break; - } - } - //out.buildmessages(); - }); - } - topid:string; - async grabBefore(id:string){ - if(this.topid&&id===this.topid){ - return; - } - - await fetch(this.info.api+"/channels/"+this.id+"/messages?before="+id+"&limit=100",{ - headers: this.headers - }).then(j=>{ - return j.json(); - }).then((response:messagejson[])=>{ - if(response.length<100){ - this.allthewayup=true; - if(response.length===0){ - this.topid=id; - } - } - let previd=id; - for(const i in response){ - let messager:Message; - let willbreak=false; - if(this.messages.has(response[i].id)){ - console.log("flaky"); - messager=this.messages.get(response[i].id) as Message; - willbreak=true; - }else{ - messager=new Message(response[i],this); - } - - this.idToNext.set(messager.id,previd); - this.idToPrev.set(previd,messager.id); - previd=messager.id; - - if(Number(i)===response.length-1&&response.length<100){ - this.topid=previd; - } - if(willbreak){ - break; - } - } - }); - } - /** - * Please dont use this, its not implemented. - * @deprecated - * @todo - **/ - async grabArround(id:string){//currently unused and no plans to use it yet - throw new Error("please don't call this, no one has implemented it :P"); - } - async buildmessages(){ - this.infinitefocus=false; - this.tryfocusinfinate(); - } - infinitefocus=false; - async tryfocusinfinate(){ - if(this.infinitefocus)return; - this.infinitefocus=true; - const messages=document.getElementById("channelw") as HTMLDivElement; - for(const thing of messages.getElementsByClassName("messagecontainer")){ - thing.remove(); - } - const loading=document.getElementById("loadingdiv") as HTMLDivElement; - const removetitle=document.getElementById("removetitle"); - //messages.innerHTML=""; - let id:string|undefined; - if(this.lastreadmessageid&&this.messages.has(this.lastreadmessageid)){ - id=this.lastreadmessageid; - }else if(this.lastreadmessageid&&(id=this.findClosest(this.lastreadmessageid))){ - - }else if(this.lastmessageid&&this.messages.has(this.lastmessageid)){ - id=this.goBackIds(this.lastmessageid,50); - } - if(!id){ - if(!removetitle){ - const title=document.createElement("h2"); - title.id="removetitle"; - title.textContent="No messages appear to be here, be the first to say something!"; - title.classList.add("titlespace"); - messages.append(title); - } - this.infinitefocus=false; - loading.classList.remove("loading"); - return; - }else if(removetitle){ - removetitle.remove(); - } - if(this.localuser.channelfocus!==this){ - return; - } - for(const elm of messages.getElementsByClassName("scroller")){ - elm.remove(); - console.warn("rouge element detected and removed") - } - messages.append(await this.infinite.getDiv(id)); - this.infinite.updatestuff(); - this.infinite.watchForChange().then(async _=>{ - //await new Promise(resolve => setTimeout(resolve, 0)); - this.infinite.focus(id,false);//if someone could figure out how to make this work correctly without this, that's be great :P - loading.classList.remove("loading"); - }); - //this.infinite.focus(id.id,false); - } - private goBackIds(id:string,back:number,returnifnotexistant=true):string|undefined{ - while(back!==0){ - const nextid=this.idToPrev.get(id); - if(nextid){ - id=nextid; - back--; - }else{ - if(returnifnotexistant){ - break; - }else{ - return undefined; - } - } - } - return id; - } - private findClosest(id:string|undefined){ - if(!this.lastmessageid||!id)return; - let flake:string|undefined=this.lastmessageid; - const time=SnowFlake.stringToUnixTime(id); - let flaketime=SnowFlake.stringToUnixTime(flake); - while(flake&&timeDate.now()){ - return; - } - this.typing=Date.now()+6000; - fetch(this.info.api+"/channels/"+this.id+"/typing",{ - method: "POST", - headers: this.headers - }); - } - get notification(){ - let notinumber:number|null=this.message_notifications; - if(Number(notinumber)===3){ - notinumber=null; - } - notinumber??=this.guild.message_notifications; - switch(Number(notinumber)){ - case 0: - return"all"; - case 1: - return"mentions"; - case 2: - return"none"; - case 3: - return"default"; - } - } - async sendMessage(content:string,{attachments=[],embeds=[],replyingto=null}: - {attachments:Blob[],embeds,replyingto:Message|null}){ - let replyjson:any; - if(replyingto){ - replyjson= - { - guild_id: replyingto.guild.id, - channel_id: replyingto.channel.id, - message_id: replyingto.id, - }; - } - if(attachments.length===0){ - const body={ - content, - nonce: Math.floor(Math.random()*1000000000), - message_reference: undefined - }; - if(replyjson){ - body.message_reference=replyjson; - } - return await fetch(this.info.api+"/channels/"+this.id+"/messages",{ - method: "POST", - headers: this.headers, - body: JSON.stringify(body) - }); - }else{ - const formData = new FormData(); - const body={ - content, - nonce: Math.floor(Math.random()*1000000000), - message_reference: undefined - }; - if(replyjson){ - body.message_reference=replyjson; - } - formData.append("payload_json", JSON.stringify(body)); - for(const i in attachments){ - formData.append("files["+i+"]",attachments[i]); - } - return await fetch(this.info.api+"/channels/"+this.id+"/messages", { - method: "POST", - body: formData, - headers: {Authorization: this.headers.Authorization} - }); - } - } - messageCreate(messagep:messageCreateJson):void{ - if(!this.hasPermission("VIEW_CHANNEL")){ - return; - } - const messagez=new Message(messagep.d,this); - this.lastmessage=messagez; - if(this.lastmessageid){ - this.idToNext.set(this.lastmessageid,messagez.id); - this.idToPrev.set(messagez.id,this.lastmessageid); - } - - this.lastmessageid=messagez.id; - - if(messagez.author===this.localuser.user){ - this.lastreadmessageid=messagez.id; - if(this.myhtml){ - this.myhtml.classList.remove("cunread"); - } - }else{ - if(this.myhtml){ - this.myhtml.classList.add("cunread"); - } - } - this.guild.unreads(); - if(this===this.localuser.channelfocus){ - if(!this.infinitefocus){ - this.tryfocusinfinate(); - } - this.infinite.addedBottom() - } - if(messagez.author===this.localuser.user){ - return; - } - if(this.localuser.lookingguild?.prevchannel===this&&document.hasFocus()){ - return; - } - if(this.notification==="all"){ - this.notify(messagez); - }else if(this.notification==="mentions"&&messagez.mentionsuser(this.localuser.user)){ - this.notify(messagez); - } - } - notititle(message:Message):string{ - return message.author.username+" > "+this.guild.properties.name+" > "+this.name; - } - notify(message:Message,deep=0){ - Voice.noises(Voice.getNotificationSound()); - if(!("Notification" in window)){ - - }else if(Notification.permission === "granted"){ - let noticontent:string|undefined|null=message.content.textContent; - if(message.embeds[0]){ - noticontent||=message.embeds[0].json.title; - noticontent||=message.content.textContent; - } - noticontent||="Blank Message"; - let imgurl:null|string=null; - const images=message.getimages(); - if(images.length){ - const image = images[0]; - if(image.proxy_url){ - imgurl||=image.proxy_url; - } - imgurl||=image.url; - } - const notification = new Notification(this.notititle(message),{ - body: noticontent, - icon: message.author.getpfpsrc(), - image: imgurl, - }); - notification.addEventListener("click",_=>{ - window.focus(); - this.getHTML(); - }); - }else if(Notification.permission !== "denied"){ - Notification.requestPermission().then(()=>{ - if(deep===3){ - return; - } - this.notify(message,deep+1); - }); - } - } - async addRoleToPerms(role:Role){ - await fetch(this.info.api+"/channels/"+this.id+"/permissions/"+role.id,{ - method: "PUT", - headers: this.headers, - body: JSON.stringify({ - allow: "0", - deny: "0", - id: role.id, - type: 0 - }) - }); - const perm=new Permissions("0","0"); - this.permission_overwrites.set(role.id,perm); - this.permission_overwritesar.push([role,perm]); - } - async updateRolePermissions(id:string,perms:Permissions){ - const permission=this.permission_overwrites.get(id); - if(permission){ - permission.allow=perms.allow; - permission.deny=perms.deny; - await fetch(this.info.api+"/channels/"+this.id+"/permissions/"+id,{ - method: "PUT", - headers: this.headers, - body: JSON.stringify({ - allow: permission.allow.toString(), - deny: permission.deny.toString(), - id, - type: 0 - }) - }); - } - } -} -Channel.setupcontextmenu(); -export{Channel}; - diff --git a/webpage/contextmenu.ts b/webpage/contextmenu.ts deleted file mode 100644 index e600071..0000000 --- a/webpage/contextmenu.ts +++ /dev/null @@ -1,88 +0,0 @@ -class Contextmenu{ - static currentmenu:HTMLElement|""; - name:string; - buttons:[string,(this:x,arg:y,e:MouseEvent)=>void,string|null,(this:x,arg:y)=>boolean,(this:x,arg:y)=>boolean,string][]; - div:HTMLDivElement; - static setup(){ - Contextmenu.currentmenu=""; - document.addEventListener("click", event=>{ - if(Contextmenu.currentmenu===""){ - return; - } - if(!Contextmenu.currentmenu.contains(event.target as Node)){ - Contextmenu.currentmenu.remove(); - Contextmenu.currentmenu=""; - } - }); - } - constructor(name:string){ - this.name=name; - this.buttons=[]; - } - addbutton(text:string,onclick:(this:x,arg:y,e:MouseEvent)=>void,img:null|string=null,shown:(this:x,arg:y)=>boolean=_=>true,enabled:(this:x,arg:y)=>boolean=_=>true){ - this.buttons.push([text,onclick,img,shown,enabled,"button"]); - return{}; - } - addsubmenu(text:string,onclick:(this:x,arg:y,e:MouseEvent)=>void,img=null,shown:(this:x,arg:y)=>boolean=_=>true,enabled:(this:x,arg:y)=>boolean=_=>true){ - this.buttons.push([text,onclick,img,shown,enabled,"submenu"]); - return{}; - } - private makemenu(x:number,y:number,addinfo:x,other:y){ - const div=document.createElement("div"); - div.classList.add("contextmenu","flexttb"); - - let visibleButtons=0; - for(const thing of this.buttons){ - if(!thing[3].bind(addinfo)(other))continue; - visibleButtons++; - - const intext=document.createElement("button"); - intext.disabled=!thing[4].bind(addinfo)(other); - intext.classList.add("contextbutton"); - intext.textContent=thing[0]; - console.log(thing); - if(thing[5]==="button"||thing[5]==="submenu"){ - intext.onclick=thing[1].bind(addinfo,other); - } - - div.appendChild(intext); - } - if(visibleButtons == 0)return; - - if(Contextmenu.currentmenu!=""){ - Contextmenu.currentmenu.remove(); - } - div.style.top = y+"px"; - div.style.left = x+"px"; - document.body.appendChild(div); - Contextmenu.keepOnScreen(div); - console.log(div); - Contextmenu.currentmenu=div; - return this.div; - } - bindContextmenu(obj:HTMLElement,addinfo:x,other:y){ - const func=event=>{ - event.preventDefault(); - event.stopImmediatePropagation(); - this.makemenu(event.clientX,event.clientY,addinfo,other); - }; - obj.addEventListener("contextmenu", func); - return func; - } - static keepOnScreen(obj:HTMLElement){ - const html = document.documentElement.getBoundingClientRect(); - const docheight=html.height; - const docwidth=html.width; - const box=obj.getBoundingClientRect(); - console.log(box,docheight,docwidth); - if(box.right>docwidth){ - console.log("test"); - obj.style.left = docwidth-box.width+"px"; - } - if(box.bottom>docheight){ - obj.style.top = docheight-box.height+"px"; - } - } -} -Contextmenu.setup(); -export{Contextmenu}; diff --git a/webpage/dialog.ts b/webpage/dialog.ts deleted file mode 100644 index 417bbc4..0000000 --- a/webpage/dialog.ts +++ /dev/null @@ -1,278 +0,0 @@ -type dialogjson=[ - "hdiv",...dialogjson[] -]|[ - "vdiv",...dialogjson[] -]|[ - "img",string,[number,number]|undefined|["fit"] -]|[ - "checkbox",string,boolean,(this:HTMLInputElement,e:Event)=>unknown -]|[ - "button",string,string,(this:HTMLButtonElement,e:Event)=>unknown -]|[ - "mdbox",string,string,(this:HTMLTextAreaElement,e:Event)=>unknown -]|[ - "textbox",string,string,(this:HTMLInputElement,e:Event)=>unknown -]|[ - "fileupload",string,(this:HTMLInputElement,e:Event)=>unknown -]|[ - "text",string -]|[ - "title",string -]|[ - "radio",string,string[],(this:unknown,e:string)=>unknown,number -]|[ - "html",HTMLElement -]|[ - "select",string,string[],(this:HTMLSelectElement,e:Event)=>unknown,number -]|[ - "tabs",[string,dialogjson][] -] -class Dialog{ - layout:dialogjson; - onclose: Function; - onopen: Function; - html:HTMLDivElement; - background: HTMLDivElement; - constructor(layout:dialogjson,onclose=_=>{},onopen=_=>{}){ - this.layout=layout; - this.onclose=onclose; - this.onopen=onopen; - const div=document.createElement("div"); - div.appendChild(this.tohtml(layout)); - this.html=div; - this.html.classList.add("centeritem"); - if(!(layout[0]==="img")){ - this.html.classList.add("nonimagecenter"); - } - } - tohtml(array:dialogjson):HTMLElement{ - switch(array[0]){ - case"img": - const img=document.createElement("img"); - img.src=array[1]; - if(array[2]!=undefined){ - if(array[2].length===2){ - img.width=array[2][0]; - img.height=array[2][1]; - }else if(array[2][0]==="fit"){ - img.classList.add("imgfit"); - } - } - return img; - case"hdiv": - const hdiv=document.createElement("div"); - hdiv.classList.add("flexltr") - - for(const thing of array){ - if(thing==="hdiv"){ - continue; - } - hdiv.appendChild(this.tohtml(thing)); - } - return hdiv; - case"vdiv": - const vdiv=document.createElement("div"); - vdiv.classList.add("flexttb"); - for(const thing of array){ - if(thing==="vdiv"){ - continue; - } - vdiv.appendChild(this.tohtml(thing)); - } - return vdiv; - case"checkbox": - { - const div=document.createElement("div"); - const checkbox = document.createElement("input"); - div.appendChild(checkbox); - const label=document.createElement("span"); - checkbox.checked=array[2]; - label.textContent=array[1]; - div.appendChild(label); - checkbox.addEventListener("change",array[3]); - checkbox.type = "checkbox"; - return div; - } - case"button": - { - const div=document.createElement("div"); - const input = document.createElement("button"); - - const label=document.createElement("span"); - input.textContent=array[2]; - label.textContent=array[1]; - div.appendChild(label); - div.appendChild(input); - input.addEventListener("click",array[3]); - return div; - } - case"mdbox": - { - const div=document.createElement("div"); - const input=document.createElement("textarea"); - input.value=array[2]; - const label=document.createElement("span"); - label.textContent=array[1]; - input.addEventListener("input",array[3]); - div.appendChild(label); - div.appendChild(document.createElement("br")); - div.appendChild(input); - return div; - } - case"textbox": - { - const div=document.createElement("div"); - const input=document.createElement("input"); - input.value=array[2]; - input.type="text"; - const label=document.createElement("span"); - label.textContent=array[1]; - console.log(array[3]); - input.addEventListener("input",array[3]); - div.appendChild(label); - div.appendChild(input); - return div; - } - case"fileupload": - { - const div=document.createElement("div"); - const input=document.createElement("input"); - input.type="file"; - const label=document.createElement("span"); - label.textContent=array[1]; - div.appendChild(label); - div.appendChild(input); - input.addEventListener("change",array[2]); - console.log(array); - return div; - } - case"text":{ - const span =document.createElement("span"); - span.textContent=array[1]; - return span; - } - case"title":{ - const span =document.createElement("span"); - span.classList.add("title"); - span.textContent=array[1]; - return span; - } - case"radio":{ - const div=document.createElement("div"); - const fieldset=document.createElement("fieldset"); - fieldset.addEventListener("change",()=>{ - let i=-1; - for(const thing of fieldset.children){ - i++; - if(i===0){ - continue; - } - const checkbox = thing.children[0].children[0] as HTMLInputElement; - if(checkbox.checked){ - array[3](checkbox.value); - } - } - }); - const legend=document.createElement("legend"); - legend.textContent=array[1]; - fieldset.appendChild(legend); - let i=0; - for(const thing of array[2]){ - const div=document.createElement("div"); - const input=document.createElement("input"); - input.classList.add("radio"); - input.type="radio"; - input.name=array[1]; - input.value=thing; - if(i===array[4]){ - input.checked=true; - } - const label=document.createElement("label"); - - label.appendChild(input); - const span=document.createElement("span"); - span.textContent=thing; - label.appendChild(span); - div.appendChild(label); - fieldset.appendChild(div); - i++; - } - div.appendChild(fieldset); - return div; - } - case"html": - return array[1]; - - case"select":{ - const div=document.createElement("div"); - const label=document.createElement("label"); - const select=document.createElement("select"); - - label.textContent=array[1]; - div.append(label); - div.appendChild(select); - for(const thing of array[2]){ - const option = document.createElement("option"); - option.textContent=thing; - select.appendChild(option); - } - select.selectedIndex=array[4]; - select.addEventListener("change",array[3]); - return div; - } - case"tabs":{ - const table=document.createElement("div"); - table.classList.add("flexttb"); - const tabs=document.createElement("div"); - tabs.classList.add("flexltr") - tabs.classList.add("tabbed-head"); - table.appendChild(tabs); - const content=document.createElement("div"); - content.classList.add("tabbed-content"); - table.appendChild(content); - - let shown:HTMLElement|undefined; - for(const thing of array[1]){ - const button=document.createElement("button"); - button.textContent=thing[0]; - tabs.appendChild(button); - - const html=this.tohtml(thing[1]); - content.append(html); - if(!shown){ - shown=html; - }else{ - html.style.display="none"; - } - button.addEventListener("click",_=>{ - if(shown){ - shown.style.display="none"; - } - html.style.display=""; - shown=html; - }); - } - return table; - } - default: - console.error("can't find element:"+array[0]," full element:",array); - return document.createElement("span"); - } - } - show(){ - this.onopen(); - console.log("fullscreen"); - this.background=document.createElement("div"); - this.background.classList.add("background"); - document.body.appendChild(this.background); - document.body.appendChild(this.html); - this.background.onclick = _=>{ - this.hide(); - }; - } - hide(){ - document.body.removeChild(this.background); - document.body.removeChild(this.html); - } -} -export{Dialog}; diff --git a/webpage/direct.ts b/webpage/direct.ts deleted file mode 100644 index caf13a2..0000000 --- a/webpage/direct.ts +++ /dev/null @@ -1,290 +0,0 @@ -import{Guild}from"./guild.js"; -import{ Channel }from"./channel.js"; -import{ Message }from"./message.js"; -import{ Localuser }from"./localuser.js"; -import{User}from"./user.js"; -import{ channeljson, dirrectjson, memberjson, messagejson }from"./jsontypes.js"; -import{ Permissions }from"./permissions.js"; -import { SnowFlake } from "./snowflake.js"; -import { Contextmenu } from "./contextmenu.js"; - -class Direct extends Guild{ - declare channelids:{[key:string]:Group}; - getUnixTime(): number { - throw new Error("Do not call this for Direct, it does not make sense"); - } - constructor(json:dirrectjson[],owner:Localuser){ - super(-1,owner,null); - this.message_notifications=0; - this.owner=owner; - if(!this.localuser){ - console.error("Owner was not included, please fix"); - } - this.headers=this.localuser.headers; - this.channels=[]; - this.channelids={}; - this.properties={}; - this.roles=[]; - this.roleids=new Map(); - this.prevchannel=undefined; - this.properties.name="Direct Messages"; - for(const thing of json){ - const temp=new Group(thing,this); - this.channels.push(temp); - this.channelids[temp.id]=temp; - } - this.headchannels=this.channels; - } - createChannelpac(json){ - const thischannel=new Group(json,this); - this.channelids[thischannel.id]=thischannel; - this.channels.push(thischannel); - this.sortchannels(); - this.printServers(); - return thischannel; - } - delChannel(json:channeljson){ - const channel=this.channelids[json.id]; - super.delChannel(json); - if(channel){ - channel.del(); - } - } - giveMember(_member:memberjson){ - console.error("not a real guild, can't give member object"); - } - getRole(ID:string){ - return null; - } - hasRole(r:string){ - return false; - } - isAdmin(){ - return false; - } - unreaddms(){ - for(const thing of this.channels){ - (thing as Group).unreads(); - } - } -} - -const dmPermissions = new Permissions("0"); -dmPermissions.setPermission("ADD_REACTIONS",1); -dmPermissions.setPermission("VIEW_CHANNEL",1); -dmPermissions.setPermission("SEND_MESSAGES",1); -dmPermissions.setPermission("EMBED_LINKS",1); -dmPermissions.setPermission("ATTACH_FILES",1); -dmPermissions.setPermission("READ_MESSAGE_HISTORY",1); -dmPermissions.setPermission("MENTION_EVERYONE",1); -dmPermissions.setPermission("USE_EXTERNAL_EMOJIS",1); -dmPermissions.setPermission("USE_APPLICATION_COMMANDS",1); -dmPermissions.setPermission("USE_EXTERNAL_STICKERS",1); -dmPermissions.setPermission("USE_EMBEDDED_ACTIVITIES",1); -dmPermissions.setPermission("USE_SOUNDBOARD",1); -dmPermissions.setPermission("USE_EXTERNAL_SOUNDS",1); -dmPermissions.setPermission("SEND_VOICE_MESSAGES",1); -dmPermissions.setPermission("SEND_POLLS",1); -dmPermissions.setPermission("USE_EXTERNAL_APPS",1); - -dmPermissions.setPermission("CONNECT",1); -dmPermissions.setPermission("SPEAK",1); -dmPermissions.setPermission("STREAM",1); -dmPermissions.setPermission("USE_VAD",1); - -class Group extends Channel{ - user:User; - static contextmenu=new Contextmenu("channel menu"); - static setupcontextmenu(){ - this.contextmenu.addbutton("Copy DM id",function(this:Group){ - navigator.clipboard.writeText(this.id); - }); - - this.contextmenu.addbutton("Mark as read",function(this:Group){ - this.readbottom(); - }); - - this.contextmenu.addbutton("Close DM",function(this:Group){ - this.deleteChannel(); - }); - - this.contextmenu.addbutton("Copy user ID",function(){ - navigator.clipboard.writeText(this.user.id); - }) - } - constructor(json:dirrectjson,owner:Direct){ - super(-1,owner,json.id); - this.owner=owner; - this.headers=this.guild.headers; - this.name=json.recipients[0]?.username; - if(json.recipients[0]){ - this.user=new User(json.recipients[0],this.localuser); - }else{ - this.user=this.localuser.user; - } - this.name??=this.localuser.user.username; - this.parent_id=undefined; - this.parent=null; - this.children=[]; - this.guild_id="@me"; - this.permission_overwrites=new Map(); - this.lastmessageid=json.last_message_id; - this.mentions=0; - this.setUpInfiniteScroller(); - this.updatePosition(); - } - updatePosition(){ - if(this.lastmessageid){ - this.position=SnowFlake.stringToUnixTime(this.lastmessageid); - }else{ - this.position=0; - } - this.position=-Math.max(this.position,this.getUnixTime()); - } - createguildHTML(){ - const div=document.createElement("div"); - Group.contextmenu.bindContextmenu(div,this,undefined); - this.html=new WeakRef(div); - div.classList.add("channeleffects"); - const myhtml=document.createElement("span"); - myhtml.textContent=this.name; - div.appendChild(this.user.buildpfp()); - div.appendChild(myhtml); - div["myinfo"]=this; - div.onclick=_=>{ - this.getHTML(); - }; - - return div; - } - async getHTML(){ - const id=++Channel.genid; - if(this.localuser.channelfocus){ - this.localuser.channelfocus.infinite.delete(); - } - if(this.guild!==this.localuser.lookingguild){ - this.guild.loadGuild(); - } - this.guild.prevchannel=this; - this.localuser.channelfocus=this; - const prom=this.infinite.delete(); - history.pushState(null, "","/channels/"+this.guild_id+"/"+this.id); - this.localuser.pageTitle("@"+this.name); - (document.getElementById("channelTopic") as HTMLElement).setAttribute("hidden",""); - - const loading=document.getElementById("loadingdiv") as HTMLDivElement; - Channel.regenLoadingMessages(); - loading.classList.add("loading"); - this.rendertyping(); - await this.putmessages(); - await prom; - if(id!==Channel.genid){ - return; - } - this.buildmessages(); - (document.getElementById("typebox") as HTMLDivElement).contentEditable=""+true; - } - messageCreate(messagep:{d:messagejson}){ - const messagez=new Message(messagep.d,this); - if(this.lastmessageid){ - this.idToNext.set(this.lastmessageid,messagez.id); - this.idToPrev.set(messagez.id,this.lastmessageid); - } - this.lastmessageid=messagez.id; - if(messagez.author===this.localuser.user){ - this.lastreadmessageid=messagez.id; - if(this.myhtml){ - this.myhtml.classList.remove("cunread"); - } - }else{ - if(this.myhtml){ - this.myhtml.classList.add("cunread"); - } - } - this.unreads(); - this.updatePosition(); - this.infinite.addedBottom(); - this.guild.sortchannels(); - if(this.myhtml){ - const parrent=this.myhtml.parentElement as HTMLElement; - parrent.prepend(this.myhtml); - } - if(this===this.localuser.channelfocus){ - if(!this.infinitefocus){ - this.tryfocusinfinate(); - } - this.infinite.addedBottom(); - } - this.unreads(); - if(messagez.author===this.localuser.user){ - return; - } - if(this.localuser.lookingguild?.prevchannel===this&&document.hasFocus()){ - return; - } - if(this.notification==="all"){ - this.notify(messagez); - }else if(this.notification==="mentions"&&messagez.mentionsuser(this.localuser.user)){ - this.notify(messagez); - } - } - notititle(message){ - return message.author.username; - } - readbottom(){ - super.readbottom(); - this.unreads(); - } - all:WeakRef=new WeakRef(document.createElement("div")); - noti:WeakRef=new WeakRef(document.createElement("div")); - del(){ - const all=this.all.deref(); - if(all){ - all.remove(); - } - if(this.myhtml){ - this.myhtml.remove(); - } - } - unreads(){ - const sentdms=document.getElementById("sentdms") as HTMLDivElement;//Need to change sometime - const current=this.all.deref(); - if(this.hasunreads){ - { - const noti=this.noti.deref(); - if(noti){ - noti.textContent=this.mentions+""; - return; - } - } - const div=document.createElement("div"); - div.classList.add("servernoti"); - const noti=document.createElement("div"); - noti.classList.add("unread","notiunread","pinged"); - noti.textContent=""+this.mentions; - this.noti=new WeakRef(noti); - div.append(noti); - const buildpfp=this.user.buildpfp(); - this.all=new WeakRef(div); - buildpfp.classList.add("mentioned"); - div.append(buildpfp); - sentdms.append(div); - div.onclick=_=>{ - this.guild.loadGuild(); - this.getHTML(); - }; - }else if(current){ - current.remove(); - }else{ - - } - } - isAdmin(): boolean{ - return false; - } - hasPermission(name: string): boolean{ - return dmPermissions.hasPermission(name); - } -} -export{Direct, Group}; -Group.setupcontextmenu(); diff --git a/webpage/embed.ts b/webpage/embed.ts deleted file mode 100644 index 0f2d098..0000000 --- a/webpage/embed.ts +++ /dev/null @@ -1,391 +0,0 @@ -import{Dialog}from"./dialog.js"; -import{Message}from"./message.js"; -import{MarkDown}from"./markdown.js"; -import{ embedjson,guildjson, invitejson }from"./jsontypes.js"; -import { getapiurls, getInstances } from "./login.js"; -import { Guild } from "./guild.js"; - -class Embed{ - type:string; - owner:Message; - json:embedjson; - constructor(json:embedjson, owner:Message){ - this.type=this.getType(json); - this.owner=owner; - this.json=json; - } - getType(json:embedjson){ - const instances=getInstances(); - if(instances&&json.type==="link"&&json.url&&URL.canParse(json.url)){ - const Url=new URL(json.url); - for(const instance of instances){ - if(instance.url&&URL.canParse(instance.url)){ - const IUrl=new URL(instance.url); - const params=new URLSearchParams(Url.search); - let host:string; - if(params.has("instance")){ - const url=params.get("instance") as string; - if(URL.canParse(url)){ - host=new URL(url).host; - }else{ - host=Url.host; - } - }else{ - host=Url.host; - } - if(IUrl.host===host){ - const code=Url.pathname.split("/")[Url.pathname.split("/").length-1]; - json.invite={ - url:instance.url, - code - } - return "invite"; - } - } - } - } - return json.type||"rich"; - } - generateHTML(){ - switch(this.type){ - case"rich": - return this.generateRich(); - case"image": - return this.generateImage(); - case"invite": - return this.generateInvite(); - case"link": - return this.generateLink(); - case "video": - case"article": - return this.generateArticle(); - default: - console.warn(`unsupported embed type ${this.type}, please add support dev :3`,this.json); - return document.createElement("div");//prevent errors by giving blank div - } - } - get message(){ - return this.owner; - } - get channel(){ - return this.message.channel; - } - get guild(){ - return this.channel.guild; - } - get localuser(){ - return this.guild.localuser; - } - generateRich(){ - const div=document.createElement("div"); - if(this.json.color){ - div.style.backgroundColor="#"+this.json.color.toString(16); - } - div.classList.add("embed-color"); - - const embed=document.createElement("div"); - embed.classList.add("embed"); - div.append(embed); - - if(this.json.author){ - const authorline=document.createElement("div"); - if(this.json.author.icon_url){ - const img=document.createElement("img"); - img.classList.add("embedimg"); - img.src=this.json.author.icon_url; - authorline.append(img); - } - const a=document.createElement("a"); - a.textContent=this.json.author.name as string; - if(this.json.author.url){ - MarkDown.safeLink(a,this.json.author.url); - } - a.classList.add("username"); - authorline.append(a); - embed.append(authorline); - } - if(this.json.title){ - const title=document.createElement("a"); - title.append(new MarkDown(this.json.title,this.channel).makeHTML()); - if(this.json.url){ - MarkDown.safeLink(title,this.json.url); - } - title.classList.add("embedtitle"); - embed.append(title); - } - if(this.json.description){ - const p=document.createElement("p"); - p.append(new MarkDown(this.json.description,this.channel).makeHTML()); - embed.append(p); - } - - embed.append(document.createElement("br")); - if(this.json.fields){ - for(const thing of this.json.fields){ - const div=document.createElement("div"); - const b=document.createElement("b"); - b.textContent=thing.name; - div.append(b); - const p=document.createElement("p"); - p.append(new MarkDown(thing.value,this.channel).makeHTML()); - p.classList.add("embedp"); - div.append(p); - - if(thing.inline){ - div.classList.add("inline"); - } - embed.append(div); - } - } - if(this.json.footer||this.json.timestamp){ - const footer=document.createElement("div"); - if(this.json?.footer?.icon_url){ - const img=document.createElement("img"); - img.src=this.json.footer.icon_url; - img.classList.add("embedicon"); - footer.append(img); - } - if(this.json?.footer?.text){ - const span=document.createElement("span"); - span.textContent=this.json.footer.text; - span.classList.add("spaceright"); - footer.append(span); - } - if(this.json?.footer&&this.json?.timestamp){ - const span=document.createElement("span"); - span.textContent="•"; - span.classList.add("spaceright"); - footer.append(span); - } - if(this.json?.timestamp){ - const span=document.createElement("span"); - span.textContent=new Date(this.json.timestamp).toLocaleString(); - footer.append(span); - } - embed.append(footer); - } - return div; - } - generateImage(){ - const img=document.createElement("img"); - img.classList.add("messageimg"); - img.onclick=function(){ - const full=new Dialog(["img",img.src,["fit"]]); - full.show(); - }; - img.src=this.json.thumbnail.proxy_url; - if(this.json.thumbnail.width){ - let scale=1; - const max=96*3; - scale=Math.max(scale,this.json.thumbnail.width/max); - scale=Math.max(scale,this.json.thumbnail.height/max); - this.json.thumbnail.width/=scale; - this.json.thumbnail.height/=scale; - } - img.style.width=this.json.thumbnail.width+"px"; - img.style.height=this.json.thumbnail.height+"px"; - console.log(this.json,"Image fix"); - return img; - } - generateLink(){ - const table=document.createElement("table"); - table.classList.add("embed","linkembed"); - const trtop=document.createElement("tr"); - table.append(trtop); - if(this.json.url&&this.json.title){ - const td=document.createElement("td"); - const a=document.createElement("a"); - MarkDown.safeLink(a,this.json.url); - a.textContent=this.json.title; - td.append(a); - trtop.append(td); - } - { - const td=document.createElement("td"); - const img=document.createElement("img"); - if(this.json.thumbnail){ - img.classList.add("embedimg"); - img.onclick=function(){ - const full=new Dialog(["img",img.src,["fit"]]); - full.show(); - }; - img.src=this.json.thumbnail.proxy_url; - td.append(img); - } - trtop.append(td); - } - const bottomtr=document.createElement("tr"); - const td=document.createElement("td"); - if(this.json.description){ - const span=document.createElement("span"); - span.textContent=this.json.description; - td.append(span); - } - bottomtr.append(td); - table.append(bottomtr); - return table; - } - invcache:[invitejson,{cdn:string,api:string}]|undefined; - generateInvite(){ - if(this.invcache&&(!this.json.invite||!this.localuser)){ - return this.generateLink(); - } - const div=document.createElement("div"); - div.classList.add("embed","inviteEmbed","flexttb"); - const json1=this.json.invite; - (async ()=>{ - let json:invitejson; - let info:{cdn:string,api:string}; - if(!this.invcache){ - if(!json1){ - div.append(this.generateLink()); - return; - } - const tempinfo=await getapiurls(json1.url);; - - if(!tempinfo){ - div.append(this.generateLink()); - return; - } - info=tempinfo; - const res=await fetch(info.api+"/invites/"+json1.code) - if(!res.ok){ - div.append(this.generateLink()); - } - json=await res.json() as invitejson; - this.invcache=[json,info]; - }else{ - [json,info]=this.invcache; - } - if(!json){ - div.append(this.generateLink()); - return; - } - if(json.guild.banner){ - const banner=document.createElement("img"); - banner.src=this.localuser.info.cdn+"/icons/"+json.guild.id+"/"+json.guild.banner+".png?size=256"; - banner.classList.add("banner"); - div.append(banner); - } - const guild:invitejson["guild"] & {info?:{cdn:string}}=json.guild; - guild.info=info; - const icon=Guild.generateGuildIcon(guild as invitejson["guild"] & {info:{cdn:string}}) - const iconrow=document.createElement("div"); - iconrow.classList.add("flexltr","flexstart"); - iconrow.append(icon); - { - const guildinfo=document.createElement("div"); - guildinfo.classList.add("flexttb","invguildinfo"); - const name=document.createElement("b"); - name.textContent=guild.name; - guildinfo.append(name); - - - const members=document.createElement("span"); - members.innerText="#"+json.channel.name+" • Members: "+guild.member_count - guildinfo.append(members); - members.classList.add("subtext"); - iconrow.append(guildinfo); - } - - div.append(iconrow); - const h2=document.createElement("h2"); - h2.textContent=`You've been invited by ${json.inviter.username}`; - div.append(h2); - const button=document.createElement("button"); - button.textContent="Accept"; - if(this.localuser.info.api.startsWith(info.api)){ - if(this.localuser.guildids.has(guild.id)){ - button.textContent="Already joined"; - button.disabled=true; - } - } - button.classList.add("acceptinvbutton"); - div.append(button); - button.onclick=_=>{ - if(this.localuser.info.api.startsWith(info.api)){ - fetch(this.localuser.info.api+"/invites/"+json.code,{ - method: "POST", - headers: this.localuser.headers, - }).then(r=>r.json()).then(_=>{ - if(_.message){ - alert(_.message); - } - }); - }else{ - if(this.json.invite){ - const params=new URLSearchParams(""); - params.set("instance",this.json.invite.url); - const encoded=params.toString(); - const url=`${location.origin}/invite/${this.json.invite.code}?${encoded}`; - window.open(url,"_blank"); - } - } - } - })() - return div; - } - generateArticle(){ - const colordiv=document.createElement("div"); - colordiv.style.backgroundColor="#000000"; - colordiv.classList.add("embed-color"); - - const div=document.createElement("div"); - div.classList.add("embed"); - if(this.json.provider){ - const provider=document.createElement("p"); - provider.classList.add("provider"); - provider.textContent=this.json.provider.name; - div.append(provider); - } - const a=document.createElement("a"); - if(this.json.url&&this.json.url){ - MarkDown.safeLink(a,this.json.url); - a.textContent=this.json.url; - div.append(a); - } - if(this.json.description){ - const description=document.createElement("p"); - description.textContent=this.json.description; - div.append(description); - } - if(this.json.thumbnail){ - const img=document.createElement("img"); - if(this.json.thumbnail.width&&this.json.thumbnail.width){ - let scale=1; - const inch=96; - scale=Math.max(scale,this.json.thumbnail.width/inch/4); - scale=Math.max(scale,this.json.thumbnail.height/inch/3); - this.json.thumbnail.width/=scale; - this.json.thumbnail.height/=scale; - img.style.width=this.json.thumbnail.width+"px"; - img.style.height=this.json.thumbnail.height+"px"; - } - img.classList.add("bigembedimg"); - if(this.json.video){ - img.onclick=async ()=>{ - if(this.json.video){ - img.remove(); - const iframe=document.createElement("iframe"); - iframe.src=this.json.video.url+"?autoplay=1"; - if(this.json.thumbnail.width&&this.json.thumbnail.width){ - iframe.style.width=this.json.thumbnail.width+"px"; - iframe.style.height=this.json.thumbnail.height+"px"; - } - div.append(iframe); - } - }; - }else{ - img.onclick=async ()=>{ - const full=new Dialog(["img",img.src,["fit"]]); - full.show(); - }; - } - img.src=this.json.thumbnail.proxy_url||this.json.thumbnail.url; - div.append(img); - } - colordiv.append(div); - return colordiv; - } -} -export{Embed}; diff --git a/webpage/emoji.ts b/webpage/emoji.ts deleted file mode 100644 index 250a1b0..0000000 --- a/webpage/emoji.ts +++ /dev/null @@ -1,230 +0,0 @@ -import{ Contextmenu }from"./contextmenu.js"; -import{ Guild }from"./guild.js"; -import{ emojijson }from"./jsontypes.js"; -import{ Localuser }from"./localuser.js"; - -class Emoji{ - static emojis:{ - name:string, - emojis:{ - name:string, - emoji:string, - }[] - }[]; - name:string; - id:string; - animated:boolean; - owner:Guild|Localuser; - get guild(){ - if(this.owner instanceof Guild){ - return this.owner; - } - } - get localuser(){ - if(this.owner instanceof Guild){ - return this.owner.localuser; - }else{ - return this.owner; - } - } - get info(){ - return this.owner.info; - } - constructor(json:{name:string,id:string,animated:boolean},owner:Guild|Localuser){ - this.name=json.name; - this.id=json.id; - this.animated=json.animated; - this.owner=owner; - } - getHTML(bigemoji:boolean=false){ - const emojiElem=document.createElement("img"); - emojiElem.classList.add("md-emoji"); - emojiElem.classList.add(bigemoji ? "bigemoji" : "smallemoji"); - emojiElem.crossOrigin="anonymous"; - emojiElem.src=this.info.cdn + "/emojis/" + this.id + "." + (this.animated ? "gif" : "png") + "?size=32"; - - emojiElem.alt=this.name; - emojiElem.loading="lazy"; - return emojiElem; - } - static decodeEmojiList(buffer:ArrayBuffer){ - const view = new DataView(buffer, 0); - let i=0; - function read16(){ - const int=view.getUint16(i); - i+=2; - return int; - } - function read8(){ - const int=view.getUint8(i); - i+=1; - return int; - } - function readString8(){ - return readStringNo(read8()); - } - function readString16(){ - return readStringNo(read16()); - } - function readStringNo(length:number){ - const array=new Uint8Array(length); - - for(let i=0;i127; - const emoji=readStringNo(len-(Number(skin_tone_support)*128)); - emojis.push({ - name, - skin_tone_support, - emoji - }); - } - build.push({ - name, - emojis - }); - } - this.emojis=build; - console.log(build); - } - static grabEmoji(){ - fetch("/emoji.bin").then(e=>{ - return e.arrayBuffer(); - }).then(e=>{ - Emoji.decodeEmojiList(e); - }); - } - static async emojiPicker(x:number,y:number, localuser:Localuser):Promise{ - let res:(r:Emoji|string)=>void; - const promise:Promise=new Promise(r=>{ - res=r; - }); - const menu=document.createElement("div"); - menu.classList.add("flexttb", "emojiPicker"); - menu.style.top=y+"px"; - menu.style.left=x+"px"; - - const title=document.createElement("h2"); - title.textContent=Emoji.emojis[0].name; - title.classList.add("emojiTitle"); - menu.append(title); - const selection=document.createElement("div"); - selection.classList.add("flexltr","dontshrink","emojirow"); - const body=document.createElement("div"); - body.classList.add("emojiBody"); - - let isFirst = true; - localuser.guilds.filter(guild=>guild.id != "@me" && guild.emojis.length > 0).forEach(guild=>{ - const select = document.createElement("div"); - select.classList.add("emojiSelect"); - - if(guild.properties.icon){ - const img = document.createElement("img"); - img.classList.add("pfp", "servericon", "emoji-server"); - img.crossOrigin = "anonymous"; - img.src = localuser.info.cdn + "/icons/" + guild.properties.id + "/" + guild.properties.icon + ".png?size=48"; - img.alt = "Server: " + guild.properties.name; - select.appendChild(img); - }else{ - const div = document.createElement("span"); - div.textContent = guild.properties.name.replace(/'s /g, " ").replace(/\w+/g, word=>word[0]).replace(/\s/g, ""); - select.append(div); - } - - selection.append(select); - - const clickEvent = ()=>{ - title.textContent = guild.properties.name; - body.innerHTML = ""; - for(const emojit of guild.emojis){ - const emojiElem = document.createElement("div"); - emojiElem.classList.add("emojiSelect"); - - const emojiClass = new Emoji({ - id: emojit.id as string, - name: emojit.name, - animated: emojit.animated as boolean - },localuser); - emojiElem.append(emojiClass.getHTML()); - body.append(emojiElem); - - emojiElem.addEventListener("click", ()=>{ - res(emojiClass); - if(Contextmenu.currentmenu!==""){ - Contextmenu.currentmenu.remove(); - } - }); - } - }; - - select.addEventListener("click", clickEvent); - if(isFirst){ - clickEvent(); - isFirst = false; - } - }); - - setTimeout(()=>{ - if(Contextmenu.currentmenu!=""){ - Contextmenu.currentmenu.remove(); - } - document.body.append(menu); - Contextmenu.currentmenu=menu; - Contextmenu.keepOnScreen(menu); - },10); - - - let i=0; - for(const thing of Emoji.emojis){ - const select=document.createElement("div"); - select.textContent=thing.emojis[0].emoji; - select.classList.add("emojiSelect"); - selection.append(select); - const clickEvent=()=>{ - title.textContent=thing.name; - body.innerHTML=""; - for(const emojit of thing.emojis){ - const emoji=document.createElement("div"); - emoji.classList.add("emojiSelect"); - emoji.textContent=emojit.emoji; - body.append(emoji); - emoji.onclick=_=>{ - res(emojit.emoji); - if(Contextmenu.currentmenu!==""){ - Contextmenu.currentmenu.remove(); - } - }; - } - }; - select.onclick=clickEvent; - if(i===0){ - clickEvent(); - } - i++; - } - menu.append(selection); - menu.append(body); - return promise; - } -} -Emoji.grabEmoji(); -export{Emoji}; diff --git a/webpage/file.ts b/webpage/file.ts deleted file mode 100644 index 0d7edd7..0000000 --- a/webpage/file.ts +++ /dev/null @@ -1,145 +0,0 @@ -import{ Message }from"./message.js"; -import{ Dialog }from"./dialog.js"; -import{ filejson }from"./jsontypes.js"; - -class File{ - owner:Message|null; - id:string; - filename:string; - content_type:string; - width:number|undefined; - height:number|undefined; - proxy_url:string|undefined; - url:string; - size:number; - constructor(fileJSON:filejson,owner:Message|null){ - this.owner=owner; - this.id=fileJSON.id; - this.filename=fileJSON.filename; - this.content_type=fileJSON.content_type; - this.width=fileJSON.width; - this.height=fileJSON.height; - this.url=fileJSON.url; - this.proxy_url=fileJSON.proxy_url; - this.content_type=fileJSON.content_type; - this.size=fileJSON.size; - } - getHTML(temp:boolean=false):HTMLElement{ - const src=this.proxy_url||this.url; - if(this.width&&this.height){ - let scale=1; - const max=96*3; - scale=Math.max(scale,this.width/max); - scale=Math.max(scale,this.height/max); - this.width/=scale; - this.height/=scale; - } - if(this.content_type.startsWith("image/")){ - const div=document.createElement("div"); - const img=document.createElement("img"); - img.classList.add("messageimg"); - div.classList.add("messageimgdiv"); - img.onclick=function(){ - const full=new Dialog(["img",img.src,["fit"]]); - full.show(); - }; - img.src=src; - div.append(img); - if(this.width){ - div.style.width=this.width+"px"; - div.style.height=this.height+"px"; - } - console.log(img); - console.log(this.width,this.height); - return div; - }else if(this.content_type.startsWith("video/")){ - const video=document.createElement("video"); - const source=document.createElement("source"); - source.src=src; - video.append(source); - source.type=this.content_type; - video.controls=!temp; - if(this.width&&this.height){ - video.width=this.width; - video.height=this.height; - } - return video; - }else if(this.content_type.startsWith("audio/")){ - const audio=document.createElement("audio"); - const source=document.createElement("source"); - source.src=src; - audio.append(source); - source.type=this.content_type; - audio.controls=!temp; - return audio; - }else{ - return this.createunknown(); - } - } - upHTML(files:Blob[],file:globalThis.File):HTMLElement{ - const div=document.createElement("div"); - const contained=this.getHTML(true); - div.classList.add("containedFile"); - div.append(contained); - const controls=document.createElement("div"); - const garbage=document.createElement("button"); - garbage.textContent="🗑"; - garbage.onclick=_=>{ - div.remove(); - files.splice(files.indexOf(file),1); - }; - controls.classList.add("controls"); - div.append(controls); - controls.append(garbage); - return div; - } - static initFromBlob(file:globalThis.File){ - return new File({ - filename: file.name, - size: file.size, - id: "null", - content_type: file.type, - width: undefined, - height: undefined, - url: URL.createObjectURL(file), - proxy_url: undefined - },null); - } - createunknown():HTMLElement{ - console.log("🗎"); - const src=this.proxy_url||this.url; - const div=document.createElement("table"); - div.classList.add("unknownfile"); - const nametr=document.createElement("tr"); - div.append(nametr); - const fileicon=document.createElement("td"); - nametr.append(fileicon); - fileicon.append("🗎"); - fileicon.classList.add("fileicon"); - fileicon.rowSpan=2; - const nametd=document.createElement("td"); - if(src){ - const a=document.createElement("a"); - a.href=src; - a.textContent=this.filename; - nametd.append(a); - }else{ - nametd.textContent=this.filename; - } - - nametd.classList.add("filename"); - nametr.append(nametd); - const sizetr=document.createElement("tr"); - const size=document.createElement("td"); - sizetr.append(size); - size.textContent="Size:"+File.filesizehuman(this.size); - size.classList.add("filesize"); - div.appendChild(sizetr); - return div; - } - static filesizehuman(fsize:number){ - const i = fsize == 0 ? 0 : Math.floor(Math.log(fsize) / Math.log(1024)); - return Number((fsize / Math.pow(1024, i)).toFixed(2)) * 1 + " " + ["Bytes", "Kilobytes", "Megabytes", "Gigabytes", "Terabytes"][i]; - } -} -export{File}; diff --git a/webpage/guild.ts b/webpage/guild.ts deleted file mode 100644 index b9c80ea..0000000 --- a/webpage/guild.ts +++ /dev/null @@ -1,617 +0,0 @@ -import{ Channel }from"./channel.js"; -import{ Localuser }from"./localuser.js"; -import{Contextmenu}from"./contextmenu.js"; -import{Role,RoleList}from"./role.js"; -import{Dialog}from"./dialog.js"; -import{Member}from"./member.js"; -import{Settings}from"./settings.js"; -import{Permissions}from"./permissions.js"; -import{ SnowFlake }from"./snowflake.js"; -import{ channeljson, guildjson, emojijson, memberjson, invitejson }from"./jsontypes.js"; -import{ User }from"./user.js"; - -class Guild extends SnowFlake{ - owner:Localuser; - headers:Localuser["headers"]; - channels:Channel[]; - properties:guildjson["properties"]; - member_count:number; - roles:Role[]; - roleids:Map; - prevchannel:Channel|undefined; - message_notifications:number; - headchannels:Channel[]; - position:number; - parent_id:string; - member:Member; - html:HTMLElement; - emojis:emojijson[]; - large:boolean; - static contextmenu=new Contextmenu("guild menu"); - static setupcontextmenu(){ - Guild.contextmenu.addbutton("Copy Guild id",function(this:Guild){ - navigator.clipboard.writeText(this.id); - }); - - Guild.contextmenu.addbutton("Mark as read",function(this:Guild){ - this.markAsRead(); - }); - - Guild.contextmenu.addbutton("Notifications",function(this:Guild){ - this.setnotifcation(); - }); - - Guild.contextmenu.addbutton("Leave guild",function(this:Guild){ - this.confirmleave(); - },null,function(_){ - return this.properties.owner_id!==this.member.user.id; - }); - - Guild.contextmenu.addbutton("Delete guild",function(this:Guild){ - this.confirmDelete(); - },null,function(_){ - return this.properties.owner_id===this.member.user.id; - }); - - Guild.contextmenu.addbutton("Create invite",function(this:Guild){ - },null,_=>true,_=>false); - Guild.contextmenu.addbutton("Settings",function(this:Guild){ - this.generateSettings(); - }); - /* -----things left for later----- - guild.contextmenu.addbutton("Leave Guild",function(){ - console.log(this) - this.deleteChannel(); - },null,_=>{return thisuser.isAdmin()}) - - guild.contextmenu.addbutton("Mute Guild",function(){ - editchannelf(this); - },null,_=>{return thisuser.isAdmin()}) - */ - } - generateSettings(){ - const settings=new Settings("Settings for "+this.properties.name); - { - const overview=settings.addButton("Overview"); - const form=overview.addForm("",_=>{},{ - headers:this.headers, - traditionalSubmit:true, - fetchURL:this.info.api+"/guilds/"+this.id, - method:"PATCH" - }) - form.addTextInput("Name:","name",{initText:this.properties.name}); - form.addMDInput("Description:","description",{initText:this.properties.description}); - form.addFileInput("Banner:","banner",{clear:true}); - form.addFileInput("Icon:","icon",{clear:true}); - let region=this.properties.region; - if(!region){ - region=""; - } - form.addTextInput("Region:","region",{initText:region}); - } - const s1=settings.addButton("roles"); - const permlist:[Role,Permissions][]=[]; - for(const thing of this.roles){ - permlist.push([thing,thing.permissions]); - } - s1.options.push(new RoleList(permlist,this,this.updateRolePermissions.bind(this))); - settings.show(); - } - constructor(json:guildjson|-1,owner:Localuser,member:memberjson|User|null){ - if(json===-1||member===null){ - super("@me"); - return; - } - if(json.stickers.length){ - console.log(json.stickers,":3"); - } - super(json.id); - this.large=json.large; - this.member_count=json.member_count; - this.emojis = json.emojis; - this.owner=owner; - this.headers=this.owner.headers; - this.channels=[]; - this.properties=json.properties; - this.roles=[]; - this.roleids=new Map(); - - this.message_notifications=0; - for(const roley of json.roles){ - const roleh=new Role(roley,this); - this.roles.push(roleh); - this.roleids.set(roleh.id,roleh); - } - if(member instanceof User){ - Member.resolveMember(member,this).then(_=>{ - if(_){ - this.member=_; - }else{ - console.error("Member was unable to resolve"); - } - }); - }else{ - Member.new(member,this).then(_=>{ - if(_){ - this.member=_; - } - }); - } - this.perminfo??={channels:{}}; - for(const thing of json.channels){ - const temp=new Channel(thing,this); - this.channels.push(temp); - this.localuser.channelids.set(temp.id,temp); - } - this.headchannels=[]; - for(const thing of this.channels){ - const parent=thing.resolveparent(this); - if(!parent){ - this.headchannels.push(thing); - } - } - this.prevchannel=this.localuser.channelids.get(this.perminfo.prevchannel); - } - get perminfo(){ - return this.localuser.perminfo.guilds[this.id]; - } - set perminfo(e){ - this.localuser.perminfo.guilds[this.id]=e; - } - notisetting(settings){ - this.message_notifications=settings.message_notifications; - } - setnotifcation(){ - let noti=this.message_notifications; - const notiselect=new Dialog( - ["vdiv", - ["radio","select notifications type", - ["all","only mentions","none"], - function(e:"all"|"only mentions"|"none"){ - noti=["all","only mentions","none"].indexOf(e); - }, - noti - ], - ["button","","submit",_=>{ - // - fetch(this.info.api+`/users/@me/guilds/${this.id}/settings/`,{ - method: "PATCH", - headers: this.headers, - body: JSON.stringify({ - message_notifications: noti - }) - }); - this.message_notifications=noti; - }] - ]); - notiselect.show(); - } - confirmleave(){ - const full= new Dialog([ - "vdiv", - ["title", - "Are you sure you want to leave?" - ], - ["hdiv", - ["button", - "", - "Yes, I'm sure", - _=>{ - this.leave().then(_=>{ - full.hide(); - }); - } - ], - ["button", - "", - "Nevermind", - _=>{ - full.hide(); - } - ] - - ] - ]); - full.show(); - } - async leave(){ - return fetch(this.info.api+"/users/@me/guilds/"+this.id,{ - method: "DELETE", - headers: this.headers - }); - } - printServers(){ - let build=""; - for(const thing of this.headchannels){ - build+=(thing.name+":"+thing.position)+"\n"; - for(const thingy of thing.children){ - build+=(" "+thingy.name+":"+thingy.position)+"\n"; - } - } - console.log(build); - } - calculateReorder(){ - let position=-1; - const build:{id:string,position:number|undefined,parent_id:string|undefined}[]=[]; - for(const thing of this.headchannels){ - const thisthing:{id:string,position:number|undefined,parent_id:string|undefined}={id: thing.id,position: undefined,parent_id: undefined}; - if(thing.position<=position){ - thing.position=(thisthing.position=position+1); - } - position=thing.position; - console.log(position); - if(thing.move_id&&thing.move_id!==thing.parent_id){ - thing.parent_id=thing.move_id; - thisthing.parent_id=thing.parent?.id; - thing.move_id=undefined; - } - if(thisthing.position||thisthing.parent_id){ - build.push(thisthing); - } - if(thing.children.length>0){ - const things=thing.calculateReorder(); - for(const thing of things){ - build.push(thing); - } - } - } - console.log(build); - this.printServers(); - if(build.length===0){ - return; - } - const serverbug=false; - if(serverbug){ - for(const thing of build){ - console.log(build,thing); - fetch(this.info.api+"/guilds/"+this.id+"/channels",{ - method: "PATCH", - headers: this.headers, - body: JSON.stringify([thing]) - }); - } - }else{ - fetch(this.info.api+"/guilds/"+this.id+"/channels",{ - method: "PATCH", - headers: this.headers, - body: JSON.stringify(build) - }); - } - } - get localuser(){ - return this.owner; - } - get info(){ - return this.owner.info; - } - sortchannels(){ - this.headchannels.sort((a,b)=>{ - return a.position-b.position; - }); - } - static generateGuildIcon(guild:Guild|(invitejson["guild"] & {info:{cdn:string}})){ - const divy=document.createElement("div"); - divy.classList.add("servernoti"); - - const noti=document.createElement("div"); - noti.classList.add("unread"); - divy.append(noti); - if(guild instanceof Guild){ - guild.localuser.guildhtml.set(guild.id,divy); - } - let icon:string|null - if(guild instanceof Guild){ - icon=guild.properties.icon; - }else{ - icon=guild.icon; - } - if(icon!==null){ - const img=document.createElement("img"); - img.classList.add("pfp","servericon"); - img.src=guild.info.cdn+"/icons/"+guild.id+"/"+icon+".png"; - divy.appendChild(img); - if(guild instanceof Guild){ - img.onclick=()=>{ - console.log(guild.loadGuild); - guild.loadGuild(); - guild.loadChannel(); - }; - Guild.contextmenu.bindContextmenu(img,guild,undefined); - } - }else{ - const div=document.createElement("div"); - let name:string - if(guild instanceof Guild){ - name=guild.properties.name; - }else{ - name=guild.name; - } - const build=name.replace(/'s /g, " ").replace(/\w+/g, word=>word[0]).replace(/\s/g, ""); - div.textContent=build; - div.classList.add("blankserver","servericon"); - divy.appendChild(div); - if(guild instanceof Guild){ - div.onclick=()=>{ - guild.loadGuild(); - guild.loadChannel(); - }; - Guild.contextmenu.bindContextmenu(div,guild,undefined); - } - } - return divy; - } - generateGuildIcon(){ - return Guild.generateGuildIcon(this); - } - confirmDelete(){ - let confirmname=""; - const full= new Dialog([ - "vdiv", - ["title", - "Are you sure you want to delete "+this.properties.name+"?" - ], - ["textbox", - "Name of server:", - "", - function(this:HTMLInputElement){ - confirmname=this.value; - } - ], - ["hdiv", - ["button", - "", - "Yes, I'm sure", - _=>{ - console.log(confirmname); - if(confirmname!==this.properties.name){ - return; - } - this.delete().then(_=>{ - full.hide(); - }); - } - ], - ["button", - "", - "Nevermind", - _=>{ - full.hide(); - } - ] - - ] - ]); - full.show(); - } - async delete(){ - return fetch(this.info.api+"/guilds/"+this.id+"/delete",{ - method: "POST", - headers: this.headers, - }); - } - unreads(html?:HTMLElement|undefined){ - if(html){ - this.html=html; - }else{ - html=this.html; - } - let read=true; - for(const thing of this.channels){ - if(thing.hasunreads){ - console.log(thing); - read=false; - break; - } - } - if(!html){ - return; - } - if(read){ - html.children[0].classList.remove("notiunread"); - }else{ - html.children[0].classList.add("notiunread"); - } - } - getHTML(){ - //this.printServers(); - this.sortchannels(); - this.printServers(); - const build=document.createElement("div"); - - for(const thing of this.headchannels){ - build.appendChild(thing.createguildHTML(this.isAdmin())); - } - return build; - } - isAdmin(){ - return this.member.isAdmin(); - } - async markAsRead(){ - const build:{read_states:{channel_id:string,message_id:string|null|undefined,read_state_type:number}[]}={read_states: []}; - for(const thing of this.channels){ - if(thing.hasunreads){ - build.read_states.push({channel_id: thing.id,message_id: thing.lastmessageid,read_state_type: 0}); - thing.lastreadmessageid=thing.lastmessageid; - if(!thing.myhtml)continue; - thing.myhtml.classList.remove("cunread"); - } - } - this.unreads(); - fetch(this.info.api+"/read-states/ack-bulk",{ - method: "POST", - headers: this.headers, - body: JSON.stringify(build) - }); - } - hasRole(r:Role|string){ - console.log("this should run"); - if(r instanceof Role){ - r=r.id; - } - return this.member.hasRole(r); - } - loadChannel(ID?:string|undefined){ - if(ID){ - const channel=this.localuser.channelids.get(ID); - if(channel){ - channel.getHTML(); - return; - } - } - if(this.prevchannel){ - console.log(this.prevchannel); - this.prevchannel.getHTML(); - return; - } - for(const thing of this.channels){ - if(thing.children.length===0){ - thing.getHTML(); - return; - } - } - } - loadGuild(){ - this.localuser.loadGuild(this.id); - } - updateChannel(json:channeljson){ - const channel=this.localuser.channelids.get(json.id); - if(channel){ - channel.updateChannel(json); - this.headchannels=[]; - for(const thing of this.channels){ - thing.children=[]; - } - this.headchannels=[]; - for(const thing of this.channels){ - const parent=thing.resolveparent(this); - if(!parent){ - this.headchannels.push(thing); - } - } - this.printServers(); - } - } - createChannelpac(json:channeljson){ - const thischannel=new Channel(json,this); - this.localuser.channelids.set(json.id,thischannel); - this.channels.push(thischannel); - thischannel.resolveparent(this); - if(!thischannel.parent){ - this.headchannels.push(thischannel); - } - this.calculateReorder(); - this.printServers(); - return thischannel; - } - createchannels(func=this.createChannel){ - let name=""; - let category=0; - const channelselect=new Dialog( - ["vdiv", - ["radio","select channel type", - ["voice","text","announcement"], - function(e){ - console.log(e); - category={text: 0,voice: 2,announcement: 5,category: 4}[e]; - }, - 1 - ], - ["textbox","Name of channel","",function(this:HTMLInputElement){ - name=this.value; - }], - ["button","","submit",function(){ - console.log(name,category); - func(name,category); - channelselect.hide(); - }] - ]); - channelselect.show(); - } - createcategory(){ - let name=""; - const category=4; - const channelselect=new Dialog( - ["vdiv", - ["textbox","Name of category","",function(this:HTMLInputElement){ - name=this.value; - }], - ["button","","submit",()=>{ - console.log(name,category); - this.createChannel(name,category); - channelselect.hide(); - }] - ]); - channelselect.show(); - } - delChannel(json:channeljson){ - const channel=this.localuser.channelids.get(json.id); - this.localuser.channelids.delete(json.id); - if(!channel) return; - this.channels.splice(this.channels.indexOf(channel),1); - const indexy=this.headchannels.indexOf(channel); - if(indexy!==-1){ - this.headchannels.splice(indexy,1); - } - - /* - const build=[]; - for(const thing of this.channels){ - console.log(thing.id); - if(thing!==channel){ - build.push(thing) - }else{ - console.log("fail"); - if(thing.parent){ - thing.parent.delChannel(json); - } - } - } - this.channels=build; - */ - this.printServers(); - } - createChannel(name:string,type:number){ - fetch(this.info.api+"/guilds/"+this.id+"/channels",{ - method: "POST", - headers: this.headers, - body: JSON.stringify({name, type}) - }); - } - async createRole(name:string){ - const fetched=await fetch(this.info.api+"/guilds/"+this.id+"roles",{ - method: "POST", - headers: this.headers, - body: JSON.stringify({ - name, - color: 0, - permissions: "0" - }) - }); - const json=await fetched.json(); - const role=new Role(json,this); - this.roleids.set(role.id,role); - this.roles.push(role); - return role; - } - async updateRolePermissions(id:string,perms:Permissions){ - const role=this.roleids[id]; - role.permissions.allow=perms.allow; - role.permissions.deny=perms.deny; - - await fetch(this.info.api+"/guilds/"+this.id+"/roles/"+role.id,{ - method: "PATCH", - headers: this.headers, - body: JSON.stringify({ - color: role.color, - hoist: role.hoist, - icon: role.icon, - mentionable: role.mentionable, - name: role.name, - permissions: role.permissions.allow.toString(), - unicode_emoji: role.unicode_emoji, - }) - }); - } -} -Guild.setupcontextmenu(); -export{ Guild }; diff --git a/webpage/home.ts b/webpage/home.ts deleted file mode 100644 index 69a9bee..0000000 --- a/webpage/home.ts +++ /dev/null @@ -1,64 +0,0 @@ -import{mobile}from"./login.js"; -console.log(mobile); -const serverbox=document.getElementById("instancebox") as HTMLDivElement; - -fetch("/instances.json").then(_=>_.json()).then((json:{name:string,description?:string,descriptionLong?:string,image?:string,url?:string,display?:boolean,online?:boolean, - uptime:{alltime:number,daytime:number,weektime:number}, - urls:{wellknown:string,api:string,cdn:string,gateway:string,login?:string}}[])=>{ - console.warn(json); - for(const instance of json){ - if(instance.display===false){ - continue; - } - const div=document.createElement("div"); - div.classList.add("flexltr","instance"); - if(instance.image){ - const img=document.createElement("img"); - img.src=instance.image; - div.append(img); - } - const statbox=document.createElement("div"); - statbox.classList.add("flexttb"); - - { - const textbox=document.createElement("div"); - textbox.classList.add("flexttb","instatancetextbox"); - const title=document.createElement("h2"); - title.innerText=instance.name; - if(instance.online!==undefined){ - const status=document.createElement("span"); - status.innerText=instance.online?"Online":"Offline"; - status.classList.add("instanceStatus"); - title.append(status); - } - textbox.append(title); - if(instance.description||instance.descriptionLong){ - const p=document.createElement("p"); - if(instance.descriptionLong){ - p.innerText=instance.descriptionLong; - }else if(instance.description){ - p.innerText=instance.description; - } - textbox.append(p); - } - statbox.append(textbox); - } - if(instance.uptime){ - const stats=document.createElement("div"); - stats.classList.add("flexltr"); - const span=document.createElement("span"); - span.innerText=`Uptime: All time: ${Math.round(instance.uptime.alltime*100)}% This week: ${Math.round(instance.uptime.weektime*100)}% Today: ${Math.round(instance.uptime.daytime*100)}%`; - stats.append(span); - statbox.append(stats); - } - div.append(statbox); - div.onclick=_=>{ - if(instance.online){ - window.location.href="/register.html?instance="+encodeURI(instance.name); - }else{ - alert("Instance is offline, can't connect"); - } - }; - serverbox.append(div); - } -}); diff --git a/webpage/index.ts b/webpage/index.ts deleted file mode 100644 index 1afdfc7..0000000 --- a/webpage/index.ts +++ /dev/null @@ -1,229 +0,0 @@ -import{ Localuser }from"./localuser.js"; -import{Contextmenu}from"./contextmenu.js"; -import{mobile, getBulkUsers,setTheme, Specialuser}from"./login.js"; -import{ MarkDown }from"./markdown.js"; -import{ Message }from"./message.js"; -import{ File }from"./file.js"; -(async ()=>{ - async function waitforload(){ - let res; - new Promise(r=>{ - res=r; - }); - document.addEventListener("DOMContentLoaded", ()=>{ - res(); - }); - await res; - } - await waitforload(); - - - - const users=getBulkUsers(); - if(!users.currentuser){ - window.location.href = "/login.html"; - } - - - function showAccountSwitcher(){ - const table=document.createElement("div"); - for(const thing of Object.values(users.users)){ - const specialuser=thing as Specialuser; - console.log(specialuser.pfpsrc); - - const userinfo=document.createElement("div"); - userinfo.classList.add("flexltr","switchtable"); - const pfp=document.createElement("img"); - userinfo.append(pfp); - - const user=document.createElement("div"); - userinfo.append(user); - user.append(specialuser.username); - user.append(document.createElement("br")); - const span=document.createElement("span"); - span.textContent=specialuser.serverurls.wellknown.replace("https://","").replace("http://",""); - user.append(span); - user.classList.add("userinfo"); - span.classList.add("serverURL"); - - pfp.src=specialuser.pfpsrc; - pfp.classList.add("pfp"); - table.append(userinfo); - userinfo.addEventListener("click",_=>{ - thisuser.unload(); - thisuser.swapped=true; - const loading=document.getElementById("loading") as HTMLDivElement; - loading.classList.remove("doneloading"); - loading.classList.add("loading"); - thisuser=new Localuser(specialuser); - users.currentuser=specialuser.uid; - localStorage.setItem("userinfos",JSON.stringify(users)); - thisuser.initwebsocket().then(_=>{ - thisuser.loaduser(); - thisuser.init(); - loading.classList.add("doneloading"); - loading.classList.remove("loading"); - console.log("done loading"); - }); - userinfo.remove(); - }); - } - { - const td=document.createElement("div"); - td.classList.add("switchtable"); - td.append("Switch accounts ⇌"); - td.addEventListener("click",_=>{ - window.location.href="/login.html"; - }); - table.append(td); - } - table.classList.add("accountSwitcher"); - if(Contextmenu.currentmenu!=""){ - Contextmenu.currentmenu.remove(); - } - Contextmenu.currentmenu=table; - console.log(table); - document.body.append(table); - } - { - const userinfo=document.getElementById("userinfo") as HTMLDivElement; - userinfo.addEventListener("click",_=>{ - _.stopImmediatePropagation(); - showAccountSwitcher(); - }); - const switchaccounts=document.getElementById("switchaccounts") as HTMLDivElement; - switchaccounts.addEventListener("click",_=>{ - _.stopImmediatePropagation(); - showAccountSwitcher(); - }); - console.log("this ran"); - } - let thisuser:Localuser; - try{ - console.log(users.users,users.currentuser); - thisuser=new Localuser(users.users[users.currentuser]); - thisuser.initwebsocket().then(_=>{ - thisuser.loaduser(); - thisuser.init(); - const loading=document.getElementById("loading") as HTMLDivElement; - loading.classList.add("doneloading"); - loading.classList.remove("loading"); - console.log("done loading"); - }); - }catch(e){ - console.error(e); - (document.getElementById("load-desc") as HTMLSpanElement).textContent="Account unable to start"; - thisuser=new Localuser(-1); - } - - - { - const menu=new Contextmenu("create rightclick");//Really should go into the localuser class, but that's a later thing - menu.addbutton("Create channel",()=>{ - if(thisuser.lookingguild){ - thisuser.lookingguild.createchannels(); - } - },null,_=>{ - return thisuser.isAdmin(); - }); - - menu.addbutton("Create category",()=>{ - if(thisuser.lookingguild){ - thisuser.lookingguild.createcategory(); - } - },null,_=>{ - return thisuser.isAdmin(); - }); - menu.bindContextmenu(document.getElementById("channels") as HTMLDivElement,0,0); - } - - const pasteimage=document.getElementById("pasteimage") as HTMLDivElement; - let replyingto:Message|null=null; - async function enter(event){ - const channel=thisuser.channelfocus; - if(!channel||!thisuser.channelfocus)return; - channel.typingstart(); - if(event.key === "Enter"&&!event.shiftKey){ - event.preventDefault(); - if(channel.editing){ - channel.editing.edit(markdown.rawString); - channel.editing=null; - }else{ - replyingto= thisuser.channelfocus.replyingto; - const replying=replyingto; - if(replyingto?.div){ - replyingto.div.classList.remove("replying"); - } - thisuser.channelfocus.replyingto=null; - channel.sendMessage(markdown.rawString,{ - attachments: images, - embeds: [], - replyingto: replying - }); - thisuser.channelfocus.makereplybox(); - } - while(images.length!=0){ - images.pop(); - pasteimage.removeChild(imageshtml.pop() as HTMLElement); - } - typebox.innerHTML=""; - } - } - - const typebox=document.getElementById("typebox") as HTMLDivElement; - const markdown=new MarkDown("",thisuser); - markdown.giveBox(typebox); - typebox["markdown"]=markdown; - typebox.addEventListener("keyup",enter); - typebox.addEventListener("keydown",event=>{ - if(event.key === "Enter"&&!event.shiftKey) event.preventDefault(); - }); - console.log(typebox); - typebox.onclick=console.log; - - /* - function getguildinfo(){ - const path=window.location.pathname.split("/"); - const channel=path[3]; - this.ws.send(JSON.stringify({op: 14, d: {guild_id: path[2], channels: {[channel]: [[0, 99]]}}})); - } - */ - - const images:Blob[]=[]; - const imageshtml:HTMLElement[]=[]; - - - - document.addEventListener("paste", async e=>{ - if(!e.clipboardData)return; - Array.from(e.clipboardData.files).forEach(async f=>{ - const file=File.initFromBlob(f); - e.preventDefault(); - const html=file.upHTML(images,f); - pasteimage.appendChild(html); - images.push(f); - imageshtml.push(html); - }); - }); - - setTheme(); - - function userSettings(){ - thisuser.showusersettings(); - } - (document.getElementById("settings") as HTMLImageElement).onclick=userSettings; - - if(mobile){ - (document.getElementById("channelw") as HTMLDivElement).onclick=()=>{ - ((document.getElementById("channels") as HTMLDivElement).parentNode as HTMLElement).classList.add("collapse"); - (document.getElementById("servertd") as HTMLDivElement).classList.add("collapse"); - (document.getElementById("servers") as HTMLDivElement).classList.add("collapse"); - }; - (document.getElementById("mobileback") as HTMLDivElement).textContent="#"; - (document.getElementById("mobileback") as HTMLDivElement).onclick=()=>{ - ((document.getElementById("channels") as HTMLDivElement).parentNode as HTMLElement).classList.remove("collapse"); - (document.getElementById("servertd") as HTMLDivElement).classList.remove("collapse"); - (document.getElementById("servers") as HTMLDivElement).classList.remove("collapse"); - }; - } -})(); diff --git a/webpage/infiniteScroller.ts b/webpage/infiniteScroller.ts deleted file mode 100644 index 7a6d593..0000000 --- a/webpage/infiniteScroller.ts +++ /dev/null @@ -1,301 +0,0 @@ -class InfiniteScroller{ - readonly getIDFromOffset:(ID:string,offset:number)=>Promise; - readonly getHTMLFromID:(ID:string)=>Promise; - readonly destroyFromID:(ID:string)=>Promise; - readonly reachesBottom:()=>void; - private readonly minDist=2000; - private readonly fillDist=3000; - private readonly maxDist=6000; - HTMLElements:[HTMLElement,string][]=[]; - div:HTMLDivElement|null; - constructor(getIDFromOffset:InfiniteScroller["getIDFromOffset"],getHTMLFromID:InfiniteScroller["getHTMLFromID"],destroyFromID:InfiniteScroller["destroyFromID"],reachesBottom:InfiniteScroller["reachesBottom"]=()=>{}){ - this.getIDFromOffset=getIDFromOffset; - this.getHTMLFromID=getHTMLFromID; - this.destroyFromID=destroyFromID; - this.reachesBottom=reachesBottom; - } - timeout:NodeJS.Timeout|null; - async getDiv(initialId:string,bottom=true):Promise{ - //div.classList.add("flexttb") - if(this.div){ - throw new Error("Div already exists, exiting.") - } - const scroll=document.createElement("div"); - scroll.classList.add("flexttb","scroller"); - this.beenloaded=false; - //this.interval=setInterval(this.updatestuff.bind(this,true),100); - - this.div=scroll; - this.div.addEventListener("scroll",_=>{ - this.checkscroll(); - if(this.scrollBottom<5){ - this.scrollBottom=5; - } - if(this.timeout===null){ - this.timeout=setTimeout(this.updatestuff.bind(this),300); - } - - this.watchForChange(); - }); - { - let oldheight=0; - new ResizeObserver(_=>{ - this.checkscroll(); - const func=this.snapBottom(); - this.updatestuff(); - const change=oldheight-scroll.offsetHeight; - if(change>0&&this.div){ - this.div.scrollTop+=change; - } - oldheight=scroll.offsetHeight; - this.watchForChange(); - func(); - }).observe(scroll); - } - new ResizeObserver(this.watchForChange.bind(this)).observe(scroll); - - await this.firstElement(initialId); - this.updatestuff(); - await this.watchForChange().then(_=>{ - this.updatestuff(); - this.beenloaded=true; - }); - return scroll; - } - beenloaded=false; - scrollBottom:number; - scrollTop:number; - needsupdate=true; - averageheight:number=60; - checkscroll(){ - if(this.beenloaded&&this.div&&!document.body.contains(this.div)){ - console.warn("not in document"); - this.div=null; - } - } - async updatestuff(){ - this.timeout=null; - if(!this.div)return; - this.scrollBottom = this.div.scrollHeight - this.div.scrollTop - this.div.clientHeight; - this.averageheight=this.div.scrollHeight/this.HTMLElements.length; - if(this.averageheight<10){ - this.averageheight=60; - } - this.scrollTop=this.div.scrollTop; - if(!this.scrollBottom && !await this.watchForChange()){ - this.reachesBottom(); - } - if(!this.scrollTop){ - await this.watchForChange(); - } - this.needsupdate=false; - //this.watchForChange(); - } - async firstElement(id:string){ - if(!this.div)return; - const html=await this.getHTMLFromID(id); - this.div.appendChild(html); - this.HTMLElements.push([html,id]); - } - async addedBottom(){ - await this.updatestuff(); - const func=this.snapBottom(); - await this.watchForChange(); - func(); - } - snapBottom(){ - const scrollBottom=this.scrollBottom; - return()=>{ - if(this.div&&scrollBottom<4){ - this.div.scrollTop=this.div.scrollHeight; - } - }; - } - private async watchForTop(already=false,fragement=new DocumentFragment()):Promise{ - if(!this.div)return false; - try{ - let again=false; - if(this.scrollTop<(already?this.fillDist:this.minDist)){ - let nextid:string|undefined; - const firstelm=this.HTMLElements.at(0); - if(firstelm){ - const previd=firstelm[1]; - nextid=await this.getIDFromOffset(previd,1); - } - - - if(!nextid){ - - }else{ - const html=await this.getHTMLFromID(nextid); - if(!html){ - this.destroyFromID(nextid); - return false; - } - again=true; - fragement.prepend(html); - this.HTMLElements.unshift([html,nextid]); - this.scrollTop+=this.averageheight; - } - } - if(this.scrollTop>this.maxDist){ - const html=this.HTMLElements.shift(); - if(html){ - again=true; - await this.destroyFromID(html[1]); - this.scrollTop-=this.averageheight; - } - } - if(again){ - await this.watchForTop(true,fragement); - } - return again; - }finally{ - if(!already){ - if(this.div.scrollTop===0){ - this.scrollTop=1; - this.div.scrollTop=10; - } - this.div.prepend(fragement,fragement); - } - } - } - async watchForBottom(already=false,fragement=new DocumentFragment()):Promise{ - let func:Function|undefined; - if(!already) func=this.snapBottom(); - if(!this.div)return false; - try{ - let again=false; - const scrollBottom = this.scrollBottom; - if(scrollBottom<(already?this.fillDist:this.minDist)){ - let nextid:string|undefined; - const lastelm=this.HTMLElements.at(-1); - if(lastelm){ - const previd=lastelm[1]; - nextid=await this.getIDFromOffset(previd,-1); - } - if(!nextid){ - }else{ - again=true; - const html=await this.getHTMLFromID(nextid); - fragement.appendChild(html); - this.HTMLElements.push([html,nextid]); - this.scrollBottom+=this.averageheight; - } - } - if(scrollBottom>this.maxDist){ - const html=this.HTMLElements.pop(); - if(html){ - await this.destroyFromID(html[1]); - this.scrollBottom-=this.averageheight; - again=true; - } - } - if(again){ - await this.watchForBottom(true,fragement); - } - return again; - }finally{ - if(!already){ - this.div.append(fragement); - if(func){ - func(); - } - } - } - } - watchtime:boolean=false; - changePromise:Promise|undefined; - async watchForChange():Promise{ - if(this.changePromise){ - this.watchtime=true; - return await this.changePromise; - }else{ - this.watchtime=false; - } - this.changePromise=new Promise(async res=>{ - try{ - try{ - if(!this.div){ - res(false);return false; - } - const out=await Promise.allSettled([this.watchForTop(),this.watchForBottom()]) as {value:boolean}[]; - const changed=(out[0].value||out[1].value); - if(this.timeout===null&&changed){ - this.timeout=setTimeout(this.updatestuff.bind(this),300); - } - if(!this.changePromise){ - console.error("something really bad happened"); - } - - res(Boolean(changed)); - return Boolean(changed); - }catch(e){ - console.error(e); - } - res(false); - return false; - }catch(e){ - throw e; - }finally{ - setTimeout(_=>{ - this.changePromise=undefined; - if(this.watchtime){ - this.watchForChange(); - } - },300); - } - }); - return await this.changePromise; - } - async focus(id:string,flash=true){ - let element:HTMLElement|undefined; - for(const thing of this.HTMLElements){ - if(thing[1]===id){ - element=thing[0]; - } - } - if(element){ - if(flash){ - element.scrollIntoView({ - behavior: "smooth", - block: "center" - }); - await new Promise(resolve=>setTimeout(resolve, 1000)); - element.classList.remove("jumped"); - await new Promise(resolve=>setTimeout(resolve, 100)); - element.classList.add("jumped"); - }else{ - element.scrollIntoView(); - } - }else{ - for(const thing of this.HTMLElements){ - await this.destroyFromID(thing[1]); - } - this.HTMLElements=[]; - await this.firstElement(id); - this.updatestuff(); - await this.watchForChange(); - await new Promise(resolve=>setTimeout(resolve, 100)); - await this.focus(id,true); - } - } - async delete():Promise{ - if(this.div){ - this.div.remove(); - this.div=null; - } - try{ - for(const thing of this.HTMLElements){ - await this.destroyFromID(thing[1]); - } - }catch(e){ - console.error(e); - } - this.HTMLElements=[]; - if(this.timeout){ - clearTimeout(this.timeout); - } - } -} -export{InfiniteScroller}; diff --git a/webpage/invite.ts b/webpage/invite.ts deleted file mode 100644 index 2f95193..0000000 --- a/webpage/invite.ts +++ /dev/null @@ -1,118 +0,0 @@ -import{getBulkUsers, Specialuser, getapiurls}from"./login.js"; -(async ()=>{ - const users=getBulkUsers(); - const well=new URLSearchParams(window.location.search).get("instance"); - const joinable:Specialuser[]=[]; - for(const thing in users.users){ - const user:Specialuser = users.users[thing]; - if(user.serverurls.wellknown.includes(well)){ - joinable.push(user); - } - console.log(users.users[thing]); - } - let urls:{api:string,cdn:string}; - if(!joinable.length&&well){ - const out=await getapiurls(well); - if(out){ - urls=out; - for(const thing in users.users){ - const user:Specialuser = users.users[thing]; - if(user.serverurls.api.includes(out.api)){ - joinable.push(user); - } - console.log(users.users[thing]); - } - }else{ - throw new Error("someone needs to handle the case where the servers don't exist"); - } - }else{ - urls=joinable[0].serverurls; - } - if(!joinable.length){ - document.getElementById("AcceptInvite").textContent="Create an account to accept the invite"; - } - const code=window.location.pathname.split("/")[2]; - let guildinfo; - fetch(`${urls.api}/invites/${code}`,{ - method: "GET" - }).then(_=>_.json()).then(json=>{ - const guildjson=json.guild; - guildinfo=guildjson; - document.getElementById("invitename").textContent=guildjson.name; - document.getElementById("invitedescription").textContent= - `${json.inviter.username} invited you to join ${guildjson.name}`; - if(guildjson.icon){ - const img=document.createElement("img"); - img.src=`${urls.cdn}/icons/${guildjson.id}/${guildjson.icon}.png`; - img.classList.add("inviteGuild"); - document.getElementById("inviteimg").append(img); - }else{ - const txt=guildjson.name.replace(/'s /g, " ").replace(/\w+/g, word=>word[0]).replace(/\s/g, ""); - const div=document.createElement("div"); - div.textContent=txt; - div.classList.add("inviteGuild"); - document.getElementById("inviteimg").append(div); - } - }); - function showAccounts(){ - const table=document.createElement("dialog"); - for(const thing of Object.values(joinable)){ - const specialuser=thing as Specialuser; - console.log(specialuser.pfpsrc); - - const userinfo=document.createElement("div"); - userinfo.classList.add("flexltr","switchtable"); - const pfp=document.createElement("img"); - userinfo.append(pfp); - - const user=document.createElement("div"); - userinfo.append(user); - user.append(specialuser.username); - user.append(document.createElement("br")); - const span=document.createElement("span"); - span.textContent=specialuser.serverurls.wellknown.replace("https://","").replace("http://",""); - user.append(span); - user.classList.add("userinfo"); - span.classList.add("serverURL"); - - pfp.src=specialuser.pfpsrc; - pfp.classList.add("pfp"); - table.append(userinfo); - userinfo.addEventListener("click",_=>{ - console.log(thing); - fetch(`${urls.api}/invites/${code}`,{ - method: "POST", - headers: { - Authorization: thing.token - } - }).then(_=>{ - users.currentuser=specialuser.uid; - localStorage.setItem("userinfos",JSON.stringify(users)); - window.location.href="/channels/"+guildinfo.id; - }); - }); - } - { - const td=document.createElement("div"); - td.classList.add("switchtable"); - td.append("Login or create an account ⇌"); - td.addEventListener("click",_=>{ - const l=new URLSearchParams("?"); - l.set("goback",window.location.href); - l.set("instance",well); - window.location.href="/login?"+l.toString(); - }); - if(!joinable.length){ - const l=new URLSearchParams("?"); - l.set("goback",window.location.href); - l.set("instance",well); - window.location.href="/login?"+l.toString(); - } - table.append(td); - } - table.classList.add("accountSwitcher"); - console.log(table); - document.body.append(table); - } - document.getElementById("AcceptInvite").addEventListener("click",showAccounts); -})(); diff --git a/webpage/localuser.ts b/webpage/localuser.ts deleted file mode 100644 index 51dee9f..0000000 --- a/webpage/localuser.ts +++ /dev/null @@ -1,1477 +0,0 @@ -import{Guild}from"./guild.js"; -import{Channel}from"./channel.js"; -import{Direct}from"./direct.js"; -import{Voice}from"./audio.js"; -import{User}from"./user.js"; -import{Dialog}from"./dialog.js"; -import{getapiurls, getBulkInfo, setTheme, Specialuser}from"./login.js"; -import{ channeljson, memberjson, presencejson, readyjson, wsjson }from"./jsontypes.js"; -import{ Member }from"./member.js"; -import{ FormError, Settings }from"./settings.js"; -import{ MarkDown }from"./markdown.js"; - -const wsCodesRetry=new Set([4000,4003,4005,4007,4008,4009]); - -class Localuser{ - badges:Map=new Map(); - lastSequence:number|null=null; - token:string; - userinfo:Specialuser; - serverurls:Specialuser["serverurls"]; - initialized:boolean; - info:Specialuser["serverurls"]; - headers:{"Content-type":string,Authorization:string}; - userConnections:Dialog; - devPortal:Dialog; - ready:readyjson; - guilds:Guild[]; - guildids:Map; - user:User; - status:string; - channelfocus:Channel|undefined; - lookingguild:Guild|undefined; - guildhtml:Map; - ws:WebSocket|undefined; - connectionSucceed=0; - errorBackoff=0; - channelids=new Map() - readonly userMap=new Map(); - instancePing={ - name: "Unknown", - }; - mfa_enabled:boolean; - get perminfo(){ - return this.userinfo.localuserStore; - } - set perminfo(e){ - this.userinfo.localuserStore=e; - } - constructor(userinfo:Specialuser|-1){ - if(userinfo===-1){ - return; - } - this.token=userinfo.token; - this.userinfo=userinfo; - this.perminfo.guilds??={}; - 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}; - } - gottenReady(ready:readyjson):void{ - 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.mfa_enabled=ready.d.user.mfa_enabled as boolean; - this.userinfo.username=this.user.username; - 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={}; - 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); - } - 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); - } - - 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.pingEndpoint(); - this.userinfo.updateLocal(); - } - 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(4001); - } - } - swapped=false; - async initwebsocket():Promise{ - let returny:()=>void; - const ws= new WebSocket(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"); - 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=""; - if(temp.op===0&&temp.t==="READY"){ - returny(); - } - await this.handleEvent(temp); - }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); - } - if(temp.op===0&&temp.t==="READY"){ - returny(); - } - await this.handleEvent(temp as readyjson); - }catch(e){ - console.error(e); - }finally{ - res(); - } - }); - }); - - ws.addEventListener("close",async event=>{ - this.ws=undefined; - console.log("WebSocket closed with code " + event.code); - - 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))){ - if(this.connectionSucceed!==0 && Date.now()>this.connectionSucceed+20000)this.errorBackoff=0; - else this.errorBackoff++; - this.connectionSucceed=0; - - (document.getElementById("load-desc") as HTMLElement).innerHTML="Unable to connect to the Spacebar server, retrying in " + Math.round(0.2 + (this.errorBackoff*2.8)) + " seconds..."; - 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; - this.userinfo.updateLocal(); - 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; - this.userinfo.updateLocal(); - 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; - this.userinfo.updateLocal(); - } - break; - } - } - setTimeout(()=>{ - if(this.swapped)return; - (document.getElementById("load-desc") as HTMLElement).textContent="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="Unable to connect to the Spacebar server. Please try logging out and back in."; - }); - - await promise; - } - async handleEvent(temp:wsjson){ - console.debug(temp); - if(temp.s)this.lastSequence=temp.s; - 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": - 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_CREATE": - { - const guildy=new Guild(temp.d,this,this.user); - this.guilds.push(guildy); - this.guildids.set(guildy.id,guildy); - (document.getElementById("servers") as HTMLDivElement).insertBefore(guildy.generateGuildIcon(),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; - } - }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(_=>{ - 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); - } - } - heartbeat_interval:number; - 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); - } - if(channel.id===this.gotoid){ - guild.loadGuild(); - guild.loadChannel(channel.id); - this.gotoid=undefined; - } - } - gotoid:string|undefined; - async goToChannel(id:string){ - - const channel=this.channelids.get(id); - if(channel){ - const guild=channel.guild; - guild.loadGuild(); - guild.loadChannel(id); - }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); - } - } - 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):Guild|undefined{ - let guild=this.guildids.get(id); - if(!guild){ - guild=this.guildids.get("@me"); - } - if(this.lookingguild===guild){ - return guild; - } - if(this.channelfocus){ - 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; - //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=document.createElement("span"); - const div=document.createElement("div"); - div.classList.add("home","servericon"); - - home.classList.add("svgtheme","svgicon","svg-home"); - home["all"]=this.guildids.get("@me"); - (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=function(){ - this["all"].loadGuild(); - this["all"].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(); - serverlist.append(divy); - } - { - const br=document.createElement("hr"); - br.classList.add("lightbr"); - serverlist.appendChild(br); - br.id="bottomseparator"; - - const div=document.createElement("div"); - div.textContent="+"; - div.classList.add("home","servericon"); - serverlist.appendChild(div); - div.onclick=_=>{ - this.createGuild(); - }; - const guilddsdiv=document.createElement("div"); - const guildDiscoveryContainer=document.createElement("span"); - guildDiscoveryContainer.classList.add("svgtheme","svgicon","svg-explore"); - guilddsdiv.classList.add("home","servericon"); - guilddsdiv.appendChild(guildDiscoveryContainer); - serverlist.appendChild(guilddsdiv); - guildDiscoveryContainer.addEventListener("click", ()=>{ - this.guildDiscovery(); - }); - } - this.unreads(); - } - createGuild(){ - let inviteurl=""; - const error=document.createElement("span"); - const fields:{name:string,icon:string|null}={ - name: "", - icon: null, - }; - const full=new Dialog(["tabs",[ - ["Join using invite",[ - "vdiv", - ["textbox", - "Invite Link/Code", - "", - function(this:HTMLInputElement){ - inviteurl=this.value; - } - ], - ["html",error], - ["button", - "", - "Submit", - _=>{ - let parsed=""; - if(inviteurl.includes("/")){ - parsed=inviteurl.split("/")[inviteurl.split("/").length-1]; - }else{ - parsed=inviteurl; - } - fetch(this.info.api+"/invites/"+parsed,{ - method: "POST", - headers: this.headers, - }).then(r=>r.json()).then(_=>{ - if(_.message){ - error.textContent=_.message; - } - }); - } - ] - - ]], - ["Create Guild", - ["vdiv", - ["title","Create a guild"], - ["fileupload","Icon:",function(event:InputEvent){ - const target=event.target as HTMLInputElement; - if(!target.files)return; - const reader=new FileReader(); - reader.readAsDataURL(target.files[0]); - reader.onload=()=>{ - fields.icon=reader.result as string; - }; - }], - ["textbox","Name:","",function(event:InputEvent){ - const target=event.target as HTMLInputElement; - fields.name=target.value; - }], - ["button","","submit",()=>{ - this.makeGuild(fields).then(_=>{ - if(_.message){ - alert(_.errors.name._errors[0].message); - }else{ - full.hide(); - } - }); - }] - ]] - ]]); - 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("guildy"); - content.textContent="Loading..."; - const full=new Dialog(["html", content]); - full.show(); - - const res=await fetch(this.info.api+"/discoverable-guilds?limit=50", { - headers: this.headers - }); - const json=await res.json(); - - content.innerHTML=""; - const title=document.createElement("h2"); - title.textContent="Guild discovery ("+json.total+" entries)"; - content.appendChild(title); - - const guilds=document.createElement("div"); - guilds.id="discovery-guild-content"; - - json.guilds.forEach(guild=>{ - 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):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):Promise{ - // - const channel=this.channelids.get(typing.d.channel_id); - if(!channel) return; - channel.typingStart(typing); - //this.typing.set(memb,Date.now()); - } - 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("Settings"); - { - const userOptions=settings.addButton("User Settings",{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("Upload pfp:",_=>{ - 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("Upload banner:",_=>{ - 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("Pronouns",_=>{ - if(newpronouns||newbio||changed){ - 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("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("Profile color",_=>{},{initColor: color}); - colorPicker.watchForChange(_=>{ - console.log(); - color=_; - hypouser.accent_color=Number.parseInt("0x"+_.substr(1),16); - changed=true; - regen(); - }); - } - { - const tas=settings.addButton("Themes & sounds"); - { - const themes=["Dark","WHITE","Light"]; - tas.addSelect("Theme:",_=>{ - localStorage.setItem("theme",themes[_]); - setTheme(); - },themes,{defaultIndex: themes.indexOf(localStorage.getItem("theme") as string)}); - } - { - const sounds=Voice.sounds; - tas.addSelect("Notification sound:",_=>{ - Voice.setNotificationSound(sounds[_]); - },sounds,{defaultIndex: sounds.indexOf(Voice.getNotificationSound())}).watchForChange(_=>{ - Voice.noises(sounds[_]); - }); - } - - { - const userinfos=getBulkInfo(); - tas.addColorInput("Accent color:",_=>{ - userinfos.accent_color=_; - localStorage.setItem("userinfos",JSON.stringify(userinfos)); - document.documentElement.style.setProperty("--accent-color", userinfos.accent_color); - },{initColor: userinfos.accent_color}); - } - } - { - const security=settings.addButton("Account Settings"); - const genSecurity=()=>{ - security.removeAll(); - if(this.mfa_enabled){ - security.addButtonInput("","Disable 2FA",()=>{ - const form=security.addSubForm("2FA Disable",(_:any)=>{ - if(_.message){ - switch(_.code){ - case 60008: - form.error("code","Invalid code"); - break; - } - }else{ - this.mfa_enabled=false; - security.returnFromSub(); - genSecurity(); - } - },{ - fetchURL: (this.info.api+"/users/@me/mfa/totp/disable"), - headers: this.headers - }); - form.addTextInput("Code:","code",{required: true}); - }); - }else{ - security.addButtonInput("","Enable 2FA",async ()=>{ - let secret=""; - for(let i=0;i<18;i++){ - secret+="ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"[Math.floor(Math.random()*32)]; - } - const form=security.addSubForm("2FA Setup",(_:any)=>{ - if(_.message){ - switch(_.code){ - case 60008: - form.error("code","Invalid code"); - break; - case 400: - form.error("password","Incorrect password"); - break; - } - }else{ - genSecurity(); - this.mfa_enabled=true; - security.returnFromSub(); - } - },{ - fetchURL: (this.info.api+"/users/@me/mfa/totp/enable/"), - headers: this.headers - }); - form.addTitle("Copy this secret into your totp(time-based one time password) app"); - form.addText(`Your secret is: ${secret} and it's 6 digits, with a 30 second token period`); - form.addTextInput("Account Password:","password",{required: true,password: true}); - form.addTextInput("Code:","code",{required: true}); - form.setValue("secret",secret); - }); - } - security.addButtonInput("","Change discriminator",()=>{ - const form=security.addSubForm("Change Discriminator",_=>{ - security.returnFromSub(); - },{ - fetchURL: (this.info.api+"/users/@me/"), - headers: this.headers, - method: "PATCH" - }); - form.addTextInput("New discriminator:","discriminator"); - }); - security.addButtonInput("","Change email",()=>{ - const form=security.addSubForm("Change Email",_=>{ - security.returnFromSub(); - },{ - fetchURL: (this.info.api+"/users/@me/"), - headers: this.headers, - method: "PATCH" - }); - form.addTextInput("Password:","password",{password: true}); - if(this.mfa_enabled){ - form.addTextInput("Code:","code"); - } - form.addTextInput("New email:","email"); - }); - security.addButtonInput("","Change username",()=>{ - const form=security.addSubForm("Change Username",_=>{ - security.returnFromSub(); - },{ - fetchURL: (this.info.api+"/users/@me/"), - headers: this.headers, - method: "PATCH" - }); - form.addTextInput("Password:","password",{password: true}); - if(this.mfa_enabled){ - form.addTextInput("Code:","code"); - } - form.addTextInput("New username:","username"); - }); - security.addButtonInput("","Change password",()=>{ - const form=security.addSubForm("Change Password",_=>{ - security.returnFromSub(); - },{ - fetchURL: (this.info.api+"/users/@me/"), - headers: this.headers, - method: "PATCH" - }); - form.addTextInput("Old password:","password",{password: true}); - if(this.mfa_enabled){ - form.addTextInput("Code:","code"); - } - let in1=""; - let in2=""; - form.addTextInput("New password:","").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,"Passwords don't match"); - } - }); - }); - }; - genSecurity(); - } - { - const connections=settings.addButton("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="This connection has been disabled server-side."; - } - - connectionContainer.appendChild(container); - }); - }); - connections.addHTMLArea(connectionContainer); - } - { - const devPortal=settings.addButton("Developer Portal"); - - const teamsRes = await fetch(this.info.api + "/teams", { - headers: this.headers - }); - const teams = await teamsRes.json(); - - devPortal.addButtonInput("", "Create application", ()=>{ - const form = devPortal.addSubForm("Create application",(json:any)=>{ - if(json.message) form.error("name", json.message); - else{ - devPortal.returnFromSub(); - this.manageApplication(json.id); - } - }, { - fetchURL: this.info.api + "/applications", - headers: this.headers, - method: "POST" - }); - - form.addTextInput("Name", "name", { required: true }); - form.addSelect("Team", "team_id", ["Personal", ...teams.map(team=>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=>{ - 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); - }); - appListContainer.appendChild(container); - }); - }); - devPortal.addHTMLArea(appListContainer); - } - settings.show(); - } - async manageApplication(appId=""){ - const res=await fetch(this.info.api+"/applications/" + appId, { - headers: this.headers - }); - const json=await res.json(); - - const fields: any={}; - const appDialog=new Dialog( - ["vdiv", - ["title", - "Editing " + json.name - ], - ["vdiv", - ["textbox", "Application name:", json.name, event=>{ - fields.name=event.target.value; - }], - ["mdbox", "Description:", json.description, event=>{ - fields.description=event.target.value; - }], - ["vdiv", - json.icon ? ["img", this.info.cdn+"/app-icons/" + appId + "/" + json.icon + ".png?size=128", [128, 128]] : ["text", "No icon"], - ["fileupload", "Application icon:", event=>{ - const reader=new FileReader(); - const files=(event.target as HTMLInputElement).files; - if(files){ - reader.readAsDataURL(files[0]); - reader.onload=()=>{ - fields.icon=reader.result; - }; - } - }] - ] - ], - ["hdiv", - ["textbox", "Privacy policy URL:", json.privacy_policy_url || "", event=>{ - fields.privacy_policy_url=event.target.value; - }], - ["textbox", "Terms of Service URL:", json.terms_of_service_url || "", event=>{ - fields.terms_of_service_url=event.target.value; - }] - ], - ["hdiv", - ["checkbox", "Make bot publicly inviteable?", json.bot_public, event=>{ - fields.bot_public=event.target.checked; - }], - ["checkbox", "Require code grant to invite the bot?", json.bot_require_code_grant, event=>{ - fields.bot_require_code_grant=event.target.checked; - }] - ], - ["hdiv", - ["button", - "", - "Save changes", - async ()=>{ - const updateRes=await fetch(this.info.api+"/applications/" + appId, { - method: "PATCH", - headers: this.headers, - body: JSON.stringify(fields) - }); - if(updateRes.ok) appDialog.hide(); - else{ - const updateJSON=await updateRes.json(); - alert("An error occurred: " + updateJSON.message); - } - } - ], - ["button", - "", - (json.bot ? "Manage" : "Add") + " bot", - async ()=>{ - if(!json.bot){ - if(!confirm("Are you sure you want to add a bot to this application? There's no going back."))return; - - const updateRes=await fetch(this.info.api+"/applications/" + appId + "/bot", { - method: "POST", - headers: this.headers - }); - const updateJSON=await updateRes.json(); - alert("Bot token:\n" + updateJSON.token); - } - - appDialog.hide(); - this.manageBot(appId); - } - ] - ] - ] - ); - appDialog.show(); - } - async manageBot(appId=""){ - const res=await fetch(this.info.api+"/applications/" + appId, { - headers: this.headers - }); - const json=await res.json(); - if(!json.bot)return alert("For some reason, this application doesn't have a bot (yet)."); - - const fields: any={ - username: json.bot.username, - avatar: json.bot.avatar ? (this.info.cdn+"/app-icons/" + appId + "/" + json.bot.avatar + ".png?size=256") : "" - }; - const botDialog=new Dialog( - ["vdiv", - ["title", - "Editing bot: " + json.bot.username - ], - ["hdiv", - ["textbox", "Bot username:", json.bot.username, event=>{ - fields.username=event.target.value; - }], - ["vdiv", - fields.avatar ? ["img", fields.avatar, [128, 128]] : ["text", "No avatar"], - ["fileupload", "Bot avatar:", event=>{ - const reader=new FileReader(); - const files=(event.target as HTMLInputElement).files; - if(files){ - const file=files[0] - reader.readAsDataURL(file); - reader.onload=()=>{ - fields.avatar=reader.result; - }; - } - }] - ] - ], - ["hdiv", - ["button", - "", - "Save changes", - async ()=>{ - const updateRes=await fetch(this.info.api+"/applications/" + appId + "/bot", { - method: "PATCH", - headers: this.headers, - body: JSON.stringify(fields) - }); - if(updateRes.ok) botDialog.hide(); - else{ - const updateJSON=await updateRes.json(); - alert("An error occurred: " + updateJSON.message); - } - } - ], - ["button", - "", - "Reset token", - async ()=>{ - if(!confirm("Are you sure you want to reset the bot token? Your bot will stop working until you update it."))return; - - const updateRes=await fetch(this.info.api+"/applications/" + appId + "/bot/reset", { - method: "POST", - headers: this.headers - }); - const updateJSON=await updateRes.json(); - alert("New token:\n" + updateJSON.token); - botDialog.hide(); - } - ] - ] - ] - ); - botDialog.show(); - } - - //---------- resolving members code ----------- - readonly waitingmembers:Mapvoid>>=new Map(); - 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(borked&&guild&&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/"+guild.id+"/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:Mapvoid>=new Map(); - noncebuild:Map=new Map(); - 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); - } - } - 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(["vdiv", - ["title", "Instance stats: " + this.instancePing.name], - ["text", "Registered users: " + json.counts.user], - ["text", "Servers: " + json.counts.guild], - ["text", "Messages: " + json.counts.message], - ["text", "Members: " + json.counts.members] - ]); - dialog.show(); - } -} -export {Localuser}; diff --git a/webpage/login.html b/webpage/login.html deleted file mode 100644 index d63bfed..0000000 --- a/webpage/login.html +++ /dev/null @@ -1,36 +0,0 @@ - - - - - Jank Client - - - - - - - -
-

Login


-
-
-

-

- -
-

- -
-



-

- -
- -
- -
- Don't have an account? -
- - - diff --git a/webpage/login.ts b/webpage/login.ts deleted file mode 100644 index 218ec64..0000000 --- a/webpage/login.ts +++ /dev/null @@ -1,454 +0,0 @@ -import{ Dialog }from"./dialog.js"; - -const mobile = /iPhone|iPad|iPod|Android/i.test(navigator.userAgent); - -function setTheme(){ - let name=localStorage.getItem("theme"); - if(!name){ - localStorage.setItem("theme","Dark"); - name="Dark"; - } - document.body.className=name+"-theme"; -} -let instances:{name:string,description?:string,descriptionLong?:string,image?:string,url?:string,display?:boolean,online?:boolean, - uptime:{alltime:number,daytime:number,weektime:number}, - urls:{wellknown:string,api:string,cdn:string,gateway:string,login?:string}}[]|null; - - -setTheme(); -function getBulkUsers(){ - const json=getBulkInfo(); - for(const thing in json.users){ - json.users[thing]=new Specialuser(json.users[thing]); - } - return json; -} -function trimswitcher(){ - const json=getBulkInfo(); - const map=new Map(); - for(const thing in json.users){ - const user=json.users[thing]; - let wellknown=user.serverurls.wellknown; - if(wellknown.at(-1)!=="/"){ - wellknown+="/"; - } - wellknown+=user.username; - if(map.has(wellknown)){ - const otheruser=map.get(wellknown); - if(otheruser[1].serverurls.wellknown.at(-1)==="/"){ - delete json.users[otheruser[0]]; - map.set(wellknown,[thing,user]); - }else{ - delete json.users[thing]; - } - }else{ - map.set(wellknown,[thing,user]); - } - } - for(const thing in json.users){ - if(thing.at(-1)==="/"){ - const user=json.users[thing]; - delete json.users[thing]; - json.users[thing.slice(0, -1)]=user; - } - } - localStorage.setItem("userinfos",JSON.stringify(json)); - console.log(json); -} - -function getBulkInfo(){ - return JSON.parse(localStorage.getItem("userinfos")); -} -function setDefaults(){ - let userinfos=getBulkInfo(); - if(!userinfos){ - localStorage.setItem("userinfos",JSON.stringify({ - currentuser: null, - users: {}, - preferences: - { - theme: "Dark", - notifications: false, - notisound: "three", - }, - })); - userinfos=getBulkInfo(); - } - if(userinfos.users===undefined){ - userinfos.users={}; - } - if(userinfos.accent_color===undefined){ - userinfos.accent_color="#242443"; - } - document.documentElement.style.setProperty("--accent-color", userinfos.accent_color); - if(userinfos.preferences===undefined){ - userinfos.preferences={ - theme: "Dark", - notifications: false, - notisound: "three", - }; - } - if(userinfos.preferences&&(userinfos.preferences.notisound===undefined)){ - userinfos.preferences.notisound="three"; - } - localStorage.setItem("userinfos",JSON.stringify(userinfos)); -} -setDefaults(); -class Specialuser{ - serverurls:{api:string,cdn:string,gateway:string,wellknown:string,login:string}; - email:string; - token:string; - loggedin; - json; - constructor(json){ - if(json instanceof Specialuser){ - console.error("specialuser can't construct from another specialuser"); - } - this.serverurls=json.serverurls; - let apistring=new URL(json.serverurls.api).toString(); - apistring=apistring.replace(/\/(v\d+\/?)?$/, "")+"/v9"; - this.serverurls.api=apistring; - this.serverurls.cdn=new URL(json.serverurls.cdn).toString().replace(/\/$/,""); - this.serverurls.gateway=new URL(json.serverurls.gateway).toString().replace(/\/$/,""); - this.serverurls.wellknown=new URL(json.serverurls.wellknown).toString().replace(/\/$/,""); - this.serverurls.login=new URL(json.serverurls.login).toString().replace(/\/$/,""); - this.email=json.email; - this.token=json.token; - this.loggedin=json.loggedin; - this.json=json; - this.json.localuserStore??={}; - if(!this.serverurls||!this.email||!this.token){ - console.error("There are fundamentally missing pieces of info missing from this user"); - } - } - set pfpsrc(e){ - this.json.pfpsrc=e; - this.updateLocal(); - } - get pfpsrc(){ - return this.json.pfpsrc; - } - set username(e){ - this.json.username=e; - this.updateLocal(); - } - get username(){ - return this.json.username; - } - set localuserStore(e){ - this.json.localuserStore=e; - this.updateLocal(); - } - get localuserStore(){ - return this.json.localuserStore; - } - get uid(){ - return this.email+this.serverurls.wellknown; - } - toJSON(){ - return this.json; - } - updateLocal(){ - const info=getBulkInfo(); - info.users[this.uid]=this.toJSON(); - localStorage.setItem("userinfos",JSON.stringify(info)); - } -} -function adduser(user){ - user=new Specialuser(user); - const info=getBulkInfo(); - info.users[user.uid]=user; - info.currentuser=user.uid; - localStorage.setItem("userinfos",JSON.stringify(info)); - return user; -} -const instancein=document.getElementById("instancein") as HTMLInputElement; -let timeout; -let instanceinfo; -const stringURLMap=new Map(); - -const stringURLsMap=new Map(); -async function getapiurls(str:string):Promise<{api:string,cdn:string,gateway:string,wellknown:string,login:string}|false>{ - if(!URL.canParse(str)){ - const val=stringURLMap.get(str); - if(val){ - str=val; - }else{ - const val=stringURLsMap.get(str); - if(val){ - const responce=await fetch(val.api+val.api.endsWith("/")?"":"/"+"ping"); - if(responce.ok){ - if(val.login){ - return val as {wellknown:string,api:string,cdn:string,gateway:string,login:string}; - }else{ - val.login=val.api; - return val as {wellknown:string,api:string,cdn:string,gateway:string,login:string}; - } - } - } - } - } - if(str.at(-1)!=="/"){ - str+="/"; - } - let api:string; - try{ - const info=await fetch(`${str}/.well-known/spacebar`).then(x=>x.json()); - api=info.api; - }catch{ - return false; - } - const url = new URL(api); - try{ - const info=await fetch(`${api}${url.pathname.includes("api") ? "" : "api"}/policies/instance/domains`).then(x=>x.json()); - return{ - api: info.apiEndpoint, - gateway: info.gateway, - cdn: info.cdn, - wellknown: str, - login: url.toString() - }; - }catch{ - const val=stringURLsMap.get(str); - if(val){ - const responce=await fetch(val.api+val.api.endsWith("/")?"":"/"+"ping"); - if(responce.ok){ - if(val.login){ - return val as {wellknown:string,api:string,cdn:string,gateway:string,login:string}; - }else{ - val.login=val.api; - return val as {wellknown:string,api:string,cdn:string,gateway:string,login:string}; - } - } - } - return false; - } -} -async function checkInstance(e:string){ - const verify=document.getElementById("verify"); - try{ - verify.textContent="Checking Instance"; - const instanceinfo=await getapiurls((instancein as HTMLInputElement).value) as {wellknown:string,api:string,cdn:string,gateway:string,login:string, value:string}; - if(instanceinfo){ - instanceinfo.value=(instancein as HTMLInputElement).value; - localStorage.setItem("instanceinfo",JSON.stringify(instanceinfo)); - verify.textContent="Instance is all good"; - if(checkInstance.alt){ - checkInstance.alt(); - } - setTimeout(_=>{ - console.log(verify.textContent); - verify.textContent=""; - },3000); - }else{ - verify.textContent="Invalid Instance, try again"; - } - }catch{ - console.log("catch"); - verify.textContent="Invalid Instance, try again"; - } -} -if(instancein){ - console.log(instancein); - instancein.addEventListener("keydown",e=>{ - const verify=document.getElementById("verify"); - verify.textContent="Waiting to check Instance"; - clearTimeout(timeout); - timeout=setTimeout(checkInstance,1000); - }); - if(localStorage.getItem("instanceinfo")){ - const json=JSON.parse(localStorage.getItem("instanceinfo")); - if(json.value){ - (instancein as HTMLInputElement).value=json.value; - }else{ - (instancein as HTMLInputElement).value=json.wellknown; - } - }else{ - checkInstance("https://spacebar.chat/"); - } -} - -async function login(username:string, password:string, captcha:string){ - if(captcha===""){ - captcha=undefined; - } - const options={ - method: "POST", - body: JSON.stringify({ - login: username, - password, - undelete: false, - captcha_key: captcha - }), - headers: { - "Content-type": "application/json; charset=UTF-8", - }}; - try{ - const info=JSON.parse(localStorage.getItem("instanceinfo")); - const api=info.login+(info.login.startsWith("/")?"/":""); - return await fetch(api+"/auth/login",options).then(response=>response.json()) - .then(response=>{ - console.log(response,response.message); - if(response.message==="Invalid Form Body"){ - return response.errors.login._errors[0].message; - console.log("test"); - } - //this.serverurls||!this.email||!this.token - console.log(response); - - if(response.captcha_sitekey){ - const capt=document.getElementById("h-captcha"); - if(!capt.children.length){ - const capty=document.createElement("div"); - capty.classList.add("h-captcha"); - - capty.setAttribute("data-sitekey", response.captcha_sitekey); - const script=document.createElement("script"); - script.src="https://js.hcaptcha.com/1/api.js"; - capt.append(script); - capt.append(capty); - }else{ - eval("hcaptcha.reset()"); - } - }else{ - console.log(response); - if(response.ticket){ - let onetimecode=""; - new Dialog(["vdiv",["title","2FA code:"],["textbox","","",function(this:HTMLInputElement){ - onetimecode=this.value; - }],["button","","Submit",function(){ - fetch(api+"/auth/mfa/totp",{ - method: "POST", - headers: { - "Content-Type": "application/json" - }, - body: JSON.stringify({ - code: onetimecode, - ticket: response.ticket, - }) - }).then(r=>r.json()).then(response=>{ - if(response.message){ - alert(response.message); - }else{ - console.warn(response); - if(!response.token)return; - adduser({serverurls: JSON.parse(localStorage.getItem("instanceinfo")),email: username,token: response.token}).username=username; - const redir=new URLSearchParams(window.location.search).get("goback"); - if(redir){ - window.location.href = redir; - }else{ - window.location.href = "/channels/@me"; - } - } - }); - }]]).show(); - }else{ - console.warn(response); - if(!response.token)return; - adduser({serverurls: JSON.parse(localStorage.getItem("instanceinfo")),email: username,token: response.token}).username=username; - const redir=new URLSearchParams(window.location.search).get("goback"); - if(redir){ - window.location.href = redir; - }else{ - window.location.href = "/channels/@me"; - } - return""; - } - } - }); - }catch(error){ - console.error("Error:", error); - } -} - -async function check(e){ - e.preventDefault(); - const h=await login(e.srcElement[1].value,e.srcElement[2].value,e.srcElement[3].value); - document.getElementById("wrong").textContent=h; - console.log(h); -} -if(document.getElementById("form")){ - document.getElementById("form").addEventListener("submit", check); -} -//this currently does not work, and need to be implemented better at some time. -/* -if ("serviceWorker" in navigator){ - navigator.serviceWorker.register("/service.js", { - scope: "/", - }).then((registration) => { - let serviceWorker:ServiceWorker; - if (registration.installing) { - serviceWorker = registration.installing; - console.log("installing"); - } else if (registration.waiting) { - serviceWorker = registration.waiting; - console.log("waiting"); - } else if (registration.active) { - serviceWorker = registration.active; - console.log("active"); - } - if (serviceWorker) { - console.log(serviceWorker.state); - serviceWorker.addEventListener("statechange", (e) => { - console.log(serviceWorker.state); - }); - } - }) -} -*/ -const switchurl=document.getElementById("switch") as HTMLAreaElement; -if(switchurl){ - switchurl.href+=window.location.search; - const instance=new URLSearchParams(window.location.search).get("instance"); - console.log(instance); - if(instance){ - instancein.value=instance; - checkInstance(""); - } -} -export{checkInstance}; -trimswitcher(); -export{mobile, getBulkUsers,getBulkInfo,setTheme,Specialuser,getapiurls,adduser}; - -const datalist=document.getElementById("instances"); -console.warn(datalist); -export function getInstances(){ - return instances; -} - -fetch("/instances.json").then(_=>_.json()).then((json:{name:string,description?:string,descriptionLong?:string,image?:string,url?:string,display?:boolean,online?:boolean, -uptime:{alltime:number,daytime:number,weektime:number}, -urls:{wellknown:string,api:string,cdn:string,gateway:string,login?:string}}[])=>{ - instances=json; - if(datalist){ - console.warn(json); - if(instancein&&instancein.value===""){ - instancein.value=json[0].name; - } - for(const instance of json){ - if(instance.display===false){ - continue; - } - const option=document.createElement("option"); - option.disabled=!instance.online; - option.value=instance.name; - if(instance.url){ - stringURLMap.set(option.value,instance.url); - if(instance.urls){ - stringURLsMap.set(instance.url,instance.urls); - } - }else if(instance.urls){ - stringURLsMap.set(option.value,instance.urls); - }else{ - option.disabled=true; - } - if(instance.description){ - option.label=instance.description; - }else{ - option.label=instance.name; - } - datalist.append(option); - } - checkInstance(""); - } -}); diff --git a/webpage/manifest.json b/webpage/manifest.json deleted file mode 100644 index e827e89..0000000 --- a/webpage/manifest.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "name": "Jank Client", - "icons": [ - { - "src": "/logo.svg", - "sizes": "512x512" - } - ], - "start_url":"/channels/@me", - "display":"standalone", - "theme_color":"#05050a" -} diff --git a/webpage/markdown.ts b/webpage/markdown.ts deleted file mode 100644 index d3e414f..0000000 --- a/webpage/markdown.ts +++ /dev/null @@ -1,752 +0,0 @@ -import{ Channel }from"./channel.js"; -import { Dialog } from "./dialog.js"; -import{ Emoji }from"./emoji.js"; -import { Guild } from "./guild.js"; -import{ Localuser }from"./localuser.js"; -import { Member } from "./member.js"; - - -class MarkDown{ - txt : string[]; - keep:boolean; - stdsize:boolean; - owner:Localuser|Channel; - info:Localuser["info"]; - constructor(text : string|string[],owner:MarkDown["owner"],{keep=false,stdsize=false} = {}){ - if((typeof text)===(typeof "")){ - this.txt=(text as string).split(""); - }else{ - this.txt=(text as string[]); - } - if(this.txt===undefined){ - this.txt=[]; - } - this.info=owner.info; - this.keep=keep; - this.owner=owner; - this.stdsize=stdsize; - } - get localuser(){ - if(this.owner instanceof Localuser){ - return this.owner; - }else{ - return this.owner.localuser; - } - } - get rawString(){ - return this.txt.join(""); - } - get textContent(){ - return this.makeHTML().textContent; - } - makeHTML({keep=this.keep,stdsize=this.stdsize}={}){ - return this.markdown(this.txt,{keep,stdsize}); - } - markdown(text : string|string[],{keep=false,stdsize=false} = {}){ - let txt : string[]; - if((typeof text)===(typeof "")){ - txt=(text as string).split(""); - }else{ - txt=(text as string[]); - } - if(txt===undefined){ - txt=[]; - } - const span=document.createElement("span"); - let current=document.createElement("span"); - function appendcurrent(){ - if(current.innerHTML!==""){ - span.append(current); - current=document.createElement("span"); - } - } - for(let i=0;i"&&txt[i+2]===" "){ - element=document.createElement("div"); - const line=document.createElement("div"); - line.classList.add("quoteline"); - element.append(line); - element.classList.add("quote"); - keepys="> "; - i+=3; - } - if(keepys){ - appendcurrent(); - if(!first&&!stdsize){ - span.appendChild(document.createElement("br")); - } - const build:string[]=[]; - for(;txt[i]!=="\n"&&txt[i]!==undefined;i++){ - build.push(txt[i]); - } - try{ - if(stdsize){ - element=document.createElement("span"); - } - if(keep){ - element.append(keepys); - //span.appendChild(document.createElement("br")); - } - element.appendChild(this.markdown(build,{keep,stdsize})); - span.append(element); - }finally{ - i-=1; - continue; - } - } - if(first){ - i++; - } - } - if(txt[i]==="\n"){ - if(!stdsize){ - appendcurrent(); - span.append(document.createElement("br")); - } - continue; - } - if(txt[i]==="`"){ - let count=1; - if(txt[i+1]==="`"){ - count++; - if(txt[i+2]==="`"){ - count++; - } - } - let build=""; - if(keep){ - build+="`".repeat(count); - } - let find=0; - let j=i+count; - let init=true; - for(;txt[j]!==undefined&&(txt[j]!=="\n"||count===3)&&find!==count;j++){ - if(txt[j]==="`"){ - find++; - }else{ - if(find!==0){ - build+="`".repeat(find); - find=0; - } - if(init&&count===3){ - if(txt[j]===" "||txt[j]==="\n"){ - init=false; - } - if(keep){ - build+=txt[j]; - } - continue; - } - build+=txt[j]; - } - } - if(stdsize){ - build=build.replaceAll("\n",""); - } - if(find===count){ - appendcurrent(); - i=j; - if(keep){ - build+="`".repeat(find); - } - if(count!==3&&!stdsize){ - const samp=document.createElement("samp"); - samp.textContent=build; - span.appendChild(samp); - }else{ - const pre=document.createElement("pre"); - if(build.at(-1)==="\n"){ - build=build.substring(0,build.length-1); - } - if(txt[i]==="\n"){ - i++; - } - pre.textContent=build; - span.appendChild(pre); - } - i--; - continue; - } - } - - if(txt[i]==="*"){ - let count=1; - if(txt[i+1]==="*"){ - count++; - if(txt[i+2]==="*"){ - count++; - } - } - let build:string[]=[]; - let find=0; - let j=i+count; - for(;txt[j]!==undefined&&find!==count;j++){ - if(txt[j]==="*"){ - find++; - }else{ - build.push(txt[j]); - if(find!==0){ - build=build.concat(new Array(find).fill("*")); - find=0; - } - } - } - if(find===count&&(count!=1||txt[i+1]!==" ")){ - appendcurrent(); - i=j; - - const stars="*".repeat(count); - if(count===1){ - const i=document.createElement("i"); - if(keep){ - i.append(stars); - } - i.appendChild(this.markdown(build,{keep,stdsize})); - if(keep){ - i.append(stars); - } - span.appendChild(i); - }else if(count===2){ - const b=document.createElement("b"); - if(keep){ - b.append(stars); - } - b.appendChild(this.markdown(build,{keep,stdsize})); - if(keep){ - b.append(stars); - } - span.appendChild(b); - }else{ - const b=document.createElement("b"); - const i=document.createElement("i"); - if(keep){ - b.append(stars); - } - b.appendChild(this.markdown(build,{keep,stdsize})); - if(keep){ - b.append(stars); - } - i.appendChild(b); - span.appendChild(i); - } - i--; - continue; - } - } - - if(txt[i]==="_"){ - let count=1; - if(txt[i+1]==="_"){ - count++; - if(txt[i+2]==="_"){ - count++; - } - } - let build:string[]=[]; - let find=0; - let j=i+count; - for(;txt[j]!==undefined&&find!==count;j++){ - if(txt[j]==="_"){ - find++; - }else{ - build.push(txt[j]); - if(find!==0){ - build=build.concat(new Array(find).fill("_")); - find=0; - } - } - } - if(find===count&&(count!=1||(txt[j+1]===" "||txt[j+1]==="\n"||txt[j+1]===undefined))){ - appendcurrent(); - i=j; - const underscores="_".repeat(count); - if(count===1){ - const i=document.createElement("i"); - if(keep){ - i.append(underscores); - } - i.appendChild(this.markdown(build,{keep,stdsize})); - if(keep){ - i.append(underscores); - } - span.appendChild(i); - }else if(count===2){ - const u=document.createElement("u"); - if(keep){ - u.append(underscores); - } - u.appendChild(this.markdown(build,{keep,stdsize})); - if(keep){ - u.append(underscores); - } - span.appendChild(u); - }else{ - const u=document.createElement("u"); - const i=document.createElement("i"); - if(keep){ - i.append(underscores); - } - i.appendChild(this.markdown(build,{keep,stdsize})); - if(keep){ - i.append(underscores); - } - u.appendChild(i); - span.appendChild(u); - } - i--; - continue; - } - } - - if(txt[i]==="~"&&txt[i+1]==="~"){ - const count=2; - let build:string[]=[]; - let find=0; - let j=i+2; - for(;txt[j]!==undefined&&find!==count;j++){ - if(txt[j]==="~"){ - find++; - }else{ - build.push(txt[j]); - if(find!==0){ - build=build.concat(new Array(find).fill("~")); - find=0; - } - } - } - if(find===count){ - appendcurrent(); - i=j-1; - const tildes="~~"; - if(count===2){ - const s=document.createElement("s"); - if(keep){ - s.append(tildes); - } - s.appendChild(this.markdown(build,{keep,stdsize})); - if(keep){ - s.append(tildes); - } - span.appendChild(s); - } - continue; - } - } - if(txt[i]==="|"&&txt[i+1]==="|"){ - const count=2; - let build:string[]=[]; - let find=0; - let j=i+2; - for(;txt[j]!==undefined&&find!==count;j++){ - if(txt[j]==="|"){ - find++; - }else{ - build.push(txt[j]); - if(find!==0){ - build=build.concat(new Array(find).fill("~")); - find=0; - } - } - } - if(find===count){ - appendcurrent(); - i=j-1; - const pipes="||"; - if(count===2){ - const j=document.createElement("j"); - if(keep){ - j.append(pipes); - } - j.appendChild(this.markdown(build,{keep,stdsize})); - j.classList.add("spoiler"); - j.onclick=MarkDown.unspoil; - if(keep){ - j.append(pipes); - } - span.appendChild(j); - } - continue; - } - } - if((!keep)&&txt[i]==="h" && txt[i + 1]==="t" && txt[i + 2]==="t" && txt[i + 3]==="p"){ - let build="http"; - let j = i+4; - const endchars=new Set(["\\", "<", ">", "|", "]"," "]); - for(; txt[j] !== undefined; j++){ - const char=txt[j]; - if(endchars.has(char)){ - break; - } - build+=char; - } - if(URL.canParse(build)){ - appendcurrent(); - const a=document.createElement("a"); - //a.href=build; - MarkDown.safeLink(a,build); - a.textContent=build; - a.target="_blank"; - i=j-1; - span.appendChild(a); - continue; - } - } - if(txt[i]==="<" && (txt[i + 1]==="@"||txt[i + 1]==="#")){ - let id=""; - let j = i+2; - const numbers=new Set(["0", "1", "2", "3", "4","5","6","7","8","9"]); - for(; txt[j] !== undefined; j++){ - const char=txt[j]; - if(!numbers.has(char)){ - break; - } - id+=char; - } - - if(txt[j]===">"){ - appendcurrent(); - const mention=document.createElement("span"); - mention.classList.add("mentionMD"); - mention.contentEditable="false"; - const char=txt[i + 1]; - i=j; - switch(char){ - case "@": - const user=this.localuser.userMap.get(id); - if(user){ - mention.textContent=`@${user.name}`; - let guild:null|Guild=null; - if(this.owner instanceof Channel){ - guild=this.owner.guild; - } - if(!keep){ - user.bind(mention,guild); - } - if(guild){ - Member.resolveMember(user,guild).then(member=>{ - if(member){ - mention.textContent=`@${member.name}`; - } - }) - } - }else{ - mention.textContent=`@unknown`; - } - break; - case "#": - const channel=this.localuser.channelids.get(id); - if(channel){ - mention.textContent=`#${channel.name}`; - if(!keep){ - mention.onclick=_=>{ - this.localuser.goToChannel(id); - } - } - }else{ - mention.textContent=`#unknown`; - } - break; - } - span.appendChild(mention); - mention.setAttribute("real",`<${char}${id}>`); - continue; - } - } - if(txt[i]==="<" && txt[i + 1]==="t" && txt[i + 2]===":"){ - let found=false; - const build=["<","t",":"]; - let j = i+3; - for(; txt[j] !== void 0; j++){ - build.push(txt[j]); - - if(txt[j]===">"){ - found=true; - break; - } - } - - if(found){ - appendcurrent(); - i=j; - const parts=build.join("").match(/^$/) as RegExpMatchArray; - const dateInput=new Date(Number.parseInt(parts[1]) * 1000); - let time=""; - if(Number.isNaN(dateInput.getTime())) time=build.join(""); - else{ - if(parts[3]==="d") time=dateInput.toLocaleString(void 0, {day: "2-digit", month: "2-digit", year: "numeric"}); - else if(parts[3]==="D") time=dateInput.toLocaleString(void 0, {day: "numeric", month: "long", year: "numeric"}); - else if(!parts[3] || parts[3]==="f") time=dateInput.toLocaleString(void 0, {day: "numeric", month: "long", year: "numeric"}) + " " + - dateInput.toLocaleString(void 0, {hour: "2-digit", minute: "2-digit"}); - else if(parts[3]==="F") time=dateInput.toLocaleString(void 0, {day: "numeric", month: "long", year: "numeric", weekday: "long"}) + " " + - dateInput.toLocaleString(void 0, {hour: "2-digit", minute: "2-digit"}); - else if(parts[3]==="t") time=dateInput.toLocaleString(void 0, {hour: "2-digit", minute: "2-digit"}); - else if(parts[3]==="T") time=dateInput.toLocaleString(void 0, {hour: "2-digit", minute: "2-digit", second: "2-digit"}); - else if(parts[3]==="R") time=Math.round((Date.now() - (Number.parseInt(parts[1]) * 1000))/1000/60) + " minutes ago"; - } - - const timeElem=document.createElement("span"); - timeElem.classList.add("markdown-timestamp"); - timeElem.textContent=time; - span.appendChild(timeElem); - continue; - } - } - - if(txt[i] === "<" && (txt[i + 1] === ":" || (txt[i + 1] === "a" && txt[i + 2] === ":"))){ - let found=false; - const build = txt[i + 1] === "a" ? ["<","a",":"] : ["<",":"]; - let j = i+build.length; - for(; txt[j] !== void 0; j++){ - build.push(txt[j]); - - if(txt[j]===">"){ - found=true; - break; - } - } - - if(found){ - const buildjoin=build.join(""); - const parts=buildjoin.match(/^<(a)?:\w+:(\d{10,30})>$/); - if(parts && parts[2]){ - appendcurrent(); - i=j; - const isEmojiOnly = txt.join("").trim()===buildjoin.trim(); - const owner=(this.owner instanceof Channel)?this.owner.guild:this.owner; - const emoji=new Emoji({name: buildjoin,id: parts[2],animated: Boolean(parts[1])},owner); - span.appendChild(emoji.getHTML(isEmojiOnly)); - - continue; - } - } - } - - if(txt[i] == "[" && !keep){ - let partsFound=0; - let j=i+1; - const build=["["]; - for(; txt[j] !== void 0; j++){ - build.push(txt[j]); - - if(partsFound === 0 && txt[j] === "]"){ - if(txt[j + 1] === "(" && - txt[j + 2] === "h" && txt[j + 3] === "t" && txt[j + 4] === "t" && txt[j + 5] === "p" && (txt[j + 6] === "s" || txt[j + 6] === ":") - ){ - partsFound++; - }else{ - break; - } - }else if(partsFound === 1 && txt[j] === ")"){ - partsFound++; - break; - } - } - - if(partsFound === 2){ - appendcurrent(); - - - const parts=build.join("").match(/^\[(.+)\]\((https?:.+?)( ('|").+('|"))?\)$/); - if(parts){ - const linkElem=document.createElement("a"); - if(URL.canParse(parts[2])){ - i=j; - MarkDown.safeLink(linkElem,parts[2]) - linkElem.textContent=parts[1]; - linkElem.target="_blank"; - linkElem.rel="noopener noreferrer"; - linkElem.title=(parts[3] ? parts[3].substring(2, parts[3].length - 1)+"\n\n" : "") + parts[2]; - span.appendChild(linkElem); - - continue; - } - } - } - } - - current.textContent+=txt[i]; - } - appendcurrent(); - return span; - } - static unspoil(e:any) : void{ - e.target.classList.remove("spoiler"); - e.target.classList.add("unspoiled"); - } - giveBox(box:HTMLDivElement){ - box.onkeydown=_=>{ - //console.log(_); - }; - let prevcontent=""; - box.onkeyup=_=>{ - const content=MarkDown.gatherBoxText(box); - if(content!==prevcontent){ - prevcontent=content; - this.txt=content.split(""); - this.boxupdate(box); - } - }; - box.onpaste=_=>{ - if(!_.clipboardData)return; - console.log(_.clipboardData.types); - const data=_.clipboardData.getData("text"); - - document.execCommand("insertHTML", false, data); - _.preventDefault(); - if(!box.onkeyup)return; - box.onkeyup(new KeyboardEvent("_")); - }; - } - boxupdate(box:HTMLElement){ - const restore = saveCaretPosition(box); - box.innerHTML=""; - box.append(this.makeHTML({keep: true})); - if(restore){ - restore(); - } - } - static gatherBoxText(element:HTMLElement):string{ - if(element.tagName.toLowerCase()==="img"){ - return(element as HTMLImageElement).alt; - } - if(element.tagName.toLowerCase()==="br"){ - return"\n"; - } - if(element.hasAttribute("real")){ - return element.getAttribute("real") as string; - } - let build=""; - for(const thing of element.childNodes){ - if(thing instanceof Text){ - const text=thing.textContent; - build+=text; - continue; - } - const text=this.gatherBoxText(thing as HTMLElement); - if(text){ - build+=text; - } - } - return build; - } - static readonly trustedDomains=new Set([location.host]) - static safeLink(elm:HTMLElement,url:string){ - if(URL.canParse(url)){ - const Url=new URL(url); - if(elm instanceof HTMLAnchorElement&&this.trustedDomains.has(Url.host)){ - elm.href=url; - elm.target="_blank"; - return; - } - elm.onmouseup=_=>{ - if(_.button===2) return; - console.log(":3") - function open(){ - const proxy=window.open(url, '_blank') - if(proxy&&_.button===1){ - proxy.focus(); - }else if(proxy){ - window.focus(); - } - } - if(this.trustedDomains.has(Url.host)){ - open(); - }else{ - const full=new Dialog([ - "vdiv", - ["title","You're leaving spacebar"], - ["text","You're going to "+Url.host+" are you sure you want to go there?"], - ["hdiv", - ["button","","Nevermind",_=>full.hide()], - ["button","","Go there",_=>{open();full.hide()}], - ["button","","Go there and trust in the future",_=>{ - open(); - full.hide(); - this.trustedDomains.add(Url.host); - }] - ] - ]); - full.show(); - } - } - }else{ - throw Error(url+" is not a valid URL") - } - } - static replace(base:HTMLElement,newelm:HTMLElement){ - const basechildren=base.children; - const newchildren=newelm.children; - for(const thing of newchildren){ - - } - } -} - -//solution from https://stackoverflow.com/questions/4576694/saving-and-restoring-caret-position-for-contenteditable-div -let text=""; -function saveCaretPosition (context){ - const selection = window.getSelection(); - if(!selection)return; - const range = selection.getRangeAt(0); - range.setStart(context, 0); - text=selection.toString(); - let len = text.length+1; - for(const str in text.split("\n")){ - if(str.length!==0){ - len--; - } - } - len+=+(text[text.length-1]==="\n"); - - return function restore(){ - if(!selection)return; - const pos = getTextNodeAtPosition(context, len); - selection.removeAllRanges(); - const range = new Range(); - range.setStart(pos.node, pos.position); - selection.addRange(range); - }; -} - -function getTextNodeAtPosition(root, index){ - const NODE_TYPE = NodeFilter.SHOW_TEXT; - const treeWalker = document.createTreeWalker(root, NODE_TYPE, elem=>{ - if(!elem.textContent)return 0; - if(index > elem.textContent.length){ - index -= elem.textContent.length; - return NodeFilter.FILTER_REJECT; - } - return NodeFilter.FILTER_ACCEPT; - }); - const c = treeWalker.nextNode(); - return{ - node: c? c: root, - position: index - }; -} -export{MarkDown}; diff --git a/webpage/member.ts b/webpage/member.ts deleted file mode 100644 index f92eaac..0000000 --- a/webpage/member.ts +++ /dev/null @@ -1,221 +0,0 @@ -import{User}from"./user.js"; -import{Role}from"./role.js"; -import{Guild}from"./guild.js"; -import{ SnowFlake }from"./snowflake.js"; -import{ memberjson, presencejson, userjson }from"./jsontypes.js"; -import{ Dialog }from"./dialog.js"; - -class Member extends SnowFlake{ - static already={}; - owner:Guild; - user:User; - roles:Role[]=[]; - nick:string; - private constructor(memberjson:memberjson,owner:Guild){ - super(memberjson.id); - this.owner=owner; - if(this.localuser.userMap.has(memberjson.id)){ - this.user=this.localuser.userMap.get(memberjson.id) as User; - }else if(memberjson.user){ - this.user=new User(memberjson.user,owner.localuser); - }else{ - throw new Error("Missing user object of this member"); - } - - for(const thing of Object.keys(memberjson)){ - if(thing==="guild"){ - continue; - } - if(thing==="owner"){ - continue; - } - if(thing==="roles"){ - for(const strrole of memberjson.roles){ - const role=this.guild.roleids.get(strrole); - if(!role) continue; - this.roles.push(role); - } - continue; - } - this[thing]=memberjson[thing]; - } - if(this.localuser.userMap.has(this?.id)){ - this.user=this.localuser.userMap.get(this?.id) as User; - } - this.roles.sort((a,b)=>{return (this.guild.roles.indexOf(a)-this.guild.roles.indexOf(b))}); - } - get guild(){ - return this.owner; - } - get localuser(){ - return this.guild.localuser; - } - get info(){ - return this.owner.info; - } - static async new(memberjson:memberjson,owner:Guild):Promise{ - let user:User; - if(owner.localuser.userMap.has(memberjson.id)){ - user=owner.localuser.userMap.get(memberjson.id) as User; - }else if(memberjson.user){ - user=new User(memberjson.user,owner.localuser); - }else{ - throw new Error("missing user object of this member"); - } - if(user.members.has(owner)){ - let memb=user.members.get(owner); - if(memb===undefined){ - memb=new Member(memberjson,owner); - user.members.set(owner,memb); - return memb; - }else if(memb instanceof Promise){ - return await memb;//I should do something else, though for now this is "good enough" - }else{ - return memb; - } - }else{ - const memb=new Member(memberjson,owner); - user.members.set(owner,memb); - return memb; - } - } - static async resolveMember(user:User,guild:Guild):Promise{ - const maybe=user.members.get(guild); - if(!user.members.has(guild)){ - const membpromise=guild.localuser.resolvemember(user.id,guild.id); - const promise=new Promise(async res=>{ - const membjson=await membpromise; - if(membjson===undefined){ - res(undefined); - }else{ - const member=new Member(membjson,guild); - const map=guild.localuser.presences; - member.getPresence(map.get(member.id)); - map.delete(member.id); - res(member); - return member; - } - }); - user.members.set(guild,promise); - } - if(maybe instanceof Promise){ - return await maybe; - }else{ - return maybe; - } - } - public getPresence(presence:presencejson|undefined){ - this.user.getPresence(presence); - } - /** - * @todo - */ - highInfo(){ - fetch(this.info.api+"/users/"+this.id+"/profile?with_mutual_guilds=true&with_mutual_friends_count=true&guild_id="+this.guild.id,{headers: this.guild.headers}); - } - hasRole(ID:string){ - console.log(this.roles,ID); - for(const thing of this.roles){ - if(thing.id===ID){ - return true; - } - } - return false; - } - getColor(){ - for(const thing of this.roles){ - const color=thing.getColor(); - if(color){ - return color; - } - } - return""; - } - isAdmin(){ - for(const role of this.roles){ - if(role.permissions.getPermission("ADMINISTRATOR")){ - return true; - } - } - return this.guild.properties.owner_id===this.user.id; - } - bind(html:HTMLElement){ - if(html.tagName==="SPAN"){ - if(!this){ - return; - } - /* - if(this.error){ - - } - */ - html.style.color=this.getColor(); - } - - //this.profileclick(html); - } - profileclick(html:HTMLElement){ - //to be implemented - } - get name(){ - return this.nick||this.user.username; - } - kick(){ - let reason=""; - const menu=new Dialog(["vdiv", - ["title","Kick "+this.name+" from "+this.guild.properties.name], - ["textbox","Reason:","",function(e:Event){ - reason=(e.target as HTMLInputElement).value; - }], - ["button","","submit",()=>{ - this.kickAPI(reason); - menu.hide(); - }] - ]); - menu.show(); - } - kickAPI(reason:string){ - const headers=structuredClone(this.guild.headers); - headers["x-audit-log-reason"]=reason; - fetch(`${this.info.api}/guilds/${this.guild.id}/members/${this.id}`,{ - method: "DELETE", - headers, - - }); - } - ban(){ - let reason=""; - const menu=new Dialog(["vdiv", - ["title","Ban "+this.name+" from "+this.guild.properties.name], - ["textbox","Reason:","",function(e:Event){ - reason=(e.target as HTMLInputElement).value; - }], - ["button","","submit",()=>{ - this.banAPI(reason); - menu.hide(); - }] - ]); - menu.show(); - } - banAPI(reason:string){ - const headers=structuredClone(this.guild.headers); - headers["x-audit-log-reason"]=reason; - fetch(`${this.info.api}/guilds/${this.guild.id}/bans/${this.id}`,{ - method: "PUT", - headers - - }); - } - hasPermission(name:string):boolean{ - if(this.isAdmin()){ - return true; - } - for(const thing of this.roles){ - if(thing.permissions.getPermission(name)){ - return true; - } - } - return false; - } -} -export{Member}; diff --git a/webpage/message.ts b/webpage/message.ts deleted file mode 100644 index 5f4795a..0000000 --- a/webpage/message.ts +++ /dev/null @@ -1,687 +0,0 @@ -import{Contextmenu}from"./contextmenu.js"; -import{User}from"./user.js"; -import{Member}from"./member.js"; -import{MarkDown}from"./markdown.js"; -import{Embed}from"./embed.js"; -import{ Channel }from"./channel.js"; -import{Localuser}from"./localuser.js"; -import{ Role }from"./role.js"; -import{File}from"./file.js"; -import{ SnowFlake }from"./snowflake.js"; -import{ memberjson, messagejson }from"./jsontypes.js"; -import{Emoji}from"./emoji.js"; -import { Dialog } from "./dialog.js"; - -class Message extends SnowFlake{ - static contextmenu=new Contextmenu("message menu"); - owner:Channel; - headers:Localuser["headers"]; - embeds:Embed[]; - author:User; - mentions:User[]; - mention_roles:Role[]; - attachments:File[];//probably should be its own class tbh, should be Attachments[] - message_reference; - type:number; - timestamp:number; - content:MarkDown; - static del:Promise; - static resolve:Function; - /* - weakdiv:WeakRef; - set div(e:HTMLDivElement){ - if(!e){ - this.weakdiv=null; - return; - } - this.weakdiv=new WeakRef(e); - } - get div(){ - return this.weakdiv?.deref(); - } - //*/ - div:HTMLDivElement|undefined; - member:Member|undefined; - reactions:messagejson["reactions"]; - static setup(){ - this.del=new Promise(_=>{ - this.resolve=_; - }); - Message.setupcmenu(); - } - static setupcmenu(){ - Message.contextmenu.addbutton("Copy raw text",function(this:Message){ - navigator.clipboard.writeText(this.content.rawString); - }); - Message.contextmenu.addbutton("Reply",function(this:Message){ - this.channel.setReplying(this); - }); - Message.contextmenu.addbutton("Copy message id",function(this:Message){ - navigator.clipboard.writeText(this.id); - }); - Message.contextmenu.addsubmenu("Add reaction",function(this:Message,arg,e:MouseEvent){ - Emoji.emojiPicker(e.x,e.y,this.localuser).then(_=>{ - this.reactionToggle(_); - }); - }); - Message.contextmenu.addbutton("Edit",function(this:Message){ - this.setEdit(); - },null,function(){ - return this.author.id===this.localuser.user.id; - }); - Message.contextmenu.addbutton("Delete message",function(this:Message){ - this.delete(); - },null,function(){ - return this.canDelete(); - }); - } - setEdit(){ - this.channel.editing=this; - const markdown=(document.getElementById("typebox") as HTMLDivElement)["markdown"] as MarkDown; - markdown.txt=this.content.rawString.split(""); - markdown.boxupdate(document.getElementById("typebox") as HTMLDivElement); - } - constructor(messagejson:messagejson,owner:Channel){ - super(messagejson.id); - this.owner=owner; - this.headers=this.owner.headers; - this.giveData(messagejson); - this.owner.messages.set(this.id,this); - } - reactionToggle(emoji:string|Emoji){ - let remove = false; - for(const thing of this.reactions){ - if(thing.emoji.name === emoji){ - remove = thing.me; - break; - } - } - let reactiontxt:string; - if(emoji instanceof Emoji){ - reactiontxt=`${emoji.name}:${emoji.id}`; - }else{ - reactiontxt=encodeURIComponent(emoji); - } - fetch(`${this.info.api}/channels/${this.channel.id}/messages/${this.id}/reactions/${reactiontxt}/@me`, { - method: remove ? "DELETE" : "PUT", - headers: this.headers - }); - } - giveData(messagejson:messagejson){ - const func=this.channel.infinite.snapBottom(); - for(const thing of Object.keys(messagejson)){ - if(thing==="attachments"){ - this.attachments=[]; - for(const thing of messagejson.attachments){ - this.attachments.push(new File(thing,this)); - } - continue; - }else if(thing==="content"){ - this.content=new MarkDown(messagejson[thing],this.channel); - continue; - }else if(thing ==="id"){ - continue; - }else if(thing==="member"){ - Member.new(messagejson.member as memberjson,this.guild).then(_=>{ - this.member=_ as Member; - }); - continue; - }else if(thing ==="embeds"){ - this.embeds=[]; - for(const thing in messagejson.embeds){ - this.embeds[thing]=new Embed(messagejson.embeds[thing],this); - } - continue; - } - this[thing]=messagejson[thing]; - } - if(messagejson.reactions?.length){ - console.log(messagejson.reactions,":3"); - } - - this.author=new User(messagejson.author,this.localuser); - for(const thing in messagejson.mentions){ - this.mentions[thing]=new User(messagejson.mentions[thing],this.localuser); - } - if(!this.member&&this.guild.id!=="@me"){ - this.author.resolvemember(this.guild).then(_=>{ - this.member=_; - }); - } - if(this.mentions.length||this.mention_roles.length){//currently mention_roles isn't implemented on the spacebar servers - console.log(this.mentions,this.mention_roles); - } - if(this.mentionsuser(this.localuser.user)){ - console.log(this); - } - if(this.div){ - this.generateMessage(); - } - func(); - } - canDelete(){ - return this.channel.hasPermission("MANAGE_MESSAGES")||this.author===this.localuser.user; - } - get channel(){ - return this.owner; - } - get guild(){ - return this.owner.guild; - } - get localuser(){ - return this.owner.localuser; - } - get info(){ - return this.owner.info; - } - messageevents(obj:HTMLDivElement){ - const func=Message.contextmenu.bindContextmenu(obj,this,undefined); - this.div=obj; - obj.classList.add("messagediv"); - } - deleteDiv(){ - if(!this.div)return; - try{ - this.div.remove(); - this.div=undefined; - }catch(e){ - console.error(e); - } - } - mentionsuser(userd:User|Member){ - if(userd instanceof User){ - return this.mentions.includes(userd); - }else if(userd instanceof Member){ - return this.mentions.includes(userd.user); - } - } - getimages(){ - const build:File[]=[]; - for(const thing of this.attachments){ - if(thing.content_type.startsWith("image/")){ - build.push(thing); - } - } - return build; - } - async edit(content){ - return await fetch(this.info.api+"/channels/"+this.channel.id+"/messages/"+this.id,{ - method: "PATCH", - headers: this.headers, - body: JSON.stringify({content}) - }); - } - delete(){ - fetch(`${this.info.api}/channels/${this.channel.id}/messages/${this.id}`,{ - headers: this.headers, - method: "DELETE", - }); - } - deleteEvent(){ - console.log("deleted") - if(this.div){ - this.div.remove(); - this.div.innerHTML=""; - this.div=undefined; - } - const prev=this.channel.idToPrev.get(this.id); - const next=this.channel.idToNext.get(this.id); - this.channel.idToPrev.delete(this.id); - this.channel.idToNext.delete(this.id); - this.channel.messages.delete(this.id); - if(prev&&next){ - this.channel.idToPrev.set(next,prev); - this.channel.idToNext.set(prev,next); - }else if(prev){ - this.channel.idToNext.delete(prev); - }else if(next){ - this.channel.idToPrev.delete(next); - } - if(prev){ - const prevmessage=this.channel.messages.get(prev); - if(prevmessage){ - prevmessage.generateMessage(); - } - } - if(this.channel.lastmessage===this||this.channel.lastmessageid===this.id){ - if(prev){ - this.channel.lastmessage=this.channel.messages.get(prev); - this.channel.lastmessageid=prev; - }else{ - this.channel.lastmessage=undefined; - this.channel.lastmessageid=undefined; - } - } - if(this.channel.lastreadmessageid===this.id){ - if(prev){ - this.channel.lastreadmessageid=prev; - }else{ - this.channel.lastreadmessageid=undefined; - } - } - console.log("deleted done") - } - reactdiv:WeakRef; - blockedPropigate(){ - const previd=this.channel.idToPrev.get(this.id); - if(!previd){ - this.generateMessage(); - return; - } - const premessage=this.channel.messages.get(previd); - if(premessage?.author===this.author){ - premessage.blockedPropigate(); - }else{ - this.generateMessage(); - } - } - generateMessage(premessage?:Message|undefined,ignoredblock=false){ - if(!this.div)return; - if(!premessage){ - premessage=this.channel.messages.get(this.channel.idToPrev.get(this.id) as string); - } - const div=this.div; - for(const user of this.mentions){ - if(user===this.localuser.user){ - div.classList.add("mentioned"); - } - } - if(this===this.channel.replyingto){ - div.classList.add("replying"); - } - div.innerHTML=""; - const build = document.createElement("div"); - - build.classList.add("flexltr","message"); - div.classList.remove("zeroheight"); - if(this.author.relationshipType===2){ - if(ignoredblock){ - if(premessage?.author!==this.author){ - const span=document.createElement("span"); - span.textContent="You have this user blocked, click to hide these messages."; - div.append(span); - span.classList.add("blocked"); - span.onclick=_=>{ - const scroll=this.channel.infinite.scrollTop; - let next:Message|undefined=this; - while(next?.author===this.author){ - next.generateMessage(); - next=this.channel.messages.get(this.channel.idToNext.get(next.id) as string); - } - if(this.channel.infinite.scollDiv&&scroll){ - this.channel.infinite.scollDiv.scrollTop=scroll; - } - }; - } - }else{ - div.classList.remove("topMessage"); - if(premessage?.author===this.author){ - div.classList.add("zeroheight"); - premessage.blockedPropigate(); - div.appendChild(build); - return div; - }else{ - build.classList.add("blocked","topMessage"); - const span=document.createElement("span"); - let count=1; - let next=this.channel.messages.get(this.channel.idToNext.get(this.id) as string); - while(next?.author===this.author){ - count++; - next=this.channel.messages.get(this.channel.idToNext.get(next.id) as string); - } - span.textContent=`You have this user blocked, click to see the ${count} blocked messages.`; - build.append(span); - span.onclick=_=>{ - const scroll=this.channel.infinite.scrollTop; - const func=this.channel.infinite.snapBottom(); - let next:Message|undefined=this; - while(next?.author===this.author){ - next.generateMessage(undefined,true); - next=this.channel.messages.get(this.channel.idToNext.get(next.id) as string); - console.log("loopy"); - } - if(this.channel.infinite.scollDiv&&scroll){ - func(); - this.channel.infinite.scollDiv.scrollTop=scroll; - } - }; - div.appendChild(build); - return div; - } - } - } - if(this.message_reference){ - const replyline=document.createElement("div"); - const line=document.createElement("hr"); - const minipfp=document.createElement("img"); - minipfp.classList.add("replypfp"); - replyline.appendChild(line); - replyline.appendChild(minipfp); - const username=document.createElement("span"); - replyline.appendChild(username); - const reply=document.createElement("div"); - username.classList.add("username"); - reply.classList.add("replytext"); - replyline.appendChild(reply); - const line2=document.createElement("hr"); - replyline.appendChild(line2); - line2.classList.add("reply"); - line.classList.add("startreply"); - replyline.classList.add("replyflex"); - this.channel.getmessage(this.message_reference.message_id).then(message=>{ - if(message.author.relationshipType===2){ - username.textContent="Blocked user"; - return; - } - const author=message.author; - reply.appendChild(message.content.makeHTML({stdsize: true})); - minipfp.src=author.getpfpsrc(); - author.bind(minipfp,this.guild); - username.textContent=author.username; - author.bind(username,this.guild); - }); - reply.onclick=_=>{ - this.channel.infinite.focus(this.message_reference.message_id); - }; - div.appendChild(replyline); - } - div.appendChild(build); - if({0: true,19: true}[this.type]||this.attachments.length!==0){ - const pfpRow = document.createElement("div"); - pfpRow.classList.add("flexltr"); - let pfpparent, current; - if(premessage!=null){ - pfpparent??=premessage; - let pfpparent2=pfpparent.all; - pfpparent2??=pfpparent; - const old=(new Date(pfpparent2.timestamp).getTime())/1000; - const newt=(new Date(this.timestamp).getTime())/1000; - current=(newt-old)>600; - } - const combine=(premessage?.author!=this.author)||(current)||this.message_reference; - if(combine){ - const pfp=this.author.buildpfp(); - this.author.bind(pfp,this.guild,false); - pfpRow.appendChild(pfp); - }else{ - div["pfpparent"]=pfpparent; - } - pfpRow.classList.add("pfprow"); - build.appendChild(pfpRow); - const text=document.createElement("div"); - text.classList.add("flexttb"); - const texttxt=document.createElement("div"); - texttxt.classList.add("commentrow","flexttb"); - text.appendChild(texttxt); - if(combine){ - const username=document.createElement("span"); - username.classList.add("username"); - this.author.bind(username,this.guild); - div.classList.add("topMessage"); - username.textContent=this.author.username; - const userwrap=document.createElement("div"); - userwrap.classList.add("flexltr"); - userwrap.appendChild(username); - if(this.author.bot){ - const username=document.createElement("span"); - username.classList.add("bot"); - username.textContent="BOT"; - userwrap.appendChild(username); - } - const time=document.createElement("span"); - time.textContent=" "+formatTime(new Date(this.timestamp)); - time.classList.add("timestamp"); - userwrap.appendChild(time); - - texttxt.appendChild(userwrap); - }else{ - div.classList.remove("topMessage"); - } - const messaged=this.content.makeHTML(); - div["txt"]=messaged; - const messagedwrap=document.createElement("div"); - messagedwrap.classList.add("flexttb"); - messagedwrap.appendChild(messaged); - texttxt.appendChild(messagedwrap); - - build.appendChild(text); - if(this.attachments.length){ - console.log(this.attachments); - const attach = document.createElement("div"); - attach.classList.add("flexltr"); - for(const thing of this.attachments){ - attach.appendChild(thing.getHTML()); - } - messagedwrap.appendChild(attach); - } - if(this.embeds.length){ - const embeds = document.createElement("div"); - embeds.classList.add("flexltr"); - for(const thing of this.embeds){ - embeds.appendChild(thing.generateHTML()); - } - messagedwrap.appendChild(embeds); - } - // - }else if(this.type===7){ - const text=document.createElement("div"); - text.classList.add("flexttb"); - const texttxt=document.createElement("div"); - text.appendChild(texttxt); - build.appendChild(text); - texttxt.classList.add("flexltr"); - const messaged=document.createElement("span"); - div["txt"]=messaged; - messaged.textContent="welcome: "; - texttxt.appendChild(messaged); - - const username=document.createElement("span"); - username.textContent=this.author.username; - //this.author.profileclick(username); - this.author.bind(username,this.guild); - texttxt.appendChild(username); - username.classList.add("username"); - - const time=document.createElement("span"); - time.textContent=" "+formatTime(new Date(this.timestamp)); - time.classList.add("timestamp"); - texttxt.append(time); - div.classList.add("topMessage"); - } - const reactions=document.createElement("div"); - reactions.classList.add("flexltr","reactiondiv"); - this.reactdiv=new WeakRef(reactions); - this.updateReactions(); - div.append(reactions); - this.bindButtonEvent(); - return(div); - } - bindButtonEvent(){ - if(this.div){ - let buttons:HTMLDivElement|undefined; - this.div.onmouseenter=_=>{ - if(buttons){ - buttons.remove(); - buttons=undefined; - } - if(this.div){ - buttons=document.createElement("div"); - buttons.classList.add("messageButtons","flexltr"); - if(this.channel.hasPermission("SEND_MESSAGES")){ - const container=document.createElement("div"); - const reply=document.createElement("span"); - reply.classList.add("svgtheme", "svg-reply", "svgicon"); - container.append(reply); - buttons.append(container); - container.onclick=_=>{ - this.channel.setReplying(this); - } - } - if(this.author===this.localuser.user){ - const container=document.createElement("div"); - const edit=document.createElement("span"); - edit.classList.add("svgtheme", "svg-edit", "svgicon"); - container.append(edit); - buttons.append(container); - container.onclick=_=>{ - this.setEdit(); - } - } - if(this.canDelete()){ - const container=document.createElement("div"); - const reply=document.createElement("span"); - reply.classList.add("svgtheme", "svg-delete", "svgicon"); - container.append(reply); - buttons.append(container); - container.onclick=_=>{ - if(_.shiftKey){ - this.delete(); - return; - } - const diaolog=new Dialog(["hdiv",["title","are you sure you want to delete this?"],["button","","yes",()=>{this.delete();diaolog.hide()}],["button","","no",()=>{diaolog.hide()}]]); - diaolog.show(); - } - } - if(buttons.childNodes.length!==0){ - this.div.append(buttons); - } - } - } - this.div.onmouseleave=_=>{ - if(buttons){ - buttons.remove(); - buttons=undefined; - } - } - } - } - updateReactions(){ - const reactdiv=this.reactdiv.deref(); - if(!reactdiv)return; - const func=this.channel.infinite.snapBottom(); - reactdiv.innerHTML=""; - for(const thing of this.reactions){ - const reaction=document.createElement("div"); - reaction.classList.add("reaction"); - if(thing.me){ - reaction.classList.add("meReacted"); - } - let emoji:HTMLElement; - if(thing.emoji.id || /\d{17,21}/.test(thing.emoji.name)){ - if(/\d{17,21}/.test(thing.emoji.name)) thing.emoji.id=thing.emoji.name;//Should stop being a thing once the server fixes this bug - const emo=new Emoji(thing.emoji as {name:string,id:string,animated:boolean},this.guild); - emoji=emo.getHTML(false); - }else{ - emoji=document.createElement("p"); - emoji.textContent=thing.emoji.name; - } - const count=document.createElement("p"); - count.textContent=""+thing.count; - count.classList.add("reactionCount"); - reaction.append(count); - reaction.append(emoji); - reactdiv.append(reaction); - - reaction.onclick=_=>{ - this.reactionToggle(thing.emoji.name); - }; - } - func(); - } - reactionAdd(data:{name:string},member:Member|{id:string}){ - for(const thing of this.reactions){ - if(thing.emoji.name===data.name){ - thing.count++; - if(member.id===this.localuser.user.id){ - thing.me=true; - this.updateReactions(); - return; - } - } - } - this.reactions.push({ - count: 1, - emoji: data, - me: member.id===this.localuser.user.id - }); - this.updateReactions(); - } - reactionRemove(data:{name:string},id:string){ - console.log("test"); - for(const i in this.reactions){ - const thing=this.reactions[i]; - console.log(thing,data); - if(thing.emoji.name===data.name){ - thing.count--; - if(thing.count===0){ - this.reactions.splice(Number(i),1); - this.updateReactions(); - return; - } - if(id===this.localuser.user.id){ - thing.me=false; - this.updateReactions(); - return; - } - } - } - } - reactionRemoveAll(){ - this.reactions = []; - this.updateReactions(); - } - reactionRemoveEmoji(emoji:Emoji){ - for(const i in this.reactions){ - const reaction = this.reactions[i]; - if((reaction.emoji.id && reaction.emoji.id == emoji.id) || (!reaction.emoji.id && reaction.emoji.name == emoji.name)){ - this.reactions.splice(Number(i), 1); - this.updateReactions(); - break; - } - } - } - buildhtml(premessage?:Message|undefined):HTMLElement{ - if(this.div){ - console.error(`HTML for ${this.id} already exists, aborting`);return this.div; - } - try{ - const div=document.createElement("div"); - this.div=div; - this.messageevents(div); - return this.generateMessage(premessage) as HTMLElement; - }catch(e){ - console.error(e); - } - return this.div as HTMLElement; - } -} -let now:string; -let yesterdayStr:string; - -function formatTime(date:Date){ - updateTimes(); - const datestring=date.toLocaleDateString(); - const formatTime = (date:Date)=>date.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }); - - if(datestring===now){ - return`Today at ${formatTime(date)}`; - }else if(datestring===yesterdayStr){ - return`Yesterday at ${formatTime(date)}`; - }else{ - return`${date.toLocaleDateString()} at ${formatTime(date)}`; - } -} -let tomorrow=0; -updateTimes(); -function updateTimes(){ - if(tomorrow>BigInt(b))&1n); - } - setPermissionbit(b:number,state:boolean,big:bigint) : bigint{ - const bit=1n<{ - e.json().then(e=>{ - if(e.captcha_sitekey){ - const capt=document.getElementById("h-captcha"); - if(!capt.children.length){ - const capty=document.createElement("div"); - capty.classList.add("h-captcha"); - - capty.setAttribute("data-sitekey", e.captcha_sitekey); - const script=document.createElement("script"); - script.src="https://js.hcaptcha.com/1/api.js"; - capt.append(script); - capt.append(capty); - }else{ - eval("hcaptcha.reset()"); - } - return; - } - if(!e.token){ - console.log(e); - if(e.errors.consent){ - error(elements[6],e.errors.consent._errors[0].message); - }else if(e.errors.password){ - error(elements[3],"Password: "+e.errors.password._errors[0].message); - }else if(e.errors.username){ - error(elements[2],"Username: "+e.errors.username._errors[0].message); - }else if(e.errors.email){ - error(elements[1],"Email: "+e.errors.email._errors[0].message); - }else if(e.errors.date_of_birth){ - error(elements[5],"Date of Birth: "+e.errors.date_of_birth._errors[0].message); - }else{ - document.getElementById("wrong").textContent=e.errors[Object.keys(e.errors)[0]]._errors[0].message; - } - }else{ - adduser({serverurls: JSON.parse(localStorage.getItem("instanceinfo")),email,token: e.token}).username=username; - localStorage.setItem("token",e.token); - const redir=new URLSearchParams(window.location.search).get("goback"); - if(redir){ - window.location.href = redir; - }else{ - window.location.href = "/channels/@me"; - } - } - }); - }); - //document.getElementById("wrong").textContent=h; - // console.log(h); -} -function error(e:HTMLFormElement,message:string){ - const p=e.parentElement; - let element=p.getElementsByClassName("suberror")[0] as HTMLElement; - if(!element){ - const div=document.createElement("div"); - div.classList.add("suberror","suberrora"); - p.append(div); - element=div; - }else{ - element.classList.remove("suberror"); - setTimeout(_=>{ - element.classList.add("suberror"); - },100); - } - element.textContent=message; -} -let TOSa=document.getElementById("TOSa"); -async function tosLogic(){ - const apiurl=new URL(JSON.parse(localStorage.getItem("instanceinfo")).api); - const tosPage=(await (await fetch(apiurl.toString()+"/ping")).json()).instance.tosPage; - if(tosPage){ - document.getElementById("TOSbox").innerHTML="I agree to the Terms of Service:"; - TOSa=document.getElementById("TOSa"); - (TOSa as HTMLAnchorElement).href=tosPage; - }else{ - document.getElementById("TOSbox").textContent="This instance has no Terms of Service, accept ToS anyways:"; - TOSa=null; - } - console.log(tosPage); -} -tosLogic(); - -checkInstance["alt"]=tosLogic; diff --git a/webpage/role.ts b/webpage/role.ts deleted file mode 100644 index 17863f1..0000000 --- a/webpage/role.ts +++ /dev/null @@ -1,169 +0,0 @@ - -import{Permissions}from"./permissions.js"; -import{Localuser}from"./localuser.js"; -import{Guild}from"./guild.js"; -import{ SnowFlake }from"./snowflake.js"; -import{ rolesjson }from"./jsontypes.js"; -class Role extends SnowFlake{ - permissions:Permissions; - owner:Guild; - color:number; - name:string; - info:Guild["info"]; - hoist:boolean; - icon:string; - mentionable:boolean; - unicode_emoji:string; - headers:Guild["headers"]; - constructor(json:rolesjson, owner:Guild){ - super(json.id); - this.headers=owner.headers; - this.info=owner.info; - for(const thing of Object.keys(json)){ - if(thing==="id"){ - continue; - } - this[thing]=json[thing]; - } - this.permissions=new Permissions(json.permissions); - this.owner=owner; - } - get guild():Guild{ - return this.owner; - } - get localuser():Localuser{ - return this.guild.localuser; - } - getColor():string|null{ - if(this.color===0){ - return null; - } - return`#${this.color.toString(16)}`; - } -} -export{Role}; -import{Options}from"./settings.js"; -class PermissionToggle implements OptionsElement{ - readonly rolejson:{name:string,readableName:string,description:string}; - permissions:Permissions; - owner:Options; - value:number; - constructor(roleJSON:PermissionToggle["rolejson"],permissions:Permissions,owner:Options){ - this.rolejson=roleJSON; - this.permissions=permissions; - this.owner=owner; - } - watchForChange(){} - generateHTML():HTMLElement{ - const div=document.createElement("div"); - div.classList.add("setting"); - const name=document.createElement("span"); - name.textContent=this.rolejson.readableName; - name.classList.add("settingsname"); - div.append(name); - - - div.append(this.generateCheckbox()); - const p=document.createElement("p"); - p.textContent=this.rolejson.description; - div.appendChild(p); - return div; - } - generateCheckbox():HTMLElement{ - const div=document.createElement("div"); - div.classList.add("tritoggle"); - const state=this.permissions.getPermission(this.rolejson.name); - - const on=document.createElement("input"); - on.type="radio"; - on.name=this.rolejson.name; - div.append(on); - if(state===1){ - on.checked=true; - } - on.onclick=_=>{ - this.permissions.setPermission(this.rolejson.name,1); - this.owner.changed(); - }; - - const no=document.createElement("input"); - no.type="radio"; - no.name=this.rolejson.name; - div.append(no); - if(state===0){ - no.checked=true; - } - no.onclick=_=>{ - this.permissions.setPermission(this.rolejson.name,0); - this.owner.changed(); - }; - if(this.permissions.hasDeny){ - const off=document.createElement("input"); - off.type="radio"; - off.name=this.rolejson.name; - div.append(off); - if(state===-1){ - off.checked=true; - } - off.onclick=_=>{ - this.permissions.setPermission(this.rolejson.name,-1); - this.owner.changed(); - }; - } - return div; - } - submit(){ - - } -} -import{ OptionsElement,Buttons }from"./settings.js"; -class RoleList extends Buttons{ - readonly permissions:[Role,Permissions][]; - permission:Permissions; - readonly guild:Guild; - readonly channel:boolean; - readonly declare buttons:[string,string][]; - readonly options:Options; - onchange:Function; - curid:string; - constructor(permissions:[Role,Permissions][],guild:Guild,onchange:Function,channel=false){ - super("Roles"); - this.guild=guild; - this.permissions=permissions; - this.channel=channel; - this.onchange=onchange; - const options=new Options("",this); - if(channel){ - this.permission=new Permissions("0","0"); - }else{ - this.permission=new Permissions("0"); - } - for(const thing of Permissions.info){ - options.options.push(new PermissionToggle(thing,this.permission,options)); - } - for(const i of permissions){ - console.log(i); - this.buttons.push([i[0].name,i[0].id]); - } - this.options=options; - } - handleString(str:string):HTMLElement{ - this.curid=str; - const arr=this.permissions.find(_=>_[0].id===str); - if(arr){ - const perm=arr[1]; - this.permission.deny=perm.deny; - this.permission.allow=perm.allow; - const role=this.permissions.find(e=>e[0].id===str); - if(role){ - this.options.name=role[0].name; - this.options.haschanged=false; - } - } - return this.options.generateHTML(); - } - save(){ - this.onchange(this.curid,this.permission); - } -} -export{RoleList}; diff --git a/webpage/service.ts b/webpage/service.ts deleted file mode 100644 index 2f274aa..0000000 --- a/webpage/service.ts +++ /dev/null @@ -1,93 +0,0 @@ -function deleteoldcache(){ - caches.delete("cache"); - console.log("this ran :P"); -} - -async function putInCache(request, response){ - console.log(request,response); - const cache = await caches.open("cache"); - console.log("Grabbed"); - try{ - console.log(await cache.put(request, response)); - }catch(error){ - console.error(error); - } -} -console.log("test"); - -let lastcache; -self.addEventListener("activate", async event=>{ - console.log("test2"); - checkCache(); -}); -async function checkCache(){ - if(checkedrecently){ - return; - } - const promise=await caches.match("/getupdates"); - if(promise){ - lastcache= await promise.text(); - } - console.log(lastcache); - fetch("/getupdates").then(async data=>{ - const text=await data.clone().text(); - console.log(text,lastcache); - if(lastcache!==text){ - deleteoldcache(); - putInCache("/getupdates",data.clone()); - } - checkedrecently=true; - setTimeout(_=>{ - checkedrecently=false; - },1000*60*30); - }); -} -var checkedrecently=false; -function samedomain(url){ - return new URL(url).origin===self.origin; -} -function isindexhtml(url){ - console.log(url); - if(new URL(url).pathname.startsWith("/channels")){ - return true; - } - return false; -} -async function getfile(event){ - checkCache(); - if(!samedomain(event.request.url)){ - return await fetch(event.request.clone()); - } - const responseFromCache = await caches.match(event.request.url); - console.log(responseFromCache,caches); - if(responseFromCache){ - console.log("cache hit"); - return responseFromCache; - } - if(isindexhtml(event.request.url)){ - console.log("is index.html"); - const responseFromCache = await caches.match("/index.html"); - if(responseFromCache){ - console.log("cache hit"); - return responseFromCache; - } - const responseFromNetwork = await fetch("/index.html"); - await putInCache("/index.html",responseFromNetwork.clone()); - return responseFromNetwork; - } - const responseFromNetwork = await fetch(event.request.clone()); - console.log(event.request.clone()); - await putInCache(event.request.clone(),responseFromNetwork.clone()); - try{ - return responseFromNetwork; - }catch(e){ - console.error(e); - } -} -self.addEventListener("fetch", (event:any)=>{ - try{ - event.respondWith(getfile(event)); - }catch(e){ - console.error(e); - } -}); diff --git a/webpage/settings.ts b/webpage/settings.ts deleted file mode 100644 index 5edb3ff..0000000 --- a/webpage/settings.ts +++ /dev/null @@ -1,945 +0,0 @@ - -interface OptionsElement {// - generateHTML():HTMLElement; - submit:()=>void; - readonly watchForChange:(func:(arg1:x)=>void)=>void; - value:x; -} -//future me stuff -class Buttons implements OptionsElement{ - readonly name:string; - readonly buttons:[string,Options|string][]; - buttonList:HTMLDivElement; - warndiv:HTMLElement; - value:unknown; - constructor(name:string){ - this.buttons=[]; - this.name=name; - } - add(name:string,thing?:Options|undefined){ - if(!thing){ - thing=new Options(name,this); - } - this.buttons.push([name,thing]); - return thing; - } - generateHTML(){ - const buttonList=document.createElement("div"); - buttonList.classList.add("Buttons"); - buttonList.classList.add("flexltr"); - this.buttonList=buttonList; - const htmlarea=document.createElement("div"); - htmlarea.classList.add("flexgrow"); - const buttonTable=document.createElement("div"); - buttonTable.classList.add("flexttb","settingbuttons"); - for(const thing of this.buttons){ - const button=document.createElement("button"); - button.classList.add("SettingsButton"); - button.textContent=thing[0]; - button.onclick=_=>{ - this.generateHTMLArea(thing[1],htmlarea); - if(this.warndiv){ - this.warndiv.remove(); - } - }; - buttonTable.append(button); - } - this.generateHTMLArea(this.buttons[0][1],htmlarea); - buttonList.append(buttonTable); - buttonList.append(htmlarea); - return buttonList; - } - handleString(str:string):HTMLElement{ - const div=document.createElement("span"); - div.textContent=str; - return div; - } - private generateHTMLArea(buttonInfo:Options|string,htmlarea:HTMLElement){ - let html:HTMLElement; - if(buttonInfo instanceof Options){ - buttonInfo.subOptions=undefined; - html=buttonInfo.generateHTML(); - }else{ - html=this.handleString(buttonInfo); - } - htmlarea.innerHTML=""; - htmlarea.append(html); - } - changed(html:HTMLElement){ - this.warndiv=html; - this.buttonList.append(html); - } - watchForChange(){} - save(){} - submit(){ - - } -} - - -class TextInput implements OptionsElement{ - readonly label:string; - readonly owner:Options; - readonly onSubmit:(str:string)=>void; - value:string; - input:WeakRef; - password:boolean; - constructor(label:string,onSubmit:(str:string)=>void,owner:Options,{initText="",password=false}={}){ - this.label=label; - this.value=initText; - this.owner=owner; - this.onSubmit=onSubmit; - this.password=password; - } - generateHTML():HTMLDivElement{ - const div=document.createElement("div"); - const span=document.createElement("span"); - span.textContent=this.label; - div.append(span); - const input=document.createElement("input"); - input.value=this.value; - input.type=this.password?"password":"text"; - input.oninput=this.onChange.bind(this); - this.input=new WeakRef(input); - div.append(input); - return div; - } - private onChange(ev:Event){ - this.owner.changed(); - const input=this.input.deref(); - if(input){ - const value=input.value as string; - this.onchange(value); - this.value=value; - } - } - onchange:(str:string)=>void=_=>{}; - watchForChange(func:(str:string)=>void){ - this.onchange=func; - } - submit(){ - this.onSubmit(this.value); - } -} - -class SettingsText implements OptionsElement{ - readonly onSubmit:(str:string)=>void; - value:void; - readonly text:string; - constructor(text:string){ - this.text=text; - } - generateHTML():HTMLSpanElement{ - const span=document.createElement("span"); - span.innerText=this.text; - return span; - } - watchForChange(){} - submit(){} -} -class SettingsTitle implements OptionsElement{ - readonly onSubmit:(str:string)=>void; - value:void; - readonly text:string; - constructor(text:string){ - this.text=text; - } - generateHTML():HTMLSpanElement{ - const span=document.createElement("h2"); - span.innerText=this.text; - return span; - } - watchForChange(){} - submit(){} -} -class CheckboxInput implements OptionsElement{ - readonly label:string; - readonly owner:Options; - readonly onSubmit:(str:boolean)=>void; - value:boolean; - input:WeakRef; - constructor(label:string,onSubmit:(str:boolean)=>void,owner:Options,{initState=false}={}){ - this.label=label; - this.value=initState; - this.owner=owner; - this.onSubmit=onSubmit; - } - generateHTML():HTMLDivElement{ - const div=document.createElement("div"); - const span=document.createElement("span"); - span.textContent=this.label; - div.append(span); - const input=document.createElement("input"); - input.type="checkbox"; - input.checked=this.value; - input.oninput=this.onChange.bind(this); - this.input=new WeakRef(input); - div.append(input); - return div; - } - private onChange(ev:Event){ - this.owner.changed(); - const input=this.input.deref(); - if(input){ - const value=input.checked as boolean; - this.onchange(value); - this.value=value; - } - } - onchange:(str:boolean)=>void=_=>{}; - watchForChange(func:(str:boolean)=>void){ - this.onchange=func; - } - submit(){ - this.onSubmit(this.value); - } -} - -class ButtonInput implements OptionsElement{ - readonly label:string; - readonly owner:Options; - readonly onClick:()=>void; - textContent:string; - value: void; - constructor(label:string,textContent:string,onClick:()=>void,owner:Options,{}={}){ - this.label=label; - this.owner=owner; - this.onClick=onClick; - this.textContent=textContent; - } - generateHTML():HTMLDivElement{ - const div=document.createElement("div"); - const span=document.createElement("span"); - span.textContent=this.label; - div.append(span); - const button=document.createElement("button"); - button.textContent=this.textContent; - button.onclick=this.onClickEvent.bind(this); - div.append(button); - return div; - } - private onClickEvent(ev:Event){ - this.onClick(); - } - watchForChange(){} - submit(){} -} - -class ColorInput implements OptionsElement{ - readonly label:string; - readonly owner:Options; - readonly onSubmit:(str:string)=>void; - colorContent:string; - input:WeakRef; - value: string; - constructor(label:string,onSubmit:(str:string)=>void,owner:Options,{initColor=""}={}){ - this.label=label; - this.colorContent=initColor; - this.owner=owner; - this.onSubmit=onSubmit; - } - generateHTML():HTMLDivElement{ - const div=document.createElement("div"); - const span=document.createElement("span"); - span.textContent=this.label; - div.append(span); - const input=document.createElement("input"); - input.value=this.colorContent; - input.type="color"; - input.oninput=this.onChange.bind(this); - this.input=new WeakRef(input); - div.append(input); - return div; - } - private onChange(ev:Event){ - this.owner.changed(); - const input=this.input.deref(); - if(input){ - const value=input.value as string; - this.value=value; - this.onchange(value); - this.colorContent=value; - } - } - onchange:(str:string)=>void=_=>{}; - watchForChange(func:(str:string)=>void){ - this.onchange=func; - } - submit(){ - this.onSubmit(this.colorContent); - } -} - -class SelectInput implements OptionsElement{ - readonly label:string; - readonly owner:Options; - readonly onSubmit:(str:number)=>void; - options:string[]; - index:number; - select:WeakRef; - get value(){ - return this.index; - } - constructor(label:string,onSubmit:(str:number)=>void,options:string[],owner:Options,{defaultIndex=0}={}){ - this.label=label; - this.index=defaultIndex; - this.owner=owner; - this.onSubmit=onSubmit; - this.options=options; - } - generateHTML():HTMLDivElement{ - const div=document.createElement("div"); - const span=document.createElement("span"); - span.textContent=this.label; - div.append(span); - const select=document.createElement("select"); - - select.onchange=this.onChange.bind(this); - for(const thing of this.options){ - const option = document.createElement("option"); - option.textContent=thing; - select.appendChild(option); - } - this.select=new WeakRef(select); - select.selectedIndex=this.index; - div.append(select); - return div; - } - private onChange(ev:Event){ - this.owner.changed(); - const select=this.select.deref(); - if(select){ - const value=select.selectedIndex; - this.onchange(value); - this.index=value; - } - } - onchange:(str:number)=>void=_=>{}; - watchForChange(func:(str:number)=>void){ - this.onchange=func; - } - submit(){ - this.onSubmit(this.index); - } -} -class MDInput implements OptionsElement{ - readonly label:string; - readonly owner:Options; - readonly onSubmit:(str:string)=>void; - value:string; - input:WeakRef; - constructor(label:string,onSubmit:(str:string)=>void,owner:Options,{initText=""}={}){ - this.label=label; - this.value=initText; - this.owner=owner; - this.onSubmit=onSubmit; - } - generateHTML():HTMLDivElement{ - const div=document.createElement("div"); - const span=document.createElement("span"); - span.textContent=this.label; - div.append(span); - div.append(document.createElement("br")); - const input=document.createElement("textarea"); - input.value=this.value; - input.oninput=this.onChange.bind(this); - this.input=new WeakRef(input); - div.append(input); - return div; - } - onChange(ev:Event){ - this.owner.changed(); - const input=this.input.deref(); - if(input){ - const value=input.value as string; - this.onchange(value); - this.value=value; - } - } - onchange:(str:string)=>void=_=>{}; - watchForChange(func:(str:string)=>void){ - this.onchange=func; - } - submit(){ - this.onSubmit(this.value); - } -} -class FileInput implements OptionsElement{ - readonly label:string; - readonly owner:Options; - readonly onSubmit:(str:FileList|null)=>void; - input:WeakRef; - value:FileList|null; - clear:boolean; - constructor(label:string,onSubmit:(str:FileList)=>void,owner:Options,{clear=false}={}){ - this.label=label; - this.owner=owner; - this.onSubmit=onSubmit; - this.clear=clear; - } - generateHTML():HTMLDivElement{ - const div=document.createElement("div"); - const span=document.createElement("span"); - span.textContent=this.label; - div.append(span); - const input=document.createElement("input"); - input.type="file"; - input.oninput=this.onChange.bind(this); - this.input=new WeakRef(input); - div.append(input); - if(this.clear){ - const button=document.createElement("button"); - button.textContent="Clear"; - button.onclick=_=>{ - if(this.onchange){ - this.onchange(null); - } - this.value=null; - this.owner.changed(); - }; - div.append(button); - } - return div; - } - onChange(ev:Event){ - this.owner.changed(); - const input=this.input.deref(); - if(input){ - this.value=input.files; - if(this.onchange){ - this.onchange(input.files); - } - } - } - onchange:((str:FileList|null)=>void)|null=null; - watchForChange(func:(str:FileList|null)=>void){ - this.onchange=func; - } - submit(){ - const input=this.input.deref(); - if(input){ - this.onSubmit(input.files); - } - } -} - -class HtmlArea implements OptionsElement{ - submit: () => void; - html:(()=>HTMLElement)|HTMLElement; - value:void; - constructor(html:(()=>HTMLElement)|HTMLElement,submit:()=>void){ - this.submit=submit; - this.html=html; - } - generateHTML(): HTMLElement{ - if(this.html instanceof Function){ - return this.html(); - }else{ - return this.html; - } - } - watchForChange(){} -} -class Options implements OptionsElement{ - name:string; - haschanged=false; - readonly options:OptionsElement[]; - readonly owner:Buttons|Options|Form; - readonly ltr:boolean; - value:void; - readonly html:WeakMap,WeakRef>=new WeakMap(); - container:WeakRef=new WeakRef(document.createElement("div")); - constructor(name:string,owner:Buttons|Options|Form,{ltr=false}={}){ - this.name=name; - this.options=[]; - this.owner=owner; - this.ltr=ltr; - } - removeAll(){ - while(this.options.length){ - this.options.pop(); - } - const container=this.container.deref(); - if(container){ - container.innerHTML=""; - } - } - watchForChange(){} - addOptions(name:string,{ltr=false}={}){ - const options=new Options(name,this,{ltr}); - this.options.push(options); - this.generate(options); - return options; - } - subOptions:Options|Form|undefined; - addSubOptions(name:string,{ltr=false}={}){ - const options=new Options(name,this,{ltr}); - this.subOptions=options; - const container=this.container.deref(); - if(container){ - this.generateContainter(); - }else{ - throw new Error("Tried to make a subOptions when the options weren't rendered"); - } - return options; - } - addSubForm(name:string,onSubmit:((arg1:object)=>void),{ltr=false,submitText="Submit",fetchURL="",headers={},method="POST",traditionalSubmit=false}={}){ - const options=new Form(name,this,onSubmit,{ltr,submitText,fetchURL,headers,method,traditionalSubmit}); - this.subOptions=options; - const container=this.container.deref(); - if(container){ - this.generateContainter(); - }else{ - throw new Error("Tried to make a subForm when the options weren't rendered"); - } - return options; - } - returnFromSub(){ - this.subOptions=undefined; - this.generateContainter(); - } - addSelect(label:string,onSubmit:(str:number)=>void,selections:string[],{defaultIndex=0}={}){ - const select=new SelectInput(label,onSubmit,selections,this,{defaultIndex}); - this.options.push(select); - this.generate(select); - return select; - } - addFileInput(label:string,onSubmit:(files:FileList)=>void,{clear=false}={}){ - const FI=new FileInput(label,onSubmit,this,{clear}); - this.options.push(FI); - this.generate(FI); - return FI; - } - addTextInput(label:string,onSubmit:(str:string)=>void,{initText="",password=false}={}){ - const textInput=new TextInput(label,onSubmit,this,{initText,password}); - this.options.push(textInput); - this.generate(textInput); - return textInput; - } - addColorInput(label:string,onSubmit:(str:string)=>void,{initColor=""}={}){ - const colorInput=new ColorInput(label,onSubmit,this,{initColor}); - this.options.push(colorInput); - this.generate(colorInput); - return colorInput; - } - addMDInput(label:string,onSubmit:(str:string)=>void,{initText=""}={}){ - const mdInput=new MDInput(label,onSubmit,this,{initText}); - this.options.push(mdInput); - this.generate(mdInput); - return mdInput; - } - addHTMLArea(html:(()=>HTMLElement)|HTMLElement,submit:()=>void=()=>{}){ - const htmlarea=new HtmlArea(html,submit); - this.options.push(htmlarea); - this.generate(htmlarea); - return htmlarea; - } - addButtonInput(label:string,textContent:string,onSubmit:()=>void){ - const button=new ButtonInput(label,textContent,onSubmit,this); - this.options.push(button); - this.generate(button); - return button; - } - addCheckboxInput(label:string,onSubmit:(str:boolean)=>void,{initState=false}={}){ - const box=new CheckboxInput(label,onSubmit,this,{initState}); - this.options.push(box); - this.generate(box); - return box; - } - addText(str:string){ - const text=new SettingsText(str); - this.options.push(text); - this.generate(text); - return text; - } - addTitle(str:string){ - const text=new SettingsTitle(str); - this.options.push(text); - this.generate(text); - return text; - } - addForm(name:string,onSubmit:((arg1:object)=>void),{ltr=false,submitText="Submit",fetchURL="",headers={},method="POST",traditionalSubmit=false}={}){ - const options=new Form(name,this,onSubmit,{ltr,submitText,fetchURL,headers,method,traditionalSubmit}); - this.options.push(options); - this.generate(options); - return options; - } - generate(elm:OptionsElement){ - const container=this.container.deref(); - if(container){ - const div=document.createElement("div"); - if(!(elm instanceof Options)){ - div.classList.add("optionElement"); - } - const html=elm.generateHTML(); - div.append(html); - this.html.set(elm,new WeakRef(div)); - container.append(div); - } - } - title:WeakRef=new WeakRef(document.createElement("h2")); - generateHTML():HTMLElement{ - const div=document.createElement("div"); - div.classList.add("titlediv"); - const title=document.createElement("h2"); - title.textContent=this.name; - div.append(title); - if(this.name!=="") title.classList.add("settingstitle"); - this.title=new WeakRef(title); - const container=document.createElement("div"); - this.container=new WeakRef(container); - container.classList.add(this.ltr?"flexltr":"flexttb","flexspace"); - this.generateContainter(); - div.append(container); - return div; - } - generateContainter(){ - const container=this.container.deref(); - if(container){ - const title=this.title.deref(); - if(title) title.innerHTML=""; - container.innerHTML=""; - if(this.subOptions){ - container.append(this.subOptions.generateHTML());//more code needed, though this is enough for now - if(title){ - const name=document.createElement("span"); - name.innerText=this.name; - name.classList.add("clickable"); - name.onclick=()=>{ - this.returnFromSub(); - }; - title.append(name," > ",this.subOptions.name); - } - }else{ - for(const thing of this.options){ - this.generate(thing); - } - if(title){ - title.innerText=this.name; - } - } - if(title&&title.innerText!==""){ - title.classList.add("settingstitle"); - }else if(title){ - title.classList.remove("settingstitle"); - } - }else{ - console.warn("tried to generate container, but it did not exist"); - } - } - changed(){ - if(this.owner instanceof Options||this.owner instanceof Form){ - this.owner.changed(); - return; - } - if(!this.haschanged){ - const div=document.createElement("div"); - div.classList.add("flexltr","savediv"); - const span=document.createElement("span"); - div.append(span); - span.textContent="Careful, you have unsaved changes"; - const button=document.createElement("button"); - button.textContent="Save changes"; - div.append(button); - this.haschanged=true; - this.owner.changed(div); - - button.onclick=_=>{ - if(this.owner instanceof Buttons){ - this.owner.save(); - } - div.remove(); - this.submit(); - }; - } - } - submit(){ - this.haschanged=false; - for(const thing of this.options){ - thing.submit(); - } - } -} -class FormError extends Error{ - elem:OptionsElement; - message:string; - constructor(elem:OptionsElement,message:string){ - super(message); - this.message=message; - this.elem=elem; - } -} -export{FormError}; -class Form implements OptionsElement{ - name:string; - readonly options:Options; - readonly owner:Options; - readonly ltr:boolean; - readonly names:Map>=new Map(); - readonly required:WeakSet>=new WeakSet(); - readonly submitText:string; - readonly fetchURL:string; - readonly headers={}; - readonly method:string; - value:object; - traditionalSubmit:boolean; - values={}; - constructor(name:string,owner:Options,onSubmit:((arg1:object)=>void),{ltr=false,submitText="Submit",fetchURL="",headers={},method="POST",traditionalSubmit=false}={}){ - this.traditionalSubmit=traditionalSubmit; - this.name=name; - this.method=method; - this.submitText=submitText; - this.options=new Options("",this,{ltr}); - this.owner=owner; - this.fetchURL=fetchURL; - this.headers=headers; - this.ltr=ltr; - this.onSubmit=onSubmit; - } - setValue(key:string,value:any){//the value can't really be anything, but I don't care enough to fix this - this.values[key]=value; - } - addSelect(label:string,formName:string,selections:string[],{defaultIndex=0,required=false}={}){ - const select=this.options.addSelect(label,_=>{},selections,{defaultIndex}); - this.names.set(formName,select); - if(required){ - this.required.add(select); - } - return select; - } - readonly fileOptions:Map=new Map(); - addFileInput(label:string,formName:string,{required=false,files="one",clear=false}={}){ - const FI=this.options.addFileInput(label,_=>{},{clear}); - if(files!=="one"&&files!=="multi") throw new Error("files should equal one or multi"); - this.fileOptions.set(FI,{files}); - this.names.set(formName,FI); - if(required){ - this.required.add(FI); - } - return FI; - } - - addTextInput(label:string,formName:string,{initText="",required=false,password=false}={}){ - const textInput=this.options.addTextInput(label,_=>{},{initText,password}); - this.names.set(formName,textInput); - if(required){ - this.required.add(textInput); - } - return textInput; - } - addColorInput(label:string,formName:string,{initColor="",required=false}={}){ - const colorInput=this.options.addColorInput(label,_=>{},{initColor}); - this.names.set(formName,colorInput); - if(required){ - this.required.add(colorInput); - } - return colorInput; - } - - addMDInput(label:string,formName:string,{initText="",required=false}={}){ - const mdInput=this.options.addMDInput(label,_=>{},{initText}); - this.names.set(formName,mdInput); - if(required){ - this.required.add(mdInput); - } - return mdInput; - } - - addCheckboxInput(label:string,formName:string,{initState=false,required=false}={}){ - const box=this.options.addCheckboxInput(label,_=>{},{initState}); - this.names.set(formName,box); - if(required){ - this.required.add(box); - } - return box; - } - addText(str:string){ - this.options.addText(str); - } - addTitle(str:string){ - this.options.addTitle(str); - } - generateHTML():HTMLElement{ - const div=document.createElement("div"); - div.append(this.options.generateHTML()); - div.classList.add("FormSettings"); - if(!this.traditionalSubmit){ - const button=document.createElement("button"); - button.onclick=_=>{ - this.submit(); - }; - button.textContent=this.submitText; - div.append(button); - } - return div; - } - onSubmit:((arg1:object)=>void); - watchForChange(func:(arg1:object)=>void){ - this.onSubmit=func; - } - changed(){ - if(this.traditionalSubmit){ - this.owner.changed(); - } - } - async submit(){ - const build={}; - for(const key of Object.keys(this.values)){ - const thing=this.values[key]; - if(thing instanceof Function){ - try{ - build[key]=thing(); - }catch(e:any){ - if(e instanceof FormError){ - const elm=this.options.html.get(e.elem); - if(elm){ - const html=elm.deref(); - if(html){ - this.makeError(html,e.message); - } - } - } - return; - } - }else{ - build[key]=thing; - } - } - const promises:Promise[]=[]; - for(const thing of this.names.keys()){ - if(thing==="")continue; - const input=this.names.get(thing) as OptionsElement; - if(input instanceof SelectInput){ - build[thing]=input.options[input.value]; - continue; - }else if(input instanceof FileInput){ - const options=this.fileOptions.get(input); - if(!options){ - throw new Error("FileInput without its options is in this form, this should never happen."); - } - if(options.files==="one"){ - console.log(input.value); - if(input.value){ - const reader=new FileReader(); - reader.readAsDataURL(input.value[0]); - const promise=new Promise((res)=>{ - reader.onload=()=>{ - build[thing]=reader.result; - res(); - }; - }) - promises.push(promise); - } - }else{ - console.error(options.files+" is not currently implemented") - } - } - build[thing]=input.value; - } - await Promise.allSettled(promises); - if(this.fetchURL!==""){ - fetch(this.fetchURL,{ - method: this.method, - body: JSON.stringify(build), - headers: this.headers - }).then(_=>_.json()).then(json=>{ - if(json.errors&&this.errors(json.errors))return; - this.onSubmit(json); - }); - }else{ - this.onSubmit(build); - } - console.warn("needs to be implemented"); - } - errors(errors:{code:number,message:string,errors:{[key:string]:{_errors:{message:string,code:string}}}}){ - if(!(errors instanceof Object)){ - return; - } - for(const error of Object.keys(errors)){ - const elm=this.names.get(error); - if(elm){ - const ref=this.options.html.get(elm); - if(ref&&ref.deref()){ - const html=ref.deref() as HTMLDivElement; - this.makeError(html,errors[error]._errors[0].message); - return true; - } - } - } - return false; - } - error(formElm:string,errorMessage:string){ - const elm=this.names.get(formElm); - if(elm){ - const htmlref=this.options.html.get(elm); - if(htmlref){ - const html=htmlref.deref(); - if(html){ - this.makeError(html,errorMessage); - } - } - }else{ - console.warn(formElm+" is not a valid form property"); - } - } - makeError(e:HTMLDivElement,message:string){ - let element=e.getElementsByClassName("suberror")[0] as HTMLElement; - if(!element){ - const div=document.createElement("div"); - div.classList.add("suberror","suberrora"); - e.append(div); - element=div; - }else{ - element.classList.remove("suberror"); - setTimeout(_=>{ - element.classList.add("suberror"); - },100); - } - element.textContent=message; - } -} -class Settings extends Buttons{ - static readonly Buttons=Buttons; - static readonly Options=Options; - html:HTMLElement|null; - constructor(name:string){ - super(name); - } - addButton(name:string,{ltr=false}={}):Options{ - const options=new Options(name,this,{ltr}); - this.add(name,options); - return options; - } - show(){ - const background=document.createElement("div"); - background.classList.add("background"); - - const title=document.createElement("h2"); - title.textContent=this.name; - title.classList.add("settingstitle"); - background.append(title); - - background.append(this.generateHTML()); - - - - const exit=document.createElement("span"); - exit.textContent="✖"; - exit.classList.add("exitsettings"); - background.append(exit); - exit.onclick=_=>{ - this.hide(); - }; - document.body.append(background); - this.html=background; - } - hide(){ - if(this.html){ - this.html.remove(); - this.html=null; - } - } -} - -export{Settings,OptionsElement,Buttons,Options}; - diff --git a/webpage/snowflake.ts b/webpage/snowflake.ts deleted file mode 100644 index 505c0dc..0000000 --- a/webpage/snowflake.ts +++ /dev/null @@ -1,18 +0,0 @@ -abstract class SnowFlake{ - public readonly id:string; - constructor(id:string){ - this.id=id; - } - getUnixTime():number{ - return SnowFlake.stringToUnixTime(this.id); - } - static stringToUnixTime(str:string){ - try{ - return Number((BigInt(str)>>22n)+1420070400000n); - }catch{ - console.error(`The ID is corrupted, it's ${str} when it should be some number.`); - return 0; - } - } -} -export{SnowFlake}; diff --git a/webpage/user.ts b/webpage/user.ts deleted file mode 100644 index 877700b..0000000 --- a/webpage/user.ts +++ /dev/null @@ -1,433 +0,0 @@ -//const usercache={}; -import{Member}from"./member.js"; -import{MarkDown}from"./markdown.js"; -import{Contextmenu}from"./contextmenu.js"; -import{Localuser}from"./localuser.js"; -import{Guild}from"./guild.js"; -import{ SnowFlake }from"./snowflake.js"; -import{ presencejson, userjson }from"./jsontypes.js"; - -class User extends SnowFlake{ - owner:Localuser; - hypotheticalpfp:boolean; - avatar:string|null; - username:string; - nickname:string|null=null; - relationshipType:0|1|2|3|4=0; - bio:MarkDown; - discriminator:string; - pronouns:string; - bot:boolean; - public_flags: number; - accent_color: number; - banner: string|undefined; - hypotheticalbanner:boolean; - premium_since: string; - premium_type: number; - theme_colors: string; - badge_ids: string[]; - members: WeakMap>=new WeakMap(); - private status:string; - clone(){ - return new User({ - username: this.username, - id: this.id+"#clone", - public_flags: this.public_flags, - discriminator: this.discriminator, - avatar: this.avatar, - accent_color: this.accent_color, - banner: this.banner, - bio: this.bio.rawString, - premium_since: this.premium_since, - premium_type: this.premium_type, - bot: this.bot, - theme_colors: this.theme_colors, - pronouns: this.pronouns, - badge_ids: this.badge_ids - },this.owner); - } - public getPresence(presence:presencejson|undefined){ - if(presence){ - this.setstatus(presence.status); - }else{ - this.setstatus("offline"); - } - } - setstatus(status:string){ - this.status=status; - } - async getStatus(){ - if(this.status){ - return this.status; - }else{ - return"offline"; - } - } - static contextmenu=new Contextmenu("User Menu"); - static setUpContextMenu(){ - this.contextmenu.addbutton("Copy user id",function(this:User){ - navigator.clipboard.writeText(this.id); - }); - this.contextmenu.addbutton("Message user",function(this:User){ - fetch(this.info.api+"/users/@me/channels", - {method: "POST", - body: JSON.stringify({recipients: [this.id]}), - headers: this.localuser.headers - }).then(_=>_.json()).then(json=>{ - this.localuser.goToChannel(json.id) - }); - }); - this.contextmenu.addbutton("Block user",function(this:User){ - this.block(); - },null,function(){ - return this.relationshipType!==2; - }); - - this.contextmenu.addbutton("Unblock user",function(this:User){ - this.unblock(); - },null,function(){ - return this.relationshipType===2; - }); - this.contextmenu.addbutton("Friend request",function(this:User){ - fetch(`${this.info.api}/users/@me/relationships/${this.id}`,{ - method: "PUT", - headers: this.owner.headers, - body: JSON.stringify({ - type: 1 - }) - }); - }); - this.contextmenu.addbutton("Kick member",function(this:User,member:Member){ - member.kick(); - },null,member=>{ - if(!member)return false; - const us=member.guild.member; - if(member.id===us.id){ - return false; - } - if(member.id===member.guild.properties.owner_id){ - return false; - } - return(us.hasPermission("KICK_MEMBERS"))||false; - }); - this.contextmenu.addbutton("Ban member",function(this:User,member:Member){ - member.ban(); - },null,member=>{ - if(!member)return false; - const us=member.guild.member; - if(member.id===us.id){ - return false; - } - if(member.id===member.guild.properties.owner_id){ - return false; - } - return(us.hasPermission("BAN_MEMBERS"))||false; - }); - } - static checkuser(user:User|userjson,owner:Localuser):User{ - if(owner.userMap.has(user.id)){ - return owner.userMap.get(user.id) as User; - }else{ - const tempuser=new User(user as userjson,owner,true); - owner.userMap.set(user.id,tempuser); - return tempuser; - } - } - get info(){ - return this.owner.info; - } - get localuser(){ - return this.owner; - } - get name(){ - return this.username; - } - constructor(userjson:userjson,owner:Localuser,dontclone=false){ - super(userjson.id); - this.owner=owner; - if(!owner){ - console.error("missing localuser"); - } - if(dontclone){ - for(const thing of Object.keys(userjson)){ - if(thing==="bio"){ - this.bio=new MarkDown(userjson[thing],this.localuser); - continue; - } - if(thing === "id"){ - continue; - } - this[thing]=userjson[thing]; - } - this.hypotheticalpfp=false; - }else{ - return User.checkuser(userjson,owner); - } - } - async resolvemember(guild:Guild){ - return await Member.resolveMember(this,guild); - } - - async getUserProfile(){ - return(await fetch(`${this.info.api}/users/${this.id.replace("#clone","")}/profile?with_mutual_guilds=true&with_mutual_friends=true`,{ - headers: this.localuser.headers - })).json(); - } - resolving:false|Promise=false; - async getBadge(id:string){ - if(this.localuser.badges.has(id)){ - return this.localuser.badges.get(id); - }else{ - if(this.resolving){ - await this.resolving; - return this.localuser.badges.get(id); - } - - const prom=await this.getUserProfile(); - this.resolving=prom; - const badges=prom.badges; - this.resolving=false; - for(const thing of badges){ - this.localuser.badges.set(thing.id,thing); - } - return this.localuser.badges.get(id); - } - } - buildpfp(){ - const pfp=document.createElement("img"); - pfp.loading="lazy"; - pfp.src=this.getpfpsrc(); - pfp.classList.add("pfp"); - pfp.classList.add("userid:"+this.id); - return pfp; - } - async buildstatuspfp(){ - const div=document.createElement("div"); - div.style.position="relative"; - const pfp=this.buildpfp(); - div.append(pfp); - { - const status=document.createElement("div"); - status.classList.add("statusDiv"); - switch(await this.getStatus()){ - case"offline": - status.classList.add("offlinestatus"); - break; - case"online": - default: - status.classList.add("onlinestatus"); - break; - } - div.append(status); - } - return div; - } - userupdate(json:userjson){ - if(json.avatar!==this.avatar){ - console.log; - this.changepfp(json.avatar); - } - } - bind(html:HTMLElement,guild:Guild|null=null,error=true){ - if(guild&&guild.id!=="@me"){ - Member.resolveMember(this,guild).then(_=>{ - User.contextmenu.bindContextmenu(html,this,_); - if(_===undefined&&error){ - const error=document.createElement("span"); - error.textContent="!"; - error.classList.add("membererror"); - html.after(error); - return; - } - if(_){ - _.bind(html); - } - }).catch(_=>{ - console.log(_); - }); - } - if(guild){ - this.profileclick(html,guild); - }else{ - this.profileclick(html); - } - } - static async resolve(id:string,localuser:Localuser){ - const json=await fetch(localuser.info.api.toString()+"/users/"+id+"/profile", - {headers: localuser.headers} - ).then(_=>_.json()); - return new User(json,localuser); - } - changepfp(update:string|null){ - this.avatar=update; - this.hypotheticalpfp=false; - const src=this.getpfpsrc(); - console.log(src); - for(const thing of document.getElementsByClassName("userid:"+this.id)){ - (thing as HTMLImageElement).src=src; - } - } - block(){ - fetch(`${this.info.api}/users/@me/relationships/${this.id}`,{ - method: "PUT", - headers: this.owner.headers, - body: JSON.stringify({ - type: 2 - }) - }); - this.relationshipType=2; - const channel=this.localuser.channelfocus; - if(channel){ - for(const thing of channel.messages){ - thing[1].generateMessage(); - } - } - } - unblock(){ - fetch(`${this.info.api}/users/@me/relationships/${this.id}`,{ - method: "DELETE", - headers: this.owner.headers, - }); - this.relationshipType=0; - const channel=this.localuser.channelfocus; - if(channel){ - for(const thing of channel.messages){ - thing[1].generateMessage(); - } - } - } - getpfpsrc(){ - if(this.hypotheticalpfp&&this.avatar){ - return this.avatar; - } - if(this.avatar!==null){ - return this.info.cdn+"/avatars/"+this.id.replace("#clone","")+"/"+this.avatar+".png"; - }else{ - const int=new Number((BigInt(this.id.replace("#clone","")) >> 22n) % 6n); - return this.info.cdn+`/embed/avatars/${int}.png`; - } - } - createjankpromises(){ - new Promise(_=>{}); - } - async buildprofile(x:number,y:number,guild:Guild|null=null){ - if(Contextmenu.currentmenu!=""){ - Contextmenu.currentmenu.remove(); - } - - - const div=document.createElement("div"); - - if(this.accent_color){ - div.style.setProperty("--accent_color","#"+this.accent_color.toString(16).padStart(6,"0")); - }else{ - div.style.setProperty("--accent_color","transparent"); - } - if(this.banner){ - const banner=document.createElement("img"); - let src:string; - if(!this.hypotheticalbanner){ - src=this.info.cdn+"/avatars/"+this.id.replace("#clone","")+"/"+this.banner+".png"; - }else{ - src=this.banner; - } - console.log(src,this.banner); - banner.src=src; - banner.classList.add("banner"); - div.append(banner); - } - if(x!==-1){ - div.style.left=x+"px"; - div.style.top=y+"px"; - div.classList.add("profile","flexttb"); - }else{ - this.setstatus("online"); - div.classList.add("hypoprofile","flexttb"); - } - const badgediv=document.createElement("div"); - badgediv.classList.add("badges"); - (async ()=>{ - if(!this.badge_ids)return; - for(const id of this.badge_ids){ - const badgejson=await this.getBadge(id); - if(badgejson){ - const badge=document.createElement(badgejson.link?"a":"div"); - badge.classList.add("badge"); - const img=document.createElement("img"); - img.src=badgejson.icon; - badge.append(img); - const span=document.createElement("span"); - span.textContent=badgejson.description; - badge.append(span); - if(badge instanceof HTMLAnchorElement){ - badge.href=badgejson.link; - } - badgediv.append(badge); - } - } - })(); - { - const pfp=await this.buildstatuspfp(); - div.appendChild(pfp); - } - { - const userbody=document.createElement("div"); - userbody.classList.add("infosection"); - div.appendChild(userbody); - const usernamehtml=document.createElement("h2"); - usernamehtml.textContent=this.username; - userbody.appendChild(usernamehtml); - userbody.appendChild(badgediv); - const discrimatorhtml=document.createElement("h3"); - discrimatorhtml.classList.add("tag"); - discrimatorhtml.textContent=this.username+"#"+this.discriminator; - userbody.appendChild(discrimatorhtml); - - const pronounshtml=document.createElement("p"); - pronounshtml.textContent=this.pronouns; - pronounshtml.classList.add("pronouns"); - userbody.appendChild(pronounshtml); - - const rule=document.createElement("hr"); - userbody.appendChild(rule); - const biohtml=this.bio.makeHTML(); - userbody.appendChild(biohtml); - if(guild){ - Member.resolveMember(this,guild).then(member=>{ - if(!member)return; - const roles=document.createElement("div"); - roles.classList.add("rolesbox"); - for(const role of member.roles){ - const div=document.createElement("div"); - div.classList.add("rolediv"); - const color=document.createElement("div"); - div.append(color); - color.style.setProperty("--role-color","#"+role.color.toString(16).padStart(6,"0")); - color.classList.add("colorrolediv"); - const span=document.createElement("span"); - div.append(span); - span.textContent=role.name; - roles.append(div); - } - userbody.append(roles); - }); - } - } - console.log(div); - - if(x!==-1){ - Contextmenu.currentmenu=div; - document.body.appendChild(div); - Contextmenu.keepOnScreen(div); - } - return div; - } - profileclick(obj:HTMLElement,guild?:Guild|undefined){ - obj.onclick=e=>{ - this.buildprofile(e.clientX,e.clientY,guild); - e.stopPropagation(); - }; - } -} -User.setUpContextMenu(); -export{User};