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 {getBulkInfo, setTheme, Specialuser} from "./login.js"; import { SnowFlake } from "./snowflake.js"; import { Message } from "./message.js"; import { channeljson, guildjson, memberjson, readyjson } from "./jsontypes.js"; import { Member } from "./member.js"; import { Settings } from "./settings.js"; import { MarkDown } from "./markdown.js"; const wsCodesRetry=new Set([4000,4003,4005,4007,4008,4009]); class Localuser{ lastSequence:number|null=null; token:string; userinfo:Specialuser; serverurls:Specialuser["serverurls"]; initialized:boolean; info:Specialuser["serverurls"]; headers:{"Content-type":string,Authorization:string}; usersettings:Settings; userConnections:Dialog; devPortal:Dialog; ready:readyjson; guilds:Guild[]; guildids:Map; user:User; status:string; channelfocus:Channel; lookingguild:Guild; guildhtml:Map; ws:WebSocket; typing:[string,number][]; connectionSucceed=0; errorBackoff=0; mfa_enabled:boolean; constructor(userinfo:Specialuser|-1){ if(userinfo===-1){ return; } this.token=userinfo.token; this.userinfo=userinfo; 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.usersettings=null; this.initialized=true; this.ready=ready; this.guilds=[]; this.guildids=new Map(); this.user=new User(ready.d.user,this); 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=null; this.lookingguild=null; 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.resolveChannelFromID(thing.id); if(!channel){continue;} const guild=channel.guild; if(guild===undefined){ continue } const guildid=guild.snowflake; this.guildids.get(guildid.id).channelids[thing.channel_id].readStateInfo(thing); } this.typing=[]; } outoffocus():void{ document.getElementById("servers").innerHTML=""; document.getElementById("channels").innerHTML=""; if(this.channelfocus){ this.channelfocus.infinite.delete(); } this.lookingguild=null; this.channelfocus=null; } unload():void{ this.initialized=false; this.outoffocus(); this.guilds=[]; this.guildids=new Map(); if(this.ws){ this.ws.close(4001) } SnowFlake.clear(); User.clear(); } swapped=false; async initwebsocket():Promise{ let returny=null const promise=new Promise((res)=>{returny=res}); this.ws = new WebSocket(this.serverurls.gateway.toString()+"?encoding=json&v=9"+(DecompressionStream?"&compress=zlib-stream":"")); this.ws.addEventListener('open', (_event) => { console.log('WebSocket connected'); this.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": !!DecompressionStream, "presence": { "status": "online", "since": new Date().getTime(), "activities": [], "afk": false } } })) }); 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(); (async ()=>{ let temp:any; while(true){ const read=(await r.read()); const data=new TextDecoder().decode(read.value); build+=data; try{ temp=JSON.parse(build); build=""; if(temp.op===0&&temp.t==="READY"){ returny(); } this.handleEvent(temp); }catch{ } } })(); } let order=new Promise((res)=>(res())); this.ws.addEventListener('message', async (event) => { const temp2=order; let res:Function; order=new Promise((r)=>(res=r)) 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(); } this.handleEvent(temp); }catch(e){ console.error(e); }finally{ res(); } }); this.ws.addEventListener("close", 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..."; setTimeout(() => { if(this.swapped) return; document.getElementById("load-desc").textContent="Retrying..."; this.initwebsocket().then(() => { this.loaduser(); this.init(); document.getElementById("loading").classList.add("doneloading"); document.getElementById("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; return; } 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": console.log(temp.d); SnowFlake.getSnowFlakeFromID(temp.d.id,Message).getObject().deleteEvent(); break; case "READY": this.gottenReady(temp as readyjson); break; case "MESSAGE_UPDATE": const message=SnowFlake.getSnowFlakeFromID(temp.d.id,Message).getObject(); message.giveData(temp.d); break; case "TYPING_START": if(this.initialized){ this.typingStart(temp); } break; case "USER_UPDATE": if(this.initialized){ const users=SnowFlake.getSnowFlakeFromID(temp.d.id,User).getObject() as User; console.log(users,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); 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": if(SnowFlake.hasSnowFlakeFromID(temp.d.message_id,Message)){ temp.d.guild_id??="@me"; const message=SnowFlake.getSnowFlakeFromID(temp.d.message_id,Message).getObject(); const guild=SnowFlake.getSnowFlakeFromID(temp.d.guild_id,Guild).getObject(); let thing:Member|{id:string}; if(temp.d.member){ thing=new Member(temp.d.member,guild); }else{ thing={id:temp.d.user_id} } message.giveReaction(temp.d.emoji,thing); } break; case "MESSAGE_REACTION_REMOVE": if(SnowFlake.hasSnowFlakeFromID(temp.d.message_id,Message)){ const message=SnowFlake.getSnowFlakeFromID(temp.d.message_id,Message).getObject(); console.log("test"); message.takeReaction(temp.d.emoji,temp.d.user_id); } break; case "GUILD_MEMBERS_CHUNK": this.gotChunk(temp.d); break; } }else if(temp.op===10){ 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; resolveChannelFromID(ID:string):Channel{ let resolve=this.guilds.find(guild => guild.channelids[ID]); if(resolve){ return resolve.channelids[ID]; } return undefined; } updateChannel(json:channeljson):void{ SnowFlake.getSnowFlakeFromID(json.guild_id,Guild).getObject().updateChannel(json); if(json.guild_id===this.lookingguild.id){ this.loadGuild(json.guild_id); } } createChannel(json:channeljson):void{ json.guild_id??="@me"; SnowFlake.getSnowFlakeFromID(json.guild_id,Guild).getObject().createChannelpac(json); if(json.guild_id===this.lookingguild.id){ this.loadGuild(json.guild_id); } } delChannel(json:channeljson):void{ json.guild_id??="@me"; this.guildids.get(json.guild_id).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]); guild.loadChannel(location[5]); this.channelfocus=guild.channelids[location[5]]; } } loaduser():void{ document.getElementById("username").textContent=this.user.username; (document.getElementById("userpfp") as HTMLImageElement).src=this.user.getpfpsrc(); document.getElementById("status").textContent=this.status; } isAdmin():boolean{ return this.lookingguild.isAdmin(); } loadGuild(id:string):Guild{ let guild=this.guildids.get(id); if(!guild){ guild=this.guildids.get("@me"); } if(this.lookingguild){ this.lookingguild.html.classList.remove("serveropen"); } if(guild.html){ guild.html.classList.add("serveropen") } this.lookingguild=guild; document.getElementById("serverName").textContent=guild.properties.name; //console.log(this.guildids,id) document.getElementById("channels").innerHTML=""; document.getElementById("channels").appendChild(guild.getHTML()); return guild; } buildservers():void{ const serverlist=document.getElementById("servers");// const outdiv=document.createElement("div"); const img=document.createElement("img"); const div=document.createElement("div"); div.classList.add("home","servericon"); img.src="/icons/home.svg"; img.classList.add("svgtheme","svgicon") img["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(img); outdiv.classList.add("servernoti") serverlist.append(outdiv); img.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("img"); guildDiscoveryContainer.src="/icons/explore.svg"; guildDiscoveryContainer.classList.add("svgtheme","svgicon"); 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 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 Server",[ "text","Not currently implemented, sorry" ]] ]]) full.show(); } 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"; this.guildids.get(messagep.d.guild_id).channelids[messagep.d.channel_id].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); } } typingStart(typing):void{ if(this.channelfocus.snowflake===typing.d.channel_id){ const memb=typing.d.member; let name; if(memb.id===this.user.snowflake){ console.log("you is typing") return; } console.log("user is typing and you should see it"); if(memb.nick){ name=memb.nick; }else{ name=memb.user.username; } let already=false; for(const thing of this.typing){ if(thing[0]===name){ thing[1]=new Date().getTime(); already=true; break; } } if(!already){ this.typing.push([name,new Date().getTime()]); } setTimeout(this.rendertyping.bind(this),10000); this.rendertyping(); } } updatepfp(file:Blob):void{ var 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, }) }); }; } updateProfile(json:{bio?:string,pronouns?:string}){ fetch(this.info.api+"/users/@me/profile",{ method:"PATCH", headers:this.headers, body:JSON.stringify(json) }); } rendertyping():void{ const typingtext=document.getElementById("typing") let build=""; const array2=[]; let showing=false; let i=0; for(const thing of this.typing){ i++; if(thing[1]>new Date().getTime()-5000){ build+=thing[0]; array2.push(thing); showing=true; if(i!==this.typing.length){ build+=","; } } } if(i>1){ build+=" are typing"; }else{ build+=" is typing"; } if(showing){ typingtext.classList.remove("hidden"); document.getElementById("typingtext").textContent=build; }else{ typingtext.classList.add("hidden"); } } showusersettings(){ const settings=new Settings("Settings"); this.usersettings=settings; { const userOptions=settings.addButton("User Settings",{ltr:true}); const hypotheticalProfile=document.createElement("div"); let file=undefined; let newpronouns:string=undefined; let newbio:string=undefined; let hypouser=this.user.clone(); function regen(){ hypotheticalProfile.textContent=""; const hypoprofile=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) } }); finput.watchForChange(_=>{ if(_.length){ file=_[0]; const blob = URL.createObjectURL(file); hypouser.avatar = blob; hypouser.hypotheticalpfp=true; regen(); } }); const pronounbox=settingsLeft.addTextInput("Pronouns",_=>{ if(newpronouns||newbio){ this.updateProfile({pronouns:newpronouns,bio:newbio}); } },{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(); }) } { 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 Security"); if(this.mfa_enabled){ security.addTextInput("Disable 2FA, totp code:",_=>{ fetch(this.info.api+"/users/@me/mfa/totp/disable",{ method:"POST", headers:this.headers, body:JSON.stringify({ code:_ }) }).then(r=>r.json()).then(json=>{ if(json.message){ alert(json.message); }else{ this.mfa_enabled=false; alert("2FA turned off successfully"); } }); }) }else{ security.addButtonInput("","Enable 2FA",async ()=>{ let secret="" for(let i=0;i<18;i++){ secret+="ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"[Math.floor(Math.random()*32)]; } let password=""; let code=""; const addmodel=new Dialog( ["vdiv", ["title","2FA set up"], ["text","Copy this secret into your totp(time-based one time password) app"], ["text",`Your secret is: ${secret} and it's 6 digits, with a 30 second token period`], ["textbox","Account password:","",function(){password=this.value}], ["textbox","Code:","",function(){code=this.value}], ["button","","Submit",()=>{ fetch(this.info.api+"/users/@me/mfa/totp/enable/",{ method:"POST", headers:this.headers, body:JSON.stringify({ password, code, secret }) }).then(r=>r.json()).then(json=>{ if(json.message){ alert(json.message); }else{ alert("2FA set up successfully"); addmodel.hide(); this.mfa_enabled=true; } }) }] ]); console.log("here :3") addmodel.show(); }) } } { 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"); let appName=""; devPortal.addTextInput("Name:", value => { appName=value }); devPortal.addButtonInput("", "Create application", async () => { if (appName.trim().length == 0) { return alert("Please enter a name for the application."); } const res=await fetch(this.info.api+"/applications", { method: "POST", headers: this.headers, body: JSON.stringify({ name: appName }) }); const json=await res.json(); this.manageApplication(json.id); }) 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(); reader.readAsDataURL(event.target.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(); reader.readAsDataURL(event.target.files[0]); 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:Mapvoid>>=new Map(); async resolvemember(id:string,guildid:string):Promise{ console.warn("this function may or may not work on any instance, use at your own risk"); //throw new Error("Not implemented on the server side and not fully implemented, do not use"); if(!this.waitingmembers.has(guildid)){ this.waitingmembers.set(guildid,new Map()); } let res:(returns:memberjson|undefined)=>void; const promise:Promise=new Promise((r)=>{ res=r; }) this.waitingmembers.get(guildid).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[]}){ console.log(chunk); chunk.members??=[]; const arr=this.noncebuild.get(chunk.nonce); 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){ console.log("got through"); this.noncebuild.delete(chunk.nonce); const func=this.noncemap.get(chunk.nonce) func([arr[0],arr[1]]); this.noncemap.delete(chunk.nonce); } } async getmembers(){ let res:Function const promise=new Promise(r=>res=r); 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 }; let res:(r:[memberjson[],string[]])=>void; const promise:Promise<[memberjson[],string[]]>=new Promise((r)=>{ res=r; }) const nonce=""+Math.floor(Math.random()*100000000000); this.noncemap.set(nonce,res); this.noncebuild.set(nonce,[[],[],[]]); 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)){ value.get(thing.id)(thing); value.delete(thing.id); } } for(const thing of prom[1]){ if(value.has(thing)){ value.get(thing)(undefined); value.delete(thing); } } this.fetchingmembers.delete(guildid); this.getmembers(); }) } } } export {Localuser};