1498 lines
42 KiB
TypeScript
1498 lines
42 KiB
TypeScript
|
|
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{ SnowFlake }from"./snowflake.js";
|
|
import{ Message }from"./message.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<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<string,Guild>;
|
|
user:User;
|
|
status:string;
|
|
channelfocus:Channel|null;
|
|
lookingguild:Guild|null;
|
|
guildhtml:Map<string, HTMLDivElement>;
|
|
ws:WebSocket|undefined;
|
|
typing:Map<Member,number>=new Map();
|
|
connectionSucceed=0;
|
|
errorBackoff=0;
|
|
readonly userMap=new Map<string,User>();
|
|
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=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) as Guild).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;
|
|
}
|
|
guild.channelids[thing.channel_id].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=null;
|
|
this.channelfocus=null;
|
|
}
|
|
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<void>{
|
|
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<void>(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<void>(res=>(res()));
|
|
|
|
ws.addEventListener("message", async event=>{
|
|
const temp2=order;
|
|
order=new Promise<void>(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 <b>" + Math.round(0.2 + (this.errorBackoff*2.8)) + "</b> 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 guild=this.guildids.get(temp.d.guild_id);
|
|
if(!guild) break;
|
|
const channel=guild.channelids[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 guild=this.guildids.get(temp.d.guild_id);
|
|
if(!guild) break;
|
|
const channel=guild.channelids[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=guild.channelids[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 guild=this.guildids.get(temp.d.guild_id);
|
|
if(!guild) break;
|
|
const channel=guild.channelids[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 guild=this.guildids.get(temp.d.guild_id);
|
|
if(!guild) break;
|
|
const channel=guild.channelids[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 guild=this.guildids.get(temp.d.guild_id);
|
|
if(!guild) break;
|
|
const channel=guild.channelids[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;
|
|
resolveChannelFromID(ID:string):Channel|undefined{
|
|
const resolve=this.guilds.find(guild=>guild.channelids[ID]);
|
|
if(resolve){
|
|
return resolve.channelids[ID];
|
|
}
|
|
return undefined;
|
|
}
|
|
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):void{
|
|
json.guild_id??="@me";
|
|
const guild=this.guildids.get(json.guild_id);
|
|
if(!guild) return;
|
|
guild.createChannelpac(json);
|
|
if(json.guild_id===this.lookingguild?.id){
|
|
this.loadGuild(json.guild_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=guild.channelids[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){
|
|
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 guild=this.guildids.get(messagep.d.guild_id);
|
|
if(!guild)return;
|
|
guild.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);
|
|
}
|
|
}
|
|
async typingStart(typing):Promise<void>{
|
|
if(this.channelfocus?.id===typing.d.channel_id){
|
|
const guild=this.guildids.get(typing.d.guild_id);
|
|
if(!guild)return;
|
|
const memb=await Member.new(typing.d.member,guild);
|
|
if(!memb)return;
|
|
if(memb.id===this.user.id){
|
|
console.log("you is typing");
|
|
return;
|
|
}
|
|
console.log("user is typing and you should see it");
|
|
this.typing.set(memb,Date.now());
|
|
setTimeout(this.rendertyping.bind(this),10000);
|
|
this.rendertyping();
|
|
}
|
|
}
|
|
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)
|
|
});
|
|
}
|
|
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.typing.keys()){
|
|
if(this.typing.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.typing.delete(thing);
|
|
}
|
|
}
|
|
if(i>1){
|
|
build+=" are typing";
|
|
}else{
|
|
build+=" is typing";
|
|
}
|
|
if(showing){
|
|
typingtext.classList.remove("hidden");
|
|
const typingtext2=document.getElementById("typingtext") as HTMLDivElement;
|
|
typingtext2.textContent=build;
|
|
}else{
|
|
typingtext.classList.add("hidden");
|
|
}
|
|
}
|
|
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();
|
|
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 -----------
|
|
readonly waitingmembers:Map<string,Map<string,(returns:memberjson|undefined)=>void>>=new Map();
|
|
readonly presences:Map<string,presencejson>=new Map();
|
|
async resolvemember(id:string,guildid:string):Promise<memberjson|undefined>{
|
|
if(guildid==="@me"){
|
|
return undefined;
|
|
}
|
|
let guildmap=this.waitingmembers.get(guildid);
|
|
if(!guildmap){
|
|
guildmap=new Map();
|
|
this.waitingmembers.set(guildid,guildmap);
|
|
}
|
|
const promise:Promise<memberjson|undefined>=new Promise(res=>{
|
|
guildmap.set(id,res);
|
|
this.getmembers();
|
|
});
|
|
return await promise;
|
|
}
|
|
fetchingmembers:Map<string,boolean>=new Map();
|
|
noncemap:Map<string,(r:[memberjson[],string[]])=>void>=new Map();
|
|
noncebuild:Map<string,[memberjson[],string[],number[]]>=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 (Tomato fork)";
|
|
}
|
|
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};
|