diff options
Diffstat (limited to 'src/server/web')
| -rw-r--r-- | src/server/web/docs.ts | 24 | ||||
| -rw-r--r-- | src/server/web/feed.ts | 54 | ||||
| -rw-r--r-- | src/server/web/index.ts | 148 | ||||
| -rw-r--r-- | src/server/web/url-preview.ts | 11 | ||||
| -rw-r--r-- | src/server/web/views/base.pug | 44 | ||||
| -rw-r--r-- | src/server/web/views/info.pug | 135 | ||||
| -rw-r--r-- | src/server/web/views/note.pug | 11 | ||||
| -rw-r--r-- | src/server/web/views/user.pug | 13 |
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 + | <#{meta.maintainer.email}> + 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 |