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
README.md
**/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 Newtonsoft.Json;
using VoidCat.Model;
using VoidCat.Services.Abstractions;
@ -10,11 +10,13 @@ public class IndexController : Controller
{
private readonly IWebHostEnvironment _webHost;
private readonly IFileMetadataStore _fileMetadata;
private readonly VoidSettings _settings;
public IndexController(IFileMetadataStore fileMetadata, IWebHostEnvironment webHost)
public IndexController(IFileMetadataStore fileMetadata, IWebHostEnvironment webHost, VoidSettings settings)
{
_fileMetadata = fileMetadata;
_webHost = webHost;
_settings = settings;
}
/// <summary>
@ -28,22 +30,57 @@ public class IndexController : Controller
{
id.TryFromBase58Guid(out var gid);
var manifestPath = Path.Combine(_webHost.WebRootPath, "asset-manifest.json");
if (!System.IO.File.Exists(manifestPath)) return StatusCode(500);
var ubDownload = new UriBuilder(_settings.SiteUrl)
{
Path = $"/d/{gid.ToBase58()}"
};
// old format hash, return 404
if (id.Length == 40 && Regex.IsMatch(id, @"[0-9a-z]{40}"))
var ubView = new UriBuilder(_settings.SiteUrl)
{
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));
}
var jsonManifest = await System.IO.File.ReadAllTextAsync(manifestPath);
return View("~/Pages/Index.cshtml", new IndexModel
else if (mime?.StartsWith("video/") ?? false)
{
Id = gid,
Meta = (await _fileMetadata.Get(gid))?.ToMeta(false),
Manifest = JsonConvert.DeserializeObject<AssetManifest>(jsonManifest)!
});
tags.Add(new("type", "video.other"));
tags.Add(new("image", ""));
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
@ -59,4 +96,41 @@ public class IndexController : Controller
public Dictionary<string, string> Files { 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>
<ItemGroup>
<PackageReference Include="AngleSharp" Version="1.0.7" />
<PackageReference Include="AWSSDK.S3" Version="3.7.103.41" />
<PackageReference Include="BencodeNET" Version="5.0.0" />
<PackageReference Include="BTCPayServer.Lightning.Common" Version="1.3.21" />

View File

@ -1,192 +1,192 @@
import {
AdminProfile,
AdminUserListResult,
ApiError,
ApiKey,
LoginSession,
PagedRequest,
PagedResponse,
PaymentOrder,
Profile,
SetPaymentConfigRequest,
SiteInfoResponse,
VoidFileResponse,
AdminProfile,
AdminUserListResult,
ApiError,
ApiKey,
LoginSession,
PagedRequest,
PagedResponse,
PaymentOrder,
Profile,
SetPaymentConfigRequest,
SiteInfoResponse,
VoidFileResponse,
} from "./index";
import {
ProgressHandler,
ProxyChallengeHandler,
StateChangeHandler,
VoidUploader,
ProgressHandler,
ProxyChallengeHandler,
StateChangeHandler,
VoidUploader,
} from "./upload";
import { StreamUploader } from "./stream-uploader";
import { XHRUploader } from "./xhr-uploader";
import {StreamUploader} from "./stream-uploader";
import {XHRUploader} from "./xhr-uploader";
export type AuthHandler = (url: string, method: string) => Promise<string>;
export class VoidApi {
readonly #uri: string;
readonly #auth?: AuthHandler;
readonly #uri: string;
readonly #auth?: AuthHandler;
constructor(uri: string, auth?: AuthHandler) {
this.#uri = uri;
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";
constructor(uri?: string, auth?: AuthHandler) {
this.#uri = uri ?? "";
this.#auth = auth;
}
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);
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, {
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
*/
getUploader(
file: File | Blob,
stateChange?: StateChangeHandler,
progress?: ProgressHandler,
proxyChallenge?: ProxyChallengeHandler,
chunkSize?: number,
): VoidUploader {
if (StreamUploader.canUse()) {
return new StreamUploader(
this.#uri,
file,
stateChange,
progress,
proxyChallenge,
this.#auth,
chunkSize,
);
} else {
return new XHRUploader(
this.#uri,
file,
stateChange,
progress,
proxyChallenge,
this.#auth,
chunkSize,
);
/**
* Get uploader for uploading files
*/
getUploader(
file: File | Blob,
stateChange?: StateChangeHandler,
progress?: ProgressHandler,
proxyChallenge?: ProxyChallengeHandler,
chunkSize?: number,
): VoidUploader {
if (StreamUploader.canUse()) {
return new StreamUploader(
this.#uri,
file,
stateChange,
progress,
proxyChallenge,
this.#auth,
chunkSize,
);
} else {
return new XHRUploader(
this.#uri,
file,
stateChange,
progress,
proxyChallenge,
this.#auth,
chunkSize,
);
}
}
}
/**
* General site information
*/
info() {
return this.#req<SiteInfoResponse>("GET", "/info");
}
/**
* General site information
*/
info() {
return this.#req<SiteInfoResponse>("GET", "/info");
}
fileInfo(id: string) {
return this.#req<VoidFileResponse>("GET", `/upload/${id}`);
}
fileInfo(id: string) {
return this.#req<VoidFileResponse>("GET", `/upload/${id}`);
}
setPaymentConfig(id: string, cfg: SetPaymentConfigRequest) {
return this.#req("POST", `/upload/${id}/payment`, cfg);
}
setPaymentConfig(id: string, cfg: SetPaymentConfigRequest) {
return this.#req("POST", `/upload/${id}/payment`, cfg);
}
createOrder(id: string) {
return this.#req<PaymentOrder>("GET", `/upload/${id}/payment`);
}
createOrder(id: string) {
return this.#req<PaymentOrder>("GET", `/upload/${id}/payment`);
}
getOrder(file: string, order: string) {
return this.#req<PaymentOrder>("GET", `/upload/${file}/payment/${order}`);
}
getOrder(file: string, order: string) {
return this.#req<PaymentOrder>("GET", `/upload/${file}/payment/${order}`);
}
login(username: string, password: string, captcha?: string) {
return this.#req<LoginSession>("POST", `/auth/login`, {
username,
password,
captcha,
});
}
login(username: string, password: string, captcha?: string) {
return this.#req<LoginSession>("POST", `/auth/login`, {
username,
password,
captcha,
});
}
register(username: string, password: string, captcha?: string) {
return this.#req<LoginSession>("POST", `/auth/register`, {
username,
password,
captcha,
});
}
register(username: string, password: string, captcha?: string) {
return this.#req<LoginSession>("POST", `/auth/register`, {
username,
password,
captcha,
});
}
getUser(id: string) {
return this.#req<Profile>("GET", `/user/${id}`);
}
getUser(id: string) {
return this.#req<Profile>("GET", `/user/${id}`);
}
updateUser(u: Profile) {
return this.#req<void>("POST", `/user/${u.id}`, u);
}
updateUser(u: Profile) {
return this.#req<void>("POST", `/user/${u.id}`, u);
}
listUserFiles(uid: string, pageReq: PagedRequest) {
return this.#req<PagedResponse<VoidFileResponse>>(
"POST",
`/user/${uid}/files`,
pageReq,
);
}
listUserFiles(uid: string, pageReq: PagedRequest) {
return this.#req<PagedResponse<VoidFileResponse>>(
"POST",
`/user/${uid}/files`,
pageReq,
);
}
submitVerifyCode(uid: string, code: string) {
return this.#req<void>("POST", `/user/${uid}/verify`, { code });
}
submitVerifyCode(uid: string, code: string) {
return this.#req<void>("POST", `/user/${uid}/verify`, {code});
}
sendNewCode(uid: string) {
return this.#req<void>("GET", `/user/${uid}/verify`);
}
sendNewCode(uid: string) {
return this.#req<void>("GET", `/user/${uid}/verify`);
}
updateFileMetadata(id: string, meta: any) {
return this.#req<void>("POST", `/upload/${id}/meta`, meta);
}
updateFileMetadata(id: string, meta: any) {
return this.#req<void>("POST", `/upload/${id}/meta`, meta);
}
listApiKeys() {
return this.#req<Array<ApiKey>>("GET", `/auth/api-key`);
}
listApiKeys() {
return this.#req<Array<ApiKey>>("GET", `/auth/api-key`);
}
createApiKey(req: any) {
return this.#req<ApiKey>("POST", `/auth/api-key`, req);
}
createApiKey(req: any) {
return this.#req<ApiKey>("POST", `/auth/api-key`, req);
}
adminListFiles(pageReq: PagedRequest) {
return this.#req<PagedResponse<VoidFileResponse>>(
"POST",
"/admin/file",
pageReq,
);
}
adminListFiles(pageReq: PagedRequest) {
return this.#req<PagedResponse<VoidFileResponse>>(
"POST",
"/admin/file",
pageReq,
);
}
adminDeleteFile(id: string) {
return this.#req<void>("DELETE", `/admin/file/${id}`);
}
adminDeleteFile(id: string) {
return this.#req<void>("DELETE", `/admin/file/${id}`);
}
adminUserList(pageReq: PagedRequest) {
return this.#req<PagedResponse<AdminUserListResult>>(
"POST",
`/admin/users`,
pageReq,
);
}
adminUserList(pageReq: PagedRequest) {
return this.#req<PagedResponse<AdminUserListResult>>(
"POST",
`/admin/users`,
pageReq,
);
}
adminUpdateUser(u: AdminProfile) {
return this.#req<void>("POST", `/admin/update-user`, u);
}
adminUpdateUser(u: AdminProfile) {
return this.#req<void>("POST", `/admin/update-user`, u);
}
}

View File

@ -248,7 +248,7 @@ export function FilePreview() {
useEffect(() => {
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);
const order = window.localStorage.getItem(`payment-${info.id}`);

View File

@ -20,8 +20,9 @@ export default defineConfig({
],
assetsInclude: [],
build: {
outDir: "build",
outDir: "build"
},
base: "/",
clearScreen: false,
resolve: {
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"
]
}