Updated 53 files, added 2030 files, deleted 170 files and renamed 1800 files (automated)

mane
Mia Raindrops 2 months ago
parent 61513acaf9
commit d08872c5ca
Signed by: Mia Raindrops
GPG Key ID: EFBDC68435A574B7

16
.gitignore vendored

@ -1,4 +1,12 @@
bans.json
verify.json
build
motd.md
server/bans.json
server/verify.json
server/motd.md
server/remote.lctsc
server/local.lctsc
launcher/server/bans.json
launcher/server/verify.json
launcher/server/motd.md
launcher/server/remote.lctsc
launcher/server/local.lctsc
debug
build

@ -0,0 +1,117 @@
#!/usr/bin/env node
const fs = require('fs');
const zlib = require('zlib');
const child_process = require("child_process");
const crypto = require("crypto");
let ignore = [
"bans.json",
"verify.json",
"motd.md",
"remote.lctsc",
"local.lctsc",
"debug",
"build",
"server.txt",
".DS_Store",
"Electron.app"
];
function scandir(path) {
let list = [];
list.push(path);
for (let file of fs.readdirSync(path)) {
if (fs.lstatSync(path + "/" + file).isDirectory()) {
if (!ignore.includes(file)) {
list.push(path + "/" + file);
list.push(...scandir(path + "/" + file));
}
} else {
if (!ignore.includes(file)) list.push(path + "/" + file);
}
}
return list;
}
// Client
let list = scandir("./client");
let pack = [];
for (let item of list) {
if (fs.lstatSync(item).isDirectory()) {
pack.push({
type: "directory",
name: item,
content: null
});
} else {
pack.push({
type: "file",
name: item,
content: zlib.brotliCompressSync(fs.readFileSync(item)).toString("base64"),
hash: crypto.createHash("sha256").update(fs.readFileSync(item)).digest("base64")
});
}
}
fs.writeFileSync("build/client.lctpk", zlib.brotliCompressSync(JSON.stringify(pack)));
// Server
list = scandir("./server");
pack = [];
for (let item of list) {
if (fs.lstatSync(item).isDirectory()) {
pack.push({
type: "directory",
name: item,
content: null
});
} else {
pack.push({
type: "file",
name: item,
content: zlib.brotliCompressSync(fs.readFileSync(item)).toString("base64"),
hash: crypto.createHash("sha256").update(fs.readFileSync(item)).digest("base64")
});
}
}
fs.writeFileSync("build/server.lctpk", zlib.brotliCompressSync(JSON.stringify(pack)));
// Shared
list = scandir("./shared");
pack = [];
for (let item of list) {
if (fs.lstatSync(item).isDirectory()) {
pack.push({
type: "directory",
name: item,
content: null,
hash: null
});
} else {
pack.push({
type: "file",
name: item,
content: zlib.brotliCompressSync(fs.readFileSync(item)).toString("base64"),
hash: crypto.createHash("sha256").update(fs.readFileSync(item)).digest("base64")
});
}
}
fs.writeFileSync("build/shared.lctpk", zlib.brotliCompressSync(JSON.stringify(pack)));
child_process.execSync("scp build/client.lctpk zephyrheights:/pool/web/cdn/localchat", { stdio: "inherit" });
child_process.execSync("scp build/server.lctpk zephyrheights:/pool/web/cdn/localchat", { stdio: "inherit" });
child_process.execSync("scp build/shared.lctpk zephyrheights:/pool/web/cdn/localchat", { stdio: "inherit" });
child_process.execSync("npx pkg -C GZip -o ../../build/server/mac/lcts -t node18-darwin-arm64 index.js", { cwd: "./launcher/server", stdio: "inherit" });
child_process.execSync("npx pkg -C GZip -o ../../build/server/linux/lcts -t node18-linuxstatic-x64 index.js", { cwd: "./launcher/server", stdio: "inherit" });
child_process.execSync("npx pkg -C GZip -o ../../build/server/win32/lcts.exe -t node18-win32-x64 index.js", { cwd: "./launcher/server", stdio: "inherit" });
child_process.execSync("npx electron-packager . Localchat --ignore build --ignore scripts --overwrite --icon icon.icns --platform=darwin --arch=arm64 --out=../../build/client", { cwd: "./launcher/client", stdio: "inherit" })
child_process.execSync("npx electron-packager . Localchat --ignore build --ignore scripts --overwrite --icon icon.icns --platform=win32 --arch=x64 --out=../../build/client", { cwd: "./launcher/client", stdio: "inherit" })

@ -134,6 +134,13 @@ commands = {
localSystemMessage("You are not currently connected to a server");
}
},
"version": (_1, _2) => {
if (window.connected) {
localSystemMessage("Localchat version " + stripHTML(window.version) + "<br>Node.js version " + process.versions.node + "<br>Chrome version " + process.versions.chrome + "<br>Electron version " + process.versions.electron + "<br>Server version " + stripHTML(window.serverVersion), true);
} else {
localSystemMessage("Localchat version " + stripHTML(window.version) + "<br>Node.js version " + process.versions.node + "<br>Chrome version " + process.versions.chrome + "<br>Electron version " + process.versions.electron, true);
}
},
"_": (argument, command) => {
localSystemMessage("Sorry, /" + command + " is not a valid slash command, use /help to get a list");
},

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 46 KiB

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 17 KiB

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="48" viewBox="0 96 960 960" width="48"><path d="M63 519 0 456q93-95 218-147.5T480 256q24 0 54 1.5t62 5.5l-42 86q-18-2-36-2.5t-38-.5q-119 0-225.5 46T63 519Zm123 118-63-63q69-67 165.5-102T508 445l-39 81q-77-2-154.5 29T186 637Zm258 255q-27-11-39.5-41t1.5-59l237-490q3-7 10.5-10t15.5-1q8 2 13 9t3 15L547 850q-8 32-41 42.5t-62-.5Zm330-255q-16-17-44.5-37T681 571l21-86q35 12 73 38.5t62 50.5l-63 63Zm123-118q-39-38-85-69t-88-50l22-90q65 27 116 63.5t98 82.5l-63 63Z" fill="#b0cccc"/></svg>

After

Width:  |  Height:  |  Size: 528 B

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="48" viewBox="0 96 960 960" width="48"><path d="m421 758 283-283-46-45-237 237-120-120-45 45 165 166Zm59 218q-82 0-155-31.5t-127.5-86Q143 804 111.5 731T80 576q0-83 31.5-156t86-127Q252 239 325 207.5T480 176q83 0 156 31.5T763 293q54 54 85.5 127T880 576q0 82-31.5 155T763 858.5q-54 54.5-127 86T480 976Zm0-60q142 0 241-99.5T820 576q0-142-99-241t-241-99q-141 0-240.5 99T140 576q0 141 99.5 240.5T480 916Zm0-340Z" fill="#b0cccc"/></svg>

After

Width:  |  Height:  |  Size: 476 B

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="48" viewBox="0 96 960 960" width="48"><path d="m232 699-63-63q70-70 146.5-105T480 496q88 0 164.5 35T791 636l-63 63q-61-61-123-87t-125-26q-63 0-125 26t-123 87ZM63 530 0 467q93-95 216.5-153T480 256q140 0 263.5 58T960 467l-63 63q-88-84-192.5-134T480 346q-120 0-224.5 50T63 530Zm417 417 148-149q-29-29-66.5-45.5T480 736q-44 0-81.5 16.5T332 798l148 149Z" fill="#b0cccc"/></svg>

After

Width:  |  Height:  |  Size: 420 B

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="48" viewBox="0 96 960 960" width="48"><path fill="#e0e3e2" d="M320 814 80 574l242-242 43 43-199 199 197 197-43 43Zm318 2-43-43 199-199-197-197 43-43 240 240-242 242Z"/></svg>

After

Width:  |  Height:  |  Size: 222 B

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="48" viewBox="0 96 960 960" width="48"><path d="m40 936 440-760 440 760H40Zm104-60h672L480 296 144 876Zm340.175-57q12.825 0 21.325-8.675 8.5-8.676 8.5-21.5 0-12.825-8.675-21.325-8.676-8.5-21.5-8.5-12.825 0-21.325 8.675-8.5 8.676-8.5 21.5 0 12.825 8.675 21.325 8.676 8.5 21.5 8.5ZM454 708h60V484h-60v224Zm26-122Z" fill="#b0cccc"/></svg>

After

Width:  |  Height:  |  Size: 382 B

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="48" viewBox="0 96 960 960" width="48"><path d="M479.982 776q14.018 0 23.518-9.482 9.5-9.483 9.5-23.5 0-14.018-9.482-23.518-9.483-9.5-23.5-9.5-14.018 0-23.518 9.482-9.5 9.483-9.5 23.5 0 14.018 9.482 23.518 9.483 9.5 23.5 9.5ZM453 623h60V370h-60v253Zm27.266 353q-82.734 0-155.5-31.5t-127.266-86q-54.5-54.5-86-127.341Q80 658.319 80 575.5q0-82.819 31.5-155.659Q143 347 197.5 293t127.341-85.5Q397.681 176 480.5 176q82.819 0 155.659 31.5Q709 239 763 293t85.5 127Q880 493 880 575.734q0 82.734-31.5 155.5T763 858.316q-54 54.316-127 86Q563 976 480.266 976Zm.234-60Q622 916 721 816.5t99-241Q820 434 721.188 335 622.375 236 480 236q-141 0-240.5 98.812Q140 433.625 140 576q0 141 99.5 240.5t241 99.5Zm-.5-340Z" fill="#ffb4ab"/></svg>

After

Width:  |  Height:  |  Size: 768 B

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="48" viewBox="0 96 960 960" width="48"><path fill="#e0e3e2" d="M480 976q-84 0-157-31.5T196 859q-54-54-85-127.5T80 574q0-84 31-156.5T196 291q54-54 127-84.5T480 176q84 0 157 30.5T764 291q54 54 85 126.5T880 574q0 84-31 157.5T764 859q-54 54-127 85.5T480 976Zm0-58q35-36 58.5-82.5T577 725H384q14 60 37.5 108t58.5 85Zm-85-12q-25-38-43-82t-30-99H172q38 71 88 111.5T395 906Zm171-1q72-23 129.5-69T788 725H639q-13 54-30.5 98T566 905ZM152 665h159q-3-27-3.5-48.5T307 574q0-25 1-44.5t4-43.5H152q-7 24-9.5 43t-2.5 45q0 26 2.5 46.5T152 665Zm221 0h215q4-31 5-50.5t1-40.5q0-20-1-38.5t-5-49.5H373q-4 31-5 49.5t-1 38.5q0 21 1 40.5t5 50.5Zm275 0h160q7-24 9.5-44.5T820 574q0-26-2.5-45t-9.5-43H649q3 35 4 53.5t1 34.5q0 22-1.5 41.5T648 665Zm-10-239h150q-33-69-90.5-115T565 246q25 37 42.5 80T638 426Zm-254 0h194q-11-53-37-102.5T480 236q-32 27-54 71t-42 119Zm-212 0h151q11-54 28-96.5t43-82.5q-75 19-131 64t-91 115Z"/></svg>

After

Width:  |  Height:  |  Size: 945 B

@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="#000000"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/></svg>
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="#e0e3e2"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/></svg>

Before

Width:  |  Height:  |  Size: 194 B

After

Width:  |  Height:  |  Size: 194 B

File diff suppressed because it is too large Load Diff

@ -0,0 +1,122 @@
const { app, BrowserWindow, globalShortcut, ipcMain, dialog } = require('electron');
const path = require('path');
const os = require("os");
if (require('os').platform() !== "darwin" && require('os').platform() !== "win32") return;
const createWindow = () => {
global.mainWindow = new BrowserWindow({
width: 500,
minWidth: 500,
height: 800,
minHeight: 800,
icon: require('os').platform() ? "./icon.icns" : "./icon.ico",
disableAutoHideCursor: true,
backgroundColor: "#000000",
darkTheme: true,
titleBarStyle: "hidden",
show: false,
fullscreenable: false,
vibrancy: "menu",
frame: false,
titleBarOverlay: {
color: "#151515",
symbolColor: "#ffffff",
height: 34
},
trafficLightPosition: {
x: 13,
y: 10
},
autoHideMenuBar: true,
webPreferences: {
nodeIntegration: true,
contextIsolation: false
}
});
mainWindow.loadFile(global._localchatPath + "/index.html");
//mainWindow.setContentProtection(true);
ipcMain.handle("open-server", async (event) => {
let select = dialog.showOpenDialogSync({
title: "Open a .lctsc file to connect to a server",
message: "Open a .lctsc file to connect to a server",
defaultPath: os.homedir(),
buttonLabel: "Connect",
filters: [
{
name: "Localchat 2.x Server Configuration",
extensions: [ "lctsc" ]
}
],
properties: [
"openFile"
]
});
return select;
});
ipcMain.on('devmode', () => {
mainWindow.openDevTools();
});
ipcMain.on('boop', () => {
if (!mainWindow.isFocused()) {
if (os.platform() === "win32") {
mainWindow.setProgressBar(1, {
mode: "indeterminate"
});
} else {
mainWindow.flashFrame(true);
}
mainWindow.once("focus", () => {
if (os.platform() === "win32") {
mainWindow.setProgressBar(-1);
} else {
mainWindow.flashFrame(false);
}
})
}
});
ipcMain.on('ready', () => {
mainWindow.setResizable(false);
mainWindow.setMaximizable(false);
mainWindow.setSize(500, 800);
mainWindow.show();
try { loaderWindow.close(); } catch (e) {}
});
ipcMain.on('past-oobe', () => {
mainWindow.setResizable(true);
mainWindow.setMaximizable(true);
});
}
app.whenReady().then(() => {
globalShortcut.register('Alt+CommandOrControl+C', () => {
if (mainWindow) {
try {
if (mainWindow.isVisible()) {
mainWindow.hide();
} else {
mainWindow.show();
}
} catch (e) {}
}
});
createWindow();
app.on('activate', () => {
if (BrowserWindow.getAllWindows().length === 0) createWindow();
});
});
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') app.quit();
});

@ -0,0 +1,15 @@
ws://127.0.0.1:27342
-----BEGIN RSA PUBLIC KEY-----
MIICCgKCAgEAsE3mcyF/exG7vqCOLpXmzpnmyyrUfkukSYDBNu5VaJWD6+YSxIrk
wQojd6jCAeWtf21mCBzPcdC3DopaPitMLWL/RYlCXI17TmaSJPQhiWjYumcibBJl
4sVHyWiOGayoBNGIxqoVpfoyGJ8DPNRwlWjEUdbp09rT8uFGlTaYf0hfVt9eWe2u
FQXoA43QLM0GXBy5hWCJLihury0Rd0dxVRH5oZkWv7U9zF2Ayj4+zQB1AwhmXvxY
qgTqmB0LT6m79WqFNn8L61VeTdldq3BvUGpGcs9/UIazsuUW3bzDkHf63BaaTp5U
B1ImojuyKMlNyQEpa9mzsS79+8S2MizBxL0LjdRkIYud/0zR+Jii+JLdX7X4JDgn
vUXOmQ4gO0+Ln0GI18Mv6nCG/BM4Xzbaoszhv0ViumwkUu53a1o0YP6Q98PU8095
6vL1GCgC8lRF/kkJ8nobhCHfEqmVjJTKRcryet/Pm0SJhMB7Hy1bniyyuHYRZI33
ltXdSjwchoiVyCuGqWpdjQWPnqpVgN053WJOVMIzCkfDWgUMzTUFZZAsOKK6ZuSx
m7PeovWxEE9iR04iOhc1Ju1K+nsPNkthgENm8Qawye9PxPe8D/wVM1iRQcaTVqPI
oB00Yx9iminuh6+abLQIt+YbT5+3bTW9HxjUj8lvAO8MQV4RcC+WhNsCAwEAAQ==
-----END RSA PUBLIC KEY-----

@ -1,669 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>LocalChat</title>
<script src="crypt.js"></script>
<script src="commands.js"></script>
<script type="text/javascript" src="lib/purify.min.js"></script>
<script src="lib/marked.min.js"></script>
<style>
body, html {
background-color: #000000;
color: white;
margin: 0;
position: fixed;
inset: 0;
font-family: system-ui, -apple-system, sans-serif;
}
* {
outline: none;
}
#chat-log {
position: fixed;
top: 26px;
left: 0;
right: 0;
bottom: 48px;
padding: 20px 20px 28px;
overflow-y: auto;
}
#input {
position: fixed;
bottom: 0;
left: 0;
right: 0;
height: 48px;
background: #151515;
border-top: 1px solid #222;
z-index: 999;
display: grid;
grid-template-columns: 48px 1fr;
}
#input-text {
background: transparent;
color: white;
border: none;
width: calc(100% - 40px);
height: calc(100% - 20px);
padding: 10px;
}
#title-bar {
background: #151515;
border-bottom: 1px solid #222;
position: fixed;
top: 0;
left: 0;
right: 0;
height: 26px;
-webkit-app-region: drag;
display: grid;
grid-template-columns: repeat(3, 1fr);
color: #ffffff77;
font-size: 14px;
}
#title-bar.platform-mac {
padding-left: 80px;
}
#title-bar.platform-windows {
padding-right: 138px;
}
#title-bar > div {
display: flex;
align-items: center;
text-align: center;
justify-content: center;
}
.message {
overflow: hidden;
}
.message-date {
font-size: 12px;
opacity: .5;
margin-right: 5px;
}
.message-author {
font-weight: 550;
}
.message-text {
margin-left: 5px;
}
.message {
margin: 5px 0;
}
.system-message .message-text {
margin-left: 0 !important;
opacity: .75;
color: #72ff72;
}
.direct-message {
color: skyblue;
}
#typing {
height: 14px;
bottom: 16px;
position: fixed;
font-size: 14px;
left: 0;
right: 0;
padding: 4px 20px 5px 20px;
background: #222222;
border-top: 1px solid #252525;
pointer-events: none;
transition: opacity 200ms, bottom 200ms;
opacity: 0;
}
#typing.shown {
opacity: 1;
bottom: 49px;
}
body.background {
background-size: cover;
background-position: center;
background-repeat: no-repeat;
}
body.background #chat-log {
background-color: rgba(0, 0, 0, .8);
}
body.background #title-bar, body.background #input {
background-color: rgba(20, 20, 20, .8);
backdrop-filter: blur(30px);
}
body.background #typing {
background-color: rgba(33, 33, 33, .8);
backdrop-filter: blur(30px);
}
.input-icon img {
width: 32px;
height: 32px;
filter: invert(1);
vertical-align: middle;
}
.input-icon {
width: 32px;
height: 32px;
margin: 8px;
border-radius: 999px;
}
.input-icon:hover {
background: rgba(255, 255, 255, .1);
}
.input-icon:active {
background: rgba(255, 255, 255, .25);
}
a {
color: inherit;
}
.message-file-preview {
max-width: 100%;
max-height: 100%;
}
</style>
</head>
<body>
<div id="title-bar">
<div>
<b>Localchat</b>
</div>
<div><span id="users">-</span>&nbsp;online</div>
<div id="ping">Offline</div>
</div>
<script>
if (require('os').platform() === "darwin") {
document.getElementById("title-bar").classList.add("platform-mac");
} else {
document.getElementById("title-bar").classList.add("platform-windows");
}
</script>
<div id="chat-log"></div>
<div id="input">
<a class="input-icon" onclick="if (window.connected) uploadFile();"><img src="icons/upload.svg"></a>
<input maxlength="500" id="input-text" type="text" placeholder="Send a message">
</div>
<div id="typing"><b id="typing-single">X</b><span id="typing-two-outer"> and <b id="typing-two">X</b></span><span id="typing-many">Multiple people</span> <span id="typing-singular">is</span> <span id="typing-plural">are</span> typing…</div>
<script>
const { ipcRenderer } = require('electron');
window.onerror = (_1, _2, _3, _4, error) => {
localSystemMessage("An error occurred: " + error.name + ": " + error.message + "; please report this on https://bugs.equestria.dev/issues/LCHT");
console.error(error);
}
let userName = localStorage.getItem("username") ?? require('os').userInfo().username;
let ws;
let wsPingInterval;
let messages = [];
let colors = localStorage.getItem("colors") ? JSON.parse(localStorage.getItem("colors")) : [
"ffffff",
"ffffff"
];
let lastMessages = "[]";
function updateBackground() {
let background = localStorage.getItem("background") ?? null;
if (background) {
try {
require('fs').readFileSync(background);
} catch (e) {
localSystemMessage("Unable to load the background image: " + e.message + "; run /background to change or reset it");
return;
}
document.body.classList.add("background");
document.body.style.backgroundImage = 'url("file://' + background.replaceAll("\\", "/") + '")';
} else {
document.body.classList.remove("background");
document.body.style.backgroundImage = null;
}
}
updateBackground();
const uuid = require('uuid-v4');
window.reconnect = true;
window.banned = false;
window.connected = false;
window.directOverview = false;
window.typing = {};
window.lastTyping = 0;
window.token = generateToken();
window.files = {};
window.ping = -1;
setInterval(() => {
if (JSON.stringify(messages) !== lastMessages) {
ipcRenderer.send("boop");
lastMessages = JSON.stringify(messages);
}
}, 100);
function stripHTML(text) {
return text.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
}
function displayMessages() {
_displayMessages();
_displayMessages();
}
function _displayMessages() {
document.getElementById("chat-log").innerHTML = messages.map(message => `
${message['type'] === "text" ? `
<div class="message user-message">
<span class="message-date" title="${new Date(message['date']).toString()}">${new Date(message['date']).toTimeString().split(":").splice(0, 2).join(":")}</span>
<span class="message-author" style="-webkit-background-clip: text; -webkit-text-fill-color: transparent; background-image: linear-gradient(90deg, #${stripHTML(message['colors'][0])} 0%, #${stripHTML(message['colors'][1])} 100%)" title="${stripHTML(message['_source'])}">${stripHTML(message['author'])}</span>
<span class="message-text">${DOMPurify.sanitize(marked.parseInline(message['text']))}</span>
</div>
`
: (message['type'] === "system" ? `
<div class="message system-message">
<span class="message-date" title="${new Date(message['date']).toString()}">${new Date(message['date']).toTimeString().split(":").splice(0, 2).join(":")}</span>
<span class="message-text">${stripHTML(message['text'])}</span>
</div>
`: (message['type'] === "motd" ? `
<div class="message system-message">
<span class="message-date" title="${new Date(message['date']).toString()}">${new Date(message['date']).toTimeString().split(":").splice(0, 2).join(":")}</span>
<span class="message-text">Connected to Localchat Server v${message['version']}:${DOMPurify.sanitize(marked.parse(message['text']))}</span>
</div>
` : (message['type'] === "dm" ? `
<div class="message user-message direct-message">
<span class="message-date" title="${new Date(message['date']).toString()}">${new Date(message['date']).toTimeString().split(":").splice(0, 2).join(":")}</span>
<span class="message-author" title="${stripHTML(message['_source'])}">${stripHTML(message['author'])}</span>
<span class="message-text">${DOMPurify.sanitize(marked.parseInline(message['text']))}</span>
</div>
` : (message['type'] === "file" ? `
<div class="message user-message">
<span class="message-date" title="${new Date(message['date']).toString()}">${new Date(message['date']).toTimeString().split(":").splice(0, 2).join(":")}</span>
<span class="message-author" title="${stripHTML(message['_source'])}">${stripHTML(message['author'])}</span>
<span class="message-text"><span style="opacity: .75;">${message['file']['name']} (${formatSize(message['file']['size'])}) · <a style="color: white; cursor: pointer;" href="data:${message['file']['type']};base64,${message['file']['content']}" download="${stripHTML(message['file']['name'])}">Download</a></span>${message['file']['type'].startsWith("image/") ? `<br><img class="message-file-preview" src="data:${message['file']['type']};base64,${message['file']['content']}">` : (message['file']['type'].startsWith("audio/") ? `<br><audio controls class="message-file-preview" src="data:${message['file']['type']};base64,${message['file']['content']}"></audio>` : (message['file']['type'].startsWith("video/") ? `<br><video class="message-file-preview" src="data:${message['file']['type']};base64,${message['file']['content']}" controls></video>` : ``))}</span>
</div>
` : ``))))}`).join("");
let el = document.getElementById("chat-log");
el.scrollTop = el.scrollHeight;
}
function connect() {
let serverInfo;
try {
serverInfo = require('fs').readFileSync(__dirname + "/server.txt").toString().trim().replaceAll("\r\n", "\n");
} catch (e) {
serverInfo = require('fs').readFileSync("./resources/app/server.txt").toString().trim().replaceAll("\r\n", "\n");
}
let url = serverInfo.split("\n")[0].trim();
window.serverURL = url;
window.serverVerifyKey = serverInfo.split("\n").splice(1).join("\n").trim();
ws = new WebSocket(url);
window.serverKey = null;
ws.onopen = (e) => {
console.log(e);
ws.send(JSON.stringify({
type: "keyExchange",
key: keys.publicKey,
token: encrypt(token, serverVerifyKey)
}));
wsPingInterval = setInterval(() => {
ws.send(JSON.stringify({
type: "ping",
date: new Date().getTime()
}));
}, 1000);
}
ws.onclose = (e) => {
if (window.connected && window.reconnect) {
localSystemMessage("You left the chat");
}
window.connected = false;
console.log(e);
document.getElementById("ping").innerText = "Offline";
document.getElementById("users").innerText = "-";
clearInterval(wsPingInterval);
if (window.reconnect) {
setTimeout(() => {
connect();
}, 1000);
}
}
setInterval(() => {
Array.from(document.getElementsByTagName("a")).filter(i => i.href).map((i) => {
i.onclick = (event) => {
event.preventDefault();
require('electron').shell.openExternal(i.href);
}
});
});
ws.onmessage = (e) => {
let data;
try {
data = JSON.parse(e.data);
if (data.type !== "pong") console.log(data);
} catch (e) {
console.log(e);
return;
}
if (data.type === "keyExchange") {
if (data.token === window.token) {
window.serverKey = data.key;
window.connected = true;
systemMessage(userName + " (" + data.address + ") joined the chat", "You joined the chat");
} else {
localSystemMessage("Unable to verify the server's identity");
ws.close();
}
} else if (data.type === "banned") {
localSystemMessage("You have been blocked from this server");
window.reconnect = false;
window.banned = true;
ws.close();
} else if (data.type === "encrypted") {
let decrypted = JSON.parse(decrypt(data.message, keys.privateKey));
if (decrypted.type !== "typing") {
decrypted._source = data.source;
if (decrypted.type === "file") {
let components = data[decrypted.file.token].split(':');
let iv_from_ciphertext = Buffer.from(components.shift(), "base64");
let decipher = crypto.createDecipheriv("aes-256-ctr", Buffer.from(decrypted.file.key, "base64"), iv_from_ciphertext);
let deciphered = decipher.update(components.join(':'), "base64", "utf8");
deciphered += decipher.final("utf8");
decrypted.file.content = deciphered;
if (crypto.createHash("md5").update(Buffer.from(deciphered, "base64")).digest("hex") !== decrypted.file.md5) {
localSystemMessage("Warning: this file is possibly corrupted, make sure to check it before opening it");
}
}
if (decrypted.type === "motd") {
let components = data["_motd"].split(':');
let iv_from_ciphertext = Buffer.from(components.shift(), "base64");
let decipher = crypto.createDecipheriv("aes-256-ctr", Buffer.from(decrypted.key, "base64"), iv_from_ciphertext);
let deciphered = decipher.update(components.join(':'), "base64", "utf8");
deciphered += decipher.final("utf8");
decrypted.text = Buffer.from(deciphered, "base64").toString();
}
messages.push(decrypted);
}
if (decrypted.type === "dm" && !window.directOverview) {
window.directOverview = true;
localSystemMessage("This is a private message from the server owner. Use /r to reply privately.")
}
if (decrypted.type === "typing") {
typing[decrypted.author] = new Date().getTime();
refreshTyping();
}
console.log(decrypted);
displayMessages();
} else if (data.type === "pong") {
document.getElementById("users").innerText = data.users;
document.getElementById("ping").innerText = (new Date().getTime() - data.original.date).toString() + " ms";
window.ping = new Date().getTime() - data.original.date;
}
}
}
setInterval(() => {
for (let user of Object.keys(typing)) {
console.log(user, typing[user], new Date() - typing[user]);
if (new Date() - typing[user] >= 5000) {
let newTyping = {};
for (let user2 of Object.keys(typing)) {
if (user !== user2) newTyping[user2] = typing[user2];
}
typing = newTyping;
}
}
refreshTyping();
}, 100);
function send(obj) {
ws.send(JSON.stringify({
type: "encrypted",
message: encrypt(JSON.stringify(obj), window.serverKey)
}));
}
function formatSize(size) {
let value = Math.abs(size);
if (value > 1024) {
if (value > 1024**2) {
if (value > 1024**3) {
if (value > 1024**4) {
return (value / 1024**4).toFixed(2) + " TiB";
} else {
return (value / 1024**3).toFixed(2) + " GiB";
}
} else {
return (value / 1024**2).toFixed(2) + " MiB";
}
} else {
return (value / 1024).toFixed(2) + " KiB";
}
} else {
return value + " B";
}
}
async function uploadFile() {
let handles = await window.showOpenFilePicker({
multiple: false
});
if (handles.length >= 1) {
let file = await handles[0].getFile();
if (file.size > 2*1024**2) {
localSystemMessage("Unable to send this file because it exceeds the maximum allowed size (2 MiB)");
} else {
let reader = new FileReader();
reader.readAsArrayBuffer(file);
reader.onload = () => {
let content = Buffer.from(reader.result).toString("base64");
let key = crypto.randomBytes(32);
let iv = crypto.randomBytes(16);
let cipher = crypto.createCipheriv("aes-256-ctr", key, iv);
let ciphered = cipher.update(content, "utf8", "base64");
ciphered += cipher.final("base64");
let encrypted = iv.toString("base64") + ':' + ciphered;
let token = generateToken();
let fileObj = {
type: "file",
author: userName,
colors,
date: new Date().getTime(),
file: {
token,
size: file.size,
name: file.name,
type: file.type,
key: key.toString("base64"),
md5: crypto.createHash("md5").update(content).digest("hex"),
content: null
}
};
let cryptObj = {
type: "encrypted",
message: encrypt(JSON.stringify(fileObj), window.serverKey)
}
cryptObj[token] = encrypted;
ws.send(JSON.stringify(cryptObj));
fileObj['_source'] = "localhost";
fileObj['file']['content'] = content;
messages.push(fileObj);
displayMessages();
}
reader.onerror = () => {
localSystemMessage("An error occurred while uploading your file, try again later");
}
}
}
}
document.getElementById("input-text").onkeydown = (e) => {
if (e.code === "Enter") {
if (document.getElementById("input-text").value.trim() === "" || !window.connected || window.banned) return;
if (document.getElementById("input-text").value.startsWith("/") && document.getElementById("input-text").value.length > 1) {
let text = document.getElementById("input-text").value;
document.getElementById("input-text").value = "";
runCommand(text);
} else {
let text = document.getElementById("input-text").value;
if (document.getElementById("input-text").value.startsWith("\\/")) text = document.getElementById("input-text").value.substring(1);
let obj = {
type: "text",
author: userName,
colors,
date: new Date().getTime(),
text
}
send(obj);
obj['_source'] = "localhost";
messages.push(obj);
if (!window.banned && !window.connected) {
localSystemMessage("Unable to send this message because you are not connected to the server");
} else if (window.banned && !window.connected) {
localSystemMessage("Unable to send this message because you are blocked from the server");
}
displayMessages();
document.getElementById("input-text").value = "";
}
} else {
if (window.connected && !window.banned && new Date().getTime() - window.lastTyping > 1000) {
send({
type: "typing",
author: userName
});
window.lastTyping = new Date().getTime();
}
}
}
function refreshTyping() {
let users = Object.keys(typing);
if (users.length === 0) {
document.getElementById("typing").classList.remove("shown");
} else if (users.length === 1) {
document.getElementById("typing").classList.add("shown");
document.getElementById("typing-plural").style.display = "none";
document.getElementById("typing-singular").style.display = "inline";
document.getElementById("typing-two-outer").style.display = "none";
document.getElementById("typing-many").style.display = "none";
document.getElementById("typing-single").style.display = "inline";
document.getElementById("typing-single").innerText = users[0];
} else if (users.length === 2) {
document.getElementById("typing").classList.add("shown");
document.getElementById("typing-plural").style.display = "inline";
document.getElementById("typing-singular").style.display = "none";
document.getElementById("typing-two-outer").style.display = "inline";
document.getElementById("typing-many").style.display = "none";
document.getElementById("typing-single").style.display = "inline";
document.getElementById("typing-single").innerText = users[0];
document.getElementById("typing-two").innerText = users[1];
} else if (users.length > 2) {
document.getElementById("typing").classList.add("shown");
document.getElementById("typing-plural").style.display = "inline";
document.getElementById("typing-singular").style.display = "none";
document.getElementById("typing-two-outer").style.display = "none";
document.getElementById("typing-many").style.display = "inline";
document.getElementById("typing-single").style.display = "none";
}
}
function systemMessage(text, localText) {
if (!localText) localText = text;
messages.push({
type: "system",
date: new Date().getTime(),
text: localText
});
displayMessages();
send({
type: "system",
date: new Date().getTime(),
text
});
}
function localSystemMessage(text) {
messages.push({
type: "system",
date: new Date().getTime(),
text
});
displayMessages();
}
connect();
</script>
</body>
</html>

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

@ -0,0 +1,159 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Localchat</title>
<style>
#loader-circle-1, #loader-circle-2, #loader-circle-3 {
animation-name: opacity;
animation-timing-function: linear;
animation-duration: 1.2s;
animation-fill-mode: forwards;
animation-iteration-count: infinite;
}
#loader-circle-2 {
animation-delay: .25s;
}
#loader-circle-3 {
animation-delay: .5s;
}
@keyframes opacity {
0% {
opacity: 0;
}
50% {
opacity: 1;
}
100% {
opacity: 0;
}
}
</style>
<script>
const { ipcRenderer } = require('electron');
ipcRenderer.on('local', (_, data) => {
loadApp(data);
});
ipcRenderer.on('download', () => {
download();
});
function download() {
document.getElementById("status").innerText = "Downloading update... 0%";
const axios = require('./node_modules/axios/dist/node/axios.cjs');
const fs = require("fs");
const os = require("os");
const zlib = require("zlib");
const crypto = require("crypto");
let tempDir = require('@electron/remote').app.getPath('userData') + "/Localchat";
if (fs.existsSync(tempDir)) fs.rmSync(tempDir, { recursive: true });
fs.mkdirSync(tempDir);
function downloadClient() {
return new Promise(async (res) => {
const response = await axios({
url: "https://static.equestria.horse/localchat/client.lctpk",
method: 'GET',
responseType: 'blob',
onDownloadProgress: (event) => {
document.getElementById("status").innerText = "Downloading update... " + Math.round(((event.loaded / event.total) / 2) * 100) + "%";
}
});
document.getElementById("status").innerText = "Extracting...";
res(JSON.parse(zlib.brotliDecompressSync(Buffer.from(await response.data.arrayBuffer())).toString()).map(i => {
if (i['content']) i['content'] = zlib.brotliDecompressSync(Buffer.from(i['content'], "base64"));
if (i['hash']) i['hash'] = crypto.createHash("sha256").update(i['content']).digest("base64") === i['hash'];
return i;
}));
});
}
function downloadShared() {
return new Promise(async (res) => {
const response = await axios({
url: "https://static.equestria.horse/localchat/shared.lctpk",
method: 'GET',
responseType: 'blob',
onDownloadProgress: (event) => {
document.getElementById("status").innerText = "Downloading update... " + Math.round((0.5 + (event.loaded / event.total) / 2) * 100) + "%";
}
});
document.getElementById("status").innerText = "Extracting...";
res(JSON.parse(zlib.brotliDecompressSync(Buffer.from(await response.data.arrayBuffer())).toString()).map(i => {
if (i['content']) i['content'] = zlib.brotliDecompressSync(Buffer.from(i['content'], "base64"));
if (i['hash']) i['hash'] = crypto.createHash("sha256").update(i['content']).digest("base64") === i['hash'];
return i;
}));
});
}
(async () => {
let _client = await downloadClient();
let _shared = await downloadShared();
let files = [..._client, ..._shared];
document.getElementById("status").innerText = "Installing update... 0%";
let total = files.length;
let index = 0;
for (let file of files) {
if (file.type === "file" && !file.hash) {
alert("Unable to continue: file " + file.name + " is corrupted");
window.close();
}
if (file.type === "file") {
fs.writeFileSync(tempDir + "/" + file.name, file.content);
} else {
if (!fs.existsSync(tempDir + "/" + file.name)) fs.mkdirSync(tempDir + "/" + file.name);
}
index++;
document.getElementById("status").innerText = "Installing update... " + Math.round((index / total) * 100) + "%";
}
document.getElementById("status").innerText = "Starting...";
if (fs.existsSync(__dirname + "/server.txt")) fs.copyFileSync(__dirname + "/server.txt", tempDir + "/client/server.txt");
if (fs.existsSync("./resources/app/server.txt")) fs.copyFileSync("./resources/app/server.txt", tempDir + "/client/server.txt");
if (fs.existsSync(__dirname + "/server.lctsc")) fs.copyFileSync(__dirname + "/server.lctsc", tempDir + "/client/server.lctsc");
if (fs.existsSync("./resources/app/server.lctsc")) fs.copyFileSync("./resources/app/server.lctsc", tempDir + "/client/server.lctsc");
loadApp(tempDir + "/client");
})();
}
function loadApp(path) {
ipcRenderer.send("start", path);
}
</script>
</head>
<body style="margin: 0; user-select: none;">
<div style="position: fixed; inset: 0; z-index: 9999; -webkit-app-region: drag;"></div>
<div style="position: fixed; inset: 0; z-index: 999; display: flex; align-items: center; justify-content: center; text-align: center;">
<div>
<svg xmlns="http://www.w3.org/2000/svg" x="0px" y="0px"
viewBox="0 0 48 48" style="filter: invert(1); opacity: .25; width: 48px;" xml:space="preserve">
<path id="loader-circle-2" d="M24.2,22.3c0.7,0,1.3-0.2,1.7-0.7c0.5-0.5,0.7-1.1,0.7-1.7s-0.2-1.2-0.7-1.7c-0.5-0.5-1-0.7-1.7-0.7s-1.3,0.2-1.7,0.7c-0.5,0.5-0.7,1.1-0.7,1.7c0,0.7,0.2,1.2,0.7,1.7C22.9,22.1,23.5,22.3,24.2,22.3z"/>
<path id="loader-circle-3" d="M33.3,22.3c0.7,0,1.2-0.2,1.7-0.7c0.5-0.5,0.7-1.1,0.7-1.7s-0.2-1.2-0.7-1.7c-0.5-0.5-1.1-0.7-1.7-0.7s-1.2,0.2-1.7,0.7c-0.5,0.5-0.7,1.1-0.7,1.7c0,0.7,0.2,1.2,0.7,1.7C32,22.1,32.6,22.3,33.3,22.3z"/>
<path d="M43.7,4.3c-0.9-0.9-2-1.4-3.2-1.4h-33C6.3,3,5.2,3.4,4.3,4.3C3.4,5.2,3,6.3,3,7.5V45l7.8-7.8h29.7c1.2,0,2.3-0.5,3.2-1.4c0.9-0.9,1.4-2,1.4-3.2v-25C45.1,6.3,44.6,5.2,43.7,4.3z M40.5,32.6H10L7.5,35V7.5h33V32.6z"/>
<path id="loader-circle-1" d="M14.7,17.5c-0.7,0-1.2,0.2-1.7,0.7c-0.5,0.5-0.7,1.1-0.7,1.7c0,0.7,0.2,1.2,0.7,1.7c0.5,0.5,1.1,0.7,1.7,0.7s1.2-0.2,1.7-0.7c0.5-0.5,0.7-1.1,0.7-1.7s-0.2-1.2-0.7-1.7C15.9,17.7,15.4,17.5,14.7,17.5z"/>
</svg>
<br>
<p style="color: white; opacity: .25; font-family: system-ui, -apple-system, sans-serif;" id="status">Starting...</p>
</div>
</div>
</body>
</html>

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 27.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 48 48" style="enable-background:new 0 0 48 48;" xml:space="preserve">
<path d="M24.2,22.3c0.7,0,1.3-0.2,1.7-0.7c0.5-0.5,0.7-1.1,0.7-1.7s-0.2-1.2-0.7-1.7c-0.5-0.5-1-0.7-1.7-0.7s-1.3,0.2-1.7,0.7
c-0.5,0.5-0.7,1.1-0.7,1.7c0,0.7,0.2,1.2,0.7,1.7C22.9,22.1,23.5,22.3,24.2,22.3z"/>
<path d="M33.3,22.3c0.7,0,1.2-0.2,1.7-0.7c0.5-0.5,0.7-1.1,0.7-1.7s-0.2-1.2-0.7-1.7c-0.5-0.5-1.1-0.7-1.7-0.7s-1.2,0.2-1.7,0.7
c-0.5,0.5-0.7,1.1-0.7,1.7c0,0.7,0.2,1.2,0.7,1.7C32,22.1,32.6,22.3,33.3,22.3z"/>
<path d="M43.7,4.3c-0.9-0.9-2-1.4-3.2-1.4h-33C6.3,3,5.2,3.4,4.3,4.3C3.4,5.2,3,6.3,3,7.5V45l7.8-7.8h29.7c1.2,0,2.3-0.5,3.2-1.4
c0.9-0.9,1.4-2,1.4-3.2v-25C45.1,6.3,44.6,5.2,43.7,4.3z M40.5,32.6H10L7.5,35V7.5h33V32.6z"/>
<path d="M14.7,17.5c-0.7,0-1.2,0.2-1.7,0.7c-0.5,0.5-0.7,1.1-0.7,1.7c0,0.7,0.2,1.2,0.7,1.7c0.5,0.5,1.1,0.7,1.7,0.7
s1.2-0.2,1.7-0.7c0.5-0.5,0.7-1.1,0.7-1.7s-0.2-1.2-0.7-1.7C15.9,17.7,15.4,17.5,14.7,17.5z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

@ -0,0 +1,47 @@
const { app, BrowserWindow, globalShortcut, ipcMain } = require('electron');
const path = require('path');
require('@electron/remote/main').initialize();
process.argv = process.argv.filter(i => !i.endsWith("main.js") && i !== ".");
const createWindow = () => {
global.loaderWindow = new BrowserWindow({
width: 256,
height: 300,
icon: require('os').platform() ? "./icon.icns" : "./icon.ico",
resizable: false,
maximizable: false,
minimizable: false,
disableAutoHideCursor: true,
backgroundColor: "#111111",
darkTheme: true,
fullscreenable: false,
frame: false,
autoHideMenuBar: true,
webPreferences: {
nodeIntegration: true,
contextIsolation: false
}
});
require("@electron/remote/main").enable(loaderWindow.webContents);
loaderWindow.loadFile('./index.html');
loaderWindow.setContentProtection(true);
ipcMain.on('start', (_, path) => {
process.chdir(path);
global._localchatPath = path;
require(path + "/main.js");
});
if (process.argv[1]) {
loaderWindow.send("local", process.argv[1]);
} else {
loaderWindow.send("download");
}
}
app.whenReady().then(() => {
createWindow();
});

@ -0,0 +1 @@
../resolve/bin/resolve

File diff suppressed because it is too large Load Diff

@ -0,0 +1,250 @@
import { Stats } from "fs";
interface IMinimatchOptions {
/**
* Dump a ton of stuff to stderr.
*
* @default false
*/
debug?: boolean | undefined;
/**
* Do not expand `{a,b}` and `{1..3}` brace sets.
*
* @default false
*/
nobrace?: boolean | undefined;
/**
* Disable `**` matching against multiple folder names.
*
* @default false
*/
noglobstar?: boolean | undefined;
/**
* Allow patterns to match filenames starting with a period,
* even if the pattern does not explicitly have a period in that spot.
*
* Note that by default, `'a/**' + '/b'` will **not** match `a/.d/b`, unless `dot` is set.
*
* @default false
*/
dot?: boolean | undefined;
/**
* Disable "extglob" style patterns like `+(a|b)`.
*
* @default false
*/
noext?: boolean | undefined;
/**
* Perform a case-insensitive match.
*
* @default false
*/
nocase?: boolean | undefined;
/**
* When a match is not found by `minimatch.match`,
* return a list containing the pattern itself if this option is set.
* Otherwise, an empty list is returned if there are no matches.
*
* @default false
*/
nonull?: boolean | undefined;
/**
* If set, then patterns without slashes will be matched
* against the basename of the path if it contains slashes. For example,
* `a?b` would match the path `/xyz/123/acb`, but not `/xyz/acb/123`.
*
* @default false
*/
matchBase?: boolean | undefined;
/**
* Suppress the behavior of treating `#` at the start of a pattern as a comment.
*
* @default false
*/
nocomment?: boolean | undefined;
/**
* Suppress the behavior of treating a leading `!` character as negation.
*
* @default false
*/
nonegate?: boolean | undefined;
/**
* Returns from negate expressions the same as if they were not negated.
* (Ie, true on a hit, false on a miss.)
*
* @default false
*/
flipNegate?: boolean | undefined;
/**
* Compare a partial path to a pattern. As long as the parts of the path that
* are present are not contradicted by the pattern, it will be treated as a
* match. This is useful in applications where you're walking through a
* folder structure, and don't yet have the full path, but want to ensure that
* you do not walk down paths that can never be a match.
*
* @default false
*
* @example
* import minimatch = require("minimatch");
*
* minimatch('/a/b', '/a/*' + '/c/d', { partial: true }) // true, might be /a/b/c/d
* minimatch('/a/b', '/**' + '/d', { partial: true }) // true, might be /a/b/.../d
* minimatch('/x/y/z', '/a/**' + '/z', { partial: true }) // false, because x !== a
*/
partial?: boolean;
/**
* Use `\\` as a path separator _only_, and _never_ as an escape
* character. If set, all `\\` characters are replaced with `/` in
* the pattern. Note that this makes it **impossible** to match
* against paths containing literal glob pattern characters, but
* allows matching with patterns constructed using `path.join()` and
* `path.resolve()` on Windows platforms, mimicking the (buggy!)
* behavior of earlier versions on Windows. Please use with
* caution, and be mindful of the caveat about Windows paths
*
* For legacy reasons, this is also set if
* `options.allowWindowsEscape` is set to the exact value `false`.
*
* @default false
*/
windowsPathsNoEscape?: boolean;
}
import fs = require("fs");
interface IGlobOptions extends IMinimatchOptions {
cwd?: string | undefined;
root?: string | undefined;
dot?: boolean | undefined;
nomount?: boolean | undefined;
mark?: boolean | undefined;
nosort?: boolean | undefined;
stat?: boolean | undefined;
silent?: boolean | undefined;
strict?: boolean | undefined;
cache?:
| { [path: string]: boolean | "DIR" | "FILE" | ReadonlyArray<string> }
| undefined;
statCache?:
| { [path: string]: false | { isDirectory(): boolean } | undefined }
| undefined;
symlinks?: { [path: string]: boolean | undefined } | undefined;
realpathCache?: { [path: string]: string } | undefined;
sync?: boolean | undefined;
nounique?: boolean | undefined;
nonull?: boolean | undefined;
debug?: boolean | undefined;
nobrace?: boolean | undefined;
noglobstar?: boolean | undefined;
noext?: boolean | undefined;
nocase?: boolean | undefined;
matchBase?: any;
nodir?: boolean | undefined;
ignore?: string | ReadonlyArray<string> | undefined;
follow?: boolean | undefined;
realpath?: boolean | undefined;
nonegate?: boolean | undefined;
nocomment?: boolean | undefined;
absolute?: boolean | undefined;
allowWindowsEscape?: boolean | undefined;
fs?: typeof fs;
}
export type CreateOptions = {
dot?: boolean;
globOptions?: IGlobOptions;
ordering?: string;
pattern?: string;
transform?: (filePath: