2584 lines
74 KiB
TypeScript
2584 lines
74 KiB
TypeScript
import {Guild} from "./guild.js";
|
|
import {Channel} from "./channel.js";
|
|
import {Direct} from "./direct.js";
|
|
import {AVoice} from "./audio/voice.js";
|
|
import {User} from "./user.js";
|
|
import {getapiurls, SW} from "./utils/utils.js";
|
|
import {getBulkInfo, setTheme, Specialuser} from "./utils/utils.js";
|
|
import {
|
|
channeljson,
|
|
guildjson,
|
|
mainuserjson,
|
|
memberjson,
|
|
memberlistupdatejson,
|
|
messageCreateJson,
|
|
messagejson,
|
|
presencejson,
|
|
readyjson,
|
|
startTypingjson,
|
|
wsjson,
|
|
} from "./jsontypes.js";
|
|
import {Member} from "./member.js";
|
|
import {Dialog, Form, FormError, Options, Settings} from "./settings.js";
|
|
import {getTextNodeAtPosition, MarkDown} from "./markdown.js";
|
|
import {Bot} from "./bot.js";
|
|
import {Role} from "./role.js";
|
|
import {VoiceFactory} 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";
|
|
|
|
const wsCodesRetry = new Set([4000, 4001, 4002, 4003, 4005, 4007, 4008, 4009]);
|
|
|
|
class Localuser {
|
|
badges = new Map<
|
|
string,
|
|
{id: string; description: string; icon: string; link?: string; translate?: boolean}
|
|
>(
|
|
badgeArr as [
|
|
string,
|
|
{id: string; description: string; icon: string; link?: string; translate?: boolean},
|
|
][],
|
|
);
|
|
lastSequence: number | null = null;
|
|
token!: string;
|
|
userinfo!: Specialuser;
|
|
serverurls!: Specialuser["serverurls"];
|
|
initialized!: boolean;
|
|
info!: Specialuser["serverurls"];
|
|
headers!: {"Content-type": string; Authorization: string};
|
|
ready!: readyjson;
|
|
guilds!: Guild[];
|
|
guildids: Map<string, Guild> = new Map();
|
|
user!: User;
|
|
status!: string;
|
|
channelfocus: Channel | undefined;
|
|
lookingguild: Guild | undefined;
|
|
guildhtml: Map<string, HTMLDivElement> = new Map();
|
|
ws: WebSocket | undefined;
|
|
connectionSucceed = 0;
|
|
errorBackoff = 0;
|
|
channelids: Map<string, Channel> = new Map();
|
|
readonly userMap: Map<string, User> = new Map();
|
|
voiceFactory?: VoiceFactory;
|
|
play?: Play;
|
|
instancePing = {
|
|
name: "Unknown",
|
|
};
|
|
mfa_enabled!: boolean;
|
|
get perminfo() {
|
|
return this.userinfo.localuserStore;
|
|
}
|
|
set perminfo(e) {
|
|
this.userinfo.localuserStore = e;
|
|
}
|
|
constructor(userinfo: Specialuser | -1) {
|
|
Play.playURL("/audio/sounds.jasf").then((_) => {
|
|
this.play = _;
|
|
});
|
|
if (userinfo === -1) {
|
|
this.rights = new Rights("");
|
|
return;
|
|
}
|
|
this.token = userinfo.token;
|
|
this.userinfo = userinfo;
|
|
this.perminfo.guilds ??= {};
|
|
this.perminfo.user ??= {};
|
|
this.serverurls = this.userinfo.serverurls;
|
|
this.initialized = false;
|
|
this.info = this.serverurls;
|
|
this.headers = {
|
|
"Content-type": "application/json; charset=UTF-8",
|
|
Authorization: this.userinfo.token,
|
|
};
|
|
const rights = this.perminfo.user.rights || "875069521787904";
|
|
this.rights = new Rights(rights);
|
|
}
|
|
async gottenReady(ready: readyjson): Promise<void> {
|
|
await I18n.done;
|
|
this.initialized = true;
|
|
this.ready = ready;
|
|
this.guilds = [];
|
|
this.guildids = new Map();
|
|
this.user = new User(ready.d.user, this);
|
|
this.user.setstatus("online");
|
|
this.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});
|
|
this.handleVoice();
|
|
this.mfa_enabled = ready.d.user.mfa_enabled as boolean;
|
|
this.userinfo.username = this.user.username;
|
|
this.userinfo.id = this.user.id;
|
|
this.userinfo.pfpsrc = this.user.getpfpsrc();
|
|
|
|
this.status = this.ready.d.user_settings.status;
|
|
this.channelfocus = undefined;
|
|
this.lookingguild = undefined;
|
|
this.guildhtml = new Map();
|
|
const members: {[key: string]: memberjson} = {};
|
|
if (ready.d.merged_members) {
|
|
for (const thing of ready.d.merged_members) {
|
|
members[thing[0].guild_id] = thing[0];
|
|
}
|
|
}
|
|
for (const thing of ready.d.guilds) {
|
|
const temp = new Guild(thing, this, members[thing.id]);
|
|
this.guilds.push(temp);
|
|
this.guildids.set(temp.id, temp);
|
|
}
|
|
{
|
|
const temp = new Direct(ready.d.private_channels, this);
|
|
this.guilds.push(temp);
|
|
this.guildids.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.guildids.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.channelids.get(thing.channel_id);
|
|
if (!channel) {
|
|
continue;
|
|
}
|
|
channel.readStateInfo(thing);
|
|
}
|
|
}
|
|
for (const thing of ready.d.relationships) {
|
|
const user = new User(thing.user, this);
|
|
user.nickname = thing.nickname;
|
|
user.relationshipType = thing.type;
|
|
this.inrelation.add(user);
|
|
}
|
|
|
|
this.pingEndpoint();
|
|
}
|
|
inrelation = new Set<User>();
|
|
outoffocus(): void {
|
|
const servers = document.getElementById("servers") as HTMLDivElement;
|
|
servers.innerHTML = "";
|
|
const channels = document.getElementById("channels") as HTMLDivElement;
|
|
channels.innerHTML = "";
|
|
if (this.channelfocus) {
|
|
this.channelfocus.infinite.delete();
|
|
}
|
|
this.lookingguild = undefined;
|
|
this.channelfocus = undefined;
|
|
}
|
|
unload(): void {
|
|
this.initialized = false;
|
|
this.outoffocus();
|
|
this.guilds = [];
|
|
this.guildids = new Map();
|
|
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;
|
|
}
|
|
const ws = new WebSocket(
|
|
(resume ? this.resume_gateway_url : this.serverurls.gateway.toString()) +
|
|
"?encoding=json&v=9" +
|
|
(DecompressionStream ? "&compress=zlib-stream" : ""),
|
|
);
|
|
this.ws = ws;
|
|
let ds: DecompressionStream;
|
|
let w: WritableStreamDefaultWriter;
|
|
let r: ReadableStreamDefaultReader;
|
|
let arr: Uint8Array;
|
|
let build = "";
|
|
if (DecompressionStream) {
|
|
ds = new DecompressionStream("deflate");
|
|
w = ds.writable.getWriter();
|
|
r = ds.readable.getReader();
|
|
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: "Jank Client",
|
|
client_build_number: 0, //might update this eventually lol
|
|
release_channel: "Custom",
|
|
browser_user_agent: navigator.userAgent,
|
|
},
|
|
compress: Boolean(DecompressionStream),
|
|
presence: {
|
|
status: "online",
|
|
since: null, //new Date().getTime()
|
|
activities: [],
|
|
afk: false,
|
|
},
|
|
},
|
|
}),
|
|
);
|
|
}
|
|
});
|
|
const textdecode = new TextDecoder();
|
|
if (DecompressionStream) {
|
|
(async () => {
|
|
while (true) {
|
|
const read = await r.read();
|
|
const data = textdecode.decode(read.value);
|
|
build += data;
|
|
try {
|
|
const temp = JSON.parse(build);
|
|
build = "";
|
|
await this.handleEvent(temp);
|
|
if (temp.op === 0 && temp.t === "READY") {
|
|
returny();
|
|
}
|
|
} catch {}
|
|
}
|
|
})();
|
|
}
|
|
});
|
|
|
|
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;
|
|
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 = new Map();
|
|
this.noncemap = new Map();
|
|
this.noncebuild = new Map();
|
|
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;
|
|
const loaddesc = document.getElementById("load-desc") as HTMLElement;
|
|
|
|
loaddesc.innerHTML = "";
|
|
loaddesc.append(
|
|
new MarkDown(
|
|
I18n.getTranslation("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.serverurls = 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.serverurls = newurls;
|
|
this.userinfo.json.serverurls = this.info;
|
|
break;
|
|
}
|
|
break;
|
|
}
|
|
case 5: {
|
|
const breakappart = new URL(this.info.wellknown).origin.split(".");
|
|
const url = "https://" + breakappart.at(-2) + "." + breakappart.at(-1);
|
|
const newurls = await getapiurls(url);
|
|
if (newurls) {
|
|
this.info = newurls;
|
|
this.serverurls = newurls;
|
|
this.userinfo.json.serverurls = this.info;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
setTimeout(
|
|
() => {
|
|
if (this.swapped) return;
|
|
(document.getElementById("load-desc") as HTMLElement).textContent =
|
|
I18n.getTranslation("retrying");
|
|
this.initwebsocket().then(() => {
|
|
this.loaduser();
|
|
this.init();
|
|
const loading = document.getElementById("loading") as HTMLElement;
|
|
loading.classList.add("doneloading");
|
|
loading.classList.remove("loading");
|
|
console.log("done loading");
|
|
});
|
|
},
|
|
200 + this.errorBackoff * 2800,
|
|
);
|
|
} else
|
|
(document.getElementById("load-desc") as HTMLElement).textContent =
|
|
I18n.getTranslation("unableToConnect");
|
|
});
|
|
await promise;
|
|
}
|
|
relationshipsUpdate = () => {};
|
|
rights: Rights;
|
|
updateRights(rights: string | number) {
|
|
this.rights.update(rights);
|
|
this.perminfo.user.rights = rights;
|
|
}
|
|
async handleEvent(temp: wsjson) {
|
|
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 "MESSAGE_CREATE":
|
|
if (this.initialized) {
|
|
this.messageCreate(temp);
|
|
}
|
|
break;
|
|
case "MESSAGE_DELETE": {
|
|
temp.d.guild_id ??= "@me";
|
|
const channel = this.channelids.get(temp.d.channel_id);
|
|
if (!channel) break;
|
|
const message = channel.messages.get(temp.d.id);
|
|
if (!message) break;
|
|
message.deleteEvent();
|
|
break;
|
|
}
|
|
case "READY":
|
|
await this.gottenReady(temp as readyjson);
|
|
break;
|
|
case "MESSAGE_UPDATE": {
|
|
temp.d.guild_id ??= "@me";
|
|
const channel = this.channelids.get(temp.d.channel_id);
|
|
if (!channel) break;
|
|
const message = channel.messages.get(temp.d.id);
|
|
if (!message) break;
|
|
message.giveData(temp.d);
|
|
break;
|
|
}
|
|
case "TYPING_START":
|
|
if (this.initialized) {
|
|
this.typingStart(temp);
|
|
}
|
|
break;
|
|
case "USER_UPDATE":
|
|
if (this.initialized) {
|
|
const users = this.userMap.get(temp.d.id);
|
|
if (users) {
|
|
users.userupdate(temp.d);
|
|
}
|
|
}
|
|
break;
|
|
case "CHANNEL_UPDATE":
|
|
if (this.initialized) {
|
|
this.updateChannel(temp.d);
|
|
}
|
|
break;
|
|
case "CHANNEL_CREATE":
|
|
if (this.initialized) {
|
|
this.createChannel(temp.d);
|
|
}
|
|
break;
|
|
case "CHANNEL_DELETE":
|
|
if (this.initialized) {
|
|
this.delChannel(temp.d);
|
|
}
|
|
break;
|
|
case "GUILD_DELETE": {
|
|
const guildy = this.guildids.get(temp.d.id);
|
|
if (guildy) {
|
|
this.guildids.delete(temp.d.id);
|
|
this.guilds.splice(this.guilds.indexOf(guildy), 1);
|
|
guildy.html.remove();
|
|
}
|
|
break;
|
|
}
|
|
case "GUILD_UPDATE": {
|
|
const guildy = this.guildids.get(temp.d.id);
|
|
if (guildy) {
|
|
guildy.update(temp.d);
|
|
}
|
|
break;
|
|
}
|
|
case "GUILD_CREATE":
|
|
(async () => {
|
|
const guildy = new Guild(temp.d, this, this.user);
|
|
this.guilds.push(guildy);
|
|
this.guildids.set(guildy.id, guildy);
|
|
const divy = guildy.generateGuildIcon();
|
|
guildy.HTMLicon = divy;
|
|
(document.getElementById("servers") as HTMLDivElement).insertBefore(
|
|
divy,
|
|
document.getElementById("bottomseparator"),
|
|
);
|
|
})();
|
|
break;
|
|
case "MESSAGE_REACTION_ADD":
|
|
{
|
|
temp.d.guild_id ??= "@me";
|
|
const guild = this.guildids.get(temp.d.guild_id);
|
|
if (!guild) break;
|
|
const channel = this.channelids.get(temp.d.channel_id);
|
|
if (!channel) break;
|
|
const message = channel.messages.get(temp.d.message_id);
|
|
if (!message) break;
|
|
let thing: Member | {id: string};
|
|
if (temp.d.member) {
|
|
thing = (await Member.new(temp.d.member, guild)) as Member;
|
|
} else {
|
|
thing = {id: temp.d.user_id};
|
|
}
|
|
message.reactionAdd(temp.d.emoji, thing);
|
|
}
|
|
break;
|
|
case "MESSAGE_REACTION_REMOVE":
|
|
{
|
|
temp.d.guild_id ??= "@me";
|
|
const channel = this.channelids.get(temp.d.channel_id);
|
|
if (!channel) break;
|
|
|
|
const message = channel.messages.get(temp.d.message_id);
|
|
if (!message) break;
|
|
|
|
message.reactionRemove(temp.d.emoji, temp.d.user_id);
|
|
}
|
|
break;
|
|
case "MESSAGE_REACTION_REMOVE_ALL":
|
|
{
|
|
temp.d.guild_id ??= "@me";
|
|
const channel = this.channelids.get(temp.d.channel_id);
|
|
if (!channel) break;
|
|
const message = channel.messages.get(temp.d.message_id);
|
|
if (!message) break;
|
|
message.reactionRemoveAll();
|
|
}
|
|
break;
|
|
case "MESSAGE_REACTION_REMOVE_EMOJI":
|
|
{
|
|
temp.d.guild_id ??= "@me";
|
|
const channel = this.channelids.get(temp.d.channel_id);
|
|
if (!channel) break;
|
|
const message = channel.messages.get(temp.d.message_id);
|
|
if (!message) break;
|
|
message.reactionRemoveEmoji(temp.d.emoji);
|
|
}
|
|
break;
|
|
case "GUILD_MEMBERS_CHUNK":
|
|
this.gotChunk(temp.d);
|
|
break;
|
|
case "GUILD_MEMBER_LIST_UPDATE": {
|
|
this.memberListUpdate(temp);
|
|
break;
|
|
}
|
|
case "VOICE_STATE_UPDATE":
|
|
if (this.voiceFactory) {
|
|
this.voiceFactory.voiceStateUpdate(temp);
|
|
}
|
|
|
|
break;
|
|
case "VOICE_SERVER_UPDATE":
|
|
if (this.voiceFactory) {
|
|
this.voiceFactory.voiceServerUpdate(temp);
|
|
}
|
|
break;
|
|
case "GUILD_ROLE_CREATE": {
|
|
const guild = this.guildids.get(temp.d.guild_id);
|
|
if (!guild) break;
|
|
guild.newRole(temp.d.role);
|
|
break;
|
|
}
|
|
case "GUILD_ROLE_UPDATE": {
|
|
const guild = this.guildids.get(temp.d.guild_id);
|
|
if (!guild) break;
|
|
guild.updateRole(temp.d.role);
|
|
break;
|
|
}
|
|
case "GUILD_ROLE_DELETE": {
|
|
const guild = this.guildids.get(temp.d.guild_id);
|
|
if (!guild) break;
|
|
guild.deleteRole(temp.d.role_id);
|
|
break;
|
|
}
|
|
case "GUILD_MEMBER_UPDATE": {
|
|
const guild = this.guildids.get(temp.d.guild_id);
|
|
if (!guild) break;
|
|
guild.memberupdate(temp.d);
|
|
break;
|
|
}
|
|
case "RELATIONSHIP_ADD": {
|
|
const user = new User(temp.d.user, this);
|
|
user.nickname = null;
|
|
user.relationshipType = temp.d.type;
|
|
this.inrelation.add(user);
|
|
this.relationshipsUpdate();
|
|
const me = this.guildids.get("@me");
|
|
if (!me) break;
|
|
me.unreads();
|
|
break;
|
|
}
|
|
case "RELATIONSHIP_REMOVE": {
|
|
const user = this.userMap.get(temp.d.id);
|
|
if (!user) return;
|
|
user.nickname = null;
|
|
user.relationshipType = 0;
|
|
this.inrelation.delete(user);
|
|
this.relationshipsUpdate();
|
|
break;
|
|
}
|
|
case "PRESENCE_UPDATE": {
|
|
if (temp.d.user) {
|
|
this.presences.set(temp.d.user.id, temp.d);
|
|
}
|
|
break;
|
|
}
|
|
case "GUILD_MEMBER_ADD": {
|
|
const guild = this.guildids.get(temp.d.guild_id);
|
|
if (!guild) break;
|
|
Member.new(temp.d, guild);
|
|
break;
|
|
}
|
|
case "GUILD_MEMBER_REMOVE": {
|
|
const guild = this.guildids.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.guildids.get(temp.d.guild_id);
|
|
if (!guild) break;
|
|
guild.emojis = temp.d.emojis;
|
|
guild.onEmojiUpdate(guild.emojis);
|
|
break;
|
|
}
|
|
default: {
|
|
//@ts-ignore
|
|
console.warn("Unhandled case " + temp.t, temp);
|
|
}
|
|
}
|
|
} 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;
|
|
if (this.connectionSucceed === 0) this.connectionSucceed = Date.now();
|
|
this.ws.send(JSON.stringify({op: 1, d: this.lastSequence}));
|
|
}, this.heartbeat_interval);
|
|
} else {
|
|
console.log("Unhandled case " + temp.d, temp);
|
|
}
|
|
}
|
|
get currentVoice() {
|
|
return this.voiceFactory?.currentVoice;
|
|
}
|
|
async joinVoice(channel: Channel) {
|
|
if (!this.voiceFactory) return;
|
|
if (!this.ws) return;
|
|
this.ws.send(JSON.stringify(this.voiceFactory.joinVoice(channel.id, channel.guild.id)));
|
|
return undefined;
|
|
}
|
|
changeVCStatus(status: string) {
|
|
const statuselm = document.getElementById("VoiceStatus");
|
|
if (!statuselm) throw new Error("Missing status element");
|
|
statuselm.textContent = status;
|
|
}
|
|
handleVoice() {
|
|
if (this.voiceFactory) {
|
|
this.voiceFactory.onJoin = (voice) => {
|
|
voice.onSatusChange = (status) => {
|
|
this.changeVCStatus(status);
|
|
};
|
|
};
|
|
}
|
|
}
|
|
|
|
heartbeat_interval: number = 0;
|
|
updateChannel(json: channeljson): void {
|
|
const guild = this.guildids.get(json.guild_id);
|
|
if (guild) {
|
|
guild.updateChannel(json);
|
|
if (json.guild_id === this.lookingguild?.id) {
|
|
this.loadGuild(json.guild_id);
|
|
}
|
|
}
|
|
}
|
|
createChannel(json: channeljson): undefined | Channel {
|
|
json.guild_id ??= "@me";
|
|
const guild = this.guildids.get(json.guild_id);
|
|
if (!guild) return;
|
|
const channel = guild.createChannelpac(json);
|
|
if (json.guild_id === this.lookingguild?.id) {
|
|
this.loadGuild(json.guild_id, true);
|
|
}
|
|
if (channel.id === this.gotoid) {
|
|
guild.loadGuild();
|
|
guild.loadChannel(channel.id);
|
|
this.gotoid = undefined;
|
|
}
|
|
return channel; // Add this line to return the 'channel' variable
|
|
}
|
|
async memberListUpdate(list: memberlistupdatejson | void) {
|
|
if (this.searching) return;
|
|
const div = document.getElementById("sideDiv") as HTMLDivElement;
|
|
div.innerHTML = "";
|
|
div.classList.remove("searchDiv");
|
|
const guild = this.lookingguild;
|
|
if (!guild) return;
|
|
const channel = this.channelfocus;
|
|
if (!channel) return;
|
|
if (list) {
|
|
const counts = new Map<string, number>();
|
|
for (const thing of list.d.ops[0].items) {
|
|
if ("member" in thing) {
|
|
await Member.new(thing.member, guild);
|
|
} else {
|
|
counts.set(thing.group.id, thing.group.count);
|
|
}
|
|
}
|
|
}
|
|
|
|
const elms: Map<Role | "offline" | "online", Member[]> = new Map([]);
|
|
for (const role of guild.roles) {
|
|
if (role.hoist) {
|
|
elms.set(role, []);
|
|
}
|
|
}
|
|
elms.set("online", []);
|
|
elms.set("offline", []);
|
|
const members = new Set(guild.members);
|
|
members.forEach((member) => {
|
|
if (!channel.hasPermission("VIEW_CHANNEL", member)) {
|
|
members.delete(member);
|
|
console.log(member, "can't see");
|
|
return;
|
|
}
|
|
});
|
|
for (const [role, list] of elms) {
|
|
members.forEach((member) => {
|
|
if (role === "offline") {
|
|
if (member.user.getStatus() === "offline") {
|
|
list.push(member);
|
|
members.delete(member);
|
|
}
|
|
return;
|
|
}
|
|
if (member.user.getStatus() === "offline") {
|
|
return;
|
|
}
|
|
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);
|
|
for (const [role, list] of elms) {
|
|
if (!list.length) continue;
|
|
const category = document.createElement("div");
|
|
category.classList.add("memberList");
|
|
let title = document.createElement("h3");
|
|
if (role === "offline") {
|
|
title.textContent = I18n.getTranslation("user.offline");
|
|
category.classList.add("offline");
|
|
} else if (role === "online") {
|
|
title.textContent = I18n.getTranslation("user.online");
|
|
} else {
|
|
title.textContent = role.name;
|
|
}
|
|
category.append(title);
|
|
const membershtml = document.createElement("div");
|
|
membershtml.classList.add("flexttb");
|
|
|
|
for (const member of list) {
|
|
const memberdiv = document.createElement("div");
|
|
const pfp = await member.user.buildstatuspfp(member);
|
|
const username = document.createElement("span");
|
|
username.classList.add("ellipsis");
|
|
username.textContent = member.name;
|
|
member.bind(username);
|
|
member.user.bind(memberdiv, member.guild, false);
|
|
memberdiv.append(pfp, username);
|
|
memberdiv.classList.add("flexltr", "liststyle", "memberListStyle");
|
|
membershtml.append(memberdiv);
|
|
}
|
|
category.append(membershtml);
|
|
div.append(category);
|
|
}
|
|
|
|
console.log(elms);
|
|
}
|
|
async getSidePannel() {
|
|
if (this.ws && this.channelfocus) {
|
|
console.log(this.channelfocus.guild.id);
|
|
if (this.channelfocus.guild.id === "@me") {
|
|
this.memberListUpdate();
|
|
return;
|
|
}
|
|
this.ws.send(
|
|
JSON.stringify({
|
|
d: {
|
|
channels: {[this.channelfocus.id]: [[0, 99]]},
|
|
guild_id: this.channelfocus.guild.id,
|
|
},
|
|
op: 14,
|
|
}),
|
|
);
|
|
} else {
|
|
console.log("false? :3");
|
|
}
|
|
}
|
|
gotoid: string | undefined;
|
|
async goToChannel(id: string, addstate = true) {
|
|
const channel = this.channelids.get(id);
|
|
if (channel) {
|
|
const guild = channel.guild;
|
|
guild.loadGuild();
|
|
guild.loadChannel(id, addstate);
|
|
} else {
|
|
this.gotoid = id;
|
|
}
|
|
}
|
|
delChannel(json: channeljson): void {
|
|
let guild_id = json.guild_id;
|
|
guild_id ??= "@me";
|
|
const guild = this.guildids.get(guild_id);
|
|
if (guild) {
|
|
guild.delChannel(json);
|
|
}
|
|
|
|
if (json.guild_id === this.lookingguild?.id) {
|
|
this.loadGuild(json.guild_id, true);
|
|
}
|
|
}
|
|
init(): void {
|
|
const location = window.location.href.split("/");
|
|
this.buildservers();
|
|
if (location[3] === "channels") {
|
|
const guild = this.loadGuild(location[4]);
|
|
if (!guild) {
|
|
return;
|
|
}
|
|
guild.loadChannel(location[5]);
|
|
this.channelfocus = this.channelids.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.lookingguild) {
|
|
return this.lookingguild.isAdmin();
|
|
} else {
|
|
return false;
|
|
}
|
|
}
|
|
loadGuild(id: string, forceReload = false): Guild | undefined {
|
|
this.searching = false;
|
|
let guild = this.guildids.get(id);
|
|
if (!guild) {
|
|
guild = this.guildids.get("@me");
|
|
}
|
|
console.log(forceReload);
|
|
if (!forceReload && this.lookingguild === guild) {
|
|
return guild;
|
|
}
|
|
if (this.channelfocus && this.lookingguild !== guild) {
|
|
this.channelfocus.infinite.delete();
|
|
this.channelfocus = undefined;
|
|
}
|
|
if (this.lookingguild) {
|
|
this.lookingguild.html.classList.remove("serveropen");
|
|
}
|
|
|
|
if (!guild) return;
|
|
if (guild.html) {
|
|
guild.html.classList.add("serveropen");
|
|
}
|
|
this.lookingguild = 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(0, 0, 0, 1) 0%, rgba(0, 0, 0, 0) 40%), url(${this.info.cdn}/banners/${guild.id}/${guild.banner})`,
|
|
);
|
|
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;
|
|
}
|
|
buildservers(): void {
|
|
const serverlist = document.getElementById("servers") 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.guildids.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.guildids.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);
|
|
for (const thing of this.guilds) {
|
|
if (thing instanceof Direct) {
|
|
(thing as Direct).unreaddms();
|
|
continue;
|
|
}
|
|
const divy = thing.generateGuildIcon();
|
|
thing.HTMLicon = divy;
|
|
serverlist.append(divy);
|
|
}
|
|
{
|
|
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();
|
|
}
|
|
createGuild() {
|
|
const full = new Dialog("");
|
|
const buttons = full.options.addButtons("", {top: true});
|
|
const viacode = buttons.add(I18n.getTranslation("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.getTranslation("invite.inviteLinkCode"), "code");
|
|
}
|
|
const guildcreate = buttons.add(I18n.getTranslation("guild.create"));
|
|
{
|
|
const form = guildcreate.addForm("", (fields: any) => {
|
|
this.makeGuild(fields).then((_) => {
|
|
if (_.message) {
|
|
alert(_.errors.name._errors[0].message);
|
|
} else {
|
|
full.hide();
|
|
}
|
|
});
|
|
});
|
|
form.addFileInput(I18n.getTranslation("guild.icon:"), "icon", {files: "one"});
|
|
form.addTextInput(I18n.getTranslation("guild.name:"), "name", {required: true});
|
|
}
|
|
full.show();
|
|
}
|
|
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() {
|
|
const content = document.createElement("div");
|
|
content.classList.add("flexttb", "guildy");
|
|
content.textContent = I18n.getTranslation("guild.loadingDiscovery");
|
|
const full = new Dialog("");
|
|
full.options.addHTMLArea(content);
|
|
full.show();
|
|
|
|
const res = await fetch(this.info.api + "/discoverable-guilds?limit=50", {
|
|
headers: this.headers,
|
|
});
|
|
const json = await res.json();
|
|
console.log([...json.guilds], json.guilds);
|
|
//@ts-ignore
|
|
json.guilds = json.guilds.sort((a, b) => {
|
|
return b.member_count - a.member_count;
|
|
});
|
|
content.innerHTML = "";
|
|
const title = document.createElement("h2");
|
|
title.textContent = I18n.getTranslation("guild.disoveryTitle", json.guilds.length + "");
|
|
content.appendChild(title);
|
|
|
|
const guilds = document.createElement("div");
|
|
guilds.id = "discovery-guild-content";
|
|
|
|
json.guilds.forEach((guild: guildjson["properties"]) => {
|
|
const content = document.createElement("div");
|
|
content.classList.add("discovery-guild");
|
|
|
|
if (guild.banner) {
|
|
const banner = document.createElement("img");
|
|
banner.classList.add("banner");
|
|
banner.crossOrigin = "anonymous";
|
|
banner.src = this.info.cdn + "/icons/" + guild.id + "/" + guild.banner + ".png?size=256";
|
|
banner.alt = "";
|
|
content.appendChild(banner);
|
|
}
|
|
|
|
const nameContainer = document.createElement("div");
|
|
nameContainer.classList.add("flex");
|
|
const img = document.createElement("img");
|
|
img.classList.add("icon");
|
|
img.crossOrigin = "anonymous";
|
|
img.src =
|
|
this.info.cdn +
|
|
(guild.icon
|
|
? "/icons/" + guild.id + "/" + guild.icon + ".png?size=48"
|
|
: "/embed/avatars/3.png");
|
|
img.alt = "";
|
|
nameContainer.appendChild(img);
|
|
|
|
const name = document.createElement("h3");
|
|
name.textContent = guild.name;
|
|
nameContainer.appendChild(name);
|
|
content.appendChild(nameContainer);
|
|
const desc = document.createElement("p");
|
|
desc.textContent = guild.description;
|
|
content.appendChild(desc);
|
|
|
|
content.addEventListener("click", async () => {
|
|
const joinRes = await fetch(this.info.api + "/guilds/" + guild.id + "/members/@me", {
|
|
method: "PUT",
|
|
headers: this.headers,
|
|
});
|
|
if (joinRes.ok) full.hide();
|
|
});
|
|
guilds.appendChild(content);
|
|
});
|
|
content.appendChild(guilds);
|
|
}
|
|
messageCreate(messagep: messageCreateJson): void {
|
|
messagep.d.guild_id ??= "@me";
|
|
const channel = this.channelids.get(messagep.d.channel_id);
|
|
if (channel) {
|
|
channel.messageCreate(messagep);
|
|
this.unreads();
|
|
}
|
|
}
|
|
unreads(): void {
|
|
for (const thing of this.guilds) {
|
|
if (thing.id === "@me") {
|
|
continue;
|
|
}
|
|
const html = this.guildhtml.get(thing.id);
|
|
thing.unreads(html);
|
|
}
|
|
}
|
|
async typingStart(typing: startTypingjson): Promise<void> {
|
|
const channel = this.channelids.get(typing.d.channel_id);
|
|
if (!channel) return;
|
|
channel.typingStart(typing);
|
|
}
|
|
updatepfp(file: Blob): void {
|
|
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,
|
|
}),
|
|
});
|
|
};
|
|
}
|
|
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 showusersettings() {
|
|
const settings = new Settings(I18n.getTranslation("localuser.settings"));
|
|
{
|
|
const userOptions = settings.addButton(I18n.getTranslation("localuser.userSettings"), {
|
|
ltr: 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.addFileInput(
|
|
I18n.getTranslation("uploadPfp"),
|
|
(_) => {
|
|
if (file) {
|
|
this.updatepfp(file);
|
|
}
|
|
},
|
|
{clear: true},
|
|
);
|
|
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.addFileInput(
|
|
I18n.getTranslation("uploadBanner"),
|
|
(_) => {
|
|
if (bfile !== undefined) {
|
|
this.updatebanner(bfile);
|
|
}
|
|
},
|
|
{clear: true},
|
|
);
|
|
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.getTranslation("pronouns"),
|
|
(_) => {
|
|
if (newpronouns !== undefined || newbio !== undefined || changed !== undefined) {
|
|
this.updateProfile({
|
|
pronouns: newpronouns,
|
|
bio: newbio,
|
|
accent_color: Number.parseInt("0x" + color.substr(1), 16),
|
|
});
|
|
}
|
|
},
|
|
{initText: this.user.pronouns},
|
|
);
|
|
pronounbox.watchForChange((_) => {
|
|
hypouser.pronouns = _;
|
|
newpronouns = _;
|
|
regen();
|
|
});
|
|
const bioBox = settingsLeft.addMDInput(I18n.getTranslation("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.getTranslation("profileColor"),
|
|
(_) => {},
|
|
{initColor: color},
|
|
);
|
|
colorPicker.watchForChange((_) => {
|
|
console.log();
|
|
color = _;
|
|
hypouser.accent_color = Number.parseInt("0x" + _.substr(1), 16);
|
|
changed = true;
|
|
regen();
|
|
});
|
|
}
|
|
{
|
|
const tas = settings.addButton(I18n.getTranslation("localuser.themesAndSounds"));
|
|
{
|
|
const themes = ["Dark", "WHITE", "Light", "Dark-Accent"];
|
|
tas.addSelect(
|
|
I18n.getTranslation("localuser.theme:"),
|
|
(_) => {
|
|
localStorage.setItem("theme", themes[_]);
|
|
setTheme();
|
|
},
|
|
themes,
|
|
{
|
|
defaultIndex: themes.indexOf(localStorage.getItem("theme") as string),
|
|
},
|
|
);
|
|
}
|
|
{
|
|
const initArea = (index: number) => {
|
|
console.log(index === sounds.length - 1);
|
|
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 = [...AVoice.sounds, I18n.localuser.customSound()];
|
|
const initIndex = sounds.indexOf(this.getNotificationSound());
|
|
tas
|
|
.addSelect(
|
|
I18n.getTranslation("localuser.notisound"),
|
|
(index) => {
|
|
this.setNotificationSound(sounds[index]);
|
|
},
|
|
sounds,
|
|
{defaultIndex: initIndex},
|
|
)
|
|
.watchForChange((index) => {
|
|
initArea(index);
|
|
this.playSound(sounds[index]);
|
|
});
|
|
|
|
const area = document.createElement("div");
|
|
initArea(initIndex);
|
|
tas.addHTMLArea(area);
|
|
}
|
|
|
|
{
|
|
let userinfos = getBulkInfo();
|
|
tas.addColorInput(
|
|
I18n.getTranslation("localuser.accentColor"),
|
|
(_) => {
|
|
userinfos = getBulkInfo();
|
|
userinfos.accent_color = _;
|
|
localStorage.setItem("userinfos", JSON.stringify(userinfos));
|
|
document.documentElement.style.setProperty("--accent-color", userinfos.accent_color);
|
|
},
|
|
{initColor: userinfos.accent_color},
|
|
);
|
|
}
|
|
}
|
|
{
|
|
const update = settings.addButton(I18n.getTranslation("localuser.updateSettings"));
|
|
const sw = update.addSelect(
|
|
I18n.getTranslation("localuser.swSettings"),
|
|
() => {},
|
|
["SWOff", "SWOffline", "SWOn"].map((e) => I18n.getTranslation("localuser." + e)),
|
|
{
|
|
defaultIndex: ["false", "offlineOnly", "true"].indexOf(
|
|
localStorage.getItem("SWMode") as string,
|
|
),
|
|
},
|
|
);
|
|
sw.onchange = (e) => {
|
|
SW.setMode(["false", "offlineOnly", "true"][e] as "false" | "offlineOnly" | "true");
|
|
};
|
|
update.addButtonInput("", I18n.getTranslation("localuser.CheckUpdate"), () => {
|
|
SW.checkUpdate();
|
|
});
|
|
update.addButtonInput("", I18n.getTranslation("localuser.clearCache"), () => {
|
|
SW.forceClear();
|
|
});
|
|
}
|
|
{
|
|
const security = settings.addButton(I18n.getTranslation("localuser.accountSettings"));
|
|
const genSecurity = () => {
|
|
security.removeAll();
|
|
if (this.mfa_enabled) {
|
|
security.addButtonInput("", I18n.getTranslation("localuser.2faDisable"), () => {
|
|
const form = security.addSubForm(
|
|
I18n.getTranslation("localuser.2faDisable"),
|
|
(_: any) => {
|
|
if (_.message) {
|
|
switch (_.code) {
|
|
case 60008:
|
|
form.error("code", I18n.getTranslation("badCode"));
|
|
break;
|
|
}
|
|
} else {
|
|
this.mfa_enabled = false;
|
|
security.returnFromSub();
|
|
genSecurity();
|
|
}
|
|
},
|
|
{
|
|
fetchURL: this.info.api + "/users/@me/mfa/totp/disable",
|
|
headers: this.headers,
|
|
},
|
|
);
|
|
form.addTextInput(I18n.getTranslation("localuser.2faCode"), "code", {required: true});
|
|
});
|
|
} else {
|
|
security.addButtonInput("", I18n.getTranslation("localuser.2faEnable"), async () => {
|
|
let secret = "";
|
|
for (let i = 0; i < 18; i++) {
|
|
secret += "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"[Math.floor(Math.random() * 32)];
|
|
}
|
|
const form = security.addSubForm(
|
|
I18n.getTranslation("localuser.setUp2fa"),
|
|
(_: any) => {
|
|
if (_.message) {
|
|
switch (_.code) {
|
|
case 60008:
|
|
form.error("code", I18n.getTranslation("localuser.badCode"));
|
|
break;
|
|
case 400:
|
|
form.error("password", I18n.getTranslation("localuser.badPassword"));
|
|
break;
|
|
}
|
|
} else {
|
|
genSecurity();
|
|
this.mfa_enabled = true;
|
|
security.returnFromSub();
|
|
}
|
|
},
|
|
{
|
|
fetchURL: this.info.api + "/users/@me/mfa/totp/enable/",
|
|
headers: this.headers,
|
|
},
|
|
);
|
|
form.addTitle(I18n.getTranslation("localuser.setUp2faInstruction"));
|
|
form.addText(I18n.getTranslation("localuser.2faCodeGive", secret));
|
|
form.addTextInput(I18n.getTranslation("localuser.password:"), "password", {
|
|
required: true,
|
|
password: true,
|
|
});
|
|
form.addTextInput(I18n.getTranslation("localuser.2faCode"), "code", {required: true});
|
|
form.setValue("secret", secret);
|
|
});
|
|
}
|
|
security.addButtonInput("", I18n.getTranslation("localuser.changeDiscriminator"), () => {
|
|
const form = security.addSubForm(
|
|
I18n.getTranslation("localuser.changeDiscriminator"),
|
|
(_) => {
|
|
security.returnFromSub();
|
|
},
|
|
{
|
|
fetchURL: this.info.api + "/users/@me/",
|
|
headers: this.headers,
|
|
method: "PATCH",
|
|
},
|
|
);
|
|
form.addTextInput(I18n.getTranslation("localuser.newDiscriminator"), "discriminator");
|
|
});
|
|
security.addButtonInput("", I18n.getTranslation("localuser.changeEmail"), () => {
|
|
const form = security.addSubForm(
|
|
I18n.getTranslation("localuser.changeEmail"),
|
|
(_) => {
|
|
security.returnFromSub();
|
|
},
|
|
{
|
|
fetchURL: this.info.api + "/users/@me/",
|
|
headers: this.headers,
|
|
method: "PATCH",
|
|
},
|
|
);
|
|
form.addTextInput(I18n.getTranslation("localuser.password:"), "password", {
|
|
password: true,
|
|
});
|
|
if (this.mfa_enabled) {
|
|
form.addTextInput(I18n.getTranslation("localuser.2faCode"), "code");
|
|
}
|
|
form.addTextInput(I18n.getTranslation("localuser.newEmail:"), "email");
|
|
});
|
|
security.addButtonInput("", I18n.getTranslation("localuser.changeUsername"), () => {
|
|
const form = security.addSubForm(
|
|
I18n.getTranslation("localuser.changeUsername"),
|
|
(_) => {
|
|
security.returnFromSub();
|
|
},
|
|
{
|
|
fetchURL: this.info.api + "/users/@me/",
|
|
headers: this.headers,
|
|
method: "PATCH",
|
|
},
|
|
);
|
|
form.addTextInput(I18n.getTranslation("localuser.password:"), "password", {
|
|
password: true,
|
|
});
|
|
if (this.mfa_enabled) {
|
|
form.addTextInput(I18n.getTranslation("localuser.2faCode"), "code");
|
|
}
|
|
form.addTextInput(I18n.getTranslation("localuser.newUsername"), "username");
|
|
});
|
|
security.addButtonInput("", I18n.getTranslation("localuser.changePassword"), () => {
|
|
const form = security.addSubForm(
|
|
I18n.getTranslation("localuser.changePassword"),
|
|
(_) => {
|
|
security.returnFromSub();
|
|
},
|
|
{
|
|
fetchURL: this.info.api + "/users/@me/",
|
|
headers: this.headers,
|
|
method: "PATCH",
|
|
},
|
|
);
|
|
form.addTextInput(I18n.getTranslation("localuser.oldPassword:"), "password", {
|
|
password: true,
|
|
});
|
|
if (this.mfa_enabled) {
|
|
form.addTextInput(I18n.getTranslation("localuser.2faCode"), "code");
|
|
}
|
|
let in1 = "";
|
|
let in2 = "";
|
|
form
|
|
.addTextInput(I18n.getTranslation("localuser.newPassword:"), "")
|
|
.watchForChange((text) => {
|
|
in1 = text;
|
|
});
|
|
const copy = form.addTextInput("New password again:", "");
|
|
copy.watchForChange((text) => {
|
|
in2 = text;
|
|
});
|
|
form.setValue("new_password", () => {
|
|
if (in1 === in2) {
|
|
return in1;
|
|
} else {
|
|
throw new FormError(copy, I18n.getTranslation("localuser.PasswordsNoMatch"));
|
|
}
|
|
});
|
|
});
|
|
|
|
security.addSelect(
|
|
I18n.getTranslation("localuser.language"),
|
|
(e) => {
|
|
I18n.setLanguage(I18n.options()[e]);
|
|
},
|
|
[...langmap.values()],
|
|
{
|
|
defaultIndex: I18n.options().indexOf(I18n.lang),
|
|
},
|
|
);
|
|
{
|
|
const box = security.addCheckboxInput(
|
|
I18n.getTranslation("localuser.enableEVoice"),
|
|
() => {},
|
|
{initState: Boolean(localStorage.getItem("Voice enabled"))},
|
|
);
|
|
box.onchange = (e) => {
|
|
if (e) {
|
|
if (confirm(I18n.getTranslation("localuser.VoiceWarning"))) {
|
|
localStorage.setItem("Voice enabled", "true");
|
|
} else {
|
|
box.value = false;
|
|
const checkbox = box.input.deref();
|
|
if (checkbox) {
|
|
checkbox.checked = false;
|
|
}
|
|
}
|
|
} else {
|
|
localStorage.removeItem("Voice enabled");
|
|
}
|
|
};
|
|
const box2 = security.addCheckboxInput("Enable logging of bad stuff", () => {}, {
|
|
initState: Boolean(localStorage.getItem("logbad")),
|
|
});
|
|
box2.onchange = (e) => {
|
|
if (e) {
|
|
if (confirm("this is meant for spacebar devs")) {
|
|
localStorage.setItem("logbad", "true");
|
|
} else {
|
|
box2.value = false;
|
|
const checkbox = box2.input.deref();
|
|
if (checkbox) {
|
|
checkbox.checked = false;
|
|
}
|
|
}
|
|
} else {
|
|
localStorage.removeItem("logbad");
|
|
}
|
|
};
|
|
}
|
|
};
|
|
genSecurity();
|
|
}
|
|
{
|
|
const connections = settings.addButton(I18n.getTranslation("localuser.connections"));
|
|
const connectionContainer = document.createElement("div");
|
|
connectionContainer.id = "connection-container";
|
|
|
|
fetch(this.info.api + "/connections", {
|
|
headers: this.headers,
|
|
})
|
|
.then((r) => r.json())
|
|
.then((json) => {
|
|
Object.keys(json)
|
|
.sort((key) => (json[key].enabled ? -1 : 1))
|
|
.forEach((key) => {
|
|
const connection = json[key];
|
|
|
|
const container = document.createElement("div");
|
|
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");
|
|
container.title = I18n.getTranslation("localuser.PasswordsNoMatch");
|
|
}
|
|
|
|
connectionContainer.appendChild(container);
|
|
});
|
|
});
|
|
connections.addHTMLArea(connectionContainer);
|
|
}
|
|
{
|
|
const devPortal = settings.addButton(I18n.getTranslation("localuser.devPortal"));
|
|
|
|
fetch(this.info.api + "/teams", {
|
|
headers: this.headers,
|
|
}).then(async (teamsRes) => {
|
|
const teams = await teamsRes.json();
|
|
|
|
devPortal.addButtonInput("", I18n.getTranslation("localuser.createApp"), () => {
|
|
const form = devPortal.addSubForm(
|
|
I18n.getTranslation("localuser.createApp"),
|
|
(json: any) => {
|
|
if (json.message) form.error("name", json.message);
|
|
else {
|
|
devPortal.returnFromSub();
|
|
this.manageApplication(json.id, devPortal);
|
|
}
|
|
},
|
|
{
|
|
fetchURL: this.info.api + "/applications",
|
|
headers: this.headers,
|
|
method: "POST",
|
|
},
|
|
);
|
|
|
|
form.addTextInput("Name:", "name", {required: true});
|
|
form.addSelect(
|
|
I18n.getTranslation("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 = document.createElement("img");
|
|
cover.crossOrigin = "anonymous";
|
|
cover.src =
|
|
this.info.cdn +
|
|
"/app-icons/" +
|
|
application.id +
|
|
"/" +
|
|
(application.cover_image || application.icon) +
|
|
".png?size=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.appendChild(container);
|
|
},
|
|
);
|
|
});
|
|
devPortal.addHTMLArea(appListContainer);
|
|
});
|
|
}
|
|
{
|
|
const deleteAccount = settings.addButton(I18n.localuser.deleteAccount()).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());
|
|
}
|
|
});
|
|
}
|
|
if (
|
|
this.rights.hasPermission("OPERATOR") ||
|
|
this.rights.hasPermission("CREATE_REGISTRATION_TOKENS")
|
|
) {
|
|
const manageInstance = settings.addButton(I18n.localuser.manageInstance());
|
|
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.addButtonInput("", I18n.manageInstance.createTokens(), () => {
|
|
const tokens = manageInstance.addSubOptions(I18n.manageInstance.createTokens(), {
|
|
noSubmit: true,
|
|
});
|
|
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: "Jank",
|
|
};
|
|
function makeURLMenu() {
|
|
urlOption
|
|
.addTextInput(I18n.manageInstance.clientURL(), () => {}, {
|
|
initText: urlOptionsJSON.url,
|
|
})
|
|
.watchForChange((str) => {
|
|
urlOptionsJSON.url = str;
|
|
});
|
|
urlOption
|
|
.addSelect(
|
|
I18n.manageInstance.regType(),
|
|
() => {},
|
|
["Jank", I18n.manageInstance.genericType()],
|
|
{
|
|
defaultIndex: ["Jank", "generic"].indexOf(urlOptionsJSON.type),
|
|
},
|
|
)
|
|
.watchForChange((i) => {
|
|
urlOptionsJSON.type = ["Jank", "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 === "Jank") {
|
|
const options = new URLSearchParams();
|
|
options.set("instance", this.info.wellknown);
|
|
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);
|
|
});
|
|
}
|
|
}
|
|
{
|
|
const jankInfo = settings.addButton(I18n.jankInfo());
|
|
const img = document.createElement("img");
|
|
img.src = "/logo.svg";
|
|
jankInfo.addHTMLArea(img);
|
|
img.width = 128;
|
|
img.height = 128;
|
|
jankInfo.addMDText(
|
|
I18n.clientDesc("Jank-Rolling", window.location.origin, this.rights.allow + ""),
|
|
);
|
|
}
|
|
settings.show();
|
|
}
|
|
readonly botTokens: Map<string, string> = new Map();
|
|
async manageApplication(appId = "", container: Options) {
|
|
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();
|
|
const form = container.addSubForm(json.name, () => {}, {
|
|
fetchURL: this.info.api + "/applications/" + appId,
|
|
method: "PATCH",
|
|
headers: this.headers,
|
|
traditionalSubmit: true,
|
|
});
|
|
form.addTextInput(I18n.getTranslation("localuser.appName"), "name", {initText: json.name});
|
|
form.addMDInput(I18n.getTranslation("localuser.description"), "description", {
|
|
initText: json.description,
|
|
});
|
|
form.addFileInput("Icon:", "icon");
|
|
form.addTextInput(I18n.getTranslation("localuser.privacyPolcyURL"), "privacy_policy_url", {
|
|
initText: json.privacy_policy_url,
|
|
});
|
|
form.addTextInput(I18n.getTranslation("localuser.TOSURL"), "terms_of_service_url", {
|
|
initText: json.terms_of_service_url,
|
|
});
|
|
form.addCheckboxInput(I18n.getTranslation("localuser.publicAvaliable"), "bot_public", {
|
|
initState: json.bot_public,
|
|
});
|
|
form.addCheckboxInput(I18n.getTranslation("localuser.requireCode"), "bot_require_code_grant", {
|
|
initState: json.bot_require_code_grant,
|
|
});
|
|
form.addButtonInput(
|
|
"",
|
|
I18n.getTranslation("localuser." + (json.bot ? "manageBot" : "addBot")),
|
|
async () => {
|
|
if (!json.bot) {
|
|
if (!confirm(I18n.getTranslation("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);
|
|
},
|
|
);
|
|
}
|
|
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.getTranslation("localuser.confuseNoBot"));
|
|
}
|
|
const bot: mainuserjson = json.bot;
|
|
const form = container.addSubForm(
|
|
I18n.getTranslation("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.getTranslation("localuser.botUsername"), "username", {
|
|
initText: bot.username,
|
|
});
|
|
form.addFileInput(I18n.getTranslation("localuser.botAvatar"), "avatar");
|
|
form.addButtonInput("", I18n.getTranslation("localuser.resetToken"), async () => {
|
|
if (!confirm(I18n.getTranslation("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.getTranslation("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.getTranslation(
|
|
"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.getTranslation("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.getTranslation("localuser.noToken"));
|
|
checkbox.setState(false);
|
|
}
|
|
} else {
|
|
delete this.perminfo.applications[appId];
|
|
}
|
|
});
|
|
form.addButtonInput("", I18n.getTranslation("localuser.advancedBot"), () => {
|
|
const token = this.botTokens.get(appId);
|
|
if (token) {
|
|
const botc = new Bot(bot, token, this);
|
|
botc.settings();
|
|
}
|
|
});
|
|
form.addButtonInput("", I18n.getTranslation("localuser.botInviteCreate"), () => {
|
|
Bot.InviteMaker(appId, form, this.info);
|
|
});
|
|
}
|
|
readonly autofillregex = Object.freeze(/[@#:]([a-z0-9 ]*)$/i);
|
|
mdBox() {
|
|
interface CustomHTMLDivElement extends HTMLDivElement {
|
|
markdown: MarkDown;
|
|
}
|
|
|
|
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);
|
|
};
|
|
}
|
|
MDReplace(replacewith: string, original: string, typebox: MarkDown) {
|
|
let raw = typebox.rawString;
|
|
raw = raw.split(original)[1];
|
|
if (raw === undefined) return;
|
|
raw = original.replace(this.autofillregex, "") + replacewith + raw;
|
|
console.log(raw);
|
|
console.log(replacewith);
|
|
console.log(original);
|
|
typebox.txt = raw.split("");
|
|
const match = original.match(this.autofillregex);
|
|
if (match) {
|
|
typebox.boxupdate(replacewith.length - match[0].length);
|
|
}
|
|
}
|
|
MDSearchOptions(
|
|
options: [string, string, void | HTMLElement][],
|
|
original: string,
|
|
div: HTMLDivElement,
|
|
typebox: MarkDown,
|
|
) {
|
|
if (!div) return;
|
|
div.innerHTML = "";
|
|
let i = 0;
|
|
const htmloptions: HTMLSpanElement[] = [];
|
|
for (const thing of options) {
|
|
if (i == 8) {
|
|
break;
|
|
}
|
|
i++;
|
|
const span = document.createElement("span");
|
|
htmloptions.push(span);
|
|
if (thing[2]) {
|
|
span.append(thing[2]);
|
|
}
|
|
|
|
span.append(thing[0]);
|
|
span.onclick = (e) => {
|
|
if (e) {
|
|
const selection = window.getSelection() as Selection;
|
|
const box = typebox.box.deref();
|
|
if (!box) return;
|
|
if (selection) {
|
|
console.warn(original);
|
|
|
|
const pos = getTextNodeAtPosition(
|
|
box,
|
|
original.length -
|
|
(original.match(this.autofillregex) as RegExpMatchArray)[0].length +
|
|
thing[1].length,
|
|
);
|
|
selection.removeAllRanges();
|
|
const range = new Range();
|
|
range.setStart(pos.node, pos.position);
|
|
selection.addRange(range);
|
|
}
|
|
e.preventDefault();
|
|
box.focus();
|
|
}
|
|
this.MDReplace(thing[1], 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":
|
|
//@ts-ignore
|
|
cur.onclick();
|
|
break;
|
|
}
|
|
return true;
|
|
}
|
|
return false;
|
|
};
|
|
this.keydown = (event) => {
|
|
if (remove()) return;
|
|
if (cancel.has(event.key)) {
|
|
event.preventDefault();
|
|
}
|
|
};
|
|
} else {
|
|
remove();
|
|
}
|
|
}
|
|
MDFindChannel(name: string, orginal: string, box: HTMLDivElement, typebox: MarkDown) {
|
|
const maybe: [number, Channel][] = [];
|
|
if (this.lookingguild && this.lookingguild.id !== "@me") {
|
|
for (const channel of this.lookingguild.channels) {
|
|
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) => ["# " + a[1].name, `<#${a[1].id}> `, undefined]),
|
|
orginal,
|
|
box,
|
|
typebox,
|
|
);
|
|
}
|
|
async getUser(id: string) {
|
|
if (this.userMap.has(id)) {
|
|
return this.userMap.get(id) as User;
|
|
}
|
|
return new User(await (await fetch(this.info.api + "/users/" + id)).json(), this);
|
|
}
|
|
MDFineMentionGen(name: string, original: string, box: HTMLDivElement, typebox: MarkDown) {
|
|
let members: [Member, number][] = [];
|
|
if (this.lookingguild) {
|
|
for (const member of this.lookingguild.members) {
|
|
const rank = member.compare(name);
|
|
if (rank > 0) {
|
|
members.push([member, rank]);
|
|
}
|
|
}
|
|
}
|
|
members.sort((a, b) => b[1] - a[1]);
|
|
this.MDSearchOptions(
|
|
members.map((a) => ["@" + a[0].name, `<@${a[0].id}> `, undefined]),
|
|
original,
|
|
box,
|
|
typebox,
|
|
);
|
|
}
|
|
MDFindMention(name: string, original: string, box: HTMLDivElement, typebox: MarkDown) {
|
|
if (this.ws && this.lookingguild) {
|
|
this.MDFineMentionGen(name, original, box, typebox);
|
|
const nonce = Math.floor(Math.random() * 10 ** 8) + "";
|
|
if (this.lookingguild.member_count <= this.lookingguild.members.size) return;
|
|
this.ws.send(
|
|
JSON.stringify({
|
|
op: 8,
|
|
d: {
|
|
guild_id: [this.lookingguild.id],
|
|
query: name,
|
|
limit: 8,
|
|
presences: true,
|
|
nonce,
|
|
},
|
|
}),
|
|
);
|
|
this.searchMap.set(nonce, async (e) => {
|
|
console.log(e);
|
|
if (e.members && e.members[0]) {
|
|
if (e.members[0].user) {
|
|
for (const thing of e.members) {
|
|
await Member.new(thing, this.lookingguild as Guild);
|
|
}
|
|
} else {
|
|
const prom1: Promise<User>[] = [];
|
|
for (const thing of e.members) {
|
|
prom1.push(this.getUser(thing.id));
|
|
}
|
|
Promise.all(prom1);
|
|
for (const thing of e.members) {
|
|
if (!this.userMap.has(thing.id)) {
|
|
console.warn("Dumb server bug for this member", thing);
|
|
continue;
|
|
}
|
|
await Member.new(thing, this.lookingguild as Guild);
|
|
}
|
|
}
|
|
this.MDFineMentionGen(name, original, box, typebox);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
findEmoji(search: string, orginal: string, box: HTMLDivElement, typebox: MarkDown) {
|
|
const emj = Emoji.searchEmoji(search, this, 10);
|
|
const map = emj.map(([emoji]): [string, string, HTMLElement] => {
|
|
return [
|
|
emoji.name,
|
|
emoji.id
|
|
? `<${emoji.animated ? "a" : ""}:${emoji.name}:${emoji.id}>`
|
|
: (emoji.emoji as string),
|
|
emoji.getHTML(),
|
|
];
|
|
});
|
|
this.MDSearchOptions(map, orginal, box, typebox);
|
|
}
|
|
search(box: HTMLDivElement, md: MarkDown, str: string, pre: boolean) {
|
|
if (!pre) {
|
|
const match = str.match(this.autofillregex);
|
|
|
|
if (match) {
|
|
const [type, search] = [match[0][0], match[0].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;
|
|
}
|
|
return;
|
|
}
|
|
}
|
|
box.innerHTML = "";
|
|
}
|
|
searching = false;
|
|
mSearch(query: string) {
|
|
const p = new URLSearchParams("?");
|
|
this.searching = true;
|
|
p.set("content", query.trim());
|
|
fetch(this.info.api + `/guilds/${this.lookingguild?.id}/messages/search/?` + p.toString(), {
|
|
headers: this.headers,
|
|
})
|
|
.then((_) => _.json())
|
|
.then((json: {messages: [messagejson][]; total_results: number}) => {
|
|
//FIXME total_results shall be ignored as it's known to be bad, spacebar bug.
|
|
const messages = json.messages
|
|
.map(([m]) => {
|
|
const c = this.channelids.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);
|
|
const sideDiv = document.getElementById("sideDiv");
|
|
const sideContainDiv = document.getElementById("sideContainDiv");
|
|
if (!sideDiv || !sideContainDiv) return;
|
|
sideDiv.innerHTML = "";
|
|
sideContainDiv.classList.add("searchDiv");
|
|
let channel: Channel | undefined = undefined;
|
|
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);
|
|
}
|
|
const html = message.buildhtml(undefined, true);
|
|
html.addEventListener("click", async () => {
|
|
try {
|
|
await message.channel.focus(message.id);
|
|
} catch (e) {
|
|
console.error(e);
|
|
}
|
|
});
|
|
sideDiv.append(html);
|
|
}
|
|
});
|
|
}
|
|
|
|
keydown: (event: KeyboardEvent) => unknown = () => {};
|
|
keyup: (event: KeyboardEvent) => boolean = () => false;
|
|
//---------- resolving members code -----------
|
|
readonly waitingmembers = new Map<
|
|
string,
|
|
Map<string, (returns: memberjson | undefined) => void>
|
|
>();
|
|
readonly presences: Map<string, presencejson> = new Map();
|
|
async resolvemember(id: string, guildid: string): Promise<memberjson | undefined> {
|
|
if (guildid === "@me") {
|
|
return undefined;
|
|
}
|
|
const guild = this.guildids.get(guildid);
|
|
const borked = true;
|
|
if (!guild || (borked && guild.member_count > 250)) {
|
|
//sorry puyo, I need to fix member resolving while it's broken on large guilds
|
|
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;
|
|
}
|
|
}
|
|
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;
|
|
}
|
|
fetchingmembers: Map<string, boolean> = new Map();
|
|
noncemap: Map<string, (r: [memberjson[], string[]]) => void> = new Map();
|
|
noncebuild: Map<string, [memberjson[], string[], number[]]> = new Map();
|
|
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.noncebuild.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.noncebuild.delete(chunk.nonce);
|
|
const func = this.noncemap.get(chunk.nonce);
|
|
if (!func) return;
|
|
func([arr[0], arr[1]]);
|
|
this.noncemap.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.noncemap.set(nonce, res);
|
|
this.noncebuild.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 +
|
|
" | Jank Client";
|
|
}
|
|
async instanceStats() {
|
|
const res = await fetch(this.info.api + "/policies/stats", {
|
|
headers: this.headers,
|
|
});
|
|
const json = await res.json();
|
|
const dialog = new Dialog("");
|
|
dialog.options.addTitle(I18n.getTranslation("instanceStats.name", this.instancePing.name));
|
|
dialog.options.addText(I18n.getTranslation("instanceStats.users", json.counts.user));
|
|
dialog.options.addText(I18n.getTranslation("instanceStats.servers", json.counts.guild));
|
|
dialog.options.addText(I18n.getTranslation("instanceStats.messages", json.counts.message));
|
|
dialog.options.addText(I18n.getTranslation("instanceStats.members", json.counts.members));
|
|
dialog.show();
|
|
}
|
|
setNotificationSound(sound: string) {
|
|
const userinfos = getBulkInfo();
|
|
userinfos.preferences.notisound = sound;
|
|
localStorage.setItem("userinfos", JSON.stringify(userinfos));
|
|
}
|
|
playSound(name = this.getNotificationSound()) {
|
|
if (this.play) {
|
|
const voice = this.play.audios.get(name);
|
|
if (voice) {
|
|
voice.play();
|
|
} else if (this.perminfo.sound && this.perminfo.sound.cSound) {
|
|
const audio = document.createElement("audio");
|
|
audio.src = this.perminfo.sound.cSound;
|
|
audio.play().catch();
|
|
}
|
|
}
|
|
}
|
|
getNotificationSound() {
|
|
const userinfos = getBulkInfo();
|
|
return userinfos.preferences.notisound;
|
|
}
|
|
}
|
|
export {Localuser};
|