From e581ead1ed58bf131b413435fef70eea5bb2f01c Mon Sep 17 00:00:00 2001 From: syuilo Date: Thu, 25 Apr 2019 12:22:03 +0900 Subject: [PATCH 01/15] Update CHANGELOG.md --- CHANGELOG.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f50bcef57..df090780a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,13 @@ If you encounter any problems with updating, please try the following: How to migrate to v11 from v10 ------------------------------ +### 移行の注意点 +**以下のデータは引き継がれません** +* 通知 +* リモートの投稿 +* リバーシの対局 + +### 手順 1. v11をインストールしたい場所に syuilo/misskey をクローン 2. config を設定する * PostgreSQL(`db`)の設定とは別に、v10からMongoDBの設定をコピペしてくる(例は下にあります) From 6fdff1348011d4ea70ead42866e96ab508ad6a54 Mon Sep 17 00:00:00 2001 From: syuilo Date: Thu, 25 Apr 2019 12:24:18 +0900 Subject: [PATCH 02/15] Update example.yml --- .config/example.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.config/example.yml b/.config/example.yml index 48b1a0fd1..2591066da 100644 --- a/.config/example.yml +++ b/.config/example.yml @@ -132,6 +132,9 @@ drive: # ulid ... Millisecond accuracy # objectid ... This is left for backward compatibility +# ONCE YOU HAVE STARTED THE INSTANCE, DO NOT CHANGE THE +# ID SETTINGS AFTER THAT! + id: 'aid' # ┌─────────────────────┐ From 867eb41618e75dc182f59562889cb3886cbc40f2 Mon Sep 17 00:00:00 2001 From: syuilo Date: Thu, 25 Apr 2019 12:37:13 +0900 Subject: [PATCH 03/15] Fix #4415 --- src/client/app/common/views/components/messaging.vue | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/client/app/common/views/components/messaging.vue b/src/client/app/common/views/components/messaging.vue index ad3b639a2..957fd389d 100644 --- a/src/client/app/common/views/components/messaging.vue +++ b/src/client/app/common/views/components/messaging.vue @@ -13,8 +13,8 @@ @click="navigate(user)" tabindex="-1" > - - + + @{{ user | acct }} From da9dd7c42391af94d41e7f11aa59f59abfa4029a Mon Sep 17 00:00:00 2001 From: syuilo Date: Thu, 25 Apr 2019 12:56:52 +0900 Subject: [PATCH 04/15] Improve API definition --- src/server/api/endpoints/hashtags/trend.ts | 28 ++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/src/server/api/endpoints/hashtags/trend.ts b/src/server/api/endpoints/hashtags/trend.ts index 84b750f2c..05d571851 100644 --- a/src/server/api/endpoints/hashtags/trend.ts +++ b/src/server/api/endpoints/hashtags/trend.ts @@ -2,6 +2,7 @@ import define from '../../define'; import { fetchMeta } from '../../../../misc/fetch-meta'; import { Notes } from '../../../../models'; import { Note } from '../../../../models/entities/note'; +import { types, bool } from '../../../../misc/schema'; /* トレンドに載るためには「『直近a分間のユニーク投稿数が今からa分前~今からb分前の間のユニーク投稿数のn倍以上』のハッシュタグの上位5位以内に入る」ことが必要 @@ -21,6 +22,33 @@ export const meta = { tags: ['hashtags'], requireCredential: false, + + res: { + type: types.array, + optional: bool.false, nullable: bool.false, + items: { + type: types.object, + optional: bool.false, nullable: bool.false, + properties: { + tag: { + type: types.string, + optional: bool.false, nullable: bool.false, + }, + chart: { + type: types.array, + optional: bool.false, nullable: bool.false, + items: { + type: types.number, + optional: bool.false, nullable: bool.false, + } + }, + usersCount: { + type: types.number, + optional: bool.false, nullable: bool.false, + } + } + } + } }; export default define(meta, async () => { From 6721d27e3f085a2335485eed0eb90409240539c1 Mon Sep 17 00:00:00 2001 From: syuilo Date: Thu, 25 Apr 2019 13:25:10 +0900 Subject: [PATCH 05/15] Improve hashtag API --- src/models/index.ts | 4 +- src/models/repositories/hashtag.ts | 71 +++++++++++++++++++++++ src/server/api/endpoints/hashtags/list.ts | 2 +- src/server/api/endpoints/hashtags/show.ts | 48 +++++++++++++++ src/server/api/openapi/schemas.ts | 46 +-------------- 5 files changed, 124 insertions(+), 47 deletions(-) create mode 100644 src/models/repositories/hashtag.ts create mode 100644 src/server/api/endpoints/hashtags/show.ts diff --git a/src/models/index.ts b/src/models/index.ts index d66e4e710..826044e7a 100644 --- a/src/models/index.ts +++ b/src/models/index.ts @@ -7,7 +7,6 @@ import { Meta } from './entities/meta'; import { SwSubscription } from './entities/sw-subscription'; import { NoteWatching } from './entities/note-watching'; import { UserListJoining } from './entities/user-list-joining'; -import { Hashtag } from './entities/hashtag'; import { NoteUnread } from './entities/note-unread'; import { RegistrationTicket } from './entities/registration-tickets'; import { UserRepository } from './repositories/user'; @@ -35,6 +34,7 @@ import { FollowingRepository } from './repositories/following'; import { AbuseUserReportRepository } from './repositories/abuse-user-report'; import { AuthSessionRepository } from './repositories/auth-session'; import { UserProfile } from './entities/user-profile'; +import { HashtagRepository } from './repositories/hashtag'; export const Apps = getCustomRepository(AppRepository); export const Notes = getCustomRepository(NoteRepository); @@ -62,7 +62,7 @@ export const Metas = getRepository(Meta); export const Mutings = getCustomRepository(MutingRepository); export const Blockings = getCustomRepository(BlockingRepository); export const SwSubscriptions = getRepository(SwSubscription); -export const Hashtags = getRepository(Hashtag); +export const Hashtags = getCustomRepository(HashtagRepository); export const AbuseUserReports = getCustomRepository(AbuseUserReportRepository); export const RegistrationTickets = getRepository(RegistrationTicket); export const AuthSessions = getCustomRepository(AuthSessionRepository); diff --git a/src/models/repositories/hashtag.ts b/src/models/repositories/hashtag.ts new file mode 100644 index 000000000..22321fca8 --- /dev/null +++ b/src/models/repositories/hashtag.ts @@ -0,0 +1,71 @@ +import { EntityRepository, Repository } from 'typeorm'; +import { Hashtag } from '../entities/hashtag'; +import { SchemaType, types, bool } from '../../misc/schema'; + +export type PackedHashtag = SchemaType; + +@EntityRepository(Hashtag) +export class HashtagRepository extends Repository { + public packMany( + hashtags: Hashtag[], + ) { + return Promise.all(hashtags.map(x => this.pack(x))); + } + + public async pack( + src: Hashtag, + ): Promise { + return { + tag: src.name, + mentionedUsersCount: src.mentionedUsersCount, + mentionedLocalUsersCount: src.mentionedLocalUsersCount, + mentionedRemoteUsersCount: src.mentionedRemoteUsersCount, + attachedUsersCount: src.attachedUsersCount, + attachedLocalUsersCount: src.attachedLocalUsersCount, + attachedRemoteUsersCount: src.attachedRemoteUsersCount, + }; + } +} + +export const packedHashtagSchema = { + type: types.object, + optional: bool.false, nullable: bool.false, + properties: { + tag: { + type: types.string, + optional: bool.false, nullable: bool.false, + description: 'The hashtag name. No # prefixed.', + example: 'misskey', + }, + mentionedUsersCount: { + type: types.number, + optional: bool.false, nullable: bool.false, + description: 'Number of all users using this hashtag.' + }, + mentionedLocalUsersCount: { + type: types.number, + optional: bool.false, nullable: bool.false, + description: 'Number of local users using this hashtag.' + }, + mentionedRemoteUsersCount: { + type: types.number, + optional: bool.false, nullable: bool.false, + description: 'Number of remote users using this hashtag.' + }, + attachedUsersCount: { + type: types.number, + optional: bool.false, nullable: bool.false, + description: 'Number of all users who attached this hashtag to profile.' + }, + attachedLocalUsersCount: { + type: types.number, + optional: bool.false, nullable: bool.false, + description: 'Number of local users who attached this hashtag to profile.' + }, + attachedRemoteUsersCount: { + type: types.number, + optional: bool.false, nullable: bool.false, + description: 'Number of remote users who attached this hashtag to profile.' + }, + } +}; diff --git a/src/server/api/endpoints/hashtags/list.ts b/src/server/api/endpoints/hashtags/list.ts index 89cc92642..9023f1191 100644 --- a/src/server/api/endpoints/hashtags/list.ts +++ b/src/server/api/endpoints/hashtags/list.ts @@ -92,5 +92,5 @@ export default define(meta, async (ps, me) => { const tags = await query.take(ps.limit!).getMany(); - return tags; + return Hashtags.packMany(tags); }); diff --git a/src/server/api/endpoints/hashtags/show.ts b/src/server/api/endpoints/hashtags/show.ts new file mode 100644 index 000000000..72a4cc7c8 --- /dev/null +++ b/src/server/api/endpoints/hashtags/show.ts @@ -0,0 +1,48 @@ +import $ from 'cafy'; +import define from '../../define'; +import { ApiError } from '../../error'; +import { Hashtags } from '../../../../models'; +import { types, bool } from '../../../../misc/schema'; + +export const meta = { + desc: { + 'ja-JP': '指定したハッシュタグの情報を取得します。', + }, + + tags: ['hashtags'], + + requireCredential: false, + + params: { + tag: { + validator: $.str, + desc: { + 'ja-JP': '対象のハッシュタグ(#なし)', + 'en-US': 'Target hashtag. (no # prefixed)' + } + } + }, + + res: { + type: types.object, + optional: bool.false, nullable: bool.false, + ref: 'Hashtag', + }, + + errors: { + noSuchHashtag: { + message: 'No such hashtag.', + code: 'NO_SUCH_HASHTAG', + id: '110ee688-193e-4a3a-9ecf-c167b2e6981e' + } + } +}; + +export default define(meta, async (ps, user) => { + const hashtag = await Hashtags.findOne({ name: ps.tag.toLowerCase() }); + if (hashtag == null) { + throw new ApiError(meta.errors.noSuchHashtag); + } + + return await Hashtags.pack(hashtag); +}); diff --git a/src/server/api/openapi/schemas.ts b/src/server/api/openapi/schemas.ts index e54f989e7..34e6f8947 100644 --- a/src/server/api/openapi/schemas.ts +++ b/src/server/api/openapi/schemas.ts @@ -11,6 +11,7 @@ import { packedFollowingSchema } from '../../../models/repositories/following'; import { packedMutingSchema } from '../../../models/repositories/muting'; import { packedBlockingSchema } from '../../../models/repositories/blocking'; import { packedNoteReactionSchema } from '../../../models/repositories/note-reaction'; +import { packedHashtagSchema } from '../../../models/repositories/hashtag'; export function convertSchemaToOpenApiSchema(schema: Schema) { const res: any = schema; @@ -74,48 +75,5 @@ export const schemas = { Muting: convertSchemaToOpenApiSchema(packedMutingSchema), Blocking: convertSchemaToOpenApiSchema(packedBlockingSchema), NoteReaction: convertSchemaToOpenApiSchema(packedNoteReactionSchema), - - Hashtag: { - type: 'object', - properties: { - tag: { - type: 'string', - description: 'The hashtag name. No # prefixed.', - example: 'misskey', - }, - mentionedUsersCount: { - type: 'number', - description: 'Number of all users using this hashtag.' - }, - mentionedLocalUsersCount: { - type: 'number', - description: 'Number of local users using this hashtag.' - }, - mentionedRemoteUsersCount: { - type: 'number', - description: 'Number of remote users using this hashtag.' - }, - attachedUsersCount: { - type: 'number', - description: 'Number of all users who attached this hashtag to profile.' - }, - attachedLocalUsersCount: { - type: 'number', - description: 'Number of local users who attached this hashtag to profile.' - }, - attachedRemoteUsersCount: { - type: 'number', - description: 'Number of remote users who attached this hashtag to profile.' - }, - }, - required: [ - 'tag', - 'mentionedUsersCount', - 'mentionedLocalUsersCount', - 'mentionedRemoteUsersCount', - 'attachedUsersCount', - 'attachedLocalUsersCount', - 'attachedRemoteUsersCount', - ] - }, + Hashtag: convertSchemaToOpenApiSchema(packedHashtagSchema), }; From 4cb58c089267caf1bec956e047b3cbc659dddaed Mon Sep 17 00:00:00 2001 From: syuilo Date: Thu, 25 Apr 2019 13:27:07 +0900 Subject: [PATCH 06/15] Refactor --- src/models/repositories/abuse-user-report.ts | 12 +++++----- src/models/repositories/blocking.ts | 14 ++++++------ src/models/repositories/drive-file.ts | 22 +++++++++--------- src/models/repositories/following.ts | 22 +++++++++--------- src/models/repositories/hashtag.ts | 12 +++++----- src/models/repositories/muting.ts | 14 ++++++------ src/models/repositories/note-favorite.ts | 14 ++++++------ src/models/repositories/note.ts | 22 +++++++++--------- src/models/repositories/notification.ts | 12 +++++----- src/models/repositories/user.ts | 24 ++++++++++---------- 10 files changed, 84 insertions(+), 84 deletions(-) diff --git a/src/models/repositories/abuse-user-report.ts b/src/models/repositories/abuse-user-report.ts index c708b265a..61d0d6e22 100644 --- a/src/models/repositories/abuse-user-report.ts +++ b/src/models/repositories/abuse-user-report.ts @@ -6,12 +6,6 @@ import { awaitAll } from '../../prelude/await-all'; @EntityRepository(AbuseUserReport) export class AbuseUserReportRepository extends Repository { - public packMany( - reports: any[], - ) { - return Promise.all(reports.map(x => this.pack(x))); - } - public async pack( src: AbuseUserReport['id'] | AbuseUserReport, ) { @@ -30,4 +24,10 @@ export class AbuseUserReportRepository extends Repository { }), }); } + + public packMany( + reports: any[], + ) { + return Promise.all(reports.map(x => this.pack(x))); + } } diff --git a/src/models/repositories/blocking.ts b/src/models/repositories/blocking.ts index fd209bce1..6ee31cece 100644 --- a/src/models/repositories/blocking.ts +++ b/src/models/repositories/blocking.ts @@ -9,13 +9,6 @@ export type PackedBlocking = SchemaType; @EntityRepository(Blocking) export class BlockingRepository extends Repository { - public packMany( - blockings: any[], - me: any - ) { - return Promise.all(blockings.map(x => this.pack(x, me))); - } - public async pack( src: Blocking['id'] | Blocking, me?: any @@ -31,6 +24,13 @@ export class BlockingRepository extends Repository { }) }); } + + public packMany( + blockings: any[], + me: any + ) { + return Promise.all(blockings.map(x => this.pack(x, me))); + } } export const packedBlockingSchema = { diff --git a/src/models/repositories/drive-file.ts b/src/models/repositories/drive-file.ts index 245db4b79..5e7e1d40f 100644 --- a/src/models/repositories/drive-file.ts +++ b/src/models/repositories/drive-file.ts @@ -67,17 +67,6 @@ export class DriveFileRepository extends Repository { return parseInt(sum, 10) || 0; } - public packMany( - files: any[], - options?: { - detail?: boolean - self?: boolean, - withUser?: boolean, - } - ) { - return Promise.all(files.map(f => this.pack(f, options))); - } - public async pack( src: DriveFile['id'] | DriveFile, options?: { @@ -111,6 +100,17 @@ export class DriveFileRepository extends Repository { user: opts.withUser ? Users.pack(file.userId!) : null }); } + + public packMany( + files: any[], + options?: { + detail?: boolean + self?: boolean, + withUser?: boolean, + } + ) { + return Promise.all(files.map(f => this.pack(f, options))); + } } export const packedDriveFileSchema = { diff --git a/src/models/repositories/following.ts b/src/models/repositories/following.ts index aba6527fa..88fee749a 100644 --- a/src/models/repositories/following.ts +++ b/src/models/repositories/following.ts @@ -49,17 +49,6 @@ export class FollowingRepository extends Repository { return following.followeeHost != null; } - public packMany( - followings: any[], - me?: any, - opts?: { - populateFollowee?: boolean; - populateFollower?: boolean; - } - ) { - return Promise.all(followings.map(x => this.pack(x, me, opts))); - } - public async pack( src: Following['id'] | Following, me?: any, @@ -85,6 +74,17 @@ export class FollowingRepository extends Repository { }) : undefined, }); } + + public packMany( + followings: any[], + me?: any, + opts?: { + populateFollowee?: boolean; + populateFollower?: boolean; + } + ) { + return Promise.all(followings.map(x => this.pack(x, me, opts))); + } } export const packedFollowingSchema = { diff --git a/src/models/repositories/hashtag.ts b/src/models/repositories/hashtag.ts index 22321fca8..a990fa3dc 100644 --- a/src/models/repositories/hashtag.ts +++ b/src/models/repositories/hashtag.ts @@ -6,12 +6,6 @@ export type PackedHashtag = SchemaType; @EntityRepository(Hashtag) export class HashtagRepository extends Repository { - public packMany( - hashtags: Hashtag[], - ) { - return Promise.all(hashtags.map(x => this.pack(x))); - } - public async pack( src: Hashtag, ): Promise { @@ -25,6 +19,12 @@ export class HashtagRepository extends Repository { attachedRemoteUsersCount: src.attachedRemoteUsersCount, }; } + + public packMany( + hashtags: Hashtag[], + ) { + return Promise.all(hashtags.map(x => this.pack(x))); + } } export const packedHashtagSchema = { diff --git a/src/models/repositories/muting.ts b/src/models/repositories/muting.ts index 1e8135a5c..9d99e08a7 100644 --- a/src/models/repositories/muting.ts +++ b/src/models/repositories/muting.ts @@ -9,13 +9,6 @@ export type PackedMuting = SchemaType; @EntityRepository(Muting) export class MutingRepository extends Repository { - public packMany( - mutings: any[], - me: any - ) { - return Promise.all(mutings.map(x => this.pack(x, me))); - } - public async pack( src: Muting['id'] | Muting, me?: any @@ -31,6 +24,13 @@ export class MutingRepository extends Repository { }) }); } + + public packMany( + mutings: any[], + me: any + ) { + return Promise.all(mutings.map(x => this.pack(x, me))); + } } export const packedMutingSchema = { diff --git a/src/models/repositories/note-favorite.ts b/src/models/repositories/note-favorite.ts index f428903c1..5bc638410 100644 --- a/src/models/repositories/note-favorite.ts +++ b/src/models/repositories/note-favorite.ts @@ -5,13 +5,6 @@ import { ensure } from '../../prelude/ensure'; @EntityRepository(NoteFavorite) export class NoteFavoriteRepository extends Repository { - public packMany( - favorites: any[], - me: any - ) { - return Promise.all(favorites.map(x => this.pack(x, me))); - } - public async pack( src: NoteFavorite['id'] | NoteFavorite, me?: any @@ -23,4 +16,11 @@ export class NoteFavoriteRepository extends Repository { note: await Notes.pack(favorite.note || favorite.noteId, me), }; } + + public packMany( + favorites: any[], + me: any + ) { + return Promise.all(favorites.map(x => this.pack(x, me))); + } } diff --git a/src/models/repositories/note.ts b/src/models/repositories/note.ts index 1dbfabe88..7b4627693 100644 --- a/src/models/repositories/note.ts +++ b/src/models/repositories/note.ts @@ -76,17 +76,6 @@ export class NoteRepository extends Repository { } } - public packMany( - notes: (Note['id'] | Note)[], - me?: User['id'] | User | null | undefined, - options?: { - detail?: boolean; - skipHide?: boolean; - } - ) { - return Promise.all(notes.map(n => this.pack(n, me, options))); - } - public async pack( src: Note['id'] | Note, me?: User['id'] | User | null | undefined, @@ -214,6 +203,17 @@ export class NoteRepository extends Repository { return packed; } + + public packMany( + notes: (Note['id'] | Note)[], + me?: User['id'] | User | null | undefined, + options?: { + detail?: boolean; + skipHide?: boolean; + } + ) { + return Promise.all(notes.map(n => this.pack(n, me, options))); + } } export const packedNoteSchema = { diff --git a/src/models/repositories/notification.ts b/src/models/repositories/notification.ts index cf77b35a0..54eec87cf 100644 --- a/src/models/repositories/notification.ts +++ b/src/models/repositories/notification.ts @@ -9,12 +9,6 @@ export type PackedNotification = SchemaType; @EntityRepository(Notification) export class NotificationRepository extends Repository { - public packMany( - notifications: any[], - ) { - return Promise.all(notifications.map(x => this.pack(x))); - } - public async pack( src: Notification['id'] | Notification, ): Promise { @@ -48,6 +42,12 @@ export class NotificationRepository extends Repository { } : {}) }); } + + public packMany( + notifications: any[], + ) { + return Promise.all(notifications.map(x => this.pack(x))); + } } export const packedNotificationSchema = { diff --git a/src/models/repositories/user.ts b/src/models/repositories/user.ts index 6b212203f..eab3acc8e 100644 --- a/src/models/repositories/user.ts +++ b/src/models/repositories/user.ts @@ -54,18 +54,6 @@ export class UserRepository extends Repository { }; } - public packMany( - users: (User['id'] | User)[], - me?: User['id'] | User | null | undefined, - options?: { - detail?: boolean, - includeSecrets?: boolean, - includeHasUnreadNotes?: boolean - } - ) { - return Promise.all(users.map(u => this.pack(u, me, options))); - } - public async pack( src: User['id'] | User, me?: User['id'] | User | null | undefined, @@ -187,6 +175,18 @@ export class UserRepository extends Repository { return await awaitAll(packed); } + public packMany( + users: (User['id'] | User)[], + me?: User['id'] | User | null | undefined, + options?: { + detail?: boolean, + includeSecrets?: boolean, + includeHasUnreadNotes?: boolean + } + ) { + return Promise.all(users.map(u => this.pack(u, me, options))); + } + public isLocalUser(user: User): user is ILocalUser { return user.host == null; } From 535d10f4694f532a5ce85f536bed2adb9804e7d8 Mon Sep 17 00:00:00 2001 From: syuilo Date: Thu, 25 Apr 2019 14:40:42 +0900 Subject: [PATCH 07/15] Improve API console --- .../common/views/components/settings/api.vue | 18 ++++++++++++- .../app/common/views/components/ui/input.vue | 4 ++- src/server/api/endpoints/endpoint.ts | 26 +++++++++++++++++++ 3 files changed, 46 insertions(+), 2 deletions(-) create mode 100644 src/server/api/endpoints/endpoint.ts diff --git a/src/client/app/common/views/components/settings/api.vue b/src/client/app/common/views/components/settings/api.vue index 74e3eb066..184fa069f 100644 --- a/src/client/app/common/views/components/settings/api.vue +++ b/src/client/app/common/views/components/settings/api.vue @@ -14,7 +14,7 @@
{{ $t('console.title') }}
- + {{ $t('console.endpoint') }} @@ -80,6 +80,22 @@ export default Vue.extend({ this.sending = false; this.res = JSON5.stringify(err, null, 2); }); + }, + + onEndpointChange() { + this.$root.api('endpoint', { endpoint: this.endpoint }).then(endpoint => { + const body = {}; + for (const p of endpoint.params) { + body[p.name] = + p.type === 'String' ? '' : + p.type === 'Number' ? 0 : + p.type === 'Boolean' ? false : + p.type === 'Array' ? [] : + p.type === 'Object' ? {} : + null; + } + this.body = JSON5.stringify(body, null, 2); + }); } } }); diff --git a/src/client/app/common/views/components/ui/input.vue b/src/client/app/common/views/components/ui/input.vue index bcb87398b..645062df2 100644 --- a/src/client/app/common/views/components/ui/input.vue +++ b/src/client/app/common/views/components/ui/input.vue @@ -23,6 +23,7 @@ @focus="focused = true" @blur="focused = false" @keydown="$emit('keydown', $event)" + @change="$emit('change', $event)" :list="id" > @@ -60,7 +62,7 @@
- + {{ $t('@.show-password') }} {{ $t('@.hide-password') }} diff --git a/src/server/api/endpoints/endpoint.ts b/src/server/api/endpoints/endpoint.ts new file mode 100644 index 000000000..48e78cd04 --- /dev/null +++ b/src/server/api/endpoints/endpoint.ts @@ -0,0 +1,26 @@ +import $ from 'cafy'; +import define from '../define'; +import endpoints from '../endpoints'; + +export const meta = { + requireCredential: false, + + tags: ['meta'], + + params: { + endpoint: { + validator: $.str, + } + }, +}; + +export default define(meta, async (ps) => { + const ep = endpoints.find(x => x.name === ps.endpoint); + if (ep == null) return null; + return { + params: Object.entries(ep.meta.params || {}).map(([k, v]) => ({ + name: k, + type: v.validator.name === 'ID' ? 'String' : v.validator.name + })) + }; +}); From 291e7e79438bedd654353726bea8a3706243205b Mon Sep 17 00:00:00 2001 From: rinsuki <428rinsuki+git@gmail.com> Date: Fri, 26 Apr 2019 00:52:58 +0900 Subject: [PATCH 08/15] =?UTF-8?q?=E3=81=8A=E3=81=99=E3=81=99=E3=82=81?= =?UTF-8?q?=E3=83=A6=E3=83=BC=E3=82=B6=E3=83=BC=E3=81=AB=E8=87=AA=E5=88=86?= =?UTF-8?q?=E8=87=AA=E8=BA=AB=E3=82=92=E5=90=AB=E3=81=BE=E3=81=AA=E3=81=84?= =?UTF-8?q?=E3=82=88=E3=81=86=E3=81=AB=20(#4803)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fix #4790 --- src/server/api/endpoints/users/recommendation.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/server/api/endpoints/users/recommendation.ts b/src/server/api/endpoints/users/recommendation.ts index 67b646a35..5b74d442c 100644 --- a/src/server/api/endpoints/users/recommendation.ts +++ b/src/server/api/endpoints/users/recommendation.ts @@ -44,6 +44,7 @@ export default define(meta, async (ps, me) => { .where('user.isLocked = FALSE') .where('user.host IS NULL') .where('user.updatedAt >= :date', { date: new Date(Date.now() - ms('7days')) }) + .where('user.id != :meId', { meId: me.id }) .orderBy('user.followersCount', 'DESC'); generateMuteQueryForUsers(query, me); From 2b8187f7abcf914248040c2ebe351d15d38fb031 Mon Sep 17 00:00:00 2001 From: syuilo Date: Fri, 26 Apr 2019 00:54:11 +0900 Subject: [PATCH 09/15] Fix bug --- src/server/api/endpoints/users/recommendation.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/server/api/endpoints/users/recommendation.ts b/src/server/api/endpoints/users/recommendation.ts index 5b74d442c..38b420c33 100644 --- a/src/server/api/endpoints/users/recommendation.ts +++ b/src/server/api/endpoints/users/recommendation.ts @@ -42,9 +42,9 @@ export const meta = { export default define(meta, async (ps, me) => { const query = Users.createQueryBuilder('user') .where('user.isLocked = FALSE') - .where('user.host IS NULL') - .where('user.updatedAt >= :date', { date: new Date(Date.now() - ms('7days')) }) - .where('user.id != :meId', { meId: me.id }) + .andWhere('user.host IS NULL') + .andWhere('user.updatedAt >= :date', { date: new Date(Date.now() - ms('7days')) }) + .andWhere('user.id != :meId', { meId: me.id }) .orderBy('user.followersCount', 'DESC'); generateMuteQueryForUsers(query, me); From 0cfca4a6182e4f3b18b8a82ebdcda9403dea08e2 Mon Sep 17 00:00:00 2001 From: YuzuRyo61 Date: Fri, 26 Apr 2019 18:29:56 +0900 Subject: [PATCH 10/15] Fix official instance address (#4805) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit いくつかのURLが misskey.xyz になってたままだったので misskey.io に訂正 --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index f69c503a9..1fae71df4 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ -[![Misskey](/assets/title.png)](https://misskey.xyz/) +[![Misskey](/assets/title.png)](https://misskey.io/) ================================================================ [![CircleCI](https://img.shields.io/circleci/project/github/syuilo/misskey.svg?style=for-the-badge&logo=circleci)](https://circleci.com/gh/syuilo/misskey) @@ -10,7 +10,7 @@ **A forever evolving, sophisticated microblogging platform.**

-Misskey is a decentralized microblogging platform born on Earth. +Misskey is a decentralized microblogging platform born on Earth. Since it exists within the Fediverse (a universe where various social media platforms are organized), it is mutually linked with other social media platforms. Why don't you take a short break from the hustle and bustle of the city, and dive into a new Internet? Find an instance! From c05586b53a8271c9cd440f446ccdc924807af163 Mon Sep 17 00:00:00 2001 From: syuilo Date: Sat, 27 Apr 2019 11:17:03 +0900 Subject: [PATCH 11/15] Improve performance --- src/server/api/endpoints/notes/global-timeline.ts | 8 +++++--- src/server/api/endpoints/notes/hybrid-timeline.ts | 8 +++++--- src/server/api/endpoints/notes/local-timeline.ts | 8 +++++--- src/server/api/endpoints/notes/timeline.ts | 6 +++++- 4 files changed, 20 insertions(+), 10 deletions(-) diff --git a/src/server/api/endpoints/notes/global-timeline.ts b/src/server/api/endpoints/notes/global-timeline.ts index 3631208da..f46fa208d 100644 --- a/src/server/api/endpoints/notes/global-timeline.ts +++ b/src/server/api/endpoints/notes/global-timeline.ts @@ -89,9 +89,11 @@ export default define(meta, async (ps, user) => { const timeline = await query.take(ps.limit!).getMany(); - if (user) { - activeUsersChart.update(user); - } + process.nextTick(() => { + if (user) { + activeUsersChart.update(user); + } + }); return await Notes.packMany(timeline, user); }); diff --git a/src/server/api/endpoints/notes/hybrid-timeline.ts b/src/server/api/endpoints/notes/hybrid-timeline.ts index c05c8dedd..7be13fc47 100644 --- a/src/server/api/endpoints/notes/hybrid-timeline.ts +++ b/src/server/api/endpoints/notes/hybrid-timeline.ts @@ -192,9 +192,11 @@ export default define(meta, async (ps, user) => { const timeline = await query.take(ps.limit!).getMany(); - if (user) { - activeUsersChart.update(user); - } + process.nextTick(() => { + if (user) { + activeUsersChart.update(user); + } + }); return await Notes.packMany(timeline, user); }); diff --git a/src/server/api/endpoints/notes/local-timeline.ts b/src/server/api/endpoints/notes/local-timeline.ts index ca84fc6ef..73cbebace 100644 --- a/src/server/api/endpoints/notes/local-timeline.ts +++ b/src/server/api/endpoints/notes/local-timeline.ts @@ -125,9 +125,11 @@ export default define(meta, async (ps, user) => { const timeline = await query.take(ps.limit!).getMany(); - if (user) { - activeUsersChart.update(user); - } + process.nextTick(() => { + if (user) { + activeUsersChart.update(user); + } + }); return await Notes.packMany(timeline, user); }); diff --git a/src/server/api/endpoints/notes/timeline.ts b/src/server/api/endpoints/notes/timeline.ts index 5e692db38..f9442f8b9 100644 --- a/src/server/api/endpoints/notes/timeline.ts +++ b/src/server/api/endpoints/notes/timeline.ts @@ -177,7 +177,11 @@ export default define(meta, async (ps, user) => { const timeline = await query.take(ps.limit!).getMany(); - activeUsersChart.update(user); + process.nextTick(() => { + if (user) { + activeUsersChart.update(user); + } + }); return await Notes.packMany(timeline, user); }); From 747a0b17913bb8cf13bcfe9f3cacfeae176ce6af Mon Sep 17 00:00:00 2001 From: syuilo Date: Sun, 28 Apr 2019 19:56:41 +0900 Subject: [PATCH 12/15] Update define.ts --- src/server/api/define.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/server/api/define.ts b/src/server/api/define.ts index 1e2600add..f9e9813a8 100644 --- a/src/server/api/define.ts +++ b/src/server/api/define.ts @@ -14,7 +14,8 @@ type Params = { export type Response = Record | void; type executor = - (params: Params, user: ILocalUser, app: App, file?: any, cleanup?: Function) => Promise>>; + (params: Params, user: ILocalUser, app: App, file?: any, cleanup?: Function) => + Promise>>; export default function (meta: T, cb: executor) : (params: any, user: ILocalUser, app: App, file?: any) => Promise { From 05b8111c1906c1285c9ddde758eda45b83792244 Mon Sep 17 00:00:00 2001 From: syuilo Date: Mon, 29 Apr 2019 09:11:57 +0900 Subject: [PATCH 13/15] Pages (#4811) * wip * wip * wip * Update page-editor.vue * wip * wip * wip * wip * wip * wip * wip * Update page-editor.variable.core.vue * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * Update aiscript.ts * wip * Update package.json * wip * wip * wip * wip * wip * Update page.vue * wip * wip * wip * wip * more info * wip fn * wip * wip * wip --- .gitignore | 1 + CHANGELOG.md | 26 + locales/ja-JP.yml | 183 ++++++- migration/1556348509290-Pages.ts | 31 ++ package.json | 3 +- src/client/app/common/scripts/aiscript.ts | 470 ++++++++++++++++++ .../app/common/scripts/collect-page-vars.ts | 24 + .../app/common/views/components/dialog.vue | 11 +- .../common/views/components/media-image.vue | 2 +- .../page-editor/page-editor.block.vue | 25 + .../page-editor/page-editor.button.vue | 54 ++ .../page-editor/page-editor.container.vue | 135 +++++ .../page-editor/page-editor.image.vue | 78 +++ .../page-editor/page-editor.input.vue | 54 ++ .../page-editor/page-editor.script-block.vue | 263 ++++++++++ .../page-editor/page-editor.section.vue | 133 +++++ .../page-editor/page-editor.switch.vue | 48 ++ .../page-editor/page-editor.text.vue | 57 +++ .../components/page-editor/page-editor.vue | 452 +++++++++++++++++ .../common/views/components/page-preview.vue | 141 ++++++ .../common/views/pages/page/page.block.vue | 34 ++ .../common/views/pages/page/page.button.vue | 42 ++ .../common/views/pages/page/page.image.vue | 36 ++ .../common/views/pages/page/page.input.vue | 43 ++ .../common/views/pages/page/page.section.vue | 55 ++ .../common/views/pages/page/page.switch.vue | 33 ++ .../app/common/views/pages/page/page.text.vue | 35 ++ .../app/common/views/pages/page/page.vue | 143 ++++++ src/client/app/desktop/script.ts | 4 + .../views/components/ui.header.account.vue | 27 +- src/client/app/desktop/views/home/pages.vue | 92 ++++ .../app/desktop/views/pages/page-editor.vue | 32 ++ src/client/app/desktop/views/pages/page.vue | 36 ++ src/client/app/mobile/script.ts | 4 + .../app/mobile/views/components/ui.nav.vue | 5 +- .../app/mobile/views/pages/page-editor.vue | 32 ++ src/client/app/mobile/views/pages/page.vue | 36 ++ src/client/app/mobile/views/pages/pages.vue | 94 ++++ src/client/themes/dark.json5 | 3 + src/client/themes/light.json5 | 3 + src/db/postgre.ts | 2 + src/models/entities/page.ts | 105 ++++ src/models/index.ts | 2 + src/models/repositories/page.ts | 61 +++ src/server/api/endpoints/i/pages.ts | 44 ++ src/server/api/endpoints/pages/create.ts | 108 ++++ src/server/api/endpoints/pages/delete.ts | 53 ++ src/server/api/endpoints/pages/show.ts | 74 +++ src/server/api/endpoints/pages/update.ts | 123 +++++ src/server/web/index.ts | 37 +- src/server/web/views/note.pug | 1 + src/server/web/views/page.pug | 30 ++ 52 files changed, 3583 insertions(+), 37 deletions(-) create mode 100644 migration/1556348509290-Pages.ts create mode 100644 src/client/app/common/scripts/aiscript.ts create mode 100644 src/client/app/common/scripts/collect-page-vars.ts create mode 100644 src/client/app/common/views/components/page-editor/page-editor.block.vue create mode 100644 src/client/app/common/views/components/page-editor/page-editor.button.vue create mode 100644 src/client/app/common/views/components/page-editor/page-editor.container.vue create mode 100644 src/client/app/common/views/components/page-editor/page-editor.image.vue create mode 100644 src/client/app/common/views/components/page-editor/page-editor.input.vue create mode 100644 src/client/app/common/views/components/page-editor/page-editor.script-block.vue create mode 100644 src/client/app/common/views/components/page-editor/page-editor.section.vue create mode 100644 src/client/app/common/views/components/page-editor/page-editor.switch.vue create mode 100644 src/client/app/common/views/components/page-editor/page-editor.text.vue create mode 100644 src/client/app/common/views/components/page-editor/page-editor.vue create mode 100644 src/client/app/common/views/components/page-preview.vue create mode 100644 src/client/app/common/views/pages/page/page.block.vue create mode 100644 src/client/app/common/views/pages/page/page.button.vue create mode 100644 src/client/app/common/views/pages/page/page.image.vue create mode 100644 src/client/app/common/views/pages/page/page.input.vue create mode 100644 src/client/app/common/views/pages/page/page.section.vue create mode 100644 src/client/app/common/views/pages/page/page.switch.vue create mode 100644 src/client/app/common/views/pages/page/page.text.vue create mode 100644 src/client/app/common/views/pages/page/page.vue create mode 100644 src/client/app/desktop/views/home/pages.vue create mode 100644 src/client/app/desktop/views/pages/page-editor.vue create mode 100644 src/client/app/desktop/views/pages/page.vue create mode 100644 src/client/app/mobile/views/pages/page-editor.vue create mode 100644 src/client/app/mobile/views/pages/page.vue create mode 100644 src/client/app/mobile/views/pages/pages.vue create mode 100644 src/models/entities/page.ts create mode 100644 src/models/repositories/page.ts create mode 100644 src/server/api/endpoints/i/pages.ts create mode 100644 src/server/api/endpoints/pages/create.ts create mode 100644 src/server/api/endpoints/pages/delete.ts create mode 100644 src/server/api/endpoints/pages/show.ts create mode 100644 src/server/api/endpoints/pages/update.ts create mode 100644 src/server/web/views/page.pug diff --git a/.gitignore b/.gitignore index 650d4f612..255b1ad4d 100644 --- a/.gitignore +++ b/.gitignore @@ -20,3 +20,4 @@ api-docs.json yarn.lock .DS_Store /files +ormconfig.json diff --git a/CHANGELOG.md b/CHANGELOG.md index df090780a..c576a5714 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -42,6 +42,32 @@ mongodb: 8. master ブランチに戻す 9. enjoy +unreleased +------------------- +### New features +#### MisskeyPages +ページ(記事)を作成できるように。 + +* 後から何度でも編集できる +* アイキャッチを設定できる +* フォントを設定できる +* 画像を好きな位置に挿入できる +* URLを決められる +* タイトルを設定できる +* 見出しを設定できる +* ページの要約を設定できる(URLプレビュー時などに便利) +* 変数や式(aka AiScript)を使用して動的なページも作れる +* 目次自動生成(coming soon) + +ページを気に入ったら「いいね」しよう (coming soon) + +### Improvements +* APIコンソールでパラメータテンプレートを表示するように + +### Fixes +* おすすめユーザーに自分自身が含まれる問題を修正 +* ユーザーサジェストで表示名が変わらない問題を修正 + 11.4.0 (2019/04/25) ------------------- ### Improvements diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index a475bc2c1..b0cb78f96 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -65,6 +65,7 @@ common: trash: "ゴミ箱" drive: "ドライブ" + pages: "ページ" messaging: "トーク" home: "ホーム" deck: "デッキ" @@ -1813,26 +1814,6 @@ docs: edit-this-page-on-github: "間違いや改善点を見つけましたか?" edit-this-page-on-github-link: "このページをGitHubで編集" - api: - entities: - properties: "プロパティ" - endpoints: - params: "パラメータ" - no-params: "パラメータはありません" - res: "レスポンス" - require-credential: "このエンドポイントは認証情報が必須です。" - require-permission: "このエンドポイントは{permission}の権限を必要とします。" - has-limit: "レートリミットがあります。" - duration-limit: "直近{duration}ミリ秒の間のこのエンドポイントへのリクエスト数の合計が{max}を超える場合はリクエストできません。" - min-interval-limit: "前回のリクエストから{interval}ミリ秒経っていない場合はリクエストできません。" - show-src: "このエンドポイントのソースコードも閲覧できます。" - show-src-link: "コードをGitHubで見る" - generated: "このドキュメントはAPI定義に基づき自動生成されています。" - props: - name: "名前" - type: "型" - description: "説明" - dev/views/index.vue: manage-apps: "アプリの管理" @@ -1857,3 +1838,165 @@ dev/views/new-app.vue: authority: "権限" authority-desc: "ここで要求した機能だけがAPIからアクセスできます。" authority-warning: "アプリ作成後も変更できますが、新たな権限を付与する場合、その時点で関連付けられているユーザーキーはすべて無効になります。" + +pages: + new-page: "ページの作成" + edit-page: "ページの編集" + page-created: "ページを作成しました" + page-updated: "ページを更新しました" + are-you-sure-delete: "このページを削除しますか?" + page-deleted: "ページを削除しました" + edit-this-page: "このページを編集" + variables: "変数" + variables-info: "変数を使うことで動的なページを作成できます。テキスト内で { 変数名 } と書くとそこに変数の値を埋め込めます。例えば Hello { thing } world! というテキストで、変数(thing)の値が ai だった場合、テキストは Hello ai world! になります。" + variables-info2: "変数の評価(値を算出すること)は上から下に行われるので、ある変数の中で自分より下の変数を参照することはできません。例えば上から A、B、C と3つの変数を定義したとき、Cの中でABを参照することはできますが、Aの中でBCを参照することはできません。" + variables-info3: "ユーザーからの入力を受け取るには、ページに「ユーザー入力」ブロックを設置し、「変数名」に入力を格納したい変数名を設定します(変数は自動で作成されます)。その変数を使ってユーザー入力に応じた動作を行えます。" + more-details: "詳しい説明" + title: "タイトル" + url: "ページURL" + summary: "ページの要約" + align-center: "中央寄せ" + font: "フォント" + fontSerif: "セリフ" + fontSansSerif: "サンセリフ" + set-eye-catchig-image: "アイキャッチ画像を設定" + remove-eye-catchig-image: "アイキャッチ画像を削除" + choose-block: "ブロックを追加" + select-type: "種類を選択" + enter-variable-name: "変数名を決めてください" + the-variable-name-is-already-used: "その変数名は既に使われています" + blocks: + text: "テキスト" + section: "セクション" + image: "画像" + button: "ボタン" + input: "ユーザー入力" + _input: + name: "変数名" + text: "タイトル" + default: "デフォルト値" + inputType: "入力の種類" + _inputType: + string: "テキスト" + number: "数値" + switch: "スイッチ" + _switch: + name: "変数名" + text: "タイトル" + default: "デフォルト値" + _button: + text: "タイトル" + action: "ボタンを押したときの動作" + _action: + dialog: "ダイアログを表示する" + _dialog: + content: "内容" + resetRandom: "乱数をリセット" + script: + categories: + flow: "制御" + logical: "論理演算" + operation: "計算" + comparison: "比較" + random: "ランダム" + value: "値" + fn: "関数" + blocks: + text: "テキスト" + multiLineText: "テキスト(複数行)" + textList: "テキストのリスト" + add: "+ 足す" + _add: + arg1: "A" + arg2: "B" + subtract: "- 引く" + _subtract: + arg1: "A" + arg2: "B" + multiply: "× 掛ける" + _multiply: + arg1: "A" + arg2: "B" + divide: "÷ 割る" + _divide: + arg1: "A" + arg2: "B" + eq: "AとBが同じ" + _eq: + arg1: "A" + arg2: "B" + notEq: "AとBが異なる" + _notEq: + arg1: "A" + arg2: "B" + and: "AかつB" + _and: + arg1: "A" + arg2: "B" + or: "AまたはB" + _or: + arg1: "A" + arg2: "B" + lt: "< AがBより小さい" + _lt: + arg1: "A" + arg2: "B" + gt: "> AがBより大きい" + _gt: + arg1: "A" + arg2: "B" + ltEq: "<= AがBと同じか小さい" + _ltEq: + arg1: "A" + arg2: "B" + gtEq: ">= AがBと同じか大きい" + _gtEq: + arg1: "A" + arg2: "B" + if: "分岐" + _if: + arg1: "もし" + arg2: "なら" + arg3: "そうでなければ" + not: "否定" + _not: + arg1: "否定" + random: "ランダム" + _random: + arg1: "確率" + rannum: "乱数" + _rannum: + arg1: "最小" + arg2: "最大" + randomPick: "リストからランダムに選択" + _randomPick: + arg1: "リスト" + dailyRandom: "ランダム (ユーザーごとに日替わり)" + _dailyRandom: + arg1: "確率" + dailyRannum: "乱数 (ユーザーごとに日替わり)" + _dailyRannum: + arg1: "最小" + arg2: "最大" + dailyRandomPick: "リストからランダムに選択 (ユーザーごとに日替わり)" + _dailyRandomPick: + arg1: "リスト" + number: "数" + ref: "変数" + in: "入力" + _in: + arg1: "スロット番号" + fn: "関数" + _fn: + arg1: "出力" + typeError: "スロット{slot}は\"{expect}\"を受け付けますが、\"{actual}\"が入れられています!" + thereIsEmptySlot: "スロット{slot}が空です!" + types: + string: "テキスト" + number: "数値" + boolean: "フラグ" + array: "リスト" + stringArray: "テキストのリスト" + emptySlot: "空のスロット" + enviromentVariables: "環境変数" + pageVariables: "ページ要素" diff --git a/migration/1556348509290-Pages.ts b/migration/1556348509290-Pages.ts new file mode 100644 index 000000000..c44b4b1f7 --- /dev/null +++ b/migration/1556348509290-Pages.ts @@ -0,0 +1,31 @@ +import {MigrationInterface, QueryRunner} from "typeorm"; + +export class Pages1556348509290 implements MigrationInterface { + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`CREATE TYPE "page_visibility_enum" AS ENUM('public', 'followers', 'specified')`); + await queryRunner.query(`CREATE TABLE "page" ("id" character varying(32) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL, "title" character varying(256) NOT NULL, "name" character varying(256) NOT NULL, "summary" character varying(256), "alignCenter" boolean NOT NULL, "font" character varying(32) NOT NULL, "userId" character varying(32) NOT NULL, "eyeCatchingImageId" character varying(32), "content" jsonb NOT NULL DEFAULT '[]', "variables" jsonb NOT NULL DEFAULT '[]', "visibility" "page_visibility_enum" NOT NULL, "visibleUserIds" character varying(32) array NOT NULL DEFAULT '{}'::varchar[], CONSTRAINT "PK_742f4117e065c5b6ad21b37ba1f" PRIMARY KEY ("id"))`); + await queryRunner.query(`CREATE INDEX "IDX_fbb4297c927a9b85e9cefa2eb1" ON "page" ("createdAt") `); + await queryRunner.query(`CREATE INDEX "IDX_af639b066dfbca78b01a920f8a" ON "page" ("updatedAt") `); + await queryRunner.query(`CREATE INDEX "IDX_b82c19c08afb292de4600d99e4" ON "page" ("name") `); + await queryRunner.query(`CREATE INDEX "IDX_ae1d917992dd0c9d9bbdad06c4" ON "page" ("userId") `); + await queryRunner.query(`CREATE INDEX "IDX_90148bbc2bf0854428786bfc15" ON "page" ("visibleUserIds") `); + await queryRunner.query(`CREATE UNIQUE INDEX "IDX_2133ef8317e4bdb839c0dcbf13" ON "page" ("userId", "name") `); + await queryRunner.query(`ALTER TABLE "page" ADD CONSTRAINT "FK_ae1d917992dd0c9d9bbdad06c4a" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "page" ADD CONSTRAINT "FK_3126dd7c502c9e4d7597ef7ef10" FOREIGN KEY ("eyeCatchingImageId") REFERENCES "drive_file"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "page" DROP CONSTRAINT "FK_3126dd7c502c9e4d7597ef7ef10"`); + await queryRunner.query(`ALTER TABLE "page" DROP CONSTRAINT "FK_ae1d917992dd0c9d9bbdad06c4a"`); + await queryRunner.query(`DROP INDEX "IDX_2133ef8317e4bdb839c0dcbf13"`); + await queryRunner.query(`DROP INDEX "IDX_90148bbc2bf0854428786bfc15"`); + await queryRunner.query(`DROP INDEX "IDX_ae1d917992dd0c9d9bbdad06c4"`); + await queryRunner.query(`DROP INDEX "IDX_b82c19c08afb292de4600d99e4"`); + await queryRunner.query(`DROP INDEX "IDX_af639b066dfbca78b01a920f8a"`); + await queryRunner.query(`DROP INDEX "IDX_fbb4297c927a9b85e9cefa2eb1"`); + await queryRunner.query(`DROP TABLE "page"`); + await queryRunner.query(`DROP TYPE "page_visibility_enum"`); + } + +} diff --git a/package.json b/package.json index b1900c8fa..780ce5563 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "misskey", "author": "syuilo ", - "version": "11.4.0", + "version": "11.5.0", "codename": "daybreak", "repository": { "type": "git", @@ -199,6 +199,7 @@ "rimraf": "2.6.3", "rndstr": "1.0.0", "s-age": "1.1.2", + "seedrandom": "3.0.1", "sharp": "0.22.0", "showdown": "1.9.0", "showdown-highlightjs-extension": "0.1.2", diff --git a/src/client/app/common/scripts/aiscript.ts b/src/client/app/common/scripts/aiscript.ts new file mode 100644 index 000000000..4ef21f994 --- /dev/null +++ b/src/client/app/common/scripts/aiscript.ts @@ -0,0 +1,470 @@ +/** + * AiScript + * evaluator & type checker + */ + +import autobind from 'autobind-decorator'; +import * as seedrandom from 'seedrandom'; + +import { + faSuperscript, + faAlignLeft, + faShareAlt, + faSquareRootAlt, + faPlus, + faMinus, + faTimes, + faDivide, + faList, + faQuoteRight, + faEquals, + faGreaterThan, + faLessThan, + faGreaterThanEqual, + faLessThanEqual, + faExclamation, + faNotEqual, + faDice, + faSortNumericUp, +} from '@fortawesome/free-solid-svg-icons'; +import { faFlag } from '@fortawesome/free-regular-svg-icons'; + +import { version } from '../../config'; + +export type Block = { + id: string; + type: string; + args: Block[]; + value: any; +}; + +export type Variable = Block & { + name: string; +}; + +type Type = 'string' | 'number' | 'boolean' | 'stringArray'; + +type TypeError = { + arg: number; + expect: Type; + actual: Type; +}; + +const funcDefs = { + if: { in: ['boolean', 0, 0], out: 0, category: 'flow', icon: faShareAlt, }, + not: { in: ['boolean'], out: 'boolean', category: 'logical', icon: faFlag, }, + or: { in: ['boolean', 'boolean'], out: 'boolean', category: 'logical', icon: faFlag, }, + and: { in: ['boolean', 'boolean'], out: 'boolean', category: 'logical', icon: faFlag, }, + add: { in: ['number', 'number'], out: 'number', category: 'operation', icon: faPlus, }, + subtract: { in: ['number', 'number'], out: 'number', category: 'operation', icon: faMinus, }, + multiply: { in: ['number', 'number'], out: 'number', category: 'operation', icon: faTimes, }, + divide: { in: ['number', 'number'], out: 'number', category: 'operation', icon: faDivide, }, + eq: { in: [0, 0], out: 'boolean', category: 'comparison', icon: faEquals, }, + notEq: { in: [0, 0], out: 'boolean', category: 'comparison', icon: faNotEqual, }, + gt: { in: ['number', 'number'], out: 'boolean', category: 'comparison', icon: faGreaterThan, }, + lt: { in: ['number', 'number'], out: 'boolean', category: 'comparison', icon: faLessThan, }, + gtEq: { in: ['number', 'number'], out: 'boolean', category: 'comparison', icon: faGreaterThanEqual, }, + ltEq: { in: ['number', 'number'], out: 'boolean', category: 'comparison', icon: faLessThanEqual, }, + rannum: { in: ['number', 'number'], out: 'number', category: 'random', icon: faDice, }, + random: { in: ['number'], out: 'boolean', category: 'random', icon: faDice, }, + randomPick: { in: [0], out: 0, category: 'random', icon: faDice, }, + dailyRannum: { in: ['number', 'number'], out: 'number', category: 'random', icon: faDice, }, + dailyRandom: { in: ['number'], out: 'boolean', category: 'random', icon: faDice, }, + dailyRandomPick: { in: [0], out: 0, category: 'random', icon: faDice, }, +}; + +const blockDefs = [ + { type: 'text', out: 'string', category: 'value', icon: faQuoteRight, }, + { type: 'multiLineText', out: 'string', category: 'value', icon: faAlignLeft, }, + { type: 'textList', out: 'stringArray', category: 'value', icon: faList, }, + { type: 'number', out: 'number', category: 'value', icon: faSortNumericUp, }, + { type: 'ref', out: null, category: 'value', icon: faSuperscript, }, + { type: 'in', out: null, category: 'value', icon: faSuperscript, }, + { type: 'fn', out: 'function', category: 'value', icon: faSuperscript, }, + ...Object.entries(funcDefs).map(([k, v]) => ({ + type: k, out: v.out || null, category: v.category, icon: v.icon + })) +]; + +type PageVar = { name: string; value: any; type: Type; }; + +const envVarsDef = { + AI: 'string', + VERSION: 'string', + LOGIN: 'boolean', + NAME: 'string', + USERNAME: 'string', + USERID: 'string', + NOTES_COUNT: 'number', + FOLLOWERS_COUNT: 'number', + FOLLOWING_COUNT: 'number', + IS_CAT: 'boolean', + MY_NOTES_COUNT: 'number', + MY_FOLLOWERS_COUNT: 'number', + MY_FOLLOWING_COUNT: 'number', +}; + +export class AiScript { + private variables: Variable[]; + private pageVars: PageVar[]; + private envVars: Record; + + public static envVarsDef = envVarsDef; + public static blockDefs = blockDefs; + public static funcDefs = funcDefs; + private opts: { + randomSeed?: string; user?: any; visitor?: any; + }; + + constructor(variables: Variable[] = [], pageVars: PageVar[] = [], opts: AiScript['opts'] = {}) { + this.variables = variables; + this.pageVars = pageVars; + this.opts = opts; + + this.envVars = { + AI: 'kawaii', + VERSION: version, + LOGIN: opts.visitor != null, + NAME: opts.visitor ? opts.visitor.name : '', + USERNAME: opts.visitor ? opts.visitor.username : '', + USERID: opts.visitor ? opts.visitor.id : '', + NOTES_COUNT: opts.visitor ? opts.visitor.notesCount : 0, + FOLLOWERS_COUNT: opts.visitor ? opts.visitor.followersCount : 0, + FOLLOWING_COUNT: opts.visitor ? opts.visitor.followingCount : 0, + IS_CAT: opts.visitor ? opts.visitor.isCat : false, + MY_NOTES_COUNT: opts.user ? opts.user.notesCount : 0, + MY_FOLLOWERS_COUNT: opts.user ? opts.user.followersCount : 0, + MY_FOLLOWING_COUNT: opts.user ? opts.user.followingCount : 0, + }; + } + + @autobind + public injectVars(vars: Variable[]) { + this.variables = vars; + } + + @autobind + public injectPageVars(pageVars: PageVar[]) { + this.pageVars = pageVars; + } + + @autobind + public updatePageVar(name: string, value: any) { + this.pageVars.find(v => v.name === name).value = value; + } + + @autobind + public updateRandomSeed(seed: string) { + this.opts.randomSeed = seed; + } + + @autobind + public static isLiteralBlock(v: Block) { + if (v.type === null) return true; + if (v.type === 'text') return true; + if (v.type === 'multiLineText') return true; + if (v.type === 'textList') return true; + if (v.type === 'number') return true; + if (v.type === 'ref') return true; + if (v.type === 'fn') return true; + if (v.type === 'in') return true; + return false; + } + + @autobind + public typeCheck(v: Block): TypeError | null { + if (AiScript.isLiteralBlock(v)) return null; + + const def = AiScript.funcDefs[v.type]; + if (def == null) { + throw new Error('Unknown type: ' + v.type); + } + + const generic: Type[] = []; + + for (let i = 0; i < def.in.length; i++) { + const arg = def.in[i]; + const type = this.typeInference(v.args[i]); + if (type === null) continue; + + if (typeof arg === 'number') { + if (generic[arg] === undefined) { + generic[arg] = type; + } else if (type !== generic[arg]) { + return { + arg: i, + expect: generic[arg], + actual: type + }; + } + } else if (type !== arg) { + return { + arg: i, + expect: arg, + actual: type + }; + } + } + + return null; + } + + @autobind + public getExpectedType(v: Block, slot: number): Type | null { + const def = AiScript.funcDefs[v.type]; + if (def == null) { + throw new Error('Unknown type: ' + v.type); + } + + const generic: Type[] = []; + + for (let i = 0; i < def.in.length; i++) { + const arg = def.in[i]; + const type = this.typeInference(v.args[i]); + if (type === null) continue; + + if (typeof arg === 'number') { + if (generic[arg] === undefined) { + generic[arg] = type; + } + } + } + + if (typeof def.in[slot] === 'number') { + return generic[def.in[slot]] || null; + } else { + return def.in[slot]; + } + } + + @autobind + public typeInference(v: Block): Type | null { + if (v.type === null) return null; + if (v.type === 'text') return 'string'; + if (v.type === 'multiLineText') return 'string'; + if (v.type === 'textList') return 'stringArray'; + if (v.type === 'number') return 'number'; + if (v.type === 'ref') { + const variable = this.variables.find(va => va.name === v.value); + if (variable) { + return this.typeInference(variable); + } + + const pageVar = this.pageVars.find(va => va.name === v.value); + if (pageVar) { + return pageVar.type; + } + + const envVar = AiScript.envVarsDef[v.value]; + if (envVar) { + return envVar; + } + + return null; + } + if (v.type === 'fn') return null; // todo + if (v.type === 'in') return null; // todo + + const generic: Type[] = []; + + const def = AiScript.funcDefs[v.type]; + + for (let i = 0; i < def.in.length; i++) { + const arg = def.in[i]; + if (typeof arg === 'number') { + const type = this.typeInference(v.args[i]); + + if (generic[arg] === undefined) { + generic[arg] = type; + } else { + if (type !== generic[arg]) { + generic[arg] = null; + } + } + } + } + + if (typeof def.out === 'number') { + return generic[def.out]; + } else { + return def.out; + } + } + + @autobind + public getVarsByType(type: Type | null): Variable[] { + if (type == null) return this.variables; + return this.variables.filter(x => (this.typeInference(x) === null) || (this.typeInference(x) === type)); + } + + @autobind + public getVarByName(name: string): Variable { + return this.variables.find(x => x.name === name); + } + + @autobind + public getEnvVarsByType(type: Type | null): string[] { + if (type == null) return Object.keys(AiScript.envVarsDef); + return Object.entries(AiScript.envVarsDef).filter(([k, v]) => type === v).map(([k, v]) => k); + } + + @autobind + public getPageVarsByType(type: Type | null): string[] { + if (type == null) return this.pageVars.map(v => v.name); + return this.pageVars.filter(v => type === v.type).map(v => v.name); + } + + @autobind + private interpolate(str: string, values: { name: string, value: any }[]) { + return str.replace(/\{(.+?)\}/g, match => + (this.getVariableValue(match.slice(1, -1).trim(), values) || '').toString()); + } + + @autobind + public evaluateVars() { + const values: { name: string, value: any }[] = []; + + for (const v of this.variables) { + values.push({ + name: v.name, + value: this.evaluate(v, values) + }); + } + + for (const v of this.pageVars) { + values.push({ + name: v.name, + value: v.value + }); + } + + for (const [k, v] of Object.entries(this.envVars)) { + values.push({ + name: k, + value: v + }); + } + + return values; + } + + @autobind + private evaluate(block: Block, values: { name: string, value: any }[], slotArg: Record = {}): any { + if (block.type === null) { + return null; + } + + if (block.type === 'number') { + return parseInt(block.value, 10); + } + + if (block.type === 'text' || block.type === 'multiLineText') { + return this.interpolate(block.value, values); + } + + if (block.type === 'textList') { + return block.value.trim().split('\n'); + } + + if (block.type === 'ref') { + return this.getVariableValue(block.value, values); + } + + if (block.type === 'in') { + return slotArg[block.value]; + } + + if (block.type === 'fn') { // ユーザー関数定義 + return { + slots: block.value.slots, + exec: slotArg => this.evaluate(block.value.expression, values, slotArg) + }; + } + + if (block.type.startsWith('fn:')) { // ユーザー関数呼び出し + const fnName = block.type.split(':')[1]; + const fn = this.getVariableValue(fnName, values); + for (let i = 0; i < fn.slots.length; i++) { + const name = fn.slots[i]; + slotArg[name] = this.evaluate(block.args[i], values); + } + return fn.exec(slotArg); + } + + if (block.args === undefined) return null; + + const date = new Date(); + const day = `${this.opts.visitor ? this.opts.visitor.id : ''} ${date.getFullYear()}/${date.getMonth()}/${date.getDate()}`; + + const funcs: { [p in keyof typeof funcDefs]: any } = { + not: (a) => !a, + eq: (a, b) => a === b, + notEq: (a, b) => a !== b, + gt: (a, b) => a > b, + lt: (a, b) => a < b, + gtEq: (a, b) => a >= b, + ltEq: (a, b) => a <= b, + or: (a, b) => a || b, + and: (a, b) => a && b, + if: (bool, a, b) => bool ? a : b, + add: (a, b) => a + b, + subtract: (a, b) => a - b, + multiply: (a, b) => a * b, + divide: (a, b) => a / b, + random: (probability) => Math.floor(seedrandom(`${this.opts.randomSeed}:${block.id}`)() * 100) < probability, + rannum: (min, max) => min + Math.floor(seedrandom(`${this.opts.randomSeed}:${block.id}`)() * (max - min + 1)), + randomPick: (list) => list[Math.floor(seedrandom(`${this.opts.randomSeed}:${block.id}`)() * list.length)], + dailyRandom: (probability) => Math.floor(seedrandom(`${day}:${block.id}`)() * 100) < probability, + dailyRannum: (min, max) => min + Math.floor(seedrandom(`${day}:${block.id}`)() * (max - min + 1)), + dailyRandomPick: (list) => list[Math.floor(seedrandom(`${day}:${block.id}`)() * list.length)], + }; + + const fnName = block.type; + + const fn = funcs[fnName]; + if (fn == null) { + console.error('Unknown function: ' + fnName); + throw new Error('Unknown function: ' + fnName); + } + + const args = block.args.map(x => this.evaluate(x, values, slotArg)); + + return fn(...args); + } + + @autobind + private getVariableValue(name: string, values: { name: string, value: any }[]): any { + const v = values.find(v => v.name === name); + if (v) { + return v.value; + } + + const pageVar = this.pageVars.find(v => v.name === name); + if (pageVar) { + return pageVar.value; + } + + if (AiScript.envVarsDef[name]) { + return this.envVars[name].value; + } + + throw new Error(`Script: No such variable '${name}'`); + } + + @autobind + public isUsedName(name: string) { + if (this.variables.some(v => v.name === name)) { + return true; + } + + if (this.pageVars.some(v => v.name === name)) { + return true; + } + + if (AiScript.envVarsDef[name]) { + return true; + } + + return false; + } +} diff --git a/src/client/app/common/scripts/collect-page-vars.ts b/src/client/app/common/scripts/collect-page-vars.ts new file mode 100644 index 000000000..86687e21f --- /dev/null +++ b/src/client/app/common/scripts/collect-page-vars.ts @@ -0,0 +1,24 @@ +export function collectPageVars(content) { + const pageVars = []; + const collect = (xs: any[]) => { + for (const x of xs) { + if (x.type === 'input') { + pageVars.push({ + name: x.name, + type: x.inputType, + value: x.default + }); + } else if (x.type === 'switch') { + pageVars.push({ + name: x.name, + type: 'boolean', + value: x.default + }); + } else if (x.children) { + collect(x.children); + } + } + }; + collect(content); + return pageVars; +} diff --git a/src/client/app/common/views/components/dialog.vue b/src/client/app/common/views/components/dialog.vue index c1ee7958c..020c88f69 100644 --- a/src/client/app/common/views/components/dialog.vue +++ b/src/client/app/common/views/components/dialog.vue @@ -22,7 +22,14 @@ - + + {{ (showCancelButton || input || select || user) ? $t('@.ok') : $t('@.got-it') }} @@ -230,7 +237,7 @@ export default Vue.extend({ font-size 32px &.success - color #37ec92 + color #85da5a &.error color #ec4137 diff --git a/src/client/app/common/views/components/media-image.vue b/src/client/app/common/views/components/media-image.vue index 255990751..6db4b40dd 100644 --- a/src/client/app/common/views/components/media-image.vue +++ b/src/client/app/common/views/components/media-image.vue @@ -36,7 +36,7 @@ export default Vue.extend({ return { hide: true }; - } + }, computed: { style(): any { let url = `url(${ diff --git a/src/client/app/common/views/components/page-editor/page-editor.block.vue b/src/client/app/common/views/components/page-editor/page-editor.block.vue new file mode 100644 index 000000000..a3e1488d1 --- /dev/null +++ b/src/client/app/common/views/components/page-editor/page-editor.block.vue @@ -0,0 +1,25 @@ + + + diff --git a/src/client/app/common/views/components/page-editor/page-editor.button.vue b/src/client/app/common/views/components/page-editor/page-editor.button.vue new file mode 100644 index 000000000..d5fc24381 --- /dev/null +++ b/src/client/app/common/views/components/page-editor/page-editor.button.vue @@ -0,0 +1,54 @@ + + + + + diff --git a/src/client/app/common/views/components/page-editor/page-editor.container.vue b/src/client/app/common/views/components/page-editor/page-editor.container.vue new file mode 100644 index 000000000..698fdfee4 --- /dev/null +++ b/src/client/app/common/views/components/page-editor/page-editor.container.vue @@ -0,0 +1,135 @@ + + + + + diff --git a/src/client/app/common/views/components/page-editor/page-editor.image.vue b/src/client/app/common/views/components/page-editor/page-editor.image.vue new file mode 100644 index 000000000..0bc1816e8 --- /dev/null +++ b/src/client/app/common/views/components/page-editor/page-editor.image.vue @@ -0,0 +1,78 @@ + + + + + diff --git a/src/client/app/common/views/components/page-editor/page-editor.input.vue b/src/client/app/common/views/components/page-editor/page-editor.input.vue new file mode 100644 index 000000000..1f3754252 --- /dev/null +++ b/src/client/app/common/views/components/page-editor/page-editor.input.vue @@ -0,0 +1,54 @@ + + + + + diff --git a/src/client/app/common/views/components/page-editor/page-editor.script-block.vue b/src/client/app/common/views/components/page-editor/page-editor.script-block.vue new file mode 100644 index 000000000..312283203 --- /dev/null +++ b/src/client/app/common/views/components/page-editor/page-editor.script-block.vue @@ -0,0 +1,263 @@ + + + + + diff --git a/src/client/app/common/views/components/page-editor/page-editor.section.vue b/src/client/app/common/views/components/page-editor/page-editor.section.vue new file mode 100644 index 000000000..d7a247b0b --- /dev/null +++ b/src/client/app/common/views/components/page-editor/page-editor.section.vue @@ -0,0 +1,133 @@ + + + + + diff --git a/src/client/app/common/views/components/page-editor/page-editor.switch.vue b/src/client/app/common/views/components/page-editor/page-editor.switch.vue new file mode 100644 index 000000000..a9cfa2844 --- /dev/null +++ b/src/client/app/common/views/components/page-editor/page-editor.switch.vue @@ -0,0 +1,48 @@ + + + + + diff --git a/src/client/app/common/views/components/page-editor/page-editor.text.vue b/src/client/app/common/views/components/page-editor/page-editor.text.vue new file mode 100644 index 000000000..7368931b2 --- /dev/null +++ b/src/client/app/common/views/components/page-editor/page-editor.text.vue @@ -0,0 +1,57 @@ + + + + + diff --git a/src/client/app/common/views/components/page-editor/page-editor.vue b/src/client/app/common/views/components/page-editor/page-editor.vue new file mode 100644 index 000000000..1bcaaa033 --- /dev/null +++ b/src/client/app/common/views/components/page-editor/page-editor.vue @@ -0,0 +1,452 @@ + + + + + diff --git a/src/client/app/common/views/components/page-preview.vue b/src/client/app/common/views/components/page-preview.vue new file mode 100644 index 000000000..d8fdbf4b0 --- /dev/null +++ b/src/client/app/common/views/components/page-preview.vue @@ -0,0 +1,141 @@ + + + + + diff --git a/src/client/app/common/views/pages/page/page.block.vue b/src/client/app/common/views/pages/page/page.block.vue new file mode 100644 index 000000000..48a89f9de --- /dev/null +++ b/src/client/app/common/views/pages/page/page.block.vue @@ -0,0 +1,34 @@ + + + diff --git a/src/client/app/common/views/pages/page/page.button.vue b/src/client/app/common/views/pages/page/page.button.vue new file mode 100644 index 000000000..5063d2712 --- /dev/null +++ b/src/client/app/common/views/pages/page/page.button.vue @@ -0,0 +1,42 @@ + + + + + diff --git a/src/client/app/common/views/pages/page/page.image.vue b/src/client/app/common/views/pages/page/page.image.vue new file mode 100644 index 000000000..1285445eb --- /dev/null +++ b/src/client/app/common/views/pages/page/page.image.vue @@ -0,0 +1,36 @@ + + + + + diff --git a/src/client/app/common/views/pages/page/page.input.vue b/src/client/app/common/views/pages/page/page.input.vue new file mode 100644 index 000000000..cda555033 --- /dev/null +++ b/src/client/app/common/views/pages/page/page.input.vue @@ -0,0 +1,43 @@ + + + + + diff --git a/src/client/app/common/views/pages/page/page.section.vue b/src/client/app/common/views/pages/page/page.section.vue new file mode 100644 index 000000000..03c009d9c --- /dev/null +++ b/src/client/app/common/views/pages/page/page.section.vue @@ -0,0 +1,55 @@ + + + + + diff --git a/src/client/app/common/views/pages/page/page.switch.vue b/src/client/app/common/views/pages/page/page.switch.vue new file mode 100644 index 000000000..715a2fee6 --- /dev/null +++ b/src/client/app/common/views/pages/page/page.switch.vue @@ -0,0 +1,33 @@ + + + diff --git a/src/client/app/common/views/pages/page/page.text.vue b/src/client/app/common/views/pages/page/page.text.vue new file mode 100644 index 000000000..eadc6f0ae --- /dev/null +++ b/src/client/app/common/views/pages/page/page.text.vue @@ -0,0 +1,35 @@ + + + + + diff --git a/src/client/app/common/views/pages/page/page.vue b/src/client/app/common/views/pages/page/page.vue new file mode 100644 index 000000000..5ca58a6a4 --- /dev/null +++ b/src/client/app/common/views/pages/page/page.vue @@ -0,0 +1,143 @@ + + + + + diff --git a/src/client/app/desktop/script.ts b/src/client/app/desktop/script.ts index 8d292ce32..00ba5db23 100644 --- a/src/client/app/desktop/script.ts +++ b/src/client/app/desktop/script.ts @@ -156,7 +156,11 @@ init(async (launch, os) => { { path: '/explore', name: 'explore', component: () => import('../common/views/pages/explore.vue').then(m => m.default) }, { path: '/explore/tags/:tag', name: 'explore-tag', props: true, component: () => import('../common/views/pages/explore.vue').then(m => m.default) }, { path: '/i/favorites', component: () => import('./views/home/favorites.vue').then(m => m.default) }, + { path: '/i/pages', component: () => import('./views/home/pages.vue').then(m => m.default) }, ]}, + { path: '/@:user/pages/:page', props: true, component: () => import('./views/pages/page.vue').then(m => m.default) }, + { path: '/i/pages/new', component: () => import('./views/pages/page-editor.vue').then(m => m.default) }, + { path: '/i/pages/edit/:page', props: true, component: () => import('./views/pages/page-editor.vue').then(m => m.default) }, { path: '/i/messaging/:user', component: MkMessagingRoom }, { path: '/i/drive', component: MkDrive }, { path: '/i/drive/folder/:folder', component: MkDrive }, diff --git a/src/client/app/desktop/views/components/ui.header.account.vue b/src/client/app/desktop/views/components/ui.header.account.vue index 7f9decfdc..05692667b 100644 --- a/src/client/app/desktop/views/components/ui.header.account.vue +++ b/src/client/app/desktop/views/components/ui.header.account.vue @@ -9,35 +9,42 @@

  • - + {{ $t('profile') }}
  • - + {{ $t('@.drive') }}

  • - + {{ $t('@.favorites') }}
  • - + {{ $t('lists') }}

  • +
  • + + + {{ $t('@.pages') }} + + +
  • - + {{ $t('follow-requests') }}{{ $store.state.i.pendingReceivedFollowRequestsCount }}

    @@ -46,14 +53,14 @@
    • - + {{ $t('@.settings') }}
    • - + {{ $t('admin') }} @@ -76,7 +83,7 @@
      • - + {{ $t('@.signout') }}

      • @@ -95,14 +102,14 @@ import MkFollowRequestsWindow from './received-follow-requests-window.vue'; import MkDriveWindow from './drive-window.vue'; import contains from '../../../common/scripts/contains'; import { faHome, faColumns } from '@fortawesome/free-solid-svg-icons'; -import { faMoon, faSun } from '@fortawesome/free-regular-svg-icons'; +import { faMoon, faSun, faStickyNote } from '@fortawesome/free-regular-svg-icons'; export default Vue.extend({ i18n: i18n('desktop/views/components/ui.header.account.vue'), data() { return { isOpen: false, - faHome, faColumns, faMoon, faSun + faHome, faColumns, faMoon, faSun, faStickyNote }; }, computed: { diff --git a/src/client/app/desktop/views/home/pages.vue b/src/client/app/desktop/views/home/pages.vue new file mode 100644 index 000000000..9f7fb6515 --- /dev/null +++ b/src/client/app/desktop/views/home/pages.vue @@ -0,0 +1,92 @@ + + + + + diff --git a/src/client/app/desktop/views/pages/page-editor.vue b/src/client/app/desktop/views/pages/page-editor.vue new file mode 100644 index 000000000..50d1e7db6 --- /dev/null +++ b/src/client/app/desktop/views/pages/page-editor.vue @@ -0,0 +1,32 @@ + + + + + diff --git a/src/client/app/desktop/views/pages/page.vue b/src/client/app/desktop/views/pages/page.vue new file mode 100644 index 000000000..1ddff08c7 --- /dev/null +++ b/src/client/app/desktop/views/pages/page.vue @@ -0,0 +1,36 @@ + + + + + diff --git a/src/client/app/mobile/script.ts b/src/client/app/mobile/script.ts index 510141f94..136bbc31c 100644 --- a/src/client/app/mobile/script.ts +++ b/src/client/app/mobile/script.ts @@ -135,6 +135,7 @@ init((launch, os) => { { path: '/signup', name: 'signup', component: MkSignup }, { path: '/i/settings', name: 'settings', component: () => import('./views/pages/settings.vue').then(m => m.default) }, { path: '/i/favorites', name: 'favorites', component: MkFavorites }, + { path: '/i/pages', name: 'pages', component: () => import('./views/pages/pages.vue').then(m => m.default) }, { path: '/i/lists', name: 'user-lists', component: MkUserLists }, { path: '/i/lists/:list', name: 'user-list', component: MkUserList }, { path: '/i/received-follow-requests', name: 'received-follow-requests', component: MkReceivedFollowRequests }, @@ -144,6 +145,8 @@ init((launch, os) => { { path: '/i/drive', name: 'drive', component: MkDrive }, { path: '/i/drive/folder/:folder', component: MkDrive }, { path: '/i/drive/file/:file', component: MkDrive }, + { path: '/i/pages/new', component: () => import('./views/pages/page-editor.vue').then(m => m.default) }, + { path: '/i/pages/edit/:page', props: true, component: () => import('./views/pages/page-editor.vue').then(m => m.default) }, { path: '/selectdrive', component: MkSelectDrive }, { path: '/search', component: MkSearch }, { path: '/tags/:tag', component: MkTag }, @@ -156,6 +159,7 @@ init((launch, os) => { { path: 'following', component: () => import('../common/views/pages/following.vue').then(m => m.default) }, { path: 'followers', component: () => import('../common/views/pages/followers.vue').then(m => m.default) }, ]}, + { path: '/@:user/pages/:page', props: true, component: () => import('./views/pages/page.vue').then(m => m.default) }, { path: '/notes/:note', component: MkNote }, { path: '/authorize-follow', component: MkFollow }, { path: '*', component: MkNotFound } diff --git a/src/client/app/mobile/views/components/ui.nav.vue b/src/client/app/mobile/views/components/ui.nav.vue index 9a3ade4c6..da9bb518e 100644 --- a/src/client/app/mobile/views/components/ui.nav.vue +++ b/src/client/app/mobile/views/components/ui.nav.vue @@ -29,6 +29,7 @@
      • {{ $t('@.favorites') }}
      • {{ $t('user-lists') }}
      • {{ $t('@.drive') }}
      • +
      • {{ $t('@.pages') }}
      • {{ $t('search') }}
      • @@ -66,7 +67,7 @@ import Vue from 'vue'; import i18n from '../../../i18n'; import { lang } from '../../../config'; import { faNewspaper, faHashtag, faHome, faColumns } from '@fortawesome/free-solid-svg-icons'; -import { faMoon, faSun } from '@fortawesome/free-regular-svg-icons'; +import { faMoon, faSun, faStickyNote } from '@fortawesome/free-regular-svg-icons'; import { search } from '../../../common/scripts/search'; export default Vue.extend({ @@ -86,7 +87,7 @@ export default Vue.extend({ announcements: [], searching: false, showNotifications: false, - faNewspaper, faHashtag, faMoon, faSun, faHome, faColumns + faNewspaper, faHashtag, faMoon, faSun, faHome, faColumns, faStickyNote }; }, diff --git a/src/client/app/mobile/views/pages/page-editor.vue b/src/client/app/mobile/views/pages/page-editor.vue new file mode 100644 index 000000000..9d549c784 --- /dev/null +++ b/src/client/app/mobile/views/pages/page-editor.vue @@ -0,0 +1,32 @@ + + + + + diff --git a/src/client/app/mobile/views/pages/page.vue b/src/client/app/mobile/views/pages/page.vue new file mode 100644 index 000000000..27ade4a39 --- /dev/null +++ b/src/client/app/mobile/views/pages/page.vue @@ -0,0 +1,36 @@ + + + + + diff --git a/src/client/app/mobile/views/pages/pages.vue b/src/client/app/mobile/views/pages/pages.vue new file mode 100644 index 000000000..100c814ad --- /dev/null +++ b/src/client/app/mobile/views/pages/pages.vue @@ -0,0 +1,94 @@ + + + + + diff --git a/src/client/themes/dark.json5 b/src/client/themes/dark.json5 index 5f44f8570..8e0c726b4 100644 --- a/src/client/themes/dark.json5 +++ b/src/client/themes/dark.json5 @@ -232,5 +232,8 @@ adminDashboardCardBg: '$secondary', adminDashboardCardFg: '$text', adminDashboardCardDivider: 'rgba(0, 0, 0, 0.3)', + + pageBlockBorder: 'rgba(255, 255, 255, 0.1)', + pageBlockBorderHover: 'rgba(255, 255, 255, 0.15)', }, } diff --git a/src/client/themes/light.json5 b/src/client/themes/light.json5 index d5680f8f8..1fff18176 100644 --- a/src/client/themes/light.json5 +++ b/src/client/themes/light.json5 @@ -232,5 +232,8 @@ adminDashboardCardBg: '$secondary', adminDashboardCardFg: '$text', adminDashboardCardDivider: 'rgba(0, 0, 0, 0.082)', + + pageBlockBorder: 'rgba(0, 0, 0, 0.1)', + pageBlockBorderHover: 'rgba(0, 0, 0, 0.15)', }, } diff --git a/src/db/postgre.ts b/src/db/postgre.ts index 71836638f..18283836a 100644 --- a/src/db/postgre.ts +++ b/src/db/postgre.ts @@ -40,6 +40,7 @@ import { Poll } from '../models/entities/poll'; import { UserKeypair } from '../models/entities/user-keypair'; import { UserPublickey } from '../models/entities/user-publickey'; import { UserProfile } from '../models/entities/user-profile'; +import { Page } from '../models/entities/page'; const sqlLogger = dbLogger.createSubLogger('sql', 'white', false); @@ -114,6 +115,7 @@ export function initDb(justBorrow = false, sync = false, log = false) { NoteReaction, NoteWatching, NoteUnread, + Page, Log, DriveFile, DriveFolder, diff --git a/src/models/entities/page.ts b/src/models/entities/page.ts new file mode 100644 index 000000000..f57ca8c7c --- /dev/null +++ b/src/models/entities/page.ts @@ -0,0 +1,105 @@ +import { Entity, Index, JoinColumn, Column, PrimaryColumn, ManyToOne } from 'typeorm'; +import { User } from './user'; +import { id } from '../id'; +import { DriveFile } from './drive-file'; + +@Entity() +@Index(['userId', 'name'], { unique: true }) +export class Page { + @PrimaryColumn(id()) + public id: string; + + @Index() + @Column('timestamp with time zone', { + comment: 'The created date of the Page.' + }) + public createdAt: Date; + + @Index() + @Column('timestamp with time zone', { + comment: 'The updated date of the Page.' + }) + public updatedAt: Date; + + @Column('varchar', { + length: 256, + }) + public title: string; + + @Index() + @Column('varchar', { + length: 256, + }) + public name: string; + + @Column('varchar', { + length: 256, nullable: true + }) + public summary: string | null; + + @Column('boolean') + public alignCenter: boolean; + + @Column('varchar', { + length: 32, + }) + public font: string; + + @Index() + @Column({ + ...id(), + comment: 'The ID of author.' + }) + public userId: User['id']; + + @ManyToOne(type => User, { + onDelete: 'CASCADE' + }) + @JoinColumn() + public user: User | null; + + @Column({ + ...id(), + nullable: true, + }) + public eyeCatchingImageId: DriveFile['id'] | null; + + @ManyToOne(type => DriveFile, { + onDelete: 'CASCADE' + }) + @JoinColumn() + public eyeCatchingImage: DriveFile | null; + + @Column('jsonb', { + default: [] + }) + public content: Record[]; + + @Column('jsonb', { + default: [] + }) + public variables: Record[]; + + /** + * public ... 公開 + * followers ... フォロワーのみ + * specified ... visibleUserIds で指定したユーザーのみ + */ + @Column('enum', { enum: ['public', 'followers', 'specified'] }) + public visibility: 'public' | 'followers' | 'specified'; + + @Index() + @Column({ + ...id(), + array: true, default: '{}' + }) + public visibleUserIds: User['id'][]; + + constructor(data: Partial) { + if (data == null) return; + + for (const [k, v] of Object.entries(data)) { + (this as any)[k] = v; + } + } +} diff --git a/src/models/index.ts b/src/models/index.ts index 826044e7a..e402d6723 100644 --- a/src/models/index.ts +++ b/src/models/index.ts @@ -35,6 +35,7 @@ import { AbuseUserReportRepository } from './repositories/abuse-user-report'; import { AuthSessionRepository } from './repositories/auth-session'; import { UserProfile } from './entities/user-profile'; import { HashtagRepository } from './repositories/hashtag'; +import { PageRepository } from './repositories/page'; export const Apps = getCustomRepository(AppRepository); export const Notes = getCustomRepository(NoteRepository); @@ -72,3 +73,4 @@ export const MessagingMessages = getCustomRepository(MessagingMessageRepository) export const ReversiGames = getCustomRepository(ReversiGameRepository); export const ReversiMatchings = getCustomRepository(ReversiMatchingRepository); export const Logs = getRepository(Log); +export const Pages = getCustomRepository(PageRepository); diff --git a/src/models/repositories/page.ts b/src/models/repositories/page.ts new file mode 100644 index 000000000..4c1b4cc79 --- /dev/null +++ b/src/models/repositories/page.ts @@ -0,0 +1,61 @@ +import { EntityRepository, Repository } from 'typeorm'; +import { Page } from '../entities/page'; +import { SchemaType, types, bool } from '../../misc/schema'; +import { Users, DriveFiles } from '..'; +import { awaitAll } from '../../prelude/await-all'; +import { DriveFile } from '../entities/drive-file'; + +export type PackedPage = SchemaType; + +@EntityRepository(Page) +export class PageRepository extends Repository { + public async pack( + src: Page, + ): Promise { + const attachedFiles: Promise[] = []; + const collectFile = (xs: any[]) => { + for (const x of xs) { + if (x.type === 'image') { + attachedFiles.push(DriveFiles.findOne({ + id: x.fileId, + userId: src.userId + })); + } + if (x.children) { + collectFile(x.children); + } + } + }; + collectFile(src.content); + return await awaitAll({ + id: src.id, + createdAt: src.createdAt.toISOString(), + updatedAt: src.updatedAt.toISOString(), + userId: src.userId, + user: Users.pack(src.user || src.userId), + content: src.content, + variables: src.variables, + title: src.title, + name: src.name, + summary: src.summary, + alignCenter: src.alignCenter, + font: src.font, + eyeCatchingImageId: src.eyeCatchingImageId, + eyeCatchingImage: src.eyeCatchingImageId ? await DriveFiles.pack(src.eyeCatchingImageId) : null, + attachedFiles: DriveFiles.packMany(await Promise.all(attachedFiles)) + }); + } + + public packMany( + pages: Page[], + ) { + return Promise.all(pages.map(x => this.pack(x))); + } +} + +export const packedPageSchema = { + type: types.object, + optional: bool.false, nullable: bool.false, + properties: { + } +}; diff --git a/src/server/api/endpoints/i/pages.ts b/src/server/api/endpoints/i/pages.ts new file mode 100644 index 000000000..5eb4db81b --- /dev/null +++ b/src/server/api/endpoints/i/pages.ts @@ -0,0 +1,44 @@ +import $ from 'cafy'; +import { ID } from '../../../../misc/cafy-id'; +import define from '../../define'; +import { Pages } from '../../../../models'; +import { makePaginationQuery } from '../../common/make-pagination-query'; + +export const meta = { + desc: { + 'ja-JP': '自分の作成したページ一覧を取得します。', + 'en-US': 'Get my pages.' + }, + + tags: ['account', 'pages'], + + requireCredential: true, + + kind: 'read:pages', + + params: { + limit: { + validator: $.optional.num.range(1, 100), + default: 10 + }, + + sinceId: { + validator: $.optional.type(ID), + }, + + untilId: { + validator: $.optional.type(ID), + }, + } +}; + +export default define(meta, async (ps, user) => { + const query = makePaginationQuery(Pages.createQueryBuilder('page'), ps.sinceId, ps.untilId) + .andWhere(`page.userId = :meId`, { meId: user.id }); + + const pages = await query + .take(ps.limit!) + .getMany(); + + return await Pages.packMany(pages); +}); diff --git a/src/server/api/endpoints/pages/create.ts b/src/server/api/endpoints/pages/create.ts new file mode 100644 index 000000000..e6b813648 --- /dev/null +++ b/src/server/api/endpoints/pages/create.ts @@ -0,0 +1,108 @@ +import $ from 'cafy'; +import * as ms from 'ms'; +import define from '../../define'; +import { ID } from '../../../../misc/cafy-id'; +import { types, bool } from '../../../../misc/schema'; +import { Pages, DriveFiles } from '../../../../models'; +import { genId } from '../../../../misc/gen-id'; +import { Page } from '../../../../models/entities/page'; +import { ApiError } from '../../error'; + +export const meta = { + desc: { + 'ja-JP': 'ページを作成します。', + }, + + tags: ['pages'], + + requireCredential: true, + + kind: 'write:pages', + + limit: { + duration: ms('1hour'), + max: 300 + }, + + params: { + title: { + validator: $.str, + }, + + name: { + validator: $.str, + }, + + summary: { + validator: $.optional.nullable.str, + }, + + content: { + validator: $.arr($.obj()) + }, + + variables: { + validator: $.arr($.obj()) + }, + + eyeCatchingImageId: { + validator: $.optional.nullable.type(ID), + }, + + font: { + validator: $.optional.str.or(['serif', 'sans-serif']), + default: 'sans-serif' + }, + + alignCenter: { + validator: $.optional.bool, + default: false + }, + }, + + res: { + type: types.object, + optional: bool.false, nullable: bool.false, + ref: 'Page', + }, + + errors: { + noSuchFile: { + message: 'No such file.', + code: 'NO_SUCH_FILE', + id: 'b7b97489-0f66-4b12-a5ff-b21bd63f6e1c' + }, + } +}; + +export default define(meta, async (ps, user) => { + let eyeCatchingImage = null; + if (ps.eyeCatchingImageId != null) { + eyeCatchingImage = await DriveFiles.findOne({ + id: ps.eyeCatchingImageId, + userId: user.id + }); + + if (eyeCatchingImage == null) { + throw new ApiError(meta.errors.noSuchFile); + } + } + + const page = await Pages.save(new Page({ + id: genId(), + createdAt: new Date(), + updatedAt: new Date(), + title: ps.title, + name: ps.name, + summary: ps.summary, + content: ps.content, + variables: ps.variables, + eyeCatchingImageId: eyeCatchingImage ? eyeCatchingImage.id : null, + userId: user.id, + visibility: 'public', + alignCenter: ps.alignCenter, + font: ps.font + })); + + return await Pages.pack(page); +}); diff --git a/src/server/api/endpoints/pages/delete.ts b/src/server/api/endpoints/pages/delete.ts new file mode 100644 index 000000000..043805aa3 --- /dev/null +++ b/src/server/api/endpoints/pages/delete.ts @@ -0,0 +1,53 @@ +import $ from 'cafy'; +import define from '../../define'; +import { ApiError } from '../../error'; +import { Pages } from '../../../../models'; +import { ID } from '../../../../misc/cafy-id'; + +export const meta = { + desc: { + 'ja-JP': '指定したページを削除します。', + }, + + tags: ['pages'], + + requireCredential: true, + + kind: 'write:pages', + + params: { + pageId: { + validator: $.type(ID), + desc: { + 'ja-JP': '対象のページのID', + 'en-US': 'Target page ID.' + } + }, + }, + + errors: { + noSuchPage: { + message: 'No such page.', + code: 'NO_SUCH_PAGE', + id: 'eb0c6e1d-d519-4764-9486-52a7e1c6392a' + }, + + accessDenied: { + message: 'Access denied.', + code: 'ACCESS_DENIED', + id: '8b741b3e-2c22-44b3-a15f-29949aa1601e' + }, + } +}; + +export default define(meta, async (ps, user) => { + const page = await Pages.findOne(ps.pageId); + if (page == null) { + throw new ApiError(meta.errors.noSuchPage); + } + if (page.userId !== user.id) { + throw new ApiError(meta.errors.accessDenied); + } + + await Pages.delete(page.id); +}); diff --git a/src/server/api/endpoints/pages/show.ts b/src/server/api/endpoints/pages/show.ts new file mode 100644 index 000000000..dd1dc9f25 --- /dev/null +++ b/src/server/api/endpoints/pages/show.ts @@ -0,0 +1,74 @@ +import $ from 'cafy'; +import define from '../../define'; +import { ApiError } from '../../error'; +import { Pages, Users } from '../../../../models'; +import { types, bool } from '../../../../misc/schema'; +import { ID } from '../../../../misc/cafy-id'; +import { Page } from '../../../../models/entities/page'; + +export const meta = { + desc: { + 'ja-JP': '指定したページの情報を取得します。', + }, + + tags: ['pages'], + + requireCredential: false, + + params: { + pageId: { + validator: $.optional.type(ID), + desc: { + 'ja-JP': '対象のページのID', + 'en-US': 'Target page ID.' + } + }, + + name: { + validator: $.optional.str, + }, + + username: { + validator: $.optional.str, + }, + }, + + res: { + type: types.object, + optional: bool.false, nullable: bool.false, + ref: 'Page', + }, + + errors: { + noSuchPage: { + message: 'No such page.', + code: 'NO_SUCH_PAGE', + id: '222120c0-3ead-4528-811b-b96f233388d7' + } + } +}; + +export default define(meta, async (ps, user) => { + let page: Page | undefined; + + if (ps.pageId) { + page = await Pages.findOne(ps.pageId); + } else if (ps.name && ps.username) { + const author = await Users.findOne({ + host: null, + usernameLower: ps.username.toLowerCase() + }); + if (author) { + page = await Pages.findOne({ + name: ps.name, + userId: author.id + }); + } + } + + if (page == null) { + throw new ApiError(meta.errors.noSuchPage); + } + + return await Pages.pack(page); +}); diff --git a/src/server/api/endpoints/pages/update.ts b/src/server/api/endpoints/pages/update.ts new file mode 100644 index 000000000..8ee34fc3b --- /dev/null +++ b/src/server/api/endpoints/pages/update.ts @@ -0,0 +1,123 @@ +import $ from 'cafy'; +import * as ms from 'ms'; +import define from '../../define'; +import { ApiError } from '../../error'; +import { Pages, DriveFiles } from '../../../../models'; +import { ID } from '../../../../misc/cafy-id'; + +export const meta = { + desc: { + 'ja-JP': '指定したページの情報を更新します。', + }, + + tags: ['pages'], + + requireCredential: true, + + kind: 'write:pages', + + limit: { + duration: ms('1hour'), + max: 300 + }, + + params: { + pageId: { + validator: $.type(ID), + desc: { + 'ja-JP': '対象のページのID', + 'en-US': 'Target page ID.' + } + }, + + title: { + validator: $.str, + }, + + name: { + validator: $.optional.str, + }, + + summary: { + validator: $.optional.nullable.str, + }, + + content: { + validator: $.arr($.obj()) + }, + + variables: { + validator: $.arr($.obj()) + }, + + eyeCatchingImageId: { + validator: $.optional.nullable.type(ID), + }, + + font: { + validator: $.optional.str.or(['serif', 'sans-serif']), + }, + + alignCenter: { + validator: $.optional.bool, + }, + }, + + errors: { + noSuchPage: { + message: 'No such page.', + code: 'NO_SUCH_PAGE', + id: '21149b9e-3616-4778-9592-c4ce89f5a864' + }, + + accessDenied: { + message: 'Access denied.', + code: 'ACCESS_DENIED', + id: '3c15cd52-3b4b-4274-967d-6456fc4f792b' + }, + + noSuchFile: { + message: 'No such file.', + code: 'NO_SUCH_FILE', + id: 'cfc23c7c-3887-490e-af30-0ed576703c82' + }, + } +}; + +export default define(meta, async (ps, user) => { + const page = await Pages.findOne(ps.pageId); + if (page == null) { + throw new ApiError(meta.errors.noSuchPage); + } + if (page.userId !== user.id) { + throw new ApiError(meta.errors.accessDenied); + } + + let eyeCatchingImage = null; + if (ps.eyeCatchingImageId != null) { + eyeCatchingImage = await DriveFiles.findOne({ + id: ps.eyeCatchingImageId, + userId: user.id + }); + + if (eyeCatchingImage == null) { + throw new ApiError(meta.errors.noSuchFile); + } + } + + await Pages.update(page.id, { + updatedAt: new Date(), + title: ps.title, + name: ps.name === undefined ? page.name : ps.name, + summary: ps.name === undefined ? page.summary : ps.summary, + content: ps.content, + variables: ps.variables, + alignCenter: ps.alignCenter === undefined ? page.alignCenter : ps.alignCenter, + font: ps.font === undefined ? page.font : ps.font, + eyeCatchingImageId: ps.eyeCatchingImageId === null + ? null + : ps.eyeCatchingImageId === undefined + ? page.eyeCatchingImageId + : eyeCatchingImage!.id, + }); +}); diff --git a/src/server/web/index.ts b/src/server/web/index.ts index 1f87cd70f..c5a3497f4 100644 --- a/src/server/web/index.ts +++ b/src/server/web/index.ts @@ -16,7 +16,7 @@ import { fetchMeta } from '../../misc/fetch-meta'; import * as pkg from '../../../package.json'; import { genOpenapiSpec } from '../api/openapi/gen-spec'; import config from '../../config'; -import { Users, Notes, Emojis, UserProfiles } from '../../models'; +import { Users, Notes, Emojis, UserProfiles, Pages } from '../../models'; import parseAcct from '../../misc/acct/parse'; import getNoteSummary from '../../misc/get-note-summary'; import { ensure } from '../../prelude/ensure'; @@ -203,6 +203,41 @@ router.get('/notes/:note', async ctx => { ctx.status = 404; }); + +// Page +router.get('/@:user/pages/:page', async ctx => { + const { username, host } = parseAcct(ctx.params.user); + const user = await Users.findOne({ + usernameLower: username.toLowerCase(), + host + }); + + if (user == null) return; + + const page = await Pages.findOne({ + name: ctx.params.page, + userId: user.id + }); + + if (page) { + const _page = await Pages.pack(page); + const meta = await fetchMeta(); + await ctx.render('page', { + page: _page, + instanceName: meta.name || 'Misskey' + }); + + if (['public'].includes(page.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 => { diff --git a/src/server/web/views/note.pug b/src/server/web/views/note.pug index dd6dda258..983c731a0 100644 --- a/src/server/web/views/note.pug +++ b/src/server/web/views/note.pug @@ -25,6 +25,7 @@ block meta meta(name='twitter:card' content='summary') + // todo if user.twitter meta(name='twitter:creator' content=`@${user.twitter.screenName}`) diff --git a/src/server/web/views/page.pug b/src/server/web/views/page.pug new file mode 100644 index 000000000..55f64ff05 --- /dev/null +++ b/src/server/web/views/page.pug @@ -0,0 +1,30 @@ +extends ./base + +block vars + - const user = page.user; + - const title = page.title; + - const url = `${config.url}/@${user.username}/${page.name}`; + +block title + = `${title} | ${instanceName}` + +block desc + meta(name='description' content= page.summary) + +block og + 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 : user.avatarUrl) + +block meta + meta(name='misskey:user-username' content=user.username) + meta(name='misskey:user-id' content=user.id) + meta(name='misskey:page-id' content=page.id) + + meta(name='twitter:card' content='summary') + + // todo + if user.twitter + meta(name='twitter:creator' content=`@${user.twitter.screenName}`) From 342061803e3688297607a8b44ff31f65575a9aae Mon Sep 17 00:00:00 2001 From: syuilo Date: Mon, 29 Apr 2019 09:28:13 +0900 Subject: [PATCH 14/15] Update dependencies :rocket: --- package.json | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/package.json b/package.json index 780ce5563..14d3bcc8f 100644 --- a/package.json +++ b/package.json @@ -93,13 +93,13 @@ "@types/websocket": "0.0.40", "@types/ws": "6.0.1", "animejs": "3.0.1", - "apexcharts": "3.6.8", + "apexcharts": "3.6.9", "autobind-decorator": "2.4.0", "autosize": "4.0.2", "autwh": "0.1.0", "bcryptjs": "2.4.3", "bootstrap-vue": "2.0.0-rc.13", - "bull": "3.7.0", + "bull": "3.8.1", "cafy": "15.1.1", "chai": "4.2.0", "chalk": "2.4.2", @@ -120,7 +120,7 @@ "feed": "2.0.4", "file-type": "10.11.0", "fuckadblock": "3.2.1", - "gulp": "4.0.0", + "gulp": "4.0.1", "gulp-cssnano": "2.1.3", "gulp-imagemin": "5.0.3", "gulp-mocha": "6.0.0", @@ -139,7 +139,7 @@ "is-root": "2.1.0", "is-svg": "4.1.0", "js-yaml": "3.13.1", - "jsdom": "14.1.0", + "jsdom": "15.0.0", "json5": "2.1.0", "json5-loader": "2.0.0", "katex": "0.10.1", @@ -159,7 +159,7 @@ "loader-utils": "1.2.3", "lolex": "3.1.0", "lookup-dns-cache": "2.1.0", - "minio": "7.0.6", + "minio": "7.0.7", "mocha": "6.1.3", "moji": "0.5.1", "moment": "2.24.0", @@ -200,7 +200,7 @@ "rndstr": "1.0.0", "s-age": "1.1.2", "seedrandom": "3.0.1", - "sharp": "0.22.0", + "sharp": "0.22.1", "showdown": "1.9.0", "showdown-highlightjs-extension": "0.1.2", "speakeasy": "2.0.0", @@ -209,7 +209,7 @@ "stylus": "0.54.5", "stylus-loader": "3.0.2", "summaly": "2.2.0", - "systeminformation": "4.1.5", + "systeminformation": "4.1.6", "syuilo-password-strength": "0.0.1", "terser-webpack-plugin": "1.2.3", "textarea-caret": "3.1.0", @@ -233,7 +233,7 @@ "vue-color": "2.7.0", "vue-content-loading": "1.6.0", "vue-cropperjs": "3.0.0", - "vue-i18n": "8.10.0", + "vue-i18n": "8.11.1", "vue-js-modal": "1.3.28", "vue-json-pretty": "1.6.0", "vue-loader": "15.7.0", From cd2de7f893ea6fa0273f3c0a5a65b34b6aff09ef Mon Sep 17 00:00:00 2001 From: syuilo Date: Mon, 29 Apr 2019 09:29:21 +0900 Subject: [PATCH 15/15] 11.5.0 --- CHANGELOG.md | 34 +++++++++++++++++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c576a5714..a2ed4e443 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -42,8 +42,40 @@ mongodb: 8. master ブランチに戻す 9. enjoy -unreleased +11.5.0 (2019/04/29) ------------------- +### 注意 +このアップデートを適用した後、プロセスを起動(もしくは再起動)する前にまず以下の手順を実行してください + +#### 1 +`ormconfig.json`という名前で、Misskeyのインストール場所(package.jsonとかがあるディレクトリ)に新たなファイルを作る。中身は次のようにします: +``` json +{ + "type": "postgres", + "host": "PostgreSQLのホスト", + "port": 5432, + "username": "PostgreSQLのユーザー名", + "password": "PostgreSQLのパスワード", + "database": "PostgreSQLのデータベース名", + "entities": ["src/models/entities/*.ts"], + "migrations": ["migration/*.ts"], + "cli": { + "migrationsDir": "migration" + } +} +``` +上記の各種PostgreSQLの設定(ポートも)は、設定ファイルに書いてあるものをコピーしてください。 + +#### 2 +``` +npm i -g ts-node +``` + +#### 3 +``` +ts-node ./node_modules/typeorm/cli.js migration:run +``` + ### New features #### MisskeyPages ページ(記事)を作成できるように。