Fixed 267U.pre2

This commit is contained in:
2026-07-26 18:25:37 +09:00
parent 50bfaeafdf
commit 317d00a284
1286 changed files with 80222 additions and 1 deletions
@@ -0,0 +1 @@
main>.tabs{padding:16px;border-bottom:4px solid #908caa}#lsEditor>.adder{margin:16px;padding:16px;border:2px solid #908caa}#lsEditor>.adder>textarea{display:block;width:100%;min-height:5em;box-sizing:border-box}#lsEditor>.record{padding:16px;border-bottom:1px solid #908caa}#lsEditor>.record>header{font-weight:700}#lsEditor>.record>textarea{display:block;width:100%;min-height:5em;box-sizing:border-box}html{background:#191724}main{background:#1f1d2e;border-radius:10px}#tl>div{padding:16px;border-bottom:1px solid #908caa}#tl>div>header{font-weight:700}*{font-family:BIZ UDGothic,Roboto,HelveticaNeue,Arial,sans-serif}#iceshrimp_app{display:none!important}body,html{background-color:#191724;color:#e0def4;justify-content:center;margin:auto;padding:10px;text-align:center}button{border-radius:999px;border:none;cursor:pointer;margin-bottom:12px;background:linear-gradient(-45deg,#9ccfd8,#31748f);line-height:50px;color:#191724;font-weight:700;font-size:20px;padding:12px}button{border-radius:999px;padding:0 12px;border:none;cursor:pointer;margin-bottom:12px}button{background:#444;line-height:40px;color:#9ccfd8;font-size:16px;padding:0 20px;margin-right:5px;margin-left:5px}button:hover{background:#555}#ls{background:linear-gradient(-45deg,#9ccfd8,#31748f);line-height:30px;color:#191724;font-weight:700;font-size:18px;padding:12px}#ls:hover{background:#9ccfd8}a{color:#9ccfd8;text-decoration:none}p,li{font-size:16px}h1{font-size:32px}code{font-family:Fira,FiraCode,monospace}textarea{background-color:#444;border:solid #aaa;border-radius:10px;color:#e0def4;margin-top:1rem;margin-bottom:1rem;width:20rem;height:7.5rem;padding:.5rem}textarea:focus{border:solid #eee}input{background-color:#666;border:solid #aaa;border-radius:10px;color:#e0def4;margin-top:1rem;margin-bottom:1rem;width:10rem;height:1rem;padding:.5rem}input:focus{border:solid #eee}
@@ -0,0 +1 @@
"use strict";window.onload=async()=>{const m=JSON.parse(localStorage.getItem("account")).token,E=(e,t={})=>new Promise((d,a)=>{m&&(t.i=m),fetch(e.indexOf("://")>-1?e:`/api/${e}`,{method:"POST",body:JSON.stringify(t),credentials:"omit",cache:"no-cache"}).then(async n=>{const o=n.status===204?null:await n.json();n.status===200?d(o):n.status===204?d():a(o.error)}).catch(a)}),u=document.getElementById("content");document.getElementById("ls").addEventListener("click",()=>{u.innerHTML="";const e=document.createElement("div");e.id="lsEditor";const t=document.createElement("div");t.classList.add("adder");const l=document.createElement("input"),d=document.createElement("textarea"),a=document.createElement("button");a.textContent="Add",a.addEventListener("click",()=>{localStorage.setItem(l.value,d.value),location.reload()}),t.appendChild(l),t.appendChild(d),t.appendChild(a),e.appendChild(t);for(let n=0;n<localStorage.length;n++){const o=localStorage.key(n),c=document.createElement("div");c.classList.add("record");const p=document.createElement("header");p.textContent=o;const r=document.createElement("textarea");r.textContent=localStorage.getItem(o);const i=document.createElement("button");i.textContent="Save",i.addEventListener("click",()=>{localStorage.setItem(o,r.value),location.reload()});const s=document.createElement("button");s.textContent="Remove",s.addEventListener("click",()=>{localStorage.removeItem(o),location.reload()}),c.appendChild(p),c.appendChild(r),c.appendChild(i),c.appendChild(s),e.appendChild(c)}u.appendChild(e)})};
+149
View File
@@ -0,0 +1,149 @@
"use strict";var b=["ar-SA","bn-BD","bul-BG","ca-ES","cs-CZ","da-DK","de-DE","el-GR","en-US","es-ES","et","fi","fil","fr-FR","gl","id-ID","it-IT","ja-JP","ja-KS","kab-KAB","kn-IN","ko-KR","nl-NL","no-NO","pl-PL","pt-BR","pt-PT","ro-RO","ru-RU","sk-SK","sv-SE","th-TH","tr-TR","ug-CN","uk-UA","vi-VN","zh-CN","zh-TW"];(async()=>{window.onerror=e=>{console.error(e),a("SOMETHING_HAPPENED",e)},window.onunhandledrejection=e=>{console.error(e),a("SOMETHING_HAPPENED_IN_PROMISE",e)};const f=localStorage.getItem("v"),s=VERSION;f!==VERSION&&localStorage.setItem("v",VERSION);const l=b;let o=localStorage.getItem("lang");(o==null||!l.includes(o))&&(l.includes(navigator.language)?o=navigator.language:(o=l.find(e=>e.split("-")[0]===navigator.language),o==null&&(o="en-US")));const c=await fetch(`/assets/locales/${o}.${s}.json`);if(c.status===200)localStorage.setItem("lang",o),localStorage.setItem("locale",await c.text()),localStorage.setItem("localeVersion",s);else{await p(),a("LOCALE_FETCH");return}function d(){import(`/assets/${CLIENT_ENTRY}`).catch(async e=>{await p(),console.error(e),a("APP_IMPORT",e)})}document.readyState!=="loading"?d():window.addEventListener("DOMContentLoaded",()=>{d()});const m=localStorage.getItem("theme");if(m){for(const[e,t]of Object.entries(JSON.parse(m)))if(document.documentElement.style.setProperty(`--${e}`,t.toString()),e==="htmlThemeColor"){for(const n of document.head.children)if(n.tagName==="META"&&n.getAttribute("name")==="theme-color"){n.setAttribute("content",t);break}}}const u=localStorage.getItem("colorSchema");u&&document.documentElement.style.setProperty("color-schema",u);let r=localStorage.getItem("fontSize");r&&(r<10&&(localStorage.setItem("fontSize",null),r=localStorage.getItem("fontSize")),document.documentElement.style.fontSize=`${r}px`),["ja-JP","ja-KS","ko-KR","zh-CN","zh-TW"].includes(o)&&document.documentElement.classList.add("useCJKFont"),localStorage.getItem("useSystemFont")&&document.documentElement.classList.add("useSystemFont");const g=localStorage.getItem("wallpaper");g&&(document.documentElement.style.backgroundImage=`url(${g})`);const i=localStorage.getItem("customCss");if(i&&i.length>0){const e=document.createElement("style");e.innerHTML=i,document.head.appendChild(e)}async function S(e){const t=document.createElement("style");t.appendChild(document.createTextNode(e)),document.head.appendChild(t)}function a(e,t){let n=document.getElementById("errors");n||(document.body.innerHTML=`
<svg class="icon-warning" xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-alert-triangle" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M12 9v2m0 4v.01"></path>
<path d="M5 19h14a2 2 0 0 0 1.84 -2.75l-7.1 -12.25a2 2 0 0 0 -3.5 0l-7.1 12.25a2 2 0 0 0 1.75 2.75"></path>
</svg>
<h1>An error has occurred!</h1>
<button class="button-big" onclick="location.reload(true);">
<span class="button-label-big">&#8635; Refresh</span>
</button>
<p class="dont-worry">Don't worry, it's (probably) not your fault.</p>
<p>Please make sure your browser is up-to-date.</p>
<p>While we don't serve ads, ad-blockers might interfere with this application,<br/> so disabling them might also fix this issue.</p>
<p>If the problem persists after refreshing, please contact your instance's administrator.<br>You may also try the following options:</p>
<a href="/flush">
<button class="button-small">
<span class="button-label-small">&#128465; Clear preferences and cache</span>
</button>
</a>
<br>
<a href="/cli">
<button class="button-small">
<span class="button-label-small">&#128491; Start the simple client</span>
</button>
</a>
<br>
<a href="/bios">
<button class="button-small">
<span class="button-label-small">&#9881; Start the repair tool</span>
</button>
</a>
<br>
<div id="errors"></div>
`,n=document.getElementById("errors"));const h=document.createElement("details");h.innerHTML=`
<br>
<summary>
<code>ERROR CODE: ${e}</code>
</summary>
<code>${JSON.stringify(t)}</code>`,n.appendChild(h),S(`
* {
font-family: Roboto,HelveticaNeue,Arial,sans-serif;
}
#iceshrimp_app,
#splash {
display: none !important;
}
body,
html {
background-color: #3b364c;
color: rgb(231, 237, 255);
justify-content: center;
margin: auto;
padding: 10px;
text-align: center;
}
button {
border-radius: 999px;
padding: 0px 12px 0px 12px;
border: none;
cursor: pointer;
margin-bottom: 12px;
}
.button-big {
background: linear-gradient(-45deg, rgb(154, 146, 255), rgb(131, 114, 245));
line-height: 50px;
}
.button-big:hover {
background: rgb(201, 197, 255);
}
.button-small {
background: #544d77;
line-height: 40px;
}
.button-small:hover {
background: #504967;
}
.button-label-big {
color: #3b364c;
font-weight: bold;
font-size: 2em;
padding: 12px;
}
.button-label-small {
color: rgb(231, 237, 255);
font-size: 16px;
padding: 12px;
}
a {
color: rgb(255, 123, 114);
text-decoration: none;
}
p,
li {
font-size: 16px;
}
.dont-worry,
#msg {
font-size: 18px;
}
.icon-warning {
color: rgb(236, 182, 55);
height: 4rem;
padding-top: 2rem;
}
h1 {
font-size: 32px;
}
code {
font-family: Fira, FiraCode, monospace;
}
details {
background: #423c55;
margin-bottom: 2rem;
padding: 0.5rem 1rem;
width: 40rem;
border-radius: 10px;
justify-content: center;
margin: auto;
}
summary {
cursor: pointer;
}
summary > * {
display: inline;
}
@media screen and (max-width: 500px) {
details {
width: 50%;
}
`)}async function p(){try{const t=await(await fetch("/api/meta",{method:"POST",cache:"no-cache"})).json();t.version!=s&&(localStorage.setItem("v",t.version),y())}catch(e){throw console.error(e),a("UPDATE_CHECK",e),e}}function y(){try{navigator.serviceWorker.controller.postMessage("clear"),navigator.serviceWorker.getRegistrations().then(e=>{e.forEach(t=>t.unregister())})}catch(e){console.error(e)}location.reload()}})();
@@ -0,0 +1 @@
html{background:#191724}main{background:#1f1d2e;border-radius:10px}#tl>div{border:1px solid #908caa;border-radius:10px;margin:10px;padding:10px;width:fit-content}#tl>div>header{font-weight:700;display:inline-flex}img{border-radius:10px;margin-right:10px}#form{text-align:center}#iceshrimp_app{display:none!important}body,html{font-family:BIZ UDGothic,Roboto,HelveticaNeue,Arial,sans-serif;background-color:#191724;color:#e0def4;justify-content:center;margin:auto;padding:10px}button{border-radius:999px;padding:0 40px;margin-top:1rem;border:none;cursor:pointer;margin-bottom:12px;background:linear-gradient(-45deg,#9ccfd8,#31748f);line-height:50px;color:#191724;font-weight:700;font-size:20px}button:hover{background:#9ccfd8}a{color:#9ccfd8;text-decoration:none}p,li{font-size:16px}h1{font-size:32px}code{font-family:Fira,FiraCode,monospace}#text{background-color:#444;border:solid #aaa;border-radius:10px;color:#e0def4;margin-top:3rem;width:20rem;height:5rem;padding:.5rem}#text:focus{border:solid #eee}@media screen and (max-width:500px){#text{width:80%}}
+1
View File
@@ -0,0 +1 @@
"use strict";window.onload=async()=>{const i=JSON.parse(localStorage.getItem("account")).token,l=(a,s={})=>new Promise((t,c)=>{i&&(s.i=i),fetch(a.indexOf("://")>-1?a:`/api/${a}`,{method:"POST",body:JSON.stringify(s),credentials:"omit",cache:"no-cache"}).then(async n=>{const o=n.status===204?null:await n.json();n.status===200?t(o):n.status===204?t():c(o.error)}).catch(c)});document.getElementById("submit").addEventListener("click",()=>{l("notes/create",{text:document.getElementById("text").value}).then(()=>{location.reload()})}),l("notes/timeline").then(a=>{const s=document.getElementById("tl");for(const e of a){const t=document.createElement("div"),c=document.createElement("header"),n=document.createElement("p"),o=document.createElement("img");n.textContent=`${e.user.name} @${e.user.username}`,o.src=e.user.avatarUrl,o.style="height: 40px";const d=document.createElement("div");if(d.textContent=`${e.text}`,t.appendChild(c),c.appendChild(o),c.appendChild(n),e.text&&t.appendChild(d),e.files)for(const r of e.files){const m=document.createElement("img");m.src=r.properties.thumbnailUrl,t.appendChild(m)}s.appendChild(t)}})};
+130
View File
@@ -0,0 +1,130 @@
import { Feed } from "feed";
import { In, IsNull } from "typeorm";
import config from "../../config/index.js";
import { Notes, DriveFiles, UserProfiles, Users } from "../../models/index.js";
export default async function(user, threadDepth = 5, history = 20, noteintitle = false, renotes = true, replies = true) {
const author = {
link: `${config.url}/@${user.username}`,
email: `${user.username}@${config.domain}`,
name: user.name || user.username
};
const profile = await UserProfiles.findOneByOrFail({
userId: user.id
});
const searchCriteria = {
userId: user.id,
visibility: In([
"public",
"home"
])
};
if (!renotes) {
searchCriteria.renoteId = IsNull();
}
if (!replies) {
searchCriteria.replyId = IsNull();
}
const notes = await Notes.find({
where: searchCriteria,
order: {
createdAt: -1
},
take: history
});
const feed = new Feed({
id: author.link,
title: `${author.name} (@${user.username}@${config.domain})`,
updated: notes[0].createdAt,
generator: "FrozenFriendsYume",
description: `${user.notesCount} Notes, ${profile.ffVisibility === "public" ? user.followingCount : "?"} Following, ${profile.ffVisibility === "public" ? user.followersCount : "?"} Followers${profile.description ? ` · ${profile.description}` : ""}`,
link: author.link,
image: await Users.getAvatarUrl(user),
feedLinks: {
json: `${author.link}.json`,
atom: `${author.link}.atom`
},
author,
copyright: user.name || user.username
});
for (const note of notes){
let contentStr = await noteToString(note, true);
let next = note.renoteId ? note.renoteId : note.replyId;
let depth = threadDepth;
while(depth > 0 && next){
const finding = await findById(next);
contentStr += finding.text;
next = finding.next;
depth -= 1;
}
let title = `${author.name} `;
if (note.renoteId) {
title += "renotes";
} else if (note.replyId) {
title += "replies";
} else {
title += "says";
}
if (noteintitle) {
const content = note.cw ?? note.text;
if (content) {
title += `: ${content}`;
} else {
title += "something";
}
}
feed.addItem({
title: title.replace(/[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/g, "").substring(0, 100),
link: `${config.url}/notes/${note.id}`,
date: note.createdAt,
description: note.cw ? note.cw.replace(/[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/g, "") : undefined,
content: contentStr.replace(/[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/g, "")
});
}
async function noteToString(note, isTheNote = false) {
const author = isTheNote ? null : await Users.findOneBy({
id: note.userId
});
let outstr = author ? `${author.name}(@${author.username}@${author.host ? author.host : config.host}) ${note.renoteId ? "renotes" : note.replyId ? "replies" : "says"}: <br>` : "";
const files = note.fileIds.length > 0 ? await DriveFiles.findBy({
id: In(note.fileIds)
}) : [];
let fileEle = "";
for (const file of files){
if (file.type.startsWith("image/")) {
fileEle += ` <br><img src="${DriveFiles.getPublicUrl(file)}">`;
} else if (file.type.startsWith("audio/")) {
fileEle += ` <br><audio controls src="${DriveFiles.getPublicUrl(file)}" type="${file.type}">`;
} else if (file.type.startsWith("video/")) {
fileEle += ` <br><video controls src="${DriveFiles.getPublicUrl(file)}" type="${file.type}">`;
} else {
fileEle += ` <br><a href="${DriveFiles.getPublicUrl(file)}" download="${file.name}">${file.name}</a>`;
}
}
outstr += `${note.cw ? note.cw + "<br>" : ""}${note.text || ""}${fileEle}`;
if (isTheNote) {
outstr += ` <span class="${note.renoteId ? "renote_note" : note.replyId ? "reply_note" : "new_note"} ${fileEle.indexOf("img src") !== -1 ? "with_img" : "without_img"}"></span>`;
}
return outstr;
}
async function findById(id) {
let text = "";
let next = null;
const findings = await Notes.findOneBy({
id: id,
visibility: In([
"public",
"home"
])
});
if (findings) {
text += `<hr>`;
text += await noteToString(findings);
next = findings.renoteId ? findings.renoteId : findings.replyId;
}
return {
text,
next
};
}
return feed;
}
+546
View File
@@ -0,0 +1,546 @@
/**
* Web Client Server
*/ import { dirname } from "node:path";
import { fileURLToPath } from "node:url";
import { readFileSync } from "node:fs";
import Koa from "koa";
import Router from "@koa/router";
import send from "koa-send";
import favicon from "koa-favicon";
import views from "koa-views";
import sharp from "sharp";
import { createBullBoard } from "@bull-board/api";
import { BullAdapter } from "@bull-board/api/bullAdapter.js";
import { KoaAdapter } from "@bull-board/koa";
import { In, IsNull } from "typeorm";
import { fetchMeta, metaToPugArgs } from "../../misc/fetch-meta.js";
import config from "../../config/index.js";
import { Users, Notes, UserProfiles, Pages, Channels, Clips, GalleryPosts } from "../../models/index.js";
import * as Acct from "../../misc/acct.js";
import { getNoteSummary } from "../../misc/get-note-summary.js";
import { queues } from "../../queue/queues.js";
import { genOpenapiSpec } from "../api/openapi/gen-spec.js";
import { urlPreviewHandler } from "./url-preview.js";
import { manifestHandler } from "./manifest.js";
import packFeed from "./feed.js";
import { MINUTE, DAY } from "../../const.js";
const _filename = fileURLToPath(import.meta.url);
const _dirname = dirname(_filename);
const staticAssets = `${_dirname}/../../../assets/`;
const clientAssets = `${_dirname}/../../../../client/assets/`;
const assets = `${_dirname}/../../../../../built/_client_dist_/`;
const swAssets = `${_dirname}/../../../../../built/_sw_dist_/`;
// Init app
const app = new Koa();
//#region Bull Dashboard
const bullBoardPath = "/queue";
// Authenticate
app.use(async (ctx, next)=>{
const url = decodeURI(ctx.path);
if (url === bullBoardPath || url.startsWith(`${bullBoardPath}/`)) {
if (!url.startsWith(bullBoardPath + '/static/')) {
ctx.set('Cache-Control', 'private, max-age=0, must-revalidate');
}
const token = ctx.cookies.get("token");
if (token == null) {
ctx.status = 401;
return;
}
const user = await Users.findOneBy({
token
});
if (user == null || !(user.isAdmin || user.isModerator)) {
ctx.status = 403;
return;
}
}
await next();
});
const serverAdapter = new KoaAdapter();
createBullBoard({
queues: queues.map((q)=>new BullAdapter(q)),
serverAdapter
});
serverAdapter.setBasePath(bullBoardPath);
const bullBoardMiddleware = serverAdapter.registerPlugin();
app.use(async (ctx, next)=>{
const url = decodeURI(ctx.path);
if (url === bullBoardPath || url.startsWith(`${bullBoardPath}/`)) {
await bullBoardMiddleware(ctx, next);
return;
}
await next();
});
//#endregion
// Init renderer
app.use(views(`${_dirname}/views`, {
extension: "pug",
options: {
version: config.version,
getClientEntry: ()=>process.env.NODE_ENV === "production" ? config.clientEntry : JSON.parse(readFileSync(`${_dirname}/../../../../../built/_client_dist_/manifest.json`, "utf-8"))["src/init.ts"],
config
}
}));
// Serve favicon
app.use(favicon(`${_dirname}/../../../assets/favicon.ico`));
// Common request handler
app.use(async (ctx, next)=>{
// IFrameの中に入れられないようにする
ctx.set("X-Frame-Options", "DENY");
await next();
});
// Init router
const router = new Router();
//#region static assets
router.get("/static-assets/{*splat}", async (ctx)=>{
await send(ctx, ctx.path.replace("/static-assets/", ""), {
root: staticAssets,
maxage: 7 * DAY
});
});
router.get("/client-assets/{*splat}", async (ctx)=>{
await send(ctx, ctx.path.replace("/client-assets/", ""), {
root: clientAssets,
maxage: 7 * DAY
});
});
router.get("/assets/{*splat}", async (ctx)=>{
await send(ctx, ctx.path.replace("/assets/", ""), {
root: assets,
maxage: 7 * DAY
});
});
// Apple touch icon
router.get("/apple-touch-icon.png", async (ctx)=>{
await send(ctx, "/apple-touch-icon.png", {
root: staticAssets
});
});
router.get("/twemoji/{*splat}", async (ctx)=>{
const path = ctx.path.replace("/twemoji/", "");
if (!path.match(/^[0-9a-f-]+\.svg$/)) {
ctx.status = 404;
return;
}
ctx.set("Content-Security-Policy", "default-src 'none'; style-src 'unsafe-inline'");
await send(ctx, path, {
root: `${_dirname}../../../../../../.yarn/unplugged/@discordapp-twemoji-npm-16.0.1-50d71ee905/node_modules/@discordapp/twemoji/dist/svg/`,
maxage: 30 * DAY
});
});
router.get("/twemoji-badge/{*splat}", async (ctx)=>{
const path = ctx.path.replace("/twemoji-badge/", "");
if (!path.match(/^[0-9a-f-]+\.png$/)) {
ctx.status = 404;
return;
}
const mask = await sharp(`${_dirname}../../../../../../.yarn/unplugged/@discordapp-twemoji-npm-16.0.1-50d71ee905/node_modules/@discordapp/twemoji/dist/svg/${path.replace(".png", "")}.svg`, {
density: 1000
}).resize(488, 488).greyscale().normalise().linear(1.75, -128 * 1.75 + 128) // 1.75x contrast
.flatten({
background: "#000"
}).extend({
top: 12,
bottom: 12,
left: 12,
right: 12,
background: "#000"
}).toColorspace("b-w").png().toBuffer();
const buffer = await sharp({
create: {
width: 512,
height: 512,
channels: 4,
background: {
r: 0,
g: 0,
b: 0,
alpha: 0
}
}
}).pipelineColorspace("b-w").boolean(mask, "eor").resize(96, 96).png().toBuffer();
ctx.set("Content-Security-Policy", "default-src 'none'; style-src 'unsafe-inline'");
ctx.set("Cache-Control", "max-age=2592000");
ctx.set("Content-Type", "image/png");
ctx.body = buffer;
});
// ServiceWorker
router.get("/sw.js", async (ctx)=>{
await send(ctx, "/sw.js", {
root: swAssets,
maxage: 10 * MINUTE
});
});
router.get("/sw.js.map", async (ctx)=>{
await send(ctx, "/sw.js.map", {
root: swAssets,
maxage: 10 * MINUTE
});
});
// Manifest
router.get("/manifest.json", manifestHandler);
router.get("/robots.txt", async (ctx)=>{
await send(ctx, "/robots.txt", {
root: staticAssets
});
});
//#endregion
// Docs
router.get("/api-doc", async (ctx)=>{
await send(ctx, "/redoc.html", {
root: staticAssets
});
});
// URL preview endpoint
router.get("/url", urlPreviewHandler);
router.get("/api.json", async (ctx)=>{
ctx.body = genOpenapiSpec();
});
const getFeed = async (acct, threadDepth, historyCount, noteInTitle, noRenotes, noReplies)=>{
const meta = await fetchMeta();
if (meta.privateMode) {
return;
}
const { username, host } = Acct.parse(acct);
const user = await Users.findOneBy({
usernameLower: username.toLowerCase(),
host: host ?? IsNull(),
isSuspended: false,
isLocked: false
});
if (!user) {
return;
}
let thread = parseInt(threadDepth, 10);
if (isNaN(thread) || thread < 0 || thread > 30) {
thread = 3;
}
let history = parseInt(historyCount, 10);
//cant be 0 here or it will get all posts
if (isNaN(history) || history <= 0 || history > 30) {
history = 20;
}
return user && await packFeed(user, thread, history, !isNaN(noteInTitle), isNaN(noRenotes), isNaN(noReplies));
};
// As the /@user[.json|.rss|.atom]/sub endpoint is complicated, we will use a regex to switch between them.
const reUser = new RegExp("^/@(?<user>[^/]+?)(?:.(?<feed>json|rss|atom)(?:\\?[^/]*)?)?(?:/(?<sub>[^/]+))?$");
router.get(reUser, async (ctx, next)=>{
const groups = reUser.exec(ctx.originalUrl)?.groups;
if (!groups) {
await next();
return;
}
ctx.params = groups;
//console.log(ctx, ctx.params, ctx.query);
if (groups.feed) {
if (groups.sub) {
await next();
return;
}
switch(groups.feed){
case "json":
await jsonFeed(ctx, next);
break;
case "rss":
await rssFeed(ctx, next);
break;
case "atom":
await atomFeed(ctx, next);
break;
}
return;
}
await userPage(ctx, next);
});
// Atom
const atomFeed = async (ctx)=>{
const feed = await getFeed(ctx.params.user, ctx.query.thread, ctx.query.history, ctx.query.noteintitle, ctx.query.norenotes, ctx.query.noreplies);
if (feed) {
ctx.set("Content-Type", "application/atom+xml; charset=utf-8");
ctx.body = feed.atom1();
} else {
ctx.status = 404;
}
};
// RSS
const rssFeed = async (ctx)=>{
const feed = await getFeed(ctx.params.user, ctx.query.thread, ctx.query.history, ctx.query.noteintitle, ctx.query.norenotes, ctx.query.noreplies);
if (feed) {
ctx.set("Content-Type", "application/rss+xml; charset=utf-8");
ctx.body = feed.rss2();
} else {
ctx.status = 404;
}
};
// JSON
const jsonFeed = async (ctx)=>{
const feed = await getFeed(ctx.params.user, ctx.query.thread, ctx.query.history, ctx.query.noteintitle, ctx.query.norenotes, ctx.query.noreplies);
if (feed) {
ctx.set("Content-Type", "application/json; charset=utf-8");
ctx.body = feed.json1();
} else {
ctx.status = 404;
}
};
//#region SSR (for crawlers)
// User
const userPage = async (ctx, next)=>{
const userParam = ctx.params.user;
const subParam = ctx.params.sub;
const { username, host } = Acct.parse(userParam);
const user = await Users.findOneBy({
usernameLower: username.toLowerCase(),
host: host ?? IsNull(),
isSuspended: false
});
if (user === null) {
await next();
return;
}
const profile = await UserProfiles.findOneByOrFail({
userId: user.id
});
const meta = await fetchMeta();
const me = profile.fields ? profile.fields.filter((filed)=>filed.value?.match(/^https?:/)).map((field)=>field.value) : [];
const userDetail = {
...metaToPugArgs(meta),
user,
profile,
me,
avatarUrl: await Users.getAvatarUrl(user),
sub: subParam
};
await ctx.render("user", userDetail);
ctx.set("Cache-Control", "public, max-age=15");
};
router.get("/users/:user", async (ctx)=>{
const user = await Users.findOneBy({
id: ctx.params.user,
host: IsNull(),
isSuspended: false
});
if (user == null) {
ctx.status = 404;
return;
}
ctx.redirect(`/@${user.username}${user.host == null ? "" : `@${user.host}`}`);
});
// Note
router.get("/notes/:note", async (ctx, next)=>{
const note = await Notes.findOneBy({
id: ctx.params.note,
visibility: In([
"public",
"home"
])
});
try {
if (note) {
await Notes.increment({
id: note.id
}, "viewCount", 1);
note.viewCount += 1;
const _note = await Notes.pack(note);
const profile = await UserProfiles.findOneByOrFail({
userId: note.userId
});
const meta = await fetchMeta();
await ctx.render("note", {
...metaToPugArgs(meta),
note: _note,
profile,
avatarUrl: await Users.getAvatarUrl(await Users.findOneByOrFail({
id: note.userId
})),
// TODO: Let locale changeable by instance setting
summary: getNoteSummary(_note)
});
ctx.set("Cache-Control", "public, max-age=15");
ctx.set("Content-Security-Policy", "default-src 'self' 'unsafe-inline'; img-src *; media-src *; frame-ancestors *");
return;
}
} catch {}
await next();
});
router.get("/posts/:note", async (ctx, next)=>{
const note = await Notes.findOneBy({
id: ctx.params.note,
visibility: In([
"public",
"home"
])
});
if (note) {
await Notes.increment({
id: note.id
}, "viewCount", 1);
note.viewCount += 1;
const _note = await Notes.pack(note);
const profile = await UserProfiles.findOneByOrFail({
userId: note.userId
});
const meta = await fetchMeta();
await ctx.render("note", {
...metaToPugArgs(meta),
note: _note,
profile,
avatarUrl: await Users.getAvatarUrl(await Users.findOneByOrFail({
id: note.userId
})),
// TODO: Let locale changeable by instance setting
summary: getNoteSummary(_note)
});
ctx.set("Cache-Control", "public, max-age=15");
return;
}
await next();
});
// Page
router.get("/@:user/pages/:page", async (ctx, next)=>{
const { username, host } = Acct.parse(ctx.params.user);
const user = await Users.findOneBy({
usernameLower: username.toLowerCase(),
host: host ?? IsNull()
});
if (user == null) return;
const page = await Pages.findOneBy({
name: ctx.params.page,
userId: user.id
});
if (page) {
const _page = await Pages.pack(page);
const profile = await UserProfiles.findOneByOrFail({
userId: page.userId
});
const meta = await fetchMeta();
await ctx.render("page", {
...metaToPugArgs(meta),
page: _page,
profile,
avatarUrl: await Users.getAvatarUrl(await Users.findOneByOrFail({
id: page.userId
}))
});
if ([
"public"
].includes(page.visibility)) {
ctx.set("Cache-Control", "public, max-age=15");
} else {
ctx.set("Cache-Control", "private, max-age=0, must-revalidate");
}
return;
}
await next();
});
// Clip
// TODO: handling of private clips
router.get("/clips/:clip", async (ctx, next)=>{
const clip = await Clips.findOneBy({
id: ctx.params.clip
});
if (clip) {
const _clip = await Clips.pack(clip);
const profile = await UserProfiles.findOneByOrFail({
userId: clip.userId
});
const meta = await fetchMeta();
await ctx.render("clip", {
...metaToPugArgs(meta),
clip: _clip,
profile,
avatarUrl: await Users.getAvatarUrl(await Users.findOneByOrFail({
id: clip.userId
}))
});
ctx.set("Cache-Control", "public, max-age=15");
return;
}
await next();
});
// Gallery post
router.get("/gallery/:post", async (ctx, next)=>{
const post = await GalleryPosts.findOneBy({
id: ctx.params.post
});
if (post) {
const _post = await GalleryPosts.pack(post);
const profile = await UserProfiles.findOneByOrFail({
userId: post.userId
});
const meta = await fetchMeta();
await ctx.render("gallery-post", {
...metaToPugArgs(meta),
post: _post,
profile,
avatarUrl: await Users.getAvatarUrl(await Users.findOneByOrFail({
id: post.userId
}))
});
ctx.set("Cache-Control", "public, max-age=15");
return;
}
await next();
});
// Channel
router.get("/channels/:channel", async (ctx, next)=>{
const channel = await Channels.findOneBy({
id: ctx.params.channel
});
if (channel) {
const _channel = await Channels.pack(channel);
const meta = await fetchMeta();
await ctx.render("channel", {
...metaToPugArgs(meta),
channel: _channel
});
ctx.set("Cache-Control", "public, max-age=15");
return;
}
await next();
});
//#endregion
router.get("/bios", async (ctx)=>{
await ctx.render("bios", {
version: config.version
});
});
router.get("/cli", async (ctx)=>{
await ctx.render("cli", {
version: config.version
});
});
const override = (source, target, depth = 0)=>[
undefined,
...target.split("/").filter((x)=>x),
...source.split("/").filter((x)=>x).splice(depth)
].join("/");
router.get("/flush", async (ctx)=>{
await ctx.render("flush");
});
// If a non-WebSocket request comes in to streaming and base html is returned with cache, the path will be cached by Proxy, etc. and it will be wrong.
router.get("/streaming", async (ctx)=>{
ctx.status = 503;
ctx.set("Cache-Control", "private, max-age=0");
});
router.get("/api/v1/streaming", async (ctx)=>{
ctx.status = 503;
ctx.set("Cache-Control", "private, max-age=0");
});
// Render base html for all requests
router.get("/", async (ctx)=>{
const meta = await fetchMeta();
await ctx.render("base", {
...metaToPugArgs(meta)
});
ctx.set("Cache-Control", "public, max-age=3");
});
router.get("{*splat}", async (ctx)=>{
const meta = await fetchMeta();
await ctx.render("base", {
...metaToPugArgs(meta)
});
ctx.set("Cache-Control", "public, max-age=3");
});
// Register router
app.use(router.routes());
export default app;
@@ -0,0 +1,13 @@
import { fetchMeta } from "../../misc/fetch-meta.js";
import fs from "fs";
import path from "path";
const manifest = JSON.parse(fs.readFileSync(path.resolve('./src/server/web/manifest.json'), 'utf-8'));
export const manifestHandler = async (ctx)=>{
const res = manifest;
const instance = await fetchMeta(true);
res.short_name = instance.name || "FrozenFriendsYume";
res.name = instance.name || "FrozenFriendsYume";
if (instance.themeColor) res.theme_color = instance.themeColor;
ctx.set("Cache-Control", "max-age=300");
ctx.body = res;
};
@@ -0,0 +1 @@
html{background-color:var(--bg);color:var(--fg)}@media(prefers-color-scheme:dark){html{--bg: rgb(17, 17, 27);--fg: rgb(224, 222, 244)}}#splash{position:fixed;z-index:10000;top:0;left:0;width:100vw;height:100vh;cursor:wait;background-color:var(--bg);opacity:1;transition:opacity .2s ease}#splashIcon{position:absolute;inset:0;margin:auto;width:64px;height:64px;pointer-events:none}#splashSpinner{position:absolute;inset:0;margin:auto;display:inline-block;width:28px;height:28px;transform:translateY(110px);display:none;color:var(--accent)}#splashSpinner>.spinner{position:absolute;top:0;left:0;width:28px;height:28px;fill-rule:evenodd;clip-rule:evenodd;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:1.5}#splashSpinner>.spinner.bg{opacity:.275}#splashSpinner>.spinner.fg{animation:splashSpinner .5s linear infinite}@keyframes splashSpinner{0%{transform:rotate(0)}to{transform:rotate(360deg)}}@keyframes tada{0%{transform:scaleZ(1)}10%,20%{transform:scale3d(.9,.9,.9) rotate3d(0,0,1,-3deg)}30%,50%,70%,90%{transform:scale3d(1.1,1.1,1.1) rotate3d(0,0,1,3deg)}40%,60%,80%{transform:scale3d(1.1,1.1,1.1) rotate3d(0,0,1,-3deg)}to{transform:scaleZ(1)}}@media(prefers-reduced-motion){#splashSpinner{display:block}}#splashText{position:absolute;inset:0;margin:auto;display:inline-block;width:70%;height:0;text-align:center;padding-top:100px;font-family:sans-serif}
@@ -0,0 +1,54 @@
import summaly from "@iceshrimp/summaly";
import { fetchMeta } from "../../misc/fetch-meta.js";
import Logger from "../../services/logger.js";
import config from "../../config/index.js";
import { query } from "../../prelude/url.js";
import { getJson } from "../../misc/fetch.js";
const logger = new Logger("url-preview");
export const urlPreviewHandler = async (ctx)=>{
const url = ctx.query.url;
if (typeof url !== "string") {
ctx.status = 400;
return;
}
const lang = ctx.query.lang;
if (Array.isArray(lang)) {
ctx.status = 400;
return;
}
const meta = await fetchMeta();
logger.info(meta.summalyProxy ? `(Proxy) Getting preview of ${url}@${lang} ...` : `Getting preview of ${url}@${lang} ...`);
try {
const summary = meta.summalyProxy ? await getJson(`${meta.summalyProxy}?${query({
url: url,
lang: lang ?? "en-US"
})}`) : await summaly.default(url, {
followRedirects: false,
lang: lang ?? "en-US",
userAgent: config.userAgent
});
logger.succ(`Got preview of ${url}: ${summary.title}`);
if (summary.url && !(summary.url.startsWith("http://") || summary.url.startsWith("https://"))) {
throw new Error("unsupported schema included");
}
if (summary.player?.url && !(summary.player.url.startsWith("http://") || summary.player.url.startsWith("https://"))) {
throw new Error("unsupported schema included");
}
summary.icon = wrap(summary.icon);
summary.thumbnail = wrap(summary.thumbnail);
// Cache 7days
ctx.set("Cache-Control", "max-age=604800, immutable");
ctx.body = summary;
} catch (err) {
logger.warn(`Failed to get preview of ${url}: ${err}`);
ctx.status = 200;
ctx.set("Cache-Control", "max-age=86400, immutable");
ctx.body = "{}";
}
};
function wrap(url) {
return url != null ? `${config.url}/proxy/preview.webp?${query({
url,
preview: "1"
})}` : null;
}
@@ -0,0 +1,87 @@
block vars
block loadClientEntry
- const clientEntry = getClientEntry();
doctype html
//
-
Thank you for using FrozenFriendsYume!
If you are reading this message... how about joining the development?
https://iceshrimp.dev/iceshrimp/iceshrimp
html
- var timestamp = Date.now();
head
meta(charset='utf-8')
meta(name='application-name' content='FrozenFriendsYume')
meta(name='referrer' content='origin')
meta(name='darkreader-lock' content='')
meta(name='theme-color' content= themeColor || '#31748f')
meta(name='theme-color-orig' content= themeColor || '#31748f')
meta(property='twitter:card' content='summary')
meta(property='og:site_name' content= instanceName || 'FrozenFriendsYume')
meta(name='viewport' content='width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=1, user-scalable=no')
link(rel='icon' href= icon || `/favicon.ico?${ timestamp }`)
link(rel='apple-touch-icon' href= icon || `/apple-touch-icon.png?${ timestamp }`)
link(rel='manifest' href='/manifest.json')
link(rel='prefetch' href=config.images.info)
link(rel='prefetch' href=config.images.notFound)
link(rel='prefetch' href=config.images.error)
link(rel='stylesheet' href=`/static-assets/instance.css?${ timestamp }`)
link(rel='modulepreload' href=`/assets/${clientEntry.file}`)
if Array.isArray(clientEntry.css)
each href in clientEntry.css
link(rel='stylesheet' href=`/assets/${href}`)
title
block title
= title || 'FrozenFriendsYume'
block desc
meta(name='description' content=desc || 'An open source, decentralized social media platform that\'s free forever! 🚀')
block meta
if privateMode
meta(name='robots' content='noindex')
block og
meta(property='og:title' content=title || 'FrozenFriendsYume')
meta(property='og:description' content=desc || 'An open source, decentralized social media platform that\'s free forever! 🚀')
meta(property='og:image' content=img)
meta(property='og:image:alt' content=alt || 'Pfp')
style
include ../style.css
script.
var VERSION = "#{version}";
var CLIENT_ENTRY = "#{clientEntry.file}";
script
include ../boot.js
body
noscript: p
| Please turn on your JavaScript
div#splash
img#splashIcon(src= splashIcon || `/static-assets/splash.png?${ timestamp }`)
span#splashText
block randomMOTD
= randomMOTD
div#splashSpinner
<svg class="spinner bg" viewBox="0 0 152 152" xmlns="http://www.w3.org/2000/svg">
<g transform="matrix(1,0,0,1,12,12)">
<circle cx="64" cy="64" r="64" style="fill:none;stroke:currentColor;stroke-width:24px;"/>
</g>
</svg>
<svg class="spinner fg" viewBox="0 0 152 152" xmlns="http://www.w3.org/2000/svg">
<g transform="matrix(1,0,0,1,12,12)">
<path d="M128,64C128,28.654 99.346,0 64,0C99.346,0 128,28.654 128,64Z" style="fill:none;stroke:currentColor;stroke-width:24px;"/>
</g>
</svg>
block content
@@ -0,0 +1,21 @@
doctype html
html
head
meta(charset='utf-8')
meta(name='application-name' content='FrozenFriendsYume')
meta(name='viewport' content='width=device-width, initial-scale=1.0')
title FrozenFriendsYume Repair Tool
style
include ../bios.css
script
include ../bios.js
body
header
h1 FrozenFriendsYume Repair Tool v#{version}
main
div.tabs
button#ls Edit local storage
div#content
@@ -0,0 +1,20 @@
extends ./base
block vars
- const title = privateMode ? instanceName : channel.name;
- const url = `${config.url}/channels/${channel.id}`;
block title
= `${title} | ${instanceName}`
block desc
unless privateMode
meta(name='description' content=channel.description)
block og
unless privateMode
meta(property='og:type' content='article')
meta(property='og:title' content= title)
meta(property='og:description' content= channel.description)
meta(property='og:url' content= url)
meta(property='og:image' content= channel.bannerUrl)
@@ -0,0 +1,23 @@
doctype html
html
head
meta(charset='utf-8')
meta(name='application-name' content='FrozenFriendsYume')
meta(name='viewport' content='width=device-width, initial-scale=1.0')
title FrozenFriendsYume Cli
style
include ../cli.css
script
include ../cli.js
body
header
h1 FrozenFriendsYume Simple Client v#{version}
main
div#form
textarea#text
br
button#submit Post
div#tl
@@ -0,0 +1,33 @@
extends ./base
block vars
- const user = clip.user;
- const title = privateMode ? instanceName : clip.name;
- const url = `${config.url}/clips/${clip.id}`;
block title
= `${title} | ${instanceName}`
block desc
unless privateMode
meta(name='description' content= clip.description)
block og
unless privateMode
meta(property='og:type' content='article')
meta(property='og:title' content= title)
meta(property='og:description' content= clip.description)
meta(property='og:url' content= url)
meta(property='og:image' content= avatarUrl)
block meta
unless privateMode
if profile.noCrawle
meta(name='robots' content='noindex')
if profile.preventAiLearning
meta(name='robots' content='noai')
meta(name='robots' content='noimageai')
meta(name='misskey:user-username' content=user.username)
meta(name='misskey:user-id' content=user.id)
meta(name='misskey:clip-id' content=clip.id)
@@ -0,0 +1,71 @@
doctype html
html
head
meta(charset='utf-8')
meta(name='application-name' content='FrozenFriendsYume')
meta(name='viewport' content='width=device-width, initial-scale=1.0')
title Flush FrozenFriendsYume
style.
* {
font-family: BIZ UDGothic, Roboto, HelveticaNeue, Arial, sans-serif;
}
body,
html {
background-color: #191724;
color: #e0def4;
justify-content: center;
margin: auto;
padding: 10px;
text-align: center;
}
a {
color: rgb(156, 207, 216);
text-decoration: none;
}
body
#msg
script.
const msg = document.getElementById('msg');
const successText = `\nSuccess Flush! <a href="/">Back to FrozenFriendsYume</a>\n成功しました。<a href="/">FrozenFriendsYumeを開き直してください。</a>`;
message('Start flushing.');
(async function() {
try {
localStorage.clear();
message('localStorage cleared.');
const idbPromises = ['MisskeyClient', 'keyval-store'].map((name, i, arr) => new Promise((res, rej) => {
const delidb = indexedDB.deleteDatabase(name);
delidb.onsuccess = () => res(message(`indexedDB "${name}" cleared. (${i + 1}/${arr.length})`));
delidb.onerror = e => rej(e)
}));
await Promise.all(idbPromises);
if (navigator.serviceWorker.controller) {
navigator.serviceWorker.controller.postMessage('clear');
await navigator.serviceWorker.getRegistrations()
.then(registrations => {
return Promise.all(registrations.map(registration => registration.unregister()));
})
.catch(e => { throw new Error(e) });
}
message(successText);
} catch (e) {
message(`\n${e}\n\nFlush Failed. <a href="/flush">Please retry.</a>\n失敗しました。<a href="/flush">もう一度試してみてください。</a>`);
message(`\nIf you retry more than 3 times, clear the browser cache or contact to instance admin.\n3回以上試しても失敗する場合、ブラウザのキャッシュを消去し、それでもだめならインスタンス管理者に連絡してみてください。\n`)
console.error(e);
setTimeout(() => {
location = '/';
}, 10000)
}
})();
function message(text) {
msg.insertAdjacentHTML('beforeend', `<p>[${(new Date()).toString()}] ${text.replace(/\n/g,'<br>')}</p>`)
}
@@ -0,0 +1,35 @@
extends ./base
block vars
- const user = post.user;
- const title = privateMode ? instanceName : post.title;
- const url = `${config.url}/gallery/${post.id}`;
block title
= `${title} | ${instanceName}`
block desc
unless privateMode
meta(name='description' content= post.description)
block og
unless privateMode
meta(property='og:type' content='article')
meta(property='og:title' content= title)
meta(property='og:description' content= post.description)
meta(property='og:url' content= url)
meta(property='og:image' content= post.files[0].thumbnailUrl)
block meta
unless privateMode
if user.host || profile.noCrawle
meta(name='robots' content='noindex')
if profile.preventAiLearning
meta(name='robots' content='noai')
meta(name='robots' content='noimageai')
meta(name='misskey:user-username' content=user.username)
meta(name='misskey:user-id' content=user.id)
if !user.host
link(rel='alternate' href=url type='application/activity+json')
@@ -0,0 +1,55 @@
extends ./base
block vars
- const user = note.user;
- const title = privateMode ? instanceName : (user.name ? `${user.name} (@${user.username}${user.host ? `@${user.host}` : ''})` : `@${user.username}`);
- const url = `${config.url}/notes/${note.id}`;
- const isRenote = note.renote && note.text == null && note.fileIds.length == 0 && note.poll == null;
- const isImage = note.files.length !== 0 && note.files[0].type.startsWith('image');
- const isVideo = note.files.length !== 0 && note.files[0].type.startsWith('video');
- const imageUrl = isImage ? note.files[0].url : isVideo ? note.files[0].thumbnailUrl : avatarUrl;
block title
= `${title} | ${instanceName}`
block desc
unless privateMode
meta(name='description' content= summary)
block og
unless privateMode
meta(property='og:type' content='article')
meta(property='og:title' content= title)
meta(property='og:description' content= summary)
meta(property='og:url' content= url)
meta(property='og:image' content= imageUrl)
if isImage && !note.files[0].isSensitive
meta(property='og:image:width' content=note.files[0].properties.width)
meta(property='og:image:height' content=note.files[0].properties.height)
meta(property='og:image:type' content=note.files[0].type)
meta(property='twitter:card' content="summary_large_image")
if isVideo
meta(property='og:video:type' content=note.files[0].type)
meta(property='og:video' content=note.files[0].url)
block meta
unless privateMode
if user.host || isRenote || profile.noCrawle
meta(name='robots' content='noindex')
if profile.preventAiLearning
meta(name='robots' content='noai')
meta(name='robots' content='noimageai')
meta(name='misskey:user-username' content=user.username)
meta(name='misskey:user-id' content=user.id)
meta(name='misskey:note-id' content=note.id)
if note.prev
link(rel='prev' href=`${config.url}/notes/${note.prev}`)
if note.next
link(rel='next' href=`${config.url}/notes/${note.next}`)
if !user.host
link(rel='alternate' href=url type='application/activity+json')
if note.uri
link(rel='alternate' href=note.uri type='application/activity+json')
@@ -0,0 +1,33 @@
extends ./base
block vars
- const user = page.user;
- const title = privateMode ? instanceName : page.title;
- const url = `${config.url}/@${user.username}/${page.name}`;
block title
= `${title} | ${instanceName}`
block desc
unless privateMode
meta(name='description' content= page.summary)
block og
unless privateMode
meta(property='og:type' content='article')
meta(property='og:title' content= title)
meta(property='og:description' content= page.summary)
meta(property='og:url' content= url)
meta(property='og:image' content= page.eyeCatchingImage ? page.eyeCatchingImage.thumbnailUrl : avatarUrl)
block meta
unless privateMode
if profile.noCrawle
meta(name='robots' content='noindex')
if profile.preventAiLearning
meta(name='robots' content='noai')
meta(name='robots' content='noimageai')
meta(name='misskey:user-username' content=user.username)
meta(name='misskey:user-id' content=user.id)
meta(name='misskey:page-id' content=page.id)
@@ -0,0 +1,43 @@
extends ./base
block vars
- const title = privateMode ? instanceName : (user.name ? `${user.name} (@${user.username})` : `@${user.username}`);
- const url = `${config.url}/@${(user.host ? `${user.username}@${user.host}` : user.username)}`;
block title
= `${title} | ${instanceName}`
block desc
unless privateMode
meta(name='description' content= profile.description)
block og
unless privateMode
meta(property='og:type' content='profile')
meta(property='og:title' content= title)
meta(property='og:description' content= profile.description)
meta(property='og:url' content= url)
meta(property='og:image' content= avatarUrl)
meta(property='profile:username' content= user.username)
block meta
unless privateMode
if user.host || profile.noCrawle
meta(name='robots' content='noindex')
if profile.preventAiLearning
meta(name='robots' content='noai')
meta(name='robots' content='noimageai')
meta(name='misskey:user-username' content=user.username)
meta(name='misskey:user-id' content=user.id)
if !sub
if !user.host
link(rel='alternate' href=`${config.url}/users/${user.id}` type='application/activity+json')
if user.uri
link(rel='alternate' href=user.uri type='application/activity+json')
if profile.url
link(rel='alternate' href=profile.url type='text/html')
each m in me
link(rel='me' href=`${m}`)