Inject tags to index.html

This commit is contained in:
Kieran 2023-12-22 13:27:07 +00:00
parent b9a9d7bd26
commit c019dcb3fb
Signed by: Kieran
GPG Key ID: DE71CEB3925BE941
8 changed files with 255 additions and 272 deletions

View File

@ -23,4 +23,6 @@
LICENSE LICENSE
README.md README.md
**/appsettings.*.json **/appsettings.*.json
**/data **/data
**/build
**/dist

View File

@ -1,6 +1,6 @@
using System.Text.RegularExpressions; using AngleSharp;
using AngleSharp.Dom;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Newtonsoft.Json;
using VoidCat.Model; using VoidCat.Model;
using VoidCat.Services.Abstractions; using VoidCat.Services.Abstractions;
@ -10,11 +10,13 @@ public class IndexController : Controller
{ {
private readonly IWebHostEnvironment _webHost; private readonly IWebHostEnvironment _webHost;
private readonly IFileMetadataStore _fileMetadata; private readonly IFileMetadataStore _fileMetadata;
private readonly VoidSettings _settings;
public IndexController(IFileMetadataStore fileMetadata, IWebHostEnvironment webHost) public IndexController(IFileMetadataStore fileMetadata, IWebHostEnvironment webHost, VoidSettings settings)
{ {
_fileMetadata = fileMetadata; _fileMetadata = fileMetadata;
_webHost = webHost; _webHost = webHost;
_settings = settings;
} }
/// <summary> /// <summary>
@ -28,22 +30,57 @@ public class IndexController : Controller
{ {
id.TryFromBase58Guid(out var gid); id.TryFromBase58Guid(out var gid);
var manifestPath = Path.Combine(_webHost.WebRootPath, "asset-manifest.json"); var ubDownload = new UriBuilder(_settings.SiteUrl)
if (!System.IO.File.Exists(manifestPath)) return StatusCode(500); {
Path = $"/d/{gid.ToBase58()}"
};
// old format hash, return 404 var ubView = new UriBuilder(_settings.SiteUrl)
if (id.Length == 40 && Regex.IsMatch(id, @"[0-9a-z]{40}"))
{ {
Response.StatusCode = 404; Path = $"/{gid.ToBase58()}"
};
var indexPath = Path.Combine(_webHost.WebRootPath, "index.html");
var indexContent = await System.IO.File.ReadAllTextAsync(indexPath);
var meta = (await _fileMetadata.Get(gid))?.ToMeta(false);
var tags = new List<KeyValuePair<string, string>>()
{
new("site_name", "void.cat"),
new("title", meta?.Name ?? ""),
new("description", meta?.Description ?? ""),
new("url", ubView.Uri.ToString()),
};
var mime = meta?.MimeType;
if (mime?.StartsWith("image/") ?? false)
{
tags.Add(new("type", "image"));
tags.Add(new("image", ubDownload.Uri.ToString()));
tags.Add(new("image:type", mime));
} }
else if (mime?.StartsWith("video/") ?? false)
var jsonManifest = await System.IO.File.ReadAllTextAsync(manifestPath);
return View("~/Pages/Index.cshtml", new IndexModel
{ {
Id = gid, tags.Add(new("type", "video.other"));
Meta = (await _fileMetadata.Get(gid))?.ToMeta(false), tags.Add(new("image", ""));
Manifest = JsonConvert.DeserializeObject<AssetManifest>(jsonManifest)! tags.Add(new("video", ubDownload.Uri.ToString()));
}); tags.Add(new("video:url", ubDownload.Uri.ToString()));
tags.Add(new("video:secure_url", ubDownload.Uri.ToString()));
tags.Add(new("video:type", mime));
}
else if (mime?.StartsWith("audio/") ?? false)
{
tags.Add(new("type", "audio.other"));
tags.Add(new("audio", ubDownload.Uri.ToString()));
tags.Add(new("audio:type", mime));
}
else
{
tags.Add(new("type", "website"));
}
var injectedHtml = await InjectTags(indexContent, tags);
return Content(injectedHtml?.ToHtml() ?? indexContent, "text/html");
} }
public class IndexModel public class IndexModel
@ -59,4 +96,41 @@ public class IndexController : Controller
public Dictionary<string, string> Files { get; init; } public Dictionary<string, string> Files { get; init; }
public List<string> Entrypoints { get; init; } public List<string> Entrypoints { get; init; }
} }
}
private async Task<IDocument?> InjectTags(string html, List<KeyValuePair<string, string>> tags)
{
var config = Configuration.Default;
var context = BrowsingContext.New(config);
var doc = await context.OpenAsync(c => c.Content(html));
foreach (var tag in tags)
{
var ogTag = doc.CreateElement("meta");
ogTag.SetAttribute("property", $"og:{tag.Key}");
ogTag.SetAttribute("content", tag.Value);
doc.Head?.AppendChild(ogTag);
switch (tag.Key.ToLower())
{
case "title":
{
var titleTag = doc.Head?.QuerySelector("title");
if (titleTag != default)
{
titleTag.TextContent = tag.Value;
}
break;
}
case "description":
{
var descriptionTag = doc.Head?.QuerySelector("meta[name='description']");
descriptionTag?.SetAttribute("content", tag.Value);
break;
}
}
}
return doc;
}
}

View File

@ -1,85 +0,0 @@
@using VoidCat.Model
@model VoidCat.Controllers.IndexController.IndexModel
@inject VoidSettings Settings
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<meta name="theme-color" content="#000000"/>
<link rel="icon" href="/favicon.ico"/>
<link rel="apple-touch-icon" href="/logo.png"/>
<link rel="manifest" href="/manifest.json"/>
@if (Model.Meta != default)
{
var ubDownload = new UriBuilder(Settings.SiteUrl)
{
Path = $"/d/{Model.Id.ToBase58()}"
};
var ubView = new UriBuilder(Settings.SiteUrl)
{
Path = $"/{Model.Id.ToBase58()}"
};
<title>void.cat - @Model.Meta.Name</title>
<meta name="description" content="@Model.Meta.Description"/>
<meta property="og:site_name" content="void.cat"/>
<meta property="og:title" content="@Model.Meta.Name"/>
<meta property="og:description" content="@Model.Meta.Description"/>
<meta property="og:url" content="@ubView"/>
var mime = Model.Meta.MimeType;
if (!string.IsNullOrEmpty(mime))
{
if (mime.StartsWith("image/"))
{
<meta property="og:type" content="image"/>
<meta property="og:image" content="@ubDownload"/>
<meta property="og:image:type" content="@mime"/>
}
else if (mime.StartsWith("video/"))
{
<meta property="og:type" content="video.other"/>
<meta property="og:image" content=""/>
<meta property="og:video" content="@ubDownload"/>
<meta property="og:video:url" content="@ubDownload"/>
<meta property="og:video:secure_url" content="@ubDownload"/>
<meta property="og:video:type" content="@mime"/>
}
else if (mime.StartsWith("audio/"))
{
<meta property="og:type" content="audio.other"/>
<meta property="og:audio" content="@ubDownload"/>
<meta property="og:audio:type" content="@mime"/>
}
}
}
else
{
<title>void.cat</title>
<meta property="og:type" content="website"/>
<meta name="description" content="void.cat - free, simple file sharing."/>
}
@foreach (var ep in Model.Manifest.Entrypoints)
{
switch (System.IO.Path.GetExtension(ep))
{
case ".css":
{
<link rel="stylesheet" href="@ep"/>
break;
}
case ".js":
{
<script defer src="@ep"></script>
break;
}
}
}
</head>
<body>
<div id="root"></div>
</body>
</html>

View File

@ -13,6 +13,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="AngleSharp" Version="1.0.7" />
<PackageReference Include="AWSSDK.S3" Version="3.7.103.41" /> <PackageReference Include="AWSSDK.S3" Version="3.7.103.41" />
<PackageReference Include="BencodeNET" Version="5.0.0" /> <PackageReference Include="BencodeNET" Version="5.0.0" />
<PackageReference Include="BTCPayServer.Lightning.Common" Version="1.3.21" /> <PackageReference Include="BTCPayServer.Lightning.Common" Version="1.3.21" />

View File

@ -1,192 +1,192 @@
import { import {
AdminProfile, AdminProfile,
AdminUserListResult, AdminUserListResult,
ApiError, ApiError,
ApiKey, ApiKey,
LoginSession, LoginSession,
PagedRequest, PagedRequest,
PagedResponse, PagedResponse,
PaymentOrder, PaymentOrder,
Profile, Profile,
SetPaymentConfigRequest, SetPaymentConfigRequest,
SiteInfoResponse, SiteInfoResponse,
VoidFileResponse, VoidFileResponse,
} from "./index"; } from "./index";
import { import {
ProgressHandler, ProgressHandler,
ProxyChallengeHandler, ProxyChallengeHandler,
StateChangeHandler, StateChangeHandler,
VoidUploader, VoidUploader,
} from "./upload"; } from "./upload";
import { StreamUploader } from "./stream-uploader"; import {StreamUploader} from "./stream-uploader";
import { XHRUploader } from "./xhr-uploader"; import {XHRUploader} from "./xhr-uploader";
export type AuthHandler = (url: string, method: string) => Promise<string>; export type AuthHandler = (url: string, method: string) => Promise<string>;
export class VoidApi { export class VoidApi {
readonly #uri: string; readonly #uri: string;
readonly #auth?: AuthHandler; readonly #auth?: AuthHandler;
constructor(uri: string, auth?: AuthHandler) { constructor(uri?: string, auth?: AuthHandler) {
this.#uri = uri; this.#uri = uri ?? "";
this.#auth = auth; this.#auth = auth;
}
async #req<T>(method: string, url: string, body?: object): Promise<T> {
const absoluteUrl = `${this.#uri}${url}`;
const headers: HeadersInit = {
Accept: "application/json",
};
if (this.#auth) {
headers["Authorization"] = await this.#auth(absoluteUrl, method);
}
if (body) {
headers["Content-Type"] = "application/json";
} }
const res = await fetch(absoluteUrl, { async #req<T>(method: string, url: string, body?: object): Promise<T> {
method, const absoluteUrl = `${this.#uri}${url}`;
headers, const headers: HeadersInit = {
mode: "cors", Accept: "application/json",
body: body ? JSON.stringify(body) : undefined, };
}); if (this.#auth) {
const text = await res.text(); headers["Authorization"] = await this.#auth(absoluteUrl, method);
if (res.ok) { }
return text ? (JSON.parse(text) as T) : ({} as T); if (body) {
} else { headers["Content-Type"] = "application/json";
throw new ApiError(res.status, text); }
const res = await fetch(absoluteUrl, {
method,
headers,
mode: "cors",
body: body ? JSON.stringify(body) : undefined,
});
const text = await res.text();
if (res.ok) {
return text ? (JSON.parse(text) as T) : ({} as T);
} else {
throw new ApiError(res.status, text);
}
} }
}
/** /**
* Get uploader for uploading files * Get uploader for uploading files
*/ */
getUploader( getUploader(
file: File | Blob, file: File | Blob,
stateChange?: StateChangeHandler, stateChange?: StateChangeHandler,
progress?: ProgressHandler, progress?: ProgressHandler,
proxyChallenge?: ProxyChallengeHandler, proxyChallenge?: ProxyChallengeHandler,
chunkSize?: number, chunkSize?: number,
): VoidUploader { ): VoidUploader {
if (StreamUploader.canUse()) { if (StreamUploader.canUse()) {
return new StreamUploader( return new StreamUploader(
this.#uri, this.#uri,
file, file,
stateChange, stateChange,
progress, progress,
proxyChallenge, proxyChallenge,
this.#auth, this.#auth,
chunkSize, chunkSize,
); );
} else { } else {
return new XHRUploader( return new XHRUploader(
this.#uri, this.#uri,
file, file,
stateChange, stateChange,
progress, progress,
proxyChallenge, proxyChallenge,
this.#auth, this.#auth,
chunkSize, chunkSize,
); );
}
} }
}
/** /**
* General site information * General site information
*/ */
info() { info() {
return this.#req<SiteInfoResponse>("GET", "/info"); return this.#req<SiteInfoResponse>("GET", "/info");
} }
fileInfo(id: string) { fileInfo(id: string) {
return this.#req<VoidFileResponse>("GET", `/upload/${id}`); return this.#req<VoidFileResponse>("GET", `/upload/${id}`);
} }
setPaymentConfig(id: string, cfg: SetPaymentConfigRequest) { setPaymentConfig(id: string, cfg: SetPaymentConfigRequest) {
return this.#req("POST", `/upload/${id}/payment`, cfg); return this.#req("POST", `/upload/${id}/payment`, cfg);
} }
createOrder(id: string) { createOrder(id: string) {
return this.#req<PaymentOrder>("GET", `/upload/${id}/payment`); return this.#req<PaymentOrder>("GET", `/upload/${id}/payment`);
} }
getOrder(file: string, order: string) { getOrder(file: string, order: string) {
return this.#req<PaymentOrder>("GET", `/upload/${file}/payment/${order}`); return this.#req<PaymentOrder>("GET", `/upload/${file}/payment/${order}`);
} }
login(username: string, password: string, captcha?: string) { login(username: string, password: string, captcha?: string) {
return this.#req<LoginSession>("POST", `/auth/login`, { return this.#req<LoginSession>("POST", `/auth/login`, {
username, username,
password, password,
captcha, captcha,
}); });
} }
register(username: string, password: string, captcha?: string) { register(username: string, password: string, captcha?: string) {
return this.#req<LoginSession>("POST", `/auth/register`, { return this.#req<LoginSession>("POST", `/auth/register`, {
username, username,
password, password,
captcha, captcha,
}); });
} }
getUser(id: string) { getUser(id: string) {
return this.#req<Profile>("GET", `/user/${id}`); return this.#req<Profile>("GET", `/user/${id}`);
} }
updateUser(u: Profile) { updateUser(u: Profile) {
return this.#req<void>("POST", `/user/${u.id}`, u); return this.#req<void>("POST", `/user/${u.id}`, u);
} }
listUserFiles(uid: string, pageReq: PagedRequest) { listUserFiles(uid: string, pageReq: PagedRequest) {
return this.#req<PagedResponse<VoidFileResponse>>( return this.#req<PagedResponse<VoidFileResponse>>(
"POST", "POST",
`/user/${uid}/files`, `/user/${uid}/files`,
pageReq, pageReq,
); );
} }
submitVerifyCode(uid: string, code: string) { submitVerifyCode(uid: string, code: string) {
return this.#req<void>("POST", `/user/${uid}/verify`, { code }); return this.#req<void>("POST", `/user/${uid}/verify`, {code});
} }
sendNewCode(uid: string) { sendNewCode(uid: string) {
return this.#req<void>("GET", `/user/${uid}/verify`); return this.#req<void>("GET", `/user/${uid}/verify`);
} }
updateFileMetadata(id: string, meta: any) { updateFileMetadata(id: string, meta: any) {
return this.#req<void>("POST", `/upload/${id}/meta`, meta); return this.#req<void>("POST", `/upload/${id}/meta`, meta);
} }
listApiKeys() { listApiKeys() {
return this.#req<Array<ApiKey>>("GET", `/auth/api-key`); return this.#req<Array<ApiKey>>("GET", `/auth/api-key`);
} }
createApiKey(req: any) { createApiKey(req: any) {
return this.#req<ApiKey>("POST", `/auth/api-key`, req); return this.#req<ApiKey>("POST", `/auth/api-key`, req);
} }
adminListFiles(pageReq: PagedRequest) { adminListFiles(pageReq: PagedRequest) {
return this.#req<PagedResponse<VoidFileResponse>>( return this.#req<PagedResponse<VoidFileResponse>>(
"POST", "POST",
"/admin/file", "/admin/file",
pageReq, pageReq,
); );
} }
adminDeleteFile(id: string) { adminDeleteFile(id: string) {
return this.#req<void>("DELETE", `/admin/file/${id}`); return this.#req<void>("DELETE", `/admin/file/${id}`);
} }
adminUserList(pageReq: PagedRequest) { adminUserList(pageReq: PagedRequest) {
return this.#req<PagedResponse<AdminUserListResult>>( return this.#req<PagedResponse<AdminUserListResult>>(
"POST", "POST",
`/admin/users`, `/admin/users`,
pageReq, pageReq,
); );
} }
adminUpdateUser(u: AdminProfile) { adminUpdateUser(u: AdminProfile) {
return this.#req<void>("POST", `/admin/update-user`, u); return this.#req<void>("POST", `/admin/update-user`, u);
} }
} }

View File

@ -248,7 +248,7 @@ export function FilePreview() {
useEffect(() => { useEffect(() => {
if (info) { if (info) {
const fileLink = info.metadata?.url ?? `${import.meta.env.VITE_API_HOST}/d/${info.id}`; const fileLink = info.metadata?.url ?? `${import.meta.env.VITE_API_HOST ?? ""}/d/${info.id}`;
setFileSize(info.metadata?.size ?? 0); setFileSize(info.metadata?.size ?? 0);
const order = window.localStorage.getItem(`payment-${info.id}`); const order = window.localStorage.getItem(`payment-${info.id}`);

View File

@ -20,8 +20,9 @@ export default defineConfig({
], ],
assetsInclude: [], assetsInclude: [],
build: { build: {
outDir: "build", outDir: "build"
}, },
base: "/",
clearScreen: false, clearScreen: false,
resolve: { resolve: {
alias: { alias: {

View File

@ -1,10 +0,0 @@
{
"files": {
"main.js": "/static/js/bundle.js",
"index.html": "/index.html",
"bundle.js.map": "/static/js/bundle.js.map",
},
"entrypoints": [
"static/js/bundle.js"
]
}