summaryrefslogtreecommitdiff
path: root/src/server/web
diff options
context:
space:
mode:
Diffstat (limited to 'src/server/web')
-rw-r--r--src/server/web/docs.ts24
-rw-r--r--src/server/web/feed.ts54
-rw-r--r--src/server/web/index.ts148
-rw-r--r--src/server/web/url-preview.ts11
-rw-r--r--src/server/web/views/base.pug44
-rw-r--r--src/server/web/views/info.pug135
-rw-r--r--src/server/web/views/note.pug11
-rw-r--r--src/server/web/views/user.pug13
8 files changed, 400 insertions, 40 deletions
diff --git a/src/server/web/docs.ts b/src/server/web/docs.ts
index d91813c869..94c18d9996 100644
--- a/src/server/web/docs.ts
+++ b/src/server/web/docs.ts
@@ -30,13 +30,13 @@ async function genVars(lang: string): Promise<{ [key: string]: any }> {
const entities = glob.sync('src/docs/api/entities/**/*.yaml', { cwd });
vars['entities'] = entities.map(x => {
- const _x = yaml.safeLoad(fs.readFileSync(cwd + x, 'utf-8')) as any;
+ const _x = yaml.safeLoad(fs.readFileSync(cwd + x, 'utf-8'));
return _x.name;
});
const docs = glob.sync(`src/docs/**/*.${lang}.md`, { cwd });
vars['docs'] = {};
- docs.forEach(x => {
+ for (const x of docs) {
const [, name] = x.match(/docs\/(.+?)\.(.+?)\.md$/);
if (vars['docs'][name] == null) {
vars['docs'][name] = {
@@ -45,7 +45,7 @@ async function genVars(lang: string): Promise<{ [key: string]: any }> {
};
}
vars['docs'][name]['title'][lang] = fs.readFileSync(cwd + x, 'utf-8').match(/^# (.+?)\r?\n/)[1];
- });
+ }
vars['kebab'] = (string: string) => string.replace(/([a-z])([A-Z])/g, '$1-$2').replace(/\s+/g, '-').toLowerCase();
@@ -121,7 +121,7 @@ const sortParams = (params: Array<{ name: string }>) => {
const extractParamDefRef = (params: Context[]) => {
let defs: any[] = [];
- params.forEach(param => {
+ for (const param of params) {
if (param.data && param.data.ref) {
const props = (param as ObjectContext<any>).props;
defs.push({
@@ -133,7 +133,7 @@ const extractParamDefRef = (params: Context[]) => {
defs = defs.concat(childDefs);
}
- });
+ }
return sortParams(defs);
};
@@ -141,7 +141,7 @@ const extractParamDefRef = (params: Context[]) => {
const extractPropDefRef = (props: any[]) => {
let defs: any[] = [];
- Object.entries(props).forEach(([k, v]) => {
+ for (const [k, v] of Object.entries(props)) {
if (v.props) {
defs.push({
name: k,
@@ -152,7 +152,7 @@ const extractPropDefRef = (props: any[]) => {
defs = defs.concat(childDefs);
}
- });
+ }
return sortParams(defs);
};
@@ -160,7 +160,7 @@ const extractPropDefRef = (props: any[]) => {
const router = new Router();
router.get('/assets/*', async ctx => {
- await send(ctx, ctx.params[0], {
+ await send(ctx as any, ctx.params[0], {
root: `${__dirname}/../../docs/assets/`,
maxage: ms('1 days')
});
@@ -189,13 +189,15 @@ router.get('/*/api/endpoints/*', async ctx => {
};
await ctx.render('../../../../src/docs/api/endpoints/view', Object.assign(await genVars(lang), vars));
+
+ ctx.set('Cache-Control', 'public, max-age=300');
});
router.get('/*/api/entities/*', async ctx => {
const lang = ctx.params[0];
const entity = ctx.params[1];
- const x = yaml.safeLoad(fs.readFileSync(path.resolve(`${__dirname}/../../../src/docs/api/entities/${entity}.yaml`), 'utf-8')) as any;
+ const x = yaml.safeLoad(fs.readFileSync(path.resolve(`${__dirname}/../../../src/docs/api/entities/${entity}.yaml`), 'utf-8'));
await ctx.render('../../../../src/docs/api/entities/view', Object.assign(await genVars(lang), {
id: `api/entities/${entity}`,
@@ -204,6 +206,8 @@ router.get('/*/api/entities/*', async ctx => {
props: sortParams(Object.entries(x.props).map(([k, v]) => parsePropDefinition(k, v))),
propDefs: extractPropDefRef(x.props)
}));
+
+ ctx.set('Cache-Control', 'public, max-age=300');
});
router.get('/*/*', async ctx => {
@@ -240,6 +244,8 @@ router.get('/*/*', async ctx => {
title: md.match(/^# (.+?)\r?\n/)[1],
src: `https://github.com/syuilo/misskey/tree/master/src/docs/${doc}.${lang}.md`
}, await genVars(lang)));
+
+ ctx.set('Cache-Control', 'public, max-age=300');
});
export default router;
diff --git a/src/server/web/feed.ts b/src/server/web/feed.ts
new file mode 100644
index 0000000000..09ac10c576
--- /dev/null
+++ b/src/server/web/feed.ts
@@ -0,0 +1,54 @@
+import { Feed } from 'feed';
+import config from '../../config';
+import Note from '../../models/note';
+import { IUser } from '../../models/user';
+import { getOriginalUrl } from '../../misc/get-drive-file-url';
+
+export default async function(user: IUser) {
+ const author: Author = {
+ link: `${config.url}/@${user.username}`,
+ name: user.name || user.username
+ };
+
+ const notes = await Note.find({
+ userId: user._id,
+ renoteId: null,
+ $or: [
+ { visibility: 'public' },
+ { visibility: 'home' }
+ ]
+ }, {
+ sort: { createdAt: -1 },
+ limit: 20
+ });
+
+ const feed = new Feed({
+ id: author.link,
+ title: `${author.name} (@${user.username}@${config.host})`,
+ updated: notes[0].createdAt,
+ generator: 'Misskey',
+ description: `${user.notesCount} Notes, ${user.followingCount} Following, ${user.followersCount} Followers${user.description ? ` · ${user.description}` : ''}`,
+ link: author.link,
+ image: user.avatarUrl,
+ feedLinks: {
+ json: `${author.link}.json`,
+ atom: `${author.link}.atom`,
+ },
+ author
+ } as FeedOptions);
+
+ for (const note of notes) {
+ const file = note._files && note._files.find(file => file.contentType.startsWith('image/'));
+
+ feed.addItem({
+ title: `New note by ${author.name}`,
+ link: `${config.url}/notes/${note._id}`,
+ date: note.createdAt,
+ description: note.cw,
+ content: note.text,
+ image: file && getOriginalUrl(file)
+ });
+ }
+
+ return feed;
+}
diff --git a/src/server/web/index.ts b/src/server/web/index.ts
index 42203471a7..69f3b8859f 100644
--- a/src/server/web/index.ts
+++ b/src/server/web/index.ts
@@ -2,20 +2,25 @@
* Web Client Server
*/
+import * as os from 'os';
import ms = require('ms');
import * as Koa from 'koa';
import * as Router from 'koa-router';
import * as send from 'koa-send';
import * as favicon from 'koa-favicon';
import * as views from 'koa-views';
+import { ObjectID } from 'mongodb';
import docs from './docs';
+import packFeed from './feed';
import User from '../../models/user';
import parseAcct from '../../misc/acct/parse';
import config from '../../config';
import Note, { pack as packNote } from '../../models/note';
import getNoteSummary from '../../misc/get-note-summary';
-const consts = require('../../const.json');
+import fetchMeta from '../../misc/fetch-meta';
+import Emoji from '../../models/emoji';
+const pkg = require('../../../package.json');
const client = `${__dirname}/../../client/`;
@@ -26,8 +31,7 @@ const app = new Koa();
app.use(views(__dirname + '/views', {
extension: 'pug',
options: {
- config,
- themeColor: consts.themeColor
+ config
}
}));
@@ -47,7 +51,7 @@ const router = new Router();
//#region static assets
router.get('/assets/*', async ctx => {
- await send(ctx, ctx.path, {
+ await send(ctx as any, ctx.path, {
root: client,
maxage: ms('7 days'),
immutable: true
@@ -56,21 +60,21 @@ router.get('/assets/*', async ctx => {
// Apple touch icon
router.get('/apple-touch-icon.png', async ctx => {
- await send(ctx, '/assets/apple-touch-icon.png', {
+ await send(ctx as any, '/assets/apple-touch-icon.png', {
root: client
});
});
// ServiceWorker
router.get(/^\/sw\.(.+?)\.js$/, async ctx => {
- await send(ctx, `/assets/sw.${ctx.params[0]}.js`, {
+ await send(ctx as any, `/assets/sw.${ctx.params[0]}.js`, {
root: client
});
});
// Manifest
router.get('/manifest.json', async ctx => {
- await send(ctx, '/assets/manifest.json', {
+ await send(ctx as any, '/assets/manifest.json', {
root: client
});
});
@@ -83,6 +87,52 @@ router.use('/docs', docs.routes());
// URL preview endpoint
router.get('/url', require('./url-preview'));
+const getFeed = async (acct: string) => {
+ const { username, host } = parseAcct(acct);
+ const user = await User.findOne({
+ usernameLower: username.toLowerCase(),
+ host
+ });
+
+ return user && await packFeed(user);
+};
+
+// Atom
+router.get('/@:user.atom', async ctx => {
+ const feed = await getFeed(ctx.params.user);
+
+ if (feed) {
+ ctx.set('Content-Type', 'application/atom+xml; charset=utf-8');
+ ctx.body = feed.atom1();
+ } else {
+ ctx.status = 404;
+ }
+});
+
+// RSS
+router.get('/@:user.rss', async ctx => {
+ const feed = await getFeed(ctx.params.user);
+
+ if (feed) {
+ ctx.set('Content-Type', 'application/rss+xml; charset=utf-8');
+ ctx.body = feed.rss2();
+ } else {
+ ctx.status = 404;
+ }
+});
+
+// JSON
+router.get('/@:user.json', async ctx => {
+ const feed = await getFeed(ctx.params.user);
+
+ if (feed) {
+ ctx.set('Content-Type', 'application/json; charset=utf-8');
+ ctx.body = feed.json1();
+ } else {
+ ctx.status = 404;
+ }
+});
+
//#region for crawlers
// User
router.get('/@:user', async (ctx, next) => {
@@ -94,34 +144,94 @@ router.get('/@:user', async (ctx, next) => {
if (user != null) {
await ctx.render('user', { user });
+ ctx.set('Cache-Control', 'public, max-age=180');
} else {
// リモートユーザーなので
await next();
}
});
+router.get('/users/:user', async ctx => {
+ if (!ObjectID.isValid(ctx.params.user)) {
+ ctx.status = 404;
+ return;
+ }
+
+ const userId = new ObjectID(ctx.params.user);
+
+ const user = await User.findOne({
+ _id: userId,
+ host: null
+ });
+
+ if (user === null) {
+ ctx.status = 404;
+ return;
+ }
+
+ ctx.redirect(`/@${user.username}${ user.host == null ? '' : '@' + user.host}`);
+});
+
// Note
router.get('/notes/:note', async ctx => {
- const note = await Note.findOne({ _id: ctx.params.note });
+ if (ObjectID.isValid(ctx.params.note)) {
+ const note = await Note.findOne({ _id: ctx.params.note });
- if (note != null) {
- const _note = await packNote(note);
- await ctx.render('note', {
- note: _note,
- summary: getNoteSummary(_note)
- });
- } else {
- ctx.status = 404;
+ if (note) {
+ const _note = await packNote(note);
+ await ctx.render('note', {
+ note: _note,
+ summary: getNoteSummary(_note)
+ });
+
+ if (['public', 'home'].includes(note.visibility)) {
+ ctx.set('Cache-Control', 'public, max-age=180');
+ } else {
+ ctx.set('Cache-Control', 'private, max-age=0, must-revalidate');
+ }
+
+ return;
+ }
}
+
+ ctx.status = 404;
});
//#endregion
+router.get('/info', async ctx => {
+ const meta = await fetchMeta();
+ const emojis = await Emoji.find({ host: null }, {
+ fields: {
+ _id: false
+ }
+ });
+ await ctx.render('info', {
+ version: pkg.version,
+ machine: os.hostname(),
+ os: os.platform(),
+ node: process.version,
+ cpu: {
+ model: os.cpus()[0].model,
+ cores: os.cpus().length
+ },
+ emojis: emojis,
+ meta: meta
+ });
+});
+
+const override = (source: string, target: string, depth: number = 0) =>
+ [, ...target.split('/').filter(x => x), ...source.split('/').filter(x => x).splice(depth)].join('/');
+
+router.get('/othello', async ctx => ctx.redirect(override(ctx.URL.pathname, 'games/reversi', 1)));
+router.get('/reversi', async ctx => ctx.redirect(override(ctx.URL.pathname, 'games')));
+
// Render base html for all requests
router.get('*', async ctx => {
- await send(ctx, `app/base.html`, {
- root: client,
- maxage: ms('5m')
+ const meta = await fetchMeta();
+ await ctx.render('base', {
+ img: meta.bannerUrl
});
+ ctx.set('Cache-Control', 'public, max-age=86400');
});
// Register router
diff --git a/src/server/web/url-preview.ts b/src/server/web/url-preview.ts
index eb835b05ac..4cda5ecb01 100644
--- a/src/server/web/url-preview.ts
+++ b/src/server/web/url-preview.ts
@@ -1,13 +1,14 @@
import * as Koa from 'koa';
import * as request from 'request-promise-native';
import summaly from 'summaly';
-import config from '../../config';
+import fetchMeta from '../../misc/fetch-meta';
+
+module.exports = async (ctx: Koa.BaseContext) => {
+ const meta = await fetchMeta();
-module.exports = async (ctx: Koa.Context) => {
try {
- const summary = config.summalyProxy ? await request.get({
- url: config.summalyProxy,
- proxy: config.proxy,
+ const summary = meta.summalyProxy ? await request.get({
+ url: meta.summalyProxy,
qs: {
url: ctx.query.url
},
diff --git a/src/server/web/views/base.pug b/src/server/web/views/base.pug
new file mode 100644
index 0000000000..dd9660b73f
--- /dev/null
+++ b/src/server/web/views/base.pug
@@ -0,0 +1,44 @@
+block vars
+
+doctype html
+
+!= '\n<!-- Thank you for using Misskey! @syuilo -->\n'
+
+html
+
+ head
+ meta(charset='utf-8')
+ meta(name='application-name' content='Misskey')
+ meta(name='referrer' content='origin')
+ meta(property='og:site_name' content='Misskey')
+ link(rel='manifest' href='/manifest.json')
+
+ title
+ block title
+ | Misskey
+
+ block desc
+ meta(name='description' content='A planet of fediverse')
+
+ block meta
+
+ block og
+ meta(property='og:image' content=img)
+
+ style
+ include ./../../../../built/client/assets/init.css
+ script
+ include ./../../../../built/client/assets/boot.js
+
+ script
+ include ./../../../../built/client/assets/safe.js
+
+ body
+ noscript: p
+ | JavaScriptを有効にしてください
+ br
+ | Please turn on your JavaScript
+ div#ini.
+ <svg viewBox="0 0 50 50">
+ <path fill=#fb4e4e d="M25.251,6.461c-10.318,0-18.683,8.365-18.683,18.683h4.068c0-8.071,6.543-14.615,14.615-14.615V6.461z" />
+ </svg>
diff --git a/src/server/web/views/info.pug b/src/server/web/views/info.pug
new file mode 100644
index 0000000000..1c4b272a62
--- /dev/null
+++ b/src/server/web/views/info.pug
@@ -0,0 +1,135 @@
+doctype html
+
+html
+
+ head
+ meta(charset='utf-8')
+ meta(name='application-name' content='Misskey')
+ title Misskey
+ style.
+ html {
+ font-family: sans-serif;
+ }
+
+ main {
+ max-width: 934px;
+ margin: 0 auto;
+ }
+
+ header {
+ padding: 5px;
+ background: rgb(153, 153, 204);
+ border: 1px solid #000;
+ box-shadow: rgb(204, 204, 204) 1px 2px 3px;
+ }
+ header:after {
+ content: '';
+ display: block;
+ clear: both;
+ }
+
+ header > h1 {
+ float: left;
+ font-size: 2em;
+ }
+
+ header > img {
+ float: right;
+ width: 220px;
+ }
+
+ table {
+ margin: 1em 0;
+ width: 100%;
+ border-collapse: collapse;
+ box-shadow: rgb(204, 204, 204) 1px 2px 3px;
+ }
+ table tr th {
+ background-color: #ccf;
+ border: 1px solid #000;
+ width: 300px;
+ font-weight: bold;
+ padding: 4px 5px;
+ text-align: left;
+ }
+ table tr td {
+ background-color: #ddd;
+ border: 1px solid #000;
+ padding: 4px 5px;
+ }
+
+ footer {
+ text-align: center;
+ }
+
+ body
+ main
+ header
+ h1 Misskey Version #{version}
+ img(src='/assets/misskey-php-like-logo.png' alt='')
+ table
+ tr
+ th Instance
+ td= meta.name
+ tr
+ th Description
+ td= meta.description
+ tr
+ th Maintainer
+ td
+ = meta.maintainer.name
+ | &lt;#{meta.maintainer.email}&gt;
+ tr
+ th System
+ td= os
+ tr
+ th Node version
+ td= node
+ tr
+ th Machine
+ td= machine
+ tr
+ th CPU
+ td= cpu.model
+ tr
+ th Original users
+ td= meta.stats.originalUsersCount
+ tr
+ th Original notes
+ td= meta.stats.originalNotesCount
+ tr
+ th Registration
+ td= !meta.disableRegistration ? 'yes' : 'no'
+ tr
+ th reCAPTCHA enabled
+ td= meta.enableRecaptcha ? 'enabled' : 'disabled'
+ tr
+ th LTL(STL) enabled
+ td= !meta.disableLocalTimeline ? 'enabled' : 'disabled'
+ tr
+ th GTL enabled
+ td= !meta.disableGlobalTimeline ? 'enabled' : 'disabled'
+ tr
+ th Cache remote files
+ td= meta.cacheRemoteFiles ? 'yes' : 'no'
+ tr
+ th Drive capacity per local user
+ td
+ = meta.localDriveCapacityMb
+ | MB
+ tr
+ th Drive capacity per remote user
+ td
+ = meta.remoteDriveCapacityMb
+ | MB
+ tr
+ th Max text length
+ td= meta.maxNoteTextLength
+ tr
+ th Emojis
+ td
+ each emoji in emojis
+ | :#{emoji.name}:
+ = ' '
+ footer
+ p Misskey is open-source software. <a href="https://github.com/syuilo/misskey">View source</a>
diff --git a/src/server/web/views/note.pug b/src/server/web/views/note.pug
index 234ecabe22..2d07aff2ed 100644
--- a/src/server/web/views/note.pug
+++ b/src/server/web/views/note.pug
@@ -1,4 +1,4 @@
-extends ../../../../src/client/app/base
+extends ./base
block vars
- const user = note.user;
@@ -11,14 +11,19 @@ block title
block desc
meta(name='description' content= summary)
-block meta
- meta(name='twitter:card' content='summary')
+block og
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= user.avatarUrl)
+block meta
+ meta(name='twitter:card' content='summary')
+
+ if user.twitter
+ meta(name='twitter:creator' content=`@${user.twitter.screenName}`)
+
if note.prev
link(rel='prev' href=`${config.url}/notes/${note.prev}`)
if note.next
diff --git a/src/server/web/views/user.pug b/src/server/web/views/user.pug
index 22c76c143b..7810a8b9b2 100644
--- a/src/server/web/views/user.pug
+++ b/src/server/web/views/user.pug
@@ -1,4 +1,4 @@
-extends ../../../../src/client/app/base
+extends ./base
block vars
- const title = user.name ? `${user.name} (@${user.username})` : `@${user.username}`;
@@ -11,14 +11,19 @@ block title
block desc
meta(name='description' content= user.description)
-block meta
- meta(name='twitter:card' content='summary')
+block og
meta(property='og:type' content='blog')
meta(property='og:title' content= title)
meta(property='og:description' content= user.description)
meta(property='og:url' content= url)
meta(property='og:image' content= img)
-
+
+block meta
+ meta(name='twitter:card' content='summary')
+
+ if user.twitter
+ meta(name='twitter:creator' content=`@${user.twitter.screenName}`)
+
if !user.host
link(rel='alternate' href=`${config.url}/users/${user._id}` type='application/activity+json')
if user.uri