Files
Fermi/src/webpage/localuser.ts
T

5574 lines
160 KiB
TypeScript

import {Guild} from "./guild.js";
import {Channel} from "./channel.js";
import {Direct, Group} from "./direct.js";
import {User} from "./user.js";
import {createImg, getapiurls, getBulkUsers, getInstances, installPGet, SW} from "./utils/utils.js";
import {getBulkInfo, setTheme, Specialuser} from "./utils/utils.js";
import {
channeljson,
expSessionJson,
guildFolder,
mainuserjson,
memberjson,
memberlistupdatejson,
messageCreateJson,
messagejson,
ConnectionJson,
presencejson,
readStateEntry,
readyjson,
startTypingjson,
wsjson,
pollUpdateJson,
} from "./jsontypes.js";
import {Member} from "./member.js";
import {buttonColor, Dialog, Form, FormError, Options, Settings} from "./settings.js";
import {getTextNodeAtPosition, MarkDown, saveCaretPosition} from "./markdown.js";
import {Bot} from "./bot.js";
import {Role} from "./role.js";
import {VoiceFactory, voiceStatusStr} from "./voice.js";
import {I18n, langmap} from "./i18n.js";
import {Emoji} from "./emoji.js";
import {Play} from "./audio/play.js";
import {Message} from "./message.js";
import {badgeArr} from "./Dbadges.js";
import {Rights} from "./rights.js";
import {Contextmenu} from "./contextmenu.js";
import {Sticker} from "./sticker.js";
import {Hover} from "./hover.js";
import {AccountSwitcher} from "./utils/switcher.js";
import {Favorites} from "./favorites.js";
import {
AnimateTristateValues,
getPreferences,
setPreferences,
ThemeOption,
} from "./utils/storage/userPreferences";
import {getDeveloperSettings, setDeveloperSettings} from "./utils/storage/devSettings";
import {getLocalSettings, ServiceWorkerModeValues} from "./utils/storage/localSettings.js";
import {PromiseLock} from "./utils/promiseLock.js";
import {CDNParams} from "./utils/cdnParams.js";
import {SnowFlake} from "./snowflake.js";
import {trimTrailingSlashes} from "./utils/netUtils.js";
import {Versions} from "./versions.js";
import {Shortcut} from "./shortcuts/shortcut.js";
import {getShortcuts, setShortcuts} from "./utils/storage/shortcuts.js";
type traceObj = {
micros: number;
calls?: (string | traceObj)[];
};
type trace = [string, traceObj];
const wsCodesRetry = new Set([4000, 4001, 4002, 4003, 4005, 4007, 4008, 4009]);
interface CustomHTMLDivElement extends HTMLDivElement {
markdown: MarkDown;
}
interface MDSearchOption {
name: string;
replace: string;
icon?: HTMLElement;
otherLogic?: () => boolean;
}
MarkDown.emoji = Emoji;
class Localuser {
readonly badges = new Map<
string,
{id: string; description: string; icon: string; link?: string; translate?: boolean}
>(badgeArr);
lastSequence: number | null = null;
get token() {
return this.headers.Authorization;
}
userinfo!: Specialuser;
initialized!: boolean;
get info() {
return this.userinfo.serverurls;
}
set info(e) {
this.userinfo.serverurls = e;
}
headers!: {"Content-type": string; Authorization: string};
user!: User;
readonly guilds = new Map<string, Guild>();
readonly channels: Map<string, Channel> = new Map();
readonly userMap: Map<string, User> = new Map();
readonly messages = new Map<string, Message>();
readonly idToPrev = new Map<string, string | undefined>();
readonly idToNext = new Map<string, string | undefined>();
static readonly globalShortcuts = new Shortcut();
get status() {
return this.user.status;
}
set status(status: string) {
this.user.setstatus(status);
}
focusChannel?: Channel;
focusGuild?: Guild;
private ws?: WebSocket;
private connectionSucceed = 0;
private errorBackoff = 0;
voiceFactory?: VoiceFactory;
play?: Play;
instancePing = {
name: "Unknown",
};
mfa_enabled!: boolean;
get perminfo() {
return this.userinfo.localuserStore;
}
set perminfo(e) {
this.userinfo.localuserStore = e;
}
static users = getBulkUsers();
static async showAccountSwitcher(thisUser?: Localuser) {
const specialUser = await new AccountSwitcher().show();
const onswap = thisUser?.onswap;
thisUser?.unload();
if (thisUser) thisUser.swapped = true;
const loading = document.getElementById("loading") as HTMLDivElement;
loading.classList.remove("doneloading");
loading.classList.add("loading");
thisUser = new Localuser(specialUser);
Localuser.users.currentuser = specialUser.uid;
sessionStorage.setItem("currentuser", specialUser.uid);
localStorage.setItem("userinfos", JSON.stringify(Localuser.users));
thisUser.initwebsocket().then(async () => {
const loaddesc = document.getElementById("load-desc") as HTMLElement;
thisUser.loaduser();
await thisUser.init();
loading.classList.add("doneloading");
loaddesc.textContent = I18n.loaded();
loading.classList.remove("loading");
console.log("done loading");
});
onswap?.(thisUser);
}
static userMenu = this.generateUserMenu();
private readonly userResMap = new Map<string, Promise<User>>();
async getUser(id: string) {
let user = this.userMap.get(id);
if (user) return user;
const cache = this.userResMap.get(id);
if (cache) return cache;
const prom = User.resolve(id, this);
this.userResMap.set(id, prom);
await prom;
this.userResMap.delete(id);
return prom;
}
static generateUserMenu() {
const menu = new Contextmenu<Localuser, void>("");
menu.addButton(
() => I18n.localuser.addStatus(),
function () {
const d = new Dialog(I18n.localuser.status());
const opt = d.float.options.addForm(
"",
() => {
const status = cust.value;
sessionStorage.setItem("cstatus", JSON.stringify({text: status}));
//this.user.setstatus(status);
d.hide();
},
{
fetchURL: this.info.api + "/users/@me/settings",
method: "PATCH",
headers: this.headers,
},
);
opt.addText(I18n.localuser.customStatusWarn());
opt.addPreprocessor((obj) => {
if ("custom_status" in obj) {
obj.custom_status = {text: obj.custom_status};
}
});
const cust = opt.addTextInput(I18n.localuser.status(), "custom_status", {});
d.show();
},
);
menu.addButton(
() => I18n.localuser.status(),
function () {
const d = new Dialog(I18n.localuser.status());
const opt = d.float.options;
const selection = ["online", "invisible", "dnd", "idle"] as const;
const smap = selection.map((_) => I18n.user[_]());
let index = selection.indexOf(this.status as "online" | "invisible" | "dnd" | "idle");
if (index === -1) {
index = 0;
}
opt
.addSelect("", () => {}, smap, {
defaultIndex: index,
})
.watchForChange(async (i) => {
const status = selection[i];
await fetch(this.info.api + "/users/@me/settings", {
body: JSON.stringify({
status,
}),
headers: this.headers,
method: "PATCH",
});
sessionStorage.setItem("status", status);
this.user.setstatus(status);
});
d.show();
},
);
menu.addButton(
() => I18n.switchAccounts(),
function () {
Localuser.showAccountSwitcher(this);
},
);
return menu;
}
async searchMembers(limit: number, query: string, guild: Guild): Promise<Member[]> {
if (guild.id !== "@me") {
return new Promise<Member[]>((res) => {
const nonce = Math.floor(Math.random() * 10 ** 8) + "";
this.ws!.send(
JSON.stringify({
op: 8,
d: {
guild_id: [guild.id],
query,
limit,
presences: true,
nonce,
},
}),
);
this.searchMap.set(nonce, async (e) => {
console.log(e);
if (e.members && e.members[0]) {
if (e.members[0].user) {
res(
(await Promise.all(e.members.map(async (_) => await Member.new(_, guild)))).filter(
(_) => _ !== undefined,
),
);
} else {
const prom1: Promise<User>[] = [];
for (const thing of e.members) {
prom1.push(this.getUser(thing.id));
}
await Promise.all(prom1);
res(
(await Promise.all(e.members.map(async (_) => await Member.new(_, guild)))).filter(
(_) => _ !== undefined,
),
);
}
}
return [];
});
});
}
return [];
}
onswap?: (l: Localuser) => void;
constructor(userinfo: Specialuser) {
Play.playURL("/audio/sounds.jasf").then((_) => {
this.play = _;
});
this.userinfo = userinfo;
this.perminfo.guilds ??= {};
this.perminfo.user ??= {};
this.perminfo.user.decorations ??= true;
this.initialized = false;
SW.postMessage({
code: "canRefresh",
host: new URL(this.info.cdn).host,
});
SW.captureEvent("refreshURL", async (e) => {
SW.postMessage({
code: "refreshedUrl",
url: await this.refreshURL(e.url),
oldurl: e.url,
});
});
this.headers = {
"Content-type": "application/json; charset=UTF-8",
Authorization: this.userinfo.token,
};
this.favorites = new Favorites(this);
const rights = this.perminfo.user.rights || "875069521787904";
this.rights = new Rights(rights);
if (this.perminfo.user.disableColors === undefined) this.perminfo.user.disableColors = true;
this.updateTranslations();
Versions.makeVersion(this.info.api, "start");
}
favorites!: Favorites;
readysup = false;
get voiceAllowed() {
return this.readysup;
}
mute = true;
deaf = false;
updateOtherMic = () => {};
updateMic(updateVoice: boolean = true) {
this.updateOtherMic();
const mic = document.getElementById("mic") as HTMLElement;
mic.classList.remove("svg-mic", "svg-micmute");
if (this.voiceFactory && updateVoice) this.voiceFactory.mute = this.mute;
if (this.mute) {
mic.classList.add("svg-micmute");
} else {
mic.classList.add("svg-mic");
}
}
trace: {trace: trace; time: Date}[] = [];
handleTrace(str: string[]) {
const json = str.map((_) => JSON.parse(_)) as trace[];
console.log(json);
this.trace.push(
...json.map((trace) => {
return {trace, time: new Date()};
}),
);
}
async queryBlog() {
this.perminfo.localuser ??= {};
const prefs = getPreferences();
const bstate = prefs.showBlogUpdates;
if (bstate === undefined) {
const pop = new Dialog("");
pop.options.addText(I18n.blog.wantUpdates());
const opts = pop.options.addOptions("", {ltr: true});
opts.addButtonInput("", I18n.yes(), async () => {
prefs.showBlogUpdates = true;
await setPreferences(prefs);
this.queryBlog();
pop.hide();
});
opts.addButtonInput("", I18n.no(), async () => {
prefs.showBlogUpdates = false;
await setPreferences(prefs);
this.queryBlog();
pop.hide();
});
pop.show();
} else if (bstate) {
const post = (await this.getPosts()).items[0];
if (this.perminfo.localuser.mostRecent !== post.url) {
this.perminfo.localuser.mostRecent = post.url;
const pop = new Dialog(post.title);
//TODO implement images for the rendering of this
pop.options.addText(post.content_html);
pop.options.addButtonInput("", I18n.blog.gotoPost(), () => {
window.open(post.url);
pop.hide();
});
pop.show();
}
}
}
guildFolders: guildFolder[] = [];
readonly unknownRead = new Map<string, readStateEntry>();
async gottenReady(ready: readyjson): Promise<void> {
this.getGifProvidors();
await I18n.done;
this.errorBackoff = 0;
this.queryBlog();
this.guildFolders = ready.d.user_settings.guild_folders;
document.body.style.setProperty("--view-rest", I18n.message.viewrest());
this.initialized = true;
this.guilds.clear();
this.channels.clear();
this.userResMap.clear();
this.inrelation.clear();
this.userMap.clear();
this.user = new User(ready.d.user, this);
this.user.setstatus(sessionStorage.getItem("status") || "online");
this.resume_gateway_url = ready.d.resume_gateway_url;
this.session_id = ready.d.session_id;
this.mdBox();
this.voiceFactory = new VoiceFactory(
{id: this.user.id},
(g) => {
if (this.ws) {
this.ws.send(JSON.stringify(g));
}
},
this.info.api.startsWith("https://"),
);
this.handleVoice();
this.mfa_enabled = ready.d.user.mfa_enabled as boolean;
this.userinfo.username = this.user.username;
this.userinfo.id = this.user.id;
this.userinfo.pfpsrc = this.user.getpfpsrc();
if (ready.d.auth_token) {
this.userinfo.token = ready.d.auth_token;
this.userinfo.json.token = ready.d.auth_token;
this.headers.Authorization = ready.d.auth_token;
this.userinfo.updateLocal();
}
this.status = ready.d.user_settings.status;
this.focusChannel = undefined;
this.focusGuild = undefined;
const members: {[key: string]: memberjson} = {};
if (ready.d.merged_members) {
for (const thing of ready.d.merged_members) {
members[thing[0].guild_id] = thing[0];
}
}
this.updateMic();
const mic = document.getElementById("mic") as HTMLElement;
mic.onclick = () => {
this.mute = !this.mute;
this.updateMic();
};
for (const thing of ready.d.guilds) {
const temp = new Guild(thing, this, members[thing.id]);
this.guilds.set(temp.id, temp);
}
{
const temp = new Direct(ready.d.private_channels, this);
this.guilds.set(temp.id, temp);
}
if (ready.d.user_guild_settings) {
console.log(ready.d.user_guild_settings.entries);
for (const thing of ready.d.user_guild_settings.entries) {
(this.guilds.get(thing.guild_id) as Guild).notisetting(thing);
}
}
if (ready.d.read_state) {
for (const thing of ready.d.read_state.entries) {
const channel = this.channels.get(thing.channel_id);
if (!channel) {
this.unknownRead.set(thing.channel_id, thing);
continue;
}
channel.readStateInfo(thing);
}
}
for (const relationship of ready.d.relationships) {
const user = new User(relationship.user, this);
user.handleRelationship(relationship);
}
this.pingEndpoint();
const prefs = getPreferences();
const ml = document.getElementById("memberlisttoggle")!;
if (prefs.checkMemberList) {
ml.classList = "";
} else {
ml.classList = "svgicon svg-friends";
}
}
readonly inrelation = new Set<User>();
outoffocus(): void {
const servers = document.getElementById("guildRail") as HTMLDivElement;
servers.innerHTML = "";
const channels = document.getElementById("channels") as HTMLDivElement;
channels.innerHTML = "";
if (this.focusChannel) {
this.focusChannel.infinite.delete();
}
this.focusGuild = undefined;
this.focusChannel = undefined;
}
giveMessage(m: messagejson) {
const c = this.channels.get(m.channel_id);
if (!c) return;
new Message(m, c);
}
unload(): void {
this.initialized = false;
this.outoffocus();
this.guilds.clear();
this.channels.clear();
this.userResMap.clear();
if (this.ws) {
this.ws.close(4040);
}
}
swapped = false;
resume_gateway_url?: string;
session_id?: string;
async initwebsocket(resume = false): Promise<void> {
let returny: () => void;
if (!this.resume_gateway_url || !this.session_id) {
resume = false;
}
if (!resume) {
this.messages.clear();
this.idToPrev.clear();
this.idToNext.clear();
}
const doComp = DecompressionStream && !getDeveloperSettings().gatewayCompression;
const ws = new WebSocket(
(resume ? this.resume_gateway_url : this.info.gateway.toString()) +
"?encoding=json&v=9" +
(doComp ? "&compress=zlib-stream" : ""),
);
this.ws = ws;
let ds: DecompressionStream;
let w: WritableStreamDefaultWriter;
let arr: Uint8Array;
if (DecompressionStream) {
ds = new DecompressionStream("deflate");
w = ds.writable.getWriter();
arr = new Uint8Array();
}
const promise = new Promise<void>((res) => {
returny = res;
ws.addEventListener("open", (_event) => {
console.log("WebSocket connected");
if (resume) {
ws.send(
JSON.stringify({
op: 6,
d: {
token: this.token,
session_id: this.session_id,
seq: this.lastSequence,
},
}),
);
this.resume_gateway_url = undefined;
this.session_id = undefined;
} else {
ws.send(
JSON.stringify({
op: 2,
d: {
token: this.token,
capabilities: 16381,
properties: {
browser: "Fermi",
client_build_number: 0, //might update this eventually lol
release_channel: "Custom",
browser_user_agent: navigator.userAgent,
},
compress: Boolean(DecompressionStream),
presence: {
status: sessionStorage.getItem("status") || "online",
since: null, //new Date().getTime()
activities: [],
afk: false,
}, //TODO think this through, it's just a stupid large number to fix op 8 requests
large_threshold: 100000000,
},
}),
);
}
});
if (DecompressionStream) {
(async () => {
let build = "";
for await (const data of ds.readable.pipeThrough(new TextDecoderStream())) {
build += data;
try {
const temp = JSON.parse(build);
build = "";
await this.handleEvent(temp);
if (temp.op === 0 && temp.t === "READY") {
console.log("in here?");
returny();
}
} catch (e) {
if (!(e instanceof SyntaxError)) {
console.error(e);
}
}
}
})();
}
});
let order = new Promise<void>((res) => res());
ws.addEventListener("message", async (event) => {
const temp2 = order;
order = new Promise<void>(async (res) => {
await temp2;
let temp: {op: number; t: string};
try {
if (event.data instanceof Blob) {
const buff = await event.data.arrayBuffer();
const array = new Uint8Array(buff);
const temparr = new Uint8Array(array.length + arr.length);
temparr.set(arr, 0);
temparr.set(array, arr.length);
arr = temparr;
const len = array.length;
if (
!(
array[len - 1] === 255 &&
array[len - 2] === 255 &&
array[len - 3] === 0 &&
array[len - 4] === 0
)
) {
return;
}
w.write(arr.buffer);
arr = new Uint8Array();
return; //had to move the while loop due to me being dumb
} else {
temp = JSON.parse(event.data);
}
await this.handleEvent(temp as readyjson);
if (temp.op === 0 && temp.t === "READY") {
returny();
}
} catch (e) {
console.error(e);
} finally {
res();
}
});
});
ws.addEventListener("close", async (event) => {
this.ws = undefined;
this.voiceFactory?.close();
console.log("WebSocket closed with code " + event.code);
if (
(event.code > 1000 && event.code < 1016 && this.errorBackoff === 0) ||
(wsCodesRetry.has(event.code) && this.errorBackoff === 0)
) {
this.errorBackoff++;
this.initwebsocket(true).then(() => {
this.loaduser();
});
return;
}
this.unload();
(document.getElementById("loading") as HTMLElement).classList.remove("doneloading");
(document.getElementById("loading") as HTMLElement).classList.add("loading");
this.fetchingmembers.clear();
this.memberNonceMap.clear();
this.memberNonceBuild.clear();
const loaddesc = document.getElementById("load-desc") as HTMLElement;
if (
(event.code > 1000 && event.code < 1016) ||
wsCodesRetry.has(event.code) ||
event.code == 4041
) {
if (this.connectionSucceed !== 0 && Date.now() > this.connectionSucceed + 20000) {
this.errorBackoff = 0;
} else this.errorBackoff++;
this.connectionSucceed = 0;
loaddesc.innerHTML = "";
loaddesc.append(
new MarkDown(
I18n.errorReconnect(Math.round(0.2 + this.errorBackoff * 2.8) + ""),
).makeHTML(),
);
switch (
this.errorBackoff //try to recover from bad domain
) {
case 3:
const newurls = await getapiurls(this.info.wellknown);
if (newurls) {
this.info = newurls;
this.userinfo.json.serverurls = this.info;
break;
}
break;
case 4: {
const newurls = await getapiurls(new URL(this.info.wellknown).origin);
if (newurls) {
this.info = newurls;
this.userinfo.json.serverurls = this.info;
break;
}
break;
}
case 5: {
const breakappart = new URL(this.info.wellknown).host.split(".");
const url = "https://" + breakappart.at(-2) + "." + breakappart.at(-1);
const newurls = await getapiurls(url);
if (newurls) {
this.info = newurls;
this.userinfo.json.serverurls = this.info;
}
break;
}
}
setTimeout(
() => {
if (this.swapped) return;
loaddesc.textContent = I18n.retrying();
this.initwebsocket().then(async () => {
console.log("FINE ME");
this.loaduser();
await this.init();
const loading = document.getElementById("loading") as HTMLElement;
loading.classList.add("doneloading");
loading.classList.remove("loading");
loaddesc.textContent = I18n.loaded();
console.log("done loading");
});
},
200 + this.errorBackoff * 2800,
);
} else loaddesc.textContent = I18n.unableToConnect();
});
console.log("here?");
await promise;
console.warn("huh");
}
readonly interNonceMap = new Map<string, Message>();
registerInterNonce(nonce: string, thing: Message) {
this.interNonceMap.set(nonce, thing);
}
relationshipsUpdate = () => {};
rights: Rights;
updateRights(rights: string | number) {
if (this.rights.isSameAs(rights)) return;
this.rights.update(rights);
this.perminfo.user.rights = rights;
}
traceSub() {
SW.captureEvent("trace", (e) => {
this.handleTrace(e.trace);
});
}
readonly relChangeUpdateMap = new Map<string, (() => void)[]>();
async relationChange(id: string): Promise<void> {
const arr = this.relChangeUpdateMap.get(id) || [];
const {promise, resolve} = Promise.withResolvers<void>();
arr.push(resolve);
this.relChangeUpdateMap.set(id, arr);
return promise;
}
conectionChange = () => {};
async handleEvent(temp: wsjson) {
try {
window.checker?.checkEvent(temp);
} catch (e) {
console.error(e);
}
if (temp.d._trace) this.handleTrace(temp.d._trace);
if (getDeveloperSettings().gatewayLogging) console.debug(temp);
if (temp.s) this.lastSequence = temp.s;
if (temp.op === 9 && this.ws) {
this.errorBackoff = 0;
this.ws.close(4041);
}
if (temp.op == 0) {
switch (temp.t) {
case "THREAD_MEMBERS_UPDATE": {
const channel = this.channels.get(temp.d.id);
if (!channel) return;
if (temp.d.added_members) {
for (const memb of temp.d.added_members) {
if (memb.user_id === this.user.id) {
channel.member = memb;
channel.parent?.createguildHTML();
} else {
//TODO store these somewhere
}
}
}
if (temp.d.removed_member_ids) {
for (const id of temp.d.removed_member_ids) {
if (id === this.user.id) {
channel.member = undefined;
channel.parent?.createguildHTML();
} else {
//TODO unstore these somewhere
}
}
}
break;
}
case "INTERACTION_FAILURE":
case "INTERACTION_CREATE":
case "INTERACTION_SUCCESS":
const m = this.interNonceMap.get(temp.d.nonce);
if (m) {
//Punt the events off to the message class
m.interactionEvents(temp);
}
break;
case "MESSAGE_CREATE":
if (this.initialized) {
this.messageCreate(temp);
}
break;
case "USER_NOTE_UPDATE": {
const u = this.userMap.get(temp.d.id);
if (u) u.note = temp.d.note;
break;
}
case "USER_CONNECTIONS_UPDATE": {
this.conectionChange();
break;
}
case "MESSAGE_POLL_VOTE_ADD":
case "MESSAGE_POLL_VOTE_REMOVE": {
const m = this.messages.get(temp.d.message_id);
m?.pollUpdate(temp);
const f = this.pollUpdateSubMap.get(temp.d.message_id);
f?.(temp);
break;
}
case "MESSAGE_DELETE": {
temp.d.guild_id ??= "@me";
const channel = this.channels.get(temp.d.channel_id);
if (!channel) break;
const message = channel.messages.get(temp.d.id);
if (!message) break;
message.deleteEvent();
break;
}
case "READY":
await this.gottenReady(temp as readyjson);
break;
case "MESSAGE_UPDATE": {
temp.d.guild_id ??= "@me";
const channel = this.channels.get(temp.d.channel_id);
if (!channel) break;
const message = channel.messages.get(temp.d.id);
if (!message) break;
message.giveData(temp.d);
break;
}
case "TYPING_START":
if (this.initialized) {
this.typingStart(temp);
}
break;
case "USER_UPDATE":
if (this.initialized) {
const users = this.userMap.get(temp.d.id);
if (users) {
users.userupdate(temp.d);
}
}
break;
case "CHANNEL_PINS_UPDATE":
temp.d.guild_id ??= "@me";
const channel = this.channels.get(temp.d.channel_id);
if (!channel) break;
delete channel.pinnedMessages;
channel.lastpin = new Date() + "";
const pinnedM = document.getElementById("pinnedMDiv");
if (pinnedM) {
pinnedM.classList.add("unreadPin");
}
break;
case "CHANNEL_UPDATE":
if (this.initialized) {
this.updateChannel(temp.d);
}
break;
case "CHANNEL_CREATE":
case "THREAD_CREATE":
if (this.initialized) {
this.createChannel(temp.d);
}
break;
case "CHANNEL_DELETE":
if (this.initialized) {
this.delChannel(temp.d);
}
break;
case "GUILD_DELETE": {
const guildy = this.guilds.get(temp.d.id);
if (guildy) {
this.guilds.delete(temp.d.id);
guildy.html.remove();
if (guildy === this.focusGuild) {
this.guilds.get("@me")?.loadGuild();
this.guilds.get("@me")?.loadChannel();
}
}
break;
}
case "GUILD_UPDATE": {
const guildy = this.guilds.get(temp.d.id);
if (guildy) {
guildy.update(temp.d);
}
break;
}
case "GUILD_CREATE":
(async () => {
const m = temp.d.members.find(({id}) => id === this.user.id);
const guildy = new Guild(temp.d, this, m ?? this.user);
this.guilds.set(guildy.id, guildy);
for (const m of temp.d.members) {
Member.newUnsafe(m, guildy);
}
const divy = this.makeGuildIcon(guildy);
guildy.HTMLicon = divy;
(document.getElementById("guildRail") as HTMLDivElement).insertBefore(
divy,
document.getElementById("bottomseparator"),
);
guildy.messageNotifications = guildy.properties.default_message_notifications;
guildy.showWelcome();
})();
break;
case "MESSAGE_REACTION_ADD":
{
temp.d.guild_id ??= "@me";
const guild = this.guilds.get(temp.d.guild_id);
if (!guild) break;
const channel = this.channels.get(temp.d.channel_id);
if (!channel) break;
const message = channel.messages.get(temp.d.message_id);
if (!message) break;
let thing: Member | {id: string};
if (temp.d.member) {
thing = (await Member.new(temp.d.member, guild)) as Member;
} else {
thing = {id: temp.d.user_id};
}
message.reactionAdd(temp.d.emoji, thing);
}
break;
case "MESSAGE_REACTION_REMOVE":
{
temp.d.guild_id ??= "@me";
const channel = this.channels.get(temp.d.channel_id);
if (!channel) break;
const message = channel.messages.get(temp.d.message_id);
if (!message) break;
message.reactionRemove(temp.d.emoji, temp.d.user_id);
}
break;
case "MESSAGE_REACTION_REMOVE_ALL":
{
temp.d.guild_id ??= "@me";
const channel = this.channels.get(temp.d.channel_id);
if (!channel) break;
const message = channel.messages.get(temp.d.message_id);
if (!message) break;
message.reactionRemoveAll();
}
break;
case "MESSAGE_REACTION_REMOVE_EMOJI":
{
temp.d.guild_id ??= "@me";
const channel = this.channels.get(temp.d.channel_id);
if (!channel) break;
const message = channel.messages.get(temp.d.message_id);
if (!message) break;
message.reactionRemoveEmoji(temp.d.emoji);
}
break;
case "GUILD_MEMBERS_CHUNK":
this.gotChunk(temp.d);
break;
case "GUILD_MEMBER_LIST_UPDATE": {
this.memberListUpdate(temp);
break;
}
case "READY_SUPPLEMENTAL":
{
temp.d.guilds.forEach((_) =>
_.voice_states.forEach((status) => {
if (this.voiceFactory && status.channel_id) {
this.voiceFactory.voiceStateUpdate(status);
console.log(status);
}
}),
);
this.readysup = temp.d.guilds.length !== 0;
}
break;
case "VOICE_STATE_UPDATE":
if (this.user.id === temp.d.user_id) {
this.mute = temp.d.self_mute;
this.updateMic(false);
}
if (this.voiceFactory) {
this.voiceFactory.voiceStateUpdate(temp.d);
}
break;
case "STREAM_SERVER_UPDATE": {
if (this.voiceFactory) {
this.voiceFactory.streamServerUpdate(temp);
}
break;
}
case "STREAM_CREATE": {
if (this.voiceFactory) {
this.voiceFactory.streamCreate(temp);
}
break;
}
case "VOICE_SERVER_UPDATE":
if (this.voiceFactory) {
this.voiceFactory.voiceServerUpdate(temp);
}
break;
case "GUILD_ROLE_CREATE": {
const guild = this.guilds.get(temp.d.guild_id);
if (!guild) break;
guild.newRole(temp.d.role);
break;
}
case "GUILD_ROLE_UPDATE": {
const guild = this.guilds.get(temp.d.guild_id);
if (!guild) break;
guild.updateRole(temp.d.role);
break;
}
case "GUILD_ROLE_DELETE": {
const guild = this.guilds.get(temp.d.guild_id);
if (!guild) break;
guild.deleteRole(temp.d.role_id);
break;
}
case "GUILD_MEMBER_UPDATE": {
const guild = this.guilds.get(temp.d.guild_id);
if (!guild) break;
guild.memberupdate(temp.d);
break;
}
case "RELATIONSHIP_UPDATE":
case "RELATIONSHIP_ADD": {
(async () => {
const user = temp.d.user ? new User(temp.d.user, this) : await this.getUser(temp.d.id);
user.handleRelationship(temp.d);
this.relationshipsUpdate();
const me = this.guilds.get("@me");
if (!me) return;
me.unreads();
const arr = this.relChangeUpdateMap.get(user.id);
if (arr) {
arr.forEach((_) => _());
this.relChangeUpdateMap.delete(user.id);
}
})();
break;
}
case "RELATIONSHIP_REMOVE": {
const user = this.userMap.get(temp.d.id);
if (!user) return;
user.removeRelation();
this.relationshipsUpdate();
const arr = this.relChangeUpdateMap.get(user.id);
if (arr) {
arr.forEach((_) => _());
this.relChangeUpdateMap.delete(user.id);
}
break;
}
case "PRESENCE_UPDATE": {
if (temp.d.user) {
const user = new User(temp.d.user, this);
this.presences.set(temp.d.user.id, temp.d);
user.setstatus(temp.d.status);
if (user === this.user) this.loaduser();
}
break;
}
case "GUILD_MEMBER_ADD": {
const guild = this.guilds.get(temp.d.guild_id);
if (!guild) break;
Member.new(temp.d, guild);
break;
}
case "GUILD_MEMBER_REMOVE": {
const guild = this.guilds.get(temp.d.guild_id);
if (!guild) break;
const user = new User(temp.d.user, this);
const member = user.members.get(guild);
if (!(member instanceof Member)) break;
member.remove();
break;
}
case "GUILD_EMOJIS_UPDATE": {
const guild = this.guilds.get(temp.d.guild_id);
if (!guild) break;
guild.emojis = temp.d.emojis;
guild.onEmojiUpdate(guild.emojis);
break;
}
case "GUILD_STICKERS_UPDATE": {
const guild = this.guilds.get(temp.d.guild_id);
if (!guild) break;
guild.stickers.clear();
temp.d.stickers.forEach((sticker) =>
guild.stickers.set(sticker.id, new Sticker(sticker, guild)),
);
guild.onStickerUpdate(guild.stickers);
break;
}
case "CHANNEL_RECIPIENT_REMOVE": {
const guild = this.guilds.get("@me") as Direct;
const channel = guild.channels.find(({id}) => id == temp.d.channel_id) as Group;
if (!channel) break;
channel.removeRec(new User(temp.d.user, this));
break;
}
case "CHANNEL_RECIPIENT_ADD": {
const guild = this.guilds.get("@me") as Direct;
const channel = guild.channels.find(({id}) => id == temp.d.channel_id) as Group;
if (!channel) break;
channel.addRec(new User(temp.d.user, this));
break;
}
case "MESSAGE_ACK": {
const channel = this.channels.get(temp.d.channel_id);
if (!channel) break;
channel.lastreadmessageid = temp.d.message_id;
channel.mentions = 0;
channel.unreads();
channel.guild.unreads();
break;
}
default: {
//@ts-expect-error
console.warn("Unhandled case " + temp.t, temp);
}
}
this.generateFavicon();
} else if (temp.op === 10) {
if (!this.ws) return;
console.log("heartbeat down");
this.heartbeat_interval = temp.d.heartbeat_interval;
this.ws.send(JSON.stringify({op: 1, d: this.lastSequence}));
} else if (temp.op === 11) {
setTimeout((_: any) => {
if (!this.ws) return;
const reasons = this.generateReasons();
if (this.connectionSucceed === 0) this.connectionSucceed = Date.now();
this.ws.send(
JSON.stringify({
op: 40,
d: {seq: this.lastSequence, qos: {ver: 27, active: !!reasons.length, reasons}},
}),
);
}, this.heartbeat_interval);
} else {
console.log("Unhandled case " + temp.d, temp);
}
}
generateReasons() {
const reasons: string[] = [];
if (!document.hidden) reasons.push("foregrounded");
return reasons;
}
setUserAudio(id: string, vol: number) {
if (!this.perminfo.userAudio) this.perminfo.userAudio = {};
this.perminfo.userAudio[id] = vol;
const v = this.voiceFactory?.currentVoice;
if (v) {
const u = v.uVolMap.get(id);
if (u) u.volume = vol / 100;
}
}
getUserAudio(id: string) {
if (!this.perminfo.userAudio) this.perminfo.userAudio = {};
return (this.perminfo.userAudio[id] as number) ?? 100;
}
get currentVoice() {
return this.voiceFactory?.currentVoice;
}
async getAudioDeviceList() {
return (await navigator.mediaDevices.enumerateDevices()).filter(
(dev) => dev.kind === "audioinput",
);
}
async setNewDefualtDevice(id: string) {
const devices = await this.getAudioDeviceList();
const d = devices.find((_) => _.deviceId === id);
if (!d) return;
const device = await navigator.mediaDevices.getUserMedia({
audio: {
deviceId: {exact: d.deviceId},
},
});
if (!device) return;
await this.voiceFactory?.currentVoice?.giveMicTrack(device);
this.perminfo.localuser.defaultAudio = id;
}
getDefaultAudio() {
return this.perminfo.localuser.defaultAudio || "default";
}
async joinVoice(channel: Channel) {
if (!this.voiceFactory) return;
if (!this.ws) return;
const v = this.voiceFactory.joinVoice(channel.id, channel.guild.id, this.mute);
if (this.perminfo.localuser.defaultAudio) {
const devices = await this.getAudioDeviceList();
const d = devices.find((_) => _.deviceId === this.perminfo.localuser.defaultAudio);
if (d) {
navigator.mediaDevices
.getUserMedia({
audio: {
deviceId: {exact: d.deviceId},
},
})
.then((_) => v.giveMicTrack(_));
return;
}
}
const d = await navigator.mediaDevices.getUserMedia({video: false, audio: true});
this.perminfo.localuser.defaultAudio = d.getAudioTracks()[0]?.id;
v.giveMicTrack(d);
return undefined;
}
regenVoiceIcons = () => {};
changeVCStatus(status: voiceStatusStr, channel: Channel) {
const statuselm = document.getElementById("VoiceStatus");
const VoiceGuild = document.getElementById("VoiceGuild");
const VoiceButtons = document.getElementById("VoiceButtons");
if (!statuselm || !VoiceGuild || !VoiceButtons) throw new Error("Missing status element");
statuselm.textContent = I18n.Voice.status[status]();
const guildName = document.createElement("span");
guildName.textContent = channel.guild.properties.name;
const channelName = document.createElement("span");
channelName.textContent = channel.name;
VoiceGuild.innerHTML = ``;
VoiceGuild.append(guildName, " / ", channelName);
VoiceGuild.onclick = () => {
this.goToChannel(channel.id);
};
VoiceButtons.innerHTML = "";
const leave = document.createElement("div");
const leaveIcon = document.createElement("span");
leaveIcon.classList.add("svg-hangup");
leave.append(leaveIcon);
leave.onclick = () => {
channel.voice?.leave();
};
const screenShare = document.createElement("div");
const screenShareIcon = document.createElement("span");
const updateStreamIcon = () => {
screenShareIcon.classList.remove("svg-stopstream", "svg-stream");
if (channel.voice?.isLive()) {
screenShareIcon.classList.add("svg-stopstream");
} else {
screenShareIcon.classList.add("svg-stream");
}
};
updateStreamIcon();
screenShare.append(screenShareIcon);
screenShare.onclick = async () => {
if (channel.voice?.isLive()) {
channel.voice.stopStream();
} else {
const stream = await navigator.mediaDevices.getDisplayMedia();
await channel.voice?.createLive(stream);
}
updateStreamIcon();
};
const video = document.createElement("div");
const videoIcon = document.createElement("span");
const updateVideoIconIcon = () => {
videoIcon.classList.remove("svg-novideo", "svg-video");
if (this.voiceFactory?.video) {
videoIcon.classList.add("svg-video");
} else {
videoIcon.classList.add("svg-novideo");
}
};
updateVideoIconIcon();
video.append(videoIcon);
video.onclick = async () => {
if (this.voiceFactory?.video) {
channel.voice?.stopVideo();
} else {
const cam = await navigator.mediaDevices.getUserMedia({
video: {
advanced: [
{
aspectRatio: 1.75,
},
],
},
});
if (!cam) return;
channel.voice?.startVideo(cam);
}
updateVideoIconIcon();
};
this.regenVoiceIcons = () => {
updateStreamIcon();
updateVideoIconIcon();
};
VoiceButtons.append(leave, video, screenShare);
const clear = () => {
statuselm.textContent = "";
VoiceGuild.textContent = "";
VoiceButtons.innerHTML = "";
};
const conSet = new Set(["notconnected"]);
if (conSet.has(status)) {
setTimeout(() => {
if (statuselm.textContent === I18n.Voice.status[status]()) {
clear();
}
}, 2000);
} else if (status === "left") {
clear();
}
}
handleVoice() {
if (this.voiceFactory) {
this.voiceFactory.onJoin = (voice) => {
voice.onSatusChange = (status) => {
let channel: Channel | undefined = this.channels.values().find((_) => _.voice === voice);
if (channel) this.changeVCStatus(status, channel);
else console.error("Uh, no channel found?");
};
};
}
}
heartbeat_interval: number = 0;
updateChannel(json: channeljson): void {
const guild = this.guilds.get(json.guild_id || "@me");
if (guild) {
guild.updateChannel(json);
if (json.guild_id === this.focusGuild?.id) {
this.loadGuild(json.guild_id);
}
}
}
async createChannel(json: channeljson): Promise<undefined | Channel> {
const c = this.channels.get(json.id);
if (c) {
c.updateChannel(json);
return c;
}
json.guild_id ??= "@me";
const guild = this.guilds.get(json.guild_id);
if (!guild) return;
if (guild.channels.find((_) => _.id === json.id)) return;
const channel = guild.createChannelpac(json);
if (json.guild_id === this.focusGuild?.id) {
this.loadGuild(json.guild_id, true);
}
if (channel.id === this.gotoid) {
guild.loadGuild();
if (channel?.type !== 4) {
await guild.loadChannel(channel.id);
}
this.gotoRes();
this.gotoRes = () => {};
this.gotoid = undefined;
}
return channel; // Add this line to return the 'channel' variable
}
listque = false;
memberListQue() {
if (this.listque) {
return;
}
this.listque = true;
setTimeout(async () => {
await this.memberListUpdate();
this.listque = false;
}, 100);
}
async memberListUpdate(list: memberlistupdatejson | void) {
if (this.searching) return;
const guild = this.focusGuild;
if (!guild) return;
const channel = this.focusChannel;
if (!channel) return;
if (channel.voice && this.voiceAllowed) {
const div = document.getElementById("sideDiv") as HTMLDivElement;
div.textContent = "";
return;
}
if (guild.id === "@me" && (channel as Group).type === 1) {
const div = document.getElementById("sideDiv") as HTMLDivElement;
div.textContent = "";
return;
}
if (list) {
if (list.d.guild_id !== guild.id) {
return;
}
const counts = new Map<string, number>();
await Promise.all(
list.d.ops[0].items.map(async (member) => {
if ("member" in member) {
if (this.userMap.get(member.member.id)?.members.has(guild)) return;
if (this.focusGuild?.id && this.focusGuild?.id !== "@me")
await this.getMember(member.member.user!.id, this.focusGuild.id);
} else {
counts.set(member.group.id, member.group.count);
}
}),
);
}
const elms: Map<Role | "offline" | "online", (Member | User)[]> = new Map([]);
for (const role of guild.roles) {
if (role.hoist) {
elms.set(role, []);
}
}
elms.set("online", []);
elms.set("offline", []);
let members = new Set<User | Member>(guild.members.values());
if (channel instanceof Group) {
members = new Set(channel.users);
members.add(this.user);
}
members.forEach((member) => {
if (member instanceof User) {
return;
}
if (!channel.hasPermission("VIEW_CHANNEL", member)) {
members.delete(member);
return;
}
});
for (const [role, list] of elms) {
members.forEach((member) => {
const user = member instanceof Member ? member.user : member;
if (role === "offline") {
if (user.getStatus() === "offline" || user.getStatus() === "invisible") {
list.push(member);
members.delete(member);
}
return;
}
if (user.getStatus() === "offline" || user.getStatus() === "invisible") {
return;
}
if (member instanceof Member) {
if (role !== "online" && member.hasRole(role.id)) {
list.push(member);
members.delete(member);
}
}
});
if (!list.length) continue;
list.sort((a, b) => {
return a.name.toLowerCase() > b.name.toLowerCase() ? 1 : -1;
});
}
const online = [...members];
online.sort((a, b) => {
return a.name.toLowerCase() > b.name.toLowerCase() ? 1 : -1;
});
elms.set("online", online);
this.generateListHTML(elms, channel);
}
instanceString(): string {
const insts = getInstances();
if (insts) {
for (const inst of insts) {
if (trimTrailingSlashes(inst.url ?? "") === trimTrailingSlashes(this.info.wellknown)) {
return inst.name;
}
}
}
console.log("failed with", insts, this.info.wellknown);
return this.info.wellknown;
}
readonly roleListMap = new WeakMap<
HTMLDivElement,
{
role: Role | "offline" | "online";
memberListMap: Map<HTMLElement, User>;
}
>();
listGuild?: Guild;
generateListHTML(elms: Map<Role | "offline" | "online", (Member | User)[]>, channel: Channel) {
const div = document.getElementById("sideDiv") as HTMLDivElement;
let roleMap = new Map<
Role | "offline" | "online",
{elm: HTMLDivElement; memberListMap: Map<HTMLElement, User>}
>();
if (channel.guild !== this.listGuild) {
this.listGuild = channel.guild;
div.innerHTML = "";
}
Array.from(div.children)
.map((_) => [this.roleListMap.get(_ as HTMLDivElement), _ as HTMLDivElement] as const)
.forEach(([role, elm]) => {
if (role && elms.get(role.role)?.length) {
if (document.contains(elm))
roleMap.set(role.role, {elm, memberListMap: role.memberListMap});
} else if (elm) {
elm.remove();
}
});
div.classList.remove("searchDiv");
div.classList.remove("hideSearchDiv");
let lastDiv: HTMLDivElement | void = undefined;
for (const [role, list] of elms) {
if (!list.length) continue;
let category: HTMLDivElement;
let memberMap: Map<HTMLElement, User>;
const getF = roleMap.get(role);
roleMap.delete(role);
if (getF) {
category = getF.elm;
memberMap = getF.memberListMap;
if (lastDiv) {
const nextElm = lastDiv.nextElementSibling as HTMLElement | null;
if (nextElm !== category) {
lastDiv.after(category);
}
} else {
const first = div.firstElementChild;
if (first !== category) {
div.prepend(category);
}
}
} else {
category = document.createElement("div");
category.classList.add("memberList");
let title = document.createElement("h3");
if (role === "offline") {
title.textContent = I18n.user.offline();
category.classList.add("offline");
} else if (role === "online") {
title.textContent = I18n.user.online();
} else {
title.textContent = role.name;
}
category.append(title);
const membershtml = document.createElement("div");
membershtml.classList.add("flexttb");
category.append(membershtml);
memberMap = new Map();
if (lastDiv) {
lastDiv.after(category);
} else {
div.prepend(category);
}
this.roleListMap.set(category, {
role,
memberListMap: memberMap,
});
}
lastDiv = category;
const membershtml = category.getElementsByTagName("div")[0];
const cur = new Set(
list.map((member) => {
return member instanceof Member ? member.user : member;
}),
);
const userToHTMLMap = new Map<User, HTMLElement>();
Array.from(membershtml.children)
.map((_) => [_ as HTMLElement, memberMap.get(_ as HTMLElement)] as const)
.forEach(([elm, memb]) => {
if (!memb || !cur.has(memb)) {
memberMap.delete(elm);
elm.remove();
} else {
userToHTMLMap.set(memb, elm);
}
});
const makeMemberDiv = (user: User, member: Member | User) => {
user.localstatusUpdate = () => {
this.memberListQue();
};
const memberdiv = document.createElement("div");
memberMap.set(memberdiv, user);
const pfp = user.buildstatuspfp(channel);
const username = document.createElement("span");
username.classList.add("ellipsis");
username.textContent = member.name;
member.subName(username);
if (user.bot) {
const bot = document.createElement("span");
bot.classList.add("bot");
bot.textContent = I18n.bot();
username.appendChild(bot);
}
memberdiv.append(pfp, username);
if (channel instanceof Group) {
Group.groupMenu.bindContextmenu(memberdiv, channel, user);
if (channel.owner_id === user.id) {
const crown = document.createElement("span");
crown.classList.add("svg-crown");
memberdiv.append(crown);
}
}
member.bind(username);
user.bind(memberdiv, member instanceof Member ? member.guild : undefined, false);
memberdiv.classList.add("flexltr", "liststyle", "memberListStyle");
return memberdiv;
};
let lastElm: void | HTMLElement = void 0;
for (const member of list) {
const user = member instanceof Member ? member.user : member;
let elm = userToHTMLMap.get(user);
if (!elm) {
elm = makeMemberDiv(user, member);
if (!lastElm) {
membershtml.append(elm);
} else {
lastElm.after(elm);
}
} else if (lastElm) {
//@ts-expect-error TS Bug, let me know when it's fixed :3
// https://github.com/microsoft/TypeScript/issues/62872
const nextElm = lastElm.nextElementSibling;
if (nextElm !== elm) {
lastElm.after(elm);
}
} else {
const first = membershtml.firstElementChild;
if (first !== elm) {
membershtml.prepend(elm);
}
}
lastElm = elm;
}
}
}
emojiPicker(x: number, y: number, guildEmojis = true) {
return Emoji.emojiPicker(x, y, guildEmojis ? this : undefined);
}
async getSidePannel() {
if (this.ws && this.focusChannel) {
console.log(this.focusChannel.guild.id);
this.memberListQue();
if (this.focusChannel.guild.id === "@me") {
return;
}
if (!this.focusChannel.visible) return;
this.ws.send(
JSON.stringify({
d: {
channels: {[this.focusChannel.id]: [[0, 99]]},
guild_id: this.focusChannel.guild.id,
},
op: 14,
}),
);
} else {
console.log("false? :3");
}
}
async goToState(state: [string, string | undefined, string | undefined]) {
const [guildid, channelid, messageid] = state;
if (!channelid) {
if (guildid === "@me") {
const dir = this.guilds.get("@me") as Direct;
dir.loadChannel(null, false);
}
return;
}
this.goToChannel(channelid, false, messageid);
}
gotoid: string | undefined;
gotoRes = () => {};
async goToChannel(channelid: string, addstate = true, messageid: undefined | string = undefined) {
const channel = this.channels.get(channelid);
if (channel?.type === 4) return;
if (channel) {
const guild = channel.guild;
guild.loadGuild();
await guild.loadChannel(channelid, addstate, messageid);
} else {
this.gotoid = channelid;
return new Promise<void>((res) => (this.gotoRes = res));
}
}
delChannel(json: channeljson): void {
let guild_id = json.guild_id;
guild_id ??= "@me";
const guild = this.guilds.get(guild_id);
if (guild) {
guild.delChannel(json);
}
if (json.guild_id === this.focusGuild?.id) {
this.loadGuild(json.guild_id, true);
}
}
async init() {
const location = window.location.href.split("/");
this.buildservers();
this.generateFavicon();
if (location[3] === "channels") {
const guild = this.loadGuild(location[4]);
if (!guild) {
return;
}
await guild.loadChannel(location[5], true, location[6]);
this.focusChannel = this.channels.get(location[5]);
}
}
loaduser(): void {
(document.getElementById("username") as HTMLSpanElement).textContent = this.user.username;
(document.getElementById("userpfp") as HTMLImageElement).src = this.user.getpfpsrc();
(document.getElementById("status") as HTMLSpanElement).textContent = this.status;
}
isAdmin(): boolean {
if (this.focusGuild) {
return this.focusGuild.member.isAdmin();
} else {
return false;
}
}
loadGuild(id: string, forceReload = false): Guild | undefined {
this.searching = false;
let guild = this.guilds.get(id);
if (!guild) {
guild = this.guilds.get("@me");
}
console.log(forceReload);
if (!forceReload && this.focusGuild === guild) {
return guild;
}
if (this.focusChannel && this.focusGuild !== guild) {
this.focusChannel.infinite.delete();
this.focusChannel = undefined;
}
if (this.focusGuild) {
this.focusGuild.html.classList.remove("serveropen");
}
if (!guild) return;
if (guild.html) {
guild.html.classList.add("serveropen");
}
this.focusGuild = guild;
(document.getElementById("serverName") as HTMLElement).textContent = guild.properties.name;
const banner = document.getElementById("servertd");
console.log(guild.banner, banner);
if (banner) {
if (guild.banner) {
//https://cdn.discordapp.com/banners/677271830838640680/fab8570de5bb51365ba8f36d7d3627ae.webp?size=240
banner.style.setProperty(
"background-image",
`linear-gradient(rgba(80, 80, 80, 1) 0%, rgba(80, 80, 80, 0) 40%), url(${this.info.cdn}/banners/${guild.id}/${guild.banner + new CDNParams({expectedSize: 128})})`,
);
banner.classList.add("Banner");
//background-image:
} else {
banner.style.removeProperty("background-image");
banner.classList.remove("Banner");
}
if (guild.id !== "@me") {
banner.style.setProperty("cursor", `pointer`);
banner.onclick = (e) => {
e.preventDefault();
e.stopImmediatePropagation();
const box = banner.getBoundingClientRect();
Guild.contextmenu.makemenu(box.left + 16, box.bottom + 5, guild, undefined);
};
} else {
banner.style.removeProperty("cursor");
banner.onclick = () => {};
}
}
//console.log(this.guildids,id)
const channels = document.getElementById("channels") as HTMLDivElement;
channels.innerHTML = "";
const html = guild.getHTML();
channels.appendChild(html);
return guild;
}
readonly dragMap = new WeakMap<
HTMLElement,
| Guild
| {
guilds: Guild[];
color?: number | null;
name: string;
id: number;
}
>();
dragged?: HTMLElement;
makeGuildDragable(
elm: HTMLElement,
thing:
| Guild
| {
guilds: Guild[];
color?: number | null;
name: string;
id: number;
},
) {
this.dragMap.set(elm, thing);
elm.addEventListener("dragstart", (e) => {
this.dragged = elm;
e.stopImmediatePropagation();
});
elm.addEventListener("dragend", () => {
delete this.dragged;
});
elm.style.position = "relative";
elm.draggable = true;
elm.addEventListener("dragenter", (event) => {
console.log("enter");
event.preventDefault();
event.stopImmediatePropagation();
});
const guildWithin = (guild?: Guild) => {
return !this.guildOrder.find((_) => _ === guild);
};
elm.addEventListener("dragover", (event) => {
event.stopImmediatePropagation();
const thingy = this.dragMap.get(this.dragged as HTMLElement);
if (!thingy) return;
if (this.dragged === elm) return;
if (!(thingy instanceof Guild) && guildWithin(thing as Guild)) return;
const height = elm.getBoundingClientRect().height;
if (event.offsetY < 0.3 * 48) {
elm.classList.add("dragTopView");
elm.classList.remove("dragFolderView");
elm.classList.remove("dragBottomView");
} else if (height - event.offsetY < 0.3 * 48) {
elm.classList.remove("dragTopView");
elm.classList.remove("dragFolderView");
elm.classList.add("dragBottomView");
} else {
elm.classList.remove("dragTopView");
elm.classList.remove("dragBottomView");
if (thingy instanceof Guild && (!(thing instanceof Guild) || !guildWithin(thing))) {
elm.classList.add("dragFolderView");
}
}
event.preventDefault();
});
elm.addEventListener("dragleave", () => {
elm.classList.remove("dragFolderView");
elm.classList.remove("dragTopView");
elm.classList.remove("dragBottomView");
});
elm.addEventListener("drop", async (event) => {
event.stopImmediatePropagation();
if (!this.dragged) return;
const drag = this.dragged;
const thingy = this.dragMap.get(this.dragged);
if (!thingy) return;
elm.classList.remove("dragFolderView");
elm.classList.remove("dragTopView");
elm.classList.remove("dragBottomView");
if (!(thingy instanceof Guild) && guildWithin(thing as Guild)) return;
if (this.dragged === elm) return;
const height = elm.getBoundingClientRect().height;
let arr = this.guildOrder;
console.warn(arr === this.guildOrder);
let found = arr.find((elm) => {
if (elm === thing) {
return true;
}
if (!(elm instanceof Guild) && elm.guilds.find((_) => _ === thing)) {
return true;
}
return false;
});
if (found && "guilds" in found && found !== thing) arr = found.guilds;
console.warn(arr === this.guildOrder);
const removeThingy = (thing = thingy) => {
const index = this.guildOrder.indexOf(thing);
if (-1 !== index) this.guildOrder.splice(index, 1);
this.guildOrder.forEach((_) => {
if (!(_ instanceof Guild)) {
const index = _.guilds.indexOf(thing as Guild);
if (-1 !== index) _.guilds.splice(index, 1);
}
});
};
console.log(arr, found);
if (event.offsetY < 0.3 * 48) {
removeThingy();
elm.before(this.dragged);
const index = arr.indexOf(thing);
arr.splice(index, 0, thingy);
} else if (height - event.offsetY < 0.3 * 48) {
removeThingy();
elm.after(this.dragged);
arr.splice(arr.indexOf(thing) + 1, 0, thingy);
} else if (thingy instanceof Guild && (!(thing instanceof Guild) || !guildWithin(thing))) {
if (thing instanceof Guild) {
await new Promise<void>((res) => {
const dia = new Dialog(I18n.folder.create());
const opt = dia.options;
const color = opt.addColorInput(I18n.folder.color(), () => {});
const name = opt.addTextInput(I18n.folder.name(), () => {});
opt.addButtonInput("", I18n.submit(), () => {
removeThingy();
let id = 1;
while (this.guildOrder.find((_) => _.id === id)) {
id++;
}
const folder = {
color: +("0x" + (color.value || "#0").split("#")[1]),
name: name.value,
guilds: [thing, thingy],
id,
};
this.guildOrder.splice(this.guildOrder.indexOf(thing), 1, folder);
const hold = document.createElement("hr");
elm.after(hold);
hold.after(
this.makeFolder(
folder,
new Map([
[thing, elm],
[thingy, drag],
]),
),
);
hold.remove();
dia.hide();
res();
});
dia.show();
});
} else {
removeThingy();
thing.guilds.push(thingy);
elm.append(drag);
}
}
console.log(this.guildOrder);
this.guildOrder = this.guildOrder.filter((folder) => {
if (folder instanceof Guild) return true;
if (folder.guilds.length === 0) {
const servers = document.getElementById("guildRail");
if (servers)
Array.from(servers.children).forEach((_) => {
const html = _ as HTMLElement;
if (this.dragMap.get(html) === folder) html.remove();
});
return false;
}
return true;
});
await this.saveGuildOrder();
});
}
async saveGuildOrder() {
const guild_folders: guildFolder[] = this.guildOrder.map((elm) => {
if (elm instanceof Guild) {
return {
id: null,
name: null,
guild_ids: [elm.id],
color: null,
};
} else {
return {
id: elm.id,
name: elm.name,
guild_ids: elm.guilds.map((guild) => guild.id),
color: elm.color,
};
}
});
await fetch(this.info.api + "/users/@me/settings", {
method: "PATCH",
headers: this.headers,
body: JSON.stringify({guild_folders}),
});
}
makeGuildIcon(guild: Guild) {
const divy = guild.generateGuildIcon();
guild.HTMLicon = divy;
this.makeGuildDragable(divy, guild);
return divy;
}
makeFolder(
folder: {color?: number | null; id: number; name: string; guilds: Guild[]},
icons = new Map<Guild, HTMLElement | undefined>(),
) {
const folderDiv = document.createElement("div");
folderDiv.classList.add("folder-div");
const iconDiv = document.createElement("div");
iconDiv.classList.add("folder-icon-div");
const icon = document.createElement("span");
icon.classList.add("svg-folder");
const menu = new Contextmenu<void, void>("");
menu.addButton(I18n.folder.edit(), () => {
const dio = new Dialog(I18n.folder.edit());
const opt = dio.options;
const name = opt.addTextInput(I18n.folder.name(), () => {}, {
initText: folder.name,
});
const color = opt.addColorInput(I18n.folder.color(), () => {}, {
initColor: "#" + (folder.color || 0).toString(16),
});
opt.addButtonInput("", I18n.submit(), async () => {
folder.name = name.value;
folder.color = +("0x" + (color.value || "#0").split("#")[1]);
icon.style.setProperty("--folder-color", "#" + folder.color.toString(16).padStart(6, "0"));
if (!folder.color && folder.color !== 0) icon.style.removeProperty("--folder-color");
await this.saveGuildOrder();
dio.hide();
});
dio.show();
});
menu.bindContextmenu(iconDiv);
if (folder.color !== null && folder.color !== undefined) {
icon.style.setProperty("--folder-color", "#" + folder.color.toString(16).padStart(6, "0"));
if (!folder.color && folder.color !== 0) icon.style.removeProperty("--folder-color");
}
iconDiv.append(icon);
const divy = document.createElement("div");
divy.append(
...folder.guilds.map((guild) => {
const icon = icons.get(guild);
if (icon) return icon;
return this.makeGuildIcon(guild);
}),
);
divy.classList.add("guilds-div-folder");
folderDiv.append(iconDiv, divy);
let height = -1;
const toggle = async (fast = false) => {
if (height === -1) {
divy.style.overflow = "clip";
height = divy.getBoundingClientRect().height;
divy.style.height = height + "px";
await new Promise((res) => requestAnimationFrame(res));
divy.style.height = "0px";
this.perminfo.folderStates[folder.id].state = true;
} else {
divy.style.height = height + "px";
if (!fast) await new Promise((res) => setTimeout(res, 200));
divy.style.height = "unset";
height = -1;
divy.style.overflow = "unset";
this.perminfo.folderStates[folder.id].state = false;
}
};
iconDiv.onclick = () => toggle();
this.perminfo.folderStates ??= {};
this.perminfo.folderStates[folder.id] ??= {};
if (this.perminfo.folderStates[folder.id].state === true) {
toggle(true);
}
this.makeGuildDragable(folderDiv, folder);
return folderDiv;
}
guildOrder: (
| Guild
| {
guilds: Guild[];
color?: number | null;
name: string;
id: number;
}
)[] = [];
buildservers(): void {
const serverlist = document.getElementById("guildRail") as HTMLDivElement; //
const outdiv = document.createElement("div");
const home: any = document.createElement("span");
const div = document.createElement("div");
div.classList.add("home", "servericon");
home.classList.add("svgicon", "svg-home");
(this.guilds.get("@me") as Guild).html = outdiv;
const unread = document.createElement("div");
unread.classList.add("unread");
outdiv.append(unread);
outdiv.append(div);
div.appendChild(home);
outdiv.classList.add("servernoti");
serverlist.append(outdiv);
home.onclick = () => {
const guild = this.guilds.get("@me");
if (!guild) return;
guild.loadGuild();
guild.loadChannel();
};
const sentdms = document.createElement("div");
sentdms.classList.add("sentdms");
serverlist.append(sentdms);
sentdms.id = "sentdms";
const br = document.createElement("hr");
br.classList.add("lightbr");
serverlist.appendChild(br);
const guilds = new Set(this.guilds.values());
const dirrect = this.guilds.get("@me") as Direct;
guilds.delete(dirrect);
const folders = this.guildFolders
.map((folder) => {
return {
guilds: folder.guild_ids
.map((id) => {
const guild = this.guilds.get(id);
if (!guild) {
console.error(`guild ${id} does not exist`);
return;
}
if (!guilds.has(guild)) {
console.error(`guild ${id} is already in a folder`);
return;
}
guilds.delete(guild);
return guild;
})
.filter((_) => _ !== undefined),
color: folder.color,
name: folder.name || "",
id: folder.id || 0,
};
})
.filter((_) => {
if (_.guilds.length === 0) {
console.error("empty folder depected");
return false;
}
return true;
})
.map((folder) => {
if (!folder.id && folder.guilds.length === 1) {
return folder.guilds[0];
}
return folder;
});
const guildOrder = [...guilds, ...folders];
this.guildOrder = guildOrder;
for (const thing of guildOrder) {
if (thing instanceof Guild) {
serverlist.append(this.makeGuildIcon(thing));
} else {
const folderDiv = this.makeFolder(thing);
serverlist.append(folderDiv);
}
}
{
const br = document.createElement("hr");
br.classList.add("lightbr");
serverlist.appendChild(br);
br.id = "bottomseparator";
const div = document.createElement("div");
const plus = document.createElement("span");
plus.classList.add("svgicon", "svg-plus");
div.classList.add("home", "servericon");
div.appendChild(plus);
serverlist.appendChild(div);
div.onclick = (_) => {
this.createGuild();
};
const guilddsdiv = document.createElement("div");
const guildDiscoveryContainer = document.createElement("span");
guildDiscoveryContainer.classList.add("svgicon", "svg-explore");
guilddsdiv.classList.add("home", "servericon");
guilddsdiv.appendChild(guildDiscoveryContainer);
serverlist.appendChild(guilddsdiv);
guildDiscoveryContainer.addEventListener("click", () => {
this.guildDiscovery();
});
}
this.unreads();
dirrect.unreaddms();
}
passTemplateID(id: string) {
this.createGuild(id);
}
createGuild(templateID?: string) {
const full = new Dialog("");
const buttons = full.options.addButtons("", {top: true});
const viacode = buttons.add(I18n.invite.joinUsing());
{
const form = viacode.addForm("", async (e: any) => {
let parsed = "";
if (e.code.includes("/")) {
parsed = e.code.split("/")[e.code.split("/").length - 1];
} else {
parsed = e.code;
}
const json = await (
await fetch(this.info.api + "/invites/" + parsed, {
method: "POST",
headers: this.headers,
})
).json();
if (json.message) {
throw new FormError(text, json.message);
}
full.hide();
});
const text = form.addTextInput(I18n.invite.inviteLinkCode(), "code");
}
const guildcreate = buttons.add(I18n.guild.create());
{
const form = guildcreate.addForm("", (fields: any) => {
this.makeGuild(fields).then((_) => {
if (_.message) {
loading.hide();
full.show();
alert(_.errors.name._errors[0].message);
} else {
loading.hide();
full.hide();
}
});
});
form.addImageInput(I18n.guild["icon:"](), "icon", {
clear: true,
});
form.addTextInput(I18n.guild["name:"](), "name", {required: true});
const loading = new Dialog("");
loading.float.options.addTitle(I18n.guild.creating());
form.onFormError = () => {
loading.hide();
full.show();
};
form.addPreprocessor(() => {
loading.show();
full.hide();
});
}
const guildcreateFromTemplate = buttons.add(I18n.guild.createFromTemplate());
{
const form = guildcreateFromTemplate.addForm(
"",
(_: any) => {
if (_.message) {
loading.hide();
full.show();
alert(_.message);
const htmlarea = buttons.htmlarea.deref();
if (htmlarea) buttons.generateHTMLArea(guildcreateFromTemplate, htmlarea);
} else {
loading.hide();
full.hide();
}
},
{
method: "POST",
headers: this.headers,
},
);
const template = form.addTextInput(I18n.guild.template(), "template", {
initText: templateID || "",
});
form.addImageInput(I18n.guild["icon:"](), "icon", {files: "one", clear: true});
form.addTextInput(I18n.guild["name:"](), "name", {required: true});
const loading = new Dialog("");
loading.float.options.addTitle(I18n.guild.creating());
form.onFormError = () => {
loading.hide();
full.show();
};
form.addPreprocessor((e) => {
loading.show();
full.hide();
if ("template" in e) delete e.template;
let code: string;
if (URL.canParse(template.value) && new URL(template.value).protocol.startsWith("http")) {
const url = new URL(template.value);
code = url.pathname.split("/").at(-1) as string;
if (url.host === "discord.com" || url.host === "discord.new") {
code = "discord:" + code;
}
} else {
code = template.value;
}
form.fetchURL = this.info.api + "/guilds/templates/" + code;
});
}
full.show();
if (templateID) {
const htmlarea = buttons.htmlarea.deref();
if (htmlarea) buttons.generateHTMLArea(guildcreateFromTemplate, htmlarea);
}
}
async makeGuild(fields: {name: string; icon: string | null}) {
return await (
await fetch(this.info.api + "/guilds", {
method: "POST",
headers: this.headers,
body: JSON.stringify(fields),
})
).json();
}
async guildDiscovery() {
this.guilds.get("@me")?.loadChannel("discover");
}
messageCreate(messagep: messageCreateJson): void {
messagep.d.guild_id ??= "@me";
const channel = this.channels.get(messagep.d.channel_id);
if (channel) {
channel.messageCreate(messagep);
this.unreads();
}
}
unreads(): void {
for (const thing of this.guilds.values()) {
if (thing.id === "@me") {
thing.unreads();
continue;
}
const html = thing.html;
thing.unreads(html);
}
}
private static favC = document.createElement("canvas");
private static favCTX = this.favC.getContext("2d") as CanvasRenderingContext2D;
private static favImg = this.getFaviconImg();
static getFaviconImg() {
const img = document.createElement("img");
img.src = "/logo.webp";
return img;
}
private last = "-1";
private generateFavicon() {
const make = () => {
const favicon = document.getElementById("favicon") as HTMLLinkElement;
let text = this.totalMentions() + "";
if (this.last === text) return;
this.last = text;
if (text === "0") {
favicon.href = "/favicon.ico";
return;
}
if (+text > 99) text = "+99";
const c = Localuser.favC;
c.width = 256;
c.height = 256;
const ctx = Localuser.favCTX;
ctx.drawImage(Localuser.favImg, 0, 0, c.width, c.height);
ctx.fillStyle = "#F00";
const pos = 0.675;
ctx.beginPath();
ctx.arc(c.width * pos, c.height * pos, c.width * (1 - pos), 0, 2 * Math.PI);
ctx.fill();
ctx.fillStyle = "#FFF";
ctx.font = `bolder ${text.length === 1 ? 150 : 100}px sans-serif`;
const messure = ctx.measureText(text);
const height = messure.fontBoundingBoxAscent + messure.fontBoundingBoxDescent;
ctx.fillText(text, c.width * pos - messure.width / 2, c.height * pos + height / 2 - 25);
favicon.href = c.toDataURL("image/x-icon");
};
if (Localuser.favImg.complete) {
make();
}
Localuser.favImg.onload = () => {
make();
};
}
totalMentions() {
let sum = 0;
for (const guild of this.guilds.values()) {
sum += guild.mentions;
}
for (const channel of (this.guilds.get("@me") as Direct).channels) {
sum += channel.mentions;
}
return sum;
}
async typingStart(typing: startTypingjson): Promise<void> {
const channel = this.channels.get(typing.d.channel_id);
if (!channel) return;
channel.typingStart(typing);
}
updatepfp(file: Blob | null): void {
if (file) {
const reader = new FileReader();
reader.readAsDataURL(file);
reader.onload = () => {
fetch(this.info.api + "/users/@me", {
method: "PATCH",
headers: this.headers,
body: JSON.stringify({
avatar: reader.result,
}),
});
};
} else {
fetch(this.info.api + "/users/@me", {
method: "PATCH",
headers: this.headers,
body: JSON.stringify({
avatar: null,
}),
});
}
}
updatebanner(file: Blob | null): void {
if (file) {
const reader = new FileReader();
reader.readAsDataURL(file);
reader.onload = () => {
fetch(this.info.api + "/users/@me", {
method: "PATCH",
headers: this.headers,
body: JSON.stringify({
banner: reader.result,
}),
});
};
} else {
fetch(this.info.api + "/users/@me", {
method: "PATCH",
headers: this.headers,
body: JSON.stringify({
banner: null,
}),
});
}
}
updateProfile(json: {bio?: string; pronouns?: string; accent_color?: number}) {
fetch(this.info.api + "/users/@me/profile", {
method: "PATCH",
headers: this.headers,
body: JSON.stringify(json),
});
}
async getPosts() {
const text = await (await fetch("https://blog.fermi.chat/feed_rss_created.xml")).text();
const xml = new DOMParser().parseFromString(text, "text/xml");
const posts = Array.from(xml.getElementsByTagName("channel")[0].getElementsByTagName("item"));
return {
items: posts.map((post) => {
return {
url: post.getElementsByTagName("link")[0].textContent,
title: post.getElementsByTagName("title")[0].textContent,
content_html: post.getElementsByTagName("description")[0].textContent,
image: post.getElementsByTagName("image")[0]?.textContent ?? null,
};
}),
} satisfies {
items: {
url: string;
title: string;
content_html: string;
image: null | string;
}[];
};
}
async getConnections() {
const json = await fetch(this.info.api + "/connections", {
headers: this.headers,
}).then((r) => r.json() as Promise<{[key: string]: {enabled: boolean; icon_url?: string}}>);
json["domain"] = {enabled: true, icon_url: "/icons/domain.svg"};
return json;
}
gifProvideors: {name: string; api_name: string}[] = [];
selectedGifProfidor?: {name: string; api_name: string};
async getGifProvidors() {
const res = (await (
await fetch(this.info.api + "/gifs", {
headers: this.headers,
})
).json()) as {name: string; api_name: string}[];
if (res[0] && !("name" in res[0])) {
if (res.length === 3) {
this.gifProvideors = [
{name: "Giphy", api_name: "giphy"},
{name: "Klipy", api_name: "klipy"},
];
}
} else {
this.gifProvideors = res.filter((_) => _.name !== "tenor");
}
this.figureDefaultProvidor();
}
async figureDefaultProvidor() {
const prefs = getPreferences();
this.selectedGifProfidor =
this.gifProvideors.find((_) => _.api_name == prefs.gifProvidor) || this.gifProvideors[0];
}
domainVerification() {
const d = new Dialog(I18n.domain.title(), {noSubmit: true});
const text = d.options.addTextInput(I18n.domain.domain(), () => {});
d.options.addButtonInput("", I18n.submit(), async () => {
const dom = text.value;
d.options.removeAll();
console.log(dom);
const res = await fetch(this.info.api + "/users/@me/connections/domain/" + dom, {
method: "POST",
headers: this.headers,
});
if (res.ok) {
d.hide();
} else {
const json = (await res.json()) as {code?: number; message: string; proof: string};
if (json.code === 50187) {
const err = d.options.addErrorBox();
d.options.addMDText(new MarkDown(I18n.domain.dnsinst(dom, json.proof)));
d.options.addButtonInput("", I18n.submit(), async () => {
const dom = text.value;
const res = await fetch(this.info.api + "/users/@me/connections/domain/" + dom, {
method: "POST",
headers: this.headers,
});
if (res.ok) {
d.hide();
} else {
err.showError((await res.json()).message);
}
});
} else {
d.options.addText(json.message);
}
}
});
d.show();
}
static initShortcuts() {
const cur = getShortcuts();
for (const [name, combo] of cur) {
this.globalShortcuts.registerKeycombo(combo, name);
}
this.globalShortcuts.registerShortcut("gifSearch", () => {
document.getElementById("gifTB")?.click();
});
}
async showusersettings() {
const prefs = getPreferences();
const localSettings = getLocalSettings();
const settings = new Settings(I18n.localuser.settings());
{
const donate = settings.addButton(I18n.donate.donate(), {initable: false});
donate.addMDText(new MarkDown(I18n.donate.mdText(window.location.origin + "/donate")));
donate.addText(I18n.donate.desc());
}
settings.addButton(I18n.localuser.general(), {
head: true,
});
{
const userOptions = settings.addButton(I18n.localuser.userSettings(), {
ltr: true,
contained: true,
});
const hypotheticalProfile = document.createElement("div");
let file: undefined | File | null;
let newpronouns: string | undefined;
let newbio: string | undefined;
const hypouser = this.user.clone();
let color: string;
async function regen() {
hypotheticalProfile.textContent = "";
const hypoprofile = await hypouser.buildprofile(-1, -1);
hypotheticalProfile.appendChild(hypoprofile);
}
regen();
const settingsLeft = userOptions.addOptions("");
const settingsRight = userOptions.addOptions("");
settingsRight.addHTMLArea(hypotheticalProfile);
const finput = settingsLeft.addImageInput(
I18n.uploadPfp(),
(_) => {
if (file !== undefined) {
this.updatepfp(file);
}
},
{clear: true, initImg: this.user.getpfpsrc()},
);
finput.img.classList.add("pfp");
finput.watchForChange((_) => {
if (!_) {
file = null;
hypouser.avatar = null;
hypouser.hypotheticalpfp = true;
regen();
return;
}
if (_.length) {
file = _[0];
const blob = URL.createObjectURL(file);
hypouser.avatar = blob;
hypouser.hypotheticalpfp = true;
regen();
}
});
let bfile: undefined | File | null;
const binput = settingsLeft.addImageInput(
I18n.uploadBanner(),
(_) => {
if (bfile !== undefined) {
this.updatebanner(bfile);
}
},
{
clear: true,
width: 96 * 3,
initImg: this.user.banner ? this.user.getBannerUrl() : "",
objectFit: "cover",
},
);
binput.watchForChange((_) => {
if (!_) {
bfile = null;
hypouser.banner = undefined;
hypouser.hypotheticalbanner = true;
regen();
return;
}
if (_.length) {
bfile = _[0];
const blob = URL.createObjectURL(bfile);
hypouser.banner = blob;
hypouser.hypotheticalbanner = true;
regen();
}
});
let changed = false;
const pronounbox = settingsLeft.addTextInput(
I18n.pronouns(),
(_) => {
if (newpronouns !== undefined || newbio !== undefined || changed !== undefined) {
this.updateProfile({
pronouns: newpronouns,
bio: newbio,
accent_color: Number.parseInt("0x" + color.substring(1), 16),
});
}
},
{initText: this.user.pronouns},
);
pronounbox.watchForChange((_) => {
hypouser.pronouns = _;
newpronouns = _;
regen();
});
const bioBox = settingsLeft.addMDInput(I18n.bio(), (_) => {}, {
initText: this.user.bio.rawString,
});
bioBox.watchForChange((_) => {
newbio = _;
hypouser.bio = new MarkDown(_, this);
regen();
});
if (this.user.accent_color) {
color = "#" + this.user.accent_color.toString(16);
} else {
color = "transparent";
}
const colorPicker = settingsLeft.addColorInput(I18n.profileColor(), (_) => {}, {
initColor: color,
});
colorPicker.watchForChange((_) => {
console.log();
color = _;
hypouser.accent_color = Number.parseInt("0x" + _.substring(1), 16);
changed = true;
regen();
});
settingsLeft.addButtonInput("", I18n.localuser.connections(), () => {
const connections = userOptions.addSubOptions(I18n.localuser.connections());
const connectionContainer = document.createElement("div");
const actConDivCont = document.createElement("div");
connectionContainer.classList.add("connection-container");
this.conectionChange = () => {
if (document.contains(settings.html)) {
remake();
} else {
this.conectionChange = () => {};
}
};
const remake = () => {
connectionContainer.innerHTML = "";
actConDivCont.innerHTML = "";
const cons = fetch(this.info.api + "/users/@me/connections", {
headers: this.headers,
});
this.getConnections().then(async (json) => {
const actCons = (await (await cons).json()) as ConnectionJson[];
const actConMap = new Map<string, ConnectionJson>(
actCons.map((_) => [_.type, _] as const),
);
const serverConnections = Object.keys(json).sort((key) => (json[key].enabled ? -1 : 1));
serverConnections
.filter((_) => !actConMap.has(_))
.forEach((key) => {
if (key === "domain") return;
const connection = json[key];
const container = document.createElement("div");
if (connection.icon_url) {
const span = document.createElement("span");
span.classList.add("conImg", "svgicon");
span.style.setProperty("mask", `url("${connection.icon_url}")`);
//span.alt = key;
container.append(span);
} else {
container.textContent = key.charAt(0).toUpperCase() + key.slice(1);
}
if (connection.enabled) {
container.addEventListener("click", async () => {
const connectionRes = await fetch(
this.info.api + "/connections/" + key + "/authorize",
{
headers: this.headers,
},
);
const connectionJSON = await connectionRes.json();
window.open(connectionJSON.url, "_blank", "noopener noreferrer");
});
} else {
container.classList.add("disabled");
}
connectionContainer.appendChild(container);
});
const container = document.createElement("div");
const span = document.createElement("span");
span.classList.add("conImg", "svgicon");
span.style.setProperty("mask", `url("/icons/domain.svg")`);
container.append(span);
container.addEventListener("click", async () => {
this.domainVerification();
});
connectionContainer.appendChild(container);
serverConnections
.filter((_) => actConMap.has(_))
.forEach((_) => {
const con = actConMap.get(_);
if (!con) return;
const connectionObj = json[_];
const actConDiv = document.createElement("div");
actConDiv.classList.add("flexttb", "actConnectionDiv");
const topRow = document.createElement("div");
actConDiv.append(topRow);
topRow.classList.add("flexltr");
if (connectionObj.icon_url) {
const span = document.createElement("span");
span.classList.add("conImg", "svgicon");
span.style.setProperty("mask", `url("${connectionObj.icon_url}")`);
//span.alt = key;
topRow.append(span);
}
const nameDiv = document.createElement("div");
nameDiv.classList.add("flexttb");
const name = document.createElement("span");
name.textContent = con.name;
const serviceName = document.createElement("span");
serviceName.textContent = _;
nameDiv.append(name, serviceName);
topRow.append(nameDiv);
const input = document.createElement("input");
input.type = "checkbox";
input.checked = !!con.visibility;
input.onchange = () => {
fetch(this.info.api + "/users/@me/connections/" + con.type + "/" + con.id, {
method: "PATCH",
body: JSON.stringify({
visibility: input.checked,
}),
headers: this.headers,
});
};
const dispRow = document.createElement("div");
dispRow.classList.add("flexltr");
actConDiv.append(dispRow);
const dispText = document.createElement("span");
dispText.textContent = I18n.connections.display();
dispRow.append(dispText, input);
const remove = document.createElement("button");
remove.textContent = I18n.connections.delete();
actConDiv.append(remove);
remove.onclick = () => {
const d = new Dialog(I18n.connections.sure());
d.options.addText(I18n.connections.sureDesc());
const row = d.options.addOptions("", {ltr: true});
row.addButtonInput("", I18n.yes(), async () => {
await fetch(
this.info.api + "/users/@me/connections/" + con.type + "/" + con.id,
{
method: "DELETE",
headers: this.headers,
},
);
d.hide();
});
row.addButtonInput("", I18n.no(), () => {
d.hide();
});
d.show();
};
actConDivCont.append(actConDiv);
});
});
};
remake();
connections.addHTMLArea(connectionContainer);
connections.addHR();
connections.addHTMLArea(actConDivCont);
});
}
{
const security = settings.addButton(I18n.localuser.accountSettings(), {contained: true});
security.addSubButtonInput(I18n.localuser.changeDiscriminator(), (sub) => {
const form = sub.addForm(
"",
(_) => {
security.returnFromSub();
},
{
fetchURL: this.info.api + "/users/@me/",
headers: this.headers,
method: "PATCH",
},
);
form.addTextInput(I18n.localuser.newDiscriminator(), "discriminator");
});
security.addSubButtonInput(I18n.localuser.changeEmail(), (sub) => {
const form = sub.addForm(
"",
(_) => {
security.returnFromSub();
},
{
fetchURL: this.info.api + "/users/@me/",
headers: this.headers,
method: "PATCH",
},
);
form.addTextInput(I18n.localuser["password:"](), "password", {
password: true,
});
if (this.mfa_enabled) {
form.addTextInput(I18n.localuser["2faCode:"](), "code");
}
form.addTextInput(I18n.localuser["newEmail:"](), "email");
});
security.addSubButtonInput(I18n.localuser.changeUsername(), (sub) => {
const form = sub.addForm(
"",
(_) => {
security.returnFromSub();
},
{
fetchURL: this.info.api + "/users/@me/",
headers: this.headers,
method: "PATCH",
},
);
form.addTextInput(I18n.localuser["password:"](), "password", {
password: true,
});
if (this.mfa_enabled) {
form.addTextInput(I18n.localuser["2faCode:"](), "code");
}
form.addTextInput(I18n.localuser.newUsername(), "username");
});
security.addSubButtonInput(I18n.localuser.changePassword(), (sub) => {
const form = sub.addForm(
"",
(_) => {
security.returnFromSub();
},
{
fetchURL: this.info.api + "/users/@me/",
headers: this.headers,
method: "PATCH",
},
);
form.addTextInput(I18n.localuser["oldPassword:"](), "password", {
password: true,
});
if (this.mfa_enabled) {
form.addTextInput(I18n.localuser["2faCode:"](), "code");
}
let in1 = "";
let in2 = "";
form
.addTextInput(I18n.localuser["newPassword:"](), "", {password: true})
.watchForChange((text) => {
in1 = text;
});
const copy = form.addTextInput("New password again:", "", {password: true});
copy.watchForChange((text) => {
in2 = text;
});
form.setValue("new_password", () => {
if (in1 === in2) {
return in1;
} else {
throw new FormError(copy, I18n.localuser.PasswordsNoMatch());
}
});
});
{
security.addButtonInput("", I18n.logout.logout(), async () => {
if (await this.userinfo.logout()) window.location.href = "/";
});
}
}
{
const lang = settings.addButton(I18n.localuser.language(), {contained: true});
const div = document.createElement("div");
div.classList.add("flexttb");
const langBuMap = new Map<string, HTMLButtonElement>();
let l = [...langmap];
const langF = l.find((_) => _[0] === I18n.lang + ".json");
l = l.filter((_) => _ !== langF);
if (langF) l.unshift(langF);
div.append(
...l.map(([key, name]) => {
const button = document.createElement("button");
button.classList.add("langButton");
button.textContent = name;
langBuMap.set(key, button);
if (key === I18n.lang + ".json") {
button.classList.add("selected");
}
button.onclick = () => {
const b = langBuMap.get(I18n.lang + ".json");
if (b) b.classList.remove("selected");
button.classList.add("selected");
I18n.setLanguage(key.replace(".json", ""));
this.updateTranslations();
};
console.log(I18n.lang, key);
return button;
}),
);
lang.addHTMLArea(div);
}
{
const prefs = getPreferences();
const tas = settings.addButton(I18n.localuser.themesAndSounds(), {contained: true});
{
const themes = ["Dark", "WHITE", "Light", "Dark-Accent"];
tas.addSelect(
I18n.localuser["theme:"](),
async (_) => {
prefs.theme = themes[_] as ThemeOption;
await setTheme(prefs.theme);
},
themes,
{
defaultIndex: themes.indexOf(prefs.theme),
},
);
}
{
const initArea = (index: number) => {
if (index === sounds.length - 1) {
const input = document.createElement("input");
input.type = "file";
input.accept = "audio/*";
input.addEventListener("change", () => {
if (input.files?.length === 1) {
const file = input.files[0];
let reader = new FileReader();
reader.onload = () => {
let dataUrl = reader.result;
if (typeof dataUrl !== "string") return;
this.perminfo.sound = {};
try {
this.perminfo.sound.cSound = dataUrl;
console.log(this.perminfo.sound.cSound);
this.playSound("custom");
} catch (_) {
alert(I18n.localuser.soundTooLarge());
}
};
reader.readAsDataURL(file);
}
});
area.append(input);
} else {
area.innerHTML = "";
}
};
const sounds = [...(this.play?.tracks || []), I18n.localuser.customSound()];
const initIndex = sounds.indexOf(this.getNotificationSound());
const select = tas.addSelect(
I18n.localuser.notisound(),
(index) => {
this.setNotificationSound(sounds[index]);
},
sounds,
{defaultIndex: initIndex},
);
select.watchForChange((index) => {
initArea(index);
this.playSound(sounds[index]);
});
const input = document.createElement("input");
input.type = "range";
input.value = this.getNotiVolume() + "";
input.min = "0";
input.max = "100";
input.onchange = () => {
this.setNotificationVolume(+input.value);
this.playSound(sounds[select.index]);
};
const area = document.createElement("div");
initArea(initIndex);
tas.addHTMLArea(area);
tas.addText(I18n.notiVolume());
tas.addHTMLArea(input);
}
{
tas.addColorInput(
I18n.localuser.accentColor(),
async (_) => {
prefs.accentColor = _;
await setPreferences(prefs);
document.documentElement.style.setProperty("--accent-color", prefs.accentColor);
},
{initColor: prefs.accentColor},
);
}
{
const options = [[null, I18n.noEmojiFont()], ...Localuser.fonts] as const;
const cur = prefs.emojiFont;
let index = options.findIndex((_) => _[1] == cur);
if (index === -1) index = 0;
tas.addSelect(
I18n.emojiSelect(),
async (index) => {
if (options[index][0]) {
prefs.emojiFont = options[index][1];
} else {
prefs.emojiFont = undefined;
}
await setPreferences(prefs);
Localuser.loadFont();
},
options.map((font) => font[1]),
{
defaultIndex: index,
},
);
}
{
const ind = this.gifProvideors.findIndex((_) => this.selectedGifProfidor === _);
console.log(prefs.gifProvidor);
tas.addSelect(
I18n.gifProvidor(),
async (i) => {
prefs.gifProvidor = this.gifProvideors[i].api_name;
console.log(prefs.gifProvidor);
await setPreferences(prefs);
Localuser.loadFont();
this.figureDefaultProvidor();
},
this.gifProvideors.map((_) => _.name),
{defaultIndex: ind === -1 ? 0 : ind},
);
}
{
tas.addCheckboxInput(
"Show today at:",
(b) => {
prefs.showToday = b;
setPreferences(prefs);
},
{
initState: prefs.showToday,
},
);
}
}
{
const blog = settings.addButton(I18n.blog.blog(), {contained: true});
blog.addCheckboxInput(
I18n.blog.blogUpdates(),
async (check) => {
prefs.showBlogUpdates = check;
await setPreferences(prefs);
},
{initState: prefs.showBlogUpdates},
);
(async () => {
const posts = await this.getPosts();
for (const post of posts.items) {
const div = document.createElement("div");
div.classList.add("flexltr", "blogDiv");
if (post.image) {
//TODO handle this case, no blog posts currently do this
}
const titleStuff = document.createElement("div");
titleStuff.classList.add("flexttb");
const h2 = document.createElement("h2");
h2.textContent = post.title;
const p = document.createElement("p");
p.textContent = post.content_html;
titleStuff.append(h2, p);
div.append(titleStuff);
blog.addHTMLArea(div);
MarkDown.safeLink(div, post.url);
}
})();
}
{
const shortcuts = settings.addButton(I18n.keyboard.shortcuts(), {contained: true});
const cur = getShortcuts();
for (const [name] of cur) {
shortcuts.addText(I18n.keyboard.descs[name]());
shortcuts.addHTMLArea(
Localuser.globalShortcuts.shortCutbutton(
name,
(short) => {
cur[name] = short;
setShortcuts(cur);
console.log(cur);
},
true,
),
);
}
}
const installP = installPGet();
if (installP) {
const c = settings.addButton(I18n.localuser.install(), {contained: true});
c.addText(I18n.localuser.installDesc());
c.addButtonInput("", I18n.localuser.installJank(), async () => {
//@ts-expect-error have to do this :3
await installP.prompt();
});
}
settings.addButton(I18n.accessibility.name(), {head: true});
{
const visuals = settings.addButton(I18n.accessibility.visuals(), {contained: true});
visuals.addCheckboxInput(
I18n.accessibility.roleColors(),
(t) => {
console.log(t);
this.perminfo.user.disableColors = !t;
},
{initState: !this.perminfo.user.disableColors},
);
visuals.addCheckboxInput(
I18n.accessibility.gradientColors(),
(t) => {
console.log(t);
this.perminfo.user.gradientColors = t;
},
{initState: this.perminfo.user.gradientColors},
);
visuals.addCheckboxInput(
I18n.channel.allowIcons(),
(t) => {
console.log(t);
this.perminfo.user.disableIcons = !t;
},
{initState: !this.perminfo.user.disableIcons},
);
visuals.addCheckboxInput(
I18n.accessibility.decorations(),
(t) => {
this.perminfo.user.decorations = t;
},
{initState: this.perminfo.user.decorations},
);
{
const cur = prefs.renderJoinAvatars;
visuals.addCheckboxInput(
I18n.renderJoinAvatars(),
async (v) => {
prefs.renderJoinAvatars = v;
await setPreferences(prefs);
},
{initState: cur},
);
}
visuals.addCheckboxInput(
I18n.checkBoxMemberList(),
(c) => {
prefs.checkMemberList = c;
const ml = document.getElementById("memberlisttoggle")!;
if (c) {
ml.classList = "";
} else {
ml.classList = "svgicon svg-friends";
}
setPreferences(prefs);
},
{
initState: !!prefs.checkMemberList,
},
);
}
{
const animations = settings.addButton(I18n.accessibility.animations(), {contained: true});
animations.addSelect(
I18n.accessibility.playGif(),
async (i) => {
prefs.animateGifs = AnimateTristateValues[i];
await setPreferences(prefs);
},
AnimateTristateValues.map((_) => I18n.accessibility.gifSettings[_]()),
{defaultIndex: AnimateTristateValues.indexOf(prefs.animateGifs)},
);
animations.addSelect(
I18n.accessibility.playIcon(),
async (i) => {
prefs.animateIcons = AnimateTristateValues[i];
await setPreferences(prefs);
},
AnimateTristateValues.map((_) => I18n.accessibility.gifSettings[_]()),
{defaultIndex: AnimateTristateValues.indexOf(prefs.animateIcons)},
);
}
settings.addButton(I18n.localuser.security(), {head: true});
{
const twofa = settings.addButton(I18n.localuser["2fa"](), {contained: true});
const gen2FA = () => {
twofa.removeAll();
if (this.mfa_enabled) {
twofa.addSubButtonInput(I18n.localuser["2faDisable"](), (sub) => {
const form = sub.addForm(
"",
(_: any) => {
if (_.message) {
switch (_.code) {
case 60008:
form.error("code", I18n.localuser.badCode());
break;
}
} else {
this.mfa_enabled = false;
twofa.returnFromSub();
gen2FA();
}
},
{
fetchURL: this.info.api + "/users/@me/mfa/totp/disable",
headers: this.headers,
},
);
form.addTextInput(I18n.localuser["2faCode:"](), "code", {required: true});
});
} else {
twofa.addSubButtonInput(I18n.localuser["2faEnable"](), async (sub) => {
let secret = "";
for (let i = 0; i < 18; i++) {
secret += "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"[Math.floor(Math.random() * 32)];
}
const form = sub.addForm(
"",
(_: any) => {
if (_.message) {
switch (_.code) {
case 60008:
form.error("code", I18n.localuser.badCode());
break;
case 400:
form.error("password", I18n.localuser.badPassword());
break;
}
} else {
gen2FA();
this.mfa_enabled = true;
twofa.returnFromSub();
}
},
{
fetchURL: this.info.api + "/users/@me/mfa/totp/enable",
headers: this.headers,
},
);
form.addTitle(I18n.localuser.setUp2faInstruction());
form.addText(I18n.localuser["2faCodeGive"](secret));
form.addTextInput(I18n.localuser["password:"](), "password", {
required: true,
password: true,
});
form.addTextInput(I18n.localuser["2faCode:"](), "code", {required: true});
form.setValue("secret", secret);
});
}
{
twofa.addSubButtonInput(I18n.webauth.manage(), (keyMenu) => {
const addKey = (key: {name: string; id: string}) => {
keyMenu.addButtonInput("", key.name, () => {
const opt = keyMenu.addSubOptions(key.name);
const button = opt.addButtonInput("", I18n.delete(), async () => {
await fetch(this.info.api + "/users/@me/mfa/webauthn/credentials/" + key.id, {
headers: this.headers,
method: "DELETE",
});
keyMenu.returnFromSub();
keyMenu.deleteElm(button);
});
});
};
keyMenu.addButtonInput("", I18n.webauth.addKey(), () => {
const form = keyMenu.addSubForm(
I18n.webauth.addKey(),
async (obj) => {
const body = obj as {ticket: string; challenge: string};
const challenge = JSON.parse(body.challenge)
.publicKey as PublicKeyCredentialCreationOptionsJSON;
console.log(challenge.challenge);
challenge.challenge = challenge.challenge
.split("=")[0]
.replaceAll("+", "-")
.replaceAll("/", "_");
console.log(challenge.challenge);
const options = PublicKeyCredential.parseCreationOptionsFromJSON(challenge);
const credential = (await navigator.credentials.create({
publicKey: options,
})) as unknown as {
rawId: ArrayBuffer;
response: {
attestationObject: ArrayBuffer;
clientDataJSON: ArrayBuffer;
};
};
if (!credential) return;
function toBase64(buf: ArrayBuffer) {
return btoa(String.fromCharCode(...new Uint8Array(buf)));
}
const res = {
rawId: toBase64(credential.rawId),
response: {
clientDataJSON: toBase64(credential.response.clientDataJSON),
attestationObject: toBase64(credential.response.attestationObject),
},
};
const key = await (
await fetch(this.info.api + "/users/@me/mfa/webauthn/credentials", {
headers: this.headers,
method: "POST",
body: JSON.stringify({
ticket: body.ticket,
credential: JSON.stringify(res),
name: name.value,
}),
})
).json();
addKey(key);
keyMenu.returnFromSub();
},
{
fetchURL: this.info.api + "/users/@me/mfa/webauthn/credentials",
method: "POST",
headers: this.headers,
tfaCheck: false,
},
);
form.addTextInput(I18n.htmlPages.pwField(), "password", {
password: true,
});
const name = form.options.addTextInput(I18n.webauth.keyname(), () => {}, {
initText: "Key",
});
});
fetch(this.info.api + "/users/@me/mfa/webauthn/credentials", {
headers: this.headers,
})
.then((_) => _.json())
.then((keys: {id: string; name: string}[]) => {
for (const key of keys) {
addKey(key);
}
});
});
}
};
gen2FA();
}
{
const manageSessions = settings.addButton(I18n.deviceManage.title(), {contained: true});
(async () => {
const json = (await (
await fetch(this.info.api + "/auth/sessions?extended=true", {headers: this.headers})
).json()) as {user_sessions: expSessionJson[]};
for (const session of json.user_sessions.sort(
(a, b) => +new Date(a.last_seen) - +new Date(b.last_seen),
)) {
const div = document.createElement("div");
div.classList.add("flexltr", "sessionDiv");
const info = document.createElement("div");
info.classList.add("flexttb");
div.append(info);
let line2 = "";
const last = session.last_seen_location_info;
if (last) {
line2 += last.country_name;
if (last.region) line2 += ", " + last.region;
if (last.city) line2 += ", " + last.city;
}
if (line2) {
line2 += " • ";
}
const format = new Intl.RelativeTimeFormat(I18n.lang, {style: "short"});
const time = (Date.now() - +new Date(session.last_seen)) / 1000;
if (time < 60) {
line2 += format.format(-Math.floor(time), "seconds");
} else if (time < 60 * 60) {
line2 += format.format(-Math.floor(time / 60), "minutes");
} else if (time < 60 * 60 * 24) {
line2 += format.format(-Math.floor(time / 60 / 60), "hours");
} else if (time < 60 * 60 * 24 * 7) {
line2 += format.format(-Math.floor(time / 60 / 60 / 24), "days");
} else if (time < 60 * 60 * 24 * 365) {
line2 += format.format(-Math.floor(time / 60 / 60 / 24 / 7), "weeks");
} else {
line2 += format.format(-Math.floor(time / 60 / 60 / 24 / 365), "years");
}
const loc = document.createElement("span");
loc.textContent = line2;
info.append(loc);
const r = manageSessions.addHTMLArea(div);
div.onclick = () => {
const sub = manageSessions.addSubOptions(I18n.deviceManage.manageDev());
sub.addText(I18n.deviceManage.ip(session.last_seen_ip));
sub.addText(I18n.deviceManage.last(session.approx_last_used_time));
if (last) {
sub.addText(I18n.deviceManage.estimateWarn());
sub.addText(I18n.deviceManage.continent(last.continent_name));
sub.addText(I18n.deviceManage.country(last.country_name));
if (last.region) sub.addText(I18n.deviceManage.region(last.region));
if (last.city) sub.addText(I18n.deviceManage.city(last.city));
if (last.postal) sub.addText(I18n.deviceManage.postal(last.postal));
sub.addText(I18n.deviceManage.longitude(last.longitude + ""));
sub.addText(I18n.deviceManage.latitude(last.latitude + ""));
}
if (session.id !== this.session_id) {
sub.addButtonInput("", I18n.deviceManage.logout(), () => {
div.remove();
r.html = document.createElement("div");
manageSessions.returnFromSub();
fetch(this.info.api + "/auth/sessions/logout", {
method: "POST",
headers: this.headers,
body: JSON.stringify({
session_id_hashes: [session.id_hash],
}),
});
});
} else sub.addText(I18n.deviceManage.curSes());
};
}
})();
}
{
const trusted = settings.addButton(I18n.localuser.trusted(), {contained: true});
trusted.addMDText(new MarkDown(I18n.localuser.trustedDesc()));
for (const thing of MarkDown.trustedDomains) {
const div = document.createElement("div");
div.classList.add("flexltr", "trustedDomain");
const name = document.createElement("span");
name.textContent = thing;
const remove = document.createElement("button");
remove.textContent = I18n.remove();
remove.onclick = () => {
MarkDown.saveTrusted();
MarkDown.trustedDomains.delete(thing);
MarkDown.saveTrusted(true);
div.remove();
};
div.append(name, remove);
trusted.addHTMLArea(div);
}
}
settings.addButton(I18n.localuser.advanced(), {head: true});
{
const devPortal = settings.addButton(I18n.localuser.devPortal(), {contained: true});
fetch(this.info.api + "/teams", {
headers: this.headers,
}).then(async (teamsRes) => {
const teams = await teamsRes.json();
const button = devPortal.addSubButtonInput(I18n.localuser.createApp(), (sub) => {
const form = sub.addForm(
"",
(json: any) => {
if (json.message) form.error("name", json.message);
else {
devPortal.returnFromSub();
this.manageApplication(json.id, devPortal, () => {
form.options.deleteElm(button);
});
}
},
{
fetchURL: this.info.api + "/applications",
headers: this.headers,
method: "POST",
},
);
form.addTextInput("Name:", "name", {required: true});
form.addSelect(
I18n.localuser["team:"](),
"team_id",
["Personal", ...teams.map((team: {name: string}) => team.name)],
{defaultIndex: 0},
);
});
const appListContainer = document.createElement("div");
appListContainer.id = "app-list-container";
fetch(this.info.api + "/applications", {
headers: this.headers,
})
.then((r) => r.json())
.then((json) => {
json.forEach(
(application: {
cover_image: any;
icon: any;
id: string | undefined;
name: string | number;
bot: any;
}) => {
const container = document.createElement("div");
if (application.cover_image || application.icon) {
const cover = createImg(
this.info.cdn +
"/app-icons/" +
application.id +
"/" +
(application.cover_image || application.icon) +
".png" +
new CDNParams({expectedSize: 256}),
);
cover.alt = "";
cover.loading = "lazy";
container.appendChild(cover);
}
const name = document.createElement("h2");
name.textContent = application.name + (application.bot ? " (Bot)" : "");
container.appendChild(name);
container.addEventListener("click", async () => {
this.manageApplication(application.id, devPortal, () => {
appListContainer.remove();
});
});
appListContainer.appendChild(container);
},
);
});
devPortal.addHTMLArea(appListContainer);
});
}
if (
this.rights.hasPermission("OPERATOR") ||
this.rights.hasPermission("CREATE_REGISTRATION_TOKENS")
) {
const manageInstance = settings.addButton(I18n.localuser.manageInstance(), {contained: true});
if (this.rights.hasPermission("OPERATOR")) {
manageInstance.addButtonInput("", I18n.manageInstance.stop(), () => {
const menu = new Dialog("");
const options = menu.float.options;
options.addTitle(I18n.manageInstance.AreYouSureStop());
const yesno = options.addOptions("", {ltr: true});
yesno.addButtonInput("", I18n.yes(), () => {
fetch(this.info.api + "/stop", {headers: this.headers, method: "POST"});
menu.hide();
});
yesno.addButtonInput("", I18n.no(), () => {
menu.hide();
});
menu.show();
});
}
if (this.rights.hasPermission("CREATE_REGISTRATION_TOKENS")) {
manageInstance.addSubButtonInput(
I18n.manageInstance.createTokens(),
(tokens) => {
const count = tokens.addTextInput(I18n.manageInstance.count(), () => {}, {
initText: "1",
});
const length = tokens.addTextInput(I18n.manageInstance.length(), () => {}, {
initText: "32",
});
const format = tokens.addSelect(
I18n.manageInstance.format(),
() => {},
[
I18n.manageInstance.TokenFormats.JSON(),
I18n.manageInstance.TokenFormats.plain(),
I18n.manageInstance.TokenFormats.URLs(),
],
{
defaultIndex: 2,
},
);
format.watchForChange((e) => {
if (e !== 2) {
urlOption.removeAll();
} else {
makeURLMenu();
}
});
const urlOption = tokens.addOptions("");
const urlOptionsJSON = {
url: window.location.origin,
type: "Fermi",
};
function makeURLMenu() {
urlOption
.addTextInput(I18n.manageInstance.clientURL(), () => {}, {
initText: urlOptionsJSON.url,
})
.watchForChange((str) => {
urlOptionsJSON.url = str;
});
urlOption
.addSelect(
I18n.manageInstance.regType(),
() => {},
["Fermi", I18n.manageInstance.genericType()],
{
defaultIndex: ["Fermi", "generic"].indexOf(urlOptionsJSON.type),
},
)
.watchForChange((i) => {
urlOptionsJSON.type = ["Fermi", "generic"][i];
});
}
makeURLMenu();
tokens.addButtonInput("", I18n.manageInstance.create(), async () => {
const params = new URLSearchParams();
params.set("count", count.value);
params.set("length", length.value);
const json = (await (
await fetch(
this.info.api + "/auth/generate-registration-tokens?" + params.toString(),
{
headers: this.headers,
},
)
).json()) as {tokens: string[]};
if (format.index === 0) {
pre.textContent = JSON.stringify(json.tokens);
} else if (format.index === 1) {
pre.textContent = json.tokens.join("\n");
} else if (format.index === 2) {
if (urlOptionsJSON.type === "Fermi") {
const options = new URLSearchParams();
options.set("instance", this.instanceString());
pre.textContent = json.tokens
.map((token) => {
options.set("token", token);
return `${urlOptionsJSON.url}/register?` + options.toString();
})
.join("\n");
} else {
const options = new URLSearchParams();
pre.textContent = json.tokens
.map((token) => {
options.set("token", token);
return `${urlOptionsJSON.url}/register?` + options.toString();
})
.join("\n");
}
}
});
tokens.addButtonInput("", I18n.manageInstance.copy(), async () => {
try {
if (pre.textContent) {
await navigator.clipboard.writeText(pre.textContent);
}
} catch (err) {
console.error(err);
}
});
const pre = document.createElement("pre");
tokens.addHTMLArea(pre);
},
{
noSubmit: true,
},
);
}
}
(async () => {
const jankInfo = settings.addButton(I18n.jankInfo(), {contained: true});
const img = document.createElement("img");
img.src = "/logo.svg";
jankInfo.addHTMLArea(img);
img.width = 128;
img.height = 128;
const ver = await (await fetch("/getupdates")).text();
jankInfo.addMDText(
new MarkDown(
I18n.clientDesc(ver, window.location.origin, this.rights.allow + ""),
undefined,
),
);
})();
{
const devSettings = settings.addButton(I18n.devSettings.name(), {
noSubmit: true,
contained: true,
});
devSettings.addText(I18n.devSettings.description());
devSettings.addHR();
const box1 = devSettings.addCheckboxInput(I18n.devSettings.logGateway(), () => {}, {
initState: getDeveloperSettings().gatewayLogging,
});
box1.onchange = (e) => {
const settings = getDeveloperSettings();
settings.gatewayLogging = e;
setDeveloperSettings(settings);
};
const box2 = devSettings.addCheckboxInput(I18n.devSettings.badUser(), () => {}, {
initState: getDeveloperSettings().logBannedFields,
});
box2.onchange = (e) => {
const settings = getDeveloperSettings();
settings.logBannedFields = e;
setDeveloperSettings(settings);
};
const box3 = devSettings.addCheckboxInput(I18n.devSettings.traces(), () => {}, {
initState: getDeveloperSettings().showTraces,
});
box3.onchange = (e) => {
const settings = getDeveloperSettings();
settings.showTraces = e;
setDeveloperSettings(settings);
};
const box4 = devSettings.addCheckboxInput(I18n.devSettings.cache(), () => {}, {
initState: getDeveloperSettings().cacheSourceMaps,
});
box4.onchange = (e) => {
const settings = getDeveloperSettings();
settings.cacheSourceMaps = e;
setDeveloperSettings(settings);
SW.postMessage({code: "isDev", dev: e});
};
devSettings.addText(I18n.devSettings.cacheDesc());
const box5 = devSettings.addCheckboxInput(I18n.devSettings.captureTrace(), () => {}, {
initState: getDeveloperSettings().interceptApiTraces,
});
box5.onchange = (e) => {
const settings = getDeveloperSettings();
settings.interceptApiTraces = e;
setDeveloperSettings(settings);
SW.traceInit();
};
const box6 = devSettings.addCheckboxInput(I18n.devSettings.gatewayComp(), () => {}, {
initState: getDeveloperSettings().gatewayCompression,
});
box6.onchange = (e) => {
const settings = getDeveloperSettings();
settings.gatewayCompression = e;
setDeveloperSettings(settings);
SW.traceInit();
};
const box7 = devSettings.addCheckboxInput(I18n.devSettings.reportSystem(), () => {}, {
initState: getDeveloperSettings().reportSystem,
});
box7.onchange = (e) => {
const settings = getDeveloperSettings();
settings.reportSystem = e;
setDeveloperSettings(settings);
SW.traceInit();
};
devSettings.addButtonInput("", I18n.devSettings.clearWellKnowns(), async () => {
const currentUserInfos = JSON.parse(localStorage.getItem("userinfos")!);
await Promise.all(
Object.keys(currentUserInfos.users).map(async (user) => {
const key =
currentUserInfos.users[user].serverurls.value ??
currentUserInfos.users[user].serverurls.wellknown ??
currentUserInfos.users[user].serverurls.api;
currentUserInfos.users[user].serverurls = await getapiurls(key);
console.log(key, currentUserInfos.users[user].serverurls);
localStorage.setItem("userinfos", JSON.stringify(currentUserInfos));
}),
);
localStorage.removeItem("instanceinfo");
await SW.postMessage({
code: "clearCdnCache",
});
// @ts-ignore - chromium is smelly for not supporting the `forceGet` option (aka skip cache)
window.location.reload(true);
});
}
if (this.trace.length && getDeveloperSettings().showTraces) {
const traces = settings.addButton(I18n.localuser.trace(), {
noSubmit: true,
contained: true,
});
const traceArr = this.trace;
const sel = traces.addSelect(
"",
() => {},
this.trace.map((_) =>
I18n.trace.traces(
_.trace[0],
_.trace[1].micros / 1000 + "",
_.time.getHours() + ":" + _.time.getMinutes(),
),
),
);
function generateTraceHTML(trace: trace, indent: number): HTMLElement {
const div = document.createElement("div");
div.classList.add("traceDiv", "flexttb");
const head = document.createElement("div");
div.append(head);
const title = document.createElement("h3");
title.textContent = I18n.trace.totalTime(trace[1].micros / 1000 + "", trace[0]);
const indents = document.createElement("span");
indents.classList.add("visually-hidden");
indents.textContent = " ".repeat(indent);
title.prepend(indents);
head.append(title);
if (!trace[1].calls) return div;
let objs: {name: string; val: traceObj}[] = [];
{
const names = trace[1].calls.filter((_) => typeof _ === "string");
const vals = trace[1].calls.filter((_) => _ instanceof Object);
let i = 0;
for (const name of names) {
const val = vals[i];
objs.push({name, val});
i++;
}
}
const bars = document.createElement("div");
bars.classList.add("flexltr", "traceBars");
const colors = ["red", "orange", "yellow", "lime", "blue", "indigo", "violet"];
let i = 0;
for (const thing of objs) {
const bar = document.createElement("div");
bar.style.setProperty(
"flex-grow",
Math.ceil((thing.val.micros / trace[1].micros) * 1000) + "",
);
bar.style.setProperty("background", colors[i % colors.length]);
bars.append(bar);
new Hover(I18n.trace.totalTime(thing.val.micros / 1000 + "", thing.name)).addEvent(bar);
i++;
}
const body = document.createElement("div");
div.append(body);
head.append(bars);
let dropped = false;
head.onclick = () => {
if (!trace[1].calls) return;
if (dropped) {
dropped = false;
body.innerHTML = "";
return;
}
let i = 0;
for (const obj of objs) {
body.append(generateTraceHTML([obj.name, obj.val], indent + 1));
i++;
}
dropped = true;
};
div.classList.add("dropDownTrace");
head.classList.add("traceHead");
return div;
}
const blank = document.createElement("div");
traces.addHTMLArea(blank);
const updateInfo = () => {
const trace = traceArr[sel.index];
blank.innerHTML = "";
blank.append(generateTraceHTML(trace.trace, 0));
};
sel.onchange = () => {
updateInfo();
};
updateInfo();
}
{
const update = settings.addButton(I18n.localuser.updateSettings(), {contained: true});
let index = ServiceWorkerModeValues.indexOf(localSettings.serviceWorkerMode);
if (index === -1) {
index = 2;
}
const sw = update.addSelect(
I18n.settings.updates.serviceWorkerMode.title(),
() => {},
ServiceWorkerModeValues.map((e) => I18n.settings.updates.serviceWorkerMode[e]()),
{
defaultIndex: index,
},
);
sw.onchange = (e) => {
SW.setMode(ServiceWorkerModeValues[e]);
};
update.addButtonInput("", I18n.localuser.CheckUpdate(), async () => {
const update = await SW.checkUpdates();
const text = update ? I18n.localuser.updatesYay() : I18n.localuser.noUpdates();
const d = new Dialog("");
d.options.addTitle(text);
if (update) {
d.options.addButtonInput("", I18n.localuser.refreshPage(), () => {
window.location.reload();
});
}
d.show();
});
update.addButtonInput("", I18n.localuser.clearCache(), () => {
SW.forceClear();
});
}
{
const instanceInfo = settings.addButton(I18n.instanceInfo.name(), {contained: true});
fetch(this.info.api + "/policies/instance", {
headers: this.headers,
})
.then((_) => _.json())
.then((body) => {
const json = body as {
instanceName: string;
instanceDescription: string | null;
frontPage: string | null;
tosPage: string | null;
correspondenceEmail: string | null;
correspondenceUserID: string | null;
image: string | null;
instanceId: string;
autoCreateBotUsers: false;
publicUrl: string | null;
};
instanceInfo.addTitle(json.instanceName);
if (json.correspondenceEmail) {
const a = document.createElement("a");
a.target = "_blank";
a.rel = "noreferrer";
a.href = "mailto:" + json.correspondenceEmail;
a.textContent = I18n.instanceInfo.contact();
instanceInfo.addHTMLArea(a);
}
if (json.tosPage)
instanceInfo.addMDText(new MarkDown(I18n.instanceInfo.tosPage(json.tosPage)));
if (json.publicUrl)
instanceInfo.addMDText(new MarkDown(I18n.instanceInfo.publicUrl(json.publicUrl)));
if (json.frontPage)
instanceInfo.addMDText(new MarkDown(I18n.instanceInfo.frontPage(json.frontPage)));
instanceInfo.addButtonInput("", I18n.instInfo(), () => {
this.instanceStats();
});
});
}
settings.addButton(I18n.localuser.danger(), {head: true});
{
const deleteAccount = settings
.addButton(I18n.localuser.deleteAccount(), {contained: true, color: buttonColor.RED})
.addForm(
"",
(e) => {
if ("message" in e) {
if (typeof e.message === "string") {
throw new FormError(password, e.message);
}
} else {
this.userinfo.remove();
window.location.href = "/";
}
},
{
headers: this.headers,
method: "POST",
fetchURL: this.info.api + "/users/@me/delete/",
traditionalSubmit: false,
submitText: I18n.localuser.deleteAccountButton(),
},
);
const shrek = deleteAccount.addTextInput(
I18n.localuser.areYouSureDelete(I18n.localuser.sillyDeleteConfirmPhrase()),
"shrek",
);
const password = deleteAccount.addTextInput(I18n.localuser["password:"](), "password", {
password: true,
});
deleteAccount.addPreprocessor((obj) => {
if ("shrek" in obj) {
if (obj.shrek !== I18n.localuser.sillyDeleteConfirmPhrase()) {
throw new FormError(shrek, I18n.localuser.mustTypePhrase());
}
delete obj.shrek;
} else {
throw new FormError(shrek, I18n.localuser.mustTypePhrase());
}
});
}
settings.show();
}
readonly botTokens: Map<string, string> = new Map();
async manageApplication(appId = "", container: Options, deleteButton: () => void) {
if (this.perminfo.applications) {
for (const item of Object.keys(this.perminfo.applications)) {
this.botTokens.set(item, this.perminfo.applications[item]);
}
}
const res = await fetch(this.info.api + "/applications/" + appId, {
headers: this.headers,
});
const json = await res.json();
console.error(json);
const form = container.addSubForm(json.name, () => {}, {
fetchURL: this.info.api + "/applications/" + appId,
method: "PATCH",
headers: this.headers,
traditionalSubmit: true,
});
form.addTextInput(I18n.localuser.appName(), "name", {initText: json.name});
form.addMDInput(I18n.localuser.description(), "description", {
initText: json.description,
});
form.addImageInput("Icon:", "icon", {
clear: true,
initImg: json.icon
? this.info.cdn +
"/app-icons/" +
appId +
"/" +
json.icon +
new CDNParams({expectedSize: 96})
: "",
});
form.addTextInput(I18n.localuser.privacyPolcyURL(), "privacy_policy_url", {
initText: json.privacy_policy_url,
});
form.addText(I18n.localuser.appID(appId));
form.addSubButtonInput(
I18n.localuser.showSecret(),
(opt) => {
opt.addText(I18n.localuser.clientSecret(json.verify_key));
},
{
intText: I18n.localuser.secret(),
},
);
form.addTextInput(I18n.localuser.TOSURL(), "terms_of_service_url", {
initText: json.terms_of_service_url,
});
form.addCheckboxInput(I18n.localuser.publicAvaliable(), "bot_public", {
initState: json.bot_public,
});
form.addCheckboxInput(I18n.localuser.requireCode(), "bot_require_code_grant", {
initState: json.bot_require_code_grant,
});
form.addButtonInput("", I18n.localuser[json.bot ? "manageBot" : "addBot"](), async () => {
if (!json.bot) {
if (!confirm(I18n.localuser.confirmAddBot())) {
return;
}
const updateRes = await fetch(this.info.api + "/applications/" + appId + "/bot", {
method: "POST",
headers: this.headers,
});
const updateJSON = await updateRes.json();
this.botTokens.set(appId, updateJSON.token);
}
this.manageBot(appId, form);
});
form.addButtonInput("", I18n.applications.delete(), () => {
const sub = form.addSubForm(
I18n.applications.delete(),
() => {
deleteButton();
container.returnFromSub();
},
{
fetchURL: this.info.api + "/applications/" + appId + "/delete",
method: "POST",
headers: this.headers,
submitText: I18n.delete(),
},
);
sub.addText(I18n.applications.sure(json.name));
});
}
async manageBot(appId = "", container: Form) {
const res = await fetch(this.info.api + "/applications/" + appId, {
headers: this.headers,
});
const json = await res.json();
if (!json.bot) {
return alert(I18n.localuser.confuseNoBot());
}
const bot: User = new User(json.bot, this);
const form = container.addSubForm(
I18n.localuser.editingBot(bot.username),
(out) => {
console.log(out);
},
{
method: "PATCH",
fetchURL: this.info.api + "/applications/" + appId + "/bot",
headers: this.headers,
traditionalSubmit: true,
},
);
form.addTextInput(I18n.localuser.botUsername(), "username", {
initText: bot.username,
});
form.addImageInput(I18n.localuser.botAvatar(), "avatar", {
initImg: bot.getpfpsrc(),
clear: true,
});
form.addButtonInput("", I18n.localuser.resetToken(), async () => {
if (!confirm(I18n.localuser.confirmReset())) {
return;
}
const updateRes = await fetch(this.info.api + "/applications/" + appId + "/bot/reset", {
method: "POST",
headers: this.headers,
});
const updateJSON = await updateRes.json();
text.setText(I18n.localuser.tokenDisplay(updateJSON.token));
this.botTokens.set(appId, updateJSON.token);
if (this.perminfo.applications[appId]) {
this.perminfo.applications[appId] = updateJSON.token;
}
});
const text = form.addText(
I18n.localuser.tokenDisplay(
this.botTokens.has(appId) ? (this.botTokens.get(appId) as string) : "*****************",
),
);
const check = form.addOptions("", {noSubmit: true});
if (!this.perminfo.applications) {
this.perminfo.applications = {};
}
const checkbox = check.addCheckboxInput(I18n.localuser.saveToken(), () => {}, {
initState: !!this.perminfo.applications[appId],
});
checkbox.watchForChange((_) => {
if (_) {
if (this.botTokens.has(appId)) {
this.perminfo.applications[appId] = this.botTokens.get(appId);
} else {
alert(I18n.localuser.noToken());
checkbox.setState(false);
}
} else {
delete this.perminfo.applications[appId];
}
});
form.addButtonInput("", I18n.localuser.advancedBot(), () => {
const token = this.botTokens.get(appId);
if (token) {
//TODO check if this is actually valid or not
const botc = new Bot(bot as unknown as mainuserjson, token, this);
botc.settings();
}
});
form.addButtonInput("", I18n.localuser.botInviteCreate(), () => {
Bot.InviteMaker(appId, form, this.info);
});
}
updateSend() {
const typebox = document.getElementById("typebox") as CustomHTMLDivElement;
if (
(typebox.markdown.rawString && typebox.markdown.rawString !== "\n") ||
document.getElementById("pasteimage")?.children.length
) {
typebox.parentElement!.classList.remove("noConent");
} else {
typebox.parentElement!.classList.add("noConent");
}
}
//TODO make this an option
readonly autofillregex = Object.freeze(/(^|\s|\n)[@#:]([a-zA-Z0-9]*)$/i);
mdBox() {
const typebox = document.getElementById("typebox") as CustomHTMLDivElement;
const typeMd = typebox.markdown;
typeMd.owner = this;
typeMd.onUpdate = (str, pre) => {
this.search(document.getElementById("searchOptions") as HTMLDivElement, typeMd, str, pre);
this.updateSend();
};
}
async pinnedClick(rect: DOMRect) {
if (!this.focusChannel) return;
await this.focusChannel.pinnedClick(rect);
}
async makeStickerBox(rect: DOMRect) {
const sticker = await Sticker.stickerPicker(
-0 + rect.right - window.innerWidth,
-20 + rect.top - window.innerHeight,
this,
);
this.favorites.addStickerFreq(sticker.id);
console.log(sticker);
if (this.focusChannel) {
this.focusChannel.sendMessage("", {
embeds: [],
attachments: [],
sticker_ids: [sticker.id],
replyingto: this.focusChannel.replyingto,
});
this.focusChannel.replyingto = null;
}
}
async makeGifBox(rect: DOMRect) {
interface fullgif {
id: string;
title: string;
url: string;
src: string;
gif_src: string;
width: number;
height: number;
preview: string;
}
const menu = document.createElement("div");
menu.classList.add("flexttb", "gifmenu");
menu.style.bottom = window.innerHeight - rect.top + 15 + "px";
menu.style.right = window.innerWidth - rect.right + "px";
document.body.append(menu);
Contextmenu.keepOnScreen(menu);
Contextmenu.declareMenu(menu);
const trending = (await (
await fetch(
this.info.api +
"/gifs/trending?" +
new URLSearchParams([
["locale", I18n.lang],
["provider", this.selectedGifProfidor?.api_name!],
]),
{headers: this.headers},
)
).json()) as {
categories: {
name: string;
src: string;
}[];
gifs: [fullgif];
};
await fetch(
this.info.api +
"/gifs/trending-gifs?" +
new URLSearchParams([
["locale", I18n.lang],
["provider", this.selectedGifProfidor?.api_name!],
]),
{headers: this.headers},
);
const gifbox = document.createElement("div");
gifbox.classList.add("gifbox");
const search = document.createElement("input");
let gifs = gifbox;
const placeGifs = (
gifs: HTMLDivElement,
gifReturns: {src: string; width: number; height: number; title?: string}[],
) => {
const width = menu.getBoundingClientRect().width;
let left = 0;
let right = width < 370 ? Infinity : 0;
console.warn(right, width);
for (const gif of gifReturns) {
const div = document.createElement("div");
div.classList.add("gifBox");
const img = createImg(gif.src);
this.refreshIfNeeded(gif.src).then((url) => {
if (url === gif.src) return;
img.setSrcs(url);
});
if (gif.title) img.alt = gif.title;
const scale = gif.width / 196;
img.width = gif.width / scale;
img.height = gif.height / scale;
div.append(img);
if (left <= right) {
div.style.top = left + "px";
left += Math.ceil(img.height) + 10;
div.style.left = "5px";
} else {
div.style.top = right + "px";
right += Math.ceil(img.height) + 10;
div.style.left = "210px";
}
gifs.append(div);
div.onclick = () => {
if (this.focusChannel) {
this.focusChannel.sendMessage(gif.src, {
embeds: [],
attachments: [],
sticker_ids: [],
replyingto: this.focusChannel.replyingto,
});
menu.remove();
this.focusChannel.replyingto = null;
}
};
}
gifs.style.height = (right == Infinity ? left : Math.max(left, right)) + "px";
};
const searchBox = async () => {
gifs.remove();
if (search.value === "") {
menu.append(gifbox);
gifs = gifbox;
return;
}
gifs = document.createElement("div");
gifs.classList.add("gifbox");
menu.append(gifs);
const sValue = search.value;
const gifReturns = (await (
await fetch(
this.info.api +
"/gifs/search?" +
new URLSearchParams([
["locale", I18n.lang],
["q", sValue],
["limit", "500"],
["provider", this.selectedGifProfidor?.api_name!],
]),
{headers: this.headers},
)
).json()) as fullgif[];
if (sValue !== search.value) {
return;
}
placeGifs(
gifs,
gifReturns.map((gif) => {
return {src: gif.gif_src, width: gif.width, height: gif.height, title: gif.title};
}),
);
};
let last = "";
search.onkeyup = () => {
if (last === search.value) {
return;
}
last = search.value;
searchBox();
};
search.classList.add("searchGifBar");
//TODO fix this once we swap over
search.placeholder = I18n.searchGifs(this.selectedGifProfidor?.name!);
const favs = this.favorites.favoriteGifs();
if (favs.length) {
favs.forEach(async (_) => (_.src = await this.refreshIfNeeded(_.src)));
const div = document.createElement("div");
div.classList.add("gifPreviewBox");
const img = document.createElement("img");
img.src = favs[0].src;
img.src = await this.refreshIfNeeded(img.src);
const title = document.createElement("span");
title.textContent = I18n.favoriteGifs();
div.append(img, title);
gifbox.append(div);
div.onclick = (e) => {
e.stopImmediatePropagation();
search.remove();
gifs.remove();
gifs = document.createElement("div");
gifs.classList.add("gifbox");
const div = document.createElement("div");
div.classList.add("flexltr", "title");
const back = document.createElement("span");
back.classList.add("svg-leftArrow");
back.onclick = (e) => {
e.stopImmediatePropagation();
div.remove();
gifs.remove();
gifs = gifbox;
menu.append(search, gifbox);
};
const title = document.createElement("h3");
title.textContent = I18n.favoriteGifs();
div.append(back, title);
menu.append(div, gifs);
placeGifs(gifs, favs);
};
}
for (const category of trending.categories) {
const div = document.createElement("div");
div.classList.add("gifPreviewBox");
const img = document.createElement("img");
img.src = category.src;
const title = document.createElement("span");
title.textContent = category.name;
div.append(img, title);
gifbox.append(div);
div.onclick = (e) => {
e.stopImmediatePropagation();
search.value = category.name;
searchBox();
};
}
menu.append(search, gifbox);
search.focus();
}
async TBEmojiMenu(rect: DOMRect) {
const typebox = document.getElementById("typebox") as CustomHTMLDivElement;
const p = saveCaretPosition(typebox);
if (!p) return;
const original = MarkDown.getText();
const emoji = await Emoji.emojiPicker(
-0 + rect.right - window.innerWidth,
-20 + rect.top - window.innerHeight,
this,
);
this.favorites.addEmoji(emoji.id || (emoji.emoji as string));
p();
const md = typebox.markdown;
this.MDReplace(
emoji.id
? `<${emoji.animated ? "a" : ""}:${emoji.name}:${emoji.id}>`
: (emoji.emoji as string),
original,
md,
null,
);
}
MDReplace(
replacewith: string,
original: string,
typebox: MarkDown,
start: RegExp | null = this.autofillregex,
) {
let raw = typebox.rawString;
let empty = raw.length === 0;
raw = original !== "" ? raw.split(original)[1] : raw;
if (raw === undefined && !empty) return;
if (empty) {
raw = "";
}
raw = (start ? original.replace(start, "") : original) + " " + replacewith + raw;
typebox.txt = raw.split("");
const match = start ? original.match(start) : true;
if (match) {
typebox.boxupdate(
replacewith.length - (match === true ? 0 : match[0].length) + 1,
false,
original.length,
);
}
}
fileExtange!: (
files: Blob[],
html: WeakMap<Blob, HTMLElement>,
) => [Blob[], WeakMap<Blob, HTMLElement>];
MDSearchOptions(
options: MDSearchOption[],
original: string,
div: HTMLDivElement = document.getElementById("searchOptions") as HTMLDivElement,
typebox?: MarkDown,
) {
if (!div) return;
div.innerHTML = "";
let i = 0;
const htmloptions: HTMLSpanElement[] = [];
for (const {name, replace, icon, otherLogic} of options) {
if (i == 8) {
break;
}
i++;
const span = document.createElement("span");
htmloptions.push(span);
if (icon) {
span.append(icon);
}
span.append(name);
span.onclick = (e) => {
if (e) {
if (replace) {
const selection = window.getSelection() as Selection;
const box = typebox?.box.deref();
if (!box) return;
if (selection) {
const pos = getTextNodeAtPosition(
box,
original.length -
(original.match(this.autofillregex) as RegExpMatchArray)[0].length +
replace.length,
);
selection.removeAllRanges();
const range = new Range();
range.setStart(pos.node, pos.position);
selection.addRange(range);
}
box.focus();
}
e.preventDefault();
}
if (!otherLogic?.() && typebox) {
this.MDReplace(replace, original, typebox);
}
div.innerHTML = "";
remove();
};
div.prepend(span);
}
const remove = () => {
if (div && div.innerHTML === "") {
this.keyup = () => false;
this.keydown = () => {};
return true;
}
return false;
};
if (htmloptions[0]) {
let curindex = 0;
let cur = htmloptions[0];
cur.classList.add("selected");
const cancel = new Set(["ArrowUp", "ArrowDown", "Enter", "Tab"]);
this.keyup = (event) => {
if (remove()) return false;
if (cancel.has(event.key)) {
switch (event.key) {
case "ArrowUp":
if (htmloptions[curindex + 1]) {
cur.classList.remove("selected");
curindex++;
cur = htmloptions[curindex];
cur.classList.add("selected");
}
break;
case "ArrowDown":
if (htmloptions[curindex - 1]) {
cur.classList.remove("selected");
curindex--;
cur = htmloptions[curindex];
cur.classList.add("selected");
}
break;
case "Enter":
case "Tab":
cur.click();
break;
}
return true;
}
return false;
};
this.keydown = (event) => {
if (remove()) return;
if (cancel.has(event.key)) {
event.preventDefault();
}
};
} else {
remove();
}
}
MDFindChannel(name: string, original: string, box: HTMLDivElement, typebox: MarkDown) {
const maybe: [number, Channel][] = [];
if (this.focusGuild && this.focusGuild.id !== "@me") {
for (const channel of this.focusGuild.channels.filter((_) => _.visible)) {
const confidence = channel.similar(name);
if (confidence > 0) {
maybe.push([confidence, channel]);
}
}
}
maybe.sort((a, b) => b[0] - a[0]);
this.MDSearchOptions(
maybe.map((a) => {
return {name: "# " + a[1].name, replace: `<#${a[1].id}> `};
}),
original,
box,
typebox,
);
}
MDFineMentionGen(name: string, original: string, box: HTMLDivElement, typebox: MarkDown) {
let members: [Member | Role | User | "@everyone" | "@here", number][] = [];
if (this.focusGuild && name !== "everyone" && name !== "here") {
if (this.focusGuild.id === "@me") {
const dirrect = this.focusChannel as Group;
for (const user of dirrect.users) {
const rank = user.compare(name);
if (rank > 0) {
members.push([user, rank]);
}
}
} else {
for (const [_, member] of this.focusGuild.members) {
if (!this.focusChannel?.hasPermission("VIEW_CHANNEL", member)) continue;
const rank = member.compare(name);
if (rank > 0) {
members.push([member, rank]);
}
}
for (const role of this.focusGuild.roles.filter((_) => _.id !== this.focusGuild?.id)) {
const rank = role.compare(name);
if (rank > 0) {
members.push([role, rank]);
}
}
}
function similar(str2: string | null | undefined) {
if (!str2) return 0;
const strl = Math.max(name.length, 1);
if (str2.includes(name)) {
return strl / str2.length;
} else if (str2.toLowerCase().includes(name.toLowerCase())) {
return strl / str2.length / 1.2;
}
return 0;
}
const everyoneScore = similar("everyone");
if (everyoneScore) members.push(["@everyone", everyoneScore]);
const hereScore = similar("here");
if (hereScore) members.push(["@here", hereScore]);
}
members.sort((a, b) => b[1] - a[1]);
this.MDSearchOptions(
members.map(([member]) => {
const icon =
member instanceof Member
? member.user.buildpfp(member)
: member instanceof User
? member.buildpfp()
: undefined;
icon?.classList.add("pfpSearch");
return {
name: typeof member === "string" ? member : "@" + member.name,
replace:
member instanceof Role
? `<@&${member.id}> `
: typeof member === "string"
? member + " "
: `<@${member.id}> `,
icon: icon,
};
}),
original,
box,
typebox,
);
}
private MDGenID = 0;
MDFindMention(name: string, original: string, box: HTMLDivElement, typebox: MarkDown) {
if (this.ws && this.focusGuild) {
const id = ++this.MDGenID;
this.MDFineMentionGen(name, original, box, typebox);
if (this.focusGuild.member_count <= this.focusGuild.members.size) return;
if (this.focusGuild.id !== "@me") {
this.focusGuild.searchMembers(8, name).then(async () => {
if (!typebox.rawString.startsWith(original)) return;
if (this.MDGenID === id) this.MDFineMentionGen(name, original, box, typebox);
});
}
}
}
findEmoji(search: string, original: string, box: HTMLDivElement, typebox: MarkDown) {
const emj = Emoji.searchEmoji(search, this, 10);
const map = emj.map(([emoji]): MDSearchOption => {
return {
name: emoji.name,
replace: emoji.id
? `<${emoji.animated ? "a" : ""}:${emoji.name}:${emoji.id}>`
: (emoji.emoji as string),
icon: emoji.getHTML(),
otherLogic: () => {
this.favorites.addEmoji(emoji.id || (emoji.emoji as string));
return false;
},
};
});
this.MDSearchOptions(map, original, box, typebox);
}
async findCommands(search: string, box: HTMLDivElement, md: MarkDown) {
const guild = this.focusGuild;
if (!guild) return;
const commands = await guild.getCommands();
const sorted = commands
.map((_) => [_, _.similar(search)] as const)
.filter((_) => _[1] !== 0)
.sort((a, b) => b[1] - a[1])
.slice(0, 10);
this.MDSearchOptions(
sorted.map(([elm]) => {
return {
name: `/${elm.localizedName}`,
replace: "",
otherLogic: () => {
this.focusChannel?.startCommand(elm);
return true;
},
};
}),
"",
box,
md,
);
console.log(sorted, search);
}
search(box: HTMLDivElement, md: MarkDown, str: string, pre: boolean) {
if (!pre) {
const match = str.match(this.autofillregex);
if (match) {
const trim = match[0].trim();
const [type, search] = [trim[0], trim.split(/@|#|:/)[1]];
switch (type) {
case "#":
this.MDFindChannel(search, str, box, md);
break;
case "@":
this.MDFindMention(search, str, box, md);
break;
case ":":
if (search.length >= 2) {
this.findEmoji(search, str, box, md);
} else {
this.MDSearchOptions([], "", box, md);
}
break;
default:
return;
}
return;
}
const command = str.match(/^\/((\s*[\w\d]+)*)$/);
if (command) {
const search = command[1];
this.findCommands(search, box, md);
}
}
box.innerHTML = "";
}
searching = false;
updateTranslations() {
const searchBox = document.getElementById("searchBox") as HTMLDivElement;
searchBox.style.setProperty("--hint-text", JSON.stringify(I18n.search.search()));
}
makePoll() {
const d = new Dialog(I18n.makePoll());
const opt = d.options;
const q = opt.addTextInput(I18n.poll.question(), () => {});
opt.addText(I18n.poll.answers());
const ansField = document.createElement("div");
ansField.classList.add("flexttb", "pollAnsM");
const answers = ["", ""] as string[];
const genAnswerField = () => {
ansField.textContent = "";
for (let i = 0; i < answers.length; i++) {
const si = i;
const div = document.createElement("div");
div.classList.add("flexltr");
const input = document.createElement("input");
input.type = "text";
input.value = answers[i];
input.onchange = () => {
answers[si] = input.value;
};
const del = document.createElement("span");
del.classList.add("svg-delete", "svgicon");
div.append(input, del);
del.onclick = () => {
answers.splice(si, 1);
genAnswerField();
};
ansField.append(div);
}
};
genAnswerField();
opt.addHTMLArea(ansField);
opt.addButtonInput("", I18n.poll.newAnswer(), () => {
answers.push("");
genAnswerField();
});
const hours = [1, 4, 8, 24, 72, 168, 336] as const;
const h = opt.addSelect(
I18n.poll.duration(),
() => {},
//@ts-ignore-error this is fine :P
hours.map((_) => I18n.poll.durCount[_ + ""]()),
{
defaultIndex: 3,
},
);
const c = opt.addCheckboxInput(I18n.poll.mult(), () => {});
opt.addButtonInput("", I18n.submit(), () => {
const chan = this.focusChannel;
if (!chan) return;
chan.sendMessage("", {
poll: {
question: {
text: q.value,
},
answers: answers.map((_) => ({poll_media: {text: _}})),
duration: hours[h.index] as number,
allow_multiselect: c.value,
},
});
d.hide();
});
d.show();
}
curSearch?: Symbol;
pollUpdateSubMap = new Map<string, (u: pollUpdateJson) => void>();
subToPollUpdate(m: string, func: ((u: pollUpdateJson) => void) | null) {
if (func) this.pollUpdateSubMap.set(m, func);
else this.pollUpdateSubMap.delete(m);
}
mSearch(query: string) {
const searchy = Symbol("search");
this.curSearch = searchy;
const p = new URLSearchParams("?");
this.searching = true;
p.set("content", query.trim());
p.set("sort_by", "timestamp");
p.set("sort_order", "desc");
let maxpage: undefined | number = undefined;
const sideDiv = document.getElementById("sideDiv");
const sideContainDiv = document.getElementById("sideContainDiv");
if (!sideDiv || !sideContainDiv) return;
let authorIds = [] as string[];
let mentionIds = [] as string[];
let channels = [] as Channel[];
const genPage = (page: number) => {
p.set("offset", page * 50 + "");
const guildSearch = this.focusGuild?.id !== "@me";
fetch(
this.info.api +
`${guildSearch ? `/guilds/${this.focusGuild?.id}` : `/channels/${this.focusChannel?.id}`}/messages/search/?` +
p.toString() +
(authorIds.length ? authorIds.map((_) => `&author_id=${_}`).join("") : "") +
(mentionIds.length ? mentionIds.map((_) => `&mentions=${_}`).join("") : "") +
(channels.length ? channels.map((_) => `&channel_id=${_.id}`).join("") : ""),
{
headers: this.headers,
},
)
.then((_) => _.json())
.then((json: {messages: [messagejson][]; total_results: number}) => {
if (this.curSearch !== searchy) {
return;
}
//FIXME total_results shall be ignored as it's known to be bad, spacebar bug.
const messages = json.messages
.map(([m]) => {
const c = this.channels.get(m.channel_id);
if (!c) return;
if (c.messages.get(m.id)) {
return c.messages.get(m.id);
}
return new Message(m, c, true);
})
.filter((_) => _ !== undefined);
sideDiv.innerHTML = "";
if (messages.length == 0 && page !== 0) {
maxpage = page - 1;
genPage(page - 1);
return;
} else if (messages.length !== 50) {
maxpage = page;
}
const sortBar = document.createElement("div");
sortBar.classList.add("flexltr", "sortBar");
const settingsB = document.createElement("button");
settingsB.textContent = I18n.search.settings();
settingsB.onclick = () => {
const d = new Dialog(I18n.search.settings());
const opt = d.options;
const b = p.get("max_id");
const before = opt.addDateInput(I18n.search.before(), () => {}, {
initText: b ? new Date(SnowFlake.stringToUnixTime(b)) : undefined,
});
before.onchange = (_) => {
if (before.dateValue) p.set("max_id", SnowFlake.DateToID(before.dateValue));
else p.delete("max_id");
console.log([...p.entries()], before.dateValue);
};
const a = p.get("min_id");
const after = opt.addDateInput(I18n.search.after(), () => {}, {
initText: a ? new Date(SnowFlake.stringToUnixTime(a)) : undefined,
});
after.onchange = (_) => {
if (after.dateValue) p.set("min_id", SnowFlake.DateToID(after.dateValue));
else p.delete("min_id");
console.log([...p.entries()], after.dateValue);
};
opt.addCheckboxInput(I18n.search.includensfw(), () => {}, {
initState: p.get("include_nsfw") !== "false",
}).onchange = (s) => {
if (s) p.delete("include_nsfw");
else p.set("include_nsfw", "false");
};
const userSearch = async (name: string, ids: string[]) => {
const g = this.focusGuild;
if (!g) return [];
g.searchMembers(8, name);
const members = [] as [User | Member, number][];
if (g.id === "@me") {
const dirrect = this.focusChannel as Group;
for (const user of dirrect.users) {
const rank = user.compare(name);
if (rank > 0) {
members.push([user, rank]);
}
}
} else {
for (const [_, member] of g.members) {
const rank = member.compare(name);
if (rank > 0) {
members.push([member, rank]);
}
}
}
const idSet = new Set(ids);
members.sort((a, b) => b[1] - a[1]);
return members
.filter((_) => !idSet.has(_[0].id))
.slice(0, 5)
.map(([_]) => {
return {value: _.id, name: _.name};
});
};
opt.addAsyncMultiSelect(I18n.search.authors(), () => {}, userSearch, {
defaultValues: authorIds
.map((id) => {
const g = this.focusGuild;
if (!g || g.id === "@me") {
return this.userMap.get(id);
} else {
return [...g.members.values()].find(({id: d}) => d === id);
}
})
.filter((_) => _ !== undefined)
.map((_) => ({value: _.id, name: _.name})),
}).onchange = (values: string[]) => {
authorIds = values;
};
opt.addAsyncMultiSelect(I18n.search.mentions(), () => {}, userSearch, {
defaultValues: mentionIds
.map((id) => {
const g = this.focusGuild;
if (!g || g.id === "@me") {
return this.userMap.get(id);
} else {
return [...g.members.values()].find(({id: d}) => d === id);
}
})
.filter((_) => _ !== undefined)
.map((_) => ({value: _.id, name: _.name})),
}).onchange = (values: string[]) => {
mentionIds = values;
};
if (this.focusGuild?.id !== "@me")
opt.addAsyncMultiSelect(
I18n.search.channels(),
() => {},
(name, ids) => {
const g = this.focusGuild;
if (!g) return [];
const c = g.channels.filter((_) => _.visible);
const maybe: [number, Channel][] = [];
for (const channel of c) {
const confidence = channel.similar(name);
if (confidence > 0) {
maybe.push([confidence, channel]);
}
}
maybe.sort((a, b) => b[0] - a[0]);
const idSet = new Set(ids);
return maybe
.filter((_) => !idSet.has(_[1].id))
.slice(0, 5)
.map(([_r, _]) => {
return {value: _.id, name: _.name};
});
},
{
defaultValues: channels.map((_) => ({name: _.name, value: _.id})),
},
).onchange = (values: string[]) => {
const g = this.focusGuild;
if (!g) return;
channels = values.map((id) => g.getChannel(id)).filter((_) => _ !== undefined);
};
d.onhide = () => {
genPage(0);
};
d.show();
};
const newB = document.createElement("button");
const old = document.createElement("button");
[newB.textContent, old.textContent] = [I18n.search.new(), I18n.search.old()];
old.onclick = () => {
p.set("sort_order", "asc");
deleteMessages();
genPage(0);
};
newB.onclick = () => {
p.set("sort_order", "desc");
deleteMessages();
genPage(0);
};
if (p.get("sort_order") === "asc") {
old.classList.add("selectedB");
} else {
newB.classList.add("selectedB");
}
const spaceElm = document.createElement("div");
spaceElm.classList.add("spaceElm");
sortBar.append(I18n.search.page(page + 1 + ""), spaceElm, settingsB, newB, old);
sideDiv.append(sortBar);
sideContainDiv.classList.add("searchDiv");
let channel: Channel | undefined = undefined;
function deleteMessages() {
for (const elm of htmls) elm.remove();
}
const htmls: HTMLElement[] = [];
sideContainDiv.classList.remove("hideSearchDiv");
for (const message of messages) {
if (channel !== message.channel) {
channel = message.channel;
const h3 = document.createElement("h3");
h3.textContent = channel.name;
h3.classList.add("channelSTitle");
sideDiv.append(h3);
htmls.push(h3);
}
const html = message.buildhtml(undefined, true);
if (message.div) console.error(message.div);
html.addEventListener("click", async () => {
try {
sideContainDiv.classList.add("hideSearchDiv");
await message.channel.focus(message.id);
} catch (e) {
console.error(e);
}
});
sideDiv.append(html);
htmls.push(html);
}
if (messages.length === 0) {
const noMs = document.createElement("h3");
noMs.textContent = I18n.search.nofind();
sideDiv.append(noMs);
}
const bottombuttons = document.createElement("div");
bottombuttons.classList.add("flexltr", "searchNavButtons");
const next = document.createElement("button");
if (page == maxpage) next.disabled = true;
next.onclick = () => {
deleteMessages();
genPage(page + 1);
};
const prev = document.createElement("button");
prev.onclick = () => {
deleteMessages();
genPage(page - 1);
};
if (page == 0) prev.disabled = true;
[next.textContent, prev.textContent] = [I18n.search.next(), I18n.search.back()];
bottombuttons.append(prev, next);
sideDiv.append(bottombuttons);
sideDiv.scrollTo({top: 0, behavior: "instant"});
});
};
if (query === "") {
sideContainDiv.classList.remove("searchDiv");
sideContainDiv.classList.remove("hideSearchDiv");
sideDiv.innerHTML = "";
this.searching = false;
this.getSidePannel();
return;
}
genPage(0);
}
keydown: (event: KeyboardEvent) => unknown = () => {};
keyup: (event: KeyboardEvent) => boolean = () => false;
handleKeyUp(event: KeyboardEvent): boolean {
if (this.keyup(event)) {
return true;
}
if (event.key === "Escape") {
if (event.ctrlKey) {
this.focusGuild?.markAsRead();
} else {
this.focusChannel?.readbottom();
this.focusChannel?.goToBottom();
}
return true;
}
return false;
}
//---------- resolving members code -----------
readonly waitingmembers = new Map<
string,
Map<string, (returns: memberjson | undefined) => void>
>();
readonly presences: Map<string, presencejson> = new Map();
static font?: FontFace;
static async loadFont() {
const prefs = getPreferences();
const fontName = prefs.emojiFont;
if (this.font) {
//TODO see when/if this can be removed
//@ts-ignore this is stupid. it's been here since 2020
document.fonts.delete(this.font);
}
const realname = this.fonts.find((_) => _[1] === fontName)?.[0];
if (realname) {
const font = new FontFace("emojiFont", `url("/emoji/${realname}")`);
await font.load();
console.error("Loaded font:", fontName, "/", realname);
//TODO see when/if this can be removed
//@ts-ignore this is stupid. it's been here since 2020
document.fonts.add(font);
console.log(font);
this.font = font;
}
}
static get fonts() {
const isFirefox = navigator.userAgent.toLowerCase().includes("firefox");
return [
[`NotoColorEmoji-Regular.${isFirefox ? "woff2" : "ttf"}`, "Noto Color Emoji"],
[`OpenMoji-color-glyf_colr_0.woff2`, "OpenMoji"],
[`Twemoji-16.0.1.${isFirefox ? "woff2" : "ttf"}`, "Twemoji"],
[`BlobmojiCompat.${isFirefox ? "woff2" : "ttf"}`, "Blobmoji"],
] as const;
}
readonly getMemberMap = new Map<string, Promise<Member | undefined>>();
async getMember(id: string, guildid: string): Promise<Member | undefined> {
const user = this.userMap.get(id);
const guild = this.guilds.get(guildid) as Guild;
if (user) {
const memb = user.members.get(guild);
if (memb) return memb;
}
const uid = id + "-" + guildid;
const prom = this.getMemberMap.get(uid);
if (prom) return prom;
const prom2 = new Promise<Member | undefined>(async (res) => {
const json = await this.resolvemember(id, guildid);
if (!json) {
res(undefined);
return;
}
res(Member.new(json, guild));
});
this.getMemberMap.set(uid, prom2);
return prom2;
}
memberLock = new PromiseLock();
async resolvemember(id: string, guildid: string): Promise<memberjson | undefined> {
if (guildid === "@me") {
return undefined;
}
const guild = this.guilds.get(guildid);
//TODO well, maybe think this over, I set the member count and whatnot, maybe for "large enough" guilds I return a member that's filled out blankly unless its needed to be fully resolved, added stuff to identify to make it work
const borked = false;
if (!guild || (borked && guild.member_count > 250)) {
const unlock = await this.memberLock.acquireLock();
try {
const req = await fetch(this.info.api + "/guilds/" + guildid + "/members/" + id, {
headers: this.headers,
});
if (req.status !== 200) {
return undefined;
}
return await req.json();
} catch {
return undefined;
} finally {
unlock();
}
}
let guildmap = this.waitingmembers.get(guildid);
if (!guildmap) {
guildmap = new Map();
this.waitingmembers.set(guildid, guildmap);
}
const promise: Promise<memberjson | undefined> = new Promise((res) => {
guildmap.set(id, res);
this.getmembers();
});
return await promise;
}
private readonly fetchingmembers = new Map<string, boolean>();
private readonly memberNonceMap = new Map<string, (r: [memberjson[], string[]]) => void>();
private readonly memberNonceBuild = new Map<string, [memberjson[], string[], number[]]>();
private readonly searchMap = new Map<
string,
(arg: {
chunk_index: number;
chunk_count: number;
nonce: string;
not_found?: string[];
members?: memberjson[];
presences: presencejson[];
}) => unknown
>();
async gotChunk(chunk: {
chunk_index: number;
chunk_count: number;
nonce: string;
not_found?: string[];
members?: memberjson[];
presences: presencejson[];
}) {
for (const thing of chunk.presences) {
if (thing.user) {
this.presences.set(thing.user.id, thing);
}
}
if (this.searchMap.has(chunk.nonce)) {
const func = this.searchMap.get(chunk.nonce);
this.searchMap.delete(chunk.nonce);
if (func) {
func(chunk);
return;
}
}
chunk.members ??= [];
const arr = this.memberNonceBuild.get(chunk.nonce);
if (!arr) return;
arr[0] = arr[0].concat(chunk.members);
if (chunk.not_found) {
arr[1] = chunk.not_found;
}
arr[2].push(chunk.chunk_index);
if (arr[2].length === chunk.chunk_count) {
this.memberNonceBuild.delete(chunk.nonce);
const func = this.memberNonceMap.get(chunk.nonce);
if (!func) return;
func([arr[0], arr[1]]);
this.memberNonceMap.delete(chunk.nonce);
}
}
async getmembers() {
const promise = new Promise((res) => {
setTimeout(res, 10);
});
await promise; //allow for more to be sent at once :P
if (this.ws) {
this.waitingmembers.forEach(async (value, guildid) => {
const keys = value.keys();
if (this.fetchingmembers.has(guildid)) {
return;
}
const build: string[] = [];
for (const key of keys) {
build.push(key);
if (build.length === 100) {
break;
}
}
if (!build.length) {
this.waitingmembers.delete(guildid);
return;
}
const promise: Promise<[memberjson[], string[]]> = new Promise((res) => {
const nonce = "" + Math.floor(Math.random() * 100000000000);
this.memberNonceMap.set(nonce, res);
this.memberNonceBuild.set(nonce, [[], [], []]);
if (!this.ws) return;
this.ws.send(
JSON.stringify({
op: 8,
d: {
user_ids: build,
guild_id: guildid,
limit: 100,
nonce,
presences: true,
},
}),
);
this.fetchingmembers.set(guildid, true);
});
const prom = await promise;
const data = prom[0];
for (const thing of data) {
if (value.has(thing.id)) {
const func = value.get(thing.id);
if (!func) {
value.delete(thing.id);
continue;
}
func(thing);
value.delete(thing.id);
}
}
for (const thing of prom[1]) {
if (value.has(thing)) {
const func = value.get(thing);
if (!func) {
value.delete(thing);
continue;
}
func(undefined);
value.delete(thing);
}
}
this.fetchingmembers.delete(guildid);
this.getmembers();
});
}
}
async pingEndpoint() {
const userInfo = getBulkInfo();
if (!userInfo.instances) userInfo.instances = {};
const wellknown = this.info.wellknown;
if (!userInfo.instances[wellknown]) {
const pingRes = await fetch(this.info.api + "/ping");
const pingJSON = await pingRes.json();
userInfo.instances[wellknown] = pingJSON;
localStorage.setItem("userinfos", JSON.stringify(userInfo));
}
this.instancePing = userInfo.instances[wellknown].instance;
this.pageTitle("Loading...");
}
pageTitle(channelName = "", guildName = "") {
(document.getElementById("channelname") as HTMLSpanElement).textContent = channelName;
(document.getElementsByTagName("title")[0] as HTMLTitleElement).textContent =
channelName +
(guildName ? " | " + guildName : "") +
" | " +
this.instancePing.name +
" | Fermi";
}
async instanceStats() {
const dialog = new Dialog("");
dialog.options.addTitle(I18n.instanceStats.name(this.instancePing.name));
dialog.show();
const res = await fetch(this.info.api + "/policies/stats", {
headers: this.headers,
});
const json = await res.json();
dialog.options.addText(I18n.instanceStats.users(json.counts.user));
dialog.options.addText(I18n.instanceStats.servers(json.counts.guild));
dialog.options.addText(I18n.instanceStats.messages(json.counts.message));
dialog.options.addText(I18n.instanceStats.members(json.counts.members));
}
async refreshIfNeeded(url: string) {
const urlObj = new URL(url);
if (urlObj.host === new URL(this.info.cdn).host) {
if (urlObj.searchParams.get("ex")) {
if (Number.parseInt(urlObj.searchParams.get("ex") || "", 16) >= Date.now() - 5000) {
return url;
}
}
const newUrl = this.refreshURL(url);
newUrl.then((_) => (url = _));
return newUrl;
}
return url;
}
private refreshTimeOut?: NodeJS.Timeout;
private urlsToRefresh: [string, (arg: string) => void][] = [];
refreshURL(url: string): Promise<string> {
if (!this.refreshTimeOut) {
this.refreshTimeOut = setTimeout(async () => {
const refreshes = this.urlsToRefresh;
this.urlsToRefresh = [];
delete this.refreshTimeOut;
const res = await fetch(this.info.api + "/attachments/refresh-urls", {
method: "POST",
body: JSON.stringify({attachment_urls: refreshes.map((_) => _[0])}),
headers: this.headers,
});
const body: {
refreshed_urls: string[];
} = await res.json();
let i = 0;
for (const url of body.refreshed_urls) {
refreshes[i][1](url);
i++;
}
}, 100);
}
return new Promise((res) => {
this.urlsToRefresh.push([url, res]);
});
}
getNotiVolume(): number {
const userinfos = getBulkInfo();
return userinfos.preferences.volume ?? 20;
}
setNotificationVolume(volume: number) {
const userinfos = getBulkInfo();
userinfos.preferences.volume = volume;
localStorage.setItem("userinfos", JSON.stringify(userinfos));
}
setNotificationSound(sound: string) {
const userinfos = getBulkInfo();
userinfos.preferences.notisound = sound;
localStorage.setItem("userinfos", JSON.stringify(userinfos));
}
playSound(name = this.getNotificationSound()) {
const volume = this.getNotiVolume();
if (this.play) {
const voice = this.play.tracks.includes(name);
if (voice) {
this.play.play(name, volume);
} else if (this.perminfo.sound && this.perminfo.sound.cSound) {
const audio = document.createElement("audio");
audio.volume = volume / 100;
audio.src = this.perminfo.sound.cSound;
audio.play().catch();
}
} else {
console.error("play object is missing");
}
}
getNotificationSound() {
const userinfos = getBulkInfo();
return userinfos.preferences.notisound;
}
}
Localuser.globalShortcuts.listen(document.body);
Localuser.initShortcuts();
export {Localuser};