Fixed 267U.pre2
This commit is contained in:
@@ -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)})};
|
||||
@@ -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">↻ 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">🗑 Clear preferences and cache</span>
|
||||
</button>
|
||||
</a>
|
||||
<br>
|
||||
<a href="/cli">
|
||||
<button class="button-small">
|
||||
<span class="button-label-small">🗫 Start the simple client</span>
|
||||
</button>
|
||||
</a>
|
||||
<br>
|
||||
<a href="/bios">
|
||||
<button class="button-small">
|
||||
<span class="button-label-small">⚙ 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%}}
|
||||
@@ -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)}})};
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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}`)
|
||||
Reference in New Issue
Block a user