update instancejson more
MY HEAD HURTS NOW ;( . . more work Finish rewrite all finished
116
src/index.ts
Normal 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
|
@ -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
|
@ -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
|
@ -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
107
src/webpage/contextmenu.ts
Normal 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
|
@ -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
|
@ -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
|
@ -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 };
|
BIN
src/webpage/emoji.bin
Normal file
259
src/webpage/emoji.ts
Normal 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 };
|
BIN
src/webpage/favicon.ico
Normal file
After Width: | Height: | Size: 906 B |
152
src/webpage/file.ts
Normal 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
|
@ -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 };
|
49
src/webpage/home.html
Normal file
|
@ -0,0 +1,49 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Jank Client</title>
|
||||
<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">
|
||||
<link href="/themes.css" rel="stylesheet" id="lightcss">
|
||||
</head>
|
||||
|
||||
<body class="Dark-theme">
|
||||
<div id="titleDiv">
|
||||
<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">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">
|
||||
|
||||
<div class="flexttb pagehead"><h1>Welcome to Jank Client</h1></div>
|
||||
<div class="pagebox">
|
||||
<p>Jank Client is a spacebar compatible client seeking to be as good as it can be with many features including:</p>
|
||||
<ul>
|
||||
<li>Direct Messaging</li>
|
||||
<li>Reactions support</li>
|
||||
<li>Invites</li>
|
||||
<li>Account switching</li>
|
||||
<li>User settings</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="pagebox">
|
||||
<h2>Spacebar compatible Instances:</h2>
|
||||
<div id="instancebox">
|
||||
</div>
|
||||
</div>
|
||||
<div class="pagebox">
|
||||
<h2>Contribute to Jank Client</h2>
|
||||
<p>We always appreciate some help, wether that be in the form of bug reports, or code, or even just pointing out some typos.</p><br>
|
||||
</a><a href="https://github.com/MathMan05/JankClient" class="TitleButtons"><h1>Github</h1></a>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
<script src="/home.js" type="module"></script>
|
||||
</html>
|
89
src/webpage/home.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
);
|
1
src/webpage/icons/announce.svg
Normal file
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 180 180"><g style="fill:red;stroke:red;stroke-opacity:1"><path d="m25 73 54-11 46-25v106l-40-24-60-11z" style="fill:red;fill-opacity:1;stroke:red;stroke-width:4;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1" transform="matrix(1.6277 0 0 1.5863 -30 -56)"/><path d="m32 90 13 2-5 32 38 7 5-32 14 2-7 46-66-11 7-46z" style="fill:red;fill-opacity:1;stroke:red;stroke-width:3.84496;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1" transform="matrix(1.6277 0 0 1.5863 -30 -56)"/></g></svg>
|
After Width: | Height: | Size: 606 B |
1
src/webpage/icons/category.svg
Normal file
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 180 180"><path d="m15 54 75 64 75-64" style="fill:none;stroke:red;stroke-width:29.0323;stroke-linecap:round;stroke-linejoin:round"/></svg>
|
After Width: | Height: | Size: 191 B |
1
src/webpage/icons/channel.svg
Normal file
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 180 180"><path d="m117 165 13-148M47 165 60 17m108 46H20m140 65H12" style="fill:red;stroke:red;stroke-width:24.2188;stroke-linecap:round;stroke-dasharray:none"/></svg>
|
After Width: | Height: | Size: 220 B |
1
src/webpage/icons/copy.svg
Normal file
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 180 180"><g fill="none" stroke="red" stroke-linecap="round" stroke-linejoin="round" stroke-width="21.9"><path d="m64 44.5 85.7.3-.5 124.3-85.2-.4.5-124.2Z"/><path d="M31.5 141.6 32 11.5h-.5l89.6.3"/></g></svg>
|
After Width: | Height: | Size: 262 B |
1
src/webpage/icons/delete.svg
Normal file
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 180 180"><g fill="red"><path d="M139 155a11 11 0 0 1-3.3 4.9 11 11 0 0 1-8 2.6 11 11 0 0 1-10.2-11.4h-16.8v.5a11 11 0 0 1-11.1 11 11 11 0 0 1-11-11v-.5H62a11 11 0 0 1-2.7 7.7 11 11 0 0 1-15.6 1 11 11 0 0 1-3.8-7.2l1 12.6h97.3z"/><path stroke="red" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.3" d="M100.1 61.5a11 11 0 0 1 .6 3v86.6h16.8a11 11 0 0 1 0-.3l6.3-87a11 11 0 0 1 .5-2.3z"/><path stroke="red" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.3" d="M146.7 33.2H32.4a14.1 14.1 0 0 0-14 15.2l8.5 117.8a14.1 14.1 0 0 0 14 13.1h97.4a14.1 14.1 0 0 0 14-13l8.5-118a14.1 14.1 0 0 0-14-15Zm0 14.2-1 15a11 11 0 0 1 .2 3l-6.3 87a11 11 0 0 1-.6 2.7l-.7 10.1H41l-1-12.6a11 11 0 0 1 0-.2l-6.3-87a11 11 0 0 1 0-.3l-1.2-17.7Z"/><path stroke="red" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.3" d="M146.7 47.4H32.5L33.7 65A11 11 0 0 1 44 53.6a11 11 0 0 1 11.3 7.9H79a11 11 0 0 1 10.6-7.9 11 11 0 0 1 10.5 7.9h24.2a11 11 0 0 1 11.3-7.9 11 11 0 0 1 7.5 3.8 11 11 0 0 1 2.5 5z"/><path stroke="red" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.3" d="M55.3 61.5a11 11 0 0 1 .5 2.2l6.3 87.1a11 11 0 0 1 0 .3h16.4V64.5a11 11 0 0 1 .5-3z"/><path stroke="red" stroke-linecap="round" stroke-linejoin="round" stroke-width="11.7" d="M19.9 15.6h140.4v5.6H19.9z"/><path stroke="red" stroke-linecap="round" stroke-linejoin="round" stroke-width="14.2" d="M76 7.1h27.6v7.5H76z"/></g></svg>
|
After Width: | Height: | Size: 1.5 KiB |
1
src/webpage/icons/edit.svg
Normal file
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 180 180"><path fill="red" stroke="red" stroke-linecap="round" stroke-linejoin="round" stroke-width="27.7" d="m112.8 40.4 26.8 26.8-84.5 84.7-41.3 14.3L28.2 125Zm26.5-26.6 26.9 26.9-4.3 4.4L135 18.2z"/></svg>
|
After Width: | Height: | Size: 260 B |
1
src/webpage/icons/explore.svg
Normal file
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 180 180"><circle cx="90" cy="90" r="64.4" style="fill:none;stroke:red;stroke-width:11.1467;stroke-linejoin:bevel;stroke-dasharray:none"/><g transform="rotate(4.6 -290 396.6) scale(.71167)"><circle cx="89.9" cy="86.9" r="15.5" style="fill:none;stroke:red;stroke-width:11.24121975;stroke-linejoin:bevel;stroke-dasharray:none"/><path d="M134.9 34.6 90.2 71.2l.3.2c6.7.3 12.5 4.8 14.3 11.2l.7.5zM88 102.3a15.5 15.5 0 0 1-12.6-9.9l-1.9-1.6-15.4 34 30-22.6Z" style="fill:red;stroke:red;stroke-width:4.2154574;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none"/></g></svg>
|
After Width: | Height: | Size: 633 B |
1
src/webpage/icons/home.svg
Normal file
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 180 180"><path d="M88.5 13.4 20.7 66.3h10l57.8-44.8 56.8 44.8h9.4z" style="fill:red;stroke:red;stroke-width:3;stroke-linecap:round;stroke-linejoin:round" transform="translate(26 32.6) scale(.72993)"/><path d="M111.3 10.6v22.8l23.4 18.4h2.4V10.6Z" style="fill:red;stroke:red;stroke-linejoin:bevel" transform="translate(26 32.6) scale(.72993)"/><path d="M131.6 88.7 90.6 56 48.4 89.3v49.4H79v-32.2h21.8v32.2h30.7z" style="fill:red;stroke:red;stroke-width:2.18979;stroke-linecap:round;stroke-linejoin:round"/></svg>
|
After Width: | Height: | Size: 565 B |
1
src/webpage/icons/reply.svg
Normal file
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 180 180"><g fill="none" stroke="red" stroke-linecap="round" stroke-linejoin="round" stroke-width="32"><path d="M155.5 164c0-36.7 0-73.4-17.2-91.8C121.1 54 86.7 54 52.3 54"/><path d="M67.8 16 26 53.9l42.3 44.4"/></g></svg>
|
After Width: | Height: | Size: 274 B |
1
src/webpage/icons/settings.svg
Normal file
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 180 180"><g fill="none" stroke="red" stroke-linecap="round" stroke-linejoin="round"><circle cx="90.7" cy="90" r="50.3" stroke-width="28"/><path stroke-width="22.4" d="M83.7 11.2h14v26.3h-14zm0 131.2h14v26.3h-14zm-44.8 8.3-10-9.9 18.7-18.6 9.9 9.9zM131.6 58l-9.9-10 18.6-18.6 10 10zm37.2 25.2v14h-26.3v-14zm-131.2 0v14H11.3v-14zm-8.3-44.8 9.9-10 18.6 18.7L48 57zm92.7 92.7 10-9.9 18.6 18.7-10 9.9z"/></g></svg>
|
After Width: | Height: | Size: 462 B |
1
src/webpage/icons/voice.svg
Normal file
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 180 180"><path d="M5 51h21l28-26h17v130H54l-28-25-21 1Z" style="fill:red;stroke-width:1.21692;stroke-dasharray:none"/><path d="M100 38c27 12 38 45 26 71-4 9-10 16-18 22" style="fill:none;fill-rule:evenodd;stroke:red;stroke-width:25;stroke-linecap:round;stroke-dasharray:none;stroke-opacity:1" transform="rotate(5 50 -2)"/><path d="M130 8c41 23 56 76 34 117-8 14-19 25-32 33" style="fill:none;fill-rule:evenodd;stroke:red;stroke-width:25;stroke-linecap:round;stroke-dasharray:none;stroke-opacity:1" transform="rotate(1 -346 -882) scale(1.02257)"/></svg>
|
After Width: | Height: | Size: 605 B |
81
src/webpage/index.html
Normal file
|
@ -0,0 +1,81 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<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">
|
||||
<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">
|
||||
|
||||
<link rel="manifest" href="/manifest.json">
|
||||
</head>
|
||||
|
||||
<body class="Dark-theme">
|
||||
<script src="/index.js" type="module"></script>
|
||||
|
||||
<div id="loading" class="loading">
|
||||
<div id="centerdiv">
|
||||
<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>
|
||||
<h2 id="switchaccounts">Switch Accounts</h2>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flexltr" id="page">
|
||||
<div id="neunence">
|
||||
<div id="servers"></div>
|
||||
</div>
|
||||
<div class="flexttb channelflex">
|
||||
<div class="servertd" id="servertd">
|
||||
<h2 id="serverName">Server Name</h2>
|
||||
</div>
|
||||
<div id="channels"></div>
|
||||
<div class="flexltr" id="userdock">
|
||||
<div class="flexltr" id="userinfo">
|
||||
<img id="userpfp" class="pfp" alt="User Profile Picture">
|
||||
|
||||
<div class="userflex">
|
||||
<p id="username">USERNAME</p>
|
||||
<p id="status">STATUS</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="user-actions">
|
||||
<span id="settings" class="svgtheme svg-settings"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flexttb messageflex">
|
||||
<div class="servertd channelnamediv">
|
||||
<span id="mobileback" hidden></span>
|
||||
<span id="channelname">Channel name</span>
|
||||
<span id="channelTopic" hidden>Channel topic</span>
|
||||
</div>
|
||||
<div id="channelw">
|
||||
<div id="loadingdiv">
|
||||
</div>
|
||||
</div>
|
||||
<div id="pasteimage"></div>
|
||||
<div id="replybox" class="hideReplyBox"></div>
|
||||
<div id="typediv">
|
||||
<div id="realbox">
|
||||
<div id="typebox" contentEditable="true"></div>
|
||||
</div>
|
||||
<div id="typing" class="hidden">
|
||||
<p id="typingtext">typing</p>
|
||||
<div class="loading-indicator">
|
||||
<div class="dot"></div>
|
||||
<div class="dot"></div>
|
||||
<div class="dot"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
259
src/webpage/index.ts
Normal 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");
|
||||
};
|
||||
}
|
||||
})();
|
323
src/webpage/infiniteScroller.ts
Normal 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 };
|
28
src/webpage/instances.json
Normal file
|
@ -0,0 +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": "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": "en",
|
||||
"country": "US",
|
||||
"display": true,
|
||||
"urls": {
|
||||
"wellknown": "https://greysilly7.xyz",
|
||||
"api": "https://spacebar.greysilly7.xyz/api/v9",
|
||||
"cdn": "https://spacebar.greysilly7.xyz",
|
||||
"gateway": "wss://spacebar.greysilly7.xyz"
|
||||
},
|
||||
"contactInfo": {
|
||||
"dicord": "greysilly7",
|
||||
"github": "https://github.com/greysilly7",
|
||||
"email": "greysilly7@gmail.com"
|
||||
}
|
||||
}
|
||||
]
|
22
src/webpage/invite.html
Normal file
|
@ -0,0 +1,22 @@
|
|||
<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="Invite" property="og:title">
|
||||
<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">
|
||||
<link href="/themes.css" rel="stylesheet" id="lightcss">
|
||||
</head>
|
||||
<div>
|
||||
<div id="invitebody">
|
||||
<div id="inviteimg"></div>
|
||||
<h1 id="invitename">Server Name</h1>
|
||||
<p id="invitedescription">Someone invited you to Server Name</p>
|
||||
<button id="AcceptInvite">Accept Invite</button>
|
||||
</div>
|
||||
</div>
|
||||
<script type="module" src="/invite.js"></script>
|
||||
</body>
|
147
src/webpage/invite.ts
Normal 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
|
@ -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
62
src/webpage/login.html
Normal 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
|
@ -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("");
|
||||
}
|
||||
}
|
||||
);
|
1
src/webpage/logo.svg
Normal file
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 135.5 135.5"><path fill="navy" d="m5.3.6 96.2-.5 24.4 70.3-101.5.5L0 .6z"/><path fill="#000036" d="m99.5.4 1.9-.2 24.4 70.3-7.2.2L94.2.4z"/><ellipse cx="74.4" cy="54.1" rx="7.3" ry="21.6"/><ellipse cx="39.9" cy="54.1" rx="7.3" ry="21.6"/><path fill="navy" d="m15.4 65.4 96.2-.6 24.4 70.3-101.5.6-24.4-70.3z"/><path fill="#000034" d="m38 130.4 96.2-.6 1.7 5.7-101.5.6-1.7-5.7z"/><path fill="#000036" d="m111.4 71 1.8-.2 22.8 65-7.2.2-22.7-65z"/><path d="M33.4 65.3H21a7.3 21.6 0 0 0 6.3 10.4 7.3 21.6 0 0 0 6.2-10.5zm22.1-.2a7.3 21.6 0 0 0 6.2 10.7A7.3 21.6 0 0 0 68 65z"/></svg>
|
After Width: | Height: | Size: 632 B |
BIN
src/webpage/logo.webp
Normal file
After Width: | Height: | Size: 1.7 KiB |
12
src/webpage/manifest.json
Normal 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
|
@ -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
|
@ -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
|
@ -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
|
@ -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 };
|
60
src/webpage/register.html
Normal file
|
@ -0,0 +1,60 @@
|
|||
<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>Create an account</h1><br>
|
||||
<form id="register" submit="registertry(e)">
|
||||
<div>
|
||||
<label for="instance"><b>Instance:</b></label><br>
|
||||
<p id="verify"></p>
|
||||
<input type="search" list="instances" placeholder="Instance URL" id="instancein" name="instance" value="" id="instancein" required>
|
||||
</div>
|
||||
<div>
|
||||
<label for="uname"><b>Email:</b></label><br>
|
||||
<input type="text" placeholder="Enter Email" name="uname" id="uname" required>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="uname"><b>Username:</b></label><br>
|
||||
<input type="text" placeholder="Enter Username" name="username" id="username" required>
|
||||
</div>
|
||||
<div>
|
||||
<label for="psw"><b>Password:</b></label><br>
|
||||
<input type="password" placeholder="Enter Password" name="psw" id="psw" required>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="psw2"><b>Enter password again:</b></label><br>
|
||||
<input type="password" placeholder="Enter Password Again" name="psw2" id="psw2" required>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="date"><b>Date of birth:</b></label><br>
|
||||
<input type="date" id="date" name="date">
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<b id="TOSbox">I agree to the <a href="" id="TOSa">Terms of Service</a>:</b>
|
||||
<input type="checkbox" id="TOS" name="TOS">
|
||||
</div>
|
||||
|
||||
<p class="wrongred" id="wrong"></p>
|
||||
<div id="h-captcha">
|
||||
|
||||
</div>
|
||||
<button type="submit" class="dontgrow">Create account</button>
|
||||
</form>
|
||||
<a href="/login.html" id="switch">Already have an account?</a>
|
||||
</div>
|
||||
<datalist id="instances"></datalist>
|
||||
<script src="/register.js" type="module"></script>
|
||||
</body>
|
152
src/webpage/register.ts
Normal 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
|
@ -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
|
@ -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
20
src/webpage/snowflake.ts
Normal 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 };
|
2208
src/webpage/style.css
Normal file
178
src/webpage/themes.css
Normal file
|
@ -0,0 +1,178 @@
|
|||
:root {
|
||||
--servertd-height: 0px;
|
||||
/* Default value */
|
||||
--red: red;
|
||||
--green: green;
|
||||
--yellow:yellow;
|
||||
--accent-color:#242443;
|
||||
}
|
||||
.Dark-theme { /* thanks to TomatoCake for the updated CSS vars and such*/
|
||||
color-scheme: dark;
|
||||
--primary-text: #FFF;
|
||||
--primary-bg: color-mix(in srgb, #2f2f2f 70%, var(--accent-color));
|
||||
--black: #000;
|
||||
--shadow: #000;
|
||||
--focus: #8888ff;
|
||||
--message-bg-hover: color-mix(in srgb, #191919 85%, var(--accent-color));
|
||||
--typing-bg: #161616;
|
||||
--timestamp-color: #a2a2a2;
|
||||
--code-bg: color-mix(in srgb, #121212 95%, var(--accent-color));
|
||||
--user-info-bg: color-mix(in srgb,#383838 85%, var(--accent-color));
|
||||
--user-dock-bg: color-mix(in srgb,#111111 90%, var(--accent-color));
|
||||
--channels-bg: color-mix(in srgb, #2a2a2a 90%, var(--accent-color));
|
||||
--channel-hover: color-mix(in srgb, #121212 70%, var(--accent-color));
|
||||
--blank-bg: #323232;
|
||||
--light-border: #92929B;
|
||||
--settings-hover: color-mix(in srgb, #000000 95%, var(--accent-color) 5%);
|
||||
--quote-bg: #7a798e;
|
||||
--button-bg: color-mix(in srgb, #191919 85%, var(--accent-color));
|
||||
--button-hover: color-mix(in srgb, #2f2f2f 70%, var(--accent-color));
|
||||
--textarea-bg: color-mix(in srgb, #484848 80%, var(--accent-color));
|
||||
--filename: #47bbff;
|
||||
--mention-bg: #F00;
|
||||
--mention-md-bg: #3b588b;
|
||||
--pronouns: #797979;
|
||||
--profile-bg: color-mix(in srgb, #232323 90%, var(--accent-color));
|
||||
--profile-info-bg: color-mix(in srgb, #121212 90%, var(--accent-color));
|
||||
--server-border: color-mix(in srgb, #000000 80%, var(--accent-color));
|
||||
--channel-name-bg: color-mix(in srgb, #2a2a2a 80%, var(--accent-color));
|
||||
--server-name-bg: color-mix(in srgb, #232323 90%, var(--accent-color));
|
||||
--reply-border: #474b76;
|
||||
--reply-bg: #0b0d20;
|
||||
--reply-text: #acacac;
|
||||
--spoiler-hover: #111111;
|
||||
--spoiler-open-bg: #1e1e1e;
|
||||
--unknown-file-bg: #141316;
|
||||
--unknown-file-border: #474555;
|
||||
--login-border: #131315;
|
||||
--loading-bg: #22232c;
|
||||
--dialog-bg: #33363d;
|
||||
--dialog-border: #1c1b31;
|
||||
--scrollbar-track: #34313c;
|
||||
--scrollbar-thumb: #201f29;
|
||||
--scrollbar-thumb-hover: #16161f;
|
||||
--markdown-timestamp: #2f2f33;
|
||||
--embed: color-mix(in srgb, #131313 90%, var(--accent-color));
|
||||
--link: #8888ff;
|
||||
--discovery-bg: #37373b;
|
||||
--message-jump:#7678b0;
|
||||
--icon-color:white;
|
||||
--server-list: color-mix(in srgb, #1d1d1d 90%, var(--accent-color));
|
||||
}
|
||||
.WHITE-theme {
|
||||
color-scheme: light;
|
||||
--primary-text: #000;
|
||||
--primary-bg: #FFF;
|
||||
--black: #FFF;
|
||||
--red: #dd6c6c;
|
||||
--green: #639d63;
|
||||
--shadow: #777;
|
||||
--focus: #47bbff;
|
||||
--message-bg-hover: #dedee2;
|
||||
--typing-bg: #dad8d8;
|
||||
--timestamp-color: #929297;
|
||||
--code-bg: #cbcbcc;
|
||||
--user-info-bg: #b0abc2;
|
||||
--user-dock-bg: #b2b2c4;
|
||||
--channels-bg: #cbcbd8;
|
||||
--channel-hover: #b8b5cc;
|
||||
--blank-bg: #ceccdd;
|
||||
--light-border: #96959e;
|
||||
--settings-hover: #b5b1bb;
|
||||
--quote-bg: #7a798e;
|
||||
--button-bg: #b7b7cc;
|
||||
--button-hover: #FFF;
|
||||
--textarea-bg: #b1b6ce;
|
||||
--filename: #47bbff;
|
||||
--mention-bg: #F00;
|
||||
--mention-md-bg: #3b588b;
|
||||
--pronouns: #6a6a6d;
|
||||
--profile-bg: #cacad8;
|
||||
--profile-info-bg: #bbbbce;
|
||||
--server-border: #bebed3;
|
||||
--channel-name-bg: #c0c0d4;
|
||||
--server-name-bg: #a3a3b5;
|
||||
--reply-border: #b1b2bd;
|
||||
--reply-bg: #d4d6e9;
|
||||
--reply-text: #2e2e30;
|
||||
--spoiler-hover: #b9b9b9;
|
||||
--spoiler-open-bg: #dadada;
|
||||
--unknown-file-bg: #bdbdbd;
|
||||
--unknown-file-border: #adadad;
|
||||
--login-border: #c3c0e0;
|
||||
--loading-bg: #b5b7cc;
|
||||
--dialog-bg: #c1c8d6;
|
||||
--dialog-border: #b9b7db;
|
||||
--scrollbar-track: #d5d1e2;
|
||||
--scrollbar-thumb: #b0afc0;
|
||||
--scrollbar-thumb-hover: #a5a5b8;
|
||||
--markdown-timestamp: #c8c8da;
|
||||
--embed: #f2f3f5;
|
||||
--link: #3333ee;
|
||||
--discovery-bg: #c6c6d8;
|
||||
--message-jump:#ccceff;
|
||||
--icon-color:black;
|
||||
}
|
||||
.Light-theme {
|
||||
color-scheme: light;
|
||||
|
||||
--primary-text: #000;
|
||||
--primary-bg: #8e90c3;
|
||||
--black: #fff;
|
||||
--shadow: #000;
|
||||
--focus: #5e50c5;
|
||||
|
||||
--message-bg-hover: #5757b5;
|
||||
--typing-bg: #d4d6e9;
|
||||
--profile-bg: #8075bf;
|
||||
--profile-info-bg: #8075bf;
|
||||
--timestamp-color: #000000;
|
||||
--code-bg: #a89adf;
|
||||
--info-bg: #6060a3;
|
||||
--user-info-bg: #796f9a;
|
||||
--user-dock-bg: #83839d;
|
||||
--channels-bg: #c2c2d1;
|
||||
--channel-hover: #726e88;
|
||||
--blank-bg: #5e50c5;
|
||||
--light-border: #000000;
|
||||
--settings-hover: #b5b1bb;
|
||||
--quote-bg: #7a798e;
|
||||
--button-bg: #5757b5;
|
||||
--button-hover: #8e90c3;
|
||||
--textarea-bg: #abb1cd;
|
||||
--filename: #47bbff;
|
||||
--mention-bg: #F00;
|
||||
--mention-md-bg: #3b588b;
|
||||
--pronouns: #202020;
|
||||
--channel-name-bg: #c0c0d4;
|
||||
--server-name-bg: #a3a3b5;
|
||||
|
||||
--server-border: #aaaac4;
|
||||
--server-hover: #7f7fa8;
|
||||
|
||||
--reply-border: #474b76;
|
||||
--reply-bg: #d4d6e9;
|
||||
--reply-text: #38383d;
|
||||
|
||||
--spoiler-hover: #34333a;
|
||||
--spoiler-open-bg: #767587;
|
||||
|
||||
--unknown-file-bg: #bdbdbd;
|
||||
--unknown-file-border: #adadad;
|
||||
|
||||
--login-border: #c3c0e0;
|
||||
--loading-bg: #b5b7cc;
|
||||
|
||||
--dialog-bg: #c1c8d6;
|
||||
--dialog-border: #b9b7db;
|
||||
|
||||
--scrollbar-track: #d2cedf;
|
||||
--scrollbar-thumb: #bdbcca;
|
||||
--scrollbar-thumb-hover: #a7a7be;
|
||||
--markdown-timestamp: #c8c8da;
|
||||
--embed: #cdccd1;
|
||||
--link: #5566cc;
|
||||
--discovery-bg: #c6c6d8;
|
||||
--message-jump:#ccceff;
|
||||
--icon-color:black;
|
||||
}
|
489
src/webpage/user.ts
Normal 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 };
|