From 369f30e5fc7003346f38df470bd056aa8cf7b464 Mon Sep 17 00:00:00 2001 From: MathMan05 Date: Wed, 9 Oct 2024 10:46:51 -0500 Subject: [PATCH 01/32] voice start --- src/webpage/audio.ts | 12 +- src/webpage/channel.ts | 21 +- src/webpage/jsontypes.ts | 212 ++++++++++++++------ src/webpage/localuser.ts | 60 ++++-- src/webpage/login.ts | 12 +- src/webpage/voice.ts | 403 +++++++++++++++++++++++++++++++++++++++ 6 files changed, 622 insertions(+), 98 deletions(-) create mode 100644 src/webpage/voice.ts diff --git a/src/webpage/audio.ts b/src/webpage/audio.ts index a92bbaf..9c096d2 100644 --- a/src/webpage/audio.ts +++ b/src/webpage/audio.ts @@ -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(); @@ -161,4 +161,4 @@ class Voice{ return userinfos.preferences.notisound; } } -export{ Voice }; +export{ AVoice as AVoice }; diff --git a/src/webpage/channel.ts b/src/webpage/channel.ts index f792ab7..3d23cb9 100644 --- a/src/webpage/channel.ts +++ b/src/webpage/channel.ts @@ -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,10 @@ 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"; declare global { interface NotificationOptions { @@ -55,6 +49,7 @@ class Channel extends SnowFlake{ idToPrev: Map = new Map(); idToNext: Map = new Map(); messages: Map = new Map(); + voice?:Voice; static setupcontextmenu(){ this.contextmenu.addbutton("Copy channel id", function(this: Channel){ navigator.clipboard.writeText(this.id); @@ -336,6 +331,9 @@ class Channel extends SnowFlake{ } this.setUpInfiniteScroller(); this.perminfo ??= {}; + if(this.type===2){ + this.voice=new Voice(this); + } } get perminfo(){ return this.guild.perminfo.channels[this.id]; @@ -840,6 +838,9 @@ class Channel extends SnowFlake{ loading.classList.add("loading"); this.rendertyping(); this.localuser.getSidePannel(); + if(this.voice){ + this.voice.join(); + } await this.putmessages(); await prom; if(id !== Channel.genid){ @@ -1334,7 +1335,7 @@ class Channel extends SnowFlake{ ); } 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; diff --git a/src/webpage/jsontypes.ts b/src/webpage/jsontypes.ts index 98a38f2..11d08a2 100644 --- a/src/webpage/jsontypes.ts +++ b/src/webpage/jsontypes.ts @@ -408,79 +408,109 @@ 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: 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, @@ -513,6 +543,65 @@ type memberlistupdatejson={ }[] } } +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; +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, dirrectjson, @@ -532,5 +621,10 @@ export{ messageCreateJson, memberChunk, invitejson, - memberlistupdatejson + memberlistupdatejson, + voiceupdate, + voiceserverupdate, + webRTCSocket, + sdpback, + opRTC12 }; diff --git a/src/webpage/localuser.ts b/src/webpage/localuser.ts index b96d8e7..67c704d 100644 --- a/src/webpage/localuser.ts +++ b/src/webpage/localuser.ts @@ -1,27 +1,17 @@ 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{channeljson,guildjson,mainuserjson,memberjson,memberlistupdatejson,messageCreateJson,presencejson,readyjson,startTypingjson,voiceupdate,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 { Voice } from "./voice.js"; const wsCodesRetry = new Set([4000, 4003, 4005, 4007, 4008, 4009]); @@ -486,8 +476,21 @@ class Localuser{ this.memberListUpdate(temp) break; } + case "VOICE_STATE_UPDATE": + if(this.waitingForVoice){ + this.waitingForVoice(temp); + } + + break; + case "VOICE_SERVER_UPDATE": + if(this.currentVoice){ + Voice.url=temp.d.endpoint; + Voice.gotUrl(); + } + break; } + }else if(temp.op === 10){ if(!this.ws)return; console.log("heartbeat down"); @@ -501,6 +504,29 @@ class Localuser{ }, this.heartbeat_interval); } } + waitingForVoice?:((arg:voiceupdate|undefined)=>void); + currentVoice?:Voice; + async joinVoice(voice:Voice){ + this.currentVoice=voice; + if(this.ws){ + this.ws.send(JSON.stringify({ + d:{ + guild_id: voice.guild.id, + channel_id: voice.channel.id, + self_mute: true,//todo + self_deaf: false,//todo + self_video: false,//What is this? I have some guesses + flags: 2//????? + }, + op:4 + })); + if(this.waitingForVoice){ + this.waitingForVoice(undefined); + } + return await new Promise((res)=>{this.waitingForVoice=res;}) + } + return undefined; + } heartbeat_interval: number = 0; updateChannel(json: channeljson): void{ const guild = this.guildids.get(json.guild_id); @@ -1167,18 +1193,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[_]); }); } diff --git a/src/webpage/login.ts b/src/webpage/login.ts index 9dc4ed0..3de326c 100644 --- a/src/webpage/login.ts +++ b/src/webpage/login.ts @@ -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; diff --git a/src/webpage/voice.ts b/src/webpage/voice.ts new file mode 100644 index 0000000..0b7aa79 --- /dev/null +++ b/src/webpage/voice.ts @@ -0,0 +1,403 @@ +import { Channel } from "./channel.js"; +import { sdpback, webRTCSocket } from "./jsontypes.js"; + +class Voice{ + owner:Channel; + static url?:string; + static gotUrl:()=>void; + static geturl=new Promise(res=>{this.gotUrl=res}) + get channel(){ + return this.owner; + } + get guild(){ + return this.owner.owner; + } + get localuser(){ + return this.owner.localuser; + } + get info(){ + return this.owner.info; + } + constructor(owner:Channel){ + this.owner=owner; + } + 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(); + 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 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.users.set(json.d.audio_ssrc,json.d.user_id); + break + + + } + } + } + offer?:string; + cleanServerSDP(sdp:string,bundle1:string,bundle2:string):string{ + if(!this.offer) throw new Error("Offer is missing :P"); + let cline:string|undefined; + console.log(sdp); + for(const line of sdp.split("\n")){ + if(line.startsWith("c=")){ + cline=line; + break; + } + } + 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).values().next().value as string; + const ICE_UFRAG=(parsed1.atr.get("ice-ufrag") as Set).values().next().value as string; + const ICE_PWD=(parsed1.atr.get("ice-pwd") as Set).values().next().value as string; + const FINGERPRINT=(parsed1.atr.get("fingerprint") as Set).values().next().value as string; + const candidate=(parsed1.atr.get("candidate") as Set).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 ${bundle1} ${bundle2}\r +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:${bundle1}\r +a=maxptime:60\r +a=inactive\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 +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:${bundle2}\r +a=inactive\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 +`; + + 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}); + const ld=pc.localDescription; + if(!ld) throw new Error("localDescription isn't defined"); + if(!this.counter) throw new Error("localDescription isn't defined"); + const counter=this.counter; + const parsed = Voice.parsesdp(ld.sdp); + const group=parsed.atr.get("group"); + if(!group) throw new Error("group isn't in sdp"); + const groupings=(group.entries().next().value as [string, string])[0].split(" ") as [string,string,string]; + groupings[2]=groupings[2].replace("\r",""); + console.log(groupings); + const remote:{sdp:string,type:RTCSdpType}={sdp:this.cleanServerSDP(counter,groupings[1],groupings[2]),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())){ + console.log(thing[1]); + if(thing[1].ssrc){ + this.ssrcMap.set(sender,thing[1].ssrc); + this.makeOp12(sender) + } + } + } + console.log(this.ssrcMap); + }); + } + } + async makeOp12(sender:RTCRtpSender){ + 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 + } + } + ] + } + })) + } + } + senders:Set=new Set(); + ssrcMap:Map=new Map(); + async continueWebRTC(data:sdpback){ + if(this.pc&&this.offer){ + const pc=this.pc; + this.negotationneeded(); + const audioStream = await navigator.mediaDevices.getUserMedia({video: false, audio: true} ); + for (const track of audioStream.getTracks()) + { + //Add track + const sender = pc.addTrack(track,audioStream); + this.senders.add(sender); + } + this.counter=data.d.sdp; + pc.ontrack = ({ streams: [stream] }) => console.log("got audio stream", stream); + + } + } + async startWebRTC(){ + const pc = new RTCPeerConnection(); + this.pc=pc; + const offer = await pc.createOffer({ + offerToReceiveAudio: true, + offerToReceiveVideo: true + }); + const sdp=offer.sdp; + this.offer=sdp; + + if(!sdp){this.ws?.close();return;} + const parsed=Voice.parsesdp(sdp); + const video=new Map(); + const audio=new Map(); + 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(); + 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>(); + const out:{version?:number,medias:{media:string,port:number,proto:string,ports:number[],atr:Map>}[],atr:Map>}={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; + } + async join(){ + console.warn("Joining"); + const json = await this.localuser.joinVoice(this); + if(!json) return; + if(!Voice.url){ + await Voice.geturl; + } + if(this.localuser.currentVoice!==this){return} + const ws=new WebSocket("ws://"+Voice.url as string); + this.ws=ws; + ws.addEventListener("message",(m)=>{ + this.packet(m); + }) + await new Promise(res=>{ + ws.addEventListener("open",()=>{ + res() + }) + }); + ws.send(JSON.stringify({ + "op": 0, + "d": { + server_id: this.guild.id, + user_id: json.d.user_id, + session_id: json.d.session_id, + token: json.d.token, + video: false, + "streams": [ + { + type: "video", + rid: "100", + quality: 100 + } + ] + } + })); + /* + const pc=new RTCPeerConnection(); + this.pc=pc; + //pc.setRemoteDescription({sdp:json.d.token,type:""}) + */ + } +} +export {Voice}; From 4ca4a894dfc465439bdf47e412523310bf70d355 Mon Sep 17 00:00:00 2001 From: MathMan05 Date: Wed, 9 Oct 2024 11:13:13 -0500 Subject: [PATCH 02/32] status --- src/webpage/channel.ts | 1 + src/webpage/voice.ts | 34 ++++++++++++++++++++++++++++++---- 2 files changed, 31 insertions(+), 4 deletions(-) diff --git a/src/webpage/channel.ts b/src/webpage/channel.ts index 3d23cb9..1d4156c 100644 --- a/src/webpage/channel.ts +++ b/src/webpage/channel.ts @@ -839,6 +839,7 @@ class Channel extends SnowFlake{ this.rendertyping(); this.localuser.getSidePannel(); if(this.voice){ + this.voice.onSatusChange=console.warn; this.voice.join(); } await this.putmessages(); diff --git a/src/webpage/voice.ts b/src/webpage/voice.ts index 0b7aa79..27ca8c0 100644 --- a/src/webpage/voice.ts +++ b/src/webpage/voice.ts @@ -6,6 +6,15 @@ class Voice{ static url?:string; static gotUrl:()=>void; static geturl=new Promise(res=>{this.gotUrl=res}) + 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; + } get channel(){ return this.owner; } @@ -193,7 +202,8 @@ a=rtcp-mux\r } ] } - })) + })); + this.status="Sending audio streams"; } } senders:Set=new Set(); @@ -202,6 +212,7 @@ a=rtcp-mux\r 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.getTracks()) { @@ -212,19 +223,27 @@ a=rtcp-mux\r this.counter=data.d.sdp; pc.ontrack = ({ streams: [stream] }) => console.log("got audio stream", stream); + }else{ + this.status="Connection failed"; } } 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.ws?.close();return;} + if(!sdp){ + this.status="No SDP"; + this.ws?.close(); + return; + } const parsed=Voice.parsesdp(sdp); const video=new Map(); const audio=new Map(); @@ -360,14 +379,20 @@ a=rtcp-mux\r } async join(){ console.warn("Joining"); + this.status="waiting for main WS"; const json = await this.localuser.joinVoice(this); - if(!json) return; + if(!json) { + this.status="bad responce from WS"; + return; + }; if(!Voice.url){ + this.status="waiting for Voice URL"; await Voice.geturl; } - if(this.localuser.currentVoice!==this){return} + if(this.localuser.currentVoice!==this){this.status="closed";return} const ws=new WebSocket("ws://"+Voice.url as string); this.ws=ws; + this.status="waiting for WS to open"; ws.addEventListener("message",(m)=>{ this.packet(m); }) @@ -376,6 +401,7 @@ a=rtcp-mux\r res() }) }); + this.status="waiting for WS to authorize"; ws.send(JSON.stringify({ "op": 0, "d": { From 0dad9fed3fa5ab4b0f611693fb783616e9a384b5 Mon Sep 17 00:00:00 2001 From: MathMan05 Date: Wed, 9 Oct 2024 11:25:48 -0500 Subject: [PATCH 03/32] rid of some logs --- src/webpage/voice.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/webpage/voice.ts b/src/webpage/voice.ts index 27ca8c0..6f8b540 100644 --- a/src/webpage/voice.ts +++ b/src/webpage/voice.ts @@ -165,7 +165,6 @@ a=rtcp-mux\r const senders=this.senders.difference(this.ssrcMap); for(const sender of senders){ for(const thing of (await sender.getStats())){ - console.log(thing[1]); if(thing[1].ssrc){ this.ssrcMap.set(sender,thing[1].ssrc); this.makeOp12(sender) From 8ca4f3368bb8cb47064b2cb292ab0919ee8cdb2d Mon Sep 17 00:00:00 2001 From: MathMan05 Date: Wed, 9 Oct 2024 12:41:55 -0500 Subject: [PATCH 04/32] fixed to voice? --- src/webpage/voice.ts | 69 ++++++++++++++++++++++++++++---------------- 1 file changed, 44 insertions(+), 25 deletions(-) diff --git a/src/webpage/voice.ts b/src/webpage/voice.ts index 6f8b540..29fa5c3 100644 --- a/src/webpage/voice.ts +++ b/src/webpage/voice.ts @@ -70,7 +70,18 @@ class Voice{ } } offer?:string; - cleanServerSDP(sdp:string,bundle1:string,bundle2:string):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:string|undefined; console.log(sdp); @@ -93,7 +104,12 @@ o=- 1420070400000 0 IN IP4 127.0.0.1\r s=-\r t=0 0\r a=msid-semantic: WMS *\r -a=group:BUNDLE ${bundle1} ${bundle2}\r +a=group:BUNDLE ${bundles.join(" ")}\r` + let i=0; + for(const grouping of parsed.medias){ + if(grouping.media==="audio"){ + build+= +` m=audio ${parsed1.port} UDP/TLS/RTP/SAVPF 111\r ${cline}\r a=rtpmap:111 opus/48000/2\r @@ -103,14 +119,17 @@ 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:${bundle1}\r +a=mid:${bundles[i]}\r a=maxptime:60\r a=inactive\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 +a=rtcp-mux\r` + }else{ +build+= +` m=video ${rtcport} UDP/TLS/RTP/SAVPF 102 103\r ${cline}\r a=rtpmap:102 H264/90000\r @@ -128,15 +147,18 @@ a=extmap:3 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extension 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:${bundle2}\r +a=mid:${bundles[i]}\r a=inactive\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 -`; - +a=rtcp-mux\r`; + } + i++ + } + build+="\n"; + console.log(build); return build; } counter?:string; @@ -149,22 +171,17 @@ a=rtcp-mux\r offerToReceiveVideo: true })).sdp; await pc.setLocalDescription({sdp:this.offer}); - const ld=pc.localDescription; - if(!ld) throw new Error("localDescription isn't defined"); - if(!this.counter) throw new Error("localDescription isn't defined"); + + + + if(!this.counter) throw new Error("counter isn't defined"); const counter=this.counter; - const parsed = Voice.parsesdp(ld.sdp); - const group=parsed.atr.get("group"); - if(!group) throw new Error("group isn't in sdp"); - const groupings=(group.entries().next().value as [string, string])[0].split(" ") as [string,string,string]; - groupings[2]=groupings[2].replace("\r",""); - console.log(groupings); - const remote:{sdp:string,type:RTCSdpType}={sdp:this.cleanServerSDP(counter,groupings[1],groupings[2]),type:"answer"}; + 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())){ + for(const thing of (await sender.sender.getStats())){ if(thing[1].ssrc){ this.ssrcMap.set(sender,thing[1].ssrc); this.makeOp12(sender) @@ -175,7 +192,7 @@ a=rtcp-mux\r }); } } - async makeOp12(sender:RTCRtpSender){ + async makeOp12(sender:RTCRtpTransceiver){ if(this.ws){ this.ws.send(JSON.stringify({ op: 12, @@ -205,19 +222,21 @@ a=rtcp-mux\r this.status="Sending audio streams"; } } - senders:Set=new Set(); - ssrcMap:Map=new Map(); + senders:Set=new Set(); + ssrcMap:Map=new Map(); 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.getTracks()) - { + for (const track of audioStream.getTracks()){ //Add track - const sender = pc.addTrack(track,audioStream); + const sender = pc.addTransceiver(track); + sender.sender.setStreams(audioStream); this.senders.add(sender); + sender.direction="sendonly"; + console.log(sender) } this.counter=data.d.sdp; pc.ontrack = ({ streams: [stream] }) => console.log("got audio stream", stream); From bb37c83d2aee268c3a691baab5e4b8d46d7eabe8 Mon Sep 17 00:00:00 2001 From: MathMan05 Date: Wed, 9 Oct 2024 14:16:34 -0500 Subject: [PATCH 05/32] closer --- src/webpage/channel.ts | 1 + src/webpage/voice.ts | 26 ++++++++++++++++++++------ 2 files changed, 21 insertions(+), 6 deletions(-) diff --git a/src/webpage/channel.ts b/src/webpage/channel.ts index 1d4156c..4804ff8 100644 --- a/src/webpage/channel.ts +++ b/src/webpage/channel.ts @@ -50,6 +50,7 @@ class Channel extends SnowFlake{ idToNext: Map = new Map(); messages: Map = new Map(); voice?:Voice; + bitrate:number=128000; static setupcontextmenu(){ this.contextmenu.addbutton("Copy channel id", function(this: Channel){ navigator.clipboard.writeText(this.id); diff --git a/src/webpage/voice.ts b/src/webpage/voice.ts index 29fa5c3..5a2b498 100644 --- a/src/webpage/voice.ts +++ b/src/webpage/voice.ts @@ -107,6 +107,12 @@ a=msid-semantic: WMS *\r a=group:BUNDLE ${bundles.join(" ")}\r` let i=0; for(const grouping of parsed.medias){ + let mode="inactive"; + for(const thing of this.senders){ + if(thing.mid===bundles[i]){ + mode="sendonly"; + } + } if(grouping.media==="audio"){ build+= ` @@ -121,7 +127,7 @@ a=extmap:3 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extension a=setup:passive\r a=mid:${bundles[i]}\r a=maxptime:60\r -a=inactive\r +a=${mode}\r a=ice-ufrag:${ICE_UFRAG}\r a=ice-pwd:${ICE_PWD}\r a=fingerprint:${FINGERPRINT}\r @@ -148,7 +154,7 @@ 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=inactive\r +a=${mode}\r a=ice-ufrag:${ICE_UFRAG}\r a=ice-pwd:${ICE_PWD}\r a=fingerprint:${FINGERPRINT}\r @@ -232,14 +238,22 @@ a=rtcp-mux\r`; const audioStream = await navigator.mediaDevices.getUserMedia({video: false, audio: true} ); for (const track of audioStream.getTracks()){ //Add track - const sender = pc.addTransceiver(track); - sender.sender.setStreams(audioStream); + const sender = pc.addTransceiver(track,{ + direction:"sendonly", + streams:[audioStream], + sendEncodings:[{active:true,maxBitrate:this.channel.bitrate}] + }); + this.channel this.senders.add(sender); - sender.direction="sendonly"; console.log(sender) } this.counter=data.d.sdp; - pc.ontrack = ({ streams: [stream] }) => console.log("got audio stream", stream); + pc.ontrack = ({ streams: [stream] }) => { + console.log("got audio stream", stream); + const audio = new Audio(); + audio.srcObject = stream; + audio.play() + }; }else{ this.status="Connection failed"; From 90809492c859d1ec4443679238c9d37401e3ca26 Mon Sep 17 00:00:00 2001 From: MathMan05 Date: Wed, 9 Oct 2024 15:01:04 -0500 Subject: [PATCH 06/32] Got audio!!! --- src/webpage/voice.ts | 53 ++++++++++++++++++++++++++------------------ 1 file changed, 32 insertions(+), 21 deletions(-) diff --git a/src/webpage/voice.ts b/src/webpage/voice.ts index 5a2b498..ba137ac 100644 --- a/src/webpage/voice.ts +++ b/src/webpage/voice.ts @@ -107,15 +107,14 @@ a=msid-semantic: WMS *\r a=group:BUNDLE ${bundles.join(" ")}\r` let i=0; for(const grouping of parsed.medias){ - let mode="inactive"; - for(const thing of this.senders){ - if(thing.mid===bundles[i]){ - mode="sendonly"; + let mode="recvonly"; + for(const _ of this.senders){ + if(i<2){ + mode="sendrecv"; } } if(grouping.media==="audio"){ - build+= -` + build+=` m=audio ${parsed1.port} UDP/TLS/RTP/SAVPF 111\r ${cline}\r a=rtpmap:111 opus/48000/2\r @@ -134,8 +133,7 @@ a=fingerprint:${FINGERPRINT}\r a=candidate:${candidate}\r a=rtcp-mux\r` }else{ -build+= -` + build+=` m=video ${rtcport} UDP/TLS/RTP/SAVPF 102 103\r ${cline}\r a=rtpmap:102 H264/90000\r @@ -187,7 +185,7 @@ a=rtcp-mux\r`; await pc.setRemoteDescription(remote); const senders=this.senders.difference(this.ssrcMap); for(const sender of senders){ - for(const thing of (await sender.sender.getStats())){ + for(const thing of (await sender.getStats())){ if(thing[1].ssrc){ this.ssrcMap.set(sender,thing[1].ssrc); this.makeOp12(sender) @@ -198,7 +196,7 @@ a=rtcp-mux\r`; }); } } - async makeOp12(sender:RTCRtpTransceiver){ + async makeOp12(sender:RTCRtpSender){ if(this.ws){ this.ws.send(JSON.stringify({ op: 12, @@ -228,8 +226,8 @@ a=rtcp-mux\r`; this.status="Sending audio streams"; } } - senders:Set=new Set(); - ssrcMap:Map=new Map(); + senders:Set=new Set(); + ssrcMap:Map=new Map(); async continueWebRTC(data:sdpback){ if(this.pc&&this.offer){ const pc=this.pc; @@ -238,20 +236,33 @@ a=rtcp-mux\r`; const audioStream = await navigator.mediaDevices.getUserMedia({video: false, audio: true} ); for (const track of audioStream.getTracks()){ //Add track - const sender = pc.addTransceiver(track,{ - direction:"sendonly", - streams:[audioStream], - sendEncodings:[{active:true,maxBitrate:this.channel.bitrate}] - }); - this.channel + console.log(track,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.channel.bitrate}] + }); + } + for(let i=0;i<10;i++){ + pc.addTransceiver("video",{ + direction:"recvonly", + streams:[], + sendEncodings:[{active:true,maxBitrate:this.channel.bitrate}] + }); + } this.counter=data.d.sdp; - pc.ontrack = ({ streams: [stream] }) => { - console.log("got audio stream", stream); + pc.ontrack = async (e) => { + console.log(e); + for(const track of e.streams[0].getTracks()){ + console.log(track); + } const audio = new Audio(); - audio.srcObject = stream; + audio.srcObject = e.streams[0]; audio.play() }; From 68fb66761538f9277a8edd934d52a69ad53bbc8b Mon Sep 17 00:00:00 2001 From: MathMan05 Date: Wed, 9 Oct 2024 15:47:31 -0500 Subject: [PATCH 07/32] various changes --- src/webpage/voice.ts | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/src/webpage/voice.ts b/src/webpage/voice.ts index ba137ac..f7a6ccd 100644 --- a/src/webpage/voice.ts +++ b/src/webpage/voice.ts @@ -257,13 +257,25 @@ a=rtcp-mux\r`; } this.counter=data.d.sdp; pc.ontrack = async (e) => { - console.log(e); - for(const track of e.streams[0].getTracks()){ + 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 audio = new Audio(); - audio.srcObject = e.streams[0]; + audio.srcObject = media; audio.play() + await new Promise(res=>setTimeout(res,1000)); + console.log(e.transceiver,this.ssrcMap.has(e.transceiver.sender)) + for(const thing of (await e.transceiver.sender.getStats())){ + if(thing[1].ssrc){ + console.log(thing[1].ssrc,this.users,this.ssrcMap) + } + } }; }else{ From 9c7daf8078796d20a54759e984a469fb7ffaadfe Mon Sep 17 00:00:00 2001 From: MathMan05 Date: Wed, 9 Oct 2024 19:32:38 -0500 Subject: [PATCH 08/32] more fixes --- src/webpage/jsontypes.ts | 128 +++++++++++++++++++-------------------- src/webpage/localuser.ts | 3 + src/webpage/voice.ts | 25 ++++++-- 3 files changed, 88 insertions(+), 68 deletions(-) diff --git a/src/webpage/jsontypes.ts b/src/webpage/jsontypes.ts index 11d08a2..d5ea1d1 100644 --- a/src/webpage/jsontypes.ts +++ b/src/webpage/jsontypes.ts @@ -483,33 +483,33 @@ type memberChunk = { 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 + 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 + op: 0, + t: "VOICE_SERVER_UPDATE", + d: { + token: string, + guild_id: string, + endpoint: string + }, + s: 6 }; type memberlistupdatejson={ op: 0, @@ -541,7 +541,7 @@ type memberlistupdatejson={ count: number, id: string }[] - } + } } type webRTCSocket= { op: 8, @@ -552,21 +552,21 @@ type webRTCSocket= { 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 - } + 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; type sdpback={ op: 4, @@ -578,29 +578,29 @@ type sdpback={ } }; 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 - } - } - ] - } + 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, diff --git a/src/webpage/localuser.ts b/src/webpage/localuser.ts index 67c704d..b9235b6 100644 --- a/src/webpage/localuser.ts +++ b/src/webpage/localuser.ts @@ -507,6 +507,9 @@ class Localuser{ waitingForVoice?:((arg:voiceupdate|undefined)=>void); currentVoice?:Voice; async joinVoice(voice:Voice){ + if(this.currentVoice){ + this.currentVoice.leave(); + } this.currentVoice=voice; if(this.ws){ this.ws.send(JSON.stringify({ diff --git a/src/webpage/voice.ts b/src/webpage/voice.ts index f7a6ccd..fe513e1 100644 --- a/src/webpage/voice.ts +++ b/src/webpage/voice.ts @@ -62,6 +62,10 @@ class Voice{ setTimeout(this.sendAlive.bind(this), 1000); break; case 12: + 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 @@ -176,8 +180,6 @@ a=rtcp-mux\r`; })).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"}; @@ -188,7 +190,7 @@ a=rtcp-mux\r`; for(const thing of (await sender.getStats())){ if(thing[1].ssrc){ this.ssrcMap.set(sender,thing[1].ssrc); - this.makeOp12(sender) + this.makeOp12(sender); } } } @@ -196,7 +198,11 @@ a=rtcp-mux\r`; }); } } - async makeOp12(sender:RTCRtpSender){ + async makeOp12(sender:RTCRtpSender|undefined|[RTCRtpSender,string]=(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, @@ -480,5 +486,16 @@ a=rtcp-mux\r`; //pc.setRemoteDescription({sdp:json.d.token,type:""}) */ } + async leave(){ + 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}; From 152ea41b6aeca3476aa02ecd5a1a2c9dd0a4aa4f Mon Sep 17 00:00:00 2001 From: MathMan05 Date: Thu, 10 Oct 2024 17:13:40 -0500 Subject: [PATCH 09/32] more fixes --- src/webpage/jsontypes.ts | 9 +++- src/webpage/voice.ts | 103 +++++++++++++++++++++++++++++++-------- 2 files changed, 90 insertions(+), 22 deletions(-) diff --git a/src/webpage/jsontypes.ts b/src/webpage/jsontypes.ts index d5ea1d1..2ab85c2 100644 --- a/src/webpage/jsontypes.ts +++ b/src/webpage/jsontypes.ts @@ -567,7 +567,14 @@ type webRTCSocket= { "modes": [],//no clue "experiments": []//no clue } -}|sdpback|opRTC12; +}|sdpback|opRTC12|{ + op: 5, + d: { + user_id: string, + speaking: 0, + ssrc: 940464811 + } +}; type sdpback={ op: 4, d: { diff --git a/src/webpage/voice.ts b/src/webpage/voice.ts index fe513e1..e1be722 100644 --- a/src/webpage/voice.ts +++ b/src/webpage/voice.ts @@ -24,9 +24,6 @@ class Voice{ get localuser(){ return this.owner.localuser; } - get info(){ - return this.owner.info; - } constructor(owner:Channel){ this.owner=owner; } @@ -42,6 +39,8 @@ class Voice{ } } readonly users= new Map(); + readonly speakingMap= new Map(); + onSpeakingChange=(_:string,_2:number)=>{}; packet(message:MessageEvent){ const data=message.data if(typeof data === "string"){ @@ -53,6 +52,10 @@ class Voice{ 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); @@ -62,14 +65,13 @@ class Voice{ 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 - - + break; } } } @@ -187,7 +189,7 @@ a=rtcp-mux\r`; await pc.setRemoteDescription(remote); const senders=this.senders.difference(this.ssrcMap); for(const sender of senders){ - for(const thing of (await sender.getStats())){ + for(const thing of (await sender.getStats() as Map)){ if(thing[1].ssrc){ this.ssrcMap.set(sender,thing[1].ssrc); this.makeOp12(sender); @@ -198,7 +200,7 @@ a=rtcp-mux\r`; }); } } - async makeOp12(sender:RTCRtpSender|undefined|[RTCRtpSender,string]=(this.ssrcMap.entries().next().value)){ + 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]; @@ -233,16 +235,61 @@ a=rtcp-mux\r`; } } senders:Set=new Set(); - ssrcMap:Map=new Map(); + recivers=new Set(); + ssrcMap:Map=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.getTracks()){ + for (const track of audioStream.getAudioTracks()){ //Add track - console.log(track,audioStream); + + this.setupMic(audioStream); const sender = pc.addTrack(track); this.senders.add(sender); console.log(sender) @@ -272,22 +319,33 @@ a=rtcp-mux\r`; for(const track of media.getTracks()){ console.log(track); } - const audio = new Audio(); - audio.srcObject = media; - audio.play() - await new Promise(res=>setTimeout(res,1000)); - console.log(e.transceiver,this.ssrcMap.has(e.transceiver.sender)) - for(const thing of (await e.transceiver.sender.getStats())){ - if(thing[1].ssrc){ - console.log(thing[1].ssrc,this.users,this.ssrcMap) - } - } + + 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() + async figureRecivers(){ + await new Promise(res=>setTimeout(res,500)); + for(const reciver of this.recivers){ + const stats=await reciver.getStats() as Map; + 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(); @@ -453,6 +511,9 @@ a=rtcp-mux\r`; if(this.localuser.currentVoice!==this){this.status="closed";return} const ws=new WebSocket("ws://"+Voice.url as string); this.ws=ws; + ws.onclose=()=>{ + this.leave(); + } this.status="waiting for WS to open"; ws.addEventListener("message",(m)=>{ this.packet(m); From 5cdb4d2184d6e3a45897c5e4f8743565a51f2065 Mon Sep 17 00:00:00 2001 From: MathMan05 Date: Fri, 11 Oct 2024 22:43:39 -0500 Subject: [PATCH 10/32] more VC work --- src/webpage/channel.ts | 59 ++++++++-- src/webpage/index.html | 25 ++-- src/webpage/localuser.ts | 64 ++++++----- src/webpage/style.css | 14 +++ src/webpage/user.ts | 2 +- src/webpage/voice.ts | 242 +++++++++++++++++++++++++++------------ 6 files changed, 279 insertions(+), 127 deletions(-) diff --git a/src/webpage/channel.ts b/src/webpage/channel.ts index 4804ff8..7d2fe0f 100644 --- a/src/webpage/channel.ts +++ b/src/webpage/channel.ts @@ -14,6 +14,7 @@ import{channeljson,embedjson,messageCreateJson,messagejson,readyjson,startTyping import{ MarkDown }from"./markdown.js"; import{ Member }from"./member.js"; import { Voice } from "./voice.js"; +import { User } from "./user.js"; declare global { interface NotificationOptions { @@ -332,8 +333,9 @@ class Channel extends SnowFlake{ } this.setUpInfiniteScroller(); this.perminfo ??= {}; - if(this.type===2){ - this.voice=new Voice(this); + 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(){ @@ -456,6 +458,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); @@ -562,6 +565,7 @@ class Channel extends SnowFlake{ const decoration = document.createElement("span"); div.appendChild(decoration); decoration.classList.add("space", "svgtheme", "svg-voice"); + }else if(this.type === 5){ // const decoration = document.createElement("span"); @@ -574,9 +578,51 @@ class Channel extends SnowFlake{ div.onclick = _=>{ this.getHTML(); }; + 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(); + } + } + 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"); + 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(); @@ -840,8 +886,7 @@ class Channel extends SnowFlake{ this.rendertyping(); this.localuser.getSidePannel(); if(this.voice){ - this.voice.onSatusChange=console.warn; - this.voice.join(); + this.localuser.joinVoice(this); } await this.putmessages(); await prom; @@ -1329,11 +1374,7 @@ class Channel extends SnowFlake{ } 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){ diff --git a/src/webpage/index.html b/src/webpage/index.html index e7b6b89..016b98a 100644 --- a/src/webpage/index.html +++ b/src/webpage/index.html @@ -35,18 +35,23 @@

Server Name

-
-
- - -
-

USERNAME

-

STATUS

-
+
+
+
+
+
+ -
- +
+

USERNAME

+

STATUS

+
+
+ +
+ +
diff --git a/src/webpage/localuser.ts b/src/webpage/localuser.ts index b9235b6..68fb87e 100644 --- a/src/webpage/localuser.ts +++ b/src/webpage/localuser.ts @@ -5,13 +5,13 @@ 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,voiceupdate,wsjson,}from"./jsontypes.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 { Voice } from "./voice.js"; +import { VoiceFactory } from "./voice.js"; const wsCodesRetry = new Set([4000, 4003, 4005, 4007, 4008, 4009]); @@ -42,6 +42,7 @@ class Localuser{ errorBackoff = 0; channelids: Map = new Map(); readonly userMap: Map = new Map(); + voiceFactory?:VoiceFactory; instancePing = { name: "Unknown", }; @@ -74,6 +75,9 @@ class Localuser{ 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.pfpsrc = this.user.getpfpsrc(); @@ -117,6 +121,7 @@ class Localuser{ this.pingEndpoint(); this.userinfo.updateLocal(); + } outoffocus(): void{ const servers = document.getElementById("servers") as HTMLDivElement; @@ -477,15 +482,14 @@ class Localuser{ break; } case "VOICE_STATE_UPDATE": - if(this.waitingForVoice){ - this.waitingForVoice(temp); + if(this.voiceFactory){ + this.voiceFactory.voiceStateUpdate(temp) } break; case "VOICE_SERVER_UPDATE": - if(this.currentVoice){ - Voice.url=temp.d.endpoint; - Voice.gotUrl(); + if(this.voiceFactory){ + this.voiceFactory.voiceServerUpdate(temp) } break; } @@ -504,32 +508,30 @@ class Localuser{ }, this.heartbeat_interval); } } - waitingForVoice?:((arg:voiceupdate|undefined)=>void); - currentVoice?:Voice; - async joinVoice(voice:Voice){ - if(this.currentVoice){ - this.currentVoice.leave(); - } - this.currentVoice=voice; - if(this.ws){ - this.ws.send(JSON.stringify({ - d:{ - guild_id: voice.guild.id, - channel_id: voice.channel.id, - self_mute: true,//todo - self_deaf: false,//todo - self_video: false,//What is this? I have some guesses - flags: 2//????? - }, - op:4 - })); - if(this.waitingForVoice){ - this.waitingForVoice(undefined); - } - return await new Promise((res)=>{this.waitingForVoice=res;}) - } + 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); diff --git a/src/webpage/style.css b/src/webpage/style.css index e3bb30d..36e4661 100644 --- a/src/webpage/style.css +++ b/src/webpage/style.css @@ -2267,4 +2267,18 @@ form div{ >div{ opacity:.4; } +} +#VoiceBox{ + padding: .05in; + display: flex; + align-items: center; + justify-content: center; + align-content: center; + width: 100%; + box-sizing: border-box; + border-radius: .1in .1in 0 0; + border: solid .01in var(--black); +} +#VoiceStatus{ + font-weight: bold; } \ No newline at end of file diff --git a/src/webpage/user.ts b/src/webpage/user.ts index 8cddb12..0867ccf 100644 --- a/src/webpage/user.ts +++ b/src/webpage/user.ts @@ -301,7 +301,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{ diff --git a/src/webpage/voice.ts b/src/webpage/voice.ts index e1be722..ada524f 100644 --- a/src/webpage/voice.ts +++ b/src/webpage/voice.ts @@ -1,11 +1,80 @@ -import { Channel } from "./channel.js"; -import { sdpback, webRTCSocket } from "./jsontypes.js"; +import { memberjson, sdpback, voiceserverupdate, voiceupdate, webRTCSocket } from "./jsontypes.js"; + +class VoiceFactory{ + settings:{id:string}; + constructor(usersettings:VoiceFactory["settings"]){ + this.settings=usersettings; + } + voices=new Map>(); + voiceChannels=new Map(); + currentVoice?:Voice; + guildUrlMap=new Map,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(); + 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,gotUrl?:()=>void}={}; + obj.geturl=new Promise(res=>{obj.gotUrl=res}); + this.guildUrlMap.set(id,obj as {geturl:Promise,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{ - owner:Channel; - static url?:string; - static gotUrl:()=>void; - static geturl=new Promise(res=>{this.gotUrl=res}) private pstatus:string="not connected"; public onSatusChange:(e:string)=>unknown=()=>{}; set status(e:string){ @@ -15,17 +84,13 @@ class Voice{ get status(){ return this.pstatus; } - get channel(){ - return this.owner; - } - get guild(){ - return this.owner.owner; - } - get localuser(){ - return this.owner.localuser; - } - constructor(owner:Channel){ - this.owner=owner; + readonly userid:string; + settings:{bitrate:number}; + urlobj:{url?:string,geturl:Promise,gotUrl:()=>void}; + constructor(userid:string,settings:Voice["settings"],urlobj:Voice["urlobj"]){ + this.userid=userid; + this.settings=settings; + this.urlobj=urlobj; } pc?:RTCPeerConnection; ws?:WebSocket; @@ -40,7 +105,28 @@ class Voice{ } readonly users= new Map(); readonly speakingMap= new Map(); - onSpeakingChange=(_:string,_2: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"){ @@ -89,14 +175,7 @@ class Voice{ console.log(bundles); if(!this.offer) throw new Error("Offer is missing :P"); - let cline:string|undefined; - console.log(sdp); - for(const line of sdp.split("\n")){ - if(line.startsWith("c=")){ - cline=line; - break; - } - } + 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); @@ -168,7 +247,6 @@ a=rtcp-mux\r`; i++ } build+="\n"; - console.log(build); return build; } counter?:string; @@ -298,18 +376,19 @@ a=rtcp-mux\r`; pc.addTransceiver("audio",{ direction:"recvonly", streams:[], - sendEncodings:[{active:true,maxBitrate:this.channel.bitrate}] + 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.channel.bitrate}] + 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; } @@ -496,58 +575,69 @@ a=rtcp-mux\r`; } return out; } + open=false; async join(){ console.warn("Joining"); + this.open=true this.status="waiting for main WS"; - const json = await this.localuser.joinVoice(this); - if(!json) { - this.status="bad responce from WS"; - return; - }; - if(!Voice.url){ - this.status="waiting for Voice URL"; - await Voice.geturl; - } - if(this.localuser.currentVoice!==this){this.status="closed";return} - const ws=new WebSocket("ws://"+Voice.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(res=>{ - ws.addEventListener("open",()=>{ - res() - }) - }); - this.status="waiting for WS to authorize"; - ws.send(JSON.stringify({ - "op": 0, - "d": { - server_id: this.guild.id, - user_id: json.d.user_id, - session_id: json.d.session_id, - token: json.d.token, - video: false, - "streams": [ - { - type: "video", - rid: "100", - quality: 100 - } - ] + } + onMemberChange=(_member:memberjson|string,_joined:boolean)=>{}; + userids=new Map(); + 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 pc=new RTCPeerConnection(); - this.pc=pc; - //pc.setRemoteDescription({sdp:json.d.token,type:""}) - */ + + 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(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(); @@ -559,4 +649,4 @@ a=rtcp-mux\r`; } } } -export {Voice}; +export {Voice,VoiceFactory}; From 83116bad5c95f7692fabf2cd7cc160faf56ae02d Mon Sep 17 00:00:00 2001 From: MathMan05 Date: Mon, 14 Oct 2024 12:55:35 -0500 Subject: [PATCH 11/32] added sounds --- src/index.ts | 2 +- src/webpage/audio.ts | 32 ++++++++++++++++++++++++++++++++ src/webpage/channel.ts | 3 +++ 3 files changed, 36 insertions(+), 1 deletion(-) diff --git a/src/index.ts b/src/index.ts index 3bbe747..882a219 100644 --- a/src/index.ts +++ b/src/index.ts @@ -149,4 +149,4 @@ app.listen(PORT, ()=>{ console.log(`Server running on port ${PORT}`); }); -export{ getApiUrls }; \ No newline at end of file +export{ getApiUrls }; diff --git a/src/webpage/audio.ts b/src/webpage/audio.ts index 9c096d2..13b90ce 100644 --- a/src/webpage/audio.ts +++ b/src/webpage/audio.ts @@ -146,6 +146,38 @@ class AVoice{ }, 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(){ diff --git a/src/webpage/channel.ts b/src/webpage/channel.ts index 7d2fe0f..4968268 100644 --- a/src/webpage/channel.ts +++ b/src/webpage/channel.ts @@ -595,6 +595,9 @@ class Channel extends SnowFlake{ await Member.new(memb,this.guild); } this.updateVoiceUsers(); + if(this.voice===this.localuser.currentVoice){ + AVoice.noises("join"); + } } } async updateVoiceUsers(){ From 33db337a7a472d309044184eeaf95b90db0a1af3 Mon Sep 17 00:00:00 2001 From: MathMan05 Date: Tue, 15 Oct 2024 14:10:21 -0500 Subject: [PATCH 12/32] index fixes --- src/webpage/localuser.ts | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/src/webpage/localuser.ts b/src/webpage/localuser.ts index 68fb87e..f2461fe 100644 --- a/src/webpage/localuser.ts +++ b/src/webpage/localuser.ts @@ -1789,16 +1789,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", { From f8e10a1e09fdbcad810c8c47783e889d27265d2e Mon Sep 17 00:00:00 2001 From: MathMan05 Date: Thu, 17 Oct 2024 20:27:34 -0500 Subject: [PATCH 13/32] initial translation support --- src/webpage/contextmenu.ts | 20 ++++--- src/webpage/i18n.ts | 96 ++++++++++++++++++++++++++++++++ src/webpage/localuser.ts | 23 ++++---- src/webpage/message.ts | 11 ++-- src/webpage/settings.ts | 2 +- src/webpage/translations/en.json | 16 ++++++ src/webpage/translations/ru.json | 12 ++++ 7 files changed, 154 insertions(+), 26 deletions(-) create mode 100644 src/webpage/i18n.ts create mode 100644 src/webpage/translations/en.json create mode 100644 src/webpage/translations/ru.json diff --git a/src/webpage/contextmenu.ts b/src/webpage/contextmenu.ts index 205c025..67cafb0 100644 --- a/src/webpage/contextmenu.ts +++ b/src/webpage/contextmenu.ts @@ -2,12 +2,12 @@ class Contextmenu{ static currentmenu: HTMLElement | ""; name: string; buttons: [ - string, - (this: x, arg: y, e: MouseEvent) => void, - string | null, - (this: x, arg: y) => boolean, - (this: x, arg: y) => boolean, - string + 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{ 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{ 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); diff --git a/src/webpage/i18n.ts b/src/webpage/i18n.ts new file mode 100644 index 0000000..7f7094e --- /dev/null +++ b/src/webpage/i18n.ts @@ -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((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}; diff --git a/src/webpage/localuser.ts b/src/webpage/localuser.ts index f2461fe..db17bf3 100644 --- a/src/webpage/localuser.ts +++ b/src/webpage/localuser.ts @@ -12,14 +12,12 @@ 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 = new Map(); lastSequence: number | null = null; token!: string; userinfo!: Specialuser; @@ -67,8 +65,10 @@ 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{ + await I18n.done; this.initialized = true; this.ready = ready; this.guilds = []; @@ -200,10 +200,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{} } })(); @@ -231,9 +231,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; @@ -244,10 +244,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{ @@ -367,7 +368,7 @@ class Localuser{ break; } case"READY": - this.gottenReady(temp as readyjson); + await this.gottenReady(temp as readyjson); break; case"MESSAGE_UPDATE": { temp.d.guild_id ??= "@me"; diff --git a/src/webpage/message.ts b/src/webpage/message.ts index fc79b87..0cc86c5 100644 --- a/src/webpage/message.ts +++ b/src/webpage/message.ts @@ -11,6 +11,7 @@ import{ SnowFlake }from"./snowflake.js"; import{ memberjson, messagejson }from"./jsontypes.js"; import{ Emoji }from"./emoji.js"; import{ Dialog }from"./dialog.js"; +import { I18n } from "./i18n.js"; class Message extends SnowFlake{ static contextmenu = new Contextmenu("message menu"); @@ -44,9 +45,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 +55,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( diff --git a/src/webpage/settings.ts b/src/webpage/settings.ts index 85d7c81..23fb76f 100644 --- a/src/webpage/settings.ts +++ b/src/webpage/settings.ts @@ -4,7 +4,7 @@ interface OptionsElement { submit: () => void; readonly watchForChange: (func: (arg1: x) => void) => void; value: x; - } +} //future me stuff class Buttons implements OptionsElement{ readonly name: string; diff --git a/src/webpage/translations/en.json b/src/webpage/translations/en.json new file mode 100644 index 0000000..8f0fd4d --- /dev/null +++ b/src/webpage/translations/en.json @@ -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" +} diff --git a/src/webpage/translations/ru.json b/src/webpage/translations/ru.json new file mode 100644 index 0000000..bbc86f1 --- /dev/null +++ b/src/webpage/translations/ru.json @@ -0,0 +1,12 @@ +{ + "@metadata": { + "authors": [ + ], + "last-updated": "2024/15/24", + "locale": "ru", + "comment":"I need some help with this :P" + }, + "ru": { + + } +} From 8a80fc69365af9d1c2ede583e5aec5ab3e5165a3 Mon Sep 17 00:00:00 2001 From: ygg2 Date: Thu, 24 Oct 2024 20:36:06 -0400 Subject: [PATCH 14/32] initial commit --- src/webpage/channel.ts | 44 +- src/webpage/contextmenu.ts | 7 + src/webpage/dialog.ts | 8 +- src/webpage/direct.ts | 3 +- src/webpage/embed.ts | 6 +- src/webpage/emoji.ts | 2 +- src/webpage/file.ts | 4 +- src/webpage/home.html | 28 +- src/webpage/home.ts | 4 +- src/webpage/icons/plus.svg | 1 + src/webpage/icons/x.svg | 1 + src/webpage/index.html | 55 +- src/webpage/index.ts | 25 +- src/webpage/infiniteScroller.ts | 2 +- src/webpage/invite.html | 4 +- src/webpage/invite.ts | 2 +- src/webpage/localuser.ts | 37 +- src/webpage/login.html | 21 +- src/webpage/login.ts | 2 +- src/webpage/markdown.ts | 4 +- src/webpage/message.ts | 84 +- src/webpage/oauth2/auth.ts | 14 +- src/webpage/oauth2/authorize.html | 6 +- src/webpage/register.html | 21 +- src/webpage/settings.ts | 33 +- src/webpage/style.css | 2284 ----------------------------- src/webpage/style2.css | 1935 ++++++++++++++++++++++++ src/webpage/themes.css | 178 --- src/webpage/themes2.css | 176 +++ src/webpage/user.ts | 8 +- 30 files changed, 2335 insertions(+), 2664 deletions(-) create mode 100644 src/webpage/icons/plus.svg create mode 100644 src/webpage/icons/x.svg delete mode 100644 src/webpage/style.css create mode 100644 src/webpage/style2.css delete mode 100644 src/webpage/themes.css create mode 100644 src/webpage/themes2.css diff --git a/src/webpage/channel.ts b/src/webpage/channel.ts index 4968268..ebb687b 100644 --- a/src/webpage/channel.ts +++ b/src/webpage/channel.ts @@ -120,13 +120,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){ @@ -489,18 +490,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)); @@ -508,8 +509,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); @@ -554,29 +555,34 @@ 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"); @@ -617,6 +623,7 @@ class Channel extends SnowFlake{ return []; } const div=document.createElement("div"); + div.classList.add("voiceuser"); const span=document.createElement("span"); span.textContent=member.name; div.append(span); @@ -812,6 +819,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"); @@ -824,14 +832,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{ diff --git a/src/webpage/contextmenu.ts b/src/webpage/contextmenu.ts index 67cafb0..56f2256 100644 --- a/src/webpage/contextmenu.ts +++ b/src/webpage/contextmenu.ts @@ -90,6 +90,13 @@ class Contextmenu{ 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); + } + }); return func; } static keepOnScreen(obj: HTMLElement){ diff --git a/src/webpage/dialog.ts b/src/webpage/dialog.ts index 2e08c11..c97251e 100644 --- a/src/webpage/dialog.ts +++ b/src/webpage/dialog.ts @@ -197,11 +197,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; diff --git a/src/webpage/direct.ts b/src/webpage/direct.ts index 1678f76..db35334 100644 --- a/src/webpage/direct.ts +++ b/src/webpage/direct.ts @@ -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","memberinfo"); const myhtml = document.createElement("span"); + myhtml.classList.add("ellipsis"); myhtml.textContent = this.name; div.appendChild(this.user.buildpfp()); div.appendChild(myhtml); diff --git a/src/webpage/embed.ts b/src/webpage/embed.ts index 6683e6a..91804fc 100644 --- a/src/webpage/embed.ts +++ b/src/webpage/embed.ts @@ -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"); diff --git a/src/webpage/emoji.ts b/src/webpage/emoji.ts index 7756967..c605345 100644 --- a/src/webpage/emoji.ts +++ b/src/webpage/emoji.ts @@ -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"); diff --git a/src/webpage/file.ts b/src/webpage/file.ts index 20862ef..63969ca 100644 --- a/src/webpage/file.ts +++ b/src/webpage/file.ts @@ -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); diff --git a/src/webpage/home.html b/src/webpage/home.html index ae7f312..a45908c 100644 --- a/src/webpage/home.html +++ b/src/webpage/home.html @@ -9,8 +9,8 @@ - - + + @@ -19,20 +19,20 @@

Jank Client

-

Spacebar Guild

+ Spacebar Guild
-

Github

+ Github +
+ + Open Client
-
+
-
-

Welcome to Jank Client

-
+

Welcome to Jank Client

-

Jank Client is a spacebar compatible client seeking to be as good as it can be with many features including: -

+

Jank Client is a Spacebar-compatible client seeking to be as good as it can be with many features including:

  • Direct Messaging
  • Reactions support
  • @@ -44,16 +44,16 @@
-

Spacebar compatible Instances:

+

Spacebar-Compatible Instances:

Contribute to Jank Client

-

We always appreciate some help, wether that be in the form of bug reports, or code, or even just pointing out +

We always appreciate some help, whether that be in the form of bug reports, or code, or even just pointing out some typos.


- -

Github

+
+ Github
diff --git a/src/webpage/home.ts b/src/webpage/home.ts index 669c0d1..9f41e77 100644 --- a/src/webpage/home.ts +++ b/src/webpage/home.ts @@ -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){ diff --git a/src/webpage/icons/plus.svg b/src/webpage/icons/plus.svg new file mode 100644 index 0000000..e487b7a --- /dev/null +++ b/src/webpage/icons/plus.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/webpage/icons/x.svg b/src/webpage/icons/x.svg new file mode 100644 index 0000000..f7fb14d --- /dev/null +++ b/src/webpage/icons/x.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/webpage/index.html b/src/webpage/index.html index 016b98a..a3f5470 100644 --- a/src/webpage/index.html +++ b/src/webpage/index.html @@ -9,17 +9,17 @@ - - - + + + - +
-
+

Jank Client is loading

This shouldn't take long

@@ -27,15 +27,13 @@
-
-
-
+
-
-

Server Name

+
+

Server Name

-
+
@@ -43,39 +41,48 @@
-
+

USERNAME

STATUS

- +
-
-
- - Channel name - +
+
+ + + + Channel name + + + +
-
-
-
+
+
+
-
+
-