update instancejson more

MY HEAD HURTS NOW ;(

.

.

more work

Finish rewrite

all finished
This commit is contained in:
MathMan05 2024-09-16 19:11:05 -05:00 committed by Scott Gould
parent 3a61417de2
commit 758bd7cc7a
No known key found for this signature in database
113 changed files with 12520 additions and 20427 deletions

View file

@ -1,159 +0,0 @@
import { getBulkInfo } from "./login.js";
class Voice {
audioCtx;
info;
playing;
myArrayBuffer;
gainNode;
buffer;
source;
constructor(wave, freq, volume = 1) {
this.audioCtx = new (window.AudioContext)();
this.info = { wave, freq };
this.playing = false;
this.myArrayBuffer = this.audioCtx.createBuffer(1, this.audioCtx.sampleRate, this.audioCtx.sampleRate);
this.gainNode = this.audioCtx.createGain();
this.gainNode.gain.value = volume;
this.gainNode.connect(this.audioCtx.destination);
this.buffer = this.myArrayBuffer.getChannelData(0);
this.source = this.audioCtx.createBufferSource();
this.source.buffer = this.myArrayBuffer;
this.source.loop = true;
this.source.start();
this.updateWave();
}
get wave() {
return this.info.wave;
}
get freq() {
return this.info.freq;
}
set wave(wave) {
this.info.wave = wave;
this.updateWave();
}
set freq(freq) {
this.info.freq = freq;
this.updateWave();
}
updateWave() {
const func = this.waveFunction();
for (let i = 0; i < this.buffer.length; i++) {
this.buffer[i] = func(i / this.audioCtx.sampleRate, this.freq);
}
}
waveFunction() {
if (typeof this.wave === "function") {
return this.wave;
}
switch (this.wave) {
case "sin":
return (t, freq) => {
return Math.sin(t * Math.PI * 2 * freq);
};
case "triangle":
return (t, freq) => {
return Math.abs((4 * t * freq) % 4 - 2) - 1;
};
case "sawtooth":
return (t, freq) => {
return ((t * freq) % 1) * 2 - 1;
};
case "square":
return (t, freq) => {
return (t * freq) % 2 < 1 ? 1 : -1;
};
case "white":
return (_t, _freq) => {
return Math.random() * 2 - 1;
};
case "noise":
return (_t, _freq) => {
return 0;
};
}
return new Function();
}
play() {
if (this.playing) {
return;
}
this.source.connect(this.gainNode);
this.playing = true;
}
stop() {
if (this.playing) {
this.source.disconnect();
this.playing = false;
}
}
static noises(noise) {
switch (noise) {
case "three": {
const voicy = new Voice("sin", 800);
voicy.play();
setTimeout(_ => {
voicy.freq = 1000;
}, 50);
setTimeout(_ => {
voicy.freq = 1300;
}, 100);
setTimeout(_ => {
voicy.stop();
}, 150);
break;
}
case "zip": {
const voicy = new Voice((t, freq) => {
return Math.sin(((t + 2) ** (Math.cos(t * 4))) * Math.PI * 2 * freq);
}, 700);
voicy.play();
setTimeout(_ => {
voicy.stop();
}, 150);
break;
}
case "square": {
const voicy = new Voice("square", 600, 0.4);
voicy.play();
setTimeout(_ => {
voicy.freq = 800;
}, 50);
setTimeout(_ => {
voicy.freq = 1000;
}, 100);
setTimeout(_ => {
voicy.stop();
}, 150);
break;
}
case "beep": {
const voicy = new Voice("sin", 800);
voicy.play();
setTimeout(_ => {
voicy.stop();
}, 50);
setTimeout(_ => {
voicy.play();
}, 100);
setTimeout(_ => {
voicy.stop();
}, 150);
break;
}
}
}
static get sounds() {
return ["three", "zip", "square", "beep"];
}
static setNotificationSound(sound) {
const userinfos = getBulkInfo();
userinfos.preferences.notisound = sound;
localStorage.setItem("userinfos", JSON.stringify(userinfos));
}
static getNotificationSound() {
const userinfos = getBulkInfo();
return userinfos.preferences.notisound;
}
}
export { Voice };

File diff suppressed because it is too large Load diff

View file

@ -1,86 +0,0 @@
class Contextmenu {
static currentmenu;
name;
buttons;
div;
static setup() {
Contextmenu.currentmenu = "";
document.addEventListener("click", event => {
if (Contextmenu.currentmenu === "") {
return;
}
if (!Contextmenu.currentmenu.contains(event.target)) {
Contextmenu.currentmenu.remove();
Contextmenu.currentmenu = "";
}
});
}
constructor(name) {
this.name = name;
this.buttons = [];
}
addbutton(text, onclick, img = null, shown = _ => true, enabled = _ => true) {
this.buttons.push([text, onclick, img, shown, enabled, "button"]);
return {};
}
addsubmenu(text, onclick, img = null, shown = _ => true, enabled = _ => true) {
this.buttons.push([text, onclick, img, shown, enabled, "submenu"]);
return {};
}
makemenu(x, y, addinfo, other) {
const div = document.createElement("div");
div.classList.add("contextmenu", "flexttb");
let visibleButtons = 0;
for (const thing of this.buttons) {
if (!thing[3].bind(addinfo)(other))
continue;
visibleButtons++;
const intext = document.createElement("button");
intext.disabled = !thing[4].bind(addinfo)(other);
intext.classList.add("contextbutton");
intext.textContent = thing[0];
console.log(thing);
if (thing[5] === "button" || thing[5] === "submenu") {
intext.onclick = thing[1].bind(addinfo, other);
}
div.appendChild(intext);
}
if (visibleButtons == 0)
return;
if (Contextmenu.currentmenu != "") {
Contextmenu.currentmenu.remove();
}
div.style.top = y + "px";
div.style.left = x + "px";
document.body.appendChild(div);
Contextmenu.keepOnScreen(div);
console.log(div);
Contextmenu.currentmenu = div;
return this.div;
}
bindContextmenu(obj, addinfo, other) {
const func = event => {
event.preventDefault();
event.stopImmediatePropagation();
this.makemenu(event.clientX, event.clientY, addinfo, other);
};
obj.addEventListener("contextmenu", func);
return func;
}
static keepOnScreen(obj) {
const html = document.documentElement.getBoundingClientRect();
const docheight = html.height;
const docwidth = html.width;
const box = obj.getBoundingClientRect();
console.log(box, docheight, docwidth);
if (box.right > docwidth) {
console.log("test");
obj.style.left = docwidth - box.width + "px";
}
if (box.bottom > docheight) {
obj.style.top = docheight - box.height + "px";
}
}
}
Contextmenu.setup();
export { Contextmenu };

View file

@ -1,244 +0,0 @@
class Dialog {
layout;
onclose;
onopen;
html;
background;
constructor(layout, onclose = _ => { }, onopen = _ => { }) {
this.layout = layout;
this.onclose = onclose;
this.onopen = onopen;
const div = document.createElement("div");
div.appendChild(this.tohtml(layout));
this.html = div;
this.html.classList.add("centeritem");
if (!(layout[0] === "img")) {
this.html.classList.add("nonimagecenter");
}
}
tohtml(array) {
switch (array[0]) {
case "img":
const img = document.createElement("img");
img.src = array[1];
if (array[2] != undefined) {
if (array[2].length === 2) {
img.width = array[2][0];
img.height = array[2][1];
}
else if (array[2][0] === "fit") {
img.classList.add("imgfit");
}
}
return img;
case "hdiv":
const hdiv = document.createElement("div");
hdiv.classList.add("flexltr");
for (const thing of array) {
if (thing === "hdiv") {
continue;
}
hdiv.appendChild(this.tohtml(thing));
}
return hdiv;
case "vdiv":
const vdiv = document.createElement("div");
vdiv.classList.add("flexttb");
for (const thing of array) {
if (thing === "vdiv") {
continue;
}
vdiv.appendChild(this.tohtml(thing));
}
return vdiv;
case "checkbox":
{
const div = document.createElement("div");
const checkbox = document.createElement("input");
div.appendChild(checkbox);
const label = document.createElement("span");
checkbox.checked = array[2];
label.textContent = array[1];
div.appendChild(label);
checkbox.addEventListener("change", array[3]);
checkbox.type = "checkbox";
return div;
}
case "button":
{
const div = document.createElement("div");
const input = document.createElement("button");
const label = document.createElement("span");
input.textContent = array[2];
label.textContent = array[1];
div.appendChild(label);
div.appendChild(input);
input.addEventListener("click", array[3]);
return div;
}
case "mdbox":
{
const div = document.createElement("div");
const input = document.createElement("textarea");
input.value = array[2];
const label = document.createElement("span");
label.textContent = array[1];
input.addEventListener("input", array[3]);
div.appendChild(label);
div.appendChild(document.createElement("br"));
div.appendChild(input);
return div;
}
case "textbox":
{
const div = document.createElement("div");
const input = document.createElement("input");
input.value = array[2];
input.type = "text";
const label = document.createElement("span");
label.textContent = array[1];
console.log(array[3]);
input.addEventListener("input", array[3]);
div.appendChild(label);
div.appendChild(input);
return div;
}
case "fileupload":
{
const div = document.createElement("div");
const input = document.createElement("input");
input.type = "file";
const label = document.createElement("span");
label.textContent = array[1];
div.appendChild(label);
div.appendChild(input);
input.addEventListener("change", array[2]);
console.log(array);
return div;
}
case "text": {
const span = document.createElement("span");
span.textContent = array[1];
return span;
}
case "title": {
const span = document.createElement("span");
span.classList.add("title");
span.textContent = array[1];
return span;
}
case "radio": {
const div = document.createElement("div");
const fieldset = document.createElement("fieldset");
fieldset.addEventListener("change", () => {
let i = -1;
for (const thing of fieldset.children) {
i++;
if (i === 0) {
continue;
}
const checkbox = thing.children[0].children[0];
if (checkbox.checked) {
array[3](checkbox.value);
}
}
});
const legend = document.createElement("legend");
legend.textContent = array[1];
fieldset.appendChild(legend);
let i = 0;
for (const thing of array[2]) {
const div = document.createElement("div");
const input = document.createElement("input");
input.classList.add("radio");
input.type = "radio";
input.name = array[1];
input.value = thing;
if (i === array[4]) {
input.checked = true;
}
const label = document.createElement("label");
label.appendChild(input);
const span = document.createElement("span");
span.textContent = thing;
label.appendChild(span);
div.appendChild(label);
fieldset.appendChild(div);
i++;
}
div.appendChild(fieldset);
return div;
}
case "html":
return array[1];
case "select": {
const div = document.createElement("div");
const label = document.createElement("label");
const select = document.createElement("select");
label.textContent = array[1];
div.append(label);
div.appendChild(select);
for (const thing of array[2]) {
const option = document.createElement("option");
option.textContent = thing;
select.appendChild(option);
}
select.selectedIndex = array[4];
select.addEventListener("change", array[3]);
return div;
}
case "tabs": {
const table = document.createElement("div");
table.classList.add("flexttb");
const tabs = document.createElement("div");
tabs.classList.add("flexltr");
tabs.classList.add("tabbed-head");
table.appendChild(tabs);
const content = document.createElement("div");
content.classList.add("tabbed-content");
table.appendChild(content);
let shown;
for (const thing of array[1]) {
const button = document.createElement("button");
button.textContent = thing[0];
tabs.appendChild(button);
const html = this.tohtml(thing[1]);
content.append(html);
if (!shown) {
shown = html;
}
else {
html.style.display = "none";
}
button.addEventListener("click", _ => {
if (shown) {
shown.style.display = "none";
}
html.style.display = "";
shown = html;
});
}
return table;
}
default:
console.error("can't find element:" + array[0], " full element:", array);
return document.createElement("span");
}
}
show() {
this.onopen();
console.log("fullscreen");
this.background = document.createElement("div");
this.background.classList.add("background");
document.body.appendChild(this.background);
document.body.appendChild(this.html);
this.background.onclick = _ => {
this.hide();
};
}
hide() {
document.body.removeChild(this.background);
document.body.removeChild(this.html);
}
}
export { Dialog };

View file

@ -1,283 +0,0 @@
import { Guild } from "./guild.js";
import { Channel } from "./channel.js";
import { Message } from "./message.js";
import { User } from "./user.js";
import { Permissions } from "./permissions.js";
import { SnowFlake } from "./snowflake.js";
import { Contextmenu } from "./contextmenu.js";
class Direct extends Guild {
getUnixTime() {
throw new Error("Do not call this for Direct, it does not make sense");
}
constructor(json, owner) {
super(-1, owner, null);
this.message_notifications = 0;
this.owner = owner;
if (!this.localuser) {
console.error("Owner was not included, please fix");
}
this.headers = this.localuser.headers;
this.channels = [];
this.channelids = {};
this.properties = {};
this.roles = [];
this.roleids = new Map();
this.prevchannel = undefined;
this.properties.name = "Direct Messages";
for (const thing of json) {
const temp = new Group(thing, this);
this.channels.push(temp);
this.channelids[temp.id] = temp;
}
this.headchannels = this.channels;
}
createChannelpac(json) {
const thischannel = new Group(json, this);
this.channelids[thischannel.id] = thischannel;
this.channels.push(thischannel);
this.sortchannels();
this.printServers();
return thischannel;
}
delChannel(json) {
const channel = this.channelids[json.id];
super.delChannel(json);
if (channel) {
channel.del();
}
}
giveMember(_member) {
console.error("not a real guild, can't give member object");
}
getRole(ID) {
return null;
}
hasRole(r) {
return false;
}
isAdmin() {
return false;
}
unreaddms() {
for (const thing of this.channels) {
thing.unreads();
}
}
}
const dmPermissions = new Permissions("0");
dmPermissions.setPermission("ADD_REACTIONS", 1);
dmPermissions.setPermission("VIEW_CHANNEL", 1);
dmPermissions.setPermission("SEND_MESSAGES", 1);
dmPermissions.setPermission("EMBED_LINKS", 1);
dmPermissions.setPermission("ATTACH_FILES", 1);
dmPermissions.setPermission("READ_MESSAGE_HISTORY", 1);
dmPermissions.setPermission("MENTION_EVERYONE", 1);
dmPermissions.setPermission("USE_EXTERNAL_EMOJIS", 1);
dmPermissions.setPermission("USE_APPLICATION_COMMANDS", 1);
dmPermissions.setPermission("USE_EXTERNAL_STICKERS", 1);
dmPermissions.setPermission("USE_EMBEDDED_ACTIVITIES", 1);
dmPermissions.setPermission("USE_SOUNDBOARD", 1);
dmPermissions.setPermission("USE_EXTERNAL_SOUNDS", 1);
dmPermissions.setPermission("SEND_VOICE_MESSAGES", 1);
dmPermissions.setPermission("SEND_POLLS", 1);
dmPermissions.setPermission("USE_EXTERNAL_APPS", 1);
dmPermissions.setPermission("CONNECT", 1);
dmPermissions.setPermission("SPEAK", 1);
dmPermissions.setPermission("STREAM", 1);
dmPermissions.setPermission("USE_VAD", 1);
class Group extends Channel {
user;
static contextmenu = new Contextmenu("channel menu");
static setupcontextmenu() {
this.contextmenu.addbutton("Copy DM id", function () {
navigator.clipboard.writeText(this.id);
});
this.contextmenu.addbutton("Mark as read", function () {
this.readbottom();
});
this.contextmenu.addbutton("Close DM", function () {
this.deleteChannel();
});
this.contextmenu.addbutton("Copy user ID", function () {
navigator.clipboard.writeText(this.user.id);
});
}
constructor(json, owner) {
super(-1, owner, json.id);
this.owner = owner;
this.headers = this.guild.headers;
this.name = json.recipients[0]?.username;
if (json.recipients[0]) {
this.user = new User(json.recipients[0], this.localuser);
}
else {
this.user = this.localuser.user;
}
this.name ??= this.localuser.user.username;
this.parent_id = undefined;
this.parent = null;
this.children = [];
this.guild_id = "@me";
this.permission_overwrites = new Map();
this.lastmessageid = json.last_message_id;
this.mentions = 0;
this.setUpInfiniteScroller();
this.updatePosition();
}
updatePosition() {
if (this.lastmessageid) {
this.position = SnowFlake.stringToUnixTime(this.lastmessageid);
}
else {
this.position = 0;
}
this.position = -Math.max(this.position, this.getUnixTime());
}
createguildHTML() {
const div = document.createElement("div");
Group.contextmenu.bindContextmenu(div, this, undefined);
this.html = new WeakRef(div);
div.classList.add("channeleffects");
const myhtml = document.createElement("span");
myhtml.textContent = this.name;
div.appendChild(this.user.buildpfp());
div.appendChild(myhtml);
div["myinfo"] = this;
div.onclick = _ => {
this.getHTML();
};
return div;
}
async getHTML() {
const id = ++Channel.genid;
if (this.localuser.channelfocus) {
this.localuser.channelfocus.infinite.delete();
}
if (this.guild !== this.localuser.lookingguild) {
this.guild.loadGuild();
}
this.guild.prevchannel = this;
this.localuser.channelfocus = this;
const prom = this.infinite.delete();
history.pushState(null, "", "/channels/" + this.guild_id + "/" + this.id);
this.localuser.pageTitle("@" + this.name);
document.getElementById("channelTopic").setAttribute("hidden", "");
const loading = document.getElementById("loadingdiv");
Channel.regenLoadingMessages();
loading.classList.add("loading");
this.rendertyping();
await this.putmessages();
await prom;
if (id !== Channel.genid) {
return;
}
this.buildmessages();
document.getElementById("typebox").contentEditable = "" + true;
}
messageCreate(messagep) {
const messagez = new Message(messagep.d, this);
if (this.lastmessageid) {
this.idToNext.set(this.lastmessageid, messagez.id);
this.idToPrev.set(messagez.id, this.lastmessageid);
}
this.lastmessageid = messagez.id;
if (messagez.author === this.localuser.user) {
this.lastreadmessageid = messagez.id;
if (this.myhtml) {
this.myhtml.classList.remove("cunread");
}
}
else {
if (this.myhtml) {
this.myhtml.classList.add("cunread");
}
}
this.unreads();
this.updatePosition();
this.infinite.addedBottom();
this.guild.sortchannels();
if (this.myhtml) {
const parrent = this.myhtml.parentElement;
parrent.prepend(this.myhtml);
}
if (this === this.localuser.channelfocus) {
if (!this.infinitefocus) {
this.tryfocusinfinate();
}
this.infinite.addedBottom();
}
this.unreads();
if (messagez.author === this.localuser.user) {
return;
}
if (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.notify(messagez);
}
}
notititle(message) {
return message.author.username;
}
readbottom() {
super.readbottom();
this.unreads();
}
all = new WeakRef(document.createElement("div"));
noti = new WeakRef(document.createElement("div"));
del() {
const all = this.all.deref();
if (all) {
all.remove();
}
if (this.myhtml) {
this.myhtml.remove();
}
}
unreads() {
const sentdms = document.getElementById("sentdms"); //Need to change sometime
const current = this.all.deref();
if (this.hasunreads) {
{
const noti = this.noti.deref();
if (noti) {
noti.textContent = this.mentions + "";
return;
}
}
const div = document.createElement("div");
div.classList.add("servernoti");
const noti = document.createElement("div");
noti.classList.add("unread", "notiunread", "pinged");
noti.textContent = "" + this.mentions;
this.noti = new WeakRef(noti);
div.append(noti);
const buildpfp = this.user.buildpfp();
this.all = new WeakRef(div);
buildpfp.classList.add("mentioned");
div.append(buildpfp);
sentdms.append(div);
div.onclick = _ => {
this.guild.loadGuild();
this.getHTML();
};
}
else if (current) {
current.remove();
}
else {
}
}
isAdmin() {
return false;
}
hasPermission(name) {
return dmPermissions.hasPermission(name);
}
}
export { Direct, Group };
Group.setupcontextmenu();

View file

@ -1,385 +0,0 @@
import { Dialog } from "./dialog.js";
import { MarkDown } from "./markdown.js";
import { getapiurls, getInstances } from "./login.js";
import { Guild } from "./guild.js";
class Embed {
type;
owner;
json;
constructor(json, owner) {
this.type = this.getType(json);
this.owner = owner;
this.json = json;
}
getType(json) {
const instances = getInstances();
if (instances && json.type === "link" && json.url && URL.canParse(json.url)) {
const Url = new URL(json.url);
for (const instance of instances) {
if (instance.url && URL.canParse(instance.url)) {
const IUrl = new URL(instance.url);
const params = new URLSearchParams(Url.search);
let host;
if (params.has("instance")) {
const url = params.get("instance");
if (URL.canParse(url)) {
host = new URL(url).host;
}
else {
host = Url.host;
}
}
else {
host = Url.host;
}
if (IUrl.host === host) {
const code = Url.pathname.split("/")[Url.pathname.split("/").length - 1];
json.invite = {
url: instance.url,
code
};
return "invite";
}
}
}
}
return json.type || "rich";
}
generateHTML() {
switch (this.type) {
case "rich":
return this.generateRich();
case "image":
return this.generateImage();
case "invite":
return this.generateInvite();
case "link":
return this.generateLink();
case "video":
case "article":
return this.generateArticle();
default:
console.warn(`unsupported embed type ${this.type}, please add support dev :3`, this.json);
return document.createElement("div"); //prevent errors by giving blank div
}
}
get message() {
return this.owner;
}
get channel() {
return this.message.channel;
}
get guild() {
return this.channel.guild;
}
get localuser() {
return this.guild.localuser;
}
generateRich() {
const div = document.createElement("div");
if (this.json.color) {
div.style.backgroundColor = "#" + this.json.color.toString(16);
}
div.classList.add("embed-color");
const embed = document.createElement("div");
embed.classList.add("embed");
div.append(embed);
if (this.json.author) {
const authorline = document.createElement("div");
if (this.json.author.icon_url) {
const img = document.createElement("img");
img.classList.add("embedimg");
img.src = this.json.author.icon_url;
authorline.append(img);
}
const a = document.createElement("a");
a.textContent = this.json.author.name;
if (this.json.author.url) {
MarkDown.safeLink(a, this.json.author.url);
}
a.classList.add("username");
authorline.append(a);
embed.append(authorline);
}
if (this.json.title) {
const title = document.createElement("a");
title.append(new MarkDown(this.json.title, this.channel).makeHTML());
if (this.json.url) {
MarkDown.safeLink(title, this.json.url);
}
title.classList.add("embedtitle");
embed.append(title);
}
if (this.json.description) {
const p = document.createElement("p");
p.append(new MarkDown(this.json.description, this.channel).makeHTML());
embed.append(p);
}
embed.append(document.createElement("br"));
if (this.json.fields) {
for (const thing of this.json.fields) {
const div = document.createElement("div");
const b = document.createElement("b");
b.textContent = thing.name;
div.append(b);
const p = document.createElement("p");
p.append(new MarkDown(thing.value, this.channel).makeHTML());
p.classList.add("embedp");
div.append(p);
if (thing.inline) {
div.classList.add("inline");
}
embed.append(div);
}
}
if (this.json.footer || this.json.timestamp) {
const footer = document.createElement("div");
if (this.json?.footer?.icon_url) {
const img = document.createElement("img");
img.src = this.json.footer.icon_url;
img.classList.add("embedicon");
footer.append(img);
}
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");
footer.append(span);
}
if (this.json?.timestamp) {
const span = document.createElement("span");
span.textContent = new Date(this.json.timestamp).toLocaleString();
footer.append(span);
}
embed.append(footer);
}
return div;
}
generateImage() {
const img = document.createElement("img");
img.classList.add("messageimg");
img.onclick = function () {
const full = new Dialog(["img", img.src, ["fit"]]);
full.show();
};
img.src = this.json.thumbnail.proxy_url;
if (this.json.thumbnail.width) {
let scale = 1;
const max = 96 * 3;
scale = Math.max(scale, this.json.thumbnail.width / max);
scale = Math.max(scale, this.json.thumbnail.height / max);
this.json.thumbnail.width /= scale;
this.json.thumbnail.height /= scale;
}
img.style.width = this.json.thumbnail.width + "px";
img.style.height = this.json.thumbnail.height + "px";
console.log(this.json, "Image fix");
return img;
}
generateLink() {
const table = document.createElement("table");
table.classList.add("embed", "linkembed");
const trtop = document.createElement("tr");
table.append(trtop);
if (this.json.url && this.json.title) {
const td = document.createElement("td");
const a = document.createElement("a");
MarkDown.safeLink(a, this.json.url);
a.textContent = this.json.title;
td.append(a);
trtop.append(td);
}
{
const td = document.createElement("td");
const img = document.createElement("img");
if (this.json.thumbnail) {
img.classList.add("embedimg");
img.onclick = function () {
const full = new Dialog(["img", img.src, ["fit"]]);
full.show();
};
img.src = this.json.thumbnail.proxy_url;
td.append(img);
}
trtop.append(td);
}
const bottomtr = document.createElement("tr");
const td = document.createElement("td");
if (this.json.description) {
const span = document.createElement("span");
span.textContent = this.json.description;
td.append(span);
}
bottomtr.append(td);
table.append(bottomtr);
return table;
}
invcache;
generateInvite() {
if (this.invcache && (!this.json.invite || !this.localuser)) {
return this.generateLink();
}
const div = document.createElement("div");
div.classList.add("embed", "inviteEmbed", "flexttb");
const json1 = this.json.invite;
(async () => {
let json;
let info;
if (!this.invcache) {
if (!json1) {
div.append(this.generateLink());
return;
}
const tempinfo = await getapiurls(json1.url);
;
if (!tempinfo) {
div.append(this.generateLink());
return;
}
info = tempinfo;
const res = await fetch(info.api + "/invites/" + json1.code);
if (!res.ok) {
div.append(this.generateLink());
}
json = await res.json();
this.invcache = [json, info];
}
else {
[json, info] = this.invcache;
}
if (!json) {
div.append(this.generateLink());
return;
}
if (json.guild.banner) {
const banner = document.createElement("img");
banner.src = this.localuser.info.cdn + "/icons/" + json.guild.id + "/" + json.guild.banner + ".png?size=256";
banner.classList.add("banner");
div.append(banner);
}
const guild = json.guild;
guild.info = info;
const icon = Guild.generateGuildIcon(guild);
const iconrow = document.createElement("div");
iconrow.classList.add("flexltr", "flexstart");
iconrow.append(icon);
{
const guildinfo = document.createElement("div");
guildinfo.classList.add("flexttb", "invguildinfo");
const name = document.createElement("b");
name.textContent = guild.name;
guildinfo.append(name);
const members = document.createElement("span");
members.innerText = "#" + json.channel.name + " • Members: " + guild.member_count;
guildinfo.append(members);
members.classList.add("subtext");
iconrow.append(guildinfo);
}
div.append(iconrow);
const h2 = document.createElement("h2");
h2.textContent = `You've been invited by ${json.inviter.username}`;
div.append(h2);
const button = document.createElement("button");
button.textContent = "Accept";
if (this.localuser.info.api.startsWith(info.api)) {
if (this.localuser.guildids.has(guild.id)) {
button.textContent = "Already joined";
button.disabled = true;
}
}
button.classList.add("acceptinvbutton");
div.append(button);
button.onclick = _ => {
if (this.localuser.info.api.startsWith(info.api)) {
fetch(this.localuser.info.api + "/invites/" + json.code, {
method: "POST",
headers: this.localuser.headers,
}).then(r => r.json()).then(_ => {
if (_.message) {
alert(_.message);
}
});
}
else {
if (this.json.invite) {
const params = new URLSearchParams("");
params.set("instance", this.json.invite.url);
const encoded = params.toString();
const url = `${location.origin}/invite/${this.json.invite.code}?${encoded}`;
window.open(url, "_blank");
}
}
};
})();
return div;
}
generateArticle() {
const colordiv = document.createElement("div");
colordiv.style.backgroundColor = "#000000";
colordiv.classList.add("embed-color");
const div = document.createElement("div");
div.classList.add("embed");
if (this.json.provider) {
const provider = document.createElement("p");
provider.classList.add("provider");
provider.textContent = this.json.provider.name;
div.append(provider);
}
const a = document.createElement("a");
if (this.json.url && this.json.url) {
MarkDown.safeLink(a, this.json.url);
a.textContent = this.json.url;
div.append(a);
}
if (this.json.description) {
const description = document.createElement("p");
description.textContent = this.json.description;
div.append(description);
}
if (this.json.thumbnail) {
const img = document.createElement("img");
if (this.json.thumbnail.width && this.json.thumbnail.width) {
let scale = 1;
const inch = 96;
scale = Math.max(scale, this.json.thumbnail.width / inch / 4);
scale = Math.max(scale, this.json.thumbnail.height / inch / 3);
this.json.thumbnail.width /= scale;
this.json.thumbnail.height /= scale;
img.style.width = this.json.thumbnail.width + "px";
img.style.height = this.json.thumbnail.height + "px";
}
img.classList.add("bigembedimg");
if (this.json.video) {
img.onclick = async () => {
if (this.json.video) {
img.remove();
const iframe = document.createElement("iframe");
iframe.src = this.json.video.url + "?autoplay=1";
if (this.json.thumbnail.width && this.json.thumbnail.width) {
iframe.style.width = this.json.thumbnail.width + "px";
iframe.style.height = this.json.thumbnail.height + "px";
}
div.append(iframe);
}
};
}
else {
img.onclick = async () => {
const full = new Dialog(["img", img.src, ["fit"]]);
full.show();
};
}
img.src = this.json.thumbnail.proxy_url || this.json.thumbnail.url;
div.append(img);
}
colordiv.append(div);
return colordiv;
}
}
export { Embed };

View file

@ -1,205 +0,0 @@
import { Contextmenu } from "./contextmenu.js";
import { Guild } from "./guild.js";
class Emoji {
static emojis;
name;
id;
animated;
owner;
get guild() {
if (this.owner instanceof Guild) {
return this.owner;
}
}
get localuser() {
if (this.owner instanceof Guild) {
return this.owner.localuser;
}
else {
return this.owner;
}
}
get info() {
return this.owner.info;
}
constructor(json, owner) {
this.name = json.name;
this.id = json.id;
this.animated = json.animated;
this.owner = owner;
}
getHTML(bigemoji = false) {
const emojiElem = document.createElement("img");
emojiElem.classList.add("md-emoji");
emojiElem.classList.add(bigemoji ? "bigemoji" : "smallemoji");
emojiElem.crossOrigin = "anonymous";
emojiElem.src = this.info.cdn + "/emojis/" + this.id + "." + (this.animated ? "gif" : "png") + "?size=32";
emojiElem.alt = this.name;
emojiElem.loading = "lazy";
return emojiElem;
}
static decodeEmojiList(buffer) {
const view = new DataView(buffer, 0);
let i = 0;
function read16() {
const int = view.getUint16(i);
i += 2;
return int;
}
function read8() {
const int = view.getUint8(i);
i += 1;
return int;
}
function readString8() {
return readStringNo(read8());
}
function readString16() {
return readStringNo(read16());
}
function readStringNo(length) {
const array = new Uint8Array(length);
for (let i = 0; i < length; i++) {
array[i] = read8();
}
//console.log(array);
return new TextDecoder("utf-8").decode(array.buffer);
}
const build = [];
let cats = read16();
for (; cats !== 0; cats--) {
const name = readString16();
const emojis = [];
let emojinumber = read16();
for (; emojinumber !== 0; emojinumber--) {
//console.log(emojis);
const name = readString8();
const len = read8();
const skin_tone_support = len > 127;
const emoji = readStringNo(len - (Number(skin_tone_support) * 128));
emojis.push({
name,
skin_tone_support,
emoji
});
}
build.push({
name,
emojis
});
}
this.emojis = build;
console.log(build);
}
static grabEmoji() {
fetch("/emoji.bin").then(e => {
return e.arrayBuffer();
}).then(e => {
Emoji.decodeEmojiList(e);
});
}
static async emojiPicker(x, y, localuser) {
let res;
const promise = new Promise(r => {
res = r;
});
const menu = document.createElement("div");
menu.classList.add("flexttb", "emojiPicker");
menu.style.top = y + "px";
menu.style.left = x + "px";
const title = document.createElement("h2");
title.textContent = Emoji.emojis[0].name;
title.classList.add("emojiTitle");
menu.append(title);
const selection = document.createElement("div");
selection.classList.add("flexltr", "dontshrink", "emojirow");
const body = document.createElement("div");
body.classList.add("emojiBody");
let isFirst = true;
localuser.guilds.filter(guild => guild.id != "@me" && guild.emojis.length > 0).forEach(guild => {
const select = document.createElement("div");
select.classList.add("emojiSelect");
if (guild.properties.icon) {
const img = document.createElement("img");
img.classList.add("pfp", "servericon", "emoji-server");
img.crossOrigin = "anonymous";
img.src = localuser.info.cdn + "/icons/" + guild.properties.id + "/" + guild.properties.icon + ".png?size=48";
img.alt = "Server: " + guild.properties.name;
select.appendChild(img);
}
else {
const div = document.createElement("span");
div.textContent = guild.properties.name.replace(/'s /g, " ").replace(/\w+/g, word => word[0]).replace(/\s/g, "");
select.append(div);
}
selection.append(select);
const clickEvent = () => {
title.textContent = guild.properties.name;
body.innerHTML = "";
for (const emojit of guild.emojis) {
const emojiElem = document.createElement("div");
emojiElem.classList.add("emojiSelect");
const emojiClass = new Emoji({
id: emojit.id,
name: emojit.name,
animated: emojit.animated
}, localuser);
emojiElem.append(emojiClass.getHTML());
body.append(emojiElem);
emojiElem.addEventListener("click", () => {
res(emojiClass);
if (Contextmenu.currentmenu !== "") {
Contextmenu.currentmenu.remove();
}
});
}
};
select.addEventListener("click", clickEvent);
if (isFirst) {
clickEvent();
isFirst = false;
}
});
setTimeout(() => {
if (Contextmenu.currentmenu != "") {
Contextmenu.currentmenu.remove();
}
document.body.append(menu);
Contextmenu.currentmenu = menu;
Contextmenu.keepOnScreen(menu);
}, 10);
let i = 0;
for (const thing of Emoji.emojis) {
const select = document.createElement("div");
select.textContent = thing.emojis[0].emoji;
select.classList.add("emojiSelect");
selection.append(select);
const clickEvent = () => {
title.textContent = thing.name;
body.innerHTML = "";
for (const emojit of thing.emojis) {
const emoji = document.createElement("div");
emoji.classList.add("emojiSelect");
emoji.textContent = emojit.emoji;
body.append(emoji);
emoji.onclick = _ => {
res(emojit.emoji);
if (Contextmenu.currentmenu !== "") {
Contextmenu.currentmenu.remove();
}
};
}
};
select.onclick = clickEvent;
if (i === 0) {
clickEvent();
}
i++;
}
menu.append(selection);
menu.append(body);
return promise;
}
}
Emoji.grabEmoji();
export { Emoji };

View file

@ -1,145 +0,0 @@
import { Dialog } from "./dialog.js";
class File {
owner;
id;
filename;
content_type;
width;
height;
proxy_url;
url;
size;
constructor(fileJSON, owner) {
this.owner = owner;
this.id = fileJSON.id;
this.filename = fileJSON.filename;
this.content_type = fileJSON.content_type;
this.width = fileJSON.width;
this.height = fileJSON.height;
this.url = fileJSON.url;
this.proxy_url = fileJSON.proxy_url;
this.content_type = fileJSON.content_type;
this.size = fileJSON.size;
}
getHTML(temp = false) {
const src = this.proxy_url || this.url;
if (this.width && this.height) {
let scale = 1;
const max = 96 * 3;
scale = Math.max(scale, this.width / max);
scale = Math.max(scale, this.height / max);
this.width /= scale;
this.height /= scale;
}
if (this.content_type.startsWith("image/")) {
const div = document.createElement("div");
const img = document.createElement("img");
img.classList.add("messageimg");
div.classList.add("messageimgdiv");
img.onclick = function () {
const full = new Dialog(["img", img.src, ["fit"]]);
full.show();
};
img.src = src;
div.append(img);
if (this.width) {
div.style.width = this.width + "px";
div.style.height = this.height + "px";
}
console.log(img);
console.log(this.width, this.height);
return div;
}
else if (this.content_type.startsWith("video/")) {
const video = document.createElement("video");
const source = document.createElement("source");
source.src = src;
video.append(source);
source.type = this.content_type;
video.controls = !temp;
if (this.width && this.height) {
video.width = this.width;
video.height = this.height;
}
return video;
}
else if (this.content_type.startsWith("audio/")) {
const audio = document.createElement("audio");
const source = document.createElement("source");
source.src = src;
audio.append(source);
source.type = this.content_type;
audio.controls = !temp;
return audio;
}
else {
return this.createunknown();
}
}
upHTML(files, file) {
const div = document.createElement("div");
const contained = this.getHTML(true);
div.classList.add("containedFile");
div.append(contained);
const controls = document.createElement("div");
const garbage = document.createElement("button");
garbage.textContent = "🗑";
garbage.onclick = _ => {
div.remove();
files.splice(files.indexOf(file), 1);
};
controls.classList.add("controls");
div.append(controls);
controls.append(garbage);
return div;
}
static initFromBlob(file) {
return new File({
filename: file.name,
size: file.size,
id: "null",
content_type: file.type,
width: undefined,
height: undefined,
url: URL.createObjectURL(file),
proxy_url: undefined
}, null);
}
createunknown() {
console.log("🗎");
const src = this.proxy_url || this.url;
const div = document.createElement("table");
div.classList.add("unknownfile");
const nametr = document.createElement("tr");
div.append(nametr);
const fileicon = document.createElement("td");
nametr.append(fileicon);
fileicon.append("🗎");
fileicon.classList.add("fileicon");
fileicon.rowSpan = 2;
const nametd = document.createElement("td");
if (src) {
const a = document.createElement("a");
a.href = src;
a.textContent = this.filename;
nametd.append(a);
}
else {
nametd.textContent = this.filename;
}
nametd.classList.add("filename");
nametr.append(nametd);
const sizetr = document.createElement("tr");
const size = document.createElement("td");
sizetr.append(size);
size.textContent = "Size:" + File.filesizehuman(this.size);
size.classList.add("filesize");
div.appendChild(sizetr);
return div;
}
static filesizehuman(fsize) {
const i = fsize == 0 ? 0 : Math.floor(Math.log(fsize) / Math.log(1024));
return Number((fsize / Math.pow(1024, i)).toFixed(2)) * 1 + " " + ["Bytes", "Kilobytes", "Megabytes", "Gigabytes", "Terabytes"][i];
}
}
export { File };

View file

@ -1,604 +0,0 @@
import { Channel } from "./channel.js";
import { Contextmenu } from "./contextmenu.js";
import { Role, RoleList } from "./role.js";
import { Dialog } from "./dialog.js";
import { Member } from "./member.js";
import { Settings } from "./settings.js";
import { SnowFlake } from "./snowflake.js";
import { User } from "./user.js";
class Guild extends SnowFlake {
owner;
headers;
channels;
properties;
member_count;
roles;
roleids;
prevchannel;
message_notifications;
headchannels;
position;
parent_id;
member;
html;
emojis;
large;
static contextmenu = new Contextmenu("guild menu");
static setupcontextmenu() {
Guild.contextmenu.addbutton("Copy Guild id", function () {
navigator.clipboard.writeText(this.id);
});
Guild.contextmenu.addbutton("Mark as read", function () {
this.markAsRead();
});
Guild.contextmenu.addbutton("Notifications", function () {
this.setnotifcation();
});
Guild.contextmenu.addbutton("Leave guild", function () {
this.confirmleave();
}, null, function (_) {
return this.properties.owner_id !== this.member.user.id;
});
Guild.contextmenu.addbutton("Delete guild", function () {
this.confirmDelete();
}, null, function (_) {
return this.properties.owner_id === this.member.user.id;
});
Guild.contextmenu.addbutton("Create invite", function () {
}, null, _ => true, _ => false);
Guild.contextmenu.addbutton("Settings", function () {
this.generateSettings();
});
/* -----things left for later-----
guild.contextmenu.addbutton("Leave Guild",function(){
console.log(this)
this.deleteChannel();
},null,_=>{return thisuser.isAdmin()})
guild.contextmenu.addbutton("Mute Guild",function(){
editchannelf(this);
},null,_=>{return thisuser.isAdmin()})
*/
}
generateSettings() {
const settings = new Settings("Settings for " + this.properties.name);
{
const overview = settings.addButton("Overview");
const form = overview.addForm("", _ => { }, {
headers: this.headers,
traditionalSubmit: true,
fetchURL: this.info.api + "/guilds/" + this.id,
method: "PATCH"
});
form.addTextInput("Name:", "name", { initText: this.properties.name });
form.addMDInput("Description:", "description", { initText: this.properties.description });
form.addFileInput("Banner:", "banner", { clear: true });
form.addFileInput("Icon:", "icon", { clear: true });
let region = this.properties.region;
if (!region) {
region = "";
}
form.addTextInput("Region:", "region", { initText: region });
}
const s1 = settings.addButton("roles");
const permlist = [];
for (const thing of this.roles) {
permlist.push([thing, thing.permissions]);
}
s1.options.push(new RoleList(permlist, this, this.updateRolePermissions.bind(this)));
settings.show();
}
constructor(json, owner, member) {
if (json === -1 || member === null) {
super("@me");
return;
}
if (json.stickers.length) {
console.log(json.stickers, ":3");
}
super(json.id);
this.large = json.large;
this.member_count = json.member_count;
this.emojis = json.emojis;
this.owner = owner;
this.headers = this.owner.headers;
this.channels = [];
this.properties = json.properties;
this.roles = [];
this.roleids = new Map();
this.message_notifications = 0;
for (const roley of json.roles) {
const roleh = new Role(roley, this);
this.roles.push(roleh);
this.roleids.set(roleh.id, roleh);
}
if (member instanceof User) {
Member.resolveMember(member, this).then(_ => {
if (_) {
this.member = _;
}
else {
console.error("Member was unable to resolve");
}
});
}
else {
Member.new(member, this).then(_ => {
if (_) {
this.member = _;
}
});
}
this.perminfo ??= { channels: {} };
for (const thing of json.channels) {
const temp = new Channel(thing, this);
this.channels.push(temp);
this.localuser.channelids.set(temp.id, temp);
}
this.headchannels = [];
for (const thing of this.channels) {
const parent = thing.resolveparent(this);
if (!parent) {
this.headchannels.push(thing);
}
}
this.prevchannel = this.localuser.channelids.get(this.perminfo.prevchannel);
}
get perminfo() {
return this.localuser.perminfo.guilds[this.id];
}
set perminfo(e) {
this.localuser.perminfo.guilds[this.id] = e;
}
notisetting(settings) {
this.message_notifications = settings.message_notifications;
}
setnotifcation() {
let noti = this.message_notifications;
const notiselect = new Dialog(["vdiv",
["radio", "select notifications type",
["all", "only mentions", "none"],
function (e) {
noti = ["all", "only mentions", "none"].indexOf(e);
},
noti],
["button", "", "submit", _ => {
//
fetch(this.info.api + `/users/@me/guilds/${this.id}/settings/`, {
method: "PATCH",
headers: this.headers,
body: JSON.stringify({
message_notifications: noti
})
});
this.message_notifications = noti;
}]]);
notiselect.show();
}
confirmleave() {
const full = new Dialog([
"vdiv",
["title",
"Are you sure you want to leave?"
],
["hdiv",
["button",
"",
"Yes, I'm sure",
_ => {
this.leave().then(_ => {
full.hide();
});
}
],
["button",
"",
"Nevermind",
_ => {
full.hide();
}
]
]
]);
full.show();
}
async leave() {
return fetch(this.info.api + "/users/@me/guilds/" + this.id, {
method: "DELETE",
headers: this.headers
});
}
printServers() {
let build = "";
for (const thing of this.headchannels) {
build += (thing.name + ":" + thing.position) + "\n";
for (const thingy of thing.children) {
build += (" " + thingy.name + ":" + thingy.position) + "\n";
}
}
console.log(build);
}
calculateReorder() {
let position = -1;
const build = [];
for (const thing of this.headchannels) {
const thisthing = { id: thing.id, position: undefined, parent_id: undefined };
if (thing.position <= position) {
thing.position = (thisthing.position = position + 1);
}
position = thing.position;
console.log(position);
if (thing.move_id && thing.move_id !== thing.parent_id) {
thing.parent_id = thing.move_id;
thisthing.parent_id = thing.parent?.id;
thing.move_id = undefined;
}
if (thisthing.position || thisthing.parent_id) {
build.push(thisthing);
}
if (thing.children.length > 0) {
const things = thing.calculateReorder();
for (const thing of things) {
build.push(thing);
}
}
}
console.log(build);
this.printServers();
if (build.length === 0) {
return;
}
const serverbug = false;
if (serverbug) {
for (const thing of build) {
console.log(build, thing);
fetch(this.info.api + "/guilds/" + this.id + "/channels", {
method: "PATCH",
headers: this.headers,
body: JSON.stringify([thing])
});
}
}
else {
fetch(this.info.api + "/guilds/" + this.id + "/channels", {
method: "PATCH",
headers: this.headers,
body: JSON.stringify(build)
});
}
}
get localuser() {
return this.owner;
}
get info() {
return this.owner.info;
}
sortchannels() {
this.headchannels.sort((a, b) => {
return a.position - b.position;
});
}
static generateGuildIcon(guild) {
const divy = document.createElement("div");
divy.classList.add("servernoti");
const noti = document.createElement("div");
noti.classList.add("unread");
divy.append(noti);
if (guild instanceof Guild) {
guild.localuser.guildhtml.set(guild.id, divy);
}
let icon;
if (guild instanceof Guild) {
icon = guild.properties.icon;
}
else {
icon = guild.icon;
}
if (icon !== null) {
const img = document.createElement("img");
img.classList.add("pfp", "servericon");
img.src = guild.info.cdn + "/icons/" + guild.id + "/" + icon + ".png";
divy.appendChild(img);
if (guild instanceof Guild) {
img.onclick = () => {
console.log(guild.loadGuild);
guild.loadGuild();
guild.loadChannel();
};
Guild.contextmenu.bindContextmenu(img, guild, undefined);
}
}
else {
const div = document.createElement("div");
let name;
if (guild instanceof Guild) {
name = guild.properties.name;
}
else {
name = guild.name;
}
const build = name.replace(/'s /g, " ").replace(/\w+/g, word => word[0]).replace(/\s/g, "");
div.textContent = build;
div.classList.add("blankserver", "servericon");
divy.appendChild(div);
if (guild instanceof Guild) {
div.onclick = () => {
guild.loadGuild();
guild.loadChannel();
};
Guild.contextmenu.bindContextmenu(div, guild, undefined);
}
}
return divy;
}
generateGuildIcon() {
return Guild.generateGuildIcon(this);
}
confirmDelete() {
let confirmname = "";
const full = new Dialog([
"vdiv",
["title",
"Are you sure you want to delete " + this.properties.name + "?"
],
["textbox",
"Name of server:",
"",
function () {
confirmname = this.value;
}
],
["hdiv",
["button",
"",
"Yes, I'm sure",
_ => {
console.log(confirmname);
if (confirmname !== this.properties.name) {
return;
}
this.delete().then(_ => {
full.hide();
});
}
],
["button",
"",
"Nevermind",
_ => {
full.hide();
}
]
]
]);
full.show();
}
async delete() {
return fetch(this.info.api + "/guilds/" + this.id + "/delete", {
method: "POST",
headers: this.headers,
});
}
unreads(html) {
if (html) {
this.html = html;
}
else {
html = this.html;
}
let read = true;
for (const thing of this.channels) {
if (thing.hasunreads) {
console.log(thing);
read = false;
break;
}
}
if (!html) {
return;
}
if (read) {
html.children[0].classList.remove("notiunread");
}
else {
html.children[0].classList.add("notiunread");
}
}
getHTML() {
//this.printServers();
this.sortchannels();
this.printServers();
const build = document.createElement("div");
for (const thing of this.headchannels) {
build.appendChild(thing.createguildHTML(this.isAdmin()));
}
return build;
}
isAdmin() {
return this.member.isAdmin();
}
async markAsRead() {
const build = { read_states: [] };
for (const thing of this.channels) {
if (thing.hasunreads) {
build.read_states.push({ channel_id: thing.id, message_id: thing.lastmessageid, read_state_type: 0 });
thing.lastreadmessageid = thing.lastmessageid;
if (!thing.myhtml)
continue;
thing.myhtml.classList.remove("cunread");
}
}
this.unreads();
fetch(this.info.api + "/read-states/ack-bulk", {
method: "POST",
headers: this.headers,
body: JSON.stringify(build)
});
}
hasRole(r) {
console.log("this should run");
if (r instanceof Role) {
r = r.id;
}
return this.member.hasRole(r);
}
loadChannel(ID) {
if (ID) {
const channel = this.localuser.channelids.get(ID);
if (channel) {
channel.getHTML();
return;
}
}
if (this.prevchannel) {
console.log(this.prevchannel);
this.prevchannel.getHTML();
return;
}
for (const thing of this.channels) {
if (thing.children.length === 0) {
thing.getHTML();
return;
}
}
}
loadGuild() {
this.localuser.loadGuild(this.id);
}
updateChannel(json) {
const channel = this.localuser.channelids.get(json.id);
if (channel) {
channel.updateChannel(json);
this.headchannels = [];
for (const thing of this.channels) {
thing.children = [];
}
this.headchannels = [];
for (const thing of this.channels) {
const parent = thing.resolveparent(this);
if (!parent) {
this.headchannels.push(thing);
}
}
this.printServers();
}
}
createChannelpac(json) {
const thischannel = new Channel(json, this);
this.localuser.channelids.set(json.id, thischannel);
this.channels.push(thischannel);
thischannel.resolveparent(this);
if (!thischannel.parent) {
this.headchannels.push(thischannel);
}
this.calculateReorder();
this.printServers();
return thischannel;
}
createchannels(func = this.createChannel) {
let name = "";
let category = 0;
const channelselect = new Dialog(["vdiv",
["radio", "select channel type",
["voice", "text", "announcement"],
function (e) {
console.log(e);
category = { text: 0, voice: 2, announcement: 5, category: 4 }[e];
},
1
],
["textbox", "Name of channel", "", function () {
name = this.value;
}],
["button", "", "submit", function () {
console.log(name, category);
func(name, category);
channelselect.hide();
}]]);
channelselect.show();
}
createcategory() {
let name = "";
const category = 4;
const channelselect = new Dialog(["vdiv",
["textbox", "Name of category", "", function () {
name = this.value;
}],
["button", "", "submit", () => {
console.log(name, category);
this.createChannel(name, category);
channelselect.hide();
}]]);
channelselect.show();
}
delChannel(json) {
const channel = this.localuser.channelids.get(json.id);
this.localuser.channelids.delete(json.id);
if (!channel)
return;
this.channels.splice(this.channels.indexOf(channel), 1);
const indexy = this.headchannels.indexOf(channel);
if (indexy !== -1) {
this.headchannels.splice(indexy, 1);
}
/*
const build=[];
for(const thing of this.channels){
console.log(thing.id);
if(thing!==channel){
build.push(thing)
}else{
console.log("fail");
if(thing.parent){
thing.parent.delChannel(json);
}
}
}
this.channels=build;
*/
this.printServers();
}
createChannel(name, type) {
fetch(this.info.api + "/guilds/" + this.id + "/channels", {
method: "POST",
headers: this.headers,
body: JSON.stringify({ name, type })
});
}
async createRole(name) {
const fetched = await fetch(this.info.api + "/guilds/" + this.id + "roles", {
method: "POST",
headers: this.headers,
body: JSON.stringify({
name,
color: 0,
permissions: "0"
})
});
const json = await fetched.json();
const role = new Role(json, this);
this.roleids.set(role.id, role);
this.roles.push(role);
return role;
}
async updateRolePermissions(id, perms) {
const role = this.roleids[id];
role.permissions.allow = perms.allow;
role.permissions.deny = perms.deny;
await fetch(this.info.api + "/guilds/" + this.id + "/roles/" + role.id, {
method: "PATCH",
headers: this.headers,
body: JSON.stringify({
color: role.color,
hoist: role.hoist,
icon: role.icon,
mentionable: role.mentionable,
name: role.name,
permissions: role.permissions.allow.toString(),
unicode_emoji: role.unicode_emoji,
})
});
}
}
Guild.setupcontextmenu();
export { Guild };

View file

@ -1,62 +0,0 @@
import { mobile } from "./login.js";
console.log(mobile);
const serverbox = document.getElementById("instancebox");
fetch("/instances.json").then(_ => _.json()).then((json) => {
console.warn(json);
for (const instance of json) {
if (instance.display === false) {
continue;
}
const div = document.createElement("div");
div.classList.add("flexltr", "instance");
if (instance.image) {
const img = document.createElement("img");
img.src = instance.image;
div.append(img);
}
const statbox = document.createElement("div");
statbox.classList.add("flexttb");
{
const textbox = document.createElement("div");
textbox.classList.add("flexttb", "instatancetextbox");
const title = document.createElement("h2");
title.innerText = instance.name;
if (instance.online !== undefined) {
const status = document.createElement("span");
status.innerText = instance.online ? "Online" : "Offline";
status.classList.add("instanceStatus");
title.append(status);
}
textbox.append(title);
if (instance.description || instance.descriptionLong) {
const p = document.createElement("p");
if (instance.descriptionLong) {
p.innerText = instance.descriptionLong;
}
else if (instance.description) {
p.innerText = instance.description;
}
textbox.append(p);
}
statbox.append(textbox);
}
if (instance.uptime) {
const stats = document.createElement("div");
stats.classList.add("flexltr");
const span = document.createElement("span");
span.innerText = `Uptime: All time: ${Math.round(instance.uptime.alltime * 100)}% This week: ${Math.round(instance.uptime.weektime * 100)}% Today: ${Math.round(instance.uptime.daytime * 100)}%`;
stats.append(span);
statbox.append(stats);
}
div.append(statbox);
div.onclick = _ => {
if (instance.online) {
window.location.href = "/register.html?instance=" + encodeURI(instance.name);
}
else {
alert("Instance is offline, can't connect");
}
};
serverbox.append(div);
}
});

View file

@ -1,212 +0,0 @@
import { Localuser } from "./localuser.js";
import { Contextmenu } from "./contextmenu.js";
import { mobile, getBulkUsers, setTheme } from "./login.js";
import { MarkDown } from "./markdown.js";
import { File } from "./file.js";
(async () => {
async function waitforload() {
let res;
new Promise(r => {
res = r;
});
document.addEventListener("DOMContentLoaded", () => {
res();
});
await res;
}
await waitforload();
const users = getBulkUsers();
if (!users.currentuser) {
window.location.href = "/login.html";
}
function showAccountSwitcher() {
const table = document.createElement("div");
for (const thing of Object.values(users.users)) {
const specialuser = thing;
console.log(specialuser.pfpsrc);
const userinfo = document.createElement("div");
userinfo.classList.add("flexltr", "switchtable");
const pfp = document.createElement("img");
userinfo.append(pfp);
const user = document.createElement("div");
userinfo.append(user);
user.append(specialuser.username);
user.append(document.createElement("br"));
const span = document.createElement("span");
span.textContent = specialuser.serverurls.wellknown.replace("https://", "").replace("http://", "");
user.append(span);
user.classList.add("userinfo");
span.classList.add("serverURL");
pfp.src = specialuser.pfpsrc;
pfp.classList.add("pfp");
table.append(userinfo);
userinfo.addEventListener("click", _ => {
thisuser.unload();
thisuser.swapped = true;
const loading = document.getElementById("loading");
loading.classList.remove("doneloading");
loading.classList.add("loading");
thisuser = new Localuser(specialuser);
users.currentuser = specialuser.uid;
localStorage.setItem("userinfos", JSON.stringify(users));
thisuser.initwebsocket().then(_ => {
thisuser.loaduser();
thisuser.init();
loading.classList.add("doneloading");
loading.classList.remove("loading");
console.log("done loading");
});
userinfo.remove();
});
}
{
const td = document.createElement("div");
td.classList.add("switchtable");
td.append("Switch accounts ⇌");
td.addEventListener("click", _ => {
window.location.href = "/login.html";
});
table.append(td);
}
table.classList.add("accountSwitcher");
if (Contextmenu.currentmenu != "") {
Contextmenu.currentmenu.remove();
}
Contextmenu.currentmenu = table;
console.log(table);
document.body.append(table);
}
{
const userinfo = document.getElementById("userinfo");
userinfo.addEventListener("click", _ => {
_.stopImmediatePropagation();
showAccountSwitcher();
});
const switchaccounts = document.getElementById("switchaccounts");
switchaccounts.addEventListener("click", _ => {
_.stopImmediatePropagation();
showAccountSwitcher();
});
console.log("this ran");
}
let thisuser;
try {
console.log(users.users, users.currentuser);
thisuser = new Localuser(users.users[users.currentuser]);
thisuser.initwebsocket().then(_ => {
thisuser.loaduser();
thisuser.init();
const loading = document.getElementById("loading");
loading.classList.add("doneloading");
loading.classList.remove("loading");
console.log("done loading");
});
}
catch (e) {
console.error(e);
document.getElementById("load-desc").textContent = "Account unable to start";
thisuser = new Localuser(-1);
}
{
const menu = new Contextmenu("create rightclick"); //Really should go into the localuser class, but that's a later thing
menu.addbutton("Create channel", () => {
if (thisuser.lookingguild) {
thisuser.lookingguild.createchannels();
}
}, null, _ => {
return thisuser.isAdmin();
});
menu.addbutton("Create category", () => {
if (thisuser.lookingguild) {
thisuser.lookingguild.createcategory();
}
}, null, _ => {
return thisuser.isAdmin();
});
menu.bindContextmenu(document.getElementById("channels"), 0, 0);
}
const pasteimage = document.getElementById("pasteimage");
let replyingto = null;
async function enter(event) {
const channel = thisuser.channelfocus;
if (!channel || !thisuser.channelfocus)
return;
channel.typingstart();
if (event.key === "Enter" && !event.shiftKey) {
event.preventDefault();
if (channel.editing) {
channel.editing.edit(markdown.rawString);
channel.editing = null;
}
else {
replyingto = thisuser.channelfocus.replyingto;
const replying = replyingto;
if (replyingto?.div) {
replyingto.div.classList.remove("replying");
}
thisuser.channelfocus.replyingto = null;
channel.sendMessage(markdown.rawString, {
attachments: images,
embeds: [],
replyingto: replying
});
thisuser.channelfocus.makereplybox();
}
while (images.length != 0) {
images.pop();
pasteimage.removeChild(imageshtml.pop());
}
typebox.innerHTML = "";
}
}
const typebox = document.getElementById("typebox");
const markdown = new MarkDown("", thisuser);
markdown.giveBox(typebox);
typebox["markdown"] = markdown;
typebox.addEventListener("keyup", enter);
typebox.addEventListener("keydown", event => {
if (event.key === "Enter" && !event.shiftKey)
event.preventDefault();
});
console.log(typebox);
typebox.onclick = console.log;
/*
function getguildinfo(){
const path=window.location.pathname.split("/");
const channel=path[3];
this.ws.send(JSON.stringify({op: 14, d: {guild_id: path[2], channels: {[channel]: [[0, 99]]}}}));
}
*/
const images = [];
const imageshtml = [];
document.addEventListener("paste", async (e) => {
if (!e.clipboardData)
return;
Array.from(e.clipboardData.files).forEach(async (f) => {
const file = File.initFromBlob(f);
e.preventDefault();
const html = file.upHTML(images, f);
pasteimage.appendChild(html);
images.push(f);
imageshtml.push(html);
});
});
setTheme();
function userSettings() {
thisuser.showusersettings();
}
document.getElementById("settings").onclick = userSettings;
if (mobile) {
document.getElementById("channelw").onclick = () => {
document.getElementById("channels").parentNode.classList.add("collapse");
document.getElementById("servertd").classList.add("collapse");
document.getElementById("servers").classList.add("collapse");
};
document.getElementById("mobileback").textContent = "#";
document.getElementById("mobileback").onclick = () => {
document.getElementById("channels").parentNode.classList.remove("collapse");
document.getElementById("servertd").classList.remove("collapse");
document.getElementById("servers").classList.remove("collapse");
};
}
})();

View file

@ -1,311 +0,0 @@
class InfiniteScroller {
getIDFromOffset;
getHTMLFromID;
destroyFromID;
reachesBottom;
minDist = 2000;
fillDist = 3000;
maxDist = 6000;
HTMLElements = [];
div;
constructor(getIDFromOffset, getHTMLFromID, destroyFromID, reachesBottom = () => { }) {
this.getIDFromOffset = getIDFromOffset;
this.getHTMLFromID = getHTMLFromID;
this.destroyFromID = destroyFromID;
this.reachesBottom = reachesBottom;
}
timeout;
async getDiv(initialId, bottom = true) {
//div.classList.add("flexttb")
if (this.div) {
throw new Error("Div already exists, exiting.");
}
const scroll = document.createElement("div");
scroll.classList.add("flexttb", "scroller");
this.beenloaded = false;
//this.interval=setInterval(this.updatestuff.bind(this,true),100);
this.div = scroll;
this.div.addEventListener("scroll", _ => {
this.checkscroll();
if (this.scrollBottom < 5) {
this.scrollBottom = 5;
}
if (this.timeout === null) {
this.timeout = setTimeout(this.updatestuff.bind(this), 300);
}
this.watchForChange();
});
{
let oldheight = 0;
new ResizeObserver(_ => {
this.checkscroll();
const func = this.snapBottom();
this.updatestuff();
const change = oldheight - scroll.offsetHeight;
if (change > 0 && this.div) {
this.div.scrollTop += change;
}
oldheight = scroll.offsetHeight;
this.watchForChange();
func();
}).observe(scroll);
}
new ResizeObserver(this.watchForChange.bind(this)).observe(scroll);
await this.firstElement(initialId);
this.updatestuff();
await this.watchForChange().then(_ => {
this.updatestuff();
this.beenloaded = true;
});
return scroll;
}
beenloaded = false;
scrollBottom;
scrollTop;
needsupdate = true;
averageheight = 60;
checkscroll() {
if (this.beenloaded && this.div && !document.body.contains(this.div)) {
console.warn("not in document");
this.div = null;
}
}
async updatestuff() {
this.timeout = null;
if (!this.div)
return;
this.scrollBottom = this.div.scrollHeight - this.div.scrollTop - this.div.clientHeight;
this.averageheight = this.div.scrollHeight / this.HTMLElements.length;
if (this.averageheight < 10) {
this.averageheight = 60;
}
this.scrollTop = this.div.scrollTop;
if (!this.scrollBottom && !await this.watchForChange()) {
this.reachesBottom();
}
if (!this.scrollTop) {
await this.watchForChange();
}
this.needsupdate = false;
//this.watchForChange();
}
async firstElement(id) {
if (!this.div)
return;
const html = await this.getHTMLFromID(id);
this.div.appendChild(html);
this.HTMLElements.push([html, id]);
}
async addedBottom() {
await this.updatestuff();
const func = this.snapBottom();
await this.watchForChange();
func();
}
snapBottom() {
const scrollBottom = this.scrollBottom;
return () => {
if (this.div && scrollBottom < 4) {
this.div.scrollTop = this.div.scrollHeight;
}
};
}
async watchForTop(already = false, fragement = new DocumentFragment()) {
if (!this.div)
return false;
try {
let again = false;
if (this.scrollTop < (already ? this.fillDist : this.minDist)) {
let nextid;
const firstelm = this.HTMLElements.at(0);
if (firstelm) {
const previd = firstelm[1];
nextid = await this.getIDFromOffset(previd, 1);
}
if (!nextid) {
}
else {
const html = await this.getHTMLFromID(nextid);
if (!html) {
this.destroyFromID(nextid);
return false;
}
again = true;
fragement.prepend(html);
this.HTMLElements.unshift([html, nextid]);
this.scrollTop += this.averageheight;
}
}
if (this.scrollTop > this.maxDist) {
const html = this.HTMLElements.shift();
if (html) {
again = true;
await this.destroyFromID(html[1]);
this.scrollTop -= this.averageheight;
}
}
if (again) {
await this.watchForTop(true, fragement);
}
return again;
}
finally {
if (!already) {
if (this.div.scrollTop === 0) {
this.scrollTop = 1;
this.div.scrollTop = 10;
}
this.div.prepend(fragement, fragement);
}
}
}
async watchForBottom(already = false, fragement = new DocumentFragment()) {
let func;
if (!already)
func = this.snapBottom();
if (!this.div)
return false;
try {
let again = false;
const scrollBottom = this.scrollBottom;
if (scrollBottom < (already ? this.fillDist : this.minDist)) {
let nextid;
const lastelm = this.HTMLElements.at(-1);
if (lastelm) {
const previd = lastelm[1];
nextid = await this.getIDFromOffset(previd, -1);
}
if (!nextid) {
}
else {
again = true;
const html = await this.getHTMLFromID(nextid);
fragement.appendChild(html);
this.HTMLElements.push([html, nextid]);
this.scrollBottom += this.averageheight;
}
}
if (scrollBottom > this.maxDist) {
const html = this.HTMLElements.pop();
if (html) {
await this.destroyFromID(html[1]);
this.scrollBottom -= this.averageheight;
again = true;
}
}
if (again) {
await this.watchForBottom(true, fragement);
}
return again;
}
finally {
if (!already) {
this.div.append(fragement);
if (func) {
func();
}
}
}
}
watchtime = false;
changePromise;
async watchForChange() {
if (this.changePromise) {
this.watchtime = true;
return await this.changePromise;
}
else {
this.watchtime = false;
}
this.changePromise = new Promise(async (res) => {
try {
try {
if (!this.div) {
res(false);
return false;
}
const out = await Promise.allSettled([this.watchForTop(), this.watchForBottom()]);
const changed = (out[0].value || out[1].value);
if (this.timeout === null && changed) {
this.timeout = setTimeout(this.updatestuff.bind(this), 300);
}
if (!this.changePromise) {
console.error("something really bad happened");
}
res(Boolean(changed));
return Boolean(changed);
}
catch (e) {
console.error(e);
}
res(false);
return false;
}
catch (e) {
throw e;
}
finally {
setTimeout(_ => {
this.changePromise = undefined;
if (this.watchtime) {
this.watchForChange();
}
}, 300);
}
});
return await this.changePromise;
}
async focus(id, flash = true) {
let element;
for (const thing of this.HTMLElements) {
if (thing[1] === id) {
element = thing[0];
}
}
if (element) {
if (flash) {
element.scrollIntoView({
behavior: "smooth",
block: "center"
});
await new Promise(resolve => setTimeout(resolve, 1000));
element.classList.remove("jumped");
await new Promise(resolve => setTimeout(resolve, 100));
element.classList.add("jumped");
}
else {
element.scrollIntoView();
}
}
else {
for (const thing of this.HTMLElements) {
await this.destroyFromID(thing[1]);
}
this.HTMLElements = [];
await this.firstElement(id);
this.updatestuff();
await this.watchForChange();
await new Promise(resolve => setTimeout(resolve, 100));
await this.focus(id, true);
}
}
async delete() {
if (this.div) {
this.div.remove();
this.div = null;
}
try {
for (const thing of this.HTMLElements) {
await this.destroyFromID(thing[1]);
}
}
catch (e) {
console.error(e);
}
this.HTMLElements = [];
if (this.timeout) {
clearTimeout(this.timeout);
}
}
}
export { InfiniteScroller };

View file

@ -1,118 +0,0 @@
import { getBulkUsers, getapiurls } from "./login.js";
(async () => {
const users = getBulkUsers();
const well = new URLSearchParams(window.location.search).get("instance");
const joinable = [];
for (const thing in users.users) {
const user = users.users[thing];
if (user.serverurls.wellknown.includes(well)) {
joinable.push(user);
}
console.log(users.users[thing]);
}
let urls;
if (!joinable.length && well) {
const out = await getapiurls(well);
if (out) {
urls = out;
for (const thing in users.users) {
const user = users.users[thing];
if (user.serverurls.api.includes(out.api)) {
joinable.push(user);
}
console.log(users.users[thing]);
}
}
else {
throw new Error("someone needs to handle the case where the servers don't exist");
}
}
else {
urls = joinable[0].serverurls;
}
if (!joinable.length) {
document.getElementById("AcceptInvite").textContent = "Create an account to accept the invite";
}
const code = window.location.pathname.split("/")[2];
let guildinfo;
fetch(`${urls.api}/invites/${code}`, {
method: "GET"
}).then(_ => _.json()).then(json => {
const guildjson = json.guild;
guildinfo = guildjson;
document.getElementById("invitename").textContent = guildjson.name;
document.getElementById("invitedescription").textContent =
`${json.inviter.username} invited you to join ${guildjson.name}`;
if (guildjson.icon) {
const img = document.createElement("img");
img.src = `${urls.cdn}/icons/${guildjson.id}/${guildjson.icon}.png`;
img.classList.add("inviteGuild");
document.getElementById("inviteimg").append(img);
}
else {
const txt = guildjson.name.replace(/'s /g, " ").replace(/\w+/g, word => word[0]).replace(/\s/g, "");
const div = document.createElement("div");
div.textContent = txt;
div.classList.add("inviteGuild");
document.getElementById("inviteimg").append(div);
}
});
function showAccounts() {
const table = document.createElement("dialog");
for (const thing of Object.values(joinable)) {
const specialuser = thing;
console.log(specialuser.pfpsrc);
const userinfo = document.createElement("div");
userinfo.classList.add("flexltr", "switchtable");
const pfp = document.createElement("img");
userinfo.append(pfp);
const user = document.createElement("div");
userinfo.append(user);
user.append(specialuser.username);
user.append(document.createElement("br"));
const span = document.createElement("span");
span.textContent = specialuser.serverurls.wellknown.replace("https://", "").replace("http://", "");
user.append(span);
user.classList.add("userinfo");
span.classList.add("serverURL");
pfp.src = specialuser.pfpsrc;
pfp.classList.add("pfp");
table.append(userinfo);
userinfo.addEventListener("click", _ => {
console.log(thing);
fetch(`${urls.api}/invites/${code}`, {
method: "POST",
headers: {
Authorization: thing.token
}
}).then(_ => {
users.currentuser = specialuser.uid;
localStorage.setItem("userinfos", JSON.stringify(users));
window.location.href = "/channels/" + guildinfo.id;
});
});
}
{
const td = document.createElement("div");
td.classList.add("switchtable");
td.append("Login or create an account ⇌");
td.addEventListener("click", _ => {
const l = new URLSearchParams("?");
l.set("goback", window.location.href);
l.set("instance", well);
window.location.href = "/login?" + l.toString();
});
if (!joinable.length) {
const l = new URLSearchParams("?");
l.set("goback", window.location.href);
l.set("instance", well);
window.location.href = "/login?" + l.toString();
}
table.append(td);
}
table.classList.add("accountSwitcher");
console.log(table);
document.body.append(table);
}
document.getElementById("AcceptInvite").addEventListener("click", showAccounts);
})();

View file

@ -1 +0,0 @@
export {};

File diff suppressed because it is too large Load diff

View file

@ -1,461 +0,0 @@
import { Dialog } from "./dialog.js";
const mobile = /iPhone|iPad|iPod|Android/i.test(navigator.userAgent);
function setTheme() {
let name = localStorage.getItem("theme");
if (!name) {
localStorage.setItem("theme", "Dark");
name = "Dark";
}
document.body.className = name + "-theme";
}
let instances;
setTheme();
function getBulkUsers() {
const json = getBulkInfo();
for (const thing in json.users) {
json.users[thing] = new Specialuser(json.users[thing]);
}
return json;
}
function trimswitcher() {
const json = getBulkInfo();
const map = new Map();
for (const thing in json.users) {
const user = json.users[thing];
let wellknown = user.serverurls.wellknown;
if (wellknown.at(-1) !== "/") {
wellknown += "/";
}
wellknown += user.username;
if (map.has(wellknown)) {
const otheruser = map.get(wellknown);
if (otheruser[1].serverurls.wellknown.at(-1) === "/") {
delete json.users[otheruser[0]];
map.set(wellknown, [thing, user]);
}
else {
delete json.users[thing];
}
}
else {
map.set(wellknown, [thing, user]);
}
}
for (const thing in json.users) {
if (thing.at(-1) === "/") {
const user = json.users[thing];
delete json.users[thing];
json.users[thing.slice(0, -1)] = user;
}
}
localStorage.setItem("userinfos", JSON.stringify(json));
console.log(json);
}
function getBulkInfo() {
return JSON.parse(localStorage.getItem("userinfos"));
}
function setDefaults() {
let userinfos = getBulkInfo();
if (!userinfos) {
localStorage.setItem("userinfos", JSON.stringify({
currentuser: null,
users: {},
preferences: {
theme: "Dark",
notifications: false,
notisound: "three",
},
}));
userinfos = getBulkInfo();
}
if (userinfos.users === undefined) {
userinfos.users = {};
}
if (userinfos.accent_color === undefined) {
userinfos.accent_color = "#242443";
}
document.documentElement.style.setProperty("--accent-color", userinfos.accent_color);
if (userinfos.preferences === undefined) {
userinfos.preferences = {
theme: "Dark",
notifications: false,
notisound: "three",
};
}
if (userinfos.preferences && (userinfos.preferences.notisound === undefined)) {
userinfos.preferences.notisound = "three";
}
localStorage.setItem("userinfos", JSON.stringify(userinfos));
}
setDefaults();
class Specialuser {
serverurls;
email;
token;
loggedin;
json;
constructor(json) {
if (json instanceof Specialuser) {
console.error("specialuser can't construct from another specialuser");
}
this.serverurls = json.serverurls;
let apistring = new URL(json.serverurls.api).toString();
apistring = apistring.replace(/\/(v\d+\/?)?$/, "") + "/v9";
this.serverurls.api = apistring;
this.serverurls.cdn = new URL(json.serverurls.cdn).toString().replace(/\/$/, "");
this.serverurls.gateway = new URL(json.serverurls.gateway).toString().replace(/\/$/, "");
this.serverurls.wellknown = new URL(json.serverurls.wellknown).toString().replace(/\/$/, "");
this.serverurls.login = new URL(json.serverurls.login).toString().replace(/\/$/, "");
this.email = json.email;
this.token = json.token;
this.loggedin = json.loggedin;
this.json = json;
this.json.localuserStore ??= {};
if (!this.serverurls || !this.email || !this.token) {
console.error("There are fundamentally missing pieces of info missing from this user");
}
}
set pfpsrc(e) {
this.json.pfpsrc = e;
this.updateLocal();
}
get pfpsrc() {
return this.json.pfpsrc;
}
set username(e) {
this.json.username = e;
this.updateLocal();
}
get username() {
return this.json.username;
}
set localuserStore(e) {
this.json.localuserStore = e;
this.updateLocal();
}
get localuserStore() {
return this.json.localuserStore;
}
get uid() {
return this.email + this.serverurls.wellknown;
}
toJSON() {
return this.json;
}
updateLocal() {
const info = getBulkInfo();
info.users[this.uid] = this.toJSON();
localStorage.setItem("userinfos", JSON.stringify(info));
}
}
function adduser(user) {
user = new Specialuser(user);
const info = getBulkInfo();
info.users[user.uid] = user;
info.currentuser = user.uid;
localStorage.setItem("userinfos", JSON.stringify(info));
return user;
}
const instancein = document.getElementById("instancein");
let timeout;
let instanceinfo;
const stringURLMap = new Map();
const stringURLsMap = new Map();
async function getapiurls(str) {
if (!URL.canParse(str)) {
const val = stringURLMap.get(str);
if (val) {
str = val;
}
else {
const val = stringURLsMap.get(str);
if (val) {
const responce = await fetch(val.api + val.api.endsWith("/") ? "" : "/" + "ping");
if (responce.ok) {
if (val.login) {
return val;
}
else {
val.login = val.api;
return val;
}
}
}
}
}
if (str.at(-1) !== "/") {
str += "/";
}
let api;
try {
const info = await fetch(`${str}/.well-known/spacebar`).then(x => x.json());
api = info.api;
}
catch {
return false;
}
const url = new URL(api);
try {
const info = await fetch(`${api}${url.pathname.includes("api") ? "" : "api"}/policies/instance/domains`).then(x => x.json());
return {
api: info.apiEndpoint,
gateway: info.gateway,
cdn: info.cdn,
wellknown: str,
login: url.toString()
};
}
catch {
const val = stringURLsMap.get(str);
if (val) {
const responce = await fetch(val.api + val.api.endsWith("/") ? "" : "/" + "ping");
if (responce.ok) {
if (val.login) {
return val;
}
else {
val.login = val.api;
return val;
}
}
}
return false;
}
}
async function checkInstance(e) {
const verify = document.getElementById("verify");
try {
verify.textContent = "Checking Instance";
const instanceinfo = await getapiurls(instancein.value);
if (instanceinfo) {
instanceinfo.value = instancein.value;
localStorage.setItem("instanceinfo", JSON.stringify(instanceinfo));
verify.textContent = "Instance is all good";
if (checkInstance.alt) {
checkInstance.alt();
}
setTimeout(_ => {
console.log(verify.textContent);
verify.textContent = "";
}, 3000);
}
else {
verify.textContent = "Invalid Instance, try again";
}
}
catch {
console.log("catch");
verify.textContent = "Invalid Instance, try again";
}
}
if (instancein) {
console.log(instancein);
instancein.addEventListener("keydown", e => {
const verify = document.getElementById("verify");
verify.textContent = "Waiting to check Instance";
clearTimeout(timeout);
timeout = setTimeout(checkInstance, 1000);
});
if (localStorage.getItem("instanceinfo")) {
const json = JSON.parse(localStorage.getItem("instanceinfo"));
if (json.value) {
instancein.value = json.value;
}
else {
instancein.value = json.wellknown;
}
}
else {
checkInstance("https://spacebar.chat/");
}
}
async function login(username, password, captcha) {
if (captcha === "") {
captcha = undefined;
}
const options = {
method: "POST",
body: JSON.stringify({
login: username,
password,
undelete: false,
captcha_key: captcha
}),
headers: {
"Content-type": "application/json; charset=UTF-8",
}
};
try {
const info = JSON.parse(localStorage.getItem("instanceinfo"));
const api = info.login + (info.login.startsWith("/") ? "/" : "");
return await fetch(api + "/auth/login", options).then(response => response.json())
.then(response => {
console.log(response, response.message);
if (response.message === "Invalid Form Body") {
return response.errors.login._errors[0].message;
console.log("test");
}
//this.serverurls||!this.email||!this.token
console.log(response);
if (response.captcha_sitekey) {
const capt = document.getElementById("h-captcha");
if (!capt.children.length) {
const capty = document.createElement("div");
capty.classList.add("h-captcha");
capty.setAttribute("data-sitekey", response.captcha_sitekey);
const script = document.createElement("script");
script.src = "https://js.hcaptcha.com/1/api.js";
capt.append(script);
capt.append(capty);
}
else {
eval("hcaptcha.reset()");
}
}
else {
console.log(response);
if (response.ticket) {
let onetimecode = "";
new Dialog(["vdiv", ["title", "2FA code:"], ["textbox", "", "", function () {
onetimecode = this.value;
}], ["button", "", "Submit", function () {
fetch(api + "/auth/mfa/totp", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
code: onetimecode,
ticket: response.ticket,
})
}).then(r => r.json()).then(response => {
if (response.message) {
alert(response.message);
}
else {
console.warn(response);
if (!response.token)
return;
adduser({ serverurls: JSON.parse(localStorage.getItem("instanceinfo")), email: username, token: response.token }).username = username;
const redir = new URLSearchParams(window.location.search).get("goback");
if (redir) {
window.location.href = redir;
}
else {
window.location.href = "/channels/@me";
}
}
});
}]]).show();
}
else {
console.warn(response);
if (!response.token)
return;
adduser({ serverurls: JSON.parse(localStorage.getItem("instanceinfo")), email: username, token: response.token }).username = username;
const redir = new URLSearchParams(window.location.search).get("goback");
if (redir) {
window.location.href = redir;
}
else {
window.location.href = "/channels/@me";
}
return "";
}
}
});
}
catch (error) {
console.error("Error:", error);
}
}
async function check(e) {
e.preventDefault();
const h = await login(e.srcElement[1].value, e.srcElement[2].value, e.srcElement[3].value);
document.getElementById("wrong").textContent = h;
console.log(h);
}
if (document.getElementById("form")) {
document.getElementById("form").addEventListener("submit", check);
}
//this currently does not work, and need to be implemented better at some time.
/*
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);
});
}
})
}
*/
const switchurl = document.getElementById("switch");
if (switchurl) {
switchurl.href += window.location.search;
const instance = new URLSearchParams(window.location.search).get("instance");
console.log(instance);
if (instance) {
instancein.value = instance;
checkInstance("");
}
}
export { checkInstance };
trimswitcher();
export { mobile, getBulkUsers, getBulkInfo, setTheme, Specialuser, getapiurls, adduser };
const datalist = document.getElementById("instances");
console.warn(datalist);
export function getInstances() {
return instances;
}
fetch("/instances.json").then(_ => _.json()).then((json) => {
instances = json;
if (datalist) {
console.warn(json);
if (instancein && instancein.value === "") {
instancein.value = json[0].name;
}
for (const instance of json) {
if (instance.display === false) {
continue;
}
const option = document.createElement("option");
option.disabled = !instance.online;
option.value = instance.name;
if (instance.url) {
stringURLMap.set(option.value, instance.url);
if (instance.urls) {
stringURLsMap.set(instance.url, instance.urls);
}
}
else if (instance.urls) {
stringURLsMap.set(option.value, instance.urls);
}
else {
option.disabled = true;
}
if (instance.description) {
option.label = instance.description;
}
else {
option.label = instance.name;
}
datalist.append(option);
}
checkInstance("");
}
});

View file

@ -1,761 +0,0 @@
import { Channel } from "./channel.js";
import { Dialog } from "./dialog.js";
import { Emoji } from "./emoji.js";
import { Localuser } from "./localuser.js";
import { Member } from "./member.js";
class MarkDown {
txt;
keep;
stdsize;
owner;
info;
constructor(text, owner, { keep = false, stdsize = false } = {}) {
if ((typeof text) === (typeof "")) {
this.txt = text.split("");
}
else {
this.txt = text;
}
if (this.txt === undefined) {
this.txt = [];
}
this.info = owner.info;
this.keep = keep;
this.owner = owner;
this.stdsize = stdsize;
}
get localuser() {
if (this.owner instanceof Localuser) {
return this.owner;
}
else {
return this.owner.localuser;
}
}
get rawString() {
return this.txt.join("");
}
get textContent() {
return this.makeHTML().textContent;
}
makeHTML({ keep = this.keep, stdsize = this.stdsize } = {}) {
return this.markdown(this.txt, { keep, stdsize });
}
markdown(text, { keep = false, stdsize = false } = {}) {
let txt;
if ((typeof text) === (typeof "")) {
txt = text.split("");
}
else {
txt = text;
}
if (txt === undefined) {
txt = [];
}
const span = document.createElement("span");
let current = document.createElement("span");
function appendcurrent() {
if (current.innerHTML !== "") {
span.append(current);
current = document.createElement("span");
}
}
for (let i = 0; i < txt.length; i++) {
if (txt[i] === "\n" || i === 0) {
const first = i === 0;
if (first) {
i--;
}
let element = document.createElement("span");
let keepys = "";
if (txt[i + 1] === "#") {
if (txt[i + 2] === "#") {
if (txt[i + 3] === "#" && txt[i + 4] === " ") {
element = document.createElement("h3");
keepys = "### ";
i += 5;
}
else if (txt[i + 3] === " ") {
element = document.createElement("h2");
element.classList.add("h2md");
keepys = "## ";
i += 4;
}
}
else if (txt[i + 2] === " ") {
element = document.createElement("h1");
keepys = "# ";
i += 3;
}
}
else if (txt[i + 1] === ">" && txt[i + 2] === " ") {
element = document.createElement("div");
const line = document.createElement("div");
line.classList.add("quoteline");
element.append(line);
element.classList.add("quote");
keepys = "> ";
i += 3;
}
if (keepys) {
appendcurrent();
if (!first && !stdsize) {
span.appendChild(document.createElement("br"));
}
const build = [];
for (; txt[i] !== "\n" && txt[i] !== undefined; i++) {
build.push(txt[i]);
}
try {
if (stdsize) {
element = document.createElement("span");
}
if (keep) {
element.append(keepys);
//span.appendChild(document.createElement("br"));
}
element.appendChild(this.markdown(build, { keep, stdsize }));
span.append(element);
}
finally {
i -= 1;
continue;
}
}
if (first) {
i++;
}
}
if (txt[i] === "\n") {
if (!stdsize) {
appendcurrent();
span.append(document.createElement("br"));
}
continue;
}
if (txt[i] === "`") {
let count = 1;
if (txt[i + 1] === "`") {
count++;
if (txt[i + 2] === "`") {
count++;
}
}
let build = "";
if (keep) {
build += "`".repeat(count);
}
let find = 0;
let j = i + count;
let init = true;
for (; txt[j] !== undefined && (txt[j] !== "\n" || count === 3) && find !== count; j++) {
if (txt[j] === "`") {
find++;
}
else {
if (find !== 0) {
build += "`".repeat(find);
find = 0;
}
if (init && count === 3) {
if (txt[j] === " " || txt[j] === "\n") {
init = false;
}
if (keep) {
build += txt[j];
}
continue;
}
build += txt[j];
}
}
if (stdsize) {
build = build.replaceAll("\n", "");
}
if (find === count) {
appendcurrent();
i = j;
if (keep) {
build += "`".repeat(find);
}
if (count !== 3 && !stdsize) {
const samp = document.createElement("samp");
samp.textContent = build;
span.appendChild(samp);
}
else {
const pre = document.createElement("pre");
if (build.at(-1) === "\n") {
build = build.substring(0, build.length - 1);
}
if (txt[i] === "\n") {
i++;
}
pre.textContent = build;
span.appendChild(pre);
}
i--;
continue;
}
}
if (txt[i] === "*") {
let count = 1;
if (txt[i + 1] === "*") {
count++;
if (txt[i + 2] === "*") {
count++;
}
}
let build = [];
let find = 0;
let j = i + count;
for (; txt[j] !== undefined && find !== count; j++) {
if (txt[j] === "*") {
find++;
}
else {
build.push(txt[j]);
if (find !== 0) {
build = build.concat(new Array(find).fill("*"));
find = 0;
}
}
}
if (find === count && (count != 1 || txt[i + 1] !== " ")) {
appendcurrent();
i = j;
const stars = "*".repeat(count);
if (count === 1) {
const i = document.createElement("i");
if (keep) {
i.append(stars);
}
i.appendChild(this.markdown(build, { keep, stdsize }));
if (keep) {
i.append(stars);
}
span.appendChild(i);
}
else if (count === 2) {
const b = document.createElement("b");
if (keep) {
b.append(stars);
}
b.appendChild(this.markdown(build, { keep, stdsize }));
if (keep) {
b.append(stars);
}
span.appendChild(b);
}
else {
const b = document.createElement("b");
const i = document.createElement("i");
if (keep) {
b.append(stars);
}
b.appendChild(this.markdown(build, { keep, stdsize }));
if (keep) {
b.append(stars);
}
i.appendChild(b);
span.appendChild(i);
}
i--;
continue;
}
}
if (txt[i] === "_") {
let count = 1;
if (txt[i + 1] === "_") {
count++;
if (txt[i + 2] === "_") {
count++;
}
}
let build = [];
let find = 0;
let j = i + count;
for (; txt[j] !== undefined && find !== count; j++) {
if (txt[j] === "_") {
find++;
}
else {
build.push(txt[j]);
if (find !== 0) {
build = build.concat(new Array(find).fill("_"));
find = 0;
}
}
}
if (find === count && (count != 1 || (txt[j + 1] === " " || txt[j + 1] === "\n" || txt[j + 1] === undefined))) {
appendcurrent();
i = j;
const underscores = "_".repeat(count);
if (count === 1) {
const i = document.createElement("i");
if (keep) {
i.append(underscores);
}
i.appendChild(this.markdown(build, { keep, stdsize }));
if (keep) {
i.append(underscores);
}
span.appendChild(i);
}
else if (count === 2) {
const u = document.createElement("u");
if (keep) {
u.append(underscores);
}
u.appendChild(this.markdown(build, { keep, stdsize }));
if (keep) {
u.append(underscores);
}
span.appendChild(u);
}
else {
const u = document.createElement("u");
const i = document.createElement("i");
if (keep) {
i.append(underscores);
}
i.appendChild(this.markdown(build, { keep, stdsize }));
if (keep) {
i.append(underscores);
}
u.appendChild(i);
span.appendChild(u);
}
i--;
continue;
}
}
if (txt[i] === "~" && txt[i + 1] === "~") {
const count = 2;
let build = [];
let find = 0;
let j = i + 2;
for (; txt[j] !== undefined && find !== count; j++) {
if (txt[j] === "~") {
find++;
}
else {
build.push(txt[j]);
if (find !== 0) {
build = build.concat(new Array(find).fill("~"));
find = 0;
}
}
}
if (find === count) {
appendcurrent();
i = j - 1;
const tildes = "~~";
if (count === 2) {
const s = document.createElement("s");
if (keep) {
s.append(tildes);
}
s.appendChild(this.markdown(build, { keep, stdsize }));
if (keep) {
s.append(tildes);
}
span.appendChild(s);
}
continue;
}
}
if (txt[i] === "|" && txt[i + 1] === "|") {
const count = 2;
let build = [];
let find = 0;
let j = i + 2;
for (; txt[j] !== undefined && find !== count; j++) {
if (txt[j] === "|") {
find++;
}
else {
build.push(txt[j]);
if (find !== 0) {
build = build.concat(new Array(find).fill("~"));
find = 0;
}
}
}
if (find === count) {
appendcurrent();
i = j - 1;
const pipes = "||";
if (count === 2) {
const j = document.createElement("j");
if (keep) {
j.append(pipes);
}
j.appendChild(this.markdown(build, { keep, stdsize }));
j.classList.add("spoiler");
j.onclick = MarkDown.unspoil;
if (keep) {
j.append(pipes);
}
span.appendChild(j);
}
continue;
}
}
if ((!keep) && txt[i] === "h" && txt[i + 1] === "t" && txt[i + 2] === "t" && txt[i + 3] === "p") {
let build = "http";
let j = i + 4;
const endchars = new Set(["\\", "<", ">", "|", "]", " "]);
for (; txt[j] !== undefined; j++) {
const char = txt[j];
if (endchars.has(char)) {
break;
}
build += char;
}
if (URL.canParse(build)) {
appendcurrent();
const a = document.createElement("a");
//a.href=build;
MarkDown.safeLink(a, build);
a.textContent = build;
a.target = "_blank";
i = j - 1;
span.appendChild(a);
continue;
}
}
if (txt[i] === "<" && (txt[i + 1] === "@" || txt[i + 1] === "#")) {
let id = "";
let j = i + 2;
const numbers = new Set(["0", "1", "2", "3", "4", "5", "6", "7", "8", "9"]);
for (; txt[j] !== undefined; j++) {
const char = txt[j];
if (!numbers.has(char)) {
break;
}
id += char;
}
if (txt[j] === ">") {
appendcurrent();
const mention = document.createElement("span");
mention.classList.add("mentionMD");
mention.contentEditable = "false";
const char = txt[i + 1];
i = j;
switch (char) {
case "@":
const user = this.localuser.userMap.get(id);
if (user) {
mention.textContent = `@${user.name}`;
let guild = null;
if (this.owner instanceof Channel) {
guild = this.owner.guild;
}
if (!keep) {
user.bind(mention, guild);
}
if (guild) {
Member.resolveMember(user, guild).then(member => {
if (member) {
mention.textContent = `@${member.name}`;
}
});
}
}
else {
mention.textContent = `@unknown`;
}
break;
case "#":
const channel = this.localuser.channelids.get(id);
if (channel) {
mention.textContent = `#${channel.name}`;
if (!keep) {
mention.onclick = _ => {
this.localuser.goToChannel(id);
};
}
}
else {
mention.textContent = `#unknown`;
}
break;
}
span.appendChild(mention);
mention.setAttribute("real", `<${char}${id}>`);
continue;
}
}
if (txt[i] === "<" && txt[i + 1] === "t" && txt[i + 2] === ":") {
let found = false;
const build = ["<", "t", ":"];
let j = i + 3;
for (; txt[j] !== void 0; j++) {
build.push(txt[j]);
if (txt[j] === ">") {
found = true;
break;
}
}
if (found) {
appendcurrent();
i = j;
const parts = build.join("").match(/^<t:([0-9]{1,16})(:([tTdDfFR]))?>$/);
const dateInput = new Date(Number.parseInt(parts[1]) * 1000);
let time = "";
if (Number.isNaN(dateInput.getTime()))
time = build.join("");
else {
if (parts[3] === "d")
time = dateInput.toLocaleString(void 0, { day: "2-digit", month: "2-digit", year: "numeric" });
else if (parts[3] === "D")
time = dateInput.toLocaleString(void 0, { day: "numeric", month: "long", year: "numeric" });
else if (!parts[3] || parts[3] === "f")
time = dateInput.toLocaleString(void 0, { day: "numeric", month: "long", year: "numeric" }) + " " +
dateInput.toLocaleString(void 0, { hour: "2-digit", minute: "2-digit" });
else if (parts[3] === "F")
time = dateInput.toLocaleString(void 0, { day: "numeric", month: "long", year: "numeric", weekday: "long" }) + " " +
dateInput.toLocaleString(void 0, { hour: "2-digit", minute: "2-digit" });
else if (parts[3] === "t")
time = dateInput.toLocaleString(void 0, { hour: "2-digit", minute: "2-digit" });
else if (parts[3] === "T")
time = dateInput.toLocaleString(void 0, { hour: "2-digit", minute: "2-digit", second: "2-digit" });
else if (parts[3] === "R")
time = Math.round((Date.now() - (Number.parseInt(parts[1]) * 1000)) / 1000 / 60) + " minutes ago";
}
const timeElem = document.createElement("span");
timeElem.classList.add("markdown-timestamp");
timeElem.textContent = time;
span.appendChild(timeElem);
continue;
}
}
if (txt[i] === "<" && (txt[i + 1] === ":" || (txt[i + 1] === "a" && txt[i + 2] === ":"))) {
let found = false;
const build = txt[i + 1] === "a" ? ["<", "a", ":"] : ["<", ":"];
let j = i + build.length;
for (; txt[j] !== void 0; j++) {
build.push(txt[j]);
if (txt[j] === ">") {
found = true;
break;
}
}
if (found) {
const buildjoin = build.join("");
const parts = buildjoin.match(/^<(a)?:\w+:(\d{10,30})>$/);
if (parts && parts[2]) {
appendcurrent();
i = j;
const isEmojiOnly = txt.join("").trim() === buildjoin.trim();
const owner = (this.owner instanceof Channel) ? this.owner.guild : this.owner;
const emoji = new Emoji({ name: buildjoin, id: parts[2], animated: Boolean(parts[1]) }, owner);
span.appendChild(emoji.getHTML(isEmojiOnly));
continue;
}
}
}
if (txt[i] == "[" && !keep) {
let partsFound = 0;
let j = i + 1;
const build = ["["];
for (; txt[j] !== void 0; j++) {
build.push(txt[j]);
if (partsFound === 0 && txt[j] === "]") {
if (txt[j + 1] === "(" &&
txt[j + 2] === "h" && txt[j + 3] === "t" && txt[j + 4] === "t" && txt[j + 5] === "p" && (txt[j + 6] === "s" || txt[j + 6] === ":")) {
partsFound++;
}
else {
break;
}
}
else if (partsFound === 1 && txt[j] === ")") {
partsFound++;
break;
}
}
if (partsFound === 2) {
appendcurrent();
const parts = build.join("").match(/^\[(.+)\]\((https?:.+?)( ('|").+('|"))?\)$/);
if (parts) {
const linkElem = document.createElement("a");
if (URL.canParse(parts[2])) {
i = j;
MarkDown.safeLink(linkElem, parts[2]);
linkElem.textContent = parts[1];
linkElem.target = "_blank";
linkElem.rel = "noopener noreferrer";
linkElem.title = (parts[3] ? parts[3].substring(2, parts[3].length - 1) + "\n\n" : "") + parts[2];
span.appendChild(linkElem);
continue;
}
}
}
}
current.textContent += txt[i];
}
appendcurrent();
return span;
}
static unspoil(e) {
e.target.classList.remove("spoiler");
e.target.classList.add("unspoiled");
}
giveBox(box) {
box.onkeydown = _ => {
//console.log(_);
};
let prevcontent = "";
box.onkeyup = _ => {
const content = MarkDown.gatherBoxText(box);
if (content !== prevcontent) {
prevcontent = content;
this.txt = content.split("");
this.boxupdate(box);
}
};
box.onpaste = _ => {
if (!_.clipboardData)
return;
console.log(_.clipboardData.types);
const data = _.clipboardData.getData("text");
document.execCommand("insertHTML", false, data);
_.preventDefault();
if (!box.onkeyup)
return;
box.onkeyup(new KeyboardEvent("_"));
};
}
boxupdate(box) {
const restore = saveCaretPosition(box);
box.innerHTML = "";
box.append(this.makeHTML({ keep: true }));
if (restore) {
restore();
}
}
static gatherBoxText(element) {
if (element.tagName.toLowerCase() === "img") {
return element.alt;
}
if (element.tagName.toLowerCase() === "br") {
return "\n";
}
if (element.hasAttribute("real")) {
return element.getAttribute("real");
}
let build = "";
for (const thing of element.childNodes) {
if (thing instanceof Text) {
const text = thing.textContent;
build += text;
continue;
}
const text = this.gatherBoxText(thing);
if (text) {
build += text;
}
}
return build;
}
static trustedDomains = new Set([location.host]);
static safeLink(elm, url) {
if (URL.canParse(url)) {
const Url = new URL(url);
if (elm instanceof HTMLAnchorElement && this.trustedDomains.has(Url.host)) {
elm.href = url;
elm.target = "_blank";
return;
}
elm.onmouseup = _ => {
if (_.button === 2)
return;
console.log(":3");
function open() {
const proxy = window.open(url, '_blank');
if (proxy && _.button === 1) {
proxy.focus();
}
else if (proxy) {
window.focus();
}
}
if (this.trustedDomains.has(Url.host)) {
open();
}
else {
const full = new Dialog([
"vdiv",
["title", "You're leaving spacebar"],
["text", "You're going to " + Url.host + " are you sure you want to go there?"],
["hdiv",
["button", "", "Nevermind", _ => full.hide()],
["button", "", "Go there", _ => { open(); full.hide(); }],
["button", "", "Go there and trust in the future", _ => {
open();
full.hide();
this.trustedDomains.add(Url.host);
}]
]
]);
full.show();
}
};
}
else {
throw Error(url + " is not a valid URL");
}
}
static replace(base, newelm) {
const basechildren = base.children;
const newchildren = newelm.children;
for (const thing of newchildren) {
}
}
}
//solution from https://stackoverflow.com/questions/4576694/saving-and-restoring-caret-position-for-contenteditable-div
let text = "";
function saveCaretPosition(context) {
const selection = window.getSelection();
if (!selection)
return;
const range = selection.getRangeAt(0);
range.setStart(context, 0);
text = selection.toString();
let len = text.length + 1;
for (const str in text.split("\n")) {
if (str.length !== 0) {
len--;
}
}
len += +(text[text.length - 1] === "\n");
return function restore() {
if (!selection)
return;
const pos = getTextNodeAtPosition(context, len);
selection.removeAllRanges();
const range = new Range();
range.setStart(pos.node, pos.position);
selection.addRange(range);
};
}
function getTextNodeAtPosition(root, index) {
const NODE_TYPE = NodeFilter.SHOW_TEXT;
const treeWalker = document.createTreeWalker(root, NODE_TYPE, elem => {
if (!elem.textContent)
return 0;
if (index > elem.textContent.length) {
index -= elem.textContent.length;
return NodeFilter.FILTER_REJECT;
}
return NodeFilter.FILTER_ACCEPT;
});
const c = treeWalker.nextNode();
return {
node: c ? c : root,
position: index
};
}
export { MarkDown };

View file

@ -1,221 +0,0 @@
import { User } from "./user.js";
import { SnowFlake } from "./snowflake.js";
import { Dialog } from "./dialog.js";
class Member extends SnowFlake {
static already = {};
owner;
user;
roles = [];
nick;
constructor(memberjson, owner) {
super(memberjson.id);
this.owner = owner;
if (this.localuser.userMap.has(memberjson.id)) {
this.user = this.localuser.userMap.get(memberjson.id);
}
else if (memberjson.user) {
this.user = new User(memberjson.user, owner.localuser);
}
else {
throw new Error("Missing user object of this member");
}
for (const thing of Object.keys(memberjson)) {
if (thing === "guild") {
continue;
}
if (thing === "owner") {
continue;
}
if (thing === "roles") {
for (const strrole of memberjson.roles) {
const role = this.guild.roleids.get(strrole);
if (!role)
continue;
this.roles.push(role);
}
continue;
}
this[thing] = memberjson[thing];
}
if (this.localuser.userMap.has(this?.id)) {
this.user = this.localuser.userMap.get(this?.id);
}
this.roles.sort((a, b) => { return (this.guild.roles.indexOf(a) - this.guild.roles.indexOf(b)); });
}
get guild() {
return this.owner;
}
get localuser() {
return this.guild.localuser;
}
get info() {
return this.owner.info;
}
static async new(memberjson, owner) {
let user;
if (owner.localuser.userMap.has(memberjson.id)) {
user = owner.localuser.userMap.get(memberjson.id);
}
else if (memberjson.user) {
user = new User(memberjson.user, owner.localuser);
}
else {
throw new Error("missing user object of this member");
}
if (user.members.has(owner)) {
let memb = user.members.get(owner);
if (memb === undefined) {
memb = new Member(memberjson, owner);
user.members.set(owner, memb);
return memb;
}
else if (memb instanceof Promise) {
return await memb; //I should do something else, though for now this is "good enough"
}
else {
return memb;
}
}
else {
const memb = new Member(memberjson, owner);
user.members.set(owner, memb);
return memb;
}
}
static async resolveMember(user, guild) {
const maybe = user.members.get(guild);
if (!user.members.has(guild)) {
const membpromise = guild.localuser.resolvemember(user.id, guild.id);
const promise = new Promise(async (res) => {
const membjson = await membpromise;
if (membjson === undefined) {
res(undefined);
}
else {
const member = new Member(membjson, guild);
const map = guild.localuser.presences;
member.getPresence(map.get(member.id));
map.delete(member.id);
res(member);
return member;
}
});
user.members.set(guild, promise);
}
if (maybe instanceof Promise) {
return await maybe;
}
else {
return maybe;
}
}
getPresence(presence) {
this.user.getPresence(presence);
}
/**
* @todo
*/
highInfo() {
fetch(this.info.api + "/users/" + this.id + "/profile?with_mutual_guilds=true&with_mutual_friends_count=true&guild_id=" + this.guild.id, { headers: this.guild.headers });
}
hasRole(ID) {
console.log(this.roles, ID);
for (const thing of this.roles) {
if (thing.id === ID) {
return true;
}
}
return false;
}
getColor() {
for (const thing of this.roles) {
const color = thing.getColor();
if (color) {
return color;
}
}
return "";
}
isAdmin() {
for (const role of this.roles) {
if (role.permissions.getPermission("ADMINISTRATOR")) {
return true;
}
}
return this.guild.properties.owner_id === this.user.id;
}
bind(html) {
if (html.tagName === "SPAN") {
if (!this) {
return;
}
/*
if(this.error){
}
*/
html.style.color = this.getColor();
}
//this.profileclick(html);
}
profileclick(html) {
//to be implemented
}
get name() {
return this.nick || this.user.username;
}
kick() {
let reason = "";
const menu = new Dialog(["vdiv",
["title", "Kick " + this.name + " from " + this.guild.properties.name],
["textbox", "Reason:", "", function (e) {
reason = e.target.value;
}],
["button", "", "submit", () => {
this.kickAPI(reason);
menu.hide();
}]]);
menu.show();
}
kickAPI(reason) {
const headers = structuredClone(this.guild.headers);
headers["x-audit-log-reason"] = reason;
fetch(`${this.info.api}/guilds/${this.guild.id}/members/${this.id}`, {
method: "DELETE",
headers,
});
}
ban() {
let reason = "";
const menu = new Dialog(["vdiv",
["title", "Ban " + this.name + " from " + this.guild.properties.name],
["textbox", "Reason:", "", function (e) {
reason = e.target.value;
}],
["button", "", "submit", () => {
this.banAPI(reason);
menu.hide();
}]]);
menu.show();
}
banAPI(reason) {
const headers = structuredClone(this.guild.headers);
headers["x-audit-log-reason"] = reason;
fetch(`${this.info.api}/guilds/${this.guild.id}/bans/${this.id}`, {
method: "PUT",
headers
});
}
hasPermission(name) {
if (this.isAdmin()) {
return true;
}
for (const thing of this.roles) {
if (thing.permissions.getPermission(name)) {
return true;
}
}
return false;
}
}
export { Member };

View file

@ -1,699 +0,0 @@
import { Contextmenu } from "./contextmenu.js";
import { User } from "./user.js";
import { Member } from "./member.js";
import { MarkDown } from "./markdown.js";
import { Embed } from "./embed.js";
import { File } from "./file.js";
import { SnowFlake } from "./snowflake.js";
import { Emoji } from "./emoji.js";
import { Dialog } from "./dialog.js";
class Message extends SnowFlake {
static contextmenu = new Contextmenu("message menu");
owner;
headers;
embeds;
author;
mentions;
mention_roles;
attachments; //probably should be its own class tbh, should be Attachments[]
message_reference;
type;
timestamp;
content;
static del;
static resolve;
/*
weakdiv:WeakRef<HTMLDivElement>;
set div(e:HTMLDivElement){
if(!e){
this.weakdiv=null;
return;
}
this.weakdiv=new WeakRef(e);
}
get div(){
return this.weakdiv?.deref();
}
//*/
div;
member;
reactions;
static setup() {
this.del = new Promise(_ => {
this.resolve = _;
});
Message.setupcmenu();
}
static setupcmenu() {
Message.contextmenu.addbutton("Copy raw text", function () {
navigator.clipboard.writeText(this.content.rawString);
});
Message.contextmenu.addbutton("Reply", function () {
this.channel.setReplying(this);
});
Message.contextmenu.addbutton("Copy message id", function () {
navigator.clipboard.writeText(this.id);
});
Message.contextmenu.addsubmenu("Add reaction", function (arg, e) {
Emoji.emojiPicker(e.x, e.y, this.localuser).then(_ => {
this.reactionToggle(_);
});
});
Message.contextmenu.addbutton("Edit", function () {
this.setEdit();
}, null, function () {
return this.author.id === this.localuser.user.id;
});
Message.contextmenu.addbutton("Delete message", function () {
this.delete();
}, null, function () {
return this.canDelete();
});
}
setEdit() {
this.channel.editing = this;
const markdown = document.getElementById("typebox")["markdown"];
markdown.txt = this.content.rawString.split("");
markdown.boxupdate(document.getElementById("typebox"));
}
constructor(messagejson, owner) {
super(messagejson.id);
this.owner = owner;
this.headers = this.owner.headers;
this.giveData(messagejson);
this.owner.messages.set(this.id, this);
}
reactionToggle(emoji) {
let remove = false;
for (const thing of this.reactions) {
if (thing.emoji.name === emoji) {
remove = thing.me;
break;
}
}
let reactiontxt;
if (emoji instanceof Emoji) {
reactiontxt = `${emoji.name}:${emoji.id}`;
}
else {
reactiontxt = encodeURIComponent(emoji);
}
fetch(`${this.info.api}/channels/${this.channel.id}/messages/${this.id}/reactions/${reactiontxt}/@me`, {
method: remove ? "DELETE" : "PUT",
headers: this.headers
});
}
giveData(messagejson) {
const func = this.channel.infinite.snapBottom();
for (const thing of Object.keys(messagejson)) {
if (thing === "attachments") {
this.attachments = [];
for (const thing of messagejson.attachments) {
this.attachments.push(new File(thing, this));
}
continue;
}
else if (thing === "content") {
this.content = new MarkDown(messagejson[thing], this.channel);
continue;
}
else if (thing === "id") {
continue;
}
else if (thing === "member") {
Member.new(messagejson.member, this.guild).then(_ => {
this.member = _;
});
continue;
}
else if (thing === "embeds") {
this.embeds = [];
for (const thing in messagejson.embeds) {
this.embeds[thing] = new Embed(messagejson.embeds[thing], this);
}
continue;
}
this[thing] = messagejson[thing];
}
if (messagejson.reactions?.length) {
console.log(messagejson.reactions, ":3");
}
this.author = new User(messagejson.author, this.localuser);
for (const thing in messagejson.mentions) {
this.mentions[thing] = new User(messagejson.mentions[thing], this.localuser);
}
if (!this.member && this.guild.id !== "@me") {
this.author.resolvemember(this.guild).then(_ => {
this.member = _;
});
}
if (this.mentions.length || this.mention_roles.length) { //currently mention_roles isn't implemented on the spacebar servers
console.log(this.mentions, this.mention_roles);
}
if (this.mentionsuser(this.localuser.user)) {
console.log(this);
}
if (this.div) {
this.generateMessage();
}
func();
}
canDelete() {
return this.channel.hasPermission("MANAGE_MESSAGES") || this.author === this.localuser.user;
}
get channel() {
return this.owner;
}
get guild() {
return this.owner.guild;
}
get localuser() {
return this.owner.localuser;
}
get info() {
return this.owner.info;
}
messageevents(obj) {
const func = Message.contextmenu.bindContextmenu(obj, this, undefined);
this.div = obj;
obj.classList.add("messagediv");
}
deleteDiv() {
if (!this.div)
return;
try {
this.div.remove();
this.div = undefined;
}
catch (e) {
console.error(e);
}
}
mentionsuser(userd) {
if (userd instanceof User) {
return this.mentions.includes(userd);
}
else if (userd instanceof Member) {
return this.mentions.includes(userd.user);
}
}
getimages() {
const build = [];
for (const thing of this.attachments) {
if (thing.content_type.startsWith("image/")) {
build.push(thing);
}
}
return build;
}
async edit(content) {
return await fetch(this.info.api + "/channels/" + this.channel.id + "/messages/" + this.id, {
method: "PATCH",
headers: this.headers,
body: JSON.stringify({ content })
});
}
delete() {
fetch(`${this.info.api}/channels/${this.channel.id}/messages/${this.id}`, {
headers: this.headers,
method: "DELETE",
});
}
deleteEvent() {
console.log("deleted");
if (this.div) {
this.div.remove();
this.div.innerHTML = "";
this.div = undefined;
}
const prev = this.channel.idToPrev.get(this.id);
const next = this.channel.idToNext.get(this.id);
this.channel.idToPrev.delete(this.id);
this.channel.idToNext.delete(this.id);
this.channel.messages.delete(this.id);
if (prev && next) {
this.channel.idToPrev.set(next, prev);
this.channel.idToNext.set(prev, next);
}
else if (prev) {
this.channel.idToNext.delete(prev);
}
else if (next) {
this.channel.idToPrev.delete(next);
}
if (prev) {
const prevmessage = this.channel.messages.get(prev);
if (prevmessage) {
prevmessage.generateMessage();
}
}
if (this.channel.lastmessage === this || this.channel.lastmessageid === this.id) {
if (prev) {
this.channel.lastmessage = this.channel.messages.get(prev);
this.channel.lastmessageid = prev;
}
else {
this.channel.lastmessage = undefined;
this.channel.lastmessageid = undefined;
}
}
if (this.channel.lastreadmessageid === this.id) {
if (prev) {
this.channel.lastreadmessageid = prev;
}
else {
this.channel.lastreadmessageid = undefined;
}
}
console.log("deleted done");
}
reactdiv;
blockedPropigate() {
const previd = this.channel.idToPrev.get(this.id);
if (!previd) {
this.generateMessage();
return;
}
const premessage = this.channel.messages.get(previd);
if (premessage?.author === this.author) {
premessage.blockedPropigate();
}
else {
this.generateMessage();
}
}
generateMessage(premessage, ignoredblock = false) {
if (!this.div)
return;
if (!premessage) {
premessage = this.channel.messages.get(this.channel.idToPrev.get(this.id));
}
const div = this.div;
for (const user of this.mentions) {
if (user === this.localuser.user) {
div.classList.add("mentioned");
}
}
if (this === this.channel.replyingto) {
div.classList.add("replying");
}
div.innerHTML = "";
const build = document.createElement("div");
build.classList.add("flexltr", "message");
div.classList.remove("zeroheight");
if (this.author.relationshipType === 2) {
if (ignoredblock) {
if (premessage?.author !== this.author) {
const span = document.createElement("span");
span.textContent = "You have this user blocked, click to hide these messages.";
div.append(span);
span.classList.add("blocked");
span.onclick = _ => {
const scroll = this.channel.infinite.scrollTop;
let next = this;
while (next?.author === this.author) {
next.generateMessage();
next = this.channel.messages.get(this.channel.idToNext.get(next.id));
}
if (this.channel.infinite.scollDiv && scroll) {
this.channel.infinite.scollDiv.scrollTop = scroll;
}
};
}
}
else {
div.classList.remove("topMessage");
if (premessage?.author === this.author) {
div.classList.add("zeroheight");
premessage.blockedPropigate();
div.appendChild(build);
return div;
}
else {
build.classList.add("blocked", "topMessage");
const span = document.createElement("span");
let count = 1;
let next = this.channel.messages.get(this.channel.idToNext.get(this.id));
while (next?.author === this.author) {
count++;
next = this.channel.messages.get(this.channel.idToNext.get(next.id));
}
span.textContent = `You have this user blocked, click to see the ${count} blocked messages.`;
build.append(span);
span.onclick = _ => {
const scroll = this.channel.infinite.scrollTop;
const func = this.channel.infinite.snapBottom();
let next = this;
while (next?.author === this.author) {
next.generateMessage(undefined, true);
next = this.channel.messages.get(this.channel.idToNext.get(next.id));
console.log("loopy");
}
if (this.channel.infinite.scollDiv && scroll) {
func();
this.channel.infinite.scollDiv.scrollTop = scroll;
}
};
div.appendChild(build);
return div;
}
}
}
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");
replyline.appendChild(reply);
const line2 = document.createElement("hr");
replyline.appendChild(line2);
line2.classList.add("reply");
line.classList.add("startreply");
replyline.classList.add("replyflex");
this.channel.getmessage(this.message_reference.message_id).then(message => {
if (message.author.relationshipType === 2) {
username.textContent = "Blocked user";
return;
}
const author = message.author;
reply.appendChild(message.content.makeHTML({ stdsize: true }));
minipfp.src = author.getpfpsrc();
author.bind(minipfp, this.guild);
username.textContent = author.username;
author.bind(username, this.guild);
});
reply.onclick = _ => {
this.channel.infinite.focus(this.message_reference.message_id);
};
div.appendChild(replyline);
}
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;
let pfpparent2 = pfpparent.all;
pfpparent2 ??= pfpparent;
const old = (new Date(pfpparent2.timestamp).getTime()) / 1000;
const newt = (new Date(this.timestamp).getTime()) / 1000;
current = (newt - old) > 600;
}
const combine = (premessage?.author != this.author) || (current) || this.message_reference;
if (combine) {
const pfp = this.author.buildpfp();
this.author.bind(pfp, this.guild, false);
pfpRow.appendChild(pfp);
}
else {
div["pfpparent"] = pfpparent;
}
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);
if (combine) {
const username = document.createElement("span");
username.classList.add("username");
this.author.bind(username, this.guild);
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");
username.classList.add("bot");
username.textContent = "BOT";
userwrap.appendChild(username);
}
const time = document.createElement("span");
time.textContent = " " + formatTime(new Date(this.timestamp));
time.classList.add("timestamp");
userwrap.appendChild(time);
texttxt.appendChild(userwrap);
}
else {
div.classList.remove("topMessage");
}
const messaged = this.content.makeHTML();
div["txt"] = messaged;
const messagedwrap = document.createElement("div");
messagedwrap.classList.add("flexttb");
messagedwrap.appendChild(messaged);
texttxt.appendChild(messagedwrap);
build.appendChild(text);
if (this.attachments.length) {
console.log(this.attachments);
const attach = document.createElement("div");
attach.classList.add("flexltr");
for (const thing of this.attachments) {
attach.appendChild(thing.getHTML());
}
messagedwrap.appendChild(attach);
}
if (this.embeds.length) {
const embeds = document.createElement("div");
embeds.classList.add("flexltr");
for (const thing of this.embeds) {
embeds.appendChild(thing.generateHTML());
}
messagedwrap.appendChild(embeds);
}
//
}
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);
const username = document.createElement("span");
username.textContent = this.author.username;
//this.author.profileclick(username);
this.author.bind(username, this.guild);
texttxt.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);
div.classList.add("topMessage");
}
const reactions = document.createElement("div");
reactions.classList.add("flexltr", "reactiondiv");
this.reactdiv = new WeakRef(reactions);
this.updateReactions();
div.append(reactions);
this.bindButtonEvent();
return (div);
}
bindButtonEvent() {
if (this.div) {
let buttons;
this.div.onmouseenter = _ => {
if (buttons) {
buttons.remove();
buttons = undefined;
}
if (this.div) {
buttons = document.createElement("div");
buttons.classList.add("messageButtons", "flexltr");
if (this.channel.hasPermission("SEND_MESSAGES")) {
const container = document.createElement("div");
const reply = document.createElement("span");
reply.classList.add("svgtheme", "svg-reply", "svgicon");
container.append(reply);
buttons.append(container);
container.onclick = _ => {
this.channel.setReplying(this);
};
}
if (this.author === this.localuser.user) {
const container = document.createElement("div");
const edit = document.createElement("span");
edit.classList.add("svgtheme", "svg-edit", "svgicon");
container.append(edit);
buttons.append(container);
container.onclick = _ => {
this.setEdit();
};
}
if (this.canDelete()) {
const container = document.createElement("div");
const reply = document.createElement("span");
reply.classList.add("svgtheme", "svg-delete", "svgicon");
container.append(reply);
buttons.append(container);
container.onclick = _ => {
if (_.shiftKey) {
this.delete();
return;
}
const diaolog = new Dialog(["hdiv", ["title", "are you sure you want to delete this?"], ["button", "", "yes", () => { this.delete(); diaolog.hide(); }], ["button", "", "no", () => { diaolog.hide(); }]]);
diaolog.show();
};
}
if (buttons.childNodes.length !== 0) {
this.div.append(buttons);
}
}
};
this.div.onmouseleave = _ => {
if (buttons) {
buttons.remove();
buttons = undefined;
}
};
}
}
updateReactions() {
const reactdiv = this.reactdiv.deref();
if (!reactdiv)
return;
const func = this.channel.infinite.snapBottom();
reactdiv.innerHTML = "";
for (const thing of this.reactions) {
const reaction = document.createElement("div");
reaction.classList.add("reaction");
if (thing.me) {
reaction.classList.add("meReacted");
}
let emoji;
if (thing.emoji.id || /\d{17,21}/.test(thing.emoji.name)) {
if (/\d{17,21}/.test(thing.emoji.name))
thing.emoji.id = thing.emoji.name; //Should stop being a thing once the server fixes this bug
const emo = new Emoji(thing.emoji, this.guild);
emoji = emo.getHTML(false);
}
else {
emoji = document.createElement("p");
emoji.textContent = thing.emoji.name;
}
const count = document.createElement("p");
count.textContent = "" + thing.count;
count.classList.add("reactionCount");
reaction.append(count);
reaction.append(emoji);
reactdiv.append(reaction);
reaction.onclick = _ => {
this.reactionToggle(thing.emoji.name);
};
}
func();
}
reactionAdd(data, member) {
for (const thing of this.reactions) {
if (thing.emoji.name === data.name) {
thing.count++;
if (member.id === this.localuser.user.id) {
thing.me = true;
this.updateReactions();
return;
}
}
}
this.reactions.push({
count: 1,
emoji: data,
me: member.id === this.localuser.user.id
});
this.updateReactions();
}
reactionRemove(data, id) {
console.log("test");
for (const i in this.reactions) {
const thing = this.reactions[i];
console.log(thing, data);
if (thing.emoji.name === data.name) {
thing.count--;
if (thing.count === 0) {
this.reactions.splice(Number(i), 1);
this.updateReactions();
return;
}
if (id === this.localuser.user.id) {
thing.me = false;
this.updateReactions();
return;
}
}
}
}
reactionRemoveAll() {
this.reactions = [];
this.updateReactions();
}
reactionRemoveEmoji(emoji) {
for (const i in this.reactions) {
const reaction = this.reactions[i];
if ((reaction.emoji.id && reaction.emoji.id == emoji.id) || (!reaction.emoji.id && reaction.emoji.name == emoji.name)) {
this.reactions.splice(Number(i), 1);
this.updateReactions();
break;
}
}
}
buildhtml(premessage) {
if (this.div) {
console.error(`HTML for ${this.id} already exists, aborting`);
return this.div;
}
try {
const div = document.createElement("div");
this.div = div;
this.messageevents(div);
return this.generateMessage(premessage);
}
catch (e) {
console.error(e);
}
return this.div;
}
}
let now;
let yesterdayStr;
function formatTime(date) {
updateTimes();
const datestring = date.toLocaleDateString();
const formatTime = (date) => date.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" });
if (datestring === now) {
return `Today at ${formatTime(date)}`;
}
else if (datestring === yesterdayStr) {
return `Yesterday at ${formatTime(date)}`;
}
else {
return `${date.toLocaleDateString()} at ${formatTime(date)}`;
}
}
let tomorrow = 0;
updateTimes();
function updateTimes() {
if (tomorrow < Date.now()) {
const d = new Date();
tomorrow = d.setHours(24, 0, 0, 0);
now = new Date().toLocaleDateString();
const yesterday = new Date(now);
yesterday.setDate(new Date().getDate() - 1);
yesterdayStr = yesterday.toLocaleDateString();
}
}
Message.setup();
export { Message };

View file

@ -1,328 +0,0 @@
class Permissions {
allow;
deny;
hasDeny;
constructor(allow, deny = "") {
this.hasDeny = Boolean(deny);
try {
this.allow = BigInt(allow);
this.deny = BigInt(deny);
}
catch {
this.allow = 0n;
this.deny = 0n;
console.error(`Something really stupid happened with a permission with allow being ${allow} and deny being, ${deny}, execution will still happen, but something really stupid happened, please report if you know what caused this.`);
}
}
getPermissionbit(b, big) {
return Boolean((big >> BigInt(b)) & 1n);
}
setPermissionbit(b, state, big) {
const bit = 1n << BigInt(b);
return (big & ~bit) | (BigInt(state) << BigInt(b)); //thanks to geotale for this code :3
}
static map;
static info;
static makeMap() {
Permissions.info = [
{
name: "CREATE_INSTANT_INVITE",
readableName: "Create invite",
description: "Allows the user to create invites for the guild"
},
{
name: "KICK_MEMBERS",
readableName: "Kick members",
description: "Allows the user to kick members from the guild"
},
{
name: "BAN_MEMBERS",
readableName: "Ban members",
description: "Allows the user to ban members from the guild"
},
{
name: "ADMINISTRATOR",
readableName: "Administrator",
description: "Allows all permissions and bypasses channel permission overwrites. This is a dangerous permission!"
},
{
name: "MANAGE_CHANNELS",
readableName: "Manage channels",
description: "Allows the user to manage and edit channels"
},
{
name: "MANAGE_GUILD",
readableName: "Manage guild",
description: "Allows management and editing of the guild"
},
{
name: "ADD_REACTIONS",
readableName: "Add reactions",
description: "Allows user to add reactions to messages"
},
{
name: "VIEW_AUDIT_LOG",
readableName: "View audit log",
description: "Allows the user to view the audit log"
},
{
name: "PRIORITY_SPEAKER",
readableName: "Priority speaker",
description: "Allows for using priority speaker in a voice channel"
},
{
name: "STREAM",
readableName: "Video",
description: "Allows the user to stream"
},
{
name: "VIEW_CHANNEL",
readableName: "View channels",
description: "Allows the user to view the channel"
},
{
name: "SEND_MESSAGES",
readableName: "Send messages",
description: "Allows user to send messages"
},
{
name: "SEND_TTS_MESSAGES",
readableName: "Send text-to-speech messages",
description: "Allows the user to send text-to-speech messages"
},
{
name: "MANAGE_MESSAGES",
readableName: "Manage messages",
description: "Allows the user to delete messages that aren't their own"
},
{
name: "EMBED_LINKS",
readableName: "Embed links",
description: "Allow links sent by this user to auto-embed"
},
{
name: "ATTACH_FILES",
readableName: "Attach files",
description: "Allows the user to attach files"
},
{
name: "READ_MESSAGE_HISTORY",
readableName: "Read message history",
description: "Allows user to read the message history"
},
{
name: "MENTION_EVERYONE",
readableName: "Mention @everyone, @here and all roles",
description: "Allows the user to mention everyone"
},
{
name: "USE_EXTERNAL_EMOJIS",
readableName: "Use external emojis",
description: "Allows the user to use external emojis"
},
{
name: "VIEW_GUILD_INSIGHTS",
readableName: "View guild insights",
description: "Allows the user to see guild insights"
},
{
name: "CONNECT",
readableName: "Connect",
description: "Allows the user to connect to a voice channel"
},
{
name: "SPEAK",
readableName: "Speak",
description: "Allows the user to speak in a voice channel"
},
{
name: "MUTE_MEMBERS",
readableName: "Mute members",
description: "Allows user to mute other members"
},
{
name: "DEAFEN_MEMBERS",
readableName: "Deafen members",
description: "Allows user to deafen other members"
},
{
name: "MOVE_MEMBERS",
readableName: "Move members",
description: "Allows the user to move members between voice channels"
},
{
name: "USE_VAD",
readableName: "Use voice activity detection",
description: "Allows users to speak in a voice channel by simply talking"
},
{
name: "CHANGE_NICKNAME",
readableName: "Change nickname",
description: "Allows the user to change their own nickname"
},
{
name: "MANAGE_NICKNAMES",
readableName: "Manage nicknames",
description: "Allows user to change nicknames of other members"
},
{
name: "MANAGE_ROLES",
readableName: "Manage roles",
description: "Allows user to edit and manage roles"
},
{
name: "MANAGE_WEBHOOKS",
readableName: "Manage webhooks",
description: "Allows management and editing of webhooks"
},
{
name: "MANAGE_GUILD_EXPRESSIONS",
readableName: "Manage expressions",
description: "Allows for managing emoji, stickers, and soundboards"
},
{
name: "USE_APPLICATION_COMMANDS",
readableName: "Use application commands",
description: "Allows the user to use application commands"
},
{
name: "REQUEST_TO_SPEAK",
readableName: "Request to speak",
description: "Allows user to request to speak in stage channel"
},
{
name: "MANAGE_EVENTS",
readableName: "Manage events",
description: "Allows user to edit and manage events"
},
{
name: "MANAGE_THREADS",
readableName: "Manage threads",
description: "Allows the user to delete and archive threads and view all private threads"
},
{
name: "CREATE_PUBLIC_THREADS",
readableName: "Create public threads",
description: "Allows the user to create public threads"
},
{
name: "CREATE_PRIVATE_THREADS",
readableName: "Create private threads",
description: "Allows the user to create private threads"
},
{
name: "USE_EXTERNAL_STICKERS",
readableName: "Use external stickers",
description: "Allows user to use external stickers"
},
{
name: "SEND_MESSAGES_IN_THREADS",
readableName: "Send messages in threads",
description: "Allows the user to send messages in threads"
},
{
name: "USE_EMBEDDED_ACTIVITIES",
readableName: "Use activities",
description: "Allows the user to use embedded activities"
},
{
name: "MODERATE_MEMBERS",
readableName: "Timeout members",
description: "Allows the user to time out other users to prevent them from sending or reacting to messages in chat and threads, and from speaking in voice and stage channels"
},
{
name: "VIEW_CREATOR_MONETIZATION_ANALYTICS",
readableName: "View creator monetization analytics",
description: "Allows for viewing role subscription insights"
},
{
name: "USE_SOUNDBOARD",
readableName: "Use soundboard",
description: "Allows for using soundboard in a voice channel"
},
{
name: "CREATE_GUILD_EXPRESSIONS",
readableName: "Create expressions",
description: "Allows for creating emojis, stickers, and soundboard sounds, and editing and deleting those created by the current user."
},
{
name: "CREATE_EVENTS",
readableName: "Create events",
description: "Allows for creating scheduled events, and editing and deleting those created by the current user."
},
{
name: "USE_EXTERNAL_SOUNDS",
readableName: "Use external sounds",
description: "Allows the usage of custom soundboard sounds from other servers"
},
{
name: "SEND_VOICE_MESSAGES",
readableName: "Send voice messages",
description: "Allows sending voice messages"
},
{
name: "SEND_POLLS",
readableName: "Create polls",
description: "Allows sending polls"
},
{
name: "USE_EXTERNAL_APPS",
readableName: "Use external apps",
description: "Allows user-installed apps to send public responses. " +
"When disabled, users will still be allowed to use their apps but the responses will be ephemeral. " +
"This only applies to apps not also installed to the server."
},
];
Permissions.map = {};
let i = 0;
for (const thing of Permissions.info) {
Permissions.map[i] = thing;
Permissions.map[thing.name] = i;
i++;
}
}
getPermission(name) {
if (this.getPermissionbit(Permissions.map[name], this.allow)) {
return 1;
}
else if (this.getPermissionbit(Permissions.map[name], this.deny)) {
return -1;
}
else {
return 0;
}
}
hasPermission(name) {
if (this.deny) {
console.warn("This function may of been used in error, think about using getPermision instead");
}
if (this.getPermissionbit(Permissions.map[name], this.allow))
return true;
if (name != "ADMINISTRATOR")
return this.hasPermission("ADMINISTRATOR");
return false;
}
setPermission(name, setto) {
const bit = Permissions.map[name];
if (!bit) {
return console.error("Tried to set permission to " + setto + " for " + name + " but it doesn't exist");
}
if (setto === 0) {
this.deny = this.setPermissionbit(bit, false, this.deny);
this.allow = this.setPermissionbit(bit, false, this.allow);
}
else if (setto === 1) {
this.deny = this.setPermissionbit(bit, false, this.deny);
this.allow = this.setPermissionbit(bit, true, this.allow);
}
else if (setto === -1) {
this.deny = this.setPermissionbit(bit, true, this.deny);
this.allow = this.setPermissionbit(bit, false, this.allow);
}
else {
console.error("invalid number entered:" + setto);
}
}
}
Permissions.makeMap();
export { Permissions };

View file

@ -1,118 +0,0 @@
import { checkInstance, adduser } from "./login.js";
if (document.getElementById("register")) {
document.getElementById("register").addEventListener("submit", registertry);
}
async function registertry(e) {
e.preventDefault();
const elements = e.srcElement;
const email = elements[1].value;
const username = elements[2].value;
if (elements[3].value !== elements[4].value) {
document.getElementById("wrong").textContent = "Passwords don't match";
return;
}
const password = elements[3].value;
const dateofbirth = elements[5].value;
const apiurl = new URL(JSON.parse(localStorage.getItem("instanceinfo")).api);
await fetch(apiurl + "/auth/register", {
body: JSON.stringify({
date_of_birth: dateofbirth,
email,
username,
password,
consent: elements[6].checked,
captcha_key: elements[7]?.value
}),
headers: {
"content-type": "application/json"
},
method: "POST"
}).then(e => {
e.json().then(e => {
if (e.captcha_sitekey) {
const capt = document.getElementById("h-captcha");
if (!capt.children.length) {
const capty = document.createElement("div");
capty.classList.add("h-captcha");
capty.setAttribute("data-sitekey", e.captcha_sitekey);
const script = document.createElement("script");
script.src = "https://js.hcaptcha.com/1/api.js";
capt.append(script);
capt.append(capty);
}
else {
eval("hcaptcha.reset()");
}
return;
}
if (!e.token) {
console.log(e);
if (e.errors.consent) {
error(elements[6], e.errors.consent._errors[0].message);
}
else if (e.errors.password) {
error(elements[3], "Password: " + e.errors.password._errors[0].message);
}
else if (e.errors.username) {
error(elements[2], "Username: " + e.errors.username._errors[0].message);
}
else if (e.errors.email) {
error(elements[1], "Email: " + e.errors.email._errors[0].message);
}
else if (e.errors.date_of_birth) {
error(elements[5], "Date of Birth: " + e.errors.date_of_birth._errors[0].message);
}
else {
document.getElementById("wrong").textContent = e.errors[Object.keys(e.errors)[0]]._errors[0].message;
}
}
else {
adduser({ serverurls: JSON.parse(localStorage.getItem("instanceinfo")), email, token: e.token }).username = username;
localStorage.setItem("token", e.token);
const redir = new URLSearchParams(window.location.search).get("goback");
if (redir) {
window.location.href = redir;
}
else {
window.location.href = "/channels/@me";
}
}
});
});
//document.getElementById("wrong").textContent=h;
// console.log(h);
}
function error(e, message) {
const p = e.parentElement;
let element = p.getElementsByClassName("suberror")[0];
if (!element) {
const div = document.createElement("div");
div.classList.add("suberror", "suberrora");
p.append(div);
element = div;
}
else {
element.classList.remove("suberror");
setTimeout(_ => {
element.classList.add("suberror");
}, 100);
}
element.textContent = message;
}
let TOSa = document.getElementById("TOSa");
async function tosLogic() {
const apiurl = new URL(JSON.parse(localStorage.getItem("instanceinfo")).api);
const tosPage = (await (await fetch(apiurl.toString() + "/ping")).json()).instance.tosPage;
if (tosPage) {
document.getElementById("TOSbox").innerHTML = "I agree to the <a href=\"\" id=\"TOSa\">Terms of Service</a>:";
TOSa = document.getElementById("TOSa");
TOSa.href = tosPage;
}
else {
document.getElementById("TOSbox").textContent = "This instance has no Terms of Service, accept ToS anyways:";
TOSa = null;
}
console.log(tosPage);
}
tosLogic();
checkInstance["alt"] = tosLogic;

View file

@ -1,160 +0,0 @@
import { Permissions } from "./permissions.js";
import { SnowFlake } from "./snowflake.js";
class Role extends SnowFlake {
permissions;
owner;
color;
name;
info;
hoist;
icon;
mentionable;
unicode_emoji;
headers;
constructor(json, owner) {
super(json.id);
this.headers = owner.headers;
this.info = owner.info;
for (const thing of Object.keys(json)) {
if (thing === "id") {
continue;
}
this[thing] = json[thing];
}
this.permissions = new Permissions(json.permissions);
this.owner = owner;
}
get guild() {
return this.owner;
}
get localuser() {
return this.guild.localuser;
}
getColor() {
if (this.color === 0) {
return null;
}
return `#${this.color.toString(16)}`;
}
}
export { Role };
import { Options } from "./settings.js";
class PermissionToggle {
rolejson;
permissions;
owner;
value;
constructor(roleJSON, permissions, owner) {
this.rolejson = roleJSON;
this.permissions = permissions;
this.owner = owner;
}
watchForChange() { }
generateHTML() {
const div = document.createElement("div");
div.classList.add("setting");
const name = document.createElement("span");
name.textContent = this.rolejson.readableName;
name.classList.add("settingsname");
div.append(name);
div.append(this.generateCheckbox());
const p = document.createElement("p");
p.textContent = this.rolejson.description;
div.appendChild(p);
return div;
}
generateCheckbox() {
const div = document.createElement("div");
div.classList.add("tritoggle");
const state = this.permissions.getPermission(this.rolejson.name);
const on = document.createElement("input");
on.type = "radio";
on.name = this.rolejson.name;
div.append(on);
if (state === 1) {
on.checked = true;
}
on.onclick = _ => {
this.permissions.setPermission(this.rolejson.name, 1);
this.owner.changed();
};
const no = document.createElement("input");
no.type = "radio";
no.name = this.rolejson.name;
div.append(no);
if (state === 0) {
no.checked = true;
}
no.onclick = _ => {
this.permissions.setPermission(this.rolejson.name, 0);
this.owner.changed();
};
if (this.permissions.hasDeny) {
const off = document.createElement("input");
off.type = "radio";
off.name = this.rolejson.name;
div.append(off);
if (state === -1) {
off.checked = true;
}
off.onclick = _ => {
this.permissions.setPermission(this.rolejson.name, -1);
this.owner.changed();
};
}
return div;
}
submit() {
}
}
import { Buttons } from "./settings.js";
class RoleList extends Buttons {
permissions;
permission;
guild;
channel;
options;
onchange;
curid;
constructor(permissions, guild, onchange, channel = false) {
super("Roles");
this.guild = guild;
this.permissions = permissions;
this.channel = channel;
this.onchange = onchange;
const options = new Options("", this);
if (channel) {
this.permission = new Permissions("0", "0");
}
else {
this.permission = new Permissions("0");
}
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;
}
handleString(str) {
this.curid = str;
const arr = this.permissions.find(_ => _[0].id === str);
if (arr) {
const perm = arr[1];
this.permission.deny = perm.deny;
this.permission.allow = perm.allow;
const role = this.permissions.find(e => e[0].id === str);
if (role) {
this.options.name = role[0].name;
this.options.haschanged = false;
}
}
return this.options.generateHTML();
}
save() {
this.onchange(this.curid, this.permission);
}
}
export { RoleList };

View file

@ -1,94 +0,0 @@
function deleteoldcache() {
caches.delete("cache");
console.log("this ran :P");
}
async function putInCache(request, response) {
console.log(request, response);
const cache = await caches.open("cache");
console.log("Grabbed");
try {
console.log(await cache.put(request, response));
}
catch (error) {
console.error(error);
}
}
console.log("test");
let lastcache;
self.addEventListener("activate", async (event) => {
console.log("test2");
checkCache();
});
async function checkCache() {
if (checkedrecently) {
return;
}
const promise = await caches.match("/getupdates");
if (promise) {
lastcache = await promise.text();
}
console.log(lastcache);
fetch("/getupdates").then(async (data) => {
const text = await data.clone().text();
console.log(text, lastcache);
if (lastcache !== text) {
deleteoldcache();
putInCache("/getupdates", data.clone());
}
checkedrecently = true;
setTimeout(_ => {
checkedrecently = false;
}, 1000 * 60 * 30);
});
}
var checkedrecently = false;
function samedomain(url) {
return new URL(url).origin === self.origin;
}
function isindexhtml(url) {
console.log(url);
if (new URL(url).pathname.startsWith("/channels")) {
return true;
}
return false;
}
async function getfile(event) {
checkCache();
if (!samedomain(event.request.url)) {
return await fetch(event.request.clone());
}
const responseFromCache = await caches.match(event.request.url);
console.log(responseFromCache, caches);
if (responseFromCache) {
console.log("cache hit");
return responseFromCache;
}
if (isindexhtml(event.request.url)) {
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 {
return responseFromNetwork;
}
catch (e) {
console.error(e);
}
}
self.addEventListener("fetch", (event) => {
try {
event.respondWith(getfile(event));
}
catch (e) {
console.error(e);
}
});

View file

@ -1,937 +0,0 @@
//future me stuff
class Buttons {
name;
buttons;
buttonList;
warndiv;
value;
constructor(name) {
this.buttons = [];
this.name = name;
}
add(name, thing) {
if (!thing) {
thing = new Options(name, this);
}
this.buttons.push([name, thing]);
return thing;
}
generateHTML() {
const buttonList = document.createElement("div");
buttonList.classList.add("Buttons");
buttonList.classList.add("flexltr");
this.buttonList = buttonList;
const htmlarea = document.createElement("div");
htmlarea.classList.add("flexgrow");
const buttonTable = document.createElement("div");
buttonTable.classList.add("flexttb", "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);
if (this.warndiv) {
this.warndiv.remove();
}
};
buttonTable.append(button);
}
this.generateHTMLArea(this.buttons[0][1], htmlarea);
buttonList.append(buttonTable);
buttonList.append(htmlarea);
return buttonList;
}
handleString(str) {
const div = document.createElement("span");
div.textContent = str;
return div;
}
generateHTMLArea(buttonInfo, htmlarea) {
let html;
if (buttonInfo instanceof Options) {
buttonInfo.subOptions = undefined;
html = buttonInfo.generateHTML();
}
else {
html = this.handleString(buttonInfo);
}
htmlarea.innerHTML = "";
htmlarea.append(html);
}
changed(html) {
this.warndiv = html;
this.buttonList.append(html);
}
watchForChange() { }
save() { }
submit() {
}
}
class TextInput {
label;
owner;
onSubmit;
value;
input;
password;
constructor(label, onSubmit, owner, { initText = "", password = false } = {}) {
this.label = label;
this.value = initText;
this.owner = owner;
this.onSubmit = onSubmit;
this.password = password;
}
generateHTML() {
const div = document.createElement("div");
const span = document.createElement("span");
span.textContent = this.label;
div.append(span);
const input = document.createElement("input");
input.value = this.value;
input.type = this.password ? "password" : "text";
input.oninput = this.onChange.bind(this);
this.input = new WeakRef(input);
div.append(input);
return div;
}
onChange(ev) {
this.owner.changed();
const input = this.input.deref();
if (input) {
const value = input.value;
this.onchange(value);
this.value = value;
}
}
onchange = _ => { };
watchForChange(func) {
this.onchange = func;
}
submit() {
this.onSubmit(this.value);
}
}
class SettingsText {
onSubmit;
value;
text;
constructor(text) {
this.text = text;
}
generateHTML() {
const span = document.createElement("span");
span.innerText = this.text;
return span;
}
watchForChange() { }
submit() { }
}
class SettingsTitle {
onSubmit;
value;
text;
constructor(text) {
this.text = text;
}
generateHTML() {
const span = document.createElement("h2");
span.innerText = this.text;
return span;
}
watchForChange() { }
submit() { }
}
class CheckboxInput {
label;
owner;
onSubmit;
value;
input;
constructor(label, onSubmit, owner, { initState = false } = {}) {
this.label = label;
this.value = initState;
this.owner = owner;
this.onSubmit = onSubmit;
}
generateHTML() {
const div = document.createElement("div");
const span = document.createElement("span");
span.textContent = this.label;
div.append(span);
const input = document.createElement("input");
input.type = "checkbox";
input.checked = this.value;
input.oninput = this.onChange.bind(this);
this.input = new WeakRef(input);
div.append(input);
return div;
}
onChange(ev) {
this.owner.changed();
const input = this.input.deref();
if (input) {
const value = input.checked;
this.onchange(value);
this.value = value;
}
}
onchange = _ => { };
watchForChange(func) {
this.onchange = func;
}
submit() {
this.onSubmit(this.value);
}
}
class ButtonInput {
label;
owner;
onClick;
textContent;
value;
constructor(label, textContent, onClick, owner, {} = {}) {
this.label = label;
this.owner = owner;
this.onClick = onClick;
this.textContent = textContent;
}
generateHTML() {
const div = document.createElement("div");
const span = document.createElement("span");
span.textContent = this.label;
div.append(span);
const button = document.createElement("button");
button.textContent = this.textContent;
button.onclick = this.onClickEvent.bind(this);
div.append(button);
return div;
}
onClickEvent(ev) {
this.onClick();
}
watchForChange() { }
submit() { }
}
class ColorInput {
label;
owner;
onSubmit;
colorContent;
input;
value;
constructor(label, onSubmit, owner, { initColor = "" } = {}) {
this.label = label;
this.colorContent = initColor;
this.owner = owner;
this.onSubmit = onSubmit;
}
generateHTML() {
const div = document.createElement("div");
const span = document.createElement("span");
span.textContent = this.label;
div.append(span);
const input = document.createElement("input");
input.value = this.colorContent;
input.type = "color";
input.oninput = this.onChange.bind(this);
this.input = new WeakRef(input);
div.append(input);
return div;
}
onChange(ev) {
this.owner.changed();
const input = this.input.deref();
if (input) {
const value = input.value;
this.value = value;
this.onchange(value);
this.colorContent = value;
}
}
onchange = _ => { };
watchForChange(func) {
this.onchange = func;
}
submit() {
this.onSubmit(this.colorContent);
}
}
class SelectInput {
label;
owner;
onSubmit;
options;
index;
select;
get value() {
return this.index;
}
constructor(label, onSubmit, options, owner, { defaultIndex = 0 } = {}) {
this.label = label;
this.index = defaultIndex;
this.owner = owner;
this.onSubmit = onSubmit;
this.options = options;
}
generateHTML() {
const div = document.createElement("div");
const span = document.createElement("span");
span.textContent = this.label;
div.append(span);
const select = document.createElement("select");
select.onchange = this.onChange.bind(this);
for (const thing of this.options) {
const option = document.createElement("option");
option.textContent = thing;
select.appendChild(option);
}
this.select = new WeakRef(select);
select.selectedIndex = this.index;
div.append(select);
return div;
}
onChange(ev) {
this.owner.changed();
const select = this.select.deref();
if (select) {
const value = select.selectedIndex;
this.onchange(value);
this.index = value;
}
}
onchange = _ => { };
watchForChange(func) {
this.onchange = func;
}
submit() {
this.onSubmit(this.index);
}
}
class MDInput {
label;
owner;
onSubmit;
value;
input;
constructor(label, onSubmit, owner, { initText = "" } = {}) {
this.label = label;
this.value = initText;
this.owner = owner;
this.onSubmit = onSubmit;
}
generateHTML() {
const div = document.createElement("div");
const span = document.createElement("span");
span.textContent = this.label;
div.append(span);
div.append(document.createElement("br"));
const input = document.createElement("textarea");
input.value = this.value;
input.oninput = this.onChange.bind(this);
this.input = new WeakRef(input);
div.append(input);
return div;
}
onChange(ev) {
this.owner.changed();
const input = this.input.deref();
if (input) {
const value = input.value;
this.onchange(value);
this.value = value;
}
}
onchange = _ => { };
watchForChange(func) {
this.onchange = func;
}
submit() {
this.onSubmit(this.value);
}
}
class FileInput {
label;
owner;
onSubmit;
input;
value;
clear;
constructor(label, onSubmit, owner, { clear = false } = {}) {
this.label = label;
this.owner = owner;
this.onSubmit = onSubmit;
this.clear = clear;
}
generateHTML() {
const div = document.createElement("div");
const span = document.createElement("span");
span.textContent = this.label;
div.append(span);
const input = document.createElement("input");
input.type = "file";
input.oninput = this.onChange.bind(this);
this.input = new WeakRef(input);
div.append(input);
if (this.clear) {
const button = document.createElement("button");
button.textContent = "Clear";
button.onclick = _ => {
if (this.onchange) {
this.onchange(null);
}
this.value = null;
this.owner.changed();
};
div.append(button);
}
return div;
}
onChange(ev) {
this.owner.changed();
const input = this.input.deref();
if (input) {
this.value = input.files;
if (this.onchange) {
this.onchange(input.files);
}
}
}
onchange = null;
watchForChange(func) {
this.onchange = func;
}
submit() {
const input = this.input.deref();
if (input) {
this.onSubmit(input.files);
}
}
}
class HtmlArea {
submit;
html;
value;
constructor(html, submit) {
this.submit = submit;
this.html = html;
}
generateHTML() {
if (this.html instanceof Function) {
return this.html();
}
else {
return this.html;
}
}
watchForChange() { }
}
class Options {
name;
haschanged = false;
options;
owner;
ltr;
value;
html = new WeakMap();
container = new WeakRef(document.createElement("div"));
constructor(name, owner, { ltr = false } = {}) {
this.name = name;
this.options = [];
this.owner = owner;
this.ltr = ltr;
}
removeAll() {
while (this.options.length) {
this.options.pop();
}
const container = this.container.deref();
if (container) {
container.innerHTML = "";
}
}
watchForChange() { }
addOptions(name, { ltr = false } = {}) {
const options = new Options(name, this, { ltr });
this.options.push(options);
this.generate(options);
return options;
}
subOptions;
addSubOptions(name, { ltr = false } = {}) {
const options = new Options(name, this, { ltr });
this.subOptions = options;
const container = this.container.deref();
if (container) {
this.generateContainter();
}
else {
throw new Error("Tried to make a subOptions when the options weren't rendered");
}
return options;
}
addSubForm(name, onSubmit, { ltr = false, submitText = "Submit", fetchURL = "", headers = {}, method = "POST", traditionalSubmit = false } = {}) {
const options = new Form(name, this, onSubmit, { ltr, submitText, fetchURL, headers, method, traditionalSubmit });
this.subOptions = options;
const container = this.container.deref();
if (container) {
this.generateContainter();
}
else {
throw new Error("Tried to make a subForm when the options weren't rendered");
}
return options;
}
returnFromSub() {
this.subOptions = undefined;
this.generateContainter();
}
addSelect(label, onSubmit, selections, { defaultIndex = 0 } = {}) {
const select = new SelectInput(label, onSubmit, selections, this, { defaultIndex });
this.options.push(select);
this.generate(select);
return select;
}
addFileInput(label, onSubmit, { clear = false } = {}) {
const FI = new FileInput(label, onSubmit, this, { clear });
this.options.push(FI);
this.generate(FI);
return FI;
}
addTextInput(label, onSubmit, { initText = "", password = false } = {}) {
const textInput = new TextInput(label, onSubmit, this, { initText, password });
this.options.push(textInput);
this.generate(textInput);
return textInput;
}
addColorInput(label, onSubmit, { initColor = "" } = {}) {
const colorInput = new ColorInput(label, onSubmit, this, { initColor });
this.options.push(colorInput);
this.generate(colorInput);
return colorInput;
}
addMDInput(label, onSubmit, { initText = "" } = {}) {
const mdInput = new MDInput(label, onSubmit, this, { initText });
this.options.push(mdInput);
this.generate(mdInput);
return mdInput;
}
addHTMLArea(html, submit = () => { }) {
const htmlarea = new HtmlArea(html, submit);
this.options.push(htmlarea);
this.generate(htmlarea);
return htmlarea;
}
addButtonInput(label, textContent, onSubmit) {
const button = new ButtonInput(label, textContent, onSubmit, this);
this.options.push(button);
this.generate(button);
return button;
}
addCheckboxInput(label, onSubmit, { initState = false } = {}) {
const box = new CheckboxInput(label, onSubmit, this, { initState });
this.options.push(box);
this.generate(box);
return box;
}
addText(str) {
const text = new SettingsText(str);
this.options.push(text);
this.generate(text);
return text;
}
addTitle(str) {
const text = new SettingsTitle(str);
this.options.push(text);
this.generate(text);
return text;
}
addForm(name, onSubmit, { ltr = false, submitText = "Submit", fetchURL = "", headers = {}, method = "POST", traditionalSubmit = false } = {}) {
const options = new Form(name, this, onSubmit, { ltr, submitText, fetchURL, headers, method, traditionalSubmit });
this.options.push(options);
this.generate(options);
return options;
}
generate(elm) {
const container = this.container.deref();
if (container) {
const div = document.createElement("div");
if (!(elm instanceof Options)) {
div.classList.add("optionElement");
}
const html = elm.generateHTML();
div.append(html);
this.html.set(elm, new WeakRef(div));
container.append(div);
}
}
title = new WeakRef(document.createElement("h2"));
generateHTML() {
const div = document.createElement("div");
div.classList.add("titlediv");
const title = document.createElement("h2");
title.textContent = this.name;
div.append(title);
if (this.name !== "")
title.classList.add("settingstitle");
this.title = new WeakRef(title);
const container = document.createElement("div");
this.container = new WeakRef(container);
container.classList.add(this.ltr ? "flexltr" : "flexttb", "flexspace");
this.generateContainter();
div.append(container);
return div;
}
generateContainter() {
const container = this.container.deref();
if (container) {
const title = this.title.deref();
if (title)
title.innerHTML = "";
container.innerHTML = "";
if (this.subOptions) {
container.append(this.subOptions.generateHTML()); //more code needed, though this is enough for now
if (title) {
const name = document.createElement("span");
name.innerText = this.name;
name.classList.add("clickable");
name.onclick = () => {
this.returnFromSub();
};
title.append(name, " > ", this.subOptions.name);
}
}
else {
for (const thing of this.options) {
this.generate(thing);
}
if (title) {
title.innerText = this.name;
}
}
if (title && title.innerText !== "") {
title.classList.add("settingstitle");
}
else if (title) {
title.classList.remove("settingstitle");
}
}
else {
console.warn("tried to generate container, but it did not exist");
}
}
changed() {
if (this.owner instanceof Options || this.owner instanceof Form) {
this.owner.changed();
return;
}
if (!this.haschanged) {
const div = document.createElement("div");
div.classList.add("flexltr", "savediv");
const span = document.createElement("span");
div.append(span);
span.textContent = "Careful, you have unsaved changes";
const button = document.createElement("button");
button.textContent = "Save changes";
div.append(button);
this.haschanged = true;
this.owner.changed(div);
button.onclick = _ => {
if (this.owner instanceof Buttons) {
this.owner.save();
}
div.remove();
this.submit();
};
}
}
submit() {
this.haschanged = false;
for (const thing of this.options) {
thing.submit();
}
}
}
class FormError extends Error {
elem;
message;
constructor(elem, message) {
super(message);
this.message = message;
this.elem = elem;
}
}
export { FormError };
class Form {
name;
options;
owner;
ltr;
names = new Map();
required = new WeakSet();
submitText;
fetchURL;
headers = {};
method;
value;
traditionalSubmit;
values = {};
constructor(name, owner, onSubmit, { ltr = false, submitText = "Submit", fetchURL = "", headers = {}, method = "POST", traditionalSubmit = false } = {}) {
this.traditionalSubmit = traditionalSubmit;
this.name = name;
this.method = method;
this.submitText = submitText;
this.options = new Options("", this, { ltr });
this.owner = owner;
this.fetchURL = fetchURL;
this.headers = headers;
this.ltr = ltr;
this.onSubmit = onSubmit;
}
setValue(key, value) {
this.values[key] = value;
}
addSelect(label, formName, selections, { defaultIndex = 0, required = false } = {}) {
const select = this.options.addSelect(label, _ => { }, selections, { defaultIndex });
this.names.set(formName, select);
if (required) {
this.required.add(select);
}
return select;
}
fileOptions = new Map();
addFileInput(label, formName, { required = false, files = "one", clear = false } = {}) {
const FI = this.options.addFileInput(label, _ => { }, { clear });
if (files !== "one" && files !== "multi")
throw new Error("files should equal one or multi");
this.fileOptions.set(FI, { files });
this.names.set(formName, FI);
if (required) {
this.required.add(FI);
}
return FI;
}
addTextInput(label, formName, { initText = "", required = false, password = false } = {}) {
const textInput = this.options.addTextInput(label, _ => { }, { initText, password });
this.names.set(formName, textInput);
if (required) {
this.required.add(textInput);
}
return textInput;
}
addColorInput(label, formName, { initColor = "", required = false } = {}) {
const colorInput = this.options.addColorInput(label, _ => { }, { initColor });
this.names.set(formName, colorInput);
if (required) {
this.required.add(colorInput);
}
return colorInput;
}
addMDInput(label, formName, { initText = "", required = false } = {}) {
const mdInput = this.options.addMDInput(label, _ => { }, { initText });
this.names.set(formName, mdInput);
if (required) {
this.required.add(mdInput);
}
return mdInput;
}
addCheckboxInput(label, formName, { initState = false, required = false } = {}) {
const box = this.options.addCheckboxInput(label, _ => { }, { initState });
this.names.set(formName, box);
if (required) {
this.required.add(box);
}
return box;
}
addText(str) {
this.options.addText(str);
}
addTitle(str) {
this.options.addTitle(str);
}
generateHTML() {
const div = document.createElement("div");
div.append(this.options.generateHTML());
div.classList.add("FormSettings");
if (!this.traditionalSubmit) {
const button = document.createElement("button");
button.onclick = _ => {
this.submit();
};
button.textContent = this.submitText;
div.append(button);
}
return div;
}
onSubmit;
watchForChange(func) {
this.onSubmit = func;
}
changed() {
if (this.traditionalSubmit) {
this.owner.changed();
}
}
async submit() {
const build = {};
for (const key of Object.keys(this.values)) {
const thing = this.values[key];
if (thing instanceof Function) {
try {
build[key] = thing();
}
catch (e) {
if (e instanceof FormError) {
const elm = this.options.html.get(e.elem);
if (elm) {
const html = elm.deref();
if (html) {
this.makeError(html, e.message);
}
}
}
return;
}
}
else {
build[key] = thing;
}
}
const promises = [];
for (const thing of this.names.keys()) {
if (thing === "")
continue;
const input = this.names.get(thing);
if (input instanceof SelectInput) {
build[thing] = input.options[input.value];
continue;
}
else if (input instanceof FileInput) {
const options = this.fileOptions.get(input);
if (!options) {
throw new Error("FileInput without its options is in this form, this should never happen.");
}
if (options.files === "one") {
console.log(input.value);
if (input.value) {
const reader = new FileReader();
reader.readAsDataURL(input.value[0]);
const promise = new Promise((res) => {
reader.onload = () => {
build[thing] = reader.result;
res();
};
});
promises.push(promise);
}
}
else {
console.error(options.files + " is not currently implemented");
}
}
build[thing] = input.value;
}
await Promise.allSettled(promises);
if (this.fetchURL !== "") {
fetch(this.fetchURL, {
method: this.method,
body: JSON.stringify(build),
headers: this.headers
}).then(_ => _.json()).then(json => {
if (json.errors && this.errors(json.errors))
return;
this.onSubmit(json);
});
}
else {
this.onSubmit(build);
}
console.warn("needs to be implemented");
}
errors(errors) {
if (!(errors instanceof Object)) {
return;
}
for (const error of Object.keys(errors)) {
const elm = this.names.get(error);
if (elm) {
const ref = this.options.html.get(elm);
if (ref && ref.deref()) {
const html = ref.deref();
this.makeError(html, errors[error]._errors[0].message);
return true;
}
}
}
return false;
}
error(formElm, errorMessage) {
const elm = this.names.get(formElm);
if (elm) {
const htmlref = this.options.html.get(elm);
if (htmlref) {
const html = htmlref.deref();
if (html) {
this.makeError(html, errorMessage);
}
}
}
else {
console.warn(formElm + " is not a valid form property");
}
}
makeError(e, message) {
let element = e.getElementsByClassName("suberror")[0];
if (!element) {
const div = document.createElement("div");
div.classList.add("suberror", "suberrora");
e.append(div);
element = div;
}
else {
element.classList.remove("suberror");
setTimeout(_ => {
element.classList.add("suberror");
}, 100);
}
element.textContent = message;
}
}
class Settings extends Buttons {
static Buttons = Buttons;
static Options = Options;
html;
constructor(name) {
super(name);
}
addButton(name, { ltr = false } = {}) {
const options = new Options(name, this, { ltr });
this.add(name, options);
return options;
}
show() {
const background = document.createElement("div");
background.classList.add("background");
const title = document.createElement("h2");
title.textContent = this.name;
title.classList.add("settingstitle");
background.append(title);
background.append(this.generateHTML());
const exit = document.createElement("span");
exit.textContent = "✖";
exit.classList.add("exitsettings");
background.append(exit);
exit.onclick = _ => {
this.hide();
};
document.body.append(background);
this.html = background;
}
hide() {
if (this.html) {
this.html.remove();
this.html = null;
}
}
}
export { Settings, Buttons, Options };

View file

@ -1,19 +0,0 @@
class SnowFlake {
id;
constructor(id) {
this.id = id;
}
getUnixTime() {
return SnowFlake.stringToUnixTime(this.id);
}
static stringToUnixTime(str) {
try {
return Number((BigInt(str) >> 22n) + 1420070400000n);
}
catch {
console.error(`The ID is corrupted, it's ${str} when it should be some number.`);
return 0;
}
}
}
export { SnowFlake };

View file

@ -1,431 +0,0 @@
//const usercache={};
import { Member } from "./member.js";
import { MarkDown } from "./markdown.js";
import { Contextmenu } from "./contextmenu.js";
import { SnowFlake } from "./snowflake.js";
class User extends SnowFlake {
owner;
hypotheticalpfp;
avatar;
username;
nickname = null;
relationshipType = 0;
bio;
discriminator;
pronouns;
bot;
public_flags;
accent_color;
banner;
hypotheticalbanner;
premium_since;
premium_type;
theme_colors;
badge_ids;
members = new WeakMap();
status;
clone() {
return new User({
username: this.username,
id: this.id + "#clone",
public_flags: this.public_flags,
discriminator: this.discriminator,
avatar: this.avatar,
accent_color: this.accent_color,
banner: this.banner,
bio: this.bio.rawString,
premium_since: this.premium_since,
premium_type: this.premium_type,
bot: this.bot,
theme_colors: this.theme_colors,
pronouns: this.pronouns,
badge_ids: this.badge_ids
}, this.owner);
}
getPresence(presence) {
if (presence) {
this.setstatus(presence.status);
}
else {
this.setstatus("offline");
}
}
setstatus(status) {
this.status = status;
}
async getStatus() {
if (this.status) {
return this.status;
}
else {
return "offline";
}
}
static contextmenu = new Contextmenu("User Menu");
static setUpContextMenu() {
this.contextmenu.addbutton("Copy user id", function () {
navigator.clipboard.writeText(this.id);
});
this.contextmenu.addbutton("Message user", function () {
fetch(this.info.api + "/users/@me/channels", { method: "POST",
body: JSON.stringify({ recipients: [this.id] }),
headers: this.localuser.headers
}).then(_ => _.json()).then(json => {
this.localuser.goToChannel(json.id);
});
});
this.contextmenu.addbutton("Block user", function () {
this.block();
}, null, function () {
return this.relationshipType !== 2;
});
this.contextmenu.addbutton("Unblock user", function () {
this.unblock();
}, null, function () {
return this.relationshipType === 2;
});
this.contextmenu.addbutton("Friend request", function () {
fetch(`${this.info.api}/users/@me/relationships/${this.id}`, {
method: "PUT",
headers: this.owner.headers,
body: JSON.stringify({
type: 1
})
});
});
this.contextmenu.addbutton("Kick member", function (member) {
member.kick();
}, null, member => {
if (!member)
return false;
const us = member.guild.member;
if (member.id === us.id) {
return false;
}
if (member.id === member.guild.properties.owner_id) {
return false;
}
return (us.hasPermission("KICK_MEMBERS")) || false;
});
this.contextmenu.addbutton("Ban member", function (member) {
member.ban();
}, null, member => {
if (!member)
return false;
const us = member.guild.member;
if (member.id === us.id) {
return false;
}
if (member.id === member.guild.properties.owner_id) {
return false;
}
return (us.hasPermission("BAN_MEMBERS")) || false;
});
}
static checkuser(user, owner) {
if (owner.userMap.has(user.id)) {
return owner.userMap.get(user.id);
}
else {
const tempuser = new User(user, owner, true);
owner.userMap.set(user.id, tempuser);
return tempuser;
}
}
get info() {
return this.owner.info;
}
get localuser() {
return this.owner;
}
get name() {
return this.username;
}
constructor(userjson, owner, dontclone = false) {
super(userjson.id);
this.owner = owner;
if (!owner) {
console.error("missing localuser");
}
if (dontclone) {
for (const thing of Object.keys(userjson)) {
if (thing === "bio") {
this.bio = new MarkDown(userjson[thing], this.localuser);
continue;
}
if (thing === "id") {
continue;
}
this[thing] = userjson[thing];
}
this.hypotheticalpfp = false;
}
else {
return User.checkuser(userjson, owner);
}
}
async resolvemember(guild) {
return await Member.resolveMember(this, guild);
}
async getUserProfile() {
return (await fetch(`${this.info.api}/users/${this.id.replace("#clone", "")}/profile?with_mutual_guilds=true&with_mutual_friends=true`, {
headers: this.localuser.headers
})).json();
}
resolving = false;
async getBadge(id) {
if (this.localuser.badges.has(id)) {
return this.localuser.badges.get(id);
}
else {
if (this.resolving) {
await this.resolving;
return this.localuser.badges.get(id);
}
const prom = await this.getUserProfile();
this.resolving = prom;
const badges = prom.badges;
this.resolving = false;
for (const thing of badges) {
this.localuser.badges.set(thing.id, thing);
}
return this.localuser.badges.get(id);
}
}
buildpfp() {
const pfp = document.createElement("img");
pfp.loading = "lazy";
pfp.src = this.getpfpsrc();
pfp.classList.add("pfp");
pfp.classList.add("userid:" + this.id);
return pfp;
}
async buildstatuspfp() {
const div = document.createElement("div");
div.style.position = "relative";
const pfp = this.buildpfp();
div.append(pfp);
{
const status = document.createElement("div");
status.classList.add("statusDiv");
switch (await this.getStatus()) {
case "offline":
status.classList.add("offlinestatus");
break;
case "online":
default:
status.classList.add("onlinestatus");
break;
}
div.append(status);
}
return div;
}
userupdate(json) {
if (json.avatar !== this.avatar) {
console.log;
this.changepfp(json.avatar);
}
}
bind(html, guild = null, error = true) {
if (guild && guild.id !== "@me") {
Member.resolveMember(this, guild).then(_ => {
User.contextmenu.bindContextmenu(html, this, _);
if (_ === undefined && error) {
const error = document.createElement("span");
error.textContent = "!";
error.classList.add("membererror");
html.after(error);
return;
}
if (_) {
_.bind(html);
}
}).catch(_ => {
console.log(_);
});
}
if (guild) {
this.profileclick(html, guild);
}
else {
this.profileclick(html);
}
}
static async resolve(id, localuser) {
const json = await fetch(localuser.info.api.toString() + "/users/" + id + "/profile", { headers: localuser.headers }).then(_ => _.json());
return new User(json, localuser);
}
changepfp(update) {
this.avatar = update;
this.hypotheticalpfp = false;
const src = this.getpfpsrc();
console.log(src);
for (const thing of document.getElementsByClassName("userid:" + this.id)) {
thing.src = src;
}
}
block() {
fetch(`${this.info.api}/users/@me/relationships/${this.id}`, {
method: "PUT",
headers: this.owner.headers,
body: JSON.stringify({
type: 2
})
});
this.relationshipType = 2;
const channel = this.localuser.channelfocus;
if (channel) {
for (const thing of channel.messages) {
thing[1].generateMessage();
}
}
}
unblock() {
fetch(`${this.info.api}/users/@me/relationships/${this.id}`, {
method: "DELETE",
headers: this.owner.headers,
});
this.relationshipType = 0;
const channel = this.localuser.channelfocus;
if (channel) {
for (const thing of channel.messages) {
thing[1].generateMessage();
}
}
}
getpfpsrc() {
if (this.hypotheticalpfp && this.avatar) {
return this.avatar;
}
if (this.avatar !== null) {
return this.info.cdn + "/avatars/" + this.id.replace("#clone", "") + "/" + this.avatar + ".png";
}
else {
const int = new Number((BigInt(this.id.replace("#clone", "")) >> 22n) % 6n);
return this.info.cdn + `/embed/avatars/${int}.png`;
}
}
createjankpromises() {
new Promise(_ => { });
}
async buildprofile(x, y, guild = null) {
if (Contextmenu.currentmenu != "") {
Contextmenu.currentmenu.remove();
}
const div = document.createElement("div");
if (this.accent_color) {
div.style.setProperty("--accent_color", "#" + this.accent_color.toString(16).padStart(6, "0"));
}
else {
div.style.setProperty("--accent_color", "transparent");
}
if (this.banner) {
const banner = document.createElement("img");
let src;
if (!this.hypotheticalbanner) {
src = this.info.cdn + "/avatars/" + this.id.replace("#clone", "") + "/" + this.banner + ".png";
}
else {
src = this.banner;
}
console.log(src, this.banner);
banner.src = src;
banner.classList.add("banner");
div.append(banner);
}
if (x !== -1) {
div.style.left = x + "px";
div.style.top = y + "px";
div.classList.add("profile", "flexttb");
}
else {
this.setstatus("online");
div.classList.add("hypoprofile", "flexttb");
}
const badgediv = document.createElement("div");
badgediv.classList.add("badges");
(async () => {
if (!this.badge_ids)
return;
for (const id of this.badge_ids) {
const badgejson = await this.getBadge(id);
if (badgejson) {
const badge = document.createElement(badgejson.link ? "a" : "div");
badge.classList.add("badge");
const img = document.createElement("img");
img.src = badgejson.icon;
badge.append(img);
const span = document.createElement("span");
span.textContent = badgejson.description;
badge.append(span);
if (badge instanceof HTMLAnchorElement) {
badge.href = badgejson.link;
}
badgediv.append(badge);
}
}
})();
{
const pfp = await this.buildstatuspfp();
div.appendChild(pfp);
}
{
const userbody = document.createElement("div");
userbody.classList.add("infosection");
div.appendChild(userbody);
const usernamehtml = document.createElement("h2");
usernamehtml.textContent = this.username;
userbody.appendChild(usernamehtml);
userbody.appendChild(badgediv);
const discrimatorhtml = document.createElement("h3");
discrimatorhtml.classList.add("tag");
discrimatorhtml.textContent = this.username + "#" + this.discriminator;
userbody.appendChild(discrimatorhtml);
const pronounshtml = document.createElement("p");
pronounshtml.textContent = this.pronouns;
pronounshtml.classList.add("pronouns");
userbody.appendChild(pronounshtml);
const rule = document.createElement("hr");
userbody.appendChild(rule);
const biohtml = this.bio.makeHTML();
userbody.appendChild(biohtml);
if (guild) {
Member.resolveMember(this, guild).then(member => {
if (!member)
return;
const roles = document.createElement("div");
roles.classList.add("rolesbox");
for (const role of member.roles) {
const div = document.createElement("div");
div.classList.add("rolediv");
const color = document.createElement("div");
div.append(color);
color.style.setProperty("--role-color", "#" + role.color.toString(16).padStart(6, "0"));
color.classList.add("colorrolediv");
const span = document.createElement("span");
div.append(span);
span.textContent = role.name;
roles.append(div);
}
userbody.append(roles);
});
}
}
console.log(div);
if (x !== -1) {
Contextmenu.currentmenu = div;
document.body.appendChild(div);
Contextmenu.keepOnScreen(div);
}
return div;
}
profileclick(obj, guild) {
obj.onclick = e => {
this.buildprofile(e.clientX, e.clientY, guild);
e.stopPropagation();
};
}
}
User.setUpContextMenu();
export { User };

2
.gitignore vendored
View file

@ -134,3 +134,5 @@ testAccount.json
CC
uptime.json
.directory
.dist/
bun.lockb

34
gulpfile.cjs Normal file
View file

@ -0,0 +1,34 @@
const gulp = require("gulp");
const ts = require("gulp-typescript");
const tsProject = ts.createProject("tsconfig.json");
// Task to compile TypeScript files
gulp.task("scripts", () => {
return tsProject.src().pipe(tsProject()).js.pipe(gulp.dest("dist"));
});
// Task to copy HTML files
gulp.task("copy-html", () => {
return gulp.src("src/**/*.html").pipe(gulp.dest("dist"));
});
// Task to copy other static assets (e.g., CSS, images)
gulp.task("copy-assets", () => {
return gulp
.src([
"src/**/*.css",
"src/**/*.bin",
"src/**/*.ico",
"src/**/*.json",
"src/**/*.js",
"src/**/*.png",
"src/**/*.jpg",
"src/**/*.jpeg",
"src/**/*.gif",
"src/**/*.svg",
])
.pipe(gulp.dest("dist"));
});
// Default task to run all tasks
gulp.task("default", gulp.series("scripts", "copy-html", "copy-assets"));

197
index.js
View file

@ -1,197 +0,0 @@
#! /usr/bin/env node
const compression = require("compression");
const express = require("express");
const fs = require("node:fs");
const app = express();
const instances=require("./webpage/instances.json");
const stats=require("./stats.js");
const instancenames=new Map();
for(const instance of instances){
instancenames.set(instance.name,instance);
}
app.use(compression());
fetch("https://raw.githubusercontent.com/spacebarchat/spacebarchat/master/instances/instances.json").then(_=>_.json()).then(json=>{
for(const instance of json){
if(!instancenames.has(instance.name)){
instances.push(instance);
}else{
const ofinst=instancenames.get(instance.name);
for(const key of Object.keys(instance)){
if(!ofinst[key]){
ofinst[key]=instance[key];
}
}
}
}
stats.observe(instances);
});
app.use("/getupdates",(req, res)=>{
const out=fs.statSync(`${__dirname}/webpage`);
res.send(out.mtimeMs+"");
});
const debugging=true;//Do not turn this off, the service worker is all kinds of jank as is, it'll really mess your day up if you disable this
function isembed(str){
return str.includes("discord")||str.includes("Spacebar");
}
async function getapiurls(str){
if(str.at(-1)!=="/"){
str+="/";
}
let api;
try{
const info=await fetch(`${str}/.well-known/spacebar`).then(x=>x.json());
api=info.api;
}catch{
return false;
}
const url = new URL(api);
try{
const info=await fetch(`${api}${url.pathname.includes("api") ? "" : "api"}/policies/instance/domains`).then(x=>x.json());
return{
api: info.apiEndpoint,
gateway: info.gateway,
cdn: info.cdn,
wellknown: str,
};
}catch{
return false;
}
}
async function inviteres(req,res){
let url;
if(URL.canParse(req.query.url)){
url=new URL(req.query.url);
}else{
const scheme = req.secure ? "https" : "http";
const host=`${scheme}://${req.get("Host")}`;
url=new URL(host);
}
try{
if(url.pathname.startsWith("invite")){
throw-1;
}
const code=url.pathname.split("/")[2];
let title="";
let description="";
let thumbnail="";
const urls=await getapiurls(url.searchParams.get("instance"));
await fetch(`${urls.api}/invites/${code}`,{
method: "GET"
}).then(_=>_.json()).then(json=>{
title=json.guild.name;
if(json.inviter){
description=json.inviter.username+" Has invited you to "+json.guild.name+(json.guild.description?json.guild.description+"\n":"");
}else{
description="you've been invited to "+json.guild.name+(json.guild.description?json.guild.description+"\n":"");
}
if(json.guild.icon){
thumbnail=`${urls.cdn}/icons/${json.guild.id}/${json.guild.icon}.png`;
}
});
const json={
type: "link",
version: "1.0",
title,
thumbnail,
description,
};
res.send(JSON.stringify(json));
}catch(e){
console.error(e);
const json={
type: "link",
version: "1.0",
title: "Jank Client",
thumbnail: "/logo.webp",
description: "A spacebar client that has DMs, replying and more",
url: url.toString()
};
res.send(JSON.stringify(json));
}
}
/*
function htmlEnc(s) {//https://stackoverflow.com/a/11561642
return s.replaceAll(/&/g, '&amp;')
.replaceAll(/</g, '&lt;')
.replaceAll(/>/g, '&gt;')
.replaceAll(/'/g, '&#39;')
.replaceAll(/"/g, '&#34;');
}
function strEscape(s){
return JSON.stringify(s);
}
html=
`<!DOCTYPE html>`+
`<html lang="en">`+
`<head>`+
`<title>${htmlEnc(title)}</title>`+
`<meta content=${strEscape(title)} property="og:title"/>`+
`<meta content=${strEscape(description)} property="og:description"/>`+
`<meta content=${strEscape(icon)} property="og:image"/>`+
`</head>`+
`</html>`
res.type('html');
res.send(html);
return true;
}catch(e){
console.error(e);
}
return false;
*/
app.use("/services/oembed", (req, res)=>{
inviteres(req, res);
});
app.use("/uptime",(req,res)=>{
console.log(req.query.name);
const uptime=stats.uptime[req.query.name];
console.log(req.query.name,uptime,stats.uptime);
res.send(uptime);
});
app.use("/", async (req, res)=>{
const scheme = req.secure ? "https" : "http";
const host=`${scheme}://${req.get("Host")}`;
const ref=host+req.originalUrl;
if(host&&ref){
const link=`${host}/services/oembed?url=${encodeURIComponent(ref)}`;
res.set("Link",`<${link}>; rel="alternate"; type="application/json+oembed"; title="Jank Client oEmbed format"`);
}else{
console.log(req);
}
if(req.path==="/"){
res.sendFile("./webpage/home.html", {root: __dirname});
return;
}
if(debugging&&req.path.startsWith("/service.js")){
res.send("dud");
return;
}
if(req.path.startsWith("/instances.json")){
res.send(JSON.stringify(instances));
return;
}
if(req.path.startsWith("/invite/")){
res.sendFile("./webpage/invite.html", {root: __dirname});
return;
}
if(fs.existsSync(`${__dirname}/webpage${req.path}`)){
res.sendFile(`./webpage${req.path}`, {root: __dirname});
}else if(req.path.endsWith(".js") && fs.existsSync(`${__dirname}/.dist${req.path}`)){
const dir=`./.dist${req.path}`;
res.sendFile(dir, {root: __dirname});
}else if(fs.existsSync(`${__dirname}/webpage${req.path}.html`)){
res.sendFile(`./webpage${req.path}.html`, {root: __dirname});
}else{
res.sendFile("./webpage/index.html", {root: __dirname});
}
});
const PORT = process.env.PORT || Number(process.argv[1]) || 8080;
app.listen(PORT, ()=>{});
console.log("this ran :P");
exports.getapiurls=getapiurls;

View file

@ -2,14 +2,18 @@
"name": "jankclient",
"version": "0.1.0",
"description": "A SpaceBar Client written in JS HTML and CSS to run, clone the repo and do either `node index.js` or `bun index.js` both bun and node are supported, and both should function as expected, if there are any problems with Jank Client on things that aren't linux, please let me know. To access Jank Client after init simply go to http://localhost:8080/login and login with your username and password.",
"main": "index.js",
"main": ".dist/index.js",
"type": "module",
"scripts": {
"lint": "eslint .",
"start": "node index.js"
"build": "npx gulp",
"start": "node dist/index.js"
},
"author": "MathMan05",
"license": "GPL-3.0",
"dependencies": {
"@types/express": "^4.17.21",
"@types/node-fetch": "^2.6.11",
"compression": "^1.7.4",
"express": "^4.19.2",
"ts-to-jsdoc": "^2.2.0"
@ -19,13 +23,17 @@
"@html-eslint/eslint-plugin": "^0.25.0",
"@html-eslint/parser": "^0.25.0",
"@stylistic/eslint-plugin": "^2.3.0",
"@types/compression": "^1.7.5",
"@types/eslint__js": "^8.42.3",
"eslint": "^8.57.0",
"eslint-plugin-html": "^8.1.1",
"eslint-plugin-sonarjs": "^1.0.4",
"eslint-plugin-unicorn": "^55.0.0",
"gulp": "^5.0.0",
"gulp-copy": "^5.0.0",
"gulp-typescript": "^6.0.0-alpha.1",
"ts-node": "^10.9.2",
"typescript": "^5.5.4",
"typescript-eslint": "^7.17.0"
}
}
}

116
src/index.ts Normal file
View file

@ -0,0 +1,116 @@
#!/usr/bin/env node
import compression from "compression";
import express, { Request, Response } from "express";
import fs from "node:fs";
import fetch from "node-fetch";
import path from "path";
import { observe, uptime } from "./stats.js";
import { getApiUrls, inviteResponse } from "./utils.js";
interface Instance {
name: string;
[key: string]: any;
}
const app = express();
import instances from "./webpage/instances.json";
const instanceNames = new Map<string, Instance>();
for (const instance of instances) {
instanceNames.set(instance.name, instance);
}
app.use(compression());
async function updateInstances(): Promise<void> {
try {
const response = await fetch(
"https://raw.githubusercontent.com/spacebarchat/spacebarchat/master/instances/instances.json"
);
const json: Instance[] = await response.json();
for (const instance of json) {
if (!instanceNames.has(instance.name)) {
instances.push(instance as any);
} else {
const existingInstance = instanceNames.get(instance.name);
if (existingInstance) {
for (const key of Object.keys(instance)) {
if (!existingInstance[key]) {
existingInstance[key] = instance[key];
}
}
}
}
}
observe(instances);
} catch (error) {
console.error("Error updating instances:", error);
}
}
updateInstances();
app.use("/getupdates", (_req: Request, res: Response) => {
try {
const stats = fs.statSync(path.join(__dirname, "webpage"));
res.send(stats.mtimeMs.toString());
} catch (error) {
console.error("Error getting updates:", error);
res.status(500).send("Error getting updates");
}
});
app.use("/services/oembed", (req: Request, res: Response) => {
inviteResponse(req, res);
});
app.use("/uptime", (req: Request, res: Response) => {
const instanceUptime = uptime[req.query.name as string];
res.send(instanceUptime);
});
app.use("/", async (req: Request, res: Response) => {
const scheme = req.secure ? "https" : "http";
const host = `${scheme}://${req.get("Host")}`;
const ref = host + req.originalUrl;
if (host && ref) {
const link = `${host}/services/oembed?url=${encodeURIComponent(ref)}`;
res.set(
"Link",
`<${link}>; rel="alternate"; type="application/json+oembed"; title="Jank Client oEmbed format"`
);
}
if (req.path === "/") {
res.sendFile(path.join(__dirname, "webpage", "home.html"));
return;
}
if (req.path.startsWith("/instances.json")) {
res.json(instances);
return;
}
if (req.path.startsWith("/invite/")) {
res.sendFile(path.join(__dirname, "webpage", "invite.html"));
return;
}
const filePath = path.join(__dirname, "webpage", req.path);
if (fs.existsSync(filePath)) {
res.sendFile(filePath);
} else if (fs.existsSync(`${filePath}.html`)) {
res.sendFile(`${filePath}.html`);
} else {
res.sendFile(path.join(__dirname, "webpage", "index.html"));
}
});
const PORT = process.env.PORT || Number(process.argv[2]) || 8080;
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});
export { getApiUrls };

247
src/stats.ts Normal file
View file

@ -0,0 +1,247 @@
import fs from "node:fs";
import path from "path";
import fetch from "node-fetch";
import { getApiUrls } from "./utils.js";
interface UptimeEntry {
time: number;
online: boolean;
}
interface UptimeObject {
[key: string]: UptimeEntry[];
}
interface Instance {
name: string;
urls?: { api: string };
url?: string;
online?: boolean;
uptime?: {
daytime: number;
weektime: number;
alltime: number;
};
}
let uptimeObject: UptimeObject = loadUptimeObject();
export { uptimeObject as uptime };
function loadUptimeObject(): UptimeObject {
const filePath = path.join(__dirname, "..", "uptime.json");
if (fs.existsSync(filePath)) {
try {
return JSON.parse(fs.readFileSync(filePath, "utf8"));
} catch (error) {
console.error("Error reading uptime.json:", error);
return {};
}
}
return {};
}
function saveUptimeObject(): void {
fs.writeFile(
`${__dirname}/uptime.json`,
JSON.stringify(uptimeObject),
(error) => {
if (error) {
console.error("Error saving uptime.json:", error);
}
}
);
}
function removeUndefinedKey(): void {
if (uptimeObject.undefined) {
delete uptimeObject.undefined;
saveUptimeObject();
}
}
removeUndefinedKey();
export async function observe(instances: Instance[]): Promise<void> {
const activeInstances = new Set<string>();
const instancePromises = instances.map((instance) =>
resolveInstance(instance, activeInstances)
);
await Promise.allSettled(instancePromises);
updateInactiveInstances(activeInstances);
}
async function resolveInstance(
instance: Instance,
activeInstances: Set<string>
): Promise<void> {
try {
calcStats(instance);
const api = await getApiUrl(instance);
if (!api) {
handleUnresolvedApi(instance);
return;
}
activeInstances.add(instance.name);
scheduleHealthCheck(instance, api);
} catch (error) {
console.error("Error resolving instance:", error);
}
}
async function getApiUrl(instance: Instance): Promise<string | null> {
if (instance.urls) {
return instance.urls.api;
}
if (instance.url) {
const urls = await getApiUrls(instance.url);
return urls ? urls.api : null;
}
return null;
}
function handleUnresolvedApi(instance: Instance): void {
setStatus(instance, false);
console.warn(`${instance.name} does not resolve api URL`, instance);
setTimeout(() => resolveInstance(instance, new Set()), 1000 * 60 * 30);
}
function scheduleHealthCheck(instance: Instance, api: string): void {
const checkInterval = 1000 * 60 * 30;
const initialDelay = Math.random() * 1000 * 60 * 10;
setTimeout(() => {
checkHealth(instance, api);
setInterval(() => checkHealth(instance, api), checkInterval);
}, initialDelay);
}
async function checkHealth(
instance: Instance,
api: string,
tries = 0
): Promise<void> {
try {
const response = await fetch(`${api}ping`, { method: "HEAD" });
if (response.ok || tries > 3) {
setStatus(instance, response.ok);
} else {
retryHealthCheck(instance, api, tries);
}
} catch (error) {
console.error("Error checking health:", error);
if (tries > 3) {
setStatus(instance, false);
} else {
retryHealthCheck(instance, api, tries);
}
}
}
function retryHealthCheck(
instance: Instance,
api: string,
tries: number
): void {
setTimeout(() => checkHealth(instance, api, tries + 1), 30000);
}
function updateInactiveInstances(activeInstances: Set<string>): void {
for (const key of Object.keys(uptimeObject)) {
if (!activeInstances.has(key)) {
setStatus(key, false);
}
}
}
function calcStats(instance: Instance): void {
const obj = uptimeObject[instance.name];
if (!obj) return;
const now = Date.now();
const day = now - 1000 * 60 * 60 * 24;
const week = now - 1000 * 60 * 60 * 24 * 7;
let totalTimePassed = 0;
let alltime = 0;
let daytime = 0;
let weektime = 0;
let online = false;
for (let i = 0; i < obj.length; i++) {
const entry = obj[i];
online = entry.online;
const stamp = entry.time;
const nextStamp = obj[i + 1]?.time || now;
const timePassed = nextStamp - stamp;
totalTimePassed += timePassed;
alltime += Number(online) * timePassed;
if (stamp + timePassed > week) {
const weekTimePassed = Math.min(timePassed, nextStamp - week);
weektime += Number(online) * weekTimePassed;
if (stamp + timePassed > day) {
const dayTimePassed = Math.min(weekTimePassed, nextStamp - day);
daytime += Number(online) * dayTimePassed;
}
}
}
instance.online = online;
instance.uptime = calculateUptimeStats(
totalTimePassed,
alltime,
daytime,
weektime,
online
);
}
function calculateUptimeStats(
totalTimePassed: number,
alltime: number,
daytime: number,
weektime: number,
online: boolean
): { daytime: number; weektime: number; alltime: number } {
const dayInMs = 1000 * 60 * 60 * 24;
const weekInMs = dayInMs * 7;
alltime /= totalTimePassed;
if (totalTimePassed > dayInMs) {
daytime = daytime || (online ? dayInMs : 0);
daytime /= dayInMs;
if (totalTimePassed > weekInMs) {
weektime = weektime || (online ? weekInMs : 0);
weektime /= weekInMs;
} else {
weektime = alltime;
}
} else {
weektime = alltime;
daytime = alltime;
}
return { daytime, weektime, alltime };
}
function setStatus(instance: string | Instance, status: boolean): void {
const name = typeof instance === "string" ? instance : instance.name;
let obj = uptimeObject[name];
if (!obj) {
obj = [];
uptimeObject[name] = obj;
}
if (obj.at(-1)?.online !== status) {
obj.push({ time: Date.now(), online: status });
saveUptimeObject();
}
if (typeof instance !== "string") {
calcStats(instance);
}
}

114
src/utils.ts Normal file
View file

@ -0,0 +1,114 @@
import fetch from "node-fetch";
import { Request, Response } from "express";
interface ApiUrls {
api: string;
gateway: string;
cdn: string;
wellknown: string;
}
interface Invite {
guild: {
name: string;
description?: string;
icon?: string;
id: string;
};
inviter?: {
username: string;
};
}
export async function getApiUrls(url: string): Promise<ApiUrls | null> {
if (!url.endsWith("/")) {
url += "/";
}
try {
const info = await fetch(`${url}.well-known/spacebar`).then((res) =>
res.json()
);
const api = info.api;
const apiUrl = new URL(api);
const policies = await fetch(
`${api}${
apiUrl.pathname.includes("api") ? "" : "api"
}/policies/instance/domains`
).then((res) => res.json());
return {
api: policies.apiEndpoint,
gateway: policies.gateway,
cdn: policies.cdn,
wellknown: url,
};
} catch (error) {
console.error("Error fetching API URLs:", error);
return null;
}
}
export async function inviteResponse(
req: Request,
res: Response
): Promise<void> {
let url: URL;
if (URL.canParse(req.query.url as string)) {
url = new URL(req.query.url as string);
} else {
const scheme = req.secure ? "https" : "http";
const host = `${scheme}://${req.get("Host")}`;
url = new URL(host);
}
try {
if (url.pathname.startsWith("invite")) {
throw new Error("Invalid invite URL");
}
const code = url.pathname.split("/")[2];
const instance = url.searchParams.get("instance");
if (!instance) {
throw new Error("Instance not specified");
}
const urls = await getApiUrls(instance);
if (!urls) {
throw new Error("Failed to get API URLs");
}
const invite = await fetch(`${urls.api}/invites/${code}`).then(
(res) => res.json() as Promise<Invite>
);
const title = invite.guild.name;
const description = invite.inviter
? `${invite.inviter.username} has invited you to ${invite.guild.name}${
invite.guild.description ? `\n${invite.guild.description}` : ""
}`
: `You've been invited to ${invite.guild.name}${
invite.guild.description ? `\n${invite.guild.description}` : ""
}`;
const thumbnail = invite.guild.icon
? `${urls.cdn}/icons/${invite.guild.id}/${invite.guild.icon}.png`
: "";
const jsonResponse = {
type: "link",
version: "1.0",
title,
thumbnail,
description,
};
res.json(jsonResponse);
} catch (error) {
console.error("Error processing invite response:", error);
const jsonResponse = {
type: "link",
version: "1.0",
title: "Jank Client",
thumbnail: "/logo.webp",
description: "A spacebar client that has DMs, replying and more",
url: url.toString(),
};
res.json(jsonResponse);
}
}

164
src/webpage/audio.ts Normal file
View file

@ -0,0 +1,164 @@
import { getBulkInfo } from "./login.js";
class Voice {
audioCtx: AudioContext;
info: { wave: string | Function; freq: number };
playing: boolean;
myArrayBuffer: AudioBuffer;
gainNode: GainNode;
buffer: Float32Array;
source: AudioBufferSourceNode;
constructor(wave: string | Function, freq: number, volume = 1) {
this.audioCtx = new window.AudioContext();
this.info = { wave, freq };
this.playing = false;
this.myArrayBuffer = this.audioCtx.createBuffer(
1,
this.audioCtx.sampleRate,
this.audioCtx.sampleRate
);
this.gainNode = this.audioCtx.createGain();
this.gainNode.gain.value = volume;
this.gainNode.connect(this.audioCtx.destination);
this.buffer = this.myArrayBuffer.getChannelData(0);
this.source = this.audioCtx.createBufferSource();
this.source.buffer = this.myArrayBuffer;
this.source.loop = true;
this.source.start();
this.updateWave();
}
get wave(): string | Function {
return this.info.wave;
}
get freq(): number {
return this.info.freq;
}
set wave(wave: string | Function) {
this.info.wave = wave;
this.updateWave();
}
set freq(freq: number) {
this.info.freq = freq;
this.updateWave();
}
updateWave(): void {
const func = this.waveFunction();
for (let i = 0; i < this.buffer.length; i++) {
this.buffer[i] = func(i / this.audioCtx.sampleRate, this.freq);
}
}
waveFunction(): Function {
if (typeof this.wave === "function") {
return this.wave;
}
switch (this.wave) {
case "sin":
return (t: number, freq: number) => {
return Math.sin(t * Math.PI * 2 * freq);
};
case "triangle":
return (t: number, freq: number) => {
return Math.abs(((4 * t * freq) % 4) - 2) - 1;
};
case "sawtooth":
return (t: number, freq: number) => {
return ((t * freq) % 1) * 2 - 1;
};
case "square":
return (t: number, freq: number) => {
return (t * freq) % 2 < 1 ? 1 : -1;
};
case "white":
return (_t: number, _freq: number) => {
return Math.random() * 2 - 1;
};
case "noise":
return (_t: number, _freq: number) => {
return 0;
};
}
return new Function();
}
play(): void {
if (this.playing) {
return;
}
this.source.connect(this.gainNode);
this.playing = true;
}
stop(): void {
if (this.playing) {
this.source.disconnect();
this.playing = false;
}
}
static noises(noise: string): void {
switch (noise) {
case "three": {
const voicy = new Voice("sin", 800);
voicy.play();
setTimeout((_) => {
voicy.freq = 1000;
}, 50);
setTimeout((_) => {
voicy.freq = 1300;
}, 100);
setTimeout((_) => {
voicy.stop();
}, 150);
break;
}
case "zip": {
const voicy = new Voice((t: number, freq: number) => {
return Math.sin((t + 2) ** Math.cos(t * 4) * Math.PI * 2 * freq);
}, 700);
voicy.play();
setTimeout((_) => {
voicy.stop();
}, 150);
break;
}
case "square": {
const voicy = new Voice("square", 600, 0.4);
voicy.play();
setTimeout((_) => {
voicy.freq = 800;
}, 50);
setTimeout((_) => {
voicy.freq = 1000;
}, 100);
setTimeout((_) => {
voicy.stop();
}, 150);
break;
}
case "beep": {
const voicy = new Voice("sin", 800);
voicy.play();
setTimeout((_) => {
voicy.stop();
}, 50);
setTimeout((_) => {
voicy.play();
}, 100);
setTimeout((_) => {
voicy.stop();
}, 150);
break;
}
}
}
static get sounds() {
return ["three", "zip", "square", "beep"];
}
static setNotificationSound(sound: string) {
const userinfos = getBulkInfo();
userinfos.preferences.notisound = sound;
localStorage.setItem("userinfos", JSON.stringify(userinfos));
}
static getNotificationSound() {
const userinfos = getBulkInfo();
return userinfos.preferences.notisound;
}
}
export { Voice };

1415
src/webpage/channel.ts Normal file

File diff suppressed because it is too large Load diff

107
src/webpage/contextmenu.ts Normal file
View file

@ -0,0 +1,107 @@
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
][];
div!: HTMLDivElement;
static setup() {
Contextmenu.currentmenu = "";
document.addEventListener("click", (event) => {
if (Contextmenu.currentmenu === "") {
return;
}
if (!Contextmenu.currentmenu.contains(event.target as Node)) {
Contextmenu.currentmenu.remove();
Contextmenu.currentmenu = "";
}
});
}
constructor(name: string) {
this.name = name;
this.buttons = [];
}
addbutton(
text: string,
onclick: (this: x, arg: y, e: MouseEvent) => void,
img: null | string = null,
shown: (this: x, arg: y) => boolean = (_) => true,
enabled: (this: x, arg: y) => boolean = (_) => true
) {
this.buttons.push([text, onclick, img, shown, enabled, "button"]);
return {};
}
addsubmenu(
text: string,
onclick: (this: x, arg: y, e: MouseEvent) => void,
img = null,
shown: (this: x, arg: y) => boolean = (_) => true,
enabled: (this: x, arg: y) => boolean = (_) => true
) {
this.buttons.push([text, onclick, img, shown, enabled, "submenu"]);
return {};
}
private makemenu(x: number, y: number, addinfo: x, other: y) {
const div = document.createElement("div");
div.classList.add("contextmenu", "flexttb");
let visibleButtons = 0;
for (const thing of this.buttons) {
if (!thing[3].bind(addinfo).call(addinfo, other)) continue;
visibleButtons++;
const intext = document.createElement("button");
intext.disabled = !thing[4].bind(addinfo).call(addinfo, other);
intext.classList.add("contextbutton");
intext.textContent = thing[0];
console.log(thing);
if (thing[5] === "button" || thing[5] === "submenu") {
intext.onclick = thing[1].bind(addinfo, other);
}
div.appendChild(intext);
}
if (visibleButtons == 0) return;
if (Contextmenu.currentmenu != "") {
Contextmenu.currentmenu.remove();
}
div.style.top = y + "px";
div.style.left = x + "px";
document.body.appendChild(div);
Contextmenu.keepOnScreen(div);
console.log(div);
Contextmenu.currentmenu = div;
return this.div;
}
bindContextmenu(obj: HTMLElement, addinfo: x, other: y) {
const func = (event: MouseEvent) => {
event.preventDefault();
event.stopImmediatePropagation();
this.makemenu(event.clientX, event.clientY, addinfo, other);
};
obj.addEventListener("contextmenu", func);
return func;
}
static keepOnScreen(obj: HTMLElement) {
const html = document.documentElement.getBoundingClientRect();
const docheight = html.height;
const docwidth = html.width;
const box = obj.getBoundingClientRect();
console.log(box, docheight, docwidth);
if (box.right > docwidth) {
console.log("test");
obj.style.left = docwidth - box.width + "px";
}
if (box.bottom > docheight) {
obj.style.top = docheight - box.height + "px";
}
}
}
Contextmenu.setup();
export { Contextmenu };

273
src/webpage/dialog.ts Normal file
View file

@ -0,0 +1,273 @@
type dialogjson =
| ["hdiv", ...dialogjson[]]
| ["vdiv", ...dialogjson[]]
| ["img", string, [number, number] | undefined | ["fit"]]
| ["checkbox", string, boolean, (this: HTMLInputElement, e: Event) => unknown]
| ["button", string, string, (this: HTMLButtonElement, e: Event) => unknown]
| ["mdbox", string, string, (this: HTMLTextAreaElement, e: Event) => unknown]
| ["textbox", string, string, (this: HTMLInputElement, e: Event) => unknown]
| ["fileupload", string, (this: HTMLInputElement, e: Event) => unknown]
| ["text", string]
| ["title", string]
| ["radio", string, string[], (this: unknown, e: string) => unknown, number]
| ["html", HTMLElement]
| [
"select",
string,
string[],
(this: HTMLSelectElement, e: Event) => unknown,
number
]
| ["tabs", [string, dialogjson][]];
class Dialog {
layout: dialogjson;
onclose: Function;
onopen: Function;
html: HTMLDivElement;
background!: HTMLDivElement;
constructor(
layout: dialogjson,
onclose = (_: any) => {},
onopen = (_: any) => {}
) {
this.layout = layout;
this.onclose = onclose;
this.onopen = onopen;
const div = document.createElement("div");
div.appendChild(this.tohtml(layout));
this.html = div;
this.html.classList.add("centeritem");
if (!(layout[0] === "img")) {
this.html.classList.add("nonimagecenter");
}
}
tohtml(array: dialogjson): HTMLElement {
switch (array[0]) {
case "img":
const img = document.createElement("img");
img.src = array[1];
if (array[2] != undefined) {
if (array[2].length === 2) {
img.width = array[2][0];
img.height = array[2][1];
} else if (array[2][0] === "fit") {
img.classList.add("imgfit");
}
}
return img;
case "hdiv":
const hdiv = document.createElement("div");
hdiv.classList.add("flexltr");
for (const thing of array) {
if (thing === "hdiv") {
continue;
}
hdiv.appendChild(this.tohtml(thing));
}
return hdiv;
case "vdiv":
const vdiv = document.createElement("div");
vdiv.classList.add("flexttb");
for (const thing of array) {
if (thing === "vdiv") {
continue;
}
vdiv.appendChild(this.tohtml(thing));
}
return vdiv;
case "checkbox": {
const div = document.createElement("div");
const checkbox = document.createElement("input");
div.appendChild(checkbox);
const label = document.createElement("span");
checkbox.checked = array[2];
label.textContent = array[1];
div.appendChild(label);
checkbox.addEventListener("change", array[3]);
checkbox.type = "checkbox";
return div;
}
case "button": {
const div = document.createElement("div");
const input = document.createElement("button");
const label = document.createElement("span");
input.textContent = array[2];
label.textContent = array[1];
div.appendChild(label);
div.appendChild(input);
input.addEventListener("click", array[3]);
return div;
}
case "mdbox": {
const div = document.createElement("div");
const input = document.createElement("textarea");
input.value = array[2];
const label = document.createElement("span");
label.textContent = array[1];
input.addEventListener("input", array[3]);
div.appendChild(label);
div.appendChild(document.createElement("br"));
div.appendChild(input);
return div;
}
case "textbox": {
const div = document.createElement("div");
const input = document.createElement("input");
input.value = array[2];
input.type = "text";
const label = document.createElement("span");
label.textContent = array[1];
console.log(array[3]);
input.addEventListener("input", array[3]);
div.appendChild(label);
div.appendChild(input);
return div;
}
case "fileupload": {
const div = document.createElement("div");
const input = document.createElement("input");
input.type = "file";
const label = document.createElement("span");
label.textContent = array[1];
div.appendChild(label);
div.appendChild(input);
input.addEventListener("change", array[2]);
console.log(array);
return div;
}
case "text": {
const span = document.createElement("span");
span.textContent = array[1];
return span;
}
case "title": {
const span = document.createElement("span");
span.classList.add("title");
span.textContent = array[1];
return span;
}
case "radio": {
const div = document.createElement("div");
const fieldset = document.createElement("fieldset");
fieldset.addEventListener("change", () => {
let i = -1;
for (const thing of Array.from(fieldset.children)) {
i++;
if (i === 0) {
continue;
}
const checkbox = thing.children[0].children[0] as HTMLInputElement;
if (checkbox.checked) {
array[3](checkbox.value);
}
}
});
const legend = document.createElement("legend");
legend.textContent = array[1];
fieldset.appendChild(legend);
let i = 0;
for (const thing of array[2]) {
const div = document.createElement("div");
const input = document.createElement("input");
input.classList.add("radio");
input.type = "radio";
input.name = array[1];
input.value = thing;
if (i === array[4]) {
input.checked = true;
}
const label = document.createElement("label");
label.appendChild(input);
const span = document.createElement("span");
span.textContent = thing;
label.appendChild(span);
div.appendChild(label);
fieldset.appendChild(div);
i++;
}
div.appendChild(fieldset);
return div;
}
case "html":
return array[1];
case "select": {
const div = document.createElement("div");
const label = document.createElement("label");
const select = document.createElement("select");
label.textContent = array[1];
div.append(label);
div.appendChild(select);
for (const thing of array[2]) {
const option = document.createElement("option");
option.textContent = thing;
select.appendChild(option);
}
select.selectedIndex = array[4];
select.addEventListener("change", array[3]);
return div;
}
case "tabs": {
const table = document.createElement("div");
table.classList.add("flexttb");
const tabs = document.createElement("div");
tabs.classList.add("flexltr");
tabs.classList.add("tabbed-head");
table.appendChild(tabs);
const content = document.createElement("div");
content.classList.add("tabbed-content");
table.appendChild(content);
let shown: HTMLElement | undefined;
for (const thing of array[1]) {
const button = document.createElement("button");
button.textContent = thing[0];
tabs.appendChild(button);
const html = this.tohtml(thing[1]);
content.append(html);
if (!shown) {
shown = html;
} else {
html.style.display = "none";
}
button.addEventListener("click", (_) => {
if (shown) {
shown.style.display = "none";
}
html.style.display = "";
shown = html;
});
}
return table;
}
default:
console.error(
"can't find element:" + array[0],
" full element:",
array
);
return document.createElement("span");
}
}
show() {
this.onopen();
console.log("fullscreen");
this.background = document.createElement("div");
this.background.classList.add("background");
document.body.appendChild(this.background);
document.body.appendChild(this.html);
this.background.onclick = (_) => {
this.hide();
};
}
hide() {
document.body.removeChild(this.background);
document.body.removeChild(this.html);
}
}
export { Dialog };

306
src/webpage/direct.ts Normal file
View file

@ -0,0 +1,306 @@
import { Guild } from "./guild.js";
import { Channel } from "./channel.js";
import { Message } from "./message.js";
import { Localuser } from "./localuser.js";
import { User } from "./user.js";
import {
channeljson,
dirrectjson,
memberjson,
messagejson,
} from "./jsontypes.js";
import { Permissions } from "./permissions.js";
import { SnowFlake } from "./snowflake.js";
import { Contextmenu } from "./contextmenu.js";
class Direct extends Guild {
declare channelids: { [key: string]: Group };
getUnixTime(): number {
throw new Error("Do not call this for Direct, it does not make sense");
}
constructor(json: dirrectjson[], owner: Localuser) {
super(-1, owner, null);
this.message_notifications = 0;
this.owner = owner;
if (!this.localuser) {
console.error("Owner was not included, please fix");
}
this.headers = this.localuser.headers;
this.channels = [];
this.channelids = {};
// @ts-ignore
this.properties = {};
this.roles = [];
this.roleids = new Map();
this.prevchannel = undefined;
this.properties.name = "Direct Messages";
for (const thing of json) {
const temp = new Group(thing, this);
this.channels.push(temp);
this.channelids[temp.id] = temp;
}
this.headchannels = this.channels;
}
createChannelpac(json: any) {
const thischannel = new Group(json, this);
this.channelids[thischannel.id] = thischannel;
this.channels.push(thischannel);
this.sortchannels();
this.printServers();
return thischannel;
}
delChannel(json: channeljson) {
const channel = this.channelids[json.id];
super.delChannel(json);
if (channel) {
channel.del();
}
}
giveMember(_member: memberjson) {
console.error("not a real guild, can't give member object");
}
getRole(/* ID: string */) {
return null;
}
hasRole(/* r: string */) {
return false;
}
isAdmin() {
return false;
}
unreaddms() {
for (const thing of this.channels) {
(thing as Group).unreads();
}
}
}
const dmPermissions = new Permissions("0");
dmPermissions.setPermission("ADD_REACTIONS", 1);
dmPermissions.setPermission("VIEW_CHANNEL", 1);
dmPermissions.setPermission("SEND_MESSAGES", 1);
dmPermissions.setPermission("EMBED_LINKS", 1);
dmPermissions.setPermission("ATTACH_FILES", 1);
dmPermissions.setPermission("READ_MESSAGE_HISTORY", 1);
dmPermissions.setPermission("MENTION_EVERYONE", 1);
dmPermissions.setPermission("USE_EXTERNAL_EMOJIS", 1);
dmPermissions.setPermission("USE_APPLICATION_COMMANDS", 1);
dmPermissions.setPermission("USE_EXTERNAL_STICKERS", 1);
dmPermissions.setPermission("USE_EMBEDDED_ACTIVITIES", 1);
dmPermissions.setPermission("USE_SOUNDBOARD", 1);
dmPermissions.setPermission("USE_EXTERNAL_SOUNDS", 1);
dmPermissions.setPermission("SEND_VOICE_MESSAGES", 1);
dmPermissions.setPermission("SEND_POLLS", 1);
dmPermissions.setPermission("USE_EXTERNAL_APPS", 1);
dmPermissions.setPermission("CONNECT", 1);
dmPermissions.setPermission("SPEAK", 1);
dmPermissions.setPermission("STREAM", 1);
dmPermissions.setPermission("USE_VAD", 1);
// @ts-ignore
class Group extends Channel {
user: User;
static contextmenu = new Contextmenu<Group, undefined>("channel menu");
static setupcontextmenu() {
this.contextmenu.addbutton("Copy DM id", function (this: Group) {
navigator.clipboard.writeText(this.id);
});
this.contextmenu.addbutton("Mark as read", function (this: Group) {
this.readbottom();
});
this.contextmenu.addbutton("Close DM", function (this: Group) {
this.deleteChannel();
});
this.contextmenu.addbutton("Copy user ID", function () {
navigator.clipboard.writeText(this.user.id);
});
}
constructor(json: dirrectjson, owner: Direct) {
super(-1, owner, json.id);
this.owner = owner;
this.headers = this.guild.headers;
this.name = json.recipients[0]?.username;
if (json.recipients[0]) {
this.user = new User(json.recipients[0], this.localuser);
} else {
this.user = this.localuser.user;
}
this.name ??= this.localuser.user.username;
this.parent_id!;
this.parent!;
this.children = [];
this.guild_id = "@me";
this.permission_overwrites = new Map();
this.lastmessageid = json.last_message_id;
this.mentions = 0;
this.setUpInfiniteScroller();
this.updatePosition();
}
updatePosition() {
if (this.lastmessageid) {
this.position = SnowFlake.stringToUnixTime(this.lastmessageid);
} else {
this.position = 0;
}
this.position = -Math.max(this.position, this.getUnixTime());
}
createguildHTML() {
const div = document.createElement("div");
Group.contextmenu.bindContextmenu(div, this, undefined);
this.html = new WeakRef(div);
div.classList.add("channeleffects");
const myhtml = document.createElement("span");
myhtml.textContent = this.name;
div.appendChild(this.user.buildpfp());
div.appendChild(myhtml);
(div as any)["myinfo"] = this;
div.onclick = (_) => {
this.getHTML();
};
return div;
}
async getHTML() {
const id = ++Channel.genid;
if (this.localuser.channelfocus) {
this.localuser.channelfocus.infinite.delete();
}
if (this.guild !== this.localuser.lookingguild) {
this.guild.loadGuild();
}
this.guild.prevchannel = this;
this.localuser.channelfocus = this;
const prom = this.infinite.delete();
history.pushState(null, "", "/channels/" + this.guild_id + "/" + this.id);
this.localuser.pageTitle("@" + this.name);
(document.getElementById("channelTopic") as HTMLElement).setAttribute(
"hidden",
""
);
const loading = document.getElementById("loadingdiv") as HTMLDivElement;
Channel.regenLoadingMessages();
loading.classList.add("loading");
this.rendertyping();
await this.putmessages();
await prom;
if (id !== Channel.genid) {
return;
}
this.buildmessages();
(document.getElementById("typebox") as HTMLDivElement).contentEditable =
"" + true;
}
messageCreate(messagep: { d: messagejson }) {
const messagez = new Message(messagep.d, this);
if (this.lastmessageid) {
this.idToNext.set(this.lastmessageid, messagez.id);
this.idToPrev.set(messagez.id, this.lastmessageid);
}
this.lastmessageid = messagez.id;
if (messagez.author === this.localuser.user) {
this.lastreadmessageid = messagez.id;
if (this.myhtml) {
this.myhtml.classList.remove("cunread");
}
} else {
if (this.myhtml) {
this.myhtml.classList.add("cunread");
}
}
this.unreads();
this.updatePosition();
this.infinite.addedBottom();
this.guild.sortchannels();
if (this.myhtml) {
const parrent = this.myhtml.parentElement as HTMLElement;
parrent.prepend(this.myhtml);
}
if (this === this.localuser.channelfocus) {
if (!this.infinitefocus) {
this.tryfocusinfinate();
}
this.infinite.addedBottom();
}
this.unreads();
if (messagez.author === this.localuser.user) {
return;
}
if (
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.notify(messagez);
}
}
notititle(message: Message) {
return message.author.username;
}
readbottom() {
super.readbottom();
this.unreads();
}
all: WeakRef<HTMLElement> = new WeakRef(document.createElement("div"));
noti: WeakRef<HTMLElement> = new WeakRef(document.createElement("div"));
del() {
const all = this.all.deref();
if (all) {
all.remove();
}
if (this.myhtml) {
this.myhtml.remove();
}
}
unreads() {
const sentdms = document.getElementById("sentdms") as HTMLDivElement; //Need to change sometime
const current = this.all.deref();
if (this.hasunreads) {
{
const noti = this.noti.deref();
if (noti) {
noti.textContent = this.mentions + "";
return;
}
}
const div = document.createElement("div");
div.classList.add("servernoti");
const noti = document.createElement("div");
noti.classList.add("unread", "notiunread", "pinged");
noti.textContent = "" + this.mentions;
this.noti = new WeakRef(noti);
div.append(noti);
const buildpfp = this.user.buildpfp();
this.all = new WeakRef(div);
buildpfp.classList.add("mentioned");
div.append(buildpfp);
sentdms.append(div);
div.onclick = (_) => {
this.guild.loadGuild();
this.getHTML();
};
} else if (current) {
current.remove();
} else {
}
}
isAdmin(): boolean {
return false;
}
hasPermission(name: string): boolean {
return dmPermissions.hasPermission(name);
}
}
export { Direct, Group };
Group.setupcontextmenu();

411
src/webpage/embed.ts Normal file
View file

@ -0,0 +1,411 @@
import { Dialog } from "./dialog.js";
import { Message } from "./message.js";
import { MarkDown } from "./markdown.js";
import { embedjson, invitejson } from "./jsontypes.js";
import { getapiurls, getInstances } from "./login.js";
import { Guild } from "./guild.js";
class Embed {
type: string;
owner: Message;
json: embedjson;
constructor(json: embedjson, owner: Message) {
this.type = this.getType(json);
this.owner = owner;
this.json = json;
}
getType(json: embedjson) {
const instances = getInstances();
if (
instances &&
json.type === "link" &&
json.url &&
URL.canParse(json.url)
) {
const Url = new URL(json.url);
for (const instance of instances) {
if (instance.url && URL.canParse(instance.url)) {
const IUrl = new URL(instance.url);
const params = new URLSearchParams(Url.search);
let host: string;
if (params.has("instance")) {
const url = params.get("instance") as string;
if (URL.canParse(url)) {
host = new URL(url).host;
} else {
host = Url.host;
}
} else {
host = Url.host;
}
if (IUrl.host === host) {
const code =
Url.pathname.split("/")[Url.pathname.split("/").length - 1];
json.invite = {
url: instance.url,
code,
};
return "invite";
}
}
}
}
return json.type || "rich";
}
generateHTML() {
switch (this.type) {
case "rich":
return this.generateRich();
case "image":
return this.generateImage();
case "invite":
return this.generateInvite();
case "link":
return this.generateLink();
case "video":
case "article":
return this.generateArticle();
default:
console.warn(
`unsupported embed type ${this.type}, please add support dev :3`,
this.json
);
return document.createElement("div"); //prevent errors by giving blank div
}
}
get message() {
return this.owner;
}
get channel() {
return this.message.channel;
}
get guild() {
return this.channel.guild;
}
get localuser() {
return this.guild.localuser;
}
generateRich() {
const div = document.createElement("div");
if (this.json.color) {
div.style.backgroundColor = "#" + this.json.color.toString(16);
}
div.classList.add("embed-color");
const embed = document.createElement("div");
embed.classList.add("embed");
div.append(embed);
if (this.json.author) {
const authorline = document.createElement("div");
if (this.json.author.icon_url) {
const img = document.createElement("img");
img.classList.add("embedimg");
img.src = this.json.author.icon_url;
authorline.append(img);
}
const a = document.createElement("a");
a.textContent = this.json.author.name as string;
if (this.json.author.url) {
MarkDown.safeLink(a, this.json.author.url);
}
a.classList.add("username");
authorline.append(a);
embed.append(authorline);
}
if (this.json.title) {
const title = document.createElement("a");
title.append(new MarkDown(this.json.title, this.channel).makeHTML());
if (this.json.url) {
MarkDown.safeLink(title, this.json.url);
}
title.classList.add("embedtitle");
embed.append(title);
}
if (this.json.description) {
const p = document.createElement("p");
p.append(new MarkDown(this.json.description, this.channel).makeHTML());
embed.append(p);
}
embed.append(document.createElement("br"));
if (this.json.fields) {
for (const thing of this.json.fields) {
const div = document.createElement("div");
const b = document.createElement("b");
b.textContent = thing.name;
div.append(b);
const p = document.createElement("p");
p.append(new MarkDown(thing.value, this.channel).makeHTML());
p.classList.add("embedp");
div.append(p);
if (thing.inline) {
div.classList.add("inline");
}
embed.append(div);
}
}
if (this.json.footer || this.json.timestamp) {
const footer = document.createElement("div");
if (this.json?.footer?.icon_url) {
const img = document.createElement("img");
img.src = this.json.footer.icon_url;
img.classList.add("embedicon");
footer.append(img);
}
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");
footer.append(span);
}
if (this.json?.timestamp) {
const span = document.createElement("span");
span.textContent = new Date(this.json.timestamp).toLocaleString();
footer.append(span);
}
embed.append(footer);
}
return div;
}
generateImage() {
const img = document.createElement("img");
img.classList.add("messageimg");
img.onclick = function () {
const full = new Dialog(["img", img.src, ["fit"]]);
full.show();
};
img.src = this.json.thumbnail.proxy_url;
if (this.json.thumbnail.width) {
let scale = 1;
const max = 96 * 3;
scale = Math.max(scale, this.json.thumbnail.width / max);
scale = Math.max(scale, this.json.thumbnail.height / max);
this.json.thumbnail.width /= scale;
this.json.thumbnail.height /= scale;
}
img.style.width = this.json.thumbnail.width + "px";
img.style.height = this.json.thumbnail.height + "px";
console.log(this.json, "Image fix");
return img;
}
generateLink() {
const table = document.createElement("table");
table.classList.add("embed", "linkembed");
const trtop = document.createElement("tr");
table.append(trtop);
if (this.json.url && this.json.title) {
const td = document.createElement("td");
const a = document.createElement("a");
MarkDown.safeLink(a, this.json.url);
a.textContent = this.json.title;
td.append(a);
trtop.append(td);
}
{
const td = document.createElement("td");
const img = document.createElement("img");
if (this.json.thumbnail) {
img.classList.add("embedimg");
img.onclick = function () {
const full = new Dialog(["img", img.src, ["fit"]]);
full.show();
};
img.src = this.json.thumbnail.proxy_url;
td.append(img);
}
trtop.append(td);
}
const bottomtr = document.createElement("tr");
const td = document.createElement("td");
if (this.json.description) {
const span = document.createElement("span");
span.textContent = this.json.description;
td.append(span);
}
bottomtr.append(td);
table.append(bottomtr);
return table;
}
invcache: [invitejson, { cdn: string; api: string }] | undefined;
generateInvite() {
if (this.invcache && (!this.json.invite || !this.localuser)) {
return this.generateLink();
}
const div = document.createElement("div");
div.classList.add("embed", "inviteEmbed", "flexttb");
const json1 = this.json.invite;
(async () => {
let json: invitejson;
let info: { cdn: string; api: string };
if (!this.invcache) {
if (!json1) {
div.append(this.generateLink());
return;
}
const tempinfo = await getapiurls(json1.url);
if (!tempinfo) {
div.append(this.generateLink());
return;
}
info = tempinfo;
const res = await fetch(info.api + "/invites/" + json1.code);
if (!res.ok) {
div.append(this.generateLink());
}
json = (await res.json()) as invitejson;
this.invcache = [json, info];
} else {
[json, info] = this.invcache;
}
if (!json) {
div.append(this.generateLink());
return;
}
if (json.guild.banner) {
const banner = document.createElement("img");
banner.src =
this.localuser.info.cdn +
"/icons/" +
json.guild.id +
"/" +
json.guild.banner +
".png?size=256";
banner.classList.add("banner");
div.append(banner);
}
const guild: invitejson["guild"] & { info?: { cdn: string } } =
json.guild;
guild.info = info;
const icon = Guild.generateGuildIcon(
guild as invitejson["guild"] & { info: { cdn: string } }
);
const iconrow = document.createElement("div");
iconrow.classList.add("flexltr", "flexstart");
iconrow.append(icon);
{
const guildinfo = document.createElement("div");
guildinfo.classList.add("flexttb", "invguildinfo");
const name = document.createElement("b");
name.textContent = guild.name;
guildinfo.append(name);
const members = document.createElement("span");
members.innerText =
"#" + json.channel.name + " • Members: " + guild.member_count;
guildinfo.append(members);
members.classList.add("subtext");
iconrow.append(guildinfo);
}
div.append(iconrow);
const h2 = document.createElement("h2");
h2.textContent = `You've been invited by ${json.inviter.username}`;
div.append(h2);
const button = document.createElement("button");
button.textContent = "Accept";
if (this.localuser.info.api.startsWith(info.api)) {
if (this.localuser.guildids.has(guild.id)) {
button.textContent = "Already joined";
button.disabled = true;
}
}
button.classList.add("acceptinvbutton");
div.append(button);
button.onclick = (_) => {
if (this.localuser.info.api.startsWith(info.api)) {
fetch(this.localuser.info.api + "/invites/" + json.code, {
method: "POST",
headers: this.localuser.headers,
})
.then((r) => r.json())
.then((_) => {
if (_.message) {
alert(_.message);
}
});
} else {
if (this.json.invite) {
const params = new URLSearchParams("");
params.set("instance", this.json.invite.url);
const encoded = params.toString();
const url = `${location.origin}/invite/${this.json.invite.code}?${encoded}`;
window.open(url, "_blank");
}
}
};
})();
return div;
}
generateArticle() {
const colordiv = document.createElement("div");
colordiv.style.backgroundColor = "#000000";
colordiv.classList.add("embed-color");
const div = document.createElement("div");
div.classList.add("embed");
if (this.json.provider) {
const provider = document.createElement("p");
provider.classList.add("provider");
provider.textContent = this.json.provider.name;
div.append(provider);
}
const a = document.createElement("a");
if (this.json.url && this.json.url) {
MarkDown.safeLink(a, this.json.url);
a.textContent = this.json.url;
div.append(a);
}
if (this.json.description) {
const description = document.createElement("p");
description.textContent = this.json.description;
div.append(description);
}
if (this.json.thumbnail) {
const img = document.createElement("img");
if (this.json.thumbnail.width && this.json.thumbnail.width) {
let scale = 1;
const inch = 96;
scale = Math.max(scale, this.json.thumbnail.width / inch / 4);
scale = Math.max(scale, this.json.thumbnail.height / inch / 3);
this.json.thumbnail.width /= scale;
this.json.thumbnail.height /= scale;
img.style.width = this.json.thumbnail.width + "px";
img.style.height = this.json.thumbnail.height + "px";
}
img.classList.add("bigembedimg");
if (this.json.video) {
img.onclick = async () => {
if (this.json.video) {
img.remove();
const iframe = document.createElement("iframe");
iframe.src = this.json.video.url + "?autoplay=1";
if (this.json.thumbnail.width && this.json.thumbnail.width) {
iframe.style.width = this.json.thumbnail.width + "px";
iframe.style.height = this.json.thumbnail.height + "px";
}
div.append(iframe);
}
};
} else {
img.onclick = async () => {
const full = new Dialog(["img", img.src, ["fit"]]);
full.show();
};
}
img.src = this.json.thumbnail.proxy_url || this.json.thumbnail.url;
div.append(img);
}
colordiv.append(div);
return colordiv;
}
}
export { Embed };

259
src/webpage/emoji.ts Normal file
View file

@ -0,0 +1,259 @@
import { Contextmenu } from "./contextmenu.js";
import { Guild } from "./guild.js";
import { Localuser } from "./localuser.js";
class Emoji {
static emojis: {
name: string;
emojis: {
name: string;
emoji: string;
}[];
}[];
name: string;
id: string;
animated: boolean;
owner: Guild | Localuser;
get guild() {
if (this.owner instanceof Guild) {
return this.owner;
}
return;
}
get localuser() {
if (this.owner instanceof Guild) {
return this.owner.localuser;
} else {
return this.owner;
}
}
get info() {
return this.owner.info;
}
constructor(
json: { name: string; id: string; animated: boolean },
owner: Guild | Localuser
) {
this.name = json.name;
this.id = json.id;
this.animated = json.animated;
this.owner = owner;
}
getHTML(bigemoji: boolean = false) {
const emojiElem = document.createElement("img");
emojiElem.classList.add("md-emoji");
emojiElem.classList.add(bigemoji ? "bigemoji" : "smallemoji");
emojiElem.crossOrigin = "anonymous";
emojiElem.src =
this.info.cdn +
"/emojis/" +
this.id +
"." +
(this.animated ? "gif" : "png") +
"?size=32";
emojiElem.alt = this.name;
emojiElem.loading = "lazy";
return emojiElem;
}
static decodeEmojiList(buffer: ArrayBuffer) {
const view = new DataView(buffer, 0);
let i = 0;
function read16() {
const int = view.getUint16(i);
i += 2;
return int;
}
function read8() {
const int = view.getUint8(i);
i += 1;
return int;
}
function readString8() {
return readStringNo(read8());
}
function readString16() {
return readStringNo(read16());
}
function readStringNo(length: number) {
const array = new Uint8Array(length);
for (let i = 0; i < length; i++) {
array[i] = read8();
}
//console.log(array);
return new TextDecoder("utf-8").decode(array.buffer);
}
const build: { name: string; emojis: { name: string; emoji: string }[] }[] =
[];
let cats = read16();
for (; cats !== 0; cats--) {
const name = readString16();
const emojis: {
name: string;
skin_tone_support: boolean;
emoji: string;
}[] = [];
let emojinumber = read16();
for (; emojinumber !== 0; emojinumber--) {
//console.log(emojis);
const name = readString8();
const len = read8();
const skin_tone_support = len > 127;
const emoji = readStringNo(len - Number(skin_tone_support) * 128);
emojis.push({
name,
skin_tone_support,
emoji,
});
}
build.push({
name,
emojis,
});
}
this.emojis = build;
console.log(build);
}
static grabEmoji() {
fetch("/emoji.bin")
.then((e) => {
return e.arrayBuffer();
})
.then((e) => {
Emoji.decodeEmojiList(e);
});
}
static async emojiPicker(
x: number,
y: number,
localuser: Localuser
): Promise<Emoji | string> {
let res: (r: Emoji | string) => void;
const promise: Promise<Emoji | string> = new Promise((r) => {
res = r;
});
const menu = document.createElement("div");
menu.classList.add("flexttb", "emojiPicker");
menu.style.top = y + "px";
menu.style.left = x + "px";
const title = document.createElement("h2");
title.textContent = Emoji.emojis[0].name;
title.classList.add("emojiTitle");
menu.append(title);
const selection = document.createElement("div");
selection.classList.add("flexltr", "dontshrink", "emojirow");
const body = document.createElement("div");
body.classList.add("emojiBody");
let isFirst = true;
localuser.guilds
.filter((guild) => guild.id != "@me" && guild.emojis.length > 0)
.forEach((guild) => {
const select = document.createElement("div");
select.classList.add("emojiSelect");
if (guild.properties.icon) {
const img = document.createElement("img");
img.classList.add("pfp", "servericon", "emoji-server");
img.crossOrigin = "anonymous";
img.src =
localuser.info.cdn +
"/icons/" +
guild.properties.id +
"/" +
guild.properties.icon +
".png?size=48";
img.alt = "Server: " + guild.properties.name;
select.appendChild(img);
} else {
const div = document.createElement("span");
div.textContent = guild.properties.name
.replace(/'s /g, " ")
.replace(/\w+/g, (word) => word[0])
.replace(/\s/g, "");
select.append(div);
}
selection.append(select);
const clickEvent = () => {
title.textContent = guild.properties.name;
body.innerHTML = "";
for (const emojit of guild.emojis) {
const emojiElem = document.createElement("div");
emojiElem.classList.add("emojiSelect");
const emojiClass = new Emoji(
{
id: emojit.id as string,
name: emojit.name,
animated: emojit.animated as boolean,
},
localuser
);
emojiElem.append(emojiClass.getHTML());
body.append(emojiElem);
emojiElem.addEventListener("click", () => {
res(emojiClass);
if (Contextmenu.currentmenu !== "") {
Contextmenu.currentmenu.remove();
}
});
}
};
select.addEventListener("click", clickEvent);
if (isFirst) {
clickEvent();
isFirst = false;
}
});
setTimeout(() => {
if (Contextmenu.currentmenu != "") {
Contextmenu.currentmenu.remove();
}
document.body.append(menu);
Contextmenu.currentmenu = menu;
Contextmenu.keepOnScreen(menu);
}, 10);
let i = 0;
for (const thing of Emoji.emojis) {
const select = document.createElement("div");
select.textContent = thing.emojis[0].emoji;
select.classList.add("emojiSelect");
selection.append(select);
const clickEvent = () => {
title.textContent = thing.name;
body.innerHTML = "";
for (const emojit of thing.emojis) {
const emoji = document.createElement("div");
emoji.classList.add("emojiSelect");
emoji.textContent = emojit.emoji;
body.append(emoji);
emoji.onclick = (_) => {
res(emojit.emoji);
if (Contextmenu.currentmenu !== "") {
Contextmenu.currentmenu.remove();
}
};
}
};
select.onclick = clickEvent;
if (i === 0) {
clickEvent();
}
i++;
}
menu.append(selection);
menu.append(body);
return promise;
}
}
Emoji.grabEmoji();
export { Emoji };

View file

Before

Width:  |  Height:  |  Size: 906 B

After

Width:  |  Height:  |  Size: 906 B

Before After
Before After

152
src/webpage/file.ts Normal file
View file

@ -0,0 +1,152 @@
import { Message } from "./message.js";
import { Dialog } from "./dialog.js";
import { filejson } from "./jsontypes.js";
class File {
owner: Message | null;
id: string;
filename: string;
content_type: string;
width: number | undefined;
height: number | undefined;
proxy_url: string | undefined;
url: string;
size: number;
constructor(fileJSON: filejson, owner: Message | null) {
this.owner = owner;
this.id = fileJSON.id;
this.filename = fileJSON.filename;
this.content_type = fileJSON.content_type;
this.width = fileJSON.width;
this.height = fileJSON.height;
this.url = fileJSON.url;
this.proxy_url = fileJSON.proxy_url;
this.content_type = fileJSON.content_type;
this.size = fileJSON.size;
}
getHTML(temp: boolean = false): HTMLElement {
const src = this.proxy_url || this.url;
if (this.width && this.height) {
let scale = 1;
const max = 96 * 3;
scale = Math.max(scale, this.width / max);
scale = Math.max(scale, this.height / max);
this.width /= scale;
this.height /= scale;
}
if (this.content_type.startsWith("image/")) {
const div = document.createElement("div");
const img = document.createElement("img");
img.classList.add("messageimg");
div.classList.add("messageimgdiv");
img.onclick = function () {
const full = new Dialog(["img", img.src, ["fit"]]);
full.show();
};
img.src = src;
div.append(img);
if (this.width) {
div.style.width = this.width + "px";
div.style.height = this.height + "px";
}
console.log(img);
console.log(this.width, this.height);
return div;
} else if (this.content_type.startsWith("video/")) {
const video = document.createElement("video");
const source = document.createElement("source");
source.src = src;
video.append(source);
source.type = this.content_type;
video.controls = !temp;
if (this.width && this.height) {
video.width = this.width;
video.height = this.height;
}
return video;
} else if (this.content_type.startsWith("audio/")) {
const audio = document.createElement("audio");
const source = document.createElement("source");
source.src = src;
audio.append(source);
source.type = this.content_type;
audio.controls = !temp;
return audio;
} else {
return this.createunknown();
}
}
upHTML(files: Blob[], file: globalThis.File): HTMLElement {
const div = document.createElement("div");
const contained = this.getHTML(true);
div.classList.add("containedFile");
div.append(contained);
const controls = document.createElement("div");
const garbage = document.createElement("button");
garbage.textContent = "🗑";
garbage.onclick = (_) => {
div.remove();
files.splice(files.indexOf(file), 1);
};
controls.classList.add("controls");
div.append(controls);
controls.append(garbage);
return div;
}
static initFromBlob(file: globalThis.File) {
return new File(
{
filename: file.name,
size: file.size,
id: "null",
content_type: file.type,
width: undefined,
height: undefined,
url: URL.createObjectURL(file),
proxy_url: undefined,
},
null
);
}
createunknown(): HTMLElement {
console.log("🗎");
const src = this.proxy_url || this.url;
const div = document.createElement("table");
div.classList.add("unknownfile");
const nametr = document.createElement("tr");
div.append(nametr);
const fileicon = document.createElement("td");
nametr.append(fileicon);
fileicon.append("🗎");
fileicon.classList.add("fileicon");
fileicon.rowSpan = 2;
const nametd = document.createElement("td");
if (src) {
const a = document.createElement("a");
a.href = src;
a.textContent = this.filename;
nametd.append(a);
} else {
nametd.textContent = this.filename;
}
nametd.classList.add("filename");
nametr.append(nametd);
const sizetr = document.createElement("tr");
const size = document.createElement("td");
sizetr.append(size);
size.textContent = "Size:" + File.filesizehuman(this.size);
size.classList.add("filesize");
div.appendChild(sizetr);
return div;
}
static filesizehuman(fsize: number) {
const i = fsize == 0 ? 0 : Math.floor(Math.log(fsize) / Math.log(1024));
return (
Number((fsize / Math.pow(1024, i)).toFixed(2)) * 1 +
" " +
["Bytes", "Kilobytes", "Megabytes", "Gigabytes", "Terabytes"][i]
);
}
}
export { File };

724
src/webpage/guild.ts Normal file
View file

@ -0,0 +1,724 @@
import { Channel } from "./channel.js";
import { Localuser } from "./localuser.js";
import { Contextmenu } from "./contextmenu.js";
import { Role, RoleList } from "./role.js";
import { Dialog } from "./dialog.js";
import { Member } from "./member.js";
import { Settings } from "./settings.js";
import { Permissions } from "./permissions.js";
import { SnowFlake } from "./snowflake.js";
import {
channeljson,
guildjson,
emojijson,
memberjson,
invitejson,
} from "./jsontypes.js";
import { User } from "./user.js";
class Guild extends SnowFlake {
owner!: Localuser;
headers!: Localuser["headers"];
channels!: Channel[];
properties!: guildjson["properties"];
member_count!: number;
roles!: Role[];
roleids!: Map<string, Role>;
prevchannel: Channel | undefined;
banner!: string;
message_notifications!: number;
headchannels!: Channel[];
position!: number;
parent_id!: string;
member!: Member;
html!: HTMLElement;
emojis!: emojijson[];
large!: boolean;
static contextmenu = new Contextmenu<Guild, undefined>("guild menu");
static setupcontextmenu() {
Guild.contextmenu.addbutton("Copy Guild id", function (this: Guild) {
navigator.clipboard.writeText(this.id);
});
Guild.contextmenu.addbutton("Mark as read", function (this: Guild) {
this.markAsRead();
});
Guild.contextmenu.addbutton("Notifications", function (this: Guild) {
this.setnotifcation();
});
Guild.contextmenu.addbutton(
"Leave guild",
function (this: Guild) {
this.confirmleave();
},
null,
function (_) {
return this.properties.owner_id !== this.member.user.id;
}
);
Guild.contextmenu.addbutton(
"Delete guild",
function (this: Guild) {
this.confirmDelete();
},
null,
function (_) {
return this.properties.owner_id === this.member.user.id;
}
);
Guild.contextmenu.addbutton(
"Create invite",
function (this: Guild) {},
null,
(_) => true,
(_) => false
);
Guild.contextmenu.addbutton("Settings", function (this: Guild) {
this.generateSettings();
});
/* -----things left for later-----
guild.contextmenu.addbutton("Leave Guild",function(){
console.log(this)
this.deleteChannel();
},null,_=>{return thisuser.isAdmin()})
guild.contextmenu.addbutton("Mute Guild",function(){
editchannelf(this);
},null,_=>{return thisuser.isAdmin()})
*/
}
generateSettings() {
const settings = new Settings("Settings for " + this.properties.name);
{
const overview = settings.addButton("Overview");
const form = overview.addForm("", (_) => {}, {
headers: this.headers,
traditionalSubmit: true,
fetchURL: this.info.api + "/guilds/" + this.id,
method: "PATCH",
});
form.addTextInput("Name:", "name", { initText: this.properties.name });
form.addMDInput("Description:", "description", {
initText: this.properties.description,
});
form.addFileInput("Banner:", "banner", { clear: true });
form.addFileInput("Icon:", "icon", { clear: true });
let region = this.properties.region;
if (!region) {
region = "";
}
form.addTextInput("Region:", "region", { initText: region });
}
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))
);
settings.show();
}
constructor(
json: guildjson | -1,
owner: Localuser,
member: memberjson | User | null
) {
if (json === -1 || member === null) {
super("@me");
return;
}
if (json.stickers.length) {
console.log(json.stickers, ":3");
}
super(json.id);
this.large = json.large;
this.member_count = json.member_count;
this.emojis = json.emojis;
this.owner = owner;
this.headers = this.owner.headers;
this.channels = [];
this.properties = json.properties;
this.roles = [];
this.roleids = new Map();
this.message_notifications = 0;
for (const roley of json.roles) {
const roleh = new Role(roley, this);
this.roles.push(roleh);
this.roleids.set(roleh.id, roleh);
}
if (member instanceof User) {
Member.resolveMember(member, this).then((_) => {
if (_) {
this.member = _;
} else {
console.error("Member was unable to resolve");
}
});
} else {
Member.new(member, this).then((_) => {
if (_) {
this.member = _;
}
});
}
this.perminfo ??= { channels: {} };
for (const thing of json.channels) {
const temp = new Channel(thing, this);
this.channels.push(temp);
this.localuser.channelids.set(temp.id, temp);
}
this.headchannels = [];
for (const thing of this.channels) {
const parent = thing.resolveparent(this);
if (!parent) {
this.headchannels.push(thing);
}
}
this.prevchannel = this.localuser.channelids.get(this.perminfo.prevchannel);
}
get perminfo() {
return this.localuser.perminfo.guilds[this.id];
}
set perminfo(e) {
this.localuser.perminfo.guilds[this.id] = e;
}
notisetting(settings: {
channel_overrides?: unknown[];
message_notifications: any;
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;
}) {
this.message_notifications = settings.message_notifications;
}
setnotifcation() {
let noti = this.message_notifications;
const notiselect = new Dialog([
"vdiv",
[
"radio",
"select notifications type",
["all", "only mentions", "none"],
function (e: string /* "all" | "only mentions" | "none" */) {
noti = ["all", "only mentions", "none"].indexOf(e);
},
noti,
],
[
"button",
"",
"submit",
(_: any) => {
//
fetch(this.info.api + `/users/@me/guilds/${this.id}/settings/`, {
method: "PATCH",
headers: this.headers,
body: JSON.stringify({
message_notifications: noti,
}),
});
this.message_notifications = noti;
},
],
]);
notiselect.show();
}
confirmleave() {
const full = new Dialog([
"vdiv",
["title", "Are you sure you want to leave?"],
[
"hdiv",
[
"button",
"",
"Yes, I'm sure",
(_: any) => {
this.leave().then((_) => {
full.hide();
});
},
],
[
"button",
"",
"Nevermind",
(_: any) => {
full.hide();
},
],
],
]);
full.show();
}
async leave() {
return fetch(this.info.api + "/users/@me/guilds/" + this.id, {
method: "DELETE",
headers: this.headers,
});
}
printServers() {
let build = "";
for (const thing of this.headchannels) {
build += thing.name + ":" + thing.position + "\n";
for (const thingy of thing.children) {
build += " " + thingy.name + ":" + thingy.position + "\n";
}
}
console.log(build);
}
calculateReorder() {
let position = -1;
const build: {
id: string;
position: number | undefined;
parent_id: string | undefined;
}[] = [];
for (const thing of this.headchannels) {
const thisthing: {
id: string;
position: number | undefined;
parent_id: string | undefined;
} = { id: thing.id, position: undefined, parent_id: undefined };
if (thing.position <= position) {
thing.position = thisthing.position = position + 1;
}
position = thing.position;
console.log(position);
if (thing.move_id && thing.move_id !== thing.parent_id) {
thing.parent_id = thing.move_id;
thisthing.parent_id = thing.parent?.id;
thing.move_id = undefined;
}
if (thisthing.position || thisthing.parent_id) {
build.push(thisthing);
}
if (thing.children.length > 0) {
const things = thing.calculateReorder();
for (const thing of things) {
build.push(thing);
}
}
}
console.log(build);
this.printServers();
if (build.length === 0) {
return;
}
const serverbug = false;
if (serverbug) {
for (const thing of build) {
console.log(build, thing);
fetch(this.info.api + "/guilds/" + this.id + "/channels", {
method: "PATCH",
headers: this.headers,
body: JSON.stringify([thing]),
});
}
} else {
fetch(this.info.api + "/guilds/" + this.id + "/channels", {
method: "PATCH",
headers: this.headers,
body: JSON.stringify(build),
});
}
}
get localuser() {
return this.owner;
}
get info() {
return this.owner.info;
}
sortchannels() {
this.headchannels.sort((a, b) => {
return a.position - b.position;
});
}
static generateGuildIcon(
guild: Guild | (invitejson["guild"] & { info: { cdn: string } })
) {
const divy = document.createElement("div");
divy.classList.add("servernoti");
const noti = document.createElement("div");
noti.classList.add("unread");
divy.append(noti);
if (guild instanceof Guild) {
guild.localuser.guildhtml.set(guild.id, divy);
}
let icon: string | null;
if (guild instanceof Guild) {
icon = guild.properties.icon;
} else {
icon = guild.icon;
}
if (icon !== null) {
const img = document.createElement("img");
img.classList.add("pfp", "servericon");
img.src = guild.info.cdn + "/icons/" + guild.id + "/" + icon + ".png";
divy.appendChild(img);
if (guild instanceof Guild) {
img.onclick = () => {
console.log(guild.loadGuild);
guild.loadGuild();
guild.loadChannel();
};
Guild.contextmenu.bindContextmenu(img, guild, undefined);
}
} else {
const div = document.createElement("div");
let name: string;
if (guild instanceof Guild) {
name = guild.properties.name;
} else {
name = guild.name;
}
const build = name
.replace(/'s /g, " ")
.replace(/\w+/g, (word) => word[0])
.replace(/\s/g, "");
div.textContent = build;
div.classList.add("blankserver", "servericon");
divy.appendChild(div);
if (guild instanceof Guild) {
div.onclick = () => {
guild.loadGuild();
guild.loadChannel();
};
Guild.contextmenu.bindContextmenu(div, guild, undefined);
}
}
return divy;
}
generateGuildIcon() {
return Guild.generateGuildIcon(this);
}
confirmDelete() {
let confirmname = "";
const full = new Dialog([
"vdiv",
[
"title",
"Are you sure you want to delete " + this.properties.name + "?",
],
[
"textbox",
"Name of server:",
"",
function (this: HTMLInputElement) {
confirmname = this.value;
},
],
[
"hdiv",
[
"button",
"",
"Yes, I'm sure",
(_: any) => {
console.log(confirmname);
if (confirmname !== this.properties.name) {
return;
}
this.delete().then((_) => {
full.hide();
});
},
],
[
"button",
"",
"Nevermind",
(_: any) => {
full.hide();
},
],
],
]);
full.show();
}
async delete() {
return fetch(this.info.api + "/guilds/" + this.id + "/delete", {
method: "POST",
headers: this.headers,
});
}
unreads(html?: HTMLElement | undefined) {
if (html) {
this.html = html;
} else {
html = this.html;
}
let read = true;
for (const thing of this.channels) {
if (thing.hasunreads) {
console.log(thing);
read = false;
break;
}
}
if (!html) {
return;
}
if (read) {
html.children[0].classList.remove("notiunread");
} else {
html.children[0].classList.add("notiunread");
}
}
getHTML() {
//this.printServers();
this.sortchannels();
this.printServers();
const build = document.createElement("div");
for (const thing of this.headchannels) {
build.appendChild(thing.createguildHTML(this.isAdmin()));
}
return build;
}
isAdmin() {
return this.member.isAdmin();
}
async markAsRead() {
const build: {
read_states: {
channel_id: string;
message_id: string | null | undefined;
read_state_type: number;
}[];
} = { read_states: [] };
for (const thing of this.channels) {
if (thing.hasunreads) {
build.read_states.push({
channel_id: thing.id,
message_id: thing.lastmessageid,
read_state_type: 0,
});
thing.lastreadmessageid = thing.lastmessageid;
if (!thing.myhtml) continue;
thing.myhtml.classList.remove("cunread");
}
}
this.unreads();
fetch(this.info.api + "/read-states/ack-bulk", {
method: "POST",
headers: this.headers,
body: JSON.stringify(build),
});
}
hasRole(r: Role | string) {
console.log("this should run");
if (r instanceof Role) {
r = r.id;
}
return this.member.hasRole(r);
}
loadChannel(ID?: string | undefined) {
if (ID) {
const channel = this.localuser.channelids.get(ID);
if (channel) {
channel.getHTML();
return;
}
}
if (this.prevchannel) {
console.log(this.prevchannel);
this.prevchannel.getHTML();
return;
}
for (const thing of this.channels) {
if (thing.children.length === 0) {
thing.getHTML();
return;
}
}
}
loadGuild() {
this.localuser.loadGuild(this.id);
}
updateChannel(json: channeljson) {
const channel = this.localuser.channelids.get(json.id);
if (channel) {
channel.updateChannel(json);
this.headchannels = [];
for (const thing of this.channels) {
thing.children = [];
}
this.headchannels = [];
for (const thing of this.channels) {
const parent = thing.resolveparent(this);
if (!parent) {
this.headchannels.push(thing);
}
}
this.printServers();
}
}
createChannelpac(json: channeljson) {
const thischannel = new Channel(json, this);
this.localuser.channelids.set(json.id, thischannel);
this.channels.push(thischannel);
thischannel.resolveparent(this);
if (!thischannel.parent) {
this.headchannels.push(thischannel);
}
this.calculateReorder();
this.printServers();
return thischannel;
}
createchannels(func = this.createChannel) {
let name = "";
let category = 0;
const channelselect = new Dialog([
"vdiv",
[
"radio",
"select channel type",
["voice", "text", "announcement"],
function (radio: string) {
console.log(radio);
category =
{ text: 0, voice: 2, announcement: 5, category: 4 }[radio] || 0;
},
1,
],
[
"textbox",
"Name of channel",
"",
function (this: HTMLInputElement) {
name = this.value;
},
],
[
"button",
"",
"submit",
function () {
console.log(name, category);
func(name, category);
channelselect.hide();
},
],
]);
channelselect.show();
}
createcategory() {
let name = "";
const category = 4;
const channelselect = new Dialog([
"vdiv",
[
"textbox",
"Name of category",
"",
function (this: HTMLInputElement) {
name = this.value;
},
],
[
"button",
"",
"submit",
() => {
console.log(name, category);
this.createChannel(name, category);
channelselect.hide();
},
],
]);
channelselect.show();
}
delChannel(json: channeljson) {
const channel = this.localuser.channelids.get(json.id);
this.localuser.channelids.delete(json.id);
if (!channel) return;
this.channels.splice(this.channels.indexOf(channel), 1);
const indexy = this.headchannels.indexOf(channel);
if (indexy !== -1) {
this.headchannels.splice(indexy, 1);
}
/*
const build=[];
for(const thing of this.channels){
console.log(thing.id);
if(thing!==channel){
build.push(thing)
}else{
console.log("fail");
if(thing.parent){
thing.parent.delChannel(json);
}
}
}
this.channels=build;
*/
this.printServers();
}
createChannel(name: string, type: number) {
fetch(this.info.api + "/guilds/" + this.id + "/channels", {
method: "POST",
headers: this.headers,
body: JSON.stringify({ name, type }),
});
}
async createRole(name: string) {
const fetched = await fetch(
this.info.api + "/guilds/" + this.id + "roles",
{
method: "POST",
headers: this.headers,
body: JSON.stringify({
name,
color: 0,
permissions: "0",
}),
}
);
const json = await fetched.json();
const role = new Role(json, this);
this.roleids.set(role.id, role);
this.roles.push(role);
return role;
}
async updateRolePermissions(id: string, perms: Permissions) {
const role = this.roleids.get(id);
if (!role) {
return;
}
role.permissions.allow = perms.allow;
role.permissions.deny = perms.deny;
await fetch(this.info.api + "/guilds/" + this.id + "/roles/" + role.id, {
method: "PATCH",
headers: this.headers,
body: JSON.stringify({
color: role.color,
hoist: role.hoist,
icon: role.icon,
mentionable: role.mentionable,
name: role.name,
permissions: role.permissions.allow.toString(),
unicode_emoji: role.unicode_emoji,
}),
});
}
}
Guild.setupcontextmenu();
export { Guild };

View file

@ -4,8 +4,7 @@
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Jank Client</title>
<meta content="Jank Client" property="og:title">
<meta content="A spacebar client that has DMs, replying and more" property="og:description">
<meta name="description" content="A spacebar client that has DMs, replying and more">
<meta content="/logo.webp" property="og:image">
<meta content="#4b458c" data-react-helmet="true" name="theme-color">
<link href="/style.css" rel="stylesheet">
@ -14,10 +13,12 @@
<body class="Dark-theme">
<div id="titleDiv">
<img src="/logo.svg" width="40">
<img src="/logo.svg" width="40" alt="Jank Client Logo">
<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></a>
<a href="https://github.com/MathMan05/JankClient" class="TitleButtons"><h1>Github</h1></a>
<a href="https://sb-jankclient.vanillaminigames.net/invite/USgYJo?instance=https%3A%2F%2Fspacebar.chat" class="TitleButtons">Spacebar Guild</a>
<a href="https://github.com/MathMan05/JankClient" class="TitleButtons">Github</a>
<a href="https://sb-jankclient.vanillaminigames.net/invite/USgYJo?instance=https%3A%2F%2Fspacebar.chat" class="TitleButtons">Spacebar Guild</a>
<a href="https://github.com/MathMan05/JankClient" class="TitleButtons">Github</a>
</div>
<div class="flexttb">

89
src/webpage/home.ts Normal file
View file

@ -0,0 +1,89 @@
import { mobile } from "./login.js";
console.log(mobile);
const serverbox = document.getElementById("instancebox") as HTMLDivElement;
fetch("/instances.json")
.then((_) => _.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;
};
}[]
) => {
console.warn(json);
for (const instance of json) {
if (instance.display === false) {
continue;
}
const div = document.createElement("div");
div.classList.add("flexltr", "instance");
if (instance.image) {
const img = document.createElement("img");
img.src = instance.image;
div.append(img);
}
const statbox = document.createElement("div");
statbox.classList.add("flexttb");
{
const textbox = document.createElement("div");
textbox.classList.add("flexttb", "instatancetextbox");
const title = document.createElement("h2");
title.innerText = instance.name;
if (instance.online !== undefined) {
const status = document.createElement("span");
status.innerText = instance.online ? "Online" : "Offline";
status.classList.add("instanceStatus");
title.append(status);
}
textbox.append(title);
if (instance.description || instance.descriptionLong) {
const p = document.createElement("p");
if (instance.descriptionLong) {
p.innerText = instance.descriptionLong;
} else if (instance.description) {
p.innerText = instance.description;
}
textbox.append(p);
}
statbox.append(textbox);
}
if (instance.uptime) {
const stats = document.createElement("div");
stats.classList.add("flexltr");
const span = document.createElement("span");
span.innerText = `Uptime: All time: ${Math.round(
instance.uptime.alltime * 100
)}% This week: ${Math.round(
instance.uptime.weektime * 100
)}% Today: ${Math.round(instance.uptime.daytime * 100)}%`;
stats.append(span);
statbox.append(stats);
}
div.append(statbox);
div.onclick = (_) => {
if (instance.online) {
window.location.href =
"/register.html?instance=" + encodeURI(instance.name);
} else {
alert("Instance is offline, can't connect");
}
};
serverbox.append(div);
}
}
);

View file

Before

Width:  |  Height:  |  Size: 606 B

After

Width:  |  Height:  |  Size: 606 B

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 191 B

After

Width:  |  Height:  |  Size: 191 B

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 220 B

After

Width:  |  Height:  |  Size: 220 B

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 262 B

After

Width:  |  Height:  |  Size: 262 B

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 260 B

After

Width:  |  Height:  |  Size: 260 B

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 633 B

After

Width:  |  Height:  |  Size: 633 B

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 565 B

After

Width:  |  Height:  |  Size: 565 B

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 274 B

After

Width:  |  Height:  |  Size: 274 B

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 462 B

After

Width:  |  Height:  |  Size: 462 B

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 605 B

After

Width:  |  Height:  |  Size: 605 B

Before After
Before After

View file

@ -2,7 +2,8 @@
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0">
<meta name="description" content="A spacebar client that has DMs, replying and more">
<title>Jank Client</title>
<meta content="Jank Client" property="og:title">
<meta content="A spacebar client that has DMs, replying and more" property="og:description">
@ -19,10 +20,10 @@
<div id="loading" class="loading">
<div id="centerdiv">
<img src="/logo.svg" style="width:3in;height:3in;">
<img src="/logo.svg" alt="Logo" class="logo-image">
<h1>Jank Client is loading</h1>
<h2 id="load-desc">This shouldn't take long</h2>
<h1 id="switchaccounts">Switch Accounts</h1>
<h2 id="switchaccounts">Switch Accounts</h2>
</div>
</div>
<div class="flexltr" id="page">
@ -36,7 +37,7 @@
<div id="channels"></div>
<div class="flexltr" id="userdock">
<div class="flexltr" id="userinfo">
<img id="userpfp" class="pfp">
<img id="userpfp" class="pfp" alt="User Profile Picture">
<div class="userflex">
<p id="username">USERNAME</p>

259
src/webpage/index.ts Normal file
View file

@ -0,0 +1,259 @@
import { Localuser } from "./localuser.js";
import { Contextmenu } from "./contextmenu.js";
import { mobile, getBulkUsers, setTheme, Specialuser } from "./login.js";
import { MarkDown } from "./markdown.js";
import { Message } from "./message.js";
import { File } from "./file.js";
(async () => {
async function waitForLoad(): Promise<void> {
return new Promise((resolve) => {
document.addEventListener("DOMContentLoaded", (_) => resolve());
});
}
await waitForLoad();
const users = getBulkUsers();
if (!users.currentuser) {
window.location.href = "/login.html";
return;
}
function showAccountSwitcher(): void {
const table = document.createElement("div");
table.classList.add("accountSwitcher");
for (const user of Object.values(users.users)) {
const specialUser = user as Specialuser;
const userInfo = document.createElement("div");
userInfo.classList.add("flexltr", "switchtable");
const pfp = document.createElement("img");
pfp.src = specialUser.pfpsrc;
pfp.classList.add("pfp");
userInfo.append(pfp);
const userDiv = document.createElement("div");
userDiv.classList.add("userinfo");
userDiv.textContent = specialUser.username;
userDiv.append(document.createElement("br"));
const span = document.createElement("span");
span.textContent = specialUser.serverurls.wellknown
.replace("https://", "")
.replace("http://", "");
span.classList.add("serverURL");
userDiv.append(span);
userInfo.append(userDiv);
table.append(userInfo);
userInfo.addEventListener("click", () => {
thisUser.unload();
thisUser.swapped = true;
const loading = document.getElementById("loading") as HTMLDivElement;
loading.classList.remove("doneloading");
loading.classList.add("loading");
thisUser = new Localuser(specialUser);
users.currentuser = specialUser.uid;
localStorage.setItem("userinfos", JSON.stringify(users));
thisUser.initwebsocket().then(() => {
thisUser.loaduser();
thisUser.init();
loading.classList.add("doneloading");
loading.classList.remove("loading");
console.log("done loading");
});
userInfo.remove();
});
}
const switchAccountDiv = document.createElement("div");
switchAccountDiv.classList.add("switchtable");
switchAccountDiv.textContent = "Switch accounts ⇌";
switchAccountDiv.addEventListener("click", () => {
window.location.href = "/login.html";
});
table.append(switchAccountDiv);
if (Contextmenu.currentmenu) {
Contextmenu.currentmenu.remove();
}
Contextmenu.currentmenu = table;
document.body.append(table);
}
const userInfoElement = document.getElementById("userinfo") as HTMLDivElement;
userInfoElement.addEventListener("click", (event) => {
event.stopImmediatePropagation();
showAccountSwitcher();
});
const switchAccountsElement = document.getElementById(
"switchaccounts"
) as HTMLDivElement;
switchAccountsElement.addEventListener("click", (event) => {
event.stopImmediatePropagation();
showAccountSwitcher();
});
let thisUser: Localuser;
try {
console.log(users.users, users.currentuser);
thisUser = new Localuser(users.users[users.currentuser]);
thisUser.initwebsocket().then(() => {
thisUser.loaduser();
thisUser.init();
const loading = document.getElementById("loading") as HTMLDivElement;
loading.classList.add("doneloading");
loading.classList.remove("loading");
console.log("done loading");
});
} catch (e) {
console.error(e);
(document.getElementById("load-desc") as HTMLSpanElement).textContent =
"Account unable to start";
thisUser = new Localuser(-1);
}
const menu = new Contextmenu("create rightclick");
menu.addbutton(
"Create channel",
() => {
if (thisUser.lookingguild) {
thisUser.lookingguild.createchannels();
}
},
null,
() => thisUser.isAdmin()
);
menu.addbutton(
"Create category",
() => {
if (thisUser.lookingguild) {
thisUser.lookingguild.createcategory();
}
},
null,
() => thisUser.isAdmin()
);
menu.bindContextmenu(
document.getElementById("channels") as HTMLDivElement,
0,
0
);
const pasteImageElement = document.getElementById(
"pasteimage"
) as HTMLDivElement;
let replyingTo: Message | null = null;
async function handleEnter(event: KeyboardEvent): Promise<void> {
const channel = thisUser.channelfocus;
if (!channel) return;
channel.typingstart();
if (event.key === "Enter" && !event.shiftKey) {
event.preventDefault();
if (channel.editing) {
channel.editing.edit(markdown.rawString);
channel.editing = null;
} else {
replyingTo = thisUser.channelfocus
? thisUser.channelfocus.replyingto
: null;
if (replyingTo?.div) {
replyingTo.div.classList.remove("replying");
}
if (thisUser.channelfocus) {
thisUser.channelfocus.replyingto = null;
}
channel.sendMessage(markdown.rawString, {
attachments: images,
// @ts-ignore This is valid according to the API
embeds: [], // Add an empty array for the embeds property
replyingto: replyingTo,
});
if (thisUser.channelfocus) {
thisUser.channelfocus.makereplybox();
}
}
while (images.length) {
images.pop();
pasteImageElement.removeChild(imagesHtml.pop() as HTMLElement);
}
typebox.innerHTML = "";
}
}
interface CustomHTMLDivElement extends HTMLDivElement {
markdown: MarkDown;
}
const typebox = document.getElementById("typebox") as CustomHTMLDivElement;
const markdown = new MarkDown("", thisUser);
typebox.markdown = markdown;
typebox.addEventListener("keyup", handleEnter);
typebox.addEventListener("keydown", (event) => {
if (event.key === "Enter" && !event.shiftKey) event.preventDefault();
});
markdown.giveBox(typebox);
const images: Blob[] = [];
const imagesHtml: HTMLElement[] = [];
document.addEventListener("paste", async (e: ClipboardEvent) => {
if (!e.clipboardData) return;
for (const file of Array.from(e.clipboardData.files)) {
const fileInstance = File.initFromBlob(file);
e.preventDefault();
const html = fileInstance.upHTML(images, file);
pasteImageElement.appendChild(html);
images.push(file);
imagesHtml.push(html);
}
});
setTheme();
function userSettings(): void {
thisUser.showusersettings();
}
(document.getElementById("settings") as HTMLImageElement).onclick =
userSettings;
if (mobile) {
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");
};
}
})();

View file

@ -0,0 +1,323 @@
class InfiniteScroller {
readonly getIDFromOffset: (
ID: string,
offset: number
) => Promise<string | undefined>;
readonly getHTMLFromID: (ID: string) => Promise<HTMLElement>;
readonly destroyFromID: (ID: string) => Promise<boolean>;
readonly reachesBottom: () => void;
private readonly minDist = 2000;
private readonly fillDist = 3000;
private readonly maxDist = 6000;
HTMLElements: [HTMLElement, string][] = [];
div: HTMLDivElement | null = null;
timeout: NodeJS.Timeout | null = null;
beenloaded = false;
scrollBottom = 0;
scrollTop = 0;
needsupdate = true;
averageheight = 60;
watchtime = false;
changePromise: Promise<boolean> | undefined;
scollDiv!: { scrollTop: number; scrollHeight: number; clientHeight: number };
constructor(
getIDFromOffset: InfiniteScroller["getIDFromOffset"],
getHTMLFromID: InfiniteScroller["getHTMLFromID"],
destroyFromID: InfiniteScroller["destroyFromID"],
reachesBottom: InfiniteScroller["reachesBottom"] = () => {}
) {
this.getIDFromOffset = getIDFromOffset;
this.getHTMLFromID = getHTMLFromID;
this.destroyFromID = destroyFromID;
this.reachesBottom = reachesBottom;
}
async getDiv(initialId: string): Promise<HTMLDivElement> {
if (this.div) {
throw new Error("Div already exists, exiting.");
}
const scroll = document.createElement("div");
scroll.classList.add("flexttb", "scroller");
this.div = scroll;
this.div.addEventListener("scroll", () => {
this.checkscroll();
if (this.scrollBottom < 5) {
this.scrollBottom = 5;
}
if (this.timeout === null) {
this.timeout = setTimeout(this.updatestuff.bind(this), 300);
}
this.watchForChange();
});
let oldheight = 0;
new ResizeObserver(() => {
this.checkscroll();
const func = this.snapBottom();
this.updatestuff();
const change = oldheight - scroll.offsetHeight;
if (change > 0 && this.div) {
this.div.scrollTop += change;
}
oldheight = scroll.offsetHeight;
this.watchForChange();
func();
}).observe(scroll);
new ResizeObserver(this.watchForChange.bind(this)).observe(scroll);
await this.firstElement(initialId);
this.updatestuff();
await this.watchForChange().then(() => {
this.updatestuff();
this.beenloaded = true;
});
return scroll;
}
checkscroll(): void {
if (this.beenloaded && this.div && !document.body.contains(this.div)) {
console.warn("not in document");
this.div = null;
}
}
async updatestuff(): Promise<void> {
this.timeout = null;
if (!this.div) return;
this.scrollBottom =
this.div.scrollHeight - this.div.scrollTop - this.div.clientHeight;
this.averageheight = this.div.scrollHeight / this.HTMLElements.length;
if (this.averageheight < 10) {
this.averageheight = 60;
}
this.scrollTop = this.div.scrollTop;
if (!this.scrollBottom && !(await this.watchForChange())) {
this.reachesBottom();
}
if (!this.scrollTop) {
await this.watchForChange();
}
this.needsupdate = false;
}
async firstElement(id: string): Promise<void> {
if (!this.div) return;
const html = await this.getHTMLFromID(id);
this.div.appendChild(html);
this.HTMLElements.push([html, id]);
}
async addedBottom(): Promise<void> {
await this.updatestuff();
const func = this.snapBottom();
await this.watchForChange();
func();
}
snapBottom(): () => void {
const scrollBottom = this.scrollBottom;
return () => {
if (this.div && scrollBottom < 4) {
this.div.scrollTop = this.div.scrollHeight;
}
};
}
private async watchForTop(
already = false,
fragment = new DocumentFragment()
): Promise<boolean> {
if (!this.div) return false;
try {
let again = false;
if (this.scrollTop < (already ? this.fillDist : this.minDist)) {
let nextid: string | undefined;
const firstelm = this.HTMLElements.at(0);
if (firstelm) {
const previd = firstelm[1];
nextid = await this.getIDFromOffset(previd, 1);
}
if (nextid) {
const html = await this.getHTMLFromID(nextid);
if (!html) {
this.destroyFromID(nextid);
return false;
}
again = true;
fragment.prepend(html);
this.HTMLElements.unshift([html, nextid]);
this.scrollTop += this.averageheight;
}
}
if (this.scrollTop > this.maxDist) {
const html = this.HTMLElements.shift();
if (html) {
again = true;
await this.destroyFromID(html[1]);
this.scrollTop -= this.averageheight;
}
}
if (again) {
await this.watchForTop(true, fragment);
}
return again;
} finally {
if (!already) {
if (this.div.scrollTop === 0) {
this.scrollTop = 1;
this.div.scrollTop = 10;
}
this.div.prepend(fragment, fragment);
}
}
}
async watchForBottom(
already = false,
fragment = new DocumentFragment()
): Promise<boolean> {
let func: Function | undefined;
if (!already) func = this.snapBottom();
if (!this.div) return false;
try {
let again = false;
const scrollBottom = this.scrollBottom;
if (scrollBottom < (already ? this.fillDist : this.minDist)) {
let nextid: string | undefined;
const lastelm = this.HTMLElements.at(-1);
if (lastelm) {
const previd = lastelm[1];
nextid = await this.getIDFromOffset(previd, -1);
}
if (nextid) {
again = true;
const html = await this.getHTMLFromID(nextid);
fragment.appendChild(html);
this.HTMLElements.push([html, nextid]);
this.scrollBottom += this.averageheight;
}
}
if (scrollBottom > this.maxDist) {
const html = this.HTMLElements.pop();
if (html) {
await this.destroyFromID(html[1]);
this.scrollBottom -= this.averageheight;
again = true;
}
}
if (again) {
await this.watchForBottom(true, fragment);
}
return again;
} finally {
if (!already) {
this.div.append(fragment);
if (func) {
func();
}
}
}
}
async watchForChange(): Promise<boolean> {
if (this.changePromise) {
this.watchtime = true;
return await this.changePromise;
} else {
this.watchtime = false;
}
this.changePromise = new Promise<boolean>(async (res) => {
try {
if (!this.div) {
res(false);
return false;
}
const out = (await Promise.allSettled([
this.watchForTop(),
this.watchForBottom(),
])) as { value: boolean }[];
const changed = out[0].value || out[1].value;
if (this.timeout === null && changed) {
this.timeout = setTimeout(this.updatestuff.bind(this), 300);
}
res(Boolean(changed));
return Boolean(changed);
} catch (e) {
console.error(e);
res(false);
return false;
} finally {
setTimeout(() => {
this.changePromise = undefined;
if (this.watchtime) {
this.watchForChange();
}
}, 300);
}
});
return await this.changePromise;
}
async focus(id: string, flash = true): Promise<void> {
let element: HTMLElement | undefined;
for (const thing of this.HTMLElements) {
if (thing[1] === id) {
element = thing[0];
}
}
if (element) {
if (flash) {
element.scrollIntoView({
behavior: "smooth",
block: "center",
});
await new Promise((resolve) => setTimeout(resolve, 1000));
element.classList.remove("jumped");
await new Promise((resolve) => setTimeout(resolve, 100));
element.classList.add("jumped");
} else {
element.scrollIntoView();
}
} else {
for (const thing of this.HTMLElements) {
await this.destroyFromID(thing[1]);
}
this.HTMLElements = [];
await this.firstElement(id);
this.updatestuff();
await this.watchForChange();
await new Promise((resolve) => setTimeout(resolve, 100));
await this.focus(id, true);
}
}
async delete(): Promise<void> {
if (this.div) {
this.div.remove();
this.div = null;
}
try {
for (const thing of this.HTMLElements) {
await this.destroyFromID(thing[1]);
}
} catch (e) {
console.error(e);
}
this.HTMLElements = [];
if (this.timeout) {
clearTimeout(this.timeout);
}
}
}
export { InfiniteScroller };

View file

@ -1,28 +1,28 @@
[
{
"name":"Spacebar",
"description":"The official Spacebar instance.",
"image":"https://raw.githubusercontent.com/spacebarchat/spacebarchat/master/branding/png/Spacebar__Icon-Discord.png",
"url":"https://spacebar.chat"
"name": "Spacebar",
"description": "The official Spacebar instance.",
"image": "https://raw.githubusercontent.com/spacebarchat/spacebarchat/master/branding/png/Spacebar__Icon-Discord.png",
"url": "https://spacebar.chat"
},
{
"name": "Fastbar",
"description": "The best Spacebar instance with 95% uptime, running under on a NVME drive running with bleeding edge stuff <3",
"image": "https://avatars.githubusercontent.com/u/65827291",
"url": "https://greysilly7.xyz",
"language": "ENG",
"country": "United States",
"language": "en",
"country": "US",
"display": true,
"urls":{
"urls": {
"wellknown": "https://greysilly7.xyz",
"api": "https://spacebar.greysilly7.xyz/api/v9",
"cdn": "https://spacebar.greysilly7.xyz",
"gateway": "wss://spacebar.greysilly7.xyz"
},
"contactInfo":{
"contactInfo": {
"dicord": "greysilly7",
"github": "https://github.com/greysilly7",
"email": "greysilly7@gmail.com"
}
}
]
]

View file

@ -4,7 +4,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Jank Client</title>
<meta content="Invite" property="og:title">
<meta content="You shouldn't see this, but this is an invite URL" property="og:description">
<meta name="description" content="You shouldn't see this, but this is an invite URL">
<meta content="/logo.webp" property="og:image">
<meta content="#4b458c" data-react-helmet="true" name="theme-color">
<link href="/style.css" rel="stylesheet">

147
src/webpage/invite.ts Normal file
View file

@ -0,0 +1,147 @@
import { getBulkUsers, Specialuser, getapiurls } from "./login.js";
(async () => {
const users = getBulkUsers();
const well = new URLSearchParams(window.location.search).get("instance");
const joinable: Specialuser[] = [];
for (const key in users.users) {
if (Object.prototype.hasOwnProperty.call(users.users, key)) {
const user: Specialuser = users.users[key];
if (well && user.serverurls.wellknown.includes(well)) {
joinable.push(user);
}
console.log(user);
}
}
let urls: { api: string; cdn: string } | undefined;
if (!joinable.length && well) {
const out = await getapiurls(well);
if (out) {
urls = out;
for (const key in users.users) {
if (Object.prototype.hasOwnProperty.call(users.users, key)) {
const user: Specialuser = users.users[key];
if (user.serverurls.api.includes(out.api)) {
joinable.push(user);
}
console.log(user);
}
}
} else {
throw new Error(
"Someone needs to handle the case where the servers don't exist"
);
}
} else {
urls = joinable[0].serverurls;
}
if (!joinable.length) {
document.getElementById("AcceptInvite")!.textContent =
"Create an account to accept the invite";
}
const code = window.location.pathname.split("/")[2];
let guildinfo: any;
fetch(`${urls!.api}/invites/${code}`, {
method: "GET",
})
.then((response) => response.json())
.then((json) => {
const guildjson = json.guild;
guildinfo = guildjson;
document.getElementById("invitename")!.textContent = guildjson.name;
document.getElementById(
"invitedescription"
)!.textContent = `${json.inviter.username} invited you to join ${guildjson.name}`;
if (guildjson.icon) {
const img = document.createElement("img");
img.src = `${urls!.cdn}/icons/${guildjson.id}/${guildjson.icon}.png`;
img.classList.add("inviteGuild");
document.getElementById("inviteimg")!.append(img);
} else {
const txt = guildjson.name
.replace(/'s /g, " ")
.replace(/\w+/g, (word: any[]) => word[0])
.replace(/\s/g, "");
const div = document.createElement("div");
div.textContent = txt;
div.classList.add("inviteGuild");
document.getElementById("inviteimg")!.append(div);
}
});
function showAccounts(): void {
const table = document.createElement("dialog");
for (const user of joinable) {
console.log(user.pfpsrc);
const userinfo = document.createElement("div");
userinfo.classList.add("flexltr", "switchtable");
const pfp = document.createElement("img");
pfp.src = user.pfpsrc;
pfp.classList.add("pfp");
userinfo.append(pfp);
const userDiv = document.createElement("div");
userDiv.classList.add("userinfo");
userDiv.textContent = user.username;
userDiv.append(document.createElement("br"));
const span = document.createElement("span");
span.textContent = user.serverurls.wellknown
.replace("https://", "")
.replace("http://", "");
span.classList.add("serverURL");
userDiv.append(span);
userinfo.append(userDiv);
table.append(userinfo);
userinfo.addEventListener("click", () => {
console.log(user);
fetch(`${urls!.api}/invites/${code}`, {
method: "POST",
headers: {
Authorization: user.token,
},
}).then(() => {
users.currentuser = user.uid;
localStorage.setItem("userinfos", JSON.stringify(users));
window.location.href = "/channels/" + guildinfo.id;
});
});
}
const td = document.createElement("div");
td.classList.add("switchtable");
td.textContent = "Login or create an account ⇌";
td.addEventListener("click", () => {
const l = new URLSearchParams("?");
l.set("goback", window.location.href);
l.set("instance", well!);
window.location.href = "/login?" + l.toString();
});
if (!joinable.length) {
const l = new URLSearchParams("?");
l.set("goback", window.location.href);
l.set("instance", well!);
window.location.href = "/login?" + l.toString();
}
table.append(td);
table.classList.add("accountSwitcher");
console.log(table);
document.body.append(table);
}
document
.getElementById("AcceptInvite")!
.addEventListener("click", showAccounts);
})();

501
src/webpage/jsontypes.ts Normal file
View file

@ -0,0 +1,501 @@
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;
};
};
};
type mainuserjson = userjson & {
flags: number;
mfa_enabled?: boolean;
email?: string;
phone?: string;
verified: boolean;
nsfw_allowed: boolean;
premium: boolean;
purchased_flags: number;
premium_usage_flags: number;
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[];
};
type memberjson = {
index?: number;
id: string;
user: userjson | null;
guild_id: string;
guild: {
id: string;
} | null;
nick?: string;
roles: string[];
joined_at: string;
premium_since: string;
deaf: boolean;
mute: boolean;
pending: boolean;
last_message_id?: boolean; //What???
};
type emojijson = {
name: string;
id?: string;
animated?: boolean;
};
type guildjson = {
application_command_counts: { [key: string]: number };
channels: channeljson[];
data_mode: string;
emojis: emojijson[];
guild_scheduled_events: [];
id: string;
large: boolean;
lazy: boolean;
member_count: number;
premium_subscription_count: number;
properties: {
region: string | null;
name: string;
description: string;
icon: string;
splash: string;
banner: string;
features: string[];
preferred_locale: string;
owner_id: string;
application_id: string;
afk_channel_id: string;
afk_timeout: number;
member_count: number;
system_channel_id: string;
verification_level: number;
explicit_content_filter: number;
default_message_notifications: number;
mfa_level: number;
vanity_url_code: number;
premium_tier: number;
premium_progress_bar_enabled: boolean;
system_channel_flags: number;
discovery_splash: string;
rules_channel_id: string;
public_updates_channel_id: string;
max_video_channel_users: number;
max_members: number;
nsfw_level: number;
hub_type: null;
home_header: null;
id: string;
latest_onboarding_question_id: string;
max_stage_video_channel_users: number;
nsfw: boolean;
safety_alerts_channel_id: string;
};
roles: rolesjson[];
stage_instances: [];
stickers: [];
threads: [];
version: string;
guild_hashes: {};
joined_at: string;
};
type startTypingjson = {
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;
};
type rolesjson = {
id: string;
guild_id: string;
color: number;
hoist: boolean;
managed: boolean;
mentionable: boolean;
name: string;
permissions: string;
position: number;
icon: string;
unicode_emoji: string;
flags: number;
};
type dirrectjson = {
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;
};
type filejson = {
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 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;
};
type presencejson = {
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";
};
type wsjson =
| {
op: 0;
d: any;
s: number;
t:
| "TYPING_START"
| "USER_UPDATE"
| "CHANNEL_UPDATE"
| "CHANNEL_CREATE"
| "CHANNEL_DELETE"
| "GUILD_DELETE"
| "GUILD_CREATE"
| "MESSAGE_REACTION_REMOVE_ALL"
| "MESSAGE_REACTION_REMOVE_EMOJI";
}
| {
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: {
guild_id?: string;
channel_id: string;
} & messagejson;
s: number;
t: "MESSAGE_UPDATE";
}
| messageCreateJson
| readyjson
| {
op: 11;
s: undefined;
d: {};
}
| {
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_REMOVE";
d: {
user_id: string;
channel_id: string;
message_id: string;
guild_id: string;
emoji: emojijson;
};
s: 3;
};
type memberChunk = {
guild_id: string;
nonce: string;
members: memberjson[];
presences: presencejson[];
chunk_index: number;
chunk_count: number;
not_found: string[];
};
export {
readyjson,
dirrectjson,
startTypingjson,
channeljson,
guildjson,
rolesjson,
userjson,
memberjson,
mainuserjson,
messagejson,
filejson,
embedjson,
emojijson,
presencejson,
wsjson,
messageCreateJson,
memberChunk,
invitejson,
};

1824
src/webpage/localuser.ts Normal file

File diff suppressed because it is too large Load diff

62
src/webpage/login.html Normal file
View file

@ -0,0 +1,62 @@
<body class="Dark-theme">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Jank Client</title>
<meta content="Jank Client" property="og:title" />
<meta
content="A spacebar client that has DMs, replying and more"
property="og:description"
/>
<meta content="/logo.webp" property="og:image" />
<meta content="#4b458c" data-react-helmet="true" name="theme-color" />
<link href="/style.css" rel="stylesheet" />
<link href="/themes.css" rel="stylesheet" id="lightcss" />
</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 />
<label for="uname"><b>Email:</b></label
><br />
<input
type="text"
placeholder="Enter email address"
name="uname"
id="uname"
required
/><br /><br />
<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>
<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>

625
src/webpage/login.ts Normal file
View file

@ -0,0 +1,625 @@
import { Dialog } from "./dialog.js";
const mobile = /iPhone|iPad|iPod|Android/i.test(navigator.userAgent);
function setTheme() {
let name = localStorage.getItem("theme");
if (!name) {
localStorage.setItem("theme", "Dark");
name = "Dark";
}
document.body.className = name + "-theme";
}
let instances:
| {
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;
};
}[]
| null;
setTheme();
function getBulkUsers() {
const json = getBulkInfo();
for (const thing in json.users) {
json.users[thing] = new Specialuser(json.users[thing]);
}
return json;
}
function trimswitcher() {
const json = getBulkInfo();
const map = new Map();
for (const thing in json.users) {
const user = json.users[thing];
let wellknown = user.serverurls.wellknown;
if (wellknown.at(-1) !== "/") {
wellknown += "/";
}
wellknown += user.username;
if (map.has(wellknown)) {
const otheruser = map.get(wellknown);
if (otheruser[1].serverurls.wellknown.at(-1) === "/") {
delete json.users[otheruser[0]];
map.set(wellknown, [thing, user]);
} else {
delete json.users[thing];
}
} else {
map.set(wellknown, [thing, user]);
}
}
for (const thing in json.users) {
if (thing.at(-1) === "/") {
const user = json.users[thing];
delete json.users[thing];
json.users[thing.slice(0, -1)] = user;
}
}
localStorage.setItem("userinfos", JSON.stringify(json));
console.log(json);
}
function getBulkInfo() {
return JSON.parse(localStorage.getItem("userinfos")!);
}
function setDefaults() {
let userinfos = getBulkInfo();
if (!userinfos) {
localStorage.setItem(
"userinfos",
JSON.stringify({
currentuser: null,
users: {},
preferences: {
theme: "Dark",
notifications: false,
notisound: "three",
},
})
);
userinfos = getBulkInfo();
}
if (userinfos.users === undefined) {
userinfos.users = {};
}
if (userinfos.accent_color === undefined) {
userinfos.accent_color = "#242443";
}
document.documentElement.style.setProperty(
"--accent-color",
userinfos.accent_color
);
if (userinfos.preferences === undefined) {
userinfos.preferences = {
theme: "Dark",
notifications: false,
notisound: "three",
};
}
if (userinfos.preferences && userinfos.preferences.notisound === undefined) {
userinfos.preferences.notisound = "three";
}
localStorage.setItem("userinfos", JSON.stringify(userinfos));
}
setDefaults();
class Specialuser {
serverurls: {
api: string;
cdn: string;
gateway: string;
wellknown: string;
login: string;
};
email: string;
token: string;
loggedin;
json;
constructor(json: any) {
if (json instanceof Specialuser) {
console.error("specialuser can't construct from another specialuser");
}
this.serverurls = json.serverurls;
let apistring = new URL(json.serverurls.api).toString();
apistring = apistring.replace(/\/(v\d+\/?)?$/, "") + "/v9";
this.serverurls.api = apistring;
this.serverurls.cdn = new URL(json.serverurls.cdn)
.toString()
.replace(/\/$/, "");
this.serverurls.gateway = new URL(json.serverurls.gateway)
.toString()
.replace(/\/$/, "");
this.serverurls.wellknown = new URL(json.serverurls.wellknown)
.toString()
.replace(/\/$/, "");
this.serverurls.login = new URL(json.serverurls.login)
.toString()
.replace(/\/$/, "");
this.email = json.email;
this.token = json.token;
this.loggedin = json.loggedin;
this.json = json;
this.json.localuserStore ??= {};
if (!this.serverurls || !this.email || !this.token) {
console.error(
"There are fundamentally missing pieces of info missing from this user"
);
}
}
set pfpsrc(e) {
this.json.pfpsrc = e;
this.updateLocal();
}
get pfpsrc() {
return this.json.pfpsrc;
}
set username(e) {
this.json.username = e;
this.updateLocal();
}
get username() {
return this.json.username;
}
set localuserStore(e) {
this.json.localuserStore = e;
this.updateLocal();
}
get localuserStore() {
return this.json.localuserStore;
}
get uid() {
return this.email + this.serverurls.wellknown;
}
toJSON() {
return this.json;
}
updateLocal() {
const info = getBulkInfo();
info.users[this.uid] = this.toJSON();
localStorage.setItem("userinfos", JSON.stringify(info));
}
}
function adduser(user: typeof Specialuser.prototype.json) {
user = new Specialuser(user);
const info = getBulkInfo();
info.users[user.uid] = user;
info.currentuser = user.uid;
localStorage.setItem("userinfos", JSON.stringify(info));
return user;
}
const instancein = document.getElementById("instancein") as HTMLInputElement;
let timeout: string | number | NodeJS.Timeout | undefined;
// let instanceinfo;
const stringURLMap = new Map<string, string>();
const stringURLsMap = new Map<
string,
{
wellknown: string;
api: string;
cdn: string;
gateway: string;
login?: string;
}
>();
async function getapiurls(str: string): Promise<
| {
api: string;
cdn: string;
gateway: string;
wellknown: string;
login: string;
}
| false
> {
if (!URL.canParse(str)) {
const val = stringURLMap.get(str);
if (val) {
str = val;
} else {
const val = stringURLsMap.get(str);
if (val) {
const responce = await fetch(
val.api + val.api.endsWith("/") ? "" : "/" + "ping"
);
if (responce.ok) {
if (val.login) {
return val as {
wellknown: string;
api: string;
cdn: string;
gateway: string;
login: string;
};
} else {
val.login = val.api;
return val as {
wellknown: string;
api: string;
cdn: string;
gateway: string;
login: string;
};
}
}
}
}
}
if (str.at(-1) !== "/") {
str += "/";
}
let api: string;
try {
const info = await fetch(`${str}/.well-known/spacebar`).then((x) =>
x.json()
);
api = info.api;
} catch {
return false;
}
const url = new URL(api);
try {
const info = await fetch(
`${api}${
url.pathname.includes("api") ? "" : "api"
}/policies/instance/domains`
).then((x) => x.json());
return {
api: info.apiEndpoint,
gateway: info.gateway,
cdn: info.cdn,
wellknown: str,
login: url.toString(),
};
} catch {
const val = stringURLsMap.get(str);
if (val) {
const responce = await fetch(
val.api + val.api.endsWith("/") ? "" : "/" + "ping"
);
if (responce.ok) {
if (val.login) {
return val as {
wellknown: string;
api: string;
cdn: string;
gateway: string;
login: string;
};
} else {
val.login = val.api;
return val as {
wellknown: string;
api: string;
cdn: string;
gateway: string;
login: string;
};
}
}
}
return false;
}
}
async function checkInstance(instance?: string) {
const verify = document.getElementById("verify");
try {
verify!.textContent = "Checking Instance";
const instanceValue = instance || (instancein as HTMLInputElement).value;
const instanceinfo = (await getapiurls(instanceValue)) as {
wellknown: string;
api: string;
cdn: string;
gateway: string;
login: string;
value: string;
};
if (instanceinfo) {
instanceinfo.value = instanceValue;
localStorage.setItem("instanceinfo", JSON.stringify(instanceinfo));
verify!.textContent = "Instance is all good";
// @ts-ignore
if (checkInstance.alt) {
// @ts-ignore
checkInstance.alt();
}
setTimeout((_: any) => {
console.log(verify!.textContent);
verify!.textContent = "";
}, 3000);
} else {
verify!.textContent = "Invalid Instance, try again";
}
} catch {
console.log("catch");
verify!.textContent = "Invalid Instance, try again";
}
}
if (instancein) {
console.log(instancein);
instancein.addEventListener("keydown", (_) => {
const verify = document.getElementById("verify");
verify!.textContent = "Waiting to check Instance";
clearTimeout(timeout);
timeout = setTimeout(() => checkInstance(), 1000);
});
if (localStorage.getItem("instanceinfo")) {
const json = JSON.parse(localStorage.getItem("instanceinfo")!);
if (json.value) {
(instancein as HTMLInputElement).value = json.value;
} else {
(instancein as HTMLInputElement).value = json.wellknown;
}
} else {
checkInstance("https://spacebar.chat/");
}
}
async function login(username: string, password: string, captcha: string) {
if (captcha === "") {
captcha = "";
}
const options = {
method: "POST",
body: JSON.stringify({
login: username,
password,
undelete: false,
captcha_key: captcha,
}),
headers: {
"Content-type": "application/json; charset=UTF-8",
},
};
try {
const info = JSON.parse(localStorage.getItem("instanceinfo")!);
const api = info.login + (info.login.startsWith("/") ? "/" : "");
return await fetch(api + "/auth/login", options)
.then((response) => response.json())
.then((response) => {
console.log(response, response.message);
if (response.message === "Invalid Form Body") {
return response.errors.login._errors[0].message;
console.log("test");
}
//this.serverurls||!this.email||!this.token
console.log(response);
if (response.captcha_sitekey) {
const capt = document.getElementById("h-captcha");
if (!capt!.children.length) {
const capty = document.createElement("div");
capty.classList.add("h-captcha");
capty.setAttribute("data-sitekey", response.captcha_sitekey);
const script = document.createElement("script");
script.src = "https://js.hcaptcha.com/1/api.js";
capt!.append(script);
capt!.append(capty);
} else {
eval("hcaptcha.reset()");
}
} else {
console.log(response);
if (response.ticket) {
let onetimecode = "";
new Dialog([
"vdiv",
["title", "2FA code:"],
[
"textbox",
"",
"",
function (this: HTMLInputElement) {
onetimecode = this.value;
},
],
[
"button",
"",
"Submit",
function () {
fetch(api + "/auth/mfa/totp", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
code: onetimecode,
ticket: response.ticket,
}),
})
.then((r) => r.json())
.then((response) => {
if (response.message) {
alert(response.message);
} else {
console.warn(response);
if (!response.token) return;
adduser({
serverurls: JSON.parse(
localStorage.getItem("instanceinfo")!
),
email: username,
token: response.token,
}).username = username;
const redir = new URLSearchParams(
window.location.search
).get("goback");
if (redir) {
window.location.href = redir;
} else {
window.location.href = "/channels/@me";
}
}
});
},
],
]).show();
} else {
console.warn(response);
if (!response.token) return;
adduser({
serverurls: JSON.parse(localStorage.getItem("instanceinfo")!),
email: username,
token: response.token,
}).username = username;
const redir = new URLSearchParams(window.location.search).get(
"goback"
);
if (redir) {
window.location.href = redir;
} else {
window.location.href = "/channels/@me";
}
return "";
}
}
});
} catch (error) {
console.error("Error:", error);
}
}
async function check(e: SubmitEvent) {
e.preventDefault();
const target = e.target as HTMLFormElement;
const h = await login(
(target[1] as HTMLInputElement).value,
(target[2] as HTMLInputElement).value,
(target[3] as HTMLInputElement).value
);
const wrongElement = document.getElementById("wrong");
if (wrongElement) {
wrongElement.textContent = h;
}
console.log(h);
}
if (document.getElementById("form")) {
const form = document.getElementById("form");
if (form) {
form.addEventListener("submit", (e: SubmitEvent) => check(e));
}
}
//this currently does not work, and need to be implemented better at some time.
/*
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);
});
}
})
}
*/
const switchurl = document.getElementById("switch") as HTMLAreaElement;
if (switchurl) {
switchurl.href += window.location.search;
const instance = new URLSearchParams(window.location.search).get("instance");
console.log(instance);
if (instance) {
instancein.value = instance;
checkInstance("");
}
}
export { checkInstance };
trimswitcher();
export {
mobile,
getBulkUsers,
getBulkInfo,
setTheme,
Specialuser,
getapiurls,
adduser,
};
const datalist = document.getElementById("instances");
console.warn(datalist);
export function getInstances() {
return instances;
}
fetch("/instances.json")
.then((_) => _.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;
};
}[]
) => {
instances = json;
if (datalist) {
console.warn(json);
if (instancein && instancein.value === "") {
instancein.value = json[0].name;
}
for (const instance of json) {
if (instance.display === false) {
continue;
}
const option = document.createElement("option");
option.disabled = !instance.online;
option.value = instance.name;
if (instance.url) {
stringURLMap.set(option.value, instance.url);
if (instance.urls) {
stringURLsMap.set(instance.url, instance.urls);
}
} else if (instance.urls) {
stringURLsMap.set(option.value, instance.urls);
} else {
option.disabled = true;
}
if (instance.description) {
option.label = instance.description;
} else {
option.label = instance.name;
}
datalist.append(option);
}
checkInstance("");
}
}
);

View file

Before

Width:  |  Height:  |  Size: 632 B

After

Width:  |  Height:  |  Size: 632 B

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

Before After
Before After

12
src/webpage/manifest.json Normal file
View file

@ -0,0 +1,12 @@
{
"name": "Jank Client",
"icons": [
{
"src": "/logo.svg",
"sizes": "512x512"
}
],
"start_url": "/channels/@me",
"display": "standalone",
"theme_color": "#05050a"
}

870
src/webpage/markdown.ts Normal file
View file

@ -0,0 +1,870 @@
import { Channel } from "./channel.js";
import { Dialog } from "./dialog.js";
import { Emoji } from "./emoji.js";
import { Guild } from "./guild.js";
import { Localuser } from "./localuser.js";
import { Member } from "./member.js";
class MarkDown {
txt: string[];
keep: boolean;
stdsize: boolean;
owner: Localuser | Channel;
info: Localuser["info"];
constructor(
text: string | string[],
owner: MarkDown["owner"],
{ keep = false, stdsize = false } = {}
) {
if (typeof text === typeof "") {
this.txt = (text as string).split("");
} else {
this.txt = text as string[];
}
if (this.txt === undefined) {
this.txt = [];
}
this.info = owner.info;
this.keep = keep;
this.owner = owner;
this.stdsize = stdsize;
}
get localuser() {
if (this.owner instanceof Localuser) {
return this.owner;
} else {
return this.owner.localuser;
}
}
get rawString() {
return this.txt.join("");
}
get textContent() {
return this.makeHTML().textContent;
}
makeHTML({ keep = this.keep, stdsize = this.stdsize } = {}) {
return this.markdown(this.txt, { keep, stdsize });
}
markdown(text: string | string[], { keep = false, stdsize = false } = {}) {
let txt: string[];
if (typeof text === typeof "") {
txt = (text as string).split("");
} else {
txt = text as string[];
}
if (txt === undefined) {
txt = [];
}
const span = document.createElement("span");
let current = document.createElement("span");
function appendcurrent() {
if (current.innerHTML !== "") {
span.append(current);
current = document.createElement("span");
}
}
for (let i = 0; i < txt.length; i++) {
if (txt[i] === "\n" || i === 0) {
const first = i === 0;
if (first) {
i--;
}
let element: HTMLElement = document.createElement("span");
let keepys = "";
if (txt[i + 1] === "#") {
if (txt[i + 2] === "#") {
if (txt[i + 3] === "#" && txt[i + 4] === " ") {
element = document.createElement("h3");
keepys = "### ";
i += 5;
} else if (txt[i + 3] === " ") {
element = document.createElement("h2");
element.classList.add("h2md");
keepys = "## ";
i += 4;
}
} else if (txt[i + 2] === " ") {
element = document.createElement("h1");
keepys = "# ";
i += 3;
}
} else if (txt[i + 1] === ">" && txt[i + 2] === " ") {
element = document.createElement("div");
const line = document.createElement("div");
line.classList.add("quoteline");
element.append(line);
element.classList.add("quote");
keepys = "> ";
i += 3;
}
if (keepys) {
appendcurrent();
if (!first && !stdsize) {
span.appendChild(document.createElement("br"));
}
const build: string[] = [];
for (; txt[i] !== "\n" && txt[i] !== undefined; i++) {
build.push(txt[i]);
}
try {
if (stdsize) {
element = document.createElement("span");
}
if (keep) {
element.append(keepys);
//span.appendChild(document.createElement("br"));
}
element.appendChild(this.markdown(build, { keep, stdsize }));
span.append(element);
} finally {
i -= 1;
continue;
}
}
if (first) {
i++;
}
}
if (txt[i] === "\n") {
if (!stdsize) {
appendcurrent();
span.append(document.createElement("br"));
}
continue;
}
if (txt[i] === "`") {
let count = 1;
if (txt[i + 1] === "`") {
count++;
if (txt[i + 2] === "`") {
count++;
}
}
let build = "";
if (keep) {
build += "`".repeat(count);
}
let find = 0;
let j = i + count;
let init = true;
for (
;
txt[j] !== undefined &&
(txt[j] !== "\n" || count === 3) &&
find !== count;
j++
) {
if (txt[j] === "`") {
find++;
} else {
if (find !== 0) {
build += "`".repeat(find);
find = 0;
}
if (init && count === 3) {
if (txt[j] === " " || txt[j] === "\n") {
init = false;
}
if (keep) {
build += txt[j];
}
continue;
}
build += txt[j];
}
}
if (stdsize) {
build = build.replaceAll("\n", "");
}
if (find === count) {
appendcurrent();
i = j;
if (keep) {
build += "`".repeat(find);
}
if (count !== 3 && !stdsize) {
const samp = document.createElement("samp");
samp.textContent = build;
span.appendChild(samp);
} else {
const pre = document.createElement("pre");
if (build.at(-1) === "\n") {
build = build.substring(0, build.length - 1);
}
if (txt[i] === "\n") {
i++;
}
pre.textContent = build;
span.appendChild(pre);
}
i--;
continue;
}
}
if (txt[i] === "*") {
let count = 1;
if (txt[i + 1] === "*") {
count++;
if (txt[i + 2] === "*") {
count++;
}
}
let build: string[] = [];
let find = 0;
let j = i + count;
for (; txt[j] !== undefined && find !== count; j++) {
if (txt[j] === "*") {
find++;
} else {
build.push(txt[j]);
if (find !== 0) {
build = build.concat(new Array(find).fill("*"));
find = 0;
}
}
}
if (find === count && (count != 1 || txt[i + 1] !== " ")) {
appendcurrent();
i = j;
const stars = "*".repeat(count);
if (count === 1) {
const i = document.createElement("i");
if (keep) {
i.append(stars);
}
i.appendChild(this.markdown(build, { keep, stdsize }));
if (keep) {
i.append(stars);
}
span.appendChild(i);
} else if (count === 2) {
const b = document.createElement("b");
if (keep) {
b.append(stars);
}
b.appendChild(this.markdown(build, { keep, stdsize }));
if (keep) {
b.append(stars);
}
span.appendChild(b);
} else {
const b = document.createElement("b");
const i = document.createElement("i");
if (keep) {
b.append(stars);
}
b.appendChild(this.markdown(build, { keep, stdsize }));
if (keep) {
b.append(stars);
}
i.appendChild(b);
span.appendChild(i);
}
i--;
continue;
}
}
if (txt[i] === "_") {
let count = 1;
if (txt[i + 1] === "_") {
count++;
if (txt[i + 2] === "_") {
count++;
}
}
let build: string[] = [];
let find = 0;
let j = i + count;
for (; txt[j] !== undefined && find !== count; j++) {
if (txt[j] === "_") {
find++;
} else {
build.push(txt[j]);
if (find !== 0) {
build = build.concat(new Array(find).fill("_"));
find = 0;
}
}
}
if (
find === count &&
(count != 1 ||
txt[j + 1] === " " ||
txt[j + 1] === "\n" ||
txt[j + 1] === undefined)
) {
appendcurrent();
i = j;
const underscores = "_".repeat(count);
if (count === 1) {
const i = document.createElement("i");
if (keep) {
i.append(underscores);
}
i.appendChild(this.markdown(build, { keep, stdsize }));
if (keep) {
i.append(underscores);
}
span.appendChild(i);
} else if (count === 2) {
const u = document.createElement("u");
if (keep) {
u.append(underscores);
}
u.appendChild(this.markdown(build, { keep, stdsize }));
if (keep) {
u.append(underscores);
}
span.appendChild(u);
} else {
const u = document.createElement("u");
const i = document.createElement("i");
if (keep) {
i.append(underscores);
}
i.appendChild(this.markdown(build, { keep, stdsize }));
if (keep) {
i.append(underscores);
}
u.appendChild(i);
span.appendChild(u);
}
i--;
continue;
}
}
if (txt[i] === "~" && txt[i + 1] === "~") {
const count = 2;
let build: string[] = [];
let find = 0;
let j = i + 2;
for (; txt[j] !== undefined && find !== count; j++) {
if (txt[j] === "~") {
find++;
} else {
build.push(txt[j]);
if (find !== 0) {
build = build.concat(new Array(find).fill("~"));
find = 0;
}
}
}
if (find === count) {
appendcurrent();
i = j - 1;
const tildes = "~~";
if (count === 2) {
const s = document.createElement("s");
if (keep) {
s.append(tildes);
}
s.appendChild(this.markdown(build, { keep, stdsize }));
if (keep) {
s.append(tildes);
}
span.appendChild(s);
}
continue;
}
}
if (txt[i] === "|" && txt[i + 1] === "|") {
const count = 2;
let build: string[] = [];
let find = 0;
let j = i + 2;
for (; txt[j] !== undefined && find !== count; j++) {
if (txt[j] === "|") {
find++;
} else {
build.push(txt[j]);
if (find !== 0) {
build = build.concat(new Array(find).fill("~"));
find = 0;
}
}
}
if (find === count) {
appendcurrent();
i = j - 1;
const pipes = "||";
if (count === 2) {
const j = document.createElement("j");
if (keep) {
j.append(pipes);
}
j.appendChild(this.markdown(build, { keep, stdsize }));
j.classList.add("spoiler");
j.onclick = MarkDown.unspoil;
if (keep) {
j.append(pipes);
}
span.appendChild(j);
}
continue;
}
}
if (
!keep &&
txt[i] === "h" &&
txt[i + 1] === "t" &&
txt[i + 2] === "t" &&
txt[i + 3] === "p"
) {
let build = "http";
let j = i + 4;
const endchars = new Set(["\\", "<", ">", "|", "]", " "]);
for (; txt[j] !== undefined; j++) {
const char = txt[j];
if (endchars.has(char)) {
break;
}
build += char;
}
if (URL.canParse(build)) {
appendcurrent();
const a = document.createElement("a");
//a.href=build;
MarkDown.safeLink(a, build);
a.textContent = build;
a.target = "_blank";
i = j - 1;
span.appendChild(a);
continue;
}
}
if (txt[i] === "<" && (txt[i + 1] === "@" || txt[i + 1] === "#")) {
let id = "";
let j = i + 2;
const numbers = new Set([
"0",
"1",
"2",
"3",
"4",
"5",
"6",
"7",
"8",
"9",
]);
for (; txt[j] !== undefined; j++) {
const char = txt[j];
if (!numbers.has(char)) {
break;
}
id += char;
}
if (txt[j] === ">") {
appendcurrent();
const mention = document.createElement("span");
mention.classList.add("mentionMD");
mention.contentEditable = "false";
const char = txt[i + 1];
i = j;
switch (char) {
case "@":
const user = this.localuser.userMap.get(id);
if (user) {
mention.textContent = `@${user.name}`;
let guild: null | Guild = null;
if (this.owner instanceof Channel) {
guild = this.owner.guild;
}
if (!keep) {
user.bind(mention, guild);
}
if (guild) {
Member.resolveMember(user, guild).then((member) => {
if (member) {
mention.textContent = `@${member.name}`;
}
});
}
} else {
mention.textContent = `@unknown`;
}
break;
case "#":
const channel = this.localuser.channelids.get(id);
if (channel) {
mention.textContent = `#${channel.name}`;
if (!keep) {
mention.onclick = (_) => {
this.localuser.goToChannel(id);
};
}
} else {
mention.textContent = `#unknown`;
}
break;
}
span.appendChild(mention);
mention.setAttribute("real", `<${char}${id}>`);
continue;
}
}
if (txt[i] === "<" && txt[i + 1] === "t" && txt[i + 2] === ":") {
let found = false;
const build = ["<", "t", ":"];
let j = i + 3;
for (; txt[j] !== void 0; j++) {
build.push(txt[j]);
if (txt[j] === ">") {
found = true;
break;
}
}
if (found) {
appendcurrent();
i = j;
const parts = build
.join("")
.match(/^<t:([0-9]{1,16})(:([tTdDfFR]))?>$/) as RegExpMatchArray;
const dateInput = new Date(Number.parseInt(parts[1]) * 1000);
let time = "";
if (Number.isNaN(dateInput.getTime())) time = build.join("");
else {
if (parts[3] === "d")
time = dateInput.toLocaleString(void 0, {
day: "2-digit",
month: "2-digit",
year: "numeric",
});
else if (parts[3] === "D")
time = dateInput.toLocaleString(void 0, {
day: "numeric",
month: "long",
year: "numeric",
});
else if (!parts[3] || parts[3] === "f")
time =
dateInput.toLocaleString(void 0, {
day: "numeric",
month: "long",
year: "numeric",
}) +
" " +
dateInput.toLocaleString(void 0, {
hour: "2-digit",
minute: "2-digit",
});
else if (parts[3] === "F")
time =
dateInput.toLocaleString(void 0, {
day: "numeric",
month: "long",
year: "numeric",
weekday: "long",
}) +
" " +
dateInput.toLocaleString(void 0, {
hour: "2-digit",
minute: "2-digit",
});
else if (parts[3] === "t")
time = dateInput.toLocaleString(void 0, {
hour: "2-digit",
minute: "2-digit",
});
else if (parts[3] === "T")
time = dateInput.toLocaleString(void 0, {
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
});
else if (parts[3] === "R")
time =
Math.round(
(Date.now() - Number.parseInt(parts[1]) * 1000) / 1000 / 60
) + " minutes ago";
}
const timeElem = document.createElement("span");
timeElem.classList.add("markdown-timestamp");
timeElem.textContent = time;
span.appendChild(timeElem);
continue;
}
}
if (
txt[i] === "<" &&
(txt[i + 1] === ":" || (txt[i + 1] === "a" && txt[i + 2] === ":"))
) {
let found = false;
const build = txt[i + 1] === "a" ? ["<", "a", ":"] : ["<", ":"];
let j = i + build.length;
for (; txt[j] !== void 0; j++) {
build.push(txt[j]);
if (txt[j] === ">") {
found = true;
break;
}
}
if (found) {
const buildjoin = build.join("");
const parts = buildjoin.match(/^<(a)?:\w+:(\d{10,30})>$/);
if (parts && parts[2]) {
appendcurrent();
i = j;
const isEmojiOnly = txt.join("").trim() === buildjoin.trim();
const owner =
this.owner instanceof Channel ? this.owner.guild : this.owner;
const emoji = new Emoji(
{ name: buildjoin, id: parts[2], animated: Boolean(parts[1]) },
owner
);
span.appendChild(emoji.getHTML(isEmojiOnly));
continue;
}
}
}
if (txt[i] == "[" && !keep) {
let partsFound = 0;
let j = i + 1;
const build = ["["];
for (; txt[j] !== void 0; j++) {
build.push(txt[j]);
if (partsFound === 0 && txt[j] === "]") {
if (
txt[j + 1] === "(" &&
txt[j + 2] === "h" &&
txt[j + 3] === "t" &&
txt[j + 4] === "t" &&
txt[j + 5] === "p" &&
(txt[j + 6] === "s" || txt[j + 6] === ":")
) {
partsFound++;
} else {
break;
}
} else if (partsFound === 1 && txt[j] === ")") {
partsFound++;
break;
}
}
if (partsFound === 2) {
appendcurrent();
const parts = build
.join("")
.match(/^\[(.+)\]\((https?:.+?)( ('|").+('|"))?\)$/);
if (parts) {
const linkElem = document.createElement("a");
if (URL.canParse(parts[2])) {
i = j;
MarkDown.safeLink(linkElem, parts[2]);
linkElem.textContent = parts[1];
linkElem.target = "_blank";
linkElem.rel = "noopener noreferrer";
linkElem.title =
(parts[3]
? parts[3].substring(2, parts[3].length - 1) + "\n\n"
: "") + parts[2];
span.appendChild(linkElem);
continue;
}
}
}
}
current.textContent += txt[i];
}
appendcurrent();
return span;
}
static unspoil(e: any): void {
e.target.classList.remove("spoiler");
e.target.classList.add("unspoiled");
}
giveBox(box: HTMLDivElement) {
box.onkeydown = (_) => {
//console.log(_);
};
let prevcontent = "";
box.onkeyup = (_) => {
const content = MarkDown.gatherBoxText(box);
if (content !== prevcontent) {
prevcontent = content;
this.txt = content.split("");
this.boxupdate(box);
}
};
box.onpaste = (_) => {
if (!_.clipboardData) return;
console.log(_.clipboardData.types);
const data = _.clipboardData.getData("text");
document.execCommand("insertHTML", false, data);
_.preventDefault();
if (!box.onkeyup) return;
box.onkeyup(new KeyboardEvent("_"));
};
}
boxupdate(box: HTMLElement) {
const restore = saveCaretPosition(box);
box.innerHTML = "";
box.append(this.makeHTML({ keep: true }));
if (restore) {
restore();
}
}
static gatherBoxText(element: HTMLElement): string {
if (element.tagName.toLowerCase() === "img") {
return (element as HTMLImageElement).alt;
}
if (element.tagName.toLowerCase() === "br") {
return "\n";
}
if (element.hasAttribute("real")) {
return element.getAttribute("real") as string;
}
let build = "";
for (const thing of Array.from(element.childNodes)) {
if (thing instanceof Text) {
const text = thing.textContent;
build += text;
continue;
}
const text = this.gatherBoxText(thing as HTMLElement);
if (text) {
build += text;
}
}
return build;
}
static readonly trustedDomains = new Set([location.host]);
static safeLink(elm: HTMLElement, url: string) {
if (URL.canParse(url)) {
const Url = new URL(url);
if (
elm instanceof HTMLAnchorElement &&
this.trustedDomains.has(Url.host)
) {
elm.href = url;
elm.target = "_blank";
return;
}
elm.onmouseup = (_) => {
if (_.button === 2) return;
console.log(":3");
function open() {
const proxy = window.open(url, "_blank");
if (proxy && _.button === 1) {
proxy.focus();
} else if (proxy) {
window.focus();
}
}
if (this.trustedDomains.has(Url.host)) {
open();
} else {
const full: Dialog = new Dialog([
"vdiv",
["title", "You're leaving spacebar"],
[
"text",
"You're going to " +
Url.host +
" are you sure you want to go there?",
],
[
"hdiv",
["button", "", "Nevermind", (_: any) => full.hide()],
[
"button",
"",
"Go there",
(_: any) => {
open();
full.hide();
},
],
[
"button",
"",
"Go there and trust in the future",
(_: any) => {
open();
full.hide();
this.trustedDomains.add(Url.host);
},
],
],
]);
full.show();
}
};
} else {
throw Error(url + " is not a valid URL");
}
}
/*
static replace(base: HTMLElement, newelm: HTMLElement) {
const basechildren = base.children;
const newchildren = newelm.children;
for (const thing of Array.from(newchildren)) {
base.append(thing);
}
}
*/
}
//solution from https://stackoverflow.com/questions/4576694/saving-and-restoring-caret-position-for-contenteditable-div
let text = "";
function saveCaretPosition(context: Node) {
const selection = window.getSelection();
if (!selection) return;
const range = selection.getRangeAt(0);
range.setStart(context, 0);
text = selection.toString();
let len = text.length + 1;
for (const str in text.split("\n")) {
if (str.length !== 0) {
len--;
}
}
len += +(text[text.length - 1] === "\n");
return function restore() {
if (!selection) return;
const pos = getTextNodeAtPosition(context, len);
selection.removeAllRanges();
const range = new Range();
range.setStart(pos.node, pos.position);
selection.addRange(range);
};
}
function getTextNodeAtPosition(root: Node, index: number) {
const NODE_TYPE = NodeFilter.SHOW_TEXT;
const treeWalker = document.createTreeWalker(root, NODE_TYPE, (elem) => {
if (!elem.textContent) return 0;
if (index > elem.textContent.length) {
index -= elem.textContent.length;
return NodeFilter.FILTER_REJECT;
}
return NodeFilter.FILTER_ACCEPT;
});
const c = treeWalker.nextNode();
return {
node: c ? c : root,
position: index,
};
}
export { MarkDown };

256
src/webpage/member.ts Normal file
View file

@ -0,0 +1,256 @@
import { User } from "./user.js";
import { Role } from "./role.js";
import { Guild } from "./guild.js";
import { SnowFlake } from "./snowflake.js";
import { memberjson, presencejson } from "./jsontypes.js";
import { Dialog } from "./dialog.js";
class Member extends SnowFlake {
static already = {};
owner: Guild;
user: User;
roles: Role[] = [];
nick!: string;
[key: string]: any;
private constructor(memberjson: memberjson, owner: Guild) {
super(memberjson.id);
this.owner = owner;
if (this.localuser.userMap.has(memberjson.id)) {
this.user = this.localuser.userMap.get(memberjson.id) as User;
} else if (memberjson.user) {
this.user = new User(memberjson.user, owner.localuser);
} else {
throw new Error("Missing user object of this member");
}
for (const key of Object.keys(memberjson)) {
if (key === "guild" || key === "owner") {
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;
}
(this as any)[key] = (memberjson as any)[key];
}
if (this.localuser.userMap.has(this?.id)) {
this.user = this.localuser.userMap.get(this?.id) as User;
}
this.roles.sort((a, b) => {
return this.guild.roles.indexOf(a) - this.guild.roles.indexOf(b);
});
}
get guild() {
return this.owner;
}
get localuser() {
return this.guild.localuser;
}
get info() {
return this.owner.info;
}
static async new(
memberjson: memberjson,
owner: Guild
): Promise<Member | undefined> {
let user: User;
if (owner.localuser.userMap.has(memberjson.id)) {
user = owner.localuser.userMap.get(memberjson.id) as User;
} else if (memberjson.user) {
user = new User(memberjson.user, owner.localuser);
} else {
throw new Error("missing user object of this member");
}
if (user.members.has(owner)) {
let memb = user.members.get(owner);
if (memb === undefined) {
memb = new Member(memberjson, owner);
user.members.set(owner, memb);
return memb;
} else if (memb instanceof Promise) {
return await memb; //I should do something else, though for now this is "good enough"
} else {
return memb;
}
} else {
const memb = new Member(memberjson, owner);
user.members.set(owner, memb);
return memb;
}
}
static async resolveMember(
user: User,
guild: Guild
): Promise<Member | undefined> {
const maybe = user.members.get(guild);
if (!user.members.has(guild)) {
const membpromise = guild.localuser.resolvemember(user.id, guild.id);
const promise = new Promise<Member | undefined>(async (res) => {
const membjson = await membpromise;
if (membjson === undefined) {
return res(undefined);
} else {
const member = new Member(membjson, guild);
const map = guild.localuser.presences;
member.getPresence(map.get(member.id));
map.delete(member.id);
res(member);
return member;
}
});
user.members.set(guild, promise);
}
if (maybe instanceof Promise) {
return await maybe;
} else {
return maybe;
}
}
public getPresence(presence: presencejson | undefined) {
this.user.getPresence(presence);
}
/**
* @todo
*/
highInfo() {
fetch(
this.info.api +
"/users/" +
this.id +
"/profile?with_mutual_guilds=true&with_mutual_friends_count=true&guild_id=" +
this.guild.id,
{ headers: this.guild.headers }
);
}
hasRole(ID: string) {
console.log(this.roles, ID);
for (const thing of this.roles) {
if (thing.id === ID) {
return true;
}
}
return false;
}
getColor() {
for (const thing of this.roles) {
const color = thing.getColor();
if (color) {
return color;
}
}
return "";
}
isAdmin() {
for (const role of this.roles) {
if (role.permissions.getPermission("ADMINISTRATOR")) {
return true;
}
}
return this.guild.properties.owner_id === this.user.id;
}
bind(html: HTMLElement) {
if (html.tagName === "SPAN") {
if (!this) {
return;
}
/*
if(this.error){
}
*/
html.style.color = this.getColor();
}
//this.profileclick(html);
}
profileclick(/* html: HTMLElement */) {
//to be implemented
}
get name() {
return this.nick || this.user.username;
}
kick() {
let reason = "";
const menu = new Dialog([
"vdiv",
["title", "Kick " + this.name + " from " + this.guild.properties.name],
[
"textbox",
"Reason:",
"",
function (e: Event) {
reason = (e.target as HTMLInputElement).value;
},
],
[
"button",
"",
"submit",
() => {
this.kickAPI(reason);
menu.hide();
},
],
]);
menu.show();
}
kickAPI(reason: string) {
const headers = structuredClone(this.guild.headers);
(headers as any)["x-audit-log-reason"] = reason;
fetch(`${this.info.api}/guilds/${this.guild.id}/members/${this.id}`, {
method: "DELETE",
headers,
});
}
ban() {
let reason = "";
const menu = new Dialog([
"vdiv",
["title", "Ban " + this.name + " from " + this.guild.properties.name],
[
"textbox",
"Reason:",
"",
function (e: Event) {
reason = (e.target as HTMLInputElement).value;
},
],
[
"button",
"",
"submit",
() => {
this.banAPI(reason);
menu.hide();
},
],
]);
menu.show();
}
banAPI(reason: string) {
const headers = structuredClone(this.guild.headers);
(headers as any)["x-audit-log-reason"] = reason;
fetch(`${this.info.api}/guilds/${this.guild.id}/bans/${this.id}`, {
method: "PUT",
headers,
});
}
hasPermission(name: string): boolean {
if (this.isAdmin()) {
return true;
}
for (const thing of this.roles) {
if (thing.permissions.getPermission(name)) {
return true;
}
}
return false;
}
}
export { Member };

769
src/webpage/message.ts Normal file
View file

@ -0,0 +1,769 @@
import { Contextmenu } from "./contextmenu.js";
import { User } from "./user.js";
import { Member } from "./member.js";
import { MarkDown } from "./markdown.js";
import { Embed } from "./embed.js";
import { Channel } from "./channel.js";
import { Localuser } from "./localuser.js";
import { Role } from "./role.js";
import { File } from "./file.js";
import { SnowFlake } from "./snowflake.js";
import { memberjson, messagejson } from "./jsontypes.js";
import { Emoji } from "./emoji.js";
import { Dialog } from "./dialog.js";
class Message extends SnowFlake {
static contextmenu = new Contextmenu<Message, undefined>("message menu");
owner: Channel;
headers: Localuser["headers"];
embeds!: Embed[];
author!: User;
mentions!: User[];
mention_roles!: Role[];
attachments!: File[]; //probably should be its own class tbh, should be Attachments[]
message_reference!: messagejson;
type!: number;
timestamp!: number;
content!: MarkDown;
static del: Promise<void>;
static resolve: Function;
/*
weakdiv:WeakRef<HTMLDivElement>;
set div(e:HTMLDivElement){
if(!e){
this.weakdiv=null;
return;
}
this.weakdiv=new WeakRef(e);
}
get div(){
return this.weakdiv?.deref();
}
//*/
div:
| (HTMLDivElement & { pfpparent?: Message | undefined; txt?: HTMLElement })
| undefined;
member: Member | undefined;
reactions!: messagejson["reactions"];
static setup() {
this.del = new Promise((_) => {
this.resolve = _;
});
Message.setupcmenu();
}
static setupcmenu() {
Message.contextmenu.addbutton("Copy raw text", function (this: Message) {
navigator.clipboard.writeText(this.content.rawString);
});
Message.contextmenu.addbutton("Reply", function (this: Message) {
this.channel.setReplying(this);
});
Message.contextmenu.addbutton("Copy message id", function (this: Message) {
navigator.clipboard.writeText(this.id);
});
Message.contextmenu.addsubmenu(
"Add reaction",
function (this: Message, _, e: MouseEvent) {
Emoji.emojiPicker(e.x, e.y, this.localuser).then((_) => {
this.reactionToggle(_);
});
}
);
Message.contextmenu.addbutton(
"Edit",
function (this: Message) {
this.setEdit();
},
null,
function () {
return this.author.id === this.localuser.user.id;
}
);
Message.contextmenu.addbutton(
"Delete message",
function (this: Message) {
this.delete();
},
null,
function () {
return this.canDelete();
}
);
}
setEdit() {
this.channel.editing = this;
const markdown = (
document.getElementById("typebox") as HTMLDivElement & {
markdown: MarkDown;
}
)["markdown"] as MarkDown;
markdown.txt = this.content.rawString.split("");
markdown.boxupdate(document.getElementById("typebox") as HTMLDivElement);
}
constructor(messagejson: messagejson, owner: Channel) {
super(messagejson.id);
this.owner = owner;
this.headers = this.owner.headers;
this.giveData(messagejson);
this.owner.messages.set(this.id, this);
}
reactionToggle(emoji: string | Emoji) {
let remove = false;
for (const thing of this.reactions) {
if (thing.emoji.name === emoji) {
remove = thing.me;
break;
}
}
let reactiontxt: string;
if (emoji instanceof Emoji) {
reactiontxt = `${emoji.name}:${emoji.id}`;
} else {
reactiontxt = encodeURIComponent(emoji);
}
fetch(
`${this.info.api}/channels/${this.channel.id}/messages/${this.id}/reactions/${reactiontxt}/@me`,
{
method: remove ? "DELETE" : "PUT",
headers: this.headers,
}
);
}
giveData(messagejson: messagejson) {
const func = this.channel.infinite.snapBottom();
for (const thing of Object.keys(messagejson)) {
if (thing === "attachments") {
this.attachments = [];
for (const thing of messagejson.attachments) {
this.attachments.push(new File(thing, this));
}
continue;
} else if (thing === "content") {
this.content = new MarkDown(messagejson[thing], this.channel);
continue;
} else if (thing === "id") {
continue;
} else if (thing === "member") {
Member.new(messagejson.member as memberjson, this.guild).then((_) => {
this.member = _ as Member;
});
continue;
} else if (thing === "embeds") {
this.embeds = [];
for (const thing in messagejson.embeds) {
this.embeds[thing] = new Embed(messagejson.embeds[thing], this);
}
continue;
}
(this as any)[thing] = (messagejson as any)[thing];
}
if (messagejson.reactions?.length) {
console.log(messagejson.reactions, ":3");
}
this.author = new User(messagejson.author, this.localuser);
for (const thing in messagejson.mentions) {
this.mentions[thing] = new User(
messagejson.mentions[thing],
this.localuser
);
}
if (!this.member && this.guild.id !== "@me") {
this.author.resolvemember(this.guild).then((_) => {
this.member = _;
});
}
if (this.mentions.length || this.mention_roles.length) {
//currently mention_roles isn't implemented on the spacebar servers
console.log(this.mentions, this.mention_roles);
}
if (this.mentionsuser(this.localuser.user)) {
console.log(this);
}
if (this.div) {
this.generateMessage();
}
func();
}
canDelete() {
return (
this.channel.hasPermission("MANAGE_MESSAGES") ||
this.author === this.localuser.user
);
}
get channel() {
return this.owner;
}
get guild() {
return this.owner.guild;
}
get localuser() {
return this.owner.localuser;
}
get info() {
return this.owner.info;
}
messageevents(obj: HTMLDivElement) {
// const func = Message.contextmenu.bindContextmenu(obj, this, undefined);
this.div = obj;
obj.classList.add("messagediv");
}
deleteDiv() {
if (!this.div) return;
try {
this.div.remove();
this.div = undefined;
} catch (e) {
console.error(e);
}
}
mentionsuser(userd: User | Member) {
if (userd instanceof User) {
return this.mentions.includes(userd);
} else if (userd instanceof Member) {
return this.mentions.includes(userd.user);
} else {
return;
}
}
getimages() {
const build: File[] = [];
for (const thing of this.attachments) {
if (thing.content_type.startsWith("image/")) {
build.push(thing);
}
}
return build;
}
async edit(content: string) {
return await fetch(
this.info.api + "/channels/" + this.channel.id + "/messages/" + this.id,
{
method: "PATCH",
headers: this.headers,
body: JSON.stringify({ content }),
}
);
}
delete() {
fetch(`${this.info.api}/channels/${this.channel.id}/messages/${this.id}`, {
headers: this.headers,
method: "DELETE",
});
}
deleteEvent() {
console.log("deleted");
if (this.div) {
this.div.remove();
this.div.innerHTML = "";
this.div = undefined;
}
const prev = this.channel.idToPrev.get(this.id);
const next = this.channel.idToNext.get(this.id);
this.channel.idToPrev.delete(this.id);
this.channel.idToNext.delete(this.id);
this.channel.messages.delete(this.id);
if (prev && next) {
this.channel.idToPrev.set(next, prev);
this.channel.idToNext.set(prev, next);
} else if (prev) {
this.channel.idToNext.delete(prev);
} else if (next) {
this.channel.idToPrev.delete(next);
}
if (prev) {
const prevmessage = this.channel.messages.get(prev);
if (prevmessage) {
prevmessage.generateMessage();
}
}
if (
this.channel.lastmessage === this ||
this.channel.lastmessageid === this.id
) {
if (prev) {
this.channel.lastmessage = this.channel.messages.get(prev);
this.channel.lastmessageid = prev;
} else {
this.channel.lastmessage = undefined;
this.channel.lastmessageid = undefined;
}
}
if (this.channel.lastreadmessageid === this.id) {
if (prev) {
this.channel.lastreadmessageid = prev;
} else {
this.channel.lastreadmessageid = undefined;
}
}
console.log("deleted done");
}
reactdiv!: WeakRef<HTMLDivElement>;
blockedPropigate() {
const previd = this.channel.idToPrev.get(this.id);
if (!previd) {
this.generateMessage();
return;
}
const premessage = this.channel.messages.get(previd);
if (premessage?.author === this.author) {
premessage.blockedPropigate();
} else {
this.generateMessage();
}
}
generateMessage(premessage?: Message | undefined, ignoredblock = false) {
if (!this.div) return;
if (!premessage) {
premessage = this.channel.messages.get(
this.channel.idToPrev.get(this.id) as string
);
}
const div = this.div;
for (const user of this.mentions) {
if (user === this.localuser.user) {
div.classList.add("mentioned");
}
}
if (this === this.channel.replyingto) {
div.classList.add("replying");
}
div.innerHTML = "";
const build = document.createElement("div");
build.classList.add("flexltr", "message");
div.classList.remove("zeroheight");
if (this.author.relationshipType === 2) {
if (ignoredblock) {
if (premessage?.author !== this.author) {
const span = document.createElement("span");
span.textContent =
"You have this user blocked, click to hide these messages.";
div.append(span);
span.classList.add("blocked");
span.onclick = (_) => {
const scroll = this.channel.infinite.scrollTop;
let next: Message | undefined = this;
while (next?.author === this.author) {
next.generateMessage();
next = this.channel.messages.get(
this.channel.idToNext.get(next.id) as string
);
}
if (this.channel.infinite.scollDiv && scroll) {
this.channel.infinite.scollDiv.scrollTop = scroll;
}
};
}
} else {
div.classList.remove("topMessage");
if (premessage?.author === this.author) {
div.classList.add("zeroheight");
premessage.blockedPropigate();
div.appendChild(build);
return div;
} else {
build.classList.add("blocked", "topMessage");
const span = document.createElement("span");
let count = 1;
let next = this.channel.messages.get(
this.channel.idToNext.get(this.id) as string
);
while (next?.author === this.author) {
count++;
next = this.channel.messages.get(
this.channel.idToNext.get(next.id) as string
);
}
span.textContent = `You have this user blocked, click to see the ${count} blocked messages.`;
build.append(span);
span.onclick = (_) => {
const scroll = this.channel.infinite.scrollTop;
const func = this.channel.infinite.snapBottom();
let next: Message | undefined = this;
while (next?.author === this.author) {
next.generateMessage(undefined, true);
next = this.channel.messages.get(
this.channel.idToNext.get(next.id) as string
);
console.log("loopy");
}
if (this.channel.infinite.scollDiv && scroll) {
func();
this.channel.infinite.scollDiv.scrollTop = scroll;
}
};
div.appendChild(build);
return div;
}
}
}
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");
replyline.appendChild(reply);
const line2 = document.createElement("hr");
replyline.appendChild(line2);
line2.classList.add("reply");
line.classList.add("startreply");
replyline.classList.add("replyflex");
// TODO: Fix this
this.channel.getmessage(this.message_reference.id).then((message) => {
if (message.author.relationshipType === 2) {
username.textContent = "Blocked user";
return;
}
const author = message.author;
reply.appendChild(message.content.makeHTML({ stdsize: true }));
minipfp.src = author.getpfpsrc();
author.bind(minipfp, this.guild);
username.textContent = author.username;
author.bind(username, this.guild);
});
reply.onclick = (_) => {
// TODO: FIX this
this.channel.infinite.focus(this.message_reference.id);
};
div.appendChild(replyline);
}
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;
// @ts-ignore
// TODO: type this
let pfpparent2 = pfpparent.all;
pfpparent2 ??= pfpparent;
const old = new Date(pfpparent2.timestamp).getTime() / 1000;
const newt = new Date(this.timestamp).getTime() / 1000;
current = newt - old > 600;
}
const combine =
premessage?.author != this.author || current || this.message_reference;
if (combine) {
const pfp = this.author.buildpfp();
this.author.bind(pfp, this.guild, false);
pfpRow.appendChild(pfp);
} else {
div["pfpparent"] = pfpparent;
}
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);
if (combine) {
const username = document.createElement("span");
username.classList.add("username");
this.author.bind(username, this.guild);
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");
username.classList.add("bot");
username.textContent = "BOT";
userwrap.appendChild(username);
}
const time = document.createElement("span");
time.textContent = " " + formatTime(new Date(this.timestamp));
time.classList.add("timestamp");
userwrap.appendChild(time);
texttxt.appendChild(userwrap);
} else {
div.classList.remove("topMessage");
}
const messaged = this.content.makeHTML();
(div as any)["txt"] = messaged;
const messagedwrap = document.createElement("div");
messagedwrap.classList.add("flexttb");
messagedwrap.appendChild(messaged);
texttxt.appendChild(messagedwrap);
build.appendChild(text);
if (this.attachments.length) {
console.log(this.attachments);
const attach = document.createElement("div");
attach.classList.add("flexltr");
for (const thing of this.attachments) {
attach.appendChild(thing.getHTML());
}
messagedwrap.appendChild(attach);
}
if (this.embeds.length) {
const embeds = document.createElement("div");
embeds.classList.add("flexltr");
for (const thing of this.embeds) {
embeds.appendChild(thing.generateHTML());
}
messagedwrap.appendChild(embeds);
}
//
} 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);
const username = document.createElement("span");
username.textContent = this.author.username;
//this.author.profileclick(username);
this.author.bind(username, this.guild);
texttxt.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);
div.classList.add("topMessage");
}
const reactions = document.createElement("div");
reactions.classList.add("flexltr", "reactiondiv");
this.reactdiv = new WeakRef(reactions);
this.updateReactions();
div.append(reactions);
this.bindButtonEvent();
return div;
}
bindButtonEvent() {
if (this.div) {
let buttons: HTMLDivElement | undefined;
this.div.onmouseenter = (_) => {
if (buttons) {
buttons.remove();
buttons = undefined;
}
if (this.div) {
buttons = document.createElement("div");
buttons.classList.add("messageButtons", "flexltr");
if (this.channel.hasPermission("SEND_MESSAGES")) {
const container = document.createElement("div");
const reply = document.createElement("span");
reply.classList.add("svgtheme", "svg-reply", "svgicon");
container.append(reply);
buttons.append(container);
container.onclick = (_) => {
this.channel.setReplying(this);
};
}
if (this.author === this.localuser.user) {
const container = document.createElement("div");
const edit = document.createElement("span");
edit.classList.add("svgtheme", "svg-edit", "svgicon");
container.append(edit);
buttons.append(container);
container.onclick = (_) => {
this.setEdit();
};
}
if (this.canDelete()) {
const container = document.createElement("div");
const reply = document.createElement("span");
reply.classList.add("svgtheme", "svg-delete", "svgicon");
container.append(reply);
buttons.append(container);
container.onclick = (_) => {
if (_.shiftKey) {
this.delete();
return;
}
const diaolog = new Dialog([
"hdiv",
["title", "are you sure you want to delete this?"],
[
"button",
"",
"yes",
() => {
this.delete();
diaolog.hide();
},
],
[
"button",
"",
"no",
() => {
diaolog.hide();
},
],
]);
diaolog.show();
};
}
if (buttons.childNodes.length !== 0) {
this.div.append(buttons);
}
}
};
this.div.onmouseleave = (_) => {
if (buttons) {
buttons.remove();
buttons = undefined;
}
};
}
}
updateReactions() {
const reactdiv = this.reactdiv.deref();
if (!reactdiv) return;
const func = this.channel.infinite.snapBottom();
reactdiv.innerHTML = "";
for (const thing of this.reactions) {
const reaction = document.createElement("div");
reaction.classList.add("reaction");
if (thing.me) {
reaction.classList.add("meReacted");
}
let emoji: HTMLElement;
if (thing.emoji.id || /\d{17,21}/.test(thing.emoji.name)) {
if (/\d{17,21}/.test(thing.emoji.name))
thing.emoji.id = thing.emoji.name; //Should stop being a thing once the server fixes this bug
const emo = new Emoji(
thing.emoji as { name: string; id: string; animated: boolean },
this.guild
);
emoji = emo.getHTML(false);
} else {
emoji = document.createElement("p");
emoji.textContent = thing.emoji.name;
}
const count = document.createElement("p");
count.textContent = "" + thing.count;
count.classList.add("reactionCount");
reaction.append(count);
reaction.append(emoji);
reactdiv.append(reaction);
reaction.onclick = (_) => {
this.reactionToggle(thing.emoji.name);
};
}
func();
}
reactionAdd(data: { name: string }, member: Member | { id: string }) {
for (const thing of this.reactions) {
if (thing.emoji.name === data.name) {
thing.count++;
if (member.id === this.localuser.user.id) {
thing.me = true;
this.updateReactions();
return;
}
}
}
this.reactions.push({
count: 1,
emoji: data,
me: member.id === this.localuser.user.id,
});
this.updateReactions();
}
reactionRemove(data: { name: string }, id: string) {
console.log("test");
for (const i in this.reactions) {
const thing = this.reactions[i];
console.log(thing, data);
if (thing.emoji.name === data.name) {
thing.count--;
if (thing.count === 0) {
this.reactions.splice(Number(i), 1);
this.updateReactions();
return;
}
if (id === this.localuser.user.id) {
thing.me = false;
this.updateReactions();
return;
}
}
}
}
reactionRemoveAll() {
this.reactions = [];
this.updateReactions();
}
reactionRemoveEmoji(emoji: Emoji) {
for (const i in this.reactions) {
const reaction = this.reactions[i];
if (
(reaction.emoji.id && reaction.emoji.id == emoji.id) ||
(!reaction.emoji.id && reaction.emoji.name == emoji.name)
) {
this.reactions.splice(Number(i), 1);
this.updateReactions();
break;
}
}
}
buildhtml(premessage?: Message | undefined): HTMLElement {
if (this.div) {
console.error(`HTML for ${this.id} already exists, aborting`);
return this.div;
}
try {
const div = document.createElement("div");
this.div = div;
this.messageevents(div);
return this.generateMessage(premessage) as HTMLElement;
} catch (e) {
console.error(e);
}
return this.div as HTMLElement;
}
}
let now: string;
let yesterdayStr: string;
function formatTime(date: Date) {
updateTimes();
const datestring = date.toLocaleDateString();
const formatTime = (date: Date) =>
date.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" });
if (datestring === now) {
return `Today at ${formatTime(date)}`;
} else if (datestring === yesterdayStr) {
return `Yesterday at ${formatTime(date)}`;
} else {
return `${date.toLocaleDateString()} at ${formatTime(date)}`;
}
}
let tomorrow = 0;
updateTimes();
function updateTimes() {
if (tomorrow < Date.now()) {
const d = new Date();
tomorrow = d.setHours(24, 0, 0, 0);
now = new Date().toLocaleDateString();
const yesterday = new Date(now);
yesterday.setDate(new Date().getDate() - 1);
yesterdayStr = yesterday.toLocaleDateString();
}
}
Message.setup();
export { Message };

347
src/webpage/permissions.ts Normal file
View file

@ -0,0 +1,347 @@
class Permissions {
allow: bigint;
deny: bigint;
readonly hasDeny: boolean;
constructor(allow: string, deny: string = "") {
this.hasDeny = Boolean(deny);
try {
this.allow = BigInt(allow);
this.deny = BigInt(deny);
} catch {
this.allow = 0n;
this.deny = 0n;
console.error(
`Something really stupid happened with a permission with allow being ${allow} and deny being, ${deny}, execution will still happen, but something really stupid happened, please report if you know what caused this.`
);
}
}
getPermissionbit(b: number, big: bigint): boolean {
return Boolean((big >> BigInt(b)) & 1n);
}
setPermissionbit(b: number, state: boolean, big: bigint): bigint {
const bit = 1n << BigInt(b);
return (big & ~bit) | (BigInt(state) << BigInt(b)); //thanks to geotale for this code :3
}
static map: {
[key: number | string]:
| { name: string; readableName: string; description: string }
| number;
};
static info: { name: string; readableName: string; description: string }[];
static makeMap() {
Permissions.info = [
//for people in the future, do not reorder these, the creation of the map realize on the order
{
name: "CREATE_INSTANT_INVITE",
readableName: "Create invite",
description: "Allows the user to create invites for the guild",
},
{
name: "KICK_MEMBERS",
readableName: "Kick members",
description: "Allows the user to kick members from the guild",
},
{
name: "BAN_MEMBERS",
readableName: "Ban members",
description: "Allows the user to ban members from the guild",
},
{
name: "ADMINISTRATOR",
readableName: "Administrator",
description:
"Allows all permissions and bypasses channel permission overwrites. This is a dangerous permission!",
},
{
name: "MANAGE_CHANNELS",
readableName: "Manage channels",
description: "Allows the user to manage and edit channels",
},
{
name: "MANAGE_GUILD",
readableName: "Manage guild",
description: "Allows management and editing of the guild",
},
{
name: "ADD_REACTIONS",
readableName: "Add reactions",
description: "Allows user to add reactions to messages",
},
{
name: "VIEW_AUDIT_LOG",
readableName: "View audit log",
description: "Allows the user to view the audit log",
},
{
name: "PRIORITY_SPEAKER",
readableName: "Priority speaker",
description: "Allows for using priority speaker in a voice channel",
},
{
name: "STREAM",
readableName: "Video",
description: "Allows the user to stream",
},
{
name: "VIEW_CHANNEL",
readableName: "View channels",
description: "Allows the user to view the channel",
},
{
name: "SEND_MESSAGES",
readableName: "Send messages",
description: "Allows user to send messages",
},
{
name: "SEND_TTS_MESSAGES",
readableName: "Send text-to-speech messages",
description: "Allows the user to send text-to-speech messages",
},
{
name: "MANAGE_MESSAGES",
readableName: "Manage messages",
description: "Allows the user to delete messages that aren't their own",
},
{
name: "EMBED_LINKS",
readableName: "Embed links",
description: "Allow links sent by this user to auto-embed",
},
{
name: "ATTACH_FILES",
readableName: "Attach files",
description: "Allows the user to attach files",
},
{
name: "READ_MESSAGE_HISTORY",
readableName: "Read message history",
description: "Allows user to read the message history",
},
{
name: "MENTION_EVERYONE",
readableName: "Mention @everyone, @here and all roles",
description: "Allows the user to mention everyone",
},
{
name: "USE_EXTERNAL_EMOJIS",
readableName: "Use external emojis",
description: "Allows the user to use external emojis",
},
{
name: "VIEW_GUILD_INSIGHTS",
readableName: "View guild insights",
description: "Allows the user to see guild insights",
},
{
name: "CONNECT",
readableName: "Connect",
description: "Allows the user to connect to a voice channel",
},
{
name: "SPEAK",
readableName: "Speak",
description: "Allows the user to speak in a voice channel",
},
{
name: "MUTE_MEMBERS",
readableName: "Mute members",
description: "Allows user to mute other members",
},
{
name: "DEAFEN_MEMBERS",
readableName: "Deafen members",
description: "Allows user to deafen other members",
},
{
name: "MOVE_MEMBERS",
readableName: "Move members",
description: "Allows the user to move members between voice channels",
},
{
name: "USE_VAD",
readableName: "Use voice activity detection",
description:
"Allows users to speak in a voice channel by simply talking",
},
{
name: "CHANGE_NICKNAME",
readableName: "Change nickname",
description: "Allows the user to change their own nickname",
},
{
name: "MANAGE_NICKNAMES",
readableName: "Manage nicknames",
description: "Allows user to change nicknames of other members",
},
{
name: "MANAGE_ROLES",
readableName: "Manage roles",
description: "Allows user to edit and manage roles",
},
{
name: "MANAGE_WEBHOOKS",
readableName: "Manage webhooks",
description: "Allows management and editing of webhooks",
},
{
name: "MANAGE_GUILD_EXPRESSIONS",
readableName: "Manage expressions",
description: "Allows for managing emoji, stickers, and soundboards",
},
{
name: "USE_APPLICATION_COMMANDS",
readableName: "Use application commands",
description: "Allows the user to use application commands",
},
{
name: "REQUEST_TO_SPEAK",
readableName: "Request to speak",
description: "Allows user to request to speak in stage channel",
},
{
name: "MANAGE_EVENTS",
readableName: "Manage events",
description: "Allows user to edit and manage events",
},
{
name: "MANAGE_THREADS",
readableName: "Manage threads",
description:
"Allows the user to delete and archive threads and view all private threads",
},
{
name: "CREATE_PUBLIC_THREADS",
readableName: "Create public threads",
description: "Allows the user to create public threads",
},
{
name: "CREATE_PRIVATE_THREADS",
readableName: "Create private threads",
description: "Allows the user to create private threads",
},
{
name: "USE_EXTERNAL_STICKERS",
readableName: "Use external stickers",
description: "Allows user to use external stickers",
},
{
name: "SEND_MESSAGES_IN_THREADS",
readableName: "Send messages in threads",
description: "Allows the user to send messages in threads",
},
{
name: "USE_EMBEDDED_ACTIVITIES",
readableName: "Use activities",
description: "Allows the user to use embedded activities",
},
{
name: "MODERATE_MEMBERS",
readableName: "Timeout members",
description:
"Allows the user to time out other users to prevent them from sending or reacting to messages in chat and threads, and from speaking in voice and stage channels",
},
{
name: "VIEW_CREATOR_MONETIZATION_ANALYTICS",
readableName: "View creator monetization analytics",
description: "Allows for viewing role subscription insights",
},
{
name: "USE_SOUNDBOARD",
readableName: "Use soundboard",
description: "Allows for using soundboard in a voice channel",
},
{
name: "CREATE_GUILD_EXPRESSIONS",
readableName: "Create expressions",
description:
"Allows for creating emojis, stickers, and soundboard sounds, and editing and deleting those created by the current user.",
},
{
name: "CREATE_EVENTS",
readableName: "Create events",
description:
"Allows for creating scheduled events, and editing and deleting those created by the current user.",
},
{
name: "USE_EXTERNAL_SOUNDS",
readableName: "Use external sounds",
description:
"Allows the usage of custom soundboard sounds from other servers",
},
{
name: "SEND_VOICE_MESSAGES",
readableName: "Send voice messages",
description: "Allows sending voice messages",
},
{
name: "SEND_POLLS",
readableName: "Create polls",
description: "Allows sending polls",
},
{
name: "USE_EXTERNAL_APPS",
readableName: "Use external apps",
description:
"Allows user-installed apps to send public responses. " +
"When disabled, users will still be allowed to use their apps but the responses will be ephemeral. " +
"This only applies to apps not also installed to the server.",
},
];
Permissions.map = {};
let i = 0;
for (const thing of Permissions.info) {
Permissions.map[i] = thing;
Permissions.map[thing.name] = i;
i++;
}
}
getPermission(name: string): number {
if (this.getPermissionbit(Permissions.map[name] as number, this.allow)) {
return 1;
} else if (
this.getPermissionbit(Permissions.map[name] as number, this.deny)
) {
return -1;
} else {
return 0;
}
}
hasPermission(name: string): boolean {
if (this.deny) {
console.warn(
"This function may of been used in error, think about using getPermision instead"
);
}
if (this.getPermissionbit(Permissions.map[name] as number, this.allow))
return true;
if (name != "ADMINISTRATOR") return this.hasPermission("ADMINISTRATOR");
return false;
}
setPermission(name: string, setto: number): void {
const bit = Permissions.map[name] as number;
if (!bit) {
return console.error(
"Tried to set permission to " +
setto +
" for " +
name +
" but it doesn't exist"
);
}
if (setto === 0) {
this.deny = this.setPermissionbit(bit, false, this.deny);
this.allow = this.setPermissionbit(bit, false, this.allow);
} else if (setto === 1) {
this.deny = this.setPermissionbit(bit, false, this.deny);
this.allow = this.setPermissionbit(bit, true, this.allow);
} else if (setto === -1) {
this.deny = this.setPermissionbit(bit, true, this.deny);
this.allow = this.setPermissionbit(bit, false, this.allow);
} else {
console.error("invalid number entered:" + setto);
}
}
}
Permissions.makeMap();
export { Permissions };

152
src/webpage/register.ts Normal file
View file

@ -0,0 +1,152 @@
import { checkInstance, adduser } from "./login.js";
const registerElement = document.getElementById("register");
if (registerElement) {
registerElement.addEventListener("submit", registertry);
}
async function registertry(e: Event) {
e.preventDefault();
const elements = (e.target as HTMLFormElement)
.elements as HTMLFormControlsCollection;
const email = (elements[1] as HTMLInputElement).value;
const username = (elements[2] as HTMLInputElement).value;
const password = (elements[3] as HTMLInputElement).value;
const confirmPassword = (elements[4] as HTMLInputElement).value;
const dateofbirth = (elements[5] as HTMLInputElement).value;
const consent = (elements[6] as HTMLInputElement).checked;
const captchaKey = (elements[7] as HTMLInputElement)?.value;
if (password !== confirmPassword) {
(document.getElementById("wrong") as HTMLElement).textContent =
"Passwords don't match";
return;
}
const instanceInfo = JSON.parse(localStorage.getItem("instanceinfo") ?? "{}");
const apiurl = new URL(instanceInfo.api);
try {
const response = await fetch(apiurl + "/auth/register", {
body: JSON.stringify({
date_of_birth: dateofbirth,
email,
username,
password,
consent,
captcha_key: captchaKey,
}),
headers: {
"content-type": "application/json",
},
method: "POST",
});
const data = await response.json();
if (data.captcha_sitekey) {
const capt = document.getElementById("h-captcha");
if (capt && !capt.children.length) {
const capty = document.createElement("div");
capty.classList.add("h-captcha");
capty.setAttribute("data-sitekey", data.captcha_sitekey);
const script = document.createElement("script");
script.src = "https://js.hcaptcha.com/1/api.js";
capt.append(script);
capt.append(capty);
} else {
eval("hcaptcha.reset()");
}
return;
}
if (!data.token) {
handleErrors(data.errors, elements);
} else {
adduser({
serverurls: instanceInfo,
email,
token: data.token,
}).username = username;
localStorage.setItem("token", data.token);
const redir = new URLSearchParams(window.location.search).get("goback");
window.location.href = redir ? redir : "/channels/@me";
}
} catch (error) {
console.error("Registration failed:", error);
}
}
function handleErrors(errors: any, elements: HTMLFormControlsCollection) {
if (errors.consent) {
showError(elements[6] as HTMLElement, errors.consent._errors[0].message);
} else if (errors.password) {
showError(
elements[3] as HTMLElement,
"Password: " + errors.password._errors[0].message
);
} else if (errors.username) {
showError(
elements[2] as HTMLElement,
"Username: " + errors.username._errors[0].message
);
} else if (errors.email) {
showError(
elements[1] as HTMLElement,
"Email: " + errors.email._errors[0].message
);
} else if (errors.date_of_birth) {
showError(
elements[5] as HTMLElement,
"Date of Birth: " + errors.date_of_birth._errors[0].message
);
} else {
(document.getElementById("wrong") as HTMLElement).textContent =
errors[Object.keys(errors)[0]]._errors[0].message;
}
}
function showError(element: HTMLElement, message: string) {
const parent = element.parentElement!;
let errorElement = parent.getElementsByClassName(
"suberror"
)[0] as HTMLElement;
if (!errorElement) {
const div = document.createElement("div");
div.classList.add("suberror", "suberrora");
parent.append(div);
errorElement = div;
} else {
errorElement.classList.remove("suberror");
setTimeout(() => {
errorElement.classList.add("suberror");
}, 100);
}
errorElement.textContent = message;
}
let TOSa = document.getElementById("TOSa") as HTMLAnchorElement | null;
async function tosLogic() {
const instanceInfo = JSON.parse(localStorage.getItem("instanceinfo") ?? "{}");
const apiurl = new URL(instanceInfo.api);
const response = await fetch(apiurl.toString() + "/ping");
const data = await response.json();
const tosPage = data.instance.tosPage;
if (tosPage) {
document.getElementById("TOSbox")!.innerHTML =
'I agree to the <a href="" id="TOSa">Terms of Service</a>:';
TOSa = document.getElementById("TOSa") as HTMLAnchorElement;
TOSa.href = tosPage;
} else {
document.getElementById("TOSbox")!.textContent =
"This instance has no Terms of Service, accept ToS anyways:";
TOSa = null;
}
console.log(tosPage);
}
tosLogic();
(checkInstance as any)["alt"] = tosLogic;

180
src/webpage/role.ts Normal file
View file

@ -0,0 +1,180 @@
import { Permissions } from "./permissions.js";
import { Localuser } from "./localuser.js";
import { Guild } from "./guild.js";
import { SnowFlake } from "./snowflake.js";
import { rolesjson } from "./jsontypes.js";
class Role extends SnowFlake {
permissions: Permissions;
owner: Guild;
color!: number;
name!: string;
info: Guild["info"];
hoist!: boolean;
icon!: string;
mentionable!: boolean;
unicode_emoji!: string;
headers: Guild["headers"];
constructor(json: rolesjson, owner: Guild) {
super(json.id);
this.headers = owner.headers;
this.info = owner.info;
for (const thing of Object.keys(json)) {
if (thing === "id") {
continue;
}
(this as any)[thing] = (json as any)[thing];
}
this.permissions = new Permissions(json.permissions);
this.owner = owner;
}
get guild(): Guild {
return this.owner;
}
get localuser(): Localuser {
return this.guild.localuser;
}
getColor(): string | null {
if (this.color === 0) {
return null;
}
return `#${this.color.toString(16)}`;
}
}
export { Role };
import { Options } from "./settings.js";
class PermissionToggle implements OptionsElement<number> {
readonly rolejson: {
name: string;
readableName: string;
description: string;
};
permissions: Permissions;
owner: Options;
value!: number;
constructor(
roleJSON: PermissionToggle["rolejson"],
permissions: Permissions,
owner: Options
) {
this.rolejson = roleJSON;
this.permissions = permissions;
this.owner = owner;
}
watchForChange() {}
generateHTML(): HTMLElement {
const div = document.createElement("div");
div.classList.add("setting");
const name = document.createElement("span");
name.textContent = this.rolejson.readableName;
name.classList.add("settingsname");
div.append(name);
div.append(this.generateCheckbox());
const p = document.createElement("p");
p.textContent = this.rolejson.description;
div.appendChild(p);
return div;
}
generateCheckbox(): HTMLElement {
const div = document.createElement("div");
div.classList.add("tritoggle");
const state = this.permissions.getPermission(this.rolejson.name);
const on = document.createElement("input");
on.type = "radio";
on.name = this.rolejson.name;
div.append(on);
if (state === 1) {
on.checked = true;
}
on.onclick = (_) => {
this.permissions.setPermission(this.rolejson.name, 1);
this.owner.changed();
};
const no = document.createElement("input");
no.type = "radio";
no.name = this.rolejson.name;
div.append(no);
if (state === 0) {
no.checked = true;
}
no.onclick = (_) => {
this.permissions.setPermission(this.rolejson.name, 0);
this.owner.changed();
};
if (this.permissions.hasDeny) {
const off = document.createElement("input");
off.type = "radio";
off.name = this.rolejson.name;
div.append(off);
if (state === -1) {
off.checked = true;
}
off.onclick = (_) => {
this.permissions.setPermission(this.rolejson.name, -1);
this.owner.changed();
};
}
return div;
}
submit() {}
}
import { OptionsElement, Buttons } from "./settings.js";
class RoleList extends Buttons {
readonly permissions: [Role, Permissions][];
permission: Permissions;
readonly guild: Guild;
readonly channel: boolean;
declare readonly buttons: [string, string][];
readonly options: Options;
onchange: Function;
curid!: string;
constructor(
permissions: [Role, Permissions][],
guild: Guild,
onchange: Function,
channel = false
) {
super("Roles");
this.guild = guild;
this.permissions = permissions;
this.channel = channel;
this.onchange = onchange;
const options = new Options("", this);
if (channel) {
this.permission = new Permissions("0", "0");
} else {
this.permission = new Permissions("0");
}
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;
}
handleString(str: string): HTMLElement {
this.curid = str;
const arr = this.permissions.find((_) => _[0].id === str);
if (arr) {
const perm = arr[1];
this.permission.deny = perm.deny;
this.permission.allow = perm.allow;
const role = this.permissions.find((e) => e[0].id === str);
if (role) {
this.options.name = role[0].name;
this.options.haschanged = false;
}
}
return this.options.generateHTML();
}
save() {
this.onchange(this.curid, this.permission);
}
}
export { RoleList };

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

@ -0,0 +1,96 @@
function deleteoldcache() {
caches.delete("cache");
console.log("this ran :P");
}
async function putInCache(request: URL | RequestInfo, response: Response) {
console.log(request, response);
const cache = await caches.open("cache");
console.log("Grabbed");
try {
console.log(await cache.put(request, response));
} catch (error) {
console.error(error);
}
}
console.log("test");
let lastcache: string;
self.addEventListener("activate", async () => {
console.log("test2");
checkCache();
});
async function checkCache() {
if (checkedrecently) {
return;
}
const promise = await caches.match("/getupdates");
if (promise) {
lastcache = await promise.text();
}
console.log(lastcache);
fetch("/getupdates").then(async (data) => {
const text = await data.clone().text();
console.log(text, lastcache);
if (lastcache !== text) {
deleteoldcache();
putInCache("/getupdates", data.clone());
}
checkedrecently = true;
setTimeout((_: any) => {
checkedrecently = false;
}, 1000 * 60 * 30);
});
}
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;
}
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());
}
const responseFromCache = await caches.match(event.request.url);
console.log(responseFromCache, caches);
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 {
return responseFromNetwork;
} catch (e) {
console.error(e);
return e;
}
}
self.addEventListener("fetch", (event: any) => {
try {
event.respondWith(getfile(event));
} catch (e) {
console.error(e);
}
});

1113
src/webpage/settings.ts Normal file

File diff suppressed because it is too large Load diff

20
src/webpage/snowflake.ts Normal file
View file

@ -0,0 +1,20 @@
abstract class SnowFlake {
public readonly id: string;
constructor(id: string) {
this.id = id;
}
getUnixTime(): number {
return SnowFlake.stringToUnixTime(this.id);
}
static stringToUnixTime(str: string) {
try {
return Number((BigInt(str) >> 22n) + 1420070400000n);
} catch {
console.error(
`The ID is corrupted, it's ${str} when it should be some number.`
);
return 0;
}
}
}
export { SnowFlake };

489
src/webpage/user.ts Normal file
View file

@ -0,0 +1,489 @@
import { Member } from "./member.js";
import { MarkDown } from "./markdown.js";
import { Contextmenu } from "./contextmenu.js";
import { Localuser } from "./localuser.js";
import { Guild } from "./guild.js";
import { SnowFlake } from "./snowflake.js";
import { presencejson, userjson } from "./jsontypes.js";
class User extends SnowFlake {
owner: Localuser;
hypotheticalpfp!: boolean;
avatar!: string | null;
username!: string;
nickname: string | null = null;
relationshipType: 0 | 1 | 2 | 3 | 4 = 0;
bio!: MarkDown;
discriminator!: string;
pronouns!: string;
bot!: boolean;
public_flags!: number;
accent_color!: number;
banner: string | undefined;
hypotheticalbanner!: boolean;
premium_since!: string;
premium_type!: number;
theme_colors!: string;
badge_ids!: string[];
members: WeakMap<Guild, Member | undefined | Promise<Member | undefined>> =
new WeakMap();
private status!: string;
resolving: false | Promise<any> = false;
constructor(userjson: userjson, owner: Localuser, dontclone = false) {
super(userjson.id);
this.owner = owner;
if (!owner) {
console.error("missing localuser");
}
if (dontclone) {
for (const key of Object.keys(userjson)) {
if (key === "bio") {
this.bio = new MarkDown(userjson[key], this.localuser);
continue;
}
if (key === "id") {
continue;
}
(this as any)[key] = (userjson as any)[key];
}
this.hypotheticalpfp = false;
} else {
return User.checkuser(userjson, owner);
}
}
clone(): User {
return new User(
{
username: this.username,
id: this.id + "#clone",
public_flags: this.public_flags,
discriminator: this.discriminator,
avatar: this.avatar,
accent_color: this.accent_color,
banner: this.banner,
bio: this.bio.rawString,
premium_since: this.premium_since,
premium_type: this.premium_type,
bot: this.bot,
theme_colors: this.theme_colors,
pronouns: this.pronouns,
badge_ids: this.badge_ids,
},
this.owner
);
}
public getPresence(presence: presencejson | undefined): void {
if (presence) {
this.setstatus(presence.status);
} else {
this.setstatus("offline");
}
}
setstatus(status: string): void {
this.status = status;
}
async getStatus(): Promise<string> {
return this.status || "offline";
}
static contextmenu = new Contextmenu<User, Member | undefined>("User Menu");
static setUpContextMenu(): void {
this.contextmenu.addbutton("Copy user id", function (this: User) {
navigator.clipboard.writeText(this.id);
});
this.contextmenu.addbutton("Message user", function (this: User) {
fetch(this.info.api + "/users/@me/channels", {
method: "POST",
body: JSON.stringify({ recipients: [this.id] }),
headers: this.localuser.headers,
})
.then((res) => res.json())
.then((json) => {
this.localuser.goToChannel(json.id);
});
});
this.contextmenu.addbutton(
"Block user",
function (this: User) {
this.block();
},
null,
function () {
return this.relationshipType !== 2;
}
);
this.contextmenu.addbutton(
"Unblock user",
function (this: User) {
this.unblock();
},
null,
function () {
return this.relationshipType === 2;
}
);
this.contextmenu.addbutton("Friend request", function (this: User) {
fetch(`${this.info.api}/users/@me/relationships/${this.id}`, {
method: "PUT",
headers: this.owner.headers,
body: JSON.stringify({
type: 1,
}),
});
});
this.contextmenu.addbutton(
"Kick member",
function (this: User, member: Member | undefined) {
member?.kick();
},
null,
(member) => {
if (!member) return false;
const us = member.guild.member;
if (member.id === us.id) {
return false;
}
if (member.id === member.guild.properties.owner_id) {
return false;
}
return us.hasPermission("KICK_MEMBERS") || false;
}
);
this.contextmenu.addbutton(
"Ban member",
function (this: User, member: Member | undefined) {
member?.ban();
},
null,
(member) => {
if (!member) return false;
const us = member.guild.member;
if (member.id === us.id) {
return false;
}
if (member.id === member.guild.properties.owner_id) {
return false;
}
return us.hasPermission("BAN_MEMBERS") || false;
}
);
}
static checkuser(user: User | userjson, owner: Localuser): User {
if (owner.userMap.has(user.id)) {
return owner.userMap.get(user.id) as User;
} else {
const tempuser = new User(user as userjson, owner, true);
owner.userMap.set(user.id, tempuser);
return tempuser;
}
}
get info() {
return this.owner.info;
}
get localuser() {
return this.owner;
}
get name() {
return this.username;
}
async resolvemember(guild: Guild): Promise<Member | undefined> {
return await Member.resolveMember(this, guild);
}
async getUserProfile(): Promise<any> {
return await fetch(
`${this.info.api}/users/${this.id.replace(
"#clone",
""
)}/profile?with_mutual_guilds=true&with_mutual_friends=true`,
{
headers: this.localuser.headers,
}
).then((res) => res.json());
}
async getBadge(id: string): Promise<any> {
if (this.localuser.badges.has(id)) {
return this.localuser.badges.get(id);
} else {
if (this.resolving) {
await this.resolving;
return this.localuser.badges.get(id);
}
const prom = await this.getUserProfile();
this.resolving = prom;
const badges = prom.badges;
this.resolving = false;
for (const badge of badges) {
this.localuser.badges.set(badge.id, badge);
}
return this.localuser.badges.get(id);
}
}
buildpfp(): HTMLImageElement {
const pfp = document.createElement("img");
pfp.loading = "lazy";
pfp.src = this.getpfpsrc();
pfp.classList.add("pfp");
pfp.classList.add("userid:" + this.id);
return pfp;
}
async buildstatuspfp(): Promise<HTMLDivElement> {
const div = document.createElement("div");
div.style.position = "relative";
const pfp = this.buildpfp();
div.append(pfp);
const status = document.createElement("div");
status.classList.add("statusDiv");
switch (await this.getStatus()) {
case "offline":
status.classList.add("offlinestatus");
break;
case "online":
default:
status.classList.add("onlinestatus");
break;
}
div.append(status);
return div;
}
userupdate(json: userjson): void {
if (json.avatar !== this.avatar) {
this.changepfp(json.avatar);
}
}
bind(html: HTMLElement, guild: Guild | null = null, error = true): void {
if (guild && guild.id !== "@me") {
Member.resolveMember(this, guild)
.then((member) => {
User.contextmenu.bindContextmenu(html, this, member);
if (member === undefined && error) {
const errorSpan = document.createElement("span");
errorSpan.textContent = "!";
errorSpan.classList.add("membererror");
html.after(errorSpan);
return;
}
if (member) {
member.bind(html);
}
})
.catch((err) => {
console.log(err);
});
}
if (guild) {
this.profileclick(html, guild);
} else {
this.profileclick(html);
}
}
static async resolve(id: string, localuser: Localuser): Promise<User> {
const json = await fetch(
localuser.info.api.toString() + "/users/" + id + "/profile",
{ headers: localuser.headers }
).then((res) => res.json());
return new User(json, localuser);
}
changepfp(update: string | null): void {
this.avatar = update;
this.hypotheticalpfp = false;
const src = this.getpfpsrc();
Array.from(document.getElementsByClassName("userid:" + this.id)).forEach(
(element) => {
(element as HTMLImageElement).src = src;
}
);
}
block(): void {
fetch(`${this.info.api}/users/@me/relationships/${this.id}`, {
method: "PUT",
headers: this.owner.headers,
body: JSON.stringify({
type: 2,
}),
});
this.relationshipType = 2;
const channel = this.localuser.channelfocus;
if (channel) {
for (const message of channel.messages) {
message[1].generateMessage();
}
}
}
unblock(): void {
fetch(`${this.info.api}/users/@me/relationships/${this.id}`, {
method: "DELETE",
headers: this.owner.headers,
});
this.relationshipType = 0;
const channel = this.localuser.channelfocus;
if (channel) {
for (const message of channel.messages) {
message[1].generateMessage();
}
}
}
getpfpsrc(): string {
if (this.hypotheticalpfp && this.avatar) {
return this.avatar;
}
if (this.avatar !== null) {
return `${this.info.cdn}/avatars/${this.id.replace("#clone", "")}/${
this.avatar
}.png`;
} else {
const int = Number((BigInt(this.id.replace("#clone", "")) >> 22n) % 6n);
return `${this.info.cdn}/embed/avatars/${int}.png`;
}
}
async buildprofile(
x: number,
y: number,
guild: Guild | null = null
): Promise<HTMLDivElement> {
if (Contextmenu.currentmenu != "") {
Contextmenu.currentmenu.remove();
}
const div = document.createElement("div");
if (this.accent_color) {
div.style.setProperty(
"--accent_color",
`#${this.accent_color.toString(16).padStart(6, "0")}`
);
} else {
div.style.setProperty("--accent_color", "transparent");
}
if (this.banner) {
const banner = document.createElement("img");
let src: string;
if (!this.hypotheticalbanner) {
src = `${this.info.cdn}/avatars/${this.id.replace("#clone", "")}/${
this.banner
}.png`;
} else {
src = this.banner;
}
banner.src = src;
banner.classList.add("banner");
div.append(banner);
}
if (x !== -1) {
div.style.left = `${x}px`;
div.style.top = `${y}px`;
div.classList.add("profile", "flexttb");
} else {
this.setstatus("online");
div.classList.add("hypoprofile", "flexttb");
}
const badgediv = document.createElement("div");
badgediv.classList.add("badges");
(async () => {
if (!this.badge_ids) return;
for (const id of this.badge_ids) {
const badgejson = await this.getBadge(id);
if (badgejson) {
const badge = document.createElement(badgejson.link ? "a" : "div");
badge.classList.add("badge");
const img = document.createElement("img");
img.src = badgejson.icon;
badge.append(img);
const span = document.createElement("span");
span.textContent = badgejson.description;
badge.append(span);
if (badge instanceof HTMLAnchorElement) {
badge.href = badgejson.link;
}
badgediv.append(badge);
}
}
})();
const pfp = await this.buildstatuspfp();
div.appendChild(pfp);
const userbody = document.createElement("div");
userbody.classList.add("infosection");
div.appendChild(userbody);
const usernamehtml = document.createElement("h2");
usernamehtml.textContent = this.username;
userbody.appendChild(usernamehtml);
userbody.appendChild(badgediv);
const discrimatorhtml = document.createElement("h3");
discrimatorhtml.classList.add("tag");
discrimatorhtml.textContent = `${this.username}#${this.discriminator}`;
userbody.appendChild(discrimatorhtml);
const pronounshtml = document.createElement("p");
pronounshtml.textContent = this.pronouns;
pronounshtml.classList.add("pronouns");
userbody.appendChild(pronounshtml);
const rule = document.createElement("hr");
userbody.appendChild(rule);
const biohtml = this.bio.makeHTML();
userbody.appendChild(biohtml);
if (guild) {
Member.resolveMember(this, guild).then((member) => {
if (!member) return;
const roles = document.createElement("div");
roles.classList.add("rolesbox");
for (const role of member.roles) {
const roleDiv = document.createElement("div");
roleDiv.classList.add("rolediv");
const color = document.createElement("div");
roleDiv.append(color);
color.style.setProperty(
"--role-color",
`#${role.color.toString(16).padStart(6, "0")}`
);
color.classList.add("colorrolediv");
const span = document.createElement("span");
roleDiv.append(span);
span.textContent = role.name;
roles.append(roleDiv);
}
userbody.append(roles);
});
}
if (x !== -1) {
Contextmenu.currentmenu = div;
document.body.appendChild(div);
Contextmenu.keepOnScreen(div);
}
return div;
}
profileclick(obj: HTMLElement, guild?: Guild): void {
obj.onclick = (e: MouseEvent) => {
this.buildprofile(e.clientX, e.clientY, guild);
e.stopPropagation();
};
}
}
User.setUpContextMenu();
export { User };

179
stats.js
View file

@ -1,179 +0,0 @@
const index = require("./index.js");
const fs=require("node:fs");
let uptimeObject={};
if(fs.existsSync(__dirname+"/uptime.json")){
try{
uptimeObject=JSON.parse(fs.readFileSync(__dirname+"/uptime.json", "utf8"));
}catch{
uptimeObject={};
}
}
if(uptimeObject.undefined){
delete uptimeObject.undefined;
updatejson();
}
async function observe(instances){
const active=new Set();
async function resolveinstance(instance){
try{
calcStats(instance);
}catch(e){
console.error(e);
}
let api;
if(instance.urls){
api=instance.urls.api;
}else if(instance.url){
const urls=await index.getapiurls(instance.url);
if(urls){
api=urls.api;
}
}
if(!api||api===""){
setStatus(instance,false);
console.warn(instance.name+" does not resolve api URL",instance);
setTimeout(_=>{
resolveinstance(instance);
},1000*60*30,);
return;
}
active.add(instance.name);
api+=api.endsWith("/")?"":"/";
async function check(tries=0){
try{
const req=await fetch(api+"ping",{method: "HEAD"})
if(tries>3||req.ok){
setStatus(instance,req.ok);
}else{
setTimeout(()=>{
check(tries+1);
},30000)
}
}catch{
if(tries>3){
setStatus(instance,false);
}else{
setTimeout(()=>{
check(tries+1);
},30000)
}
}
}
setTimeout(
_=>{
check();
setInterval(_=>{
check();
},1000*60*30);
},Math.random()*1000*60*10
);
}
const promlist=[];
for(const instance of instances){
promlist.push(resolveinstance(instance));
}
await Promise.allSettled(promlist);
for(const key of Object.keys(uptimeObject)){
if(!active.has(key)){
setStatus(key,false);
}
}
}
function calcStats(instance){
const obj=uptimeObject[instance.name];
if(!obj)return;
const day=Date.now()-1000*60*60*24;
const week=Date.now()-1000*60*60*24*7;
let alltime=-1;
let totalTimePassed=0;
let daytime=-1;
let weektime=-1;
let online=false;
let i=0;
for(const thing of obj){
online=thing.online;
let stamp=thing.time;
if(alltime===-1){
alltime=0;
}
let timepassed;
if(obj[i+1]){
timepassed=obj[i+1].time-stamp;
}else{
timepassed=Date.now()-stamp;
}
totalTimePassed+=timepassed;
alltime+=online*timepassed;
if(stamp+timepassed>week){
if(stamp<week){
timepassed-=week-stamp;
stamp=week;
}
weektime+=online*timepassed;
if(stamp+timepassed>day){
if(stamp<day){
timepassed-=day-stamp;
stamp=day;
}
daytime+=online*timepassed;
}
}
i++;
}
console.log(daytime);
instance.online=online;
alltime/=totalTimePassed;
if(totalTimePassed>1000*60*60*24){
if(daytime===-1){
daytime=online*1000*60*60*24;
}
daytime/=1000*60*60*24;
if(totalTimePassed>1000*60*60*24*7){
if(weektime===-1){
weektime=online*1000*60*60*24*7;
}
weektime/=1000*60*60*24*7;
}else{
weektime=alltime;
}
}else{
weektime=alltime;
daytime=alltime;
}
instance.uptime={daytime,weektime,alltime};
}
/**
* @param {string|Object} instance
* @param {boolean} status
*/
function setStatus(instance,status){
let name=instance.name;
if(typeof instance==="string"){
name=instance;
}
let obj=uptimeObject[name];
let needSetting=false;
if(!obj){
obj=[];
uptimeObject[name]=obj;
needSetting=true;
}else{
if(obj.at(-1).online!==status){
needSetting=true;
}
}
if(needSetting){
obj.push({time: Date.now(),online: status});
updatejson();
}
if(typeof instance!=="string"){
calcStats(instance);
}
}
function updatejson(){
fs.writeFile(__dirname+"/uptime.json",JSON.stringify(uptimeObject),_=>{});
}
exports.observe=observe;
exports.uptime=uptimeObject;

View file

@ -1,20 +1,39 @@
{
{
"compilerOptions": {
"target": "es2022",
"moduleResolution": "Bundler",
"module":"es2022",
"strict": false,
"allowSyntheticDefaultImports": true,
"alwaysStrict": true,
"declaration": true,
"declarationMap": true,
"esModuleInterop": true,
"outDir": "./.dist",
"importHelpers": false,
"incremental": true,
"lib": [
"esnext",
"DOM"
],
"module": "ES2022",
"moduleResolution": "Bundler",
"newLine": "lf",
"noEmitHelpers": false,
"noFallthroughCasesInSwitch": true,
"noImplicitReturns": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"preserveConstEnums": true,
"pretty": true,
"removeComments": false,
"noImplicitThis":true,
"useUnknownInCatchVariables":true,
"strictNullChecks":true
"resolveJsonModule": true,
"sourceMap": true,
"strict": true,
"target": "ES2022",
"useDefineForClassFields": true,
"resolvePackageJsonImports": true,
"outDir": "./dist",
},
"include": [
"./webpage/*.ts"
"src/**/*.ts"
],
"exclude": [
"node_modules"
]
}
}

View file

@ -1,164 +0,0 @@
import{getBulkInfo}from"./login.js";
class Voice{
audioCtx:AudioContext;
info:{wave:string|Function,freq:number};
playing:boolean;
myArrayBuffer:AudioBuffer;
gainNode:GainNode;
buffer:Float32Array;
source:AudioBufferSourceNode;
constructor(wave:string|Function,freq:number,volume=1){
this.audioCtx = new (window.AudioContext)();
this.info={wave,freq};
this.playing=false;
this.myArrayBuffer=this.audioCtx.createBuffer(
1,
this.audioCtx.sampleRate,
this.audioCtx.sampleRate,
);
this.gainNode = this.audioCtx.createGain();
this.gainNode.gain.value=volume;
this.gainNode.connect(this.audioCtx.destination);
this.buffer=this.myArrayBuffer.getChannelData(0);
this.source = this.audioCtx.createBufferSource();
this.source.buffer = this.myArrayBuffer;
this.source.loop=true;
this.source.start();
this.updateWave();
}
get wave():string|Function{
return this.info.wave;
}
get freq():number{
return this.info.freq;
}
set wave(wave:string|Function){
this.info.wave=wave;
this.updateWave();
}
set freq(freq:number){
this.info.freq=freq;
this.updateWave();
}
updateWave():void{
const func=this.waveFunction();
for(let i = 0; i < this.buffer.length; i++){
this.buffer[i]=func(i/this.audioCtx.sampleRate,this.freq);
}
}
waveFunction():Function{
if(typeof this.wave === "function"){
return this.wave;
}
switch(this.wave){
case"sin":
return(t:number,freq:number)=>{
return Math.sin(t*Math.PI*2*freq);
};
case"triangle":
return(t:number,freq:number)=>{
return Math.abs((4*t*freq)%4-2)-1;
};
case"sawtooth":
return(t:number,freq:number)=>{
return((t*freq)%1)*2-1;
};
case"square":
return(t:number,freq:number)=>{
return(t*freq)%2<1?1:-1;
};
case"white":
return(_t:number,_freq:number)=>{
return Math.random()*2-1;
};
case"noise":
return(_t:number,_freq:number)=>{
return 0;
};
}
return new Function();
}
play():void{
if(this.playing){
return;
}
this.source.connect(this.gainNode);
this.playing=true;
}
stop():void{
if(this.playing){
this.source.disconnect();
this.playing=false;
}
}
static noises(noise:string):void{
switch(noise){
case"three":{
const voicy=new Voice("sin",800);
voicy.play();
setTimeout(_=>{
voicy.freq=1000;
},50);
setTimeout(_=>{
voicy.freq=1300;
},100);
setTimeout(_=>{
voicy.stop();
},150);
break;
}
case"zip":{
const voicy=new Voice((t:number,freq:number)=>{
return Math.sin(((t+2)**(Math.cos(t*4)))*Math.PI*2*freq);
},700);
voicy.play();
setTimeout(_=>{
voicy.stop();
},150);
break;
}
case"square":{
const voicy=new Voice("square",600,0.4);
voicy.play();
setTimeout(_=>{
voicy.freq=800;
},50);
setTimeout(_=>{
voicy.freq=1000;
},100);
setTimeout(_=>{
voicy.stop();
},150);
break;
}
case"beep":{
const voicy=new Voice("sin",800);
voicy.play();
setTimeout(_=>{
voicy.stop();
},50);
setTimeout(_=>{
voicy.play();
},100);
setTimeout(_=>{
voicy.stop();
},150);
break;
}
}
}
static get sounds(){
return["three","zip","square","beep"];
}
static setNotificationSound(sound:string){
const userinfos=getBulkInfo();
userinfos.preferences.notisound=sound;
localStorage.setItem("userinfos",JSON.stringify(userinfos));
}
static getNotificationSound(){
const userinfos=getBulkInfo();
return userinfos.preferences.notisound;
}
}
export{Voice};

File diff suppressed because it is too large Load diff

View file

@ -1,88 +0,0 @@
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][];
div:HTMLDivElement;
static setup(){
Contextmenu.currentmenu="";
document.addEventListener("click", event=>{
if(Contextmenu.currentmenu===""){
return;
}
if(!Contextmenu.currentmenu.contains(event.target as Node)){
Contextmenu.currentmenu.remove();
Contextmenu.currentmenu="";
}
});
}
constructor(name:string){
this.name=name;
this.buttons=[];
}
addbutton(text:string,onclick:(this:x,arg:y,e:MouseEvent)=>void,img:null|string=null,shown:(this:x,arg:y)=>boolean=_=>true,enabled:(this:x,arg:y)=>boolean=_=>true){
this.buttons.push([text,onclick,img,shown,enabled,"button"]);
return{};
}
addsubmenu(text:string,onclick:(this:x,arg:y,e:MouseEvent)=>void,img=null,shown:(this:x,arg:y)=>boolean=_=>true,enabled:(this:x,arg:y)=>boolean=_=>true){
this.buttons.push([text,onclick,img,shown,enabled,"submenu"]);
return{};
}
private makemenu(x:number,y:number,addinfo:x,other:y){
const div=document.createElement("div");
div.classList.add("contextmenu","flexttb");
let visibleButtons=0;
for(const thing of this.buttons){
if(!thing[3].bind(addinfo)(other))continue;
visibleButtons++;
const intext=document.createElement("button");
intext.disabled=!thing[4].bind(addinfo)(other);
intext.classList.add("contextbutton");
intext.textContent=thing[0];
console.log(thing);
if(thing[5]==="button"||thing[5]==="submenu"){
intext.onclick=thing[1].bind(addinfo,other);
}
div.appendChild(intext);
}
if(visibleButtons == 0)return;
if(Contextmenu.currentmenu!=""){
Contextmenu.currentmenu.remove();
}
div.style.top = y+"px";
div.style.left = x+"px";
document.body.appendChild(div);
Contextmenu.keepOnScreen(div);
console.log(div);
Contextmenu.currentmenu=div;
return this.div;
}
bindContextmenu(obj:HTMLElement,addinfo:x,other:y){
const func=event=>{
event.preventDefault();
event.stopImmediatePropagation();
this.makemenu(event.clientX,event.clientY,addinfo,other);
};
obj.addEventListener("contextmenu", func);
return func;
}
static keepOnScreen(obj:HTMLElement){
const html = document.documentElement.getBoundingClientRect();
const docheight=html.height;
const docwidth=html.width;
const box=obj.getBoundingClientRect();
console.log(box,docheight,docwidth);
if(box.right>docwidth){
console.log("test");
obj.style.left = docwidth-box.width+"px";
}
if(box.bottom>docheight){
obj.style.top = docheight-box.height+"px";
}
}
}
Contextmenu.setup();
export{Contextmenu};

View file

@ -1,278 +0,0 @@
type dialogjson=[
"hdiv",...dialogjson[]
]|[
"vdiv",...dialogjson[]
]|[
"img",string,[number,number]|undefined|["fit"]
]|[
"checkbox",string,boolean,(this:HTMLInputElement,e:Event)=>unknown
]|[
"button",string,string,(this:HTMLButtonElement,e:Event)=>unknown
]|[
"mdbox",string,string,(this:HTMLTextAreaElement,e:Event)=>unknown
]|[
"textbox",string,string,(this:HTMLInputElement,e:Event)=>unknown
]|[
"fileupload",string,(this:HTMLInputElement,e:Event)=>unknown
]|[
"text",string
]|[
"title",string
]|[
"radio",string,string[],(this:unknown,e:string)=>unknown,number
]|[
"html",HTMLElement
]|[
"select",string,string[],(this:HTMLSelectElement,e:Event)=>unknown,number
]|[
"tabs",[string,dialogjson][]
]
class Dialog{
layout:dialogjson;
onclose: Function;
onopen: Function;
html:HTMLDivElement;
background: HTMLDivElement;
constructor(layout:dialogjson,onclose=_=>{},onopen=_=>{}){
this.layout=layout;
this.onclose=onclose;
this.onopen=onopen;
const div=document.createElement("div");
div.appendChild(this.tohtml(layout));
this.html=div;
this.html.classList.add("centeritem");
if(!(layout[0]==="img")){
this.html.classList.add("nonimagecenter");
}
}
tohtml(array:dialogjson):HTMLElement{
switch(array[0]){
case"img":
const img=document.createElement("img");
img.src=array[1];
if(array[2]!=undefined){
if(array[2].length===2){
img.width=array[2][0];
img.height=array[2][1];
}else if(array[2][0]==="fit"){
img.classList.add("imgfit");
}
}
return img;
case"hdiv":
const hdiv=document.createElement("div");
hdiv.classList.add("flexltr")
for(const thing of array){
if(thing==="hdiv"){
continue;
}
hdiv.appendChild(this.tohtml(thing));
}
return hdiv;
case"vdiv":
const vdiv=document.createElement("div");
vdiv.classList.add("flexttb");
for(const thing of array){
if(thing==="vdiv"){
continue;
}
vdiv.appendChild(this.tohtml(thing));
}
return vdiv;
case"checkbox":
{
const div=document.createElement("div");
const checkbox = document.createElement("input");
div.appendChild(checkbox);
const label=document.createElement("span");
checkbox.checked=array[2];
label.textContent=array[1];
div.appendChild(label);
checkbox.addEventListener("change",array[3]);
checkbox.type = "checkbox";
return div;
}
case"button":
{
const div=document.createElement("div");
const input = document.createElement("button");
const label=document.createElement("span");
input.textContent=array[2];
label.textContent=array[1];
div.appendChild(label);
div.appendChild(input);
input.addEventListener("click",array[3]);
return div;
}
case"mdbox":
{
const div=document.createElement("div");
const input=document.createElement("textarea");
input.value=array[2];
const label=document.createElement("span");
label.textContent=array[1];
input.addEventListener("input",array[3]);
div.appendChild(label);
div.appendChild(document.createElement("br"));
div.appendChild(input);
return div;
}
case"textbox":
{
const div=document.createElement("div");
const input=document.createElement("input");
input.value=array[2];
input.type="text";
const label=document.createElement("span");
label.textContent=array[1];
console.log(array[3]);
input.addEventListener("input",array[3]);
div.appendChild(label);
div.appendChild(input);
return div;
}
case"fileupload":
{
const div=document.createElement("div");
const input=document.createElement("input");
input.type="file";
const label=document.createElement("span");
label.textContent=array[1];
div.appendChild(label);
div.appendChild(input);
input.addEventListener("change",array[2]);
console.log(array);
return div;
}
case"text":{
const span =document.createElement("span");
span.textContent=array[1];
return span;
}
case"title":{
const span =document.createElement("span");
span.classList.add("title");
span.textContent=array[1];
return span;
}
case"radio":{
const div=document.createElement("div");
const fieldset=document.createElement("fieldset");
fieldset.addEventListener("change",()=>{
let i=-1;
for(const thing of fieldset.children){
i++;
if(i===0){
continue;
}
const checkbox = thing.children[0].children[0] as HTMLInputElement;
if(checkbox.checked){
array[3](checkbox.value);
}
}
});
const legend=document.createElement("legend");
legend.textContent=array[1];
fieldset.appendChild(legend);
let i=0;
for(const thing of array[2]){
const div=document.createElement("div");
const input=document.createElement("input");
input.classList.add("radio");
input.type="radio";
input.name=array[1];
input.value=thing;
if(i===array[4]){
input.checked=true;
}
const label=document.createElement("label");
label.appendChild(input);
const span=document.createElement("span");
span.textContent=thing;
label.appendChild(span);
div.appendChild(label);
fieldset.appendChild(div);
i++;
}
div.appendChild(fieldset);
return div;
}
case"html":
return array[1];
case"select":{
const div=document.createElement("div");
const label=document.createElement("label");
const select=document.createElement("select");
label.textContent=array[1];
div.append(label);
div.appendChild(select);
for(const thing of array[2]){
const option = document.createElement("option");
option.textContent=thing;
select.appendChild(option);
}
select.selectedIndex=array[4];
select.addEventListener("change",array[3]);
return div;
}
case"tabs":{
const table=document.createElement("div");
table.classList.add("flexttb");
const tabs=document.createElement("div");
tabs.classList.add("flexltr")
tabs.classList.add("tabbed-head");
table.appendChild(tabs);
const content=document.createElement("div");
content.classList.add("tabbed-content");
table.appendChild(content);
let shown:HTMLElement|undefined;
for(const thing of array[1]){
const button=document.createElement("button");
button.textContent=thing[0];
tabs.appendChild(button);
const html=this.tohtml(thing[1]);
content.append(html);
if(!shown){
shown=html;
}else{
html.style.display="none";
}
button.addEventListener("click",_=>{
if(shown){
shown.style.display="none";
}
html.style.display="";
shown=html;
});
}
return table;
}
default:
console.error("can't find element:"+array[0]," full element:",array);
return document.createElement("span");
}
}
show(){
this.onopen();
console.log("fullscreen");
this.background=document.createElement("div");
this.background.classList.add("background");
document.body.appendChild(this.background);
document.body.appendChild(this.html);
this.background.onclick = _=>{
this.hide();
};
}
hide(){
document.body.removeChild(this.background);
document.body.removeChild(this.html);
}
}
export{Dialog};

View file

@ -1,290 +0,0 @@
import{Guild}from"./guild.js";
import{ Channel }from"./channel.js";
import{ Message }from"./message.js";
import{ Localuser }from"./localuser.js";
import{User}from"./user.js";
import{ channeljson, dirrectjson, memberjson, messagejson }from"./jsontypes.js";
import{ Permissions }from"./permissions.js";
import { SnowFlake } from "./snowflake.js";
import { Contextmenu } from "./contextmenu.js";
class Direct extends Guild{
declare channelids:{[key:string]:Group};
getUnixTime(): number {
throw new Error("Do not call this for Direct, it does not make sense");
}
constructor(json:dirrectjson[],owner:Localuser){
super(-1,owner,null);
this.message_notifications=0;
this.owner=owner;
if(!this.localuser){
console.error("Owner was not included, please fix");
}
this.headers=this.localuser.headers;
this.channels=[];
this.channelids={};
this.properties={};
this.roles=[];
this.roleids=new Map();
this.prevchannel=undefined;
this.properties.name="Direct Messages";
for(const thing of json){
const temp=new Group(thing,this);
this.channels.push(temp);
this.channelids[temp.id]=temp;
}
this.headchannels=this.channels;
}
createChannelpac(json){
const thischannel=new Group(json,this);
this.channelids[thischannel.id]=thischannel;
this.channels.push(thischannel);
this.sortchannels();
this.printServers();
return thischannel;
}
delChannel(json:channeljson){
const channel=this.channelids[json.id];
super.delChannel(json);
if(channel){
channel.del();
}
}
giveMember(_member:memberjson){
console.error("not a real guild, can't give member object");
}
getRole(ID:string){
return null;
}
hasRole(r:string){
return false;
}
isAdmin(){
return false;
}
unreaddms(){
for(const thing of this.channels){
(thing as Group).unreads();
}
}
}
const dmPermissions = new Permissions("0");
dmPermissions.setPermission("ADD_REACTIONS",1);
dmPermissions.setPermission("VIEW_CHANNEL",1);
dmPermissions.setPermission("SEND_MESSAGES",1);
dmPermissions.setPermission("EMBED_LINKS",1);
dmPermissions.setPermission("ATTACH_FILES",1);
dmPermissions.setPermission("READ_MESSAGE_HISTORY",1);
dmPermissions.setPermission("MENTION_EVERYONE",1);
dmPermissions.setPermission("USE_EXTERNAL_EMOJIS",1);
dmPermissions.setPermission("USE_APPLICATION_COMMANDS",1);
dmPermissions.setPermission("USE_EXTERNAL_STICKERS",1);
dmPermissions.setPermission("USE_EMBEDDED_ACTIVITIES",1);
dmPermissions.setPermission("USE_SOUNDBOARD",1);
dmPermissions.setPermission("USE_EXTERNAL_SOUNDS",1);
dmPermissions.setPermission("SEND_VOICE_MESSAGES",1);
dmPermissions.setPermission("SEND_POLLS",1);
dmPermissions.setPermission("USE_EXTERNAL_APPS",1);
dmPermissions.setPermission("CONNECT",1);
dmPermissions.setPermission("SPEAK",1);
dmPermissions.setPermission("STREAM",1);
dmPermissions.setPermission("USE_VAD",1);
class Group extends Channel{
user:User;
static contextmenu=new Contextmenu<Group,undefined>("channel menu");
static setupcontextmenu(){
this.contextmenu.addbutton("Copy DM id",function(this:Group){
navigator.clipboard.writeText(this.id);
});
this.contextmenu.addbutton("Mark as read",function(this:Group){
this.readbottom();
});
this.contextmenu.addbutton("Close DM",function(this:Group){
this.deleteChannel();
});
this.contextmenu.addbutton("Copy user ID",function(){
navigator.clipboard.writeText(this.user.id);
})
}
constructor(json:dirrectjson,owner:Direct){
super(-1,owner,json.id);
this.owner=owner;
this.headers=this.guild.headers;
this.name=json.recipients[0]?.username;
if(json.recipients[0]){
this.user=new User(json.recipients[0],this.localuser);
}else{
this.user=this.localuser.user;
}
this.name??=this.localuser.user.username;
this.parent_id=undefined;
this.parent=null;
this.children=[];
this.guild_id="@me";
this.permission_overwrites=new Map();
this.lastmessageid=json.last_message_id;
this.mentions=0;
this.setUpInfiniteScroller();
this.updatePosition();
}
updatePosition(){
if(this.lastmessageid){
this.position=SnowFlake.stringToUnixTime(this.lastmessageid);
}else{
this.position=0;
}
this.position=-Math.max(this.position,this.getUnixTime());
}
createguildHTML(){
const div=document.createElement("div");
Group.contextmenu.bindContextmenu(div,this,undefined);
this.html=new WeakRef(div);
div.classList.add("channeleffects");
const myhtml=document.createElement("span");
myhtml.textContent=this.name;
div.appendChild(this.user.buildpfp());
div.appendChild(myhtml);
div["myinfo"]=this;
div.onclick=_=>{
this.getHTML();
};
return div;
}
async getHTML(){
const id=++Channel.genid;
if(this.localuser.channelfocus){
this.localuser.channelfocus.infinite.delete();
}
if(this.guild!==this.localuser.lookingguild){
this.guild.loadGuild();
}
this.guild.prevchannel=this;
this.localuser.channelfocus=this;
const prom=this.infinite.delete();
history.pushState(null, "","/channels/"+this.guild_id+"/"+this.id);
this.localuser.pageTitle("@"+this.name);
(document.getElementById("channelTopic") as HTMLElement).setAttribute("hidden","");
const loading=document.getElementById("loadingdiv") as HTMLDivElement;
Channel.regenLoadingMessages();
loading.classList.add("loading");
this.rendertyping();
await this.putmessages();
await prom;
if(id!==Channel.genid){
return;
}
this.buildmessages();
(document.getElementById("typebox") as HTMLDivElement).contentEditable=""+true;
}
messageCreate(messagep:{d:messagejson}){
const messagez=new Message(messagep.d,this);
if(this.lastmessageid){
this.idToNext.set(this.lastmessageid,messagez.id);
this.idToPrev.set(messagez.id,this.lastmessageid);
}
this.lastmessageid=messagez.id;
if(messagez.author===this.localuser.user){
this.lastreadmessageid=messagez.id;
if(this.myhtml){
this.myhtml.classList.remove("cunread");
}
}else{
if(this.myhtml){
this.myhtml.classList.add("cunread");
}
}
this.unreads();
this.updatePosition();
this.infinite.addedBottom();
this.guild.sortchannels();
if(this.myhtml){
const parrent=this.myhtml.parentElement as HTMLElement;
parrent.prepend(this.myhtml);
}
if(this===this.localuser.channelfocus){
if(!this.infinitefocus){
this.tryfocusinfinate();
}
this.infinite.addedBottom();
}
this.unreads();
if(messagez.author===this.localuser.user){
return;
}
if(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.notify(messagez);
}
}
notititle(message){
return message.author.username;
}
readbottom(){
super.readbottom();
this.unreads();
}
all:WeakRef<HTMLElement>=new WeakRef(document.createElement("div"));
noti:WeakRef<HTMLElement>=new WeakRef(document.createElement("div"));
del(){
const all=this.all.deref();
if(all){
all.remove();
}
if(this.myhtml){
this.myhtml.remove();
}
}
unreads(){
const sentdms=document.getElementById("sentdms") as HTMLDivElement;//Need to change sometime
const current=this.all.deref();
if(this.hasunreads){
{
const noti=this.noti.deref();
if(noti){
noti.textContent=this.mentions+"";
return;
}
}
const div=document.createElement("div");
div.classList.add("servernoti");
const noti=document.createElement("div");
noti.classList.add("unread","notiunread","pinged");
noti.textContent=""+this.mentions;
this.noti=new WeakRef(noti);
div.append(noti);
const buildpfp=this.user.buildpfp();
this.all=new WeakRef(div);
buildpfp.classList.add("mentioned");
div.append(buildpfp);
sentdms.append(div);
div.onclick=_=>{
this.guild.loadGuild();
this.getHTML();
};
}else if(current){
current.remove();
}else{
}
}
isAdmin(): boolean{
return false;
}
hasPermission(name: string): boolean{
return dmPermissions.hasPermission(name);
}
}
export{Direct, Group};
Group.setupcontextmenu();

View file

@ -1,391 +0,0 @@
import{Dialog}from"./dialog.js";
import{Message}from"./message.js";
import{MarkDown}from"./markdown.js";
import{ embedjson,guildjson, invitejson }from"./jsontypes.js";
import { getapiurls, getInstances } from "./login.js";
import { Guild } from "./guild.js";
class Embed{
type:string;
owner:Message;
json:embedjson;
constructor(json:embedjson, owner:Message){
this.type=this.getType(json);
this.owner=owner;
this.json=json;
}
getType(json:embedjson){
const instances=getInstances();
if(instances&&json.type==="link"&&json.url&&URL.canParse(json.url)){
const Url=new URL(json.url);
for(const instance of instances){
if(instance.url&&URL.canParse(instance.url)){
const IUrl=new URL(instance.url);
const params=new URLSearchParams(Url.search);
let host:string;
if(params.has("instance")){
const url=params.get("instance") as string;
if(URL.canParse(url)){
host=new URL(url).host;
}else{
host=Url.host;
}
}else{
host=Url.host;
}
if(IUrl.host===host){
const code=Url.pathname.split("/")[Url.pathname.split("/").length-1];
json.invite={
url:instance.url,
code
}
return "invite";
}
}
}
}
return json.type||"rich";
}
generateHTML(){
switch(this.type){
case"rich":
return this.generateRich();
case"image":
return this.generateImage();
case"invite":
return this.generateInvite();
case"link":
return this.generateLink();
case "video":
case"article":
return this.generateArticle();
default:
console.warn(`unsupported embed type ${this.type}, please add support dev :3`,this.json);
return document.createElement("div");//prevent errors by giving blank div
}
}
get message(){
return this.owner;
}
get channel(){
return this.message.channel;
}
get guild(){
return this.channel.guild;
}
get localuser(){
return this.guild.localuser;
}
generateRich(){
const div=document.createElement("div");
if(this.json.color){
div.style.backgroundColor="#"+this.json.color.toString(16);
}
div.classList.add("embed-color");
const embed=document.createElement("div");
embed.classList.add("embed");
div.append(embed);
if(this.json.author){
const authorline=document.createElement("div");
if(this.json.author.icon_url){
const img=document.createElement("img");
img.classList.add("embedimg");
img.src=this.json.author.icon_url;
authorline.append(img);
}
const a=document.createElement("a");
a.textContent=this.json.author.name as string;
if(this.json.author.url){
MarkDown.safeLink(a,this.json.author.url);
}
a.classList.add("username");
authorline.append(a);
embed.append(authorline);
}
if(this.json.title){
const title=document.createElement("a");
title.append(new MarkDown(this.json.title,this.channel).makeHTML());
if(this.json.url){
MarkDown.safeLink(title,this.json.url);
}
title.classList.add("embedtitle");
embed.append(title);
}
if(this.json.description){
const p=document.createElement("p");
p.append(new MarkDown(this.json.description,this.channel).makeHTML());
embed.append(p);
}
embed.append(document.createElement("br"));
if(this.json.fields){
for(const thing of this.json.fields){
const div=document.createElement("div");
const b=document.createElement("b");
b.textContent=thing.name;
div.append(b);
const p=document.createElement("p");
p.append(new MarkDown(thing.value,this.channel).makeHTML());
p.classList.add("embedp");
div.append(p);
if(thing.inline){
div.classList.add("inline");
}
embed.append(div);
}
}
if(this.json.footer||this.json.timestamp){
const footer=document.createElement("div");
if(this.json?.footer?.icon_url){
const img=document.createElement("img");
img.src=this.json.footer.icon_url;
img.classList.add("embedicon");
footer.append(img);
}
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");
footer.append(span);
}
if(this.json?.timestamp){
const span=document.createElement("span");
span.textContent=new Date(this.json.timestamp).toLocaleString();
footer.append(span);
}
embed.append(footer);
}
return div;
}
generateImage(){
const img=document.createElement("img");
img.classList.add("messageimg");
img.onclick=function(){
const full=new Dialog(["img",img.src,["fit"]]);
full.show();
};
img.src=this.json.thumbnail.proxy_url;
if(this.json.thumbnail.width){
let scale=1;
const max=96*3;
scale=Math.max(scale,this.json.thumbnail.width/max);
scale=Math.max(scale,this.json.thumbnail.height/max);
this.json.thumbnail.width/=scale;
this.json.thumbnail.height/=scale;
}
img.style.width=this.json.thumbnail.width+"px";
img.style.height=this.json.thumbnail.height+"px";
console.log(this.json,"Image fix");
return img;
}
generateLink(){
const table=document.createElement("table");
table.classList.add("embed","linkembed");
const trtop=document.createElement("tr");
table.append(trtop);
if(this.json.url&&this.json.title){
const td=document.createElement("td");
const a=document.createElement("a");
MarkDown.safeLink(a,this.json.url);
a.textContent=this.json.title;
td.append(a);
trtop.append(td);
}
{
const td=document.createElement("td");
const img=document.createElement("img");
if(this.json.thumbnail){
img.classList.add("embedimg");
img.onclick=function(){
const full=new Dialog(["img",img.src,["fit"]]);
full.show();
};
img.src=this.json.thumbnail.proxy_url;
td.append(img);
}
trtop.append(td);
}
const bottomtr=document.createElement("tr");
const td=document.createElement("td");
if(this.json.description){
const span=document.createElement("span");
span.textContent=this.json.description;
td.append(span);
}
bottomtr.append(td);
table.append(bottomtr);
return table;
}
invcache:[invitejson,{cdn:string,api:string}]|undefined;
generateInvite(){
if(this.invcache&&(!this.json.invite||!this.localuser)){
return this.generateLink();
}
const div=document.createElement("div");
div.classList.add("embed","inviteEmbed","flexttb");
const json1=this.json.invite;
(async ()=>{
let json:invitejson;
let info:{cdn:string,api:string};
if(!this.invcache){
if(!json1){
div.append(this.generateLink());
return;
}
const tempinfo=await getapiurls(json1.url);;
if(!tempinfo){
div.append(this.generateLink());
return;
}
info=tempinfo;
const res=await fetch(info.api+"/invites/"+json1.code)
if(!res.ok){
div.append(this.generateLink());
}
json=await res.json() as invitejson;
this.invcache=[json,info];
}else{
[json,info]=this.invcache;
}
if(!json){
div.append(this.generateLink());
return;
}
if(json.guild.banner){
const banner=document.createElement("img");
banner.src=this.localuser.info.cdn+"/icons/"+json.guild.id+"/"+json.guild.banner+".png?size=256";
banner.classList.add("banner");
div.append(banner);
}
const guild:invitejson["guild"] & {info?:{cdn:string}}=json.guild;
guild.info=info;
const icon=Guild.generateGuildIcon(guild as invitejson["guild"] & {info:{cdn:string}})
const iconrow=document.createElement("div");
iconrow.classList.add("flexltr","flexstart");
iconrow.append(icon);
{
const guildinfo=document.createElement("div");
guildinfo.classList.add("flexttb","invguildinfo");
const name=document.createElement("b");
name.textContent=guild.name;
guildinfo.append(name);
const members=document.createElement("span");
members.innerText="#"+json.channel.name+" • Members: "+guild.member_count
guildinfo.append(members);
members.classList.add("subtext");
iconrow.append(guildinfo);
}
div.append(iconrow);
const h2=document.createElement("h2");
h2.textContent=`You've been invited by ${json.inviter.username}`;
div.append(h2);
const button=document.createElement("button");
button.textContent="Accept";
if(this.localuser.info.api.startsWith(info.api)){
if(this.localuser.guildids.has(guild.id)){
button.textContent="Already joined";
button.disabled=true;
}
}
button.classList.add("acceptinvbutton");
div.append(button);
button.onclick=_=>{
if(this.localuser.info.api.startsWith(info.api)){
fetch(this.localuser.info.api+"/invites/"+json.code,{
method: "POST",
headers: this.localuser.headers,
}).then(r=>r.json()).then(_=>{
if(_.message){
alert(_.message);
}
});
}else{
if(this.json.invite){
const params=new URLSearchParams("");
params.set("instance",this.json.invite.url);
const encoded=params.toString();
const url=`${location.origin}/invite/${this.json.invite.code}?${encoded}`;
window.open(url,"_blank");
}
}
}
})()
return div;
}
generateArticle(){
const colordiv=document.createElement("div");
colordiv.style.backgroundColor="#000000";
colordiv.classList.add("embed-color");
const div=document.createElement("div");
div.classList.add("embed");
if(this.json.provider){
const provider=document.createElement("p");
provider.classList.add("provider");
provider.textContent=this.json.provider.name;
div.append(provider);
}
const a=document.createElement("a");
if(this.json.url&&this.json.url){
MarkDown.safeLink(a,this.json.url);
a.textContent=this.json.url;
div.append(a);
}
if(this.json.description){
const description=document.createElement("p");
description.textContent=this.json.description;
div.append(description);
}
if(this.json.thumbnail){
const img=document.createElement("img");
if(this.json.thumbnail.width&&this.json.thumbnail.width){
let scale=1;
const inch=96;
scale=Math.max(scale,this.json.thumbnail.width/inch/4);
scale=Math.max(scale,this.json.thumbnail.height/inch/3);
this.json.thumbnail.width/=scale;
this.json.thumbnail.height/=scale;
img.style.width=this.json.thumbnail.width+"px";
img.style.height=this.json.thumbnail.height+"px";
}
img.classList.add("bigembedimg");
if(this.json.video){
img.onclick=async ()=>{
if(this.json.video){
img.remove();
const iframe=document.createElement("iframe");
iframe.src=this.json.video.url+"?autoplay=1";
if(this.json.thumbnail.width&&this.json.thumbnail.width){
iframe.style.width=this.json.thumbnail.width+"px";
iframe.style.height=this.json.thumbnail.height+"px";
}
div.append(iframe);
}
};
}else{
img.onclick=async ()=>{
const full=new Dialog(["img",img.src,["fit"]]);
full.show();
};
}
img.src=this.json.thumbnail.proxy_url||this.json.thumbnail.url;
div.append(img);
}
colordiv.append(div);
return colordiv;
}
}
export{Embed};

View file

@ -1,230 +0,0 @@
import{ Contextmenu }from"./contextmenu.js";
import{ Guild }from"./guild.js";
import{ emojijson }from"./jsontypes.js";
import{ Localuser }from"./localuser.js";
class Emoji{
static emojis:{
name:string,
emojis:{
name:string,
emoji:string,
}[]
}[];
name:string;
id:string;
animated:boolean;
owner:Guild|Localuser;
get guild(){
if(this.owner instanceof Guild){
return this.owner;
}
}
get localuser(){
if(this.owner instanceof Guild){
return this.owner.localuser;
}else{
return this.owner;
}
}
get info(){
return this.owner.info;
}
constructor(json:{name:string,id:string,animated:boolean},owner:Guild|Localuser){
this.name=json.name;
this.id=json.id;
this.animated=json.animated;
this.owner=owner;
}
getHTML(bigemoji:boolean=false){
const emojiElem=document.createElement("img");
emojiElem.classList.add("md-emoji");
emojiElem.classList.add(bigemoji ? "bigemoji" : "smallemoji");
emojiElem.crossOrigin="anonymous";
emojiElem.src=this.info.cdn + "/emojis/" + this.id + "." + (this.animated ? "gif" : "png") + "?size=32";
emojiElem.alt=this.name;
emojiElem.loading="lazy";
return emojiElem;
}
static decodeEmojiList(buffer:ArrayBuffer){
const view = new DataView(buffer, 0);
let i=0;
function read16(){
const int=view.getUint16(i);
i+=2;
return int;
}
function read8(){
const int=view.getUint8(i);
i+=1;
return int;
}
function readString8(){
return readStringNo(read8());
}
function readString16(){
return readStringNo(read16());
}
function readStringNo(length:number){
const array=new Uint8Array(length);
for(let i=0;i<length;i++){
array[i]=read8();
}
//console.log(array);
return new TextDecoder("utf-8").decode(array.buffer);
}
const build:{name:string,emojis:{name:string,emoji:string}[]}[]=[];
let cats=read16();
for(;cats!==0;cats--){
const name=readString16();
const emojis:{
name:string,
skin_tone_support:boolean,
emoji:string
}[]=[];
let emojinumber=read16();
for(;emojinumber!==0;emojinumber--){
//console.log(emojis);
const name=readString8();
const len=read8();
const skin_tone_support=len>127;
const emoji=readStringNo(len-(Number(skin_tone_support)*128));
emojis.push({
name,
skin_tone_support,
emoji
});
}
build.push({
name,
emojis
});
}
this.emojis=build;
console.log(build);
}
static grabEmoji(){
fetch("/emoji.bin").then(e=>{
return e.arrayBuffer();
}).then(e=>{
Emoji.decodeEmojiList(e);
});
}
static async emojiPicker(x:number,y:number, localuser:Localuser):Promise<Emoji|string>{
let res:(r:Emoji|string)=>void;
const promise:Promise<Emoji|string>=new Promise(r=>{
res=r;
});
const menu=document.createElement("div");
menu.classList.add("flexttb", "emojiPicker");
menu.style.top=y+"px";
menu.style.left=x+"px";
const title=document.createElement("h2");
title.textContent=Emoji.emojis[0].name;
title.classList.add("emojiTitle");
menu.append(title);
const selection=document.createElement("div");
selection.classList.add("flexltr","dontshrink","emojirow");
const body=document.createElement("div");
body.classList.add("emojiBody");
let isFirst = true;
localuser.guilds.filter(guild=>guild.id != "@me" && guild.emojis.length > 0).forEach(guild=>{
const select = document.createElement("div");
select.classList.add("emojiSelect");
if(guild.properties.icon){
const img = document.createElement("img");
img.classList.add("pfp", "servericon", "emoji-server");
img.crossOrigin = "anonymous";
img.src = localuser.info.cdn + "/icons/" + guild.properties.id + "/" + guild.properties.icon + ".png?size=48";
img.alt = "Server: " + guild.properties.name;
select.appendChild(img);
}else{
const div = document.createElement("span");
div.textContent = guild.properties.name.replace(/'s /g, " ").replace(/\w+/g, word=>word[0]).replace(/\s/g, "");
select.append(div);
}
selection.append(select);
const clickEvent = ()=>{
title.textContent = guild.properties.name;
body.innerHTML = "";
for(const emojit of guild.emojis){
const emojiElem = document.createElement("div");
emojiElem.classList.add("emojiSelect");
const emojiClass = new Emoji({
id: emojit.id as string,
name: emojit.name,
animated: emojit.animated as boolean
},localuser);
emojiElem.append(emojiClass.getHTML());
body.append(emojiElem);
emojiElem.addEventListener("click", ()=>{
res(emojiClass);
if(Contextmenu.currentmenu!==""){
Contextmenu.currentmenu.remove();
}
});
}
};
select.addEventListener("click", clickEvent);
if(isFirst){
clickEvent();
isFirst = false;
}
});
setTimeout(()=>{
if(Contextmenu.currentmenu!=""){
Contextmenu.currentmenu.remove();
}
document.body.append(menu);
Contextmenu.currentmenu=menu;
Contextmenu.keepOnScreen(menu);
},10);
let i=0;
for(const thing of Emoji.emojis){
const select=document.createElement("div");
select.textContent=thing.emojis[0].emoji;
select.classList.add("emojiSelect");
selection.append(select);
const clickEvent=()=>{
title.textContent=thing.name;
body.innerHTML="";
for(const emojit of thing.emojis){
const emoji=document.createElement("div");
emoji.classList.add("emojiSelect");
emoji.textContent=emojit.emoji;
body.append(emoji);
emoji.onclick=_=>{
res(emojit.emoji);
if(Contextmenu.currentmenu!==""){
Contextmenu.currentmenu.remove();
}
};
}
};
select.onclick=clickEvent;
if(i===0){
clickEvent();
}
i++;
}
menu.append(selection);
menu.append(body);
return promise;
}
}
Emoji.grabEmoji();
export{Emoji};

View file

@ -1,145 +0,0 @@
import{ Message }from"./message.js";
import{ Dialog }from"./dialog.js";
import{ filejson }from"./jsontypes.js";
class File{
owner:Message|null;
id:string;
filename:string;
content_type:string;
width:number|undefined;
height:number|undefined;
proxy_url:string|undefined;
url:string;
size:number;
constructor(fileJSON:filejson,owner:Message|null){
this.owner=owner;
this.id=fileJSON.id;
this.filename=fileJSON.filename;
this.content_type=fileJSON.content_type;
this.width=fileJSON.width;
this.height=fileJSON.height;
this.url=fileJSON.url;
this.proxy_url=fileJSON.proxy_url;
this.content_type=fileJSON.content_type;
this.size=fileJSON.size;
}
getHTML(temp:boolean=false):HTMLElement{
const src=this.proxy_url||this.url;
if(this.width&&this.height){
let scale=1;
const max=96*3;
scale=Math.max(scale,this.width/max);
scale=Math.max(scale,this.height/max);
this.width/=scale;
this.height/=scale;
}
if(this.content_type.startsWith("image/")){
const div=document.createElement("div");
const img=document.createElement("img");
img.classList.add("messageimg");
div.classList.add("messageimgdiv");
img.onclick=function(){
const full=new Dialog(["img",img.src,["fit"]]);
full.show();
};
img.src=src;
div.append(img);
if(this.width){
div.style.width=this.width+"px";
div.style.height=this.height+"px";
}
console.log(img);
console.log(this.width,this.height);
return div;
}else if(this.content_type.startsWith("video/")){
const video=document.createElement("video");
const source=document.createElement("source");
source.src=src;
video.append(source);
source.type=this.content_type;
video.controls=!temp;
if(this.width&&this.height){
video.width=this.width;
video.height=this.height;
}
return video;
}else if(this.content_type.startsWith("audio/")){
const audio=document.createElement("audio");
const source=document.createElement("source");
source.src=src;
audio.append(source);
source.type=this.content_type;
audio.controls=!temp;
return audio;
}else{
return this.createunknown();
}
}
upHTML(files:Blob[],file:globalThis.File):HTMLElement{
const div=document.createElement("div");
const contained=this.getHTML(true);
div.classList.add("containedFile");
div.append(contained);
const controls=document.createElement("div");
const garbage=document.createElement("button");
garbage.textContent="🗑";
garbage.onclick=_=>{
div.remove();
files.splice(files.indexOf(file),1);
};
controls.classList.add("controls");
div.append(controls);
controls.append(garbage);
return div;
}
static initFromBlob(file:globalThis.File){
return new File({
filename: file.name,
size: file.size,
id: "null",
content_type: file.type,
width: undefined,
height: undefined,
url: URL.createObjectURL(file),
proxy_url: undefined
},null);
}
createunknown():HTMLElement{
console.log("🗎");
const src=this.proxy_url||this.url;
const div=document.createElement("table");
div.classList.add("unknownfile");
const nametr=document.createElement("tr");
div.append(nametr);
const fileicon=document.createElement("td");
nametr.append(fileicon);
fileicon.append("🗎");
fileicon.classList.add("fileicon");
fileicon.rowSpan=2;
const nametd=document.createElement("td");
if(src){
const a=document.createElement("a");
a.href=src;
a.textContent=this.filename;
nametd.append(a);
}else{
nametd.textContent=this.filename;
}
nametd.classList.add("filename");
nametr.append(nametd);
const sizetr=document.createElement("tr");
const size=document.createElement("td");
sizetr.append(size);
size.textContent="Size:"+File.filesizehuman(this.size);
size.classList.add("filesize");
div.appendChild(sizetr);
return div;
}
static filesizehuman(fsize:number){
const i = fsize == 0 ? 0 : Math.floor(Math.log(fsize) / Math.log(1024));
return Number((fsize / Math.pow(1024, i)).toFixed(2)) * 1 + " " + ["Bytes", "Kilobytes", "Megabytes", "Gigabytes", "Terabytes"][i];
}
}
export{File};

View file

@ -1,617 +0,0 @@
import{ Channel }from"./channel.js";
import{ Localuser }from"./localuser.js";
import{Contextmenu}from"./contextmenu.js";
import{Role,RoleList}from"./role.js";
import{Dialog}from"./dialog.js";
import{Member}from"./member.js";
import{Settings}from"./settings.js";
import{Permissions}from"./permissions.js";
import{ SnowFlake }from"./snowflake.js";
import{ channeljson, guildjson, emojijson, memberjson, invitejson }from"./jsontypes.js";
import{ User }from"./user.js";
class Guild extends SnowFlake{
owner:Localuser;
headers:Localuser["headers"];
channels:Channel[];
properties:guildjson["properties"];
member_count:number;
roles:Role[];
roleids:Map<string,Role>;
prevchannel:Channel|undefined;
message_notifications:number;
headchannels:Channel[];
position:number;
parent_id:string;
member:Member;
html:HTMLElement;
emojis:emojijson[];
large:boolean;
static contextmenu=new Contextmenu<Guild,undefined>("guild menu");
static setupcontextmenu(){
Guild.contextmenu.addbutton("Copy Guild id",function(this:Guild){
navigator.clipboard.writeText(this.id);
});
Guild.contextmenu.addbutton("Mark as read",function(this:Guild){
this.markAsRead();
});
Guild.contextmenu.addbutton("Notifications",function(this:Guild){
this.setnotifcation();
});
Guild.contextmenu.addbutton("Leave guild",function(this:Guild){
this.confirmleave();
},null,function(_){
return this.properties.owner_id!==this.member.user.id;
});
Guild.contextmenu.addbutton("Delete guild",function(this:Guild){
this.confirmDelete();
},null,function(_){
return this.properties.owner_id===this.member.user.id;
});
Guild.contextmenu.addbutton("Create invite",function(this:Guild){
},null,_=>true,_=>false);
Guild.contextmenu.addbutton("Settings",function(this:Guild){
this.generateSettings();
});
/* -----things left for later-----
guild.contextmenu.addbutton("Leave Guild",function(){
console.log(this)
this.deleteChannel();
},null,_=>{return thisuser.isAdmin()})
guild.contextmenu.addbutton("Mute Guild",function(){
editchannelf(this);
},null,_=>{return thisuser.isAdmin()})
*/
}
generateSettings(){
const settings=new Settings("Settings for "+this.properties.name);
{
const overview=settings.addButton("Overview");
const form=overview.addForm("",_=>{},{
headers:this.headers,
traditionalSubmit:true,
fetchURL:this.info.api+"/guilds/"+this.id,
method:"PATCH"
})
form.addTextInput("Name:","name",{initText:this.properties.name});
form.addMDInput("Description:","description",{initText:this.properties.description});
form.addFileInput("Banner:","banner",{clear:true});
form.addFileInput("Icon:","icon",{clear:true});
let region=this.properties.region;
if(!region){
region="";
}
form.addTextInput("Region:","region",{initText:region});
}
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)));
settings.show();
}
constructor(json:guildjson|-1,owner:Localuser,member:memberjson|User|null){
if(json===-1||member===null){
super("@me");
return;
}
if(json.stickers.length){
console.log(json.stickers,":3");
}
super(json.id);
this.large=json.large;
this.member_count=json.member_count;
this.emojis = json.emojis;
this.owner=owner;
this.headers=this.owner.headers;
this.channels=[];
this.properties=json.properties;
this.roles=[];
this.roleids=new Map();
this.message_notifications=0;
for(const roley of json.roles){
const roleh=new Role(roley,this);
this.roles.push(roleh);
this.roleids.set(roleh.id,roleh);
}
if(member instanceof User){
Member.resolveMember(member,this).then(_=>{
if(_){
this.member=_;
}else{
console.error("Member was unable to resolve");
}
});
}else{
Member.new(member,this).then(_=>{
if(_){
this.member=_;
}
});
}
this.perminfo??={channels:{}};
for(const thing of json.channels){
const temp=new Channel(thing,this);
this.channels.push(temp);
this.localuser.channelids.set(temp.id,temp);
}
this.headchannels=[];
for(const thing of this.channels){
const parent=thing.resolveparent(this);
if(!parent){
this.headchannels.push(thing);
}
}
this.prevchannel=this.localuser.channelids.get(this.perminfo.prevchannel);
}
get perminfo(){
return this.localuser.perminfo.guilds[this.id];
}
set perminfo(e){
this.localuser.perminfo.guilds[this.id]=e;
}
notisetting(settings){
this.message_notifications=settings.message_notifications;
}
setnotifcation(){
let noti=this.message_notifications;
const notiselect=new Dialog(
["vdiv",
["radio","select notifications type",
["all","only mentions","none"],
function(e:"all"|"only mentions"|"none"){
noti=["all","only mentions","none"].indexOf(e);
},
noti
],
["button","","submit",_=>{
//
fetch(this.info.api+`/users/@me/guilds/${this.id}/settings/`,{
method: "PATCH",
headers: this.headers,
body: JSON.stringify({
message_notifications: noti
})
});
this.message_notifications=noti;
}]
]);
notiselect.show();
}
confirmleave(){
const full= new Dialog([
"vdiv",
["title",
"Are you sure you want to leave?"
],
["hdiv",
["button",
"",
"Yes, I'm sure",
_=>{
this.leave().then(_=>{
full.hide();
});
}
],
["button",
"",
"Nevermind",
_=>{
full.hide();
}
]
]
]);
full.show();
}
async leave(){
return fetch(this.info.api+"/users/@me/guilds/"+this.id,{
method: "DELETE",
headers: this.headers
});
}
printServers(){
let build="";
for(const thing of this.headchannels){
build+=(thing.name+":"+thing.position)+"\n";
for(const thingy of thing.children){
build+=(" "+thingy.name+":"+thingy.position)+"\n";
}
}
console.log(build);
}
calculateReorder(){
let position=-1;
const build:{id:string,position:number|undefined,parent_id:string|undefined}[]=[];
for(const thing of this.headchannels){
const thisthing:{id:string,position:number|undefined,parent_id:string|undefined}={id: thing.id,position: undefined,parent_id: undefined};
if(thing.position<=position){
thing.position=(thisthing.position=position+1);
}
position=thing.position;
console.log(position);
if(thing.move_id&&thing.move_id!==thing.parent_id){
thing.parent_id=thing.move_id;
thisthing.parent_id=thing.parent?.id;
thing.move_id=undefined;
}
if(thisthing.position||thisthing.parent_id){
build.push(thisthing);
}
if(thing.children.length>0){
const things=thing.calculateReorder();
for(const thing of things){
build.push(thing);
}
}
}
console.log(build);
this.printServers();
if(build.length===0){
return;
}
const serverbug=false;
if(serverbug){
for(const thing of build){
console.log(build,thing);
fetch(this.info.api+"/guilds/"+this.id+"/channels",{
method: "PATCH",
headers: this.headers,
body: JSON.stringify([thing])
});
}
}else{
fetch(this.info.api+"/guilds/"+this.id+"/channels",{
method: "PATCH",
headers: this.headers,
body: JSON.stringify(build)
});
}
}
get localuser(){
return this.owner;
}
get info(){
return this.owner.info;
}
sortchannels(){
this.headchannels.sort((a,b)=>{
return a.position-b.position;
});
}
static generateGuildIcon(guild:Guild|(invitejson["guild"] & {info:{cdn:string}})){
const divy=document.createElement("div");
divy.classList.add("servernoti");
const noti=document.createElement("div");
noti.classList.add("unread");
divy.append(noti);
if(guild instanceof Guild){
guild.localuser.guildhtml.set(guild.id,divy);
}
let icon:string|null
if(guild instanceof Guild){
icon=guild.properties.icon;
}else{
icon=guild.icon;
}
if(icon!==null){
const img=document.createElement("img");
img.classList.add("pfp","servericon");
img.src=guild.info.cdn+"/icons/"+guild.id+"/"+icon+".png";
divy.appendChild(img);
if(guild instanceof Guild){
img.onclick=()=>{
console.log(guild.loadGuild);
guild.loadGuild();
guild.loadChannel();
};
Guild.contextmenu.bindContextmenu(img,guild,undefined);
}
}else{
const div=document.createElement("div");
let name:string
if(guild instanceof Guild){
name=guild.properties.name;
}else{
name=guild.name;
}
const build=name.replace(/'s /g, " ").replace(/\w+/g, word=>word[0]).replace(/\s/g, "");
div.textContent=build;
div.classList.add("blankserver","servericon");
divy.appendChild(div);
if(guild instanceof Guild){
div.onclick=()=>{
guild.loadGuild();
guild.loadChannel();
};
Guild.contextmenu.bindContextmenu(div,guild,undefined);
}
}
return divy;
}
generateGuildIcon(){
return Guild.generateGuildIcon(this);
}
confirmDelete(){
let confirmname="";
const full= new Dialog([
"vdiv",
["title",
"Are you sure you want to delete "+this.properties.name+"?"
],
["textbox",
"Name of server:",
"",
function(this:HTMLInputElement){
confirmname=this.value;
}
],
["hdiv",
["button",
"",
"Yes, I'm sure",
_=>{
console.log(confirmname);
if(confirmname!==this.properties.name){
return;
}
this.delete().then(_=>{
full.hide();
});
}
],
["button",
"",
"Nevermind",
_=>{
full.hide();
}
]
]
]);
full.show();
}
async delete(){
return fetch(this.info.api+"/guilds/"+this.id+"/delete",{
method: "POST",
headers: this.headers,
});
}
unreads(html?:HTMLElement|undefined){
if(html){
this.html=html;
}else{
html=this.html;
}
let read=true;
for(const thing of this.channels){
if(thing.hasunreads){
console.log(thing);
read=false;
break;
}
}
if(!html){
return;
}
if(read){
html.children[0].classList.remove("notiunread");
}else{
html.children[0].classList.add("notiunread");
}
}
getHTML(){
//this.printServers();
this.sortchannels();
this.printServers();
const build=document.createElement("div");
for(const thing of this.headchannels){
build.appendChild(thing.createguildHTML(this.isAdmin()));
}
return build;
}
isAdmin(){
return this.member.isAdmin();
}
async markAsRead(){
const build:{read_states:{channel_id:string,message_id:string|null|undefined,read_state_type:number}[]}={read_states: []};
for(const thing of this.channels){
if(thing.hasunreads){
build.read_states.push({channel_id: thing.id,message_id: thing.lastmessageid,read_state_type: 0});
thing.lastreadmessageid=thing.lastmessageid;
if(!thing.myhtml)continue;
thing.myhtml.classList.remove("cunread");
}
}
this.unreads();
fetch(this.info.api+"/read-states/ack-bulk",{
method: "POST",
headers: this.headers,
body: JSON.stringify(build)
});
}
hasRole(r:Role|string){
console.log("this should run");
if(r instanceof Role){
r=r.id;
}
return this.member.hasRole(r);
}
loadChannel(ID?:string|undefined){
if(ID){
const channel=this.localuser.channelids.get(ID);
if(channel){
channel.getHTML();
return;
}
}
if(this.prevchannel){
console.log(this.prevchannel);
this.prevchannel.getHTML();
return;
}
for(const thing of this.channels){
if(thing.children.length===0){
thing.getHTML();
return;
}
}
}
loadGuild(){
this.localuser.loadGuild(this.id);
}
updateChannel(json:channeljson){
const channel=this.localuser.channelids.get(json.id);
if(channel){
channel.updateChannel(json);
this.headchannels=[];
for(const thing of this.channels){
thing.children=[];
}
this.headchannels=[];
for(const thing of this.channels){
const parent=thing.resolveparent(this);
if(!parent){
this.headchannels.push(thing);
}
}
this.printServers();
}
}
createChannelpac(json:channeljson){
const thischannel=new Channel(json,this);
this.localuser.channelids.set(json.id,thischannel);
this.channels.push(thischannel);
thischannel.resolveparent(this);
if(!thischannel.parent){
this.headchannels.push(thischannel);
}
this.calculateReorder();
this.printServers();
return thischannel;
}
createchannels(func=this.createChannel){
let name="";
let category=0;
const channelselect=new Dialog(
["vdiv",
["radio","select channel type",
["voice","text","announcement"],
function(e){
console.log(e);
category={text: 0,voice: 2,announcement: 5,category: 4}[e];
},
1
],
["textbox","Name of channel","",function(this:HTMLInputElement){
name=this.value;
}],
["button","","submit",function(){
console.log(name,category);
func(name,category);
channelselect.hide();
}]
]);
channelselect.show();
}
createcategory(){
let name="";
const category=4;
const channelselect=new Dialog(
["vdiv",
["textbox","Name of category","",function(this:HTMLInputElement){
name=this.value;
}],
["button","","submit",()=>{
console.log(name,category);
this.createChannel(name,category);
channelselect.hide();
}]
]);
channelselect.show();
}
delChannel(json:channeljson){
const channel=this.localuser.channelids.get(json.id);
this.localuser.channelids.delete(json.id);
if(!channel) return;
this.channels.splice(this.channels.indexOf(channel),1);
const indexy=this.headchannels.indexOf(channel);
if(indexy!==-1){
this.headchannels.splice(indexy,1);
}
/*
const build=[];
for(const thing of this.channels){
console.log(thing.id);
if(thing!==channel){
build.push(thing)
}else{
console.log("fail");
if(thing.parent){
thing.parent.delChannel(json);
}
}
}
this.channels=build;
*/
this.printServers();
}
createChannel(name:string,type:number){
fetch(this.info.api+"/guilds/"+this.id+"/channels",{
method: "POST",
headers: this.headers,
body: JSON.stringify({name, type})
});
}
async createRole(name:string){
const fetched=await fetch(this.info.api+"/guilds/"+this.id+"roles",{
method: "POST",
headers: this.headers,
body: JSON.stringify({
name,
color: 0,
permissions: "0"
})
});
const json=await fetched.json();
const role=new Role(json,this);
this.roleids.set(role.id,role);
this.roles.push(role);
return role;
}
async updateRolePermissions(id:string,perms:Permissions){
const role=this.roleids[id];
role.permissions.allow=perms.allow;
role.permissions.deny=perms.deny;
await fetch(this.info.api+"/guilds/"+this.id+"/roles/"+role.id,{
method: "PATCH",
headers: this.headers,
body: JSON.stringify({
color: role.color,
hoist: role.hoist,
icon: role.icon,
mentionable: role.mentionable,
name: role.name,
permissions: role.permissions.allow.toString(),
unicode_emoji: role.unicode_emoji,
})
});
}
}
Guild.setupcontextmenu();
export{ Guild };

View file

@ -1,64 +0,0 @@
import{mobile}from"./login.js";
console.log(mobile);
const serverbox=document.getElementById("instancebox") as HTMLDivElement;
fetch("/instances.json").then(_=>_.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}}[])=>{
console.warn(json);
for(const instance of json){
if(instance.display===false){
continue;
}
const div=document.createElement("div");
div.classList.add("flexltr","instance");
if(instance.image){
const img=document.createElement("img");
img.src=instance.image;
div.append(img);
}
const statbox=document.createElement("div");
statbox.classList.add("flexttb");
{
const textbox=document.createElement("div");
textbox.classList.add("flexttb","instatancetextbox");
const title=document.createElement("h2");
title.innerText=instance.name;
if(instance.online!==undefined){
const status=document.createElement("span");
status.innerText=instance.online?"Online":"Offline";
status.classList.add("instanceStatus");
title.append(status);
}
textbox.append(title);
if(instance.description||instance.descriptionLong){
const p=document.createElement("p");
if(instance.descriptionLong){
p.innerText=instance.descriptionLong;
}else if(instance.description){
p.innerText=instance.description;
}
textbox.append(p);
}
statbox.append(textbox);
}
if(instance.uptime){
const stats=document.createElement("div");
stats.classList.add("flexltr");
const span=document.createElement("span");
span.innerText=`Uptime: All time: ${Math.round(instance.uptime.alltime*100)}% This week: ${Math.round(instance.uptime.weektime*100)}% Today: ${Math.round(instance.uptime.daytime*100)}%`;
stats.append(span);
statbox.append(stats);
}
div.append(statbox);
div.onclick=_=>{
if(instance.online){
window.location.href="/register.html?instance="+encodeURI(instance.name);
}else{
alert("Instance is offline, can't connect");
}
};
serverbox.append(div);
}
});

View file

@ -1,229 +0,0 @@
import{ Localuser }from"./localuser.js";
import{Contextmenu}from"./contextmenu.js";
import{mobile, getBulkUsers,setTheme, Specialuser}from"./login.js";
import{ MarkDown }from"./markdown.js";
import{ Message }from"./message.js";
import{ File }from"./file.js";
(async ()=>{
async function waitforload(){
let res;
new Promise(r=>{
res=r;
});
document.addEventListener("DOMContentLoaded", ()=>{
res();
});
await res;
}
await waitforload();
const users=getBulkUsers();
if(!users.currentuser){
window.location.href = "/login.html";
}
function showAccountSwitcher(){
const table=document.createElement("div");
for(const thing of Object.values(users.users)){
const specialuser=thing as Specialuser;
console.log(specialuser.pfpsrc);
const userinfo=document.createElement("div");
userinfo.classList.add("flexltr","switchtable");
const pfp=document.createElement("img");
userinfo.append(pfp);
const user=document.createElement("div");
userinfo.append(user);
user.append(specialuser.username);
user.append(document.createElement("br"));
const span=document.createElement("span");
span.textContent=specialuser.serverurls.wellknown.replace("https://","").replace("http://","");
user.append(span);
user.classList.add("userinfo");
span.classList.add("serverURL");
pfp.src=specialuser.pfpsrc;
pfp.classList.add("pfp");
table.append(userinfo);
userinfo.addEventListener("click",_=>{
thisuser.unload();
thisuser.swapped=true;
const loading=document.getElementById("loading") as HTMLDivElement;
loading.classList.remove("doneloading");
loading.classList.add("loading");
thisuser=new Localuser(specialuser);
users.currentuser=specialuser.uid;
localStorage.setItem("userinfos",JSON.stringify(users));
thisuser.initwebsocket().then(_=>{
thisuser.loaduser();
thisuser.init();
loading.classList.add("doneloading");
loading.classList.remove("loading");
console.log("done loading");
});
userinfo.remove();
});
}
{
const td=document.createElement("div");
td.classList.add("switchtable");
td.append("Switch accounts ⇌");
td.addEventListener("click",_=>{
window.location.href="/login.html";
});
table.append(td);
}
table.classList.add("accountSwitcher");
if(Contextmenu.currentmenu!=""){
Contextmenu.currentmenu.remove();
}
Contextmenu.currentmenu=table;
console.log(table);
document.body.append(table);
}
{
const userinfo=document.getElementById("userinfo") as HTMLDivElement;
userinfo.addEventListener("click",_=>{
_.stopImmediatePropagation();
showAccountSwitcher();
});
const switchaccounts=document.getElementById("switchaccounts") as HTMLDivElement;
switchaccounts.addEventListener("click",_=>{
_.stopImmediatePropagation();
showAccountSwitcher();
});
console.log("this ran");
}
let thisuser:Localuser;
try{
console.log(users.users,users.currentuser);
thisuser=new Localuser(users.users[users.currentuser]);
thisuser.initwebsocket().then(_=>{
thisuser.loaduser();
thisuser.init();
const loading=document.getElementById("loading") as HTMLDivElement;
loading.classList.add("doneloading");
loading.classList.remove("loading");
console.log("done loading");
});
}catch(e){
console.error(e);
(document.getElementById("load-desc") as HTMLSpanElement).textContent="Account unable to start";
thisuser=new Localuser(-1);
}
{
const menu=new Contextmenu("create rightclick");//Really should go into the localuser class, but that's a later thing
menu.addbutton("Create channel",()=>{
if(thisuser.lookingguild){
thisuser.lookingguild.createchannels();
}
},null,_=>{
return thisuser.isAdmin();
});
menu.addbutton("Create category",()=>{
if(thisuser.lookingguild){
thisuser.lookingguild.createcategory();
}
},null,_=>{
return thisuser.isAdmin();
});
menu.bindContextmenu(document.getElementById("channels") as HTMLDivElement,0,0);
}
const pasteimage=document.getElementById("pasteimage") as HTMLDivElement;
let replyingto:Message|null=null;
async function enter(event){
const channel=thisuser.channelfocus;
if(!channel||!thisuser.channelfocus)return;
channel.typingstart();
if(event.key === "Enter"&&!event.shiftKey){
event.preventDefault();
if(channel.editing){
channel.editing.edit(markdown.rawString);
channel.editing=null;
}else{
replyingto= thisuser.channelfocus.replyingto;
const replying=replyingto;
if(replyingto?.div){
replyingto.div.classList.remove("replying");
}
thisuser.channelfocus.replyingto=null;
channel.sendMessage(markdown.rawString,{
attachments: images,
embeds: [],
replyingto: replying
});
thisuser.channelfocus.makereplybox();
}
while(images.length!=0){
images.pop();
pasteimage.removeChild(imageshtml.pop() as HTMLElement);
}
typebox.innerHTML="";
}
}
const typebox=document.getElementById("typebox") as HTMLDivElement;
const markdown=new MarkDown("",thisuser);
markdown.giveBox(typebox);
typebox["markdown"]=markdown;
typebox.addEventListener("keyup",enter);
typebox.addEventListener("keydown",event=>{
if(event.key === "Enter"&&!event.shiftKey) event.preventDefault();
});
console.log(typebox);
typebox.onclick=console.log;
/*
function getguildinfo(){
const path=window.location.pathname.split("/");
const channel=path[3];
this.ws.send(JSON.stringify({op: 14, d: {guild_id: path[2], channels: {[channel]: [[0, 99]]}}}));
}
*/
const images:Blob[]=[];
const imageshtml:HTMLElement[]=[];
document.addEventListener("paste", async e=>{
if(!e.clipboardData)return;
Array.from(e.clipboardData.files).forEach(async f=>{
const file=File.initFromBlob(f);
e.preventDefault();
const html=file.upHTML(images,f);
pasteimage.appendChild(html);
images.push(f);
imageshtml.push(html);
});
});
setTheme();
function userSettings(){
thisuser.showusersettings();
}
(document.getElementById("settings") as HTMLImageElement).onclick=userSettings;
if(mobile){
(document.getElementById("channelw") as HTMLDivElement).onclick=()=>{
((document.getElementById("channels") as HTMLDivElement).parentNode as HTMLElement).classList.add("collapse");
(document.getElementById("servertd") as HTMLDivElement).classList.add("collapse");
(document.getElementById("servers") as HTMLDivElement).classList.add("collapse");
};
(document.getElementById("mobileback") as HTMLDivElement).textContent="#";
(document.getElementById("mobileback") as HTMLDivElement).onclick=()=>{
((document.getElementById("channels") as HTMLDivElement).parentNode as HTMLElement).classList.remove("collapse");
(document.getElementById("servertd") as HTMLDivElement).classList.remove("collapse");
(document.getElementById("servers") as HTMLDivElement).classList.remove("collapse");
};
}
})();

View file

@ -1,301 +0,0 @@
class InfiniteScroller{
readonly getIDFromOffset:(ID:string,offset:number)=>Promise<string|undefined>;
readonly getHTMLFromID:(ID:string)=>Promise<HTMLElement>;
readonly destroyFromID:(ID:string)=>Promise<boolean>;
readonly reachesBottom:()=>void;
private readonly minDist=2000;
private readonly fillDist=3000;
private readonly maxDist=6000;
HTMLElements:[HTMLElement,string][]=[];
div:HTMLDivElement|null;
constructor(getIDFromOffset:InfiniteScroller["getIDFromOffset"],getHTMLFromID:InfiniteScroller["getHTMLFromID"],destroyFromID:InfiniteScroller["destroyFromID"],reachesBottom:InfiniteScroller["reachesBottom"]=()=>{}){
this.getIDFromOffset=getIDFromOffset;
this.getHTMLFromID=getHTMLFromID;
this.destroyFromID=destroyFromID;
this.reachesBottom=reachesBottom;
}
timeout:NodeJS.Timeout|null;
async getDiv(initialId:string,bottom=true):Promise<HTMLDivElement>{
//div.classList.add("flexttb")
if(this.div){
throw new Error("Div already exists, exiting.")
}
const scroll=document.createElement("div");
scroll.classList.add("flexttb","scroller");
this.beenloaded=false;
//this.interval=setInterval(this.updatestuff.bind(this,true),100);
this.div=scroll;
this.div.addEventListener("scroll",_=>{
this.checkscroll();
if(this.scrollBottom<5){
this.scrollBottom=5;
}
if(this.timeout===null){
this.timeout=setTimeout(this.updatestuff.bind(this),300);
}
this.watchForChange();
});
{
let oldheight=0;
new ResizeObserver(_=>{
this.checkscroll();
const func=this.snapBottom();
this.updatestuff();
const change=oldheight-scroll.offsetHeight;
if(change>0&&this.div){
this.div.scrollTop+=change;
}
oldheight=scroll.offsetHeight;
this.watchForChange();
func();
}).observe(scroll);
}
new ResizeObserver(this.watchForChange.bind(this)).observe(scroll);
await this.firstElement(initialId);
this.updatestuff();
await this.watchForChange().then(_=>{
this.updatestuff();
this.beenloaded=true;
});
return scroll;
}
beenloaded=false;
scrollBottom:number;
scrollTop:number;
needsupdate=true;
averageheight:number=60;
checkscroll(){
if(this.beenloaded&&this.div&&!document.body.contains(this.div)){
console.warn("not in document");
this.div=null;
}
}
async updatestuff(){
this.timeout=null;
if(!this.div)return;
this.scrollBottom = this.div.scrollHeight - this.div.scrollTop - this.div.clientHeight;
this.averageheight=this.div.scrollHeight/this.HTMLElements.length;
if(this.averageheight<10){
this.averageheight=60;
}
this.scrollTop=this.div.scrollTop;
if(!this.scrollBottom && !await this.watchForChange()){
this.reachesBottom();
}
if(!this.scrollTop){
await this.watchForChange();
}
this.needsupdate=false;
//this.watchForChange();
}
async firstElement(id:string){
if(!this.div)return;
const html=await this.getHTMLFromID(id);
this.div.appendChild(html);
this.HTMLElements.push([html,id]);
}
async addedBottom(){
await this.updatestuff();
const func=this.snapBottom();
await this.watchForChange();
func();
}
snapBottom(){
const scrollBottom=this.scrollBottom;
return()=>{
if(this.div&&scrollBottom<4){
this.div.scrollTop=this.div.scrollHeight;
}
};
}
private async watchForTop(already=false,fragement=new DocumentFragment()):Promise<boolean>{
if(!this.div)return false;
try{
let again=false;
if(this.scrollTop<(already?this.fillDist:this.minDist)){
let nextid:string|undefined;
const firstelm=this.HTMLElements.at(0);
if(firstelm){
const previd=firstelm[1];
nextid=await this.getIDFromOffset(previd,1);
}
if(!nextid){
}else{
const html=await this.getHTMLFromID(nextid);
if(!html){
this.destroyFromID(nextid);
return false;
}
again=true;
fragement.prepend(html);
this.HTMLElements.unshift([html,nextid]);
this.scrollTop+=this.averageheight;
}
}
if(this.scrollTop>this.maxDist){
const html=this.HTMLElements.shift();
if(html){
again=true;
await this.destroyFromID(html[1]);
this.scrollTop-=this.averageheight;
}
}
if(again){
await this.watchForTop(true,fragement);
}
return again;
}finally{
if(!already){
if(this.div.scrollTop===0){
this.scrollTop=1;
this.div.scrollTop=10;
}
this.div.prepend(fragement,fragement);
}
}
}
async watchForBottom(already=false,fragement=new DocumentFragment()):Promise<boolean>{
let func:Function|undefined;
if(!already) func=this.snapBottom();
if(!this.div)return false;
try{
let again=false;
const scrollBottom = this.scrollBottom;
if(scrollBottom<(already?this.fillDist:this.minDist)){
let nextid:string|undefined;
const lastelm=this.HTMLElements.at(-1);
if(lastelm){
const previd=lastelm[1];
nextid=await this.getIDFromOffset(previd,-1);
}
if(!nextid){
}else{
again=true;
const html=await this.getHTMLFromID(nextid);
fragement.appendChild(html);
this.HTMLElements.push([html,nextid]);
this.scrollBottom+=this.averageheight;
}
}
if(scrollBottom>this.maxDist){
const html=this.HTMLElements.pop();
if(html){
await this.destroyFromID(html[1]);
this.scrollBottom-=this.averageheight;
again=true;
}
}
if(again){
await this.watchForBottom(true,fragement);
}
return again;
}finally{
if(!already){
this.div.append(fragement);
if(func){
func();
}
}
}
}
watchtime:boolean=false;
changePromise:Promise<boolean>|undefined;
async watchForChange():Promise<boolean>{
if(this.changePromise){
this.watchtime=true;
return await this.changePromise;
}else{
this.watchtime=false;
}
this.changePromise=new Promise<boolean>(async res=>{
try{
try{
if(!this.div){
res(false);return false;
}
const out=await Promise.allSettled([this.watchForTop(),this.watchForBottom()]) as {value:boolean}[];
const changed=(out[0].value||out[1].value);
if(this.timeout===null&&changed){
this.timeout=setTimeout(this.updatestuff.bind(this),300);
}
if(!this.changePromise){
console.error("something really bad happened");
}
res(Boolean(changed));
return Boolean(changed);
}catch(e){
console.error(e);
}
res(false);
return false;
}catch(e){
throw e;
}finally{
setTimeout(_=>{
this.changePromise=undefined;
if(this.watchtime){
this.watchForChange();
}
},300);
}
});
return await this.changePromise;
}
async focus(id:string,flash=true){
let element:HTMLElement|undefined;
for(const thing of this.HTMLElements){
if(thing[1]===id){
element=thing[0];
}
}
if(element){
if(flash){
element.scrollIntoView({
behavior: "smooth",
block: "center"
});
await new Promise(resolve=>setTimeout(resolve, 1000));
element.classList.remove("jumped");
await new Promise(resolve=>setTimeout(resolve, 100));
element.classList.add("jumped");
}else{
element.scrollIntoView();
}
}else{
for(const thing of this.HTMLElements){
await this.destroyFromID(thing[1]);
}
this.HTMLElements=[];
await this.firstElement(id);
this.updatestuff();
await this.watchForChange();
await new Promise(resolve=>setTimeout(resolve, 100));
await this.focus(id,true);
}
}
async delete():Promise<void>{
if(this.div){
this.div.remove();
this.div=null;
}
try{
for(const thing of this.HTMLElements){
await this.destroyFromID(thing[1]);
}
}catch(e){
console.error(e);
}
this.HTMLElements=[];
if(this.timeout){
clearTimeout(this.timeout);
}
}
}
export{InfiniteScroller};

View file

@ -1,118 +0,0 @@
import{getBulkUsers, Specialuser, getapiurls}from"./login.js";
(async ()=>{
const users=getBulkUsers();
const well=new URLSearchParams(window.location.search).get("instance");
const joinable:Specialuser[]=[];
for(const thing in users.users){
const user:Specialuser = users.users[thing];
if(user.serverurls.wellknown.includes(well)){
joinable.push(user);
}
console.log(users.users[thing]);
}
let urls:{api:string,cdn:string};
if(!joinable.length&&well){
const out=await getapiurls(well);
if(out){
urls=out;
for(const thing in users.users){
const user:Specialuser = users.users[thing];
if(user.serverurls.api.includes(out.api)){
joinable.push(user);
}
console.log(users.users[thing]);
}
}else{
throw new Error("someone needs to handle the case where the servers don't exist");
}
}else{
urls=joinable[0].serverurls;
}
if(!joinable.length){
document.getElementById("AcceptInvite").textContent="Create an account to accept the invite";
}
const code=window.location.pathname.split("/")[2];
let guildinfo;
fetch(`${urls.api}/invites/${code}`,{
method: "GET"
}).then(_=>_.json()).then(json=>{
const guildjson=json.guild;
guildinfo=guildjson;
document.getElementById("invitename").textContent=guildjson.name;
document.getElementById("invitedescription").textContent=
`${json.inviter.username} invited you to join ${guildjson.name}`;
if(guildjson.icon){
const img=document.createElement("img");
img.src=`${urls.cdn}/icons/${guildjson.id}/${guildjson.icon}.png`;
img.classList.add("inviteGuild");
document.getElementById("inviteimg").append(img);
}else{
const txt=guildjson.name.replace(/'s /g, " ").replace(/\w+/g, word=>word[0]).replace(/\s/g, "");
const div=document.createElement("div");
div.textContent=txt;
div.classList.add("inviteGuild");
document.getElementById("inviteimg").append(div);
}
});
function showAccounts(){
const table=document.createElement("dialog");
for(const thing of Object.values(joinable)){
const specialuser=thing as Specialuser;
console.log(specialuser.pfpsrc);
const userinfo=document.createElement("div");
userinfo.classList.add("flexltr","switchtable");
const pfp=document.createElement("img");
userinfo.append(pfp);
const user=document.createElement("div");
userinfo.append(user);
user.append(specialuser.username);
user.append(document.createElement("br"));
const span=document.createElement("span");
span.textContent=specialuser.serverurls.wellknown.replace("https://","").replace("http://","");
user.append(span);
user.classList.add("userinfo");
span.classList.add("serverURL");
pfp.src=specialuser.pfpsrc;
pfp.classList.add("pfp");
table.append(userinfo);
userinfo.addEventListener("click",_=>{
console.log(thing);
fetch(`${urls.api}/invites/${code}`,{
method: "POST",
headers: {
Authorization: thing.token
}
}).then(_=>{
users.currentuser=specialuser.uid;
localStorage.setItem("userinfos",JSON.stringify(users));
window.location.href="/channels/"+guildinfo.id;
});
});
}
{
const td=document.createElement("div");
td.classList.add("switchtable");
td.append("Login or create an account ⇌");
td.addEventListener("click",_=>{
const l=new URLSearchParams("?");
l.set("goback",window.location.href);
l.set("instance",well);
window.location.href="/login?"+l.toString();
});
if(!joinable.length){
const l=new URLSearchParams("?");
l.set("goback",window.location.href);
l.set("instance",well);
window.location.href="/login?"+l.toString();
}
table.append(td);
}
table.classList.add("accountSwitcher");
console.log(table);
document.body.append(table);
}
document.getElementById("AcceptInvite").addEventListener("click",showAccounts);
})();

View file

@ -1,453 +0,0 @@
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
}
}
}
type mainuserjson= userjson & {
flags: number,
mfa_enabled?: boolean,
email?: string,
phone?: string,
verified: boolean,
nsfw_allowed: boolean,
premium: boolean,
purchased_flags: number,
premium_usage_flags: number,
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[],
}
type memberjson= {
index?:number,
id: string,
user: userjson|null,
guild_id: string,
guild: {
id: string
}|null,
nick?: string,
roles: string[],
joined_at: string,
premium_since: string,
deaf: boolean,
mute: boolean,
pending: boolean,
last_message_id?: boolean//What???
}
type emojijson={
name:string,
id?:string,
animated?:boolean
}
type guildjson={
application_command_counts: {[key:string]:number},
channels: channeljson[],
data_mode: string,
emojis: emojijson[],
guild_scheduled_events: [],
id: string,
large: boolean,
lazy: boolean,
member_count: number,
premium_subscription_count: number,
properties: {
region: string|null,
name: string,
description: string,
icon: string,
splash: string,
banner: string,
features: string[],
preferred_locale: string,
owner_id: string,
application_id: string,
afk_channel_id: string,
afk_timeout: number,
system_channel_id: string,
verification_level: number,
explicit_content_filter: number,
default_message_notifications: number,
mfa_level: number,
vanity_url_code: number,
premium_tier: number,
premium_progress_bar_enabled: boolean,
system_channel_flags: number,
discovery_splash: string,
rules_channel_id: string,
public_updates_channel_id: string,
max_video_channel_users: number,
max_members: number,
nsfw_level: number,
hub_type: null,
home_header: null,
id: string,
latest_onboarding_question_id: string,
max_stage_video_channel_users: number,
nsfw: boolean,
safety_alerts_channel_id: string
},
roles: rolesjson[],
stage_instances: [],
stickers: [],
threads: [],
version: string,
guild_hashes: {},
joined_at: string
}
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
}
type rolesjson={
id: string,
guild_id: string,
color: number,
hoist: boolean,
managed: boolean,
mentionable: boolean,
name: string,
permissions: string,
position: number,
icon: string,
unicode_emoji: string,
flags: number
}
type dirrectjson={
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
}
type filejson={
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 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
}
type presencejson={
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"
}
type wsjson={
op:0,
d:any,
s:number,
t:"TYPING_START"|"USER_UPDATE"|"CHANNEL_UPDATE"|"CHANNEL_CREATE"|"CHANNEL_DELETE"|"GUILD_DELETE"|"GUILD_CREATE"|"MESSAGE_REACTION_REMOVE_ALL"|"MESSAGE_REACTION_REMOVE_EMOJI"
}|{
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:{
guild_id?:string,
channel_id:string
}&messagejson,
s:number,
t:"MESSAGE_UPDATE"
}|messageCreateJson|readyjson|{
op:11,
s:undefined,
d:{}
}|{
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_REMOVE",
d: {
user_id: string,
channel_id: string,
message_id: string,
guild_id: string,
emoji: emojijson
},
"s": 3
}
type memberChunk={
guild_id: string,
nonce: string,
members: memberjson[],
presences: presencejson[],
chunk_index: number,
chunk_count: number,
not_found: string[]
}
export{readyjson,dirrectjson,channeljson,guildjson,rolesjson,userjson,memberjson,mainuserjson,messagejson,filejson,embedjson,emojijson,presencejson,wsjson,messageCreateJson,memberChunk,invitejson};

File diff suppressed because it is too large Load diff

Some files were not shown because too many files have changed in this diff Show more