Merge pull request #70 from MathMan05/Voice

Time to merge I think
This commit is contained in:
MathMan05 2024-10-30 15:50:26 -05:00 committed by GitHub
commit 80e6fb1924
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
41 changed files with 4624 additions and 3306 deletions

View file

@ -102,10 +102,6 @@ app.use("/", async (req: Request, res: Response)=>{
res.sendFile(path.join(__dirname, "webpage", "invite.html"));
return;
}
if(req.path.endsWith("service.js")){
res.send("nope :3");
return;
}
const filePath = path.join(__dirname, "webpage", req.path);
try{
await fs.access(filePath);
@ -149,4 +145,4 @@ app.listen(PORT, ()=>{
console.log(`Server running on port ${PORT}`);
});
export{ getApiUrls };
export{ getApiUrls };

View file

@ -1,6 +1,6 @@
import{ getBulkInfo }from"./login.js";
class Voice{
class AVoice{
audioCtx: AudioContext;
info: { wave: string | Function; freq: number };
playing: boolean;
@ -95,7 +95,7 @@ class Voice{
static noises(noise: string): void{
switch(noise){
case"three": {
const voicy = new Voice("sin", 800);
const voicy = new AVoice("sin", 800);
voicy.play();
setTimeout(_=>{
voicy.freq = 1000;
@ -109,7 +109,7 @@ class Voice{
break;
}
case"zip": {
const voicy = new Voice((t: number, freq: number)=>{
const voicy = new AVoice((t: number, freq: number)=>{
return Math.sin((t + 2) ** Math.cos(t * 4) * Math.PI * 2 * freq);
}, 700);
voicy.play();
@ -119,7 +119,7 @@ class Voice{
break;
}
case"square": {
const voicy = new Voice("square", 600, 0.4);
const voicy = new AVoice("square", 600, 0.4);
voicy.play();
setTimeout(_=>{
voicy.freq = 800;
@ -133,7 +133,7 @@ class Voice{
break;
}
case"beep": {
const voicy = new Voice("sin", 800);
const voicy = new AVoice("sin", 800);
voicy.play();
setTimeout(_=>{
voicy.stop();
@ -146,6 +146,38 @@ class Voice{
}, 150);
break;
}
case "join":{
const voicy = new AVoice("triangle", 600,.1);
voicy.play();
setTimeout(_=>{
voicy.freq=800;
}, 75);
setTimeout(_=>{
voicy.freq=1000;
}, 150);
setTimeout(_=>{
voicy.stop();
}, 200);
break;
}
case "leave":{
const voicy = new AVoice("triangle", 850,.5);
voicy.play();
setTimeout(_=>{
voicy.freq=700;
}, 100);
setTimeout(_=>{
voicy.stop();
voicy.freq=400;
}, 180);
setTimeout(_=>{
voicy.play();
}, 200);
setTimeout(_=>{
voicy.stop();
}, 250);
break;
}
}
}
static get sounds(){
@ -161,4 +193,4 @@ class Voice{
return userinfos.preferences.notisound;
}
}
export{ Voice };
export{ AVoice as AVoice };

View file

@ -1,6 +1,6 @@
"use strict";
import{ Message }from"./message.js";
import{ Voice }from"./audio.js";
import{ AVoice }from"./audio.js";
import{ Contextmenu }from"./contextmenu.js";
import{ Dialog }from"./dialog.js";
import{ Guild }from"./guild.js";
@ -10,16 +10,11 @@ import{ Settings }from"./settings.js";
import{ Role, RoleList }from"./role.js";
import{ InfiniteScroller }from"./infiniteScroller.js";
import{ SnowFlake }from"./snowflake.js";
import{
channeljson,
embedjson,
messageCreateJson,
messagejson,
readyjson,
startTypingjson,
}from"./jsontypes.js";
import{channeljson,embedjson,messageCreateJson,messagejson,readyjson,startTypingjson}from"./jsontypes.js";
import{ MarkDown }from"./markdown.js";
import{ Member }from"./member.js";
import { Voice } from "./voice.js";
import { User } from "./user.js";
declare global {
interface NotificationOptions {
@ -55,6 +50,8 @@ class Channel extends SnowFlake{
idToPrev: Map<string, string> = new Map();
idToNext: Map<string, string> = new Map();
messages: Map<string, Message> = new Map();
voice?:Voice;
bitrate:number=128000;
static setupcontextmenu(){
this.contextmenu.addbutton("Copy channel id", function(this: Channel){
navigator.clipboard.writeText(this.id);
@ -64,7 +61,7 @@ class Channel extends SnowFlake{
this.readbottom();
});
this.contextmenu.addbutton("Settings[temp]", function(this: Channel){
this.contextmenu.addbutton("Settings", function(this: Channel){
this.generateSettings();
});
@ -79,17 +76,6 @@ class Channel extends SnowFlake{
}
);
this.contextmenu.addbutton(
"Edit channel",
function(this: Channel){
this.editChannel();
},
null,
function(){
return this.isAdmin();
}
);
this.contextmenu.addbutton(
"Make invite",
function(this: Channel){
@ -123,13 +109,14 @@ class Channel extends SnowFlake{
const div = document.createElement("div");
div.classList.add("invitediv");
const text = document.createElement("span");
text.classList.add("ellipsis");
div.append(text);
let uses = 0;
let expires = 1800;
const copycontainer = document.createElement("div");
copycontainer.classList.add("copycontainer");
const copy = document.createElement("span");
copy.classList.add("copybutton", "svgtheme", "svg-copy");
copy.classList.add("copybutton", "svgicon", "svg-copy");
copycontainer.append(copy);
copycontainer.onclick = _=>{
if(text.textContent){
@ -207,15 +194,33 @@ class Channel extends SnowFlake{
generateSettings(){
this.sortPerms();
const settings = new Settings("Settings for " + this.name);
const s1 = settings.addButton("roles");
{
const gensettings=settings.addButton("Settings");
const form=gensettings.addForm("",()=>{},{
fetchURL:this.info.api + "/channels/" + this.id,
method: "PATCH",
headers: this.headers,
});
form.addTextInput("Name:","name",{initText:this.name});
form.addMDInput("Topic:","topic",{initText:this.topic});
form.addCheckboxInput("NSFW:","nsfw",{initState:this.nsfw});
if(this.type!==4){
const options=["voice", "text", "announcement"];
form.addSelect("Type:","type",options,{
defaultIndex:options.indexOf({0:"text", 2:"voice", 5:"announcement", 4:"category" }[this.type] as string)
})
}
form.addPreprocessor((obj:any)=>{
obj.type={text: 0, voice: 2, announcement: 5, category: 4 }[obj.type as string]
})
}
const s1 = settings.addButton("Permisions");
s1.options.push(
new RoleList(
this.permission_overwritesar,
this.guild,
this.updateRolePermissions.bind(this),
true
this
)
);
settings.show();
@ -336,6 +341,10 @@ class Channel extends SnowFlake{
}
this.setUpInfiniteScroller();
this.perminfo ??= {};
if(this.type===2&&this.localuser.voiceFactory){
this.voice=this.localuser.voiceFactory.makeVoice(this.guild.id,this.id,{bitrate:this.bitrate});
this.setUpVoice();
}
}
get perminfo(){
return this.guild.perminfo.channels[this.id];
@ -457,6 +466,7 @@ class Channel extends SnowFlake{
get visable(){
return this.hasPermission("VIEW_CHANNEL");
}
voiceUsers=new WeakRef(document.createElement("div"));
createguildHTML(admin = false): HTMLDivElement{
const div = document.createElement("div");
this.html = new WeakRef(div);
@ -487,18 +497,18 @@ class Channel extends SnowFlake{
const decdiv = document.createElement("div");
const decoration = document.createElement("span");
decoration.classList.add("svgtheme", "collapse-icon", "svg-category");
decoration.classList.add("svgicon", "collapse-icon", "svg-category");
decdiv.appendChild(decoration);
const myhtml = document.createElement("p2");
myhtml.classList.add("ellipsis");
myhtml.textContent = this.name;
decdiv.appendChild(myhtml);
caps.appendChild(decdiv);
const childrendiv = document.createElement("div");
if(admin){
const addchannel = document.createElement("span");
addchannel.textContent = "+";
addchannel.classList.add("addchannel");
addchannel.classList.add("addchannel","svgicon","svg-plus");
caps.appendChild(addchannel);
addchannel.onclick = _=>{
this.guild.createchannels(this.createChannel.bind(this));
@ -506,8 +516,8 @@ class Channel extends SnowFlake{
this.coatDropDiv(decdiv, childrendiv);
}
div.appendChild(caps);
caps.classList.add("capsflex");
decdiv.classList.add("channeleffects");
caps.classList.add("flexltr","capsflex");
decdiv.classList.add("flexltr","channeleffects");
decdiv.classList.add("channel");
Channel.contextmenu.bindContextmenu(decdiv, this,undefined);
@ -552,32 +562,84 @@ class Channel extends SnowFlake{
}
// @ts-ignore I dont wanna deal with this
div.all = this;
const button = document.createElement("button");
button.classList.add("channelbutton");
div.append(button);
const myhtml = document.createElement("span");
myhtml.classList.add("ellipsis");
myhtml.textContent = this.name;
if(this.type === 0){
const decoration = document.createElement("span");
div.appendChild(decoration);
decoration.classList.add("space", "svgtheme", "svg-channel");
button.appendChild(decoration);
decoration.classList.add("space", "svgicon", "svg-channel");
}else if(this.type === 2){
//
const decoration = document.createElement("span");
div.appendChild(decoration);
decoration.classList.add("space", "svgtheme", "svg-voice");
button.appendChild(decoration);
decoration.classList.add("space", "svgicon", "svg-voice");
}else if(this.type === 5){
//
const decoration = document.createElement("span");
div.appendChild(decoration);
decoration.classList.add("space", "svgtheme", "svg-announce");
button.appendChild(decoration);
decoration.classList.add("space", "svgicon", "svg-announce");
}else{
console.log(this.type);
}
div.appendChild(myhtml);
div.onclick = _=>{
button.appendChild(myhtml);
button.onclick = _=>{
this.getHTML();
const toggle = document.getElementById("maintoggle") as HTMLInputElement;
toggle.checked = true;
};
if(this.type===2){
const voiceUsers=document.createElement("div");
div.append(voiceUsers);
this.voiceUsers=new WeakRef(voiceUsers);
this.updateVoiceUsers();
}
}
return div;
}
async setUpVoice(){
if(!this.voice) return;
this.voice.onMemberChange=async (memb,joined)=>{
console.log(memb,joined);
if(typeof memb!=="string"){
await Member.new(memb,this.guild);
}
this.updateVoiceUsers();
if(this.voice===this.localuser.currentVoice){
AVoice.noises("join");
}
}
}
async updateVoiceUsers(){
const voiceUsers=this.voiceUsers.deref();
if(!voiceUsers||!this.voice) return;
console.warn(this.voice.userids)
const html=(await Promise.all(this.voice.userids.entries().toArray().map(async _=>{
const user=await User.resolve(_[0],this.localuser);
console.log(user);
const member=await Member.resolveMember(user,this.guild);
const array=[member,_[1]] as [Member, typeof _[1]];
return array;
}))).flatMap(([member,_obj])=>{
if(!member){
console.warn("This is weird, member doesn't exist :P");
return [];
}
const div=document.createElement("div");
div.classList.add("voiceuser");
const span=document.createElement("span");
span.textContent=member.name;
div.append(span);
return div;
});
voiceUsers.innerHTML="";
voiceUsers.append(...html);
}
get myhtml(){
if(this.html){
return this.html.deref();
@ -684,68 +746,6 @@ class Channel extends SnowFlake{
}),
});
}
editChannel(){
let name = this.name;
let topic = this.topic;
let nsfw = this.nsfw;
const thisid = this.id;
const thistype = this.type;
const full = new Dialog([
"hdiv",
[
"vdiv",
[
"textbox",
"Channel name:",
this.name,
function(this: HTMLInputElement){
name = this.value;
},
],
[
"mdbox",
"Channel topic:",
this.topic,
function(this: HTMLTextAreaElement){
topic = this.value;
},
],
[
"checkbox",
"NSFW Channel",
this.nsfw,
function(this: HTMLInputElement){
nsfw = this.checked;
},
],
[
"button",
"",
"submit",
()=>{
fetch(this.info.api + "/channels/" + thisid, {
method: "PATCH",
headers: this.headers,
body: JSON.stringify({
name,
type: thistype,
topic,
bitrate: 64000,
user_limit: 0,
nsfw,
flags: 0,
rate_limit_per_user: 0,
}),
});
console.log(full);
full.hide();
},
],
],
]);
full.show();
console.log(full);
}
deleteChannel(){
fetch(this.info.api + "/channels/" + this.id, {
method: "DELETE",
@ -764,6 +764,7 @@ class Channel extends SnowFlake{
}
makereplybox(){
const replybox = document.getElementById("replybox") as HTMLElement;
const typebox = document.getElementById("typebox") as HTMLElement;
if(this.replyingto){
replybox.innerHTML = "";
const span = document.createElement("span");
@ -776,14 +777,16 @@ class Channel extends SnowFlake{
replybox.classList.add("hideReplyBox");
this.replyingto = null;
replybox.innerHTML = "";
typebox.classList.remove("typeboxreplying");
};
replybox.classList.remove("hideReplyBox");
X.textContent = "⦻";
X.classList.add("cancelReply");
X.classList.add("cancelReply","svgicon","svg-x");
replybox.append(span);
replybox.append(X);
typebox.classList.add("typeboxreplying");
}else{
replybox.classList.add("hideReplyBox");
typebox.classList.remove("typeboxreplying");
}
}
async getmessage(id: string): Promise<Message>{
@ -840,6 +843,9 @@ class Channel extends SnowFlake{
loading.classList.add("loading");
this.rendertyping();
this.localuser.getSidePannel();
if(this.voice&&localStorage.getItem("Voice enabled")){
this.localuser.joinVoice(this);
}
await this.putmessages();
await prom;
if(id !== Channel.genid){
@ -972,12 +978,7 @@ class Channel extends SnowFlake{
return;
}
await fetch(
this.info.api +
"/channels/" +
this.id +
"/messages?limit=100&after=" +
id,
{
this.info.api + "/channels/" +this.id +"/messages?limit=100&after=" +id,{
headers: this.headers,
}
)
@ -1175,12 +1176,11 @@ class Channel extends SnowFlake{
this.children = [];
this.guild_id = json.guild_id;
const oldover=this.permission_overwrites;
this.permission_overwrites = new Map();
this.permission_overwritesar=[];
for(const thing of json.permission_overwrites){
if(
thing.id === "1182819038095799904" ||
thing.id === "1182820803700625444"
){
if(thing.id === "1182819038095799904" || thing.id === "1182820803700625444"){
continue;
}
this.permission_overwrites.set(
@ -1195,9 +1195,26 @@ class Channel extends SnowFlake{
}
}
}
const nchange=[...new Set<string>().union(oldover).difference(this.permission_overwrites)];
const pchange=[...new Set<string>().union(this.permission_overwrites).difference(oldover)];
for(const thing of nchange){
const role=this.guild.roleids.get(thing);
if(role){
this.croleUpdate(role,new Permissions("0"),false)
}
}
for(const thing of pchange){
const role=this.guild.roleids.get(thing);
const perms=this.permission_overwrites.get(thing);
if(role&&perms){
this.croleUpdate(role,perms,true);
}
}
console.log(pchange,nchange);
this.topic = json.topic;
this.nsfw = json.nsfw;
}
croleUpdate:(role:Role,perm:Permissions,added:boolean)=>unknown=()=>{};
typingstart(){
if(this.typing > Date.now()){
return;
@ -1310,31 +1327,25 @@ class Channel extends SnowFlake{
return;
}
if(
this.localuser.lookingguild?.prevchannel === this &&
document.hasFocus()
this.localuser.lookingguild?.prevchannel === this && document.hasFocus()
){
return;
}
if(this.notification === "all"){
this.notify(messagez);
}else if(
this.notification === "mentions" &&
messagez.mentionsuser(this.localuser.user)
this.notification === "mentions" && messagez.mentionsuser(this.localuser.user)
){
this.notify(messagez);
}
}
notititle(message: Message): string{
return(
message.author.username +
" > " +
this.guild.properties.name +
" > " +
this.name
message.author.username + " > " + this.guild.properties.name + " > " + this.name
);
}
notify(message: Message, deep = 0){
Voice.noises(Voice.getNotificationSound());
AVoice.noises(AVoice.getNotificationSound());
if(!("Notification" in window)){
}else if(Notification.permission === "granted"){
let noticontent: string | undefined | null = message.content.textContent;
@ -1393,20 +1404,22 @@ class Channel extends SnowFlake{
if(permission){
permission.allow = perms.allow;
permission.deny = perms.deny;
await fetch(
this.info.api + "/channels/" + this.id + "/permissions/" + id,
{
method: "PUT",
headers: this.headers,
body: JSON.stringify({
allow: permission.allow.toString(),
deny: permission.deny.toString(),
id,
type: 0,
}),
}
);
}else{
//this.permission_overwrites.set(id,perms);
}
await fetch(
this.info.api + "/channels/" + this.id + "/permissions/" + id,
{
method: "PUT",
headers: this.headers,
body: JSON.stringify({
allow: perms.allow.toString(),
deny: perms.deny.toString(),
id,
type: 0,
}),
}
);
}
}
Channel.setupcontextmenu();

View file

@ -2,12 +2,12 @@ class Contextmenu<x, y>{
static currentmenu: HTMLElement | "";
name: string;
buttons: [
string,
(this: x, arg: y, e: MouseEvent) => void,
string | null,
(this: x, arg: y) => boolean,
(this: x, arg: y) => boolean,
string
string|(()=>string),
(this: x, arg: y, e: MouseEvent) => void,
string | null,
(this: x, arg: y) => boolean,
(this: x, arg: y) => boolean,
string
][];
div!: HTMLDivElement;
static setup(){
@ -27,7 +27,7 @@ class Contextmenu<x, y>{
this.buttons = [];
}
addbutton(
text: string,
text: string|(()=>string),
onclick: (this: x, arg: y, e: MouseEvent) => void,
img: null | string = null,
shown: (this: x, arg: y) => boolean = _=>true,
@ -58,7 +58,11 @@ class Contextmenu<x, y>{
const intext = document.createElement("button");
intext.disabled = !thing[4].bind(addinfo).call(addinfo, other);
intext.classList.add("contextbutton");
intext.textContent = thing[0];
if(thing[0] instanceof Function){
intext.textContent = thing[0]();
}else{
intext.textContent = thing[0];
}
console.log(thing);
if(thing[5] === "button" || thing[5] === "submenu"){
intext.onclick = thing[1].bind(addinfo, other);
@ -86,6 +90,13 @@ class Contextmenu<x, y>{
this.makemenu(event.clientX, event.clientY, addinfo, other);
};
obj.addEventListener("contextmenu", func);
obj.addEventListener("touchstart",(event: TouchEvent)=>{
if(event.touches.length > 1){
event.preventDefault();
event.stopImmediatePropagation();
this.makemenu(event.touches[0].clientX, event.touches[0].clientY, addinfo, other);
}
},{passive:true});
return func;
}
static keepOnScreen(obj: HTMLElement){

View file

@ -11,13 +11,7 @@ type dialogjson =
| ["title", string]
| ["radio", string, string[], (this: unknown, e: string) => unknown, number]
| ["html", HTMLElement]
| [
"select",
string,
string[],
(this: HTMLSelectElement, e: Event) => unknown,
number
]
| ["select", string, string[], (this: HTMLSelectElement, e: Event) => unknown, number]
| ["tabs", [string, dialogjson][]];
class Dialog{
layout: dialogjson;
@ -197,11 +191,17 @@ class Dialog{
case"select": {
const div = document.createElement("div");
const label = document.createElement("label");
const selectSpan = document.createElement("span");
selectSpan.classList.add("selectspan");
const select = document.createElement("select");
const selectArrow = document.createElement("span");
selectArrow.classList.add("svgicon","svg-category","selectarrow");
label.textContent = array[1];
selectSpan.append(select);
selectSpan.append(selectArrow);
div.append(label);
div.appendChild(select);
div.appendChild(selectSpan);
for(const thing of array[2]){
const option = document.createElement("option");
option.textContent = thing;

View file

@ -153,8 +153,9 @@ class Group extends Channel{
const div = document.createElement("div");
Group.contextmenu.bindContextmenu(div, this,undefined);
this.html = new WeakRef(div);
div.classList.add("channeleffects");
div.classList.add("flexltr","liststyle");
const myhtml = document.createElement("span");
myhtml.classList.add("ellipsis");
myhtml.textContent = this.name;
div.appendChild(this.user.buildpfp());
div.appendChild(myhtml);

View file

@ -157,13 +157,11 @@ Url.pathname.split("/")[Url.pathname.split("/").length - 1];
if(this.json?.footer?.text){
const span = document.createElement("span");
span.textContent = this.json.footer.text;
span.classList.add("spaceright");
footer.append(span);
}
if(this.json?.footer && this.json?.timestamp){
const span = document.createElement("span");
span.textContent = "•";
span.classList.add("spaceright");
span.textContent = " • ";
footer.append(span);
}
if(this.json?.timestamp){
@ -288,7 +286,7 @@ json.guild;
guild as invitejson["guild"] & { info: { cdn: string } }
);
const iconrow = document.createElement("div");
iconrow.classList.add("flexltr", "flexstart");
iconrow.classList.add("flexltr");
iconrow.append(icon);
{
const guildinfo = document.createElement("div");

View file

@ -143,7 +143,7 @@ class Emoji{
title.classList.add("emojiTitle");
menu.append(title);
const selection = document.createElement("div");
selection.classList.add("flexltr", "dontshrink", "emojirow");
selection.classList.add("flexltr", "emojirow");
const body = document.createElement("div");
body.classList.add("emojiBody");

View file

@ -83,7 +83,9 @@ class File{
div.append(contained);
const controls = document.createElement("div");
const garbage = document.createElement("button");
garbage.textContent = "🗑";
const icon = document.createElement("span");
icon.classList.add("svgicon","svg-delete");
garbage.append(icon);
garbage.onclick = _=>{
div.remove();
files.splice(files.indexOf(file), 1);

View file

@ -13,6 +13,7 @@ import{
emojijson,
memberjson,
invitejson,
rolesjson,
}from"./jsontypes.js";
import{ User }from"./user.js";
@ -114,16 +115,67 @@ class Guild extends SnowFlake{
}
form.addTextInput("Region:", "region", { initText: region });
}
const s1 = settings.addButton("roles");
const s1 = settings.addButton("Roles");
const permlist: [Role, Permissions][] = [];
for(const thing of this.roles){
permlist.push([thing, thing.permissions]);
}
s1.options.push(
new RoleList(permlist, this, this.updateRolePermissions.bind(this))
new RoleList(permlist, this, this.updateRolePermissions.bind(this),false)
);
settings.show();
}
roleUpdate:(role:Role,added:-1|0|1)=>unknown=()=>{};
sortRoles(){
this.roles.sort((a,b)=>(b.position-a.position));
}
async recalcRoles(){
let position=this.roles.length;
const map=this.roles.map(_=>{
position--;
return {id:_.id,position};
})
await fetch(this.info.api+"/guilds/"+this.id+"/roles",{
method:"PATCH",
body:JSON.stringify(map),
headers:this.headers
})
}
newRole(rolej:rolesjson){
const role=new Role(rolej,this);
this.roles.push(role);
this.roleids.set(role.id, role);
this.sortRoles();
this.roleUpdate(role,1);
}
updateRole(rolej:rolesjson){
const role=this.roleids.get(rolej.id) as Role;
role.newJson(rolej);
this.roleUpdate(role,0);
}
memberupdate(json:memberjson){
let member:undefined|Member=undefined;
for(const thing of this.members){
if(thing.id===json.id){
member=thing;
break;
}
}
if(!member) return;
member.update(json);
if(member===this.member){
console.log(member);
this.loadGuild();
}
}
deleteRole(id:string){
const role = this.roleids.get(id);
if(!role) return;
this.roleids.delete(id);
this.roles.splice(this.roles.indexOf(role),1);
this.roleUpdate(role,-1);
}
constructor(
json: guildjson | -1,
owner: Localuser,
@ -153,6 +205,7 @@ class Guild extends SnowFlake{
this.roles.push(roleh);
this.roleids.set(roleh.id, roleh);
}
this.sortRoles();
if(member instanceof User){
Member.resolveMember(member, this).then(_=>{
if(_){

View file

@ -11,28 +11,29 @@
<meta content="#4b458c" data-react-helmet="true" name="theme-color">
<link href="/style.css" rel="stylesheet">
<link href="/themes.css" rel="stylesheet" id="lightcss">
<style>body.no-theme{background:#16191b;}@media(prefers-color-scheme:light){body.no-theme{background:#9397bd;}}</style>
</head>
<body class="Dark-theme" style="overflow-y: scroll;">
<body class="no-theme" style="overflow-y: scroll;">
<div id="titleDiv">
<img src="/logo.svg" width="40">
<h1 id="pageTitle">Jank Client</h1>
<a href="https://sb-jankclient.vanillaminigames.net/invite/USgYJo?instance=https%3A%2F%2Fspacebar.chat"
class="TitleButtons">
<h1>Spacebar Guild</h1>
Spacebar Guild
</a>
<a href="https://github.com/MathMan05/JankClient" class="TitleButtons">
<h1>Github</h1>
Github
</a>
<a href="/channels/@me" class="TitleButtons">
Open Client
</a>
</div>
<div class="flexttb">
<div id="homePage">
<div class="flexttb pagehead">
<h1>Welcome to Jank Client</h1>
</div>
<h1 class="pagehead">Welcome to Jank Client</h1>
<div class="pagebox">
<p>Jank Client is a spacebar compatible client seeking to be as good as it can be with many features including:
</p>
<p>Jank Client is a Spacebar-compatible client seeking to be as good as it can be with many features including:</p>
<ul>
<li>Direct Messaging</li>
<li>Reactions support</li>
@ -44,16 +45,16 @@
</ul>
</div>
<div class="pagebox">
<h2>Spacebar compatible Instances:</h2>
<h2>Spacebar-Compatible Instances:</h2>
<div id="instancebox">
</div>
</div>
<div class="pagebox">
<h2>Contribute to Jank Client</h2>
<p>We always appreciate some help, wether that be in the form of bug reports, or code, or even just pointing out
<p>We always appreciate some help, whether that be in the form of bug reports, or code, or even just pointing out
some typos.</p><br>
</a><a href="https://github.com/MathMan05/JankClient" class="TitleButtons">
<h1>Github</h1>
<a href="https://github.com/MathMan05/JankClient" class="TitleButtons">
Github
</a>
</div>
</div>

View file

@ -7,22 +7,22 @@ fetch("/instances.json")
.then(
(
json: {
name: string;
description?: string;
descriptionLong?: string;
image?: string;
url?: string;
display?: boolean;
online?: boolean;
uptime: { alltime: number; daytime: number; weektime: number };
urls: {
wellknown: string;
api: string;
cdn: string;
gateway: string;
login?: string;
};
}[]
name: string;
description?: string;
descriptionLong?: string;
image?: string;
url?: string;
display?: boolean;
online?: boolean;
uptime: { alltime: number; daytime: number; weektime: number };
urls: {
wellknown: string;
api: string;
cdn: string;
gateway: string;
login?: string;
};
}[]
)=>{
console.warn(json);
for(const instance of json){
@ -37,11 +37,11 @@ login?: string;
div.append(img);
}
const statbox = document.createElement("div");
statbox.classList.add("flexttb");
statbox.classList.add("flexttb","flexgrow");
{
const textbox = document.createElement("div");
textbox.classList.add("flexttb", "instatancetextbox");
textbox.classList.add("flexttb", "instancetextbox");
const title = document.createElement("h2");
title.innerText = instance.name;
if(instance.online !== undefined){
@ -77,8 +77,7 @@ login?: string;
div.append(statbox);
div.onclick = _=>{
if(instance.online){
window.location.href =
"/register.html?instance=" + encodeURI(instance.name);
window.location.href = "/register.html?instance=" + encodeURI(instance.name);
}else{
alert("Instance is offline, can't connect");
}

96
src/webpage/i18n.ts Normal file
View file

@ -0,0 +1,96 @@
type translation={
[key:string]:string|{[key:string]:string}
};
let res:()=>unknown=()=>{};
class I18n{
static lang:string;
static translations:{[key:string]:string}[]=[];
static done=new Promise<void>((res2,_reject)=>{
res=res2;
});
static async create(json:translation|string,lang:string){
if(typeof json === "string"){
json=await (await fetch(json)).json() as translation;
}
const translations:{[key:string]:string}[]=[];
let translation=json[lang];
if(!translation){
translation=json[lang[0]+lang[1]];
if(!translation){
console.error(lang+" does not exist in the translations");
translation=json["en"];
lang="en";
}
}
translations.push(await this.toTranslation(translation,lang));
if(lang!=="en"){
translations.push(await this.toTranslation(json["en"],"en"))
}
this.lang=lang;
this.translations=translations;
res();
}
static getTranslation(msg:string,...params:string[]):string{
let str:string|undefined;
for(const json of this.translations){
str=json[msg];
if(str){
break;
}
}
if(str){
return this.fillInBlanks(str,params);
}else{
throw new Error(msg+" not found")
}
}
static fillInBlanks(msg:string,params:string[]):string{
//thanks to geotale for the regex
msg=msg.replace(/{{(.+?)}}/g,
(str, match:string) => {
const [op,strsSplit]=this.fillInBlanks(match,params).split(":");
const [first,...strs]=strsSplit.split("|");
switch(op.toUpperCase()){
case "PLURAL":{
const numb=Number(first);
if(numb===0){
return strs[strs.length-1];
}
return strs[Math.min(strs.length-1,numb-1)];
}
case "GENDER":{
if(first==="male"){
return strs[0];
}else if(first==="female"){
return strs[1];
}else if(first==="neutral"){
if(strs[2]){
return strs[2];
}else{
return strs[0];
}
}
}
}
return str;
}
);
msg=msg.replace(/\$\d+/g,(str, match:string) => {
const number=Number(match);
if(params[number-1]){
return params[number-1];
}else{
return str;
}
});
return msg;
}
private static async toTranslation(trans:string|{[key:string]:string},lang:string):Promise<{[key:string]:string}>{
if(typeof trans==='string'){
return this.toTranslation((await (await fetch(trans)).json() as translation)[lang],lang);
}else{
return trans;
}
}
}
export{I18n};

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 180 180"><path fill="#c9c9c9" stroke="red" stroke-linecap="round" stroke-width="24" d="M90 168V12M12 90h156"/></svg>

After

Width:  |  Height:  |  Size: 169 B

1
src/webpage/icons/x.svg Normal file
View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 180 180"><path fill="red" d="M90 0A90 90 0 0 0 0 90a90 90 0 0 0 90 90 90 90 0 0 0 90-90A90 90 0 0 0 90 0zM61.7 49.7a12 12 0 0 1 8.5 3.5L90 73l19.8-19.8a12 12 0 0 1 8.5-3.5 12 12 0 0 1 8.5 3.5 12 12 0 0 1 0 17L107 90l19.8 19.8a12 12 0 0 1 0 17 12 12 0 0 1-17 0L90 107l-19.8 19.8a12 12 0 0 1-17 0 12 12 0 0 1 0-17L73 90 53.2 70.2a12 12 0 0 1 0-17 12 12 0 0 1 8.5-3.5z"/></svg>

After

Width:  |  Height:  |  Size: 427 B

View file

@ -11,15 +11,15 @@
<meta content="#4b458c" data-react-helmet="true" name="theme-color">
<link href="/style.css" rel="stylesheet">
<link href="/themes.css" rel="stylesheet" id="lightcss">
<style>body.no-theme,#loading{background:#16191b;}@media(prefers-color-scheme:light){body.no-theme,#loading{background:#9397bd;}}</style>
<link rel="manifest" href="/manifest.json">
</head>
<body class="Dark-theme">
<body class="no-theme">
<script src="/index.js" type="module"></script>
<div id="loading" class="loading">
<div id="centerdiv">
<div class="centeritem">
<img src="/logo.svg" style="width:3in;height:3in;">
<h1>Jank Client is loading</h1>
<h2 id="load-desc">This shouldn't take long</h2>
@ -27,50 +27,62 @@
</div>
</div>
<div class="flexltr" id="page">
<div id="neunence">
<div id="servers"></div>
</div>
<div id="servers"></div>
<div class="flexttb channelflex">
<div class="servertd" id="servertd">
<h2 id="serverName">Server Name</h2>
<div class="flexltr header" id="servertd">
<h2 id="serverName" class="ellipsis">Server Name</h2>
</div>
<div id="channels"></div>
<div class="flexltr" id="userdock">
<div class="flexltr" id="userinfo">
<img id="userpfp" class="pfp">
<div class="userflex">
<p id="username">USERNAME</p>
<p id="status">STATUS</p>
</div>
<div class="flexttb">
<div class="flexltr" id="VoiceBox">
<span id="VoiceStatus"></span>
</div>
<div class="flexltr" id="userdock">
<div class="flexltr" id="userinfo">
<img id="userpfp" class="pfp">
<div id="user-actions">
<span id="settings" class="svgtheme svg-settings"></span>
<div class="flexttb userflex">
<p id="username">USERNAME</p>
<p id="status">STATUS</p>
</div>
</div>
<div id="user-actions">
<span id="settings" class="svgicon svg-settings"></span>
</div>
</div>
</div>
</div>
<div class="flexttb messageflex">
<div class="servertd channelnamediv">
<span id="mobileback" hidden></span>
<span id="channelname">Channel name</span>
<span id="channelTopic" hidden>Channel topic</span>
<div class="flexttb flexgrow" id="mainarea">
<div class="flexltr header channelnamediv">
<label for="maintoggle" id="maintoggleicon">
<span class="svgicon svg-category"></span>
</label>
<input type="checkbox" id="maintoggle">
<span class="flexltr">
<span id="channelname">Channel name</span>
<span id="channelTopic" class="ellipsis" hidden>Channel topic</span>
</span>
<label for="memberlisttoggle" id="memberlisttoggleicon">
<span class="svgicon svg-channel"></span>
</label>
<input type="checkbox" id="memberlisttoggle" checked>
</div>
<div class="flexltr">
<div class="flexttb">
<div id="channelw">
<div class="flexltr flexgrow">
<div class="flexttb flexgrow">
<div id="channelw" class="flexltr">
<div id="loadingdiv">
</div>
</div>
<div id="pasteimage"></div>
<div id="pasteimage" class="flexltr"></div>
<div id="replybox" class="hideReplyBox"></div>
<div id="typediv">
<div id="realbox">
<div id="typebox" contentEditable="true"></div>
</div>
<div id="typing" class="hidden">
<div id="typing" class="hidden flexltr">
<p id="typingtext">typing</p>
<div class="loading-indicator">
<div class="flexltr loading-indicator">
<div class="dot"></div>
<div class="dot"></div>
<div class="dot"></div>

View file

@ -22,7 +22,7 @@ import{ File }from"./file.js";
function showAccountSwitcher(): void{
const table = document.createElement("div");
table.classList.add("accountSwitcher");
table.classList.add("flexttb","accountSwitcher");
for(const user of Object.values(users.users)){
const specialUser = user as Specialuser;
@ -235,25 +235,12 @@ import{ File }from"./file.js";
userSettings;
if(mobile){
const channelWrapper = document.getElementById(
"channelw"
) as HTMLDivElement;
const channelWrapper = document.getElementById("channelw") as HTMLDivElement;
channelWrapper.onclick = ()=>{
(
document.getElementById("channels")!.parentNode as HTMLElement
).classList.add("collapse");
document.getElementById("servertd")!.classList.add("collapse");
document.getElementById("servers")!.classList.add("collapse");
};
const mobileBack = document.getElementById("mobileback") as HTMLDivElement;
mobileBack.textContent = "#";
mobileBack.onclick = ()=>{
(
document.getElementById("channels")!.parentNode as HTMLElement
).classList.remove("collapse");
document.getElementById("servertd")!.classList.remove("collapse");
document.getElementById("servers")!.classList.remove("collapse");
const toggle = document.getElementById("maintoggle") as HTMLInputElement;
toggle.checked = true;
};
const memberListToggle = document.getElementById("memberlisttoggle") as HTMLInputElement;
memberListToggle.checked = false;
}
})();

View file

@ -39,7 +39,7 @@ offset: number
}
const scroll = document.createElement("div");
scroll.classList.add("flexttb", "scroller");
scroll.classList.add("scroller");
this.div = scroll;
this.div.addEventListener("scroll", ()=>{

View file

@ -1,4 +1,5 @@
<body class="Dark-theme">
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
@ -10,14 +11,17 @@
<meta content="#4b458c" data-react-helmet="true" name="theme-color">
<link href="/style.css" rel="stylesheet">
<link href="/themes.css" rel="stylesheet" id="lightcss">
<style>body.no-theme{background:#16191b;}@media(prefers-color-scheme:light){body.no-theme{background:#9397bd;}}</style>
</head>
<div>
<div id="invitebody">
<div id="inviteimg"></div>
<h1 id="invitename">Server Name</h1>
<p id="invitedescription">Someone invited you to Server Name</p>
<button id="AcceptInvite">Accept Invite</button>
<body class="no-theme">
<div>
<div id="invitebody">
<div id="inviteimg"></div>
<h1 id="invitename">Server Name</h1>
<p id="invitedescription">Someone invited you to Server Name</p>
<button id="AcceptInvite">Accept Invite</button>
</div>
</div>
</div>
<script type="module" src="/invite.js"></script>
</body>
<script type="module" src="/invite.js"></script>
</body>
</html>

View file

@ -136,7 +136,7 @@ document.getElementById("inviteimg")!.append(div);
}
table.append(td);
table.classList.add("accountSwitcher");
table.classList.add("flexttb","accountSwitcher");
console.log(table);
document.body.append(table);
}

View file

@ -1,120 +1,120 @@
type readyjson = {
op: 0;
t: "READY";
s: number;
d: {
v: number;
user: mainuserjson;
user_settings: {
index: number;
afk_timeout: number;
allow_accessibility_detection: boolean;
animate_emoji: boolean;
animate_stickers: number;
contact_sync_enabled: boolean;
convert_emoticons: boolean;
custom_status: string;
default_guilds_restricted: boolean;
detect_platform_accounts: boolean;
developer_mode: boolean;
disable_games_tab: boolean;
enable_tts_command: boolean;
explicit_content_filter: 0;
friend_discovery_flags: 0;
friend_source_flags: {
all: boolean;
}; //might be missing things here
gateway_connected: boolean;
gif_auto_play: boolean;
guild_folders: []; //need an example of this not empty
guild_positions: []; //need an example of this not empty
inline_attachment_media: boolean;
inline_embed_media: boolean;
locale: string;
message_display_compact: boolean;
native_phone_integration_enabled: boolean;
render_embeds: boolean;
render_reactions: boolean;
restricted_guilds: []; //need an example of this not empty
show_current_game: boolean;
status: string;
stream_notifications_enabled: boolean;
theme: string;
timezone_offset: number;
view_nsfw_guilds: boolean;
};
guilds: guildjson[];
relationships: {
id: string;
type: 0 | 1 | 2 | 3 | 4;
nickname: string | null;
user: userjson;
}[];
read_state: {
entries: {
id: string;
channel_id: string;
last_message_id: string;
last_pin_timestamp: string;
mention_count: number; //in theory, the server doesn't actually send this as far as I'm aware
}[];
partial: boolean;
version: number;
};
user_guild_settings: {
entries: {
channel_overrides: unknown[]; //will have to find example
message_notifications: number;
flags: number;
hide_muted_channels: boolean;
mobile_push: boolean;
mute_config: null;
mute_scheduled_events: boolean;
muted: boolean;
notify_highlights: number;
suppress_everyone: boolean;
suppress_roles: boolean;
version: number;
guild_id: string;
}[];
partial: boolean;
version: number;
};
private_channels: dirrectjson[];
session_id: string;
country_code: string;
users: userjson[];
merged_members: [memberjson][];
sessions: {
active: boolean;
activities: []; //will need to find example of this
client_info: {
version: number;
};
session_id: string;
status: string;
}[];
resume_gateway_url: string;
consents: {
personalization: {
consented: boolean;
};
};
experiments: []; //not sure if I need to do this :P
guild_join_requests: []; //need to get examples
connected_accounts: []; //need to get examples
guild_experiments: []; //need to get examples
geo_ordered_rtc_regions: []; //need to get examples
api_code_version: number;
friend_suggestion_count: number;
analytics_token: string;
tutorial: boolean;
session_type: string;
auth_session_id_hash: string;
notification_settings: {
flags: number;
};
};
op: 0;
t: "READY";
s: number;
d: {
v: number;
user: mainuserjson;
user_settings: {
index: number;
afk_timeout: number;
allow_accessibility_detection: boolean;
animate_emoji: boolean;
animate_stickers: number;
contact_sync_enabled: boolean;
convert_emoticons: boolean;
custom_status: string;
default_guilds_restricted: boolean;
detect_platform_accounts: boolean;
developer_mode: boolean;
disable_games_tab: boolean;
enable_tts_command: boolean;
explicit_content_filter: 0;
friend_discovery_flags: 0;
friend_source_flags: {
all: boolean;
}; //might be missing things here
gateway_connected: boolean;
gif_auto_play: boolean;
guild_folders: []; //need an example of this not empty
guild_positions: []; //need an example of this not empty
inline_attachment_media: boolean;
inline_embed_media: boolean;
locale: string;
message_display_compact: boolean;
native_phone_integration_enabled: boolean;
render_embeds: boolean;
render_reactions: boolean;
restricted_guilds: []; //need an example of this not empty
show_current_game: boolean;
status: string;
stream_notifications_enabled: boolean;
theme: string;
timezone_offset: number;
view_nsfw_guilds: boolean;
};
guilds: guildjson[];
relationships: {
id: string;
type: 0 | 1 | 2 | 3 | 4;
nickname: string | null;
user: userjson;
}[];
read_state: {
entries: {
id: string;
channel_id: string;
last_message_id: string;
last_pin_timestamp: string;
mention_count: number; //in theory, the server doesn't actually send this as far as I'm aware
}[];
partial: boolean;
version: number;
};
user_guild_settings: {
entries: {
channel_overrides: unknown[]; //will have to find example
message_notifications: number;
flags: number;
hide_muted_channels: boolean;
mobile_push: boolean;
mute_config: null;
mute_scheduled_events: boolean;
muted: boolean;
notify_highlights: number;
suppress_everyone: boolean;
suppress_roles: boolean;
version: number;
guild_id: string;
}[];
partial: boolean;
version: number;
};
private_channels: dirrectjson[];
session_id: string;
country_code: string;
users: userjson[];
merged_members: [memberjson][];
sessions: {
active: boolean;
activities: []; //will need to find example of this
client_info: {
version: number;
};
session_id: string;
status: string;
}[];
resume_gateway_url: string;
consents: {
personalization: {
consented: boolean;
};
};
experiments: []; //not sure if I need to do this :P
guild_join_requests: []; //need to get examples
connected_accounts: []; //need to get examples
guild_experiments: []; //need to get examples
geo_ordered_rtc_regions: []; //need to get examples
api_code_version: number;
friend_suggestion_count: number;
analytics_token: string;
tutorial: boolean;
session_type: string;
auth_session_id_hash: string;
notification_settings: {
flags: number;
};
};
};
type mainuserjson = userjson & {
flags: number;
@ -129,20 +129,20 @@ type mainuserjson = userjson & {
disabled: boolean;
};
type userjson = {
username: string;
discriminator: string;
id: string;
public_flags: number;
avatar: string | null;
accent_color: number;
banner?: string;
bio: string;
bot: boolean;
premium_since: string;
premium_type: number;
theme_colors: string;
pronouns: string;
badge_ids: string[];
username: string;
discriminator: string;
id: string;
public_flags: number;
avatar: string | null;
accent_color: number;
banner?: string;
bio: string;
bot: boolean;
premium_since: string;
premium_type: number;
theme_colors: string;
pronouns: string;
badge_ids: string[];
};
type memberjson = {
index?: number;
@ -163,9 +163,9 @@ type memberjson = {
last_message_id?: boolean; //What???
};
type emojijson = {
name: string;
id?: string;
animated?: boolean;
name: string;
id?: string;
animated?: boolean;
};
type guildjson = {
@ -225,37 +225,37 @@ type guildjson = {
joined_at: string;
};
type startTypingjson = {
d: {
channel_id: string;
guild_id?: string;
user_id: string;
timestamp: number;
member?: memberjson;
};
d: {
channel_id: string;
guild_id?: string;
user_id: string;
timestamp: number;
member?: memberjson;
};
};
type channeljson = {
id: string;
created_at: string;
name: string;
icon: string;
type: number;
last_message_id: string;
guild_id: string;
parent_id: string;
last_pin_timestamp: string;
default_auto_archive_duration: number;
permission_overwrites: {
id: string;
allow: string;
deny: string;
}[];
video_quality_mode: null;
nsfw: boolean;
topic: string;
retention_policy_id: string;
flags: number;
default_thread_rate_limit_per_user: number;
position: number;
id: string;
created_at: string;
name: string;
icon: string;
type: number;
last_message_id: string;
guild_id: string;
parent_id: string;
last_pin_timestamp: string;
default_auto_archive_duration: number;
permission_overwrites: {
id: string;
allow: string;
deny: string;
}[];
video_quality_mode: null;
nsfw: boolean;
topic: string;
retention_policy_id: string;
flags: number;
default_thread_rate_limit_per_user: number;
position: number;
};
type rolesjson = {
id: string;
@ -272,127 +272,136 @@ type rolesjson = {
flags: number;
};
type dirrectjson = {
id: string;
flags: number;
last_message_id: string;
type: number;
recipients: userjson[];
is_spam: boolean;
id: string;
flags: number;
last_message_id: string;
type: number;
recipients: userjson[];
is_spam: boolean;
};
type messagejson = {
id: string;
channel_id: string;
guild_id: string;
author: userjson;
member?: memberjson;
content: string;
timestamp: string;
edited_timestamp: string;
tts: boolean;
mention_everyone: boolean;
mentions: []; //need examples to fix
mention_roles: []; //need examples to fix
attachments: filejson[];
embeds: embedjson[];
reactions: {
count: number;
emoji: emojijson; //very likely needs expanding
me: boolean;
}[];
nonce: string;
pinned: boolean;
type: number;
id: string;
channel_id: string;
guild_id: string;
author: userjson;
member?: memberjson;
content: string;
timestamp: string;
edited_timestamp: string;
tts: boolean;
mention_everyone: boolean;
mentions: []; //need examples to fix
mention_roles: []; //need examples to fix
attachments: filejson[];
embeds: embedjson[];
reactions: {
count: number;
emoji: emojijson; //very likely needs expanding
me: boolean;
}[];
nonce: string;
pinned: boolean;
type: number;
};
type filejson = {
id: string;
filename: string;
content_type: string;
width?: number;
height?: number;
proxy_url: string | undefined;
url: string;
size: number;
id: string;
filename: string;
content_type: string;
width?: number;
height?: number;
proxy_url: string | undefined;
url: string;
size: number;
};
type embedjson = {
type: string | null;
color?: number;
author: {
icon_url?: string;
name?: string;
url?: string;
title?: string;
};
title?: string;
url?: string;
description?: string;
fields?: {
name: string;
value: string;
inline: boolean;
}[];
footer?: {
icon_url?: string;
text?: string;
thumbnail?: string;
};
timestamp?: string;
thumbnail: {
proxy_url: string;
url: string;
width: number;
height: number;
};
provider: {
name: string;
};
video?: {
url: string;
width?: number | null;
height?: number | null;
proxy_url?: string;
};
invite?: {
url: string;
code: string;
};
type: string | null;
color?: number;
author: {
icon_url?: string;
name?: string;
url?: string;
title?: string;
};
title?: string;
url?: string;
description?: string;
fields?: {
name: string;
value: string;
inline: boolean;
}[];
footer?: {
icon_url?: string;
text?: string;
thumbnail?: string;
};
timestamp?: string;
thumbnail: {
proxy_url: string;
url: string;
width: number;
height: number;
};
provider: {
name: string;
};
video?: {
url: string;
width?: number | null;
height?: number | null;
proxy_url?: string;
};
invite?: {
url: string;
code: string;
};
};
type invitejson = {
code: string;
temporary: boolean;
uses: number;
max_use: number;
max_age: number;
created_at: string;
expires_at: string;
guild_id: string;
channel_id: string;
inviter_id: string;
target_user_id: string | null;
target_user_type: string | null;
vanity_url: string | null;
flags: number;
guild: guildjson["properties"];
channel: channeljson;
inviter: userjson;
code: string;
temporary: boolean;
uses: number;
max_use: number;
max_age: number;
created_at: string;
expires_at: string;
guild_id: string;
channel_id: string;
inviter_id: string;
target_user_id: string | null;
target_user_type: string | null;
vanity_url: string | null;
flags: number;
guild: guildjson["properties"];
channel: channeljson;
inviter: userjson;
};
type presencejson = {
status: string;
since: number | null;
activities: any[]; //bit more complicated but not now
afk: boolean;
user?: userjson;
status: string;
since: number | null;
activities: any[]; //bit more complicated but not now
afk: boolean;
user?: userjson;
};
type messageCreateJson = {
op: 0;
d: {
guild_id?: string;
channel_id?: string;
} & messagejson;
s: number;
t: "MESSAGE_CREATE";
op: 0;
d: {
guild_id?: string;
channel_id?: string;
} & messagejson;
s: number;
t: "MESSAGE_CREATE";
};
type roleCreate={
op: 0,
t: "GUILD_ROLE_CREATE",
d: {
guild_id: string,
role: rolesjson
},
s: 6
}
type wsjson =
| {
roleCreate | {
op: 0;
d: any;
s: number;
@ -408,79 +417,130 @@ type wsjson =
| "MESSAGE_REACTION_REMOVE_EMOJI";
}
| {
op: 0;
t: "GUILD_MEMBERS_CHUNK";
d: memberChunk;
s: number;
op: 0;
t: "GUILD_MEMBERS_CHUNK";
d: memberChunk;
s: number;
}
| {
op: 0;
d: {
id: string;
guild_id?: string;
channel_id: string;
};
s: number;
t: "MESSAGE_DELETE";
op: 0;
d: {
id: string;
guild_id?: string;
channel_id: string;
};
s: number;
t: "MESSAGE_DELETE";
}
| {
op: 0;
d: {
guild_id?: string;
channel_id: string;
} & messagejson;
s: number;
t: "MESSAGE_UPDATE";
op: 0;
d: {
guild_id?: string;
channel_id: string;
} & messagejson;
s: number;
t: "MESSAGE_UPDATE";
}
| messageCreateJson
| readyjson
| {
op: 11;
s: undefined;
d: {};
op: 11;
s: undefined;
d: {};
}
| {
op: 10;
s: undefined;
d: {
heartbeat_interval: number;
};
op: 10;
s: undefined;
d: {
heartbeat_interval: number;
};
}
| {
op: 0;
t: "MESSAGE_REACTION_ADD";
d: {
user_id: string;
channel_id: string;
message_id: string;
guild_id?: string;
emoji: emojijson;
member?: memberjson;
};
s: number;
op: 0;
t: "MESSAGE_REACTION_ADD";
d: {
user_id: string;
channel_id: string;
message_id: string;
guild_id?: string;
emoji: emojijson;
member?: memberjson;
};
s: number;
}
| {
op: 0;
t: "MESSAGE_REACTION_REMOVE";
d: {
user_id: string;
channel_id: string;
message_id: string;
guild_id: string;
emoji: emojijson;
};
s: 3;
}|memberlistupdatejson;
type memberChunk = {
guild_id: string;
nonce: string;
members: memberjson[];
presences: presencejson[];
chunk_index: number;
chunk_count: number;
not_found: string[];
};
op: 0;
t: "MESSAGE_REACTION_REMOVE";
d: {
user_id: string;
channel_id: string;
message_id: string;
guild_id: string;
emoji: emojijson;
};
s: number;
}|{
op: 0,
t: "GUILD_ROLE_UPDATE",
d: {
guild_id: string,
role: rolesjson
},
"s": number
}|{
op: 0,
t: "GUILD_ROLE_DELETE",
d: {
guild_id: string,
role_id: string
},
s:number
}|{
op: 0,
t: "GUILD_MEMBER_UPDATE",
d: memberjson,
"s": 3
}|memberlistupdatejson|voiceupdate|voiceserverupdate;
type memberChunk = {
guild_id: string;
nonce: string;
members: memberjson[];
presences: presencejson[];
chunk_index: number;
chunk_count: number;
not_found: string[];
};
type voiceupdate={
op: 0,
t: "VOICE_STATE_UPDATE",
d: {
guild_id: string,
channel_id: string,
user_id: string,
member: memberjson,
session_id: string,
token: string,
deaf: boolean,
mute: boolean,
self_deaf: boolean,
self_mute: boolean,
self_video: boolean,
suppress: boolean
},
s: number
};
type voiceserverupdate={
op: 0,
t: "VOICE_SERVER_UPDATE",
d: {
token: string,
guild_id: string,
endpoint: string
},
s: 6
};
type memberlistupdatejson={
op: 0,
s: number,
@ -511,7 +571,73 @@ type memberlistupdatejson={
count: number,
id: string
}[]
}
}
type webRTCSocket= {
op: 8,
d: {
heartbeat_interval: number
}
}|{
op:6,
d:{t: number}
}|{
op: 2,
d: {
ssrc: number,
"streams": {
type: "video",//probally more options, but idk
rid: string,
quality: number,
ssrc: number,
rtx_ssrc:number
}[],
ip: number,
port: number,
"modes": [],//no clue
"experiments": []//no clue
}
}|sdpback|opRTC12|{
op: 5,
d: {
user_id: string,
speaking: 0,
ssrc: 940464811
}
};
type sdpback={
op: 4,
d: {
audioCodec: string,
videoCodec: string,
media_session_id: string,
sdp: string
}
};
type opRTC12={
op: 12,
d: {
user_id: string,
audio_ssrc: number,
video_ssrc: number,
streams: [
{
type: "video",
rid: "100",
ssrc: number,
active: boolean,
quality: 100,
rtx_ssrc: number,
max_bitrate: 2500000,
max_framerate: number,
max_resolution: {
type: "fixed",
width: number,
height: number
}
}
]
}
}
export{
readyjson,
@ -532,5 +658,10 @@ export{
messageCreateJson,
memberChunk,
invitejson,
memberlistupdatejson
memberlistupdatejson,
voiceupdate,
voiceserverupdate,
webRTCSocket,
sdpback,
opRTC12
};

View file

@ -1,35 +1,23 @@
import{ Guild }from"./guild.js";
import{ Channel }from"./channel.js";
import{ Direct }from"./direct.js";
import{ Voice }from"./audio.js";
import{ AVoice }from"./audio.js";
import{ User }from"./user.js";
import{ Dialog }from"./dialog.js";
import{ getapiurls, getBulkInfo, setTheme, Specialuser }from"./login.js";
import{
channeljson,
guildjson,
mainuserjson,
memberjson,
memberlistupdatejson,
messageCreateJson,
presencejson,
readyjson,
startTypingjson,
wsjson,
}from"./jsontypes.js";
import{ getapiurls, getBulkInfo, setTheme, Specialuser, SW }from"./login.js";
import{channeljson,guildjson,mainuserjson,memberjson,memberlistupdatejson,messageCreateJson,presencejson,readyjson,startTypingjson,wsjson,}from"./jsontypes.js";
import{ Member }from"./member.js";
import{ Form, FormError, Options, Settings }from"./settings.js";
import{ MarkDown }from"./markdown.js";
import { Bot } from "./bot.js";
import { Role } from "./role.js";
import { VoiceFactory } from "./voice.js";
import { I18n } from "./i18n.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();
badges: Map<string,{ id: string; description: string; icon: string; link: string }> = new Map();
lastSequence: number | null = null;
token!: string;
userinfo!: Specialuser;
@ -52,6 +40,7 @@ class Localuser{
errorBackoff = 0;
channelids: Map<string, Channel> = new Map();
readonly userMap: Map<string, User> = new Map();
voiceFactory?:VoiceFactory;
instancePing = {
name: "Unknown",
};
@ -76,26 +65,33 @@ class Localuser{
"Content-type": "application/json; charset=UTF-8",
Authorization: this.userinfo.token,
};
I18n.create("/translations/en.json","en")
}
gottenReady(ready: readyjson): void{
async gottenReady(ready: readyjson): Promise<void>{
await I18n.done;
this.initialized = true;
this.ready = ready;
this.guilds = [];
this.guildids = new Map();
this.user = new User(ready.d.user, this);
this.user.setstatus("online");
this.voiceFactory=new VoiceFactory({id:this.user.id});
this.handleVoice();
this.mfa_enabled = ready.d.user.mfa_enabled as boolean;
this.userinfo.username = this.user.username;
this.userinfo.id = this.user.id;
this.userinfo.pfpsrc = this.user.getpfpsrc();
this.status = this.ready.d.user_settings.status;
this.channelfocus = undefined;
this.lookingguild = undefined;
this.guildhtml = new Map();
const members: { [key: string]: memberjson } = {};
for(const thing of ready.d.merged_members){
members[thing[0].guild_id] = thing[0];
if(ready.d.merged_members){
for(const thing of ready.d.merged_members){
members[thing[0].guild_id] = thing[0];
}
}
for(const thing of ready.d.guilds){
const temp = new Guild(thing, this, members[thing.id]);
this.guilds.push(temp);
@ -127,6 +123,7 @@ class Localuser{
this.pingEndpoint();
this.userinfo.updateLocal();
}
outoffocus(): void{
const servers = document.getElementById("servers") as HTMLDivElement;
@ -205,10 +202,10 @@ class Localuser{
try{
const temp = JSON.parse(build);
build = "";
await this.handleEvent(temp);
if(temp.op === 0 && temp.t === "READY"){
returny();
}
await this.handleEvent(temp);
}catch{}
}
})();
@ -236,9 +233,9 @@ class Localuser{
if(
!(
array[len - 1] === 255 &&
array[len - 2] === 255 &&
array[len - 3] === 0 &&
array[len - 4] === 0
array[len - 2] === 255 &&
array[len - 3] === 0 &&
array[len - 4] === 0
)
){
return;
@ -249,10 +246,11 @@ class Localuser{
}else{
temp = JSON.parse(event.data);
}
await this.handleEvent(temp as readyjson);
if(temp.op === 0 && temp.t === "READY"){
returny();
}
await this.handleEvent(temp as readyjson);
}catch(e){
console.error(e);
}finally{
@ -357,137 +355,174 @@ class Localuser{
if(temp.s)this.lastSequence = temp.s;
if(temp.op == 0){
switch(temp.t){
case"MESSAGE_CREATE":
if(this.initialized){
this.messageCreate(temp);
}
break;
case"MESSAGE_DELETE": {
temp.d.guild_id ??= "@me";
const channel = this.channelids.get(temp.d.channel_id);
if(!channel)break;
const message = channel.messages.get(temp.d.id);
if(!message)break;
message.deleteEvent();
break;
}
case"READY":
this.gottenReady(temp as readyjson);
break;
case"MESSAGE_UPDATE": {
temp.d.guild_id ??= "@me";
const channel = this.channelids.get(temp.d.channel_id);
if(!channel)break;
const message = channel.messages.get(temp.d.id);
if(!message)break;
message.giveData(temp.d);
break;
}
case"TYPING_START":
if(this.initialized){
this.typingStart(temp);
}
break;
case"USER_UPDATE":
if(this.initialized){
const users = this.userMap.get(temp.d.id);
if(users){
users.userupdate(temp.d);
case"MESSAGE_CREATE":
if(this.initialized){
this.messageCreate(temp);
}
}
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":
{
break;
case"MESSAGE_DELETE": {
temp.d.guild_id ??= "@me";
const guild = this.guildids.get(temp.d.guild_id);
if(!guild)break;
const channel = this.channelids.get(temp.d.channel_id);
if(!channel)break;
const message = channel.messages.get(temp.d.message_id);
const message = channel.messages.get(temp.d.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.deleteEvent();
break;
}
case"READY":
await this.gottenReady(temp as readyjson);
break;
case"MESSAGE_UPDATE": {
temp.d.guild_id ??= "@me";
const channel = this.channelids.get(temp.d.channel_id);
if(!channel)break;
const message = channel.messages.get(temp.d.id);
if(!message)break;
message.giveData(temp.d);
break;
}
case"TYPING_START":
if(this.initialized){
this.typingStart(temp);
}
message.reactionAdd(temp.d.emoji, thing);
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;
}
break;
case"MESSAGE_REACTION_REMOVE":
case"GUILD_CREATE": {
const guildy = new Guild(temp.d, this, this.user);
this.guilds.push(guildy);
this.guildids.set(guildy.id, guildy);
(document.getElementById("servers") as HTMLDivElement).insertBefore(
guildy.generateGuildIcon(),
document.getElementById("bottomseparator")
);
break;
}
case"MESSAGE_REACTION_ADD":
{
temp.d.guild_id ??= "@me";
const guild = this.guildids.get(temp.d.guild_id);
if(!guild)break;
const channel = this.channelids.get(temp.d.channel_id);
if(!channel)break;
const message = channel.messages.get(temp.d.message_id);
if(!message)break;
let thing: Member | { id: string };
if(temp.d.member){
thing = (await Member.new(temp.d.member, guild)) as Member;
}else{
thing = { id: temp.d.user_id };
}
message.reactionAdd(temp.d.emoji, thing);
}
break;
case"MESSAGE_REACTION_REMOVE":
{
temp.d.guild_id ??= "@me";
const channel = this.channelids.get(temp.d.channel_id);
if(!channel)break;
const message = channel.messages.get(temp.d.message_id);
if(!message)break;
message.reactionRemove(temp.d.emoji, temp.d.user_id);
}
break;
case"MESSAGE_REACTION_REMOVE_ALL":
{
temp.d.guild_id ??= "@me";
const channel = this.channelids.get(temp.d.channel_id);
if(!channel)break;
const message = channel.messages.get(temp.d.message_id);
if(!message)break;
message.reactionRemoveAll();
}
break;
case"MESSAGE_REACTION_REMOVE_EMOJI":
{
temp.d.guild_id ??= "@me";
const channel = this.channelids.get(temp.d.channel_id);
if(!channel)break;
const message = channel.messages.get(temp.d.message_id);
if(!message)break;
message.reactionRemoveEmoji(temp.d.emoji);
}
break;
case"GUILD_MEMBERS_CHUNK":
this.gotChunk(temp.d);
break;
case"GUILD_MEMBER_LIST_UPDATE":
{
temp.d.guild_id ??= "@me";
const channel = this.channelids.get(temp.d.channel_id);
if(!channel)break;
const message = channel.messages.get(temp.d.message_id);
if(!message)break;
message.reactionRemove(temp.d.emoji, temp.d.user_id);
this.memberListUpdate(temp)
break;
}
break;
case"MESSAGE_REACTION_REMOVE_ALL":
{
temp.d.guild_id ??= "@me";
const channel = this.channelids.get(temp.d.channel_id);
if(!channel)break;
const message = channel.messages.get(temp.d.message_id);
if(!message)break;
message.reactionRemoveAll();
case "VOICE_STATE_UPDATE":
if(this.voiceFactory){
this.voiceFactory.voiceStateUpdate(temp)
}
break;
case "VOICE_SERVER_UPDATE":
if(this.voiceFactory){
this.voiceFactory.voiceServerUpdate(temp)
}
break;
case "GUILD_ROLE_CREATE":{
const guild=this.guildids.get(temp.d.guild_id);
if(!guild) break;
guild.newRole(temp.d.role);
break;
}
break;
case"MESSAGE_REACTION_REMOVE_EMOJI":
{
temp.d.guild_id ??= "@me";
const channel = this.channelids.get(temp.d.channel_id);
if(!channel)break;
const message = channel.messages.get(temp.d.message_id);
if(!message)break;
message.reactionRemoveEmoji(temp.d.emoji);
case "GUILD_ROLE_UPDATE":{
const guild=this.guildids.get(temp.d.guild_id);
if(!guild) break;
guild.updateRole(temp.d.role);
break;
}
case "GUILD_ROLE_DELETE":{
const guild=this.guildids.get(temp.d.guild_id);
if(!guild) break;
guild.deleteRole(temp.d.role_id);
break;
}
case "GUILD_MEMBER_UPDATE":{
const guild=this.guildids.get(temp.d.guild_id);
if(!guild) break;
guild.memberupdate(temp.d)
break
}
break;
case"GUILD_MEMBERS_CHUNK":
this.gotChunk(temp.d);
break;
case"GUILD_MEMBER_LIST_UPDATE":
{
this.memberListUpdate(temp)
break;
}
}
}else if(temp.op === 10){
if(!this.ws)return;
console.log("heartbeat down");
@ -501,6 +536,30 @@ class Localuser{
}, this.heartbeat_interval);
}
}
get currentVoice(){
return this.voiceFactory?.currentVoice;
}
async joinVoice(channel:Channel){
if(!this.voiceFactory) return;
if(!this.ws) return;
this.ws.send(JSON.stringify(this.voiceFactory.joinVoice(channel.id,channel.guild.id)));
return undefined;
}
changeVCStatus(status:string){
const statuselm=document.getElementById("VoiceStatus");
if(!statuselm) throw new Error("Missing status element");
statuselm.textContent=status;
}
handleVoice(){
if(this.voiceFactory){
this.voiceFactory.onJoin=voice=>{
voice.onSatusChange=status=>{
this.changeVCStatus(status);
}
}
}
}
heartbeat_interval: number = 0;
updateChannel(json: channeljson): void{
const guild = this.guildids.get(json.guild_id);
@ -603,11 +662,12 @@ class Localuser{
const memberdiv=document.createElement("div");
const pfp=await member.user.buildstatuspfp();
const username=document.createElement("span");
username.classList.add("ellipsis");
username.textContent=member.name;
member.bind(username)
member.user.bind(memberdiv,member.guild,false);
memberdiv.append(pfp,username);
memberdiv.classList.add("flexltr");
memberdiv.classList.add("flexltr","liststyle");
membershtml.append(memberdiv);
}
category.append(membershtml);
@ -722,7 +782,7 @@ class Localuser{
const div = document.createElement("div");
div.classList.add("home", "servericon");
home.classList.add("svgtheme", "svgicon", "svg-home");
home.classList.add("svgicon", "svg-home");
home.all = this.guildids.get("@me");
(this.guildids.get("@me") as Guild).html = outdiv;
const unread = document.createElement("div");
@ -760,19 +820,17 @@ class Localuser{
br.id = "bottomseparator";
const div = document.createElement("div");
div.textContent = "+";
const plus = document.createElement("span");
plus.classList.add("svgicon", "svg-plus");
div.classList.add("home", "servericon");
div.appendChild(plus);
serverlist.appendChild(div);
div.onclick = _=>{
this.createGuild();
};
const guilddsdiv = document.createElement("div");
const guildDiscoveryContainer = document.createElement("span");
guildDiscoveryContainer.classList.add(
"svgtheme",
"svgicon",
"svg-explore"
);
guildDiscoveryContainer.classList.add("svgicon", "svg-explore");
guilddsdiv.classList.add("home", "servericon");
guilddsdiv.appendChild(guildDiscoveryContainer);
serverlist.appendChild(guilddsdiv);
@ -838,7 +896,7 @@ class Localuser{
["title", "Create a guild"],
[
"fileupload",
"Icon:",
"Icon: ",
function(event: Event){
const target = event.target as HTMLInputElement;
if(!target.files)return;
@ -861,7 +919,7 @@ class Localuser{
[
"button",
"",
"submit",
"Submit",
()=>{
this.makeGuild(fields).then(_=>{
if(_.message){
@ -889,7 +947,7 @@ class Localuser{
}
async guildDiscovery(){
const content = document.createElement("div");
content.classList.add("guildy");
content.classList.add("flexttb","guildy");
content.textContent = "Loading...";
const full = new Dialog(["html", content]);
full.show();
@ -1104,7 +1162,7 @@ class Localuser{
});
let changed = false;
const pronounbox = settingsLeft.addTextInput(
"Pronouns",
"Pronouns:",
_=>{
if(newpronouns || newbio || changed){
this.updateProfile({
@ -1136,7 +1194,7 @@ class Localuser{
color = "transparent";
}
const colorPicker = settingsLeft.addColorInput(
"Profile color",
"Profile color:",
_=>{},
{ initColor: color }
);
@ -1149,9 +1207,9 @@ class Localuser{
});
}
{
const tas = settings.addButton("Themes & sounds");
const tas = settings.addButton("Themes & Sounds");
{
const themes = ["Dark", "WHITE", "Light"];
const themes = ["Dark", "WHITE", "Light", "Dark-Accent"];
tas.addSelect(
"Theme:",
_=>{
@ -1167,18 +1225,18 @@ class Localuser{
);
}
{
const sounds = Voice.sounds;
const sounds = AVoice.sounds;
tas
.addSelect(
"Notification sound:",
_=>{
Voice.setNotificationSound(sounds[_]);
AVoice.setNotificationSound(sounds[_]);
},
sounds,
{ defaultIndex: sounds.indexOf(Voice.getNotificationSound()) }
{ defaultIndex: sounds.indexOf(AVoice.getNotificationSound()) }
)
.watchForChange(_=>{
Voice.noises(sounds[_]);
AVoice.noises(sounds[_]);
});
}
@ -1197,6 +1255,40 @@ class Localuser{
{ initColor: userinfos.accent_color }
);
}
{
const box=tas.addCheckboxInput("Enable experimental Voice support",()=>{},{initState:Boolean(localStorage.getItem("Voice enabled"))});
box.onchange=(e)=>{
if(e){
if(confirm("Are you sure you want to enable this, this is very experimental and is likely to cause issues. (this feature is for devs, please don't enable if you don't know what you're doing)")){
localStorage.setItem("Voice enabled","true")
}else{
box.value=true;
const checkbox=box.input.deref();
if(checkbox){
checkbox.checked=false;
}
}
}else{
localStorage.removeItem("Voice enabled");
}
}
}
}
{
const update=settings.addButton("Update settings")
const sw=update.addSelect("Service Worker setting",()=>{},["False","Offline only","True"],{
defaultIndex:["false","offlineOnly","true"].indexOf(localStorage.getItem("SWMode") as string)
});
sw.onchange=(e)=>{
SW.setMode(["false","offlineOnly","true"][e] as "false"|"offlineOnly"|"true")
}
update.addButtonInput("","Check for update",()=>{
SW.checkUpdate();
});
update.addButtonInput("","Clear cache",()=>{
SW.forceClear();
});
}
{
const security = settings.addButton("Account Settings");
@ -1427,9 +1519,9 @@ class Localuser{
}
);
form.addTextInput("Name", "name", { required: true });
form.addTextInput("Name:", "name", { required: true });
form.addSelect(
"Team",
"Team:",
"team_id",
["Personal", ...teams.map((team: { name: string })=>team.name)],
{
@ -1545,7 +1637,7 @@ class Localuser{
});
form.addTextInput("Bot username:","username",{initText:bot.username});
form.addFileInput("Bot avatar:","avatar");
form.addButtonInput("Reset Token:","Reset",async ()=>{
form.addButtonInput("","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;
}
@ -1585,7 +1677,7 @@ class Localuser{
this.userinfo.updateLocal();
}
});
form.addButtonInput("","Advanced bot settings",()=>{
form.addButtonInput("","Advanced Bot Settings",()=>{
const token=this.botTokens.get(appId);
if(token){
const botc=new Bot(bot,token,this);
@ -1758,16 +1850,8 @@ class Localuser{
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";
(document.getElementById("channelname") as HTMLSpanElement).textContent = channelName;
(document.getElementsByTagName("title")[0] as HTMLTitleElement).textContent = channelName + (guildName ? " | " + guildName : "") + " | " + this.instancePing.name +" | Jank Client";
}
async instanceStats(){
const res = await fetch(this.info.api + "/policies/stats", {

View file

@ -1,4 +1,5 @@
<body class="Dark-theme">
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
@ -12,51 +13,49 @@
<meta content="#4b458c" data-react-helmet="true" name="theme-color">
<link href="/style.css" rel="stylesheet">
<link href="/themes.css" rel="stylesheet" id="lightcss">
<style>body.no-theme{background:#16191b;}@media(prefers-color-scheme:light){body.no-theme{background:#9397bd;}}</style>
</head>
<div id="logindiv">
<h1>Login</h1>
<br >
<form id="form" submit="check(e)">
<label for="instance"><b>Instance:</b></label
><br >
<p id="verify"></p>
<input
type="search"
list="instances"
placeholder="Instance URL"
name="instance"
id="instancein"
value=""
id="instancein"
required
><br ><br >
<body class="no-theme">
<div id="logindiv">
<h1>Login</h1>
<form id="form" submit="check(e)">
<label for="instance"><b>Instance:</b></label>
<p id="verify"></p>
<input
type="search"
list="instances"
placeholder="Instance URL"
name="instance"
id="instancein"
value=""
required
>
<label for="uname"><b>Email:</b></label
><br >
<input
type="text"
placeholder="Enter email address"
name="uname"
id="uname"
required
><br ><br >
<label for="uname"><b>Email:</b></label>
<input
type="text"
placeholder="Enter email address"
name="uname"
id="uname"
required
>
<label for="psw"><b>Password:</b></label
><br >
<input
type="password"
placeholder="Enter Password"
name="psw"
id="psw"
required
><br ><br ><br ><br >
<p class="wrongred" id="wrong"></p>
<label for="psw"><b>Password:</b></label>
<input
type="password"
placeholder="Enter Password"
name="psw"
id="psw"
required
>
<p class="wrongred" id="wrong"></p>
<div id="h-captcha"></div>
<button type="submit">Login</button>
</form>
<a href="/register.html" id="switch">Don't have an account?</a>
</div>
<datalist id="instances"></datalist>
<script src="/login.js" type="module"></script>
</body>
<div id="h-captcha"></div>
<button type="submit">Login</button>
</form>
<a href="/register.html" id="switch">Don't have an account?</a>
</div>
<datalist id="instances"></datalist>
<script src="/login.js" type="module"></script>
</body>
</html>

View file

@ -47,7 +47,7 @@ function trimswitcher(){
if(wellknown.at(-1) !== "/"){
wellknown += "/";
}
wellknown += user.username;
wellknown =(user.id||user.email)+"@"+wellknown;
if(map.has(wellknown)){
const otheruser = map.get(wellknown);
if(otheruser[1].serverurls.wellknown.at(-1) === "/"){
@ -95,7 +95,7 @@ function setDefaults(){
userinfos.users = {};
}
if(userinfos.accent_color === undefined){
userinfos.accent_color = "#242443";
userinfos.accent_color = "#3096f7";
}
document.documentElement.style.setProperty(
"--accent-color",
@ -116,12 +116,12 @@ function setDefaults(){
setDefaults();
class Specialuser{
serverurls: {
api: string;
cdn: string;
gateway: string;
wellknown: string;
login: string;
};
api: string;
cdn: string;
gateway: string;
wellknown: string;
login: string;
};
email: string;
token: string;
loggedin;
@ -178,6 +178,13 @@ login: string;
get localuserStore(){
return this.json.localuserStore;
}
set id(e){
this.json.id = e;
this.updateLocal();
}
get id(){
return this.json.id;
}
get uid(){
return this.email + this.serverurls.wellknown;
}
@ -532,31 +539,55 @@ if(document.getElementById("form")){
}
}
//this currently does not work, and need to be implemented better at some time.
/*
if ("serviceWorker" in navigator){
if(!localStorage.getItem("SWMode")){
localStorage.setItem("SWMode","true");
}
class SW{
static worker:undefined|ServiceWorker;
static setMode(mode:"false"|"offlineOnly"|"true"){
localStorage.setItem("SWMode",mode);
if(this.worker){
this.worker.postMessage({data:mode,code:"setMode"});
}
}
static checkUpdate(){
if(this.worker){
this.worker.postMessage({code:"CheckUpdate"});
}
}
static forceClear(){
if(this.worker){
this.worker.postMessage({code:"ForceClear"});
}
}
}
export {SW};
if ("serviceWorker" in navigator){
navigator.serviceWorker.register("/service.js", {
scope: "/",
}).then((registration) => {
let serviceWorker:ServiceWorker;
if (registration.installing) {
serviceWorker = registration.installing;
console.log("installing");
} else if (registration.waiting) {
serviceWorker = registration.waiting;
console.log("waiting");
} else if (registration.active) {
serviceWorker = registration.active;
console.log("active");
}
if (serviceWorker) {
console.log(serviceWorker.state);
serviceWorker.addEventListener("statechange", (e) => {
console.log(serviceWorker.state);
});
}
let serviceWorker:ServiceWorker|undefined;
if (registration.installing) {
serviceWorker = registration.installing;
console.log("installing");
} else if (registration.waiting) {
serviceWorker = registration.waiting;
console.log("waiting");
} else if (registration.active) {
serviceWorker = registration.active;
console.log("active");
}
SW.worker=serviceWorker;
SW.setMode(localStorage.getItem("SWMode") as "false"|"offlineOnly"|"true");
if (serviceWorker) {
console.log(serviceWorker.state);
serviceWorker.addEventListener("statechange", (_) => {
console.log(serviceWorker.state);
});
}
})
}
*/
}
const switchurl = document.getElementById("switch") as HTMLAreaElement;
if(switchurl){
switchurl.href += window.location.search;

View file

@ -765,12 +765,12 @@ txt[j + 1] === undefined)
}else{
const full: Dialog = new Dialog([
"vdiv",
["title", "You're leaving spacebar"],
["title", "You're leaving Spacebar"],
[
"text",
"You're going to " +
Url.host +
" are you sure you want to go there?",
". Are you sure you want to go there?",
],
[
"hdiv",

View file

@ -49,6 +49,32 @@ class Member extends SnowFlake{
return this.guild.roles.indexOf(a) - this.guild.roles.indexOf(b);
});
}
update(memberjson: memberjson){
this.roles=[];
for(const key of Object.keys(memberjson)){
if(key === "guild" || key === "owner" || key === "user"){
continue;
}
if(key === "roles"){
for(const strrole of memberjson.roles){
const role = this.guild.roleids.get(strrole);
if(!role)continue;
this.roles.push(role);
}
continue;
}
if(key === "presence"){
this.getPresence(memberjson.presence);
continue;
}
(this as any)[key] = (memberjson as any)[key];
}
this.roles.sort((a, b)=>{
return this.guild.roles.indexOf(a) - this.guild.roles.indexOf(b);
});
}
get guild(){
return this.owner;
}
@ -241,6 +267,24 @@ class Member extends SnowFlake{
]);
menu.show();
}
addRole(role:Role){
const roles=this.roles.map(_=>_.id)
roles.push(role.id);
fetch(this.info.api+"/guilds/"+this.guild.id+"/members/"+this.id,{
method:"PATCH",
headers:this.guild.headers,
body:JSON.stringify({roles})
})
}
removeRole(role:Role){
let roles=this.roles.map(_=>_.id)
roles=roles.filter(_=>_!==role.id);
fetch(this.info.api+"/guilds/"+this.guild.id+"/members/"+this.id,{
method:"PATCH",
headers:this.guild.headers,
body:JSON.stringify({roles})
})
}
banAPI(reason: string){
const headers = structuredClone(this.guild.headers);
(headers as any)["x-audit-log-reason"] = reason;

View file

@ -11,6 +11,8 @@ import{ SnowFlake }from"./snowflake.js";
import{ memberjson, messagejson }from"./jsontypes.js";
import{ Emoji }from"./emoji.js";
import{ Dialog }from"./dialog.js";
import{ mobile }from"./login.js";
import { I18n } from "./i18n.js";
class Message extends SnowFlake{
static contextmenu = new Contextmenu<Message, undefined>("message menu");
@ -44,9 +46,7 @@ class Message extends SnowFlake{
return this.weakdiv?.deref();
}
//*/
div:
| (HTMLDivElement & { pfpparent?: Message | undefined; txt?: HTMLElement })
| undefined;
div:(HTMLDivElement & { pfpparent?: Message | undefined; txt?: HTMLElement })| undefined;
member: Member | undefined;
reactions!: messagejson["reactions"];
static setup(){
@ -56,13 +56,13 @@ class Message extends SnowFlake{
Message.setupcmenu();
}
static setupcmenu(){
Message.contextmenu.addbutton("Copy raw text", function(this: Message){
Message.contextmenu.addbutton(I18n.getTranslation.bind(I18n,"copyrawtext"), function(this: Message){
navigator.clipboard.writeText(this.content.rawString);
});
Message.contextmenu.addbutton("Reply", function(this: Message){
Message.contextmenu.addbutton(I18n.getTranslation.bind(I18n,"reply"), function(this: Message){
this.channel.setReplying(this);
});
Message.contextmenu.addbutton("Copy message id", function(this: Message){
Message.contextmenu.addbutton(I18n.getTranslation.bind(I18n,"copymessageid"), function(this: Message){
navigator.clipboard.writeText(this.id);
});
Message.contextmenu.addsubmenu(
@ -404,22 +404,19 @@ class Message extends SnowFlake{
}
if(this.message_reference){
const replyline = document.createElement("div");
const line = document.createElement("hr");
const minipfp = document.createElement("img");
minipfp.classList.add("replypfp");
replyline.appendChild(line);
replyline.appendChild(minipfp);
const username = document.createElement("span");
replyline.appendChild(username);
const reply = document.createElement("div");
username.classList.add("username");
reply.classList.add("replytext");
reply.classList.add("replytext","ellipsis");
replyline.appendChild(reply);
const line2 = document.createElement("hr");
replyline.appendChild(line2);
line2.classList.add("reply");
line.classList.add("startreply");
replyline.classList.add("replyflex");
replyline.classList.add("flexltr","replyflex");
// TODO: Fix this
this.channel.getmessage(this.message_reference.message_id).then(message=>{
if(message.author.relationshipType === 2){
@ -432,6 +429,11 @@ class Message extends SnowFlake{
author.bind(minipfp, this.guild);
username.textContent = author.username;
author.bind(username, this.guild);
Member.resolveMember(author, this.guild).then(_=>{
if(_){
username.textContent=_.name;
}
})
});
reply.onclick = _=>{
// TODO: FIX this
@ -442,7 +444,6 @@ class Message extends SnowFlake{
div.appendChild(build);
if({ 0: true, 19: true }[this.type] || this.attachments.length !== 0){
const pfpRow = document.createElement("div");
pfpRow.classList.add("flexltr");
let pfpparent, current;
if(premessage != null){
pfpparent ??= premessage;
@ -466,18 +467,19 @@ class Message extends SnowFlake{
pfpRow.classList.add("pfprow");
build.appendChild(pfpRow);
const text = document.createElement("div");
text.classList.add("flexttb");
const texttxt = document.createElement("div");
texttxt.classList.add("commentrow", "flexttb");
text.appendChild(texttxt);
text.classList.add("commentrow", "flexttb");
if(combine){
const username = document.createElement("span");
username.classList.add("username");
this.author.bind(username, this.guild);
Member.resolveMember(this.author, this.guild).then(_=>{
if(_){
username.textContent=_.name;
}
})
div.classList.add("topMessage");
username.textContent = this.author.username;
const userwrap = document.createElement("div");
userwrap.classList.add("flexltr");
userwrap.appendChild(username);
if(this.author.bot){
const username = document.createElement("span");
@ -490,7 +492,7 @@ class Message extends SnowFlake{
time.classList.add("timestamp");
userwrap.appendChild(time);
texttxt.appendChild(userwrap);
text.appendChild(userwrap);
}else{
div.classList.remove("topMessage");
}
@ -499,13 +501,13 @@ class Message extends SnowFlake{
const messagedwrap = document.createElement("div");
messagedwrap.classList.add("flexttb");
messagedwrap.appendChild(messaged);
texttxt.appendChild(messagedwrap);
text.appendChild(messagedwrap);
build.appendChild(text);
if(this.attachments.length){
console.log(this.attachments);
const attach = document.createElement("div");
attach.classList.add("flexltr");
attach.classList.add("flexltr","attachments");
for(const thing of this.attachments){
attach.appendChild(thing.getHTML());
}
@ -513,7 +515,6 @@ class Message extends SnowFlake{
}
if(this.embeds.length){
const embeds = document.createElement("div");
embeds.classList.add("flexltr");
for(const thing of this.embeds){
embeds.appendChild(thing.generateHTML());
}
@ -522,27 +523,23 @@ class Message extends SnowFlake{
//
}else if(this.type === 7){
const text = document.createElement("div");
text.classList.add("flexttb");
const texttxt = document.createElement("div");
text.appendChild(texttxt);
build.appendChild(text);
texttxt.classList.add("flexltr");
const messaged = document.createElement("span");
div.txt = messaged;
messaged.textContent = "welcome: ";
texttxt.appendChild(messaged);
text.appendChild(messaged);
const username = document.createElement("span");
username.textContent = this.author.username;
//this.author.profileclick(username);
this.author.bind(username, this.guild);
texttxt.appendChild(username);
text.appendChild(username);
username.classList.add("username");
const time = document.createElement("span");
time.textContent = " " + formatTime(new Date(this.timestamp));
time.classList.add("timestamp");
texttxt.append(time);
text.append(time);
div.classList.add("topMessage");
}
const reactions = document.createElement("div");
@ -557,6 +554,7 @@ class Message extends SnowFlake{
if(this.div){
let buttons: HTMLDivElement | undefined;
this.div.onmouseenter = _=>{
if(mobile)return;
if(buttons){
buttons.remove();
buttons = undefined;
@ -565,9 +563,9 @@ class Message extends SnowFlake{
buttons = document.createElement("div");
buttons.classList.add("messageButtons", "flexltr");
if(this.channel.hasPermission("SEND_MESSAGES")){
const container = document.createElement("div");
const container = document.createElement("button");
const reply = document.createElement("span");
reply.classList.add("svgtheme", "svg-reply", "svgicon");
reply.classList.add("svg-reply", "svgicon");
container.append(reply);
buttons.append(container);
container.onclick = _=>{
@ -575,9 +573,9 @@ class Message extends SnowFlake{
};
}
if(this.author === this.localuser.user){
const container = document.createElement("div");
const container = document.createElement("button");
const edit = document.createElement("span");
edit.classList.add("svgtheme", "svg-edit", "svgicon");
edit.classList.add("svg-edit", "svgicon");
container.append(edit);
buttons.append(container);
container.onclick = _=>{
@ -585,9 +583,9 @@ class Message extends SnowFlake{
};
}
if(this.canDelete()){
const container = document.createElement("div");
const container = document.createElement("button");
const reply = document.createElement("span");
reply.classList.add("svgtheme", "svg-delete", "svgicon");
reply.classList.add("svg-delete", "svgicon");
container.append(reply);
buttons.append(container);
container.onclick = _=>{
@ -596,25 +594,28 @@ class Message extends SnowFlake{
return;
}
const diaolog = new Dialog([
"hdiv",
["title", "are you sure you want to delete this?"],
"vdiv",
["title", "Are you sure you want to delete this?"],
[
"button",
"",
"yes",
()=>{
this.delete();
diaolog.hide();
},
],
[
"button",
"",
"no",
()=>{
diaolog.hide();
},
],
"hdiv",
[
"button",
"",
"Yes",
()=>{
this.delete();
diaolog.hide();
},
],
[
"button",
"",
"No",
()=>{
diaolog.hide();
},
],
]
]);
diaolog.show();
};

View file

@ -102,18 +102,24 @@ type botjsonfetch={
}
}
const dialog=document.createElement("dialog");
dialog.classList.add("accountSwitcher");
dialog.classList.add("flexttb","accountSwitcher");
const h1=document.createElement("h1");
dialog.append(h1);
h1.textContent="Invite to server:";
const select=document.createElement("select");
const selectSpan=document.createElement("span");
selectSpan.classList.add("selectspan");
const selectArrow = document.createElement("span");
selectArrow.classList.add("svgicon","svg-category","selectarrow");
for(const guild of guilds){
const option=document.createElement("option");
option.textContent=guild.name;
option.value=guild.id;
select.append(option);
}
dialog.append(select);
selectSpan.append(select);
selectSpan.append(selectArrow);
dialog.append(selectSpan);
const button=document.createElement("button");
button.textContent="Invite";
dialog.append(button);
@ -193,7 +199,7 @@ type botjsonfetch={
}
table.append(td);
table.classList.add("accountSwitcher");
table.classList.add("flexttb","accountSwitcher");
console.log(table);
document.body.append(table);
}
@ -221,7 +227,7 @@ type botjsonfetch={
const int = Number((BigInt(json.bot.id) >> 22n) % 6n);
pfp.src=`${urls.cdn}/embed/avatars/${int}.png`;
}
const perms=document.getElementById("permsions") as HTMLDivElement;
const perms=document.getElementById("permissions") as HTMLDivElement;
if(perms&&permstr){
const permisions=new Permissions(permstr)

View file

@ -1,4 +1,5 @@
<body class="Dark-theme">
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
@ -9,15 +10,18 @@
<meta content="#4b458c" data-react-helmet="true" name="theme-color">
<link href="/style.css" rel="stylesheet">
<link href="/themes.css" rel="stylesheet" id="lightcss">
<style>body.no-theme{background:#16191b;}@media(prefers-color-scheme:light){body.no-theme{background:#9397bd;}}</style>
</head>
<div>
<div id="invitebody">
<img id="inviteimg" class="pfp"/>
<h1 id="invitename">Bot Name</h1>
<p id="invitedescription">Add Bot</p>
<div id="permsions"><h1>This will allow the bot to:</h1></div>
<button id="AcceptInvite">Add to server</button>
<body class="no-theme">
<div>
<div id="invitebody">
<img id="inviteimg" class="pfp"/>
<h1 id="invitename">Bot Name</h1>
<p id="invitedescription">Add Bot</p>
<div id="permissions"><h1>This will allow the bot to:</h1></div>
<button id="AcceptInvite">Add to server</button>
</div>
</div>
</div>
<script type="module" src="/oauth2/auth.js"></script>
</body>
<script type="module" src="/oauth2/auth.js"></script>
</body>
</html>

View file

@ -1,5 +1,5 @@
<body class="Dark-theme">
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
@ -10,53 +10,55 @@
<meta content="#4b458c" data-react-helmet="true" name="theme-color">
<link href="/style.css" rel="stylesheet">
<link href="/themes.css" rel="stylesheet" id="lightcss">
<style>body.no-theme{background:#16191b;}@media(prefers-color-scheme:light){body.no-theme{background:#9397bd;}}</style>
</head>
<div id="logindiv">
<h1>Create an account</h1><br>
<form id="register" submit="registertry(e)">
<div>
<label for="instance"><b>Instance:</b></label><br>
<p id="verify"></p>
<input type="search" list="instances" placeholder="Instance URL" id="instancein" name="instance" value=""
id="instancein" required>
</div>
<div>
<label for="uname"><b>Email:</b></label><br>
<input type="text" placeholder="Enter Email" name="uname" id="uname" required>
</div>
<body class="no-theme">
<div id="logindiv">
<h1>Create an account</h1>
<form id="register" submit="registertry(e)">
<div>
<label for="instance"><b>Instance:</b></label>
<p id="verify"></p>
<input type="search" list="instances" placeholder="Instance URL" id="instancein" name="instance" value="" required>
</div>
<div>
<label for="uname"><b>Email:</b></label>
<input type="text" placeholder="Enter Email" name="uname" id="uname" required>
</div>
<div>
<label for="uname"><b>Username:</b></label><br>
<input type="text" placeholder="Enter Username" name="username" id="username" required>
</div>
<div>
<label for="psw"><b>Password:</b></label><br>
<input type="password" placeholder="Enter Password" name="psw" id="psw" required>
</div>
<div>
<label for="uname"><b>Username:</b></label>
<input type="text" placeholder="Enter Username" name="username" id="username" required>
</div>
<div>
<label for="psw"><b>Password:</b></label>
<input type="password" placeholder="Enter Password" name="psw" id="psw" required>
</div>
<div>
<label for="psw2"><b>Enter password again:</b></label><br>
<input type="password" placeholder="Enter Password Again" name="psw2" id="psw2" required>
</div>
<div>
<label for="psw2"><b>Enter password again:</b></label>
<input type="password" placeholder="Enter Password Again" name="psw2" id="psw2" required>
</div>
<div>
<label for="date"><b>Date of birth:</b></label><br>
<input type="date" id="date" name="date">
</div>
<div>
<label for="date"><b>Date of birth:</b></label>
<input type="date" id="date" name="date">
</div>
<div>
<b id="TOSbox">I agree to the <a href="" id="TOSa">Terms of Service</a>:</b>
<input type="checkbox" id="TOS" name="TOS">
</div>
<div>
<b id="TOSbox">I agree to the <a href="" id="TOSa">Terms of Service</a>:</b>
<input type="checkbox" id="TOS" name="TOS">
</div>
<p class="wrongred" id="wrong"></p>
<div id="h-captcha">
<p class="wrongred" id="wrong"></p>
<div id="h-captcha">
</div>
<button type="submit" class="dontgrow">Create account</button>
</form>
<a href="/login.html" id="switch">Already have an account?</a>
</div>
<datalist id="instances"></datalist>
<script src="/register.js" type="module"></script>
</body>
</div>
<button type="submit" class="dontgrow">Create account</button>
</form>
<a href="/login.html" id="switch">Already have an account?</a>
</div>
<datalist id="instances"></datalist>
<script src="/register.js" type="module"></script>
</body>
</html>

View file

@ -3,6 +3,7 @@ import{ Localuser }from"./localuser.js";
import{ Guild }from"./guild.js";
import{ SnowFlake }from"./snowflake.js";
import{ rolesjson }from"./jsontypes.js";
import{ Search }from"./search.js";
class Role extends SnowFlake{
permissions: Permissions;
owner: Guild;
@ -13,6 +14,7 @@ class Role extends SnowFlake{
icon!: string;
mentionable!: boolean;
unicode_emoji!: string;
position!:number;
headers: Guild["headers"];
constructor(json: rolesjson, owner: Guild){
super(json.id);
@ -27,6 +29,15 @@ class Role extends SnowFlake{
this.permissions = new Permissions(json.permissions);
this.owner = owner;
}
newJson(json: rolesjson){
for(const thing of Object.keys(json)){
if(thing === "id"||thing==="permissions"){
continue;
}
(this as any)[thing] = (json as any)[thing];
}
this.permissions.allow=BigInt(json.permissions);
}
get guild(): Guild{
return this.owner;
}
@ -39,6 +50,14 @@ class Role extends SnowFlake{
}
return`#${this.color.toString(16)}`;
}
canManage(){
if(this.guild.member.hasPermission("MANAGE_ROLES")){
let max=-Infinity;
this.guild.member.roles.forEach(r=>max=Math.max(max,r.position))
return this.position<=max||this.guild.properties.owner_id===this.guild.member.id;
}
return false;
}
}
export{ Role };
import{ Options }from"./settings.js";
@ -121,22 +140,25 @@ class PermissionToggle implements OptionsElement<number>{
submit(){}
}
import{ OptionsElement, Buttons }from"./settings.js";
import { Contextmenu } from "./contextmenu.js";
import { Channel } from "./channel.js";
class RoleList extends Buttons{
readonly permissions: [Role, Permissions][];
permissions: [Role, Permissions][];
permission: Permissions;
readonly guild: Guild;
readonly channel: boolean;
declare readonly buttons: [string, string][];
readonly channel: false|Channel;
declare buttons: [string, string][];
readonly options: Options;
onchange: Function;
curid!: string;
constructor(
permissions: [Role, Permissions][],
guild: Guild,
onchange: Function,
channel = false
){
super("Roles");
curid?: string;
get info(){
return this.guild.info;
}
get headers(){
return this.guild.headers;
}
constructor(permissions:[Role, Permissions][], guild:Guild, onchange:Function, channel:false|Channel){
super("");
this.guild = guild;
this.permissions = permissions;
this.channel = channel;
@ -147,16 +169,238 @@ class RoleList extends Buttons{
}else{
this.permission = new Permissions("0");
}
this.makeguildmenus(options);
for(const thing of Permissions.info){
options.options.push(
new PermissionToggle(thing, this.permission, options)
);
}
for(const i of permissions){
console.log(i);
this.buttons.push([i[0].name, i[0].id]);
}
this.options = options;
guild.roleUpdate=this.groleUpdate.bind(this);
if(channel){
channel.croleUpdate=this.croleUpdate.bind(this);
}
}
private groleUpdate(role:Role,added:1|0|-1){
if(!this.channel){
if(added===1){
this.permissions.push([role,role.permissions]);
}
}
if(added===-1){
this.permissions=this.permissions.filter(r=>r[0]!==role);
}
this.redoButtons();
}
private croleUpdate(role:Role,perm:Permissions,added:boolean){
if(added){
this.permissions.push([role,perm])
}else{
this.permissions=this.permissions.filter(r=>r[0]!==role);
}
this.redoButtons();
}
makeguildmenus(option:Options){
option.addButtonInput("","Display settings",()=>{
const role=this.guild.roleids.get(this.curid as string);
if(!role) return;
const form=option.addSubForm("Display settings",()=>{},{
fetchURL:this.info.api+"/guilds/"+this.guild.id+"/roles/"+this.curid,
method:"PATCH",
headers:this.headers,
traditionalSubmit:true
});
form.addTextInput("Role Name:","name",{
initText:role.name
});
form.addCheckboxInput("Hoisted:","hoist",{
initState:role.hoist
});
form.addCheckboxInput("Allow anyone to ping this role:","mentionable",{
initState:role.mentionable
});
const color="#"+role.color.toString(16).padStart(6,"0");
form.addColorInput("Color","color",{
initColor:color
});
form.addPreprocessor((obj:any)=>{
obj.color=Number("0x"+obj.color.substring(1));
console.log(obj.color);
})
})
}
static channelrolemenu=this.ChannelRoleMenu();
static guildrolemenu=this.GuildRoleMenu();
private static ChannelRoleMenu(){
const menu=new Contextmenu<RoleList,Role>("role settings");
menu.addbutton("Remove role",function(role){
if(!this.channel) return;
console.log(role);
fetch(this.info.api+"/channels/"+this.channel.id+"/permissions/"+role.id,{
method:"DELETE",
headers:this.headers
})
},null);
return menu;
}
private static GuildRoleMenu(){
const menu=new Contextmenu<RoleList,Role>("role settings");
menu.addbutton("Delete Role",function(role){
if(!confirm("Are you sure you want to delete "+role.name+"?")) return;
console.log(role);
fetch(this.info.api+"/guilds/"+this.guild.id+"/roles/"+role.id,{
method:"DELETE",
headers:this.headers
})
},null);
return menu;
}
redoButtons(){
this.buttons=[];
this.permissions.sort(([a],[b])=>b.position-a.position);
for(const i of this.permissions){
this.buttons.push([i[0].name, i[0].id]);
}
console.log("in here :P")
if(!this.buttonList)return;
console.log("in here :P");
const elms=Array.from(this.buttonList.children);
const div=elms[0] as HTMLDivElement;
const div2=elms[1] as HTMLDivElement;
console.log(div);
div.innerHTML="";
div.append(this.buttonListGen(div2));//not actually sure why the html is needed
}
buttonMap=new WeakMap<HTMLButtonElement,Role>();
dragged?:HTMLButtonElement;
buttonDragEvents(button:HTMLButtonElement,role:Role){
this.buttonMap.set(button,role);
button.addEventListener("dragstart", e=>{
this.dragged = button;
e.stopImmediatePropagation();
});
button.addEventListener("dragend", ()=>{
this.dragged = undefined;
});
button.addEventListener("dragenter", event=>{
console.log("enter");
event.preventDefault();
return true;
});
button.addEventListener("dragover", event=>{
event.preventDefault();
return true;
});
button.addEventListener("drop", _=>{
const role2=this.buttonMap.get(this.dragged as HTMLButtonElement);
if(!role2) return;
const index2=this.guild.roles.indexOf(role2);
this.guild.roles.splice(index2,1);
const index=this.guild.roles.indexOf(role);
this.guild.roles.splice(index+1,0,role2);
this.guild.recalcRoles();
console.log(role);
});
}
buttonListGen(html:HTMLElement){
const buttonTable=document.createElement("div");
buttonTable.classList.add("flexttb");
const roleRow=document.createElement("div");
roleRow.classList.add("flexltr");
roleRow.append("Roles");
const add=document.createElement("span");
add.classList.add("svg-plus","svgicon","addrole");
add.onclick=async (e)=>{
const box=add.getBoundingClientRect();
e.stopPropagation();
if(this.channel){
const roles:[Role,string[]][]=[];
for(const role of this.guild.roles){
if(this.permissions.find(r=>r[0]==role)){
continue;
}
roles.push([role,[role.name]]);
}
const search=new Search(roles);
const found=await search.find(box.left,box.top);
if(!found) return;
console.log(found);
this.onchange(found.id,new Permissions("0","0"));
}else{
const bar=document.createElement("input");
bar.classList.add("fixedsearch");
bar.style.left=(box.left^0)+"px";
bar.style.top=(box.top^0)+"px";
document.body.append(bar);
if(Contextmenu.currentmenu != ""){
Contextmenu.currentmenu.remove();
}
Contextmenu.currentmenu=bar;
Contextmenu.keepOnScreen(bar);
bar.onchange=()=>{
bar.remove();
console.log(bar.value)
if(bar.value==="") return;
fetch(this.info.api+`/guilds/${this.guild.id}/roles`,{
method:"POST",
headers:this.headers,
body:JSON.stringify({
color:0,
name:bar.value,
permissions:""
})
})
}
}
}
roleRow.append(add);
buttonTable.append(roleRow);
for(const thing of this.buttons){
const button = document.createElement("button");
button.classList.add("SettingsButton");
button.textContent = thing[0];
const role=this.guild.roleids.get(thing[1]);
if(role){
if(!this.channel){
if(role.canManage()){
this.buttonDragEvents(button,role);
button.draggable=true;
RoleList.guildrolemenu.bindContextmenu(button,this,role)
}
}else{
if(role.canManage()){
RoleList.channelrolemenu.bindContextmenu(button,this,role)
}
}
}
button.onclick = _=>{
this.generateHTMLArea(thing[1], html);
if(this.warndiv){
this.warndiv.remove();
}
};
buttonTable.append(button);
}
return buttonTable;
}
generateButtons(html:HTMLElement):HTMLDivElement{
const div = document.createElement("div");
div.classList.add("settingbuttons");
div.append(this.buttonListGen(html));
return div;
}
handleString(str: string): HTMLElement{
this.curid = str;

72
src/webpage/search.ts Normal file
View file

@ -0,0 +1,72 @@
import { Contextmenu } from "./contextmenu.js";
class Search<E>{
options:Map<string,E>;
readonly keys:string[];
constructor(options:[E,string[]][]){
const map=options.flatMap(e=>{
const val=e[1].map(f=>[f,e[0]]);
return val as [string,E][];
})
this.options=new Map(map);
this.keys=[...this.options.keys()];
}
generateList(str:string,max:number,res:(e:E)=>void){
str=str.toLowerCase();
const options=this.keys.filter(e=>{
return e.toLowerCase().includes(str)
});
const div=document.createElement("div");
div.classList.add("OptionList","flexttb");
for(const option of options.slice(0, max)){
const hoption=document.createElement("span");
hoption.textContent=option;
hoption.onclick=()=>{
if(!this.options.has(option)) return;
res(this.options.get(option) as E)
}
div.append(hoption);
}
return div;
}
async find(x:number,y:number,max=4):Promise<E|undefined>{
return new Promise<E|undefined>((res)=>{
const container=document.createElement("div");
container.classList.add("fixedsearch");
console.log((x^0)+"",(y^0)+"");
container.style.left=(x^0)+"px";
container.style.top=(y^0)+"px";
const remove=container.remove;
container.remove=()=>{
remove.call(container);
res(undefined);
}
function resolve(e:E){
res(e);
container.remove();
}
const bar=document.createElement("input");
const options=document.createElement("div");
const keydown=()=>{
const html=this.generateList(bar.value,max,resolve);
options.innerHTML="";
options.append(html);
}
bar.oninput=keydown;
keydown();
bar.type="text";
container.append(bar);
container.append(options);
document.body.append(container);
if(Contextmenu.currentmenu != ""){
Contextmenu.currentmenu.remove();
}
Contextmenu.currentmenu=container;
Contextmenu.keepOnScreen(container);
})
}
}
export {Search};

View file

@ -13,13 +13,13 @@ async function putInCache(request: URL | RequestInfo, response: Response){
console.error(error);
}
}
console.log("test");
let lastcache: string;
self.addEventListener("activate", async ()=>{
console.log("test2");
console.log("Service Worker activated");
checkCache();
});
async function checkCache(){
if(checkedrecently){
return;
@ -34,7 +34,7 @@ async function checkCache(){
console.log(text, lastcache);
if(lastcache !== text){
deleteoldcache();
putInCache("/getupdates", data.clone());
putInCache("/getupdates", data);
}
checkedrecently = true;
setTimeout((_: any)=>{
@ -43,54 +43,99 @@ async function checkCache(){
});
}
var checkedrecently = false;
function samedomain(url: string | URL){
return new URL(url).origin === self.origin;
}
function isindexhtml(url: string | URL){
console.log(url);
if(new URL(url).pathname.startsWith("/channels")){
return true;
const htmlFiles=new Set(["/index","/login","/home","/register","/oauth2/auth"]);
function isHtml(url:string):string|void{
const path=new URL(url).pathname;
if(htmlFiles.has(path)||htmlFiles.has(path+".html")){
return path+path.endsWith(".html")?"":".html";
}
return false;
}
async function getfile(event: {
request: { url: URL | RequestInfo; clone: () => string | URL | Request };
}){
checkCache();
if(!samedomain(event.request.url.toString())){
return await fetch(event.request.clone());
let enabled="false";
let offline=false;
function toPath(url:string):string{
const Url= new URL(url);
let html=isHtml(url);
if(!html){
const path=Url.pathname;
if(path.startsWith("/channels")){
html="./index.html"
}else if(path.startsWith("/invite")){
html="./invite.html"
}
}
const responseFromCache = await caches.match(event.request.url);
console.log(responseFromCache, caches);
return html||Url.pathname;
}
let fails=0;
async function getfile(event: FetchEvent):Promise<Response>{
checkCache();
if(!samedomain(event.request.url)||enabled==="false"||(enabled==="offlineOnly"&&!offline)){
const responce=await fetch(event.request.clone());
if(samedomain(event.request.url)){
if(enabled==="offlineOnly"&&responce.ok){
putInCache(toPath(event.request.url),responce.clone());
}
if(!responce.ok){
fails++;
if(fails>5){
offline=true;
}
}
}
return responce;
}
let path=toPath(event.request.url);
if(path === "/instances.json"){
return await fetch(path);
}
console.log("Getting path: "+path);
const responseFromCache = await caches.match(path);
if(responseFromCache){
console.log("cache hit");
return responseFromCache;
}
if(isindexhtml(event.request.url.toString())){
console.log("is index.html");
const responseFromCache = await caches.match("/index.html");
if(responseFromCache){
console.log("cache hit");
return responseFromCache;
}
const responseFromNetwork = await fetch("/index.html");
await putInCache("/index.html", responseFromNetwork.clone());
return responseFromNetwork;
}
const responseFromNetwork = await fetch(event.request.clone());
console.log(event.request.clone());
await putInCache(event.request.clone(), responseFromNetwork.clone());
try{
const responseFromNetwork = await fetch(path);
if(responseFromNetwork.ok){
await putInCache(path, responseFromNetwork.clone());
}
return responseFromNetwork;
}catch(e){
console.error(e);
return e;
return new Response(null);
}
}
self.addEventListener("fetch", (event: any)=>{
self.addEventListener("fetch", (e)=>{
const event=e as FetchEvent;
try{
event.respondWith(getfile(event));
}catch(e){
console.error(e);
}
});
self.addEventListener("message", (message)=>{
const data=message.data;
switch(data.code){
case "setMode":
enabled=data.data;
break;
case "CheckUpdate":
checkedrecently=false;
checkCache();
break;
case "ForceClear":
deleteoldcache();
break;
}
})

View file

@ -4,7 +4,7 @@ interface OptionsElement<x> {
submit: () => void;
readonly watchForChange: (func: (arg1: x) => void) => void;
value: x;
}
}
//future me stuff
class Buttons implements OptionsElement<unknown>{
readonly name: string;
@ -30,31 +30,37 @@ class Buttons implements OptionsElement<unknown>{
this.buttonList = buttonList;
const htmlarea = document.createElement("div");
htmlarea.classList.add("flexgrow");
const buttonTable = this.generateButtons(htmlarea);
if(this.buttons[0]){
this.generateHTMLArea(this.buttons[0][1], htmlarea);
}
buttonList.append(buttonTable);
buttonList.append(htmlarea);
return buttonList;
}
generateButtons(optionsArea:HTMLElement){
const buttonTable = document.createElement("div");
buttonTable.classList.add("flexttb", "settingbuttons");
buttonTable.classList.add("settingbuttons");
for(const thing of this.buttons){
const button = document.createElement("button");
button.classList.add("SettingsButton");
button.textContent = thing[0];
button.onclick = _=>{
this.generateHTMLArea(thing[1], htmlarea);
this.generateHTMLArea(thing[1], optionsArea);
if(this.warndiv){
this.warndiv.remove();
}
};
buttonTable.append(button);
}
this.generateHTMLArea(this.buttons[0][1], htmlarea);
buttonList.append(buttonTable);
buttonList.append(htmlarea);
return buttonList;
return buttonTable;
}
handleString(str: string): HTMLElement{
const div = document.createElement("span");
div.textContent = str;
return div;
}
private generateHTMLArea(
generateHTMLArea(
buttonInfo: Options | string,
htmlarea: HTMLElement
){
@ -202,8 +208,8 @@ class CheckboxInput implements OptionsElement<boolean>{
const input = this.input.deref();
if(input){
const value = input.checked as boolean;
this.onchange(value);
this.value = value;
this.onchange(value);
}
}
setState(state:boolean){
@ -244,9 +250,12 @@ class ButtonInput implements OptionsElement<void>{
}
generateHTML(): HTMLDivElement{
const div = document.createElement("div");
const span = document.createElement("span");
span.textContent = this.label;
div.append(span);
if(this.label){
const span = document.createElement("span");
span.classList.add("inlinelabel");
span.textContent = this.label;
div.append(span);
}
const button = document.createElement("button");
button.textContent = this.textContent;
button.onclick = this.onClickEvent.bind(this);
@ -338,6 +347,8 @@ class SelectInput implements OptionsElement<number>{
const span = document.createElement("span");
span.textContent = this.label;
div.append(span);
const selectSpan = document.createElement("span");
selectSpan.classList.add("selectspan");
const select = document.createElement("select");
select.onchange = this.onChange.bind(this);
@ -348,7 +359,11 @@ class SelectInput implements OptionsElement<number>{
}
this.select = new WeakRef(select);
select.selectedIndex = this.index;
div.append(select);
selectSpan.append(select);
const selectArrow = document.createElement("span");
selectArrow.classList.add("svgicon","svg-category","selectarrow");
selectSpan.append(selectArrow);
div.append(selectSpan);
return div;
}
private onChange(){
@ -438,11 +453,13 @@ class FileInput implements OptionsElement<FileList | null>{
const span = document.createElement("span");
span.textContent = this.label;
div.append(span);
const innerDiv = document.createElement("div");
innerDiv.classList.add("flexltr","fileinputdiv");
const input = document.createElement("input");
input.type = "file";
input.oninput = this.onChange.bind(this);
this.input = new WeakRef(input);
div.append(input);
innerDiv.append(input);
if(this.clear){
const button = document.createElement("button");
button.textContent = "Clear";
@ -453,8 +470,9 @@ class FileInput implements OptionsElement<FileList | null>{
this.value = null;
this.owner.changed();
};
div.append(button);
innerDiv.append(button);
}
div.append(innerDiv);
return div;
}
onChange(){
@ -719,7 +737,7 @@ class Options implements OptionsElement<void>{
title: WeakRef<HTMLElement> = new WeakRef(document.createElement("h2"));
generateHTML(): HTMLElement{
const div = document.createElement("div");
div.classList.add("titlediv");
div.classList.add("flexttb","titlediv");
const title = document.createElement("h2");
title.textContent = this.name;
div.append(title);
@ -1054,6 +1072,10 @@ class Form implements OptionsElement<object>{
this.owner.changed();
}
}
preprocessor:(obj:Object)=>void=()=>{};
addPreprocessor(func:(obj:Object)=>void){
this.preprocessor=func;
}
async submit(){
if(this.options.subOptions){
this.options.subOptions.submit();
@ -1118,6 +1140,7 @@ class Form implements OptionsElement<object>{
}
console.log("middle2");
await Promise.allSettled(promises);
this.preprocessor(build);
if(this.fetchURL !== ""){
fetch(this.fetchURL, {
method: this.method,
@ -1199,7 +1222,7 @@ class Settings extends Buttons{
}
show(){
const background = document.createElement("div");
background.classList.add("background");
background.classList.add("flexttb","menu","background");
const title = document.createElement("h2");
title.textContent = this.name;
@ -1209,8 +1232,7 @@ class Settings extends Buttons{
background.append(this.generateHTML());
const exit = document.createElement("span");
exit.textContent = "✖";
exit.classList.add("exitsettings");
exit.classList.add("exitsettings","svgicon","svg-x");
background.append(exit);
exit.onclick = _=>{
this.hide();

File diff suppressed because it is too large Load diff

View file

@ -1,178 +1,176 @@
:root {
--servertd-height: 0px;
/* Default value */
--red: red;
--green: green;
--yellow:yellow;
--accent-color:#242443;
--font: "acumin-pro", "Helvetica Neue", Helvetica, Arial, sans-serif;
--black: #000000;
--red: #ff5555;
--yellow: #ffc159;
--green: #1c907b;
}
.Dark-theme { /* thanks to TomatoCake for the updated CSS vars and such*/
/* Themes. See themes.txt */
.Dark-theme {
color-scheme: dark;
--primary-text: #FFF;
--primary-bg: color-mix(in srgb, #2f2f2f 70%, var(--accent-color));
--black: #000;
--shadow: #000;
--focus: #8888ff;
--message-bg-hover: color-mix(in srgb, #191919 85%, var(--accent-color));
--typing-bg: #161616;
--timestamp-color: #a2a2a2;
--code-bg: color-mix(in srgb, #121212 95%, var(--accent-color));
--user-info-bg: color-mix(in srgb,#383838 85%, var(--accent-color));
--user-dock-bg: color-mix(in srgb,#111111 90%, var(--accent-color));
--channels-bg: color-mix(in srgb, #2a2a2a 90%, var(--accent-color));
--channel-hover: color-mix(in srgb, #121212 70%, var(--accent-color));
--blank-bg: #323232;
--light-border: #92929B;
--settings-hover: color-mix(in srgb, #000000 95%, var(--accent-color) 5%);
--quote-bg: #7a798e;
--button-bg: color-mix(in srgb, #191919 85%, var(--accent-color));
--button-hover: color-mix(in srgb, #2f2f2f 70%, var(--accent-color));
--textarea-bg: color-mix(in srgb, #484848 80%, var(--accent-color));
--filename: #47bbff;
--mention-bg: #F00;
--mention-md-bg: #3b588b;
--pronouns: #797979;
--profile-bg: color-mix(in srgb, #232323 90%, var(--accent-color));
--profile-info-bg: color-mix(in srgb, #121212 90%, var(--accent-color));
--server-border: color-mix(in srgb, #000000 80%, var(--accent-color));
--channel-name-bg: color-mix(in srgb, #2a2a2a 80%, var(--accent-color));
--server-name-bg: color-mix(in srgb, #232323 90%, var(--accent-color));
--reply-border: #474b76;
--reply-bg: #0b0d20;
--reply-text: #acacac;
--spoiler-hover: #111111;
--spoiler-open-bg: #1e1e1e;
--unknown-file-bg: #141316;
--unknown-file-border: #474555;
--login-border: #131315;
--loading-bg: #22232c;
--dialog-bg: #33363d;
--dialog-border: #1c1b31;
--scrollbar-track: #34313c;
--scrollbar-thumb: #201f29;
--scrollbar-thumb-hover: #16161f;
--markdown-timestamp: #2f2f33;
--embed: color-mix(in srgb, #131313 90%, var(--accent-color));
--link: #8888ff;
--discovery-bg: #37373b;
--message-jump:#7678b0;
--icon-color:white;
--server-list: color-mix(in srgb, #1d1d1d 90%, var(--accent-color));
--primary-bg: #303339;
--primary-hover: #272b31;
--primary-text: #dfdfdf;
--primary-text-soft: #adb8b9;
--secondary-bg: #16191b;
--secondary-hover: #252b2c;
--servers-bg: #141718;
--channels-bg: #25282b;
--channel-selected: #3c4046;
--typebox-bg: #3a3e45;
--button-bg: #4e5457;
--button-hover: #6b7174;
--spoiler-bg: #000000;
--link: #5ca9ed;
--primary-text-prominent: #efefef;
--dock-bg: #1b1e20;
--card-bg: #000000;
}
.WHITE-theme {
color-scheme: light;
--primary-text: #000;
--primary-bg: #FFF;
--black: #FFF;
--red: #dd6c6c;
--green: #639d63;
--shadow: #777;
--focus: #47bbff;
--message-bg-hover: #dedee2;
--typing-bg: #dad8d8;
--timestamp-color: #929297;
--code-bg: #cbcbcc;
--user-info-bg: #b0abc2;
--user-dock-bg: #b2b2c4;
--channels-bg: #cbcbd8;
--channel-hover: #b8b5cc;
--blank-bg: #ceccdd;
--light-border: #96959e;
--settings-hover: #b5b1bb;
--quote-bg: #7a798e;
--button-bg: #b7b7cc;
--button-hover: #FFF;
--textarea-bg: #b1b6ce;
--filename: #47bbff;
--mention-bg: #F00;
--mention-md-bg: #3b588b;
--pronouns: #6a6a6d;
--profile-bg: #cacad8;
--profile-info-bg: #bbbbce;
--server-border: #bebed3;
--channel-name-bg: #c0c0d4;
--server-name-bg: #a3a3b5;
--reply-border: #b1b2bd;
--reply-bg: #d4d6e9;
--reply-text: #2e2e30;
--spoiler-hover: #b9b9b9;
--spoiler-open-bg: #dadada;
--unknown-file-bg: #bdbdbd;
--unknown-file-border: #adadad;
--login-border: #c3c0e0;
--loading-bg: #b5b7cc;
--dialog-bg: #c1c8d6;
--dialog-border: #b9b7db;
--scrollbar-track: #d5d1e2;
--scrollbar-thumb: #b0afc0;
--scrollbar-thumb-hover: #a5a5b8;
--markdown-timestamp: #c8c8da;
--embed: #f2f3f5;
--link: #3333ee;
--discovery-bg: #c6c6d8;
--message-jump:#ccceff;
--icon-color:black;
--primary-bg: #fefefe;
--primary-hover: #f6f6f9;
--primary-text: #4b4b59;
--primary-text-soft: #656575;
--secondary-bg: #e0e0ea;
--secondary-hover: #d0d0dd;
--servers-bg: #b4b4ca;
--channels-bg: #eaeaf0;
--channel-selected: #c7c7d9;
--typebox-bg: #ededf4;
--button-bg: #cacad8;
--button-hover: #b3b3c4;
--spoiler-bg: #dadada;
--link: #056cd9;
--black: #4b4b59;
--green: #68d79d;
--primary-text-prominent: #08080d;
--secondary-text: #3c3c46;
--secondary-text-soft: #4c4c5a;
--dock-bg: #d1d1df;
--dock-hover: #b8b8d0;
}
.Light-theme {
color-scheme: light;
--primary-text: #000;
--primary-bg: #8e90c3;
--black: #fff;
--shadow: #000;
--focus: #5e50c5;
--primary-bg: #aaafce;
--primary-hover: #b1b6d4;
--primary-text: #060415;
--primary-text-soft: #424268;
--message-bg-hover: #5757b5;
--typing-bg: #d4d6e9;
--profile-bg: #8075bf;
--profile-info-bg: #8075bf;
--timestamp-color: #000000;
--code-bg: #a89adf;
--info-bg: #6060a3;
--user-info-bg: #796f9a;
--user-dock-bg: #83839d;
--channels-bg: #c2c2d1;
--channel-hover: #726e88;
--blank-bg: #5e50c5;
--light-border: #000000;
--settings-hover: #b5b1bb;
--quote-bg: #7a798e;
--button-bg: #5757b5;
--button-hover: #8e90c3;
--textarea-bg: #abb1cd;
--filename: #47bbff;
--mention-bg: #F00;
--mention-md-bg: #3b588b;
--pronouns: #202020;
--channel-name-bg: #c0c0d4;
--server-name-bg: #a3a3b5;
--secondary-bg: #9397bd;
--secondary-hover: #9ea5cc;
--servers-bg: #7a7aaa;
--channels-bg: #babdd2;
--channel-selected: #9c9fbf;
--typebox-bg: #bac0df;
--server-border: #aaaac4;
--server-hover: #7f7fa8;
--button-bg: #babdd2;
--button-hover: #9c9fbf;
--spoiler-bg: #34333a;
--link: #283c8b;
--reply-border: #474b76;
--reply-bg: #d4d6e9;
--reply-text: #38383d;
--spoiler-hover: #34333a;
--spoiler-open-bg: #767587;
--unknown-file-bg: #bdbdbd;
--unknown-file-border: #adadad;
--login-border: #c3c0e0;
--loading-bg: #b5b7cc;
--dialog-bg: #c1c8d6;
--dialog-border: #b9b7db;
--scrollbar-track: #d2cedf;
--scrollbar-thumb: #bdbcca;
--scrollbar-thumb-hover: #a7a7be;
--markdown-timestamp: #c8c8da;
--embed: #cdccd1;
--link: #5566cc;
--discovery-bg: #c6c6d8;
--message-jump:#ccceff;
--icon-color:black;
--black: #434392;
--red: #ca304d;
--secondary-text-soft: #211f2e;
--blank-bg: #494985;
--spoiler-text: #e4e6ed;
}
.Dark-Accent-theme {
color-scheme: dark;
--primary-bg: color-mix(in srgb, #3f3f3f 65%, var(--accent-color));
--primary-hover: color-mix(in srgb, #373737 68%, var(--accent-color));
--primary-text: #ebebeb;
--primary-text-soft: #ebebebb8;
--secondary-bg: color-mix(in srgb, #222222 72%, var(--accent-color));
--secondary-hover: color-mix(in srgb, #222222 65%, var(--accent-color));
--servers-bg: color-mix(in srgb, #0b0b0b 70%, var(--accent-color));
--channels-bg: color-mix(in srgb, #292929 68%, var(--accent-color));
--channel-selected: color-mix(in srgb, #555555 65%, var(--accent-color));
--typebox-bg: color-mix(in srgb, #666666 60%, var(--accent-color));
--button-bg: color-mix(in srgb, #777777 56%, var(--accent-color));
--button-hover: color-mix(in srgb, #585858 58%, var(--accent-color));
--spoiler: color-mix(in srgb, #101010 72%, var(--accent-color));
--link: color-mix(in srgb, #99ccff 75%, var(--accent-color));
--black: color-mix(in srgb, #000000 90%, var(--accent-color));
--icon: color-mix(in srgb, #ffffff, var(--accent-color));
--dock-bg: color-mix(in srgb, #171717 68%, var(--accent-color));
--spoiler-hover: color-mix(in srgb, #111111 80%, var(--accent-color));
--card-bg: color-mix(in srgb, #0b0b0b 70%, var(--accent-color));
}
/* Optional Variables */
body {
--primary-text-prominent: var(--primary-text);
--secondary-text: var(--primary-text);
--secondary-text-soft: var(--primary-text-soft);
--text-input-bg: var(--secondary-bg);
--button-text: var(--primary-text);
--button-disabled-text: color-mix(in srgb, var(--button-text), transparent);
--icon: var(--accent-color);
--focus: var(--accent-color);
--shadow: color-mix(in srgb, var(--black) 30%, transparent);
--scrollbar: var(--primary-text-soft);
--scrollbar-track: var(--primary-hover);
--blank-bg: var(--channels-bg);
--divider: color-mix(in srgb, var(--primary-text), transparent);
--channels-header-bg: var(--channels-bg);
--channel-hover: color-mix(in srgb, var(--channel-selected) 60%, transparent);
--dock-bg: var(--secondary-bg);
--dock-hover: var(--secondary-hover);
--user-info-bg: var(--dock-bg);
--user-info-text: var(--secondary-text);
--main-header-bg: transparent;
--message-jump-bg: color-mix(in srgb, var(--accent-color) 20%, transparent);
--code-bg: var(--secondary-bg);
--code-text: var(--secondary-text);
--spoiler-text: var(--primary-text);
--spoiler-hover: color-mix(in srgb, var(--spoiler-bg), var(--primary-text-soft) 10%);
--quote-line: color-mix(in srgb, var(--primary-text-soft), transparent);
--reply-line: color-mix(in srgb, var(--primary-text-soft) 20%, transparent);
--reply-text: var(--primary-text-soft);
--reply-highlight: var(--accent-color);
--mention: color-mix(in srgb, var(--accent-color) 80%, transparent);
--mention-highlight: var(--yellow);
--reaction-bg: var(--secondary-bg);
--reaction-reacted-bg: var(--secondary-hover);
--filename: var(--link);
--embed-bg: var(--secondary-bg);
--sidebar-bg: var(--channels-bg);
--sidebar-hover: var(--channel-hover);
--card-bg: var(--primary-bg);
--role-bg: var(--primary-bg);
--role-text: var(--primary-text);
--settings-bg: var(--primary-bg);
--settings-header-bg: var(--main-header-bg);
--settings-panel-bg: var(--channels-bg);
--settings-panel-selected: var(--channel-selected);
--settings-panel-hover: color-mix(in srgb, var(--settings-panel-selected), transparent);
--loading-bg: var(--secondary-bg);
--loading-text: var(--secondary-text);
}

View file

@ -0,0 +1,16 @@
{
"@metadata": {
"authors": [
"MathMan05"
],
"last-updated": "2024/15/24",
"locale": "en",
"comment":"Don't know how often I'll update this top part lol"
},
"en": {
"reply": "Reply",
"copyrawtext":"Copy raw text",
"copymessageid":"Copy message id"
},
"ru": "./ru.json"
}

View file

@ -0,0 +1,12 @@
{
"@metadata": {
"authors": [
],
"last-updated": "2024/15/24",
"locale": "ru",
"comment":"I need some help with this :P"
},
"ru": {
}
}

View file

@ -5,6 +5,8 @@ import{ Localuser }from"./localuser.js";
import{ Guild }from"./guild.js";
import{ SnowFlake }from"./snowflake.js";
import{ presencejson, userjson }from"./jsontypes.js";
import { Role } from "./role.js";
import { Search } from "./search.js";
class User extends SnowFlake{
owner: Localuser;
@ -174,6 +176,58 @@ class User extends SnowFlake{
return us.hasPermission("BAN_MEMBERS") || false;
}
);
this.contextmenu.addbutton(
"Add roles",
async function(this: User, member: Member | undefined,e){
if(member){
e.stopPropagation();
const roles:[Role,string[]][]=[];
for(const role of member.guild.roles){
if(!role.canManage()||member.roles.indexOf(role)!==-1){
continue;
}
roles.push([role,[role.name]]);
}
const search=new Search(roles);
const result=await search.find(e.x,e.y);
if(!result) return;
member.addRole(result);
}
},
null,
member=>{
if(!member)return false;
const us = member.guild.member;
console.log(us.hasPermission("MANAGE_ROLES"))
return us.hasPermission("MANAGE_ROLES") || false;
}
);
this.contextmenu.addbutton(
"Remove roles",
async function(this: User, member: Member | undefined,e){
if(member){
e.stopPropagation();
const roles:[Role,string[]][]=[];
for(const role of member.roles){
if(!role.canManage()){
continue;
}
roles.push([role,[role.name]]);
}
const search=new Search(roles);
const result=await search.find(e.x,e.y);
if(!result) return;
member.removeRole(result);
}
},
null,
member=>{
if(!member)return false;
const us = member.guild.member;
console.log(us.hasPermission("MANAGE_ROLES"))
return us.hasPermission("MANAGE_ROLES") || false;
}
);
}
static checkuser(user: User | userjson, owner: Localuser): User{
@ -245,7 +299,7 @@ class User extends SnowFlake{
async buildstatuspfp(): Promise<HTMLDivElement>{
const div = document.createElement("div");
div.style.position = "relative";
div.classList.add("pfpDiv")
const pfp = this.buildpfp();
div.append(pfp);
const status = document.createElement("div");
@ -301,7 +355,7 @@ class User extends SnowFlake{
localuser.info.api.toString() + "/users/" + id + "/profile",
{ headers: localuser.headers }
).then(res=>res.json());
return new User(json, localuser);
return new User(json.user, localuser);
}
changepfp(update: string | null): void{
@ -399,7 +453,7 @@ class User extends SnowFlake{
div.classList.add("profile", "flexttb");
}else{
this.setstatus("online");
div.classList.add("hypoprofile", "flexttb");
div.classList.add("hypoprofile", "profile", "flexttb");
}
const badgediv = document.createElement("div");
badgediv.classList.add("badges");
@ -426,7 +480,7 @@ class User extends SnowFlake{
const pfp = await this.buildstatuspfp();
div.appendChild(pfp);
const userbody = document.createElement("div");
userbody.classList.add("infosection");
userbody.classList.add("flexttb","infosection");
div.appendChild(userbody);
const usernamehtml = document.createElement("h2");
usernamehtml.textContent = this.username;
@ -449,8 +503,9 @@ class User extends SnowFlake{
if(guild){
Member.resolveMember(this, guild).then(member=>{
if(!member)return;
usernamehtml.textContent=member.name;
const roles = document.createElement("div");
roles.classList.add("rolesbox");
roles.classList.add("flexltr","rolesbox");
for(const role of member.roles){
const roleDiv = document.createElement("div");
roleDiv.classList.add("rolediv");

652
src/webpage/voice.ts Normal file
View file

@ -0,0 +1,652 @@
import { memberjson, sdpback, voiceserverupdate, voiceupdate, webRTCSocket } from "./jsontypes.js";
class VoiceFactory{
settings:{id:string};
constructor(usersettings:VoiceFactory["settings"]){
this.settings=usersettings;
}
voices=new Map<string,Map<string,Voice>>();
voiceChannels=new Map<string,Voice>();
currentVoice?:Voice;
guildUrlMap=new Map<string,{url?:string,geturl:Promise<void>,gotUrl:()=>void}>();
makeVoice(guildid:string,channelId:string,settings:Voice["settings"]){
let guild=this.voices.get(guildid);
if(!guild){
this.setUpGuild(guildid);
guild=new Map();
this.voices.set(guildid,guild);
}
const urlobj=this.guildUrlMap.get(guildid);
if(!urlobj) throw new Error("url Object doesn't exist (InternalError)");
const voice=new Voice(this.settings.id,settings,urlobj);
this.voiceChannels.set(channelId,voice);
guild.set(channelId,voice);
return voice;
}
onJoin=(_voice:Voice)=>{};
onLeave=(_voice:Voice)=>{};
joinVoice(channelId:string,guildId:string){
if(this.currentVoice){
this.currentVoice.leave();
}
const voice=this.voiceChannels.get(channelId);
if(!voice) throw new Error(`Voice ${channelId} does not exist`);
voice.join();
this.currentVoice=voice;
this.onJoin(voice);
return {
d:{
guild_id: guildId,
channel_id: channelId,
self_mute: true,//todo
self_deaf: false,//todo
self_video: false,//What is this? I have some guesses
flags: 2//?????
},
op:4
}
}
userMap=new Map<string,Voice>();
voiceStateUpdate(update:voiceupdate){
const prev=this.userMap.get(update.d.user_id);
console.log(prev,this.userMap);
if(prev){
prev.disconnect(update.d.user_id);
this.onLeave(prev);
}
const voice=this.voiceChannels.get(update.d.channel_id);
if(voice){
this.userMap.set(update.d.user_id,voice);
voice.voiceupdate(update);
}
}
private setUpGuild(id:string){
const obj:{url?:string,geturl?:Promise<void>,gotUrl?:()=>void}={};
obj.geturl=new Promise<void>(res=>{obj.gotUrl=res});
this.guildUrlMap.set(id,obj as {geturl:Promise<void>,gotUrl:()=>void});
}
voiceServerUpdate(update:voiceserverupdate){
const obj=this.guildUrlMap.get(update.d.guild_id);
if(!obj) return;
obj.url=update.d.endpoint;
obj.gotUrl();
}
}
class Voice{
private pstatus:string="not connected";
public onSatusChange:(e:string)=>unknown=()=>{};
set status(e:string){
this.pstatus=e;
this.onSatusChange(e);
}
get status(){
return this.pstatus;
}
readonly userid:string;
settings:{bitrate:number};
urlobj:{url?:string,geturl:Promise<void>,gotUrl:()=>void};
constructor(userid:string,settings:Voice["settings"],urlobj:Voice["urlobj"]){
this.userid=userid;
this.settings=settings;
this.urlobj=urlobj;
}
pc?:RTCPeerConnection;
ws?:WebSocket;
timeout:number=30000;
interval:NodeJS.Timeout=0 as unknown as NodeJS.Timeout;
time:number=0;
seq:number=0;
sendAlive(){
if(this.ws){
this.ws.send(JSON.stringify({ op: 3,d:10}));
}
}
readonly users= new Map<number,string>();
readonly speakingMap= new Map<string,number>();
onSpeakingChange=(_userid:string,_speaking:number)=>{};
disconnect(userid:string){
console.warn(userid);
if(userid===this.userid){
this.leave();
}
const ssrc=this.speakingMap.get(userid);
if(ssrc){
this.users.delete(ssrc);
for(const thing of this.ssrcMap){
if(thing[1]===ssrc){
this.ssrcMap.delete(thing[0]);
}
}
}
this.speakingMap.delete(userid);
this.userids.delete(userid);
console.log(this.userids,userid);
//there's more for sure, but this is "good enough" for now
this.onMemberChange(userid,false);
}
packet(message:MessageEvent){
const data=message.data
if(typeof data === "string"){
const json:webRTCSocket = JSON.parse(data);
switch(json.op){
case 2:
this.startWebRTC();
break;
case 4:
this.continueWebRTC(json);
break;
case 5:
this.speakingMap.set(json.d.user_id,json.d.speaking);
this.onSpeakingChange(json.d.user_id,json.d.speaking);
break;
case 6:
this.time=json.d.t;
setTimeout(this.sendAlive.bind(this), this.timeout);
break;
case 8:
this.timeout=json.d.heartbeat_interval;
setTimeout(this.sendAlive.bind(this), 1000);
break;
case 12:
this.figureRecivers();
if(!this.users.has(json.d.audio_ssrc)){
console.log("redo 12!");
this.makeOp12();
}
this.users.set(json.d.audio_ssrc,json.d.user_id);
break;
}
}
}
offer?:string;
cleanServerSDP(sdp:string):string{
const pc=this.pc;
if(!pc) throw new Error("pc isn't defined")
const ld=pc.localDescription;
if(!ld) throw new Error("localDescription isn't defined");
const parsed = Voice.parsesdp(ld.sdp);
const group=parsed.atr.get("group");
if(!group) throw new Error("group isn't in sdp");
const [_,...bundles]=(group.entries().next().value as [string, string])[0].split(" ");
bundles[bundles.length-1]=bundles[bundles.length-1].replace("\r","");
console.log(bundles);
if(!this.offer) throw new Error("Offer is missing :P");
let cline=sdp.split("\n").find(line=>line.startsWith("c="));
if(!cline) throw new Error("c line wasn't found");
const parsed1=Voice.parsesdp(sdp).medias[0];
//const parsed2=Voice.parsesdp(this.offer);
const rtcport=(parsed1.atr.get("rtcp") as Set<string>).values().next().value as string;
const ICE_UFRAG=(parsed1.atr.get("ice-ufrag") as Set<string>).values().next().value as string;
const ICE_PWD=(parsed1.atr.get("ice-pwd") as Set<string>).values().next().value as string;
const FINGERPRINT=(parsed1.atr.get("fingerprint") as Set<string>).values().next().value as string;
const candidate=(parsed1.atr.get("candidate") as Set<string>).values().next().value as string;
let build=`v=0\r
o=- 1420070400000 0 IN IP4 127.0.0.1\r
s=-\r
t=0 0\r
a=msid-semantic: WMS *\r
a=group:BUNDLE ${bundles.join(" ")}\r`
let i=0;
for(const grouping of parsed.medias){
let mode="recvonly";
for(const _ of this.senders){
if(i<2){
mode="sendrecv";
}
}
if(grouping.media==="audio"){
build+=`
m=audio ${parsed1.port} UDP/TLS/RTP/SAVPF 111\r
${cline}\r
a=rtpmap:111 opus/48000/2\r
a=fmtp:111 minptime=10;useinbandfec=1;usedtx=1\r
a=rtcp:${rtcport}\r
a=rtcp-fb:111 transport-cc\r
a=extmap:1 urn:ietf:params:rtp-hdrext:ssrc-audio-level\r
a=extmap:3 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01/r/n
a=setup:passive\r
a=mid:${bundles[i]}\r
a=maxptime:60\r
a=${mode}\r
a=ice-ufrag:${ICE_UFRAG}\r
a=ice-pwd:${ICE_PWD}\r
a=fingerprint:${FINGERPRINT}\r
a=candidate:${candidate}\r
a=rtcp-mux\r`
}else{
build+=`
m=video ${rtcport} UDP/TLS/RTP/SAVPF 102 103\r
${cline}\r
a=rtpmap:102 H264/90000\r
a=rtpmap:103 rtx/90000\r
a=fmtp:102 x-google-max-bitrate=2500;level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f\r
a=fmtp:103 apt=102\r
a=rtcp:${rtcport}\r
a=rtcp-fb:102 ccm fir\r
a=rtcp-fb:102 nack\r
a=rtcp-fb:102 nack pli\r
a=rtcp-fb:102 goog-remb\r
a=rtcp-fb:102 transport-cc\r
a=extmap:2 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time/r/n
a=extmap:3 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01/r/n
a=extmap:14 urn:ietf:params:rtp-hdrext:toffset\r
a=extmap:13 urn:3gpp:video-orientation\r
a=extmap:5 http://www.webrtc.org/experiments/rtp-hdrext/playout-delay/r/na=setup:passive/r/n
a=mid:${bundles[i]}\r
a=${mode}\r
a=ice-ufrag:${ICE_UFRAG}\r
a=ice-pwd:${ICE_PWD}\r
a=fingerprint:${FINGERPRINT}\r
a=candidate:${candidate}\r
a=rtcp-mux\r`;
}
i++
}
build+="\n";
return build;
}
counter?:string;
negotationneeded(){
if(this.pc&&this.offer){
const pc=this.pc;
pc.addEventListener("negotiationneeded", async ()=>{
this.offer=(await pc.createOffer({
offerToReceiveAudio: true,
offerToReceiveVideo: true
})).sdp;
await pc.setLocalDescription({sdp:this.offer});
if(!this.counter) throw new Error("counter isn't defined");
const counter=this.counter;
const remote:{sdp:string,type:RTCSdpType}={sdp:this.cleanServerSDP(counter),type:"answer"};
console.log(remote);
await pc.setRemoteDescription(remote);
const senders=this.senders.difference(this.ssrcMap);
for(const sender of senders){
for(const thing of (await sender.getStats() as Map<string, any>)){
if(thing[1].ssrc){
this.ssrcMap.set(sender,thing[1].ssrc);
this.makeOp12(sender);
}
}
}
console.log(this.ssrcMap);
});
}
}
async makeOp12(sender:RTCRtpSender|undefined|[RTCRtpSender,number]=(this.ssrcMap.entries().next().value)){
if(!sender) throw new Error("sender doesn't exist");
if(sender instanceof Array){
sender=sender[0];
}
if(this.ws){
this.ws.send(JSON.stringify({
op: 12,
d: {
audio_ssrc: this.ssrcMap.get(sender),
video_ssrc: 0,
rtx_ssrc: 0,
streams: [
{
type: "video",
rid: "100",
ssrc: 0,//TODO
active: false,
quality: 100,
rtx_ssrc: 0,//TODO
max_bitrate: 2500000,//TODO
max_framerate: 0,//TODO
max_resolution: {
type: "fixed",
width: 0,//TODO
height: 0//TODO
}
}
]
}
}));
this.status="Sending audio streams";
}
}
senders:Set<RTCRtpSender>=new Set();
recivers=new Set<RTCRtpReceiver>();
ssrcMap:Map<RTCRtpSender,number>=new Map();
speaking=false;
async setupMic(audioStream:MediaStream){
const audioContext = new AudioContext();
const analyser = audioContext.createAnalyser();
const microphone = audioContext.createMediaStreamSource(audioStream);
analyser.smoothingTimeConstant = 0;
analyser.fftSize = 32;
microphone.connect(analyser);
const array=new Float32Array(1);
const interval=setInterval(()=>{
if(!this.ws){
clearInterval(interval);
}
analyser.getFloatFrequencyData(array);
const value=array[0]+65;
if(value<0){
if(this.speaking){
this.speaking=false;
this.sendSpeaking();
console.log("not speaking")
}
}else if(!this.speaking){
console.log("speaking");
this.speaking=true;
this.sendSpeaking();
}
},500);
}
async sendSpeaking(){
if(!this.ws) return;
const pair=this.ssrcMap.entries().next().value;
if(!pair) return
this.ws.send(JSON.stringify({
op:5,
d:{
speaking:+this.speaking,
delay:5,//not sure
ssrc:pair[1]
}
}))
}
async continueWebRTC(data:sdpback){
if(this.pc&&this.offer){
const pc=this.pc;
this.negotationneeded();
this.status="Starting Audio streams";
const audioStream = await navigator.mediaDevices.getUserMedia({video: false, audio: true} );
for (const track of audioStream.getAudioTracks()){
//Add track
this.setupMic(audioStream);
const sender = pc.addTrack(track);
this.senders.add(sender);
console.log(sender)
}
for(let i=0;i<10;i++){
pc.addTransceiver("audio",{
direction:"recvonly",
streams:[],
sendEncodings:[{active:true,maxBitrate:this.settings.bitrate}]
});
}
for(let i=0;i<10;i++){
pc.addTransceiver("video",{
direction:"recvonly",
streams:[],
sendEncodings:[{active:true,maxBitrate:this.settings.bitrate}]
});
}
this.counter=data.d.sdp;
pc.ontrack = async (e) => {
this.status="Done";
if(e.track.kind==="video"){
return;
}
const media=e.streams[0];
console.log("got audio:",e);
for(const track of media.getTracks()){
console.log(track);
}
const context= new AudioContext();
await context.resume();
const ss=context.createMediaStreamSource(media);
console.log(media);
ss.connect(context.destination);
new Audio().srcObject = media;//weird I know, but it's for chromium/webkit bug
this.recivers.add(e.receiver)
};
}else{
this.status="Connection failed";
}
}
reciverMap=new Map<number,RTCRtpReceiver>()
async figureRecivers(){
await new Promise(res=>setTimeout(res,500));
for(const reciver of this.recivers){
const stats=await reciver.getStats() as Map<string,any>;
for(const thing of (stats)){
if(thing[1].ssrc){
this.reciverMap.set(thing[1].ssrc,reciver)
}
}
}
console.log(this.reciverMap);
}
async startWebRTC(){
this.status="Making offer";
const pc = new RTCPeerConnection();
this.pc=pc;
const offer = await pc.createOffer({
offerToReceiveAudio: true,
offerToReceiveVideo: true
});
this.status="Starting RTC connection";
const sdp=offer.sdp;
this.offer=sdp;
if(!sdp){
this.status="No SDP";
this.ws?.close();
return;
}
const parsed=Voice.parsesdp(sdp);
const video=new Map<string,[number,number]>();
const audio=new Map<string,number>();
let cur:[number,number]|undefined;
let i=0;
for(const thing of parsed.medias){
try{
if(thing.media==="video"){
const rtpmap=thing.atr.get("rtpmap");
if(!rtpmap) continue;
for(const codecpair of rtpmap){
const [port, codec]=codecpair.split(" ");
if(cur&&codec.split("/")[0]==="rtx"){
cur[1]=Number(port);
cur=undefined;
continue
}
if(video.has(codec.split("/")[0])) continue;
cur=[Number(port),-1];
video.set(codec.split("/")[0],cur);
}
}else if(thing.media==="audio"){
const rtpmap=thing.atr.get("rtpmap");
if(!rtpmap) continue;
for(const codecpair of rtpmap){
const [port, codec]=codecpair.split(" ");
if(audio.has(codec.split("/")[0])) { continue};
audio.set(codec.split("/")[0],Number(port));
}
}
}finally{
i++;
}
}
const codecs:{
name: string,
type: "video"|"audio",
priority: number,
payload_type: number,
rtx_payload_type: number|null
}[]=[];
const include=new Set<string>();
const audioAlloweds=new Map([["opus",{priority:1000,}]]);
for(const thing of audio){
if(audioAlloweds.has(thing[0])){
include.add(thing[0]);
codecs.push({
name:thing[0],
type:"audio",
priority:audioAlloweds.get(thing[0])?.priority as number,
payload_type:thing[1],
rtx_payload_type:null
});
}
}
const videoAlloweds=new Map([["H264",{priority:1000}],["VP8",{priority:2000}],["VP9",{priority:3000}]]);
for(const thing of video){
if(videoAlloweds.has(thing[0])){
include.add(thing[0]);
codecs.push({
name:thing[0],
type:"video",
priority:videoAlloweds.get(thing[0])?.priority as number,
payload_type:thing[1][0],
rtx_payload_type:thing[1][1]
});
}
}
let sendsdp="a=extmap-allow-mixed";
let first=true;
for(const media of parsed.medias){
for(const thing of first?["ice-ufrag","ice-pwd","ice-options","fingerprint","extmap","rtpmap"]:["extmap","rtpmap"]){
const thing2=media.atr.get(thing);
if(!thing2) continue;
for(const thing3 of thing2){
if(thing === "rtpmap"){
const name=thing3.split(" ")[1].split("/")[0];
if(include.has(name)){
include.delete(name);
}else{
continue;
}
}
sendsdp+=`\na=${thing}:${thing3}`;
}
}
first=false;
}
if(this.ws){
this.ws.send(JSON.stringify({
d:{
codecs,
protocol:"webrtc",
data:sendsdp,
sdp:sendsdp
},
op:1
}));
}
}
static parsesdp(sdp:string){
let currentA=new Map<string,Set<string>>();
const out:{version?:number,medias:{media:string,port:number,proto:string,ports:number[],atr:Map<string,Set<string>>}[],atr:Map<string,Set<string>>}={medias:[],atr:currentA};
for(const line of sdp.split("\n")){
const [code,setinfo]=line.split("=");
switch(code){
case "v":
out.version=Number(setinfo);
break;
case "o":
case "s":
case "t":
break;
case "m":
currentA=new Map();
const [media,port,proto,...ports]=setinfo.split(" ");
const portnums=ports.map(Number);
out.medias.push({media,port:Number(port),proto,ports:portnums,atr:currentA});
break;
case "a":
const [key, ...value] = setinfo.split(":");
if(!currentA.has(key)){
currentA.set(key,new Set());
}
currentA.get(key)?.add(value.join(":"));
break;
}
}
return out;
}
open=false;
async join(){
console.warn("Joining");
this.open=true
this.status="waiting for main WS";
}
onMemberChange=(_member:memberjson|string,_joined:boolean)=>{};
userids=new Map<string,{}>();
async voiceupdate(update:voiceupdate){
console.log("Update!");
this.userids.set(update.d.member.id,{deaf:update.d.deaf,muted:update.d.mute});
this.onMemberChange(update.d.member,true);
if(update.d.member.id===this.userid&&this.open){
if(!update) {
this.status="bad responce from WS";
return;
};
if(!this.urlobj.url){
this.status="waiting for Voice URL";
await this.urlobj.geturl;
if(!this.open){this.leave();return}
}
const ws=new WebSocket("ws://"+this.urlobj.url as string);
this.ws=ws;
ws.onclose=()=>{
this.leave();
}
this.status="waiting for WS to open";
ws.addEventListener("message",(m)=>{
this.packet(m);
})
await new Promise<void>(res=>{
ws.addEventListener("open",()=>{
res()
})
});
if(!this.ws){
this.leave();
return;
}
this.status="waiting for WS to authorize";
ws.send(JSON.stringify({
"op": 0,
"d": {
server_id: update.d.guild_id,
user_id: update.d.user_id,
session_id: update.d.session_id,
token: update.d.token,
video: false,
"streams": [
{
type: "video",
rid: "100",
quality: 100
}
]
}
}));
}
}
async leave(){
this.open=false;
this.status="Left voice chat";
if(this.ws){
this.ws.close();
this.ws=undefined;
}
if(this.pc){
this.pc.close();
this.pc=undefined;
}
}
}
export {Voice,VoiceFactory};

View file

@ -9,7 +9,8 @@
"incremental": true,
"lib": [
"esnext",
"DOM"
"DOM",
"webworker"
],
"module": "ESNext",
"moduleResolution": "Bundler",