refactor: 💩 monokit

This commit is contained in:
Jack W. 2024-04-21 17:13:43 -04:00
parent f441e06a21
commit d120378e75
No known key found for this signature in database
147 changed files with 4191 additions and 3257 deletions

3
.gitignore vendored
View file

@ -3,4 +3,5 @@ node_modules
.data
out
dist
tsconfig.tsbuildinfo
tsconfig.tsbuildinfo
.svelte-kit

View file

@ -1,15 +1,13 @@
{
"maxDiscordFiles": 1000,
"maxDiscordFiles": 500,
"maxDiscordFileSize": 10485760,
"targetGuild": "906767804575928390",
"targetChannel": "1024080525993971913",
"requestTimeout": 3600000,
"targetChannel": "1160783463696302182",
"requestTimeout": 1800000,
"maxUploadIdLength": 30,
"accounts": {
"registrationEnabled": true,
"requiredForUpload": false
"requiredForUpload": true
},
"mail": {
"transport": {
"host": "smtp.fastmail.com",
@ -22,4 +20,4 @@
},
"trustProxy": true,
"forceSSL": false
}
}

View file

@ -5,12 +5,17 @@
"main": "index.js",
"type": "module",
"scripts": {
"start": "node ./out/server/index.js",
"test": "echo \"Error: no test specified\" && exit 1",
"dev": "vite",
"build": "tsc --build src/server && vite build",
"preview": "vite preview"
"dev": "vite dev",
"build": "vite build",
"preview": "vite preview",
"check": "svelte-kit sync && svelte-check --tsconfig ./jsconfig.json",
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./jsconfig.json --watch",
"lint": "prettier --check .",
"format": "prettier --write ."
},
"browserslist": [
"last 1 version"
],
"keywords": [],
"author": "Etcetera (https://cetera.uk)",
"license": "Unlicense",
@ -31,14 +36,17 @@
"dotenv": "^16.0.2",
"express": "^4.18.1",
"formidable": "^3.5.1",
"hono": "^4.0.10",
"hono": "4.0.10",
"multer": "^1.4.5-lts.1",
"node-fetch": "^3.3.2",
"nodemailer": "^6.9.3",
"typescript": "^5.2.2"
},
"devDependencies": {
"@sveltejs/vite-plugin-svelte": "^2.4.6",
"@hono/vite-dev-server": "^0.10.0",
"@sveltejs/adapter-auto": "^3.2.0",
"@sveltejs/kit": "^2.5.7",
"@sveltejs/vite-plugin-svelte": "^3.1.0",
"@tsconfig/svelte": "^4.0.1",
"@types/bytes": "^3.1.1",
"@types/cookie-parser": "^1.4.3",
@ -46,9 +54,9 @@
"@types/range-parser": "^1.2.6",
"discord-api-types": "^0.37.61",
"sass": "^1.57.1",
"svelte": "^3.55.1",
"svelte": "4",
"svelte-preprocess": "^5.1.3",
"tslib": "^2.6.2",
"vite": "^4.5.0"
"vite": "5"
}
}

View file

@ -6,8 +6,8 @@ settings:
dependencies:
'@hono/node-server':
specifier: ^1.2.0
version: 1.2.0
specifier: ^1.8.2
version: 1.8.2
'@types/body-parser':
specifier: ^1.19.2
version: 1.19.3
@ -29,24 +29,30 @@ dependencies:
bytes:
specifier: ^3.1.2
version: 3.1.2
commander:
specifier: ^11.1.0
version: 11.1.0
cookie-parser:
specifier: ^1.4.6
version: 1.4.6
discord.js:
specifier: ^14.7.1
version: 14.13.0
dotenv:
specifier: ^16.0.2
version: 16.3.1
express:
specifier: ^4.18.1
version: 4.18.2
formidable:
specifier: ^3.5.1
version: 3.5.1
hono:
specifier: ^3.8.3
version: 3.8.3
specifier: ^4.0.10
version: 4.0.10
multer:
specifier: ^1.4.5-lts.1
version: 1.4.5-lts.1
node-fetch:
specifier: ^3.3.2
version: 3.3.2
nodemailer:
specifier: ^6.9.3
version: 6.9.5
@ -55,92 +61,99 @@ dependencies:
version: 5.2.2
devDependencies:
'@hono/vite-dev-server':
specifier: ^0.10.0
version: 0.10.0(hono@4.0.10)
'@sveltejs/vite-plugin-svelte':
specifier: ^2.4.6
version: 2.4.6(svelte@3.59.2)(vite@4.5.0)
'@tsconfig/svelte':
specifier: ^4.0.1
version: 4.0.1
'@types/bytes':
specifier: ^3.1.1
version: 3.1.2
'@types/cookie-parser':
specifier: ^1.4.3
version: 1.4.4
'@types/formidable':
specifier: ^3.4.5
version: 3.4.5
'@types/range-parser':
specifier: ^1.2.6
version: 1.2.6
discord-api-types:
specifier: ^0.37.61
version: 0.37.71
sass:
specifier: ^1.57.1
version: 1.69.0
svelte:
specifier: ^3.55.1
version: 3.59.2
svelte-preprocess:
specifier: ^5.1.3
version: 5.1.3(sass@1.69.0)(svelte@3.59.2)(typescript@5.2.2)
tslib:
specifier: ^2.6.2
version: 2.6.2
vite:
specifier: ^4.5.0
version: 4.5.0(sass@1.69.0)
packages:
/@discordjs/builders@1.6.5:
resolution: {integrity: sha512-SdweyCs/+mHj+PNhGLLle7RrRFX9ZAhzynHahMCLqp5Zeq7np7XC6/mgzHc79QoVlQ1zZtOkTTiJpOZu5V8Ufg==}
engines: {node: '>=16.11.0'}
/@cloudflare/workerd-darwin-64@1.20240320.1:
resolution: {integrity: sha512-ioG5k2M17xyiAlK/k3L21NZLMVeSHMjwlmGtZyCyzSLL5/zGINcgZ5yPLV0UuWiysw07/6Jjzm5Sx94hzMVybg==}
engines: {node: '>=16'}
cpu: [x64]
os: [darwin]
requiresBuild: true
dev: true
optional: true
/@cloudflare/workerd-darwin-arm64@1.20240320.1:
resolution: {integrity: sha512-Ga6RDdnFEIsN4WuWsaP9bLGvK9K7pEIVoSIgmw6vweVlD8UK/a2MPGrsF1ogwdeCTCOMY8wUh9poL/Yu48IPpg==}
engines: {node: '>=16'}
cpu: [arm64]
os: [darwin]
requiresBuild: true
dev: true
optional: true
/@cloudflare/workerd-linux-64@1.20240320.1:
resolution: {integrity: sha512-KFof5H8eU0NXv+pUAU7Lk/OLtOmfsioTJqu0v6kPL7QsTGsgzj5sEQNcQ8DONSze549Yflu5W00qpA2cPz9eWQ==}
engines: {node: '>=16'}
cpu: [x64]
os: [linux]
requiresBuild: true
dev: true
optional: true
/@cloudflare/workerd-linux-arm64@1.20240320.1:
resolution: {integrity: sha512-t+kGc6dGdkKvVMGcHCPhlCsUZF5dj8xbAFvLB7DAJ8T79ys30rmY2Lu/C8vKlhjH9TJhbzgKmPaJ0wC/K4euvw==}
engines: {node: '>=16'}
cpu: [arm64]
os: [linux]
requiresBuild: true
dev: true
optional: true
/@cloudflare/workerd-windows-64@1.20240320.1:
resolution: {integrity: sha512-9xDylCOsuzWqGuANkuUByiJ5RHeMqgw37FiI7rn8I6zdGAc/alOB9B4Bh7B73WC2uEpFL+XCEjcHZ6NmsO4NaQ==}
engines: {node: '>=16'}
cpu: [x64]
os: [win32]
requiresBuild: true
dev: true
optional: true
/@cspotcode/source-map-support@0.8.1:
resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==}
engines: {node: '>=12'}
dependencies:
'@discordjs/formatters': 0.3.2
'@discordjs/util': 1.0.1
'@sapphire/shapeshift': 3.9.2
discord-api-types: 0.37.50
fast-deep-equal: 3.1.3
ts-mixer: 6.0.3
tslib: 2.6.2
dev: false
/@discordjs/collection@1.5.3:
resolution: {integrity: sha512-SVb428OMd3WO1paV3rm6tSjM4wC+Kecaa1EUGX7vc6/fddvw/6lg90z4QtCqm21zvVe92vMMDt9+DkIvjXImQQ==}
engines: {node: '>=16.11.0'}
dev: false
/@discordjs/formatters@0.3.2:
resolution: {integrity: sha512-lE++JZK8LSSDRM5nLjhuvWhGuKiXqu+JZ/DsOR89DVVia3z9fdCJVcHF2W/1Zxgq0re7kCzmAJlCMMX3tetKpA==}
engines: {node: '>=16.11.0'}
dependencies:
discord-api-types: 0.37.50
dev: false
/@discordjs/rest@2.0.1:
resolution: {integrity: sha512-/eWAdDRvwX/rIE2tuQUmKaxmWeHmGealttIzGzlYfI4+a7y9b6ZoMp8BG/jaohs8D8iEnCNYaZiOFLVFLQb8Zg==}
engines: {node: '>=16.11.0'}
dependencies:
'@discordjs/collection': 1.5.3
'@discordjs/util': 1.0.1
'@sapphire/async-queue': 1.5.0
'@sapphire/snowflake': 3.5.1
'@vladfrangu/async_event_emitter': 2.2.2
discord-api-types: 0.37.50
magic-bytes.js: 1.5.0
tslib: 2.6.2
undici: 5.22.1
dev: false
/@discordjs/util@1.0.1:
resolution: {integrity: sha512-d0N2yCxB8r4bn00/hvFZwM7goDcUhtViC5un4hPj73Ba4yrChLSJD8fy7Ps5jpTLg1fE9n4K0xBLc1y9WGwSsA==}
engines: {node: '>=16.11.0'}
dev: false
/@discordjs/ws@1.0.1:
resolution: {integrity: sha512-avvAolBqN3yrSvdBPcJ/0j2g42ABzrv3PEL76e3YTp2WYMGH7cuspkjfSyNWaqYl1J+669dlLp+YFMxSVQyS5g==}
engines: {node: '>=16.11.0'}
dependencies:
'@discordjs/collection': 1.5.3
'@discordjs/rest': 2.0.1
'@discordjs/util': 1.0.1
'@sapphire/async-queue': 1.5.0
'@types/ws': 8.5.6
'@vladfrangu/async_event_emitter': 2.2.2
discord-api-types: 0.37.50
tslib: 2.6.2
ws: 8.14.2
transitivePeerDependencies:
- bufferutil
- utf-8-validate
dev: false
'@jridgewell/trace-mapping': 0.3.9
dev: true
/@esbuild/android-arm64@0.18.20:
resolution: {integrity: sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==}
@ -340,32 +353,46 @@ packages:
dev: true
optional: true
/@hono/node-server@1.2.0:
resolution: {integrity: sha512-aHT8lDMLpd7ioXJ1/057+h+oE/k7rCOWmjklYDsE0jE4CoNB9XzG4f8dRHvw4s5HJFocaYDiGgYM/V0kYbQ0ww==}
engines: {node: '>=18.0.0'}
dev: false
/@fastify/busboy@2.1.1:
resolution: {integrity: sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==}
engines: {node: '>=14'}
dev: true
/@hono/node-server@1.8.2:
resolution: {integrity: sha512-h8l2TBLCPHZBUrrkosZ6L5CpBLj6zdESyF4B+zngiCDF7aZFQJ0alVbLx7jn8PCVi9EyoFf8a4hOZFi1tD95EA==}
engines: {node: '>=18.14.1'}
/@hono/vite-dev-server@0.10.0(hono@4.0.10):
resolution: {integrity: sha512-JWqdgH59x/PKDrwVCS5EW4eOL4fV+JOuzlKgaHk5eQUgE9vkPwyWwmf8f8rXjsXt5zxOKS3XMlf8sZeglFg3hw==}
engines: {node: '>=18.14.1'}
peerDependencies:
hono: '*'
dependencies:
'@hono/node-server': 1.8.2
hono: 4.0.10
miniflare: 3.20240320.1
minimatch: 9.0.4
transitivePeerDependencies:
- bufferutil
- supports-color
- utf-8-validate
dev: true
/@jridgewell/resolve-uri@3.1.2:
resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==}
engines: {node: '>=6.0.0'}
dev: true
/@jridgewell/sourcemap-codec@1.4.15:
resolution: {integrity: sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==}
dev: true
/@sapphire/async-queue@1.5.0:
resolution: {integrity: sha512-JkLdIsP8fPAdh9ZZjrbHWR/+mZj0wvKS5ICibcLrRI1j84UmLMshx5n9QmL8b95d4onJ2xxiyugTgSAX7AalmA==}
engines: {node: '>=v14.0.0', npm: '>=7.0.0'}
dev: false
/@sapphire/shapeshift@3.9.2:
resolution: {integrity: sha512-YRbCXWy969oGIdqR/wha62eX8GNHsvyYi0Rfd4rNW6tSVVa8p0ELiMEuOH/k8rgtvRoM+EMV7Csqz77YdwiDpA==}
engines: {node: '>=v14.0.0', npm: '>=7.0.0'}
/@jridgewell/trace-mapping@0.3.9:
resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==}
dependencies:
fast-deep-equal: 3.1.3
lodash: 4.17.21
dev: false
/@sapphire/snowflake@3.5.1:
resolution: {integrity: sha512-BxcYGzgEsdlG0dKAyOm0ehLGm2CafIrfQTZGWgkfKYbj+pNNsorZ7EotuZukc2MT70E0UbppVbtpBrqpzVzjNA==}
engines: {node: '>=v14.0.0', npm: '>=7.0.0'}
dev: false
'@jridgewell/resolve-uri': 3.1.2
'@jridgewell/sourcemap-codec': 1.4.15
dev: true
/@sveltejs/vite-plugin-svelte-inspector@1.0.4(@sveltejs/vite-plugin-svelte@2.4.6)(svelte@3.59.2)(vite@4.5.0):
resolution: {integrity: sha512-zjiuZ3yydBtwpF3bj0kQNV0YXe+iKE545QGZVTaylW3eAzFr+pJ/cwK8lZEaRp4JtaJXhD5DyWAV4AxLh6DgaQ==}
@ -403,6 +430,10 @@ packages:
- supports-color
dev: true
/@tsconfig/svelte@4.0.1:
resolution: {integrity: sha512-B+XlGpmuAQzJqDoBATNCvEPqQg0HkO7S8pM14QDI5NsmtymzRexQ1N+nX2H6RTtFbuFgaZD4I8AAi8voGg0GLg==}
dev: true
/@types/body-parser@1.19.3:
resolution: {integrity: sha512-oyl4jvAfTGX9Bt6Or4H9ni1Z447/tQuxnZsytsCaExKlmJiU8sFgnIBRzJUpKwB5eWn9HuBYlUlVA74q/yN0eQ==}
dependencies:
@ -440,6 +471,12 @@ packages:
'@types/qs': 6.9.8
'@types/serve-static': 1.15.3
/@types/formidable@3.4.5:
resolution: {integrity: sha512-s7YPsNVfnsng5L8sKnG/Gbb2tiwwJTY1conOkJzTMRvJAlLFW1nEua+ADsJQu8N1c0oTHx9+d5nqg10WuT9gHQ==}
dependencies:
'@types/node': 20.8.3
dev: true
/@types/http-errors@2.0.2:
resolution: {integrity: sha512-lPG6KlZs88gef6aD85z3HNkztpj7w2R7HmR3gygjfXCQmsLloWNARFkMuzKiiY8FGdh1XDpgBdrSf4aKDiA7Kg==}
@ -464,6 +501,10 @@ packages:
'@types/node': 20.8.3
dev: false
/@types/pug@2.0.10:
resolution: {integrity: sha512-Sk/uYFOBAB7mb74XcpizmH0KOR2Pv3D2Hmrh1Dmy5BmK3MpdSa5kqZcg6EKBdklU0bFXX9gCfzvpnyUehrPIuA==}
dev: true
/@types/qs@6.9.8:
resolution: {integrity: sha512-u95svzDlTysU5xecFNTgfFG5RUWu1A9P0VzgpcIiGZA9iraHOdSzcxMxQ55DyeRaGCSxQi7LxXDI4rzq/MYfdg==}
@ -483,17 +524,6 @@ packages:
'@types/mime': 3.0.2
'@types/node': 20.8.3
/@types/ws@8.5.6:
resolution: {integrity: sha512-8B5EO9jLVCy+B58PLHvLDuOD8DRVMgQzq8d55SjLCOn9kqGyqOvy27exVaTio1q1nX5zLu8/6N0n2ThSxOM6tg==}
dependencies:
'@types/node': 20.8.3
dev: false
/@vladfrangu/async_event_emitter@2.2.2:
resolution: {integrity: sha512-HIzRG7sy88UZjBJamssEczH5q7t5+axva19UbZLO6u0ySbYPrwzWiXBcC0WuHyhKKoeCyneH+FvYzKQq/zTtkQ==}
engines: {node: '>=v14.0.0', npm: '>=7.0.0'}
dev: false
/accepts@1.3.8:
resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==}
engines: {node: '>= 0.6'}
@ -502,6 +532,17 @@ packages:
negotiator: 0.6.3
dev: false
/acorn-walk@8.3.2:
resolution: {integrity: sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A==}
engines: {node: '>=0.4.0'}
dev: true
/acorn@8.11.3:
resolution: {integrity: sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==}
engines: {node: '>=0.4.0'}
hasBin: true
dev: true
/anymatch@3.1.3:
resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==}
engines: {node: '>= 8'}
@ -518,6 +559,16 @@ packages:
resolution: {integrity: sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==}
dev: false
/as-table@1.0.55:
resolution: {integrity: sha512-xvsWESUJn0JN421Xb9MQw6AsMHRCUknCe0Wjlxvjud80mU4E6hQf1A6NzQKcYNmYw62MfzEtXc+badstZP3JpQ==}
dependencies:
printable-characters: 1.0.42
dev: true
/asap@2.0.6:
resolution: {integrity: sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==}
dev: false
/asynckit@0.4.0:
resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==}
dev: false
@ -531,6 +582,10 @@ packages:
- debug
dev: false
/balanced-match@1.0.2:
resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
dev: true
/binary-extensions@2.2.0:
resolution: {integrity: sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==}
engines: {node: '>=8'}
@ -576,6 +631,19 @@ packages:
- supports-color
dev: false
/brace-expansion@1.1.11:
resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==}
dependencies:
balanced-match: 1.0.2
concat-map: 0.0.1
dev: true
/brace-expansion@2.0.1:
resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==}
dependencies:
balanced-match: 1.0.2
dev: true
/braces@3.0.2:
resolution: {integrity: sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==}
engines: {node: '>=8'}
@ -583,6 +651,10 @@ packages:
fill-range: 7.0.1
dev: true
/buffer-crc32@0.2.13:
resolution: {integrity: sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==}
dev: true
/buffer-from@1.1.2:
resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==}
dev: false
@ -606,6 +678,15 @@ packages:
get-intrinsic: 1.2.1
dev: false
/capnp-ts@0.7.0:
resolution: {integrity: sha512-XKxXAC3HVPv7r674zP0VC3RTXz+/JKhfyw94ljvF80yynK6VkTnqE3jMuN8b3dUVmmc43TjyxjW4KTsmB3c86g==}
dependencies:
debug: 4.3.4
tslib: 2.6.2
transitivePeerDependencies:
- supports-color
dev: true
/chokidar@3.5.3:
resolution: {integrity: sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==}
engines: {node: '>= 8.10.0'}
@ -628,6 +709,15 @@ packages:
delayed-stream: 1.0.0
dev: false
/commander@11.1.0:
resolution: {integrity: sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==}
engines: {node: '>=16'}
dev: false
/concat-map@0.0.1:
resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==}
dev: true
/concat-stream@1.6.2:
resolution: {integrity: sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==}
engines: {'0': node >= 0.8}
@ -670,12 +760,20 @@ packages:
/cookie@0.5.0:
resolution: {integrity: sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==}
engines: {node: '>= 0.6'}
dev: false
/core-util-is@1.0.3:
resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==}
dev: false
/data-uri-to-buffer@2.0.2:
resolution: {integrity: sha512-ND9qDTLc6diwj+Xe5cdAgVTbLVdXbtxTJRXRhli8Mowuaan+0EJOtdqJ0QCHNSSPyoXGx9HX2/VMnKeC34AChA==}
dev: true
/data-uri-to-buffer@4.0.1:
resolution: {integrity: sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==}
engines: {node: '>= 12'}
dev: false
/debug@2.6.9:
resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==}
peerDependencies:
@ -719,32 +817,21 @@ packages:
engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16}
dev: false
/discord-api-types@0.37.50:
resolution: {integrity: sha512-X4CDiMnDbA3s3RaUXWXmgAIbY1uxab3fqe3qwzg5XutR3wjqi7M3IkgQbsIBzpqBN2YWr/Qdv7JrFRqSgb4TFg==}
/detect-indent@6.1.0:
resolution: {integrity: sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA==}
engines: {node: '>=8'}
dev: true
/dezalgo@1.0.4:
resolution: {integrity: sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==}
dependencies:
asap: 2.0.6
wrappy: 1.0.2
dev: false
/discord.js@14.13.0:
resolution: {integrity: sha512-Kufdvg7fpyTEwANGy9x7i4od4yu5c6gVddGi5CKm4Y5a6sF0VBODObI3o0Bh7TGCj0LfNT8Qp8z04wnLFzgnbA==}
engines: {node: '>=16.11.0'}
dependencies:
'@discordjs/builders': 1.6.5
'@discordjs/collection': 1.5.3
'@discordjs/formatters': 0.3.2
'@discordjs/rest': 2.0.1
'@discordjs/util': 1.0.1
'@discordjs/ws': 1.0.1
'@sapphire/snowflake': 3.5.1
'@types/ws': 8.5.6
discord-api-types: 0.37.50
fast-deep-equal: 3.1.3
lodash.snakecase: 4.1.1
tslib: 2.6.2
undici: 5.22.1
ws: 8.14.2
transitivePeerDependencies:
- bufferutil
- utf-8-validate
dev: false
/discord-api-types@0.37.71:
resolution: {integrity: sha512-oYDVWoiQdblr9DpwOgpi5d78dVhPcoN9YZCCqYZf2T0v9+iICs7k2bYGumoHuYMtaIitpp5aQNs+2guVkgjbOA==}
dev: true
/dotenv@16.3.1:
resolution: {integrity: sha512-IPzF4w4/Rd94bA9imS68tZBaYyBWSCE47V1RGuMrB94iyTOIEwRmVL2x/4An+6mETpLrKJ5hQkB8W4kFAadeIQ==}
@ -760,6 +847,10 @@ packages:
engines: {node: '>= 0.8'}
dev: false
/es6-promise@3.3.1:
resolution: {integrity: sha512-SOp9Phqvqn7jtEUxPWdWfWoLmyt2VaJ6MpvP9Comy1MceMXqE6bxvaTu4iaxpYYPzhny28Lc+M87/c2cPK6lDg==}
dev: true
/esbuild@0.18.20:
resolution: {integrity: sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==}
engines: {node: '>=12'}
@ -799,6 +890,11 @@ packages:
engines: {node: '>= 0.6'}
dev: false
/exit-hook@2.2.1:
resolution: {integrity: sha512-eNTPlAD67BmP31LDINZ3U7HSF8l57TxOY2PmBJ1shpCvpnxBF93mWCE8YHBnXs8qiUZJc9WDcWIeC3a2HIAMfw==}
engines: {node: '>=6'}
dev: true
/express@4.18.2:
resolution: {integrity: sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==}
engines: {node: '>= 0.10.0'}
@ -838,8 +934,12 @@ packages:
- supports-color
dev: false
/fast-deep-equal@3.1.3:
resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==}
/fetch-blob@3.2.0:
resolution: {integrity: sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==}
engines: {node: ^12.20 || >= 14.13}
dependencies:
node-domexception: 1.0.0
web-streams-polyfill: 3.3.3
dev: false
/fill-range@7.0.1:
@ -883,6 +983,21 @@ packages:
mime-types: 2.1.35
dev: false
/formdata-polyfill@4.0.10:
resolution: {integrity: sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==}
engines: {node: '>=12.20.0'}
dependencies:
fetch-blob: 3.2.0
dev: false
/formidable@3.5.1:
resolution: {integrity: sha512-WJWKelbRHN41m5dumb0/k8TeAx7Id/y3a+Z7QfhxP/htI9Js5zYaEDtG8uMgG0vM0lOlqnmjE99/kfpOYi/0Og==}
dependencies:
dezalgo: 1.0.4
hexoid: 1.0.0
once: 1.4.0
dev: false
/forwarded@0.2.0:
resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==}
engines: {node: '>= 0.6'}
@ -893,6 +1008,10 @@ packages:
engines: {node: '>= 0.6'}
dev: false
/fs.realpath@1.0.0:
resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==}
dev: true
/fsevents@2.3.3:
resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
@ -914,6 +1033,13 @@ packages:
has-symbols: 1.0.3
dev: false
/get-source@2.0.12:
resolution: {integrity: sha512-X5+4+iD+HoSeEED+uwrQ07BOQr0kEDFMVqqpBuI+RaZBpBpHCuXxo70bjar6f0b0u/DQJsJ7ssurpP0V60Az+w==}
dependencies:
data-uri-to-buffer: 2.0.2
source-map: 0.6.1
dev: true
/glob-parent@5.1.2:
resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==}
engines: {node: '>= 6'}
@ -921,6 +1047,25 @@ packages:
is-glob: 4.0.3
dev: true
/glob-to-regexp@0.4.1:
resolution: {integrity: sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==}
dev: true
/glob@7.2.3:
resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==}
dependencies:
fs.realpath: 1.0.0
inflight: 1.0.6
inherits: 2.0.4
minimatch: 3.1.2
once: 1.4.0
path-is-absolute: 1.0.1
dev: true
/graceful-fs@4.2.11:
resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==}
dev: true
/has-proto@1.0.1:
resolution: {integrity: sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==}
engines: {node: '>= 0.4'}
@ -936,11 +1081,15 @@ packages:
engines: {node: '>= 0.4.0'}
dev: false
/hono@3.8.3:
resolution: {integrity: sha512-NLJgUCKKMvijBy+V+U1FQTsNwHk2bD1KGlWJA9+qaCNWgx5St9bhfQwxrpcTGvG2Gi2naemTWCzBavDNXOqO6Q==}
engines: {node: '>=16.0.0'}
/hexoid@1.0.0:
resolution: {integrity: sha512-QFLV0taWQOZtvIRIAdBChesmogZrtuXvVWsFHZTk2SU+anspqZ2vMnoLg7IE1+Uk16N19APic1BuF8bC8c2m5g==}
engines: {node: '>=8'}
dev: false
/hono@4.0.10:
resolution: {integrity: sha512-sq0RFAC3Ij+bkhZu90EGAQnVI1EhohRsjo9BU+BjXLbC71GSy41JjsFqCeg8MRpO2Gdu0A4MXF5licO89tn/rw==}
engines: {node: '>=16.0.0'}
/http-errors@2.0.0:
resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==}
engines: {node: '>= 0.8'}
@ -963,9 +1112,15 @@ packages:
resolution: {integrity: sha512-fsXeu4J4i6WNWSikpI88v/PcVflZz+6kMhUfIwc5SY+poQRPnaf5V7qds6SUyUN3cVxEzuCab7QIoLOQ+DQ1wA==}
dev: true
/inflight@1.0.6:
resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==}
dependencies:
once: 1.4.0
wrappy: 1.0.2
dev: true
/inherits@2.0.4:
resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==}
dev: false
/ipaddr.js@1.9.1:
resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==}
@ -1005,18 +1160,6 @@ packages:
engines: {node: '>=6'}
dev: true
/lodash.snakecase@4.1.1:
resolution: {integrity: sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw==}
dev: false
/lodash@4.17.21:
resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==}
dev: false
/magic-bytes.js@1.5.0:
resolution: {integrity: sha512-wJkXvutRbNWcc37tt5j1HyOK1nosspdh3dj6LUYYAvF6JYNqs53IfRvK9oEpcwiDA1NdoIi64yAMfdivPeVAyw==}
dev: false
/magic-string@0.30.5:
resolution: {integrity: sha512-7xlpfBaQaP/T6Vh8MO/EqXSW5En6INHEvEXQiuff7Gku0PWjU3uf6w/j9o7O+SpB5fOAkrI5HeoNgwjEO0pFsA==}
engines: {node: '>=12'}
@ -1056,16 +1199,55 @@ packages:
hasBin: true
dev: false
/min-indent@1.0.1:
resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==}
engines: {node: '>=4'}
dev: true
/miniflare@3.20240320.1:
resolution: {integrity: sha512-MoHhT+XaFPQtplNIkJc5NtWOi5u/7VkmBUWyyxDH7ehHk4xRT2PDkMCvVOUIcaqbHNIBzigyoYegdYmZcYtdCg==}
engines: {node: '>=16.13'}
hasBin: true
dependencies:
'@cspotcode/source-map-support': 0.8.1
acorn: 8.11.3
acorn-walk: 8.3.2
capnp-ts: 0.7.0
exit-hook: 2.2.1
glob-to-regexp: 0.4.1
stoppable: 1.1.0
undici: 5.28.3
workerd: 1.20240320.1
ws: 8.16.0
youch: 3.3.3
zod: 3.22.4
transitivePeerDependencies:
- bufferutil
- supports-color
- utf-8-validate
dev: true
/minimatch@3.1.2:
resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==}
dependencies:
brace-expansion: 1.1.11
dev: true
/minimatch@9.0.4:
resolution: {integrity: sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==}
engines: {node: '>=16 || 14 >=14.17'}
dependencies:
brace-expansion: 2.0.1
dev: true
/minimist@1.2.8:
resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==}
dev: false
/mkdirp@0.5.6:
resolution: {integrity: sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==}
hasBin: true
dependencies:
minimist: 1.2.8
dev: false
/ms@2.0.0:
resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==}
@ -1092,6 +1274,11 @@ packages:
xtend: 4.0.2
dev: false
/mustache@4.2.0:
resolution: {integrity: sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==}
hasBin: true
dev: true
/nanoid@3.3.6:
resolution: {integrity: sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==}
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
@ -1103,6 +1290,20 @@ packages:
engines: {node: '>= 0.6'}
dev: false
/node-domexception@1.0.0:
resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==}
engines: {node: '>=10.5.0'}
dev: false
/node-fetch@3.3.2:
resolution: {integrity: sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==}
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
dependencies:
data-uri-to-buffer: 4.0.1
fetch-blob: 3.2.0
formdata-polyfill: 4.0.10
dev: false
/nodemailer@6.9.5:
resolution: {integrity: sha512-/dmdWo62XjumuLc5+AYQZeiRj+PRR8y8qKtFCOyuOl1k/hckZd8durUUHs/ucKx6/8kN+wFxqKJlQ/LK/qR5FA==}
engines: {node: '>=6.0.0'}
@ -1129,11 +1330,21 @@ packages:
ee-first: 1.1.1
dev: false
/once@1.4.0:
resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==}
dependencies:
wrappy: 1.0.2
/parseurl@1.3.3:
resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==}
engines: {node: '>= 0.8'}
dev: false
/path-is-absolute@1.0.1:
resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==}
engines: {node: '>=0.10.0'}
dev: true
/path-to-regexp@0.1.7:
resolution: {integrity: sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==}
dev: false
@ -1156,6 +1367,10 @@ packages:
source-map-js: 1.0.2
dev: true
/printable-characters@1.0.42:
resolution: {integrity: sha512-dKp+C4iXWK4vVYZmYSd0KBH5F/h1HoZRsbJ82AVKRO3PEo8L4lBS/vLwhVtpwwuYcoIsVY+1JYKR268yn480uQ==}
dev: true
/process-nextick-args@2.0.1:
resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==}
dev: false
@ -1219,6 +1434,13 @@ packages:
picomatch: 2.3.1
dev: true
/rimraf@2.7.1:
resolution: {integrity: sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==}
hasBin: true
dependencies:
glob: 7.2.3
dev: true
/rollup@3.29.4:
resolution: {integrity: sha512-oWzmBZwvYrU0iJHtDmhsm662rC15FRXmcjCk1xD771dFDx5jJ02ufAQQTn0etB2emNk4J9EZg/yWKpsn9BWGRw==}
engines: {node: '>=14.18.0', npm: '>=8.0.0'}
@ -1239,6 +1461,15 @@ packages:
resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==}
dev: false
/sander@0.5.1:
resolution: {integrity: sha512-3lVqBir7WuKDHGrKRDn/1Ye3kwpXaDOMsiRP1wd6wpZW56gJhsbp5RqQpA6JG/P+pkXizygnr1dKR8vzWaVsfA==}
dependencies:
es6-promise: 3.3.1
graceful-fs: 4.2.11
mkdirp: 0.5.6
rimraf: 2.7.1
dev: true
/sass@1.69.0:
resolution: {integrity: sha512-l3bbFpfTOGgQZCLU/gvm1lbsQ5mC/WnLz3djL2v4WCJBDrWm58PO+jgngcGRNnKUh6wSsdm50YaovTqskZ0xDQ==}
engines: {node: '>=14.0.0'}
@ -1294,16 +1525,43 @@ packages:
object-inspect: 1.12.3
dev: false
/sorcery@0.11.0:
resolution: {integrity: sha512-J69LQ22xrQB1cIFJhPfgtLuI6BpWRiWu1Y3vSsIwK/eAScqJxd/+CJlUuHQRdX2C9NGFamq+KqNywGgaThwfHw==}
hasBin: true
dependencies:
'@jridgewell/sourcemap-codec': 1.4.15
buffer-crc32: 0.2.13
minimist: 1.2.8
sander: 0.5.1
dev: true
/source-map-js@1.0.2:
resolution: {integrity: sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==}
engines: {node: '>=0.10.0'}
dev: true
/source-map@0.6.1:
resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==}
engines: {node: '>=0.10.0'}
dev: true
/stacktracey@2.1.8:
resolution: {integrity: sha512-Kpij9riA+UNg7TnphqjH7/CzctQ/owJGNbFkfEeve4Z4uxT5+JapVLFXcsurIfN34gnTWZNJ/f7NMG0E8JDzTw==}
dependencies:
as-table: 1.0.55
get-source: 2.0.12
dev: true
/statuses@2.0.1:
resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==}
engines: {node: '>= 0.8'}
dev: false
/stoppable@1.1.0:
resolution: {integrity: sha512-KXDYZ9dszj6bzvnEMRYvxgeTHU74QBFL54XKtP3nyMuJ81CFYtABZ3bAzL2EdFUaEwJOBOgENyFj3R7oTzDyyw==}
engines: {node: '>=4', npm: '>=6'}
dev: true
/streamsearch@1.1.0:
resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==}
engines: {node: '>=10.0.0'}
@ -1315,6 +1573,13 @@ packages:
safe-buffer: 5.1.2
dev: false
/strip-indent@3.0.0:
resolution: {integrity: sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==}
engines: {node: '>=8'}
dependencies:
min-indent: 1.0.1
dev: true
/svelte-hmr@0.15.3(svelte@3.59.2):
resolution: {integrity: sha512-41snaPswvSf8TJUhlkoJBekRrABDXDMdpNpT2tfHIv4JuhgvHqLMhEPGtaQn0BmbNSTkuz2Ed20DF2eHw0SmBQ==}
engines: {node: ^12.20 || ^14.13.1 || >= 16}
@ -1324,6 +1589,54 @@ packages:
svelte: 3.59.2
dev: true
/svelte-preprocess@5.1.3(sass@1.69.0)(svelte@3.59.2)(typescript@5.2.2):
resolution: {integrity: sha512-xxAkmxGHT+J/GourS5mVJeOXZzne1FR5ljeOUAMXUkfEhkLEllRreXpbl3dIYJlcJRfL1LO1uIAPpBpBfiqGPw==}
engines: {node: '>= 16.0.0', pnpm: ^8.0.0}
requiresBuild: true
peerDependencies:
'@babel/core': ^7.10.2
coffeescript: ^2.5.1
less: ^3.11.3 || ^4.0.0
postcss: ^7 || ^8
postcss-load-config: ^2.1.0 || ^3.0.0 || ^4.0.0 || ^5.0.0
pug: ^3.0.0
sass: ^1.26.8
stylus: ^0.55.0
sugarss: ^2.0.0 || ^3.0.0 || ^4.0.0
svelte: ^3.23.0 || ^4.0.0-next.0 || ^4.0.0 || ^5.0.0-next.0
typescript: '>=3.9.5 || ^4.0.0 || ^5.0.0'
peerDependenciesMeta:
'@babel/core':
optional: true
coffeescript:
optional: true
less:
optional: true
postcss:
optional: true
postcss-load-config:
optional: true
pug:
optional: true
sass:
optional: true
stylus:
optional: true
sugarss:
optional: true
typescript:
optional: true
dependencies:
'@types/pug': 2.0.10
detect-indent: 6.1.0
magic-string: 0.30.5
sass: 1.69.0
sorcery: 0.11.0
strip-indent: 3.0.0
svelte: 3.59.2
typescript: 5.2.2
dev: true
/svelte@3.59.2:
resolution: {integrity: sha512-vzSyuGr3eEoAtT/A6bmajosJZIUWySzY2CzB3w2pgPvnkUjGqlDnsNnA0PMO+mMAhuyMul6C2uuZzY6ELSkzyA==}
engines: {node: '>= 8'}
@ -1341,13 +1654,9 @@ packages:
engines: {node: '>=0.6'}
dev: false
/ts-mixer@6.0.3:
resolution: {integrity: sha512-k43M7uCG1AkTyxgnmI5MPwKoUvS/bRvLvUb7+Pgpdlmok8AoqmUaZxUUw8zKM5B1lqZrt41GjYgnvAi0fppqgQ==}
dev: false
/tslib@2.6.2:
resolution: {integrity: sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==}
dev: false
dev: true
/type-is@1.6.18:
resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==}
@ -1365,14 +1674,13 @@ packages:
resolution: {integrity: sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==}
engines: {node: '>=14.17'}
hasBin: true
dev: false
/undici@5.22.1:
resolution: {integrity: sha512-Ji2IJhFXZY0x/0tVBXeQwgPlLWw13GVzpsWPQ3rV50IFMMof2I55PZZxtm4P6iNq+L5znYN9nSTAq0ZyE6lSJw==}
/undici@5.28.3:
resolution: {integrity: sha512-3ItfzbrhDlINjaP0duwnNsKpDQk3acHI3gVJ1z4fmwMK31k5G9OVIAMLSIaP6w4FaGkaAkN6zaQO9LUvZ1t7VA==}
engines: {node: '>=14.0'}
dependencies:
busboy: 1.6.0
dev: false
'@fastify/busboy': 2.1.1
dev: true
/unpipe@1.0.0:
resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==}
@ -1440,8 +1748,29 @@ packages:
vite: 4.5.0(sass@1.69.0)
dev: true
/ws@8.14.2:
resolution: {integrity: sha512-wEBG1ftX4jcglPxgFCMJmZ2PLtSbJ2Peg6TmpJFTbe9GZYOQCDPdMYu/Tm0/bGZkw8paZnJY45J4K2PZrLYq8g==}
/web-streams-polyfill@3.3.3:
resolution: {integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==}
engines: {node: '>= 8'}
dev: false
/workerd@1.20240320.1:
resolution: {integrity: sha512-nuavAGGjh0qqM6RF5zxTHyUwEqdLCHchodbrpbh/xlJpFGnJVY5C1YgSi2S9aLkJJoa0/25Ta/+EzXEbApA/3w==}
engines: {node: '>=16'}
hasBin: true
requiresBuild: true
optionalDependencies:
'@cloudflare/workerd-darwin-64': 1.20240320.1
'@cloudflare/workerd-darwin-arm64': 1.20240320.1
'@cloudflare/workerd-linux-64': 1.20240320.1
'@cloudflare/workerd-linux-arm64': 1.20240320.1
'@cloudflare/workerd-windows-64': 1.20240320.1
dev: true
/wrappy@1.0.2:
resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==}
/ws@8.16.0:
resolution: {integrity: sha512-HS0c//TP7Ina87TfiPUz1rQzMhHrl/SG2guqRcTOIUYD2q8uhUdNHZYJUaQ8aTGPzCh+c6oawMKW35nFl1dxyQ==}
engines: {node: '>=10.0.0'}
peerDependencies:
bufferutil: ^4.0.1
@ -1451,9 +1780,21 @@ packages:
optional: true
utf-8-validate:
optional: true
dev: false
dev: true
/xtend@4.0.2:
resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==}
engines: {node: '>=0.4'}
dev: false
/youch@3.3.3:
resolution: {integrity: sha512-qSFXUk3UZBLfggAW3dJKg0BMblG5biqSF8M34E06o5CSsZtH92u9Hqmj2RzGiHDi64fhe83+4tENFP2DB6t6ZA==}
dependencies:
cookie: 0.5.0
mustache: 4.2.0
stacktracey: 2.1.8
dev: true
/zod@3.22.4:
resolution: {integrity: sha512-iC+8Io04lddc+mVqQ9AZ7OQ2MrUKGN+oIQyq1vemgt46jwCwLfhq7/pwnBnNXXXZb8VTVLKwp9EDkx+ryxIWmg==}
dev: true

View file

@ -1,15 +1,15 @@
import { Hono } from "hono"
import * as Accounts from "../../../lib/accounts.js"
import * as auth from "../../../lib/auth.js"
import * as Accounts from "../../lib/accounts.js"
import * as auth from "../../lib/auth.js"
import { writeFile } from "fs/promises"
import { sendMail } from "../../../lib/mail.js"
import { sendMail } from "../../lib/mail.js"
import {
getAccount,
requiresAccount,
requiresAdmin,
requiresPermissions,
} from "../../../lib/middleware.js"
import Files from "../../../lib/files.js"
} from "../../lib/middleware.js"
import Files from "../../lib/files.js"
export let adminRoutes = new Hono<{
Variables: {

View file

@ -1,24 +1,24 @@
import { Hono, Handler } from "hono"
import { getCookie, setCookie } from "hono/cookie"
import * as Accounts from "../../../lib/accounts.js"
import * as auth from "../../../lib/auth.js"
import { sendMail } from "../../../lib/mail.js"
import * as Accounts from "../../lib/accounts.js"
import * as auth from "../../lib/auth.js"
import { sendMail } from "../../lib/mail.js"
import {
getAccount,
noAPIAccess,
requiresAccount,
requiresPermissions,
} from "../../../lib/middleware.js"
import { accountRatelimit } from "../../../lib/ratelimit.js"
} from "../../lib/middleware.js"
import { accountRatelimit } from "../../lib/ratelimit.js"
import ServeError from "../../../lib/errors.js"
import ServeError from "../../lib/errors.js"
import Files, {
FileVisibility,
generateFileId,
id_check_regex,
} from "../../../lib/files.js"
} from "../../lib/files.js"
import { writeFile } from "fs/promises"
import { writeFile, readFile } from "fs/promises"
export let authRoutes = new Hono<{
Variables: {
@ -26,50 +26,16 @@ export let authRoutes = new Hono<{
}
}>()
import config from "../../../../../config.json" assert {type:"json"}
// We need to import config.json at run time because split is a genius. Ideally we would use .env files for this
const config = JSON.parse(
// Don't use __dirname. For all we know this fyle could be a hashed chunk.
// Also if this were NixOS you can't modify the built folder after build
await readFile(process.cwd() + "/config.json", "utf-8")
)
authRoutes.all("*", getAccount)
export default function (files: Files) {
authRoutes.post("/login", async (ctx) => {
const body = await ctx.req.json()
if (
typeof body.username != "string" ||
typeof body.password != "string"
) {
return ServeError(ctx, 400, "please provide a username or password")
}
if (auth.validate(getCookie(ctx, "auth")!))
return ctx.text("You are already authed")
/*
check if account exists
*/
let acc = Accounts.getFromUsername(body.username)
if (!acc) {
return ServeError(ctx, 401, "username or password incorrect")
}
if (!Accounts.password.check(acc.id, body.password)) {
return ServeError(ctx, 401, "username or password incorrect")
}
/*
assign token
*/
setCookie(ctx, "auth", auth.create(acc.id, 3 * 24 * 60 * 60 * 1000), {
path: "/",
sameSite: "Strict",
secure: true,
httpOnly: true,
maxAge: 3 * 24 * 60 * 60 * 1000,
})
return ctx.text("")
})
authRoutes.post("/create", async (ctx) => {
if (!config.accounts.registrationEnabled) {
return ServeError(ctx, 403, "account registration disabled")

View file

@ -1,12 +1,12 @@
import { Hono } from "hono"
import * as Accounts from "../../../lib/accounts.js"
import * as Accounts from "../../lib/accounts.js"
import { writeFile } from "fs/promises"
import Files from "../../../lib/files.js"
import Files from "../../lib/files.js"
import {
getAccount,
requiresAccount,
requiresPermissions,
} from "../../../lib/middleware.js"
} from "../../lib/middleware.js"
export let fileApiRoutes = new Hono<{
Variables: {

15
src/api/v0/index.ts Normal file
View file

@ -0,0 +1,15 @@
import { Hono } from "hono"
import type Files from "../../lib/files.js"
import primaryApi from "./primaryApi.js"
import adminRoutes from "./adminRoutes.js"
import authRoutes from "./authRoutes.js"
import fileApiRoutes from "./fileApiRoutes.js"
export default function install(root: Hono, files: Files) {
const router = new Hono()
router.route("/", primaryApi(files, root))
router.route("/admin", adminRoutes(files))
router.route("/auth", authRoutes(files))
router.route("/files", fileApiRoutes(files))
root.route("/", router)
}

47
src/api/v0/primaryApi.ts Normal file
View file

@ -0,0 +1,47 @@
import { Hono } from "hono"
import * as Accounts from "../../lib/accounts.js"
import * as auth from "../../lib/auth.js"
import RangeParser, { type Range } from "range-parser"
import ServeError from "../../lib/errors.js"
import Files, { WebError } from "../../lib/files.js"
import { getAccount, requiresPermissions } from "../../lib/middleware.js"
import { Readable } from "node:stream"
import type { ReadableStream as StreamWebReadable } from "node:stream/web"
import formidable from "formidable"
import { HttpBindings } from "@hono/node-server"
import { type StatusCode } from "hono/utils/http-status"
export let primaryApi = new Hono<{
Variables: {
account: Accounts.Account
}
Bindings: HttpBindings
}>()
primaryApi.all("*", getAccount)
export default function (files: Files, apiRoot: Hono) {
primaryApi.get("/file/:fileId", async (ctx) =>
apiRoot.fetch(
new Request(
new URL(
`/api/v1/file/${ctx.req.param("fileId")}`,
ctx.req.raw.url
).href,
ctx.req.raw
),
ctx.env
)
)
primaryApi.post("/upload", async (ctx) =>
apiRoot.fetch(
new Request(new URL(`/api/v1/file`, ctx.req.raw.url).href, {
...ctx.req.raw,
method: "PUT",
}),
ctx.env
)
)
return primaryApi
}

462
src/api/v1/account.ts Normal file
View file

@ -0,0 +1,462 @@
// Modules
import { type Context, Hono } from "hono"
import { getCookie, setCookie } from "hono/cookie"
// Libs
import Files, { id_check_regex } from "../../lib/files.js"
import * as Accounts from "../../lib/accounts.js"
import * as auth from "../../lib/auth.js"
import {
assertAPI,
getAccount,
login,
noAPIAccess,
requiresAccount,
requiresPermissions,
} from "../../lib/middleware.js"
import ServeError from "../../lib/errors.js"
import { CodeMgr, sendMail } from "../../lib/mail.js"
import { readFile } from "fs/promises"
// We need to import config.json at run time because split is a genius. Ideally we would use .env files for this
const config = JSON.parse(
// Don't use __dirname. For all we know this fyle could be a hashed chunk.
// Also if this were NixOS you can't modify the built folder after build
await readFile(process.cwd() + "/config.json", "utf-8")
)
const router = new Hono<{
Variables: {
account: Accounts.Account
target: Accounts.Account
}
}>()
type UserUpdateParameters = Partial<
Omit<Accounts.Account, "password"> & {
password: string
currentPassword?: string
}
>
type Message = [200 | 400 | 401 | 403 | 429 | 501, string]
// there's probably a less stupid way to do this than `K in keyof Pick<UserUpdateParameters, T>`
// @Jack5079 make typings better if possible
type Validator<
T extends keyof Partial<Accounts.Account>,
ValueNotNull extends boolean
> =
/**
* @param actor The account performing this action
* @param target The target account for this action
* @param params Changes being patched in by the user
*/
(
actor: Accounts.Account,
target: Accounts.Account,
params: UserUpdateParameters &
(ValueNotNull extends true
? {
[K in keyof Pick<
UserUpdateParameters,
T
>]-?: UserUpdateParameters[K]
}
: {}),
ctx: Context
) => Accounts.Account[T] | Message
// this type is so stupid stg
type ValidatorWithSettings<T extends keyof Partial<Accounts.Account>> =
| {
acceptsNull: true
validator: Validator<T, false>
}
| {
acceptsNull?: false
validator: Validator<T, true>
}
const validators: {
[T in keyof Partial<Accounts.Account>]:
| Validator<T, true>
| ValidatorWithSettings<T>
} = {
defaultFileVisibility(actor, target, params) {
if (
["public", "private", "anonymous"].includes(
params.defaultFileVisibility
)
)
return params.defaultFileVisibility
else return [400, "invalid file visibility"]
},
email: {
acceptsNull: true,
validator: (actor, target, params, ctx) => {
if (
!params.currentPassword || // actor on purpose here to allow admins
(params.currentPassword &&
Accounts.password.check(actor.id, params.currentPassword))
)
return [401, "current password incorrect"]
if (!params.email) {
if (target.email) {
sendMail(
target.email,
`Email disconnected`,
`<b>Hello there!</b> Your email address (<span code>${target.email}</span>) has been disconnected from the monofile account <span username>${target.username}</span>. Thank you for using monofile.`
).catch()
}
return undefined
}
if (typeof params.email !== "string")
return [400, "email must be string"]
if (actor.admin) return params.email
// send verification email
if (
(CodeMgr.codes.verifyEmail.byUser.get(target.id)?.length ||
0) >= 2
)
return [429, "you have too many active codes"]
let code = new CodeMgr.Code("verifyEmail", target.id, params.email)
sendMail(
params.email,
`Hey there, ${target.username} - let's connect your email`,
`<b>Hello there!</b> You are recieving this message because you decided to link your email, <span code>${
params.email.split("@")[0]
}<span style="opacity:0.5">@${
params.email.split("@")[1]
}</span></span>, to your account, <span username>${
target.username
}</span>. If you would like to continue, please <a href="https://${ctx.req.header(
"Host"
)}/go/verify/${
code.id
}"><span code>click here</span></a>, or go to https://${ctx.req.header(
"Host"
)}/go/verify/${code.id}.`
)
return [200, "please check your inbox"]
},
},
password(actor, target, params) {
if (
!params.currentPassword || // actor on purpose here to allow admins
(params.currentPassword &&
Accounts.password.check(actor.id, params.currentPassword))
)
return [401, "current password incorrect"]
if (typeof params.password != "string" || params.password.length < 8)
return [400, "password must be 8 characters or longer"]
if (target.email) {
sendMail(
target.email,
`Your login details have been updated`,
`<b>Hello there!</b> Your password on your account, <span username>${target.username}</span>, has been updated` +
`${
actor != target
? ` by <span username>${actor.username}</span>`
: ""
}. ` +
`Please update your saved login details accordingly.`
).catch()
}
return Accounts.password.hash(params.password)
},
username(actor, target, params) {
if (
!params.currentPassword || // actor on purpose here to allow admins
(params.currentPassword &&
Accounts.password.check(actor.id, params.currentPassword))
)
return [401, "current password incorrect"]
if (
typeof params.username != "string" ||
params.username.length < 3 ||
params.username.length > 20
)
return [
400,
"username must be between 3 and 20 characters in length",
]
if (Accounts.getFromUsername(params.username))
return [400, "account with this username already exists"]
if (
(params.username.match(/[A-Za-z0-9_\-\.]+/) || [])[0] !=
params.username
)
return [400, "username has invalid characters"]
if (target.email) {
sendMail(
target.email,
`Your login details have been updated`,
`<b>Hello there!</b> Your username on your account, <span username>${target.username}</span>, has been updated` +
`${
actor != target
? ` by <span username>${actor.username}</span>`
: ""
} to <span username>${params.username}</span>. ` +
`Please update your saved login details accordingly.`
).catch()
}
return params.username
},
customCSS: {
acceptsNull: true,
validator: (actor, target, params) => {
if (
!params.customCSS ||
(params.customCSS.match(id_check_regex)?.[0] ==
params.customCSS &&
params.customCSS.length <= config.maxUploadIdLength)
)
return params.customCSS
else return [400, "bad file id"]
},
},
embed(actor, target, params) {
if (typeof params.embed !== "object")
return [400, "must use an object for embed"]
if (params.embed.color === undefined) {
params.embed.color = target.embed?.color
} else if (
!(
(params.embed.color.toLowerCase().match(/[a-f0-9]+/)?.[0] ==
params.embed.color.toLowerCase() &&
params.embed.color.length == 6) ||
params.embed.color == null
)
)
return [400, "bad embed color"]
if (params.embed.largeImage === undefined) {
params.embed.largeImage = target.embed?.largeImage
} else params.embed.largeImage = Boolean(params.embed.largeImage)
return params.embed
},
admin(actor, target, params) {
if (actor.admin && !target.admin) return params.admin
else if (!actor.admin) return [400, "cannot promote yourself"]
else return [400, "cannot demote an admin"]
},
}
router.use(getAccount)
router.all("/:user", async (ctx, next) => {
let acc =
ctx.req.param("user") == "me"
? ctx.get("account")
: ctx.req.param("user").startsWith("@")
? Accounts.getFromUsername(ctx.req.param("user").slice(1))
: Accounts.getFromId(ctx.req.param("user"))
if (acc != ctx.get("account") && !ctx.get("account")?.admin)
return ServeError(ctx, 403, "you cannot manage this user")
if (!acc) return ServeError(ctx, 404, "account does not exist")
ctx.set("target", acc)
return next()
})
function isMessage(object: any): object is Message {
return (
Array.isArray(object) &&
object.length == 2 &&
typeof object[0] == "number" &&
typeof object[1] == "string"
)
}
export default function (files: Files) {
router.post("/", async (ctx) => {
const body = await ctx.req.json()
if (!config.accounts.registrationEnabled) {
return ServeError(ctx, 403, "account registration disabled")
}
if (auth.validate(getCookie(ctx, "auth")!)) {
return ServeError(ctx, 400, "you are already logged in")
}
if (Accounts.getFromUsername(body.username)) {
return ServeError(
ctx,
400,
"account with this username already exists"
)
}
if (body.username.length < 3 || body.username.length > 20) {
return ServeError(
ctx,
400,
"username must be over or equal to 3 characters or under or equal to 20 characters in length"
)
}
if (
(body.username.match(/[A-Za-z0-9_\-\.]+/) || [])[0] != body.username
) {
return ServeError(ctx, 400, "username contains invalid characters")
}
if (body.password.length < 8) {
return ServeError(
ctx,
400,
"password must be 8 characters or longer"
)
}
return Accounts.create(body.username, body.password)
.then((account) => {
login(ctx, account)
return ctx.text("logged in")
})
.catch(() => {
return ServeError(ctx, 500, "internal server error")
})
})
router.patch(
"/:user",
requiresAccount,
requiresPermissions("manage"),
async (ctx) => {
const body = (await ctx.req.json()) as UserUpdateParameters
const actor = ctx.get("account")!
const target = ctx.get("target")!
if (Array.isArray(body)) return ServeError(ctx, 400, "invalid body")
let results: (
| [
keyof Accounts.Account,
Accounts.Account[keyof Accounts.Account]
]
| Message
)[] = (
Object.entries(body).filter(
(e) => e[0] !== "currentPassword"
) as [
keyof Accounts.Account,
UserUpdateParameters[keyof Accounts.Account]
][]
).map(([x, v]) => {
if (!validators[x])
return [
400,
`the ${x} parameter cannot be set or is not a valid parameter`,
] as Message
let validator = (
typeof validators[x] == "object"
? validators[x]
: {
validator: validators[x] as Validator<
typeof x,
false
>,
acceptsNull: false,
}
) as ValidatorWithSettings<typeof x>
if (!validator.acceptsNull && !v)
return [
400,
`the ${x} validator does not accept null values`,
] as Message
return [
x,
validator.validator(actor, target, body as any, ctx),
] as [
keyof Accounts.Account,
Accounts.Account[keyof Accounts.Account]
]
})
let allMsgs = results.map((v) => {
if (isMessage(v)) return v
target[v[0]] = v[1] as never // lol
return [200, "OK"] as Message
})
await Accounts.save()
if (allMsgs.length == 1)
return ctx.text(
...(allMsgs[0]!.reverse() as [Message[1], Message[0]])
) // im sorry
else return ctx.json(allMsgs)
}
)
router.delete("/:user", requiresAccount, noAPIAccess, async (ctx) => {
let acc = ctx.get("target")
auth.AuthTokens.filter((e) => e.account == acc?.id).forEach((token) => {
auth.invalidate(token.token)
})
await Accounts.deleteAccount(acc.id)
if (acc.email) {
await sendMail(
acc.email,
"Notice of account deletion",
`Your account, <span username>${acc.username}</span>, has been removed. Thank you for using monofile.`
).catch()
return ctx.text("OK")
}
return ctx.text("account deleted")
})
router.get("/:user", requiresAccount, async (ctx) => {
let acc = ctx.get("target")
let sessionToken = auth.tokenFor(ctx)!
return ctx.json({
...acc,
password: undefined,
email:
auth.getType(sessionToken) == "User" ||
auth.getPermissions(sessionToken)?.includes("email")
? acc.email
: undefined,
activeSessions: auth.AuthTokens.filter(
(e) =>
e.type != "App" &&
e.account == acc.id &&
(e.expire > Date.now() || !e.expire)
).length,
})
})
router.get("/css", async (ctx) => {
let acc = ctx.get("account")
if (acc?.customCSS) return ctx.redirect(`/file/${acc.customCSS}`)
else return ctx.text("")
})
return router
}

186
src/api/v1/file/index.ts Normal file
View file

@ -0,0 +1,186 @@
import { Hono } from "hono"
import * as Accounts from "../../../lib/accounts.js"
import * as auth from "../../../lib/auth.js"
import RangeParser, { type Range } from "range-parser"
import ServeError from "../../../lib/errors.js"
import Files, { WebError } from "../../../lib/files.js"
import { getAccount, requiresPermissions } from "../../../lib/middleware.js"
import { Readable } from "node:stream"
import type { ReadableStream as StreamWebReadable } from "node:stream/web"
import formidable from "formidable"
import { HttpBindings } from "@hono/node-server"
import { type StatusCode } from "hono/utils/http-status"
declare const MONOFILE_VERSION: string // see vite.config.js
const router = new Hono<{
Variables: {
account: Accounts.Account
}
Bindings: HttpBindings
}>()
router.all("*", getAccount)
export default function (files: Files) {
router.on(["PUT", "POST"], "/", requiresPermissions("upload"), (ctx) => {
return new Promise((resolve, reject) => {
ctx.env.incoming.removeAllListeners("data") // remove hono's buffering
let errEscalated = false
function escalate(err: Error) {
if (errEscalated) return
errEscalated = true
console.error(err)
if ("httpCode" in err) ctx.status(err.httpCode as StatusCode)
else if (err instanceof WebError)
ctx.status(err.statusCode as StatusCode)
else ctx.status(400)
resolve(ctx.body(err.message))
}
let acc = ctx.get("account") as Accounts.Account | undefined
if (
!ctx.req
.header("Content-Type")
?.startsWith("multipart/form-data")
)
return resolve(ctx.body("must be multipart/form-data", 400))
if (!ctx.req.raw.body)
return resolve(ctx.body("body must be supplied", 400))
let file = files.createWriteStream(acc?.id)
file.on("error", escalate).on("finish", async () => {
if (!ctx.env.incoming.readableEnded)
await new Promise((res) =>
ctx.env.incoming.once("end", res)
)
file.commit()
.then((id) => resolve(ctx.body(id!)))
.catch(escalate)
})
let parser = formidable({
maxFieldsSize: 65536,
maxFileSize:
files.config.maxDiscordFileSize *
files.config.maxDiscordFiles,
maxFiles: 1,
})
let acceptNewData = true
parser.onPart = function (part) {
if (!part.originalFilename || !part.mimetype) {
parser._handlePart(part)
return
}
// lol
if (part.name == "file") {
if (!acceptNewData || file.writableEnded)
return part.emit(
"error",
new WebError(
400,
"cannot set file after previously setting up another upload"
)
)
acceptNewData = false
file.setName(part.originalFilename || "")
file.setType(part.mimetype || "")
file.on("drain", () => ctx.env.incoming.resume())
file.on("error", (err) => part.emit("error", err))
part.on("data", (data: Buffer) => {
if (!file.write(data)) ctx.env.incoming.pause()
})
part.on("end", () => file.end())
}
}
parser.on("field", async (k, v) => {
if (k == "uploadId") {
if (files.files[v] && ctx.req.method == "POST")
return file.destroy(
new WebError(409, "file already exists")
)
file.setUploadId(v)
// I'M GONNA KILL MYSELF!!!!
} else if (k == "file") {
if (!acceptNewData || file.writableEnded)
return file.destroy(
new WebError(
400,
"cannot set file after previously setting up another upload"
)
)
acceptNewData = false
let res = await fetch(v, {
headers: {
"user-agent": `monofile ${
MONOFILE_VERSION
} (+https://${ctx.req.header("Host")})`,
},
}).catch(escalate)
if (!res) return
if (
!file.setName(
res.headers
.get("Content-Disposition")
?.match(/filename="(.*)"/)?.[1] ||
v.split("/")[v.split("/").length - 1] ||
"generic"
)
)
return
if (res.headers.has("Content-Type"))
if (!file.setType(res.headers.get("Content-Type")!))
return
if (!res.ok)
return file.destroy(
new WebError(
500,
`got ${res.status} ${res.statusText}`
)
)
if (!res.body)
return file.destroy(
new WebError(500, `Internal Server Error`)
)
if (
res.headers.has("Content-Length") &&
!Number.isNaN(
parseInt(res.headers.get("Content-Length")!, 10)
) &&
parseInt(res.headers.get("Content-Length")!, 10) >
files.config.maxDiscordFileSize *
files.config.maxDiscordFiles
)
return file.destroy(
new WebError(413, `file reports to be too large`)
)
Readable.fromWeb(res.body as StreamWebReadable).pipe(file)
}
})
parser.parse(ctx.env.incoming).catch((e) => console.error(e))
parser.on("error", (err) => {
escalate(err)
if (!file.destroyed) file.destroy(err)
})
})
})
return router
}

View file

@ -0,0 +1,186 @@
import { Hono } from "hono"
import * as Accounts from "../../../lib/accounts.js"
import * as auth from "../../../lib/auth.js"
import RangeParser, { type Range } from "range-parser"
import ServeError from "../../../lib/errors.js"
import Files, { WebError } from "../../../lib/files.js"
import { getAccount, requiresPermissions } from "../../../lib/middleware.js"
import { Readable } from "node:stream"
import type { ReadableStream as StreamWebReadable } from "node:stream/web"
import formidable from "formidable"
import { HttpBindings } from "@hono/node-server"
import { type StatusCode } from "hono/utils/http-status"
const router = new Hono<{
Variables: {
account: Accounts.Account
}
Bindings: HttpBindings
}>()
router.all("*", getAccount)
export default function (files: Files, apiRoot: Hono) {
router.get("/:id", async (ctx) => {
const fileId = ctx.req.param("id")
let acc = ctx.get("account") as Accounts.Account
let file = files.files[fileId]
ctx.header("Accept-Ranges", "bytes")
ctx.header("Access-Control-Allow-Origin", "*")
ctx.header("Content-Security-Policy", "sandbox allow-scripts")
if (file) {
ctx.header(
"Content-Disposition",
`${
ctx.req.query("attachment") == "1" ? "attachment" : "inline"
}; filename="${encodeURI(
file.filename.replaceAll("\n", "\\n")
)}"`
)
ctx.header("ETag", file.md5)
if (file.lastModified) {
let lm = new Date(file.lastModified)
// TERRIFYING
ctx.header(
"Last-Modified",
`${
["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"][
lm.getUTCDay()
]
}, ${lm.getUTCDate()} ` +
`${
[
"Jan",
"Feb",
"Mar",
"Apr",
"May",
"Jun",
"Jul",
"Aug",
"Sep",
"Oct",
"Nov",
"Dec",
][lm.getUTCMonth()]
}` +
` ${lm.getUTCFullYear()} ${lm
.getUTCHours()
.toString()
.padStart(2, "0")}` +
`:${lm.getUTCMinutes().toString().padStart(2, "0")}:${lm
.getUTCSeconds()
.toString()
.padStart(2, "0")} GMT`
)
}
if (file.visibility == "private") {
if (acc?.id != file.owner) {
return ServeError(ctx, 403, "you do not own this file")
}
if (
auth.getType(auth.tokenFor(ctx)!) == "App" &&
auth
.getPermissions(auth.tokenFor(ctx)!)
?.includes("private")
) {
return ServeError(ctx, 403, "insufficient permissions")
}
}
let range: Range | undefined
ctx.header("Content-Type", file.mime)
if (file.sizeInBytes) {
ctx.header("Content-Length", file.sizeInBytes.toString())
if (file.chunkSize && ctx.req.header("Range")) {
let ranges = RangeParser(
file.sizeInBytes,
ctx.req.header("Range") || ""
)
if (ranges) {
if (typeof ranges == "number")
return ServeError(
ctx,
ranges == -1 ? 416 : 400,
ranges == -1
? "unsatisfiable ranges"
: "invalid ranges"
)
if (ranges.length > 1)
return ServeError(
ctx,
400,
"multiple ranges not supported"
)
range = ranges[0]
ctx.status(206)
ctx.header(
"Content-Length",
(range.end - range.start + 1).toString()
)
ctx.header(
"Content-Range",
`bytes ${range.start}-${range.end}/${file.sizeInBytes}`
)
}
}
}
if (ctx.req.method == "HEAD") return ctx.body(null)
return files
.readFileStream(fileId, range)
.then(async (stream) => {
let rs = new ReadableStream({
start(controller) {
stream.once("end", () => controller.close())
stream.once("error", (err) => controller.error(err))
},
cancel(reason) {
stream.destroy(
reason instanceof Error
? reason
: new Error(reason)
)
},
})
stream.pipe(ctx.env.outgoing)
return new Response(rs, ctx.body(null))
})
.catch((err) => {
return ServeError(ctx, err.status, err.message)
})
} else {
return ServeError(ctx, 404, "file not found")
}
})
router.on(["PUT", "POST"], "/:id", async (ctx) => {
ctx.env.incoming.push(
`--${
ctx.req.header("content-type")?.match(/boundary=(\S+)/)?.[1]
}\r\n` +
`Content-Disposition: form-data; name="uploadId"\r\n\r\n` +
ctx.req.param("id") +
"\r\n"
)
return apiRoot.fetch(
new Request(
new URL(`/api/v1/file`, ctx.req.raw.url).href,
ctx.req.raw
),
ctx.env
)
})
return router
}

15
src/api/v1/index.ts Normal file
View file

@ -0,0 +1,15 @@
import { Hono } from "hono"
import account from "./account.js"
import session from "./session.js"
import file from "./file/index.js"
import individual from "./file/individual.js"
import type Files from "../../lib/files.js"
export default function install(root: Hono, files: Files) {
const router = new Hono()
router.route("/account", account(files))
router.route("/session", session(files))
router.route("/file", file(files)) // /file API
router.route("/file", individual(files, root)) // /file/:id
root.route("/api/v1", router)
}

View file

@ -1,20 +1,15 @@
// Modules
import { Hono } from "hono"
import { getCookie, setCookie } from "hono/cookie"
// Libs
import Files, { id_check_regex } from "../../../lib/files.js"
import * as Accounts from "../../../lib/accounts.js"
import * as auth from "../../../lib/auth.js"
import {
getAccount,
login,
requiresAccount
} from "../../../lib/middleware.js"
import ServeError from "../../../lib/errors.js"
import Files, { id_check_regex } from "../../lib/files.js"
import * as Accounts from "../../lib/accounts.js"
import * as auth from "../../lib/auth.js"
import { getAccount, login, requiresAccount } from "../../lib/middleware.js"
import ServeError from "../../lib/errors.js"
const router = new Hono<{
Variables: {
@ -51,12 +46,11 @@ export default function (files: Files) {
return ctx.text("logged in")
})
router.get("/", requiresAccount, ctx => {
router.get("/", requiresAccount, (ctx) => {
let sessionToken = auth.tokenFor(ctx)
return ctx.json({
expiry: auth.AuthTokens.find(
(e) => e.token == sessionToken
)?.expire,
expiry: auth.AuthTokens.find((e) => e.token == sessionToken)
?.expire,
})
})

View file

@ -1,12 +1,11 @@
import fs from "fs/promises"
import bytes from "bytes"
import ServeError from "../../../lib/errors.js"
import * as Accounts from "../../../lib/accounts.js"
import type Files from "../../../lib/files.js"
import pkg from "../../../../../package.json" assert {type:"json"}
import { CodeMgr } from "../../../lib/mail.js"
import ServeError from "../../lib/errors.js"
import * as Accounts from "../../lib/accounts.js"
import type Files from "../../lib/files.js"
import { CodeMgr } from "../../lib/mail.js"
import { Hono } from "hono"
import { getAccount, login } from "../../../lib/middleware.js"
import { getAccount, login } from "../../lib/middleware.js"
export let router = new Hono<{
Variables: {
account: Accounts.Account
@ -20,7 +19,11 @@ export default function (files: Files) {
if (code) {
if (currentAccount != undefined && !code.check(currentAccount.id)) {
return ServeError(ctx, 403, "you are logged in on a different account")
return ServeError(
ctx,
403,
"you are logged in on a different account"
)
}
if (!currentAccount) {
@ -32,10 +35,10 @@ export default function (files: Files) {
currentAccount.email = code.data
await Accounts.save()
return ctx.redirect('/')
return ctx.redirect("/")
} else return ServeError(ctx, 404, "code not found")
})
return router
}
}

8
src/api/web/index.ts Normal file
View file

@ -0,0 +1,8 @@
import { Hono } from "hono"
import type Files from "../../lib/files.js"
import go from "./go.js"
export default function install(root: Hono, files: Files) {
const router = new Hono()
router.route("/go", go(files))
root.route("/api/v1", router)
}

13
src/app.html Normal file
View file

@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<link rel="icon" href="%sveltekit.assets%/favicon.svg">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="/api/v1/account/me/css">
%sveltekit.head%
</head>
<body data-sveltekit-preload-data="hover">
<div style="display: contents">%sveltekit.body%</div>
</body>
</html>

View file

@ -4,8 +4,7 @@ import Files from "./lib/files.js"
import { program } from "commander"
import { basename } from "path"
import { Writable } from "node:stream"
import pkg from "../../package.json" assert { type: "json" }
import config from "../../config.json" assert { type: "json" }
import pkg from "../package.json" assert { type: "json" }
import { fileURLToPath } from "url"
import { dirname } from "path"
@ -15,6 +14,13 @@ const __dirname = dirname(fileURLToPath(import.meta.url))
if (!fs.existsSync(__dirname + "/../../.data/"))
fs.mkdirSync(__dirname + "/../../.data/")
// We need to import config.json at run time because split is a genius. Ideally we would use .env files for this
const config = JSON.parse(
// Don't use __dirname. For all we know this fyle could be a hashed chunk.
// Also if this were NixOS you can't modify the built folder after build
fs.readFileSync(process.cwd() + "/config.json", "utf-8")
)
// discord
let files = new Files(config)
@ -23,65 +29,65 @@ program
.description("Quickly run monofile to execute a query or so")
.version(pkg.version)
program.command("list")
program
.command("list")
.alias("ls")
.description("List files in the database")
.action(() => {
Object.keys(files.files).forEach(e => console.log(e))
Object.keys(files.files).forEach((e) => console.log(e))
})
program.command("download")
program
.command("download")
.alias("dl")
.description("Download a file from the database")
.argument("<id>", "ID of the file you'd like to download")
.option("-o, --output <path>", 'Folder or filename to output to')
.option("-o, --output <path>", "Folder or filename to output to")
.action(async (id, options) => {
await (new Promise<void>(resolve => setTimeout(() => resolve(), 1000)))
await new Promise<void>((resolve) => setTimeout(() => resolve(), 1000))
let fp = files.files[id]
if (!fp)
throw `file ${id} not found`
let out = options.output as string || `./`
if (!fp) throw `file ${id} not found`
let out = (options.output as string) || `./`
if (fs.existsSync(out) && (await stat(out)).isDirectory())
out = `${out.replace(/\/+$/, "")}/${fp.filename}`
let filestream = await files.readFileStream(id)
let prog=0
filestream.on("data", dt => {
prog+=dt.byteLength
console.log(`Downloading ${fp.filename}: ${Math.floor(prog/(fp.sizeInBytes??0)*10000)/100}% (${Math.floor(prog/(1024*1024))}MiB/${Math.floor((fp.sizeInBytes??0)/(1024*1024))}MiB)`)
let prog = 0
filestream.on("data", (dt) => {
prog += dt.byteLength
console.log(
`Downloading ${fp.filename}: ${
Math.floor((prog / (fp.sizeInBytes ?? 0)) * 10000) / 100
}% (${Math.floor(prog / (1024 * 1024))}MiB/${Math.floor(
(fp.sizeInBytes ?? 0) / (1024 * 1024)
)}MiB)`
)
})
filestream.pipe(
fs.createWriteStream(out)
)
filestream.pipe(fs.createWriteStream(out))
})
program.command("upload")
program
.command("upload")
.alias("up")
.description("Upload a file to the instance")
.argument("<file>", "Path to the file you'd like to upload")
.option("-id, --fileid <id>", 'Custom file ID to use')
.option("-id, --fileid <id>", "Custom file ID to use")
.action(async (file, options) => {
await (new Promise<void>(resolve => setTimeout(() => resolve(), 1000)))
await new Promise<void>((resolve) => setTimeout(() => resolve(), 1000))
if (!(fs.existsSync(file) && (await stat(file)).isFile()))
throw `${file} is not a file`
let writable = files.createWriteStream()
writable
.setName(basename(file))
?.setType("application/octet-stream")
writable.setName(basename(file))?.setType("application/octet-stream")
if (options.id) writable.setUploadId(options.id)
if (!(writable instanceof Writable))
@ -90,7 +96,7 @@ program.command("upload")
console.log(`started: ${file}`)
writable.on("drain", () => {
console.log("Drained");
console.log("Drained")
})
writable.on("finish", async () => {
@ -108,11 +114,8 @@ program.command("upload")
writable.on("close", () => {
console.log("Closed.")
});
;(await fs.createReadStream(file)).pipe(
writable
)
})
;(await fs.createReadStream(file)).pipe(writable)
})
program.parse()
program.parse()

1
src/consts.d.ts vendored Normal file
View file

@ -0,0 +1 @@
declare const MONOFILE_VERSION: string

View file

@ -1,54 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>$FileId</title>
<!--metaTags-->
<meta name="og:site_name" content="$Uploader">
<meta name="title" content="$FileName">
<meta name="description" content="$FileSize file on monofile $Version, the Discord-based file sharing service">
<link
rel="stylesheet"
href="./style/downloads.scss"
>
<link
rel="stylesheet"
href="/api/v1/account/me/css"
>
<link
rel="icon"
type="image/svg"
href="/static/assets/icons/file_icon.svg"
>
</head>
<body>
<div id="appContent">
<div id="uploadWindow">
<h1>
$FileName
</h1>
<p style="color:#999999">
<span class="number">$FileSize</span>&nbsp;&nbsp;&nbsp;&nbsp;uploaded by <span class="number">$Uploader</span>
</p>
<!--preview-->
<button style="position:relative;width:100%;top:10px;">
<a id="dlbtn" href="/file/$FileId" download="$FileName" style="position:absolute;left:0px;top:0px;height:100%;width:100%;"></a>
download
</button>
<div style="min-height:15px" />
</div>
</div>
</body>
</html>

View file

@ -1,41 +1,21 @@
<!DOCTYPE html>
<html lang="en">
<head>
<link
rel="stylesheet"
href="./style/error.scss"
>
<link
rel="icon"
type="image/svg"
href="/static/assets/icons/error.svg"
>
<link
rel="stylesheet"
href="/api/v1/account/me/css"
>
<meta
name="viewport"
<link rel="stylesheet" href="./style/error.scss" />
<link rel="icon" type="image/svg" href="/assets/icons/error.svg" />
<link rel="stylesheet" href="/api/v1/account/me/css" />
<meta
name="viewport"
content="width=device-width, initial-scale=1.0, user-scalable=0"
>
<title>$code</title>
<meta name="theme-color" content="rgb(30, 33, 36)">
/>
<title>%sveltekit.status%</title>
<meta name="theme-color" content="rgb(30, 33, 36)" />
</head>
<body>
<p class="error">
<span class="code">$code</span>
&nbsp;$text
<span class="code">%sveltekit.status%</span>
&nbsp;%sveltekit.error.message%
</p>
</body>
</html>

View file

@ -1,38 +0,0 @@
<!--
for some reason (don't know why)
certain things break
when not in quirks mode
so i'm not adding in the
doctype html
-->
<html lang="en">
<head>
<link rel="stylesheet" href="./style/app.scss" />
<link
rel="icon"
type="image/svg"
href="/static/assets/icons/icon.svg"
/>
<link rel="stylesheet" href="/api/v1/account/me/css" />
<meta
name="viewport"
content="width=device-width, initial-scale=1.0, user-scalable=0"
/>
<script type="module" src="./svelte/index.ts"></script>
<title>monofile</title>
<meta name="title" content="monofile" />
<meta
name="description"
content="The open-source Discord-based file sharing service"
/>
<meta name="theme-color" content="rgb(30, 33, 36)" />
</head>
<body></body>
</html>

30
src/lib/errors.ts Normal file
View file

@ -0,0 +1,30 @@
import { readFile } from "fs/promises"
import type { Context } from "hono"
import type { StatusCode } from "hono/utils/http-status"
import errorPage from "../error.html?raw"
/**
* @description Serves an error as a response to a request with an error page attached
* @param ctx Express response object
* @param code Error code
* @param reason Error reason
*/
export default async function ServeError(
ctx: Context,
code: number = 500,
reason: string
) {
// serve error
return ctx.req.header("accept")?.includes("text/html")
? ctx.html(
errorPage
.replaceAll("%sveltekit.status%", code.toString())
.replaceAll("%sveltekit.error.message%", reason),
code as StatusCode /*,
{
"x-backup-status-message": reason, // glitch default nginx configuration
}*/
)
: ctx.text(reason, code as StatusCode)
}

View file

@ -3,8 +3,9 @@ import { Readable, Writable } from "node:stream"
import crypto from "node:crypto"
import { files } from "./accounts.js"
import { Client as API } from "./DiscordAPI/index.js"
import type {APIAttachment} from "discord-api-types/v10"
import type { APIAttachment } from "discord-api-types/v10"
import "dotenv/config"
import { readFileSync } from "node:fs"
import * as Accounts from "./accounts.js"
@ -32,10 +33,12 @@ export function generateFileId(length: number = 5) {
/**
* @description Assert multiple conditions... this exists out of pure laziness
* @param conditions
* @param conditions
*/
function multiAssert(conditions: Map<boolean, { message: string, status: number }>) {
function multiAssert(
conditions: Map<boolean, { message: string; status: number }>
) {
for (let [cond, err] of conditions.entries()) {
if (cond) return err
}
@ -80,18 +83,15 @@ export interface StatusCodeError {
}
export class WebError extends Error {
readonly statusCode: number = 500
constructor(status: number, message: string) {
super(message)
this.statusCode = status
}
}
export class ReadStream extends Readable {
files: Files
pointer: FilePointer
@ -100,52 +100,60 @@ export class ReadStream extends Readable {
position: number = 0
ranges: {
useRanges: boolean,
byteStart: number,
useRanges: boolean
byteStart: number
byteEnd: number
scan_msg_begin: number,
scan_msg_end: number,
scan_files_begin: number,
scan_msg_begin: number
scan_msg_end: number
scan_files_begin: number
scan_files_end: number
}
id: number = Math.random()
aborter?: AbortController
constructor(files: Files, pointer: FilePointer, range?: {start: number, end: number}) {
constructor(
files: Files,
pointer: FilePointer,
range?: { start: number; end: number }
) {
super()
console.log(this.id, range)
this.files = files
this.pointer = pointer
let useRanges =
Boolean(range && pointer.chunkSize && pointer.sizeInBytes)
let useRanges = Boolean(
range && pointer.chunkSize && pointer.sizeInBytes
)
this.ranges = {
useRanges,
scan_msg_begin: 0,
scan_msg_end: pointer.messageids.length - 1,
scan_files_begin:
useRanges
scan_files_begin: useRanges
? Math.floor(range!.start / pointer.chunkSize!)
: 0,
scan_files_end:
useRanges
scan_files_end: useRanges
? Math.ceil(range!.end / pointer.chunkSize!) - 1
: -1,
byteStart: range?.start || 0,
byteEnd: range?.end || 0
byteEnd: range?.end || 0,
}
if (useRanges)
this.ranges.scan_msg_begin = Math.floor(this.ranges.scan_files_begin / 10),
this.ranges.scan_msg_end = Math.ceil(this.ranges.scan_files_end / 10),
this.msgIdx = this.ranges.scan_msg_begin
(this.ranges.scan_msg_begin = Math.floor(
this.ranges.scan_files_begin / 10
)),
(this.ranges.scan_msg_end = Math.ceil(
this.ranges.scan_files_end / 10
)),
(this.msgIdx = this.ranges.scan_msg_begin)
console.log(this.ranges)
}
async _read() {/*
async _read() {
/*
console.log("Calling for more data")
if (this.busy) return
this.busy = true
@ -160,24 +168,32 @@ export class ReadStream extends Readable {
this.pushData()
}
async _destroy(error: Error | null, callback: (error?: Error | null | undefined) => void): Promise<void> {
if (this.aborter)
this.aborter.abort()
async _destroy(
error: Error | null,
callback: (error?: Error | null | undefined) => void
): Promise<void> {
if (this.aborter) this.aborter.abort()
callback()
}
async getNextAttachment() {
// return first in our attachment buffer
let ret = this.attachmentBuffer.splice(0,1)[0]
let ret = this.attachmentBuffer.splice(0, 1)[0]
if (ret) return ret
console.log(this.id, this.msgIdx, this.ranges.scan_msg_end, this.pointer.messageids[this.msgIdx])
console.log(
this.id,
this.msgIdx,
this.ranges.scan_msg_end,
this.pointer.messageids[this.msgIdx]
)
// oh, there's none left. let's fetch a new message, then.
if (
!this.pointer.messageids[this.msgIdx]
|| this.msgIdx > this.ranges.scan_msg_end
) return null
!this.pointer.messageids[this.msgIdx] ||
this.msgIdx > this.ranges.scan_msg_end
)
return null
let msg = await this.files.api
.fetchMessage(this.pointer.messageids[this.msgIdx])
@ -190,95 +206,113 @@ export class ReadStream extends Readable {
let attach = msg.attachments
console.log(attach)
this.attachmentBuffer = this.ranges.useRanges ? attach.slice(
this.msgIdx == this.ranges.scan_msg_begin
? this.ranges.scan_files_begin - this.ranges.scan_msg_begin * 10
: 0,
this.msgIdx == this.ranges.scan_msg_end
? this.ranges.scan_files_end - this.ranges.scan_msg_end * 10 + 1
: attach.length
) : attach
this.attachmentBuffer = this.ranges.useRanges
? attach.slice(
this.msgIdx == this.ranges.scan_msg_begin
? this.ranges.scan_files_begin -
this.ranges.scan_msg_begin * 10
: 0,
this.msgIdx == this.ranges.scan_msg_end
? this.ranges.scan_files_end -
this.ranges.scan_msg_end * 10 +
1
: attach.length
)
: attach
console.log(this.attachmentBuffer)
}
this.msgIdx++
return this.attachmentBuffer.splice(0,1)[0]
return this.attachmentBuffer.splice(0, 1)[0]
}
async getPusherForWebStream(webStream: ReadableStream) {
const reader = await webStream.getReader()
let pushing = false // acts as a debounce just in case
// (words of a girl paranoid from writing readfilestream)
// (words of a girl paranoid from writing readfilestream)
let pushToStream = this.push.bind(this)
let stream = this
return function() {
return function () {
if (pushing) return
pushing = true
return reader.read().catch(e => {
// Probably means an AbortError; whatever it is we'll need to abort
if (webStream.locked) reader.releaseLock()
webStream.cancel().catch(e => undefined)
if (!stream.destroyed) stream.destroy()
return e
}).then(result => {
if (result instanceof Error || !result) return result
let pushed
if (!result.done) {
pushing = false
pushed = pushToStream(result.value)
}
return {readyForMore: pushed || false, streamDone: result.done }
})
return reader
.read()
.catch((e) => {
// Probably means an AbortError; whatever it is we'll need to abort
if (webStream.locked) reader.releaseLock()
webStream.cancel().catch((e) => undefined)
if (!stream.destroyed) stream.destroy()
return e
})
.then((result) => {
if (result instanceof Error || !result) return result
let pushed
if (!result.done) {
pushing = false
pushed = pushToStream(result.value)
}
return {
readyForMore: pushed || false,
streamDone: result.done,
}
})
}
}
async getNextChunk() {
let scanning_chunk = await this.getNextAttachment()
console.log(this.id, "Next chunk requested; got attachment", scanning_chunk)
console.log(
this.id,
"Next chunk requested; got attachment",
scanning_chunk
)
if (!scanning_chunk) return null
let {
byteStart, byteEnd, scan_files_begin, scan_files_end
} = this.ranges
let { byteStart, byteEnd, scan_files_begin, scan_files_end } =
this.ranges
let headers: HeadersInit =
this.ranges.useRanges
? {
Range: `bytes=${
this.position == 0
? byteStart - scan_files_begin * this.pointer.chunkSize!
: "0"
}-${
this.attachmentBuffer.length == 0 && this.msgIdx == scan_files_end
? byteEnd - scan_files_end * this.pointer.chunkSize!
: ""
}`,
}
: {}
let headers: HeadersInit = this.ranges.useRanges
? {
Range: `bytes=${
this.position == 0
? byteStart -
scan_files_begin * this.pointer.chunkSize!
: "0"
}-${
this.attachmentBuffer.length == 0 &&
this.msgIdx == scan_files_end
? byteEnd - scan_files_end * this.pointer.chunkSize!
: ""
}`,
}
: {}
this.aborter = new AbortController()
let response = await fetch(scanning_chunk.url, {headers, signal: this.aborter.signal})
.catch((e: Error) => {
console.error(e)
return {body: e}
})
let response = await fetch(scanning_chunk.url, {
headers,
signal: this.aborter.signal,
}).catch((e: Error) => {
console.error(e)
return { body: e }
})
this.position++
return response.body
}
currentPusher?: (() => Promise<{readyForMore: boolean, streamDone: boolean } | void> | undefined)
currentPusher?: () =>
| Promise<{ readyForMore: boolean; streamDone: boolean } | void>
| undefined
busy: boolean = false
async pushData(): Promise<boolean | undefined> {
// uh oh, we don't have a currentPusher
// let's make one then
if (!this.currentPusher) {
@ -292,7 +326,8 @@ export class ReadStream extends Readable {
// or the stream has ended.
// let's destroy the stream
console.log(this.id, "Ending", next)
if (next) this.destroy(next); else this.push(null)
if (next) this.destroy(next)
else this.push(null)
return
}
}
@ -304,12 +339,10 @@ export class ReadStream extends Readable {
this.currentPusher = undefined
return this.pushData()
} else return result?.readyForMore
}
}
export class UploadStream extends Writable {
uploadId?: string
name?: string
mime?: string
@ -331,7 +364,11 @@ export class UploadStream extends Writable {
async _write(data: Buffer, encoding: string, callback: () => void) {
console.log("Write to stream attempted")
if (this.filled + data.byteLength > (this.files.config.maxDiscordFileSize*this.files.config.maxDiscordFiles))
if (
this.filled + data.byteLength >
this.files.config.maxDiscordFileSize *
this.files.config.maxDiscordFiles
)
return this.destroy(new WebError(413, "maximum file size exceeded"))
this.hash.update(data)
@ -343,21 +380,32 @@ export class UploadStream extends Writable {
while (position < data.byteLength) {
let capture = Math.min(
((this.files.config.maxDiscordFileSize*10) - (this.filled % (this.files.config.maxDiscordFileSize*10))),
data.byteLength-position
this.files.config.maxDiscordFileSize * 10 -
(this.filled % (this.files.config.maxDiscordFileSize * 10)),
data.byteLength - position
)
console.log(
`Capturing ${capture} bytes for megachunk, ${
data.subarray(position, position + capture).byteLength
}`
)
console.log(`Capturing ${capture} bytes for megachunk, ${data.subarray(position, position + capture).byteLength}`)
if (!this.current) await this.getNextStream()
if (!this.current) {
this.destroy(new Error("getNextStream called during debounce")); return
this.destroy(new Error("getNextStream called during debounce"))
return
}
readyForMore = this.current.push( data.subarray(position, position+capture) )
console.log(`pushed ${data.byteLength} byte chunk`);
position += capture, this.filled += capture
readyForMore = this.current.push(
data.subarray(position, position + capture)
)
console.log(`pushed ${data.byteLength} byte chunk`)
;(position += capture), (this.filled += capture)
// message is full, so tell the next run to get a new message
if (this.filled % (this.files.config.maxDiscordFileSize*10) == 0) {
if (
this.filled % (this.files.config.maxDiscordFileSize * 10) ==
0
) {
this.current!.push(null)
this.current = undefined
}
@ -369,24 +417,27 @@ export class UploadStream extends Writable {
async _final(callback: (error?: Error | null | undefined) => void) {
if (this.current) {
this.current.push(null);
this.current.push(null)
// i probably dnt need this but whateverrr :3
await new Promise((res,rej) => this.once("debounceReleased", res))
await new Promise((res, rej) => this.once("debounceReleased", res))
}
callback()
}
aborted: boolean = false
async _destroy(error: Error | null, callback: (err?: Error|null) => void) {
async _destroy(
error: Error | null,
callback: (err?: Error | null) => void
) {
this.error = error || undefined
await this.abort()
callback(error)
}
/**
/**
* @description Cancel & unlock the file. When destroy() is called with a non-WebError, this is automatically called
*/
*/
async abort() {
if (this.aborted) return
this.aborted = true
@ -406,8 +457,13 @@ export class UploadStream extends Writable {
async commit() {
if (this.errored) throw this.error
if (!this.writableFinished) {
let err = Error("attempted to commit file when the stream was still unfinished")
if (!this.destroyed) {this.destroy(err)}; throw err
let err = Error(
"attempted to commit file when the stream was still unfinished"
)
if (!this.destroyed) {
this.destroy(err)
}
throw err
}
// Perform checks
@ -421,7 +477,7 @@ export class UploadStream extends Writable {
}
if (!this.uploadId) this.setUploadId(generateFileId())
let ogf = this.files.files[this.uploadId!]
this.files.files[this.uploadId!] = {
@ -430,19 +486,18 @@ export class UploadStream extends Writable {
messageids: this.messages,
owner: this.owner,
sizeInBytes: this.filled,
visibility: ogf ? ogf.visibility
: (
this.owner
? Accounts.getFromId(this.owner)?.defaultFileVisibility
: undefined
),
visibility: ogf
? ogf.visibility
: this.owner
? Accounts.getFromId(this.owner)?.defaultFileVisibility
: undefined,
// so that json.stringify doesnt include tag:undefined
...((ogf||{}).tag ? {tag:ogf.tag} : {}),
...((ogf || {}).tag ? { tag: ogf.tag } : {}),
chunkSize: this.files.config.maxDiscordFileSize,
md5: this.hash.digest("hex"),
lastModified: Date.now()
lastModified: Date.now(),
}
delete this.files.locks[this.uploadId!]
@ -456,52 +511,72 @@ export class UploadStream extends Writable {
setName(name: string) {
if (this.name)
return this.destroy( new WebError(400, "duplicate attempt to set filename") )
return this.destroy(
new WebError(400, "duplicate attempt to set filename")
)
if (name.length > 512)
return this.destroy( new WebError(400, "filename can be a maximum of 512 characters") )
this.name = name;
return this.destroy(
new WebError(400, "filename can be a maximum of 512 characters")
)
this.name = name
return this
}
setType(type: string) {
setType(type: string) {
if (this.mime)
return this.destroy( new WebError(400, "duplicate attempt to set mime type") )
return this.destroy(
new WebError(400, "duplicate attempt to set mime type")
)
if (type.length > 256)
return this.destroy( new WebError(400, "mime type can be a maximum of 256 characters") )
this.mime = type;
return this.destroy(
new WebError(
400,
"mime type can be a maximum of 256 characters"
)
)
this.mime = type
return this
}
setUploadId(id: string) {
if (this.uploadId)
return this.destroy( new WebError(400, "duplicate attempt to set upload ID") )
if (!id || id.match(id_check_regex)?.[0] != id
|| id.length > this.files.config.maxUploadIdLength)
return this.destroy( new WebError(400, "invalid file ID") )
return this.destroy(
new WebError(400, "duplicate attempt to set upload ID")
)
if (
!id ||
id.match(id_check_regex)?.[0] != id ||
id.length > this.files.config.maxUploadIdLength
)
return this.destroy(new WebError(400, "invalid file ID"))
if (this.files.files[id] && this.files.files[id].owner != this.owner)
return this.destroy( new WebError(403, "you don't own this file") )
return this.destroy(new WebError(403, "you don't own this file"))
if (this.files.locks[id])
return this.destroy( new WebError(409, "a file with this ID is already being uploaded") )
return this.destroy(
new WebError(
409,
"a file with this ID is already being uploaded"
)
)
this.files.locks[id] = true
this.uploadId = id
return this
}
}
// merged StreamBuffer helper
filled: number = 0
current?: Readable
messages: string[] = []
private newmessage_debounce : boolean = true
private async startMessage(): Promise<Readable | undefined> {
private newmessage_debounce: boolean = true
private async startMessage(): Promise<Readable | undefined> {
if (!this.newmessage_debounce) return
this.newmessage_debounce = false
@ -510,24 +585,28 @@ export class UploadStream extends Writable {
let stream = new Readable({
read() {
// this is stupid but it should work
console.log("Read called; calling on server to execute callback")
console.log(
"Read called; calling on server to execute callback"
)
wrt.emit("exec-callback")
}
},
})
stream.pause()
console.log(`Starting a message`)
this.files.api.send(stream).then(message => {
this.messages.push(message.id)
console.log(`Sent: ${message.id}`)
this.newmessage_debounce = true
this.emit("debounceReleased")
}).catch(e => {
if (!this.errored) this.destroy(e)
})
this.files.api
.send(stream)
.then((message) => {
this.messages.push(message.id)
console.log(`Sent: ${message.id}`)
this.newmessage_debounce = true
this.emit("debounceReleased")
})
.catch((e) => {
if (!this.errored) this.destroy(e)
})
return stream
}
private async getNextStream() {
@ -536,12 +615,14 @@ export class UploadStream extends Writable {
if (this.current) return this.current
else if (this.newmessage_debounce) {
// startmessage.... idk
this.current = await this.startMessage();
this.current = await this.startMessage()
return this.current
} else {
return new Promise((resolve, reject) => {
console.log("Waiting for debounce to be released...")
this.once("debounceReleased", async () => resolve(await this.getNextStream()))
this.once("debounceReleased", async () =>
resolve(await this.getNextStream())
)
})
}
}
@ -550,16 +631,16 @@ export class UploadStream extends Writable {
export default class Files {
config: Configuration
api: API
files: { [key: string]: FilePointer } = {}
files: Record<string, FilePointer> = Object.create(null) // { [key: string]: FilePointer } = {}
data_directory: string = `${process.cwd()}/.data`
locks: Record<string, boolean> = {} // I'll, like, do something more proper later
locks: Record<string, boolean> = Object.create(null) // I'll, like, do something more proper later
constructor(config: Configuration) {
this.config = config
this.api = new API(process.env.TOKEN!, config)
readFile(this.data_directory+ "/files.json")
readFile(this.data_directory + "/files.json")
.then((buf) => {
this.files = JSON.parse(buf.toString() || "{}")
})
@ -574,7 +655,7 @@ export default class Files {
/**
* @description Saves file database
*
*
*/
async write(): Promise<void> {
await writeFile(
@ -592,7 +673,7 @@ export default class Files {
* @param uploadId Target file's ID
*/
async update( uploadId: string ) {
async update(uploadId: string) {
let target_file = this.files[uploadId]
let attachment_sizes = []
@ -604,12 +685,12 @@ export default class Files {
}
if (!target_file.sizeInBytes)
target_file.sizeInBytes = attachment_sizes.reduce((a, b) => a + b, 0)
if (!target_file.chunkSize)
target_file.chunkSize = attachment_sizes[0]
target_file.sizeInBytes = attachment_sizes.reduce(
(a, b) => a + b,
0
)
if (!target_file.chunkSize) target_file.chunkSize = attachment_sizes[0]
}
/**
@ -624,7 +705,8 @@ export default class Files {
): Promise<ReadStream> {
if (this.files[uploadId]) {
let file = this.files[uploadId]
if (!file.sizeInBytes || !file.chunkSize) await this.update(uploadId)
if (!file.sizeInBytes || !file.chunkSize)
await this.update(uploadId)
return new ReadStream(this, file, range)
} else {
throw { status: 404, message: "not found" }
@ -648,9 +730,15 @@ export default class Files {
}
delete this.files[uploadId]
if (!noWrite) this.write().catch((err) => {
throw err
})
if (!noWrite)
this.write().catch((err) => {
throw err
})
}
}
const config = JSON.parse(
// Don't use __dirname. For all we know this fyle could be a hashed chunk.
// Also if this were NixOS you can't modify the built folder after build
readFileSync(process.cwd() + "/config.json", "utf-8")
)
export const files_singleton = new Files(config)

View file

@ -1,7 +1,13 @@
import { createTransport } from "nodemailer"
import "dotenv/config"
import config from "../../../config.json" assert {type:"json"}
import { generateFileId } from "./files.js"
import { readFile } from "fs/promises"
// We need to import config.json at run time because split is a genius. Ideally we would use .env files for this
const config = JSON.parse(
// Don't use __dirname. For all we know this fyle could be a hashed chunk.
// Also if this were NixOS you can't modify the built folder after build
await readFile(process.cwd() + "/config.json", "utf-8")
)
let mailConfig = config.mail,
transport = createTransport({
@ -37,25 +43,30 @@ export function sendMail(to: string, subject: string, content: string) {
}
export namespace CodeMgr {
export const Intents = ["verifyEmail", "recoverAccount"] as const
export const Intents = [
"verifyEmail",
"recoverAccount"
] as const
export type Intent = (typeof Intents)[number]
export type Intent = typeof Intents[number]
export function isIntent(intent: string): intent is Intent { return intent in Intents }
export function isIntent(intent: string): intent is Intent {
return intent in Intents
}
export let codes = Object.fromEntries(
Intents.map(e => [
e,
{byId: new Map<string, Code>(), byUser: new Map<string, Code[]>()}
])) as Record<Intent, { byId: Map<string, Code>, byUser: Map<string, Code[]> }>
Intents.map((e) => [
e,
{
byId: new Map<string, Code>(),
byUser: new Map<string, Code[]>(),
},
])
) as Record<
Intent,
{ byId: Map<string, Code>; byUser: Map<string, Code[]> }
>
// this is stupid whyd i write this
export class Code {
export class Code {
readonly id: string = generateFileId(12)
readonly for: string
@ -65,25 +76,30 @@ export namespace CodeMgr {
readonly data: any
constructor(intent: Intent, forUser: string, data?: any, time: number = 15*60*1000) {
this.for = forUser;
constructor(
intent: Intent,
forUser: string,
data?: any,
time: number = 15 * 60 * 1000
) {
this.for = forUser
this.intent = intent
this.expiryClear = setTimeout(this.terminate.bind(this), time)
this.data = data
codes[intent].byId.set(this.id, this);
codes[intent].byId.set(this.id, this)
let byUser = codes[intent].byUser.get(this.for)
if (!byUser) {
byUser = []
codes[intent].byUser.set(this.for, byUser);
codes[intent].byUser.set(this.for, byUser)
}
byUser.push(this)
}
terminate() {
codes[this.intent].byId.delete(this.id);
codes[this.intent].byId.delete(this.id)
let bu = codes[this.intent].byUser.get(this.id)!
bu.splice(bu.indexOf(this), 1)
clearTimeout(this.expiryClear)
@ -93,5 +109,4 @@ export namespace CodeMgr {
return forUser === this.for
}
}
}
}

View file

@ -95,12 +95,14 @@ export const assertAPI = function (
// Not really middleware but a utility
export const login = (ctx: Context, account: string) => setCookie(ctx, "auth", auth.create(account, 3 * 24 * 60 * 60 * 1000), {
path: "/",
sameSite: "Strict",
secure: true,
httpOnly: true
})
export const login = (ctx: Context, account: string) =>
setCookie(ctx, "auth", auth.create(account, 3 * 24 * 60 * 60 * 1000), {
path: "/",
sameSite: "Strict",
secure: true,
httpOnly: true,
maxAge: 34560
})
type SchemeType = "array" | "object" | "string" | "number" | "boolean"

107
src/monofile.tsx Normal file
View file

@ -0,0 +1,107 @@
import { Hono } from "hono"
import fs from "fs"
import { readFile } from "fs/promises"
import { files_singleton } from "./lib/files.js"
import { fileURLToPath } from "url"
import { dirname } from "path"
import v0 from "./api/v0/index.js"
import v1 from "./api/v1/index.js"
import preview_and_verify from "./api/web/index.js"
// We need to import config.json at run time because split is a genius. Ideally we would use .env files for this
const config = JSON.parse(
// Don't use __dirname. For all we know this fyle could be a hashed chunk.
// Also if this were NixOS you can't modify the built folder after build
fs.readFileSync(process.cwd() + "/config.json", "utf-8")
)
declare const MONOFILE_VERSION: string // see vite.config.ts
const app = new Hono()
// app.get(
// "/assets/*",
// serveStatic({
// rewriteRequestPath: (path) => {
// return path.replace("/assets", "/assets")
// },
// })
// )
// app.get(
// "/vite/*",
// serveStatic({
// rewriteRequestPath: (path) => {
// return path.replace("/vite", "/dist/vite")
// },
// })
// )
// respond to the MOLLER method
// get it?
// haha...
app.on(["MOLLER"], "*", async (ctx) => {
ctx.header("Content-Type", "image/webp")
return ctx.body(await readFile("./assets/moller.png"))
})
//app.use(bodyParser.text({limit:(config.maxDiscordFileSize*config.maxDiscordFiles)+1048576,type:["application/json","text/plain"]}))
// check for ssl, if not redirect
if (config.trustProxy) {
// app.enable("trust proxy")
}
if (config.forceSSL) {
app.use(async (ctx, next) => {
if (new URL(ctx.req.url).protocol == "http") {
return ctx.redirect(
`https://${ctx.req.header("host")}${
new URL(ctx.req.url).pathname
}`
)
} else {
return next()
}
})
}
app.get("/server", (ctx) =>
ctx.json({
...config,
version: MONOFILE_VERSION,
files: Object.keys(files.files).length,
})
)
// funcs
// init data
const __dirname = dirname(fileURLToPath(import.meta.url))
if (!fs.existsSync(__dirname + "/../.data/"))
fs.mkdirSync(__dirname + "/../.data/")
// discord
let files = files_singleton
globalThis.__files = files // Kill me
v0(app, files)
v1(app, files)
preview_and_verify(app, files)
// moved here to ensure it's matched last
app.get("/:fileId", async (ctx) =>
app.fetch(
new Request(
new URL(
`/api/v1/file/${ctx.req.param("fileId")}`,
ctx.req.raw.url
).href,
ctx.req.raw
),
ctx.env
)
)
console.log("This is monofile.")
export default app

19
src/routes/+error.svelte Normal file
View file

@ -0,0 +1,19 @@
<script>
import "../style/error.scss"
import { page } from "$app/stores"
</script>
<svelte:head>
<link rel="icon" type="image/svg" href="/assets/icons/error.svg" />
<link rel="stylesheet" href="/api/v1/account/me/css" />
<meta
name="viewport"
content="width=device-width, initial-scale=1.0, user-scalable=0"
/>
<title>{$page.status}</title>
<meta name="theme-color" content="rgb(30, 33, 36)" />
</svelte:head>
<p class="error">
<span class="code">{$page.status}</span>
&nbsp;{$page.error.message}
</p>

33
src/routes/+page.svelte Normal file
View file

@ -0,0 +1,33 @@
<script lang="ts">
import { onMount } from "svelte"
import Topbar from "./components/Topbar.svelte"
import PulldownManager from "./components/PulldownManager.svelte"
import UploadWindow from "./components/UploadWindow.svelte"
import { pulldownManager } from "./components/stores.js"
import "../style/app.scss"
let topbar: Topbar
let pulldown: PulldownManager
onMount(() => {
pulldownManager.set(pulldown)
})
</script>
<svelte:head>
<title>monofile</title>
<meta name="title" content="monofile" />
<meta
name="description"
content="The open-source Discord-based file sharing service"
/>
<meta name="theme-color" content="rgb(30, 33, 36)" />
</svelte:head>
<Topbar bind:this={topbar} {pulldown} />
<div id="appContent">
<PulldownManager bind:this={pulldown} />
<UploadWindow />
</div>

1
src/routes/+page.ts Normal file
View file

@ -0,0 +1 @@
export const ssr = false

View file

@ -0,0 +1,16 @@
// This is monofile.
import monofile from "../../monofile"
const hook: import("./$types").RequestHandler = (req) => {
console.log("[MONOFILE]", req.request.method, req.url.pathname)
return monofile.fetch(req.request) ?? console.log("[MONOFILE]", "Returned undefined!")
}
export const GET = hook
export const HEAD = hook
export const POST = hook
export const PUT = hook
export const PATCH = hook
export const DELETE = hook
export const OPTIONS = hook

View file

@ -0,0 +1,2 @@
// TODO do auth here instead of in each route
// TODO make sure I don't suffer from https://github.com/sveltejs/kit/issues/6315 - await parent() ?

View file

@ -0,0 +1,114 @@
import * as Accounts from "$lib/accounts"
import * as auth from "$lib/auth.js"
import { error } from "@sveltejs/kit"
import RangeParser from "range-parser"
import { files_singleton as files } from "$lib/files"
const get: import("./$types").RequestHandler = async ({
params,
cookies,
request,
url,
}) => {
const { fileId } = params
const token =
cookies.get("auth") ??
(request.headers.get("authorization")?.startsWith("Bearer ")
? request.headers.get("authorization")?.split(" ")[1]
: undefined)!
const account = Accounts.getFromToken(token)
const file = files.files[fileId]
if (!file) throw error(404, "file not found")
const lastModified = new Date(file.lastModified)
if (file.visibility == "private") {
if (account?.id != file.owner) {
throw error(403, "you do not own this file")
}
if (
auth.getType(token) == "App" &&
auth.getPermissions(token)?.includes("private")
) {
throw error(403, "insufficient permissions")
}
}
let range: RangeParser.Range | undefined
if (file.chunkSize && request.headers.get("range")) {
let ranges = RangeParser(
file.sizeInBytes,
request.headers.get("range") || ""
)
if (ranges) {
if (typeof ranges == "number")
throw error(416, "unsatisfiable ranges")
if (ranges.length > 1)
throw error(400, "multiple ranges not supported")
range = ranges[0]
}
}
let stream: ReadableStream | undefined
if (request.method == "HEAD") {
stream = undefined
} else {
stream = files.readFileStream(fileId, range)
}
return new Response(stream, {
status: range ? 206 : 200,
headers: {
"Content-Length": file.sizeInBytes,
...(range && {
"Content-Length": (range.end - range.start + 1).toString(),
"Content-Range": `bytes ${range.start}-${range.end}/${file.sizeInBytes}`,
}),
"Content-Type": file.mime,
ETag: file.md5,
"Content-Disposition": `${
url.searchParams.get("attachment") == "1"
? "attachment"
: "inline"
}; filename="${encodeURI(file.filename.replaceAll("\n", "\\n"))}"`,
"Access-Control-Allow-Origin": "*",
"Content-Security-Policy": "sandbox allow-scripts",
// TERRIFYING
"Last-Modified":
`${
["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"][
lastModified.getUTCDay()
]
}, ${lastModified.getUTCDate()} ` +
`${
[
"Jan",
"Feb",
"Mar",
"Apr",
"May",
"Jun",
"Jul",
"Aug",
"Sep",
"Oct",
"Nov",
"Dec",
][lastModified.getUTCMonth()]
}` +
` ${lastModified.getUTCFullYear()} ${lastModified
.getUTCHours()
.toString()
.padStart(2, "0")}` +
`:${lastModified
.getUTCMinutes()
.toString()
.padStart(2, "0")}:${lastModified
.getUTCSeconds()
.toString()
.padStart(2, "0")} GMT`,
},
})
}
export const GET = get
export const HEAD = get

View file

@ -137,7 +137,7 @@
}
</script>
<div id="uploadWindow">
<main>
<h1>
monofile
{#if notificationPermission === "default"}
@ -375,4 +375,4 @@
>
</p>
<div style:height="10px" />
</div>
</main>

View file

@ -0,0 +1,108 @@
<script lang="ts">
import { fade, slide } from "svelte/transition"
interface BaseModalOption {
name: string
icon: string
id: string | number | symbol | boolean
}
type ModalOption =
| (BaseModalOption & { inputSettings: { password?: boolean }; id: any })
| (BaseModalOption & { description: string })
type ModalOptions = ModalOption[]
type OptionPickerReturns = ({ selected: any } & Record<any, any>) | null
let activeModal:
| {
resolve: (val: OptionPickerReturns) => void
title: string
modal: ModalOptions
}
| undefined
let modalResults: Record<string | number | symbol, string> = {}
export function picker(
title: string,
mdl: ModalOptions
): Promise<OptionPickerReturns> {
if (activeModal) forceCancel()
return new Promise<OptionPickerReturns>((resolve, reject) => {
activeModal = {
resolve,
title,
modal: mdl,
}
modalResults = {}
})
}
export function forceCancel() {
if (activeModal && activeModal.resolve) {
activeModal.resolve(null)
}
activeModal = undefined
}
</script>
{#if activeModal}
<div class="modalContainer" transition:fade={{ duration: 200 }}>
<button class="mdHitbox" on:click|self={forceCancel}></button>
<div class="modal" transition:slide={{ duration: 200 }}>
<div class="optPicker">
<div class="category">
<p style:margin-bottom="10px">{activeModal.title}</p>
</div>
{#each activeModal.modal as option (option.id)}
{#if "inputSettings" in option}
<div class="inp">
<img src={option.icon} alt={option.id.toString()} />
<!-- i have to do this stupidness because of svelte but -->
<!-- its reason for blocking this is pretty good sooooo -->
{#if option.inputSettings.password}
<input
placeholder={option.name}
type="password"
bind:value={modalResults[option.id]}
/>
{:else}
<input
placeholder={option.name}
bind:value={modalResults[option.id]}
/>
{/if}
</div>
{:else}
<button
on:click={() => {
activeModal?.resolve({
...modalResults,
selected: option.id,
})
activeModal = undefined
modalResults = {}
}}
>
<img src={option.icon} alt={option.id.toString()} />
<p>
{option.name}<span
><br />{option.description}</span
>
</p>
</button>
{/if}
{/each}
<button on:click={forceCancel}>
<img src="/assets/icons/delete.svg" alt="cancel" />
<p>Cancel</p>
</button>
</div>
</div>
</div>
{/if}

View file

@ -0,0 +1,435 @@
import { fetchAccountData, account, refreshNeeded } from "../stores"
import { get } from "svelte/store"
import type OptionPicker from "./OptionPicker.svelte"
export function deleteAccount(optPicker: OptionPicker) {
optPicker
.picker("What should we do with your files?", [
{
name: "Delete my files",
icon: "/assets/icons/admin/delete_file.svg",
description: "Your files will be permanently deleted",
id: true,
},
{
name: "Do nothing",
icon: "/assets/icons/file.svg",
description: "Your files will not be affected",
id: false,
},
])
.then((exp) => {
if (exp) {
let deleteFiles = exp.selected
optPicker
.picker(`Enter your username to continue.`, [
{
name: "Enter your username",
icon: "/assets/icons/person.svg",
inputSettings: {},
id: "username",
},
{
name: `Delete account ${
deleteFiles ? "& files" : ""
}`,
icon: "/assets/icons/delete_account.svg",
description: `This cannot be undone.`,
id: true,
},
])
.then((fin) => {
if (fin && fin.selected) {
if (fin.username != (get(account) || {}).username) {
optPicker.picker(
"Incorrect username. Please try again.",
[]
)
return
}
fetch(`/auth/delete_account`, {
method: "POST",
body: JSON.stringify({
deleteFiles,
}),
}).then((response) => {
if (response.status != 200) {
optPicker.picker(
`${response.status} ${
response.headers.get(
"x-backup-status-message"
) ||
response.statusText ||
""
}`,
[]
)
}
fetchAccountData()
})
}
})
}
})
}
export function userChange(optPicker: OptionPicker) {
optPicker
.picker("Change username", [
{
name: "New username",
icon: "/assets/icons/person.svg",
id: "username",
inputSettings: {},
},
{
name: "Update username",
icon: "/assets/icons/update.svg",
description: "",
id: true,
},
])
.then((exp) => {
if (exp && exp.selected) {
fetch(`/auth/change_username`, {
method: "POST",
body: JSON.stringify({
username: exp.username,
}),
}).then((response) => {
if (response.status != 200) {
optPicker.picker(
`${response.status} ${
response.headers.get(
"x-backup-status-message"
) ||
response.statusText ||
""
}`,
[]
)
}
fetchAccountData()
})
}
})
}
export function forgotPassword(optPicker: OptionPicker) {
optPicker
.picker("Forgot your password?", [
{
name: "Username",
icon: "/assets/icons/person.svg",
id: "user",
inputSettings: {},
},
{
name: "OK",
icon: "/assets/icons/update.svg",
description: "",
id: true,
},
])
.then((exp) => {
if (exp && exp.selected) {
fetch(`/auth/request_emergency_login`, {
method: "POST",
body: JSON.stringify({
account: exp.user,
}),
}).then((response) => {
if (response.status != 200) {
optPicker.picker(
`${response.status} ${
response.headers.get(
"x-backup-status-message"
) ||
response.statusText ||
""
}`,
[]
)
} else {
optPicker.picker(
`Please follow the instructions sent to your inbox.`,
[]
)
}
})
}
})
}
export function emailPotentialRemove(optPicker: OptionPicker) {
optPicker
.picker("What would you like to do?", [
{
name: "Set a new email",
icon: "/assets/icons/change_email.svg",
description: "",
id: "set",
},
{
name: "Disconnect email",
icon: "/assets/icons/disconnect_email.svg",
description: "",
id: "disconnect",
},
])
.then((exp) => {
if (exp && exp.selected) {
switch (exp.selected) {
case "set":
emailChange(optPicker)
break
case "disconnect":
fetch("/auth/remove_email", { method: "POST" }).then(
(response) => {
if (response.status != 200) {
optPicker.picker(
`${response.status} ${
response.headers.get(
"x-backup-status-message"
) ||
response.statusText ||
""
}`,
[]
)
}
fetchAccountData()
}
)
}
}
})
}
export function emailChange(optPicker: OptionPicker) {
optPicker
.picker("Change email", [
{
name: "New email",
icon: "/assets/icons/mail.svg",
id: "email",
inputSettings: {},
},
{
name: "Request email change",
icon: "/assets/icons/update.svg",
description: "",
id: true,
},
])
.then((exp) => {
if (exp && exp.selected) {
fetch(`/auth/request_email_change`, {
method: "POST",
body: JSON.stringify({
email: exp.email,
}),
}).then((response) => {
if (response.status != 200) {
optPicker.picker(
`${response.status} ${
response.headers.get(
"x-backup-status-message"
) ||
response.statusText ||
""
}`,
[]
)
} else {
optPicker.picker(
`Please continue to your inbox at ${
exp.email.split("@")[1]
} and click on the attached link.`,
[]
)
}
})
}
})
}
export function pwdChng(optPicker: OptionPicker) {
optPicker
.picker("Change password", [
{
name: "New password",
icon: "/assets/icons/change_password.svg",
id: "password",
inputSettings: {
password: true,
},
},
{
name: "Update password",
icon: "/assets/icons/update.svg",
description: "This will log you out of all sessions",
id: true,
},
])
.then((exp) => {
if (exp && exp.selected) {
fetch(`/auth/change_password`, {
method: "POST",
body: JSON.stringify({
password: exp.password,
}),
}).then((response) => {
if (response.status != 200) {
optPicker.picker(
`${response.status} ${
response.headers.get(
"x-backup-status-message"
) ||
response.statusText ||
""
}`,
[]
)
}
fetchAccountData()
})
}
})
}
export function customcss(optPicker: OptionPicker) {
optPicker
.picker("Set custom CSS", [
{
name: "Enter a file ID",
icon: "/assets/icons/file.svg",
id: "fileid",
inputSettings: {},
},
{
name: "OK",
icon: "/assets/icons/update.svg",
description: "Refresh to apply changes",
id: true,
},
])
.then((exp) => {
if (exp && exp.selected) {
fetch(`/api/v1/account/customization/css`, {
method: "PUT",
body: JSON.stringify({
fileId: exp.fileid,
}),
}).then((response) => {
if (response.status != 200) {
optPicker.picker(
`${response.status} ${
response.headers.get(
"x-backup-status-message"
) ||
response.statusText ||
""
}`,
[]
)
}
fetchAccountData()
refreshNeeded.set(true)
})
}
})
}
export function embedColor(optPicker: OptionPicker) {
optPicker
.picker("Set embed color", [
{
name: "FFFFFF",
icon: "/assets/icons/pound.svg",
id: "color",
inputSettings: {},
},
{
name: "OK",
icon: "/assets/icons/update.svg",
description: "",
id: true,
},
])
.then((exp) => {
if (exp && exp.selected) {
fetch(`/api/v1/account/customization/embed/color`, {
method: "POST",
body: JSON.stringify({
color: exp.color,
}),
}).then((response) => {
if (response.status != 200) {
optPicker.picker(
`${response.status} ${
response.headers.get(
"x-backup-status-message"
) ||
response.statusText ||
""
}`,
[]
)
}
fetchAccountData()
})
}
})
}
export function embedSize(optPicker: OptionPicker) {
optPicker
.picker("Set embed image size", [
{
name: "Large",
icon: "/assets/icons/image.svg",
description: "",
id: true,
},
{
name: "Small",
icon: "/assets/icons/small_image.svg",
description: "",
id: false,
},
])
.then((exp) => {
if (exp && exp.selected !== null) {
fetch(`/api/v1/account/customization/embed/size`, {
method: "POST",
body: JSON.stringify({
largeImage: exp.selected,
}),
}).then((response) => {
if (response.status != 200) {
optPicker.picker(
`${response.status} ${
response.headers.get(
"x-backup-status-message"
) ||
response.statusText ||
""
}`,
[]
)
}
fetchAccountData()
})
}
})
}

View file

@ -0,0 +1,311 @@
import { fetchAccountData, fetchFilePointers, account } from "../stores"
import { get } from "svelte/store"
import type OptionPicker from "./OptionPicker.svelte"
export function pwdReset(optPicker: OptionPicker) {
optPicker
.picker("Reset password", [
{
name: "Target user",
icon: "/assets/icons/person.svg",
id: "target",
inputSettings: {},
},
{
name: "New password",
icon: "/assets/icons/change_password.svg",
id: "password",
inputSettings: {
password: true,
},
},
{
name: "Update password",
icon: "/assets/icons/update.svg",
description:
"This will log the target user out of all sessions",
id: true,
},
])
.then((exp) => {
if (exp && exp.selected) {
fetch(`/admin/reset`, {
method: "POST",
body: JSON.stringify({
target: exp.target,
password: exp.password,
}),
}).then((response) => {
if (response.status != 200) {
optPicker.picker(
`${response.status} ${
response.headers.get(
"x-backup-status-message"
) ||
response.statusText ||
""
}`,
[]
)
}
})
}
})
}
export function chgOwner(optPicker: OptionPicker) {
optPicker
.picker("Transfer file ownership", [
{
name: "File ID",
icon: "/assets/icons/file.svg",
id: "file",
inputSettings: {},
},
{
name: "New owner",
icon: "/assets/icons/person.svg",
id: "owner",
inputSettings: {},
},
{
name: "Transfer file ownership",
icon: "/assets/icons/update.svg",
description: "This will transfer the file to this user",
id: true,
},
])
.then((exp) => {
if (exp && exp.selected) {
fetch(`/admin/transfer`, {
method: "POST",
body: JSON.stringify({
owner: exp.owner,
target: exp.file,
}),
}).then((response) => {
if (response.status != 200) {
optPicker.picker(
`${response.status} ${
response.headers.get(
"x-backup-status-message"
) ||
response.statusText ||
""
}`,
[]
)
}
})
}
})
}
export function chgId(optPicker: OptionPicker) {
optPicker
.picker("Change file ID", [
{
name: "Target file",
icon: "/assets/icons/file.svg",
id: "file",
inputSettings: {},
},
{
name: "New ID",
icon: "/assets/icons/admin/change_file_id.svg",
id: "new",
inputSettings: {},
},
{
name: "Update",
icon: "/assets/icons/update.svg",
description: "File will not be available at its old ID",
id: true,
},
])
.then((exp) => {
if (exp && exp.selected) {
fetch(`/admin/idchange`, {
method: "POST",
body: JSON.stringify({
target: exp.file,
new: exp.new,
}),
}).then((response) => {
if (response.status != 200) {
optPicker.picker(
`${response.status} ${
response.headers.get(
"x-backup-status-message"
) ||
response.statusText ||
""
}`,
[]
)
}
})
}
})
}
export function delFile(optPicker: OptionPicker) {
optPicker
.picker("Delete file", [
{
name: "File ID",
icon: "/assets/icons/file.svg",
id: "file",
inputSettings: {},
},
{
name: "Delete",
icon: "/assets/icons/admin/delete_file.svg",
description: "This can't be undone",
id: true,
},
])
.then((exp) => {
if (exp && exp.selected) {
fetch(`/admin/delete`, {
method: "POST",
body: JSON.stringify({
target: exp.file,
}),
}).then((response) => {
if (response.status != 200) {
optPicker.picker(
`${response.status} ${
response.headers.get(
"x-backup-status-message"
) ||
response.statusText ||
""
}`,
[]
)
}
})
}
})
}
export function elevateUser(optPicker: OptionPicker) {
optPicker
.picker("Elevate user", [
{
name: "Username",
icon: "/assets/icons/person.svg",
id: "user",
inputSettings: {},
},
{
name: "Elevate to admin",
icon: "/assets/icons/update.svg",
description: "",
id: true,
},
])
.then((exp) => {
if (exp && exp.selected) {
fetch(`/admin/elevate`, {
method: "POST",
body: JSON.stringify({
target: exp.user,
}),
}).then((response) => {
if (response.status != 200) {
optPicker.picker(
`${response.status} ${
response.headers.get(
"x-backup-status-message"
) ||
response.statusText ||
""
}`,
[]
)
}
})
}
})
}
// im really lazy so i just stole this from account.js
export function deleteAccount(optPicker: OptionPicker) {
optPicker
.picker("What should we do with the target account's files?", [
{
name: "Delete files",
icon: "/assets/icons/admin/delete_file.svg",
description: "Files will be permanently deleted",
id: true,
},
{
name: "Do nothing",
icon: "/assets/icons/file.svg",
description: "Files will not be affected",
id: false,
},
])
.then((exp) => {
if (exp) {
let deleteFiles = exp.selected
optPicker
.picker(
`Enter the target account's username to continue.`,
[
{
name: "Enter account username",
icon: "/assets/icons/person.svg",
inputSettings: {},
id: "username",
},
{
name: "Optional reason",
icon: "/assets/icons/more.svg",
inputSettings: {},
id: "reason",
},
{
name: `Delete account ${
deleteFiles ? "& its files" : ""
}`,
icon: "/assets/icons/delete_account.svg",
description: `This cannot be undone.`,
id: true,
},
]
)
.then((fin) => {
if (fin && fin.selected) {
fetch(`/admin/delete_account`, {
method: "POST",
body: JSON.stringify({
target: fin.username,
reason: fin.reason,
deleteFiles,
}),
}).then((response) => {
if (response.status != 200) {
optPicker.picker(
`${response.status} ${
response.headers.get(
"x-backup-status-message"
) ||
response.statusText ||
""
}`,
[]
)
}
fetchAccountData()
})
}
})
}
})
}

View file

@ -0,0 +1,272 @@
import { fetchAccountData, fetchFilePointers, account } from "../stores"
import { get } from "svelte/store"
import type OptionPicker from "./OptionPicker.svelte"
import type { FilePointer } from "../../../lib/files"
export let options = {
FV: [
{
name: "Public",
icon: "/assets/icons/public.svg",
description: "Everyone can view your uploads",
id: "public",
},
{
name: "Anonymous",
icon: "/assets/icons/anonymous.svg",
description: "Your username will be hidden",
id: "anonymous",
},
{
name: "Private",
icon: "/assets/icons/private.svg",
description: "Nobody but you can view your uploads",
id: "private",
},
],
FV2: [
{
name: "Public",
icon: "/assets/icons/public.svg",
description: "Everyone can view this file",
id: "public",
},
{
name: "Anonymous",
icon: "/assets/icons/anonymous.svg",
description: "Your username will be hidden",
id: "anonymous",
},
{
name: "Private",
icon: "/assets/icons/private.svg",
description: "Nobody but you can view this file",
id: "private",
},
],
AYS: [
{
name: "Yes",
icon: "/assets/icons/update.svg",
id: true,
},
],
}
export function dfv(optPicker: OptionPicker) {
optPicker.picker("Default file visibility", options.FV).then((exp) => {
if (exp && exp.selected) {
fetch(`/auth/dfv`, {
method: "POST",
body: JSON.stringify({
defaultFileVisibility: exp.selected,
}),
}).then((response) => {
if (response.status != 200) {
optPicker.picker(
`${response.status} ${
response.headers.get("x-backup-status-message") ||
response.statusText ||
""
}`,
[]
)
}
fetchAccountData()
})
}
})
}
export function update_all_files(optPicker: OptionPicker) {
optPicker
.picker("You sure?", [
{
name: "Yeah",
icon: "/assets/icons/update.svg",
description: `This will make all of your files ${
get(account)?.defaultFileVisibility || "public"
}`,
id: true,
},
])
.then((exp) => {
if (exp && exp.selected) {
fetch(`/files/manage`, {
method: "POST",
body: JSON.stringify({
target: get(account)?.files,
action: "changeFileVisibility",
value: get(account)?.defaultFileVisibility,
}),
}).then((response) => {
if (response.status != 200) {
optPicker.picker(
`${response.status} ${
response.headers.get(
"x-backup-status-message"
) ||
response.statusText ||
""
}`,
[]
)
}
fetchAccountData()
})
}
})
}
export function fileOptions(
optPicker: OptionPicker,
file: FilePointer & { id: string }
) {
optPicker
.picker(file.filename, [
{
name: file.tag ? "Remove tag" : "Tag file",
icon: `/assets/icons/${file.tag ? "tag_remove" : "tag"}.svg`,
description: file.tag || `File has no tag`,
id: "tag",
},
{
name: "Change file visibility",
icon: `/assets/icons/${file.visibility || "public"}.svg`,
description: `File is currently ${file.visibility || "public"}`,
id: "changeFileVisibility",
},
{
name: "Delete file",
icon: `/assets/icons/admin/delete_file.svg`,
description: ``,
id: "delete",
},
])
.then((exp) => {
if (exp && exp.selected) {
switch (exp.selected) {
case "delete":
fetch(`/files/manage`, {
method: "POST",
body: JSON.stringify({
target: [file.id],
action: "delete",
}),
}).then((response) => {
if (response.status != 200) {
optPicker.picker(
`${response.status} ${
response.headers.get(
"x-backup-status-message"
) ||
response.statusText ||
""
}`,
[]
)
}
fetchFilePointers()
})
break
case "changeFileVisibility":
optPicker
.picker("Set file visibility", options.FV2)
.then((exp) => {
if (exp && exp.selected) {
fetch(`/files/manage`, {
method: "POST",
body: JSON.stringify({
target: [file.id],
action: "changeFileVisibility",
value: exp.selected,
}),
}).then((response) => {
if (response.status != 200) {
optPicker.picker(
`${response.status} ${
response.headers.get(
"x-backup-status-message"
) ||
response.statusText ||
""
}`,
[]
)
}
fetchFilePointers()
})
}
})
break
case "tag":
if (file.tag) {
fetch(`/files/manage`, {
method: "POST",
body: JSON.stringify({
target: [file.id],
action: "setTag",
}),
}).then(fetchFilePointers)
return
}
optPicker
.picker("Enter a tag (max 30char)", [
{
name: "Tag name",
icon: "/assets/icons/tag.svg",
id: "tag",
inputSettings: {},
},
{
name: "OK",
icon: "/assets/icons/update.svg",
description: "",
id: true,
},
])
.then((exp) => {
if (exp && exp.selected) {
fetch(`/files/manage`, {
method: "POST",
body: JSON.stringify({
target: [file.id],
action: "setTag",
value: exp.tag || null,
}),
}).then((response) => {
if (response.status != 200) {
optPicker.picker(
`${response.status} ${
response.headers.get(
"x-backup-status-message"
) ||
response.statusText ||
""
}`,
[]
)
}
fetchFilePointers()
})
}
})
break
}
}
})
}

View file

@ -0,0 +1,401 @@
<script lang="ts">
import Pulldown from "./Pulldown.svelte"
import { padding_scaleY } from "../transition/padding_scaleY"
import { circIn, circOut } from "svelte/easing"
import {
account,
fetchAccountData,
serverStats,
refreshNeeded,
} from "../stores"
import { fade } from "svelte/transition"
import OptionPicker from "../prompts/OptionPicker.svelte"
import * as accOpts from "../prompts/account"
import * as uplOpts from "../prompts/uploads"
import * as admOpts from "../prompts/admin"
let targetAction: "login" | "create"
let inProgress: boolean
let authError: { status: number; message: string } | undefined
let pwErr: HTMLDivElement
let optPicker: OptionPicker
// lazy
let username: string
let password: string
let execute = () => {
if (inProgress) return
inProgress = true
fetch(`/api/v1/session`, {
method: "POST",
body: JSON.stringify({
username,
password,
}),
})
.then(async (res) => {
inProgress = false
if (res.status != 200) {
authError = await res.json().catch(() => {
return {
status: res.status,
message:
res.headers.get("x-backup-status-message") ||
res.statusText ||
"",
}
})
} else {
;(authError = undefined), (username = ""), (password = "")
fetchAccountData()
}
})
.catch(() => {})
}
$: {
if (pwErr && authError) {
pwErr.animate(
{
backgroundColor: ["#885555", "#663333"],
easing: "ease-out",
},
650
)
}
}
// actual account menu
</script>
<Pulldown name="accounts">
<OptionPicker bind:this={optPicker} />
{#if $account}
<div class="loggedIn" transition:fade={{ duration: 200 }}>
<h1>
Hey there, <span class="monospace">@{$account.username}</span>
</h1>
<div class="optPicker">
<div class="category">
<p>Account</p>
</div>
<button on:click={() => accOpts.userChange(optPicker)}>
<img
src="/assets/icons/change_username.svg"
alt="change username"
/>
<p>Change username</p>
</button>
<button
on:click={() =>
($account?.email
? accOpts.emailPotentialRemove
: accOpts.emailChange)(optPicker)}
>
<img src="/assets/icons/mail.svg" alt="change email" />
<p>
Change email{#if $account.email}<span
class="monospaceText"
><br />{$account.email}</span
>{/if}
</p>
</button>
<button on:click={() => accOpts.pwdChng(optPicker)}>
<img
src="/assets/icons/change_password.svg"
alt="change password"
/>
<p>
Change password<span
><br />You will be logged out of all sessions</span
>
</p>
</button>
{#if !$account.admin}
<button on:click={() => accOpts.deleteAccount(optPicker)}>
<img
src="/assets/icons/delete_account.svg"
alt="delete account"
/>
<p>Delete account</p>
</button>
{/if}
<div class="category">
<p>Uploads</p>
</div>
<button on:click={() => uplOpts.dfv(optPicker)}>
<img
src={`/assets/icons/${$account.defaultFileVisibility || "public"}.svg`}
alt={$account.defaultFileVisibility || "public"}
/>
<p>
Default file visibility<span
><br />Uploads will be
<strong
>{$account.defaultFileVisibility ||
"public"}</strong
> by default</span
>
</p>
</button>
<button on:click={() => uplOpts.update_all_files(optPicker)}>
<img src="/assets/icons/update.svg" alt="update" />
<p>
Make all of my files {$account.defaultFileVisibility ||
"public"}<span
><br />Matches your default file visibility</span
>
</p>
</button>
<div class="category">
<p>Customization</p>
</div>
<button on:click={() => accOpts.customcss(optPicker)}>
<img src="/assets/icons/paint.svg" alt="customcss" />
<p>
Set custom CSS<span
><br />{@html $account.customCSS
? `Using file ID <span class="number">${$account.customCSS}</span>`
: "No custom CSS set"}</span
>
</p>
</button>
<button on:click={() => accOpts.embedColor(optPicker)}>
<img src="/assets/icons/pound.svg" alt="embedColor" />
<p>
Set custom embed color<span
><br />{@html $account?.embed?.color
? `Using custom color <span class="number">${$account?.embed?.color}</span>`
: ""}</span
>
</p>
</button>
<button on:click={() => accOpts.embedSize(optPicker)}>
<img src="/assets/icons/image.svg" alt="embedSize" />
<p>
Set embed image size <span
><br />Images currently appear {$account?.embed
?.largeImage
? `large`
: "small"} in embeds</span
>
</p>
</button>
{#if $refreshNeeded}
<button
on:click={() => window.location.reload()}
transition:fade={{ duration: 200 }}
>
<img src="/assets/icons/refresh.svg" alt="refresh" />
<p>
Refresh<span
><br />Changes were made which require a refresh</span
>
</p>
</button>
{/if}
<div class="category">
<p>Sessions</p>
</div>
<button
on:click={() =>
fetch(`/api/v1/auth/logout_sessions`, {
method: "POST",
}).then(() => fetchAccountData())}
>
<img src="/assets/icons/logout_all.svg" alt="logout_all" />
<p>
Log out all sessions<span
><br />{$account?.sessionCount} session(s) active</span
>
</p>
</button>
<button
on:click={() =>
fetch(`/api/v1/session`, { method: "DELETE" }).then(
() => fetchAccountData()
)}
>
<img src="/assets/icons/logout.svg" alt="logout" />
<p>
Log out<span
><br />Session expires {new Date(
$account?.sessionExpires
).toLocaleDateString()}</span
>
</p>
</button>
{#if $account.admin}
<div class="category">
<p>Admin</p>
</div>
<button on:click={() => admOpts.deleteAccount(optPicker)}>
<img
src="/assets/icons/delete_account.svg"
alt="delete account"
/>
<p>Delete user account</p>
</button>
<button on:click={() => admOpts.pwdReset(optPicker)}>
<img
src="/assets/icons/change_password.svg"
alt="change password"
/>
<p>Change user password</p>
</button>
<button on:click={() => admOpts.elevateUser(optPicker)}>
<img
src="/assets/icons/admin/elevate_user.svg"
alt="elevate account"
/>
<p>Elevate account to admin</p>
</button>
<button on:click={() => admOpts.chgOwner(optPicker)}>
<img
src="/assets/icons/link.svg"
alt="change file owner"
/>
<p>Change file owner</p>
</button>
<button on:click={() => admOpts.chgId(optPicker)}>
<img
src="/assets/icons/admin/change_file_id.svg"
alt="change file id"
/>
<p>
Change file ID<span
><br />Potentially buggy, usage not recommended</span
>
</p>
</button>
<button on:click={() => admOpts.delFile(optPicker)}>
<img
src="/assets/icons/admin/delete_file.svg"
alt="delete file"
/>
<p>Delete file</p>
</button>
{/if}
<p
style="font-size:12px;color:#AAAAAA;text-align:center;"
class="monospace"
>
<br />{$account.id}
</p>
</div>
</div>
{:else}
<div class="notLoggedIn" transition:fade={{ duration: 200 }}>
<div class="container_div">
<h1>monofile <span style:color="#999999">accounts</span></h1>
<p class="flavor">Gain control of your uploads.</p>
{#if targetAction}
<div
class="fields"
out:padding_scaleY|local={{ easingFunc: circIn }}
in:padding_scaleY|local
>
{#if !$serverStats?.accounts.registrationEnabled && targetAction == "create"}
<div class="pwError">
<div style:background-color="#554C33">
<p>
Account registration has been disabled
by this instance's owner
</p>
</div>
</div>
{/if}
{#if authError}
<div
class="pwError"
out:padding_scaleY|local={{
easingFunc: circIn,
}}
in:padding_scaleY|local
>
<div bind:this={pwErr}>
<p>
<strong>{authError.status}</strong>
{authError.message}
</p>
</div>
</div>
{/if}
<input
placeholder="username"
type="text"
bind:value={username}
/>
<input
placeholder="password"
type="password"
bind:value={password}
/>
<button on:click={execute}
>{@html inProgress
? "<span class=loader><i></i> <i></i> <i></i></span>"
: targetAction == "login"
? "Log in"
: "Create account"}</button
>
{#if targetAction == "login"}
<button
class="flavor"
on:click={() =>
accOpts.forgotPassword(optPicker)}
>I forgot my password</button
>
{/if}
</div>
{:else}
<div
class="lgBtnContainer"
out:padding_scaleY|local={{ easingFunc: circIn }}
in:padding_scaleY|local
>
<button on:click={() => (targetAction = "login")}
>Log in</button
>
<button on:click={() => (targetAction = "create")}
>Sign up</button
>
</div>
{/if}
</div>
</div>
{/if}
</Pulldown>

View file

@ -0,0 +1,100 @@
<script lang="ts">
import Pulldown from "./Pulldown.svelte"
import {
account,
fetchFilePointers,
files,
pulldownManager,
} from "../stores.js"
import { fade } from "svelte/transition"
import { flip } from "svelte/animate"
import { fileOptions } from "../prompts/uploads"
import OptionPicker from "../prompts/OptionPicker.svelte"
let picker: OptionPicker
let query = ""
fetchFilePointers()
</script>
<Pulldown name="files">
<OptionPicker bind:this={picker} />
{#if $account?.username}<div class="loggedIn">
<input
type="text"
placeholder={`Search ${$files.length} file(s)`}
class="searchBar"
bind:value={query}
/>
<div class="fileList">
<!-- Probably wildly inefficient but who cares, I just wanna get this over with -->
{#each $files.filter((f) => f && (f.filename
.toLowerCase()
.includes(query.toLowerCase()) || f.id
.toLowerCase()
.includes(query.toLowerCase()) || f.tag?.includes(query.toLowerCase()))) as file (file.id)}
<div
class="flFile"
transition:fade={{ duration: 200 }}
animate:flip={{ duration: 200 }}
>
<button
class="hitbox"
on:click={() => window.open(`/download/${file.id}`)}
></button>
<!-- this is bad, but I'm lazy -->
<div class="flexCont">
<div class="fileInfo">
<h2>{file.filename}</h2>
<p class="detail">
<img
src="/assets/icons/{file.visibility ||
'public'}.svg"
alt={file.visibility || "public"}
/>&nbsp;
<span class="number">{file.id}</span
>&nbsp;&nbsp;&nbsp;&nbsp;<span class="cd"
>{file.mime.split(";")[0]}</span
>
{#if file.reserved}
<br />
<img
src="/assets/icons/update.svg"
alt="uploading"
/>&nbsp; Uploading...
{/if}
{#if file.tag}
<br />
<img
src="/assets/icons/tag.svg"
alt="tag"
/>&nbsp;
<span class="cd">{file.tag}</span>
{/if}
</p>
</div>
<button
class="more"
on:click={() => fileOptions(picker, file)}
>
<img src="/assets/icons/more.svg" alt="more" />
</button>
</div>
</div>
{/each}
</div>
</div>
{:else}
<div class="notLoggedIn">
<div style:height="10px" />
<p class="flavor">Log in to view uploads</p>
<button on:click={$pulldownManager.openPulldown("account")}
>OK</button
>
<div style:height="14px" />
</div>
{/if}
</Pulldown>

View file

@ -0,0 +1,79 @@
import { writable } from "svelte/store"
//import type Pulldown from "./pulldowns/Pulldown.svelte"
import type { SvelteComponent } from "svelte"
import type { Account } from "../../lib/accounts"
import type { FilePointer } from "../../lib/files"
type cfg = {
maxDiscordFiles: number
maxDiscordFileSize: number
targetChannel: string
requestTimeout: number
maxUploadIdLength: number
accounts: {
registrationEnabled: boolean
requiredForUpload: boolean
}
mail: {
transport: {
host: string
port: number
secure: boolean
}
send: {
from: string
}
}
trustProxy: boolean
forceSSL: boolean
}
export let refreshNeeded = writable(false)
export let pulldownManager = writable<SvelteComponent>()
export let account = writable<
(Account & { sessionCount: number; sessionExpires: number }) | undefined
>()
export let serverStats = writable<
(cfg & { version: string; files: number }) | undefined
>()
export let files = writable<(FilePointer & { id: string })[]>([])
export let fetchAccountData = function () {
fetch("/auth/me")
.then(async (response) => {
if (response.status == 200) {
account.set(await response.json())
} else {
account.set(undefined)
}
})
.catch((err) => {
console.error(err)
})
}
export let fetchFilePointers = function () {
fetch("/files/list", { cache: "no-cache" })
.then(async (response) => {
if (response.status == 200) {
files.set(await response.json())
} else {
files.set([])
}
})
.catch((err) => {
console.error(err)
})
}
export let refresh_stats = () => {
fetch("/server")
.then(async (data) => {
serverStats.set(await data.json())
})
.catch((err) => {
console.error(err)
})
}
fetchAccountData()

View file

@ -0,0 +1,46 @@
import * as Accounts from "$lib/accounts"
import { files_singleton as files } from "$lib/files"
import { error } from "@sveltejs/kit"
export const load: import("./$types").PageServerLoad = async ({
params,
url,
cookies,
request,
}) => {
const acc = Accounts.getFromToken(cookies.get("auth")!)
const { fileId } = params
const host = url.host
const file = files[fileId]
if (file) {
if (file.visibility == "private" && acc?.id != file.owner) {
throw error(403, "you do not own this file")
}
let fileOwner = file.owner ? Accounts.getFromId(file.owner) : undefined
return {
embedColor:
fileOwner?.embed?.color &&
file.visibility != "anonymous" &&
request.headers.get("user-agent")?.includes("Discordbot")
? `#${fileOwner.embed.color}`
: "rgb(30, 33, 36)",
mime: file.mime,
size: file.sizeInBytes,
filename: file.filename,
url: `https://${host}/file/${fileId}`,
owner:
!file.owner || file.visibility == "anonymous"
? "Anonymous"
: `@${fileOwner?.username || "Deleted User"}`,
visibility: file.visibility,
largeImage:
fileOwner?.embed?.largeImage &&
file.visibility != "anonymous" &&
file.mime.startsWith("image/"),
id: fileId,
}
} else throw error(404, "file not found")
}
export const csr = false

View file

@ -0,0 +1,75 @@
<script>
import "../../../style/downloads.scss"
import {page} from "$app/stores"
import bytes from "bytes"
export let data
</script>
<svelte:head>
<title>{data.id}</title>
<meta name="og:site_name" content={data.owner}>
<meta name="title" content={data.filename}>
<meta name="description" content="{bytes(data.size)} file on monofile {MONOFILE_VERSION}, the Discord-based file sharing service">
<meta name="theme-color" content={data.embedColor}>
<link
rel="stylesheet"
href="./style/downloads.scss"
>
<link
rel="stylesheet"
href="/api/v1/account/me/css"
>
<link
rel="icon"
type="image/svg"
href="/assets/icons/file_icon.svg"
>
<link rel="canonical" href="{$page.url.origin}/download/{data.id}" />
{#if data.mime.startsWith("image/")}
{#if data.largeImage}
<meta name="twitter:card" content="summary_large_image">
{/if}
<meta name="og:image" content="{$page.url.origin}/file/{data.id}">
{/if}
{#if data.mime.startsWith("video/")}
<meta property="og:video:url" content="{$page.url.origin}/cpt/{data.id}/video.{data.mime.split("/")[1] == "quicktime" ? "mov" : data.mime.split("/")[1]}" />
<meta property="og:video:secure_url" content="{$page.url.origin}/cpt/{data.id}/video.{data.mime.split("/")[1] == "quicktime" ? "mov" : data.mime.split("/")[1]}" />
<meta property="og:type" content="video.other">
<!-- honestly probably good enough for now -->
<meta property="twitter:image" content="0">
<!-- quick lazy fix as a fallback -->
<!-- maybe i'll improve this later, but probably not. -->
{#if data.size >= 26214400}
<meta property="og:video:width" content="1280">
<meta property="og:video:height" content="720">
{/if}
{/if}
</svelte:head>
<div id="appContent">
<main>
<h1>{data.filename}</h1>
<p style="color:#999999">
<span class="number">{bytes(data.size)}</span>&nbsp;&nbsp;&nbsp;&nbsp;uploaded by <span class="number">{data.owner}</span>
</p>
{#if data.mime.startsWith("image/")}
<div style="min-height:10px"></div><img src="/file/{data.id}" />
{:else if data.mime.startsWith("video/")}
<div style="min-height:10px"></div><video src="/file/{data.id}" controls></video>
{:else if data.mime.startsWith("audio/")}
<div style="min-height:10px"></div><audio src="/file/{data.id}" controls></audio>
{/if}
<button style="position:relative;width:100%;top:10px;">
<a id="dlbtn" href="/api/v1/file/{data.id}" download={data.filename} style="position:absolute;left:0px;top:0px;height:100%;width:100%;"></a>
download
</button>
<div style="min-height:15px" />
</main>
</div>

View file

@ -1,136 +0,0 @@
import { serve } from "@hono/node-server"
import { serveStatic } from "@hono/node-server/serve-static"
import { Hono } from "hono"
import fs from "fs"
import { readFile } from "fs/promises"
import Files from "./lib/files.js"
import { getAccount } from "./lib/middleware.js"
import APIRouter from "./routes/api.js"
import preview from "./routes/api/web/preview.js"
import {fileURLToPath} from "url"
import {dirname} from "path"
import pkg from "../../package.json" assert {type:"json"}
import config from "../../config.json" assert {type:"json"}
const app = new Hono()
app.get(
"/static/assets/*",
serveStatic({
rewriteRequestPath: (path) => {
return path.replace("/static/assets", "/assets")
},
})
)
app.get(
"/static/vite/*",
serveStatic({
rewriteRequestPath: (path) => {
return path.replace("/static/vite", "/dist/static/vite")
},
})
)
// respond to the MOLLER method
// get it?
// haha...
app.on(["MOLLER"], "*", async (ctx) => {
ctx.header("Content-Type", "image/webp")
return ctx.body( await readFile("./assets/moller.png") )
})
//app.use(bodyParser.text({limit:(config.maxDiscordFileSize*config.maxDiscordFiles)+1048576,type:["application/json","text/plain"]}))
// check for ssl, if not redirect
if (config.trustProxy) {
// app.enable("trust proxy")
}
if (config.forceSSL) {
app.use(async (ctx, next) => {
if (new URL(ctx.req.url).protocol == "http") {
return ctx.redirect(
`https://${ctx.req.header("host")}${
new URL(ctx.req.url).pathname
}`
)
} else {
return next()
}
})
}
app.get("/server", (ctx) =>
ctx.json({
...config,
version: pkg.version,
files: Object.keys(files.files).length,
})
)
// funcs
// init data
const __dirname = dirname(fileURLToPath(import.meta.url))
if (!fs.existsSync(__dirname + "/../.data/"))
fs.mkdirSync(__dirname + "/../.data/")
// discord
let files = new Files(config)
const apiRouter = new APIRouter(files)
apiRouter.loadAPIMethods().then(() => {
app.route("/", apiRouter.root)
console.log("API OK!")
// moved here to ensure it's matched last
app.get("/:fileId", async (ctx) =>
app.fetch(
new Request(
(new URL(
`/api/v1/file/${ctx.req.param("fileId")}`, ctx.req.raw.url)).href,
ctx.req.raw
),
ctx.env
)
)
// listen on 3000 or MONOFILE_PORT
// moved here to prevent a crash if someone manages to access monofile before api routes are mounted
serve(
{
fetch: app.fetch,
port: Number(process.env.MONOFILE_PORT || 3000),
serverOptions: {
//@ts-ignore
requestTimeout: config.requestTimeout
}
},
(info) => {
console.log("Web OK!", info.port, info.address)
}
)
})
// index, clone
app.get("/", async (ctx) =>
ctx.html(
await fs.promises.readFile(process.cwd() + "/dist/index.html", "utf-8")
)
)
/*
routes should be in this order:
index
api
dl pages
file serving
*/
export default app

View file

@ -1,36 +0,0 @@
import { readFile } from "fs/promises"
import type { Context } from "hono"
import type { StatusCode } from "hono/utils/http-status"
let errorPage: string
/**
* @description Serves an error as a response to a request with an error page attached
* @param ctx Express response object
* @param code Error code
* @param reason Error reason
*/
export default async function ServeError(
ctx: Context,
code: number,
reason: string
) {
// fetch error page if not cached
errorPage ??= (
(await readFile(`${process.cwd()}/dist/error.html`).catch((err) =>
console.error(err)
)) ?? "<pre>$code $text</pre>"
).toString()
// serve error
return ctx.req.header("accept")?.includes("text/html") ? ctx.html(
errorPage
.replaceAll("$code", code.toString())
.replaceAll("$text", reason),
code as StatusCode/*,
{
"x-backup-status-message": reason, // glitch default nginx configuration
}*/
) : ctx.text(reason, code as StatusCode)
}

View file

@ -1,89 +0,0 @@
import { Hono } from "hono"
import { readFile, readdir } from "fs/promises"
import Files from "../lib/files.js"
import {fileURLToPath} from "url"
import {dirname} from "path"
const APIDirectory = dirname(fileURLToPath(import.meta.url)) + "/api"
interface APIMount {
file: string
to: string
}
type APIMountResolvable = string | APIMount
interface APIDefinition {
name: string
baseURL: string
mount: APIMountResolvable[]
}
function resolveMount(mount: APIMountResolvable): APIMount {
return typeof mount == "string" ? { file: mount, to: "/" + mount } : mount
}
class APIVersion {
readonly definition: APIDefinition
readonly apiPath: string
readonly apiRoot: Hono
readonly root: Hono = new Hono()
readonly files: Files
constructor(definition: APIDefinition, files: Files, apiRoot: Hono) {
this.definition = definition
this.apiPath = APIDirectory + "/" + definition.name
this.files = files
this.apiRoot = apiRoot
}
async load() {
for (let _mount of this.definition.mount) {
let mount = resolveMount(_mount);
// no idea if there's a better way to do this but this is all i can think of
let { default: route } = await import(`${this.apiPath}/${mount.file}.js`) as { default: (files: Files, apiRoot: Hono) => Hono }
this.root.route(mount.to, route(this.files, this.apiRoot))
}
}
}
export default class APIRouter {
readonly files: Files
readonly root: Hono = new Hono()
constructor(files: Files) {
this.files = files
}
/**
* @description Mounts an APIDefinition to the APIRouter.
* @param definition Definition to mount.
*/
private async mount(definition: APIDefinition) {
console.log(`mounting APIDefinition ${definition.name}`)
let def = new APIVersion(definition, this.files, this.root)
await def.load()
this.root.route(
definition.baseURL,
def.root
)
}
async loadAPIMethods() {
let files = await readdir(APIDirectory)
for (let version of files) {
let def = JSON.parse(
(
await readFile(
`${process.cwd()}/src/server/routes/api/${version}/api.json`
)
).toString()
) as APIDefinition
await this.mount(def)
}
}
}

View file

@ -1,10 +0,0 @@
{
"name": "v0",
"baseURL": "/",
"mount": [
{ "file": "primaryApi", "to": "/" },
{ "file": "adminRoutes", "to": "/admin" },
{ "file": "authRoutes", "to": "/auth" },
{ "file": "fileApiRoutes", "to": "/files" }
]
}

View file

@ -1,50 +0,0 @@
import { Hono } from "hono"
import * as Accounts from "../../../lib/accounts.js"
import * as auth from "../../../lib/auth.js"
import RangeParser, { type Range } from "range-parser"
import ServeError from "../../../lib/errors.js"
import Files, { WebError } from "../../../lib/files.js"
import { getAccount, requiresPermissions } from "../../../lib/middleware.js"
import {Readable} from "node:stream"
import type {ReadableStream as StreamWebReadable} from "node:stream/web"
import formidable from "formidable"
import { HttpBindings } from "@hono/node-server"
import pkg from "../../../../../package.json" assert {type: "json"}
import { type StatusCode } from "hono/utils/http-status"
export let primaryApi = new Hono<{
Variables: {
account: Accounts.Account
},
Bindings: HttpBindings
}>()
primaryApi.all("*", getAccount)
export default function (files: Files, apiRoot: Hono) {
primaryApi.get("/file/:fileId", async (ctx) =>
apiRoot.fetch(
new Request(
(new URL(
`/api/v1/file/${ctx.req.param("fileId")}`, ctx.req.raw.url)).href,
ctx.req.raw
),
ctx.env
)
)
primaryApi.post("/upload", async (ctx) =>
apiRoot.fetch(
new Request(
(new URL(
`/api/v1/file`, ctx.req.raw.url)).href,
{
...ctx.req.raw,
method: "PUT"
}
),
ctx.env
)
)
return primaryApi
}

View file

@ -1,384 +0,0 @@
// Modules
import { type Context, Hono } from "hono"
import { getCookie, setCookie } from "hono/cookie"
// Libs
import Files, { id_check_regex } from "../../../lib/files.js"
import * as Accounts from "../../../lib/accounts.js"
import * as auth from "../../../lib/auth.js"
import {
assertAPI,
getAccount,
login,
noAPIAccess,
requiresAccount,
requiresPermissions,
} from "../../../lib/middleware.js"
import ServeError from "../../../lib/errors.js"
import { CodeMgr, sendMail } from "../../../lib/mail.js"
import Configuration from "../../../../../config.json" assert {type:"json"}
const router = new Hono<{
Variables: {
account: Accounts.Account,
target: Accounts.Account
}
}>()
type UserUpdateParameters = Partial<Omit<Accounts.Account, "password"> & { password: string, currentPassword?: string }>
type Message = [200 | 400 | 401 | 403 | 429 | 501, string]
// there's probably a less stupid way to do this than `K in keyof Pick<UserUpdateParameters, T>`
// @Jack5079 make typings better if possible
type Validator<T extends keyof Partial<Accounts.Account>, ValueNotNull extends boolean> =
/**
* @param actor The account performing this action
* @param target The target account for this action
* @param params Changes being patched in by the user
*/
(actor: Accounts.Account, target: Accounts.Account, params: UserUpdateParameters & (ValueNotNull extends true ? {
[K in keyof Pick<UserUpdateParameters, T>]-? : UserUpdateParameters[K]
} : {}), ctx: Context) => Accounts.Account[T] | Message
// this type is so stupid stg
type ValidatorWithSettings<T extends keyof Partial<Accounts.Account>> = {
acceptsNull: true,
validator: Validator<T, false>
} | {
acceptsNull?: false,
validator: Validator<T, true>
}
const validators: {
[T in keyof Partial<Accounts.Account>]:
Validator<T, true> | ValidatorWithSettings<T>
} = {
defaultFileVisibility(actor, target, params) {
if (["public", "private", "anonymous"].includes(params.defaultFileVisibility))
return params.defaultFileVisibility
else return [400, "invalid file visibility"]
},
email: {
acceptsNull: true,
validator: (actor, target, params, ctx) => {
if (!params.currentPassword // actor on purpose here to allow admins
|| (params.currentPassword && Accounts.password.check(actor.id, params.currentPassword)))
return [401, "current password incorrect"]
if (!params.email) {
if (target.email) {
sendMail(
target.email,
`Email disconnected`,
`<b>Hello there!</b> Your email address (<span code>${target.email}</span>) has been disconnected from the monofile account <span username>${target.username}</span>. Thank you for using monofile.`
).catch()
}
return undefined
}
if (typeof params.email !== "string") return [400, "email must be string"]
if (actor.admin)
return params.email
// send verification email
if ((CodeMgr.codes.verifyEmail.byUser.get(target.id)?.length || 0) >= 2) return [429, "you have too many active codes"]
let code = new CodeMgr.Code("verifyEmail", target.id, params.email)
sendMail(
params.email,
`Hey there, ${target.username} - let's connect your email`,
`<b>Hello there!</b> You are recieving this message because you decided to link your email, <span code>${
params.email.split("@")[0]
}<span style="opacity:0.5">@${
params.email.split("@")[1]
}</span></span>, to your account, <span username>${
target.username
}</span>. If you would like to continue, please <a href="https://${ctx.req.header(
"Host"
)}/go/verify/${code.id}"><span code>click here</span></a>, or go to https://${ctx.req.header(
"Host"
)}/go/verify/${code.id}.`
)
return [200, "please check your inbox"]
}
},
password(actor, target, params) {
if (
!params.currentPassword // actor on purpose here to allow admins
|| (params.currentPassword && Accounts.password.check(actor.id, params.currentPassword))
) return [401, "current password incorrect"]
if (
typeof params.password != "string"
|| params.password.length < 8
) return [400, "password must be 8 characters or longer"]
if (target.email) {
sendMail(
target.email,
`Your login details have been updated`,
`<b>Hello there!</b> Your password on your account, <span username>${target.username}</span>, has been updated`
+ `${actor != target ? ` by <span username>${actor.username}</span>` : ""}. `
+ `Please update your saved login details accordingly.`
).catch()
}
return Accounts.password.hash(params.password)
},
username(actor, target, params) {
if (!params.currentPassword // actor on purpose here to allow admins
|| (params.currentPassword && Accounts.password.check(actor.id, params.currentPassword)))
return [401, "current password incorrect"]
if (
typeof params.username != "string"
|| params.username.length < 3
|| params.username.length > 20
) return [400, "username must be between 3 and 20 characters in length"]
if (Accounts.getFromUsername(params.username))
return [400, "account with this username already exists"]
if ((params.username.match(/[A-Za-z0-9_\-\.]+/) || [])[0] != params.username)
return [400, "username has invalid characters"]
if (target.email) {
sendMail(
target.email,
`Your login details have been updated`,
`<b>Hello there!</b> Your username on your account, <span username>${target.username}</span>, has been updated`
+ `${actor != target ? ` by <span username>${actor.username}</span>` : ""} to <span username>${params.username}</span>. `
+ `Please update your saved login details accordingly.`
).catch()
}
return params.username
},
customCSS: {
acceptsNull: true,
validator: (actor, target, params) => {
if (
!params.customCSS ||
(params.customCSS.match(id_check_regex)?.[0] == params.customCSS &&
params.customCSS.length <= Configuration.maxUploadIdLength)
) return params.customCSS
else return [400, "bad file id"]
}
},
embed(actor, target, params) {
if (typeof params.embed !== "object") return [400, "must use an object for embed"]
if (params.embed.color === undefined) {
params.embed.color = target.embed?.color
} else if (!((params.embed.color.toLowerCase().match(/[a-f0-9]+/)?.[0] ==
params.embed.color.toLowerCase() &&
params.embed.color.length == 6) || params.embed.color == null)) return [400, "bad embed color"]
if (params.embed.largeImage === undefined) {
params.embed.largeImage = target.embed?.largeImage
} else params.embed.largeImage = Boolean(params.embed.largeImage)
return params.embed
},
admin(actor, target, params) {
if (actor.admin && !target.admin) return params.admin
else if (!actor.admin) return [400, "cannot promote yourself"]
else return [400, "cannot demote an admin"]
}
}
router.use(getAccount)
router.all("/:user", async (ctx, next) => {
let acc =
ctx.req.param("user") == "me"
? ctx.get("account")
: (
ctx.req.param("user").startsWith("@")
? Accounts.getFromUsername(ctx.req.param("user").slice(1))
: Accounts.getFromId(ctx.req.param("user"))
)
if (
acc != ctx.get("account")
&& !ctx.get("account")?.admin
) return ServeError(ctx, 403, "you cannot manage this user")
if (!acc) return ServeError(ctx, 404, "account does not exist")
ctx.set("target", acc)
return next()
})
function isMessage(object: any): object is Message {
return Array.isArray(object)
&& object.length == 2
&& typeof object[0] == "number"
&& typeof object[1] == "string"
}
export default function (files: Files) {
router.post("/", async (ctx) => {
const body = await ctx.req.json()
if (!Configuration.accounts.registrationEnabled) {
return ServeError(ctx, 403, "account registration disabled")
}
if (auth.validate(getCookie(ctx, "auth")!)) {
return ServeError(ctx, 400, "you are already logged in")
}
if (Accounts.getFromUsername(body.username)) {
return ServeError(
ctx,
400,
"account with this username already exists"
)
}
if (body.username.length < 3 || body.username.length > 20) {
return ServeError(
ctx,
400,
"username must be over or equal to 3 characters or under or equal to 20 characters in length"
)
}
if (
(body.username.match(/[A-Za-z0-9_\-\.]+/) || [])[0] != body.username
) {
return ServeError(ctx, 400, "username contains invalid characters")
}
if (body.password.length < 8) {
return ServeError(
ctx,
400,
"password must be 8 characters or longer"
)
}
return Accounts.create(body.username, body.password)
.then((account) => {
login(ctx, account)
return ctx.text("logged in")
})
.catch(() => {
return ServeError(ctx, 500, "internal server error")
})
})
router.patch(
"/:user",
requiresAccount,
requiresPermissions("manage"),
async (ctx) => {
const body = await ctx.req.json() as UserUpdateParameters
const actor = ctx.get("account")!
const target = ctx.get("target")!
if (Array.isArray(body))
return ServeError(ctx, 400, "invalid body")
let results: ([keyof Accounts.Account, Accounts.Account[keyof Accounts.Account]]|Message)[] =
(Object.entries(body)
.filter(e => e[0] !== "currentPassword") as [keyof Accounts.Account, UserUpdateParameters[keyof Accounts.Account]][])
.map(([x, v]) => {
if (!validators[x])
return [400, `the ${x} parameter cannot be set or is not a valid parameter`] as Message
let validator =
(typeof validators[x] == "object"
? validators[x]
: {
validator: validators[x] as Validator<typeof x, false>,
acceptsNull: false
}) as ValidatorWithSettings<typeof x>
if (!validator.acceptsNull && !v)
return [400, `the ${x} validator does not accept null values`] as Message
return [
x,
validator.validator(actor, target, body as any, ctx)
] as [keyof Accounts.Account, Accounts.Account[keyof Accounts.Account]]
})
let allMsgs = results.map((v) => {
if (isMessage(v))
return v
target[v[0]] = v[1] as never // lol
return [200, "OK"] as Message
})
await Accounts.save()
if (allMsgs.length == 1)
return ctx.text(...allMsgs[0]!.reverse() as [Message[1], Message[0]]) // im sorry
else return ctx.json(allMsgs)
}
)
router.delete("/:user", requiresAccount, noAPIAccess, async (ctx) => {
let acc = ctx.get("target")
auth.AuthTokens.filter((e) => e.account == acc?.id).forEach(
(token) => {
auth.invalidate(token.token)
}
)
await Accounts.deleteAccount(acc.id)
if (acc.email) {
await sendMail(
acc.email,
"Notice of account deletion",
`Your account, <span username>${
acc.username
}</span>, has been removed. Thank you for using monofile.`
).catch()
return ctx.text("OK")
}
return ctx.text("account deleted")
})
router.get("/:user", requiresAccount, async (ctx) => {
let acc = ctx.get("target")
let sessionToken = auth.tokenFor(ctx)!
return ctx.json({
...acc,
password: undefined,
email:
auth.getType(sessionToken) == "User" ||
auth.getPermissions(sessionToken)?.includes("email")
? acc.email
: undefined,
activeSessions: auth.AuthTokens.filter(
(e) =>
e.type != "App" &&
e.account == acc.id &&
(e.expire > Date.now() || !e.expire)
).length,
})
})
router.get("/css", async (ctx) => {
let acc = ctx.get('account')
if (acc?.customCSS)
return ctx.redirect(`/file/${acc.customCSS}`)
else return ctx.text("")
})
return router
}

View file

@ -1,16 +0,0 @@
{
"name": "v1",
"baseURL": "/api/v1",
"mount": [
"account",
"session",
{
"file": "file/index",
"to": "/file"
},
{
"file": "file/individual",
"to": "/file"
}
]
}

View file

@ -1,155 +0,0 @@
import { Hono } from "hono"
import * as Accounts from "../../../../lib/accounts.js"
import * as auth from "../../../../lib/auth.js"
import RangeParser, { type Range } from "range-parser"
import ServeError from "../../../../lib/errors.js"
import Files, { WebError } from "../../../../lib/files.js"
import { getAccount, requiresPermissions } from "../../../../lib/middleware.js"
import {Readable} from "node:stream"
import type {ReadableStream as StreamWebReadable} from "node:stream/web"
import formidable from "formidable"
import { HttpBindings } from "@hono/node-server"
import pkg from "../../../../../../package.json" assert {type: "json"}
import { type StatusCode } from "hono/utils/http-status"
const router = new Hono<{
Variables: {
account: Accounts.Account
},
Bindings: HttpBindings
}>()
router.all("*", getAccount)
export default function(files: Files) {
router.on(
["PUT", "POST"],
"/",
requiresPermissions("upload"),
(ctx) => { return new Promise((resolve,reject) => {
ctx.env.incoming.removeAllListeners("data") // remove hono's buffering
let errEscalated = false
function escalate(err:Error) {
if (errEscalated) return
errEscalated = true
console.error(err)
if ("httpCode" in err)
ctx.status(err.httpCode as StatusCode)
else if (err instanceof WebError)
ctx.status(err.statusCode as StatusCode)
else ctx.status(400)
resolve(ctx.body(err.message))
}
let acc = ctx.get("account") as Accounts.Account | undefined
if (!ctx.req.header("Content-Type")?.startsWith("multipart/form-data"))
return resolve(ctx.body("must be multipart/form-data", 400))
if (!ctx.req.raw.body)
return resolve(ctx.body("body must be supplied", 400))
let file = files.createWriteStream(acc?.id)
file
.on("error", escalate)
.on("finish", async () => {
if (!ctx.env.incoming.readableEnded) await new Promise(res => ctx.env.incoming.once("end", res))
file.commit()
.then(id => resolve(ctx.body(id!)))
.catch(escalate)
})
let parser = formidable({
maxFieldsSize: 65536,
maxFileSize: files.config.maxDiscordFileSize*files.config.maxDiscordFiles,
maxFiles: 1
})
let acceptNewData = true
parser.onPart = function(part) {
if (!part.originalFilename || !part.mimetype) {
parser._handlePart(part)
return
}
// lol
if (part.name == "file") {
if (!acceptNewData || file.writableEnded)
return part.emit("error", new WebError(400, "cannot set file after previously setting up another upload"))
acceptNewData = false
file.setName(part.originalFilename || "")
file.setType(part.mimetype || "")
file.on("drain", () => ctx.env.incoming.resume())
file.on("error", (err) => part.emit("error", err))
part.on("data", (data: Buffer) => {
if (!file.write(data))
ctx.env.incoming.pause()
})
part.on("end", () => file.end())
}
}
parser.on("field", async (k,v) => {
if (k == "uploadId") {
if (files.files[v] && ctx.req.method == "POST")
return file.destroy(new WebError(409, "file already exists"))
file.setUploadId(v)
// I'M GONNA KILL MYSELF!!!!
} else if (k == "file") {
if (!acceptNewData || file.writableEnded)
return file.destroy(new WebError(400, "cannot set file after previously setting up another upload"))
acceptNewData = false
let res = await fetch(v, {
headers: {
"user-agent": `monofile ${pkg.version} (+https://${ctx.req.header("Host")})`
}
}).catch(escalate)
if (!res) return
if (!file
.setName(
res.headers.get("Content-Disposition")
?.match(/filename="(.*)"/)?.[1]
|| v.split("/")[
v.split("/").length - 1
] || "generic"
)) return
if (res.headers.has("Content-Type"))
if (!file.setType(res.headers.get("Content-Type")!))
return
if (!res.ok) return file.destroy(new WebError(500, `got ${res.status} ${res.statusText}`))
if (!res.body) return file.destroy(new WebError(500, `Internal Server Error`))
if (
res.headers.has("Content-Length")
&& !Number.isNaN(parseInt(res.headers.get("Content-Length")!,10))
&& parseInt(res.headers.get("Content-Length")!,10) > files.config.maxDiscordFileSize*files.config.maxDiscordFiles
)
return file.destroy(new WebError(413, `file reports to be too large`))
Readable.fromWeb(res.body as StreamWebReadable)
.pipe(file)
}
})
parser.parse(ctx.env.incoming)
.catch(e => console.error(e))
parser.on('error', (err) => {
escalate(err)
if (!file.destroyed) file.destroy(err)
})
})}
)
return router
}

View file

@ -1,137 +0,0 @@
import { Hono } from "hono"
import * as Accounts from "../../../../lib/accounts.js"
import * as auth from "../../../../lib/auth.js"
import RangeParser, { type Range } from "range-parser"
import ServeError from "../../../../lib/errors.js"
import Files, { WebError } from "../../../../lib/files.js"
import { getAccount, requiresPermissions } from "../../../../lib/middleware.js"
import {Readable} from "node:stream"
import type {ReadableStream as StreamWebReadable} from "node:stream/web"
import formidable from "formidable"
import { HttpBindings } from "@hono/node-server"
import pkg from "../../../../../../package.json" assert {type: "json"}
import { type StatusCode } from "hono/utils/http-status"
const router = new Hono<{
Variables: {
account: Accounts.Account
},
Bindings: HttpBindings
}>()
router.all("*", getAccount)
export default function(files: Files, apiRoot: Hono) {
router.get("/:id", async (ctx) => {
const fileId = ctx.req.param("id")
let acc = ctx.get("account") as Accounts.Account
let file = files.files[fileId]
ctx.header("Accept-Ranges", "bytes")
ctx.header("Access-Control-Allow-Origin", "*")
ctx.header("Content-Security-Policy", "sandbox allow-scripts")
if (file) {
ctx.header("Content-Disposition", `${ctx.req.query("attachment") == "1" ? "attachment" : "inline"}; filename="${encodeURI(file.filename.replaceAll("\n","\\n"))}"`)
ctx.header("ETag", file.md5)
if (file.lastModified) {
let lm = new Date(file.lastModified)
// TERRIFYING
ctx.header("Last-Modified",
`${['Sun','Mon','Tue','Wed','Thu','Fri','Sat'][lm.getUTCDay()]}, ${lm.getUTCDate()} `
+ `${['Jan','Feb','Mar','Apr','May','Jun',"Jul",'Aug','Sep','Oct','Nov','Dec'][lm.getUTCMonth()]}`
+ ` ${lm.getUTCFullYear()} ${lm.getUTCHours().toString().padStart(2,"0")}`
+ `:${lm.getUTCMinutes().toString().padStart(2,"0")}:${lm.getUTCSeconds().toString().padStart(2,"0")} GMT`
)
}
if (file.visibility == "private") {
if (acc?.id != file.owner) {
return ServeError(ctx, 403, "you do not own this file")
}
if (
auth.getType(auth.tokenFor(ctx)!) == "App" &&
auth
.getPermissions(auth.tokenFor(ctx)!)
?.includes("private")
) {
return ServeError(ctx, 403, "insufficient permissions")
}
}
let range: Range | undefined
ctx.header("Content-Type", file.mime)
if (file.sizeInBytes) {
ctx.header("Content-Length", file.sizeInBytes.toString())
if (file.chunkSize && ctx.req.header("Range")) {
let ranges = RangeParser(file.sizeInBytes, ctx.req.header("Range") || "")
if (ranges) {
if (typeof ranges == "number")
return ServeError(ctx, ranges == -1 ? 416 : 400, ranges == -1 ? "unsatisfiable ranges" : "invalid ranges")
if (ranges.length > 1) return ServeError(ctx, 400, "multiple ranges not supported")
range = ranges[0]
ctx.status(206)
ctx.header(
"Content-Length",
(range.end - range.start + 1).toString()
)
ctx.header(
"Content-Range",
`bytes ${range.start}-${range.end}/${file.sizeInBytes}`
)
}
}
}
if (ctx.req.method == "HEAD")
return ctx.body(null)
return files
.readFileStream(fileId, range)
.then(async (stream) => {
let rs = new ReadableStream({
start(controller) {
stream.once("end", () => controller.close())
stream.once("error", (err) => controller.error(err))
},
cancel(reason) {
stream.destroy(reason instanceof Error ? reason : new Error(reason))
}
})
stream.pipe(ctx.env.outgoing)
return new Response(rs, ctx.body(null))
})
.catch((err) => {
return ServeError(ctx, err.status, err.message)
})
} else {
return ServeError(ctx, 404, "file not found")
}
})
router.on(["PUT", "POST"], "/:id", async (ctx) => {
ctx.env.incoming.push(
`--${ctx.req.header("content-type")?.match(/boundary=(\S+)/)?.[1]}\r\n`
+ `Content-Disposition: form-data; name="uploadId"\r\n\r\n`
+ ctx.req.param("id")
+ "\r\n"
)
return apiRoot.fetch(
new Request(
(new URL(
`/api/v1/file`, ctx.req.raw.url)).href,
ctx.req.raw
),
ctx.env
)
})
return router
}

View file

@ -1,114 +0,0 @@
import fs from "fs/promises"
import bytes from "bytes"
import ServeError from "../../../lib/errors.js"
import * as Accounts from "../../../lib/accounts.js"
import type Files from "../../../lib/files.js"
import pkg from "../../../../../package.json" assert {type:"json"}
import { Hono } from "hono"
import { getAccount } from "../../../lib/middleware.js"
export let router = new Hono<{
Variables: {
account: Accounts.Account
}
}>()
export default function (files: Files) {
router.get("/:fileId", getAccount, async (ctx) => {
let acc = ctx.get("account") as Accounts.Account
const fileId = ctx.req.param("fileId")
const host = ctx.req.header("Host")
const file = files.files[fileId]
if (file) {
if (file.visibility == "private" && acc?.id != file.owner) {
return ServeError(ctx, 403, "you do not own this file")
}
const template = await fs
.readFile(process.cwd() + "/dist/download.html", "utf8")
.catch(() => {
throw ctx.status(500)
})
let fileOwner = file.owner
? Accounts.getFromId(file.owner)
: undefined
return ctx.html(
template
.replaceAll("$FileId", fileId)
.replaceAll("$Version", pkg.version)
.replaceAll(
"$FileSize",
file.sizeInBytes
? bytes(file.sizeInBytes)
: "[File size unknown]"
)
.replaceAll(
"$FileName",
file.filename
.replaceAll("&", "&amp;")
.replaceAll("<", "&lt;")
.replaceAll(">", "&gt;")
)
.replace(
"<!--metaTags-->",
(file.mime.startsWith("image/")
? `<meta name="og:image" content="https://${host}/file/${fileId}" />`
: file.mime.startsWith("video/")
? `<meta property="og:video:url" content="https://${host}/cpt/${fileId}/video.${
file.mime.split("/")[1] == "quicktime"
? "mov"
: file.mime.split("/")[1]
}" />
<meta property="og:video:secure_url" content="https://${host}/cpt/${fileId}/video.${
file.mime.split("/")[1] == "quicktime"
? "mov"
: file.mime.split("/")[1]
}" />
<meta property="og:type" content="video.other">
<!-- honestly probably good enough for now -->
<meta property="twitter:image" content="0">` +
// quick lazy fix as a fallback
// maybe i'll improve this later, but probably not.
((file.sizeInBytes || 0) >= 26214400
? `
<meta property="og:video:width" content="1280">
<meta property="og:video:height" content="720">`
: "")
: "") +
(fileOwner?.embed?.largeImage &&
file.visibility != "anonymous" &&
file.mime.startsWith("image/")
? `<meta name="twitter:card" content="summary_large_image">`
: "") +
`\n<meta name="theme-color" content="${
fileOwner?.embed?.color &&
file.visibility != "anonymous" &&
(ctx.req.header("user-agent") || "").includes(
"Discordbot"
)
? `#${fileOwner.embed.color}`
: "rgb(30, 33, 36)"
}">`
)
.replace(
"<!--preview-->",
file.mime.startsWith("image/")
? `<div style="min-height:10px"></div><img src="/file/${fileId}" />`
: file.mime.startsWith("video/")
? `<div style="min-height:10px"></div><video src="/file/${fileId}" controls></video>`
: file.mime.startsWith("audio/")
? `<div style="min-height:10px"></div><audio src="/file/${fileId}" controls></audio>`
: ""
)
.replaceAll(
"$Uploader",
!file.owner || file.visibility == "anonymous"
? "Anonymous"
: `@${fileOwner?.username || "Deleted User"}`
)
)
} else return ServeError(ctx, 404, "file not found")
})
return router
}

View file

@ -1,107 +0,0 @@
{
"include":["**/*"],
"compilerOptions": {
/* Visit https://aka.ms/tsconfig to read more about this file */
/* Projects */
// "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */
// "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */
// "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */
// "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */
// "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */
// "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */
/* Language and Environment */
"target": "es2021", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
// "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */
// "jsx": "preserve", /* Specify what JSX code is generated. */
// "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */
// "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */
// "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */
// "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */
// "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */
// "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */
// "noLib": true, /* Disable including any library files, including the default lib.d.ts. */
// "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */
// "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */
/* Modules */
"module": "nodenext", /* Specify what module code is generated. */
//"rootDir": "./src/", /* Specify the root folder within your source files. */
"moduleResolution": "nodenext", /* Specify how TypeScript looks up a file from a given module specifier. */
// "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */
// "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */
// "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */
// "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */
// "types": [], /* Specify type package names to be included without being referenced in a source file. */
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
// "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */
"resolveJsonModule": true, /* Enable importing .json files. */
// "noResolve": true, /* Disallow 'import's, 'require's or '<reference>'s from expanding the number of files TypeScript should add to a project. */
/* JavaScript Support */
// "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */
// "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */
// "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */
/* Emit */
// "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */
// "declarationMap": true, /* Create sourcemaps for d.ts files. */
// "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */
// "sourceMap": true, /* Create source map files for emitted JavaScript files. */
// "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */
"outDir": "../../out/server", /* Specify an output folder for all emitted files. */
// "removeComments": true, /* Disable emitting comments. */
// "noEmit": true, /* Disable emitting files from a compilation. */
// "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */
// "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */
// "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */
// "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
// "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */
// "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */
// "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */
// "newLine": "crlf", /* Set the newline character for emitting files. */
// "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */
// "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */
// "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */
// "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */
// "declarationDir": "./", /* Specify the output directory for generated declaration files. */
// "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */
/* Interop Constraints */
// "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */
// "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */
"esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */
// "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */
"forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */
/* Type Checking */
"strict": true, /* Enable all strict type-checking options. */
// "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */
// "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */
// "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */
// "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */
// "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */
// "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */
// "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */
// "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */
// "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */
// "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */
// "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */
// "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */
// "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */
// "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */
// "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */
// "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */
// "allowUnusedLabels": true, /* Disable error reporting for unused labels. */
// "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */
/* Completeness */
// "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */
"skipLibCheck": true /* Skip type checking all .d.ts files. */
},
"references": [
{ "path": "../../tsconfig.json" }
]
}

View file

@ -3,20 +3,15 @@
from the server but it's fine for now
*/
@import url("/static/assets/fonts/inconsolata.css");
@import url("/static/assets/fonts/source_sans.css");
@import url("/static/assets/fonts/fira_code.css");
@import url("/assets/fonts/inconsolata.css");
@import url("/assets/fonts/source_sans.css");
@import url("/assets/fonts/fira_code.css");
$FallbackFonts:
-apple-system,
system-ui,
BlinkMacSystemFont,
"Segoe UI",
Roboto,
$FallbackFonts: -apple-system, system-ui, BlinkMacSystemFont, "Segoe UI", Roboto,
sans-serif;
%normal {
font-family: "Source Sans Pro", $FallbackFonts
font-family: "Source Sans Pro", $FallbackFonts;
}
/*
@ -25,14 +20,17 @@ $FallbackFonts:
(it's just in case)
*/
*:not(span), .normal { @extend %normal; }
*:not(span),
.normal {
@extend %normal;
}
/*
for code blocks / terminal
*/
.monospace {
font-family: "Fira Code", monospace
font-family: "Fira Code", monospace;
}
/*
@ -49,37 +47,33 @@ $darkish: rgb(54, 62, 70);
body {
background-color: rgb(30, 33, 36); // this is here so that
// pulling down to refresh
// on mobile looks good
// pulling down to refresh
// on mobile looks good
}
#appContent {
background-color: $Background
background-color: $Background;
}
/*
scrollbars
*/
* {
/* nice scrollbars aren't needed on mobile so */
@media screen and (min-width:500px) {
&::-webkit-scrollbar {
width:5px;
}
&::-webkit-scrollbar-track {
background-color:#222222;
}
&::-webkit-scrollbar-thumb {
background-color:#333;
&:hover {
background-color:#373737;
}
}
/* nice scrollbars aren't needed on mobile so */
@media screen and (min-width: 500px) {
::-webkit-scrollbar {
width: 5px;
}
}
::-webkit-scrollbar-track {
background-color: #222222;
}
::-webkit-scrollbar-thumb {
background-color: #333;
&:hover {
background-color: #373737;
}
}
}

View file

@ -1,4 +1,4 @@
#uploadWindow {
main {
#add_new_files {
background-color:#191919;
border: 1px solid gray;
@ -112,4 +112,4 @@
}
}
}
}
}

View file

@ -1,6 +1,6 @@
// should probably start using mixins for thingss like this
#uploadWindow {
main {
.file {
background-color:#191919;
border: 1px solid gray;

View file

@ -1,7 +1,7 @@
@use "uploader/add_new_files";
@use "uploader/file";
#uploadWindow {
main {
position:absolute;
left:50%;
top:50%;
@ -83,4 +83,4 @@
top:10px;
padding:0px;
}
}
}

View file

@ -19,8 +19,8 @@
}
}
#uploadWindow {
main {
img, video, audio {
width:100%;
}
}
}

View file

@ -1,168 +1,166 @@
#uploadWindow {
color: #FFFFFF
main {
color: #ffffff;
}
body {
background-color:#DDDDDD;
background-color: #dddddd;
}
#appContent {
background: darkgray;
@media screen and (max-width:500px) {
background:white;
@media screen and (max-width: 500px) {
background: white;
}
}
#uploadWindow {
main {
background: white;
color:black;
color: black;
h1, p, a {
h1,
p,
a {
margin: 0px;
font-size: 14px;
}
a {
color:rgb(153, 153, 153);
color: rgb(153, 153, 153);
}
h1 {
font-weight:600;
font-weight: 600;
font-size: 25px;
text-align:center;
text-align: center;
@media screen and (max-width:500px) {
@media screen and (max-width: 500px) {
font-size: 30px;
}
}
& > p:nth-of-type(1) {
text-align:center;
text-align: center;
font-style: italic;
font-weight:600;
font-weight: 600;
font-size: 16px;
color:black !important;
@media screen and (max-width:500px) {
color: black !important;
@media screen and (max-width: 500px) {
font-size: 21px;
}
}
button {
cursor:pointer;
color:black;
border:none;
outline:none;
padding:5px;
background: #AAAAAA;
cursor: pointer;
color: black;
border: none;
outline: none;
padding: 5px;
background: #aaaaaa;
@media screen and (max-width: 500px) {
font-size:16px;
padding:10px;
font-size: 16px;
padding: 10px;
}
&:hover {
outline: 1px solid #333333;
color: black;
background-color:#AAAAAA;
background-color: #aaaaaa;
}
}
& > button:nth-last-of-type(1) {
background-color:#66AAFF;
background-color: #66aaff;
&:hover {
background-color:#66AAFF;
background-color: #66aaff;
}
}
#add_new_files {
background-color: #AAAAAA66;
border:1px solid #AAAAAA;
background-color: #aaaaaa66;
border: 1px solid #aaaaaa;
#file_add_btns {
button, input[type=text] {
transition-duration:0s;
button,
input[type="text"] {
transition-duration: 0s;
@media screen and (max-width: 500px) {
font-size:16px;
padding:10px;
font-size: 16px;
padding: 10px;
}
}
input[type=text] {
input[type="text"] {
font-family: "Fira Code", monospace;
background-color:#AAAAAA;
color:black;
background-color: #aaaaaa;
color: black;
}
button {
cursor:pointer;
background-color:#AAAAAA;
cursor: pointer;
background-color: #aaaaaa;
color: black;
&:hover {
flex-basis: 50%;
transition-duration:0s;
background-color:#AAAAAA;
transition-duration: 0s;
background-color: #aaaaaa;
color: black;
outline: 1px solid #333333;
}
}
.fileUpload {
background-color:#AAAAAA;
transition-duration:250ms;
background-color: #aaaaaa;
transition-duration: 250ms;
&:hover {
transition-duration:0s;
background-color:#AAAAAA;
transition-duration: 0s;
background-color: #aaaaaa;
}
}
}
}
.file {
background-color: #AAAAAA66;
border: 1px solid #AAAAAA;
background-color: #aaaaaa66;
border: 1px solid #aaaaaa;
input[type=text] {
input[type="text"] {
font-family: "Fira Code", monospace;
background-color:#AAAAAA;
color:black;
background-color: #aaaaaa;
color: black;
}
}
}
* {
/* nice scrollbars aren't needed on mobile so */
@media screen and (min-width:500px) {
&::-webkit-scrollbar {
width:5px;
}
/* nice scrollbars aren't needed on mobile so */
@media screen and (min-width: 500px) {
::-webkit-scrollbar {
width: 5px;
}
&::-webkit-scrollbar-track {
background-color:#AAAAAA;
}
::-webkit-scrollbar-track {
background-color: #aaaaaa;
}
&::-webkit-scrollbar-thumb {
background-color:#DDDDDD;
::-webkit-scrollbar-thumb {
background-color: #dddddd;
&:hover {
background-color:#FFFFFF;
}
&:hover {
background-color: #ffffff;
}
}
}
#topbar {
background-color: #DDDDDD;
background-color: #dddddd;
}
.error {
.code {
color: black;
}
}
}

View file

@ -1,22 +0,0 @@
<script lang="ts">
import { onMount } from "svelte";
import Topbar from "./elem/Topbar.svelte";
import PulldownManager from "./elem/PulldownManager.svelte";
import UploadWindow from "./elem/UploadWindow.svelte";
import { pulldownManager } from "./elem/stores.js";
let topbar: Topbar;
let pulldown: PulldownManager;
onMount(() => {
pulldownManager.set(pulldown)
})
</script>
<Topbar bind:this={topbar} pulldown={pulldown} />
<div id="appContent">
<PulldownManager bind:this={pulldown} />
<UploadWindow/>
</div>

View file

@ -1,80 +0,0 @@
<script lang="ts">
import { fade, slide } from "svelte/transition";
interface BaseModalOption {
name:string,
icon:string,
id: string | number | symbol | boolean
}
type ModalOption = BaseModalOption & {inputSettings: {password?: boolean}, id: any} | BaseModalOption & { description: string }
type ModalOptions = ModalOption[]
type OptionPickerReturns = {selected: any} & Record<any,any> | null
let activeModal: {resolve: (val: OptionPickerReturns) => void, title: string, modal: ModalOptions } | undefined;
let modalResults: Record<string | number | symbol, string> = {};
export function picker(title: string,mdl: ModalOptions): Promise<OptionPickerReturns> {
if (activeModal) forceCancel()
return new Promise<OptionPickerReturns>((resolve,reject) => {
activeModal = {
resolve,
title,
modal:mdl
}
modalResults = {}
})
}
export function forceCancel() {
if (activeModal && activeModal.resolve) {
activeModal.resolve(null)
}
activeModal = undefined
}
</script>
{#if activeModal}
<div class="modalContainer" transition:fade={{duration:200}}>
<button class="mdHitbox" on:click|self={forceCancel}></button>
<div class="modal" transition:slide={{duration:200}}>
<div class="optPicker">
<div class="category">
<p style:margin-bottom="10px">{activeModal.title}</p>
</div>
{#each activeModal.modal as option (option.id)}
{#if "inputSettings" in option}
<div class="inp">
<img src={option.icon} alt={option.id.toString()}>
<!-- i have to do this stupidness because of svelte but -->
<!-- its reason for blocking this is pretty good sooooo -->
{#if option.inputSettings.password}
<input placeholder={option.name} type="password" bind:value={modalResults[option.id]}>
{:else}
<input placeholder={option.name} bind:value={modalResults[option.id]}>
{/if}
</div>
{:else}
<button on:click={() => {activeModal?.resolve({...modalResults,selected:option.id});activeModal=undefined;modalResults={};}}>
<img src={option.icon} alt={option.id.toString()}>
<p>{option.name}<span><br />{option.description}</span></p>
</button>
{/if}
{/each}
<button on:click={forceCancel}>
<img src="/static/assets/icons/delete.svg" alt="cancel">
<p>Cancel</p>
</button>
</div>
</div>
</div>
{/if}

View file

@ -1,331 +0,0 @@
import { fetchAccountData, account, refreshNeeded } from "../stores"
import { get } from "svelte/store";
import type OptionPicker from "./OptionPicker.svelte";
export function deleteAccount(optPicker: OptionPicker) {
optPicker.picker("What should we do with your files?",[
{
name: "Delete my files",
icon: "/static/assets/icons/admin/delete_file.svg",
description: "Your files will be permanently deleted",
id: true
},
{
name: "Do nothing",
icon: "/static/assets/icons/file.svg",
description: "Your files will not be affected",
id: false
}
]).then((exp) => {
if (exp) {
let deleteFiles = exp.selected
optPicker.picker(`Enter your username to continue.`,[
{
name: "Enter your username",
icon: "/static/assets/icons/person.svg",
inputSettings: {},
id:"username"
},
{
name: `Delete account ${deleteFiles ? "& files" : ""}`,
icon: "/static/assets/icons/delete_account.svg",
description: `This cannot be undone.`,
id: true
}
]).then((fin) => {
if (fin && fin.selected) {
if (fin.username != (get(account)||{}).username) {
optPicker.picker("Incorrect username. Please try again.",[])
return
}
fetch(`/auth/delete_account`,{method:"POST", body:JSON.stringify({
deleteFiles
})}).then((response) => {
if (response.status != 200) {
optPicker.picker(`${response.status} ${response.headers.get("x-backup-status-message") || response.statusText || ""}`,[])
}
fetchAccountData()
})
}
})
}
})
}
export function userChange(optPicker: OptionPicker) {
optPicker.picker("Change username",[
{
name: "New username",
icon: "/static/assets/icons/person.svg",
id: "username",
inputSettings: {}
},
{
name: "Update username",
icon: "/static/assets/icons/update.svg",
description: "",
id: true
}
]).then((exp) => {
if (exp && exp.selected) {
fetch(`/auth/change_username`,{method:"POST", body:JSON.stringify({
username:exp.username
})}).then((response) => {
if (response.status != 200) {
optPicker.picker(`${response.status} ${response.headers.get("x-backup-status-message") || response.statusText || ""}`,[])
}
fetchAccountData()
})
}
})
}
export function forgotPassword(optPicker: OptionPicker) {
optPicker.picker("Forgot your password?",[
{
name: "Username",
icon: "/static/assets/icons/person.svg",
id: "user",
inputSettings: {}
},
{
name: "OK",
icon: "/static/assets/icons/update.svg",
description: "",
id: true
}
]).then((exp) => {
if (exp && exp.selected) {
fetch(`/auth/request_emergency_login`,{method:"POST", body:JSON.stringify({
account:exp.user
})}).then((response) => {
if (response.status != 200) {
optPicker.picker(`${response.status} ${response.headers.get("x-backup-status-message") || response.statusText || ""}`,[])
} else {
optPicker.picker(`Please follow the instructions sent to your inbox.`,[])
}
})
}
})
}
export function emailPotentialRemove(optPicker: OptionPicker) {
optPicker.picker("What would you like to do?",[
{
name: "Set a new email",
icon: "/static/assets/icons/change_email.svg",
description: "",
id: "set"
},
{
name: "Disconnect email",
icon: "/static/assets/icons/disconnect_email.svg",
description: "",
id: "disconnect"
}
]).then((exp) => {
if (exp && exp.selected) {
switch (exp.selected) {
case "set":
emailChange(optPicker);
break
case "disconnect":
fetch("/auth/remove_email", {method: "POST"}).then((response) => {
if (response.status != 200) {
optPicker.picker(`${response.status} ${response.headers.get("x-backup-status-message") || response.statusText || ""}`,[])
}
fetchAccountData()
})
}
}
})
}
export function emailChange(optPicker: OptionPicker) {
optPicker.picker("Change email",[
{
name: "New email",
icon: "/static/assets/icons/mail.svg",
id: "email",
inputSettings: {}
},
{
name: "Request email change",
icon: "/static/assets/icons/update.svg",
description: "",
id: true
}
]).then((exp) => {
if (exp && exp.selected) {
fetch(`/auth/request_email_change`,{method:"POST", body:JSON.stringify({
email:exp.email
})}).then((response) => {
if (response.status != 200) {
optPicker.picker(`${response.status} ${response.headers.get("x-backup-status-message") || response.statusText || ""}`,[])
} else {
optPicker.picker(`Please continue to your inbox at ${exp.email.split("@")[1]} and click on the attached link.`,[])
}
})
}
})
}
export function pwdChng(optPicker: OptionPicker) {
optPicker.picker("Change password",[
{
name: "New password",
icon: "/static/assets/icons/change_password.svg",
id: "password",
inputSettings: {
password: true
}
},
{
name: "Update password",
icon: "/static/assets/icons/update.svg",
description: "This will log you out of all sessions",
id: true
}
]).then((exp) => {
if (exp && exp.selected) {
fetch(`/auth/change_password`,{method:"POST", body:JSON.stringify({
password:exp.password
})}).then((response) => {
if (response.status != 200) {
optPicker.picker(`${response.status} ${response.headers.get("x-backup-status-message") || response.statusText || ""}`,[])
}
fetchAccountData()
})
}
})
}
export function customcss(optPicker: OptionPicker) {
optPicker.picker("Set custom CSS",[
{
name: "Enter a file ID",
icon: "/static/assets/icons/file.svg",
id: "fileid",
inputSettings: {}
},
{
name: "OK",
icon: "/static/assets/icons/update.svg",
description: "Refresh to apply changes",
id: true
}
]).then((exp) => {
if (exp && exp.selected) {
fetch(`/api/v1/account/customization/css`, {
method: "PUT",
body: JSON.stringify({
fileId: exp.fileid,
}),
}).then((response) => {
if (response.status != 200) {
optPicker.picker(
`${response.status} ${
response.headers.get("x-backup-status-message") ||
response.statusText ||
""
}`,
[]
)
}
fetchAccountData()
refreshNeeded.set(true)
})
}
})
}
export function embedColor(optPicker: OptionPicker) {
optPicker.picker("Set embed color",[
{
name: "FFFFFF",
icon: "/static/assets/icons/pound.svg",
id: "color",
inputSettings: {}
},
{
name: "OK",
icon: "/static/assets/icons/update.svg",
description: "",
id: true
}
]).then((exp) => {
if (exp && exp.selected) {
fetch(`/api/v1/account/customization/embed/color`, {
method: "POST",
body: JSON.stringify({
color: exp.color,
}),
}).then((response) => {
if (response.status != 200) {
optPicker.picker(
`${response.status} ${
response.headers.get("x-backup-status-message") ||
response.statusText ||
""
}`,
[]
)
}
fetchAccountData()
})
}
})
}
export function embedSize(optPicker: OptionPicker) {
optPicker.picker("Set embed image size",[
{
name: "Large",
icon: "/static/assets/icons/image.svg",
description: "",
id: true
},
{
name: "Small",
icon: "/static/assets/icons/small_image.svg",
description: "",
id: false
}
]).then((exp) => {
if (exp && exp.selected !== null) {
fetch(`/api/v1/account/customization/embed/size`, {
method: "POST",
body: JSON.stringify({
largeImage: exp.selected,
}),
}).then((response) => {
if (response.status != 200) {
optPicker.picker(
`${response.status} ${
response.headers.get("x-backup-status-message") ||
response.statusText ||
""
}`,
[]
)
}
fetchAccountData()
})
}
})
}

View file

@ -1,231 +0,0 @@
import { fetchAccountData, fetchFilePointers, account } from "../stores"
import { get } from "svelte/store";
import type OptionPicker from "./OptionPicker.svelte";
export function pwdReset(optPicker: OptionPicker) {
optPicker.picker("Reset password",[
{
name: "Target user",
icon: "/static/assets/icons/person.svg",
id: "target",
inputSettings: {}
},
{
name: "New password",
icon: "/static/assets/icons/change_password.svg",
id: "password",
inputSettings: {
password: true
}
},
{
name: "Update password",
icon: "/static/assets/icons/update.svg",
description: "This will log the target user out of all sessions",
id: true
}
]).then((exp) => {
if (exp && exp.selected) {
fetch(`/admin/reset`,{method:"POST", body:JSON.stringify({
target: exp.target,
password:exp.password
})}).then((response) => {
if (response.status != 200) {
optPicker.picker(`${response.status} ${response.headers.get("x-backup-status-message") || response.statusText || ""}`,[])
}
})
}
})
}
export function chgOwner(optPicker: OptionPicker) {
optPicker.picker("Transfer file ownership",[
{
name: "File ID",
icon: "/static/assets/icons/file.svg",
id: "file",
inputSettings: {}
},
{
name: "New owner",
icon: "/static/assets/icons/person.svg",
id: "owner",
inputSettings: {}
},
{
name: "Transfer file ownership",
icon: "/static/assets/icons/update.svg",
description: "This will transfer the file to this user",
id: true
}
]).then((exp) => {
if (exp && exp.selected) {
fetch(`/admin/transfer`,{method:"POST", body:JSON.stringify({
owner: exp.owner,
target: exp.file
})}).then((response) => {
if (response.status != 200) {
optPicker.picker(`${response.status} ${response.headers.get("x-backup-status-message") || response.statusText || ""}`,[])
}
})
}
})
}
export function chgId(optPicker: OptionPicker) {
optPicker.picker("Change file ID",[
{
name: "Target file",
icon: "/static/assets/icons/file.svg",
id: "file",
inputSettings: {}
},
{
name: "New ID",
icon: "/static/assets/icons/admin/change_file_id.svg",
id: "new",
inputSettings: {}
},
{
name: "Update",
icon: "/static/assets/icons/update.svg",
description: "File will not be available at its old ID",
id: true
}
]).then((exp) => {
if (exp && exp.selected) {
fetch(`/admin/idchange`,{method:"POST", body:JSON.stringify({
target: exp.file,
new: exp.new
})}).then((response) => {
if (response.status != 200) {
optPicker.picker(`${response.status} ${response.headers.get("x-backup-status-message") || response.statusText || ""}`,[])
}
})
}
})
}
export function delFile(optPicker: OptionPicker) {
optPicker.picker("Delete file",[
{
name: "File ID",
icon: "/static/assets/icons/file.svg",
id: "file",
inputSettings: {}
},
{
name: "Delete",
icon: "/static/assets/icons/admin/delete_file.svg",
description: "This can't be undone",
id: true
}
]).then((exp) => {
if (exp && exp.selected) {
fetch(`/admin/delete`,{method:"POST", body:JSON.stringify({
target: exp.file
})}).then((response) => {
if (response.status != 200) {
optPicker.picker(`${response.status} ${response.headers.get("x-backup-status-message") || response.statusText || ""}`,[])
}
})
}
})
}
export function elevateUser(optPicker: OptionPicker) {
optPicker.picker("Elevate user",[
{
name: "Username",
icon: "/static/assets/icons/person.svg",
id: "user",
inputSettings: {}
},
{
name: "Elevate to admin",
icon: "/static/assets/icons/update.svg",
description: "",
id: true
}
]).then((exp) => {
if (exp && exp.selected) {
fetch(`/admin/elevate`,{method:"POST", body:JSON.stringify({
target: exp.user
})}).then((response) => {
if (response.status != 200) {
optPicker.picker(`${response.status} ${response.headers.get("x-backup-status-message") || response.statusText || ""}`,[])
}
})
}
})
}
// im really lazy so i just stole this from account.js
export function deleteAccount(optPicker: OptionPicker) {
optPicker.picker("What should we do with the target account's files?",[
{
name: "Delete files",
icon: "/static/assets/icons/admin/delete_file.svg",
description: "Files will be permanently deleted",
id: true
},
{
name: "Do nothing",
icon: "/static/assets/icons/file.svg",
description: "Files will not be affected",
id: false
}
]).then((exp) => {
if (exp) {
let deleteFiles = exp.selected
optPicker.picker(`Enter the target account's username to continue.`,[
{
name: "Enter account username",
icon: "/static/assets/icons/person.svg",
inputSettings: {},
id:"username"
},
{
name: "Optional reason",
icon: "/static/assets/icons/more.svg",
inputSettings: {},
id:"reason"
},
{
name: `Delete account ${deleteFiles ? "& its files" : ""}`,
icon: "/static/assets/icons/delete_account.svg",
description: `This cannot be undone.`,
id: true
}
]).then((fin) => {
if (fin && fin.selected) {
fetch(`/admin/delete_account`,{method:"POST", body:JSON.stringify({
target: fin.username,
reason: fin.reason,
deleteFiles
})}).then((response) => {
if (response.status != 200) {
optPicker.picker(`${response.status} ${response.headers.get("x-backup-status-message") || response.statusText || ""}`,[])
}
fetchAccountData()
})
}
})
}
})
}

View file

@ -1,220 +0,0 @@
import { fetchAccountData, fetchFilePointers, account } from "../stores"
import { get } from "svelte/store";
import type OptionPicker from "./OptionPicker.svelte"
import type { FilePointer } from "../../../server/lib/files";
export let options = {
FV: [
{
name: "Public",
icon: "/static/assets/icons/public.svg",
description: "Everyone can view your uploads",
id: "public"
},
{
name: "Anonymous",
icon: "/static/assets/icons/anonymous.svg",
description: "Your username will be hidden",
id: "anonymous"
},
{
name: "Private",
icon: "/static/assets/icons/private.svg",
description: "Nobody but you can view your uploads",
id: "private"
}
],
FV2: [
{
name: "Public",
icon: "/static/assets/icons/public.svg",
description: "Everyone can view this file",
id: "public"
},
{
name: "Anonymous",
icon: "/static/assets/icons/anonymous.svg",
description: "Your username will be hidden",
id: "anonymous"
},
{
name: "Private",
icon: "/static/assets/icons/private.svg",
description: "Nobody but you can view this file",
id: "private"
}
],
AYS: [
{
name: "Yes",
icon: "/static/assets/icons/update.svg",
id: true
}
]
}
export function dfv(optPicker: OptionPicker) {
optPicker.picker("Default file visibility",options.FV).then((exp) => {
if (exp && exp.selected) {
fetch(`/auth/dfv`,{method:"POST", body:JSON.stringify({
defaultFileVisibility: exp.selected
})}).then((response) => {
if (response.status != 200) {
optPicker.picker(`${response.status} ${response.headers.get("x-backup-status-message") || response.statusText || ""}`,[])
}
fetchAccountData()
})
}
})
}
export function update_all_files(optPicker: OptionPicker) {
optPicker.picker("You sure?",[
{
name: "Yeah",
icon: "/static/assets/icons/update.svg",
description: `This will make all of your files ${get(account)?.defaultFileVisibility || "public"}`,
id: true
}
]).then((exp) => {
if (exp && exp.selected) {
fetch(`/files/manage`,{method:"POST", body:JSON.stringify({
target:get(account)?.files,
action: "changeFileVisibility",
value: get(account)?.defaultFileVisibility
})}).then((response) => {
if (response.status != 200) {
optPicker.picker(`${response.status} ${response.headers.get("x-backup-status-message") || response.statusText || ""}`,[])
}
fetchAccountData()
})
}
})
}
export function fileOptions(optPicker: OptionPicker, file: FilePointer & {id:string}) {
optPicker.picker(file.filename,[
{
name: file.tag ? "Remove tag" : "Tag file",
icon: `/static/assets/icons/${file.tag ? "tag_remove" : "tag"}.svg`,
description: file.tag || `File has no tag`,
id: "tag"
},
{
name: "Change file visibility",
icon: `/static/assets/icons/${file.visibility||"public"}.svg`,
description: `File is currently ${file.visibility||"public"}`,
id: "changeFileVisibility"
},
{
name: "Delete file",
icon: `/static/assets/icons/admin/delete_file.svg`,
description: ``,
id: "delete"
}
]).then((exp) => {
if (exp && exp.selected) {
switch( exp.selected ) {
case "delete":
fetch(`/files/manage`,{method:"POST", body:JSON.stringify({
target: [ file.id ],
action: "delete",
})}).then((response) => {
if (response.status != 200) {
optPicker.picker(`${response.status} ${response.headers.get("x-backup-status-message") || response.statusText || ""}`,[])
}
fetchFilePointers();
})
break;
case "changeFileVisibility":
optPicker.picker("Set file visibility", options.FV2).then((exp) => {
if (exp && exp.selected) {
fetch(`/files/manage`, {method: "POST", body: JSON.stringify({
target: [ file.id ],
action: "changeFileVisibility",
value: exp.selected
})}).then((response) => {
if (response.status != 200) {
optPicker.picker(`${response.status} ${response.headers.get("x-backup-status-message") || response.statusText || ""}`,[])
}
fetchFilePointers();
})
}
})
break;
case "tag":
if (file.tag) {
fetch(`/files/manage`, {method: "POST", body: JSON.stringify({
target: [ file.id ],
action: "setTag"
})}).then(fetchFilePointers)
return
}
optPicker.picker("Enter a tag (max 30char)",[
{
name: "Tag name",
icon: "/static/assets/icons/tag.svg",
id: "tag",
inputSettings: {}
},
{
name: "OK",
icon: "/static/assets/icons/update.svg",
description: "",
id: true
}
]).then((exp) => {
if (exp && exp.selected) {
fetch(`/files/manage`, {method: "POST", body: JSON.stringify({
target: [ file.id ],
action: "setTag",
value: exp.tag || null
})}).then((response) => {
if (response.status != 200) {
optPicker.picker(`${response.status} ${response.headers.get("x-backup-status-message") || response.statusText || ""}`,[])
}
fetchFilePointers();
})
}
})
break
}
}
})
}

View file

@ -1,243 +0,0 @@
<script lang="ts">
import Pulldown from "./Pulldown.svelte"
import { padding_scaleY } from "../transition/padding_scaleY"
import { circIn,circOut } from "svelte/easing"
import { account, fetchAccountData, serverStats, refreshNeeded } from "../stores";
import { fade } from "svelte/transition";
import OptionPicker from "../prompts/OptionPicker.svelte";
import * as accOpts from "../prompts/account";
import * as uplOpts from "../prompts/uploads";
import * as admOpts from "../prompts/admin";
let targetAction: "login"|"create"
let inProgress: boolean
let authError:{status:number,message:string}|undefined
let pwErr: HTMLDivElement
let optPicker: OptionPicker;
// lazy
let username: string
let password: string
let execute = () => {
if (inProgress) return
inProgress = true
fetch(`/auth/${targetAction}`, {
method: "POST",
body: JSON.stringify({
username, password
})
}).then(async (res) => {
inProgress = false
if (res.status != 200) {
authError = await res.json().catch(() => {
return {
status: res.status,
message: res.headers.get("x-backup-status-message") || res.statusText || ""
}
})
} else {
authError = undefined, username = "", password = "";
fetchAccountData();
}
}).catch(() => {})
}
$: {
if (pwErr && authError) {
pwErr.animate({
backgroundColor: ["#885555","#663333"],
easing: "ease-out"
},650)
}
}
// actual account menu
</script>
<Pulldown name="accounts">
<OptionPicker bind:this={optPicker} />
{#if $account}
<div class="loggedIn" transition:fade={{duration:200}}>
<h1>
Hey there, <span class="monospace">@{$account.username}</span>
</h1>
<div class="optPicker">
<div class="category">
<p>Account</p>
</div>
<button on:click={() => accOpts.userChange(optPicker)}>
<img src="/static/assets/icons/change_username.svg" alt="change username">
<p>Change username</p>
</button>
<button on:click={() => ($account?.email ? accOpts.emailPotentialRemove : accOpts.emailChange)(optPicker)}>
<img src="/static/assets/icons/mail.svg" alt="change email">
<p>Change email{#if $account.email}<span class="monospaceText"><br />{$account.email}</span>{/if}</p>
</button>
<button on:click={() => accOpts.pwdChng(optPicker)}>
<img src="/static/assets/icons/change_password.svg" alt="change password">
<p>Change password<span><br />You will be logged out of all sessions</span></p>
</button>
{#if !$account.admin}
<button on:click={() => accOpts.deleteAccount(optPicker)}>
<img src="/static/assets/icons/delete_account.svg" alt="delete account">
<p>Delete account</p>
</button>
{/if}
<div class="category">
<p>Uploads</p>
</div>
<button on:click={() => uplOpts.dfv(optPicker)}>
<img src={`/static/assets/icons/${$account.defaultFileVisibility || "public"}.svg`} alt={$account.defaultFileVisibility || "public"}>
<p>Default file visibility<span><br />Uploads will be <strong>{$account.defaultFileVisibility || "public"}</strong> by default</span></p>
</button>
<button on:click={() => uplOpts.update_all_files(optPicker)}>
<img src="/static/assets/icons/update.svg" alt="update">
<p>Make all of my files {$account.defaultFileVisibility || "public"}<span><br />Matches your default file visibility</span></p>
</button>
<div class="category">
<p>Customization</p>
</div>
<button on:click={() => accOpts.customcss(optPicker)}>
<img src="/static/assets/icons/paint.svg" alt="customcss">
<p>Set custom CSS<span><br />{@html $account.customCSS ? `Using file ID <span class="number">${$account.customCSS}</span>` : "No custom CSS set"}</span></p>
</button>
<button on:click={() => accOpts.embedColor(optPicker)}>
<img src="/static/assets/icons/pound.svg" alt="embedColor">
<p>Set custom embed color<span><br />{@html $account?.embed?.color ? `Using custom color <span class="number">${$account?.embed?.color}</span>` : ""}</span></p>
</button>
<button on:click={() => accOpts.embedSize(optPicker)}>
<img src="/static/assets/icons/image.svg" alt="embedSize">
<p>Set embed image size <span><br />Images currently appear {$account?.embed?.largeImage ? `large` : "small"} in embeds</span></p>
</button>
{#if $refreshNeeded}
<button on:click={() => window.location.reload()} transition:fade={{duration: 200}}>
<img src="/static/assets/icons/refresh.svg" alt="refresh">
<p>Refresh<span><br />Changes were made which require a refresh</span></p>
</button>
{/if}
<div class="category">
<p>Sessions</p>
</div>
<button on:click={() => fetch(`/auth/logout_sessions`,{method:"POST"}).then(() => fetchAccountData())}>
<img src="/static/assets/icons/logout_all.svg" alt="logout_all">
<p>Log out all sessions<span><br />{$account?.sessionCount} session(s) active</span></p>
</button>
<button on:click={() => fetch(`/auth/logout`,{method:"POST"}).then(() => fetchAccountData())}>
<img src="/static/assets/icons/logout.svg" alt="logout">
<p>Log out<span><br />Session expires {new Date($account?.sessionExpires).toLocaleDateString()}</span></p>
</button>
{#if $account.admin}
<div class="category">
<p>Admin</p>
</div>
<button on:click={() => admOpts.deleteAccount(optPicker)}>
<img src="/static/assets/icons/delete_account.svg" alt="delete account">
<p>Delete user account</p>
</button>
<button on:click={() => admOpts.pwdReset(optPicker)}>
<img src="/static/assets/icons/change_password.svg" alt="change password">
<p>Change user password</p>
</button>
<button on:click={() => admOpts.elevateUser(optPicker)}>
<img src="/static/assets/icons/admin/elevate_user.svg" alt="elevate account">
<p>Elevate account to admin</p>
</button>
<button on:click={() => admOpts.chgOwner(optPicker)}>
<img src="/static/assets/icons/link.svg" alt="change file owner">
<p>Change file owner</p>
</button>
<button on:click={() => admOpts.chgId(optPicker)}>
<img src="/static/assets/icons/admin/change_file_id.svg" alt="change file id">
<p>Change file ID<span><br />Potentially buggy, usage not recommended</span></p>
</button>
<button on:click={() => admOpts.delFile(optPicker)}>
<img src="/static/assets/icons/admin/delete_file.svg" alt="delete file">
<p>Delete file</p>
</button>
{/if}
<p style="font-size:12px;color:#AAAAAA;text-align:center;" class="monospace"><br />{$account.id}</p>
</div>
</div>
{:else}
<div class="notLoggedIn" transition:fade={{duration:200}}>
<div class="container_div">
<h1>monofile <span style:color="#999999">accounts</span></h1>
<p class="flavor">Gain control of your uploads.</p>
{#if targetAction}
<div class="fields" out:padding_scaleY|local={{easingFunc:circIn}} in:padding_scaleY|local>
{#if !$serverStats?.accounts.registrationEnabled && targetAction == "create"}
<div class="pwError">
<div style:background-color="#554C33">
<p>Account registration has been disabled by this instance's owner</p>
</div>
</div>
{/if}
{#if authError}
<div class="pwError" out:padding_scaleY|local={{easingFunc:circIn}} in:padding_scaleY|local>
<div bind:this={pwErr}>
<p><strong>{authError.status}</strong> {authError.message}</p>
</div>
</div>
{/if}
<input placeholder="username" type="text" bind:value={username}>
<input placeholder="password" type="password" bind:value={password}>
<button on:click={execute}>{@html inProgress ? "<span class=loader><i>•</i> <i>•</i> <i>•</i></span>" : (targetAction=="login" ? "Log in" : "Create account") }</button>
{#if targetAction == "login"}
<button class="flavor" on:click={() => accOpts.forgotPassword(optPicker)}>I forgot my password</button>
{/if}
</div>
{:else}
<div class="lgBtnContainer" out:padding_scaleY|local={{easingFunc:circIn}} in:padding_scaleY|local>
<button on:click={() => targetAction="login"}>Log in</button>
<button on:click={() => targetAction="create"}>Sign up</button>
</div>
{/if}
</div>
</div>
{/if}
</Pulldown>

View file

@ -1,63 +0,0 @@
<script lang="ts">
import Pulldown from "./Pulldown.svelte";
import { account, fetchFilePointers, files, pulldownManager } from "../stores.js";
import { fade } from "svelte/transition";
import { flip } from "svelte/animate";
import { fileOptions } from "../prompts/uploads";
import OptionPicker from "../prompts/OptionPicker.svelte";
let picker: OptionPicker;
let query = "";
fetchFilePointers();
</script>
<Pulldown name="files">
<OptionPicker bind:this={picker} />
{#if $account?.username}<div class="loggedIn">
<input type="text" placeholder={`Search ${$files.length} file(s)`} class="searchBar" bind:value={query}>
<div class="fileList">
<!-- Probably wildly inefficient but who cares, I just wanna get this over with -->
{#each $files.filter(f => f&&(f.filename.toLowerCase().includes(query.toLowerCase()) || f.id.toLowerCase().includes(query.toLowerCase()) || f.tag?.includes(query.toLowerCase()))) as file (file.id)}
<div class="flFile" transition:fade={{duration:200}} animate:flip={{duration:200}}>
<button class="hitbox" on:click={() => window.open(`/download/${file.id}`)}></button> <!-- this is bad, but I'm lazy -->
<div class="flexCont">
<div class="fileInfo">
<h2>{file.filename}</h2>
<p class="detail">
<img src="/static/assets/icons/{file.visibility || "public"}.svg" alt={file.visibility||"public"} />&nbsp;
<span class="number">{file.id}</span>&nbsp;&nbsp;&nbsp;&nbsp;<span class="cd">{file.mime.split(";")[0]}</span>
{#if file.reserved}
<br />
<img src="/static/assets/icons/update.svg" alt="uploading"/>&nbsp;
Uploading...
{/if}
{#if file.tag}
<br />
<img src="/static/assets/icons/tag.svg" alt="tag"/>&nbsp;
<span class="cd">{file.tag}</span>
{/if}
</p>
</div>
<button class="more" on:click={() => fileOptions(picker, file)}>
<img src="/static/assets/icons/more.svg" alt="more" />
</button>
</div>
</div>
{/each}
</div>
</div>
{:else}
<div class="notLoggedIn">
<div style:height="10px" />
<p class="flavor">Log in to view uploads</p>
<button on:click={$pulldownManager.openPulldown("account")}>OK</button>
<div style:height="14px" />
</div>
{/if}
</Pulldown>

View file

@ -1,40 +0,0 @@
import { writable } from "svelte/store"
//import type Pulldown from "./pulldowns/Pulldown.svelte"
import type { SvelteComponent } from "svelte"
import type { Account } from "../../server/lib/accounts"
import type cfg from "../../../config.json"
import type { FilePointer } from "../../server/lib/files"
export let refreshNeeded = writable(false)
export let pulldownManager = writable<SvelteComponent>()
export let account = writable<Account & {sessionCount: number, sessionExpires: number}|undefined>()
export let serverStats = writable<typeof cfg & {version: string, files: number} | undefined>()
export let files = writable<(FilePointer & {id:string})[]>([])
export let fetchAccountData = function() {
fetch("/auth/me").then(async (response) => {
if (response.status == 200) {
account.set(await response.json())
} else {
account.set(undefined)
}
}).catch((err) => { console.error(err) })
}
export let fetchFilePointers = function() {
fetch("/files/list", { cache: "no-cache" }).then(async (response) => {
if (response.status == 200) {
files.set(await response.json())
} else {
files.set([])
}
}).catch((err) => { console.error(err) })
}
export let refresh_stats = () => {
fetch("/server").then(async (data) => {
serverStats.set(await data.json())
}).catch((err) => { console.error(err) })
}
fetchAccountData()

View file

@ -1 +0,0 @@
/// <reference types="svelte" />

View file

@ -1,5 +0,0 @@
import App from "./App.svelte"
new App({
target: document.body
})

View file

@ -1,18 +0,0 @@
{
"extends": "@tsconfig/svelte/tsconfig.json",
"include": ["**/*"],
"compilerOptions": {
"target": "ESNext",
"outDir": "../../dist/static/vite",
"useDefineForClassFields": true,
"module": "ESNext",
"resolveJsonModule": true,
"allowJs": true,
"checkJs": true,
"isolatedModules": true,
"moduleResolution": "bundler"
},
"references": [
{ "path": "../../tsconfig.json" }
]
}

View file

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

View file

Before

Width:  |  Height:  |  Size: 401 KiB

After

Width:  |  Height:  |  Size: 401 KiB

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