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 {Fullscreen} from "./fullscreen.js"; import {setTheme, Specialuser} from "./login.js"; import { SnowFlake } from "./snowflake.js"; import { Message } from "./message.js"; import { channeljson, readyjson, userjson } from "./jsontypes.js"; const wsCodesRetry=new Set([4000,4003,4005,4007,4008,4009]); class Localuser{ lastSequence:number|null=null; token:string; userinfo:Specialuser; serverurls; initialized:boolean; info; headers:{"Content-type":string,Authorization:string}; usersettings:Fullscreen; userConnections:Fullscreen; devPortal:Fullscreen; ready:readyjson; guilds:Guild[]; guildids:Map; user:User; status:string; channelfocus:Channel; lookingguild:Guild; guildhtml:Map; ws:WebSocket; typing:[string,number][]; wsinterval:NodeJS.Timeout; connectionSucceed=0; errorBackoff=0; constructor(userinfo:Specialuser){ 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.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; clearInterval(this.wsinterval); this.outoffocus(); this.guilds=[]; this.guildids=new Map(); this.ws.close(4001) SnowFlake.clear(); User.clear(); } 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; if(DecompressionStream){ ds = new DecompressionStream("deflate"); w= ds.writable.getWriter(); r=ds.readable.getReader(); arr=new Uint8Array(); } let build=""; this.ws.addEventListener('message', async (event) => { let temp:{op:number,t:string}; 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(); //console.log(data,test); while(true){ const read=(await r.read()); const data=new TextDecoder().decode(read.value); if(data===""){ break; } build+=data; console.log("temp"); try{ temp=JSON.parse(build); build=""; if(temp.op===0&&temp.t==="READY"){ returny(); } this.handleEvent(temp); }catch{ } } }else{ temp=JSON.parse(event.data); } if(temp.op===0&&temp.t==="READY"){ returny(); } this.handleEvent(temp); }); this.ws.addEventListener("close", event => { console.log("WebSocket closed with code " + event.code); if (this.wsinterval) clearInterval(this.wsinterval); this.unload(); document.getElementById("loading").classList.remove("doneloading"); document.getElementById("loading").classList.add("loading"); 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(() => { 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); this.genusersettings(); 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")); } } }else if(temp.op===10){ console.log("heartbeat down") this.wsinterval=setInterval(_=>{ if (this.connectionSucceed===0) this.connectionSucceed=Date.now() this.ws.send(JSON.stringify({op:1,d:this.lastSequence})) },temp.d.heartbeat_interval) } } 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 div=document.createElement("div"); div.textContent="⌂"; div.classList.add("home","servericon") div["all"]=this.guildids.get("@me"); this.guildids.get("@me").html=outdiv; const unread=document.createElement("div"); unread.classList.add("unread"); outdiv.append(unread); outdiv.appendChild(div); outdiv.classList.add("servernoti") serverlist.append(outdiv); div.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 guildDiscoveryContainer=document.createElement("div"); guildDiscoveryContainer.textContent="🧭"; guildDiscoveryContainer.classList.add("home","servericon"); serverlist.appendChild(guildDiscoveryContainer); guildDiscoveryContainer.addEventListener("click", ()=>{ this.guildDiscovery(); }); } this.unreads(); } createGuild(){ let inviteurl=""; const error=document.createElement("span"); const full=new Fullscreen(["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.toString()+"/v9/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 Fullscreen(["html", content]); full.show(); const res=await fetch(this.info.api.toString()+"/v9/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.toString()+"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.toString()+(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.toString()+"/v9/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.toString()+"/users/@me",{ method:"PATCH", headers:this.headers, body:JSON.stringify({ avatar:reader.result, }) }); }; } updatepronouns(pronouns:string):void{ fetch(this.info.api.toString()+"/users/@me/profile",{ method:"PATCH", headers:this.headers, body:JSON.stringify({ pronouns:pronouns, }) }); } updatebio(bio:string):void{ fetch(this.info.api.toString()+"/v9/users/@me/profile",{ method:"PATCH", headers:this.headers, body:JSON.stringify({ bio:bio, }) }); } 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"); } } genusersettings():void{ const hypotheticalProfile=document.createElement("div"); let file=null; let newprouns=null; let newbio=null; let hypouser=this.user.clone(); function regen(){ hypotheticalProfile.textContent=""; const hypoprofile=hypouser.buildprofile(-1,-1); hypotheticalProfile.appendChild(hypoprofile) } regen(); this.usersettings=new Fullscreen( ["hdiv", ["vdiv", ["fileupload","upload pfp:",function(e){ console.log(this.files[0]) file=this.files[0]; const blob = URL.createObjectURL(this.files[0]); hypouser.avatar = blob; hypouser.hypotheticalpfp=true; regen(); }], ["textbox","Pronouns:",this.user.pronouns,function(e){ console.log(this.value); hypouser.pronouns=this.value; newprouns=this.value; regen(); }], ["mdbox","Bio:",this.user.bio.rawString,function(e){ console.log(this.value); hypouser.bio=this.value; newbio=this.value; regen(); }], ["button","update user content:","submit",()=>{ if(file!==null){ this.updatepfp(file); } if(newprouns!==null){ this.updatepronouns(newprouns); } if(newbio!==null){ this.updatebio(newbio); } }], ["select","Theme:",["Dark","Light","WHITE"],e=>{ localStorage.setItem("theme",["Dark","Light","WHITE"][e.target.selectedIndex]); setTheme(); },["Dark","Light","WHITE"].indexOf(localStorage.getItem("theme"))], ["select","Notification sound:",Voice.sounds,e=>{ Voice.setNotificationSound(Voice.sounds[e.target.selectedIndex]); Voice.noises(Voice.sounds[e.target.selectedIndex]); },Voice.sounds.indexOf(Voice.getNotificationSound())] ], ["vdiv", ["html",hypotheticalProfile] ] ],_=>{},function(this:Localuser){ console.log(this); hypouser=this.user.clone(); regen(); file=null; newprouns=null; newbio=null; }.bind(this)) const connectionContainer=document.createElement("div"); connectionContainer.id="connection-container"; this.userConnections=new Fullscreen( ["html", connectionContainer ], () => {}, async () => { connectionContainer.innerHTML=""; const res=await fetch(this.info.api.toString()+"/v9/connections", { headers: this.headers }); const json=await res.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.toString()+"/v9/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); }) } ); let appName=""; const appListContainer=document.createElement("div"); appListContainer.id="app-list-container"; this.devPortal=new Fullscreen( ["vdiv", ["hdiv", ["textbox", "Name:", appName, event => { appName=event.target.value; }], ["button", "", "Create application", async () => { if (appName.trim().length == 0) return alert("Please enter a name for the application."); const res=await fetch(this.info.api.toString()+"/v9/applications", { method: "POST", headers: this.headers, body: JSON.stringify({ name: appName }) }); const json=await res.json(); this.manageApplication(json.id); this.devPortal.hide(); } ] ], ["html", appListContainer ] ], () => {}, async () => { appListContainer.innerHTML=""; const res=await fetch(this.info.api.toString()+"/v9/applications", { headers: this.headers }); const json=await res.json(); json.forEach(application => { const container=document.createElement("div"); if (application.cover_image) { const cover=document.createElement("img"); cover.crossOrigin="anonymous"; cover.src=this.info.cdn.toString()+"/app-icons/" + application.id + "/" + application.cover_image + ".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.devPortal.hide(); this.manageApplication(application.id); }); appListContainer.appendChild(container); }) } ) } async manageApplication(appId="") { const res=await fetch(this.info.api.toString()+"/v9/applications/" + appId, { headers: this.headers }); const json=await res.json(); const fields: any={}; const appDialog=new Fullscreen( ["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.toString()+"/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.toString()+"/v9/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.toString()+"/v9/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.toString()+"/v9/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.toString()+"/app-icons/" + appId + "/" + json.bot.avatar + ".png?size=256") : "" }; const botDialog=new Fullscreen( ["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.toString()+"/v9/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.toString()+"/v9/applications/" + appId + "/bot/reset", { method: "POST", headers: this.headers }); const updateJSON=await updateRes.json(); alert("New token:\n" + updateJSON.token); botDialog.hide(); } ] ] ] ); botDialog.show(); } } export {Localuser};